diff --git a/.buildkite/cuda/test-ready.yml b/.buildkite/cuda/test-ready.yml index ff55409b3..1b9afdd0f 100644 --- a/.buildkite/cuda/test-ready.yml +++ b/.buildkite/cuda/test-ready.yml @@ -125,32 +125,8 @@ steps: - vllm_omni/outputs/ commands: - timeout 20m pytest -s -v tests/e2e/features/custom_pipeline/ -m "core_model" - agents: - queue: "gpu_1_queue" - plugins: - - docker#v5.2.0: - image: public.ecr.aws/q9t5s3a7/vllm-ci-test-repo:$BUILDKITE_COMMIT - always-pull: true - propagate-environment: true - # vLLM 0.27 added a pre-flight capacity check in - # distributed/device_communicators/shm_broadcast.py. Without this - # the container gets Docker's 64 MiB default and every test in - # this step dies at engine start with - # RuntimeError: Insufficient space in /dev/shm: - # 240 MiB required, 64 MiB available - # (9 failed in 8.7s on build #2949). vLLM 0.26 had no such check - # and allocated regardless, so this step passed on main while - # being under-provisioned the whole time — 0.27 turned a silent - # risk into an explicit error. - # - # This is the only inline docker plugin block in .buildkite/cuda/; - # every other step inherits common/ci_mirror_hardwares.yml, which - # already sets 8gb. Matching that rather than inventing a value. - shm-size: "8gb" - environment: - - "HF_HOME=/fsx/hf_cache" - volumes: - - "/fsx/hf_cache:/fsx/hf_cache" + mirror_hardwares: l4_1 + - label: "RLHF · VeRL-Omni E2E Test" source_file_dependencies: diff --git a/.claude/skills/add-tts-model/SKILL.md b/.claude/skills/add-tts-model/SKILL.md index 034ac7cd4..8c11d04ba 100644 --- a/.claude/skills/add-tts-model/SKILL.md +++ b/.claude/skills/add-tts-model/SKILL.md @@ -345,8 +345,8 @@ Heavier scenarios in the same file use **`advanced_model` only** (streaming, ext @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_clone_en_non_streaming_001(omni_server, openai_client) -> None: - openai_client.send_audio_speech_request({...}) +def test_voice_clone_en_non_streaming_001(omni_server, online_client) -> None: + online_client.send_audio_speech_request({...}) ``` **L4 consolidation:** Prefer parametrized `OmniServerParams` rows (`default`, `async_chunk`, feature flags) in one expansion module rather than many merge-only files ([#1832](https://github.com/vllm-project/vllm-omni/issues/1832)). @@ -363,8 +363,8 @@ def test_voice_clone_en_non_streaming_001(omni_server, openai_client) -> None: | Path | Fixture | Call | |------|---------|------| -| Online `/v1/*` | `openai_client` | `openai_client.send_*_request(request_config)` | -| Offline inference | `omni_runner_handler` | `omni_runner_handler.send_*_request(request_config)` | +| Online `/v1/*` | `online_client` | `online_client.send_*_request(request_config)` | +| Offline inference | `offline_client` | `offline_client.send_*_request(request_config)` | 1. **Grep `runtime.py` first** — reuse `send_omni_request`, `send_diffusion_request`, `send_audio_speech_request` (online + offline Qwen-style TTS), `send_single_stage_tts_request` (Coqui XTTS / MOSS-TTS-Nano offline), etc. 2. **No matching helper** → add `send__request` (or `send__http_request` for negative/dfx) in **`runtime.py`** with general `assert_*` bundled inside, **then** call it from the test. @@ -396,7 +396,7 @@ See [vllm-omni-test skill](../vllm-omni-test/SKILL.md) § **Runtime send helpers `/health` return non-200 until you're actually ready. - **Mark tests per the CI Levels table** — baseline smoke: `core_model` + `advanced_model`; heavier cases: `advanced_model` only; L4 expansion: `full_model` - **No per-model helper modules** — do not create `tests/helpers/{slug}.py`; keep constants and `request_config` payloads in the test file -- **Online and offline e2e go through `runtime.py`** — `openai_client.send_audio_speech_request` (online); `omni_runner_handler.send_audio_speech_request` (Qwen-style offline) or `send_single_stage_tts_request` (single-stage offline). Add a new `send_*_request` in `runtime.py` when none fits; do not embed `omni.generate` or HTTP in tests +- **Online and offline e2e go through `runtime.py`** — `online_client.send_audio_speech_request` (online); `offline_client.send_audio_speech_request` (Qwen-style offline) or `send_single_stage_tts_request` (single-stage offline). Add a new `send_*_request` in `runtime.py` when none fits; do not embed `omni.generate` or HTTP in tests ## Phase 4: Async Chunk (Streaming) diff --git a/.claude/skills/vllm-omni-test/SKILL.md b/.claude/skills/vllm-omni-test/SKILL.md index 1ccdb3324..fb5ed25ff 100644 --- a/.claude/skills/vllm-omni-test/SKILL.md +++ b/.claude/skills/vllm-omni-test/SKILL.md @@ -94,8 +94,8 @@ Existing references: `tests/e2e/offline_inference/test_qwen2_5_omni.py` (L2-styl | Scenario | Typical location | Fixtures / runner pattern | Baseline markers & level | |----------|------------------|---------------------------|---------------------------| -| **Offline inference e2e** | `tests/e2e/offline_inference/` | **Module (default):** `omni_runner` + `omni_runner_handler`. **Function (isolation only):** `omni_runner_function` + `omni_runner_handler_function`. Diffusion/TTS may use `Omni(...).generate` directly | L2: `core_model` + **one of** `omni` / `tts` / `diffusion`; `@hardware_test(...)` when GPU/NPU is required | -| **Online serving e2e** | `tests/e2e/online_serving/` | **Module (default):** `omni_server` + `openai_client`. **Function (isolation only):** `omni_server_function` + `openai_client_function`. Clients: `send_omni_request` (omni), `send_audio_speech_request` (tts), `send_diffusion_request` / `send_video_diffusion_request` / `send_images_generations_request` (diffusion) | Baseline smoke: **`core_model` + `advanced_model`**; heavier paths: `advanced_model` only; L4 expansion: `full_model` | +| **Offline inference e2e** | `tests/e2e/offline_inference/` | **Module (default):** `omni_runner` + `offline_client`. **Function (isolation only):** `omni_runner_function` + `offline_client_function`. Diffusion/TTS may use `Omni(...).generate` directly | L2: `core_model` + **one of** `omni` / `tts` / `diffusion`; `@hardware_test(...)` when GPU/NPU is required | +| **Online serving e2e** | `tests/e2e/online_serving/` | **Module (default):** `omni_server` + `online_client`. **Function (isolation only):** `omni_server_function` + `online_client_function`. Clients: `send_omni_request` (omni), `send_audio_speech_request` (tts), `send_diffusion_request` / `send_video_diffusion_request` / `send_images_generations_request` (diffusion) | Baseline smoke: **`core_model` + `advanced_model`**; heavier paths: `advanced_model` only; L4 expansion: `full_model` | | **Documentation / runnable examples** | `tests/examples/offline_inference/`, `tests/examples/online_serving/` | **Offline docs (preferred):** extract Python/Bash blocks from the doc README (e.g. `ReadmeSnippet.extract_readme_snippets`), `pytest.mark.parametrize` each snippet, run via `example_runner.run` with a stable `output_subfolder`. **Online docs:** copy client/request scripts into dedicated tests and keep them in sync with the doc page. | Usually **L4**: `advanced_model`, often `example` plus hardware marks matching the nightly docs-example job (see `.buildkite/cuda/test-nightly.yml`). Full conventions: [docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md](../../../docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md) (introduced in [PR #1910](https://github.com/vllm-project/vllm-omni/pull/1910): naming, output directory layout, skip rules, avoid trimming `num_inference_steps` without a strong CI reason). | | **Performance / benchmark** | `tests/dfx/perf/tests/*.json` + `run_*_benchmark.py` | JSON or script-driven server + load config; assert explicit metrics / baselines | L4 Perf: JSON `mark` with `full_model` + `omni`/`tts`/`diffusion`; wire `test-nightly.yml` Perf steps | | **Invalid parameter / negative HTTP validation** | `tests/dfx/reliability/invalid_param_test/` | Live `omni_server` + low-level `send_*_http_request` with `err_code` / `err_message` | `pytest.mark.slow` + `omni` / `tts` / `diffusion` + `@hardware_marks` (`H100` or `L4`); CI in **`test-weekly.yml`** (not ready/merge/nightly) | @@ -110,9 +110,9 @@ After choosing offline/online/docs/perf, classify the **product under test** and |-----------|------------------------------|----------------------------|----------------------------------------| | **What it is** | Multimodal LLM pipeline (thinker/talker/stages; text + vision + audio I/O) | Speech synthesis / voice models | Generative diffusion (noise → image, audio, text, or video) | | **Examples** | Qwen2.5-Omni, Qwen3-Omni | Qwen3-TTS, VoxCPM2, Higgs-Audio, Step-Audio2 | Qwen-Image, BAGEL, Wan2.2, HunyuanVideo | -| **Offline runner** | `omni_runner` + `omni_runner_handler`, `generate_multimodal` | `Omni(...).generate` with deploy YAML + TTS params | `Omni(...).generate` + `OmniDiffusionSamplingParams` | -| **Online client** | `openai_client.send_omni_request` (chat completions, modalities) | `openai_client.send_audio_speech_request` (`/v1/audio/speech`) | `send_diffusion_request` (chat/T2I), `send_video_diffusion_request` (`/v1/videos`, X2V), or `send_images_generations_http_request` / `send_images_edits_http_request` (DALL-E routes) | -| **Typical assertions** | Stage outputs, text/audio keywords via `OmniRunnerHandler` / response handler | WAV bytes, stream chunks, speech endpoint contract | Image/video dimensions, `final_output_type`, `assert_diffusion_response` | +| **Offline runner** | `omni_runner` + `offline_client`, `generate_multimodal` | `Omni(...).generate` with deploy YAML + TTS params | `Omni(...).generate` + `OmniDiffusionSamplingParams` | +| **Online client** | `online_client.send_omni_request` (chat completions, modalities) | `online_client.send_audio_speech_request` (`/v1/audio/speech`) | `send_diffusion_request` (chat/T2I), `send_video_diffusion_request` (`/v1/videos`, X2V), or `send_images_generations_http_request` / `send_images_edits_http_request` (DALL-E routes) | +| **Typical assertions** | Stage outputs, text/audio keywords via `OfflineOmniClient` / response handler | WAV bytes, stream chunks, speech endpoint contract | Image/video dimensions, `final_output_type`, `assert_diffusion_response` | | **Deploy YAML** | Per-model deploy configs (`ci/qwen3_omni_moe.yaml`, …) | `qwen3_tts.yaml`, `voxcpm2.yaml`, … | Often default serve; parallel/offload YAML for heavy DiT | | **Nightly group** (`test-nightly.yml`) | **Omni Model Test** — `-m "full_model and omni"` | **TTS Model Test** — `-m "full_model and tts"` | **Diffusion X2I(&A&T)** *or* **Diffusion X2V** (see Step 3 table; same `diffusion` marker, different YAML group / file shard) | | **L4 pressure** | Expansion per modality/model as needed | Expansion + accuracy/perf in TTS group | X2I: merge feature combos per [#1832](https://github.com/vllm-project/vllm-omni/issues/1832); X2V: separate nightly group | @@ -175,9 +175,9 @@ See **L1 unit test constraints (mocking)** below for the full do/don't list. @pytest.mark.omni @hardware_test(...) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_(omni_runner, omni_runner_handler) -> None: +def test_(omni_runner, offline_client) -> None: request_config = {"prompts": ..., "modalities": [...]} # optional: images, videos, audios - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) ``` *Offline generative e2e — **Diffusion** (representative):* @@ -187,8 +187,8 @@ def test_(omni_runner, omni_runner_handler) -> None: @pytest.mark.diffusion @hardware_test(...) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_text_to_image_001(omni_runner_handler) -> None: - omni_runner_handler.send_diffusion_request({"prompt": "...", "extra_body": {"num_inference_steps": 4, ...}}) +def test_text_to_image_001(offline_client) -> None: + offline_client.send_diffusion_request({"prompt": "...", "extra_body": {"num_inference_steps": 4, ...}}) ``` *Offline TTS e2e — **Qwen3-TTS** (two-stage; representative):* @@ -198,8 +198,8 @@ def test_text_to_image_001(omni_runner_handler) -> None: @pytest.mark.tts @hardware_test(...) @pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True) -def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None: - omni_runner_handler.send_audio_speech_request({ +def test_text_to_audio_001(omni_runner, offline_client) -> None: + offline_client.send_audio_speech_request({ "input": "...", "task_type": "Base", "ref_audio": REF_AUDIO_URL, @@ -214,8 +214,8 @@ def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None: @pytest.mark.tts @hardware_test(...) @pytest.mark.parametrize("omni_runner", tts_server_params, indirect=True) -def test_voice_clone_001(omni_runner_handler) -> None: - omni_runner_handler.send_single_stage_tts_request({ +def test_voice_clone_001(offline_client) -> None: + offline_client.send_single_stage_tts_request({ "input": "...", "language": "en", "prompt_audio_path": REF_AUDIO_PATH, @@ -231,9 +231,9 @@ def test_voice_clone_001(omni_runner_handler) -> None: @pytest.mark.omni @hardware_test(...) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_text_001(omni_server, openai_client) -> None: +def test_text_to_text_001(omni_server, online_client) -> None: request_config = {"model": omni_server.model, "messages": ..., "modalities": ["text"]} - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) ``` *Online serving e2e — **TTS** (representative):* @@ -244,9 +244,9 @@ def test_text_to_text_001(omni_server, openai_client) -> None: @pytest.mark.tts @hardware_test(...) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_text_to_audio_001(omni_server, openai_client) -> None: +def test_text_to_audio_001(omni_server, online_client) -> None: request_config = {"model": omni_server.model, "input": "...", "response_format": "wav", ...} - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) ``` *Online serving e2e — **Diffusion** X2I (representative):* @@ -255,8 +255,8 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: @pytest.mark.core_model @pytest.mark.diffusion @pytest.mark.parametrize("omni_server", _get_default_case(MODEL), indirect=True) -def test_text_to_image_001(omni_server, openai_client) -> None: - openai_client.send_diffusion_request({...}) # chat completions + extra_body +def test_text_to_image_001(omni_server, online_client) -> None: + online_client.send_diffusion_request({...}) # chat completions + extra_body ``` *Online serving e2e — **Diffusion** X2V (representative):* @@ -264,8 +264,8 @@ def test_text_to_image_001(omni_server, openai_client) -> None: ```python @pytest.mark.core_model @pytest.mark.diffusion -def test_text_to_video_001(omni_server, openai_client) -> None: - openai_client.send_video_diffusion_request({"model": ..., "form_data": {...}}) # /v1/videos +def test_text_to_video_001(omni_server, online_client) -> None: + online_client.send_video_diffusion_request({"model": ..., "form_data": {...}}) # /v1/videos ``` *Documentation example tests:* follow the **Preferred Test Strategy** in [l4_doc_example_tests.inc.md](../../../docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md): dynamic extraction for offline READMEs; explicit copied client code for online pages until extraction is justified; use the documented **naming**, **output directory** (page folder + case id), and **skipping** rules (e.g. Gradio-only scripts). @@ -324,21 +324,21 @@ vLLM-Omni e2e tests start a real **OmniServer** (online) or **OmniRunner** (offl | Scope | Online fixtures | Offline fixtures | When to use | |-------|-----------------|------------------|-------------| -| **Module** (default) | `omni_server` → `openai_client` | `omni_runner` → `omni_runner_handler` | **Default for L2/L3/L4 expansion** — amortize model/server init across `test_*` in the same module. Same `OmniServerParams` / runner config can be reused by multiple tests. | -| **Function** | `omni_server_function` → `openai_client_function` | `omni_runner_function` → `omni_runner_handler_function` | **Only when required** — each `test_*` must get a **clean** server/runner (no shared engine/GPU state). Typical: `tests/dfx/reliability/`, sleep/wakeup, crash/restart, tests that mutate global server state. | +| **Module** (default) | `omni_server` → `online_client` | `omni_runner` → `offline_client` | **Default for L2/L3/L4 expansion** — amortize model/server init across `test_*` in the same module. Same `OmniServerParams` / runner config can be reused by multiple tests. | +| **Function** | `omni_server_function` → `online_client_function` | `omni_runner_function` → `offline_client_function` | **Only when required** — each `test_*` must get a **clean** server/runner (no shared engine/GPU state). Typical: `tests/dfx/reliability/`, sleep/wakeup, crash/restart, tests that mutate global server state. | **Rules:** 1. **Default to module scope** (`omni_server` / `omni_runner`) unless the scenario **explicitly** needs a fresh instance per test function. -2. **Indirect parametrize name must match the fixture name**: `@pytest.mark.parametrize("omni_server", ...)` with `omni_server` + `openai_client`; `@pytest.mark.parametrize("omni_server_function", ...)` with `omni_server_function` + `openai_client_function`. Do not mix module fixture with function client (or vice versa). +2. **Indirect parametrize name must match the fixture name**: `@pytest.mark.parametrize("omni_server", ...)` with `omni_server` + `online_client`; `@pytest.mark.parametrize("omni_server_function", ...)` with `omni_server_function` + `online_client_function`. Do not mix module fixture with function client (or vice versa). 3. Different `OmniServerParams` per `test_*` (e.g. default vs `--enable-cpu-offload`) is still OK with **module** `omni_server` — pytest parametrizes per test node; only switch to `_function` when isolation between tests matters, not merely because `server_args` differ. 4. One `test_*` with **many** server configs (expansion matrix) → single function + `@pytest.mark.parametrize("omni_server", [...], indirect=True)` + module `omni_server` (see in-tree `test_qwen_image_expansion.py`). ```python # Default — module-scoped server (L4 expansion) @pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=H100)], indirect=True) -def test_foo_images_generations_default_1024(omni_server, openai_client) -> None: - openai_client.send_images_generations_request({...}) +def test_foo_images_generations_default_1024(omni_server, online_client) -> None: + online_client.send_images_generations_request({...}) # Function-scoped — reliability / per-test clean state only @pytest.mark.parametrize( @@ -346,9 +346,9 @@ def test_foo_images_generations_default_1024(omni_server, openai_client) -> None [pytest.param(OmniServerParams(model=MODEL), marks=H100)], indirect=True, ) -def test_foo_sleep_wakeup_cycle(omni_server_function, openai_client_function) -> None: - openai_client_function.send_omni_sleep_http_request({...}) - openai_client_function.send_omni_wakeup_http_request({...}) +def test_foo_sleep_wakeup_cycle(omni_server_function, online_client_function) -> None: + online_client_function.send_omni_sleep_http_request({...}) + online_client_function.send_omni_wakeup_http_request({...}) ``` #### Runtime send helpers — online **and** offline (`tests/helpers/runtime.py`) @@ -357,19 +357,19 @@ def test_foo_sleep_wakeup_cycle(omni_server_function, openai_client_function) -> | Principle | Action | |-----------|--------| -| **Reuse first** | Grep `runtime.py` for an existing **`send_*_request`** on `OpenAIClientHandler` (online) or `OmniRunnerHandler` (offline) that matches the endpoint / pipeline shape. | +| **Reuse first** | Grep `runtime.py` for an existing **`send_*_request`** on `OnlineOmniClient` (online) or `OfflineOmniClient` (offline) that matches the endpoint / pipeline shape. | | **Extend when close** | If an existing `send_*_request` almost fits (e.g. missing one optional field), extend it in `runtime.py` — do not fork logic in the test file. | | **Add when missing** | No suitable helper → add **`send__request`** (high-level: call + general `assert_*`) or **`send___http_request`** (low-level HTTP for negative/dfx) in **`runtime.py` first**, then call it from tests. | | **Test module owns payload only** | In `test_*.py`: `MODEL`, deploy path, vendored media, `get_prompt()`, and inline **`request_config` dicts** only. **No** `omni.generate(...)`, raw `requests.post`, OpenAI SDK calls, or `_collect_audio()` / `_process_output()` in e2e tests. | -**Online** (`openai_client` from `omni_server`): `OpenAIClientHandler.send_*_request`. +**Online** (`online_client` from `omni_server`): `OnlineOmniClient.send_*_request`. -**Offline** (`omni_runner_handler` from `omni_runner`): `OmniRunnerHandler.send_*_request`. +**Offline** (`offline_client` from `omni_runner`): `OfflineOmniClient.send_*_request`. | Do | Don't | |----|-------| -| Online: `openai_client.send_omni_request`, `send_diffusion_request`, `send_audio_speech_request`, `send_video_diffusion_request`, `send_images_generations_request`, … | `requests.post(f"{base_url}/v1/...", json=…)` or `client.chat.completions.create(...)` inside a test | -| Offline: `omni_runner_handler.send_omni_request`, `send_diffusion_request`, `send_audio_speech_request`, `send_single_stage_tts_request`, `send_single_stage_tts_batch_request`, … | `omni_runner.omni.generate(...)` + hand-rolled tensor/WAV extraction in `test_*.py` | +| Online: `online_client.send_omni_request`, `send_diffusion_request`, `send_audio_speech_request`, `send_video_diffusion_request`, `send_images_generations_request`, … | `requests.post(f"{base_url}/v1/...", json=…)` or `client.chat.completions.create(...)` inside a test | +| Offline: `offline_client.send_omni_request`, `send_diffusion_request`, `send_audio_speech_request`, `send_single_stage_tts_request`, `send_single_stage_tts_batch_request`, … | `omni_runner.omni.generate(...)` + hand-rolled tensor/WAV extraction in `test_*.py` | | Add missing `send_*` to **`runtime.py`** first; bundle general `assert_*` inside the send helper | A one-off `def _post_*` / `def _collect_audio` at the bottom of a test module | | Mirror naming/style of neighboring `send_*` (docstring, `request_config` dict, optional `run_level`, `err_code` / `err_message` for negative cases) | Different parameter shapes per test file for the same endpoint | @@ -392,7 +392,7 @@ def test_foo_sleep_wakeup_cycle(omni_server_function, openai_client_function) -> Changing `request_config` fields (`size`, `n`, `seed`, server flags) is **not** special validation — the general `assert_*` should read those from `request_config` inside `send_*`. -**`send_*` ↔ `assert_*` pairing (online `OpenAIClientHandler`):** +**`send_*` ↔ `assert_*` pairing (online `OnlineOmniClient`):** | High-level `send_*` (prefer in L2+ e2e) | Assert already invoked inside `runtime.py` | Low-level HTTP-only sibling | |----------------------------------------|--------------------------------------------|-----------------------------| @@ -405,7 +405,7 @@ Changing `request_config` fields (`size`, `n`, `seed`, server flags) is **not** **Rule:** Tests call high-level `send_*_request` for the general contract. **Never** call the same bundled `assert_*` again. Call an extra `assert_*` in the test **only** for special, case-specific validation. -**Common `OpenAIClientHandler` entry points** (non-exhaustive — grep `runtime.py` before adding): +**Common `OnlineOmniClient` entry points** (non-exhaustive — grep `runtime.py` before adding): | Area | High-level (SDK + assert) | Low-level HTTP (`*_http_request`) | |------|---------------------------|-----------------------------------| @@ -416,7 +416,7 @@ Changing `request_config` fields (`size`, `n`, `seed`, server flags) is **not** | TTS (online) | `send_audio_speech_request` | `send_audio_speech_http_request`, `send_audio_generate_http_request`, … | | Ops / meta | — | `send_health_http_request`, `send_models_http_request`, `send_omni_sleep_http_request`, … | -**Common `OmniRunnerHandler` entry points** (offline — grep `runtime.py` before adding): +**Common `OfflineOmniClient` entry points** (offline — grep `runtime.py` before adding): | Area | High-level (`send_*_request` + assert) | Notes | |------|----------------------------------------|-------| @@ -426,7 +426,7 @@ Changing `request_config` fields (`size`, `n`, `seed`, server flags) is **not** | Single-stage TTS (Coqui XTTS, MOSS-TTS-Nano, …) | `send_single_stage_tts_request`, `send_single_stage_tts_batch_request` | `prompt` + `additional_information` + `omni.generate` — **do not** duplicate in tests | | New model family | **Add** `send__request` here first | Then call from `tests/e2e/offline_inference/test_*.py` | -L1 tests under `tests/entrypoints/` may use **FastAPI `TestClient`** or direct handler calls with mocks; they do **not** need `OpenAIClientHandler` / `OmniRunnerHandler`. **L2+ online and offline e2e** must use `runtime.py` `send_*` helpers. +L1 tests under `tests/entrypoints/` may use **FastAPI `TestClient`** or direct handler calls with mocks; they do **not** need `OnlineOmniClient` / `OfflineOmniClient`. **L2+ online and offline e2e** must use `runtime.py` `send_*` helpers. #### Invalid parameter validation (`tests/dfx/reliability/invalid_param_test/`) @@ -452,7 +452,7 @@ When the user asks for **invalid parameter validation**, **invalid request bodie | **Module markers** | `pytestmark = [pytest.mark.slow, pytest.mark.]` | | **Server fixture** | `_PARAMS` / `_QWEN3_TTS_SPEECH`-style list of `pytest.param(OmniServerParams(...), id="...", marks=hardware_marks(...))`; `@pytest.mark.parametrize("omni_server", _PARAMS, indirect=True)` | | **Hardware** | `hardware_marks(res={"cuda": "H100"})` for heavy diffusion/omni/video; `hardware_marks(res={"cuda": "L4"})` for smaller TTS models (must match weekly `-m "slow and L4"` step) | -| **HTTP client** | **Low-level** `openai_client.send_*_http_request({..., "err_code": 400, "err_message": (...)} )` — **not** `send_*_request` (success path) | +| **HTTP client** | **Low-level** `online_client.send_*_http_request({..., "err_code": 400, "err_message": (...)} )` — **not** `send_*_request` (success path) | | **Case shape** | Prefer **one `test_*` per route family** + `@pytest.mark.parametrize("body_spec, err_message", [...])` with stable `id=` per case; or dedicated `test__malformed_json` when not parametrized | | **Body helpers** | `_minimal__json(omni_server)` / `_minimal_*_form_data()`; merge overrides with `body.update(body_spec)` | | **Known gaps** | `pytest.mark.skip(reason="…#3649")` as `_SKIP_ISSUE_3649` when server validation is not yet strict (mirror neighboring cases) | @@ -485,13 +485,13 @@ When the user asks for **invalid parameter validation**, **invalid request bodie @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) def test_images_generations_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, body_spec: dict[str, object], err_message: str | tuple[str, ...], ) -> None: body = _minimal_images_gen_json(omni_server) body.update(body_spec) - openai_client.send_images_generations_http_request( + online_client.send_images_generations_http_request( {"json": body, "timeout": 300, "err_code": 400, "err_message": err_message} ) ``` @@ -542,18 +542,18 @@ def send_images_generations_request(self, request_config: dict[str, Any], ...) - # tests/e2e/.../test_foo_expansion.py — one case per test_* (no case_id branching) @pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True) -def test_foo_images_generations_default_1024(omni_server, openai_client) -> None: - openai_client.send_images_generations_request({"json": body, "timeout": 300}) +def test_foo_images_generations_default_1024(omni_server, online_client) -> None: + online_client.send_images_generations_request({"json": body, "timeout": 300}) @pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True) -def test_foo_images_generations_sizes_256_512_1024(omni_server, openai_client) -> None: +def test_foo_images_generations_sizes_256_512_1024(omni_server, online_client) -> None: for size in ("256x256", "512x512", "1024x1024"): - openai_client.send_images_generations_request({"json": {**body, "size": size}, "timeout": 300}) + online_client.send_images_generations_request({"json": {**body, "size": size}, "timeout": 300}) # chat diffusion — separate test; send_diffusion_request bundles assert_diffusion_response @pytest.mark.parametrize("omni_server", [pytest.param(OmniServerParams(model=MODEL), marks=SINGLE_L4)], indirect=True) -def test_foo_chat_completions_t2i_fallback(omni_server, openai_client) -> None: - openai_client.send_diffusion_request({...}) +def test_foo_chat_completions_t2i_fallback(omni_server, online_client) -> None: + online_client.send_diffusion_request({...}) ``` L1 tests under `tests/entrypoints/` may keep **minimal** asserts next to the handler under test when validating a single branch; still prefer `assertions.py` when the same JSON/media check appears in more than one file or level (e.g. L1 protocol test + L4 e2e). diff --git a/.claude/skills/vllm-omni-test/references/test-routing.md b/.claude/skills/vllm-omni-test/references/test-routing.md index 394046b91..4f2ad013d 100644 --- a/.claude/skills/vllm-omni-test/references/test-routing.md +++ b/.claude/skills/vllm-omni-test/references/test-routing.md @@ -279,7 +279,7 @@ When adding or modifying tests, do not stop at “where the file lives” — al 4. **API client + assert placement**: **General** validation → implement in `assertions.py`, call **inside** `send_*_request` in `runtime.py`; tests call **`send_*_request` only**. **Special** case validation → `assert_*` in `assertions.py`, called **in the test** after `send_*_request`. Low-level `send_*_http_request` is for negative/dfx tests (`err_code`), not ordinary L2+ success e2e. 5. **Shared assertions**: logic in **`tests/helpers/assertions.py`**; general checks inside `send_*_request`; special checks only in the test. No `_assert_*` in `test_*.py`. 6. **One case → one `test_*`**: function name reflects what is validated; no `if case_id == ...` mega-test merging multiple scenarios. -7. **Fixture scope**: default **`omni_server` + `openai_client`** / **`omni_runner` + `omni_runner_handler`** (module). Use **`omni_server_function` + `openai_client_function`** / **`omni_runner_function` + `omni_runner_handler_function`** only when each `test_*` must spawn a fresh instance. Parametrize name must match fixture (`omni_server` vs `omni_server_function`). +7. **Fixture scope**: default **`omni_server` + `online_client`** / **`omni_runner` + `offline_client`** (module). Use **`omni_server_function` + `online_client_function`** / **`omni_runner_function` + `offline_client_function`** only when each `test_*` must spawn a fresh instance. Parametrize name must match fixture (`omni_server` vs `omni_server_function`). 8. **Type marker**: `omni`, `tts`, or `diffusion` on every model e2e module. 9. **Diffusion L4 Function**: wire `*_expansion.py` into **X2I(&A&T)** or **X2V** **Function Test** in **`test-nightly.yml` only** — do not add `test-merge.yml` unless the user also requested L3. 10. **Diffusion L4 Perf** (only when requested): add `tests/dfx/perf/tests/test__vllm_omni.json` + **Perf Test · <Model>** step (artifact upload); not part of “L4 functional cases” by default. diff --git a/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md b/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md index 5e65d2f48..3411b4377 100644 --- a/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md +++ b/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md @@ -1,8 +1,8 @@ -**Scope** +#### Scope For diffusion models, the L4 functionality test covers all or common *diffusion features* that are supported by this model, including several [parallelism acceleration methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism_acceleration/), [CPU offloading](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cpu_offload/), [TeaCache](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/teacache/) and [Cache-DiT](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cache_dit_acceleration/) cache backends, [quantization methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/quantization/overview/). -**Test Case Design** +#### Test Case Design For a *high priority* model (currently listed in [issue #1832](https://github.com/vllm-project/vllm-omni/issues/1832)), we use several test cases, each with multiple features turned on, so that each supported feature is tested in at least one test case. This is to relieve the GPU workload on the CI machine. The suggested test case combination is as follows: @@ -34,12 +34,12 @@ For a *normal priority* model, further reduce the number of test cases. Currently all the features are available in online serving mode. Hence, only need to add `tests/e2e/online_serving/test_{model}_expansion.py`. -**Code Style** +#### Code Style -- Validation: test that the multimodal output files of your model have the correct shapes. `OpenAIClientHandler.send_diffusion_request` should have taken care of this. +- Validation: test that the multimodal output files of your model have the correct shapes. `OnlineOmniClient.send_diffusion_request` should have taken care of this. - Test marks: always add `full_model` and `diffusion` for L4 nightly `test_*_expansion.py` cases. Add GPU-related marks if needed. Ref: [Markers for Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/test_writing_guide/#markers-for-tests). - To maximize code reuse, you may refer to - - `tests/conftest.py` for `omni_server` (running server in subprocess) and `openai_client` fixtures (sending requests and validating output), `generate_synthetic_image` and `assert_XXX_valid` helper. + - `tests/conftest.py` for `omni_server` (running server in subprocess) and `online_client` fixtures (sending requests and validating output), `generate_synthetic_image` and `assert_XXX_valid` helper. - `tests/helpers/mark.py` for `@hardware_test(...)` and `hardware_marks`. - [Parametrizing tests (pytest doc)](https://docs.pytest.org/en/stable/example/parametrize.html) to reuse test function implementation for different cases. - Doc: add a concise docstring for each test function. diff --git a/tests/conftest.py b/tests/conftest.py index 747b13c35..2f5caca26 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Root pytest entrypoint for the vLLM-Omni test suite. -- `tests/conftest.py` stays thin: plugin registration + compatibility re-exports. +- `tests/conftest.py` stays thin: plugin registration only. - Importable utilities live under `tests/helpers/`. - Helper unit tests live under `tests/helpers/tests/`. - Fixtures live under `tests/helpers/fixtures/` and are loaded via `pytest_plugins`. @@ -19,12 +22,11 @@ bootstrap_vllm_layer_custom_op_modules() pytest_plugins = ( "tests.helpers.fixtures.config", - "tests.helpers.fixtures.env", + "tests.helpers.fixtures.clean", "tests.helpers.fixtures.log", "tests.helpers.fixtures.media", "tests.helpers.fixtures.run_args", "tests.helpers.fixtures.runtime", - "tests.helpers.fixtures.speaker_cache", "tests.helpers.fixtures.xdist", ) @@ -32,46 +34,3 @@ pytest_plugins = ( def pytest_terminal_summary(terminalreporter, exitstatus, config): # Marker for Buildkite log folding before pytest summary lines. terminalreporter.write_line("--- Running Summary") - - -# Backward-compatible lazy re-exports. -# (Many tests still import from `tests.conftest`; migrate these imports to `tests.helpers.*` over time.) -# Keep these lazy so conftest import does not trigger heavy helper dependencies. -_ASSERTION_EXPORT_NAMES = ( - "assert_audio_speech_response", - "assert_diffusion_response", - "assert_http_error", - "assert_image_diffusion_response", - "assert_image_valid", - "assert_omni_response", - "assert_video_diffusion_response", - "assert_video_valid", -) -_MEDIA_EXPORT_NAMES = ( - "convert_audio_bytes_to_text", - "convert_audio_file_to_text", - "cosine_similarity_text", - "decode_b64_image", - "generate_synthetic_audio", - "generate_synthetic_image", - "generate_synthetic_video", -) -_STAGE_CONFIG_EXPORT_NAMES = ("modify_stage_config",) -_RUNTIME_EXPORT_NAMES = ( - "DiffusionResponse", - "HttpResponse", - "OmniResponse", - "OmniRunner", - "OmniRunnerHandler", - "OmniServer", - "OmniServerParams", - "OmniServerStageCli", - "OpenAIClientHandler", - "dummy_messages_from_mix_data", -) -_LAZY_EXPORT_MODULES = { - **{name: "tests.helpers.assertions" for name in _ASSERTION_EXPORT_NAMES}, - **{name: "tests.helpers.media" for name in _MEDIA_EXPORT_NAMES}, - **{name: "tests.helpers.stage_config" for name in _STAGE_CONFIG_EXPORT_NAMES}, - **{name: "tests.helpers.runtime" for name in _RUNTIME_EXPORT_NAMES}, -} diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_audio_diffusion.py b/tests/dfx/reliability/invalid_param_test/test_invalid_audio_diffusion.py index ee1515fa6..2cc549c5d 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_audio_diffusion.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_audio_diffusion.py @@ -1,13 +1,15 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """``POST /v1/audio/generate`` validation (live Stable Audio Open server).""" from __future__ import annotations +from typing import Any + import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.slow, pytest.mark.diffusion] @@ -95,20 +97,20 @@ _PARAMS = [ @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) def test_audio_generate_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, overrides: dict[str, object], err_message: str | tuple[str, ...], ) -> None: - body = {"model": omni_server.model, "input": "ambient electronic pad"} + body: dict[str, Any] = {"model": omni_server.model, "input": "ambient electronic pad"} body.update(overrides) - openai_client.send_audio_generate_http_request( + online_client.send_audio_generate_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) -def test_audio_generate_missing_input(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_audio_generate_http_request( +def test_audio_generate_missing_input(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_audio_generate_http_request( { "json": {"model": omni_server.model}, "timeout": 120, diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_audio_speech.py b/tests/dfx/reliability/invalid_param_test/test_invalid_audio_speech.py index e30591c19..0e26905b0 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_audio_speech.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_audio_speech.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """HTTP + WebSocket validation for Qwen3-TTS Base, Higgs Audio V3 and Audex TTA ``/v1/audio/speech`` (and related routes).""" from __future__ import annotations @@ -13,8 +13,8 @@ from typing import Any import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.media import load_test_audio_data_url -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.media import get_asset_path +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient from tests.helpers.stage_config import get_deploy_config_path # Speech / voice-upload numeric caps for these tests only (not exported from vllm_omni). @@ -26,7 +26,7 @@ _SPEECH_API_MAX_NEW_TOKENS = 4096 _VOICE_UPLOAD_MAX_CONSENT_LEN = 1024 _VOICE_UPLOAD_MAX_REF_TEXT_CHARS = 8192 _VOICE_UPLOAD_MAX_SPEAKER_DESCRIPTION_CHARS = 2048 -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." # Qwen3-TTS 0.6B expects 1024-dim speaker embeddings (see serving_speech / talker hidden_size). _QWEN3_TTS_06B_SPEAKER_EMBEDDING_DIM = 1024 @@ -57,6 +57,7 @@ _QWEN3_TTS_SPEECH = [ ), ] + _HIGGS_AUDIO_V3_SPEECH = [ pytest.param( OmniServerParams( @@ -84,7 +85,7 @@ _AUDEX_TTA_SPEECH = [ ] -def _apply_batch_overrides(body: dict[str, object], *, loc: str, overrides: dict[str, object]) -> None: +def _apply_batch_overrides(body: dict[str, Any], *, loc: str, overrides: dict[str, object]) -> None: if loc == "batch": body.update(overrides) return @@ -112,8 +113,8 @@ def _pcm_wav_mono_bytes(*, duration_s: float = 1.1, sample_rate: int = 44100) -> @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) -def test_speech_malformed_json(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_audio_speech_http_request( +def test_speech_malformed_json(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_audio_speech_http_request( { "raw_body": "{", "timeout": 120, @@ -124,8 +125,8 @@ def test_speech_malformed_json(omni_server: OmniServer, openai_client: OpenAICli @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) -def test_speech_missing_required_fields(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_audio_speech_http_request( +def test_speech_missing_required_fields(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_audio_speech_http_request( { "json": {"model": omni_server.model}, "timeout": 120, @@ -242,13 +243,13 @@ def test_speech_missing_required_fields(omni_server: OmniServer, openai_client: @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) def test_speech_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, overrides: dict[str, object], err_message: str | tuple[str, ...], ) -> None: ov = dict(overrides) embedding_only = bool(ov.pop(_SPEECH_INVALID_EMBEDDING_ONLY_BODY, False)) - body = { + body: dict[str, Any] = { "model": omni_server.model, "input": "Hello.", "voice": "clone", @@ -261,7 +262,7 @@ def test_speech_invalid_field_values( body.pop("ref_audio", None) body.pop("ref_text", None) body.update(ov) - openai_client.send_audio_speech_http_request( + online_client.send_audio_speech_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -328,12 +329,12 @@ def test_speech_invalid_field_values( @pytest.mark.parametrize("omni_server", _HIGGS_AUDIO_V3_SPEECH, indirect=True) def test_speech_higgs_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, extra_json: dict[str, object], err_message: str | tuple[str, ...], ) -> None: body = {"model": omni_server.model, **extra_json} - openai_client.send_audio_speech_http_request( + online_client.send_audio_speech_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -359,12 +360,12 @@ def test_speech_higgs_invalid_field_values( @pytest.mark.parametrize("omni_server", _AUDEX_TTA_SPEECH, indirect=True) def test_speech_audex_tta_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, overrides: dict[str, object], err_message: str | tuple[str, ...], ) -> None: body = {"model": omni_server.model, "input": _AUDEX_TTA_CAPTION, **overrides} - openai_client.send_audio_speech_http_request( + online_client.send_audio_speech_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -375,15 +376,15 @@ def test_speech_audex_tta_invalid_field_values( @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) -def test_speech_batch_empty_items(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_audio_speech_batch_http_request( +def test_speech_batch_empty_items(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_audio_speech_batch_http_request( {"json": {"items": []}, "timeout": 120, "err_code": (400, 422), "err_message": ("items", "least 1 item")} ) @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) -def test_speech_batch_missing_items(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_audio_speech_batch_http_request( +def test_speech_batch_missing_items(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_audio_speech_batch_http_request( { "json": {"model": omni_server.model}, "timeout": 120, @@ -618,14 +619,14 @@ def test_speech_batch_missing_items(omni_server: OmniServer, openai_client: Open @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) def test_speech_batch_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, loc: str, overrides: dict[str, object], err_message: str | tuple[str, ...], ) -> None: ov = dict(overrides) strip_clone = bool(ov.pop(_SPEECH_BATCH_NO_CLONE_FIELDS, False)) - body = { + body: dict[str, Any] = { "model": omni_server.model, "voice": "clone", "ref_audio": REF_AUDIO_URL, @@ -638,7 +639,7 @@ def test_speech_batch_invalid_field_values( body.pop("ref_audio", None) body.pop("ref_text", None) _apply_batch_overrides(body, loc=loc, overrides=ov) - openai_client.send_audio_speech_batch_http_request( + online_client.send_audio_speech_batch_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -670,7 +671,7 @@ _SPEECH_STREAM_WS_SESSION_STREAM_AUDIO_WAV_FRAMES = object() @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) def test_speech_stream_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, send_frames_spec: Any, err_message: str | tuple[str, ...], ) -> None: @@ -687,7 +688,7 @@ def test_speech_stream_invalid_requests( else: send_frames = send_frames_spec assert isinstance(send_frames, str) - openai_client.send_audio_speech_stream_ws_request( + online_client.send_audio_speech_stream_ws_request( { "send_frames": send_frames, "timeout": 120, @@ -893,7 +894,7 @@ def _voices_upload_multipart_files(kind: str | None) -> dict[str, Any] | None: @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) def test_voices_create_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, multipart_kind: str | None, form_template: dict[str, Any], err_message: str | tuple[str, ...], @@ -909,7 +910,7 @@ def test_voices_create_invalid_requests( files = _voices_upload_multipart_files(multipart_kind) if files is not None: cfg["files"] = files - openai_client.send_audio_voices_create_http_request(cfg) + online_client.send_audio_voices_create_http_request(cfg) @pytest.mark.parametrize( @@ -962,13 +963,13 @@ def test_voices_create_invalid_requests( @pytest.mark.parametrize("omni_server", _QWEN3_TTS_SPEECH, indirect=True) def test_voices_delete_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, voice_name: str, err_code: int | tuple[int, ...], err_message: str | tuple[str, ...], ) -> None: """DELETE ``/v1/audio/voices/{name}``: missing voice (404), odd segments (often 404), ``.``paths (405).""" - openai_client.send_audio_voices_delete_http_request( + online_client.send_audio_voices_delete_http_request( { "name": voice_name, "timeout": 120, @@ -1021,7 +1022,7 @@ _MINIMAX_MUSIC3_MAX_FRAMES = 9000 @pytest.mark.parametrize("omni_server", _MINIMAX_MUSIC3_SPEECH, indirect=True) def test_minimax_music3_speech_invalid_field_values( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, overrides: dict[str, object], err_message: str | tuple[str, ...], ) -> None: @@ -1038,6 +1039,6 @@ def test_minimax_music3_speech_invalid_field_values( body.pop(key, None) else: body[key] = value - openai_client.send_audio_speech_http_request( + online_client.send_audio_speech_http_request( {"json": body, "timeout": 120, "err_code": 400, "err_message": err_message} ) diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_image_editing.py b/tests/dfx/reliability/invalid_param_test/test_invalid_image_editing.py index 5ffd25632..9bc5e44fe 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_image_editing.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_image_editing.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """``POST /v1/images/edits`` multipart validation (live ``Qwen/Qwen-Image-Edit``).""" from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Any import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.slow, pytest.mark.diffusion] @@ -72,7 +72,7 @@ def _finalize_edits_form_data(template: dict[str, Any], server_model: str) -> di @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) def test_images_edits_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, tiny_png_bytes: bytes, include_image: bool, data_template: dict[str, Any], @@ -87,4 +87,4 @@ def test_images_edits_invalid_requests( } if include_image: cfg["files"] = {"image": ("x.png", tiny_png_bytes, "image/png")} - openai_client.send_images_edits_http_request(cfg) + online_client.send_images_edits_http_request(cfg) diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_image_generation.py b/tests/dfx/reliability/invalid_param_test/test_invalid_image_generation.py index 5e9424c88..daad00957 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_image_generation.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_image_generation.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """``POST /v1/images/generations`` validation (live ``Qwen/Qwen-Image``).""" from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Any import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.slow, pytest.mark.diffusion] @@ -126,7 +126,7 @@ def _minimal_images_gen_json(omni_server: OmniServer) -> dict[str, object]: @pytest.mark.parametrize("omni_server", _PARAMS, indirect=True) def test_images_generations_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, body_spec: Any, err_message: str | tuple[str, ...], ) -> None: @@ -137,6 +137,6 @@ def test_images_generations_invalid_requests( assert isinstance(body_spec, dict) body = _minimal_images_gen_json(omni_server) body.update(body_spec) - openai_client.send_images_generations_http_request( + online_client.send_images_generations_http_request( {"json": body, "timeout": 300, "err_code": 400, "err_message": err_message} ) diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_omni_chat.py b/tests/dfx/reliability/invalid_param_test/test_invalid_omni_chat.py index 9e39b059b..989c85432 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_omni_chat.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_omni_chat.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Invalid inputs on Qwen3-Omni: ``POST /v1/chat/completions``, ``WS /v1/video/chat/stream``, ``WS /v1/realtime``.""" from __future__ import annotations @@ -10,7 +10,7 @@ from typing import Any import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient from tests.helpers.stage_config import get_deploy_config_path pytestmark = [pytest.mark.slow, pytest.mark.omni] @@ -140,7 +140,7 @@ def _chat_completions_request_without_expectations(omni_server: OmniServer, case @pytest.mark.parametrize("omni_server", _QWEN3_OMNI_SERVER, indirect=True) def test_chat_completions_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, case_id: str, err_code: int | tuple[int, ...], err_message: str | tuple[str, ...], @@ -148,7 +148,7 @@ def test_chat_completions_invalid_requests( cfg = _chat_completions_request_without_expectations(omni_server, case_id) cfg["err_code"] = err_code cfg["err_message"] = err_message - openai_client.send_chat_completions_http_request(cfg)[0] + online_client.send_chat_completions_http_request(cfg)[0] # ───────────────────────────────────────────────────────────────────────────── @@ -174,7 +174,7 @@ _VIDEO_CHAT_WS_SESSION_MODALITIES_SCALAR = object() @pytest.mark.parametrize("omni_server", _QWEN3_OMNI_SERVER, indirect=True) def test_video_chat_stream_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, send_frames_spec: Any, err_message: str, ) -> None: @@ -183,7 +183,7 @@ def test_video_chat_stream_invalid_requests( else: send_frames = send_frames_spec assert isinstance(send_frames, str) - openai_client.send_video_chat_stream_ws_request( + online_client.send_video_chat_stream_ws_request( { "send_frames": send_frames, "timeout": 120, @@ -243,11 +243,12 @@ _REALTIME_WS_INVALID_AUDIO_APPEND = object() @pytest.mark.parametrize("omni_server", _QWEN3_OMNI_SERVER, indirect=True) def test_realtime_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, send_frames_spec: Any, ws_error_code: str, err_message: str | tuple[str, ...], ) -> None: + send_frames: Any if send_frames_spec is _REALTIME_WS_MODEL_MISMATCH: send_frames = json.dumps( { @@ -262,7 +263,7 @@ def test_realtime_invalid_requests( ] else: send_frames = send_frames_spec - openai_client.send_realtime_ws_request( + online_client.send_realtime_ws_request( { "send_frames": send_frames, "timeout": 120, diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_server_control.py b/tests/dfx/reliability/invalid_param_test/test_invalid_server_control.py index ad3f659f1..4be838316 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_server_control.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_server_control.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Omni sleep/wakeup JSON validation and level-2 wake contract (live ``Qwen/Qwen-Image``).""" from __future__ import annotations @@ -9,7 +9,7 @@ from typing import Any, Literal import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.slow, pytest.mark.diffusion, pytest.mark.omni] @@ -86,7 +86,7 @@ _QWEN_IMAGE = [ @pytest.mark.parametrize("omni_server", _QWEN_IMAGE, indirect=True) def test_omni_sleep_wakeup_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, endpoint: Literal["sleep", "wakeup"], json_body: dict[str, Any], err_message: str | tuple[str, ...], @@ -98,9 +98,9 @@ def test_omni_sleep_wakeup_invalid_requests( "err_message": err_message, } if endpoint == "sleep": - openai_client.send_omni_sleep_http_request(cfg) + online_client.send_omni_sleep_http_request(cfg) else: - openai_client.send_omni_wakeup_http_request(cfg) + online_client.send_omni_wakeup_http_request(cfg) # Keep this after the invalid-JSON cases: sleep(level=2) discards weights and the @@ -108,7 +108,7 @@ def test_omni_sleep_wakeup_invalid_requests( @pytest.mark.parametrize("omni_server", _QWEN_IMAGE, indirect=True) def test_wakeup_after_level2_sleep_fails( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ) -> None: """Regression for #4473 Repro A via HTTP: wake after sleep(level=2) → 501. @@ -116,7 +116,7 @@ def test_wakeup_after_level2_sleep_fails( Live serve surfaces ``NotImplementedError`` as OpenAI-style JSON with code 501. """ assert omni_server is not None - sleep_resps = openai_client.send_omni_sleep_http_request( + sleep_resps = online_client.send_omni_sleep_http_request( { "json": {"stage_ids": [0], "level": 2}, "timeout": 180, @@ -124,10 +124,11 @@ def test_wakeup_after_level2_sleep_fails( ) sleep_resp = sleep_resps[0] assert sleep_resp.status_code == 200, sleep_resp - assert sleep_resp.json_body is not None - assert sleep_resp.json_body.get("status") == "SUCCESS", sleep_resp.json_body + json_body = sleep_resp.json_body + assert isinstance(json_body, dict) + assert json_body.get("status") == "SUCCESS", json_body - openai_client.send_omni_wakeup_http_request( + online_client.send_omni_wakeup_http_request( { "json": {"stage_ids": [0]}, "timeout": 120, diff --git a/tests/dfx/reliability/invalid_param_test/test_invalid_video_generation.py b/tests/dfx/reliability/invalid_param_test/test_invalid_video_generation.py index c0dd133c9..75ff814df 100644 --- a/tests/dfx/reliability/invalid_param_test/test_invalid_video_generation.py +++ b/tests/dfx/reliability/invalid_param_test/test_invalid_video_generation.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """HTTP video job routes (live ``Wan-AI/Wan2.2-T2V-A14B-Diffusers``). WebSocket ``/v1/video/chat/stream`` and ``/v1/realtime`` invalid-input checks live in @@ -13,7 +13,7 @@ from typing import Any import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.slow, pytest.mark.diffusion] @@ -54,12 +54,12 @@ def _minimal_video_form_data() -> dict[str, str]: @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) def test_post_videos_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, form_extra: dict[str, str] | None, err_message: str | tuple[str, ...], ) -> None: data: dict[str, str] = {} if form_extra is None else {**_minimal_video_form_data(), **form_extra} - openai_client.send_videos_create_http_request( + online_client.send_videos_create_http_request( {"data": data, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -85,12 +85,12 @@ def test_post_videos_invalid_requests( @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) def test_post_videos_sync_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, form_extra: dict[str, str] | None, err_message: str | tuple[str, ...], ) -> None: data: dict[str, str] = {} if form_extra is None else {**_minimal_video_form_data(), **form_extra} - openai_client.send_videos_sync_http_request( + online_client.send_videos_sync_http_request( {"data": data, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -111,11 +111,11 @@ def test_post_videos_sync_invalid_requests( @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) def test_get_videos_list_invalid_requests( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, params: dict[str, Any], err_message: str | tuple[str, ...], ) -> None: - openai_client.send_videos_list_http_request( + online_client.send_videos_list_http_request( {"params": params, "timeout": 120, "err_code": 400, "err_message": err_message} ) @@ -126,8 +126,8 @@ def test_get_videos_list_invalid_requests( @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) -def test_get_video_invalid_requests(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_video_retrieve_http_request( +def test_get_video_invalid_requests(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_video_retrieve_http_request( { "video_id": "does-not-exist", "timeout": 120, @@ -143,8 +143,8 @@ def test_get_video_invalid_requests(omni_server: OmniServer, openai_client: Open @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) -def test_delete_video_invalid_requests(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_video_delete_http_request( +def test_delete_video_invalid_requests(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_video_delete_http_request( { "video_id": "does-not-exist", "timeout": 120, @@ -160,8 +160,8 @@ def test_delete_video_invalid_requests(omni_server: OmniServer, openai_client: O @pytest.mark.parametrize("omni_server", _WAN_T2V, indirect=True) -def test_get_video_content_invalid_requests(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: - openai_client.send_video_content_http_request( +def test_get_video_content_invalid_requests(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: + online_client.send_video_content_http_request( { "video_id": "does-not-exist", "timeout": 120, diff --git a/tests/dfx/reliability/test_reliability_hunyuan_image.py b/tests/dfx/reliability/test_reliability_hunyuan_image.py index 43a684d1d..ae0133503 100644 --- a/tests/dfx/reliability/test_reliability_hunyuan_image.py +++ b/tests/dfx/reliability/test_reliability_hunyuan_image.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ HunyuanImage-3.0-Instruct reliability integration tests (DiT-only deploy). @@ -35,7 +38,7 @@ from tests.dfx.reliability.helpers import ( worker_residual_timeout_after_kill_signal, ) from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OpenAIClientHandler +from tests.helpers.runtime import OnlineOmniClient RELIABILITY_SCENARIOS: list[dict[str, Any]] = [ { @@ -133,8 +136,8 @@ def _image_request_config(omni_server: Any) -> dict[str, Any]: } -def _submit_image_generation(openai_client: OpenAIClientHandler, omni_server: Any) -> None: - responses = openai_client.send_images_generations_http_request(_image_request_config(omni_server)) +def _submit_image_generation(online_client: OnlineOmniClient, omni_server: Any) -> None: + responses = online_client.send_images_generations_http_request(_image_request_config(omni_server)) resp = responses[0] if resp.status_code >= 400: raise RuntimeError( @@ -189,7 +192,7 @@ def _assert_post_fault_health_terminal(host: str, port: int, *, scenario: str) - @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_gpu_oom_image_large_request_failure( omni_server_function, - openai_client_function, + online_client_function, ) -> None: stage_config_path = getattr(omni_server_function, "stage_config_path", None) device_spec = resolve_oom_device_spec(OOM_INJECTION_CONFIG, stage_config_path) @@ -202,7 +205,7 @@ def test_reliability_fault_gpu_oom_image_large_request_failure( ) try: try: - _submit_image_generation(openai_client_function, omni_server_function) + _submit_image_generation(online_client_function, omni_server_function) except Exception as exc: assert_fault_exception(exc, FAULT_ERROR_KEYWORDS) else: @@ -222,10 +225,10 @@ def test_reliability_fault_gpu_oom_image_large_request_failure( @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_process_kill_request_failure( omni_server_after_fault_function, - openai_client_function, + online_client_function, ) -> None: try: - _submit_image_generation(openai_client_function, omni_server_after_fault_function) + _submit_image_generation(online_client_function, omni_server_after_fault_function) except Exception as exc: assert_fault_exception(exc, PROCESS_KILL_ERROR_KEYWORDS) else: @@ -244,7 +247,7 @@ def test_reliability_fault_process_kill_request_failure( @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( omni_server_after_fault_function, - openai_client_function, + online_client_function, ) -> None: host = omni_server_after_fault_function.host port = omni_server_after_fault_function.port @@ -280,7 +283,7 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( start = time.monotonic() with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: futures = [ - executor.submit(_submit_image_generation, openai_client_function, omni_server_after_fault_function) + executor.submit(_submit_image_generation, online_client_function, omni_server_after_fault_function) for _ in range(3) ] done, pending = concurrent.futures.wait( @@ -335,12 +338,12 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_process_kill_worker_with_load_request_failure( omni_server_function, - openai_client_function, + online_client_function, fault_injector: FaultInjector, ) -> None: scenario = "kill_worker_with_load" load_result = run_fault_injection_with_rate_load( - submit_request=lambda: _submit_image_generation(openai_client_function, omni_server_function), + submit_request=lambda: _submit_image_generation(online_client_function, omni_server_function), inject_fault=lambda: fault_injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -363,13 +366,13 @@ def test_reliability_fault_process_kill_worker_with_load_request_failure( @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_process_kill_serve_root_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: scenario = f"kill_serve_root_with_load_{signal_name.lower()}" injector = make_server_root_kill_fault_injector(signal_name=signal_name, post_kill_wait_seconds=2.0) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: _submit_image_generation(openai_client_function, omni_server_function), + submit_request=lambda: _submit_image_generation(online_client_function, omni_server_function), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -448,7 +451,7 @@ def test_reliability_fault_process_kill_tree_no_load_fast_fail_and_cleanup( @pytest.mark.parametrize("omni_server_function", HUNYUAN_PARAMS, indirect=True) def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: scenario = f"kill_serve_tree_with_load_{signal_name.lower()}" @@ -458,7 +461,7 @@ def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( inter_kill_wait_seconds=0.1, ) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: _submit_image_generation(openai_client_function, omni_server_function), + submit_request=lambda: _submit_image_generation(online_client_function, omni_server_function), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, diff --git a/tests/dfx/reliability/test_reliability_qwen3_omni.py b/tests/dfx/reliability/test_reliability_qwen3_omni.py index d12a11ba1..10331d88d 100644 --- a/tests/dfx/reliability/test_reliability_qwen3_omni.py +++ b/tests/dfx/reliability/test_reliability_qwen3_omni.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Qwen3-Omni reliability integration tests. """ @@ -63,7 +66,7 @@ DEPLOY_CONFIGS_DIR = Path(__file__).resolve().parent.parent.parent.parent / "vll def _default_oom_device_spec() -> str: """Use currently visible CUDA ordinals to avoid invalid device index in sidecar.""" - count = torch.accelerator.device_count() + count = torch.accelerator.device_count() or 0 if count <= 0: return "0" return ",".join(str(i) for i in range(count)) @@ -414,7 +417,7 @@ def test_reliability_fault_gpu_oom_error_contract_consistent_chat_speech( @pytest.mark.slow @hardware_test(res={"cuda": "H100"}, num_cards=2) @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) -def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_function, openai_client_function) -> None: +def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_function, online_client_function) -> None: device_spec = resolve_oom_device_spec( OOM_INJECTION_CONFIG, _deploy_config_path_from_omni_server(omni_server_function), @@ -444,7 +447,7 @@ def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_functi "key_words": {"audio": ["test"]}, } try: - openai_client_function.send_omni_request(request_config, request_num=1) + online_client_function.send_omni_request(request_config, request_num=1) except Exception as exc: assert_fault_exception(exc, FAULT_ERROR_KEYWORDS) else: @@ -456,7 +459,7 @@ def test_reliability_fault_gpu_oom_chat_large_payload_failure(omni_server_functi @pytest.mark.slow @hardware_test(res={"cuda": "H100"}, num_cards=2) @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) -def test_reliability_fault_gpu_oom_concurrent_pressure_failure(omni_server_function, openai_client_function) -> None: +def test_reliability_fault_gpu_oom_concurrent_pressure_failure(omni_server_function, online_client_function) -> None: device_spec = resolve_oom_device_spec( OOM_INJECTION_CONFIG, _deploy_config_path_from_omni_server(omni_server_function), @@ -487,7 +490,7 @@ def test_reliability_fault_gpu_oom_concurrent_pressure_failure(omni_server_funct "key_words": {"audio": ["test"]}, } try: - openai_client_function.send_omni_request(request_config, request_num=4) + online_client_function.send_omni_request(request_config, request_num=4) except Exception as exc: assert_fault_exception(exc, FAULT_ERROR_KEYWORDS) else: @@ -505,7 +508,7 @@ def test_reliability_fault_gpu_oom_concurrent_pressure_failure(omni_server_funct ) @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) def test_reliability_fault_process_kill_request_failure( - omni_server_after_fault_function, openai_client_function + omni_server_after_fault_function, online_client_function ) -> None: messages = dummy_messages_from_mix_data( system_prompt=_get_system_prompt(), @@ -519,7 +522,7 @@ def test_reliability_fault_process_kill_request_failure( "key_words": {"text": ["beijing"]}, } try: - openai_client_function.send_omni_request(request_config, request_num=1) + online_client_function.send_omni_request(request_config, request_num=1) except Exception as exc: assert_fault_exception(exc, PROCESS_KILL_ERROR_KEYWORDS) else: @@ -567,7 +570,7 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( assert elapsed <= _PROCESS_KILL_WORKER_CHAT_FF_MAX_ELAPSED_SEC + 1.0, ( f"[process_kill fast_fail] request did not fail fast after fault: {elapsed:.2f}s" ) - assert ff_status >= 500, ( + assert ff_status is not None and ff_status >= 500, ( f"[process_kill fast_fail] expected server-side failure after fault, " f"got status={ff_status}, body={ff_body[:200]!r}" ) @@ -652,13 +655,13 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) def test_reliability_fault_process_kill_worker_with_load_request_failure( omni_server_function, - openai_client_function, + online_client_function, fault_injector: FaultInjector, ) -> None: request_config = _chat_request_config(omni_server_function) scenario = "kill_worker_with_load" load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_omni_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_omni_request(request_config, request_num=1), inject_fault=lambda: fault_injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -704,14 +707,14 @@ def test_reliability_fault_process_kill_serve_root_no_load_fast_fail_and_cleanup @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) def test_reliability_fault_process_kill_serve_root_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: request_config = _chat_request_config(omni_server_function) scenario = f"kill_serve_root_with_load_{signal_name.lower()}" injector = make_server_root_kill_fault_injector(signal_name=signal_name, post_kill_wait_seconds=2.0) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_omni_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_omni_request(request_config, request_num=1), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -764,7 +767,7 @@ def test_reliability_fault_process_kill_tree_no_load_fast_fail_and_cleanup( @pytest.mark.parametrize("omni_server_function", QWEN_PARAMS, indirect=True) def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: request_config = _chat_request_config(omni_server_function) @@ -775,7 +778,7 @@ def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( inter_kill_wait_seconds=0.1, ) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_omni_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_omni_request(request_config, request_num=1), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, diff --git a/tests/dfx/reliability/test_reliability_wan22.py b/tests/dfx/reliability/test_reliability_wan22.py index 84175429d..8a86c1c92 100644 --- a/tests/dfx/reliability/test_reliability_wan22.py +++ b/tests/dfx/reliability/test_reliability_wan22.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Wan2.2 reliability integration tests. """ @@ -183,7 +186,7 @@ def _assert_post_fault_health_terminal(host: str, port: int, *, scenario: str) - @pytest.mark.slow @hardware_test(res={"cuda": "H100"}, num_cards=1) @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) -def test_reliability_fault_gpu_oom_video_large_request_failure(omni_server_function, openai_client_function) -> None: +def test_reliability_fault_gpu_oom_video_large_request_failure(omni_server_function, online_client_function) -> None: stage_config_path = getattr(omni_server_function, "stage_config_path", None) device_spec = resolve_oom_device_spec(OOM_INJECTION_CONFIG, stage_config_path) handle = inject_gpu_oom( @@ -211,7 +214,7 @@ def test_reliability_fault_gpu_oom_video_large_request_failure(omni_server_funct "stream": False, } try: - openai_client_function.send_video_diffusion_request(request_config, request_num=1) + online_client_function.send_video_diffusion_request(request_config, request_num=1) except Exception as exc: assert_fault_exception(exc, FAULT_ERROR_KEYWORDS) else: @@ -230,11 +233,11 @@ def test_reliability_fault_gpu_oom_video_large_request_failure(omni_server_funct @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) def test_reliability_fault_process_kill_request_failure( omni_server_after_fault_function, - openai_client_function, + online_client_function, ) -> None: request_config = _video_request_config() try: - openai_client_function.send_video_diffusion_request(request_config, request_num=1) + online_client_function.send_video_diffusion_request(request_config, request_num=1) except Exception as exc: assert_fault_exception(exc, PROCESS_KILL_ERROR_KEYWORDS) else: @@ -251,7 +254,7 @@ def test_reliability_fault_process_kill_request_failure( @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( omni_server_after_fault_function, - openai_client_function, + online_client_function, ) -> None: """Black-box: after process kill, /v1/videos fails fast and concurrent calls don't hang. @@ -308,7 +311,7 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( start = time.monotonic() with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor: futures = [ - executor.submit(openai_client_function.send_video_diffusion_request, request_config, 1) for _ in range(3) + executor.submit(online_client_function.send_video_diffusion_request, request_config, 1) for _ in range(3) ] done, pending = concurrent.futures.wait( futures, @@ -367,13 +370,13 @@ def test_reliability_fault_process_kill_health_fast_fail_and_concurrent( @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) def test_reliability_fault_process_kill_worker_with_load_request_failure( omni_server_function, - openai_client_function, + online_client_function, fault_injector: FaultInjector, ) -> None: request_config = _video_request_config() scenario = "kill_worker_with_load" load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_video_diffusion_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_video_diffusion_request(request_config, request_num=1), inject_fault=lambda: fault_injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -516,7 +519,7 @@ def test_reliability_video_oom_recovers_after_fault_removed( @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) def test_reliability_fault_process_kill_serve_root_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: """Black-box: during in-flight video requests, kill serve root and verify post-fault behavior/cleanup.""" @@ -524,7 +527,7 @@ def test_reliability_fault_process_kill_serve_root_with_load_fast_fail_and_clean scenario = f"kill_serve_root_with_load_{signal_name.lower()}" injector = make_server_root_kill_fault_injector(signal_name=signal_name, post_kill_wait_seconds=2.0) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_video_diffusion_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_video_diffusion_request(request_config, request_num=1), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, @@ -601,7 +604,7 @@ def test_reliability_fault_process_kill_tree_no_load_fast_fail_and_cleanup( @pytest.mark.parametrize("omni_server_function", DIFFUSION_VIDEO_PARAMS, indirect=True) def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( omni_server_function, - openai_client_function, + online_client_function, signal_name: str, ) -> None: """Black-box: during in-flight video requests, kill server process tree and verify post-fault behavior/cleanup.""" @@ -613,7 +616,7 @@ def test_reliability_fault_process_kill_tree_with_load_fast_fail_and_cleanup( inter_kill_wait_seconds=0.1, ) load_result = run_fault_injection_with_rate_load( - submit_request=lambda: openai_client_function.send_video_diffusion_request(request_config, request_num=1), + submit_request=lambda: online_client_function.send_video_diffusion_request(request_config, request_num=1), inject_fault=lambda: injector(omni_server_function), num_requests=INFLIGHT_INJECTION_REQUEST_COUNT, request_rate=INFLIGHT_INJECTION_REQUEST_RATE, diff --git a/tests/diffusion/cache/test_cache_dit.py b/tests/diffusion/cache/test_cache_dit.py index 0723c1d62..341d03d3a 100644 --- a/tests/diffusion/cache/test_cache_dit.py +++ b/tests/diffusion/cache/test_cache_dit.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Model specific tests for CacheDiT enablement. @@ -13,6 +13,7 @@ from unittest.mock import Mock, patch import pytest import torch from cache_dit.caching.cache_blocks.pattern_0_1_2 import CachedBlocks_Pattern_0_1_2 +from vllm.distributed import parallel_state import vllm_omni.diffusion.cache.cachedit as cd_backend import vllm_omni.diffusion.cache.cachedit.model_specific as cd_model_specific @@ -61,6 +62,18 @@ def test_custom_cache_dit_enablers_are_registered_explicitly(): assert cd_backend.CUSTOM_DIT_ENABLERS == expected_enablers +@pytest.fixture() +def init_fake_tp_group(mocker): + """Provide a fake TP group so vLLM linear layers can be instantiated.""" + mock_tp = mocker.MagicMock() + mock_tp.world_size = 1 + mock_tp.rank_in_group = 0 + old = parallel_state._TP + parallel_state._TP = mock_tp + yield + parallel_state._TP = old + + def test_wan22_vace_uses_wan22_custom_cache_dit_enabler(): assert cd_backend.CUSTOM_DIT_ENABLERS["Wan22VACEPipeline"] is cd_model_specific.enable_cache_for_wan22 @@ -110,7 +123,9 @@ def test_cachedit_consumers_use_package_api(): for module in modules: if module == legacy_module or module.startswith(internal_prefix): - invalid_imports.append(f"{source_path.relative_to(package_root)}:{node.lineno}: {module}") + invalid_imports.append( + f"{source_path.relative_to(package_root)}:{getattr(node, 'lineno', '?')}: {module}" + ) assert not invalid_imports, "Cache-DiT consumers must use the package API:\n" + "\n".join(invalid_imports) diff --git a/tests/diffusion/diffusion_backend/test_diffusers_backend.py b/tests/diffusion/diffusion_backend/test_diffusers_backend.py index babc23b4a..ec351d746 100644 --- a/tests/diffusion/diffusion_backend/test_diffusers_backend.py +++ b/tests/diffusion/diffusion_backend/test_diffusers_backend.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project from collections import namedtuple from types import SimpleNamespace @@ -10,7 +10,7 @@ from diffusers import DiffusionPipeline # pyright: ignore[reportPrivateImportUs from PIL import Image from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data from vllm_omni.diffusion.data import ( DiffusionOutput, DiffusionParallelConfig, @@ -651,7 +651,7 @@ class TestDiffusersBackendEndToEndExecution: def test_t2i_random_weights( self, omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): messages = dummy_messages_from_mix_data(content_text="a photo of an astronaut riding a horse on mars") @@ -667,8 +667,10 @@ class TestDiffusersBackendEndToEndExecution: }, } - response = openai_client.send_diffusion_request(request_config) - image: Image.Image = response[0].images[0] # pyright: ignore[reportOptionalSubscript] + response = online_client.send_diffusion_request(request_config) + images = response[0].images + assert images is not None + image: Image.Image = images[0] # Request config has incomplete width/height, so internal assertion in `send_diffusion_request` is incomplete. assert image.size == (512, 512) diff --git a/tests/diffusion/distributed/test_tensor_parallel.py b/tests/diffusion/distributed/test_tensor_parallel.py index c50c4336d..e277e351a 100644 --- a/tests/diffusion/distributed/test_tensor_parallel.py +++ b/tests/diffusion/distributed/test_tensor_parallel.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """DiT tensor-parallel parity and perf checks for diffusion models. @@ -18,8 +18,8 @@ import pytest import torch from PIL import Image -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from tests.helpers.runtime import OmniRunner from vllm_omni.diffusion.data import DiffusionParallelConfig from vllm_omni.inputs.data import OmniDiffusionSamplingParams diff --git a/tests/diffusion/offloader/test_diffusion_cpu_offload.py b/tests/diffusion/offloader/test_diffusion_cpu_offload.py index 810f0dd52..80a03180f 100644 --- a/tests/diffusion/offloader/test_diffusion_cpu_offload.py +++ b/tests/diffusion/offloader/test_diffusion_cpu_offload.py @@ -1,25 +1,29 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + import gc +from typing import Any import numpy as np import pytest import torch from vllm.distributed.parallel_state import cleanup_dist_env_and_memory -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from tests.helpers.runtime import OmniRunner from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.platforms import current_omni_platform -AUDIO_MODEL = { +AUDIO_MODEL: dict[str, dict[str, int | None]] = { "stabilityai/stable-audio-open-1.0": {"cuda": 100, "rocm": None}, } -IMAGE_MODELS = { +IMAGE_MODELS: dict[str, dict[str, int | None]] = { "riverclouds/qwen_image_random": {"cuda": 2200, "rocm": 2100}, } -MODELS = {**AUDIO_MODEL, **IMAGE_MODELS} +MODELS: dict[str, dict[str, int | None]] = {**AUDIO_MODEL, **IMAGE_MODELS} MODEL_MARKS = { "riverclouds/qwen_image_random": pytest.mark.core_model, @@ -29,12 +33,12 @@ MODEL_MARKS = { _GATED_MODELS = {"stabilityai/stable-audio-open-1.0"} -AUDIO_MODEL_PARAMS = { +AUDIO_MODEL_PARAMS: dict[str, dict[str, Any]] = { "runner_params": {}, "sampler_params": {}, } -IMAGE_MODEL_PARAMS = { +IMAGE_MODEL_PARAMS: dict[str, dict[str, Any]] = { "runner_params": {}, "sampler_params": { "height": 256, @@ -43,7 +47,7 @@ IMAGE_MODEL_PARAMS = { } -def inference(model_name: str, offload: bool = True): +def inference(model_name: str, offload: bool = True) -> tuple[float, Any]: gc.collect() current_omni_platform.empty_cache() device_index = current_omni_platform.current_device() @@ -133,6 +137,7 @@ def test_cpu_offload_diffusion_model(model_name: str): threshold = MODELS[model_name][platform] if threshold is None: pytest.skip(f"Threshold not defined for {platform} on {model_name}") + assert threshold is not None assert offload_peak_memory + threshold < no_offload_peak_memory, ( f"Offload peak memory {offload_peak_memory} MB should be less than " diff --git a/tests/diffusion/offloader/test_diffusion_layerwise_offload.py b/tests/diffusion/offloader/test_diffusion_layerwise_offload.py index 26944b202..3918f23e4 100644 --- a/tests/diffusion/offloader/test_diffusion_layerwise_offload.py +++ b/tests/diffusion/offloader/test_diffusion_layerwise_offload.py @@ -1,36 +1,41 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +from typing import Any + import numpy as np import pytest import torch from vllm.distributed.parallel_state import cleanup_dist_env_and_memory -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from tests.helpers.runtime import OmniRunner from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.platforms import current_omni_platform -AUDIO_MODEL = { +AUDIO_MODEL: dict[str, dict[str, int | None]] = { "stabilityai/stable-audio-open-1.0": {"cuda": 1500, "rocm": 1500}, } -IMAGE_VIDEO_MODELS = { +IMAGE_VIDEO_MODELS: dict[str, dict[str, int | None]] = { "riverclouds/qwen_image_random": {"cuda": 4500, "rocm": None}, # "Wan-AI/Wan2.2-T2V-A14B-Diffusers": {"cuda": 45000, "rocm": None}, } -MODELS = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS} +MODELS: dict[str, dict[str, int | None]] = {**AUDIO_MODEL, **IMAGE_VIDEO_MODELS} MODEL_MARKS = { "riverclouds/qwen_image_random": pytest.mark.core_model, "stabilityai/stable-audio-open-1.0": pytest.mark.full_model, } -AUDIO_MODEL_PARAMS = { +AUDIO_MODEL_PARAMS: dict[str, dict[str, Any]] = { "runner_params": {}, "sampler_params": {}, } -IMAGE_VIDEO_MODELS_PARAMS = { +IMAGE_VIDEO_MODELS_PARAMS: dict[str, dict[str, Any]] = { "runner_params": {"boundary_ratio": 0.875, "flow_shift": 5.0}, "sampler_params": {"height": 480, "width": 640, "num_frames": 5}, } @@ -55,7 +60,7 @@ def run_inference( model_name: str, layerwise_offload: bool = False, num_inference_steps: int = 3, -) -> float: +) -> tuple[float, Any]: current_omni_platform.empty_cache() device_index = current_omni_platform.current_device() monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02) @@ -140,6 +145,7 @@ def test_layerwise_offload_diffusion_model(model_name: str): if expected_saved_memory is None: pytest.skip(f"Threshold not defined for {platform} on {model_name}") + assert expected_saved_memory is not None # Verify that layerwise offloading significantly reduces memory usage # Passes only if the actual savings exceeds the expected savings diff --git a/tests/diffusion/quantization/test_gguf_diffusion.py b/tests/diffusion/quantization/test_gguf_diffusion.py index 243282b0c..ad2e72edc 100644 --- a/tests/diffusion/quantization/test_gguf_diffusion.py +++ b/tests/diffusion/quantization/test_gguf_diffusion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """End-to-end tests for GGUF quantization on diffusion models. Validates that GGUF-quantized diffusion models generate valid images and @@ -19,8 +19,8 @@ import numpy as np import pytest import torch -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from tests.helpers.runtime import OmniRunner from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.outputs import OmniRequestOutput diff --git a/tests/e2e/accuracy/helpers.py b/tests/e2e/accuracy/helpers.py index 99880fc73..832d34557 100644 --- a/tests/e2e/accuracy/helpers.py +++ b/tests/e2e/accuracy/helpers.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + import json import os import re @@ -14,7 +17,7 @@ from hashlib import sha1 from io import BytesIO from mimetypes import guess_type from pathlib import Path -from typing import Any +from typing import IO, Any from urllib.parse import urlparse import numpy as np @@ -35,11 +38,14 @@ class IdentityFtfy: def apply_ftfy_mock(*, wan_i2v_module: Any | None = None) -> None: """Mock ftfy library for test cases in the main process""" - if wan_i2v_module is None: - from diffusers.pipelines.wan import pipeline_wan_i2v as wan_i2v_module + module: Any = wan_i2v_module + if module is None: + from diffusers.pipelines.wan import pipeline_wan_i2v as wan_pipeline - if not hasattr(wan_i2v_module, "ftfy"): - wan_i2v_module.ftfy = IdentityFtfy() + module = wan_pipeline + + if not hasattr(module, "ftfy"): + module.ftfy = IdentityFtfy() print("ftfy (text encoding sanitizer) is not installed. Using mock ftfy implementation (identity function)") else: print("ftfy (text encoding sanitizer) is installed. Using actual ftfy implementation.") @@ -118,6 +124,7 @@ def probe_binary(binary: str, *, test_name: str = "video similarity e2e test") - resolved = shutil.which(binary) if resolved is None: pytest.skip(f"{binary} is required for {test_name}.") + assert resolved is not None return resolved @@ -235,7 +242,7 @@ def video_artifact_dir(result_root: Path, source: str) -> Path: def send_video_request_with_timeout( - openai_client, + online_client, request_config: dict[str, Any], *, timeout_seconds: int, @@ -266,7 +273,7 @@ def send_video_request_with_timeout( normalized_form_data["image_reference"] = json.dumps({"image_url": image_reference}) start_time = time.perf_counter() - create_url = openai_client._build_url("/v1/videos") + create_url = online_client._build_url("/v1/videos") response = requests.post( create_url, data=normalized_form_data, @@ -280,8 +287,8 @@ def send_video_request_with_timeout( raise requests.HTTPError(f"{exc}; response body: {response.text}", response=response) from exc job_data = response.json() video_id = job_data["id"] - openai_client._wait_until_video_completed(video_id, timeout_seconds=timeout_seconds) - video_content = openai_client._download_video_content(video_id) + online_client._wait_until_video_completed(video_id, timeout_seconds=timeout_seconds) + video_content = online_client._download_video_content(video_id) print(f"online_video_e2e_latency_s={time.perf_counter() - start_time:.3f}") return video_content @@ -571,11 +578,11 @@ class SemanticSimilarityScorer: raise ImportError("FlagEmbedding not installed. Install with: pip install FlagEmbedding") return self._text_model - def text_similarity(self, text1: str, text2: str) -> float: + def text_similarity(self, text1: str, text2: str) -> dict[str, float | int]: """ Compute semantic similarity between two texts using BGE-M3 """ - results = {} + results: dict[str, float | int] = {} # 1. Text prefix matching (maximum matching character length) match_count = 0 for char_vllm, char_ref in zip(text1, text2): @@ -653,7 +660,7 @@ def dreamzero_cfg_parallel_size() -> int: def _dreamzero_parity_vllm_env() -> dict[str, str]: - from tests.helpers.env import pick_least_used_device_indices + from tests.helpers.clean import pick_least_used_device_indices from tests.helpers.runtime import get_open_port parallel_size = dreamzero_cfg_parallel_size() @@ -683,8 +690,9 @@ def dreamzero_parity_vllm_params(model: str | None = None): ] if parallel_size > 1: server_args.extend(["--cfg-parallel-size", str(parallel_size)]) + resolved_model = model or os.environ.get("VLLM_DREAMZERO_MODEL") or _DREAMZERO_DEFAULT_MODEL return OmniServerParams( - model=model or os.environ.get("VLLM_DREAMZERO_MODEL", _DREAMZERO_DEFAULT_MODEL), + model=resolved_model, stage_config_path=get_deploy_config_path("dreamzero_tp1_cfg2.yaml"), server_args=server_args, env_dict=_dreamzero_parity_vllm_env(), @@ -725,9 +733,9 @@ def dreamzero_parity_vllm_client( def _dreamzero_create_openpi_vllm_client(host: str, port: int) -> Any: from openpi_client import msgpack_numpy - _, WebsocketClientPolicy = load_dreamzero_upstream_modules() + _, websocket_client_policy_cls = load_dreamzero_upstream_modules() - class _OpenPIWebsocketClientPolicy(WebsocketClientPolicy): + class _OpenPIWebsocketClientPolicy(websocket_client_policy_cls): # type: ignore[misc, valid-type] def __init__( self, host: str = "127.0.0.1", @@ -869,7 +877,7 @@ class _DreamZeroParityServer(ABC): self.port = get_open_port() if port is None else port self.ready_timeout_s = ready_timeout_s self.proc: subprocess.Popen[str] | None = None - self._log_file = None + self._log_file: IO[str] | None = None @abstractmethod def _build_argv(self) -> list[str]: @@ -932,7 +940,7 @@ class DreamZeroUpstreamServer(_DreamZeroParityServer): ] def _build_env(self) -> dict[str, str]: - from tests.helpers.env import pick_least_used_device_indices + from tests.helpers.clean import pick_least_used_device_indices dreamzero_repo, _ = require_dreamzero_parity_env() env = os.environ.copy() diff --git a/tests/e2e/accuracy/hunyuanvideo15_i2v/test_hunyuanvideo15_i2v_video_similarity.py b/tests/e2e/accuracy/hunyuanvideo15_i2v/test_hunyuanvideo15_i2v_video_similarity.py index b830cdc6c..eb8f2d769 100644 --- a/tests/e2e/accuracy/hunyuanvideo15_i2v/test_hunyuanvideo15_i2v_video_similarity.py +++ b/tests/e2e/accuracy/hunyuanvideo15_i2v/test_hunyuanvideo15_i2v_video_similarity.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import os @@ -149,7 +152,7 @@ def _generate_offline_video(*, image_source: str) -> Path: def _generate_online_video( *, omni_server, - openai_client, + online_client, image_source: str, timeout_seconds: int, ) -> Path: @@ -171,7 +174,7 @@ def _generate_online_video( "image_reference": build_online_image_reference(online_image_source), } online_video_bytes = send_video_request_with_timeout( - openai_client, + online_client, request_config, timeout_seconds=timeout_seconds, ) @@ -204,7 +207,7 @@ def test_hunyuanvideo15_i2v_diffusers_offline_generates_video( @pytest.mark.parametrize("omni_server", SERVER_CASES, indirect=True) def test_hunyuanvideo15_i2v_online_serving_generates_video( omni_server, - openai_client, + online_client, hunyuanvideo15_i2v_image_source: str | None, hunyuanvideo15_online_timeout_seconds: int, ) -> None: @@ -216,7 +219,7 @@ def test_hunyuanvideo15_i2v_online_serving_generates_video( validate_image_source(image_source) online_path = _generate_online_video( omni_server=omni_server, - openai_client=openai_client, + online_client=online_client, image_source=image_source, timeout_seconds=hunyuanvideo15_online_timeout_seconds, ) diff --git a/tests/e2e/accuracy/hunyuanvideo15_t2v/test_hunyuanvideo15_t2v_video_similarity.py b/tests/e2e/accuracy/hunyuanvideo15_t2v/test_hunyuanvideo15_t2v_video_similarity.py index f2c14e2b0..f5b90822d 100644 --- a/tests/e2e/accuracy/hunyuanvideo15_t2v/test_hunyuanvideo15_t2v_video_similarity.py +++ b/tests/e2e/accuracy/hunyuanvideo15_t2v/test_hunyuanvideo15_t2v_video_similarity.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import os @@ -115,7 +118,7 @@ def _generate_offline_video() -> Path: return offline_path -def _generate_online_video(*, omni_server, openai_client, timeout_seconds: int) -> Path: +def _generate_online_video(*, omni_server, online_client, timeout_seconds: int) -> Path: online_path, _ = _artifact_paths() request_config = { "model": omni_server.model, @@ -132,7 +135,7 @@ def _generate_online_video(*, omni_server, openai_client, timeout_seconds: int) }, } online_video_bytes = send_video_request_with_timeout( - openai_client, + online_client, request_config, timeout_seconds=timeout_seconds, ) @@ -161,7 +164,7 @@ def test_hunyuanvideo15_t2v_diffusers_offline_generates_video() -> None: @pytest.mark.parametrize("omni_server", SERVER_CASES, indirect=True) def test_hunyuanvideo15_t2v_online_serving_generates_video( omni_server, - openai_client, + online_client, hunyuanvideo15_online_timeout_seconds: int, ) -> None: if not torch.cuda.is_available(): @@ -170,7 +173,7 @@ def test_hunyuanvideo15_t2v_online_serving_generates_video( probe_binary("ffprobe") online_path = _generate_online_video( omni_server=omni_server, - openai_client=openai_client, + online_client=online_client, timeout_seconds=hunyuanvideo15_online_timeout_seconds, ) assert online_path.exists(), f"Expected online video artifact at {online_path}" diff --git a/tests/e2e/accuracy/qwen3_omni/qwen3_omni_acc_bench_core.py b/tests/e2e/accuracy/qwen3_omni/qwen3_omni_acc_bench_core.py index dfc53a06e..d612b96a5 100644 --- a/tests/e2e/accuracy/qwen3_omni/qwen3_omni_acc_bench_core.py +++ b/tests/e2e/accuracy/qwen3_omni/qwen3_omni_acc_bench_core.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Shared helpers for Qwen3-Omni Daily-Omni / Seed-TTS ``vllm bench serve --omni`` accuracy runs. Local dataset paths are **optional**. When ``VLLM_DAILY_OMNI_QA_JSON`` + ``VLLM_DAILY_OMNI_VIDEO_DIR`` @@ -28,7 +29,7 @@ DEFAULT_SEED_TTS_HF_REPO = "zhaochenyang20/seed-tts-eval" class OmniBenchServerEndpoint(Protocol): - """Anything with ``host`` / ``port`` / ``model`` (e.g. :class:`tests.conftest.OmniServer`).""" + """Anything with ``host`` / ``port`` / ``model`` (e.g. :class:`tests.helpers.runtime.OmniServer`).""" host: str port: int diff --git a/tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py b/tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py index 121551664..7c48053e1 100644 --- a/tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py +++ b/tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Qwen3-Omni accuracy benchmarks (Daily-Omni MCQ + Seed-TTS WER) via ``vllm bench serve --omni``. Starts a **module-scoped** Omni OpenAI-compatible server (same pattern as ``tests/dfx/perf`` and @@ -67,12 +68,8 @@ def get_chunk_config(config_path: str | None = None): """Load the qwen3_omni CI deploy yaml with async_chunk modifications for streaming mode.""" if config_path is None: config_path = _CI_DEPLOY - # TODO: remove this workaround once legacy `stage_args` path is deleted. - # The pipeline (qwen3_omni/pipeline.py) already wires - # thinker2talker_async_chunk / talker2code2wav_async_chunk on stage 0/1, - # so only async_chunk needs flipping. Writing nested `engine_args:` into - # the new-schema overlay trips _parse_stage_deploy's legacy branch and - # drops flat fields (load_format, max_num_seqs, ...). + # Pipeline already wires thinker2talker_async_chunk / talker2code2wav_async_chunk + # on stage 0/1; only the top-level async_chunk flag needs flipping here. return modify_stage_config(config_path, updates={"async_chunk": True}) diff --git a/tests/e2e/accuracy/test_diffusers_backend_similarity.py b/tests/e2e/accuracy/test_diffusers_backend_similarity.py index 68f890c64..be3d7f84c 100644 --- a/tests/e2e/accuracy/test_diffusers_backend_similarity.py +++ b/tests/e2e/accuracy/test_diffusers_backend_similarity.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Run vLLM-Omni with diffusers backend, then run diffusers directly. Compare the output similarity. @@ -39,9 +39,9 @@ from tests.e2e.accuracy.helpers import ( from tests.e2e.accuracy.helpers import ( run_ffmpeg_similarity as _run_ffmpeg_similarity, ) -from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup +from tests.helpers.clean import cleanup_test_environment from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniServer, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OnlineOmniClient from vllm_omni.diffusion.models.diffusers_adapter.pipeline_diffusers_adapter import ( CUDA_FLASH_ATTENTION_BACKEND_ATTEMPTS, ) @@ -128,7 +128,7 @@ def _run_vllm_omni_wan22_i2v( "seed": SEED, } with OmniServer(model, server_args, env_dict=env_to_apply_ftfy_mock_in_subproc(), use_omni=True) as omni_server: - client = OpenAIClientHandler( + client = OnlineOmniClient( host=omni_server.host, port=omni_server.port, run_level="full_model", @@ -139,16 +139,20 @@ def _run_vllm_omni_wan22_i2v( "image_reference": f"data:image/png;base64,{pil_to_base64(conditioning_image, 'png')}", } result = client.send_video_diffusion_request(request_config)[0] - video_bytes = result.videos[0] # pyright: ignore[reportOptionalSubscript] # Guaranteed not None + videos = result.videos + assert videos is not None + video_bytes = videos[0] output_path.write_bytes(video_bytes) - return result.e2e_latency # pyright: ignore[reportReturnType] # Guaranteed not None + latency = result.e2e_latency + assert latency is not None + return latency def _run_diffusers_wan22_i2v(*, model: str, output_path: Path, conditioning_image: Image.Image) -> float: from diffusers import WanImageToVideoPipeline # pyright: ignore[reportPrivateImportUsage] from diffusers.schedulers.scheduling_unipc_multistep import UniPCMultistepScheduler - run_pre_test_cleanup() + cleanup_test_environment() apply_ftfy_mock() pipe: WanImageToVideoPipeline | None = None try: @@ -190,7 +194,7 @@ def _run_diffusers_wan22_i2v(*, model: str, output_path: Path, conditioning_imag gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() def _run_vllm_omni_qwen_image(*, model: str, output_path: Path) -> tuple[Image.Image, float]: @@ -248,7 +252,7 @@ def _run_vllm_omni_qwen_image(*, model: str, output_path: Path) -> tuple[Image.I def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> tuple[Image.Image, float]: - run_pre_test_cleanup() + cleanup_test_environment() pipe: DiffusionPipeline | None = None try: pipe = DiffusionPipeline.from_pretrained( @@ -288,7 +292,7 @@ def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> tuple[Image.I gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() @pytest.mark.benchmark diff --git a/tests/e2e/accuracy/test_qwen_image.py b/tests/e2e/accuracy/test_qwen_image.py index 6bb985050..a09014c8d 100644 --- a/tests/e2e/accuracy/test_qwen_image.py +++ b/tests/e2e/accuracy/test_qwen_image.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import base64 @@ -13,7 +16,7 @@ from diffusers.pipelines.pipeline_utils import DiffusionPipeline from PIL import Image from tests.e2e.accuracy.helpers import assert_images_pixel_close, assert_similarity, model_output_dir -from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup +from tests.helpers.clean import cleanup_test_environment from tests.helpers.mark import hardware_test from tests.helpers.runtime import OmniServer @@ -149,7 +152,7 @@ def _run_vllm_omni_qwen_image(*, model: str, output_path: Path) -> Image.Image: def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> Image.Image: - run_pre_test_cleanup() + cleanup_test_environment() pipe: DiffusionPipeline | None = None try: pipe = DiffusionPipeline.from_pretrained( @@ -179,7 +182,7 @@ def _run_diffusers_qwen_image(*, model: str, output_path: Path) -> Image.Image: gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() def _run_vllm_omni_qwen_image_2512(*, model: str, output_path: Path) -> Image.Image: @@ -211,7 +214,7 @@ def _run_vllm_omni_qwen_image_2512(*, model: str, output_path: Path) -> Image.Im def _run_diffusers_qwen_image_2512(*, model: str, output_path: Path) -> Image.Image: - run_pre_test_cleanup() + cleanup_test_environment() pipe: DiffusionPipeline | None = None try: pipe = DiffusionPipeline.from_pretrained( @@ -241,7 +244,7 @@ def _run_diffusers_qwen_image_2512(*, model: str, output_path: Path) -> Image.Im gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() @pytest.mark.benchmark diff --git a/tests/e2e/accuracy/test_qwen_image_edit.py b/tests/e2e/accuracy/test_qwen_image_edit.py index 935a032d8..9c3b9dd9c 100644 --- a/tests/e2e/accuracy/test_qwen_image_edit.py +++ b/tests/e2e/accuracy/test_qwen_image_edit.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import gc @@ -12,7 +15,7 @@ from PIL import Image from benchmarks.accuracy.common import decode_base64_image, pil_to_png_bytes from tests.e2e.accuracy.helpers import assert_images_pixel_close, assert_similarity, model_output_dir -from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup +from tests.helpers.clean import cleanup_test_environment from tests.helpers.mark import hardware_test from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServer @@ -109,7 +112,7 @@ def _run_diffusers_image_edit( input_images: list[Image.Image], output_path: Path, ) -> Image.Image: - run_pre_test_cleanup() + cleanup_test_environment() pipe: QwenImageEditPipeline | QwenImageEditPlusPipeline | None = None try: images = input_images[0] if len(input_images) == 1 else input_images @@ -140,7 +143,7 @@ def _run_diffusers_image_edit( gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() def _load_edit_2511_input_images() -> list[Image.Image]: @@ -189,7 +192,7 @@ def _run_diffusers_image_edit_2511( input_images: list[Image.Image], output_path: Path, ) -> Image.Image: - run_pre_test_cleanup() + cleanup_test_environment() pipe: QwenImageEditPlusPipeline | None = None try: pipe = QwenImageEditPlusPipeline.from_pretrained( @@ -222,7 +225,7 @@ def _run_diffusers_image_edit_2511( gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() def _vllm_omni_output_single_image( diff --git a/tests/e2e/accuracy/test_qwen_image_layered.py b/tests/e2e/accuracy/test_qwen_image_layered.py index 3ad8d3328..53354a739 100644 --- a/tests/e2e/accuracy/test_qwen_image_layered.py +++ b/tests/e2e/accuracy/test_qwen_image_layered.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import base64 @@ -13,7 +16,7 @@ from diffusers.pipelines.pipeline_utils import DiffusionPipeline from PIL import Image from tests.e2e.accuracy.helpers import assert_image_sequence_similarity, model_output_dir -from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup +from tests.helpers.clean import cleanup_test_environment from tests.helpers.mark import hardware_test from tests.helpers.runtime import OmniServer @@ -93,7 +96,7 @@ def _run_vllm_omni_qwen_image_layered(*, model: str, input_image: Image.Image, o def _run_diffusers_qwen_image_layered(*, model: str, input_image: Image.Image, output_dir: Path) -> list[Image.Image]: - run_pre_test_cleanup() + cleanup_test_environment() pipe: DiffusionPipeline | None = None try: pipe = DiffusionPipeline.from_pretrained( @@ -126,7 +129,7 @@ def _run_diffusers_qwen_image_layered(*, model: str, input_image: Image.Image, o gc.collect() if torch.cuda.is_available(): torch.accelerator.empty_cache() - run_post_test_cleanup() + cleanup_test_environment() @pytest.mark.benchmark diff --git a/tests/e2e/accuracy/test_video_streaming_output_similarity.py b/tests/e2e/accuracy/test_video_streaming_output_similarity.py index 1aa7cdfc2..cdb2c4a28 100644 --- a/tests/e2e/accuracy/test_video_streaming_output_similarity.py +++ b/tests/e2e/accuracy/test_video_streaming_output_similarity.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Online serving smoke test for streaming video output similarity. """ @@ -9,7 +12,7 @@ import pytest from tests.e2e.accuracy.helpers import assert_video_similarity_metrics, probe_video from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import DiffusionResponse, OmniServer, OpenAIClientHandler +from tests.helpers.runtime import DiffusionResponse, OmniServer, OnlineOmniClient, get_model_prefix pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -67,8 +70,8 @@ def _request_config(model: str, *, serialize_extra_params: bool) -> dict: } -def _client_for(server: OmniServer, *, run_level: str) -> OpenAIClientHandler: - return OpenAIClientHandler( +def _client_for(server: OmniServer, *, run_level: str) -> OnlineOmniClient: + return OnlineOmniClient( host=server.host, port=server.port, api_key="EMPTY", @@ -95,12 +98,11 @@ def _write_video_artifact(response: DiffusionResponse, output_path: Path) -> Pat ) def test_helios_streaming_output_matches_non_streaming( model: str, - model_prefix: str, run_level: str, tmp_path: Path, record_property, ): - model = model_prefix + model + model = get_model_prefix() + model with OmniServer(model, _server_args(streaming_output=True), use_omni=True) as server: client = _client_for(server, run_level=run_level) diff --git a/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py b/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py index a09d2330a..a995a9cf9 100644 --- a/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py +++ b/tests/e2e/accuracy/wan22_i2v/test_wan22_i2v_video_similarity.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + from __future__ import annotations import os @@ -338,7 +341,7 @@ def _artifact_paths(image_source: str) -> tuple[Path, Path, Path]: def _generate_online_video( *, omni_server, - openai_client, + online_client, image_source: str, online_timeout_seconds: int, ) -> Path: @@ -360,7 +363,7 @@ def _generate_online_video( "image_reference": _build_online_image_reference(image_source), } online_video_bytes = _send_video_request_with_timeout( - openai_client, + online_client, request_config, timeout_seconds=_online_timeout_seconds(online_timeout_seconds), ) @@ -412,7 +415,7 @@ def test_wan22_i2v_diffusers_offline_generates_video( @pytest.mark.parametrize("omni_server", SERVER_CASES, indirect=True) def test_wan22_i2v_online_serving_generates_video( omni_server, - openai_client, + online_client, wan22_i2v_image_source: str | None, wan22_i2v_online_timeout_seconds: int, ) -> None: @@ -424,7 +427,7 @@ def test_wan22_i2v_online_serving_generates_video( _validate_image_source(image_source) online_path = _generate_online_video( omni_server=omni_server, - openai_client=openai_client, + online_client=online_client, image_source=image_source, online_timeout_seconds=wan22_i2v_online_timeout_seconds, ) diff --git a/tests/e2e/features/comfyui/test_comfyui_integration.py b/tests/e2e/features/comfyui/test_comfyui_integration.py index 5dcb9c449..b58e33cb8 100644 --- a/tests/e2e/features/comfyui/test_comfyui_integration.py +++ b/tests/e2e/features/comfyui/test_comfyui_integration.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Integration tests for ComfyUI nodes that use the Omni API client, with a mocked AsyncOmni and a real API server running in a background process. These tests cover the integration between ComfyUI node and the API server, without actual model inference logic. @@ -39,6 +42,7 @@ from pytest_mock import MockerFixture from vllm import SamplingParams from vllm.outputs import CompletionOutput, RequestOutput +from tests.helpers.runtime import get_open_port from vllm_omni.entrypoints.async_omni import AsyncOmni as RealAsyncOmni from vllm_omni.entrypoints.cli.serve import OmniServeCommand from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniSamplingParams @@ -515,7 +519,7 @@ def mock_async_omni( @pytest.fixture -def api_server(unused_tcp_port_factory, server_case: ServerCase, mock_async_omni, tmp_path): +def api_server(server_case: ServerCase, mock_async_omni, tmp_path): """Set up a API server in background process from command line with parametrized model name and mocked AsyncOmni.""" # Override the STORAGE_MANAGER path to a writable temp directory before # forking the server subprocess. The default /tmp/storage may be owned @@ -533,7 +537,7 @@ def api_server(unused_tcp_port_factory, server_case: ServerCase, mock_async_omni cmd = OmniServeCommand() cmd.subparser_init(subparsers) - port = unused_tcp_port_factory() + port = get_open_port() args = parser.parse_args(["serve", server_case.served_model, "--omni", "--port", str(port)]) def run_server(): @@ -550,11 +554,12 @@ def api_server(unused_tcp_port_factory, server_case: ServerCase, mock_async_omni wait_poll_interval = 1 for _ in range(wait_time // wait_poll_interval): try: - response = requests.get(f"http://127.0.0.1:{port}/health") + response = requests.get(f"http://127.0.0.1:{port}/health", timeout=wait_poll_interval) if response.status_code == 200: break - except requests.ConnectionError: - time.sleep(wait_poll_interval) + except requests.RequestException: + pass + time.sleep(wait_poll_interval) else: if server_process.is_alive(): server_process.terminate() diff --git a/tests/e2e/offline_inference/test_covo_audio_expansion.py b/tests/e2e/offline_inference/test_covo_audio_expansion.py index 11672686e..e743814d3 100644 --- a/tests/e2e/offline_inference/test_covo_audio_expansion.py +++ b/tests/e2e/offline_inference/test_covo_audio_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ E2E tests for Covo-Audio-Chat model with audio input and audio/text output. """ @@ -25,7 +28,7 @@ def get_question(): @pytest.mark.omni @hardware_test(res={"cuda": "H100"}, num_cards={"cuda": 1}) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_audio_to_audio(omni_runner, omni_runner_handler) -> None: +def test_audio_to_audio(omni_runner, offline_client) -> None: """ Test audio input → text + audio output. Deploy Setting: default yaml @@ -44,4 +47,4 @@ def test_audio_to_audio(omni_runner, omni_runner_handler) -> None: "audios": (audio, 16000), "modalities": ["audio"], } - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) diff --git a/tests/e2e/offline_inference/test_ernie_image_expansion.py b/tests/e2e/offline_inference/test_ernie_image_expansion.py index a43db7d7e..c986a6366 100644 --- a/tests/e2e/offline_inference/test_ernie_image_expansion.py +++ b/tests/e2e/offline_inference/test_ernie_image_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ End-to-end test for ErnieImage text-to-image generation. @@ -11,7 +11,7 @@ Equivalent to running: import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.diffusion.data import DiffusionParallelConfig from vllm_omni.inputs.data import OmniDiffusionSamplingParams @@ -37,9 +37,9 @@ pytestmark = [ @hardware_test(res={"cuda": "L4"}, num_cards=2) -def test_ernie_image_text_to_image(omni_runner_handler: OmniRunnerHandler) -> None: +def test_ernie_image_text_to_image(offline_client: OfflineOmniClient) -> None: request_config = { - "model": omni_runner_handler.runner.model_name, + "model": offline_client.runner.model_name, "prompt": "A photo of a cat sitting on a laptop", "sampling_params": OmniDiffusionSamplingParams( height=512, @@ -49,4 +49,4 @@ def test_ernie_image_text_to_image(omni_runner_handler: OmniRunnerHandler) -> No seed=42, ), } - omni_runner_handler.send_diffusion_request(request_config) + offline_client.send_diffusion_request(request_config) diff --git a/tests/e2e/offline_inference/test_flux1_dev_autoround_w4a16_expansion.py b/tests/e2e/offline_inference/test_flux1_dev_autoround_w4a16_expansion.py index ec48a49f7..92a710f6e 100644 --- a/tests/e2e/offline_inference/test_flux1_dev_autoround_w4a16_expansion.py +++ b/tests/e2e/offline_inference/test_flux1_dev_autoround_w4a16_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for FLUX AutoRound W4A16 quantized inference. These tests require: @@ -19,8 +19,8 @@ import pytest import torch from vllm.distributed.parallel_state import cleanup_dist_env_and_memory -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from tests.helpers.runtime import OmniRunner from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.outputs import OmniRequestOutput diff --git a/tests/e2e/offline_inference/test_flux1_kontext_expansion.py b/tests/e2e/offline_inference/test_flux1_kontext_expansion.py index abf7bbc2c..61a47a3b0 100644 --- a/tests/e2e/offline_inference/test_flux1_kontext_expansion.py +++ b/tests/e2e/offline_inference/test_flux1_kontext_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Tests for FluxKontext model pipeline. @@ -15,7 +15,7 @@ import pytest from vllm.assets.image import ImageAsset from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.diffusion.data import DiffusionParallelConfig from vllm_omni.inputs.data import OmniDiffusionSamplingParams @@ -47,18 +47,18 @@ def _sampling_512() -> OmniDiffusionSamplingParams: @hardware_test(res={"cuda": "H100"}, num_cards=2) -def test_flux_kontext_text_to_image(omni_runner_handler: OmniRunnerHandler): +def test_flux_kontext_text_to_image(offline_client: OfflineOmniClient): """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) + offline_client.send_diffusion_request(request_config) @hardware_test(res={"cuda": "H100"}, num_cards=2) -def test_flux_kontext_image_edit(omni_runner_handler: OmniRunnerHandler): +def test_flux_kontext_image_edit(offline_client: OfflineOmniClient): """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 = { @@ -67,4 +67,4 @@ def test_flux_kontext_image_edit(omni_runner_handler: OmniRunnerHandler): "modalities": ["img2img"], "sampling_params": _sampling_512(), } - omni_runner_handler.send_diffusion_request(request_config) + offline_client.send_diffusion_request(request_config) diff --git a/tests/e2e/offline_inference/test_flux1_schnell_expansion.py b/tests/e2e/offline_inference/test_flux1_schnell_expansion.py index de6a758f6..9b61f876f 100644 --- a/tests/e2e/offline_inference/test_flux1_schnell_expansion.py +++ b/tests/e2e/offline_inference/test_flux1_schnell_expansion.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Tests for Flux1 Schnell.""" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.inputs.data import OmniDiffusionSamplingParams MODEL = "black-forest-labs/FLUX.1-schnell" @@ -21,7 +21,7 @@ pytestmark = [ @hardware_test(res={"cuda": "H100"}, num_cards=1) -def test_flux_schnell_text_to_image(omni_runner_handler: OmniRunnerHandler): +def test_flux_schnell_text_to_image(offline_client: OfflineOmniClient): """Test FLUX.1-schnell text-to-image generation.""" request_config = { "model": MODEL, @@ -33,4 +33,4 @@ def test_flux_schnell_text_to_image(omni_runner_handler: OmniRunnerHandler): seed=42, ), } - omni_runner_handler.send_diffusion_request(request_config) + offline_client.send_diffusion_request(request_config) diff --git a/tests/e2e/offline_inference/test_flux2_klein.py b/tests/e2e/offline_inference/test_flux2_klein.py index ea3943454..845cf2789 100644 --- a/tests/e2e/offline_inference/test_flux2_klein.py +++ b/tests/e2e/offline_inference/test_flux2_klein.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ End-to-end test for Flux2 Klein inpainting. -Inpainting uses ``omni_runner_handler.send_diffusion_request`` with +Inpainting uses ``offline_client.send_diffusion_request`` with ``multi_modal_data`` containing ``image`` and ``mask_image``; see -:meth:`OmniRunnerHandler.send_diffusion_request` in ``tests.helpers.runtime``. +:meth:`OfflineOmniClient.send_diffusion_request` in ``tests.helpers.runtime``. """ import pytest @@ -14,7 +14,7 @@ import torch from PIL import Image, ImageDraw from tests.helpers.mark import hardware_test -from tests.helpers.runtime import DiffusionResponse, OmniRunnerHandler +from tests.helpers.runtime import DiffusionResponse, OfflineOmniClient from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.platforms import current_omni_platform @@ -47,15 +47,17 @@ def _create_test_inputs(color: tuple = (100, 150, 200)): 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) + images = response.images + assert images is not None + if images and isinstance(images[0], list): + return [frame for frames in images for frame in frames] + return list(images) def _send_inpaint_with_generator( - omni_runner_handler: OmniRunnerHandler, prompt: str, input_image, mask_image, generator: torch.Generator + offline_client: OfflineOmniClient, prompt: str, input_image, mask_image, generator: torch.Generator ) -> DiffusionResponse: - return omni_runner_handler.send_diffusion_request( + return offline_client.send_diffusion_request( { "model": MODEL, "prompt": prompt, @@ -75,15 +77,15 @@ def _send_inpaint_with_generator( @pytest.mark.slow @pytest.mark.diffusion @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_flux2_klein_inpaint_deterministic(omni_runner_handler: OmniRunnerHandler): +def test_flux2_klein_inpaint_deterministic(offline_client: OfflineOmniClient): 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) - 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) + r1 = _send_inpaint_with_generator(offline_client, "A red flower in a field", input_image, mask_image, gen1) + r2 = _send_inpaint_with_generator(offline_client, "A red flower in a field", input_image, mask_image, gen2) images1 = _images_from_response(r1) images2 = _images_from_response(r2) @@ -96,14 +98,14 @@ def test_flux2_klein_inpaint_deterministic(omni_runner_handler: OmniRunnerHandle @pytest.mark.slow @pytest.mark.diffusion @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_flux2_klein_inpaint_different_seeds_different_output(omni_runner_handler: OmniRunnerHandler): +def test_flux2_klein_inpaint_different_seeds_different_output(offline_client: OfflineOmniClient): 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) - 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) + r1 = _send_inpaint_with_generator(offline_client, "A beautiful landscape", input_image, mask_image, gen1) + r2 = _send_inpaint_with_generator(offline_client, "A beautiful landscape", input_image, mask_image, gen2) images1 = _images_from_response(r1) images2 = _images_from_response(r2) diff --git a/tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py b/tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py index 86b8cee8a..b38e26993 100644 --- a/tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py +++ b/tests/e2e/offline_inference/test_glm_image_autoround_w4a16_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for GLM-Image AutoRound W4A16 quantized inference. These tests cover text-to-image and image-to-image generation with @@ -19,10 +19,10 @@ import pytest from PIL import Image from vllm import SamplingParams -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test from tests.helpers.media import generate_synthetic_image -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.monitor import DeviceMemoryMonitor +from tests.helpers.runtime import OfflineOmniClient from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.outputs import OmniRequestOutput @@ -121,7 +121,7 @@ pytestmark = [ @pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True) @hardware_test(res={"cuda": "H100"}, num_cards=2) -def test_glm_image_autoround_w4a16_generates_image(omni_runner_handler: OmniRunnerHandler): +def test_glm_image_autoround_w4a16_generates_image(offline_client: OfflineOmniClient): """Load the W4A16 quantized GLM-Image model and verify it produces a valid image.""" gc.collect() current_omni_platform.empty_cache() @@ -152,7 +152,7 @@ def test_glm_image_autoround_w4a16_generates_image(omni_runner_handler: OmniRunn seed=42, ) - outputs = omni_runner_handler.runner.generate( + outputs = offline_client.runner.generate( [prompt_dict], [ar_params, diffusion_params], ) @@ -186,7 +186,7 @@ def test_glm_image_autoround_w4a16_generates_image(omni_runner_handler: OmniRunn @pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True) @hardware_test(res={"cuda": "H100"}, num_cards=2) -def test_glm_image_autoround_w4a16_image_to_image(omni_runner_handler: OmniRunnerHandler): +def test_glm_image_autoround_w4a16_image_to_image(offline_client: OfflineOmniClient): """Load the W4A16 quantized GLM-Image and verify image-to-image generation works.""" ref_image_arr = generate_synthetic_image(WIDTH, HEIGHT)["np_array"] @@ -217,7 +217,7 @@ def test_glm_image_autoround_w4a16_image_to_image(omni_runner_handler: OmniRunne seed=42, ) - outputs = omni_runner_handler.runner.generate( + outputs = offline_client.runner.generate( [prompt_dict], [ar_params, diffusion_params], ) diff --git a/tests/e2e/offline_inference/test_higgs_audio_v2_expansion.py b/tests/e2e/offline_inference/test_higgs_audio_v2_expansion.py index 00482a132..53134446d 100644 --- a/tests/e2e/offline_inference/test_higgs_audio_v2_expansion.py +++ b/tests/e2e/offline_inference/test_higgs_audio_v2_expansion.py @@ -1,12 +1,12 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Offline expansion tests for higgs-audio v2. Mirrors examples/offline_inference/text_to_speech/higgs_audio_v2/end2end.py: build Stage-0 prompt_token_ids with the model's own processor + plain-text chat template, drive the engine via ``Omni.generate``, and inspect the -Stage-1 PCM payload directly. The OmniRunnerHandler.send_audio_speech_request +Stage-1 PCM payload directly. The OfflineOmniClient.send_audio_speech_request helper applies a Qwen-style ChatML wrap that is not valid for higgs, so this test bypasses it (same approach as test_voxtral_tts.py). diff --git a/tests/e2e/offline_inference/test_indextts2_expansion.py b/tests/e2e/offline_inference/test_indextts2_expansion.py index ba5b984f9..c95de98ea 100644 --- a/tests/e2e/offline_inference/test_indextts2_expansion.py +++ b/tests/e2e/offline_inference/test_indextts2_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E offline inference tests for IndexTTS2 two-stage pipeline. Stage 0 (GPT AR) → Stage 1 (S2Mel + BigVGAN). Output is 22050 Hz mono WAV. @@ -8,12 +8,11 @@ Covers: basic TTS. from __future__ import annotations -from pathlib import Path - import pytest import torch from tests.helpers.mark import hardware_test +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 import Omni @@ -29,8 +28,7 @@ _OMNI_RUNNER_PARAM = ( STAGE_CONFIG, ) -ASSETS_DIR = Path(__file__).resolve().parents[2] / "assets" / "indextts2" -REF_AUDIO = str(ASSETS_DIR / "ref_audio.wav") +REF_AUDIO = str(get_asset_path("indextts2/ref_audio.wav")) pytestmark = [ pytest.mark.slow, @@ -55,9 +53,12 @@ def _sample_rate_from_mm(mm: dict) -> int: sr = mm.get("sr") if isinstance(sr, list): sr = sr[-1] if sr else None - if hasattr(sr, "item"): - return int(sr.item()) - return int(sr) if sr is not None else SAMPLE_RATE + if sr is None: + return SAMPLE_RATE + item = getattr(sr, "item", None) + if callable(item): + return int(item()) + return int(sr) def _build_request(text: str, ref_audio: str, **extra) -> dict: diff --git a/tests/e2e/offline_inference/test_krea2_expansion.py b/tests/e2e/offline_inference/test_krea2_expansion.py index a8f63032b..f26f21276 100644 --- a/tests/e2e/offline_inference/test_krea2_expansion.py +++ b/tests/e2e/offline_inference/test_krea2_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """L4 functionality tests for the Krea 2 text-to-image diffusion pipeline (``Krea2Pipeline``). @@ -15,7 +15,7 @@ import numpy as np import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.lora.request import LoRARequest from vllm_omni.lora.utils import stable_lora_int_id @@ -49,9 +49,9 @@ def _sampling() -> OmniDiffusionSamplingParams: @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True) -def test_krea2_text_to_image_001(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_text_to_image_001(offline_client: OfflineOmniClient) -> None: """Basic functional smoke: a single prompt produces a decoded image.""" - omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) + offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) @hardware_test(res={"cuda": "H100"}) @@ -60,22 +60,22 @@ def test_krea2_text_to_image_001(omni_runner_handler: OmniRunnerHandler) -> None [(MODEL, None, {"enable_layerwise_offload": True})], indirect=True, ) -def test_krea2_layerwise_offload(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_layerwise_offload(offline_client: OfflineOmniClient) -> None: """Exercise layerwise CPU offload on the DiT (SupportsComponentDiscovery + _layerwise_offload_blocks_attrs).""" - omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) + offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) @hardware_test(res={"cuda": "H100"}) @pytest.mark.cache @pytest.mark.parametrize("omni_runner", [(MODEL, None, {"cache_backend": "cache_dit"})], indirect=True) -def test_krea2_cache_dit(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_cache_dit(offline_client: OfflineOmniClient) -> None: """Exercise Cache-DiT on Krea 2 via the custom Krea2Pipeline enabler (``enable_cache_for_krea2``). Validates the docs' Cache-DiT support claim for Krea 2. ``has_separate_cfg`` is checkpoint-aware (False for the distilled Turbo no-CFG path, True for the Raw CFG path); the default Turbo checkpoint exercises the no-CFG branch. """ - omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) + offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) @hardware_test(res={"cuda": "H100"}, num_cards=2) @@ -88,13 +88,13 @@ def test_krea2_cache_dit(omni_runner_handler: OmniRunnerHandler) -> None: [(MODEL, None, {"use_hsdp": True, "hsdp_shard_size": 2})], indirect=True, ) -def test_krea2_hsdp(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_hsdp(offline_client: OfflineOmniClient) -> None: """Exercise Hybrid Sharded Data Parallel (HSDP) weight sharding on the DiT across 2 GPUs. Validates the docs' HSDP-support claim for Krea 2. Scheduled on the multi-GPU (distributed_cuda) shard by ``num_cards=2``; skipped automatically on boxes with fewer than 2 CUDA devices. """ - omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) + offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) @hardware_test(res={"cuda": "H100"}, num_cards=2) @@ -107,27 +107,29 @@ def test_krea2_hsdp(omni_runner_handler: OmniRunnerHandler) -> None: [(MODEL, None, {"tensor_parallel_size": 2, "vae_patch_parallel_size": 2})], indirect=True, ) -def test_krea2_vae_patch_parallel(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_vae_patch_parallel(offline_client: OfflineOmniClient) -> None: """Exercise VAE patch-parallel (decode) across 2 GPUs. Validates the docs' VAE-patch-parallel (decode) support claim for Krea 2. Scheduled on the multi-GPU (distributed_cuda) shard by ``num_cards=2``; skipped automatically on boxes with fewer than 2 CUDA devices. """ - omni_runner_handler.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) + offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()}) -def _generate(handler: OmniRunnerHandler, lora_request: LoRARequest | None, lora_scale: float = 1.0) -> np.ndarray: +def _generate(handler: OfflineOmniClient, lora_request: LoRARequest | None, lora_scale: float = 1.0) -> np.ndarray: sp = _sampling() sp.lora_request = lora_request sp.lora_scale = lora_scale resp = handler.send_diffusion_request({"model": MODEL, "prompt": LORA_PROMPT, "sampling_params": sp}) - return np.asarray(resp.images[0].convert("RGB"), dtype=np.int16) + images = resp.images + assert images is not None + return np.asarray(images[0].convert("RGB"), dtype=np.int16) @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", [(MODEL, None)], indirect=True) -def test_krea2_lora(omni_runner_handler: OmniRunnerHandler) -> None: +def test_krea2_lora(offline_client: OfflineOmniClient) -> None: """Validate diffusion LoRA on Krea 2: visible effect, scale sensitivity, clean deactivation. Uses ``LORA`` (a vLLM-Omni-compatible PEFT repackaging of ``krea/Krea-2-LoRA-darkbrush``, @@ -137,10 +139,10 @@ def test_krea2_lora(omni_runner_handler: OmniRunnerHandler) -> None: """ lora_request = LoRARequest(lora_name="darkbrush", lora_int_id=stable_lora_int_id(LORA), lora_path=LORA) - baseline = _generate(omni_runner_handler, None) - img_1x = _generate(omni_runner_handler, lora_request, lora_scale=1.0) - img_2x = _generate(omni_runner_handler, lora_request, lora_scale=2.0) - restored = _generate(omni_runner_handler, None) + baseline = _generate(offline_client, None) + img_1x = _generate(offline_client, lora_request, lora_scale=1.0) + img_2x = _generate(offline_client, lora_request, lora_scale=2.0) + restored = _generate(offline_client, None) diff_1x = np.abs(baseline - img_1x).mean() diff_2x = np.abs(baseline - img_2x).mean() diff --git a/tests/e2e/offline_inference/test_ming_flash_omni_expansion.py b/tests/e2e/offline_inference/test_ming_flash_omni_expansion.py index 6d4bd197a..f8d43eaba 100644 --- a/tests/e2e/offline_inference/test_ming_flash_omni_expansion.py +++ b/tests/e2e/offline_inference/test_ming_flash_omni_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project import os @@ -53,7 +53,7 @@ test_params = [(m, c) for m in models for c in [get_eager_config()]] @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True) -def test_text_to_text(omni_runner, omni_runner_handler) -> None: +def test_text_to_text(omni_runner, offline_client) -> None: """ Test text-only input processing and text output generation. Input Modal: text @@ -62,14 +62,14 @@ def test_text_to_text(omni_runner, omni_runner_handler) -> None: prompt = build_prompt("请详细介绍鹦鹉的生活习性。") request_config = {"prompts": prompt, "modalities": ["text"]} - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True) -def test_image_to_text(omni_runner, omni_runner_handler) -> None: +def test_image_to_text(omni_runner, offline_client) -> None: """ Test image understanding with text output. Input Modal: image + text @@ -79,14 +79,14 @@ 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_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True) -def test_audio_to_text(omni_runner, omni_runner_handler) -> None: +def test_audio_to_text(omni_runner, offline_client) -> None: """ Test audio understanding with text output. Input Modal: audio + text @@ -98,14 +98,14 @@ 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_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True) -def test_video_to_text(omni_runner, omni_runner_handler) -> None: +def test_video_to_text(omni_runner, offline_client) -> None: """ Test video understanding with text output. Input Modal: video + text @@ -115,14 +115,14 @@ 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_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params_thinker, indirect=True) -def test_mixed_to_text(omni_runner, omni_runner_handler) -> None: +def test_mixed_to_text(omni_runner, offline_client) -> None: """ Test mixed modality input (image + audio) with text output. Input Modal: image + audio + text @@ -135,14 +135,14 @@ 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_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_text_to_audio(omni_runner, omni_runner_handler) -> None: +def test_text_to_audio(omni_runner, offline_client) -> None: """ Test text input with audio output via the thinker+talker pipeline. Input Modal: text @@ -151,14 +151,14 @@ def test_text_to_audio(omni_runner, omni_runner_handler) -> None: prompt = build_prompt("请简单介绍一下北京。") request_config = {"prompts": prompt, "modalities": ["audio"]} - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=4) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_image_to_audio(omni_runner, omni_runner_handler) -> None: +def test_image_to_audio(omni_runner, offline_client) -> None: """ Test image + text input with audio output via the thinker+talker pipeline. Input Modal: image + text @@ -168,4 +168,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_omni_request(request_config) + offline_client.send_omni_request(request_config) diff --git a/tests/e2e/offline_inference/test_qwen2_5_omni_autoround_w4a16_expansion.py b/tests/e2e/offline_inference/test_qwen2_5_omni_autoround_w4a16_expansion.py index 770dc8682..41a84d677 100644 --- a/tests/e2e/offline_inference/test_qwen2_5_omni_autoround_w4a16_expansion.py +++ b/tests/e2e/offline_inference/test_qwen2_5_omni_autoround_w4a16_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for Qwen2.5-Omni AutoRound W4A16 quantized inference. These tests cover text, audio, image, video, and mixed-modality inputs @@ -68,13 +68,13 @@ quant_params = [(QUANTIZED_MODEL, stage_config)] @_SKIP_ISSUE_5652 @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): +def test_text_to_text(omni_runner, offline_client): """Text input → text output with W4A16 quantized Qwen2.5-Omni.""" request_config = { "prompts": "What is the capital of China?", "modalities": ["text"], } - response = omni_runner_handler.send_omni_request(request_config) + response = offline_client.send_omni_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -87,7 +87,7 @@ def test_text_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_audio_to_text(omni_runner, offline_client): """Audio input → text output with W4A16 quantized Qwen2.5-Omni.""" audio = generate_synthetic_audio(1, 1, 16000)["np_array"] if len(audio.shape) == 2: @@ -98,7 +98,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler): "audios": (audio, 16000), "modalities": ["text"], } - response = omni_runner_handler.send_omni_request(request_config) + response = offline_client.send_omni_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -111,7 +111,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_image_to_text(omni_runner, offline_client): """Image input → text output with W4A16 quantized Qwen2.5-Omni.""" image = generate_synthetic_image(16, 16)["np_array"] @@ -120,7 +120,7 @@ def test_image_to_text(omni_runner, omni_runner_handler): "images": image, "modalities": ["text"], } - response = omni_runner_handler.send_omni_request(request_config) + response = offline_client.send_omni_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -133,7 +133,7 @@ def test_image_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_video_to_text(omni_runner, offline_client): """Video input → text output with W4A16 quantized Qwen2.5-Omni.""" video = generate_synthetic_video(16, 16, 30)["np_array"] @@ -142,7 +142,7 @@ def test_video_to_text(omni_runner, omni_runner_handler): "videos": video, "modalities": ["text"], } - response = omni_runner_handler.send_omni_request(request_config) + response = offline_client.send_omni_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -155,7 +155,7 @@ def test_video_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_mix_to_audio(omni_runner, offline_client): """Mixed-modality input → audio output with W4A16 quantized Qwen2.5-Omni.""" video = generate_synthetic_video(16, 16, 30)["np_array"] image = generate_synthetic_image(16, 16)["np_array"] @@ -170,5 +170,5 @@ def test_mix_to_audio(omni_runner, omni_runner_handler): "audios": (audio, 16000), "modalities": ["audio"], } - response = omni_runner_handler.send_omni_request(request_config) + response = offline_client.send_omni_request(request_config) assert response.success, "Request failed" diff --git a/tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py b/tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py index 9d368a688..e2465234a 100644 --- a/tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py +++ b/tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ E2E tests for Qwen2.5-Omni model with mixed modality inputs, audio and text output. """ @@ -55,7 +58,7 @@ def get_question(prompt_type="mix"): @pytest.mark.omni @hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 4, "rocm": 2, "xpu": 3}) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_mix_to_audio(omni_runner, omni_runner_handler) -> None: +def test_mix_to_audio(omni_runner, offline_client) -> None: """ Test multi-modal input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -79,14 +82,14 @@ def test_mix_to_audio(omni_runner, omni_runner_handler) -> None: } # Test single completion - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @hardware_test(res={"cuda": "L4", "rocm": "MI325", "xpu": "B60"}, num_cards={"cuda": 4, "rocm": 2, "xpu": 3}) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_text_to_text(omni_runner, omni_runner_handler) -> None: +def test_text_to_text(omni_runner, offline_client) -> None: """ Test text input processing and text output generation via OpenAI API. Deploy Setting: default yaml @@ -99,4 +102,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_omni_request(request_config) + offline_client.send_omni_request(request_config) diff --git a/tests/e2e/offline_inference/test_qwen3_omni.py b/tests/e2e/offline_inference/test_qwen3_omni.py index fd3a27d2e..52a0b3556 100644 --- a/tests/e2e/offline_inference/test_qwen3_omni.py +++ b/tests/e2e/offline_inference/test_qwen3_omni.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ E2E offline tests for Omni model with video input and audio output. """ @@ -57,23 +60,23 @@ def get_question(prompt_type="video"): @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_runner", test_params, indirect=True) -def test_video_to_audio(omni_runner, omni_runner_handler) -> None: +def test_video_to_audio(omni_runner, offline_client) -> None: """Test processing video, generating audio output.""" video = generate_synthetic_video(224, 224, 300)["np_array"] request_config = {"prompts": get_question(), "videos": video, "modalities": ["audio"]} # Test single completion - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) @pytest.mark.advanced_model @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1) @pytest.mark.parametrize("omni_runner", thinker_test_params, indirect=True) -def test_thinker_only_model_request(omni_runner, omni_runner_handler) -> None: +def test_thinker_only_model_request(omni_runner, offline_client) -> None: """Test that we can load and run a request through a model that only has the thinker stage.""" request_config = {"prompts": "what color is the sky?", "modalities": ["text"]} # Test single completion - omni_runner_handler.send_omni_request(request_config) + offline_client.send_omni_request(request_config) diff --git a/tests/e2e/offline_inference/test_qwen3_omni_autoround_w4a16_expansion.py b/tests/e2e/offline_inference/test_qwen3_omni_autoround_w4a16_expansion.py index f9b9b8352..b1f17356b 100644 --- a/tests/e2e/offline_inference/test_qwen3_omni_autoround_w4a16_expansion.py +++ b/tests/e2e/offline_inference/test_qwen3_omni_autoround_w4a16_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for Qwen3-Omni AutoRound W4A16 quantized inference. These tests cover text, audio, image, video, and mixed-modality inputs @@ -80,13 +80,13 @@ quant_params = [(QUANTIZED_MODEL, stage_config)] @_SKIP_ISSUE_5652 @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): +def test_text_to_text(omni_runner, offline_client): """Text input → text output with W4A16 quantized Qwen3-Omni.""" request_config = { "prompts": "What is the capital of France?", "modalities": ["text"], } - response = omni_runner_handler.send_request(request_config) + response = offline_client.send_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -99,7 +99,7 @@ def test_text_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_audio_to_text(omni_runner, offline_client): """Audio input → text output with W4A16 quantized Qwen3-Omni.""" audio = generate_synthetic_audio(1, 1, 16000)["np_array"] if len(audio.shape) == 2: @@ -110,7 +110,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 = offline_client.send_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -123,7 +123,7 @@ def test_audio_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_image_to_text(omni_runner, offline_client): """Image input → text output with W4A16 quantized Qwen3-Omni.""" image = generate_synthetic_image(16, 16)["np_array"] @@ -132,7 +132,7 @@ def test_image_to_text(omni_runner, omni_runner_handler): "images": image, "modalities": ["text"], } - response = omni_runner_handler.send_request(request_config) + response = offline_client.send_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -145,7 +145,7 @@ def test_image_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_video_to_text(omni_runner, offline_client): """Video input → text output with W4A16 quantized Qwen3-Omni.""" video = generate_synthetic_video(224, 224, 300)["np_array"] @@ -154,7 +154,7 @@ def test_video_to_text(omni_runner, omni_runner_handler): "videos": video, "modalities": ["text"], } - response = omni_runner_handler.send_request(request_config) + response = offline_client.send_request(request_config) assert response.success, "Request failed" assert response.text_content and len(response.text_content.strip()) > 0 @@ -167,7 +167,7 @@ def test_video_to_text(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_video_to_audio(omni_runner, offline_client): """Video input → audio output with W4A16 quantized Qwen3-Omni.""" video = generate_synthetic_video(224, 224, 300)["np_array"] @@ -176,7 +176,7 @@ def test_video_to_audio(omni_runner, omni_runner_handler): "videos": video, "modalities": ["audio"], } - response = omni_runner_handler.send_request(request_config) + response = offline_client.send_request(request_config) assert response.success, "Request failed" @@ -188,7 +188,7 @@ def test_video_to_audio(omni_runner, omni_runner_handler): @_SKIP_ISSUE_5652 @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): +def test_mix_to_audio(omni_runner, offline_client): """Mixed-modality input → audio output with W4A16 quantized Qwen3-Omni.""" video = generate_synthetic_video(224, 224, 300)["np_array"] image = generate_synthetic_image(16, 16)["np_array"] @@ -203,5 +203,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 = offline_client.send_request(request_config) assert response.success, "Request failed" diff --git a/tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py b/tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py index 5d1b3dda7..ad6935e71 100644 --- a/tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py +++ b/tests/e2e/offline_inference/test_qwen3_omni_modelopt_nvfp4_w4a4.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for Qwen3-Omni ModelOpt NVFP4 W4A4 — Marlin fallback path. Loads the published full-thinker NVFP4 W4A4 checkpoint on H100 (sm_90), @@ -74,7 +74,7 @@ quant_params = [(QUANTIZED_MODEL, _stage_config())] @hardware_test(res={"cuda": "H100"}, num_cards=2) @pytest.mark.parametrize("omni_runner", quant_params, indirect=True) -def test_marlin_fallback_no_nan_collapse(omni_runner, omni_runner_handler): +def test_marlin_fallback_no_nan_collapse(omni_runner, offline_client): """H100 Marlin FP4 fallback: text in -> coherent text out. On H100 the upstream NVFP4 PWAL routes through @@ -89,7 +89,7 @@ def test_marlin_fallback_no_nan_collapse(omni_runner, omni_runner_handler): degenerate-output signature that indicates NaN propagation through the FP4 GEMM. """ - response = omni_runner_handler.send_omni_request({"prompts": "Why is the sky blue?", "modalities": ["text"]}) + response = offline_client.send_omni_request({"prompts": "Why is the sky blue?", "modalities": ["text"]}) assert response.success, "request failed" text = (response.text_content or "").strip() assert text, "empty response — likely server returned only EOS" diff --git a/tests/e2e/offline_inference/test_qwen3_tts_base.py b/tests/e2e/offline_inference/test_qwen3_tts_base.py index a076526a4..f800a1163 100644 --- a/tests/e2e/offline_inference/test_qwen3_tts_base.py +++ b/tests/e2e/offline_inference/test_qwen3_tts_base.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E offline tests for Qwen3-TTS Base model with text input and audio output. @@ -15,12 +15,12 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" # See tests/e2e/online_serving/test_qwen3_tts_base.py for the vendored-asset rationale. -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -70,7 +70,7 @@ def get_prompt(): @pytest.mark.tts @hardware_test(res={"cuda": "L4", "xpu": "B60"}, 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: +def test_text_to_audio_001(omni_runner, offline_client) -> None: """ Test text input processing and audio output via offline Omni runner. Deploy Setting: qwen3_tts_no_async_chunk.yaml + enforce_eager=true @@ -87,4 +87,4 @@ def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - omni_runner_handler.send_audio_speech_request(request_config) + offline_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/offline_inference/test_qwen3_tts_customvoice.py b/tests/e2e/offline_inference/test_qwen3_tts_customvoice.py index 4fa2d4b6f..b303b1cda 100644 --- a/tests/e2e/offline_inference/test_qwen3_tts_customvoice.py +++ b/tests/e2e/offline_inference/test_qwen3_tts_customvoice.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E offline tests for Qwen3-TTS CustomVoice model with text input and audio output. @@ -64,7 +64,7 @@ def get_prompt(): @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: +def test_text_to_audio_001(omni_runner, offline_client) -> None: """ Test text input processing and audio output via offline Omni runner. Deploy Setting: qwen3_tts_no_async_chunk.yaml + enforce_eager=true @@ -74,4 +74,4 @@ def test_text_to_audio_001(omni_runner, omni_runner_handler) -> None: Datasets: few requests """ request_config = {"input": get_prompt(), "voice": "vivian"} - omni_runner_handler.send_audio_speech_request(request_config) + offline_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/offline_inference/test_stable_audio_expansion.py b/tests/e2e/offline_inference/test_stable_audio_expansion.py index 92f5bbfb3..759c6c500 100644 --- a/tests/e2e/offline_inference/test_stable_audio_expansion.py +++ b/tests/e2e/offline_inference/test_stable_audio_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Stable Audio offline e2e: real weights, FP8 + TeaCache (single job to save GPU). @@ -15,7 +15,6 @@ import numpy as np import pytest import torch -from tests.helpers import skip_if_gated_repo_inaccessible from tests.helpers.assertions import assert_audio_valid from tests.helpers.mark import hardware_test from vllm_omni import Omni @@ -25,8 +24,6 @@ from vllm_omni.platforms import current_omni_platform pytestmark = [pytest.mark.slow, pytest.mark.diffusion] -_MODEL_REPO = "stabilityai/stable-audio-open-1.0" - _SAMPLE_RATE = 44100 _CLIP_DURATION_S = 2.0 @@ -80,7 +77,6 @@ def test_stable_audio_quantization_and_teacache() -> None: CI should provide ``HF_TOKEN`` if the checkpoint is gated. """ - skip_if_gated_repo_inaccessible(_MODEL_REPO) # ``model_class_name`` must be passed explicitly: the default-stage-cfg # factory in ``async_omni_engine.py`` reads it out of ``kwargs`` when # deciding ``final_output_type`` (#2077), and at construction time the diff --git a/tests/e2e/offline_inference/test_wan22_autoround_w4a16_expansion.py b/tests/e2e/offline_inference/test_wan22_autoround_w4a16_expansion.py index 044435cb5..4ac51a344 100644 --- a/tests/e2e/offline_inference/test_wan22_autoround_w4a16_expansion.py +++ b/tests/e2e/offline_inference/test_wan22_autoround_w4a16_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E tests for Wan2.2 AutoRound W4A16 quantized inference. These tests cover I2V (image-to-video) and T2V (text-to-video) generation @@ -19,8 +19,8 @@ import pytest import torch from PIL import Image -from tests.helpers.env import DeviceMemoryMonitor from tests.helpers.mark import hardware_test +from tests.helpers.monitor import DeviceMemoryMonitor from vllm_omni.inputs.data import OmniDiffusionSamplingParams from vllm_omni.platforms import current_omni_platform @@ -91,7 +91,7 @@ def _create_test_image(width: int = WIDTH, height: int = HEIGHT) -> Image.Image: return Image.fromarray(arr) -def _generate_i2v_video(omni_runner_handler, prompt: str = "A cat sitting on a table, smooth motion") -> tuple: +def _generate_i2v_video(offline_client, prompt: str = "A cat sitting on a table, smooth motion") -> tuple: """Generate one I2V video, return (frames, peak_memory_mb).""" gc.collect() current_omni_platform.empty_cache() @@ -101,7 +101,7 @@ def _generate_i2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t monitor.start() image = _create_test_image() - response = omni_runner_handler.send_diffusion_request( + response = offline_client.send_diffusion_request( { "prompt": prompt, "images": image, @@ -112,7 +112,7 @@ def _generate_i2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t peak = monitor.peak_used_mb monitor.stop() - assert response.success, f"Request failed: {response.error_message}" + assert response.success, "Request failed" assert response.images is not None and len(response.images) > 0, "Expected image output" frames = response.images[0] @@ -122,7 +122,7 @@ def _generate_i2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t return frames, peak -def _generate_t2v_video(omni_runner_handler, prompt: str = "A cat sitting on a table") -> tuple: +def _generate_t2v_video(offline_client, prompt: str = "A cat sitting on a table") -> tuple: """Generate one T2V video, return (frames, peak_memory_mb).""" gc.collect() current_omni_platform.empty_cache() @@ -131,7 +131,7 @@ def _generate_t2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t monitor = DeviceMemoryMonitor(device_index=device_index, interval=0.02) monitor.start() - response = omni_runner_handler.send_diffusion_request( + response = offline_client.send_diffusion_request( { "prompt": prompt, "sampling_params": _sampling_params_t2v(), @@ -141,7 +141,7 @@ def _generate_t2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t peak = monitor.peak_used_mb monitor.stop() - assert response.success, f"Request failed: {response.error_message}" + assert response.success, "Request failed" assert response.images is not None and len(response.images) > 0, "Expected image output" frames = response.images[0] @@ -158,9 +158,9 @@ def _generate_t2v_video(omni_runner_handler, prompt: str = "A cat sitting on a t @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", quant_i2v_params, indirect=True) -def test_wan22_i2v_autoround_w4a16_generates_video(omni_runner, omni_runner_handler): +def test_wan22_i2v_autoround_w4a16_generates_video(omni_runner, offline_client): """Load the W4A16 quantized Wan2.2 I2V model and verify it produces a valid video.""" - frames, _ = _generate_i2v_video(omni_runner_handler) + frames, _ = _generate_i2v_video(offline_client) assert frames is not None, "Expected video frames output" assert hasattr(frames, "shape"), "Expected frames to have a shape attribute" @@ -182,9 +182,9 @@ def test_wan22_i2v_autoround_w4a16_generates_video(omni_runner, omni_runner_hand @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", quant_t2v_params, indirect=True) -def test_wan22_t2v_autoround_w4a16_generates_video(omni_runner, omni_runner_handler): +def test_wan22_t2v_autoround_w4a16_generates_video(omni_runner, offline_client): """Load the W4A16 quantized Wan2.2 T2V model and verify it produces a valid video.""" - frames, _ = _generate_t2v_video(omni_runner_handler) + frames, _ = _generate_t2v_video(offline_client) assert frames is not None, "Expected video frames output" assert hasattr(frames, "shape"), "Expected frames to have a shape attribute" @@ -204,9 +204,9 @@ def test_wan22_t2v_autoround_w4a16_generates_video(omni_runner, omni_runner_hand @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", quant_i2v_params, indirect=True) -def test_wan22_i2v_autoround_w4a16_quant_peak(omni_runner, omni_runner_handler): +def test_wan22_i2v_autoround_w4a16_quant_peak(omni_runner, offline_client): """Measure peak GPU memory of W4A16 quantized I2V model.""" - frames, peak = _generate_i2v_video(omni_runner_handler) + frames, peak = _generate_i2v_video(offline_client) assert frames is not None, "Expected video frames output" _memory_results["quant_i2v"] = peak @@ -220,9 +220,9 @@ def test_wan22_i2v_autoround_w4a16_quant_peak(omni_runner, omni_runner_handler): @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", baseline_i2v_params, indirect=True) -def test_wan22_i2v_autoround_w4a16_baseline_peak(omni_runner, omni_runner_handler): +def test_wan22_i2v_autoround_w4a16_baseline_peak(omni_runner, offline_client): """Measure peak GPU memory of BF16 baseline I2V model.""" - frames, peak = _generate_i2v_video(omni_runner_handler) + frames, peak = _generate_i2v_video(offline_client) assert frames is not None, "Expected video frames output" _memory_results["baseline_i2v"] = peak @@ -261,9 +261,9 @@ def test_wan22_i2v_autoround_w4a16_memory_savings(): @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", quant_t2v_params, indirect=True) -def test_wan22_t2v_autoround_w4a16_quant_peak(omni_runner, omni_runner_handler): +def test_wan22_t2v_autoround_w4a16_quant_peak(omni_runner, offline_client): """Measure peak GPU memory of W4A16 quantized T2V model.""" - frames, peak = _generate_t2v_video(omni_runner_handler) + frames, peak = _generate_t2v_video(offline_client) assert frames is not None, "Expected video frames output" _memory_results["quant_t2v"] = peak @@ -277,9 +277,9 @@ def test_wan22_t2v_autoround_w4a16_quant_peak(omni_runner, omni_runner_handler): @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_runner", baseline_t2v_params, indirect=True) -def test_wan22_t2v_autoround_w4a16_baseline_peak(omni_runner, omni_runner_handler): +def test_wan22_t2v_autoround_w4a16_baseline_peak(omni_runner, offline_client): """Measure peak GPU memory of BF16 baseline T2V model.""" - frames, peak = _generate_t2v_video(omni_runner_handler) + frames, peak = _generate_t2v_video(offline_client) assert frames is not None, "Expected video frames output" _memory_results["baseline_t2v"] = peak diff --git a/tests/e2e/offline_inference/test_wan22_t2v.py b/tests/e2e/offline_inference/test_wan22_t2v.py index f3d03f399..805fbd1fe 100644 --- a/tests/e2e/offline_inference/test_wan22_t2v.py +++ b/tests/e2e/offline_inference/test_wan22_t2v.py @@ -1,7 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.inputs.data import OmniDiffusionSamplingParams MODEL = "Wan-AI/Wan2.2-T2V-A14B-Diffusers" @@ -13,7 +16,7 @@ NEGATIVE_PROMPT = "low quality, blurry, watermark, text" @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): +def test_text_to_video_001(offline_client: OfflineOmniClient): sampling = OmniDiffusionSamplingParams( height=512, width=512, @@ -29,4 +32,4 @@ def test_text_to_video_001(omni_runner_handler: OmniRunnerHandler): "negative_prompt": NEGATIVE_PROMPT, "sampling_params": sampling, } - omni_runner_handler.send_diffusion_request(request_config) + offline_client.send_diffusion_request(request_config) diff --git a/tests/e2e/offline_inference/test_z_image.py b/tests/e2e/offline_inference/test_z_image.py index edf158ffd..ede9ea1ba 100644 --- a/tests/e2e/offline_inference/test_z_image.py +++ b/tests/e2e/offline_inference/test_z_image.py @@ -1,10 +1,10 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project import pytest from tests.helpers.mark import hardware_test -from tests.helpers.runtime import OmniRunnerHandler +from tests.helpers.runtime import OfflineOmniClient from vllm_omni.inputs.data import OmniDiffusionSamplingParams MODEL = "Tongyi-MAI/Z-Image-Turbo" @@ -15,7 +15,7 @@ PROMPT = "A high-detail studio photo of an orange tabby cat sitting on a laptop @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): +def test_zimage(offline_client: OfflineOmniClient): # high resolution may cause OOM on L4 sampling = OmniDiffusionSamplingParams( height=256, @@ -30,4 +30,4 @@ def test_zimage(omni_runner_handler: OmniRunnerHandler): "prompt": PROMPT, "sampling_params": sampling, } - omni_runner_handler.send_diffusion_request(request_config) + offline_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py b/tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py index cb77c40e7..506b39c82 100644 --- a/tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py +++ b/tests/e2e/online_serving/helpers/minicpmo_4_5_duplex.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Shared fixtures and argument builders for MiniCPM-o 4.5 duplex E2E tests.""" from __future__ import annotations @@ -11,7 +14,7 @@ from types import SimpleNamespace import pytest from huggingface_hub import snapshot_download -from tests.helpers.runtime import OmniServerParams +from tests.helpers.runtime import OmniServerParams, get_model_prefix from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config MODEL = "openbmb/MiniCPM-o-4_5" @@ -73,10 +76,11 @@ def validated_soft_interrupt_wav() -> Path: return validated_wav(SOFT_INTERRUPT_WAV, SOFT_INTERRUPT_SHA256) -def resolve_ref_audio(model_prefix: str) -> Path: +def resolve_ref_audio() -> Path: # ``MODEL`` may be a local checkpoint directory instead of a Hub repo id # (``Path("/prefix") / "/abs/path"`` collapses to the absolute path), so only # fall back to the Hub cache when no such directory exists on disk. + model_prefix = get_model_prefix() model_root = Path(model_prefix) / MODEL if model_prefix else Path(MODEL) if not model_root.is_dir(): model_root = Path(snapshot_download(MODEL, local_files_only=True)) diff --git a/tests/e2e/online_serving/test_bagel.py b/tests/e2e/online_serving/test_bagel.py index dca846108..84c2302cc 100644 --- a/tests/e2e/online_serving/test_bagel.py +++ b/tests/e2e/online_serving/test_bagel.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ End-to-end online serving test for Bagel text2img and img2img generation. @@ -79,7 +79,7 @@ def _build_img2img_messages(prompt: str, image_b64: str) -> list[dict]: @pytest.mark.diffusion @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_bagel_text2img_online(omni_server, openai_client) -> None: +def test_bagel_text2img_online(omni_server, online_client) -> None: """Test Bagel text2img via OpenAI-compatible chat completions API.""" request_config = { "model": omni_server.model, @@ -94,14 +94,14 @@ def test_bagel_text2img_online(omni_server, openai_client) -> None: }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.slow @pytest.mark.diffusion @hardware_test(res={"cuda": "H100", "rocm": "MI325"}) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_bagel_img2img_online(omni_server, openai_client) -> None: +def test_bagel_img2img_online(omni_server, online_client) -> None: """Test Bagel img2img via OpenAI-compatible chat completions API.""" input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB") buffer = BytesIO() @@ -119,4 +119,4 @@ def test_bagel_img2img_online(omni_server, openai_client) -> None: }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_bagel_expansion.py b/tests/e2e/online_serving/test_bagel_expansion.py index 3308efdcb..e22614a6b 100644 --- a/tests/e2e/online_serving/test_bagel_expansion.py +++ b/tests/e2e/online_serving/test_bagel_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by the following models: @@ -19,7 +22,7 @@ assert_diffusion_response validates successful generation and the expected import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -183,7 +186,7 @@ def _get_diffusion_feature_cases(model: str): ) def test_bagel( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """L4 diffusion feature coverage for Bagel on H100. @@ -218,4 +221,4 @@ def test_bagel( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_bagel_multi_replicas.py b/tests/e2e/online_serving/test_bagel_multi_replicas.py index 6409ddd58..c360b63e8 100644 --- a/tests/e2e/online_serving/test_bagel_multi_replicas.py +++ b/tests/e2e/online_serving/test_bagel_multi_replicas.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Weekly e2e coverage for BAGEL diffusion multi-replica serving. This test needs 4 H100 GPUs; keep it out of generic test_bagel_* jobs. @@ -33,7 +36,7 @@ test_params = [ @hardware_test(res={"cuda": "H100"}, num_cards=4) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_multi_stage_diffusion_uses_multi_replica_dit_stage(omni_server, openai_client) -> None: +def test_multi_stage_diffusion_uses_multi_replica_dit_stage(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": dummy_messages_from_mix_data( @@ -49,7 +52,7 @@ def test_multi_stage_diffusion_uses_multi_replica_dit_stage(omni_server, openai_ }, } - responses = openai_client.send_diffusion_request(request_config, request_num=ROUTE_STRESS_REQUESTS) + responses = online_client.send_diffusion_request(request_config, request_num=ROUTE_STRESS_REQUESTS) assert len(responses) == ROUTE_STRESS_REQUESTS assert all(response.success for response in responses) diff --git a/tests/e2e/online_serving/test_cosmos3.py b/tests/e2e/online_serving/test_cosmos3.py index 09cb2365b..f28e2ae26 100644 --- a/tests/e2e/online_serving/test_cosmos3.py +++ b/tests/e2e/online_serving/test_cosmos3.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Online serving L2 smoke tests for ``nvidia/Cosmos3-Nano``. @@ -8,6 +8,7 @@ broader image/video similarity checks stay in ``tests/e2e/accuracy``. """ import os +from typing import Any import pytest @@ -16,7 +17,7 @@ from tests.helpers.media import generate_synthetic_image, generate_synthetic_vid from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, ) os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" @@ -56,9 +57,9 @@ def _get_diffusion_feature_cases(model: str): @pytest.mark.core_model @pytest.mark.diffusion @pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True) -def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: +def test_text_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: """Default Cosmos3 T2I smoke through ``/v1/images/generations``.""" - responses = openai_client.send_images_generations_http_request( + responses = online_client.send_images_generations_http_request( { "json": { "model": omni_server.model, @@ -85,12 +86,12 @@ def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientH @pytest.mark.core_model @pytest.mark.diffusion @pytest.mark.parametrize("omni_server", _get_diffusion_feature_cases(MODEL), indirect=True) -def test_video_generation_modes_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) -> None: +def test_video_generation_modes_001(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: """Default Cosmos3 T2V, I2V, and V2V smoke through the async ``/v1/videos`` endpoint.""" image_reference = f"data:image/jpeg;base64,{generate_synthetic_image(256, 256, seed=42)['base64']}" video_reference = f"data:video/mp4;base64,{generate_synthetic_video(256, 256, 5)['base64']}" - video_cases = [ + video_cases: list[dict[str, Any]] = [ { "form_data": { **VIDEO_SHAPE, @@ -118,8 +119,8 @@ def test_video_generation_modes_001(omni_server: OmniServer, openai_client: Open ] for case in video_cases: - request_config = { + request_config: dict[str, Any] = { "model": omni_server.model, **case, } - openai_client.send_video_diffusion_request(request_config) + online_client.send_video_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py b/tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py index c8a7cfe63..cae854be3 100644 --- a/tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py +++ b/tests/e2e/online_serving/test_cosyvoice3_tts_expansion.py @@ -14,7 +14,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path from vllm_omni.platforms import current_omni_platform @@ -33,7 +33,7 @@ MODEL = "FunAudioLLM/Fun-CosyVoice3-0.5B-2512" # Official CosyVoice zero-shot prompt audio and its transcript. Vendored under # tests/assets/ so the server does not depend on raw.githubusercontent.com being # reachable at request time (same rationale as issue #3263 for Qwen3-TTS). -REF_AUDIO_URL = load_test_audio_data_url("cosyvoice3/zero_shot_prompt.wav") +REF_AUDIO_URL = get_asset_path("cosyvoice3/zero_shot_prompt.wav", as_data_url=True) REF_TEXT = "希望你以后能够做的比我还好呦。" @@ -82,7 +82,7 @@ tts_async_chunk_server_params = [ @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_clone_zh_001(omni_server, openai_client) -> None: +def test_voice_clone_zh_001(omni_server, online_client) -> None: """ Test voice cloning TTS with Chinese text via OpenAI API. Deploy Setting: default yaml @@ -100,12 +100,12 @@ def test_voice_clone_zh_001(omni_server, openai_client) -> None: "ref_text": REF_TEXT, } _set_rocm_request_timeout(request_config) - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, 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: +def test_voice_clone_zh_002(omni_server, online_client) -> None: """ Test voice cloning TTS with Chinese text via async_chunk streaming. Deploy Setting: cosyvoice3.yaml with default ``async_chunk: true`` @@ -124,12 +124,12 @@ def test_voice_clone_zh_002(omni_server, openai_client) -> None: "ref_text": REF_TEXT, } _set_rocm_request_timeout(request_config) - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_clone_en_001(omni_server, openai_client) -> None: +def test_voice_clone_en_001(omni_server, online_client) -> None: """ Test voice cloning TTS with English text via OpenAI API. Deploy Setting: default yaml @@ -147,4 +147,4 @@ def test_voice_clone_en_001(omni_server, openai_client) -> None: "ref_text": REF_TEXT, } _set_rocm_request_timeout(request_config) - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_covo_audio_expansion.py b/tests/e2e/online_serving/test_covo_audio_expansion.py index 33771d647..6a387269b 100644 --- a/tests/e2e/online_serving/test_covo_audio_expansion.py +++ b/tests/e2e/online_serving/test_covo_audio_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Covo-Audio-Chat model. """ @@ -37,7 +37,7 @@ test_params = [ @pytest.mark.omni @hardware_test(res={"cuda": "H100"}, num_cards=1) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_audio_to_audio_001(omni_server, openai_client) -> None: +def test_audio_to_audio_001(omni_server, online_client) -> None: """ Input Modal: text + audio Output Modal: audio @@ -58,4 +58,4 @@ def test_audio_to_audio_001(omni_server, openai_client) -> None: "stream": False, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) diff --git a/tests/e2e/online_serving/test_dynin_omni_expansion.py b/tests/e2e/online_serving/test_dynin_omni_expansion.py index 1f16a584b..c4cd40ff8 100644 --- a/tests/e2e/online_serving/test_dynin_omni_expansion.py +++ b/tests/e2e/online_serving/test_dynin_omni_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Example online tests for Dynin-Omni model. """ @@ -64,7 +64,7 @@ def _build_i2i_messages(prompt: str) -> list[dict]: @hardware_test(res={"cuda": "H100", "rocm": "MI325"}) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_send_i2i_request_001(omni_server, openai_client) -> None: +def test_send_i2i_request_001(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _build_i2i_messages(I2I_PROMPT), @@ -73,27 +73,27 @@ def test_send_i2i_request_001(omni_server, openai_client) -> None: "sampling_params_list": [dict(_I2I_STAGE_SAMPLING) for _ in range(_STAGE_COUNT)], }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_send_t2i_request_001(omni_server, openai_client) -> None: +def test_send_t2i_request_001(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _build_t2i_messages(T2I_PROMPT), "modalities": ["image"], } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_send_t2s_request_001(omni_server, openai_client) -> None: +def test_send_t2s_request_001(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _build_t2s_messages(T2S_PROMPT), "modalities": ["audio"], "audio_ref_text": T2S_PROMPT, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) diff --git a/tests/e2e/online_serving/test_ernie_image_expansion.py b/tests/e2e/online_serving/test_ernie_image_expansion.py index 848fec4e6..b06267b39 100644 --- a/tests/e2e/online_serving/test_ernie_image_expansion.py +++ b/tests/e2e/online_serving/test_ernie_image_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ End-to-end test for ErnieImage text-to-image generation via online serving. @@ -11,7 +11,7 @@ from tests.helpers.mark import hardware_marks from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -45,7 +45,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_ernie_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_ernie_image(omni_server: OmniServer, online_client: OnlineOmniClient): """Test each diffusion feature with ERNIE-Image (text-to-image), one feature per case.""" messages = dummy_messages_from_mix_data(content_text=T2I_PROMPT) request_config = { @@ -60,4 +60,4 @@ def test_ernie_image(omni_server: OmniServer, openai_client: OpenAIClientHandler "seed": 42, }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_flux2_expansion.py b/tests/e2e/online_serving/test_flux2_expansion.py index 2659cb927..d7c20e832 100644 --- a/tests/e2e/online_serving/test_flux2_expansion.py +++ b/tests/e2e/online_serving/test_flux2_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Tests for Flux2 Klein; currently Dev is implemented separately, but ideally these models will fold together in the future. @@ -15,7 +18,7 @@ from tests.helpers.mark import hardware_marks from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -113,7 +116,7 @@ def _get_diffusion_feature_cases(model: str): ), indirect=True, ) -def test_flux2_klein(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux2_klein(omni_server: OmniServer, online_client: OnlineOmniClient): messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT) request_config = { "model": omni_server.model, @@ -128,4 +131,4 @@ def test_flux2_klein(omni_server: OmniServer, openai_client: OpenAIClientHandler }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_flux_2_dev_expansion.py b/tests/e2e/online_serving/test_flux_2_dev_expansion.py index 19a14ce39..932b3cfa5 100644 --- a/tests/e2e/online_serving/test_flux_2_dev_expansion.py +++ b/tests/e2e/online_serving/test_flux_2_dev_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ End-to-end diffusion coverage for FLUX.2-dev in online serving mode. @@ -19,7 +22,7 @@ image resolution. import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -116,7 +119,7 @@ def _get_flux_2_dev_feature_cases(model: str): ) def test_flux_2_dev( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Validate FLUX.2-dev online serving with CPU offload.""" @@ -135,7 +138,7 @@ def test_flux_2_dev( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -145,14 +148,14 @@ def test_flux_2_dev( ) def test_flux_2_dev_batched_chat_completions( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Validate batched chat completions for diffusion models.""" messages = [ [{"role": "user", "content": PROMPT}], [{"role": "user", "content": "A sunset over the ocean."}], ] - responses = openai_client.send_batched_chat_completions_http_request( + responses = online_client.send_batched_chat_completions_http_request( { "json": { "model": omni_server.model, @@ -167,5 +170,7 @@ def test_flux_2_dev_batched_chat_completions( assert responses and len(responses) == 1 resp = responses[0] assert resp.success - choices = resp.json_body["choices"] + body = resp.json_body + assert isinstance(body, dict) + choices = body["choices"] assert len(choices) == len(messages) diff --git a/tests/e2e/online_serving/test_flux_kontext_expansion.py b/tests/e2e/online_serving/test_flux_kontext_expansion.py index ede5ff56b..79460c631 100644 --- a/tests/e2e/online_serving/test_flux_kontext_expansion.py +++ b/tests/e2e/online_serving/test_flux_kontext_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by the FluxKontext model. @@ -7,7 +10,7 @@ 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 +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -32,7 +35,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_flux_kontext_text_to_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux_kontext_text_to_image(omni_server: OmniServer, online_client: OnlineOmniClient): """Test text-to-image generation with FluxKontext in regular end-user scenarios.""" messages = dummy_messages_from_mix_data(content_text="A photo of a cat sitting on a laptop keyboard") @@ -47,7 +50,7 @@ def test_flux_kontext_text_to_image(omni_server: OmniServer, openai_client: Open }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -55,7 +58,7 @@ def test_flux_kontext_text_to_image(omni_server: OmniServer, openai_client: Open _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_flux_kontext_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux_kontext_image_edit(omni_server: OmniServer, online_client: OnlineOmniClient): """Test image editing with FluxKontext in regular end-user scenarios.""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -74,7 +77,7 @@ def test_flux_kontext_image_edit(omni_server: OmniServer, openai_client: OpenAIC }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -82,7 +85,7 @@ def test_flux_kontext_image_edit(omni_server: OmniServer, openai_client: OpenAIC _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_flux_kontext_image_edit_no_negative(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux_kontext_image_edit_no_negative(omni_server: OmniServer, online_client: OnlineOmniClient): """Test image editing with FluxKontext without negative prompt.""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -99,7 +102,7 @@ def test_flux_kontext_image_edit_no_negative(omni_server: OmniServer, openai_cli }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -107,7 +110,7 @@ def test_flux_kontext_image_edit_no_negative(omni_server: OmniServer, openai_cli _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_flux_kontext_high_resolution(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux_kontext_high_resolution(omni_server: OmniServer, online_client: OnlineOmniClient): """Test high-resolution generation with FluxKontext.""" messages = dummy_messages_from_mix_data(content_text="A beautiful landscape with mountains and a lake") @@ -122,7 +125,7 @@ def test_flux_kontext_high_resolution(omni_server: OmniServer, openai_client: Op }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -130,7 +133,7 @@ def test_flux_kontext_high_resolution(omni_server: OmniServer, openai_client: Op _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_flux_kontext_multiple_outputs(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_flux_kontext_multiple_outputs(omni_server: OmniServer, online_client: OnlineOmniClient): """Test generating multiple outputs with FluxKontext.""" messages = dummy_messages_from_mix_data(content_text="A photo of a cat sitting on a laptop keyboard") @@ -146,4 +149,4 @@ def test_flux_kontext_multiple_outputs(omni_server: OmniServer, openai_client: O }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_glm_image_expansion.py b/tests/e2e/online_serving/test_glm_image_expansion.py index 42e8d444a..290272364 100644 --- a/tests/e2e/online_serving/test_glm_image_expansion.py +++ b/tests/e2e/online_serving/test_glm_image_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by the following model: @@ -25,7 +28,7 @@ from tests.helpers.media import generate_synthetic_image from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config @@ -152,7 +155,7 @@ MODES = ["t2i", "i2i"] def test_glm_image( omni_server: OmniServer, mode: str, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """GLM-Image diffusion feature coverage for t2i and i2i on two H100s.""" @@ -180,4 +183,4 @@ def test_glm_image( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_glm_tts_expansion.py b/tests/e2e/online_serving/test_glm_tts_expansion.py index c558e0c40..89a98f6fb 100644 --- a/tests/e2e/online_serving/test_glm_tts_expansion.py +++ b/tests/e2e/online_serving/test_glm_tts_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E expansion tests for GLM-TTS online serving (nightly CI). @@ -14,7 +14,7 @@ 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.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -24,7 +24,7 @@ MODEL = os.environ.get("GLM_TTS_MODEL_PATH", "zai-org/GLM-TTS") REF_TEXT = "他当时还跟线下其他的站姐吵架,然后,打架进局子了。" DEPLOY_CONFIG = get_deploy_config_path("glm_tts.yaml") -REF_AUDIO_URL = load_test_audio_data_url("glm_tts/jiayan_zh.wav") +REF_AUDIO_URL = get_asset_path("glm_tts/jiayan_zh.wav", as_data_url=True) SYNC_EXTRA_ARGS = [ "--trust-remote-code", @@ -64,7 +64,7 @@ tts_async_chunk_server_params = [ @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True) -def test_voice_clone_zh_sync(omni_server, openai_client) -> None: +def test_voice_clone_zh_sync(omni_server, online_client) -> None: """ Test voice cloning TTS with Chinese text. Deploy Setting: glm_tts.yaml with ``--no-async-chunk`` (sync two-stage) @@ -80,12 +80,12 @@ def test_voice_clone_zh_sync(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True) -def test_voice_clone_zh_async_chunk(omni_server, openai_client) -> None: +def test_voice_clone_zh_async_chunk(omni_server, online_client) -> None: """ Test voice cloning TTS with Chinese text via async_chunk streaming. Deploy Setting: glm_tts.yaml default ``async_chunk: true`` @@ -102,12 +102,12 @@ def test_voice_clone_zh_async_chunk(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_sync_server_params, indirect=True) -def test_models_endpoint(omni_server, openai_client) -> None: +def test_models_endpoint(omni_server, online_client) -> None: """Test the /v1/models endpoint returns loaded model.""" - models = openai_client.client.models.list() + models = online_client.client.models.list() assert len(models.data) > 0 diff --git a/tests/e2e/online_serving/test_hidream_i1_full_expansion.py b/tests/e2e/online_serving/test_hidream_i1_full_expansion.py index a362d4902..dea3b3c9a 100644 --- a/tests/e2e/online_serving/test_hidream_i1_full_expansion.py +++ b/tests/e2e/online_serving/test_hidream_i1_full_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ End-to-end diffusion coverage for HiDream-I1-Full in online serving mode. @@ -12,7 +15,7 @@ image resolution. import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -49,7 +52,7 @@ def _get_hidream_i1_image_feature_cases(model: str): @pytest.mark.skip(reason="https://github.com/vllm-project/vllm-omni/issues/4662") def test_hidream_i1_image( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Validate HiDream-I1-Full online serving with CPU offload.""" @@ -67,4 +70,4 @@ def test_hidream_i1_image( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_higgs_audio_v2_expansion.py b/tests/e2e/online_serving/test_higgs_audio_v2_expansion.py index 8927eb039..e549c6bfc 100644 --- a/tests/e2e/online_serving/test_higgs_audio_v2_expansion.py +++ b/tests/e2e/online_serving/test_higgs_audio_v2_expansion.py @@ -1,17 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online expansion tests for higgs-audio v2 against /v1/audio/speech. v1 scope is plain text -> 24 kHz speech plus shallow voice clone via ref_audio + ref_text (inline) or voice= (after POST /v1/audio/voices). -The model-aware request validator -(vllm_omni/entrypoints/openai/tts_adapters/higgs_audio_v2::validate) -rejects multi-speaker tags, language overrides, task_type, and bare -voice= for names that do not match an uploaded speaker — this suite -exercises both the happy path (plain text in, audio bytes out) and the -validator rejections, so a regression that loosens the schema will fail -loudly. +Model-aware validator rejections live in L1 +``tests/entrypoints/openai_api/test_tts_adapter.py`` +(``test_higgs_audio_v2_validate_*``). """ from __future__ import annotations @@ -27,7 +23,7 @@ os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0") import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -63,7 +59,7 @@ _MIN_AUDIO_BYTES = 20_000 # speech) + its transcript. See tests/e2e/online_serving/test_qwen3_tts_base.py # for the asset rationale — keeping a single shared reference clip across TTS # tests avoids duplicating WAVs in the repo. -_REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +_REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) _REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -72,9 +68,9 @@ class TestHiggsAudioV2OnlineHappyPath: """Plain-text -> audio happy paths against the live HTTP server.""" @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_wav(self, omni_server, openai_client) -> None: + def test_plain_text_wav(self, omni_server, online_client) -> None: """Single non-streaming WAV request — covers the canonical TTS happy path.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello world.", @@ -86,7 +82,7 @@ class TestHiggsAudioV2OnlineHappyPath: ) @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None: + def test_plain_text_pcm_streaming(self, omni_server, online_client) -> None: """Streaming PCM via the shared-memory connector's codec_streaming path. higgs_audio_v2.yaml pins ``codec_streaming: true`` + ``async_chunk: false``, @@ -102,7 +98,7 @@ class TestHiggsAudioV2OnlineHappyPath: content correctness is separately guarded by the full_model transcript check. """ - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Streaming the quick brown fox over the lazy dog.", @@ -116,9 +112,9 @@ class TestHiggsAudioV2OnlineHappyPath: ) @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None: + def test_plain_text_with_max_new_tokens(self, omni_server, online_client) -> None: """max_new_tokens is one of the few extra fields the higgs validator accepts.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Innovation distinguishes between a leader and a follower.", @@ -131,9 +127,9 @@ class TestHiggsAudioV2OnlineHappyPath: ) @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_concurrent_plain_text(self, omni_server, openai_client) -> None: + def test_concurrent_plain_text(self, omni_server, online_client) -> None: """Three concurrent non-streaming requests — guards the per-slot audio state.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "It was the night before my birthday.", @@ -146,104 +142,6 @@ class TestHiggsAudioV2OnlineHappyPath: ) -# --------------------------------------------------------------------------- -# Validator rejections served over HTTP. Each case targets one out-of-scope -# field; the validator returns 4xx via the OpenAI error path. We keep the -# error-message substring loose so a phrasing tweak in the validator does not -# break CI, but tight enough to catch a regression that silently accepts the -# field. -# --------------------------------------------------------------------------- - - -@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -class TestHiggsAudioV2OnlineValidatorRejections: - """Out-of-scope fields must come back as 4xx with a higgs-named message.""" - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_ref_audio_without_ref_text(self, omni_server, openai_client) -> None: - """Voice clone needs the transcript too — half-supplied is a 4xx.""" - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": "Hello world.", - "ref_audio": _REF_AUDIO_URL, - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "ref_text", - } - ) - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_ref_text_without_ref_audio(self, omni_server, openai_client) -> None: - """Symmetric guard: ref_text alone is not enough — must come with ref_audio.""" - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": "Hello world.", - "ref_text": "some transcript", - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "ref_audio", - } - ) - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_task_type(self, omni_server, openai_client) -> None: - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": "Hello world.", - "task_type": "Base", - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "task_type", - } - ) - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_language_override(self, omni_server, openai_client) -> None: - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": "Hello world.", - "language": "Chinese", - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "language", - } - ) - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_multi_speaker_tag_in_text(self, omni_server, openai_client) -> None: - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": "[SPEAKER0] hi", - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "multi-speaker", - } - ) - - @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_rejects_empty_input(self, omni_server, openai_client) -> None: - openai_client.send_audio_speech_request( - { - "model": omni_server.model, - "input": " ", - "response_format": "wav", - "timeout": DEFAULT_SPEECH_TIMEOUT_S, - "status_code": (400, 422), - "err_message": "empty", - } - ) - - @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) class TestHiggsAudioV2OnlineVoiceClone: """Shallow voice clone: ref_audio + ref_text -> speech in the cloned voice. @@ -256,9 +154,9 @@ class TestHiggsAudioV2OnlineVoiceClone: """ @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_voice_clone_basic(self, omni_server, openai_client) -> None: + def test_voice_clone_basic(self, omni_server, online_client) -> None: """Single non-streaming WAV request driven by the qwen3_tts ref clip.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello world.", @@ -272,9 +170,9 @@ class TestHiggsAudioV2OnlineVoiceClone: ) @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_voice_clone_pcm_streaming(self, omni_server, openai_client) -> None: + def test_voice_clone_pcm_streaming(self, omni_server, online_client) -> None: """Voice clone over the streaming PCM path.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Innovation distinguishes a leader from a follower.", diff --git a/tests/e2e/online_serving/test_higgs_audio_v3.py b/tests/e2e/online_serving/test_higgs_audio_v3.py index df0522670..8550f74a4 100644 --- a/tests/e2e/online_serving/test_higgs_audio_v3.py +++ b/tests/e2e/online_serving/test_higgs_audio_v3.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """End-to-end online tests for higgs-audio v3 against /v1/audio/speech. Mirrors the higgs_audio_v2 test layout. Covers the plain-text-in / audio-out @@ -21,7 +21,7 @@ os.environ.setdefault("VLLM_MOE_USE_DEEP_GEMM", "0") import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -55,7 +55,7 @@ _SKIP_ISSUE_6418 = pytest.mark.skip( # Reuse the shared TTS reference clip (clean ~5 s 24 kHz mono human speech) # vendored under tests/assets/qwen3_tts/. Keeps a single WAV across TTS # tests rather than duplicating asset bytes. -_REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +_REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) _REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -66,9 +66,9 @@ class TestHiggsAudioV3OnlineHappyPath: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_wav(self, omni_server, openai_client) -> None: + def test_plain_text_wav(self, omni_server, online_client) -> None: """Single non-streaming WAV request - canonical TTS happy path.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello world.", @@ -86,9 +86,9 @@ class TestHiggsAudioV3OnlineHappyPath: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_with_max_new_tokens(self, omni_server, openai_client) -> None: + def test_plain_text_with_max_new_tokens(self, omni_server, online_client) -> None: """``max_new_tokens`` is one of the few extra fields the v3 validator accepts.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Innovation distinguishes between a leader and a follower.", @@ -104,9 +104,9 @@ class TestHiggsAudioV3OnlineHappyPath: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_concurrent_plain_text(self, omni_server, openai_client) -> None: + def test_concurrent_plain_text(self, omni_server, online_client) -> None: """Three concurrent non-streaming requests - guards per-slot audio state and Stage-0 PC under batching.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "it was the night before my birthday", @@ -120,7 +120,7 @@ class TestHiggsAudioV3OnlineHappyPath: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_plain_text_pcm_streaming(self, omni_server, openai_client) -> None: + def test_plain_text_pcm_streaming(self, omni_server, online_client) -> None: """Streaming PCM via talker2code2wav_async_chunk + the connector's codec_streaming path. The deploy YAML pins ``async_chunk: true`` and ``codec_streaming: true`` so chunks @@ -136,7 +136,7 @@ class TestHiggsAudioV3OnlineHappyPath: ~0.16 dB margin over measured single-request output (1.16 dB on L4), so flake risk is too high there. """ - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "The quick brown fox jumps over the lazy dog.", @@ -151,7 +151,7 @@ class TestHiggsAudioV3OnlineHappyPath: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_concurrent_pcm_streaming(self, omni_server, openai_client) -> None: + def test_concurrent_pcm_streaming(self, omni_server, online_client) -> None: """Three concurrent streaming requests - guards per-request frame cursors in ``talker2code2wav_async_chunk`` and per-slot delay-pattern state under batched AR. @@ -161,7 +161,7 @@ class TestHiggsAudioV3OnlineHappyPath: over the measured worst-of-3 on L4 (-0.42 dB) while still well above the catastrophic-failure region. """ - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "She sells seashells by the seashore.", @@ -194,9 +194,9 @@ class TestHiggsAudioV3OnlineInlineControlTokens: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_inline_emotion_and_expressive(self, omni_server, openai_client) -> None: + def test_inline_emotion_and_expressive(self, omni_server, online_client) -> None: """Delivery tokens (emotion + expressive_high) at the start of input.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": ("<|emotion:amusement|><|prosody:expressive_high|>Wait, that was actually hilarious."), @@ -211,9 +211,9 @@ class TestHiggsAudioV3OnlineInlineControlTokens: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_inline_style_whispering(self, omni_server, openai_client) -> None: + def test_inline_style_whispering(self, omni_server, online_client) -> None: """Style token at the start - ``whispering``.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "<|style:whispering|>It is just between you and me, alright?", @@ -228,9 +228,9 @@ class TestHiggsAudioV3OnlineInlineControlTokens: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_inline_prosody_speed_and_pitch(self, omni_server, openai_client) -> None: + def test_inline_prosody_speed_and_pitch(self, omni_server, online_client) -> None: """Two prosody tokens at the start - slow speed plus low pitch.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": ( @@ -247,9 +247,9 @@ class TestHiggsAudioV3OnlineInlineControlTokens: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_inline_pause_mid_text(self, omni_server, openai_client) -> None: + def test_inline_pause_mid_text(self, omni_server, online_client) -> None: """Positional pause token placed inline between two clauses.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hold on a moment <|prosody:pause|> let me think about it.", @@ -264,9 +264,9 @@ class TestHiggsAudioV3OnlineInlineControlTokens: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_inline_sfx_with_onomatopoeia(self, omni_server, openai_client) -> None: + def test_inline_sfx_with_onomatopoeia(self, omni_server, online_client) -> None: """SFX token paired with its written onomatopoeia (``<|sfx:laughter|>Hehe``).""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": ( @@ -288,9 +288,9 @@ class TestHiggsAudioV3OnlineVoiceClone: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_voice_clone_ref_audio_ref_text(self, omni_server, openai_client) -> None: + def test_voice_clone_ref_audio_ref_text(self, omni_server, online_client) -> None: """Canonical vllm-omni voice clone via ``ref_audio`` + ``ref_text``.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello world.", @@ -305,14 +305,14 @@ class TestHiggsAudioV3OnlineVoiceClone: @pytest.mark.slow @pytest.mark.tts @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_voice_clone_references_alias(self, omni_server, openai_client) -> None: + def test_voice_clone_references_alias(self, omni_server, online_client) -> None: """BosonAI cookbook payload: ``references=[{audio_path, text}]``. ``normalize_references_alias`` (``protocol/audio.py``) is supposed to translate the cookbook field into ``ref_audio`` / ``ref_text``; without it the request would silently fall through to zero-shot synthesis. """ - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello world.", diff --git a/tests/e2e/online_serving/test_hunyuan_video_15_expansion.py b/tests/e2e/online_serving/test_hunyuan_video_15_expansion.py index a448335c6..dfe545327 100644 --- a/tests/e2e/online_serving/test_hunyuan_video_15_expansion.py +++ b/tests/e2e/online_serving/test_hunyuan_video_15_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Tests of common diffusion feature combinations in online serving mode for HunyuanVideo-1.5-T2V (480p). @@ -12,7 +12,7 @@ Coverage (H100, since model cannot fit L4): import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -82,7 +82,7 @@ def _get_diffusion_feature_cases(model: str): ) def test_hunyuan_video_15_t2v( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """L4 diffusion feature coverage for HunyuanVideo-1.5-T2V on H100.""" form_data = { @@ -101,4 +101,4 @@ def test_hunyuan_video_15_t2v( "form_data": form_data, } - openai_client.send_video_diffusion_request(request_config) + online_client.send_video_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_indextts2_expansion.py b/tests/e2e/online_serving/test_indextts2_expansion.py index d971ef166..da9bb6dd8 100644 --- a/tests/e2e/online_serving/test_indextts2_expansion.py +++ b/tests/e2e/online_serving/test_indextts2_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """E2E online serving tests for IndexTTS2 via /v1/audio/speech endpoint. Two-stage pipeline: GPT AR → S2Mel + BigVGAN. Output is 22050 Hz mono WAV. @@ -15,7 +15,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -23,7 +23,7 @@ pytestmark = [pytest.mark.slow, pytest.mark.tts] MODEL = "IndexTeam/IndexTTS-2" -REF_AUDIO_URL = load_test_audio_data_url("indextts2/ref_audio.wav") +REF_AUDIO_URL = get_asset_path("indextts2/ref_audio.wav", as_data_url=True) tts_server_params = [ pytest.param( @@ -39,7 +39,7 @@ tts_server_params = [ @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_basic_english(omni_server, openai_client) -> None: +def test_basic_english(omni_server, online_client) -> None: """ Basic English TTS: text + ref_audio, no emotion. Deploy Setting: default yaml @@ -55,12 +55,12 @@ def test_basic_english(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "min_audio_bytes": 1024, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_streaming(omni_server, openai_client) -> None: +def test_streaming(omni_server, online_client) -> None: """ stream=True compatibility: IndexTTS2 itself is non-async-chunking, but the OpenAI speech endpoint should still accept the streaming request path and @@ -80,4 +80,4 @@ def test_streaming(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "min_audio_bytes": 1024, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_krea2_expansion.py b/tests/e2e/online_serving/test_krea2_expansion.py index f76a567f4..eb1d6d078 100644 --- a/tests/e2e/online_serving/test_krea2_expansion.py +++ b/tests/e2e/online_serving/test_krea2_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """L4 online-serving tests for the Krea 2 text-to-image diffusion pipeline. @@ -29,7 +29,7 @@ import requests from PIL import Image from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -76,7 +76,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_krea2(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_krea2(omni_server: OmniServer, online_client: OnlineOmniClient): """Serve Krea 2 and validate the OpenAI-compatible image-generation route. guidance_scale resolution is checkpoint-aware inside the pipeline (distilled -> no-CFG, @@ -96,7 +96,7 @@ def test_krea2(omni_server: OmniServer, openai_client: OpenAIClientHandler): "seed": 42, }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) def _basic_lora_payload(lora: dict | None = None) -> dict: diff --git a/tests/e2e/online_serving/test_lance.py b/tests/e2e/online_serving/test_lance.py index 9c1a40be9..54e782e10 100644 --- a/tests/e2e/online_serving/test_lance.py +++ b/tests/e2e/online_serving/test_lance.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """End-to-end online serving test for Lance (single-stage). Verifies that the Lance pipeline can serve text-to-image and image-edit @@ -100,7 +100,7 @@ def _build_img2img_messages(prompt: str, image_b64: str) -> list[dict]: @pytest.mark.diffusion @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_lance_text2img_online(omni_server, openai_client) -> None: +def test_lance_text2img_online(omni_server, online_client) -> None: """Lance text2img via the OpenAI-compatible chat completions API.""" request_config = { "model": omni_server.model, @@ -115,7 +115,7 @@ def test_lance_text2img_online(omni_server, openai_client) -> None: }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.core_model @@ -123,7 +123,7 @@ def test_lance_text2img_online(omni_server, openai_client) -> None: @pytest.mark.diffusion @hardware_test(res={"cuda": "H100"}) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_lance_img2img_online(omni_server, openai_client) -> None: +def test_lance_img2img_online(omni_server, online_client) -> None: """Lance image_edit via the OpenAI-compatible chat completions API.""" input_image = ImageAsset("2560px-Gfp-wisconsin-madison-the-nature-boardwalk").pil_image.convert("RGB") buffer = BytesIO() @@ -141,4 +141,4 @@ def test_lance_img2img_online(omni_server, openai_client) -> None: }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_longcat_image_edit_expansion.py b/tests/e2e/online_serving/test_longcat_image_edit_expansion.py index 7c7e0788c..af6bbac50 100644 --- a/tests/e2e/online_serving/test_longcat_image_edit_expansion.py +++ b/tests/e2e/online_serving/test_longcat_image_edit_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Recommended tests of diffusion features that are available in online serving mode and are supported by the following model: @@ -16,7 +19,7 @@ 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 +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -66,7 +69,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_longcat_image_edit(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_longcat_image_edit(omni_server: OmniServer, online_client: OnlineOmniClient): """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) @@ -84,4 +87,4 @@ def test_longcat_image_edit(omni_server: OmniServer, openai_client: OpenAIClient }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_longcat_image_expansion.py b/tests/e2e/online_serving/test_longcat_image_expansion.py index 5ed7bb156..5616708c1 100644 --- a/tests/e2e/online_serving/test_longcat_image_expansion.py +++ b/tests/e2e/online_serving/test_longcat_image_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Recommended tests of diffusion features that are available in online serving mode and are supported by the following model: @@ -15,7 +18,7 @@ This validates: import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -66,7 +69,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(MODEL), indirect=True, ) -def test_longcat_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_longcat_image(omni_server: OmniServer, online_client: OnlineOmniClient): """Test the recommended feature combinations for LongCat-Image.""" messages = dummy_messages_from_mix_data(content_text=TEXT_TO_IMAGE_PROMPT) @@ -83,4 +86,4 @@ def test_longcat_image(omni_server: OmniServer, openai_client: OpenAIClientHandl }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_mimo_audio_expansion.py b/tests/e2e/online_serving/test_mimo_audio_expansion.py index e5722ddb8..a95d75569 100644 --- a/tests/e2e/online_serving/test_mimo_audio_expansion.py +++ b/tests/e2e/online_serving/test_mimo_audio_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E expansion tests for MiMo-Audio online serving (nightly Omni · Function Test with H100). """ @@ -77,7 +77,7 @@ def get_max_batch_size(size_type="few"): @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_text_001(omni_server, openai_client) -> None: +def test_text_to_text_001(omni_server, online_client) -> None: """ Test text input processing and text-only output generation via OpenAI API. Deploy Setting: default yaml @@ -95,13 +95,13 @@ def test_text_to_text_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1) @pytest.mark.parametrize("omni_server", test_params, indirect=True) @pytest.mark.skip(reason="CI failed 8571") -def test_audio_to_text_audio_001(omni_server, openai_client) -> None: +def test_audio_to_text_audio_001(omni_server, online_client) -> None: """ Test audio and text input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -126,4 +126,4 @@ def test_audio_to_text_audio_001(omni_server, openai_client) -> None: }, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) diff --git a/tests/e2e/online_serving/test_ming_flash_omni_expansion.py b/tests/e2e/online_serving/test_ming_flash_omni_expansion.py index 6f6881d68..97a634906 100644 --- a/tests/e2e/online_serving/test_ming_flash_omni_expansion.py +++ b/tests/e2e/online_serving/test_ming_flash_omni_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E online serving tests for Ming-flash-omni-2.0 model (Thinker stage). Tests multimodal understanding via OpenAI-compatible API. @@ -70,7 +70,7 @@ def get_max_batch_size(size_type="few"): @_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: +def test_text_to_text_001(omni_server, online_client) -> None: """ Input Modal: text Output Modal: text @@ -90,13 +90,13 @@ def test_text_to_text_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @_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: +def test_text_to_text_stream_001(omni_server, online_client) -> None: """ Input Modal: text Output Modal: text @@ -116,13 +116,13 @@ def test_text_to_text_stream_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @_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: +def test_image_to_text_001(omni_server, online_client) -> None: """ Input Modal: image + text Output Modal: text @@ -143,13 +143,13 @@ def test_image_to_text_001(omni_server, openai_client) -> None: "modalities": ["text"], } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @_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: +def test_audio_to_text_001(omni_server, online_client) -> None: """ Input Modal: audio + text Output Modal: text @@ -170,13 +170,13 @@ def test_audio_to_text_001(omni_server, openai_client) -> None: "modalities": ["text"], } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @_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: +def test_video_to_text_001(omni_server, online_client) -> None: """ Input Modal: video + text Output Modal: text @@ -197,13 +197,13 @@ def test_video_to_text_001(omni_server, openai_client) -> None: "modalities": ["text"], } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @_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: +def test_mix_to_text_001(omni_server, online_client) -> None: """ Input Modal: text + audio + image + video Output Modal: text @@ -228,4 +228,4 @@ def test_mix_to_text_001(omni_server, openai_client) -> None: "modalities": ["text"], } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) diff --git a/tests/e2e/online_serving/test_ming_tts_expansion.py b/tests/e2e/online_serving/test_ming_tts_expansion.py index 0f7a53606..4352a8966 100644 --- a/tests/e2e/online_serving/test_ming_tts_expansion.py +++ b/tests/e2e/online_serving/test_ming_tts_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E expansion tests for Ming-omni-tts online serving (nightly CI). @@ -56,7 +56,7 @@ def get_prompt(prompt_type="zh"): @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", no_async_chunk_params, indirect=True) -def test_text_to_audio_non_streaming_001(omni_server, openai_client) -> None: +def test_text_to_audio_non_streaming_001(omni_server, online_client) -> None: """ Deploy Setting: ming_tts.yaml with --no-async-chunk Input Modal: text @@ -71,12 +71,12 @@ def test_text_to_audio_non_streaming_001(omni_server, openai_client) -> None: "response_format": "wav", "timeout": 300.0, } - openai_client.send_audio_speech_request(request_config, request_num=2) + online_client.send_audio_speech_request(request_config, request_num=2) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", async_chunk_params, indirect=True) -def test_text_to_audio_streaming_001(omni_server, openai_client) -> None: +def test_text_to_audio_streaming_001(omni_server, online_client) -> None: """ Deploy Setting: ming_tts.yaml (async_chunk=true) Input Modal: text + voice @@ -93,4 +93,4 @@ def test_text_to_audio_streaming_001(omni_server, openai_client) -> None: "response_format": "wav", "timeout": 300.0, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_minicpmo_4_5_duplex.py b/tests/e2e/online_serving/test_minicpmo_4_5_duplex.py index d63811022..cff29e662 100644 --- a/tests/e2e/online_serving/test_minicpmo_4_5_duplex.py +++ b/tests/e2e/online_serving/test_minicpmo_4_5_duplex.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """CI coverage for the MiniCPM-o 4.5 native-duplex Realtime API.""" from __future__ import annotations @@ -107,8 +110,8 @@ async def _run_protocol_smoke(*, url: str, model: str, ref_audio: Path) -> list[ @pytest.mark.core_model @hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", SERVER_PARAMS, indirect=True) -def test_duplex_websocket_protocol_smoke(omni_server, model_prefix: str) -> None: - ref_audio = resolve_ref_audio(model_prefix) +def test_duplex_websocket_protocol_smoke(omni_server) -> None: + ref_audio = resolve_ref_audio() events = asyncio.run( _run_protocol_smoke( url=realtime_url(omni_server), @@ -125,11 +128,11 @@ def test_duplex_websocket_protocol_smoke(omni_server, model_prefix: str) -> None @pytest.mark.advanced_model @hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", SERVER_PARAMS, indirect=True) -def test_duplex_single_session_response_required(omni_server, model_prefix: str, tmp_path: Path) -> None: +def test_duplex_single_session_response_required(omni_server, tmp_path: Path) -> None: args = demo_args( omni_server=omni_server, input_wav=validated_input_wav(), - ref_audio=resolve_ref_audio(model_prefix), + ref_audio=resolve_ref_audio(), output_dir=tmp_path / "single_session", ) args.turns = 2 @@ -151,13 +154,13 @@ def test_duplex_single_session_response_required(omni_server, model_prefix: str, @pytest.mark.advanced_model @hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", SERVER_PARAMS, indirect=True) -def test_duplex_two_sessions_resume_and_takeover(omni_server, model_prefix: str, tmp_path: Path) -> None: +def test_duplex_two_sessions_resume_and_takeover(omni_server, tmp_path: Path) -> None: result = asyncio.run( run_multi_session( multi_session_args( omni_server=omni_server, input_wav=validated_input_wav(), - ref_audio=resolve_ref_audio(model_prefix), + ref_audio=resolve_ref_audio(), output_dir=tmp_path / "multi_session", response_required=True, ) diff --git a/tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py b/tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py index 68204fe28..3d9be0e3b 100644 --- a/tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py +++ b/tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Nightly lifecycle coverage for the MiniCPM-o 4.5 native-duplex API.""" from __future__ import annotations @@ -31,11 +34,11 @@ pytestmark = [pytest.mark.full_model, pytest.mark.omni] @hardware_test(res={"cuda": "H100", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", SERVER_PARAMS, indirect=True) -def test_duplex_admission_and_expiry_reaper(omni_server, model_prefix: str, tmp_path: Path) -> None: +def test_duplex_admission_and_expiry_reaper(omni_server, tmp_path: Path) -> None: args = multi_session_args( omni_server=omni_server, input_wav=validated_input_wav(), - ref_audio=resolve_ref_audio(model_prefix), + ref_audio=resolve_ref_audio(), output_dir=tmp_path / "admission_expiry", response_required=False, ) @@ -59,7 +62,7 @@ def test_duplex_admission_and_expiry_reaper(omni_server, model_prefix: str, tmp_ # so it never reaches a mid-stream decision point. @hardware_test(res={"cuda": "H100"}, num_cards=1) @pytest.mark.parametrize("omni_server", SERVER_PARAMS, indirect=True) -def test_duplex_soft_interrupt(omni_server, model_prefix: str, tmp_path: Path) -> None: +def test_duplex_soft_interrupt(omni_server, tmp_path: Path) -> None: input_wav = validated_soft_interrupt_wav() result = asyncio.run( run_soft_interrupt( @@ -67,7 +70,7 @@ def test_duplex_soft_interrupt(omni_server, model_prefix: str, tmp_path: Path) - url=realtime_url(omni_server), model=omni_server.model, input_wav=str(input_wav), - ref_audio=str(resolve_ref_audio(model_prefix)), + ref_audio=str(resolve_ref_audio()), output_dir=str(tmp_path / "soft_interrupt"), summary_output=None, chunk_ms=200, diff --git a/tests/e2e/online_serving/test_moss_tts_expansion.py b/tests/e2e/online_serving/test_moss_tts_expansion.py index e2e79d653..598326a1e 100644 --- a/tests/e2e/online_serving/test_moss_tts_expansion.py +++ b/tests/e2e/online_serving/test_moss_tts_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E online tests for the full MOSS-TTS family via /v1/audio/speech. @@ -96,7 +96,7 @@ tts_server_params = [ @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: +def test_text_to_audio_001(omni_server, online_client, ref_audio_data_url) -> None: """ Realtime voice_clone via /v1/audio/speech, non-streaming. Deploy Setting: moss_tts_realtime.yaml @@ -118,12 +118,12 @@ def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> No "min_audio_bytes": _MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_text_to_audio_002_streaming(omni_server, openai_client, ref_audio_data_url) -> None: +def test_text_to_audio_002_streaming(omni_server, online_client, ref_audio_data_url) -> None: """ Realtime voice_clone via /v1/audio/speech, streaming PCM. Deploy Setting: moss_tts_realtime.yaml @@ -147,12 +147,12 @@ def test_text_to_audio_002_streaming(omni_server, openai_client, ref_audio_data_ "min_hnr_db": -5.0, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_text_to_audio_003_chinese(omni_server, openai_client, ref_audio_data_url) -> None: +def test_text_to_audio_003_chinese(omni_server, online_client, ref_audio_data_url) -> None: """ Realtime voice_clone via /v1/audio/speech, Chinese input. Deploy Setting: moss_tts_realtime.yaml @@ -172,4 +172,4 @@ def test_text_to_audio_003_chinese(omni_server, openai_client, ref_audio_data_ur "min_audio_bytes": _MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_moss_tts_nano_expansion.py b/tests/e2e/online_serving/test_moss_tts_nano_expansion.py index 3468a9656..3ea8c91ea 100644 --- a/tests/e2e/online_serving/test_moss_tts_nano_expansion.py +++ b/tests/e2e/online_serving/test_moss_tts_nano_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for MOSS-TTS-Nano model with text input and audio output. @@ -86,7 +86,7 @@ tts_server_params = [ @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: +def test_text_to_audio_001(omni_server, online_client, ref_audio_data_url) -> None: """ Test voice_clone mode (no ref_text) via /v1/audio/speech. Deploy Setting: default yaml @@ -103,12 +103,12 @@ def test_text_to_audio_001(omni_server, openai_client, ref_audio_data_url) -> No "ref_audio": ref_audio_data_url, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @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: +def test_text_to_audio_002(omni_server, online_client, ref_audio_data_url) -> None: """ Test streaming voice_clone via /v1/audio/speech. Deploy Setting: default yaml @@ -135,12 +135,12 @@ def test_text_to_audio_002(omni_server, openai_client, ref_audio_data_url) -> No "min_hnr_db": -5.0, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @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: +def test_text_to_audio_003(omni_server, online_client, ref_audio_data_url) -> None: """ Test Chinese voice_clone via /v1/audio/speech. Deploy Setting: default yaml @@ -157,12 +157,12 @@ def test_text_to_audio_003(omni_server, openai_client, ref_audio_data_url) -> No "ref_audio": ref_audio_data_url, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @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: +def test_text_to_audio_004_ref_text_ignored(omni_server, online_client, ref_audio_data_url) -> None: """ Sending ``ref_text`` alongside ``ref_audio`` is accepted but ignored. @@ -180,4 +180,4 @@ def test_text_to_audio_004_ref_text_ignored(omni_server, openai_client, ref_audi "ref_text": REF_AUDIO_TRANSCRIPT, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_nextstep_expansion.py b/tests/e2e/online_serving/test_nextstep_expansion.py index 381079320..3e0bc53b5 100644 --- a/tests/e2e/online_serving/test_nextstep_expansion.py +++ b/tests/e2e/online_serving/test_nextstep_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Online serving E2E for NextStep-1.1 text-to-image (tensor parallel). """ @@ -10,7 +13,7 @@ from tests.helpers.mark import hardware_marks from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -66,7 +69,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases(model=os.environ.get("VLLM_TEST_NEXTSTEP_MODEL", _DEFAULT_MODEL)), indirect=True, ) -def test_nextstep_11(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_nextstep_11(omni_server: OmniServer, online_client: OnlineOmniClient): messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT) request_config = { "model": omni_server.model, @@ -82,4 +85,4 @@ def test_nextstep_11(omni_server: OmniServer, openai_client: OpenAIClientHandler }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_omnivoice_expansion.py b/tests/e2e/online_serving/test_omnivoice_expansion.py index 78326f74c..3a042082e 100644 --- a/tests/e2e/online_serving/test_omnivoice_expansion.py +++ b/tests/e2e/online_serving/test_omnivoice_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for OmniVoice TTS model via /v1/audio/speech endpoint. @@ -14,7 +14,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import get_asset_path, load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path from vllm_omni.entrypoints.openai.serving_speech import _DEFAULT_VOICE_NAME @@ -47,7 +47,7 @@ TEST_PARAMS = [ _DEFAULT_MIN_AUDIO_BYTES = 5000 -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -63,7 +63,7 @@ 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: + def test_speech_auto_voice(self, omni_server, online_client) -> None: """Test auto voice TTS generation (text only, no reference audio).""" request_config = { "model": omni_server.model, @@ -72,7 +72,7 @@ class TestOmniVoiceTTS: "timeout": 180.0, "min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) @@ -80,7 +80,7 @@ class TestOmniVoiceSeed: """E2E tests for OmniVoice Seed Params.""" @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_speech_auto_voice_seed_deterministic(self, omni_server, openai_client) -> None: + def test_speech_auto_voice_seed_deterministic(self, omni_server, online_client) -> None: cfg = { "model": omni_server.model, "input": get_prompt("text"), @@ -89,12 +89,12 @@ class TestOmniVoiceSeed: "min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES, } - r1 = openai_client.send_audio_speech_request(cfg)[0] - r2 = openai_client.send_audio_speech_request(cfg)[0] + r1 = online_client.send_audio_speech_request(cfg)[0] + r2 = online_client.send_audio_speech_request(cfg)[0] assert r1.audio_bytes == r2.audio_bytes @hardware_test(res={"cuda": "L4"}, num_cards=1) - def test_speech_auto_voice_seed_non_deterministic(self, omni_server, openai_client) -> None: + def test_speech_auto_voice_seed_non_deterministic(self, omni_server, online_client) -> None: cfg = { "model": omni_server.model, "input": get_prompt("text"), @@ -105,8 +105,8 @@ class TestOmniVoiceSeed: cfg1 = {**cfg, "seed": 42} cfg2 = {**cfg, "seed": 43} - r1 = openai_client.send_audio_speech_request(cfg1)[0] - r2 = openai_client.send_audio_speech_request(cfg2)[0] + r1 = online_client.send_audio_speech_request(cfg1)[0] + r2 = online_client.send_audio_speech_request(cfg2)[0] assert r1.audio_bytes != r2.audio_bytes @@ -116,7 +116,7 @@ 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: + def test_voice_clone_ref_audio_only(self, omni_server, online_client) -> None: """Test voice cloning with ref_audio only (x_vector mode).""" request_config = { "model": omni_server.model, @@ -126,10 +126,10 @@ class TestOmniVoiceVoiceCloning: "timeout": 180.0, "min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_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: + def test_voice_clone_ref_audio_and_text(self, omni_server, online_client) -> None: """Test voice cloning with ref_audio and ref_text (in-context mode).""" request_config = { "model": omni_server.model, @@ -140,13 +140,13 @@ class TestOmniVoiceVoiceCloning: "timeout": 180.0, "min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) @pytest.mark.parametrize("voice", [_DEFAULT_VOICE_NAME, {"id": _DEFAULT_VOICE_NAME}]) -def test_speech_with_voice_param_accepted(omni_server, openai_client, voice) -> None: +def test_speech_with_voice_param_accepted(omni_server, online_client, voice) -> None: """The default voice should be accepted as both a plain string and a VoiceID dict, even when the model has no uploaded speakers.""" request_config = { @@ -157,26 +157,12 @@ def test_speech_with_voice_param_accepted(omni_server, openai_client, voice) -> "timeout": 180.0, "min_audio_bytes": _DEFAULT_MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_unknown_voice_rejected(omni_server, openai_client) -> None: - """An unrecognized voice name should be rejected with a 400.""" - request_config = { - "model": omni_server.model, - "input": get_prompt("text"), - "voice": "nonexistent_voice", - "status_code": 400, - "err_message": "Invalid voice", - } - openai_client.send_audio_speech_request(request_config) - - -@hardware_test(res={"cuda": "L4"}, num_cards=1) -@pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_voice_request_before_and_after_clone_registration(omni_server, openai_client) -> None: +def test_voice_request_before_and_after_clone_registration(omni_server, online_client) -> None: """The default voice should always be listed and usable, even after a different cloned voice is registered.""" speech_config = { @@ -186,11 +172,11 @@ def test_voice_request_before_and_after_clone_registration(omni_server, openai_c } # 1) "default" should appear in the voices list before any uploads - voices = openai_client.send_audio_voices_list_http_request()[0] + voices = online_client.send_audio_voices_list_http_request()[0] assert _DEFAULT_VOICE_NAME in voices.json_body["voices"] # 2) Speech with the default voice should succeed - openai_client.send_audio_speech_request(speech_config) + online_client.send_audio_speech_request(speech_config) # 3) Register a cloned voice under a different name register_config = { @@ -203,20 +189,20 @@ def test_voice_request_before_and_after_clone_registration(omni_server, openai_c ), }, } - openai_client.send_audio_voices_create_http_request(register_config) + online_client.send_audio_voices_create_http_request(register_config) # 4) "default" should still be listed alongside the new voice - voices = openai_client.send_audio_voices_list_http_request()[0] + voices = online_client.send_audio_voices_list_http_request()[0] assert _DEFAULT_VOICE_NAME in voices.json_body["voices"] assert "bar" in voices.json_body["voices"] # 5) Speech with the default voice should still succeed - openai_client.send_audio_speech_request(speech_config) + online_client.send_audio_speech_request(speech_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", TEST_PARAMS, indirect=True) -def test_registered_default_voice_overrides_placeholder(omni_server, openai_client) -> None: +def test_registered_default_voice_overrides_placeholder(omni_server, online_client) -> None: """When a real voice is uploaded under the default name, it should be used as a cloned voice; after deletion, the placeholder behavior resumes.""" speech_config = { @@ -226,7 +212,7 @@ def test_registered_default_voice_overrides_placeholder(omni_server, openai_clie } # 1) Placeholder default works before any registration - openai_client.send_audio_speech_request(speech_config) + online_client.send_audio_speech_request(speech_config) # 2) Register a real voice under the default name register_config = { @@ -239,23 +225,23 @@ def test_registered_default_voice_overrides_placeholder(omni_server, openai_clie ), }, } - openai_client.send_audio_voices_create_http_request(register_config) + online_client.send_audio_voices_create_http_request(register_config) # 3) After creating the voice with the default name, it's visible as an uploaded_voice - voices = openai_client.send_audio_voices_list_http_request()[0] + voices = online_client.send_audio_voices_list_http_request()[0] assert _DEFAULT_VOICE_NAME in voices.json_body["voices"] uploaded_names = [v["name"] for v in voices.json_body["uploaded_voices"]] assert _DEFAULT_VOICE_NAME in uploaded_names - openai_client.send_audio_speech_request(speech_config) + online_client.send_audio_speech_request(speech_config) # 4) Delete the uploaded default voice - openai_client.send_audio_voices_delete_http_request( + online_client.send_audio_voices_delete_http_request( {"name": _DEFAULT_VOICE_NAME}, ) # 5) Ensure the placeholder behavior is consistent, but it's not an uploaded voice now - voices = openai_client.send_audio_voices_list_http_request()[0] + voices = online_client.send_audio_voices_list_http_request()[0] assert _DEFAULT_VOICE_NAME in voices.json_body["voices"] uploaded_names = [v["name"] for v in voices.json_body["uploaded_voices"]] assert _DEFAULT_VOICE_NAME not in uploaded_names - openai_client.send_audio_speech_request(speech_config) + online_client.send_audio_speech_request(speech_config) diff --git a/tests/e2e/online_serving/test_qwen2_5_omni_expansion.py b/tests/e2e/online_serving/test_qwen2_5_omni_expansion.py index bbf9ca2b6..1e5e59aed 100644 --- a/tests/e2e/online_serving/test_qwen2_5_omni_expansion.py +++ b/tests/e2e/online_serving/test_qwen2_5_omni_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ E2E Online tests for Qwen2.5-Omni model with video input and audio output. """ @@ -59,7 +62,7 @@ def get_max_batch_size(size_type="few"): @pytest.mark.omni @hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_mix_to_text_audio_001(omni_server, openai_client) -> None: +def test_mix_to_text_audio_001(omni_server, online_client) -> None: """ Test multi-modal input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -90,13 +93,13 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None: } # Test single completion - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @pytest.mark.slow @pytest.mark.omni @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_text_001(omni_server, openai_client) -> None: +def test_text_to_text_001(omni_server, online_client) -> None: """ Test text input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -114,4 +117,4 @@ def test_text_to_text_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) diff --git a/tests/e2e/online_serving/test_qwen3_omni.py b/tests/e2e/online_serving/test_qwen3_omni.py index 036740ead..f6ede8944 100644 --- a/tests/e2e/online_serving/test_qwen3_omni.py +++ b/tests/e2e/online_serving/test_qwen3_omni.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ E2E Online tests for Qwen3-Omni model with video input and audio output. """ @@ -82,7 +85,7 @@ def get_max_batch_size(size_type="few"): @pytest.mark.skipif(_USE_PD, reason="Temporarily skip PD mode in this test module.") @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=3 if _USE_PD else 2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_mix_to_text_audio_001(omni_server, openai_client) -> None: +def test_mix_to_text_audio_001(omni_server, online_client) -> None: """ Test multi-modal input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -113,7 +116,7 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None: } # Test single completion - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @pytest.mark.advanced_model @@ -122,7 +125,7 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None: @pytest.mark.skipif(_USE_PD, reason="Temporarily skip PD mode in this test module.") @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=3 if _USE_PD else 2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_text_001(omni_server, openai_client) -> None: +def test_text_to_text_001(omni_server, online_client) -> None: """ Test text input processing and text/audio output generation via OpenAI API. Deploy Setting: default yaml @@ -140,10 +143,10 @@ def test_text_to_text_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) -def _run_prefix_cache_check(openai_client, request_config: dict): +def _run_prefix_cache_check(online_client, request_config: dict): """Make two requests given a request config, and validate that: 1. The second request actually had cached tokens 2. The number of cached tokens is divisible by the block size used in @@ -157,8 +160,8 @@ def _run_prefix_cache_check(openai_client, request_config: dict): Returns the (first, second) responses for additional per-test assertions. """ - first_response = openai_client.send_omni_request(request_config, request_num=1)[0] - cached_response = openai_client.send_omni_request(request_config, request_num=1)[0] + first_response = online_client.send_omni_request(request_config, request_num=1)[0] + cached_response = online_client.send_omni_request(request_config, request_num=1)[0] assert first_response.cached_tokens is not None @@ -180,7 +183,7 @@ def _run_prefix_cache_check(openai_client, request_config: dict): @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_thinker_prefix_caching_text_output(omni_server, openai_client) -> None: +def test_thinker_prefix_caching_text_output(omni_server, online_client) -> None: """ Test thinker prefix caching by sending identical requests with an image (i.e., a large shared prefix) and verifying that the second request uses cached tokens @@ -206,7 +209,7 @@ def test_thinker_prefix_caching_text_output(omni_server, openai_client) -> None: "stream": False, "modalities": ["text"], } - first_response, _ = _run_prefix_cache_check(openai_client, request_config) + first_response, _ = _run_prefix_cache_check(online_client, request_config) assert first_response.multimodal_tokens is not None assert first_response.multimodal_tokens.get("image", 0) > 0 @@ -217,7 +220,7 @@ def test_thinker_prefix_caching_text_output(omni_server, openai_client) -> None: @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_thinker_prefix_caching_audio_output(omni_server, openai_client) -> None: +def test_thinker_prefix_caching_audio_output(omni_server, online_client) -> None: """ Verify that thinker prefix caching does not hang when the request produces audio output (text + audio modalities). Sends two identical @@ -239,7 +242,7 @@ def test_thinker_prefix_caching_audio_output(omni_server, openai_client) -> None }, } - _run_prefix_cache_check(openai_client, request_config) + _run_prefix_cache_check(online_client, request_config) @pytest.mark.advanced_model @@ -247,13 +250,13 @@ def test_thinker_prefix_caching_audio_output(omni_server, openai_client) -> None @pytest.mark.omni @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_completions_rejected_for_thinker_talker(omni_server, openai_client) -> None: +def test_completions_rejected_for_thinker_talker(omni_server, online_client) -> None: """Ensure Thinker-talker models reject /v1/completions; we do this because the thinker-talker handoff implementations currently use ChatML <|im_start|> and <|im_end|> markers to segment the input sequence; when we don't have them, the talker does not get any embeddings, which breaks the server. """ - responses = openai_client.send_completions_http_request( + responses = online_client.send_completions_http_request( { "json": { "model": omni_server.model, diff --git a/tests/e2e/online_serving/test_qwen3_omni_expansion.py b/tests/e2e/online_serving/test_qwen3_omni_expansion.py index 99066468d..12a65670e 100644 --- a/tests/e2e/online_serving/test_qwen3_omni_expansion.py +++ b/tests/e2e/online_serving/test_qwen3_omni_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Qwen3-Omni model. """ @@ -139,7 +139,7 @@ def get_max_batch_size(size_type="few"): @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_audio_001(omni_server, openai_client) -> None: +def test_text_to_audio_001(omni_server, online_client) -> None: """ Input Modal: text Output Modal: audio @@ -156,12 +156,12 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_text_to_text_audio_001(omni_server, openai_client) -> None: +def test_text_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text Output Modal: text, audio @@ -176,12 +176,12 @@ def test_text_to_text_audio_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_video_to_text_001(omni_server, openai_client) -> None: +def test_text_video_to_text_001(omni_server, online_client) -> None: """ Input Modal: long synthetic video (120s @ 30fps, LONG_VIDEO_FRAMES frames) Output Modal: text @@ -200,12 +200,12 @@ def test_text_video_to_text_001(omni_server, openai_client) -> None: "key_words": {"video": VIDEO_KEY}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_text_audio_to_text_audio_001(omni_server, openai_client) -> None: +def test_text_audio_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text, audio Output Modal: text, audio @@ -223,12 +223,12 @@ def test_text_audio_to_text_audio_001(omni_server, openai_client) -> None: "key_words": {"audio": AUDIO_KEY}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_text_audio_to_text_audio_002(omni_server, openai_client) -> None: +def test_text_audio_to_text_audio_002(omni_server, online_client) -> None: """ Input Modal: text, long-duration audio (~LONG_AUDIO_DURATION_SEC s WAV) Output Modal: text, audio @@ -248,12 +248,12 @@ def test_text_audio_to_text_audio_002(omni_server, openai_client) -> None: "key_words": {"audio": AUDIO_KEY}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_text_image_to_text_audio_001(omni_server, openai_client) -> None: +def test_text_image_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text, image Output Modal: text, audio @@ -272,12 +272,12 @@ def test_text_image_to_text_audio_001(omni_server, openai_client) -> None: "key_words": {"image": IMAGE_KEY}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_large_image_to_text_audio_001(omni_server, openai_client) -> None: +def test_large_image_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text, high-resolution image (1080p-class JPEG) Output Modal: text, audio @@ -300,12 +300,12 @@ def test_large_image_to_text_audio_001(omni_server, openai_client) -> None: "key_words": {"image": IMAGE_KEY}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_text_video_to_text_audio_001(omni_server, openai_client) -> None: +def test_text_video_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text, video Output Modal: text, audio @@ -325,13 +325,13 @@ def test_text_video_to_text_audio_001(omni_server, openai_client) -> None: "key_words": {"video": VIDEO_KEY}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @pytest.mark.skip(reason="There is a known issue with shape mismatch error.") @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params + test_token_params, indirect=True) -def test_mix_to_text_audio_001(omni_server, openai_client) -> None: +def test_mix_to_text_audio_001(omni_server, online_client) -> None: """ Input Modal: text, audio, image, video Output Modal: text, audio @@ -355,12 +355,12 @@ def test_mix_to_text_audio_001(omni_server, openai_client) -> None: "stream": True, "key_words": {"audio": AUDIO_KEY, "image": IMAGE_KEY, "video": VIDEO_KEY}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_audio_in_video_001(omni_server, openai_client) -> None: +def test_audio_in_video_001(omni_server, online_client) -> None: """ Input Modal: text + video (synthetic MP4 with embedded audio; ``use_audio_in_video`` uses audio from the video). Output Modal: text, audio @@ -381,12 +381,12 @@ def test_audio_in_video_001(omni_server, openai_client) -> None: "use_audio_in_video": True, "key_words": {"video": VIDEO_KEY}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_audio_in_video_002(omni_server, openai_client) -> None: +def test_audio_in_video_002(omni_server, online_client) -> None: """ Input Modal: text + video (synthetic MP4 with embedded audio; ``use_audio_in_video`` uses audio from the video). Output Modal: text, audio @@ -408,12 +408,12 @@ def test_audio_in_video_002(omni_server, openai_client) -> None: "key_words": {"video": VIDEO_KEY}, } - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_audio_in_video_default_loader_sampling_regression(omni_server, openai_client) -> None: +def test_audio_in_video_default_loader_sampling_regression(omni_server, online_client) -> None: """ Regression: ``use_audio_in_video`` with default video loader sampling (no ``media_io_kwargs.video.fps``) after a prior request that pinned fps. @@ -438,7 +438,7 @@ def test_audio_in_video_default_loader_sampling_regression(omni_server, openai_c } # Prime mm-cache with explicit loader fps (historical failure mode). - openai_client.send_omni_request( + online_client.send_omni_request( { **base_config, "extra_body": {"media_io_kwargs": {"video": {"fps": 1, "num_frames": 128}}}, @@ -446,12 +446,12 @@ def test_audio_in_video_default_loader_sampling_regression(omni_server, openai_c ) # Default loader sampling: must succeed without explicit media_io fps. - openai_client.send_omni_request(dict(base_config)) + online_client.send_omni_request(dict(base_config)) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_one_word_prompt_001(omni_server, openai_client) -> None: +def test_one_word_prompt_001(omni_server, online_client) -> None: """Catastrophic-regression gate on one-word pronunciation. Whether a one-word answer is pronounced intelligibly is sampled, not guaranteed: the @@ -498,14 +498,14 @@ def test_one_word_prompt_001(omni_server, openai_client) -> None: # concurrently where the previous request_num=5 path judged serially -- worth watching # on tighter cards, since each judge loads whisper on the same device as the talker and # code2wav stages. - openai_client.send_omni_request( + online_client.send_omni_request( request_config, request_num=40, min_successes=29, max_concurrency=get_max_batch_size() ) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_speaker_001(omni_server, openai_client) -> None: +def test_speaker_001(omni_server, online_client) -> None: """ Input Modal: text only (one-word answer constraint). Output Modal: text, audio (default ``modalities``); ``key_words`` only assert on text. @@ -525,12 +525,12 @@ def test_speaker_001(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_speaker_002(omni_server, openai_client) -> None: +def test_speaker_002(omni_server, online_client) -> None: """ Input Modal: text only (one-word answer constraint). Output Modal: text, audio (default ``modalities``); ``key_words`` only assert on text. @@ -555,7 +555,7 @@ def test_speaker_002(omni_server, openai_client) -> None: _max_retries = 10 for attempt in range(_max_retries): try: - openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + online_client.send_omni_request(request_config, request_num=get_max_batch_size()) break except AssertionError as e: if _gender_assert_substr not in str(e) or attempt == _max_retries - 1: @@ -565,7 +565,7 @@ def test_speaker_002(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_speaker_003(omni_server, openai_client) -> None: +def test_speaker_003(omni_server, online_client) -> None: """ Input Modal: text only (one-word answer constraint). Output Modal: text, audio (default ``modalities``); ``key_words`` only assert on text. @@ -585,12 +585,12 @@ def test_speaker_003(omni_server, openai_client) -> None: "key_words": {"text": ["beijing"]}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_language_001(omni_server, openai_client) -> None: +def test_language_001(omni_server, online_client) -> None: """ Input Modal: text only (one-word answer constraint). Output Modal: text, audio (default ``modalities``); ``key_words`` only assert on text. @@ -609,12 +609,12 @@ def test_language_001(omni_server, openai_client) -> None: "key_words": {"text": ["北京"]}, } - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_audio_long_output_001(omni_server, openai_client) -> None: +def test_text_to_audio_long_output_001(omni_server, online_client) -> None: """ Input Modal: text only (long-form generation prompt). Output Modal: text, audio (default ``modalities``); @@ -627,7 +627,7 @@ def test_text_to_audio_long_output_001(omni_server, openai_client) -> None: ) request_config = {"model": omni_server.model, "messages": messages, "stream": True} - responses = openai_client.send_omni_request(request_config, request_num=get_max_batch_size()) + responses = online_client.send_omni_request(request_config, request_num=get_max_batch_size()) text = responses[0].text_content if responses else "" word_count = len(text.split()) assert word_count >= 200, f"Expected at least 200 words in long output, got {word_count}" diff --git a/tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py b/tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py index f269836f3..1a27a5449 100644 --- a/tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py +++ b/tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Core-model CI guard for Qwen3-Omni multi-replica stage-pool routing on 4 GPUs. """ @@ -90,7 +93,7 @@ def _assert_audio_outputs(responses: list[OmniResponse], *, expect_text: bool) - @pytest.mark.omni @hardware_test(res={"cuda": "H100"}, num_cards=4) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_only_batch_uses_multi_replica_talker(omni_server, openai_client) -> None: +def test_text_only_batch_uses_multi_replica_talker(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _text_messages(), @@ -98,7 +101,7 @@ def test_text_only_batch_uses_multi_replica_talker(omni_server, openai_client) - "modalities": ["text"], } - responses = openai_client.send_omni_request(request_config, request_num=ROUTE_STRESS_REQUESTS) + responses = online_client.send_omni_request(request_config, request_num=ROUTE_STRESS_REQUESTS) _assert_batch_size(responses, ROUTE_STRESS_REQUESTS) _assert_text_outputs(responses) @@ -107,7 +110,7 @@ def test_text_only_batch_uses_multi_replica_talker(omni_server, openai_client) - @pytest.mark.omni @hardware_test(res={"cuda": "H100"}, num_cards=4) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_text_to_audio_stream_batch_uses_multi_replica_vocoder(omni_server, openai_client) -> None: +def test_text_to_audio_stream_batch_uses_multi_replica_vocoder(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _text_messages(), @@ -115,7 +118,7 @@ def test_text_to_audio_stream_batch_uses_multi_replica_vocoder(omni_server, open "modalities": ["audio"], } - responses = openai_client.send_omni_request(request_config, request_num=ROUTE_STRESS_REQUESTS) + responses = online_client.send_omni_request(request_config, request_num=ROUTE_STRESS_REQUESTS) _assert_batch_size(responses, ROUTE_STRESS_REQUESTS) _assert_audio_outputs(responses, expect_text=False) @@ -124,13 +127,13 @@ def test_text_to_audio_stream_batch_uses_multi_replica_vocoder(omni_server, open @pytest.mark.omni @hardware_test(res={"cuda": "H100"}, num_cards=4) @pytest.mark.parametrize("omni_server", test_params, indirect=True) -def test_mixed_modal_stream_batch_generates_text_and_audio(omni_server, openai_client) -> None: +def test_mixed_modal_stream_batch_generates_text_and_audio(omni_server, online_client) -> None: request_config = { "model": omni_server.model, "messages": _mixed_messages(), "stream": True, } - responses = openai_client.send_omni_request(request_config, request_num=MIXED_MODAL_REQUESTS) + responses = online_client.send_omni_request(request_config, request_num=MIXED_MODAL_REQUESTS) _assert_batch_size(responses, MIXED_MODAL_REQUESTS) _assert_audio_outputs(responses, expect_text=True) diff --git a/tests/e2e/online_serving/test_qwen3_tts_base.py b/tests/e2e/online_serving/test_qwen3_tts_base.py index 60caf8f82..85f3600e0 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_base.py +++ b/tests/e2e/online_serving/test_qwen3_tts_base.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Qwen3-TTS model with text input and audio output. @@ -14,7 +14,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -24,7 +24,7 @@ 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 # regions cannot reach the original Aliyun OSS host; see issue #3263). -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -59,7 +59,7 @@ tts_server_params = [ @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: +def test_text_to_audio_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -80,14 +80,14 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few")) + online_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few")) @pytest.mark.advanced_model @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: +def test_text_to_audio_002(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -108,4 +108,4 @@ def test_text_to_audio_002(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py index 0edaa4668..e126877db 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py +++ b/tests/e2e/online_serving/test_qwen3_tts_base_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Qwen3-TTS model with text input and audio output. @@ -12,7 +12,7 @@ import os import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -23,7 +23,7 @@ os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" # See tests/e2e/online_serving/test_qwen3_tts_base.py for the vendored-asset rationale. -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) REF_TEXT = "Okay. Yeah. I resent you. I love you. I respect you. But you know what? You blew it! And thanks to you." @@ -41,7 +41,7 @@ def get_max_batch_size(size_type="few"): return batch_sizes.get(size_type, 5) -tts_server_params = [ +tts_async_chunk_server_params = [ pytest.param( OmniServerParams( model=MODEL, @@ -50,9 +50,12 @@ tts_server_params = [ ), id="async_chunk", ), - # Synchronous (no async-chunk) variant — ``--no-async-chunk`` alone - # flips the deploy yaml's bool and the pipeline dispatches to the - # end-to-end codec processor. No variant yaml / pipeline needed. +] + +# Synchronous (no async-chunk) variant — ``--no-async-chunk`` alone +# flips the deploy yaml's bool and the pipeline dispatches to the +# end-to-end codec processor. No variant yaml / pipeline needed. +tts_no_async_chunk_server_params = [ pytest.param( OmniServerParams( model=MODEL, @@ -63,10 +66,16 @@ tts_server_params = [ ), ] +DEFAULT_AUDIO_SPEECH_TIMEOUT_S = 180.0 + @hardware_test(res={"cuda": "L4"}, num_cards=1) -@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_clone_streaming_001(omni_server, openai_client) -> None: +@pytest.mark.parametrize( + "omni_server", + tts_async_chunk_server_params + tts_no_async_chunk_server_params, + indirect=True, +) +def test_voice_clone_streaming_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -87,12 +96,16 @@ def test_voice_clone_streaming_001(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few")) + online_client.send_audio_speech_request(request_config, request_num=get_max_batch_size("few")) @hardware_test(res={"cuda": "L4"}, num_cards=1) -@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_response_format_001(omni_server, openai_client) -> None: +@pytest.mark.parametrize( + "omni_server", + tts_async_chunk_server_params + tts_no_async_chunk_server_params, + indirect=True, +) +def test_response_format_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -111,4 +124,31 @@ def test_response_format_001(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "ref_text": REF_TEXT, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) + + +@hardware_test(res={"cuda": "L4"}, num_cards=1) +@pytest.mark.parametrize("omni_server", tts_async_chunk_server_params, indirect=True) +def test_xvector_then_icl_same_ref_audio_keeps_engine_alive(omni_server, online_client) -> None: + """Regression for #5049: x-vector-only then ICL on the same ``ref_audio``. + + Pre-fix, readiness was mode-agnostic: an x-vector request marked the + artifact ready (embedding only, no ``ref_code``), so the following ICL + request took the artifact-only path and killed EngineCore. After the fix + (readiness keyed by ``(artifact_key, x_vector_only)``), both modes succeed + and the server stays up. Only runs on the ``async_chunk`` server param. + """ + base_request = { + "model": omni_server.model, + "input": get_prompt(), + "stream": False, + "timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S, + "response_format": "wav", + "task_type": "Base", + "ref_audio": REF_AUDIO_URL, + "min_audio_bytes": 1, + } + + online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) + online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": False, "ref_text": REF_TEXT}) + online_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) diff --git a/tests/e2e/online_serving/test_qwen3_tts_customvoice.py b/tests/e2e/online_serving/test_qwen3_tts_customvoice.py index c01507bb7..6192f8410 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_customvoice.py +++ b/tests/e2e/online_serving/test_qwen3_tts_customvoice.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Qwen3-TTS model with text input and audio output. @@ -93,7 +93,7 @@ def test_default_cuda_graph_startup(omni_server) -> None: @pytest.mark.tts @hardware_test(res={"cuda": "L4", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_text_to_audio_001(omni_server, openai_client) -> None: +def test_text_to_audio_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -111,7 +111,7 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: "voice": "vivian", } - openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size()) + online_client.send_audio_speech_request(request_config, request_num=get_max_batch_size()) @pytest.mark.core_model @@ -119,7 +119,7 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: @pytest.mark.tts @hardware_test(res={"cuda": "L4", "npu": "A3"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_text_to_audio_002(omni_server, openai_client) -> None: +def test_text_to_audio_002(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -138,4 +138,4 @@ def test_text_to_audio_002(omni_server, openai_client) -> None: "voice": "vivian", } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen3_tts_customvoice_expansion.py b/tests/e2e/online_serving/test_qwen3_tts_customvoice_expansion.py index 9c941cad8..07808af27 100644 --- a/tests/e2e/online_serving/test_qwen3_tts_customvoice_expansion.py +++ b/tests/e2e/online_serving/test_qwen3_tts_customvoice_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for Qwen3-TTS model with text input and audio output. @@ -62,7 +62,7 @@ tts_server_params = [ @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_001(omni_server, openai_client) -> None: +def test_voice_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -84,7 +84,7 @@ def test_voice_001(omni_server, openai_client) -> None: # Retry once on assertion failures from transcript similarity / gender checks (flaky ASR or estimators). for attempt in range(2): try: - openai_client.send_audio_speech_request(request_config, request_num=get_max_batch_size()) + online_client.send_audio_speech_request(request_config, request_num=get_max_batch_size()) break except AssertionError: if attempt == 0: @@ -94,7 +94,7 @@ def test_voice_001(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_002(omni_server, openai_client) -> None: +def test_voice_002(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -112,12 +112,12 @@ def test_voice_002(omni_server, openai_client) -> None: "voice": "Serena", } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_003(omni_server, openai_client) -> None: +def test_voice_003(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -135,12 +135,12 @@ def test_voice_003(omni_server, openai_client) -> None: "voice": "SERENA", } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_language_001(omni_server, openai_client) -> None: +def test_language_001(omni_server, online_client) -> None: """ Test text input processing and audio output via OpenAI API. Deploy Setting: default yaml @@ -158,4 +158,4 @@ def test_language_001(omni_server, openai_client) -> None: "voice": "vivian", } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen3_tts_ref_audio_artifact_mode_e2e.py b/tests/e2e/online_serving/test_qwen3_tts_ref_audio_artifact_mode_e2e.py deleted file mode 100644 index e6c396932..000000000 --- a/tests/e2e/online_serving/test_qwen3_tts_ref_audio_artifact_mode_e2e.py +++ /dev/null @@ -1,88 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project -"""E2E regression for Qwen3-TTS ref_audio artifact mode collision (#5049). - -Reproduces the issue's exact sequence on a live server: an -``x_vector_only_mode=true`` request followed by an ICL -(``x_vector_only_mode=false`` + ``ref_text``) request using the **same** -``ref_audio``. - -Before the fix, the ref_audio artifact readiness was mode-agnostic: the -x-vector-only request (which caches a speaker embedding but no ``ref_code``) -marked the artifact "ready", so the ICL request took the artifact-only serving -path (``ref_audio`` stripped before dispatch), found no ``ref_code`` and no -audio to recompute from, and raised in ``build_prompt_embeds`` -> -``EngineDeadError`` — taking the whole server down rather than failing the one -request. - -After the fix (readiness keyed by ``(artifact_key, x_vector_only)``), the ICL -request no longer matches the x-vector artifact, so it sends ``ref_audio`` and -the worker computes ``ref_code``. Both requests succeed and the server stays up. -""" - -import os - -os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" - -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 - -MODEL = "Qwen/Qwen3-TTS-12Hz-0.6B-Base" -DEFAULT_AUDIO_SPEECH_TIMEOUT_S = 180.0 - -# Vendored under tests/assets/qwen3_tts/clone_2.wav (see test_qwen3_tts_base.py). -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." -INPUT_TEXT = "The weather is nice today, perfect for a walk in the park." - -tts_server_params = [ - pytest.param( - OmniServerParams( - model=MODEL, - stage_config_path=get_deploy_config_path("qwen3_tts.yaml"), - server_args=["--trust-remote-code"], - ), - id="async_chunk", - ) -] - - -@pytest.mark.advanced_model -@pytest.mark.core_model -@pytest.mark.tts -@hardware_test(res={"cuda": "L4"}, num_cards=1) -@pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_xvector_then_icl_same_ref_audio_keeps_engine_alive(omni_server, openai_client) -> None: - """Regression for #5049: x-vector-only then ICL on the same ref_audio. - - Both requests must return audio and the engine must stay alive (pre-fix the - second request killed EngineCore). - """ - base_request = { - "model": omni_server.model, - "input": INPUT_TEXT, - "stream": False, - "timeout": DEFAULT_AUDIO_SPEECH_TIMEOUT_S, - "response_format": "wav", - "task_type": "Base", - "ref_audio": REF_AUDIO_URL, - # Assert non-empty audio came back (not just HTTP 200). - "min_audio_bytes": 1, - } - - # 1) x-vector-only request: caches a speaker embedding (no ref_code) and, on - # completion, marks the ref_audio artifact "ready". - openai_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) - - # 2) ICL request with the SAME ref_audio. Pre-fix this reused the x-vector - # artifact via the artifact-only path, hit the missing ref_code, and - # killed EngineCore (#5049). It must now succeed with audio. - openai_client.send_audio_speech_request({**base_request, "x_vector_only_mode": False, "ref_text": REF_TEXT}) - - # 3) The server must still be serving after the ICL request (pre-fix this - # would fail with EngineDeadError / connection refused). - openai_client.send_audio_speech_request({**base_request, "x_vector_only_mode": True}) diff --git a/tests/e2e/online_serving/test_qwen_image.py b/tests/e2e/online_serving/test_qwen_image.py index c40cfbda8..aa037d9b8 100644 --- a/tests/e2e/online_serving/test_qwen_image.py +++ b/tests/e2e/online_serving/test_qwen_image.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Online serving tests for ``Qwen/Qwen-Image`` (text-to-image). @@ -24,7 +24,7 @@ import os import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" @@ -61,7 +61,7 @@ def _get_default_case(model: str): @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: +def test_text_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: """Default Qwen-Image T2I smoke (single ``default`` server config).""" messages = dummy_messages_from_mix_data(content_text=T2I_PROMPT) request_config = { @@ -76,14 +76,14 @@ def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientH "seed": 42, }, } - openai_client.send_diffusion_request(request_config) + online_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: +def test_batch_001(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: """Concurrent T2I: one ``request_config`` dict per prompt (``send_diffusion_request`` list mode).""" request_config = [ { @@ -100,4 +100,4 @@ def test_batch_001(omni_server: OmniServer, openai_client: OpenAIClientHandler) } for prompt in TEST_PROMPTS ] - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen_image_edit.py b/tests/e2e/online_serving/test_qwen_image_edit.py index 14d27318c..dc1f60521 100644 --- a/tests/e2e/online_serving/test_qwen_image_edit.py +++ b/tests/e2e/online_serving/test_qwen_image_edit.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Online serving tests for ``Qwen/Qwen-Image-Edit-2511`` (image-to-image via chat completions). @@ -8,7 +8,7 @@ Online serving tests for ``Qwen/Qwen-Image-Edit-2511`` (image-to-image via chat - ``test_multi_images_to_image_001``: two reference images, fixed 512×512. - ``test_different_sizes_001``: ``slow`` — 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``). + per list index in parallel and merges images (see ``OnlineOmniClient.send_diffusion_request``). ``_get_diffusion_feature_cases`` registers a single ``default`` ``OmniServerParams`` row. @@ -24,7 +24,7 @@ from tests.helpers.media import generate_synthetic_image from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -59,7 +59,7 @@ def _get_diffusion_feature_cases(model: str = MODEL): _get_diffusion_feature_cases(), indirect=True, ) -def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_single_image_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient): """Single-reference edit smoke for ``Qwen/Qwen-Image-Edit-2511`` (CFG when negative prompt + ``true_cfg_scale`` > 1).""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -79,7 +79,7 @@ def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenA }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.slow @@ -89,7 +89,7 @@ def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenA _get_diffusion_feature_cases(), indirect=True, ) -def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_multi_images_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient): """Two-reference edit smoke for ``Qwen/Qwen-Image-Edit-2511``.""" image_data_url_list = [f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" for _ in range(2)] @@ -110,7 +110,7 @@ def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenA }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.slow @@ -120,7 +120,7 @@ def test_multi_images_to_image_001(omni_server: OmniServer, openai_client: OpenA _get_diffusion_feature_cases(), indirect=True, ) -def test_different_sizes_001(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_different_sizes_001(omni_server: OmniServer, online_client: OnlineOmniClient): """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']}", @@ -143,4 +143,4 @@ def test_different_sizes_001(omni_server: OmniServer, openai_client: OpenAIClien }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen_image_edit_expansion.py b/tests/e2e/online_serving/test_qwen_image_edit_expansion.py index e5e8c6c2b..47bc2809b 100644 --- a/tests/e2e/online_serving/test_qwen_image_edit_expansion.py +++ b/tests/e2e/online_serving/test_qwen_image_edit_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by Qwen-Image-Edit-2511 (single image input and two image inputs). @@ -7,7 +10,7 @@ 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 +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -122,7 +125,7 @@ def _get_diffusion_feature_cases(model: str = MODEL): _get_diffusion_feature_cases(), indirect=True, ) -def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, online_client: OnlineOmniClient): """Test Qwen-Image-Edit-2511 with a single image input. Regression: with tea_cache enabled and zero_cond_t=True, the TeaCache @@ -149,7 +152,7 @@ def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_clien }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -157,7 +160,7 @@ def test_qwen_image_edit_2511_single_image(omni_server: OmniServer, openai_clien _get_diffusion_feature_cases(), indirect=True, ) -def test_qwen_image_edit_2511_two_images(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image_edit_2511_two_images(omni_server: OmniServer, online_client: OnlineOmniClient): """Test Qwen-Image-Edit-2511 with two image inputs.""" image_data_url_1 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" image_data_url_2 = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -180,4 +183,4 @@ def test_qwen_image_edit_2511_two_images(omni_server: OmniServer, openai_client: }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen_image_expansion.py b/tests/e2e/online_serving/test_qwen_image_expansion.py index c900ca4ce..a5a489ad8 100644 --- a/tests/e2e/online_serving/test_qwen_image_expansion.py +++ b/tests/e2e/online_serving/test_qwen_image_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by the following text-to-image models: @@ -13,7 +16,7 @@ See docs/user_guide/diffusion_acceleration.md. import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.full_model] @@ -132,7 +135,7 @@ def _get_diffusion_feature_cases(model: str): _get_diffusion_feature_cases("Qwen/Qwen-Image"), indirect=True, ) -def test_qwen_image(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image(omni_server: OmniServer, online_client: OnlineOmniClient): """Test each diffusion feature with Qwen-Image (text-to-image), one feature per case.""" messages = dummy_messages_from_mix_data(content_text=T2I_PROMPT) request_config = { @@ -147,7 +150,7 @@ def test_qwen_image(omni_server: OmniServer, openai_client: OpenAIClientHandler) "seed": 42, }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.parametrize( @@ -155,7 +158,7 @@ def test_qwen_image(omni_server: OmniServer, openai_client: OpenAIClientHandler) _get_diffusion_feature_cases("Qwen/Qwen-Image-2512"), indirect=True, ) -def test_qwen_image_2512(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_qwen_image_2512(omni_server: OmniServer, online_client: OnlineOmniClient): """Test each diffusion feature with Qwen-Image-2512 (text-to-image), one feature per case.""" messages = dummy_messages_from_mix_data(content_text=T2I_PROMPT) request_config = { @@ -170,4 +173,4 @@ def test_qwen_image_2512(omni_server: OmniServer, openai_client: OpenAIClientHan "seed": 42, }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen_image_layered.py b/tests/e2e/online_serving/test_qwen_image_layered.py index 289e35764..298f4ca9e 100644 --- a/tests/e2e/online_serving/test_qwen_image_layered.py +++ b/tests/e2e/online_serving/test_qwen_image_layered.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Online serving tests for ``Qwen/Qwen-Image-Layered`` (layer decomposition). @@ -18,7 +18,7 @@ from tests.helpers.media import generate_synthetic_image from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -47,7 +47,7 @@ def _get_diffusion_feature_cases(model: str): @pytest.mark.slow @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): +def test_single_image_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient): """Default Qwen-Image-Layered smoke (single ``default`` server config).""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -64,4 +64,4 @@ def test_single_image_to_image_001(omni_server: OmniServer, openai_client: OpenA }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_qwen_image_layered_expansion.py b/tests/e2e/online_serving/test_qwen_image_layered_expansion.py index 8c5001a4a..bc36c9fa8 100644 --- a/tests/e2e/online_serving/test_qwen_image_layered_expansion.py +++ b/tests/e2e/online_serving/test_qwen_image_layered_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Comprehensive tests of diffusion features that are available in online serving mode and are supported by Qwen-Image-Layered model. @@ -12,11 +12,16 @@ Kept cases (maximum feature coverage, no cpu-offload): Total distinct features covered: cache_dit, Ulysses-SP, CFG-Parallel, layerwise-offload. """ +import base64 +import io +from typing import Any + import pytest +from PIL import Image from tests.helpers.mark import hardware_marks -from tests.helpers.media import decode_b64_image, generate_synthetic_image -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.media import generate_synthetic_image +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -82,7 +87,7 @@ FEATURE_CASES = [ @pytest.mark.parametrize("omni_server", FEATURE_CASES, indirect=True) -def test_feature(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_feature(omni_server: OmniServer, online_client: OnlineOmniClient): """Test feature combinations with Qwen-Image-Layered.""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -99,7 +104,7 @@ def test_feature(omni_server: OmniServer, openai_client: OpenAIClientHandler): }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) # ── Guard: Issue #1969 – layered output must return `layers` images ───────── @@ -123,19 +128,19 @@ LAYERS_GUARD_CASES = [ ] -def _collect_image_url_items(openai_client: OpenAIClientHandler, request_config: dict): +def _collect_image_url_items(online_client: OnlineOmniClient, request_config: dict): """Send a request and return all image_url content items from the API response. Handles both object-style (Pydantic) and dict-style (raw JSON) content items, because the OpenAI SDK may return either depending on how the server serializes the multimodal content list. """ - chat_completion = openai_client.client.chat.completions.create( + chat_completion = online_client.client.chat.completions.create( model=request_config["model"], messages=request_config["messages"], extra_body=request_config.get("extra_body"), ) - image_items = [] + image_items: list[Any] = [] for choice in chat_completion.choices: content = getattr(choice.message, "content", None) assert content is not None, "API response content is None" @@ -164,7 +169,7 @@ def _collect_image_url_items(openai_client: OpenAIClientHandler, request_config: ) def test_layered_output_image_count( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, expected_layers: int, ): """Guard for https://github.com/vllm-project/vllm-omni/issues/1969 @@ -189,7 +194,7 @@ def test_layered_output_image_count( }, } - image_items = _collect_image_url_items(openai_client, request_config) + image_items = _collect_image_url_items(online_client, request_config) # Core assertion: the API must NOT collapse multiple layers into 1 image assert len(image_items) != 1 or expected_layers == 1, ( @@ -209,7 +214,8 @@ def test_layered_output_image_count( url = item.image_url.url assert url.startswith("data:image"), f"image_url item {i} is not a data URI: {url[:80]}" b64 = url.split(",", 1)[1] - img = decode_b64_image(b64) + img = Image.open(io.BytesIO(base64.b64decode(b64))) + img.load() assert img is not None, f"Failed to decode image at index {i}" @@ -231,7 +237,7 @@ PROMPT_CASES = [ @pytest.mark.parametrize("omni_server", PROMPT_CASES, indirect=True) -def test_empty_prompt(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_empty_prompt(omni_server: OmniServer, online_client: OnlineOmniClient): """Test feature combinations with Qwen-Image-Layered.""" image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" @@ -248,4 +254,4 @@ def test_empty_prompt(omni_server: OmniServer, openai_client: OpenAIClientHandle }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_sd3_expansion.py b/tests/e2e/online_serving/test_sd3_expansion.py index 412dad857..9793d8a4b 100644 --- a/tests/e2e/online_serving/test_sd3_expansion.py +++ b/tests/e2e/online_serving/test_sd3_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Tests for Stable Diffusion 3.5 medium model. """ @@ -8,7 +11,7 @@ from tests.helpers.mark import hardware_marks from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -63,7 +66,7 @@ def _get_diffusion_feature_cases(model: str): ), indirect=True, ) -def test_sd3_medium(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_sd3_medium(omni_server: OmniServer, online_client: OnlineOmniClient): messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT) request_config = { "model": omni_server.model, @@ -78,4 +81,4 @@ def test_sd3_medium(omni_server: OmniServer, openai_client: OpenAIClientHandler) }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_sdxl.py b/tests/e2e/online_serving/test_sdxl.py index 4d7fc34d5..052d9e57e 100644 --- a/tests/e2e/online_serving/test_sdxl.py +++ b/tests/e2e/online_serving/test_sdxl.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Tests for Stable Diffusion XL Base 1.0 model. @@ -10,7 +13,7 @@ from tests.helpers.mark import hardware_marks from tests.helpers.runtime import ( OmniServer, OmniServerParams, - OpenAIClientHandler, + OnlineOmniClient, dummy_messages_from_mix_data, ) @@ -41,7 +44,7 @@ def _get_diffusion_feature_cases(model: str): ), indirect=True, ) -def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_text_to_image_001(omni_server: OmniServer, online_client: OnlineOmniClient): messages = dummy_messages_from_mix_data(content_text=POSITIVE_PROMPT) request_config = { "model": omni_server.model, @@ -56,4 +59,4 @@ def test_text_to_image_001(omni_server: OmniServer, openai_client: OpenAIClientH }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_step_audio2_expansion.py b/tests/e2e/online_serving/test_step_audio2_expansion.py index 9c8280b59..5542d81df 100644 --- a/tests/e2e/online_serving/test_step_audio2_expansion.py +++ b/tests/e2e/online_serving/test_step_audio2_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E expansion tests for Step-Audio2 online serving (weekly CI). @@ -94,22 +94,22 @@ def _build_audio_to_text_request_config(omni_server, audio_base64: str) -> dict: @hardware_test(res={"cuda": "H100"}, num_cards=1) -def test_single_audio_to_text_request(omni_server, openai_client) -> None: +def test_single_audio_to_text_request(omni_server, online_client) -> None: """Test a single audio-to-text request via the chat completions API.""" request_config = _build_audio_to_text_request_config( omni_server, _synthetic_audio_base64(duration_sec=2), ) - openai_client.send_omni_request(request_config) + online_client.send_omni_request(request_config) @hardware_test(res={"cuda": "H100"}, num_cards=1) @pytest.mark.parametrize("request_num", [2, 4]) -def test_concurrent_audio_to_text_requests(omni_server, openai_client, request_num: int) -> None: +def test_concurrent_audio_to_text_requests(omni_server, online_client, request_num: int) -> None: """Test concurrent audio-to-text requests.""" request_config = _build_audio_to_text_request_config( omni_server, _synthetic_audio_base64(duration_sec=2), ) - responses = openai_client.send_omni_request(request_config, request_num=request_num) + responses = online_client.send_omni_request(request_config, request_num=request_num) assert len(responses) == request_num diff --git a/tests/e2e/online_serving/test_voxcpm2_tts.py b/tests/e2e/online_serving/test_voxcpm2_tts.py index 5597bf367..0d0364138 100644 --- a/tests/e2e/online_serving/test_voxcpm2_tts.py +++ b/tests/e2e/online_serving/test_voxcpm2_tts.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online tests for VoxCPM2 with text input and audio output. @@ -50,7 +50,7 @@ tts_server_params = [ @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: +def test_text_to_audio_001(omni_server, online_client) -> None: """ Test zero-shot text-to-audio via OpenAI API. Deploy Setting: voxcpm2.yaml @@ -68,14 +68,14 @@ def test_text_to_audio_001(omni_server, openai_client) -> None: "voice": "default", "min_audio_bytes": _MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config, request_num=MAX_CONCURRENT) + online_client.send_audio_speech_request(request_config, request_num=MAX_CONCURRENT) @pytest.mark.slow @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: +def test_text_to_audio_002(omni_server, online_client) -> None: """ Test zero-shot streaming text-to-audio via OpenAI API. Deploy Setting: voxcpm2.yaml @@ -94,4 +94,4 @@ def test_text_to_audio_002(omni_server, openai_client) -> None: "voice": "default", "min_audio_bytes": _MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_voxcpm2_tts_expansion.py b/tests/e2e/online_serving/test_voxcpm2_tts_expansion.py index de73ae6da..d630c8627 100644 --- a/tests/e2e/online_serving/test_voxcpm2_tts_expansion.py +++ b/tests/e2e/online_serving/test_voxcpm2_tts_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E Online expansion tests for VoxCPM2 (voice clone, PCM, concurrency). @@ -12,7 +12,7 @@ import os import pytest from tests.helpers.mark import hardware_test -from tests.helpers.media import load_test_audio_data_url +from tests.helpers.media import get_asset_path from tests.helpers.runtime import OmniServerParams from tests.helpers.stage_config import get_deploy_config_path @@ -25,7 +25,7 @@ DEFAULT_AUDIO_SPEECH_TIMEOUT_S = 300.0 _MIN_AUDIO_BYTES = 40_000 MAX_CONCURRENT = 4 -REF_AUDIO_URL = load_test_audio_data_url("qwen3_tts/clone_2.wav") +REF_AUDIO_URL = get_asset_path("qwen3_tts/clone_2.wav", as_data_url=True) def get_prompt(prompt_type="text"): @@ -49,7 +49,7 @@ tts_server_params = [ @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_voice_clone_streaming_001(omni_server, openai_client) -> None: +def test_voice_clone_streaming_001(omni_server, online_client) -> None: """ Test voice-clone text-to-audio via OpenAI API. Deploy Setting: voxcpm2.yaml @@ -69,12 +69,12 @@ def test_voice_clone_streaming_001(omni_server, openai_client) -> None: "ref_audio": REF_AUDIO_URL, "min_audio_bytes": _MIN_AUDIO_BYTES, } - openai_client.send_audio_speech_request(request_config, request_num=MAX_CONCURRENT) + online_client.send_audio_speech_request(request_config, request_num=MAX_CONCURRENT) @hardware_test(res={"cuda": "L4"}, num_cards=1) @pytest.mark.parametrize("omni_server", tts_server_params, indirect=True) -def test_response_format_001(omni_server, openai_client) -> None: +def test_response_format_001(omni_server, online_client) -> None: """ Test voice-clone non-stream PCM output via OpenAI API. Deploy Setting: voxcpm2.yaml @@ -94,4 +94,4 @@ def test_response_format_001(omni_server, openai_client) -> None: "min_audio_bytes": _MIN_AUDIO_BYTES, "min_hnr_db": -2.0, } - openai_client.send_audio_speech_request(request_config) + online_client.send_audio_speech_request(request_config) diff --git a/tests/e2e/online_serving/test_voxtral_tts_expansion.py b/tests/e2e/online_serving/test_voxtral_tts_expansion.py index dc8db9d73..278e563e8 100644 --- a/tests/e2e/online_serving/test_voxtral_tts_expansion.py +++ b/tests/e2e/online_serving/test_voxtral_tts_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ E2E tests for Voxtral TTS online serving. @@ -31,9 +31,9 @@ pytestmark = [ @hardware_test(res={"cuda": "L4", "xpu": "B60"}, num_cards=1) -def test_speech_english_basic(omni_server, openai_client) -> None: +def test_speech_english_basic(omni_server, online_client) -> None: """Test basic English TTS generation.""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "how are you", @@ -47,9 +47,9 @@ def test_speech_english_basic(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4", "xpu": "B60"}, num_cards=1) -def test_speech_english_streaming(omni_server, openai_client) -> None: +def test_speech_english_streaming(omni_server, online_client) -> None: """Test basic streaming English TTS generation (PCM via streaming API).""" - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Hello, how are you?", @@ -65,11 +65,11 @@ def test_speech_english_streaming(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_speech_different_voices(omni_server, openai_client) -> None: +def test_speech_different_voices(omni_server, online_client) -> None: """Test TTS with different voice presets.""" voices = ["casual_female", "neutral_male"] for voice in voices: - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Testing voice selection.", @@ -82,11 +82,11 @@ def test_speech_different_voices(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_speech_speed(omni_server, openai_client) -> None: +def test_speech_speed(omni_server, online_client) -> None: """Request with speed parameters.""" speeds = [0.5, 1, 1.5, 2, 2.5] for speed in speeds: - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "The boy was there when the sun rose.", @@ -100,7 +100,7 @@ def test_speech_speed(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_speech_instructions(omni_server, openai_client) -> None: +def test_speech_instructions(omni_server, online_client) -> None: """Request with instructions parameters.""" instructions = [ "Speak formally", @@ -109,7 +109,7 @@ def test_speech_instructions(omni_server, openai_client) -> None: "Speak with a chirpy happy voice", ] for instruction in instructions: - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "The boy was there when the sun rose.", @@ -134,11 +134,11 @@ def test_speech_instructions(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_speech_response_formats(omni_server, openai_client) -> None: +def test_speech_response_formats(omni_server, online_client) -> None: """Test TTS with different response formats.""" response_formats = ["wav", "mp3"] for response_format in response_formats: - openai_client.send_audio_speech_request( + online_client.send_audio_speech_request( { "model": omni_server.model, "input": "Testing various response formats.", @@ -151,7 +151,7 @@ def test_speech_response_formats(omni_server, openai_client) -> None: @hardware_test(res={"cuda": "L4"}, num_cards=1) -def test_speech_batches(omni_server, openai_client) -> None: +def test_speech_batches(omni_server, online_client) -> None: """Test TTS batches.""" items = [ {"input": "The birch canoe slid on the smooth planks."}, @@ -161,7 +161,7 @@ def test_speech_batches(omni_server, openai_client) -> None: {"input": "Rice is often served in round bowls."}, ] - openai_client.send_audio_speech_batch_http_request( + online_client.send_audio_speech_batch_http_request( { "json": { "model": omni_server.model, diff --git a/tests/e2e/online_serving/test_wan22_expansion.py b/tests/e2e/online_serving/test_wan22_expansion.py index 85eee128a..322cde3cd 100644 --- a/tests/e2e/online_serving/test_wan22_expansion.py +++ b/tests/e2e/online_serving/test_wan22_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Comprehensive tests of diffusion features that are available in online serving mode. @@ -19,7 +22,7 @@ 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 +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.diffusion, pytest.mark.full_model] @@ -140,7 +143,7 @@ def _get_wan22_feature_cases(): ) def test_wan22_diffusion_features( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): model_path = omni_server.model is_i2v_or_ti2v = any(kw in model_path for kw in ["I2V", "TI2V"]) @@ -176,4 +179,4 @@ def test_wan22_diffusion_features( if is_i2v_or_ti2v: request_config["image_reference"] = f"data:image/jpeg;base64,{generate_synthetic_image(512, 512)['base64']}" - openai_client.send_video_diffusion_request(request_config) + online_client.send_video_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_wan22_t2v.py b/tests/e2e/online_serving/test_wan22_t2v.py index e6fe38086..f3f21ae7d 100644 --- a/tests/e2e/online_serving/test_wan22_t2v.py +++ b/tests/e2e/online_serving/test_wan22_t2v.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Online serving smoke for ``Wan-AI/Wan2.2-T2V-A14B-Diffusers`` (text-to-video via ``/v1/videos``). @@ -18,7 +18,7 @@ import os import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" @@ -44,7 +44,7 @@ def _get_diffusion_feature_cases(model: str): @pytest.mark.advanced_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) -> None: +def test_text_to_video_001(omni_server: OmniServer, online_client: OnlineOmniClient) -> None: """Default Wan2.2 T2V smoke: async ``/v1/videos`` job completes and returns video bytes.""" request_config = { "model": omni_server.model, @@ -60,4 +60,4 @@ def test_text_to_video_001(omni_server: OmniServer, openai_client: OpenAIClientH "seed": 42, }, } - openai_client.send_video_diffusion_request(request_config) + online_client.send_video_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_wan_2_1_vace_expansion.py b/tests/e2e/online_serving/test_wan_2_1_vace_expansion.py index 4c6380314..cf24e34b0 100644 --- a/tests/e2e/online_serving/test_wan_2_1_vace_expansion.py +++ b/tests/e2e/online_serving/test_wan_2_1_vace_expansion.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """ Comprehensive e2e tests of diffusion features for Wan2.1-VACE in online serving mode. @@ -24,7 +24,7 @@ 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, OnlineOmniClient pytestmark = [pytest.mark.diffusion, pytest.mark.slow] @@ -150,9 +150,9 @@ def _get_vace_feature_cases(): _get_vace_feature_cases(), indirect=True, ) -def test_wan_2_1_vace(omni_server: OmniServer, openai_client: OpenAIClientHandler): +def test_wan_2_1_vace(omni_server: OmniServer, online_client: OnlineOmniClient): """Test VACE T2V generation with all supported diffusion acceleration features.""" - openai_client.send_video_diffusion_request( + online_client.send_video_diffusion_request( { "model": MODEL, "form_data": { diff --git a/tests/e2e/online_serving/test_z_image.py b/tests/e2e/online_serving/test_z_image.py index b1180f514..7e3af1132 100644 --- a/tests/e2e/online_serving/test_z_image.py +++ b/tests/e2e/online_serving/test_z_image.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Tests of common diffusion feature combinations in online serving mode for Z-Image. @@ -11,7 +14,7 @@ exercise Z-Image's supported parallel feature combinations: import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data MODEL = "Tongyi-MAI/Z-Image-Turbo" PROMPT = "A high-detail studio photo of an orange tabby cat sitting on a laptop keyboard." @@ -41,7 +44,7 @@ def _get_diffusion_feature_cases(): ) def test_basic_001( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Exercise supported Z-Image diffusion features in minimal CI cases.""" messages = dummy_messages_from_mix_data(content_text=PROMPT) @@ -57,7 +60,7 @@ def test_basic_001( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) @pytest.mark.slow @@ -69,7 +72,7 @@ def test_basic_001( ) def test_different_sizes_001( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Exercise supported Z-Image diffusion features in minimal CI cases.""" messages = dummy_messages_from_mix_data(content_text=PROMPT) @@ -85,4 +88,4 @@ def test_different_sizes_001( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/e2e/online_serving/test_zimage_expansion.py b/tests/e2e/online_serving/test_zimage_expansion.py index ac27267e5..c37a947f4 100644 --- a/tests/e2e/online_serving/test_zimage_expansion.py +++ b/tests/e2e/online_serving/test_zimage_expansion.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Tests of common diffusion feature combinations in online serving mode for Z-Image. @@ -14,7 +17,7 @@ exercise Z-Image's supported feature combinations: import pytest from tests.helpers.mark import hardware_marks -from tests.helpers.runtime import OmniServer, OmniServerParams, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient pytestmark = [pytest.mark.diffusion, pytest.mark.full_model] @@ -110,7 +113,7 @@ def _get_diffusion_feature_cases(): ) def test_zimage( omni_server: OmniServer, - openai_client: OpenAIClientHandler, + online_client: OnlineOmniClient, ): """Exercise supported Z-Image diffusion features in minimal CI cases.""" request_config = { @@ -125,4 +128,4 @@ def test_zimage( }, } - openai_client.send_diffusion_request(request_config) + online_client.send_diffusion_request(request_config) diff --git a/tests/engine/test_async_omni_engine_stage_init.py b/tests/engine/test_async_omni_engine_stage_init.py index 0f7d4dab6..a5447a652 100644 --- a/tests/engine/test_async_omni_engine_stage_init.py +++ b/tests/engine/test_async_omni_engine_stage_init.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + import concurrent.futures import contextlib import importlib @@ -443,6 +446,7 @@ def test_initialize_local_llm_replica_scopes_runtime_env(monkeypatch): plan.engine_args_dict = {} runtime_env_var = "VLLM_OMNI_TEST_STAGE_RUNTIME_ENV" + runtime._init_visible_devices_baseline = "0" plan.metadata.runtime_cfg = { "devices": "0", "env": {runtime_env_var: "stage-value"}, @@ -663,11 +667,12 @@ def test_stage_runtime_passes_log_stats_to_llm_replica_launch(monkeypatch): monkeypatch.setattr(runtime_mod, "acquire_device_locks", lambda *_args, **_kwargs: []) monkeypatch.setattr(runtime_mod, "launch_stage_replica", _capture_launch_stage_replica) - monkeypatch.setattr( - runtime_mod.StageEngineCoreClientBase, - "make_async_mp_client", - lambda **kwargs: captured.__setitem__("client_log_stats", kwargs["log_stats"]) or stage_client, - ) + + def _make_async_mp_client(**kwargs): + captured["client_log_stats"] = kwargs["log_stats"] + return stage_client + + monkeypatch.setattr(runtime_mod.StageEngineCoreClientBase, "make_async_mp_client", _make_async_mp_client) assert runtime._initialize_local_llm_replica(plan, stage_init_timeout=1) is stage_client assert captured["log_stats"] is True @@ -889,7 +894,7 @@ def test_initialize_stages_cleans_up_late_successful_replicas_after_early_multi_ def _initialize_stage_replicas(_stage_plans, _stage_init_timeout): exc = RuntimeError("replica launch failed") - exc._initialized_clients_by_stage = {0: [None, initialized_client]} + setattr(exc, "_initialized_clients_by_stage", {0: [None, initialized_client]}) raise exc monkeypatch.setattr(runtime, "_initialize_stage_replicas", _initialize_stage_replicas) diff --git a/tests/entrypoints/openai_api/test_tts_adapter.py b/tests/entrypoints/openai_api/test_tts_adapter.py index e87cab05f..4f6ed2457 100644 --- a/tests/entrypoints/openai_api/test_tts_adapter.py +++ b/tests/entrypoints/openai_api/test_tts_adapter.py @@ -1,4 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Unit tests for the TTS serving adapter registry (RFC #4327). Pure-Python registry/resolution logic; no model or GPU resources are loaded. @@ -6,6 +7,7 @@ Pure-Python registry/resolution logic; no model or GPU resources are loaded. import asyncio from types import SimpleNamespace +from typing import Any import pytest @@ -17,6 +19,7 @@ from vllm_omni.entrypoints.openai.tts_adapters import ( detect_tts_model_type, resolve_adapter, ) +from vllm_omni.entrypoints.openai.tts_adapters.higgs_audio_v2 import HiggsAudioV2Adapter from vllm_omni.entrypoints.openai.tts_adapters.indextts2 import ( IndexTTS2Adapter, IndexTTS25Adapter, @@ -234,5 +237,58 @@ def test_diffusion_adapter_extra_body_params_fallback(): assert _DiffAdapter.extra_body_params() == frozenset() +def _higgs_v2_adapter() -> HiggsAudioV2Adapter: + server = SimpleNamespace( + _apply_uploaded_speaker=lambda request: None, + uploaded_speakers={}, + ) + return HiggsAudioV2Adapter(SimpleNamespace(server=server)) + + +def _higgs_v2_request(**overrides: Any) -> SimpleNamespace: + fields: dict[str, Any] = { + "input": "Hello world.", + "ref_audio": None, + "ref_text": None, + "voice": None, + "x_vector_only_mode": None, + "speaker_embedding": None, + "instructions": None, + "task_type": None, + "language": None, + "speed": None, + "max_new_tokens": None, + } + fields.update(overrides) + return SimpleNamespace(**fields) + + +@pytest.mark.parametrize( + "overrides, err_substr", + [ + pytest.param({"ref_audio": "data:audio/wav;base64,AA=="}, "ref_text", id="ref_audio_without_ref_text"), + pytest.param({"ref_text": "some transcript"}, "ref_audio", id="ref_text_without_ref_audio"), + pytest.param({"task_type": "Base"}, "task_type", id="task_type"), + pytest.param({"language": "Chinese"}, "language", id="language_override"), + pytest.param({"input": "[SPEAKER0] hi"}, "multi-speaker", id="multi_speaker_tag"), + pytest.param({"input": " "}, "empty", id="input_whitespace_only"), + ], +) +def test_higgs_audio_v2_validate_rejects_out_of_scope_fields(overrides: dict[str, object], err_substr: str) -> None: + """Adapter-only policy checks formerly covered by invalid_param e2e on a live V2 server.""" + adapter = _higgs_v2_adapter() + err = adapter.validate(_higgs_v2_request(**overrides)) + assert err is not None + assert err_substr.lower() in err.lower() + + +def test_higgs_audio_v2_validate_accepts_plain_text_and_paired_clone() -> None: + adapter = _higgs_v2_adapter() + assert adapter.validate(_higgs_v2_request()) is None + assert ( + adapter.validate(_higgs_v2_request(ref_audio="data:audio/wav;base64,AA==", ref_text="some transcript")) is None + ) + + if __name__ == "__main__": pytest.main([__file__, "-v"]) diff --git a/tests/entrypoints/test_omni_sleep_mode.py b/tests/entrypoints/test_omni_sleep_mode.py index 10d614dea..821e5ac80 100644 --- a/tests/entrypoints/test_omni_sleep_mode.py +++ b/tests/entrypoints/test_omni_sleep_mode.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Omni sleep mode: entrypoint-level VRAM/ACK tests plus multi-stage e2e. @@ -152,13 +152,13 @@ def _build_llm_stages() -> tuple[list[dict], list[dict]]: @pytest.fixture(scope="module", autouse=True) def _module_device_cleanup(): """One device cleanup pass around the module (engines are class-scoped).""" - from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup + from tests.helpers.clean import cleanup_test_environment print("\n=== PRE-MODULE DEVICE CLEANUP (sleep_mode) ===") - run_pre_test_cleanup() + cleanup_test_environment() yield print("\n=== POST-MODULE DEVICE CLEANUP (sleep_mode) ===") - run_post_test_cleanup() + cleanup_test_environment() @pytest_asyncio.fixture(scope="class", loop_scope="class") diff --git a/tests/helpers/__init__.py b/tests/helpers/__init__.py index 29cd9567e..9fa313409 100644 --- a/tests/helpers/__init__.py +++ b/tests/helpers/__init__.py @@ -1,6 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Shared, importable test helper utilities. -Submodules (``assertions``, ``env``, ``media``, ``runtime``, …) are imported +Submodules (``assertions``, ``clean``, ``client``, ``media``, ``runtime``, …) are imported explicitly by callers. Unit tests for these helpers live under ``tests/``. Avoid star-importing everything here: that ran before refactor only inside the old monolithic ``conftest``; a greedy ``__init__`` changes import order and can diff --git a/tests/helpers/assertions.py b/tests/helpers/assertions.py index 70c772435..8b43233c7 100644 --- a/tests/helpers/assertions.py +++ b/tests/helpers/assertions.py @@ -1,5 +1,9 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Assertion and response validation helpers for tests.""" +import base64 import io import json import math @@ -14,7 +18,7 @@ from pathlib import Path from typing import TYPE_CHECKING, Any, Literal, TypeVar if TYPE_CHECKING: - from tests.helpers.runtime import DiffusionResponse + from tests.helpers.client import DiffusionResponse import av import numpy as np @@ -24,7 +28,6 @@ from PIL import Image from tests.helpers.media import ( convert_audio_bytes_to_text, cosine_similarity_text, - decode_b64_image, preprocess_text, ) @@ -171,7 +174,7 @@ def _short_transcript_contains_expected(transcript: str, expected: str) -> bool: def assert_image_diffusion_response( response: "DiffusionResponse", request_config: dict[str, Any], - run_level: str = None, + run_level: str | None = None, ) -> None: """ Validate image diffusion response. @@ -243,14 +246,15 @@ def assert_images_generations_response( assert isinstance(item, dict), "Image generation data entries must be objects" b64_json = item.get("b64_json") assert isinstance(b64_json, str) and b64_json, "Image generation response is missing b64_json" - image = decode_b64_image(b64_json) + image = Image.open(io.BytesIO(base64.b64decode(b64_json))) + image.load() assert_image_valid(image, width=width, height=height) def assert_video_diffusion_response( response: "DiffusionResponse", request_config: dict[str, Any], - run_level: str = None, + run_level: str | None = None, ) -> None: """ Validate video diffusion response. @@ -319,7 +323,7 @@ def assert_video_first_frame_matches( def assert_audio_diffusion_response( response: "DiffusionResponse", request_config: dict[str, Any], - run_level: str = None, + run_level: str | None = None, ) -> None: """ Validate audio diffusion response. @@ -634,7 +638,7 @@ def _response_has_audio_output(response: Any) -> bool: return bool(audio_data) -def _omni_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str) -> bool: +def _omni_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str | None) -> bool: if run_level not in {"advanced_model", "full_model"}: return False modalities = request_config.get("modalities", ["text", "audio"]) @@ -656,7 +660,7 @@ def _omni_assertion_needs_audio_transcript(request_config: dict[str, Any], run_l return "text" in modalities -def _speech_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str) -> bool: +def _speech_assertion_needs_audio_transcript(request_config: dict[str, Any], run_level: str | None) -> bool: if run_level not in {"advanced_model", "full_model"}: return False if request_config.get("response_format") == "pcm": @@ -667,7 +671,7 @@ def _speech_assertion_needs_audio_transcript(request_config: dict[str, Any], run def _resolve_audio_transcript( response: Any, request_config: dict[str, Any], - run_level: str, + run_level: str | None, *, speech_api: bool, ) -> str | None: @@ -842,28 +846,13 @@ def _assert_transcript_matches( ) -def assert_audio_speech_response(response: Any, request_config: dict[str, Any], run_level: str) -> None: - """Validate speech API results from :class:`~tests.helpers.runtime.OmniResponse`. +def assert_audio_speech_response(response: Any, request_config: dict[str, Any], run_level: str | None = None) -> None: + """Validate speech API results from :class:`~tests.helpers.client.OmniResponse`. - When ``request_config`` carries ``status_code`` and/or ``err_message``, the - request is expected to be rejected: assert it failed and that the HTTP status - / error text match. Otherwise the normal success-path checks run. + Success-path checks only. Negative / contract cases belong on + :meth:`~tests.helpers.client.OnlineOmniClient.send_audio_speech_http_request` + with :func:`assert_http_error`. """ - expected_status = request_config.get("status_code") - expected_err = request_config.get("err_message") - if expected_status is not None or expected_err is not None: - assert not response.success, "Expected an error response, but the request succeeded." - if expected_status is not None: - allowed = expected_status if isinstance(expected_status, (list, tuple)) else (expected_status,) - assert response.status_code in allowed, f"Expected HTTP status in {allowed}, got {response.status_code}" - if expected_err is not None: - alternatives = expected_err if isinstance(expected_err, (list, tuple)) else (expected_err,) - error_text = response.error_message or "" - assert any(alt in error_text for alt in alternatives), ( - f"Expected one of {alternatives} in error text, got: {error_text!r}" - ) - return - assert response.success, "The request failed." # Optional floor on decoded audio size (models with very short clips may use a lower value). @@ -910,7 +899,9 @@ def assert_audio_speech_response(response: Any, request_config: dict[str, Any], ) -def assert_diffusion_response(response: "DiffusionResponse", request_config: dict[str, Any], run_level: str = None): +def assert_diffusion_response( + response: "DiffusionResponse", request_config: dict[str, Any], run_level: str | None = None +): assert response.success, "The request failed." has_any_content = any(content is not None for content in (response.images, response.videos, response.audios)) assert has_any_content, "Response contains no images, videos, or audios" @@ -976,9 +967,9 @@ def assert_http_error( err_message: str | tuple[str, ...] | list[str] | None = None, websocket_json_message: bool = False, ) -> dict[str, Any] | None: - """Validate a raw-HTTP :class:`~tests.helpers.runtime.HttpResponse`-like object. + """Validate a raw-HTTP :class:`~tests.helpers.client.HttpResponse`-like object. - Used by :class:`~tests.helpers.runtime.OpenAIClientHandler` ``send_*_http_request`` helpers when + Used by :class:`~tests.helpers.client.OnlineOmniClient` ``send_*_http_request`` helpers when ``request_config`` contains optional ``err_code`` and/or ``err_message``. When ``websocket_json_message=True``, only ``json_body`` is checked (first JSON WebSocket text frame). diff --git a/tests/helpers/clean.py b/tests/helpers/clean.py new file mode 100644 index 000000000..6502349ad --- /dev/null +++ b/tests/helpers/clean.py @@ -0,0 +1,252 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Test cleanup helpers (device memory, distributed state, and memory monitoring). + +``vllm_omni.platforms`` is imported only inside functions that need it so importing this module +at pytest plugin load does not run before session autouse fixtures. +""" + +from __future__ import annotations + +import gc +import logging +import os +import subprocess +import time + +from vllm_omni.platforms import current_omni_platform + +logger = logging.getLogger(__name__) + + +def get_physical_device_indices(devices): + """Map logical device indices to physical IDs via the platform visibility env. + + Uses ``current_omni_platform.device_control_env_var`` (e.g. ``CUDA_VISIBLE_DEVICES``, + ``ASCEND_RT_VISIBLE_DEVICES``) rather than hard-coding CUDA. + """ + visible_devices = os.environ.get(current_omni_platform.device_control_env_var) + if visible_devices is None: + return devices + visible_indices = [int(x) for x in visible_devices.split(",") if x.strip() != ""] + index_mapping = {i: physical for i, physical in enumerate(visible_indices)} + return [index_mapping[i] for i in devices if i in index_mapping] + + +def pick_least_used_device_indices(num_devices: int) -> list[int]: + """Return physical device indices for the least-used accelerators. + + Args: + num_devices: Number of device indices to return. + + Returns: + Physical device indices sorted from least to most used memory. + """ + if num_devices <= 0: + raise ValueError(f"num_devices must be positive, got {num_devices}.") + + logical_count = current_omni_platform.get_device_count() + if logical_count < num_devices: + raise RuntimeError(f"Need {num_devices} devices, but only {logical_count} are available.") + + device_usage: list[tuple[int, int]] = [] + for device_index in range(logical_count): + with current_omni_platform.device(device_index): + free_bytes, total_bytes = current_omni_platform.get_device_memory() + device_usage.append((total_bytes - free_bytes, device_index)) + device_usage.sort() + logical_indices = [device_index for _, device_index in device_usage[:num_devices]] + return get_physical_device_indices(logical_indices) + + +def wait_for_gpu_memory_to_clear( + *, + devices: list[int], + threshold_bytes: int | None = None, + threshold_ratio: float | None = None, + timeout_s: float = 120, +) -> None: + assert threshold_bytes is not None or threshold_ratio is not None + devices = get_physical_device_indices(devices) + start_time = time.time() + + device_list = ", ".join(str(d) for d in devices) + if threshold_bytes is not None: + condition_str = f"Memory usage ≤ {threshold_bytes / 2**30:.2f} GiB" + + def is_free(used, total): + return used <= threshold_bytes / 2**30 + else: + ratio = threshold_ratio + assert ratio is not None + condition_str = f"Memory usage ratio ≤ {ratio * 100:.1f}%" + + def is_free(used, total): + return used / total <= ratio + + print(f"[Device Memory Monitor] Waiting for device(s) {device_list} to free memory, Condition: {condition_str}") + + def get_mem_gib(device: int) -> tuple[float, float]: + with current_omni_platform.device(device): + free_bytes, total_bytes = current_omni_platform.mem_get_info() + return (total_bytes - free_bytes) / 2**30, total_bytes / 2**30 + + while True: + output_raw = {d: get_mem_gib(d) for d in devices} + output = { + d: f"{used:.1f}GiB/{total:.1f}GiB ({(used / total) * 100 if total > 0 else 0:.1f}%)" + for d, (used, total) in output_raw.items() + } + + print("[Device Memory Status] Current usage:") + for device_id, mem_info in output.items(): + print(f" Device {device_id}: {mem_info}") + + dur_s = time.time() - start_time + if all(is_free(used, total) for used, total in output_raw.values()): + print(f"[Device Memory Freed] Device(s) {device_list} meet memory condition") + print(f" Condition: {condition_str}") + print(f" Wait time: {dur_s:.1f} seconds ({dur_s / 60:.1f} minutes)") + break + + if dur_s >= timeout_s: + raise ValueError( + f"[Device Memory Timeout] Device(s) {device_list} still don't meet memory condition after {dur_s:.1f} seconds\n" + f"Condition: {condition_str}\n" + f"Current status:\n" + "\n".join(f" Device {d}: {output[d]}" for d in devices) + ) + + gc.collect() + current_omni_platform.empty_cache() + time.sleep(5) + + +def _run_smi(label: str, cmd: list[str], head_lines: int, timeout: float = 5) -> None: + print("\n" + "=" * 80) + print(label) + print("=" * 80) + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) + if result.returncode == 0 and result.stdout.strip(): + lines = result.stdout.strip().split("\n") + for line in lines[:head_lines]: + print(line) + if len(lines) > head_lines: + print(f"... (showing first {head_lines} of {len(lines)} lines)") + else: + print(f"{cmd[0]} command failed or produced no output") + except (subprocess.TimeoutExpired, FileNotFoundError): + print(f"{cmd[0]} not available or timed out") + except Exception as e: + print(f"Error running {' '.join(cmd)}: {e}") + + +def _print_device_processes() -> None: + """Print device information via platform SMI tools.""" + if current_omni_platform.is_cuda(): + _run_smi("NVIDIA GPU Information (nvidia-smi)", ["nvidia-smi"], 20) + _run_smi("Detailed GPU Processes (nvidia-smi pmon)", ["nvidia-smi", "pmon", "-c", "1"], 100, timeout=3) + elif current_omni_platform.is_npu(): + _run_smi("Ascend NPU Information (npu-smi info)", ["npu-smi", "info"], 40) + elif current_omni_platform.is_rocm(): + _run_smi("AMD GPU Information (amd-smi)", ["amd-smi"], 30) + _run_smi("Detailed AMD GPU Processes (amd-smi process)", ["amd-smi", "process"], 100, timeout=3) + elif current_omni_platform.is_xpu(): + _run_smi("Intel XPU Information (xpu-smi discovery)", ["xpu-smi", "discovery"], 40) + elif current_omni_platform.is_musa(): + _run_smi("Moore Threads GPU Information (mthreads-gmi)", ["mthreads-gmi"], 30) + else: + print("\n" + "=" * 80) + print("WARNING: No supported device platform detected") + print("=" * 80) + + +def _cleanup_stale_device_locks() -> None: + """Remove stale device-initialization lock files whose recorded PID is dead. + + Lock files at ``/tmp/vllm_omni_device_*_init.lock`` may persist after a + crashed / killed test run and block subsequent orchestrator startups. + """ + import glob as _glob + + for lock_file in _glob.glob("/tmp/vllm_omni_device_*_init.lock"): + try: + with open(lock_file) as fh: + content = fh.read().strip() + if not content: + continue + pid = int(content) + except (OSError, ValueError): + continue + + try: + os.kill(pid, 0) + except ProcessLookupError: + print(f" Removing stale device lock {lock_file} (PID {pid} is dead)") + try: + os.unlink(lock_file) + except OSError: + pass + except PermissionError: + pass + + +def cleanup_test_environment(*, shutdown_ray: bool = False) -> None: + """Tear down distributed state and reset device memory for tests.""" + from vllm import envs + from vllm.distributed.parallel_state import ( + destroy_distributed_environment, + destroy_model_parallel, + ) + + envs.disable_envs_cache() + + if current_omni_platform.is_rocm(): + from vllm._aiter_ops import rocm_aiter_ops + + rocm_aiter_ops.refresh_env_variables() + + gc.unfreeze() + destroy_model_parallel() + destroy_distributed_environment() + if shutdown_ray: + import ray + + ray.shutdown() + + print("Pre-test device status:") + _cleanup_stale_device_locks() + + num_devices = current_omni_platform.device_count() + if num_devices > 0: + try: + wait_for_gpu_memory_to_clear( + devices=list(range(num_devices)), + threshold_ratio=0.05, + timeout_s=60, + ) + except Exception as e: + print(f"Device cleanup note: {e}") + + gc.collect() + if not current_omni_platform.is_cpu(): + current_omni_platform.empty_cache() + try: + import torch + + torch._C._host_emptyCache() + except AttributeError: + logger.warning("torch._C._host_emptyCache() only available in Pytorch >=2.5") + + if current_omni_platform.is_available(): + print("Post-test device status:") + _print_device_processes() + + +__all__ = [ + "cleanup_test_environment", + "get_physical_device_indices", + "pick_least_used_device_indices", + "wait_for_gpu_memory_to_clear", +] diff --git a/tests/helpers/client.py b/tests/helpers/client.py new file mode 100644 index 000000000..edda87790 --- /dev/null +++ b/tests/helpers/client.py @@ -0,0 +1,2147 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Online/offline request clients and response types for tests.""" + +from __future__ import annotations + +import asyncio +import base64 +import concurrent.futures +import copy +import io +import json +import time +from dataclasses import asdict, dataclass +from io import BytesIO +from pathlib import Path +from typing import TYPE_CHECKING, Any, cast +from urllib.parse import quote + +import numpy as np +import requests +import soundfile as sf +import torch +from openai import OpenAI, omit +from PIL import Image +from vllm.logger import init_logger + +from tests.helpers.assertions import ( + SuccessRateGate, + assert_audio_speech_response, + assert_diffusion_response, + assert_http_error, + assert_images_generations_response, + assert_omni_response, + collect_at_success_rate, +) +from tests.helpers.media import ( + _merge_base64_audio_to_segment, +) +from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt +from vllm_omni.outputs import OmniRequestOutput + +if TYPE_CHECKING: + from tests.helpers.runtime import OmniRunner + +logger = init_logger(__name__) + + +def _parse_response_json(r: requests.Response) -> dict[str, Any] | list[Any] | None: + try: + data = r.json() + if isinstance(data, (dict, list)): + return data + except Exception: + pass + return None + + +def _split_request_config_by_per_output_sizes(cfg: dict[str, Any]) -> list[dict[str, Any]] | None: + """If ``extra_body`` has list ``height``/``width``, return one config per index (scalar h/w, ``num_outputs_per_prompt=1``).""" + eb = cfg.get("extra_body") + if not eb: + return None + h, w = eb.get("height"), eb.get("width") + if (isinstance(h, (list, tuple)) or isinstance(w, (list, tuple))) and not ( + isinstance(h, (list, tuple)) and isinstance(w, (list, tuple)) + ): + raise ValueError("extra_body height and width must both be lists or both be scalars") + if not (isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))): + return None + if len(h) != len(w): + raise ValueError(f"height and width lists must have equal length; got {len(h)=} {len(w)=}") + n = len(h) + n_out = eb.get("num_outputs_per_prompt") + if n_out is not None: + n_out = int(n_out) + if n_out != n: + raise ValueError( + "When height/width are lists, num_outputs_per_prompt must equal their length; " + f"got num_outputs_per_prompt={n_out}, len(lists)={n}" + ) + splits: list[dict[str, Any]] = [] + for i in range(n): + sub = copy.deepcopy(cfg) + sub_eb = dict(sub.get("extra_body") or {}) + sub_eb["height"] = int(h[i]) + sub_eb["width"] = int(w[i]) + sub_eb["num_outputs_per_prompt"] = 1 + sub["extra_body"] = sub_eb + splits.append(sub) + return splits + + +@dataclass +class OmniResponse: + """Decoded multimodal / chat output from the OpenAI SDK or offline runner (not raw ``requests``).""" + + text_content: str | None = None + audio_data: list[str] | None = None + audio_content: str | None = None + audio_format: str | None = None + audio_bytes: bytes | None = None + #: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before the + #: OpenAI client call through response parsing and local post-process (e.g. audio decode). + e2e_latency: float | None = None + success: bool = False + prompt_tokens: int | None = None + cached_tokens: int | None = None + multimodal_tokens: dict[str, int] | None = None + logprobs: list | None = None + + +@dataclass +class DiffusionResponse: + """Decoded diffusion output from chat completions or offline runner (not raw ``requests``).""" + + text_content: str | None = None + images: list[Image.Image] | None = None + audios: list[Any] | None = None + videos: list[Any] | None = None + #: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before + #: ``chat.completions.create`` through local image / audio decode. + e2e_latency: float | None = None + success: bool = False + + +@dataclass +class HttpResponse: + """Normalized view of a ``requests`` response from :class:`OnlineOmniClient` HTTP helpers.""" + + status_code: int + success: bool + error_message: str | None = None + json_body: dict[str, Any] | list[Any] | None = None + + +@dataclass +class WebSocketJsonResponse: + """First JSON object delivered as a text WebSocket frame (streaming endpoints).""" + + json_body: dict[str, Any] + + +@dataclass +class OpenPIWebSocketResponse: + """Msgpack WebSocket session against ``/v1/realtime/robot/openpi``.""" + + server_metadata: dict[str, Any] + operation_responses: list[Any] + actions: dict[str, np.ndarray] | None = None + action_tensors: list[np.ndarray] | None = None + + +def build_openpi_droid_observation(*, session_id: str = "gr00t-smoke") -> dict[str, Any]: + """Build a minimal DROID-style observation payload for the OpenPI robot endpoint.""" + identity_eef_9d = np.zeros((1, 1, 9), dtype=np.float32) + identity_eef_9d[..., 3:] = np.array([1, 0, 0, 0, 1, 0], dtype=np.float32) + return { + "session_id": session_id, + "video": { + "exterior_image_1_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8), + "wrist_image_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8), + }, + "state": { + "eef_9d": identity_eef_9d, + "gripper_position": np.zeros((1, 1, 1), dtype=np.float32), + "joint_position": np.zeros((1, 1, 7), dtype=np.float32), + }, + "language": {"annotation.language.language_instruction": [["pick up the object"]]}, + } + + +DREAMZERO_DEFAULT_PROMPT = ( + "Move the pan forward and use the brush in the middle of the plates to brush the inside of the pan" +) +DREAMZERO_ACTION_HORIZON = 24 +DREAMZERO_ACTION_DIM = 8 +DREAMZERO_CAMERA_FILES = { + "observation/exterior_image_0_left": "exterior_image_1_left.mp4", + "observation/exterior_image_1_left": "exterior_image_2_left.mp4", + "observation/wrist_image_left": "wrist_image_left.mp4", +} + + +def _require_opencv() -> Any: + try: + import cv2 + except ImportError as exc: # pragma: no cover - optional e2e dependency + raise ModuleNotFoundError("DreamZero OpenPI test dependencies are missing: opencv-python") from exc + return cv2 + + +def load_dreamzero_camera_frames(video_dir: Path) -> dict[str, np.ndarray]: + cv2 = _require_opencv() + camera_frames: dict[str, np.ndarray] = {} + for camera_key, file_name in DREAMZERO_CAMERA_FILES.items(): + video_path = video_dir / file_name + if not video_path.exists(): + raise FileNotFoundError(f"Missing DreamZero test asset: {video_path}") + cap = cv2.VideoCapture(str(video_path)) + frames = [] + while True: + ok, frame = cap.read() + if not ok: + break + frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) + cap.release() + if not frames: + raise RuntimeError(f"No frames loaded from {video_path}") + camera_frames[camera_key] = np.stack(frames, axis=0) + return camera_frames + + +def build_dreamzero_demo_observations( + camera_frames: dict[str, np.ndarray], + *, + prompt: str, + session_id: str, + num_chunks: int = 2, +) -> list[dict[str, Any]]: + if num_chunks < 1: + raise ValueError("num_chunks must be at least 1") + + relative_offsets = [-23, -16, -8, 0] + total_frames = min(frames.shape[0] for frames in camera_frames.values()) + frame_schedules = [[0]] + current_frame = 23 + for _ in range(num_chunks - 1): + indices = [max(current_frame + offset, 0) for offset in relative_offsets] + if indices[-1] >= total_frames: + break + frame_schedules.append(indices) + current_frame += DREAMZERO_ACTION_HORIZON + + observations: list[dict[str, Any]] = [] + for frame_indices in frame_schedules: + obs: dict[str, Any] = {} + for camera_key, all_frames in camera_frames.items(): + selected = all_frames[frame_indices] + obs[camera_key] = selected[0] if len(frame_indices) == 1 else selected + obs["observation/joint_position"] = np.zeros(7, dtype=np.float32) + obs["observation/cartesian_position"] = np.zeros(6, dtype=np.float32) + obs["observation/gripper_position"] = np.zeros(1, dtype=np.float32) + obs["prompt"] = prompt + obs["session_id"] = session_id + observations.append(obs) + return observations + + +class OpenPIWebSocketSession: + """Persistent msgpack session for ``/v1/realtime/robot/openpi``.""" + + DEFAULT_PING_INTERVAL_SECS = 300 + DEFAULT_PING_TIMEOUT_SECS = 3600 + + @staticmethod + def _require_dependencies() -> tuple[Any, Any]: + try: + import websockets.sync.client as websockets_client + except ImportError as exc: # pragma: no cover - optional e2e dependency + raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: websockets") from exc + try: + from openpi_client import msgpack_numpy + except ImportError as exc: # pragma: no cover - optional e2e dependency + raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: openpi-client") from exc + return websockets_client, msgpack_numpy + + def __init__( + self, + uri: str, + *, + ping_interval: float = DEFAULT_PING_INTERVAL_SECS, + ping_timeout: float = DEFAULT_PING_TIMEOUT_SECS, + open_timeout: float = 120.0, + close_timeout: float = 120.0, + ) -> None: + websockets_client, msgpack_numpy = self._require_dependencies() + self._msgpack_numpy = msgpack_numpy + self._packer = msgpack_numpy.Packer() + self._conn = websockets_client.connect( + uri, + compression=None, + max_size=None, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + open_timeout=open_timeout, + close_timeout=close_timeout, + ) + server_metadata = msgpack_numpy.unpackb(self._conn.recv()) + if not isinstance(server_metadata, dict): + raise TypeError(f"Expected dict metadata from {uri}, got {type(server_metadata)!r}") + self._server_metadata = server_metadata + + def get_server_metadata(self) -> dict[str, Any]: + return dict(self._server_metadata) + + def infer(self, obs: dict[str, Any]) -> dict[str, np.ndarray]: + response = self._send_operation("infer", obs) + if not isinstance(response, dict): + raise TypeError(f"Expected dict infer response, got {type(response)!r}") + return {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()} + + def reset(self, reset_info: dict[str, Any] | None = None) -> str: + response = self._send_operation("reset", dict(reset_info or {})) + return str(response["status"]) + + def close(self) -> None: + self._conn.close() + + def _send_operation(self, endpoint: str, payload: dict[str, Any]) -> Any: + body = dict(payload) + body["endpoint"] = endpoint + self._conn.send(self._packer.pack(body)) + raw = self._conn.recv() + if isinstance(raw, str): + raise RuntimeError(f"OpenPI {endpoint!r} failed: {raw}") + decoded = self._msgpack_numpy.unpackb(raw) + if isinstance(decoded, dict) and decoded.get("type") == "error": + raise RuntimeError(f"OpenPI {endpoint!r} failed: {decoded.get('message')}") + if endpoint == "reset": + if not isinstance(decoded, dict) or decoded.get("status") != "reset successful": + raise RuntimeError(f"Unexpected OpenPI reset response: {decoded!r}") + return decoded + return decoded + + +def _merge_http_expectation_kwargs( + base: dict[str, Any] | None, + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, +) -> dict[str, Any]: + cfg = dict(base or {}) + if err_code is not None: + cfg["err_code"] = err_code + if err_message is not None: + cfg["err_message"] = err_message + return cfg + + +def _merge_ws_expectation_kwargs( + base: dict[str, Any] | None, + *, + err_message: str | tuple[str, ...] | list[str] | None = None, + ws_json_type: str | None = None, + ws_error_code: str | None = None, +) -> dict[str, Any]: + cfg = dict(base or {}) + if err_message is not None: + cfg["err_message"] = err_message + if ws_json_type is not None: + cfg["ws_json_type"] = ws_json_type + if ws_error_code is not None: + cfg["ws_error_code"] = ws_error_code + return cfg + + +def _run_ws_expectations_from_request_config(cfg: dict[str, Any], resp: WebSocketJsonResponse) -> None: + jb = resp.json_body + want_type = cfg.get("ws_json_type") + if want_type is not None: + assert jb.get("type") == want_type, (jb, want_type) + want_code = cfg.get("ws_error_code") + if want_code is not None: + assert jb.get("code") == want_code, (jb, want_code) + err_message = cfg.get("err_message") + if err_message is not None: + assert_http_error(resp, err_message=err_message, websocket_json_message=True) + + +def _merge_diffusion_responses(parts: list[DiffusionResponse]) -> DiffusionResponse: + """Concatenate images in order; ``e2e_latency`` is wall-clock of the batch (set by caller) or max of parts.""" + merged = DiffusionResponse() + merged.success = all(p.success for p in parts) and len(parts) > 0 + imgs: list[Image.Image] = [] + for p in parts: + if p.images: + imgs.extend(p.images) + merged.images = imgs if imgs else None + latencies = [p.e2e_latency for p in parts if p.e2e_latency is not None] + merged.e2e_latency = max(latencies) if latencies else None + return merged + + +class OnlineOmniClient: + def __init__( + self, + host: str = "127.0.0.1", + port: int | None = None, + api_key: str = "EMPTY", + run_level: str | None = None, + *, + log_stats: bool = True, + ): + if port is None: + from tests.helpers.runtime import get_open_port + + port = get_open_port() + self.base_url = f"http://{host}:{port}" + self.client = OpenAI(base_url=f"http://{host}:{port}/v1", api_key=api_key) + self.run_level = run_level + self.log_stats = log_stats + + def _print_client_stat(self, message: str) -> None: + if self.log_stats: + print(message, flush=True) + + def _process_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse: + """Wall clock from *before* ``chat.completions.create`` through stream drain + local decode.""" + result = OmniResponse() + try: + text_content = "" + audio_data = [] + for chunk in chat_completion: + for choice in chunk.choices: + content = getattr(getattr(choice, "delta", None), "content", None) + modality = getattr(chunk, "modality", None) + if modality == "audio" and content: + audio_data.append(content) + elif modality == "text" and content: + text_content += content + # Usage is yielded after the last token + if chunk.usage: + result.prompt_tokens = chunk.usage.prompt_tokens + if details := getattr(chunk.usage, "prompt_tokens_details", None): + result.cached_tokens = details.cached_tokens + result.multimodal_tokens = getattr(details, "multimodal_tokens", None) + + if audio_data: + merged_seg = _merge_base64_audio_to_segment(audio_data) + wav_buf = BytesIO() + merged_seg.export(wav_buf, format="wav") + result.audio_bytes = wav_buf.getvalue() + result.text_content = text_content + result.audio_data = audio_data + result.e2e_latency = time.perf_counter() - wall_start + result.success = True + except Exception as e: + msg = f"Stream processing error: {str(e)}" + print(f"Error: {msg}") + return result + + def _process_non_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse: + """Wall clock from *before* ``chat.completions.create`` through response parse + local decode.""" + result = OmniResponse() + try: + audio_data = None + text_content = None + for choice in chat_completion.choices: + if hasattr(choice.message, "audio") and choice.message.audio is not None: + audio_data = choice.message.audio.data + if hasattr(choice.message, "content") and choice.message.content is not None: + text_content = choice.message.content + # Extract cached & prompt token counts for prefix caching tests + usage = getattr(chat_completion, "usage", None) + if usage: + result.prompt_tokens = usage.prompt_tokens + if details := getattr(usage, "prompt_tokens_details", None): + result.cached_tokens = details.cached_tokens + result.multimodal_tokens = getattr(details, "multimodal_tokens", None) + if audio_data: + result.audio_bytes = base64.b64decode(audio_data) + result.text_content = text_content + result.e2e_latency = time.perf_counter() - wall_start + if chat_completion.choices and chat_completion.choices[0].logprobs is not None: + result.logprobs = chat_completion.choices[0].logprobs.content + result.success = True + except Exception as e: + msg = f"Non-stream processing error: {str(e)}" + print(f"Error: {msg}") + return result + + def _process_diffusion_response(self, chat_completion, *, wall_start: float) -> DiffusionResponse: + """Wall clock from *before* ``chat.completions.create`` through image decode.""" + result = DiffusionResponse() + try: + images = [] + audios = [] + for choice in chat_completion.choices: + content = getattr(choice.message, "content", None) + if isinstance(content, list): + for item in content: + image_url = None + if isinstance(item, dict): + image_url = item.get("image_url", {}).get("url") + else: + image_url_obj = getattr(item, "image_url", None) + image_url = getattr(image_url_obj, "url", None) if image_url_obj else None + if image_url and image_url.startswith("data:image"): + b64_data = image_url.split(",", 1)[1] + img = Image.open(io.BytesIO(base64.b64decode(b64_data))) + img.load() + images.append(img) + + # OpenAI audio responses (e.g. AudioX text-to-audio) populate `message.audio`. + audio_obj = getattr(choice.message, "audio", None) + audio_b64 = getattr(audio_obj, "data", None) if audio_obj is not None else None + if audio_b64: + audios.append( + { + "wav_bytes": base64.b64decode(audio_b64), + "id": getattr(audio_obj, "id", None), + "expires_at": getattr(audio_obj, "expires_at", None), + } + ) + result.images = images if images else None + result.audios = audios if audios else None + result.e2e_latency = time.perf_counter() - wall_start + result.success = True + except Exception as e: + msg = f"Diffusion response processing error: {str(e)}" + print(f"Error: {msg}") + return result + + def _http_response_from_requests(self, r: requests.Response) -> HttpResponse: + payload = _parse_response_json(r) + ok = 200 <= r.status_code < 300 + return HttpResponse( + status_code=r.status_code, + success=ok, + error_message=None if ok else (r.text[:8000] if r.text else None), + json_body=payload, + ) + + def send_health_http_request( + self, + request_config: dict[str, Any] | None = None, + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/health`` (raw ``requests``). + + ``request_config``: optional ``timeout`` plus optional ``err_code`` / ``err_message`` for + :func:`~tests.helpers.assertions.assert_http_error` (also as keyword-only args). + """ + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = requests.get(self._build_url("/health"), timeout=float(cfg.get("timeout", 120.0))) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_models_http_request( + self, + request_config: dict[str, Any] | None = None, + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/v1/models``. Optional ``timeout`` and HTTP assertions (see :func:`~tests.helpers.assertions.assert_http_error`).""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = requests.get( + self._build_url("/v1/models"), + headers={"Accept": "application/json"}, + timeout=float(cfg.get("timeout", 120.0)), + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_chat_completions_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/chat/completions`` with ``json`` or ``raw_body`` (malformed-body / contract tests).""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/chat/completions", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_batched_chat_completions_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """Post to batched chat completions.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/chat/completions/batch", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_completions_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/completions`` with ``json`` or ``raw_body``.""" + # TODO (Alex): A lot of these helpers should be consolidated as they differ only by endpoint + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/completions", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_omni_sleep_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/omni/sleep`` — ``json`` or ``raw_body``, ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/omni/sleep", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_omni_wakeup_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/omni/wakeup``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/omni/wakeup", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_voices_list_http_request( + self, + request_config: dict[str, Any] | None = None, + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/v1/audio/voices``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = requests.get( + self._build_url("/v1/audio/voices"), + headers={"Accept": "application/json"}, + timeout=float(cfg.get("timeout", 120.0)), + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_voices_create_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/audio/voices`` (multipart): ``data`` / ``files`` / ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_form_endpoint("/v1/audio/voices", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_voices_delete_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """DELETE ``/v1/audio/voices/{name}`` — requires ``name``, optional ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + name = cfg["name"] + timeout = float(cfg.get("timeout", 120.0)) + path = f"/v1/audio/voices/{quote(str(name), safe='')}" + r = requests.delete( + self._build_url(path), + headers={"Accept": "application/json"}, + timeout=timeout, + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_speech_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/audio/speech`` with ``json`` or ``raw_body``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/audio/speech", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_speech_batch_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/audio/speech/batch``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/audio/speech/batch", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_audio_generate_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/audio/generate``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/audio/generate", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_images_generations_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/images/generations`` — ``json`` or ``raw_body``, ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_json_endpoint("/v1/images/generations", cfg, default_timeout=300.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + if cfg.get("err_code") is None: + assert resp.success, resp.error_message + payload = resp.json_body + assert isinstance(payload, dict) + assert_images_generations_response(payload, cfg, run_level=self.run_level) + return [resp] + + def send_images_edits_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/images/edits`` — ``data`` / ``files`` / ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_form_endpoint("/v1/images/edits", cfg, default_timeout=300.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_videos_create_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/videos`` (async job) — multipart ``data`` / ``files``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_form_endpoint("/v1/videos", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_videos_sync_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """POST ``/v1/videos/sync``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = self._post_form_endpoint("/v1/videos/sync", cfg, default_timeout=120.0) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_videos_list_http_request( + self, + request_config: dict[str, Any] | None = None, + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/v1/videos`` — optional ``params``, ``timeout``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + r = requests.get( + self._build_url("/v1/videos"), + params=cfg.get("params"), + headers={"Accept": "application/json"}, + timeout=float(cfg.get("timeout", 120.0)), + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_video_retrieve_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/v1/videos/{video_id}``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + video_id = cfg["video_id"] + timeout = float(cfg.get("timeout", 120.0)) + r = requests.get( + self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"), + headers={"Accept": "application/json"}, + timeout=timeout, + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_video_delete_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """DELETE ``/v1/videos/{video_id}``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + video_id = cfg["video_id"] + timeout = float(cfg.get("timeout", 120.0)) + r = requests.delete( + self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"), + headers={"Accept": "application/json"}, + timeout=timeout, + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def send_video_content_http_request( + self, + request_config: dict[str, Any], + *, + err_code: int | tuple[int, ...] | list[int] | None = None, + err_message: str | tuple[str, ...] | list[str] | None = None, + ) -> list[HttpResponse]: + """GET ``/v1/videos/{video_id}/content``.""" + cfg = _merge_http_expectation_kwargs( + request_config, + err_code=err_code, + err_message=err_message, + ) + video_id = cfg["video_id"] + timeout = float(cfg.get("timeout", 120.0)) + r = requests.get( + self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}/content"), + timeout=timeout, + ) + resp = self._http_response_from_requests(r) + assert_http_error( + resp, + err_code=cfg.get("err_code"), + err_message=cfg.get("err_message"), + ) + return [resp] + + def _build_ws_url(self, path: str) -> str: + """Turn HTTP ``base_url`` into ``ws`` / ``wss`` for WebSocket helpers.""" + base = self.base_url.rstrip("/") + suffix = "/" + path.lstrip("/") + if base.startswith("http://"): + return "ws://" + base.removeprefix("http://") + suffix + if base.startswith("https://"): + return "wss://" + base.removeprefix("https://") + suffix + raise ValueError(f"Unsupported base_url for WebSocket: {base!r}") + + def _send_websocket_first_json_request( + self, + path: str, + cfg: dict[str, Any], + ) -> list[WebSocketJsonResponse]: + """Connect, optionally send text frames, return first JSON text frame as :class:`WebSocketJsonResponse`. + + ``request_config`` keys: + + - ``send_frames``: optional ``str`` or sequence of ``str`` raw WebSocket text frames (omit when the server + speaks first, e.g. ``/v1/realtime`` rejection path). + - ``ws_skip_types``: optional event ``type`` strings to ignore while waiting for the first matching frame + (e.g. ``["session.created"]`` on ``/v1/realtime``). + - ``timeout``: seconds to wait for the first inbound text frame (default ``120``). + - ``ws_max_size``: passed through as ``max_size`` to :func:`websockets.connect` when the key is present. + """ + send_frames_raw = cfg.get("send_frames") + if send_frames_raw is None: + frames: list[str] = [] + elif isinstance(send_frames_raw, str): + frames = [send_frames_raw] + else: + frames = list(send_frames_raw) + + timeout = float(cfg.get("timeout", 120.0)) + uri = self._build_ws_url(path) + skip_types = set(cfg.get("ws_skip_types") or []) + + connect_kw: dict[str, Any] = {} + if "ws_max_size" in cfg: + connect_kw["max_size"] = cfg["ws_max_size"] + + async def _recv_first_json_object() -> WebSocketJsonResponse: + import websockets + + async with websockets.connect(uri, **connect_kw) as ws: + for frame in frames: + await ws.send(frame) + while True: + raw = await asyncio.wait_for(ws.recv(), timeout=timeout) + if not isinstance(raw, str): + raise AssertionError(f"Expected JSON text frame from {uri}, got {type(raw).__name__}") + try: + data = json.loads(raw) + except json.JSONDecodeError as exc: + raise AssertionError(f"Expected JSON text frame from {uri}, body={raw[:500]!r}") from exc + if not isinstance(data, dict): + raise AssertionError(f"Expected JSON object from {uri}, got {type(data).__name__}") + if skip_types and data.get("type") in skip_types: + continue + return WebSocketJsonResponse(json_body=data) + + resp = asyncio.run(_recv_first_json_object()) + _run_ws_expectations_from_request_config(cfg, resp) + return [resp] + + def send_audio_speech_stream_ws_request( + self, + request_config: dict[str, Any], + *, + err_message: str | tuple[str, ...] | list[str] | None = None, + ws_json_type: str | None = None, + ws_error_code: str | None = None, + ) -> list[WebSocketJsonResponse]: + """WebSocket ``/v1/audio/speech/stream`` — send ``send_frames`` then read first JSON text frame.""" + cfg = _merge_ws_expectation_kwargs( + request_config, + err_message=err_message, + ws_json_type=ws_json_type, + ws_error_code=ws_error_code, + ) + return self._send_websocket_first_json_request("/v1/audio/speech/stream", cfg) + + def send_video_chat_stream_ws_request( + self, + request_config: dict[str, Any], + *, + err_message: str | tuple[str, ...] | list[str] | None = None, + ws_json_type: str | None = None, + ws_error_code: str | None = None, + ) -> list[WebSocketJsonResponse]: + """WebSocket ``/v1/video/chat/stream`` — send ``send_frames`` then read first JSON text frame.""" + cfg = _merge_ws_expectation_kwargs( + request_config, + err_message=err_message, + ws_json_type=ws_json_type, + ws_error_code=ws_error_code, + ) + return self._send_websocket_first_json_request("/v1/video/chat/stream", cfg) + + def send_realtime_ws_request( + self, + request_config: dict[str, Any] | None = None, + *, + err_message: str | tuple[str, ...] | list[str] | None = None, + ws_json_type: str | None = None, + ws_error_code: str | None = None, + ) -> list[WebSocketJsonResponse]: + """WebSocket ``/v1/realtime`` — optional outbound frames, then first JSON text frame (often server-initiated).""" + cfg = _merge_ws_expectation_kwargs( + request_config, + err_message=err_message, + ws_json_type=ws_json_type, + ws_error_code=ws_error_code, + ) + return self._send_websocket_first_json_request("/v1/realtime", cfg) + + def send_robot_openpi_ws_request( + self, + request_config: dict[str, Any] | None = None, + ) -> list[OpenPIWebSocketResponse]: + """WebSocket ``/v1/realtime/robot/openpi`` — msgpack metadata plus optional infer/reset ops. + + ``request_config`` keys: + + - ``operations``: optional sequence of ``{"endpoint": "infer"|"reset", "payload": {...}}``. + - ``run_default_policy_session``: when true and ``operations`` is omitted, run infer then reset + using :func:`build_openpi_droid_observation`. + - ``run_dreamzero_policy_session``: when true and ``operations`` is omitted, run the DreamZero + infer/reset/infer sequence using :func:`build_dreamzero_demo_observations`. + - ``video_dir``: required for ``run_dreamzero_policy_session``. + - ``prompt``: language instruction for DreamZero observations (optional). + - ``session_id``: used by the default policy session observation builder. + - ``num_chunks``: DreamZero chunk count (default ``2``). + - ``timeout``: seconds to wait for each inbound msgpack frame (default ``120``). + - ``ping_interval`` / ``ping_timeout``: forwarded to :func:`websockets.sync.client.connect`. + """ + cfg = dict(request_config or {}) + operations_cfg = cfg.get("operations") + if operations_cfg is not None: + operations = [dict(op) for op in operations_cfg] + elif cfg.get("run_dreamzero_policy_session"): + video_dir = cfg.get("video_dir") + if video_dir is None: + raise ValueError("run_dreamzero_policy_session requires video_dir") + session_id = str(cfg.get("session_id", "dreamzero-smoke")) + prompt = str(cfg.get("prompt", DREAMZERO_DEFAULT_PROMPT)) + num_chunks = int(cfg.get("num_chunks", 2)) + observations = build_dreamzero_demo_observations( + load_dreamzero_camera_frames(Path(video_dir)), + prompt=prompt, + session_id=session_id, + num_chunks=num_chunks, + ) + operations = [{"endpoint": "infer", "payload": obs} for obs in observations] + operations.append({"endpoint": "reset", "payload": {}}) + operations.append({"endpoint": "infer", "payload": observations[0]}) + elif cfg.get("run_default_policy_session"): + session_id = str(cfg.get("session_id", "gr00t-smoke")) + operations = [ + {"endpoint": "infer", "payload": build_openpi_droid_observation(session_id=session_id)}, + {"endpoint": "reset", "payload": {}}, + ] + else: + operations = [] + timeout = float(cfg.get("timeout", 120.0)) + ping_interval = float(cfg.get("ping_interval", OpenPIWebSocketSession.DEFAULT_PING_INTERVAL_SECS)) + ping_timeout = float(cfg.get("ping_timeout", OpenPIWebSocketSession.DEFAULT_PING_TIMEOUT_SECS)) + + uri = self._build_ws_url("/v1/realtime/robot/openpi") + session = OpenPIWebSocketSession( + uri, + ping_interval=ping_interval, + ping_timeout=ping_timeout, + open_timeout=timeout, + close_timeout=timeout, + ) + try: + operation_responses: list[Any] = [] + actions: dict[str, np.ndarray] | None = None + action_tensors: list[np.ndarray] = [] + for operation in operations: + endpoint = str(operation["endpoint"]) + payload = dict(operation.get("payload") or {}) + response = session._send_operation(endpoint, payload) + operation_responses.append(response) + if endpoint == "infer": + if isinstance(response, dict): + actions = {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()} + else: + action_tensors.append(np.asarray(response, dtype=np.float32)) + finally: + session.close() + + return [ + OpenPIWebSocketResponse( + server_metadata=session.get_server_metadata(), + operation_responses=operation_responses, + actions=actions, + action_tensors=action_tensors or None, + ) + ] + + def send_omni_request( + self, + request_config: dict[str, Any], + request_num: int = 1, + *, + min_successes: int | None = None, + max_concurrency: int | None = None, + ) -> list[OmniResponse]: + """Chat completions via the OpenAI Python SDK (not raw HTTP). + + ``min_successes`` gates the batch on how many of the ``request_num`` requests pass + rather than requiring each one to, and returns only the successes; ``max_concurrency`` + caps how many are in flight at once. See ``SuccessRateGate``. + """ + responses: list[OmniResponse] = [] + stream = request_config.get("stream", False) + modalities = request_config.get("modalities", ["text", "audio"]) + extra_body: dict[str, Any] = {} + if "speaker" in request_config: + extra_body["speaker"] = request_config["speaker"] + if request_config.get("use_audio_in_video"): + mm = dict(extra_body.get("mm_processor_kwargs") or {}) + mm["use_audio_in_video"] = True + extra_body["mm_processor_kwargs"] = mm + if "sampling_params_list" in request_config: + extra_body["sampling_params_list"] = request_config["sampling_params_list"] + if request_config.get("extra_body"): + extra_body.update(request_config["extra_body"]) + + create_kwargs: dict[str, Any] = { + "model": request_config.get("model"), + "messages": request_config.get("messages"), + "stream": stream, + "modalities": modalities, + } + if "logprobs" in request_config: + create_kwargs["logprobs"] = request_config["logprobs"] + if "top_logprobs" in request_config: + create_kwargs["top_logprobs"] = request_config["top_logprobs"] + if "stream_options" in request_config: + create_kwargs["stream_options"] = request_config["stream_options"] + if extra_body: + create_kwargs["extra_body"] = extra_body + + def _one(): + wall_start = time.perf_counter() + chat_completion = self.client.chat.completions.create(**create_kwargs) + return ( + self._process_stream_omni_response(chat_completion, wall_start=wall_start) + if stream + else self._process_non_stream_omni_response(chat_completion, wall_start=wall_start) + ) + + if min_successes is not None: + gate = SuccessRateGate(min_successes=min_successes, concurrency=max_concurrency) + + def _sample() -> OmniResponse: + resp = _one() + assert_omni_response(resp, request_config, run_level=self.run_level) + return resp + + # Not _print_client_stat: the count is the gate's verdict, so it has to survive + # log_stats=False. + return collect_at_success_rate( + _sample, + gate, + request_num=request_num, + report=lambda line: print(f"[omni] {line}", flush=True), + ) + + if request_num == 1: + resp = _one() + assert_omni_response(resp, request_config, run_level=self.run_level) + if resp.e2e_latency is not None: + self._print_client_stat(f"[omni] request#1 success in {resp.e2e_latency:.3f}s") + else: + self._print_client_stat("[omni] request#1 completed") + responses.append(resp) + return responses + + with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: + futures = {executor.submit(_one): i + 1 for i in range(request_num)} + for future in concurrent.futures.as_completed(futures): + request_idx = futures[future] + resp = future.result() + assert_omni_response(resp, request_config, run_level=self.run_level) + if resp.e2e_latency is not None: + self._print_client_stat(f"[omni] request#{request_idx} success in {resp.e2e_latency:.3f}s") + else: + self._print_client_stat(f"[omni] request#{request_idx} completed") + responses.append(resp) + return responses + + def _process_stream_audio_speech_response( + self, response, *, response_format: str | None = None, wall_start: float + ) -> OmniResponse: + """ + Process streaming /v1/audio/speech responses into an OmniResponse. + + This mirrors _process_stream_omni_response but operates on low-level + audio bytes. Whisper transcription runs in assert_audio_speech_response + when the run_level requires it. + """ + result = OmniResponse() + + try: + # Aggregate all audio bytes from the streaming response. + data = bytearray() + + # Preferred OpenAI helper. + if hasattr(response, "iter_bytes") and callable(getattr(response, "iter_bytes")): + for chunk in response.iter_bytes(): + if chunk: + data.extend(chunk) + else: + # Generic iterable-of-bytes fallback (e.g., generator or list of chunks). + try: + iterator = iter(response) + except TypeError: + iterator = None + + if iterator is not None: + for chunk in iterator: + if not chunk: + continue + if isinstance(chunk, (bytes, bytearray)): + data.extend(chunk) + elif hasattr(chunk, "data"): + data.extend(chunk.data) # type: ignore[arg-type] + elif hasattr(chunk, "content"): + data.extend(chunk.content) # type: ignore[arg-type] + else: + raise TypeError(f"Unsupported stream chunk type: {type(chunk)}") + else: + raise TypeError(f"Unsupported audio speech streaming response type: {type(response)}") + + raw_bytes = bytes(data) + + # Populate OmniResponse. + result.audio_bytes = raw_bytes + result.e2e_latency = time.perf_counter() - wall_start + result.success = True + result.audio_format = getattr(response, "response", None) + if result.audio_format is not None: + result.audio_format = result.audio_format.headers.get("content-type", "") + + except Exception as e: + msg = f"Audio speech stream processing error: {str(e)}" + print(f"Error: {msg}") + + return result + + def _process_non_stream_audio_speech_response( + self, response, *, response_format: str | None = None, wall_start: float + ) -> OmniResponse: + """ + Process non-streaming /v1/audio/speech responses into an OmniResponse. + + This mirrors _process_non_stream_omni_response but for the binary + audio payload returned by audio.speech.create. + """ + result = OmniResponse() + + try: + # OpenAI non-streaming audio.speech.create returns HttpxBinaryResponseContent (.read() or .content) + if hasattr(response, "read") and callable(getattr(response, "read")): + raw_bytes = response.read() + elif hasattr(response, "content"): + raw_bytes = response.content # type: ignore[assignment] + else: + raise TypeError(f"Unsupported audio speech response type: {type(response)}") + + result.audio_bytes = raw_bytes + result.e2e_latency = time.perf_counter() - wall_start + result.success = True + result.audio_format = getattr(response, "response", None) + if result.audio_format is not None: + result.audio_format = result.audio_format.headers.get("content-type", "") + + except Exception as e: + msg = f"Audio speech non-stream processing error: {str(e)}" + print(f"Error: {msg}") + + return result + + def send_audio_speech_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]: + """ + Call the /v1/audio/speech endpoint using the same configuration-dict + style as send_omni_request, but via the OpenAI Python client's + audio.speech APIs. + + Expected keys in request_config: + - model: model name/path (required) + - input: text to synthesize (required) + - response_format: audio format such as "wav" or "pcm" (optional) + - task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body) + - min_audio_bytes: optional minimum ``len(audio_bytes)`` checked in ``assert_audio_speech_response`` + - timeout: request timeout in seconds (float, optional, default 120.0) + - stream: whether to use streaming API (bool, optional, default False) + + For negative / contract checks (expected 4xx), use + :meth:`send_audio_speech_http_request` with ``err_code`` / ``err_message``. + """ + timeout = float(request_config.get("timeout", 120.0)) + + model = request_config["model"] + text_input = request_config["input"] + stream = bool(request_config.get("stream", False)) + voice = request_config.get("voice", None) + + # Standard OpenAI param: use omit when not provided to keep default behavior. + response_format = request_config.get("response_format", omit) + + # Qwen3-TTS custom fields, forwarded via extra_body. + extra_body: dict[str, Any] = {} + # Keep this list aligned with vllm_omni.entrypoints.openai.protocol.audio params. + for key in ( + "task_type", + "ref_text", + "ref_audio", + "language", + "max_new_tokens", + "seed", + "instructions", + "speed", + "stream_format", + "x_vector_only_mode", + ): + if key in request_config: + extra_body[key] = request_config[key] + + responses: list[OmniResponse] = [] + + speech_fmt: str | None = None if response_format is omit else str(response_format).lower() + + print(f"[audio.speech] start model={model}, stream={stream}, request_num={request_num}, timeout={timeout:.1f}s") + + if request_num == 1: + if stream: + # Use streaming response helper. + wall_start = time.perf_counter() + with self.client.audio.speech.with_streaming_response.create( + model=model, + input=text_input, + response_format=response_format, + extra_body=extra_body or None, + timeout=timeout, + voice=voice, + ) as resp: + omni_resp = self._process_stream_audio_speech_response( + resp, response_format=speech_fmt, wall_start=wall_start + ) + else: + # Non-streaming response. + wall_start = time.perf_counter() + resp = self.client.audio.speech.create( + model=model, + input=text_input, + response_format=response_format, + extra_body=extra_body or None, + timeout=timeout, + voice=voice, + ) + omni_resp = self._process_non_stream_audio_speech_response( + resp, response_format=speech_fmt, wall_start=wall_start + ) + + assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) + if omni_resp.e2e_latency is not None: + self._print_client_stat(f"[audio.speech] request#1 success in {omni_resp.e2e_latency:.3f}s") + else: + self._print_client_stat("[audio.speech] request#1 completed") + responses.append(omni_resp) + return responses + else: + # request_num > 1: concurrent requests (use same params as single-request path) + + if stream: + + def _stream_task(request_idx: int): + wall_start = time.perf_counter() + with self.client.audio.speech.with_streaming_response.create( + model=model, + input=text_input, + response_format=response_format, + extra_body=extra_body or None, + timeout=timeout, + voice=voice, + ) as resp: + result = self._process_stream_audio_speech_response( + resp, response_format=speech_fmt, wall_start=wall_start + ) + if result.e2e_latency is not None: + self._print_client_stat( + f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s" + ) + else: + self._print_client_stat(f"[audio.speech] request#{request_idx} completed") + return result + + with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: + futures = {executor.submit(_stream_task, i + 1): i + 1 for i in range(request_num)} + for future in concurrent.futures.as_completed(futures): + request_idx = futures[future] + try: + omni_resp = future.result() + except Exception as e: + print( + f"[audio.speech] request#{request_idx} failed " + f"(stream={stream}, timeout={timeout:.1f}s): {e!r}" + ) + raise + assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) + responses.append(omni_resp) + else: + + def _non_stream_task(request_idx: int): + wall_start = time.perf_counter() + r = self.client.audio.speech.create( + model=model, + input=text_input, + response_format=response_format, + extra_body=extra_body or None, + timeout=timeout, + voice=voice, + ) + result = self._process_non_stream_audio_speech_response( + r, response_format=speech_fmt, wall_start=wall_start + ) + if result.e2e_latency is not None: + self._print_client_stat( + f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s" + ) + else: + self._print_client_stat(f"[audio.speech] request#{request_idx} completed") + return result + + with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: + futures = {executor.submit(_non_stream_task, i + 1): i + 1 for i in range(request_num)} + for future in concurrent.futures.as_completed(futures): + request_idx = futures[future] + try: + omni_resp = future.result() + except Exception as e: + print( + f"[audio.speech] request#{request_idx} failed " + f"(stream={stream}, timeout={timeout:.1f}s): {e!r}" + ) + raise + assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) + responses.append(omni_resp) + + return responses + + def send_diffusion_request( + self, request_config: dict[str, Any] | list[dict[str, Any]], request_num: int = 1 + ) -> list[DiffusionResponse]: + """ + Send OpenAI requests for diffusion models. + If ``extra_body`` has list ``height``/``width``, sends one chat completion per index in parallel + (scalar h/w, ``num_outputs_per_prompt=1`` each) and merges images in list order. + + Args: + request_config: A single request configuration dict, or a list of + request configuration dicts (one request per element) + request_num: Number of requests to send concurrently, defaults to 1 (single request) + Returns: + list[DiffusionResponse]: List of DiffusionResponse objects containing the response data + """ + responses: list[DiffusionResponse] = [] + + def _create_from_config(cfg: dict[str, Any]) -> tuple[Any, float]: + stream = cfg.get("stream", False) + if stream: + raise NotImplementedError("Streaming is not currently implemented for diffusion model e2e test") + modalities = cfg.get("modalities", omit) # Most diffusion models don't require modalities param + eb = cfg.get("extra_body") + extra = copy.deepcopy(eb) if eb else None + wall_start = time.perf_counter() + chat_completion = self.client.chat.completions.create( + model=cfg.get("model"), + messages=cfg.get("messages"), + extra_body=extra, + modalities=modalities, + ) + return chat_completion, wall_start + + if isinstance(request_config, list): + if not request_config: + raise ValueError("request_config list must not be empty") + if request_num != 1: + raise ValueError("request_num is not supported when request_config is a list") + with concurrent.futures.ThreadPoolExecutor(max_workers=len(request_config)) as executor: + futures = { + executor.submit(_create_from_config, cfg): (i + 1, cfg) for i, cfg in enumerate(request_config) + } + for future in concurrent.futures.as_completed(futures): + request_idx, cfg = futures[future] + chat_completion, wall_start = future.result() + response = self._process_diffusion_response(chat_completion, wall_start=wall_start) + assert_diffusion_response(response, cfg, run_level=self.run_level) + if response.e2e_latency is not None: + self._print_client_stat( + f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s" + ) + else: + self._print_client_stat(f"[diffusion] request#{request_idx} completed") + responses.append(response) + return responses + + size_splits = _split_request_config_by_per_output_sizes(request_config) + if size_splits is not None: + if request_num != 1: + raise ValueError( + "request_num must be 1 when extra_body height/width are lists (split into concurrent per-size calls)" + ) + t0 = time.perf_counter() + with concurrent.futures.ThreadPoolExecutor(max_workers=len(size_splits)) as executor: + split_futures = [executor.submit(_create_from_config, sub) for sub in size_splits] + chat_completions = [f.result() for f in split_futures] + parts = [self._process_diffusion_response(cc, wall_start=ws) for cc, ws in chat_completions] + merged = _merge_diffusion_responses(parts) + merged.e2e_latency = time.perf_counter() - t0 + assert_diffusion_response(merged, request_config, run_level=self.run_level) + if merged.e2e_latency is not None: + self._print_client_stat(f"[diffusion] request#1 success in {merged.e2e_latency:.3f}s") + else: + self._print_client_stat("[diffusion] request#1 completed") + return [merged] + + if request_num == 1: + # Send single request + chat_completion, wall_start = _create_from_config(request_config) + response = self._process_diffusion_response(chat_completion, wall_start=wall_start) + assert_diffusion_response(response, request_config, run_level=self.run_level) + if response.e2e_latency is not None: + self._print_client_stat(f"[diffusion] request#1 success in {response.e2e_latency:.3f}s") + else: + self._print_client_stat("[diffusion] request#1 completed") + responses.append(response) + return responses + + # Send concurrent requests for the same request_config + with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: + idx_by_future = {executor.submit(_create_from_config, request_config): i + 1 for i in range(request_num)} + for future in concurrent.futures.as_completed(idx_by_future): + request_idx = idx_by_future[future] + chat_completion, wall_start = future.result() + response = self._process_diffusion_response(chat_completion, wall_start=wall_start) + assert_diffusion_response(response, request_config, run_level=self.run_level) + if response.e2e_latency is not None: + self._print_client_stat(f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s") + else: + self._print_client_stat(f"[diffusion] request#{request_idx} completed") + responses.append(response) + return responses + + def send_video_diffusion_request( + self, request_config: dict[str, Any], request_num: int = 1 + ) -> list[DiffusionResponse]: + """ + Send native /v1/videos requests: multipart ``form_data`` job create, poll until done, download content. + + For raw HTTP to video routes without polling, use ``send_videos_create_http_request``, etc. + """ + if request_num != 1: + raise NotImplementedError("Concurrent video diffusion requests are not currently implemented") + + form_data = request_config.get("form_data") + if not isinstance(form_data, dict): + raise ValueError("Video request_config must contain 'form_data'") + normalized_form_data = {key: str(value) for key, value in form_data.items() if value is not None} + files: dict[str, tuple[str, BytesIO, str]] = {} + image_reference = request_config.get("image_reference") + video_reference = request_config.get("video_reference") + if image_reference and video_reference: + raise ValueError("Only one of image_reference or video_reference can be provided") + if image_reference: + if image_reference.startswith("data:image"): + header, encoded = image_reference.split(",", 1) + content_type = header.split(";")[0].removeprefix("data:") + extension = content_type.split("/")[-1] + file_data = base64.b64decode(encoded) + files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type) + else: + normalized_form_data["image_reference"] = json.dumps({"image_url": image_reference}) + if video_reference: + if video_reference.startswith("data:video"): + header, encoded = video_reference.split(",", 1) + content_type = header.split(";")[0].removeprefix("data:") + extension = content_type.split("/")[-1] + file_data = base64.b64decode(encoded) + files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type) + else: + normalized_form_data["video_reference"] = json.dumps({"video_url": video_reference}) + + result = DiffusionResponse() + create_url = self._build_url("/v1/videos") + response = requests.post( + create_url, + data=normalized_form_data, + files=files, + headers={"Accept": "application/json"}, + timeout=60, + ) + start_time = time.perf_counter() + response.raise_for_status() + job_data = response.json() + video_id = job_data["id"] + self._wait_until_video_completed(video_id) + end_time = time.perf_counter() + video_content = self._download_video_content(video_id) + result.success = True + result.videos = [video_content] + result.e2e_latency = end_time - start_time + assert_diffusion_response(result, request_config, run_level=self.run_level) + if result.e2e_latency is not None: + self._print_client_stat(f"[diffusion] request#1 success in {result.e2e_latency:.3f}s") + else: + self._print_client_stat("[diffusion] request#1 completed") + return [result] + + def _post_json_endpoint( + self, + path: str, + request_config: dict[str, Any], + *, + default_timeout: float, + ) -> requests.Response: + url = self._build_url(path) + timeout = float(request_config.get("timeout", default_timeout)) + if "raw_body" in request_config: + raw = request_config["raw_body"] + payload = raw.encode("utf-8") if isinstance(raw, str) else raw + return requests.post( + url, + data=payload, + headers={"Content-Type": "application/json", "Accept": "application/json"}, + timeout=timeout, + ) + if "json" not in request_config: + raise ValueError(f"{path} request_config must include 'json' or 'raw_body'") + return requests.post( + url, + json=request_config["json"], + headers={"Accept": "application/json", "Content-Type": "application/json"}, + timeout=timeout, + ) + + def _post_form_endpoint( + self, + path: str, + request_config: dict[str, Any], + *, + default_timeout: float = 120.0, + ) -> requests.Response: + url = self._build_url(path) + timeout = float(request_config.get("timeout", default_timeout)) + data = request_config.get("data") + files = request_config.get("files") + if data is None and not files: + data = {} + return requests.post( + url, + data=data, + files=files, + headers={"Accept": "application/json"} if not files else {"Accept": "application/json"}, + timeout=timeout, + ) + + def send_streaming_video_diffusion_request( + self, + request_config: dict[str, Any], + request_num: int = 1, + *, + timeout_seconds: float = 600.0, + ) -> list[DiffusionResponse]: + """ + Send a native ``/v1/realtime/video`` WebSocket request and return one + finalized MP4 artifact assembled from the streamed binary fragments. + """ + if request_num != 1: + raise NotImplementedError("Concurrent streaming video diffusion requests are not currently implemented") + + response = asyncio.run( + self._send_streaming_video_diffusion_request_once( + request_config, + timeout_seconds=timeout_seconds, + ) + ) + assert_diffusion_response(response, request_config, run_level=self.run_level) + if response.e2e_latency is not None: + self._print_client_stat(f"[diffusion.stream] request#1 success in {response.e2e_latency:.3f}s") + else: + self._print_client_stat("[diffusion.stream] request#1 completed") + return [response] + + async def _send_streaming_video_diffusion_request_once( + self, + request_config: dict[str, Any], + *, + timeout_seconds: float, + ) -> DiffusionResponse: + form_data = request_config.get("form_data") + if not isinstance(form_data, dict): + raise ValueError("Video request_config must contain 'form_data'") + payload: dict[str, Any] = { + "type": "session.start", + **{key: value for key, value in form_data.items() if value is not None}, + } + model = request_config.get("model") + if model is not None: + payload["model"] = model + payload.setdefault("format", "m4s") + + fps = float(payload.get("fps") or 16) + stream_format = payload["format"] + url = self._build_ws_url("/v1/realtime/video") + + result = DiffusionResponse() + chunks: list[bytes] = [] + start_time = time.perf_counter() + deadline = start_time + timeout_seconds + + import websockets + + async with websockets.connect(url, max_size=None) as websocket: + await websocket.send(json.dumps(payload)) + + while True: + remaining = deadline - time.perf_counter() + if remaining <= 0: + raise TimeoutError(f"Streaming video request did not complete within {timeout_seconds}s") + + message = await asyncio.wait_for(websocket.recv(), timeout=remaining) + if isinstance(message, bytes): + chunks.append(message) + continue + + msg = json.loads(message) + msg_type = msg.get("type") + if msg_type == "video.start": + stream_format = msg.get("format") or stream_format + continue + if msg_type == "session.done": + break + if msg_type == "error": + raise RuntimeError(str(msg.get("message", msg))) + + from vllm_omni.diffusion.utils.media_utils import finalize_streaming_video_bytes + from vllm_omni.entrypoints.openai.video_api_utils import StreamingVideoFormat + + streamed_bytes = b"".join(chunks) + if not streamed_bytes: + raise RuntimeError("Streaming video request completed without binary video chunks") + result.videos = [ + finalize_streaming_video_bytes( + streamed_bytes, + input_format=cast(StreamingVideoFormat, stream_format), + fps=fps, + ) + ] + result.e2e_latency = time.perf_counter() - start_time + result.success = True + return result + + def _wait_until_video_completed( + self, video_id: str, poll_interval_seconds: int = 2, timeout_seconds: int = 300 + ) -> None: + status_url = self._build_url(f"/v1/videos/{video_id}") + deadline = time.monotonic() + timeout_seconds + while time.monotonic() < deadline: + status_resp = requests.get(status_url, headers={"Accept": "application/json"}, timeout=30) + status_resp.raise_for_status() + status_data = status_resp.json() + current_status = status_data["status"] + if current_status == "completed": + return + if current_status == "failed": + error_msg = status_data.get("last_error", "Unknown error") + raise RuntimeError(f"Job failed: {error_msg}") + time.sleep(poll_interval_seconds) + raise TimeoutError(f"Video job {video_id} did not complete within {timeout_seconds}s") + + def _download_video_content(self, video_id: str) -> bytes: + download_url = self._build_url(f"/v1/videos/{video_id}/content") + video_resp = requests.get(download_url, stream=True, timeout=60) + video_resp.raise_for_status() + video_bytes = BytesIO() + for chunk in video_resp.iter_content(chunk_size=8192): + if chunk: + video_bytes.write(chunk) + return video_bytes.getvalue() + + def _build_url(self, path: str) -> str: + return f"{self.base_url.rstrip('/')}/{path.lstrip('/')}" + + +class OfflineOmniClient: + def __init__(self, omni_runner: OmniRunner): + self.runner = omni_runner + + def _process_omni_output(self, outputs: list[OmniRequestOutput]) -> OmniResponse: + result = OmniResponse() + try: + text_content = None + audio_content = None + for stage_output in outputs: + if getattr(stage_output, "final_output_type", None) == "text": + text_content = stage_output.outputs[0].text + if getattr(stage_output, "final_output_type", None) == "audio": + audio_content = stage_output.outputs[0].multimodal_output["audio"] + result.audio_content = audio_content + result.text_content = text_content + result.success = True + except Exception as e: + msg = f"Output processing error: {str(e)}" + result.success = False + print(f"Error: {msg}") + return result + + def _process_diffusion_output(self, outputs: list[OmniRequestOutput]) -> DiffusionResponse: + result = DiffusionResponse() + output = outputs[0] + if isinstance(output.images[0], list): + # Returning frames of images as a video + result.videos = output.images + else: + # Returning actual images + result.images = output.images + # [TODO] Add audio processing when tests are introduced + result.success = True + return result + + def send_omni_request(self, request_config: dict[str, Any] | None = None) -> OmniResponse: + if request_config is None: + request_config = {} + prompts_value = request_config.get("prompts") + prompts: str | list[str] = prompts_value if isinstance(prompts_value, (str, list)) else "" + videos = request_config.get("videos") + images = request_config.get("images") + audios = request_config.get("audios") + modalities = request_config.get("modalities", ["text", "audio"]) + outputs = self.runner.generate_multimodal( + prompts=prompts, videos=videos, images=images, audios=audios, modalities=modalities + ) + response = self._process_omni_output(outputs) + assert_omni_response(response, request_config, run_level="core_model") + return response + + def send_diffusion_request(self, request_config: dict[str, Any]) -> DiffusionResponse: + prompt = request_config.get("prompt") + if prompt is None: + prompts = request_config.get("prompts") + if not prompts: + raise ValueError("request_config must contain a prompt") + if len(prompts) > 1: + raise ValueError( + "In the current internal data structure, " + "only one prompt is supported for diffusion requests. " + "Because one prompt can contain multiple images or videos, " + "the current internal data structure is ambiguous when multiple prompts are provided." + ) + prompt = prompts[0] + + # Sync previous `extra_body` field with sampling params object + sampling_params: OmniDiffusionSamplingParams | None = request_config.get("sampling_params") + if sampling_params is None: + extra_body = request_config.get("extra_body", {}) + if extra_body: + sampling_params = OmniDiffusionSamplingParams(**extra_body) + else: + extra_body = asdict(sampling_params) + request_config["extra_body"] = extra_body + if not extra_body: + logger.warning("No sampling params provided in request_config, will skip output assertion") + + negative_prompt = extra_body.get("negative_prompt") or request_config.get("negative_prompt") + videos = request_config.get("videos") + images = request_config.get("images") + audios = request_config.get("audios") + # Full dict (e.g. image + mask_image for inpainting) or partial; merged with top-level image/video keys. + extra_multi_modal = request_config.get("multi_modal_data") + modalities = request_config.get("modalities") # only used by limited models. Do not add default value here + + prompt_object = OmniTextPrompt(prompt=prompt) + if negative_prompt: + prompt_object["negative_prompt"] = negative_prompt + multi_modal: dict = {} + if extra_multi_modal is not None: + multi_modal.update(dict(extra_multi_modal)) + if videos is not None: + multi_modal["video"] = videos + if images is not None: + multi_modal["image"] = images + if audios is not None: + multi_modal["audio"] = audios + if multi_modal: + prompt_object["multi_modal_data"] = multi_modal + if modalities: + prompt_object["modalities"] = modalities # pyright: ignore[reportGeneralTypeIssues] + + start_time = time.perf_counter() + raw_outputs = self.runner.generate([prompt_object], [sampling_params] if sampling_params else None) + end_time = time.perf_counter() + if not isinstance(raw_outputs, list): + raw_outputs = list(raw_outputs) + + response = self._process_diffusion_output(raw_outputs) + response.e2e_latency = end_time - start_time + assert_diffusion_response(response, request_config, run_level="core_model") + return response + + def send_audio_speech_request(self, request_config: dict[str, Any]) -> OmniResponse: + """ + Offline TTS: text -> audio via generate_multimodal, then validate with assert_audio_speech_response. + + request_config must contain: + - 'input' or 'prompts': text to synthesize. + Optional keys: + - 'voice' -> speaker (CustomVoice) + - 'task_type' -> task_type in additional_information (default: "CustomVoice") + - 'language' -> language in additional_information (default: "Auto") + - 'max_new_tokens' -> max_new_tokens in additional_information (default: 2048) + - 'response_format' -> desired audio format (used only for assertion) + """ + input_text = request_config.get("input") or request_config.get("prompts") + if input_text is None: + raise ValueError("request_config must contain 'input' or 'prompts' for TTS") + if isinstance(input_text, list): + input_text = input_text[0] if input_text else "" + + mm_processor_kwargs: dict[str, Any] = {} + if "voice" in request_config: + mm_processor_kwargs["speaker"] = request_config["voice"] + if "task_type" in request_config: + mm_processor_kwargs["task_type"] = request_config["task_type"] + if "ref_audio" in request_config: + mm_processor_kwargs["ref_audio"] = request_config["ref_audio"] + if "ref_text" in request_config: + mm_processor_kwargs["ref_text"] = request_config["ref_text"] + if "language" in request_config: + mm_processor_kwargs["language"] = request_config["language"] + if "max_new_tokens" in request_config: + mm_processor_kwargs["max_new_tokens"] = request_config["max_new_tokens"] + + outputs = self.runner.generate_multimodal( + prompts=input_text, + modalities=["audio"], + mm_processor_kwargs=mm_processor_kwargs or None, + ) + mm_out: dict[str, Any] | None = None + for stage_out in outputs: + if getattr(stage_out, "final_output_type", None) == "audio": + mm_out = stage_out.outputs[0].multimodal_output + break + if mm_out is None: + raise AssertionError("No audio output from pipeline") + + audio_data = mm_out.get("audio") + if audio_data is None: + raise AssertionError("No audio tensor in multimodal output") + + sr_raw = mm_out.get("sr") + sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw + sr = int(sr_val.item() if hasattr(sr_val, "item") else sr_val) + wav_tensor = torch.cat(audio_data, dim=-1) if isinstance(audio_data, list) else audio_data + wav_buf = io.BytesIO() + sf.write( + wav_buf, + wav_tensor.float().cpu().numpy().reshape(-1), + samplerate=sr, + format="WAV", + subtype="PCM_16", + ) + result = OmniResponse(success=True, audio_bytes=wav_buf.getvalue(), audio_format="audio/wav") + assert_audio_speech_response(result, request_config, run_level="core_model") + return result + + def start_profile(self, profile_prefix: str | None = None, stages: list[int] | None = None) -> list[Any]: + return self.runner.start_profile(profile_prefix=profile_prefix, stages=stages) + + def stop_profile(self, stages: list[int] | None = None) -> list[Any]: + return self.runner.stop_profile(stages=stages) + + +# Backward-compatible aliases (historical test fixture / type names). +OpenAIClientHandler = OnlineOmniClient +OmniRunnerHandler = OfflineOmniClient + + +__all__ = [ + "DiffusionResponse", + "HttpResponse", + "WebSocketJsonResponse", + "OpenPIWebSocketResponse", + "build_openpi_droid_observation", + "build_dreamzero_demo_observations", + "DREAMZERO_ACTION_DIM", + "DREAMZERO_ACTION_HORIZON", + "DREAMZERO_CAMERA_FILES", + "DREAMZERO_DEFAULT_PROMPT", + "load_dreamzero_camera_frames", + "OpenPIWebSocketSession", + "OmniResponse", + "OfflineOmniClient", + "OnlineOmniClient", + "OpenAIClientHandler", + "OmniRunnerHandler", +] diff --git a/tests/helpers/env.py b/tests/helpers/env.py deleted file mode 100644 index 39cec30e7..000000000 --- a/tests/helpers/env.py +++ /dev/null @@ -1,297 +0,0 @@ -"""Test environment / lifecycle helpers (device cleanup hooks and memory monitoring for tests). - -``vllm_omni.platforms`` is imported only inside functions that need it so importing this module -at pytest plugin load does not run before session autouse fixtures. -""" - -from __future__ import annotations - -import gc -import os -import subprocess -import threading -import time -from contextlib import contextmanager - -import torch - -from vllm_omni.platforms import current_omni_platform - - -def get_physical_device_indices(devices): - visible_devices = os.environ.get("CUDA_VISIBLE_DEVICES") - if visible_devices is None: - return devices - visible_indices = [int(x) for x in visible_devices.split(",")] - index_mapping = {i: physical for i, physical in enumerate(visible_indices)} - return [index_mapping[i] for i in devices if i in index_mapping] - - -def pick_least_used_device_indices(num_devices: int) -> list[int]: - """Return physical device indices for the least-used accelerators. - - Args: - num_devices: Number of device indices to return. - - Returns: - Physical device indices sorted from least to most used memory. - """ - if num_devices <= 0: - raise ValueError(f"num_devices must be positive, got {num_devices}.") - - logical_count = current_omni_platform.get_device_count() - if logical_count < num_devices: - raise RuntimeError(f"Need {num_devices} devices, but only {logical_count} are available.") - - device_usage: list[tuple[int, int]] = [] - for device_index in range(logical_count): - with current_omni_platform.device(device_index): - free_bytes, total_bytes = current_omni_platform.get_device_memory() - device_usage.append((total_bytes - free_bytes, device_index)) - device_usage.sort() - logical_indices = [device_index for _, device_index in device_usage[:num_devices]] - return get_physical_device_indices(logical_indices) - - -def wait_for_gpu_memory_to_clear( - *, - devices: list[int], - threshold_bytes: int | None = None, - threshold_ratio: float | None = None, - timeout_s: float = 120, -) -> None: - assert threshold_bytes is not None or threshold_ratio is not None - devices = get_physical_device_indices(devices) - start_time = time.time() - - device_list = ", ".join(str(d) for d in devices) - if threshold_bytes is not None: - condition_str = f"Memory usage ≤ {threshold_bytes / 2**30:.2f} GiB" - - def is_free(used, total): - return used <= threshold_bytes / 2**30 - else: - condition_str = f"Memory usage ratio ≤ {threshold_ratio * 100:.1f}%" - - def is_free(used, total): - return used / total <= threshold_ratio - - print(f"[GPU Memory Monitor] Waiting for GPU {device_list} to free memory, Condition: {condition_str}") - - @contextmanager - def smi_scope(): - if current_omni_platform.is_rocm(): - from amdsmi import amdsmi_init, amdsmi_shut_down - - amdsmi_init() - try: - yield - finally: - amdsmi_shut_down() - elif current_omni_platform.is_cuda(): - from vllm.third_party.pynvml import nvmlInit, nvmlShutdown - - nvmlInit() - try: - yield - finally: - nvmlShutdown() - else: - yield - - def get_mem_gib(device: int) -> tuple[float, float]: - if current_omni_platform.is_rocm(): - from amdsmi import amdsmi_get_gpu_vram_usage, amdsmi_get_processor_handles - - info = amdsmi_get_gpu_vram_usage(amdsmi_get_processor_handles()[device]) - return info["vram_used"] / 2**10, info["vram_total"] / 2**10 - if current_omni_platform.is_npu(): - free_bytes, total_bytes = torch.npu.mem_get_info(device) - return (total_bytes - free_bytes) / 2**30, total_bytes / 2**30 - from vllm.third_party.pynvml import nvmlDeviceGetHandleByIndex, nvmlDeviceGetMemoryInfo - - info = nvmlDeviceGetMemoryInfo(nvmlDeviceGetHandleByIndex(device)) - return info.used / 2**30, info.total / 2**30 - - with smi_scope(): - while True: - output_raw = {d: get_mem_gib(d) for d in devices} - output = { - d: f"{used:.1f}GiB/{total:.1f}GiB ({(used / total) * 100 if total > 0 else 0:.1f}%)" - for d, (used, total) in output_raw.items() - } - - print("[GPU Memory Status] Current usage:") - for device_id, mem_info in output.items(): - print(f" GPU {device_id}: {mem_info}") - - dur_s = time.time() - start_time - if all(is_free(used, total) for used, total in output_raw.values()): - print(f"[GPU Memory Freed] Devices {device_list} meet memory condition") - print(f" Condition: {condition_str}") - print(f" Wait time: {dur_s:.1f} seconds ({dur_s / 60:.1f} minutes)") - break - - if dur_s >= timeout_s: - raise ValueError( - f"[GPU Memory Timeout] Devices {device_list} still don't meet memory condition after {dur_s:.1f} seconds\n" - f"Condition: {condition_str}\n" - f"Current status:\n" + "\n".join(f" GPU {d}: {output[d]}" for d in devices) - ) - - gc.collect() - current_omni_platform.empty_cache() - time.sleep(5) - - -def _run_smi(label: str, cmd: list[str], head_lines: int, timeout: float = 5) -> None: - print("\n" + "=" * 80) - print(label) - print("=" * 80) - try: - result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout) - if result.returncode == 0 and result.stdout.strip(): - lines = result.stdout.strip().split("\n") - for line in lines[:head_lines]: - print(line) - if len(lines) > head_lines: - print(f"... (showing first {head_lines} of {len(lines)} lines)") - else: - print(f"{cmd[0]} command failed or produced no output") - except (subprocess.TimeoutExpired, FileNotFoundError): - print(f"{cmd[0]} not available or timed out") - except Exception as e: - print(f"Error running {' '.join(cmd)}: {e}") - - -def _print_gpu_processes() -> None: - """Print device information via nvidia-smi/npu-smi/amd-smi.""" - from vllm_omni.platforms import current_omni_platform - - if current_omni_platform.is_cuda(): - _run_smi("NVIDIA GPU Information (nvidia-smi)", ["nvidia-smi"], 20) - _run_smi("Detailed GPU Processes (nvidia-smi pmon)", ["nvidia-smi", "pmon", "-c", "1"], 100, timeout=3) - elif current_omni_platform.is_npu(): - _run_smi("Ascend NPU Information (npu-smi info)", ["npu-smi", "info"], 40) - elif current_omni_platform.is_rocm(): - _run_smi("AMD GPU Information (amd-smi)", ["amd-smi"], 30) - _run_smi("Detailed AMD GPU Processes (amd-smi process)", ["amd-smi", "process"], 100, timeout=3) - else: - print("\n" + "=" * 80) - print("WARNING: No supported device platform detected") - print("=" * 80) - - print("\n" + "=" * 80) - print("System Processes with GPU keywords") - print("=" * 80) - - -def _cleanup_stale_device_locks() -> None: - """Remove stale device-initialization lock files whose recorded PID is dead. - - Lock files at ``/tmp/vllm_omni_device_*_init.lock`` may persist after a - crashed / killed test run and block subsequent orchestrator startups. - """ - import glob as _glob - - for lock_file in _glob.glob("/tmp/vllm_omni_device_*_init.lock"): - try: - with open(lock_file) as fh: - content = fh.read().strip() - if not content: - continue - pid = int(content) - except (OSError, ValueError): - continue - - try: - os.kill(pid, 0) - except ProcessLookupError: - print(f" Removing stale device lock {lock_file} (PID {pid} is dead)") - try: - os.unlink(lock_file) - except OSError: - pass - except PermissionError: - pass - - -def run_pre_test_cleanup() -> None: - print("Pre-test GPU status:") - - _cleanup_stale_device_locks() - - num_gpus = current_omni_platform.device_count() - if num_gpus > 0: - try: - wait_for_gpu_memory_to_clear( - devices=list(range(num_gpus)), - threshold_ratio=0.05, - timeout_s=60, - ) - except Exception as e: - print(f"Pre-test cleanup note: {e}") - - -def run_post_test_cleanup() -> None: - if current_omni_platform.is_available(): - gc.collect() - current_omni_platform.empty_cache() - - print("Post-test GPU status:") - _print_gpu_processes() - - -class DeviceMemoryMonitor: - """Poll global device memory usage.""" - - def __init__(self, device_index: int, interval: float = 0.05): - self.device_index = device_index - self.interval = interval - self._peak_used_mb = 0.0 - self._stop_event = threading.Event() - self._thread: threading.Thread | None = None - - def start(self) -> None: - from vllm_omni.platforms import current_omni_platform - - def monitor_loop() -> None: - while not self._stop_event.is_set(): - try: - with current_omni_platform.device(self.device_index): - free_bytes, total_bytes = current_omni_platform.mem_get_info() - used_mb = (total_bytes - free_bytes) / (1024**2) - self._peak_used_mb = max(self._peak_used_mb, used_mb) - except Exception: - pass - time.sleep(self.interval) - - self._thread = threading.Thread(target=monitor_loop, daemon=False) - self._thread.start() - - def stop(self) -> None: - if self._thread is None: - return - self._stop_event.set() - self._thread.join(timeout=2.0) - - @property - def peak_used_mb(self) -> float: - from vllm_omni.platforms import current_omni_platform - - fallback_alloc = current_omni_platform.max_memory_allocated(device=self.device_index) / (1024**2) - fallback_reserved = current_omni_platform.max_memory_reserved(device=self.device_index) / (1024**2) - return max(self._peak_used_mb, fallback_alloc, fallback_reserved) - - def __del__(self): - self.stop() - - -__all__ = [ - "DeviceMemoryMonitor", - "get_physical_device_indices", - "pick_least_used_device_indices", - "run_post_test_cleanup", - "run_pre_test_cleanup", - "wait_for_gpu_memory_to_clear", -] diff --git a/tests/helpers/fixtures/clean.py b/tests/helpers/fixtures/clean.py new file mode 100644 index 000000000..c07f563bb --- /dev/null +++ b/tests/helpers/fixtures/clean.py @@ -0,0 +1,49 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Opt-in cleanup fixtures for GPU memory, pipeline registry, and speaker cache.""" + +from __future__ import annotations + +import pytest + +from vllm_omni.config.pipeline_registry import OMNI_PIPELINES + + +@pytest.fixture +def clean_gpu_memory_between_tests(): + """Opt-in GPU pre/post hooks for a test (no environment-variable gate). + + Use as a test parameter or ``@pytest.mark.usefixtures("clean_gpu_memory_between_tests")``. + """ + from tests.helpers.clean import cleanup_test_environment + + print("\n=== PRE-TEST DEVICE CLEANUP ===") + cleanup_test_environment() + yield + cleanup_test_environment() + + +@pytest.fixture +def clean_pipeline_registry(): + """Ensure the OMNI_PIPELINES are in a clean state for a test that mutates it.""" + snapshot = dict(OMNI_PIPELINES) + yield + OMNI_PIPELINES.clear() + OMNI_PIPELINES.update(snapshot) + + +@pytest.fixture +def clean_speaker_cache(): + """Reset the process-wide speaker cache singleton before and after the test.""" + import vllm_omni.utils.speaker_cache as _sc + + def _reset(): + with _sc._SINGLETON_LOCK: + if _sc._SINGLETON is not None: + _sc._SINGLETON.clear() + _sc._SINGLETON = None + + _reset() + yield + _reset() diff --git a/tests/helpers/fixtures/config.py b/tests/helpers/fixtures/config.py index 2e309b672..2f46004a6 100644 --- a/tests/helpers/fixtures/config.py +++ b/tests/helpers/fixtures/config.py @@ -1,14 +1,47 @@ -"""Fixtures config creation & out of tree registry management.""" +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Session-scoped config and environment fixtures.""" + +from __future__ import annotations + +import os import pytest - -from vllm_omni.config.pipeline_registry import OMNI_PIPELINES +import torch +from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config -@pytest.fixture -def clean_pipeline_registry(): - """Ensure the OMNI_PIPELINES are in a clean state for a test that mutates it.""" - snapshot = dict(OMNI_PIPELINES) +@pytest.fixture(scope="session", autouse=True) +def default_env(): + # Keep behavior but avoid import-time side effects (RFC #2299). + keys = ("VLLM_WORKER_MULTIPROC_METHOD", "VLLM_TARGET_DEVICE") + previous = {key: os.environ.get(key) for key in keys} + os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = previous["VLLM_WORKER_MULTIPROC_METHOD"] or "spawn" + os.environ["VLLM_TARGET_DEVICE"] = previous["VLLM_TARGET_DEVICE"] or ( + "cuda" if torch.cuda.is_available() and torch.accelerator.device_count() > 0 else "cpu" + ) yield - OMNI_PIPELINES.clear() - OMNI_PIPELINES.update(snapshot) + for key, value in previous.items(): + if value is None: + os.environ.pop(key, None) + else: + os.environ[key] = value + + +@pytest.fixture(scope="session", autouse=True) +def default_vllm_config(): + """Set a default VllmConfig for the whole test session. + + Session scope ensures module-scoped fixtures (e.g. ``omni_runner``) and + deferred imports of ``tests.helpers.runtime`` both see the same context. + Function-scoped autouse ran too late for ``OmniRunner`` setup and could + desynchronize vLLM init vs request preprocessing (e.g. renderer state). + """ + # Use CPU device if no GPU is available (e.g., in CI environments) + has_gpu = torch.cuda.is_available() and torch.accelerator.device_count() > 0 + device = "cuda" if has_gpu else "cpu" + device_config = DeviceConfig(device=device) + + with set_current_vllm_config(VllmConfig(device_config=device_config)): + yield diff --git a/tests/helpers/fixtures/env.py b/tests/helpers/fixtures/env.py deleted file mode 100644 index 2d38e4907..000000000 --- a/tests/helpers/fixtures/env.py +++ /dev/null @@ -1,74 +0,0 @@ -import os - -import pytest -import torch -from vllm.distributed import parallel_state - - -@pytest.fixture(scope="session", autouse=True) -def default_env(): - # Keep behavior but avoid import-time side effects (RFC #2299). - keys = ("VLLM_WORKER_MULTIPROC_METHOD", "VLLM_TARGET_DEVICE") - previous = {key: os.environ.get(key) for key in keys} - os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = previous["VLLM_WORKER_MULTIPROC_METHOD"] or "spawn" - os.environ["VLLM_TARGET_DEVICE"] = previous["VLLM_TARGET_DEVICE"] or ( - "cuda" if torch.cuda.is_available() and torch.accelerator.device_count() > 0 else "cpu" - ) - yield - for key, value in previous.items(): - if value is None: - os.environ.pop(key, None) - else: - os.environ[key] = value - - -@pytest.fixture(scope="session") -def model_prefix() -> str: - prefix = os.environ.get("MODEL_PREFIX", "") - return f"{prefix.rstrip('/')}/" if prefix else "" - - -@pytest.fixture -def clean_gpu_memory_between_tests(): - """Opt-in GPU pre/post hooks for a test (no environment-variable gate). - - Use as a test parameter or ``@pytest.mark.usefixtures("clean_gpu_memory_between_tests")``. - """ - from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup - - print("\n=== PRE-TEST GPU CLEANUP ===") - run_pre_test_cleanup() - yield - run_post_test_cleanup() - - -@pytest.fixture(scope="session", autouse=True) -def default_vllm_config(): - """Set a default VllmConfig for the whole test session. - - Session scope ensures module-scoped fixtures (e.g. ``omni_runner``) and - deferred imports of ``tests.helpers.runtime`` both see the same context. - Function-scoped autouse ran too late for ``OmniRunner`` setup and could - desynchronize vLLM init vs request preprocessing (e.g. renderer state). - """ - from vllm.config import DeviceConfig, VllmConfig, set_current_vllm_config - - # Use CPU device if no GPU is available (e.g., in CI environments) - has_gpu = torch.cuda.is_available() and torch.accelerator.device_count() > 0 - device = "cuda" if has_gpu else "cpu" - device_config = DeviceConfig(device=device) - - with set_current_vllm_config(VllmConfig(device_config=device_config)): - yield - - -@pytest.fixture() -def init_fake_tp_group(mocker): - """Provide a fake TP group so vllm linear layers can be instantiated.""" - mock_tp = mocker.MagicMock() - mock_tp.world_size = 1 - mock_tp.rank_in_group = 0 - old = parallel_state._TP - parallel_state._TP = mock_tp - yield - parallel_state._TP = old diff --git a/tests/helpers/fixtures/runtime.py b/tests/helpers/fixtures/runtime.py index 3bffa475d..ead132fd3 100644 --- a/tests/helpers/fixtures/runtime.py +++ b/tests/helpers/fixtures/runtime.py @@ -1,10 +1,13 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Runtime fixtures (OmniRunner / OmniServer). Imports are deferred to fixture time. Loading ``tests.helpers.runtime`` at plugin import time (before session fixtures) pulls in vLLM/vllm_omni too early and breaks initialization order vs the legacy -monolithic conftest. Defer imports until fixtures run so ``default_env`` / -``default_vllm_config`` run first. Implementation helpers live in -``tests.helpers.runtime`` (``iter_omni_server`` / ``iter_omni_runner``). +monolithic conftest. Defer imports until fixtures run so session fixtures in +``tests.helpers.fixtures.config`` run first. Server/runner helpers live in +``tests.helpers.runtime``; request clients live in ``tests.helpers.client``. """ from __future__ import annotations @@ -25,15 +28,14 @@ omni_fixture_lock = threading.Lock() def omni_server_function( request: pytest.FixtureRequest, run_level: str, - model_prefix: str, ) -> Generator[OmniServer, Any, None]: from tests.helpers.runtime import iter_omni_server - yield from iter_omni_server(request, run_level, model_prefix, omni_fixture_lock) + yield from iter_omni_server(request, run_level, omni_fixture_lock) @pytest.fixture(scope="module") -def omni_server(request: pytest.FixtureRequest, run_level: str, model_prefix: str) -> Generator[OmniServer, Any, None]: +def omni_server(request: pytest.FixtureRequest, run_level: str) -> Generator[OmniServer, Any, None]: """Start vLLM-Omni through the standard or stage-CLI launcher. The fixture stays module-scoped because multi-stage initialization is costly. @@ -42,16 +44,16 @@ def omni_server(request: pytest.FixtureRequest, run_level: str, model_prefix: st """ from tests.helpers.runtime import iter_omni_server - yield from iter_omni_server(request, run_level, model_prefix, omni_fixture_lock) + yield from iter_omni_server(request, run_level, omni_fixture_lock) @pytest.fixture -def openai_client(request: pytest.FixtureRequest, run_level: str): +def online_client(request: pytest.FixtureRequest, run_level: str): """Resolve ``omni_server`` lazily so parametrized server fixtures work like upstream.""" - from tests.helpers.runtime import OpenAIClientHandler + from tests.helpers.client import OnlineOmniClient server = request.getfixturevalue("omni_server") - return OpenAIClientHandler( + return OnlineOmniClient( host=server.host, port=server.port, api_key="EMPTY", @@ -61,12 +63,18 @@ def openai_client(request: pytest.FixtureRequest, run_level: str): @pytest.fixture -def openai_client_function(request: pytest.FixtureRequest, run_level: str): +def openai_client(online_client): + """Historical alias for :func:`online_client`.""" + return online_client + + +@pytest.fixture +def online_client_function(request: pytest.FixtureRequest, run_level: str): """Resolve ``omni_server_function`` lazily for function-scoped reliability tests.""" - from tests.helpers.runtime import OpenAIClientHandler + from tests.helpers.client import OnlineOmniClient server = request.getfixturevalue("omni_server_function") - return OpenAIClientHandler( + return OnlineOmniClient( host=server.host, port=server.port, api_key="EMPTY", @@ -75,10 +83,15 @@ def openai_client_function(request: pytest.FixtureRequest, run_level: str): ) +@pytest.fixture +def openai_client_function(online_client_function): + """Historical alias for :func:`online_client_function`.""" + return online_client_function + + @pytest.fixture(scope="function") def omni_runner_function( request: pytest.FixtureRequest, - model_prefix: str, run_level: str, ) -> Generator[OmniRunner, Any, None]: """Function-scoped :class:`~tests.helpers.runtime.OmniRunner` (cf. :func:`omni_server_function`). @@ -88,30 +101,42 @@ def omni_runner_function( """ from tests.helpers.runtime import iter_omni_runner - yield from iter_omni_runner(request, model_prefix, run_level, omni_fixture_lock) + yield from iter_omni_runner(request, run_level, omni_fixture_lock) @pytest.fixture(scope="module") -def omni_runner(request: pytest.FixtureRequest, model_prefix: str, run_level: str) -> Generator[OmniRunner, Any, None]: +def omni_runner(request: pytest.FixtureRequest, run_level: str) -> Generator[OmniRunner, Any, None]: """Module-scoped :class:`~tests.helpers.runtime.OmniRunner` (cf. :func:`omni_server`). Reuses one runner for the whole module to amortize multi-stage init cost. """ from tests.helpers.runtime import iter_omni_runner - yield from iter_omni_runner(request, model_prefix, run_level, omni_fixture_lock) + yield from iter_omni_runner(request, run_level, omni_fixture_lock) @pytest.fixture -def omni_runner_handler_function(omni_runner_function: OmniRunner): - """Resolve :class:`~tests.helpers.runtime.OmniRunnerHandler` for :func:`omni_runner_function`.""" - from tests.helpers.runtime import OmniRunnerHandler +def offline_client_function(omni_runner_function: OmniRunner): + """Resolve :class:`~tests.helpers.client.OfflineOmniClient` for :func:`omni_runner_function`.""" + from tests.helpers.client import OfflineOmniClient - return OmniRunnerHandler(omni_runner_function) + return OfflineOmniClient(omni_runner_function) @pytest.fixture -def omni_runner_handler(omni_runner: OmniRunner): - from tests.helpers.runtime import OmniRunnerHandler +def omni_runner_handler_function(offline_client_function): + """Historical alias for :func:`offline_client_function`.""" + return offline_client_function - return OmniRunnerHandler(omni_runner) + +@pytest.fixture +def offline_client(omni_runner: OmniRunner): + from tests.helpers.client import OfflineOmniClient + + return OfflineOmniClient(omni_runner) + + +@pytest.fixture +def omni_runner_handler(offline_client): + """Historical alias for :func:`offline_client`.""" + return offline_client diff --git a/tests/helpers/fixtures/speaker_cache.py b/tests/helpers/fixtures/speaker_cache.py deleted file mode 100644 index cf7a62122..000000000 --- a/tests/helpers/fixtures/speaker_cache.py +++ /dev/null @@ -1,21 +0,0 @@ -"""Fixtures for the process-wide speaker cache singleton.""" - -from __future__ import annotations - -import pytest - - -@pytest.fixture -def fresh_speaker_cache(): - """Reset the process-wide speaker cache singleton before and after the test.""" - import vllm_omni.utils.speaker_cache as _sc - - def _reset(): - with _sc._SINGLETON_LOCK: - if _sc._SINGLETON is not None: - _sc._SINGLETON.clear() - _sc._SINGLETON = None - - _reset() - yield - _reset() diff --git a/tests/helpers/mark.py b/tests/helpers/mark.py index 676732a8e..94e7db758 100644 --- a/tests/helpers/mark.py +++ b/tests/helpers/mark.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """Pytest marks and decorators for hardware / resource selection (CUDA, ROCm, …).""" from __future__ import annotations @@ -180,10 +180,8 @@ def hardware_marks(*, res: dict[str, str], num_cards: int | dict[str, int] = 1): all_marks: list[pytest.MarkDecorator] = [] for platform, resource in res.items(): cards = num_cards_dict[platform] - if platform in ("cuda", "rocm", "xpu"): + if platform in ("cuda", "rocm", "xpu", "musa"): marks = gpu_marks(res=resource, num_cards=cards) - elif platform == "musa": - marks = musa_marks(res=resource, num_cards=cards) elif platform == "npu": marks = npu_marks(res=resource, num_cards=cards) else: diff --git a/tests/helpers/media.py b/tests/helpers/media.py index ed8ef035e..623c218ad 100644 --- a/tests/helpers/media.py +++ b/tests/helpers/media.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Synthetic media generation and media/text utilities for tests.""" import atexit @@ -7,18 +10,20 @@ import hashlib import io import logging import math +import mimetypes import multiprocessing import os import random import re import subprocess +import sys import tempfile import threading import time from concurrent.futures.process import BrokenProcessPool from contextlib import contextmanager from pathlib import Path -from typing import Any +from typing import Any, Literal, TypedDict, overload import numpy as np import soundfile as sf @@ -392,8 +397,16 @@ def generate_synthetic_video( import cv2 import imageio + class _BouncingBall(TypedDict): + x: float + y: float + vx: float + vy: float + radius: int + color_bgr: tuple[int, int, int] + num_balls = random.randint(3, 8) - balls = [] + balls: list[_BouncingBall] = [] for _ in range(num_balls): radius = min(width, height) // 8 if radius < 1: @@ -405,7 +418,16 @@ def generate_synthetic_video( vx = speed * math.cos(angle) vy = speed * math.sin(angle) color_bgr = (random.randint(50, 255), random.randint(50, 255), random.randint(50, 255)) - balls.append({"x": x, "y": y, "vx": vx, "vy": vy, "radius": radius, "color_bgr": color_bgr}) + balls.append( + { + "x": float(x), + "y": float(y), + "vx": float(vx), + "vy": float(vy), + "radius": radius, + "color_bgr": color_bgr, + } + ) video_frames = [] for _ in range(num_frames): @@ -525,27 +547,34 @@ _AUDIO_MIME_BY_SUFFIX = { } -def get_asset_path(relative_path: str | os.PathLike) -> Path: - """Resolve a path under ``tests/assets/`` to its absolute on-disk location.""" - return _TEST_ASSETS_ROOT / Path(relative_path) +def _asset_mime_type(path: Path) -> str: + mime = _AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower()) + if mime is not None: + return mime + guessed, _ = mimetypes.guess_type(path.name) + return guessed or "application/octet-stream" -def load_test_audio_data_url(relative_path: str | os.PathLike) -> str: - """Load a vendored test audio file under ``tests/assets/`` as a base64 data URL. +@overload +def get_asset_path(relative_path: str | os.PathLike, *, as_data_url: Literal[False] = False) -> Path: ... - Used by tests that need real reference audio (e.g. voice cloning) without - relying on the server's ability to fetch external URLs at request time. + +@overload +def get_asset_path(relative_path: str | os.PathLike, *, as_data_url: Literal[True]) -> str: ... + + +def get_asset_path(relative_path: str | os.PathLike, *, as_data_url: bool = False) -> Path | str: + """Resolve a path under ``tests/assets/``. + + When ``as_data_url`` is true, read the file and return a base64 data URL + suitable for embedding reference media in API requests without external URLs. """ - path = get_asset_path(relative_path) - mime = _AUDIO_MIME_BY_SUFFIX.get(path.suffix.lower(), "application/octet-stream") + path = _TEST_ASSETS_ROOT / Path(relative_path) + if not as_data_url: + return path + encoded = base64.b64encode(path.read_bytes()).decode("ascii") - return f"data:{mime};base64,{encoded}" - - -def decode_b64_image(b64: str): - img = Image.open(io.BytesIO(base64.b64decode(b64))) - img.load() - return img + return f"data:{_asset_mime_type(path)};base64,{encoded}" def concat_audio(audio_val) -> np.ndarray: @@ -658,6 +687,10 @@ def _merge_base64_audio_to_segment(base64_list: list[str]) -> _AudioBuffer: @contextmanager def _serialize_whisper_model_download(model_size: str = "small"): """Serialize Whisper cache writes across processes (Linux/Unix), per model.""" + if sys.platform == "win32": + yield + return + import fcntl lock_path = Path.home() / ".cache" / "whisper" / f".{model_size}_model_download.lock" @@ -836,12 +869,10 @@ __all__ = [ "convert_audio_bytes_to_text", "convert_audio_file_to_text", "cosine_similarity_text", - "decode_b64_image", "generate_synthetic_audio", "generate_synthetic_image", "generate_synthetic_video", "get_asset_path", - "load_test_audio_data_url", "preprocess_text", "release_audio_transcriber", ] diff --git a/tests/helpers/monitor.py b/tests/helpers/monitor.py new file mode 100644 index 000000000..3402bd186 --- /dev/null +++ b/tests/helpers/monitor.py @@ -0,0 +1,55 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Device memory monitoring helpers for tests.""" + +from __future__ import annotations + +import threading +import time + +from vllm_omni.platforms import current_omni_platform + + +class DeviceMemoryMonitor: + """Poll global device memory usage.""" + + def __init__(self, device_index: int, interval: float = 0.05): + self.device_index = device_index + self.interval = interval + self._peak_used_mb = 0.0 + self._stop_event = threading.Event() + self._thread: threading.Thread | None = None + + def start(self) -> None: + def monitor_loop() -> None: + while not self._stop_event.is_set(): + try: + with current_omni_platform.device(self.device_index): + free_bytes, total_bytes = current_omni_platform.mem_get_info() + used_mb = (total_bytes - free_bytes) / (1024**2) + self._peak_used_mb = max(self._peak_used_mb, used_mb) + except Exception: + pass + time.sleep(self.interval) + + self._thread = threading.Thread(target=monitor_loop, daemon=False) + self._thread.start() + + def stop(self) -> None: + if self._thread is None: + return + self._stop_event.set() + self._thread.join(timeout=2.0) + + @property + def peak_used_mb(self) -> float: + fallback_alloc = current_omni_platform.max_memory_allocated(device=self.device_index) / (1024**2) + fallback_reserved = current_omni_platform.max_memory_reserved(device=self.device_index) / (1024**2) + return max(self._peak_used_mb, fallback_alloc, fallback_reserved) + + def __del__(self): + self.stop() + + +__all__ = ["DeviceMemoryMonitor"] diff --git a/tests/helpers/process.py b/tests/helpers/process.py deleted file mode 100644 index 094de9652..000000000 --- a/tests/helpers/process.py +++ /dev/null @@ -1,134 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project - -import functools -import os -import signal -import subprocess -import sys -import tempfile -from collections.abc import Callable -from contextlib import ExitStack, suppress -from typing import Any, Literal - -import cloudpickle -from typing_extensions import ParamSpec -from vllm.platforms import current_platform - -_P = ParamSpec("_P") - - -def fork_new_process_for_each_test(func: Callable[_P, None]) -> Callable[_P, None]: - """Decorator to fork a new process for each test function.""" - - @functools.wraps(func) - def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None: - os.setpgrp() - from _pytest.outcomes import Skipped - - with ( - tempfile.NamedTemporaryFile( - delete=False, mode="w+b", prefix=f"vllm_test_{func.__name__}_{os.getpid()}_", suffix=".exc" - ) as exc_file, - ExitStack() as delete_after, - ): - exc_file_path = exc_file.name - delete_after.callback(os.remove, exc_file_path) - pid = os.fork() - if pid == 0: - delete_after.pop_all() - try: - func(*args, **kwargs) - except Skipped as e: - print(str(e)) - os._exit(0) - except Exception as e: - import traceback - - tb_string = traceback.format_exc() - exc_to_serialize: dict[str, Any] - try: - exc_to_serialize = {"pickled_exception": e} - cloudpickle.dumps(exc_to_serialize) - except (Exception, KeyboardInterrupt): - exc_to_serialize = { - "exception_type": type(e).__name__, - "exception_msg": str(e), - "traceback": tb_string, - } - try: - with open(exc_file_path, "wb") as f: - cloudpickle.dump(exc_to_serialize, f) - except Exception: - print(tb_string) - os._exit(1) - else: - os._exit(0) - else: - pgid = os.getpgid(pid) - _pid, _exitcode = os.waitpid(pid, 0) - old_signal_handler = signal.signal(signal.SIGTERM, signal.SIG_IGN) - os.killpg(pgid, signal.SIGTERM) - signal.signal(signal.SIGTERM, old_signal_handler) - if _exitcode != 0: - exc_info = {} - if os.path.exists(exc_file_path): - with suppress(Exception), open(exc_file_path, "rb") as f: - exc_info = cloudpickle.load(f) - if (original_exception := exc_info.get("pickled_exception")) is not None: - assert isinstance(original_exception, Exception) - raise original_exception - if (original_tb := exc_info.get("traceback")) is not None: - raise AssertionError( - f"Test {func.__name__} failed when called with args {args} and kwargs {kwargs}" - f" (exit code: {_exitcode}):\n{original_tb}" - ) from None - raise AssertionError( - f"function {func.__name__} failed when called with args {args} and kwargs {kwargs}" - f" (exit code: {_exitcode})" - ) from None - - return wrapper - - -def spawn_new_process_for_each_test(f: Callable[_P, None]) -> Callable[_P, None]: - """Decorator to spawn a new process for each test function.""" - - @functools.wraps(f) - def wrapper(*args: _P.args, **kwargs: _P.kwargs) -> None: - if os.environ.get("RUNNING_IN_SUBPROCESS") == "1": - return f(*args, **kwargs) - - import torch.multiprocessing as mp - - with suppress(RuntimeError): - mp.set_start_method("spawn") - - module_name = f.__module__ - env = os.environ.copy() - env["RUNNING_IN_SUBPROCESS"] = "1" - - with tempfile.TemporaryDirectory() as tempdir: - output_filepath = os.path.join(tempdir, "new_process.tmp") - input_bytes = cloudpickle.dumps((f, output_filepath)) - cmd = [sys.executable, "-m", f"{module_name}"] - returned = subprocess.run(cmd, input=input_bytes, capture_output=True, env=env) - try: - returned.check_returncode() - except Exception as e: - raise RuntimeError(f"Error raised in subprocess:\n{returned.stderr.decode()}") from e - - return wrapper - - -def create_new_process_for_each_test( - method: Literal["spawn", "fork"] | None = None, -) -> Callable[[Callable[_P, None]], Callable[_P, None]]: - """Creates a decorator that runs each test function in a new process.""" - if method is None: - use_spawn = current_platform.is_xpu() - method = "spawn" if use_spawn else "fork" - assert method in ["spawn", "fork"], "Method must be either 'spawn' or 'fork'" - if method == "fork": - return fork_new_process_for_each_test - return spawn_new_process_for_each_test diff --git a/tests/helpers/runtime.py b/tests/helpers/runtime.py index 0554a8fe7..db4cba893 100644 --- a/tests/helpers/runtime.py +++ b/tests/helpers/runtime.py @@ -1,16 +1,13 @@ # SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project -"""Server/client/runner runtime primitives for tests.""" +"""Server and offline runner runtime primitives for tests. + +Request clients and response types live in ``tests.helpers.client``; +this module re-exports them for backward-compatible imports. +""" -import asyncio -import base64 -import concurrent.futures -import copy import errno -import gc -import io -import json import os import socket import subprocess @@ -20,129 +17,26 @@ import threading import time from collections.abc import Generator from contextlib import contextmanager -from dataclasses import asdict, dataclass -from io import BytesIO from pathlib import Path -from typing import Any, NamedTuple, cast -from urllib.parse import quote +from typing import Any, NamedTuple import numpy as np import psutil -import requests -import soundfile as sf -import torch import yaml -from openai import APIError, OpenAI, omit -from PIL import Image -from vllm import TextPrompt, envs -from vllm.distributed.parallel_state import ( - destroy_distributed_environment, - destroy_model_parallel, -) +from vllm import TextPrompt from vllm.logger import init_logger -from tests.helpers.assertions import ( - SuccessRateGate, - assert_audio_speech_response, - assert_diffusion_response, - assert_http_error, - assert_images_generations_response, - assert_omni_response, - collect_at_success_rate, -) -from tests.helpers.env import run_post_test_cleanup, run_pre_test_cleanup +from tests.helpers.clean import cleanup_test_environment from tests.helpers.media import ( - _merge_base64_audio_to_segment, - decode_b64_image, release_audio_transcriber, ) +from tests.helpers.stage_config import load_stage_ids, load_stage_replica_counts from tests.model_tests.diffusion.utils import resolve_tiny_model_path from vllm_omni.config.stage_config import resolve_deploy_yaml -from vllm_omni.inputs.data import OmniDiffusionSamplingParams, OmniTextPrompt from vllm_omni.outputs import OmniRequestOutput -from vllm_omni.platforms import current_omni_platform logger = init_logger(__name__) - -def cleanup_dist_env_and_memory(shutdown_ray: bool = False): - # Reset environment variable cache - envs.disable_envs_cache() - - # Reset rocm_aiter_ops class variables to match current os.environ. - # These are class-level attributes that persist across tests and are - # NOT restored by monkeypatch (which only restores os.environ). - from vllm_omni.platforms import current_omni_platform - - if current_omni_platform.is_rocm(): - from vllm._aiter_ops import rocm_aiter_ops - - rocm_aiter_ops.refresh_env_variables() - - # Ensure all objects are not frozen before cleanup - gc.unfreeze() - - destroy_model_parallel() - destroy_distributed_environment() - if shutdown_ray: - import ray # Lazy import Ray - - ray.shutdown() - gc.collect() - - if not current_omni_platform.is_cpu(): - current_omni_platform.empty_cache() - try: - torch._C._host_emptyCache() - except AttributeError: - logger.warning("torch._C._host_emptyCache() only available in Pytorch >=2.5") - - -def _parse_response_json(r: requests.Response) -> dict[str, Any] | list[Any] | None: - try: - data = r.json() - if isinstance(data, (dict, list)): - return data - except Exception: - pass - return None - - -def _split_request_config_by_per_output_sizes(cfg: dict[str, Any]) -> list[dict[str, Any]] | None: - """If ``extra_body`` has list ``height``/``width``, return one config per index (scalar h/w, ``num_outputs_per_prompt=1``).""" - eb = cfg.get("extra_body") - if not eb: - return None - h, w = eb.get("height"), eb.get("width") - if (isinstance(h, (list, tuple)) or isinstance(w, (list, tuple))) and not ( - isinstance(h, (list, tuple)) and isinstance(w, (list, tuple)) - ): - raise ValueError("extra_body height and width must both be lists or both be scalars") - if not (isinstance(h, (list, tuple)) and isinstance(w, (list, tuple))): - return None - if len(h) != len(w): - raise ValueError(f"height and width lists must have equal length; got {len(h)=} {len(w)=}") - n = len(h) - n_out = eb.get("num_outputs_per_prompt") - if n_out is not None: - n_out = int(n_out) - if n_out != n: - raise ValueError( - "When height/width are lists, num_outputs_per_prompt must equal their length; " - f"got num_outputs_per_prompt={n_out}, len(lists)={n}" - ) - splits: list[dict[str, Any]] = [] - for i in range(n): - sub = copy.deepcopy(cfg) - sub_eb = dict(sub.get("extra_body") or {}) - sub_eb["height"] = int(h[i]) - sub_eb["width"] = int(w[i]) - sub_eb["num_outputs_per_prompt"] = 1 - sub["extra_body"] = sub_eb - splits.append(sub) - return splits - - PromptAudioInput = list[tuple[Any, int]] | tuple[Any, int] | None PromptImageInput = list[Any] | Any | None PromptVideoInput = list[Any] | Any | None @@ -181,27 +75,30 @@ def get_open_port(host: str = "127.0.0.1", *, max_attempts: int = 128) -> int: ) from last_exc -def get_distributed_init_method() -> str: - """Return a fresh ``file://`` init_method for ``torch.distributed`` process groups. +def get_distributed_init_method(prefix: str = "torch_dist_init_") -> str: + """Return a ``file://`` init_method for a ``torch.distributed`` process group. - Rendezvous happens through a filesystem path instead of a pre-agreed TCP port, - so there's no bind-time race to retry around: the path is unique per call and - the real rank-0 process is the only one that ever creates it. + Args: + prefix: Prefix for the temporary rendezvous filename. Defaults to + ``torch_dist_init_``. + + Returns: + An init_method string for ``torch.distributed.init_process_group``. """ - with tempfile.NamedTemporaryFile(prefix="torch_dist_init_") as f: + with tempfile.NamedTemporaryFile(prefix=prefix) as f: return f"file://{f.name}" def dummy_messages_from_mix_data( - system_prompt: dict[str, Any] = None, + system_prompt: dict[str, Any] | None = None, video_data_url: Any = None, audio_data_url: Any = None, image_data_url: Any = None, - content_text: str = None, + content_text: str | None = None, ): """Create messages with video、image、audio data URL for OpenAI API.""" if content_text is not None: - content = [{"type": "text", "text": content_text}] + content: list[dict[str, Any]] = [{"type": "text", "text": content_text}] else: content = [] @@ -264,9 +161,7 @@ class OmniServer: env_dict: dict[str, str] | None = None, use_omni: bool = True, ) -> None: - run_pre_test_cleanup() - run_post_test_cleanup() - cleanup_dist_env_and_memory() + cleanup_test_environment() self.model = model args = list(serve_args) self.serve_args = args @@ -279,7 +174,6 @@ class OmniServer: def _start_server(self) -> None: env = os.environ.copy() - env.setdefault("VLLM_WORKER_MULTIPROC_METHOD", "spawn") if self.env_dict is not None: env.update(self.env_dict) @@ -340,7 +234,7 @@ class OmniServer: if proc.status() != psutil.STATUS_ZOMBIE: return False try: - os.waitpid(proc.pid, os.WNOHANG) + os.waitpid(proc.pid, getattr(os, "WNOHANG", 0)) except ChildProcessError: pass return True @@ -464,15 +358,19 @@ class OmniServer: pass def __enter__(self): - self._start_server() + try: + self._start_server() + except BaseException: + # ``__exit__`` is not invoked when ``__enter__`` raises; roll back + # any processes / log handles already launched. + self.__exit__(None, None, None) + raise return self def __exit__(self, exc_type, exc_val, exc_tb): if self.proc: self._kill_process_tree(self.proc.pid) - run_pre_test_cleanup() - run_post_test_cleanup() - cleanup_dist_env_and_memory() + cleanup_test_environment() class OmniServerStageCli(OmniServer): @@ -491,7 +389,6 @@ class OmniServerStageCli(OmniServer): super().__init__(model, serve_args or [], port=port, env_dict=env_dict, use_omni=True) self.stage_config_path = stage_config_path self.master_port = get_open_port() - self.visible_device_list = self._load_visible_device_list(env_dict) resolved_cfg = resolve_deploy_yaml(stage_config_path) # Dump the resolved deploy config so CI logs show each stage's # gpu_memory_utilization / max_model_len / max_num_seqs after @@ -502,113 +399,15 @@ class OmniServerStageCli(OmniServer): f"{yaml.safe_dump(resolved_cfg, sort_keys=False, default_flow_style=False)}", flush=True, ) - self.stage_runtime_devices = self._load_stage_runtime_devices(resolved_cfg) - self.stage_ids = stage_ids or self._load_stage_ids(resolved_cfg) + self.stage_ids = stage_ids or load_stage_ids(resolved_cfg) if 0 not in self.stage_ids: raise ValueError(f"Stage CLI test requires stage_id=0 in config: {stage_config_path}") - self.stage_replica_counts = self._load_stage_replica_counts(resolved_cfg) + self.stage_replica_counts = load_stage_replica_counts(resolved_cfg) self.stage_procs: dict[tuple[int, int], subprocess.Popen] = {} + self._stage_log_paths: dict[tuple[int, int], Path] = {} + self._stage_log_files: dict[tuple[int, int], Any] = {} self.proc = None - @staticmethod - def _stage_entries(cfg: dict) -> list[dict]: - """Return the list of stage entries from either legacy (``stage_args``) - or new-schema (``stages``) deploy YAMLs.""" - return cfg.get("stage_args") or cfg.get("stages") or [] - - @staticmethod - def _load_stage_ids(resolved_config: dict) -> list[int]: - stage_ids = [ - stage["stage_id"] for stage in OmniServerStageCli._stage_entries(resolved_config) if "stage_id" in stage - ] - if not stage_ids: - raise ValueError("No stage IDs found in resolved config") - return stage_ids - - @staticmethod - def _load_stage_runtime_devices(resolved_config: dict) -> dict[int, str]: - runtime_devices: dict[int, str] = {} - for stage in OmniServerStageCli._stage_entries(resolved_config): - stage_id = stage.get("stage_id") - # New schema: stage.devices is flat at stage level. - # Legacy schema: stage.runtime.devices is nested. - devices = stage.get("devices") or stage.get("runtime", {}).get("devices") - if stage_id is not None and devices: - runtime_devices[int(stage_id)] = str(devices) - return runtime_devices - - @staticmethod - def _load_stage_replica_counts(resolved_config: dict) -> dict[int, int]: - replica_counts: dict[int, int] = {} - for stage in OmniServerStageCli._stage_entries(resolved_config): - stage_id = stage.get("stage_id") - if stage_id is None: - continue - replica_counts[int(stage_id)] = max( - 1, - int(stage.get("num_replicas") or stage.get("runtime", {}).get("num_replicas", 1)), - ) - return replica_counts - - @classmethod - def _parse_device_list(cls, devices: str | int) -> list[str]: - if isinstance(devices, int): - if devices < 0: - raise ValueError("Device IDs must be non-negative integers") - return [str(devices)] - return [token.strip() for token in str(devices).split(",") if token.strip()] - - @classmethod - def _load_visible_device_list(cls, env_dict: dict[str, str] | None) -> list[str] | None: - env = os.environ.copy() - if env_dict is not None: - env.update(env_dict) - - env_var = getattr(current_omni_platform, "device_control_env_var", None) - if env_var and env_var in env: - return [token.strip() for token in env[env_var].split(",") if token.strip()] - return None - - @classmethod - def _map_stage_devices(cls, stage_id: int, visible_device_list: list[str] | None, devices: str) -> str: - device_list = cls._parse_device_list(devices) - - if visible_device_list is None: - return ",".join(device_list) - - if not all(device.isdigit() for device in device_list): - raise ValueError("Logical devices must be non-negative integers") - - logical_ids = [int(device) for device in device_list] - if logical_ids and max(logical_ids) >= len(visible_device_list): - raise ValueError( - f"Stage {stage_id} has logical IDs {device_list}, one or more of which exceed the number of visible devices" - ) - - return ",".join(visible_device_list[idx] for idx in logical_ids) - - def _devices_for_replica(self, stage_id: int, devices: str, replica_id: int) -> str: - replica_count = self.stage_replica_counts.get(stage_id, 1) - if replica_count == 1: - return devices - - device_list = self._parse_device_list(devices) - if len(device_list) % replica_count != 0: - raise ValueError( - f"Stage {stage_id} has {len(device_list)} device(s) for {replica_count} replica(s); " - "device count must be divisible by replica count" - ) - devices_per_replica = len(device_list) // replica_count - start = replica_id * devices_per_replica - return ",".join(device_list[start : start + devices_per_replica]) - - def _set_stage_device_env(self, stage_id: int, env: dict[str, str], devices: str, replica_id: int = 0) -> None: - replica_devices = self._devices_for_replica(stage_id, devices, replica_id) - mapped_devices = self._map_stage_devices(stage_id, self.visible_device_list, replica_devices) - env_var = getattr(current_omni_platform, "device_control_env_var", None) - if env_var: - env[env_var] = mapped_devices - def _build_stage_cmd(self, stage_id: int, *, headless: bool, replica_id: int = 0) -> list[str]: cmd = [ sys.executable, @@ -639,7 +438,6 @@ class OmniServerStageCli(OmniServer): def _launch_stage(self, stage_id: int, *, headless: bool, replica_id: int = 0) -> None: env = os.environ.copy() - env["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn" if self.env_dict is not None: env.update(self.env_dict) @@ -649,11 +447,9 @@ class OmniServerStageCli(OmniServer): # debugging "Stage N exited before API server ready" doesn't rely on # guessing; the file is surfaced in the RuntimeError message. log_path = Path(tempfile.gettempdir()) / f"omni_stage_{stage_id}_replica_{replica_id}_{self.master_port}.log" - self._stage_log_paths = getattr(self, "_stage_log_paths", {}) stage_key = (stage_id, replica_id) self._stage_log_paths[stage_key] = log_path - log_fh = open(log_path, "w", buffering=1) # noqa: SIM115 - closed in __exit__ - self._stage_log_files = getattr(self, "_stage_log_files", {}) + log_fh = open(log_path, "w", buffering=1) # noqa: SIM115 - closed in ``__exit__`` self._stage_log_files[stage_key] = log_fh proc = subprocess.Popen( cmd, @@ -673,7 +469,7 @@ class OmniServerStageCli(OmniServer): for (stage_id, replica_id), proc in self.stage_procs.items(): ret = proc.poll() if ret is not None: - log_path = getattr(self, "_stage_log_paths", {}).get((stage_id, replica_id)) + log_path = self._stage_log_paths.get((stage_id, replica_id)) tail = "" if log_path and log_path.exists(): try: @@ -730,7 +526,7 @@ class OmniServerStageCli(OmniServer): captures them post-run. Head covers engine init; tail covers whatever state the stage was in when it was torn down. """ - log_paths = getattr(self, "_stage_log_paths", {}) or {} + log_paths = self._stage_log_paths for stage_id, replica_id in sorted(log_paths): log_path = log_paths[(stage_id, replica_id)] if not log_path or not log_path.exists(): @@ -764,1903 +560,30 @@ class OmniServerStageCli(OmniServer): proc = self.stage_procs[stage_key] if proc.poll() is None: self._kill_process_tree(proc.pid) - run_pre_test_cleanup() - run_post_test_cleanup() - cleanup_dist_env_and_memory() - -@dataclass -class OmniResponse: - """Decoded multimodal / chat output from the OpenAI SDK or offline runner (not raw ``requests``).""" - - text_content: str | None = None - audio_data: list[str] | None = None - audio_content: str | None = None - audio_format: str | None = None - audio_bytes: bytes | None = None - #: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before the - #: OpenAI client call through response parsing and local post-process (e.g. audio decode). - e2e_latency: float | None = None - success: bool = False - prompt_tokens: int | None = None - cached_tokens: int | None = None - multimodal_tokens: dict[str, int] | None = None - logprobs: list | None = None - #: HTTP status + error text for the error-handling path (e.g. validator - #: rejections); populated when the OpenAI client raises an APIError. - status_code: int | None = None - error_message: str | None = None - - -@dataclass -class DiffusionResponse: - """Decoded diffusion output from chat completions or offline runner (not raw ``requests``).""" - - text_content: str | None = None - images: list[Image.Image] | None = None - audios: list[Any] | None = None - videos: list[Any] | None = None - #: End-to-end wall time in **seconds** (``perf_counter`` delta), from just before - #: ``chat.completions.create`` through local image / audio decode. - e2e_latency: float | None = None - success: bool = False - - -@dataclass -class HttpResponse: - """Normalized view of a ``requests`` response from :class:`OpenAIClientHandler` HTTP helpers.""" - - status_code: int - success: bool - error_message: str | None = None - json_body: dict[str, Any] | list[Any] | None = None - - -@dataclass -class WebSocketJsonResponse: - """First JSON object delivered as a text WebSocket frame (streaming endpoints).""" - - json_body: dict[str, Any] - - -@dataclass -class OpenPIWebSocketResponse: - """Msgpack WebSocket session against ``/v1/realtime/robot/openpi``.""" - - server_metadata: dict[str, Any] - operation_responses: list[Any] - actions: dict[str, np.ndarray] | None = None - action_tensors: list[np.ndarray] | None = None - - -def build_openpi_droid_observation(*, session_id: str = "gr00t-smoke") -> dict[str, Any]: - """Build a minimal DROID-style observation payload for the OpenPI robot endpoint.""" - identity_eef_9d = np.zeros((1, 1, 9), dtype=np.float32) - identity_eef_9d[..., 3:] = np.array([1, 0, 0, 0, 1, 0], dtype=np.float32) - return { - "session_id": session_id, - "video": { - "exterior_image_1_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8), - "wrist_image_left": np.zeros((1, 2, 256, 256, 3), dtype=np.uint8), - }, - "state": { - "eef_9d": identity_eef_9d, - "gripper_position": np.zeros((1, 1, 1), dtype=np.float32), - "joint_position": np.zeros((1, 1, 7), dtype=np.float32), - }, - "language": {"annotation.language.language_instruction": [["pick up the object"]]}, - } - - -DREAMZERO_DEFAULT_PROMPT = ( - "Move the pan forward and use the brush in the middle of the plates to brush the inside of the pan" -) -DREAMZERO_ACTION_HORIZON = 24 -DREAMZERO_ACTION_DIM = 8 -DREAMZERO_CAMERA_FILES = { - "observation/exterior_image_0_left": "exterior_image_1_left.mp4", - "observation/exterior_image_1_left": "exterior_image_2_left.mp4", - "observation/wrist_image_left": "wrist_image_left.mp4", -} - - -def _require_opencv() -> Any: - try: - import cv2 - except ImportError as exc: # pragma: no cover - optional e2e dependency - raise ModuleNotFoundError("DreamZero OpenPI test dependencies are missing: opencv-python") from exc - return cv2 - - -def load_dreamzero_camera_frames(video_dir: Path) -> dict[str, np.ndarray]: - cv2 = _require_opencv() - camera_frames: dict[str, np.ndarray] = {} - for camera_key, file_name in DREAMZERO_CAMERA_FILES.items(): - video_path = video_dir / file_name - if not video_path.exists(): - raise FileNotFoundError(f"Missing DreamZero test asset: {video_path}") - cap = cv2.VideoCapture(str(video_path)) - frames = [] - while True: - ok, frame = cap.read() - if not ok: - break - frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) - cap.release() - if not frames: - raise RuntimeError(f"No frames loaded from {video_path}") - camera_frames[camera_key] = np.stack(frames, axis=0) - return camera_frames - - -def build_dreamzero_demo_observations( - camera_frames: dict[str, np.ndarray], - *, - prompt: str, - session_id: str, - num_chunks: int = 2, -) -> list[dict[str, Any]]: - if num_chunks < 1: - raise ValueError("num_chunks must be at least 1") - - relative_offsets = [-23, -16, -8, 0] - total_frames = min(frames.shape[0] for frames in camera_frames.values()) - frame_schedules = [[0]] - current_frame = 23 - for _ in range(num_chunks - 1): - indices = [max(current_frame + offset, 0) for offset in relative_offsets] - if indices[-1] >= total_frames: - break - frame_schedules.append(indices) - current_frame += DREAMZERO_ACTION_HORIZON - - observations: list[dict[str, Any]] = [] - for frame_indices in frame_schedules: - obs: dict[str, Any] = {} - for camera_key, all_frames in camera_frames.items(): - selected = all_frames[frame_indices] - obs[camera_key] = selected[0] if len(frame_indices) == 1 else selected - obs["observation/joint_position"] = np.zeros(7, dtype=np.float32) - obs["observation/cartesian_position"] = np.zeros(6, dtype=np.float32) - obs["observation/gripper_position"] = np.zeros(1, dtype=np.float32) - obs["prompt"] = prompt - obs["session_id"] = session_id - observations.append(obs) - return observations - - -class OpenPIWebSocketSession: - """Persistent msgpack session for ``/v1/realtime/robot/openpi``.""" - - DEFAULT_PING_INTERVAL_SECS = 300 - DEFAULT_PING_TIMEOUT_SECS = 3600 - - @staticmethod - def _require_dependencies() -> tuple[Any, Any]: - try: - import websockets.sync.client as websockets_client - except ImportError as exc: # pragma: no cover - optional e2e dependency - raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: websockets") from exc - try: - from openpi_client import msgpack_numpy - except ImportError as exc: # pragma: no cover - optional e2e dependency - raise ModuleNotFoundError("GR00T OpenPI test dependencies are missing: openpi-client") from exc - return websockets_client, msgpack_numpy - - def __init__( - self, - uri: str, - *, - ping_interval: float = DEFAULT_PING_INTERVAL_SECS, - ping_timeout: float = DEFAULT_PING_TIMEOUT_SECS, - open_timeout: float = 120.0, - close_timeout: float = 120.0, - ) -> None: - websockets_client, msgpack_numpy = self._require_dependencies() - self._msgpack_numpy = msgpack_numpy - self._packer = msgpack_numpy.Packer() - self._conn = websockets_client.connect( - uri, - compression=None, - max_size=None, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - open_timeout=open_timeout, - close_timeout=close_timeout, - ) - server_metadata = msgpack_numpy.unpackb(self._conn.recv()) - if not isinstance(server_metadata, dict): - raise TypeError(f"Expected dict metadata from {uri}, got {type(server_metadata)!r}") - self._server_metadata = server_metadata - - def get_server_metadata(self) -> dict[str, Any]: - return dict(self._server_metadata) - - def infer(self, obs: dict[str, Any]) -> dict[str, np.ndarray]: - response = self._send_operation("infer", obs) - if not isinstance(response, dict): - raise TypeError(f"Expected dict infer response, got {type(response)!r}") - return {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()} - - def reset(self, reset_info: dict[str, Any] | None = None) -> str: - response = self._send_operation("reset", dict(reset_info or {})) - return str(response["status"]) - - def close(self) -> None: - self._conn.close() - - def _send_operation(self, endpoint: str, payload: dict[str, Any]) -> Any: - body = dict(payload) - body["endpoint"] = endpoint - self._conn.send(self._packer.pack(body)) - raw = self._conn.recv() - if isinstance(raw, str): - raise RuntimeError(f"OpenPI {endpoint!r} failed: {raw}") - decoded = self._msgpack_numpy.unpackb(raw) - if isinstance(decoded, dict) and decoded.get("type") == "error": - raise RuntimeError(f"OpenPI {endpoint!r} failed: {decoded.get('message')}") - if endpoint == "reset": - if not isinstance(decoded, dict) or decoded.get("status") != "reset successful": - raise RuntimeError(f"Unexpected OpenPI reset response: {decoded!r}") - return decoded - return decoded - - -def _merge_http_expectation_kwargs( - base: dict[str, Any] | None, - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, -) -> dict[str, Any]: - cfg = dict(base or {}) - if err_code is not None: - cfg["err_code"] = err_code - if err_message is not None: - cfg["err_message"] = err_message - return cfg - - -def _merge_ws_expectation_kwargs( - base: dict[str, Any] | None, - *, - err_message: str | tuple[str, ...] | list[str] | None = None, - ws_json_type: str | None = None, - ws_error_code: str | None = None, -) -> dict[str, Any]: - cfg = dict(base or {}) - if err_message is not None: - cfg["err_message"] = err_message - if ws_json_type is not None: - cfg["ws_json_type"] = ws_json_type - if ws_error_code is not None: - cfg["ws_error_code"] = ws_error_code - return cfg - - -def _run_ws_expectations_from_request_config(cfg: dict[str, Any], resp: WebSocketJsonResponse) -> None: - jb = resp.json_body - want_type = cfg.get("ws_json_type") - if want_type is not None: - assert jb.get("type") == want_type, (jb, want_type) - want_code = cfg.get("ws_error_code") - if want_code is not None: - assert jb.get("code") == want_code, (jb, want_code) - err_message = cfg.get("err_message") - if err_message is not None: - assert_http_error(resp, err_message=err_message, websocket_json_message=True) - - -def _merge_diffusion_responses(parts: list[DiffusionResponse]) -> DiffusionResponse: - """Concatenate images in order; ``e2e_latency`` is wall-clock of the batch (set by caller) or max of parts.""" - merged = DiffusionResponse() - merged.success = all(p.success for p in parts) and len(parts) > 0 - imgs: list[Image.Image] = [] - for p in parts: - if p.images: - imgs.extend(p.images) - merged.images = imgs if imgs else None - latencies = [p.e2e_latency for p in parts if p.e2e_latency is not None] - merged.e2e_latency = max(latencies) if latencies else None - return merged - - -class OpenAIClientHandler: - def __init__( - self, - host: str = "127.0.0.1", - port: int = None, - api_key: str = "EMPTY", - run_level: str = None, - *, - log_stats: bool = True, - ): - if port is None: - port = get_open_port() - self.base_url = f"http://{host}:{port}" - self.client = OpenAI(base_url=f"http://{host}:{port}/v1", api_key=api_key) - self.run_level = run_level - self.log_stats = log_stats - - def _print_client_stat(self, message: str) -> None: - if self.log_stats: - print(message, flush=True) - - def _process_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse: - """Wall clock from *before* ``chat.completions.create`` through stream drain + local decode.""" - result = OmniResponse() - try: - text_content = "" - audio_data = [] - for chunk in chat_completion: - for choice in chunk.choices: - content = getattr(getattr(choice, "delta", None), "content", None) - modality = getattr(chunk, "modality", None) - if modality == "audio" and content: - audio_data.append(content) - elif modality == "text" and content: - text_content += content - # Usage is yielded after the last token - if chunk.usage: - result.prompt_tokens = chunk.usage.prompt_tokens - if details := getattr(chunk.usage, "prompt_tokens_details", None): - result.cached_tokens = details.cached_tokens - result.multimodal_tokens = getattr(details, "multimodal_tokens", None) - - if audio_data: - merged_seg = _merge_base64_audio_to_segment(audio_data) - wav_buf = BytesIO() - merged_seg.export(wav_buf, format="wav") - result.audio_bytes = wav_buf.getvalue() - result.text_content = text_content - result.audio_data = audio_data - result.e2e_latency = time.perf_counter() - wall_start - result.success = True - except Exception as e: - msg = f"Stream processing error: {str(e)}" - print(f"Error: {msg}") - return result - - def _process_non_stream_omni_response(self, chat_completion, *, wall_start: float) -> OmniResponse: - """Wall clock from *before* ``chat.completions.create`` through response parse + local decode.""" - result = OmniResponse() - try: - audio_data = None - text_content = None - for choice in chat_completion.choices: - if hasattr(choice.message, "audio") and choice.message.audio is not None: - audio_data = choice.message.audio.data - if hasattr(choice.message, "content") and choice.message.content is not None: - text_content = choice.message.content - # Extract cached & prompt token counts for prefix caching tests - usage = getattr(chat_completion, "usage", None) - if usage: - result.prompt_tokens = usage.prompt_tokens - if details := getattr(usage, "prompt_tokens_details", None): - result.cached_tokens = details.cached_tokens - result.multimodal_tokens = getattr(details, "multimodal_tokens", None) - if audio_data: - result.audio_bytes = base64.b64decode(audio_data) - result.text_content = text_content - result.e2e_latency = time.perf_counter() - wall_start - if chat_completion.choices and chat_completion.choices[0].logprobs is not None: - result.logprobs = chat_completion.choices[0].logprobs.content - result.success = True - except Exception as e: - msg = f"Non-stream processing error: {str(e)}" - print(f"Error: {msg}") - return result - - def _process_diffusion_response(self, chat_completion, *, wall_start: float) -> DiffusionResponse: - """Wall clock from *before* ``chat.completions.create`` through image decode.""" - result = DiffusionResponse() - try: - images = [] - audios = [] - for choice in chat_completion.choices: - content = getattr(choice.message, "content", None) - if isinstance(content, list): - for item in content: - image_url = None - if isinstance(item, dict): - image_url = item.get("image_url", {}).get("url") - else: - image_url_obj = getattr(item, "image_url", None) - image_url = getattr(image_url_obj, "url", None) if image_url_obj else None - if image_url and image_url.startswith("data:image"): - b64_data = image_url.split(",", 1)[1] - images.append(decode_b64_image(b64_data)) - - # OpenAI audio responses populate `message.audio`. - audio_obj = getattr(choice.message, "audio", None) - audio_b64 = getattr(audio_obj, "data", None) if audio_obj is not None else None - if audio_b64: - audios.append( - { - "wav_bytes": base64.b64decode(audio_b64), - "id": getattr(audio_obj, "id", None), - "expires_at": getattr(audio_obj, "expires_at", None), - } - ) - result.images = images if images else None - result.audios = audios if audios else None - result.e2e_latency = time.perf_counter() - wall_start - result.success = True - except Exception as e: - msg = f"Diffusion response processing error: {str(e)}" - print(f"Error: {msg}") - return result - - def _http_response_from_requests(self, r: requests.Response) -> HttpResponse: - payload = _parse_response_json(r) - ok = 200 <= r.status_code < 300 - return HttpResponse( - status_code=r.status_code, - success=ok, - error_message=None if ok else (r.text[:8000] if r.text else None), - json_body=payload, - ) - - def send_health_http_request( - self, - request_config: dict[str, Any] | None = None, - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/health`` (raw ``requests``). - - ``request_config``: optional ``timeout`` plus optional ``err_code`` / ``err_message`` for - :func:`~tests.helpers.assertions.assert_http_error` (also as keyword-only args). - """ - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = requests.get(self._build_url("/health"), timeout=float(cfg.get("timeout", 120.0))) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_models_http_request( - self, - request_config: dict[str, Any] | None = None, - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/v1/models``. Optional ``timeout`` and HTTP assertions (see :func:`~tests.helpers.assertions.assert_http_error`).""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = requests.get( - self._build_url("/v1/models"), - headers={"Accept": "application/json"}, - timeout=float(cfg.get("timeout", 120.0)), - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_chat_completions_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/chat/completions`` with ``json`` or ``raw_body`` (malformed-body / contract tests).""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/chat/completions", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_batched_chat_completions_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """Post to batched chat completions.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/chat/completions/batch", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_completions_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/completions`` with ``json`` or ``raw_body``.""" - # TODO (Alex): A lot of these helpers should be consolidated as they differ only by endpoint - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/completions", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_omni_sleep_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/omni/sleep`` — ``json`` or ``raw_body``, ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/omni/sleep", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_omni_wakeup_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/omni/wakeup``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/omni/wakeup", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_voices_list_http_request( - self, - request_config: dict[str, Any] | None = None, - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/v1/audio/voices``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = requests.get( - self._build_url("/v1/audio/voices"), - headers={"Accept": "application/json"}, - timeout=float(cfg.get("timeout", 120.0)), - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_voices_create_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/audio/voices`` (multipart): ``data`` / ``files`` / ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_form_endpoint("/v1/audio/voices", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_voices_delete_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """DELETE ``/v1/audio/voices/{name}`` — requires ``name``, optional ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - name = cfg["name"] - timeout = float(cfg.get("timeout", 120.0)) - path = f"/v1/audio/voices/{quote(str(name), safe='')}" - r = requests.delete( - self._build_url(path), - headers={"Accept": "application/json"}, - timeout=timeout, - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_speech_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/audio/speech`` with ``json`` or ``raw_body``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/audio/speech", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_speech_batch_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/audio/speech/batch``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/audio/speech/batch", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_audio_generate_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/audio/generate``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/audio/generate", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_images_generations_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/images/generations`` — ``json`` or ``raw_body``, ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_json_endpoint("/v1/images/generations", cfg, default_timeout=300.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - if cfg.get("err_code") is None: - assert resp.success, resp.error_message - payload = resp.json_body - assert isinstance(payload, dict) - assert_images_generations_response(payload, cfg, run_level=self.run_level) - return [resp] - - def send_images_edits_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/images/edits`` — ``data`` / ``files`` / ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_form_endpoint("/v1/images/edits", cfg, default_timeout=300.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_videos_create_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/videos`` (async job) — multipart ``data`` / ``files``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_form_endpoint("/v1/videos", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_videos_sync_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """POST ``/v1/videos/sync``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = self._post_form_endpoint("/v1/videos/sync", cfg, default_timeout=120.0) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_videos_list_http_request( - self, - request_config: dict[str, Any] | None = None, - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/v1/videos`` — optional ``params``, ``timeout``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - r = requests.get( - self._build_url("/v1/videos"), - params=cfg.get("params"), - headers={"Accept": "application/json"}, - timeout=float(cfg.get("timeout", 120.0)), - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_video_retrieve_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/v1/videos/{video_id}``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - video_id = cfg["video_id"] - timeout = float(cfg.get("timeout", 120.0)) - r = requests.get( - self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"), - headers={"Accept": "application/json"}, - timeout=timeout, - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_video_delete_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """DELETE ``/v1/videos/{video_id}``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - video_id = cfg["video_id"] - timeout = float(cfg.get("timeout", 120.0)) - r = requests.delete( - self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}"), - headers={"Accept": "application/json"}, - timeout=timeout, - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def send_video_content_http_request( - self, - request_config: dict[str, Any], - *, - err_code: int | tuple[int, ...] | list[int] | None = None, - err_message: str | tuple[str, ...] | list[str] | None = None, - ) -> list[HttpResponse]: - """GET ``/v1/videos/{video_id}/content``.""" - cfg = _merge_http_expectation_kwargs( - request_config, - err_code=err_code, - err_message=err_message, - ) - video_id = cfg["video_id"] - timeout = float(cfg.get("timeout", 120.0)) - r = requests.get( - self._build_url(f"/v1/videos/{quote(str(video_id), safe='')}/content"), - timeout=timeout, - ) - resp = self._http_response_from_requests(r) - assert_http_error( - resp, - err_code=cfg.get("err_code"), - err_message=cfg.get("err_message"), - ) - return [resp] - - def _build_ws_url(self, path: str) -> str: - """Turn HTTP ``base_url`` into ``ws`` / ``wss`` for WebSocket helpers.""" - base = self.base_url.rstrip("/") - suffix = "/" + path.lstrip("/") - if base.startswith("http://"): - return "ws://" + base.removeprefix("http://") + suffix - if base.startswith("https://"): - return "wss://" + base.removeprefix("https://") + suffix - raise ValueError(f"Unsupported base_url for WebSocket: {base!r}") - - def _send_websocket_first_json_request( - self, - path: str, - cfg: dict[str, Any], - ) -> list[WebSocketJsonResponse]: - """Connect, optionally send text frames, return first JSON text frame as :class:`WebSocketJsonResponse`. - - ``request_config`` keys: - - - ``send_frames``: optional ``str`` or sequence of ``str`` raw WebSocket text frames (omit when the server - speaks first, e.g. ``/v1/realtime`` rejection path). - - ``ws_skip_types``: optional event ``type`` strings to ignore while waiting for the first matching frame - (e.g. ``["session.created"]`` on ``/v1/realtime``). - - ``timeout``: seconds to wait for the first inbound text frame (default ``120``). - - ``ws_max_size``: passed through as ``max_size`` to :func:`websockets.connect` when the key is present. - """ - send_frames_raw = cfg.get("send_frames") - if send_frames_raw is None: - frames: list[str] = [] - elif isinstance(send_frames_raw, str): - frames = [send_frames_raw] - else: - frames = list(send_frames_raw) - - timeout = float(cfg.get("timeout", 120.0)) - uri = self._build_ws_url(path) - skip_types = set(cfg.get("ws_skip_types") or []) - - connect_kw: dict[str, Any] = {} - if "ws_max_size" in cfg: - connect_kw["max_size"] = cfg["ws_max_size"] - - async def _recv_first_json_object() -> WebSocketJsonResponse: - import websockets - - async with websockets.connect(uri, **connect_kw) as ws: - for frame in frames: - await ws.send(frame) - while True: - raw = await asyncio.wait_for(ws.recv(), timeout=timeout) - if not isinstance(raw, str): - raise AssertionError(f"Expected JSON text frame from {uri}, got {type(raw).__name__}") + # Close per-stage log handles; delete temp files unless VLLM_OMNI_KEEP_LOG is set. + keep_logs = os.environ.get("VLLM_OMNI_KEEP_LOG", "").lower() in ("1", "true", "yes") + log_files = self._stage_log_files + log_paths = self._stage_log_paths + for stage_key in set(log_files) | set(log_paths): + log_fh = log_files.get(stage_key) + if log_fh is not None: + try: + log_fh.close() + except Exception: + pass + if not keep_logs: + log_path = log_paths.get(stage_key) + if log_path is not None: try: - data = json.loads(raw) - except json.JSONDecodeError as exc: - raise AssertionError(f"Expected JSON text frame from {uri}, body={raw[:500]!r}") from exc - if not isinstance(data, dict): - raise AssertionError(f"Expected JSON object from {uri}, got {type(data).__name__}") - if skip_types and data.get("type") in skip_types: - continue - return WebSocketJsonResponse(json_body=data) - - resp = asyncio.run(_recv_first_json_object()) - _run_ws_expectations_from_request_config(cfg, resp) - return [resp] - - def send_audio_speech_stream_ws_request( - self, - request_config: dict[str, Any], - *, - err_message: str | tuple[str, ...] | list[str] | None = None, - ws_json_type: str | None = None, - ws_error_code: str | None = None, - ) -> list[WebSocketJsonResponse]: - """WebSocket ``/v1/audio/speech/stream`` — send ``send_frames`` then read first JSON text frame.""" - cfg = _merge_ws_expectation_kwargs( - request_config, - err_message=err_message, - ws_json_type=ws_json_type, - ws_error_code=ws_error_code, - ) - return self._send_websocket_first_json_request("/v1/audio/speech/stream", cfg) - - def send_video_chat_stream_ws_request( - self, - request_config: dict[str, Any], - *, - err_message: str | tuple[str, ...] | list[str] | None = None, - ws_json_type: str | None = None, - ws_error_code: str | None = None, - ) -> list[WebSocketJsonResponse]: - """WebSocket ``/v1/video/chat/stream`` — send ``send_frames`` then read first JSON text frame.""" - cfg = _merge_ws_expectation_kwargs( - request_config, - err_message=err_message, - ws_json_type=ws_json_type, - ws_error_code=ws_error_code, - ) - return self._send_websocket_first_json_request("/v1/video/chat/stream", cfg) - - def send_realtime_ws_request( - self, - request_config: dict[str, Any] | None = None, - *, - err_message: str | tuple[str, ...] | list[str] | None = None, - ws_json_type: str | None = None, - ws_error_code: str | None = None, - ) -> list[WebSocketJsonResponse]: - """WebSocket ``/v1/realtime`` — optional outbound frames, then first JSON text frame (often server-initiated).""" - cfg = _merge_ws_expectation_kwargs( - request_config, - err_message=err_message, - ws_json_type=ws_json_type, - ws_error_code=ws_error_code, - ) - return self._send_websocket_first_json_request("/v1/realtime", cfg) - - def send_robot_openpi_ws_request( - self, - request_config: dict[str, Any] | None = None, - ) -> list[OpenPIWebSocketResponse]: - """WebSocket ``/v1/realtime/robot/openpi`` — msgpack metadata plus optional infer/reset ops. - - ``request_config`` keys: - - - ``operations``: optional sequence of ``{"endpoint": "infer"|"reset", "payload": {...}}``. - - ``run_default_policy_session``: when true and ``operations`` is omitted, run infer then reset - using :func:`build_openpi_droid_observation`. - - ``run_dreamzero_policy_session``: when true and ``operations`` is omitted, run the DreamZero - infer/reset/infer sequence using :func:`build_dreamzero_demo_observations`. - - ``video_dir``: required for ``run_dreamzero_policy_session``. - - ``prompt``: language instruction for DreamZero observations (optional). - - ``session_id``: used by the default policy session observation builder. - - ``num_chunks``: DreamZero chunk count (default ``2``). - - ``timeout``: seconds to wait for each inbound msgpack frame (default ``120``). - - ``ping_interval`` / ``ping_timeout``: forwarded to :func:`websockets.sync.client.connect`. - """ - cfg = dict(request_config or {}) - operations_cfg = cfg.get("operations") - if operations_cfg is not None: - operations = [dict(op) for op in operations_cfg] - elif cfg.get("run_dreamzero_policy_session"): - video_dir = cfg.get("video_dir") - if video_dir is None: - raise ValueError("run_dreamzero_policy_session requires video_dir") - session_id = str(cfg.get("session_id", "dreamzero-smoke")) - prompt = str(cfg.get("prompt", DREAMZERO_DEFAULT_PROMPT)) - num_chunks = int(cfg.get("num_chunks", 2)) - observations = build_dreamzero_demo_observations( - load_dreamzero_camera_frames(Path(video_dir)), - prompt=prompt, - session_id=session_id, - num_chunks=num_chunks, - ) - operations = [{"endpoint": "infer", "payload": obs} for obs in observations] - operations.append({"endpoint": "reset", "payload": {}}) - operations.append({"endpoint": "infer", "payload": observations[0]}) - elif cfg.get("run_default_policy_session"): - session_id = str(cfg.get("session_id", "gr00t-smoke")) - operations = [ - {"endpoint": "infer", "payload": build_openpi_droid_observation(session_id=session_id)}, - {"endpoint": "reset", "payload": {}}, - ] - else: - operations = [] - timeout = float(cfg.get("timeout", 120.0)) - ping_interval = float(cfg.get("ping_interval", OpenPIWebSocketSession.DEFAULT_PING_INTERVAL_SECS)) - ping_timeout = float(cfg.get("ping_timeout", OpenPIWebSocketSession.DEFAULT_PING_TIMEOUT_SECS)) - - uri = self._build_ws_url("/v1/realtime/robot/openpi") - session = OpenPIWebSocketSession( - uri, - ping_interval=ping_interval, - ping_timeout=ping_timeout, - open_timeout=timeout, - close_timeout=timeout, - ) - try: - operation_responses: list[Any] = [] - actions: dict[str, np.ndarray] | None = None - action_tensors: list[np.ndarray] = [] - for operation in operations: - endpoint = str(operation["endpoint"]) - payload = dict(operation.get("payload") or {}) - response = session._send_operation(endpoint, payload) - operation_responses.append(response) - if endpoint == "infer": - if isinstance(response, dict): - actions = {str(key): np.asarray(value, dtype=np.float32) for key, value in response.items()} - else: - action_tensors.append(np.asarray(response, dtype=np.float32)) - finally: - session.close() - - return [ - OpenPIWebSocketResponse( - server_metadata=session.get_server_metadata(), - operation_responses=operation_responses, - actions=actions, - action_tensors=action_tensors or None, - ) - ] - - def send_omni_request( - self, - request_config: dict[str, Any], - request_num: int = 1, - *, - min_successes: int | None = None, - max_concurrency: int | None = None, - ) -> list[OmniResponse]: - """Chat completions via the OpenAI Python SDK (not raw HTTP). - - ``min_successes`` gates the batch on how many of the ``request_num`` requests pass - rather than requiring each one to, and returns only the successes; ``max_concurrency`` - caps how many are in flight at once. See ``SuccessRateGate``. - """ - responses: list[OmniResponse] = [] - stream = request_config.get("stream", False) - modalities = request_config.get("modalities", ["text", "audio"]) - extra_body: dict[str, Any] = {} - if "speaker" in request_config: - extra_body["speaker"] = request_config["speaker"] - if request_config.get("use_audio_in_video"): - mm = dict(extra_body.get("mm_processor_kwargs") or {}) - mm["use_audio_in_video"] = True - extra_body["mm_processor_kwargs"] = mm - if "sampling_params_list" in request_config: - extra_body["sampling_params_list"] = request_config["sampling_params_list"] - if request_config.get("extra_body"): - extra_body.update(request_config["extra_body"]) - - create_kwargs: dict[str, Any] = { - "model": request_config.get("model"), - "messages": request_config.get("messages"), - "stream": stream, - "modalities": modalities, - } - if "logprobs" in request_config: - create_kwargs["logprobs"] = request_config["logprobs"] - if "top_logprobs" in request_config: - create_kwargs["top_logprobs"] = request_config["top_logprobs"] - if "stream_options" in request_config: - create_kwargs["stream_options"] = request_config["stream_options"] - if extra_body: - create_kwargs["extra_body"] = extra_body - - def _one(): - wall_start = time.perf_counter() - chat_completion = self.client.chat.completions.create(**create_kwargs) - return ( - self._process_stream_omni_response(chat_completion, wall_start=wall_start) - if stream - else self._process_non_stream_omni_response(chat_completion, wall_start=wall_start) - ) - - if min_successes is not None: - gate = SuccessRateGate(min_successes=min_successes, concurrency=max_concurrency) - - def _sample() -> OmniResponse: - resp = _one() - assert_omni_response(resp, request_config, run_level=self.run_level) - return resp - - # Not _print_client_stat: the count is the gate's verdict, so it has to survive - # log_stats=False. - return collect_at_success_rate( - _sample, - gate, - request_num=request_num, - report=lambda line: print(f"[omni] {line}", flush=True), - ) - - if request_num == 1: - resp = _one() - assert_omni_response(resp, request_config, run_level=self.run_level) - if resp.e2e_latency is not None: - self._print_client_stat(f"[omni] request#1 success in {resp.e2e_latency:.3f}s") - else: - self._print_client_stat("[omni] request#1 completed") - responses.append(resp) - return responses - - with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: - futures = {executor.submit(_one): i + 1 for i in range(request_num)} - for future in concurrent.futures.as_completed(futures): - request_idx = futures[future] - resp = future.result() - assert_omni_response(resp, request_config, run_level=self.run_level) - if resp.e2e_latency is not None: - self._print_client_stat(f"[omni] request#{request_idx} success in {resp.e2e_latency:.3f}s") - else: - self._print_client_stat(f"[omni] request#{request_idx} completed") - responses.append(resp) - return responses - - def _process_stream_audio_speech_response( - self, response, *, response_format: str | None = None, wall_start: float - ) -> OmniResponse: - """ - Process streaming /v1/audio/speech responses into an OmniResponse. - - This mirrors _process_stream_omni_response but operates on low-level - audio bytes. Whisper transcription runs in assert_audio_speech_response - when the run_level requires it. - """ - result = OmniResponse() - - try: - # Aggregate all audio bytes from the streaming response. - data = bytearray() - - # Preferred OpenAI helper. - if hasattr(response, "iter_bytes") and callable(getattr(response, "iter_bytes")): - for chunk in response.iter_bytes(): - if chunk: - data.extend(chunk) - else: - # Generic iterable-of-bytes fallback (e.g., generator or list of chunks). - try: - iterator = iter(response) - except TypeError: - iterator = None - - if iterator is not None: - for chunk in iterator: - if not chunk: - continue - if isinstance(chunk, (bytes, bytearray)): - data.extend(chunk) - elif hasattr(chunk, "data"): - data.extend(chunk.data) # type: ignore[arg-type] - elif hasattr(chunk, "content"): - data.extend(chunk.content) # type: ignore[arg-type] - else: - raise TypeError(f"Unsupported stream chunk type: {type(chunk)}") - else: - raise TypeError(f"Unsupported audio speech streaming response type: {type(response)}") - - raw_bytes = bytes(data) - - # Populate OmniResponse. - result.audio_bytes = raw_bytes - result.e2e_latency = time.perf_counter() - wall_start - result.success = True - result.audio_format = getattr(response, "response", None) - if result.audio_format is not None: - result.audio_format = result.audio_format.headers.get("content-type", "") - - except Exception as e: - msg = f"Audio speech stream processing error: {str(e)}" - print(f"Error: {msg}") - - return result - - def _process_non_stream_audio_speech_response( - self, response, *, response_format: str | None = None, wall_start: float - ) -> OmniResponse: - """ - Process non-streaming /v1/audio/speech responses into an OmniResponse. - - This mirrors _process_non_stream_omni_response but for the binary - audio payload returned by audio.speech.create. - """ - result = OmniResponse() - - try: - # OpenAI non-streaming audio.speech.create returns HttpxBinaryResponseContent (.read() or .content) - if hasattr(response, "read") and callable(getattr(response, "read")): - raw_bytes = response.read() - elif hasattr(response, "content"): - raw_bytes = response.content # type: ignore[assignment] - else: - raise TypeError(f"Unsupported audio speech response type: {type(response)}") - - result.audio_bytes = raw_bytes - result.e2e_latency = time.perf_counter() - wall_start - result.success = True - result.audio_format = getattr(response, "response", None) - if result.audio_format is not None: - result.audio_format = result.audio_format.headers.get("content-type", "") - - except Exception as e: - msg = f"Audio speech non-stream processing error: {str(e)}" - print(f"Error: {msg}") - - return result - - def send_audio_speech_request(self, request_config: dict[str, Any], request_num: int = 1) -> list[OmniResponse]: - """ - Call the /v1/audio/speech endpoint using the same configuration-dict - style as send_omni_request, but via the OpenAI Python client's - audio.speech APIs. - - Expected keys in request_config: - - model: model name/path (required) - - input: text to synthesize (required) - - response_format: audio format such as "wav" or "pcm" (optional) - - task_type, ref_text, ref_audio: TTS-specific extras (optional, passed via extra_body) - - min_audio_bytes: optional minimum ``len(audio_bytes)`` checked in ``assert_audio_speech_response`` - - status_code: if set, HTTP status is asserted (int or e.g. ``(400, 422)``); uses APIError handling - - err_message: optional substring(s) to match against error text (``str`` or list/tuple of alternatives; - see ``assert_audio_speech_response``). If set, uses the same APIError path as ``status_code``. - - When both ``status_code`` and ``err_message`` are absent (or each is ``None``), the normal request path - is used (no try/except around ``APIError``). - - timeout: request timeout in seconds (float, optional, default 120.0) - - stream: whether to use streaming API (bool, optional, default False) - """ - timeout = float(request_config.get("timeout", 120.0)) - - model = request_config["model"] - text_input = request_config["input"] - stream = bool(request_config.get("stream", False)) - voice = request_config.get("voice", None) - - # Standard OpenAI param: use omit when not provided to keep default behavior. - response_format = request_config.get("response_format", omit) - - # Qwen3-TTS custom fields, forwarded via extra_body. - extra_body: dict[str, Any] = {} - # Keep this list aligned with vllm_omni.entrypoints.openai.protocol.audio params. - for key in ( - "task_type", - "ref_text", - "ref_audio", - "language", - "max_new_tokens", - "seed", - "instructions", - "speed", - "stream_format", - "x_vector_only_mode", - ): - if key in request_config: - extra_body[key] = request_config[key] - - responses: list[OmniResponse] = [] - - speech_fmt: str | None = None if response_format is omit else str(response_format).lower() - - print(f"[audio.speech] start model={model}, stream={stream}, request_num={request_num}, timeout={timeout:.1f}s") - - # Error validation path: only when at least one of these is set to a non-``None`` value. - expect_error_handling = (request_config.get("status_code") is not None) or ( - request_config.get("err_message") is not None - ) - if expect_error_handling and request_num != 1: - raise ValueError( - "request_config error validation (status_code / err_message) is only supported when request_num=1" - ) - - if request_num == 1: - if expect_error_handling: - # ``status`` and/or ``err_message`` requested: catch APIError (4xx) and (optionally) assert body text; - # HTTP 200 with JSON error body is handled in ``assert_audio_speech_response``. - try: - if stream: - wall_start = time.perf_counter() - with self.client.audio.speech.with_streaming_response.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) as resp: - omni_resp = self._process_stream_audio_speech_response( - resp, response_format=speech_fmt, wall_start=wall_start - ) - else: - wall_start = time.perf_counter() - resp = self.client.audio.speech.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) - omni_resp = self._process_non_stream_audio_speech_response( - resp, response_format=speech_fmt, wall_start=wall_start - ) - except APIError as e: - sc = getattr(e, "status_code", None) - if sc is None: - raise - omni_resp = OmniResponse( - success=False, - status_code=sc, - error_message=str(e), - ) - else: - if getattr(omni_resp, "status_code", None) is None: - omni_resp.status_code = 200 - elif stream: - # Use streaming response helper. - wall_start = time.perf_counter() - with self.client.audio.speech.with_streaming_response.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) as resp: - omni_resp = self._process_stream_audio_speech_response( - resp, response_format=speech_fmt, wall_start=wall_start - ) - else: - # Non-streaming response. - wall_start = time.perf_counter() - resp = self.client.audio.speech.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) - omni_resp = self._process_non_stream_audio_speech_response( - resp, response_format=speech_fmt, wall_start=wall_start - ) - - assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) - if omni_resp.e2e_latency is not None: - self._print_client_stat(f"[audio.speech] request#1 success in {omni_resp.e2e_latency:.3f}s") - else: - self._print_client_stat("[audio.speech] request#1 completed") - responses.append(omni_resp) - return responses - else: - # request_num > 1: concurrent requests (use same params as single-request path) - - if stream: - - def _stream_task(request_idx: int): - wall_start = time.perf_counter() - with self.client.audio.speech.with_streaming_response.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) as resp: - result = self._process_stream_audio_speech_response( - resp, response_format=speech_fmt, wall_start=wall_start - ) - if result.e2e_latency is not None: - self._print_client_stat( - f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s" - ) - else: - self._print_client_stat(f"[audio.speech] request#{request_idx} completed") - return result - - with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: - futures = {executor.submit(_stream_task, i + 1): i + 1 for i in range(request_num)} - for future in concurrent.futures.as_completed(futures): - request_idx = futures[future] - try: - omni_resp = future.result() - except Exception as e: - print( - f"[audio.speech] request#{request_idx} failed " - f"(stream={stream}, timeout={timeout:.1f}s): {e!r}" - ) - raise - assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) - responses.append(omni_resp) - else: - - def _non_stream_task(request_idx: int): - wall_start = time.perf_counter() - r = self.client.audio.speech.create( - model=model, - input=text_input, - response_format=response_format, - extra_body=extra_body or None, - timeout=timeout, - voice=voice, - ) - result = self._process_non_stream_audio_speech_response( - r, response_format=speech_fmt, wall_start=wall_start - ) - if result.e2e_latency is not None: - self._print_client_stat( - f"[audio.speech] request#{request_idx} success in {result.e2e_latency:.3f}s" - ) - else: - self._print_client_stat(f"[audio.speech] request#{request_idx} completed") - return result - - with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: - futures = {executor.submit(_non_stream_task, i + 1): i + 1 for i in range(request_num)} - for future in concurrent.futures.as_completed(futures): - request_idx = futures[future] - try: - omni_resp = future.result() - except Exception as e: - print( - f"[audio.speech] request#{request_idx} failed " - f"(stream={stream}, timeout={timeout:.1f}s): {e!r}" - ) - raise - assert_audio_speech_response(omni_resp, request_config, run_level=self.run_level) - responses.append(omni_resp) - - return responses - - def send_diffusion_request( - self, request_config: dict[str, Any] | list[dict[str, Any]], request_num: int = 1 - ) -> list[DiffusionResponse]: - """ - Send OpenAI requests for diffusion models. - If ``extra_body`` has list ``height``/``width``, sends one chat completion per index in parallel - (scalar h/w, ``num_outputs_per_prompt=1`` each) and merges images in list order. - - Args: - request_config: A single request configuration dict, or a list of - request configuration dicts (one request per element) - request_num: Number of requests to send concurrently, defaults to 1 (single request) - Returns: - list[DiffusionResponse]: List of DiffusionResponse objects containing the response data - """ - responses: list[DiffusionResponse] = [] - - def _create_from_config(cfg: dict[str, Any]) -> tuple[Any, float]: - stream = cfg.get("stream", False) - if stream: - raise NotImplementedError("Streaming is not currently implemented for diffusion model e2e test") - modalities = cfg.get("modalities", omit) # Most diffusion models don't require modalities param - eb = cfg.get("extra_body") - extra = copy.deepcopy(eb) if eb else None - wall_start = time.perf_counter() - chat_completion = self.client.chat.completions.create( - model=cfg.get("model"), - messages=cfg.get("messages"), - extra_body=extra, - modalities=modalities, - ) - return chat_completion, wall_start - - if isinstance(request_config, list): - if not request_config: - raise ValueError("request_config list must not be empty") - if request_num != 1: - raise ValueError("request_num is not supported when request_config is a list") - with concurrent.futures.ThreadPoolExecutor(max_workers=len(request_config)) as executor: - futures = { - executor.submit(_create_from_config, cfg): (i + 1, cfg) for i, cfg in enumerate(request_config) - } - for future in concurrent.futures.as_completed(futures): - request_idx, cfg = futures[future] - chat_completion, wall_start = future.result() - response = self._process_diffusion_response(chat_completion, wall_start=wall_start) - assert_diffusion_response(response, cfg, run_level=self.run_level) - if response.e2e_latency is not None: - self._print_client_stat( - f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s" - ) - else: - self._print_client_stat(f"[diffusion] request#{request_idx} completed") - responses.append(response) - return responses - - size_splits = _split_request_config_by_per_output_sizes(request_config) - if size_splits is not None: - if request_num != 1: - raise ValueError( - "request_num must be 1 when extra_body height/width are lists (split into concurrent per-size calls)" - ) - t0 = time.perf_counter() - with concurrent.futures.ThreadPoolExecutor(max_workers=len(size_splits)) as executor: - futures = [executor.submit(_create_from_config, sub) for sub in size_splits] - chat_completions = [f.result() for f in futures] - parts = [self._process_diffusion_response(cc, wall_start=ws) for cc, ws in chat_completions] - merged = _merge_diffusion_responses(parts) - merged.e2e_latency = time.perf_counter() - t0 - assert_diffusion_response(merged, request_config, run_level=self.run_level) - if merged.e2e_latency is not None: - self._print_client_stat(f"[diffusion] request#1 success in {merged.e2e_latency:.3f}s") - else: - self._print_client_stat("[diffusion] request#1 completed") - return [merged] - - if request_num == 1: - # Send single request - chat_completion, wall_start = _create_from_config(request_config) - response = self._process_diffusion_response(chat_completion, wall_start=wall_start) - assert_diffusion_response(response, request_config, run_level=self.run_level) - if response.e2e_latency is not None: - self._print_client_stat(f"[diffusion] request#1 success in {response.e2e_latency:.3f}s") - else: - self._print_client_stat("[diffusion] request#1 completed") - responses.append(response) - return responses - - # Send concurrent requests for the same request_config - with concurrent.futures.ThreadPoolExecutor(max_workers=request_num) as executor: - futures = {executor.submit(_create_from_config, request_config): i + 1 for i in range(request_num)} - for future in concurrent.futures.as_completed(futures): - request_idx = futures[future] - chat_completion, wall_start = future.result() - response = self._process_diffusion_response(chat_completion, wall_start=wall_start) - assert_diffusion_response(response, request_config, run_level=self.run_level) - if response.e2e_latency is not None: - self._print_client_stat(f"[diffusion] request#{request_idx} success in {response.e2e_latency:.3f}s") - else: - self._print_client_stat(f"[diffusion] request#{request_idx} completed") - responses.append(response) - return responses - - def send_video_diffusion_request( - self, request_config: dict[str, Any], request_num: int = 1 - ) -> list[DiffusionResponse]: - """ - Send native /v1/videos requests: multipart ``form_data`` job create, poll until done, download content. - - For raw HTTP to video routes without polling, use ``send_videos_create_http_request``, etc. - """ - if request_num != 1: - raise NotImplementedError("Concurrent video diffusion requests are not currently implemented") - - form_data = request_config.get("form_data") - if not isinstance(form_data, dict): - raise ValueError("Video request_config must contain 'form_data'") - normalized_form_data = {key: str(value) for key, value in form_data.items() if value is not None} - files: dict[str, tuple[str, BytesIO, str]] = {} - image_reference = request_config.get("image_reference") - video_reference = request_config.get("video_reference") - if image_reference and video_reference: - raise ValueError("Only one of image_reference or video_reference can be provided") - if image_reference: - if image_reference.startswith("data:image"): - header, encoded = image_reference.split(",", 1) - content_type = header.split(";")[0].removeprefix("data:") - extension = content_type.split("/")[-1] - file_data = base64.b64decode(encoded) - files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type) - else: - normalized_form_data["image_reference"] = json.dumps({"image_url": image_reference}) - if video_reference: - if video_reference.startswith("data:video"): - header, encoded = video_reference.split(",", 1) - content_type = header.split(";")[0].removeprefix("data:") - extension = content_type.split("/")[-1] - file_data = base64.b64decode(encoded) - files["input_reference"] = (f"reference.{extension}", BytesIO(file_data), content_type) - else: - normalized_form_data["video_reference"] = json.dumps({"video_url": video_reference}) - - result = DiffusionResponse() - create_url = self._build_url("/v1/videos") - response = requests.post( - create_url, - data=normalized_form_data, - files=files, - headers={"Accept": "application/json"}, - timeout=60, - ) - start_time = time.perf_counter() - response.raise_for_status() - job_data = response.json() - video_id = job_data["id"] - self._wait_until_video_completed(video_id) - end_time = time.perf_counter() - video_content = self._download_video_content(video_id) - result.success = True - result.videos = [video_content] - result.e2e_latency = end_time - start_time - assert_diffusion_response(result, request_config, run_level=self.run_level) - if result.e2e_latency is not None: - self._print_client_stat(f"[diffusion] request#1 success in {result.e2e_latency:.3f}s") - else: - self._print_client_stat("[diffusion] request#1 completed") - return [result] - - def _post_json_endpoint( - self, - path: str, - request_config: dict[str, Any], - *, - default_timeout: float, - ) -> requests.Response: - url = self._build_url(path) - timeout = float(request_config.get("timeout", default_timeout)) - if "raw_body" in request_config: - raw = request_config["raw_body"] - payload = raw.encode("utf-8") if isinstance(raw, str) else raw - return requests.post( - url, - data=payload, - headers={"Content-Type": "application/json", "Accept": "application/json"}, - timeout=timeout, - ) - if "json" not in request_config: - raise ValueError(f"{path} request_config must include 'json' or 'raw_body'") - return requests.post( - url, - json=request_config["json"], - headers={"Accept": "application/json", "Content-Type": "application/json"}, - timeout=timeout, - ) - - def _post_form_endpoint( - self, - path: str, - request_config: dict[str, Any], - *, - default_timeout: float = 120.0, - ) -> requests.Response: - url = self._build_url(path) - timeout = float(request_config.get("timeout", default_timeout)) - data = request_config.get("data") - files = request_config.get("files") - if data is None and not files: - data = {} - return requests.post( - url, - data=data, - files=files, - headers={"Accept": "application/json"} if not files else {"Accept": "application/json"}, - timeout=timeout, - ) - - def send_streaming_video_diffusion_request( - self, - request_config: dict[str, Any], - request_num: int = 1, - *, - timeout_seconds: float = 600.0, - ) -> list[DiffusionResponse]: - """ - Send a native ``/v1/realtime/video`` WebSocket request and return one - finalized MP4 artifact assembled from the streamed binary fragments. - """ - if request_num != 1: - raise NotImplementedError("Concurrent streaming video diffusion requests are not currently implemented") - - response = asyncio.run( - self._send_streaming_video_diffusion_request_once( - request_config, - timeout_seconds=timeout_seconds, - ) - ) - assert_diffusion_response(response, request_config, run_level=self.run_level) - if response.e2e_latency is not None: - self._print_client_stat(f"[diffusion.stream] request#1 success in {response.e2e_latency:.3f}s") - else: - self._print_client_stat("[diffusion.stream] request#1 completed") - return [response] - - async def _send_streaming_video_diffusion_request_once( - self, - request_config: dict[str, Any], - *, - timeout_seconds: float, - ) -> DiffusionResponse: - form_data = request_config.get("form_data") - if not isinstance(form_data, dict): - raise ValueError("Video request_config must contain 'form_data'") - payload: dict[str, Any] = { - "type": "session.start", - **{key: value for key, value in form_data.items() if value is not None}, - } - model = request_config.get("model") - if model is not None: - payload["model"] = model - payload.setdefault("format", "m4s") - - fps = float(payload.get("fps") or 16) - stream_format = payload["format"] - url = self._build_ws_url("/v1/realtime/video") - - result = DiffusionResponse() - chunks: list[bytes] = [] - start_time = time.perf_counter() - deadline = start_time + timeout_seconds - - import websockets - - async with websockets.connect(url, max_size=None) as websocket: - await websocket.send(json.dumps(payload)) - - while True: - remaining = deadline - time.perf_counter() - if remaining <= 0: - raise TimeoutError(f"Streaming video request did not complete within {timeout_seconds}s") - - message = await asyncio.wait_for(websocket.recv(), timeout=remaining) - if isinstance(message, bytes): - chunks.append(message) - continue - - msg = json.loads(message) - msg_type = msg.get("type") - if msg_type == "video.start": - stream_format = msg.get("format") or stream_format - continue - if msg_type == "session.done": - break - if msg_type == "error": - raise RuntimeError(str(msg.get("message", msg))) - - from vllm_omni.diffusion.utils.media_utils import finalize_streaming_video_bytes - from vllm_omni.entrypoints.openai.video_api_utils import StreamingVideoFormat - - streamed_bytes = b"".join(chunks) - if not streamed_bytes: - raise RuntimeError("Streaming video request completed without binary video chunks") - result.videos = [ - finalize_streaming_video_bytes( - streamed_bytes, - input_format=cast(StreamingVideoFormat, stream_format), - fps=fps, - ) - ] - result.e2e_latency = time.perf_counter() - start_time - result.success = True - return result - - def _wait_until_video_completed( - self, video_id: str, poll_interval_seconds: int = 2, timeout_seconds: int = 300 - ) -> None: - status_url = self._build_url(f"/v1/videos/{video_id}") - deadline = time.monotonic() + timeout_seconds - while time.monotonic() < deadline: - status_resp = requests.get(status_url, headers={"Accept": "application/json"}, timeout=30) - status_resp.raise_for_status() - status_data = status_resp.json() - current_status = status_data["status"] - if current_status == "completed": - return - if current_status == "failed": - error_msg = status_data.get("last_error", "Unknown error") - raise RuntimeError(f"Job failed: {error_msg}") - time.sleep(poll_interval_seconds) - raise TimeoutError(f"Video job {video_id} did not complete within {timeout_seconds}s") - - def _download_video_content(self, video_id: str) -> bytes: - download_url = self._build_url(f"/v1/videos/{video_id}/content") - video_resp = requests.get(download_url, stream=True, timeout=60) - video_resp.raise_for_status() - video_bytes = BytesIO() - for chunk in video_resp.iter_content(chunk_size=8192): - if chunk: - video_bytes.write(chunk) - return video_bytes.getvalue() - - def _build_url(self, path: str) -> str: - return f"{self.base_url.rstrip('/')}/{path.lstrip('/')}" + Path(log_path).unlink(missing_ok=True) + except Exception: + pass + self._stage_log_files = {} + if not keep_logs: + self._stage_log_paths = {} + + cleanup_test_environment() class OmniRunner: @@ -2669,7 +592,6 @@ class OmniRunner: model_name: str, seed: int = 42, stage_init_timeout: int = 600, - batch_timeout: int = 10, # Bumped from 900s -> 1800s to give CI cold-cache loads of large # diffusion models enough headroom (Buildkite #8418 hit a 6-second # overrun loading Tongyi-MAI/Z-Image-Turbo: weights alone took 690s, @@ -2683,26 +605,30 @@ class OmniRunner: **kwargs, ) -> None: startup_t0 = time.perf_counter() - cleanup_dist_env_and_memory() - run_pre_test_cleanup() - run_post_test_cleanup() + cleanup_test_environment() self.model_name = model_name self.seed = seed self._prompt_len_estimate_cache: dict[str, Any] = {} - from vllm_omni.entrypoints.omni import Omni + self.omni: Any = None + try: + from vllm_omni.entrypoints.omni import Omni - self.omni = Omni( - model=model_name, - log_stats=log_stats, - stage_init_timeout=stage_init_timeout, - batch_timeout=batch_timeout, - init_timeout=init_timeout, - deploy_config=deploy_config, - **kwargs, - ) - startup_s = time.perf_counter() - startup_t0 - if log_stats: - print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True) + self.omni = Omni( + model=model_name, + log_stats=log_stats, + stage_init_timeout=stage_init_timeout, + init_timeout=init_timeout, + deploy_config=deploy_config, + **kwargs, + ) + startup_s = time.perf_counter() - startup_t0 + if log_stats: + print(f"OmniRunner startup took {startup_s:.3f}s (model={model_name})", flush=True) + except BaseException: + # ``with OmniRunner(...)`` never reaches ``__enter__``/``__exit__`` + # when construction fails after worker processes have started. + self.__exit__(None, None, None) + raise def get_default_sampling_params_list(self) -> list[Any]: if not hasattr(self.omni, "default_sampling_params_list"): @@ -2877,14 +803,14 @@ class OmniRunner: f"<|im_start|>user\n{user_content}<|im_end|>\n" f"<|im_start|>assistant\n" ) - input_dict: dict[str, Any] = {"prompt": full_prompt} + prompt_dict: dict[str, Any] = {"prompt": full_prompt} if multi_modal_data: - input_dict["multi_modal_data"] = multi_modal_data + prompt_dict["multi_modal_data"] = multi_modal_data if modalities: - input_dict["modalities"] = modalities + prompt_dict["modalities"] = modalities if mm_processor_kwargs: - input_dict["mm_processor_kwargs"] = mm_processor_kwargs - omni_inputs.append(input_dict) + prompt_dict["mm_processor_kwargs"] = mm_processor_kwargs + omni_inputs.append(prompt_dict) return omni_inputs def generate( @@ -2963,197 +889,11 @@ class OmniRunner: return self def __exit__(self, exc_type, exc_val, exc_tb): - if hasattr(self.omni, "close"): - self.omni.close() + omni = getattr(self, "omni", None) + if omni is not None and hasattr(omni, "close"): + omni.close() self._cleanup_process() - run_pre_test_cleanup() - run_post_test_cleanup() - cleanup_dist_env_and_memory() - - -class OmniRunnerHandler: - def __init__(self, omni_runner: OmniRunner): - self.runner = omni_runner - - def _process_omni_output(self, outputs: list[OmniRequestOutput]) -> OmniResponse: - result = OmniResponse() - try: - text_content = None - audio_content = None - for stage_output in outputs: - if getattr(stage_output, "final_output_type", None) == "text": - text_content = stage_output.outputs[0].text - if getattr(stage_output, "final_output_type", None) == "audio": - audio_content = stage_output.outputs[0].multimodal_output["audio"] - result.audio_content = audio_content - result.text_content = text_content - result.success = True - except Exception as e: - msg = f"Output processing error: {str(e)}" - result.success = False - print(f"Error: {msg}") - return result - - def _process_diffusion_output(self, outputs: list[OmniRequestOutput]) -> DiffusionResponse: - result = DiffusionResponse() - output = outputs[0] - if isinstance(output.images[0], list): - # Returning frames of images as a video - result.videos = output.images - else: - # Returning actual images - result.images = output.images - # [TODO] Add audio processing when tests are introduced - result.success = True - return result - - def send_omni_request(self, request_config: dict[str, Any] | None = None) -> OmniResponse: - if request_config is None: - request_config = {} - prompts = request_config.get("prompts") - videos = request_config.get("videos") - images = request_config.get("images") - audios = request_config.get("audios") - modalities = request_config.get("modalities", ["text", "audio"]) - outputs = self.runner.generate_multimodal( - prompts=prompts, videos=videos, images=images, audios=audios, modalities=modalities - ) - response = self._process_omni_output(outputs) - assert_omni_response(response, request_config, run_level="core_model") - return response - - def send_diffusion_request(self, request_config: dict[str, Any]) -> DiffusionResponse: - prompt = request_config.get("prompt") - if prompt is None: - prompts = request_config.get("prompts") - if not prompts: - raise ValueError("request_config must contain a prompt") - if len(prompts) > 1: - raise ValueError( - "In the current internal data structure, " - "only one prompt is supported for diffusion requests. " - "Because one prompt can contain multiple images or videos, " - "the current internal data structure is ambiguous when multiple prompts are provided." - ) - prompt = prompts[0] - - # Sync previous `extra_body` field with sampling params object - sampling_params: OmniDiffusionSamplingParams | None = request_config.get("sampling_params") - if sampling_params is None: - extra_body = request_config.get("extra_body", {}) - if extra_body: - sampling_params = OmniDiffusionSamplingParams(**extra_body) - else: - extra_body = asdict(sampling_params) - request_config["extra_body"] = extra_body - if not extra_body: - logger.warning("No sampling params provided in request_config, will skip output assertion") - - negative_prompt = extra_body.get("negative_prompt") or request_config.get("negative_prompt") - videos = request_config.get("videos") - images = request_config.get("images") - audios = request_config.get("audios") - # Full dict (e.g. image + mask_image for inpainting) or partial; merged with top-level image/video keys. - extra_multi_modal = request_config.get("multi_modal_data") - modalities = request_config.get("modalities") # only used by limited models. Do not add default value here - - prompt_object = OmniTextPrompt(prompt=prompt) - if negative_prompt: - prompt_object["negative_prompt"] = negative_prompt - multi_modal: dict = {} - if extra_multi_modal is not None: - multi_modal.update(dict(extra_multi_modal)) - if videos is not None: - multi_modal["video"] = videos - if images is not None: - multi_modal["image"] = images - if audios is not None: - multi_modal["audio"] = audios - if multi_modal: - prompt_object["multi_modal_data"] = multi_modal - if modalities: - prompt_object["modalities"] = modalities # pyright: ignore[reportGeneralTypeIssues] - - start_time = time.perf_counter() - response = self.runner.generate([prompt_object], [sampling_params] if sampling_params else None) - end_time = time.perf_counter() - - response = self._process_diffusion_output(response) - response.e2e_latency = end_time - start_time - assert_diffusion_response(response, request_config, run_level="core_model") - return response - - def send_audio_speech_request(self, request_config: dict[str, Any]) -> OmniResponse: - """ - Offline TTS: text -> audio via generate_multimodal, then validate with assert_audio_speech_response. - - request_config must contain: - - 'input' or 'prompts': text to synthesize. - Optional keys: - - 'voice' -> speaker (CustomVoice) - - 'task_type' -> task_type in additional_information (default: "CustomVoice") - - 'language' -> language in additional_information (default: "Auto") - - 'max_new_tokens' -> max_new_tokens in additional_information (default: 2048) - - 'response_format' -> desired audio format (used only for assertion) - """ - input_text = request_config.get("input") or request_config.get("prompts") - if input_text is None: - raise ValueError("request_config must contain 'input' or 'prompts' for TTS") - if isinstance(input_text, list): - input_text = input_text[0] if input_text else "" - - mm_processor_kwargs: dict[str, Any] = {} - if "voice" in request_config: - mm_processor_kwargs["speaker"] = request_config["voice"] - if "task_type" in request_config: - mm_processor_kwargs["task_type"] = request_config["task_type"] - if "ref_audio" in request_config: - mm_processor_kwargs["ref_audio"] = request_config["ref_audio"] - if "ref_text" in request_config: - mm_processor_kwargs["ref_text"] = request_config["ref_text"] - if "language" in request_config: - mm_processor_kwargs["language"] = request_config["language"] - if "max_new_tokens" in request_config: - mm_processor_kwargs["max_new_tokens"] = request_config["max_new_tokens"] - - outputs = self.runner.generate_multimodal( - prompts=input_text, - modalities=["audio"], - mm_processor_kwargs=mm_processor_kwargs or None, - ) - mm_out: dict[str, Any] | None = None - for stage_out in outputs: - if getattr(stage_out, "final_output_type", None) == "audio": - mm_out = stage_out.outputs[0].multimodal_output - break - if mm_out is None: - raise AssertionError("No audio output from pipeline") - - audio_data = mm_out.get("audio") - if audio_data is None: - raise AssertionError("No audio tensor in multimodal output") - - sr_raw = mm_out.get("sr") - sr_val = sr_raw[-1] if isinstance(sr_raw, list) and sr_raw else sr_raw - sr = int(sr_val.item() if hasattr(sr_val, "item") else sr_val) - wav_tensor = torch.cat(audio_data, dim=-1) if isinstance(audio_data, list) else audio_data - wav_buf = io.BytesIO() - sf.write( - wav_buf, - wav_tensor.float().cpu().numpy().reshape(-1), - samplerate=sr, - format="WAV", - subtype="PCM_16", - ) - result = OmniResponse(success=True, audio_bytes=wav_buf.getvalue(), audio_format="audio/wav") - assert_audio_speech_response(result, request_config, run_level="core_model") - return result - - def start_profile(self, profile_prefix: str | None = None, stages: list[int] | None = None) -> list[Any]: - return self.runner.start_profile(profile_prefix=profile_prefix, stages=stages) - - def stop_profile(self, stages: list[int] | None = None) -> list[Any]: - return self.runner.stop_profile(stages=stages) + cleanup_test_environment() # --------------------------------------------------------------------------- @@ -3177,15 +917,21 @@ def _whisper_device_free_around(): release_audio_transcriber() +def get_model_prefix() -> str: + """Return ``MODEL_PREFIX`` with a trailing slash, or ``""`` when unset.""" + prefix = os.environ.get("MODEL_PREFIX", "") + return f"{prefix.rstrip('/')}/" if prefix else "" + + def iter_omni_server( request: Any, run_level: str, - model_prefix: str, omni_fixture_lock: threading.Lock, ) -> Generator[Any, Any, None]: """Start/stop an Omni HTTP server; used by ``omni_server`` / ``omni_server_function`` fixtures.""" from tests.helpers.stage_config import stage_config_path_for_run_level + model_prefix = get_model_prefix() with omni_fixture_lock, _whisper_device_free_around(): params: OmniServerParams = request.param # For now, when a tiny model is substituted, we preserve the original model @@ -3267,13 +1013,13 @@ def iter_omni_server( def iter_omni_runner( request: Any, - model_prefix: str, run_level: str, omni_fixture_lock: threading.Lock, ) -> Generator[Any, None, None]: """Yield an :class:`OmniRunner`; used by ``omni_runner`` / ``omni_runner_function`` fixtures.""" from tests.helpers.stage_config import stage_config_path_for_run_level + model_prefix = get_model_prefix() with omni_fixture_lock, _whisper_device_free_around(): param = request.param if not isinstance(param, (tuple, list)) or len(param) not in (2, 3): @@ -3439,6 +1185,26 @@ def pi0_openpi_validate_session_result( raise AssertionError(f"Action {index} contains non-finite values") +# Re-export client types for backward-compatible imports. +from tests.helpers.client import ( # noqa: E402 + DREAMZERO_ACTION_DIM, + DREAMZERO_ACTION_HORIZON, + DREAMZERO_CAMERA_FILES, + DiffusionResponse, + HttpResponse, + OfflineOmniClient, + OmniResponse, + OmniRunnerHandler, + OnlineOmniClient, + OpenAIClientHandler, + OpenPIWebSocketResponse, + OpenPIWebSocketSession, + WebSocketJsonResponse, + build_dreamzero_demo_observations, + build_openpi_droid_observation, + load_dreamzero_camera_frames, +) + __all__ = [ "DiffusionResponse", "HttpResponse", @@ -3453,11 +1219,14 @@ __all__ = [ "OpenPIWebSocketSession", "OmniResponse", "OmniRunner", - "OmniRunnerHandler", + "OfflineOmniClient", "OmniServer", "OmniServerParams", "OmniServerStageCli", + "OnlineOmniClient", "OpenAIClientHandler", + "OmniRunnerHandler", + "get_model_prefix", "get_open_port", "dummy_messages_from_mix_data", "pi0_openpi_require_dependencies", diff --git a/tests/helpers/stage_config.py b/tests/helpers/stage_config.py index 8706bb1bc..084f84590 100644 --- a/tests/helpers/stage_config.py +++ b/tests/helpers/stage_config.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Config/message construction helpers used by tests.""" import atexit @@ -11,8 +14,8 @@ import yaml def modify_stage_config( yaml_path: str, - updates: dict[str, Any] = None, - deletes: dict[str, Any] = None, + updates: dict[str, Any] | None = None, + deletes: dict[str, Any] | None = None, ) -> str: """ Modify configurations in a YAML file, supporting both top-level and stage-specific modifications, @@ -23,17 +26,17 @@ def modify_stage_config( updates: Dictionary containing both top-level and stage-specific modifications to add or update. Format: { 'async_chunk': True, - 'stage_args': { - 0: {'engine_args.max_model_len': 5800}, - 1: {'engine_args.max_num_seqs': 2} + 'stages': { + 0: {'max_model_len': 5800}, + 1: {'max_num_seqs': 2} } } deletes: Dictionary containing configurations to delete. Format: { 'old_config': None, # Delete entire key - 'stage_args': { - 0: ['engine_args.old_param'], - 1: ['runtime.unused_setting'] + 'stages': { + 0: ['old_param'], + 1: ['unused_setting'] } } @@ -164,24 +167,20 @@ def modify_stage_config( else: print(f"Path {path} does not exist") - _stage_key = "stages" if "stages" in config else "stage_args" + stages = get_stage_entries(config) # Apply deletions first if deletes: for key, value in deletes.items(): - if key in ("stage_args", "stages"): + if key == "stages": if value and isinstance(value, dict): - stage_args = config.get(_stage_key, []) - if not stage_args: - raise ValueError("stage_args does not exist in config") - for stage_id, delete_paths in value.items(): if not delete_paths: continue # Find stage by ID target_stage = None - for stage in stage_args: + for stage in stages: if stage.get("stage_id") == int(stage_id): target_stage = stage break @@ -204,28 +203,24 @@ def modify_stage_config( # Apply updates if updates: for key, value in updates.items(): - if key in ("stage_args", "stages"): + if key == "stages": if value and isinstance(value, dict): - stage_args = config.get(_stage_key, []) - if not stage_args: - raise ValueError("stage_args does not exist in config") - for stage_id, stage_updates in value.items(): # Find stage by ID target_stage = None - for stage in stage_args: + for stage in stages: if stage.get("stage_id") == int(stage_id): target_stage = stage break if target_stage is None: - available_ids = [s.get("stage_id") for s in stage_args if "stage_id" in s] + available_ids = [s.get("stage_id") for s in stages if "stage_id" in s] raise KeyError(f"Stage ID {stage_id} not found, available: {available_ids}") # Apply updates to this stage for update_path, val in stage_updates.items(): # Check if this is a simple key (not dot-separated) - # Example: 'engine_input_source' vs 'engine_args.max_model_len' + # Example: 'engine_input_source' vs nested 'default_sampling_params.max_tokens' if "." not in update_path: # Direct key assignment (e.g., updating a list value) target_stage[update_path] = val @@ -624,6 +619,36 @@ def _materialize_ci_overlay(model_type: str) -> Path: return out +def get_stage_entries(cfg: dict[str, Any]) -> list[dict[str, Any]]: + """Return the ``stages`` list from a new-schema deploy config dict. + + Raises ``ValueError`` when the top-level ``stages`` key is missing. + """ + stages = cfg.get("stages") + if stages is None: + raise ValueError("Deploy config missing top-level 'stages'") + return list(stages) + + +def load_stage_ids(resolved_config: dict[str, Any]) -> list[int]: + """Return ``stage_id`` values from a resolved deploy config dict.""" + stage_ids = [stage["stage_id"] for stage in get_stage_entries(resolved_config) if "stage_id" in stage] + if not stage_ids: + raise ValueError("No stage IDs found in resolved config") + return stage_ids + + +def load_stage_replica_counts(resolved_config: dict[str, Any]) -> dict[int, int]: + """Return ``{stage_id: num_replicas}`` from flat stage entries (default 1).""" + replica_counts: dict[int, int] = {} + for stage in get_stage_entries(resolved_config): + stage_id = stage.get("stage_id") + if stage_id is None: + continue + replica_counts[int(stage_id)] = max(1, int(stage.get("num_replicas", 1))) + return replica_counts + + def get_deploy_config_path(rel_path: str) -> str: """Resolve a deploy yaml; ``ci/.yaml`` materializes from ``_CI_OVERLAYS``.""" if rel_path.startswith("ci/") and rel_path.endswith(".yaml"): @@ -638,33 +663,20 @@ def get_deploy_config_stage(rel_path: str, stage_id: int) -> dict[str, Any]: with open(get_deploy_config_path(rel_path), encoding="utf-8") as f: cfg = yaml.safe_load(f) or {} - stage_key = "stages" if "stages" in cfg else "stage_args" - for stage in cfg.get(stage_key, []): + for stage in get_stage_entries(cfg): if stage.get("stage_id") == stage_id: return stage raise KeyError(f"No stage_id={stage_id} in deploy config {rel_path!r}") -def _get_config_value_by_path(config_dict: dict, path: str) -> Any: - """Read a dot-separated path from a nested dict (e.g. ``engine_args.load_format``).""" - current: Any = config_dict - for key in path.split("."): - if not isinstance(current, dict) or key not in current: - return None - current = current[key] - return current - - -def _stage_load_format_paths(stage_config_path: str) -> tuple[str, str, list[int]]: - """Return ``(stage_key, load_format_field_path, stage_ids)`` for a deploy YAML.""" +def _stage_ids_from_deploy_yaml(stage_config_path: str) -> list[int]: + """Return ``stage_id`` values from a new-schema deploy YAML (``stages``).""" with open(stage_config_path, encoding="utf-8") as f: cfg = yaml.safe_load(f) or {} - new_schema_stages = cfg.get("stages") - stage_key = "stages" if new_schema_stages is not None else "stage_args" - load_format_path = "load_format" if new_schema_stages is not None else "engine_args.load_format" - stage_entries = cfg.get(stage_key, []) - stage_ids = [stage["stage_id"] for stage in stage_entries if "stage_id" in stage] - return stage_key, load_format_path, stage_ids + try: + return load_stage_ids(cfg) + except ValueError as exc: + raise ValueError(f"Deploy YAML missing top-level 'stages': {stage_config_path}") from exc def _add_dummy_load_format( @@ -674,10 +686,10 @@ def _add_dummy_load_format( """For ``core_model`` runs, patch every stage in the deploy YAML to ``load_format: dummy``.""" if run_level != "core_model" or stage_config_path is None: return stage_config_path - stage_key, load_format_path, stage_ids = _stage_load_format_paths(stage_config_path) + stage_ids = _stage_ids_from_deploy_yaml(stage_config_path) return modify_stage_config( stage_config_path, - updates={stage_key: {stage_id: {load_format_path: "dummy"} for stage_id in stage_ids}}, + updates={"stages": {stage_id: {"load_format": "dummy"} for stage_id in stage_ids}}, ) @@ -688,25 +700,27 @@ def _delete_dummy_load_format( """For ``advanced_model`` / ``full_model``, strip ``load_format: dummy`` so real weights load.""" if run_level not in {"advanced_model", "full_model"} or stage_config_path is None: return stage_config_path - stage_key, load_format_path, _stage_ids = _stage_load_format_paths(stage_config_path) with open(stage_config_path, encoding="utf-8") as f: cfg = yaml.safe_load(f) or {} - stage_entries = cfg.get(stage_key, []) + try: + stages = get_stage_entries(cfg) + except ValueError as exc: + raise ValueError(f"Deploy YAML missing top-level 'stages': {stage_config_path}") from exc deletes: dict[int, list[str]] = {} - for stage in stage_entries: + for stage in stages: stage_id = stage.get("stage_id") if stage_id is None: continue - if _get_config_value_by_path(stage, load_format_path) == "dummy": - deletes[stage_id] = [load_format_path] + if stage.get("load_format") == "dummy": + deletes[stage_id] = ["load_format"] if not deletes: return stage_config_path return modify_stage_config( stage_config_path, - deletes={stage_key: deletes}, + deletes={"stages": deletes}, ) @@ -720,6 +734,9 @@ def stage_config_path_for_run_level(stage_config_path: str | None, run_level: st __all__ = [ "get_deploy_config_path", "get_deploy_config_stage", + "get_stage_entries", + "load_stage_ids", + "load_stage_replica_counts", "modify_stage_config", "stage_config_path_for_run_level", ] diff --git a/tests/helpers/tests/test_runtime_omni_runner.py b/tests/helpers/tests/test_runtime_omni_runner.py new file mode 100644 index 000000000..aaada20f2 --- /dev/null +++ b/tests/helpers/tests/test_runtime_omni_runner.py @@ -0,0 +1,40 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + +"""Tests for OmniRunner startup rollback.""" + +from __future__ import annotations + +import pytest + +from tests.helpers.runtime import OmniRunner + +pytestmark = [pytest.mark.core_model, pytest.mark.cpu] + + +def test_runner_init_rolls_back_on_omni_startup_failure(monkeypatch: pytest.MonkeyPatch) -> None: + """``__exit__`` is skipped when construction raises; rollback must still run.""" + cleaned: list[str] = [] + monkeypatch.setattr( + "tests.helpers.runtime.cleanup_test_environment", + lambda: cleaned.append("env"), + ) + monkeypatch.setattr( + OmniRunner, + "_cleanup_process", + lambda self: cleaned.append("process"), + ) + + class _BoomOmni: + def __init__(self, *args, **kwargs) -> None: + raise RuntimeError("Orchestrator initialization failed") + + monkeypatch.setattr("vllm_omni.entrypoints.omni.Omni", _BoomOmni) + + with pytest.raises(RuntimeError, match="Orchestrator initialization failed"): + with OmniRunner("fake-model"): + raise AssertionError("context body must not run after a failed constructor") + + assert cleaned.count("process") == 1 + # Once at the start of ``__init__``, once from the constructor rollback. + assert cleaned.count("env") == 2 diff --git a/tests/helpers/tests/test_runtime_stage_cli.py b/tests/helpers/tests/test_runtime_stage_cli.py index 5a2ab5d68..564f12cf1 100644 --- a/tests/helpers/tests/test_runtime_stage_cli.py +++ b/tests/helpers/tests/test_runtime_stage_cli.py @@ -1,8 +1,12 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Tests for OmniServerStageCli process planning helpers.""" from __future__ import annotations from pathlib import Path +from types import SimpleNamespace import pytest @@ -38,23 +42,48 @@ def test_stage_cli_builds_headless_replica_cmd(tmp_path: Path) -> None: assert cmd[cmd.index("--deploy-config") + 1] == server.stage_config_path -def test_stage_cli_splits_devices_per_replica(tmp_path: Path) -> None: +def test_stage_cli_loads_stage_ids_and_replica_counts(tmp_path: Path) -> None: server = OmniServerStageCli("fake-model", _write_stage_config(tmp_path), []) - assert server._devices_for_replica(1, "1,2,3", 0) == "1" - assert server._devices_for_replica(1, "1,2,3", 1) == "2" - assert server._devices_for_replica(1, "1,2,3", 2) == "3" + assert server.stage_ids == [0, 1] + assert server.stage_replica_counts == {0: 1, 1: 3} -def test_stage_cli_maps_visible_devices_after_replica_split(tmp_path: Path) -> None: - server = OmniServerStageCli( - "fake-model", - _write_stage_config(tmp_path), - [], - env_dict={"CUDA_VISIBLE_DEVICES": "4,5,6,7"}, - ) - env: dict[str, str] = {} +def test_stage_cli_enter_rolls_back_launched_stages_on_startup_failure( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + """``__exit__`` is skipped when ``__enter__`` raises; rollback must still run.""" + monkeypatch.setattr("tests.helpers.runtime.time.sleep", lambda *_args, **_kwargs: None) + monkeypatch.setattr("tests.helpers.runtime.cleanup_test_environment", lambda: None) - server._set_stage_device_env(1, env, "1,2,3", replica_id=1) + server = OmniServerStageCli("fake-model", _write_stage_config(tmp_path), ["--disable-log-stats"]) + log_path = tmp_path / "stage0.log" + log_fh = open(log_path, "w", encoding="utf-8") + log_fh.write("partial launch\n") + log_fh.flush() - assert env["CUDA_VISIBLE_DEVICES"] == "6" + proc = SimpleNamespace(pid=4242, poll=lambda: None) + killed: list[int] = [] + monkeypatch.setattr(server, "_kill_process_tree", lambda pid: killed.append(pid)) + + def _fake_launch(stage_id: int, *, headless: bool, replica_id: int = 0) -> None: + stage_key = (stage_id, replica_id) + server.stage_procs[stage_key] = proc # type: ignore[assignment] + server._stage_log_paths[stage_key] = log_path + server._stage_log_files[stage_key] = log_fh + if stage_id == 0 and replica_id == 0: + server.proc = proc # type: ignore[assignment] + + def _fail_alive() -> None: + raise RuntimeError("Stage 0 replica 0 exited with code 1") + + monkeypatch.setattr(server, "_launch_stage", _fake_launch) + monkeypatch.setattr(server, "_ensure_stage_processes_alive", _fail_alive) + + with pytest.raises(RuntimeError, match="exited with code 1"): + with server: + raise AssertionError("context body must not run after a failed enter") + + assert killed == [4242] + assert log_fh.closed + assert not log_path.exists() diff --git a/tests/helpers/tests/test_runtime_transcriber_release.py b/tests/helpers/tests/test_runtime_transcriber_release.py index 6bc7d4221..5e7f29ebe 100644 --- a/tests/helpers/tests/test_runtime_transcriber_release.py +++ b/tests/helpers/tests/test_runtime_transcriber_release.py @@ -1,5 +1,5 @@ # SPDX-License-Identifier: Apache-2.0 -# SPDX-FileCopyrightText: Copyright contributors to the vLLM project +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project """The server/runner fixtures free the Whisper judge around each instance. @@ -48,7 +48,7 @@ def _server_generator(): param=OmniServerParams(model="fake-model"), node=SimpleNamespace(get_closest_marker=lambda name: None), ) - return runtime.iter_omni_server(request, "core_model", "", threading.Lock()) + return runtime.iter_omni_server(request, "core_model", threading.Lock()) def test_releases_before_construction_and_after_teardown(events): diff --git a/tests/model_tests/diffusion/task_runners.py b/tests/model_tests/diffusion/task_runners.py index 2be2338c1..4fb31fb5c 100644 --- a/tests/model_tests/diffusion/task_runners.py +++ b/tests/model_tests/diffusion/task_runners.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Definitions running and validating individual tasks, e.g., text to image, image to image, and so on. These are called by the core test runner. @@ -6,12 +9,13 @@ image to image, and so on. These are called by the core test runner. import base64 import io from dataclasses import replace +from typing import Any import numpy as np import pytest from PIL import Image -from tests.helpers.runtime import DiffusionResponse, OmniServer, OpenAIClientHandler, dummy_messages_from_mix_data +from tests.helpers.runtime import DiffusionResponse, OmniServer, OnlineOmniClient, dummy_messages_from_mix_data from tests.model_tests.diffusion.config_types import DiffusionTasks from vllm_omni.entrypoints.omni import Omni from vllm_omni.inputs.data import OmniDiffusionSamplingParams @@ -48,7 +52,7 @@ IMAGE_GEN_EXTRA_BODY = { } # Online form_data for video generation requests (multipart /v1/videos API) -VIDEO_GEN_FORM_DATA = { +VIDEO_GEN_FORM_DATA: dict[str, Any] = { "height": HEIGHT, "width": WIDTH, "num_inference_steps": 4, @@ -171,7 +175,7 @@ def _build_online_image_data_url() -> str: def _run_online_t2i( - server: OmniServer, client: OpenAIClientHandler, extra_body: dict | None = None + server: OmniServer, client: OnlineOmniClient, extra_body: dict | None = None ) -> list[DiffusionResponse]: """Run a text to image request through the server.""" messages = dummy_messages_from_mix_data(content_text=PROMPT) @@ -184,7 +188,7 @@ def _run_online_t2i( def _run_online_i2i( - server: OmniServer, client: OpenAIClientHandler, extra_body: dict | None = None + server: OmniServer, client: OnlineOmniClient, extra_body: dict | None = None ) -> list[DiffusionResponse]: """Run an image to image request through the server.""" image_data_url = _build_online_image_data_url() @@ -278,7 +282,7 @@ def run_and_validate_multi_output(omni: Omni, task_type: DiffusionTasks): def _run_online_t2v( - server: OmniServer, client: OpenAIClientHandler, form_data: dict | None = None + server: OmniServer, client: OnlineOmniClient, form_data: dict | None = None ) -> list[DiffusionResponse]: """Run a text to video request through the server's /v1/videos API.""" data = dict(form_data or VIDEO_GEN_FORM_DATA) @@ -287,7 +291,7 @@ def _run_online_t2v( return client.send_video_diffusion_request({"form_data": data}) -def _run_online_i2v(server: OmniServer, client: OpenAIClientHandler) -> list[DiffusionResponse]: +def _run_online_i2v(server: OmniServer, client: OnlineOmniClient) -> list[DiffusionResponse]: """Run an image to video request through the server's /v1/videos API.""" data = dict(VIDEO_GEN_FORM_DATA) data.setdefault("prompt", PROMPT) @@ -306,17 +310,17 @@ def _get_online_videos(responses: list[DiffusionResponse]) -> list: ### Online task runners -def run_and_validate_online_text_to_image_request(server: OmniServer, client: OpenAIClientHandler): +def run_and_validate_online_text_to_image_request(server: OmniServer, client: OnlineOmniClient): """Run and validate a text to image request through the server.""" _validate_images(_get_online_images(_run_online_t2i(server, client))) -def run_and_validate_online_image_to_image_request(server: OmniServer, client: OpenAIClientHandler): +def run_and_validate_online_image_to_image_request(server: OmniServer, client: OnlineOmniClient): """Run and validate an image to image request through the server.""" _validate_images(_get_online_images(_run_online_i2i(server, client))) -def run_and_validate_online_determinism(server: OmniServer, client: OpenAIClientHandler, task_type: DiffusionTasks): +def run_and_validate_online_determinism(server: OmniServer, client: OnlineOmniClient, task_type: DiffusionTasks): """Checks for determinism through the server, dispatching by task type. Video is skipped online only: send_video_diffusion_request returns encoded @@ -342,7 +346,7 @@ def run_and_validate_online_determinism(server: OmniServer, client: OpenAIClient raise ValueError(f"Task type {task_type} is not yet supported for determinism checks") -def run_and_validate_online_multi_output(server: OmniServer, client: OpenAIClientHandler, task_type: DiffusionTasks): +def run_and_validate_online_multi_output(server: OmniServer, client: OnlineOmniClient, task_type: DiffusionTasks): """Checks for multi-output through the server, dispatching by task type. Video is skipped online only: send_video_diffusion_request explicitly @@ -368,11 +372,11 @@ def run_and_validate_online_multi_output(server: OmniServer, client: OpenAIClien raise ValueError(f"Task type {task_type} is not yet supported for multi-output checks") -def run_and_validate_online_text_to_video_request(server: OmniServer, client: OpenAIClientHandler): +def run_and_validate_online_text_to_video_request(server: OmniServer, client: OnlineOmniClient): """Run and validate a text to video request through the server.""" _get_online_videos(_run_online_t2v(server, client)) -def run_and_validate_online_image_to_video_request(server: OmniServer, client: OpenAIClientHandler): +def run_and_validate_online_image_to_video_request(server: OmniServer, client: OnlineOmniClient): """Run and validate an image to video request through the server.""" _get_online_videos(_run_online_i2v(server, client)) diff --git a/tests/model_tests/diffusion/test_common_online.py b/tests/model_tests/diffusion/test_common_online.py index d039ce614..9c7ef8a0a 100644 --- a/tests/model_tests/diffusion/test_common_online.py +++ b/tests/model_tests/diffusion/test_common_online.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """ Analogous to test_common_offline, but for server tests. Validates the full online serving stack (CLI arg parsing, subprocess, API routing, response @@ -6,7 +9,7 @@ encoding) using tiny models. import pytest -from tests.helpers.runtime import OmniServer, OpenAIClientHandler +from tests.helpers.runtime import OmniServer, OnlineOmniClient from tests.model_tests.diffusion.case_filtering import get_parametrized_options from tests.model_tests.diffusion.config_types import ( DiffusionAccs, @@ -50,7 +53,7 @@ def test_online_on_supported_tasks( # TODO: We may want to revisit run_level validation here, # because checks for things like image size etc should not # depend on whether or not the weights are real or random - client = OpenAIClientHandler( + client = OnlineOmniClient( host=server.host, port=server.port, api_key="EMPTY", diff --git a/tests/utils/test_speaker_cache.py b/tests/utils/test_speaker_cache.py index 373baacb4..ea50c5881 100644 --- a/tests/utils/test_speaker_cache.py +++ b/tests/utils/test_speaker_cache.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + import threading import pytest @@ -144,7 +147,7 @@ class TestSpeakerEmbeddingCacheBehavior: class TestSingleton: - def test_singleton_identity(self, fresh_speaker_cache): + def test_singleton_identity(self, clean_speaker_cache): a = get_speaker_cache() b = get_speaker_cache() assert a is b diff --git a/tests/utils/test_speaker_cache_integration.py b/tests/utils/test_speaker_cache_integration.py index 8a24659f0..01b886725 100644 --- a/tests/utils/test_speaker_cache_integration.py +++ b/tests/utils/test_speaker_cache_integration.py @@ -1,3 +1,6 @@ +# SPDX-License-Identifier: Apache-2.0 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project + """Integration tests for the process-wide speaker cache across serving + models.""" import pytest @@ -26,7 +29,7 @@ class TestSpeakerCacheIntegration: assert cache.get(fish_key) is None assert cache.get(bob_key) is not None - def test_singleton_shared_across_call_sites(self, fresh_speaker_cache): + def test_singleton_shared_across_call_sites(self, clean_speaker_cache): cache_a = get_speaker_cache() cache_b = get_speaker_cache() assert cache_a is cache_b