Compare commits

..
Author SHA1 Message Date
Sy03 6ffdf1a8f1 [Bugfix][Qwen3-TTS] Handle codec generations that exhaust their token budget
Signed-off-by: Sy03 <1370724210@qq.com>
2026-08-29 14:50:42 +08:00
177 changed files with 933 additions and 10952 deletions

No files matched your search

+1 -1
View File
@@ -175,7 +175,7 @@ steps:
grade: Blocking
commands:
- export VLLM_WORKER_MULTIPROC_METHOD=spawn
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py tests/e2e/offline_inference/test_qwen3_omni_colocate_async.py -m 'advanced_model and cuda' --run-level 'advanced_model'
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py -m 'advanced_model and cuda' --run-level 'advanced_model'
- label: "Qwen3-TTS CustomVoice E2E Test · Shard %N/%t"
agent_pool: mi300_1
+1 -3
View File
@@ -122,7 +122,6 @@ steps:
- label: "Omni · Qwen3-Omni Test"
source_file_dependencies:
- tests/e2e/offline_inference/test_qwen3_omni.py
- tests/e2e/offline_inference/test_qwen3_omni_colocate_async.py
- tests/e2e/online_serving/test_qwen3_omni.py
- tests/helpers/runtime.py
- vllm_omni/entrypoints/openai/serving_chat.py
@@ -133,10 +132,9 @@ steps:
- vllm_omni/model_executor/models/qwen3_tts/cuda_graph_decoder_wrapper.py
- vllm_omni/model_executor/stage_input_processors/qwen3_omni.py
- vllm_omni/deploy/qwen3_omni_moe.yaml
- vllm_omni/deploy/qwen3_omni_moe_thinking.yaml
timeout_in_minutes: 50
commands:
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py tests/e2e/offline_inference/test_qwen3_omni_colocate_async.py -m 'advanced_model and cuda' --run-level 'advanced_model'
- pytest -s -v tests/e2e/offline_inference/test_qwen3_omni.py tests/e2e/online_serving/test_qwen3_omni.py -m 'advanced_model and cuda' --run-level 'advanced_model'
mirror_hardwares: h100_2
- label: "Omni · MiniCPM-o 4.5 Test"
+34 -108
View File
@@ -12,22 +12,16 @@ steps:
if: build.env("NIGHTLY") == "1" || build.pull_request.labels includes "nightly-test" || build.pull_request.labels includes "omni-test"
steps:
- label: ":full_moon: Omni · Function Test with H100 · Single-GPU"
timeout_in_minutes: 90
- label: ":full_moon: Omni · Function Test with H100"
timeout_in_minutes: 120
commands:
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_1" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
mirror_hardwares: h100_1
- label: ":full_moon: Omni · Function Test with H100 · 2-GPU"
timeout_in_minutes: 90
commands:
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni and cards_2" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py
- pytest -sv tests/e2e --run-level "full_model" -m "full_model and H100 and omni" --ignore=tests/e2e/accuracy --ignore=tests/e2e/online_serving/test_qwen3_omni_multi_replicas.py --ignore=tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py
mirror_hardwares: h100_2
- label: ":full_moon: Omni · MiniCPM-o 4.5 Duplex Test"
timeout_in_minutes: 50
commands:
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py -m "full_model and cuda and H100 and omni and cards_1" --run-level "full_model"
- pytest -s -v tests/e2e/online_serving/test_minicpmo_4_5_duplex_expansion.py -m "full_model and cuda and H100 and omni" --run-level "full_model"
mirror_hardwares: h100_1
- label: ":full_moon: Omni · Doc Test with L4"
@@ -37,10 +31,10 @@ steps:
- pytest -s -v tests/examples/ -m "full_model and omni and L4" --run-level "full_model"
mirror_hardwares: l4_4
- label: ":full_moon: Omni · Doc Test with H100 · 2-GPU"
- label: ":full_moon: Omni · Doc Test with H100"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/examples/ -m "full_model and omni and H100 and cards_2" --run-level "full_model"
- pytest -s -v tests/examples/ -m "full_model and omni and H100" --run-level "full_model"
mirror_hardwares: h100_2
- label: ":full_moon: Omni · Accuracy Test"
@@ -142,29 +136,15 @@ steps:
- pytest -s -v tests/e2e/ -m "full_model and L4 and tts" --run-level "full_model" --ignore=tests/e2e/accuracy
mirror_hardwares: l4_1
- label: ":full_moon: TTS · Perf Test · Single-GPU"
key: nightly-tts-performance-single
- label: ":full_moon: TTS · Perf Test"
key: nightly-tts-performance
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json -m "H100 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
mirror_hardwares: h100_1
- label: ":full_moon: TTS · Perf Test · 2-GPU"
key: nightly-tts-performance-2gpu
timeout_in_minutes: 180
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json -m "H100 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
@@ -199,6 +179,7 @@ steps:
build.pull_request.labels includes "nightly-test" ||
build.pull_request.labels includes "diffusion-x2iat-test"
steps:
# Use explicit file shards so single-card cases do not reserve 2+ GPUs.
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100 · Single-GPU"
timeout_in_minutes: 120
@@ -206,24 +187,24 @@ steps:
- >-
pytest -sv
tests/e2e/online_serving/test_qwen_image_expansion.py
-m "full_model and diffusion and H100 and cards_1"
-m "full_model and diffusion and H100 and not distributed_cuda"
--run-level "full_model"
mirror_hardwares: h100_1
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100 · 2-GPU"
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with H100 · Multi-GPU Qwen-Image"
timeout_in_minutes: 120
commands:
- >-
pytest -sv
tests/e2e/online_serving/test_qwen_image_expansion.py
-m "full_model and diffusion and H100 and cards_2"
-m "full_model and diffusion and H100 and distributed_cuda"
--run-level "full_model"
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with L4 · Multi-GPU"
- label: ":full_moon: Diffusion X2I(&A&T) · Function Test with L4"
timeout_in_minutes: 120
commands:
- pytest -sv tests/e2e/ -k "not test_wan and not hunyuan" -m "full_model and diffusion and L4 and not cards_1" --run-level "full_model" --ignore=tests/e2e/accuracy
- pytest -sv tests/e2e/ -k "not test_wan and not hunyuan" -m "full_model and diffusion and L4" --run-level "full_model" --ignore=tests/e2e/accuracy
mirror_hardwares: l4_4
- label: ":full_moon: Diffusion X2I(&A&T) · Doc Test"
@@ -254,8 +235,8 @@ steps:
- pytest -s -v tests/e2e/accuracy/test_hidream_o1_image.py -m full_model --run-level full_model
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image · Single-GPU"
key: nightly-diffusion-x2iat-performance-qwen-image-single
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image"
key: nightly-diffusion-x2iat-performance-qwen-image
timeout_in_minutes: 180
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
@@ -263,23 +244,7 @@ steps:
- export CACHE_DIT_VERSION=1.5.0
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json -m "H100 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
exit $$EXIT
mirror_hardwares: h100_1
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image · 4-GPU"
key: nightly-diffusion-x2iat-performance-qwen-image-4gpu
timeout_in_minutes: 180
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
- export CACHE_DIT_VERSION=1.5.0
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json -m "H100 and cards_4"
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
@@ -295,28 +260,16 @@ steps:
build.pull_request.labels includes "diffusion-x2v-test"
steps:
- label: ":full_moon: Diffusion X2V · Wan2.2 T2V Function Test · Single-GPU"
- label: ":full_moon: Diffusion X2V · Wan2.2 T2V Function Test"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "t2v" -m "full_model and cuda and H100 and cards_1" --run-level "full_model"
mirror_hardwares: h100_1
- label: ":full_moon: Diffusion X2V · Wan2.2 T2V Function Test · 2-GPU"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "t2v" -m "full_model and cuda and H100 and cards_2" --run-level "full_model"
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "t2v" -m "full_model and cuda" --run-level "full_model"
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2V · Wan2.2 I2V/TI2V Function Test · Single-GPU"
- label: ":full_moon: Diffusion X2V · Wan2.2 I2V/TI2V Function Test"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "i2v" -m "full_model and cuda and H100 and cards_1" --run-level "full_model"
mirror_hardwares: h100_1
- label: ":full_moon: Diffusion X2V · Wan2.2 I2V/TI2V Function Test · 2-GPU"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "i2v" -m "full_model and cuda and H100 and cards_2" --run-level "full_model"
- pytest -s -v tests/e2e/online_serving/test_wan22_expansion.py -k "i2v" -m "full_model and cuda" --run-level "full_model"
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2V · HunyuanVideo-1.5 Function Test"
@@ -357,32 +310,17 @@ steps:
- pytest -s -v tests/e2e/accuracy/hunyuanvideo15_i2v/test_hunyuanvideo15_i2v_video_similarity.py -m full_model --run-level full_model
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion X2V · Perf Test · Single-GPU"
key: nightly-diffusion-x2v-performance-single
- label: ":full_moon: Diffusion X2V · Perf Test"
key: nightly-diffusion-x2v-performance
timeout_in_minutes: 180
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
exit $$EXIT
mirror_hardwares: h100_1
- label: ":full_moon: Diffusion X2V · Perf Test · 2-GPU"
key: nightly-diffusion-x2v-performance-2gpu
timeout_in_minutes: 180
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json
EXIT1=$$?
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json -m "H100 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json
EXIT2=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
@@ -433,16 +371,10 @@ steps:
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and L4' --run-level "full_model"
mirror_hardwares: l4_4
- label: ":full_moon: Diffusion · Distributed Test with H100 · 2-GPU"
- label: ":full_moon: Diffusion · Distributed Test with H100"
timeout_in_minutes: 60
commands:
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and H100 and cards_2' --run-level "full_model"
mirror_hardwares: h100_2
- label: ":full_moon: Diffusion · Distributed Test with H100 · 4-GPU"
timeout_in_minutes: 60
commands:
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and H100 and cards_4' --run-level "full_model"
- pytest -sv tests/diffusion/distributed/ -m 'full_model and cuda and H100' --run-level "full_model"
mirror_hardwares: h100_4
- label: ":full_moon: Diffusion · Offloader Test"
@@ -477,25 +409,19 @@ steps:
depends_on:
- nightly-omni-performance-no-async-chunk
- nightly-omni-performance-async-chunk
- nightly-tts-performance-single
- nightly-tts-performance-2gpu
- nightly-diffusion-x2iat-performance-qwen-image-single
- nightly-diffusion-x2iat-performance-qwen-image-4gpu
- nightly-diffusion-x2v-performance-single
- nightly-diffusion-x2v-performance-2gpu
- nightly-tts-performance
- nightly-diffusion-x2iat-performance-qwen-image
- nightly-diffusion-x2v-performance
if: build.env("NIGHTLY") == "1" && build.env("EMAIL_DISTRIBUTION") == "1"
commands:
- pip install openpyxl
- export DEFAULT_INPUT_DIR=tests/dfx/perf/results
- export DEFAULT_OUTPUT_DIR=tests/dfx/perf/results
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-tts-performance-single
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-tts-performance-2gpu
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-tts-performance
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-no-async-chunk
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-omni-performance-async-chunk
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-single
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image-4gpu
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2v-performance-single
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2v-performance-2gpu
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2iat-performance-qwen-image
- buildkite-agent artifact download "tests/dfx/perf/results/*.json" . --step nightly-diffusion-x2v-performance
- python tools/nightly/generate_nightly_perf_excel.py
- python tools/nightly/generate_nightly_perf_html.py
- python tools/nightly/send_nightly_email.py --report-file "tests/dfx/perf/results/*.xlsx, tests/dfx/perf/results/*.html"
+14 -48
View File
@@ -44,16 +44,10 @@ steps:
- pytest -s -v tests/dfx/reliability/test_reliability_hunyuan_image.py -m "slow"
mirror_hardwares: h100_4
- label: "Reliability Test · Invalid parameters · H100 · Single-GPU"
timeout_in_minutes: 90
- label: "Reliability Test · Invalid parameters · H100"
timeout_in_minutes: 120
commands:
- pytest -s -v tests/dfx/reliability/invalid_param_test/ -m "slow and H100 and cards_1"
mirror_hardwares: h100_1
- label: "Reliability Test · Invalid parameters · H100 · 2-GPU"
timeout_in_minutes: 90
commands:
- pytest -s -v tests/dfx/reliability/invalid_param_test/ -m "slow and H100 and cards_2"
- pytest -s -v tests/dfx/reliability/invalid_param_test/ -m "slow and H100"
mirror_hardwares: h100_2
- label: "Reliability Test · Invalid parameters · L4"
@@ -111,37 +105,27 @@ steps:
if: build.env("NON_CRITICAL") == "1"
steps:
# Qwen2.5 expansion (CUDA 4-card / online 2-card) stays on this l4_4 job.
- label: "Omni · L4 · Multi-GPU"
- label: "Omni · L4"
timeout_in_minutes: 90
commands:
- export VLLM_ALLOW_LONG_MAX_MODEL_LEN="1"
- pytest -s -v tests/e2e/*/test_qwen2_5_omni_expansion.py -m "slow and L4 and omni and not cards_1" --run-level "core_model"
- pytest -s -v tests/e2e/*/test_qwen2_5_omni_expansion.py --run-level "core_model"
- >-
pytest -s -v tests/e2e/
--ignore=tests/e2e/online_serving/test_qwen2_5_omni_expansion.py
--ignore=tests/e2e/offline_inference/test_qwen2_5_omni_expansion.py
-m "slow and L4 and omni and not cards_1"
-m "slow and L4 and omni"
--run-level "full_model"
mirror_hardwares: l4_4
- label: "Omni · H100 · Single-GPU"
- label: "Omni · H100"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and H100 and omni and cards_1"
tests/e2e/ -m "slow and H100 and omni"
--run-level "full_model"
mirror_hardwares: h100_1
- label: "Omni · H100 · 4-GPU"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and H100 and omni and cards_4"
--run-level "full_model"
mirror_hardwares: h100_4
mirror_hardwares: h100_2
- label: "TTS · L4"
timeout_in_minutes: 120
@@ -185,21 +169,12 @@ steps:
--run-level "full_model"
mirror_hardwares: l4_1
- label: "Diffusion · L4 · Single-GPU"
- label: "Diffusion · L4"
timeout_in_minutes: 60
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and L4 and cards_1"
--run-level "full_model"
mirror_hardwares: l4_1
- label: "Diffusion · L4 · Multi-GPU"
timeout_in_minutes: 60
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and L4 and not cards_1"
tests/e2e/ -m "slow and diffusion and L4"
--run-level "full_model"
mirror_hardwares: l4_4
@@ -208,24 +183,15 @@ steps:
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and H100 and cards_1"
tests/e2e/ -m "slow and diffusion and H100 and not distributed_cuda"
--run-level "full_model"
mirror_hardwares: h100_1
- label: "Diffusion · H100 · 2-GPU"
- label: "Diffusion · H100 · Multi-GPU"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and H100 and cards_2"
--run-level "full_model"
mirror_hardwares: h100_2
- label: "Diffusion · H100 · 4-GPU"
timeout_in_minutes: 90
commands:
- >-
pytest -sv
tests/e2e/ -m "slow and diffusion and H100 and cards_4"
tests/e2e/ -m "slow and diffusion and H100 and distributed_cuda"
--run-level "full_model"
mirror_hardwares: h100_4
-12
View File
@@ -10,18 +10,6 @@ steps:
-m 'core_model and npu and A3 and omni'
--run-level 'core_model'
- label: "Diffusion NPU layer tests"
key: ready-diffusion-npu-layer-tests
depends_on: upload-ready-pipeline
mirror_hardwares: a3_npu_2
commands:
- >-
pytest -s -v
tests/diffusion/layers/test_activation.py
tests/diffusion/layers/test_fused_qk_norm_rope_npu.py
-m 'core_model and npu and A3'
--run-level 'core_model'
- group: ":card_index_dividers: Omni Model Test"
key: ready-omni-group
depends_on: upload-ready-pipeline
+1 -1
View File
@@ -338,7 +338,7 @@ Classify the model's **CI priority** first:
**L2 & L3 online — same file, dual marks on the baseline smoke:** The **first / simplest** case in `test_{slug}.py` (default deploy, minimal steps, single prompt) should carry **both** `@pytest.mark.core_model` and `@pytest.mark.advanced_model` on the **same** function so L2 (`test-ready.yml`) and L3 (`test-merge.yml`) share one smoke test. Heavier deploy variants or API paths in the same file use **`advanced_model` only**. When L3 moves to nightly, migrate those heavier cases into `test_{slug}_expansion.py` with `full_model` and remove the dedicated `test-merge.yml` job (see `test_longcat_image_expansion.py`, `test_qwen_image_expansion.py`).
**L4 design (high priority):** Combine multiple supported features (Cache-DiT, TP, USP, CFG, HSDP, CPU offload, quantization) into **few parametrized** `OmniServerParams` rows so each feature appears in at least one case without exploding GPU jobs. Shard single-GPU vs multi-GPU cases across the nightly X2I/X2V function steps (`cards_1` vs `not cards_1`).
**L4 design (high priority):** Combine multiple supported features (Cache-DiT, TP, USP, CFG, HSDP, CPU offload, quantization) into **few parametrized** `OmniServerParams` rows so each feature appears in at least one case without exploding GPU jobs. Shard single-GPU vs multi-GPU cases across the nightly X2I/X2V function steps (1-GPU vs `distributed_cuda`).
**L4 design (medium / low):** One or two parametrized rows covering the best quality/perf trade-off; skip perf JSON unless the model is high priority.
@@ -34,7 +34,7 @@ but the changes are correct — stage the modified files and re-commit.
| `check-forbidden-imports` | Stdlib `re`/`base64`, pickle, Hugging Face Hub API, or direct Triton/TileLang | `import regex as re` and `pybase64`; use `vllm.transformers_utils.repo_utils`. Do **not** add the file to `allowed_files` without review |
| `check-torch-cuda-call` | New `torch.cuda.*` call site | Use `current_omni_platform` / `OmniPlatform`; do not grow `ALLOWED_FILES` without review |
| `check-tts-adapter-migration` | New `self._tts_model_type` branch in `serving_speech.py` | Put per-model logic in `tts_adapters/`. Lower `MAX_MODEL_TYPE_BRANCHES` when removing branches; do not raise it without review |
| `check-mark` | New `tests/**/test_*.py` missing level/hardware mark, or direct `pytest.mark.H100` | Add `core_model`/`advanced_model`/… plus `cpu`/`cuda`/`hardware_test(`; SKU marks only via helpers |
| `check-test-ci-coverage` | New `tests/**/test_*.py` missing level or hardware mark | Add `core_model`/`advanced_model`/… plus `cpu`/`cuda`/`hardware_test(` |
| `mypy-3.10` | Type error on changed `vllm_omni/` files | Fix the types; model trees are excluded. Extra versions: `pre-commit run --hook-stage manual mypy-3.12` |
| `markdownlint-cli2` | Docs/README markdown lint | Accept auto-fix under `docs/` / `recipes/` / root README |
| `check-buildkite` | Invalid `.buildkite/*.yml` | Fix the YAML; do not grow `SKIP_FILES` without review |
+3 -3
View File
@@ -52,8 +52,8 @@ Always attach markers deliberately:
- `tts` — speech synthesis / TTS-only models (`/v1/audio/speech`, voice clone, etc.)
- `diffusion` — generative diffusion models (image / audio / text / video from noise)
- **Cross-cutting area** (when relevant): `parallel`, `cache`, `example`, `benchmark`
- **Hardware**: `cpu`, `gpu`, `cuda`, `rocm`, `npu`, …; SKU (`H100`, `L4`, …) and `cards_{n}` via `@hardware_test(...)` / `hardware_marks(...)` only — never `pytest.mark.H100` (`check-mark` rejects it)
- Optional: `slow`. Multi-card: `num_cards=N` on the helper (filter with `cards_N` / `not cards_1`)
- **Hardware**: `cpu`, `gpu`, `cuda`, `rocm`, `npu`, `L4`, `H100`, `distributed_cuda`, …
- Optional: `slow`, distributed markers when multi-card is required
**Baseline smoke (L2 + L3):** The simplest e2e case per model — default deploy, minimal request — should usually carry **both** `@pytest.mark.core_model` and `@pytest.mark.advanced_model` on the **same** test function so `test-ready.yml` and `test-merge.yml` share one test. `send_*_request` picks validation depth from `--run-level`. References: `test_voxcpm2_tts.py::test_text_to_audio_001`, `test_qwen3_tts_customvoice.py::test_text_to_audio_001`. Heavier scenarios use **`advanced_model` only**; L4 expansion uses **`full_model`**.
@@ -274,7 +274,7 @@ def test_text_to_video_001(omni_server, online_client) -> None:
**3. Cross-cutting rules**
- New `.py` files need the Omni SPDX header (`Copyright contributors to the vLLM-Omni project`, not `vLLM project`). `tests/` may keep stdlib `re`/`base64`; pickle is still banned unless the file is already on the pickle allowlist. Every collected `tests/**/test_*.py` needs a CI **level** mark and a **hardware** platform mark/helper (`check-mark` runs locally; GHA skips it). Do not write `pytest.mark.H100` / `pytest.mark.L4`; use the helpers so `cards_{n}` is attached. Do not grow `allowed_files` to land a test. Policy: [docs/contributing/README.md](../../../docs/contributing/README.md#linting).
- New `.py` files need the Omni SPDX header (`Copyright contributors to the vLLM-Omni project`, not `vLLM project`). `tests/` may keep stdlib `re`/`base64`; pickle is still banned unless the file is already on the pickle allowlist. Every collected `tests/**/test_*.py` needs a CI **level** mark and a **hardware** mark/helper (`check-test-ci-coverage` runs locally; GHA skips it). Do not grow `allowed_files` to land a test. Policy: [docs/contributing/README.md](../../../docs/contributing/README.md#linting).
- Reuse existing fixtures for the chosen scenario; do not mix “online client” assumptions into offline `OmniRunner` tests without a clear reason.
- Avoid external network dependency in assertions unless the scenario is explicitly “online serving” or doc examples that require a model hub (then align with CI secrets/cache).
- Keep **one test function = one intent** (one modality combo, one endpoint contract, or one acceleration combo).
@@ -79,7 +79,7 @@ E2E (L2+) should not rely on mocks unless documenting a rare exception; prefer r
- `full_model` — L4 nightly (`test-nightly.yml`); some expansion tests still carry both `advanced_model` and `full_model` during migration
2. **Model type** (pick one): `omni`, `tts`, or `diffusion`
3. **Cross-cutting** when relevant: `parallel`, `cache`, `example`, `benchmark`
4. **Hardware**: `cpu`, `cuda`, `rocm`, `npu`, `L4`, `H100`, `cards_1`, …
4. **Hardware**: `cpu`, `cuda`, `rocm`, `npu`, `L4`, `H100`, `distributed_cuda`, …
5. Multi-card: `@hardware_test(...)` in `tests/helpers/mark.py`
## Command Templates
@@ -244,7 +244,7 @@ pytest -s -v dfx/reliability/invalid_param_test/test_invalid_image_generation.py
```bash
cd tests
pytest -s -v -m "core_model and L4 and not cards_1" --run-level=core_model
pytest -s -v -m "core_model and distributed_cuda and L4" --run-level=core_model
```
### Concrete e2e paths (common in-tree)
+1 -1
View File
@@ -27,4 +27,4 @@ jobs:
# Don't run historical-debt hooks against the whole tree in CI yet.
# They still run locally on changed files.
env:
SKIP: check-mark,markdownlint-cli2,shellcheck,check-spdx-header,mypy-3.10
SKIP: check-test-ci-coverage,markdownlint-cli2,shellcheck,check-spdx-header,mypy-3.10
+2 -2
View File
@@ -105,8 +105,8 @@ repos:
<<: *mypy_common
stages: [manual]
- id: check-mark
name: Ensure test files have CI marks (no direct SKU)
- id: check-test-ci-coverage
name: Ensure test files have pytestmarks
entry: python tools/pre_commit/check_test_marks.py
language: python
types: [python]
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.28.0
ARG VLLM_ASCEND_TAG=v0.27.0
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
# WORKDIR /vllm-workspace/vllm
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.28.0-a3
ARG VLLM_ASCEND_TAG=v0.27.0-a3
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
# WORKDIR /vllm-workspace/vllm
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.28.0
ARG VLLM_ASCEND_TAG=v0.27.0
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
ARG APP_DIR=/vllm-workspace/vllm-omni
+1 -1
View File
@@ -1,5 +1,5 @@
ARG VLLM_ASCEND_IMAGE=quay.io/atlas-ci/vllm-ascend
ARG VLLM_ASCEND_TAG=v0.28.0-a3
ARG VLLM_ASCEND_TAG=v0.27.0-a3
FROM ${VLLM_ASCEND_IMAGE}:${VLLM_ASCEND_TAG}
ARG APP_DIR=/vllm-workspace/vllm-omni
+6 -8
View File
@@ -49,7 +49,7 @@ vLLM-Omni's `pre-commit` hooks will now run automatically every time you commit.
`--all-files` run stays green while historical debt is cleaned up. The
current `SKIP` list in
[`.github/workflows/pre-commit.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.github/workflows/pre-commit.yml)
is `check-mark`, `markdownlint-cli2`, `shellcheck`,
is `check-test-ci-coverage`, `markdownlint-cli2`, `shellcheck`,
`check-spdx-header`, and `mypy-3.10`. Those hooks still run on **your
commit** for changed files. A passing GitHub pre-commit check does not mean
they passed locally. Hooks **not** on that list (forbidden imports,
@@ -66,7 +66,7 @@ already there). `check-pickle-imports` is gone: pickle is now one rule inside
| `markdownlint-cli2` | Markdown in `docs/`, `recipes/`, `README.md`, `CONTRIBUTING.md` (not `.claude/` / `.cursor/` / `CLAUDE.md`) | skipped |
| `mypy-3.10` | Type-check changed `vllm_omni/` files (model trees excluded). `tests/` uses `--follow-imports skip` | skipped |
| `mypy-3.11` / `3.12` / `3.13` | Same checker, extra Python versions | not installed; `pre-commit run --hook-stage manual mypy-3.12` |
| `check-mark` | Every `tests/**/test_*.py` has a CI level mark and a hardware platform mark or helper; no direct `pytest.mark.H100` / SKU | skipped |
| `check-test-ci-coverage` | Every `tests/**/test_*.py` has a CI level mark and a hardware mark/helper | skipped |
| `check-tts-adapter-migration` | `self._tts_model_type` branches in `serving_speech.py` must not increase | runs |
| `shellcheck` | `*.sh` quoting / undefined vars | skipped |
| `check-spdx-header` | Omni SPDX header on `.py` / `.pyi` / `.sh` / `.rs` / `.proto` | skipped |
@@ -144,13 +144,11 @@ pre-commit run --hook-stage manual mypy-3.12
#### Test CI marks
`check-mark` requires each collected test module under `tests/` to
`check-test-ci-coverage` requires each collected test module under `tests/` to
have at least one CI **level** mark (`core_model`, `advanced_model`,
`full_model`, `local_model`, or `slow`) and a **hardware** platform mark
(`cpu`, `cuda`, …) or helper (`hardware_test(` / `hardware_marks(`). SKU marks
(`H100`, `L4`, …) must come from those helpers so `cards_{n}` is attached;
direct `pytest.mark.H100` is rejected. GitHub Actions skips this hook; local
commit does not. See the
`full_model`, `local_model`, or `slow`) and a **hardware** mark (`cpu`, `cuda`,
`H100`, …) or helper (`hardware_test(` / `hardware_marks(`). GitHub Actions
skips this hook; local commit does not. See the
[test writing guide](./ci/test_writing_guide.md).
#### TTS adapter ratchet
+1 -1
View File
@@ -402,7 +402,7 @@ loads the model once per mode.
Before splitting, confirm each half actually collects at least one test under the job's forwarded filters — pytest exits with code 5 ("no tests collected") if a half is empty, which the script reports as a failure, red-ing a job that used to pass as one combined invocation.
Also check the tests' `num_cards` against the job's `mirror_hardwares` preset.
`_cuda_marks` turns `num_cards > 1` into `skipif(device_count() < num_cards)`, so a
`cuda_marks` turns `num_cards > 1` into `skipif(device_count() < num_cards)`, so a
test asking for more GPUs than the preset provides is silently skipped and its
coverage artifact comes back empty. Splitting such a job produces a file that
looks fine and measures nothing — "Diffusion · Z Image Test" is the current
@@ -81,8 +81,8 @@ Supported form:
| Form | Example | Effect |
| ---- | ------- | ------ |
| Array (single platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}}, "full_model", "diffusion"]` | Hardware marks from `hardware_marks(...)`; `num_cards` adds `cards_{n}` (including `cards_1` when omitted). String entries become extra pytest marks. |
| Array (multi-platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100", "rocm": "MI325"}, "num_cards": 2}}, "full_model", "omni"]` | Same as above, but declares **multiple platforms** on one item. All platforms must share the same `num_cards` (int or identical dict values). Different counts need `@hardware_test` or one `hardware_marks()` per `pytest.param`. Filter examples: `-m "full_model and H100 and cards_2 and cuda"`, `-m "full_model and MI325 and cards_2 and rocm"`. |
| Array (single platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}}, "full_model", "diffusion"]` | Hardware marks from `hardware_marks(...)`; `num_cards` > 1 adds `distributed_*` (+ CUDA `skipif_cuda` when applicable). String entries become extra pytest marks. |
| Array (multi-platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100", "rocm": "MI325", "npu": "A2"}, "num_cards": {"cuda": 2, "rocm": 2, "npu": 1}}}, "full_model", "omni"]` | Same as above, but declares **multiple platforms** in one case. Filter examples: `-m "full_model and H100 and cuda"`, `-m "full_model and MI325 and rocm"`. |
Recommended for L4 perf cases:
@@ -157,7 +157,7 @@ pytest -s -v tests/dfx/perf/scripts/run_benchmark.py \
```
<!-- markdownlint-disable-next-line MD051 -->
See also [Markers for Tests](#markers-for-tests) for registered hardware markers (`H100`, `L4`, `cuda`, `cards_1`, …).
See also [Markers for Tests](#markers-for-tests) for registered hardware markers (`H100`, `L4`, `cuda`, `distributed_cuda`, …).
#### `server_params` Configuration
+4 -4
View File
@@ -88,7 +88,7 @@ Failed jobs: 1/2
```bash
cd tests
pytest -s -v test_xxxx.py --run-level=core_model
pytest -s -v -m "core_model and L4 and not cards_1" --run-level=core_model
pytest -s -v -m "core_model and distributed_cuda and L4" --run-level=core_model
```
=== "L3 level"
@@ -119,7 +119,7 @@ Failed jobs: 1/2
```bash
pytest -s -v test_xxxx.py --run-level=advanced_model
pytest -s -v -m "advanced_model and L4 and not cards_1" --run-level=advanced_model
pytest -s -v -m "advanced_model and distributed_cuda and L4" --run-level=advanced_model
```
=== "L4 level"
@@ -153,12 +153,12 @@ Failed jobs: 1/2
```bash
cd tests
pytest -s -v test_xxxx.py --run-level=full_model
pytest -s -v -m "full_model and L4 and not cards_1" --run-level=full_model
pytest -s -v -m "full_model and distributed_cuda and L4" --run-level=full_model
pytest -s -v -m "full_model and (omni or tts) and H100" --run-level=full_model
```
If you only want to run specific test cases on a particular platform, you can use:
```bash
pytest -s -v -m "full_model and L4 and not cards_1" --run-level=full_model
pytest -s -v -m "full_model and distributed_cuda and L4" --run-level=full_model
```
Note: ``run_benchmark.py`` and ``run_diffusion_benchmark.py`` accept an optional ``--test-config-file``. If omitted, each loads every ``*.json`` under ``tests/dfx/perf/tests/`` (omni/tts vs diffusion split by ``is_diffusion_perf_config``) and pytest ``-m`` filters by each case's JSON ``mark``:
```bash
+47 -34
View File
@@ -48,12 +48,17 @@ Defined in `pyproject.toml`:
| `MI325` | Tests that require MI325 GPU (AMD/ROCm) * |
| `A2` | Tests that require A2 NPU * |
| `A3` | Tests that require A3 NPU * |
| `cards_{n}` | Tests that require *n* accelerator cards (`cards_1` … `cards_8`); auto-added from `num_cards` * |
| `distributed_cuda` | Tests that require multi cards on CUDA platform * |
| `distributed_rocm` | Tests that require multi cards on ROCm platform * |
| `distributed_npu` | Tests that require multi cards on NPU platform * |
| `skipif_cuda` | Skip if the num of CUDA cards is less than the required * |
| `skipif_rocm` | Skip if the num of ROCm cards is less than the required * |
| `skipif_npu` | Skip if the num of NPU cards is less than the required * |
| `slow` | Slow tests (may skip in quick CI) |
| `benchmark` | Benchmark tests (decorator on runner test functions; perf JSON uses `full_model` + type marker instead) |
| `local_model` | Tests requiring local / non-HF-hub model weights |
\* Means those markers are auto-added by `@hardware_test` (parametrization decorator) or `hardware_marks` (only returning the list of marks for flexibility). Do **not** write `pytest.mark.H100` / `pytest.mark.L4` (or other SKU names) by hand: `check-mark` rejects that so `cards_{n}` cannot be skipped.
\* Means those markers are auto-added by `@hardware_test` (parametrization decorator) or `hardware_marks` (only returning the list of marks for flexibility).
#### Example usage for markers
@@ -76,23 +81,25 @@ def test_video_to_audio()
This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/helpers/mark.py` performs the following actions:
1. **Applies platform and resource markers**
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `H200`, `B200`, `MI325`, `B60`, `A2`, `A3`).
Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `MI325`, `B60`, `A2`, `A3`).
```python
@pytest.mark.cuda
@pytest.mark.L4
```
2. **Handles card-count filtering**
Adds a filterable marker `cards_{n}` (including `cards_1` when `num_cards` is omitted / 1). Use this with the SKU and platform, e.g. `-m "H100 and cards_4 and cuda"` vs `-m "H100 and cards_2 and cuda"`. Any multi-card CUDA test is `-m "cuda and not cards_1"`.
When `res` lists more than one platform **and** `num_cards` differs by platform, `@hardware_test` collects **one variant per platform** so those `cards_{n}` marks do not stack. `-m "H100 and cards_4 and cuda"` then selects only the CUDA/H100 4-card variant, not a ROCm 2-card sibling. The same count on every platform stays a single item.
2. **Handles multi-card (distributed) scenarios**
For tests requiring multiple cards, it automatically adds distributed markers such as `distributed_cuda`, `distributed_rocm`, or `distributed_npu`.
```python
@pytest.mark.distributed_cuda(num_cards=num_cards)
```
3. **Supports flexible card requirements**
Accepts `num_cards` as either a single integer for all platforms or as a dictionary with per-platform values. If not specified, defaults to 1 card per platform.
4. **Integrates resource validation**
On CUDA and XPU, when `num_cards > 1`, attaches pytest's built-in `skipif` if the machine has fewer devices than requested. This is not a named `skipif_*` marker and is not used for `-m` filtering.
On CUDA, adds a skip marker (`skipif_cuda`) if the system does not have the required number of devices.
Support for `skipif_rocm` and `skipif_npu` will be implemented later.
5. **Works with pytest filtering**
Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "H100 and cards_2"`, `-m "L4 and not cards_1"`).
Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "distributed_cuda and L4"`).
##### Example usage for decorator
@@ -110,14 +117,13 @@ This decorator is intended to make hardware-aware, cross-platform test authoring
num_cards=2,
)
```
- `res` must be a dict. SKU values come from `pyproject.toml` markers tagged `[hardware-resource]` plus the platform key (`[cuda]`, `[rocm]`, `[xpu]`, `[npu]`, `[musa]`).
- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), XPU (B60), MUSA (S5000), NPU (A2/A3)
- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
- Filtering examples:
- CUDA H100 4-card: `pytest -m "H100 and cards_4 and cuda"`
- CUDA H100 2-card: `pytest -m "H100 and cards_2 and cuda"`
- CUDA multi-card (any count): `pytest -m "L4 and not cards_1"`
- ROCm multi-card: `pytest -m "MI325 and not cards_1"`
- NPU multi-card: `pytest -m "npu and not cards_1"`
- CUDA only: `pytest -m "distributed_cuda and L4"`
- ROCm only: `pytest -m "distributed_rocm and MI325"`
- NPU only: `pytest -m "distributed_npu"`
#### Function: `hardware_marks`
@@ -173,30 +179,37 @@ If you want to add support for a new platform (e.g., "tpu" for a new accelerator
[tool.pytest.ini_options]
markers = [
# ... existing markers ...
"tpu: [hardware-platform] Tests that require TPU device",
# Add [gpu] next to [hardware-platform] if this platform should also get pytest.mark.gpu.
"TPU_V3: [hardware-resource] [tpu] Tests that require TPU v3 hardware",
"cards_1: [hardware-cards] Tests that require 1 accelerator card",
"cards_2: [hardware-cards] Tests that require 2 accelerator cards",
"tpu: Tests that require TPU device",
"TPU_V3: Tests that require TPU v3 hardware",
"distributed_tpu: Tests that require multiple TPU devices",
]
```
`check-mark` treats `[hardware-platform]` names (including `cpu`/`gpu`) as allowed `pytest.mark.<name>` values. `hardware_test(res=...)` still rejects `cpu`/`gpu` as `res` keys.
2. **Implement a marker construction function for your platform** in `vllm-omni/tests/helpers/mark.py`:
```python
# In vllm-omni/tests/helpers/mark.py
def _tpu_marks(*, res: str, num_cards: int):
_require_sku("tpu", res)
return [pytest.mark.tpu, getattr(pytest.mark, res), _cards_mark(num_cards)]
def tpu_marks(*, res: str, num_cards: int):
test_platform = pytest.mark.tpu
if res == "TPU_V3":
test_resource = pytest.mark.TPU_V3
else:
raise ValueError(
f"Invalid TPU resource type: {res}. Supported: TPU_V3")
if num_cards == 1:
return [test_platform, test_resource]
else:
test_distributed = pytest.mark.distributed_tpu(num_cards=num_cards)
# Optionally: add skipif_tpu when implemented
return [test_platform, test_resource, test_distributed]
```
3. **Register the builder** in `_marks_for_platform`:
3. **Update `hardware_marks` to recognize your new platform**:
In the relevant place (see the `hardware_marks` implementation), add:
```python
builders = {
# ... existing platforms ...
"tpu": _tpu_marks,
}
if platform == "tpu":
marks = tpu_marks(res=resource, num_cards=cards)
```
(`hardware_test` / `hardware_marks` both go through `_marks_for_platform`.)
(`hardware_test` calls `hardware_marks` internally, so both will pick up the change.)
4. **(Recommended) Add a test using your new markers**:
```python
@hardware_test(
@@ -210,11 +223,11 @@ If you want to add support for a new platform (e.g., "tpu" for a new accelerator
**Summary**:
- Add pytest markers for your new platform/resources
- Implement a private marker function (`_xxx_marks`)
- Register it in `_marks_for_platform`
- You're done: tests using `@hardware_test` or `hardware_marks` with your platform now automatically get the correct markers and `cards_{n}` filtering.
- Implement a marker function (`xxx_marks`)
- Plug into `hardware_marks`
- You're done: tests using `@hardware_test` or `hardware_marks` with your platform now automatically get the correct markers, distribution, and isolation!
See code in `vllm-omni/tests/helpers/mark.py` for existing examples (`_cuda_marks`, `_rocm_marks`, `_npu_marks`).
See code in `vllm-omni/tests/helpers/mark.py` for existing examples (`cuda_marks`, `rocm_marks`, `npu_marks`).
## Test case style
+2 -2
View File
@@ -438,7 +438,7 @@ selects the scheduler and worker family:
Key topology fields belong to `StagePipelineConfig`:
| Field | Description |
| ------- | ------------- |
|-------|-------------|
| `model_stage` | Logical stage name (`ar_stage`, `decoder`, etc.). |
| `execution_type` | Runtime family for this stage. |
| `input_sources` | Upstream stage IDs that provide input. |
@@ -960,7 +960,7 @@ Common failures and fixes:
| `check-forbidden-imports` | Stdlib `re`/`base64`, pickle, Hugging Face Hub API, or direct Triton/TileLang | `import regex as re` and `pybase64`; do not grow the allowlist without review |
| `check-torch-cuda-call` | New `torch.cuda.*` call site | Use `current_omni_platform`; do not grow `ALLOWED_FILES` without review |
| `check-tts-adapter-migration` | New `_tts_model_type` branch in `serving_speech.py` | Put logic in `tts_adapters/`; lower `MAX_MODEL_TYPE_BRANCHES` when removing branches |
| `check-mark` | Test file missing level/hardware mark, or direct `pytest.mark.H100` | Add `core_model`/… plus `cpu`/`cuda`/`hardware_test(`; SKU marks only via helpers |
| `check-test-ci-coverage` | Test file missing level or hardware mark | Add `core_model`/… plus `cpu`/`cuda`/`hardware_test(` |
| `shellcheck` | Native `shellcheck` missing, or script warning | Install via apt/dnf/brew (or `shellcheck.exe` on Windows). See [Linting](../README.md#linting) |
| `mypy-3.10` / `markdownlint-cli2` / `check-buildkite` | Types, docs markdown, or Buildkite YAML | See [Linting](../README.md#linting) |
@@ -26,8 +26,6 @@ Legend: ✅ supported, ⚠️ compatibility path or limited validation, ❌ unsu
| **TP > 1** | ⚠️ Ordinary TP-aware loader only; no direct checkpoint mmap. | ⚠️ Ordinary TP-aware loader only; no direct checkpoint mmap. |
| **HSDP** | ❌ Rejected to avoid double-sharding parameters. | ⚠️ Limited end-to-end coverage. |
| **Per-tensor online FP8 linears** | ✅ Ordinary loader finalizes weights and scales before DLO sharding. | ✅ Ordinary loader retains complete rank-local tensors. |
| **Online INT8 linears** | ✅ Ordinary loader finalizes the int8 weight (contiguous or transposed view) and fp32 scale before DLO sharding. | ✅ Ordinary loader retains complete rank-local tensors. |
| **Online MXFP8 linears** | ✅ Ordinary loader finalizes the fp8 weight (contiguous) and e8m0 block scale (contiguous or transposed view) before DLO sharding. | ✅ Ordinary loader retains complete rank-local tensors. |
| **Other online quantization methods** | ❌ Rejected until runtime packing and scale layouts are validated. | ⚠️ Allowed through the ordinary loader; validation is method-specific. |
| **Model-level or standard layerwise CPU offload** | ❌ Disabled because DLO takes priority. | ❌ Disabled because DLO takes priority. |
| **Resident leading layers** | ❌ Rejected. | ✅ Requires eligible resident paths in the model's `OffloadPlan`. |
@@ -440,15 +438,11 @@ requirements.
Direct checkpoint mmap can back either transfer path. It is currently limited
to proven TP1, non-HSDP, non-online-quantized layouts. Other layouts use the
ordinary loader. Per-tensor online FP8, online INT8, and online MXFP8 linears
can use DLO AllGather after the ordinary loader finalizes their runtime
weights and scales; DLO then shards and reconstructs those tensors with their
recorded layouts (they all keep plain 1-byte dtypes over ordinary strided
views: online INT8 has an int8 weight plus fp32 scale, online MXFP8 has an
fp8 weight plus e8m0 block scale, each with a contiguous or transposed-view
finalized tensor, all covered by the same physical-order packing as online
FP8). Other online methods must use `--dlo-no-use-allgather` or disable
online quantization until their runtime layouts are validated.
ordinary loader. Per-tensor online FP8 linears can use DLO AllGather after the
ordinary loader finalizes their runtime weights and scales; DLO then shards and
reconstructs those tensors with their recorded layouts. Other online methods
must use `--dlo-no-use-allgather` or disable online quantization until their
runtime layouts are validated.
The Host Weight Runtime representation, publication, and no-AllGather consumer
contracts are merged; see
@@ -465,8 +459,8 @@ Current source-level validation includes:
- HSDP + DLO without AllGather acceptance at configuration level;
- loader preflight fallback for TP, HSDP, online quantization, unknown custom
loaders, missing keys, and shape/dtype mismatches;
- ordinary-loader fallback for per-tensor online FP8, online INT8, and online
MXFP8 linears followed by DLO sharding of finalized weights and scales;
- ordinary-loader fallback for per-tensor online FP8 linears followed by DLO
sharding of finalized weights and scales;
- exact loader-to-backend plan transfer and ordinary-loader fallback;
- ordered publication hashing, overlapped durability, unordered parallel
checksum fallback, and node-local source-digest reuse/invalidation;
+48 -68
View File
@@ -22,60 +22,59 @@ We use **Qwen-Image** as the reference implementation for decode parallel, and *
**VAE Patch Parallelism** is an acceleration technique for both **encoding** and **decoding**. Instead of processing the entire tensor at once, the tensor is:
- Split into multiple spatial tiles
+ Split into multiple spatial tiles
- Distributed across multiple ranks
+ Distributed across multiple ranks
- Encoded/Decoded in parallel
+ Encoded/Decoded in parallel
- Merged to reconstruct the final output
+ Merged to reconstruct the final output
This approach:
- Distributes computation across multiple devices
+ Distributes computation across multiple devices
- Reduces peak memory usage per device
+ Reduces peak memory usage per device
- Accelerates encoding/decoding latency
+ Accelerates encoding/decoding latency
### When to Use Encode vs Decode Parallel
| Operation | Use Case | Example |
| ------------------- | ---------------------------- | --------------------------------- |
| **Decode Parallel** | Text-to-Image, Text-to-Video | Latent → Image/Video |
| **Encode Parallel** | Image-to-Video (I2V) | Image → Latent (for conditioning) |
| Operation | Use Case | Example |
|-----------|----------|---------|
| **Decode Parallel** | Text-to-Image, Text-to-Video | Latent → Image/Video |
| **Encode Parallel** | Image-to-Video (I2V) | Image → Latent (for conditioning) |
### Architecture
We introduce **DistributedVaeExecutor** as the core component responsible for distributed VAE encoding/decoding.
The executor is model-agnostic and accepts three function parameters:
- split – Partition the latent into tiles
+ split – Partition the latent into tiles
- exec – Decode a single tile
+ exec – Decode a single tile
- merge – Combine decoded tiles into the final output
+ merge – Combine decoded tiles into the final output
#### Execution Flow
- Call split(z) to generate a list of TileTask and a GridSpec
+ Call split(z) to generate a list of TileTask and a GridSpec
- Dispatch tasks across ranks using workload-based balancing
+ Dispatch tasks across ranks using workload-based balancing
- Each rank executes exec(task) on its assigned tiles
+ Each rank executes exec(task) on its assigned tiles
- Gather decoded tile results to rank 0
+ Gather decoded tile results to rank 0
- Rank 0 performs merge(...)
+ Rank 0 performs merge(...)
- (Optional) Broadcast final result to all ranks
+ (Optional) Broadcast final result to all ranks
This design separates:
- Distributed execution logic
+ Distributed execution logic
- Model-specific tiling and merging logic
+ Model-specific tiling and merging logic
#### Why split / exec / merge is necessary?
@@ -83,23 +82,23 @@ The latent tensor cannot be arbitrarily partitioned.
During decoding:
- Each output pixel may depend on neighboring pixels
+ Each output pixel may depend on neighboring pixels
- The receptive field is model-dependent
+ The receptive field is model-dependent
Therefore:
- Tiles must include overlap
+ Tiles must include overlap
- Merge must perform blending to avoid seams
+ Merge must perform blending to avoid seams
## Step-by-Step Implementation (Decode)
### Step 1: Implement DistributedAutoencoderKLQwenImage
`QwenImagePipeline` use `AutoencoderKLQwenImage` for vae, so implement a distributed version:
```python
```
class DistributedAutoencoderKLQwenImage(AutoencoderKLQwenImage, DistributedVaeMixin):
@classmethod
def from_pretrained(cls, *args: Any, **kwargs: Any):
@@ -107,35 +106,28 @@ class DistributedAutoencoderKLQwenImage(AutoencoderKLQwenImage, DistributedVaeMi
model.init_distributed()
return model
```
**Key points**:
- Inherit both AutoencoderKLQwenImage and DistributedVaeMixin
- Call init_distributed() after loading weights
+ Inherit both AutoencoderKLQwenImage and DistributedVaeMixin
+ Call init_distributed() after loading weights
### Step 2: Implement split/exec/merge
Reuse `AutoencoderKLQwenImage.tiled_decode` logic and divide it into three stages. And we need return tiles with `GridSpec` and `TileTask`:
```python
```
class GridSpec:
split_dims: tuple[int, ...] # Tensor dimensions being split (e.g., (2, 3) for (B, C, H, W))
grid_shape: tuple[int, ...] # Tile grid layout (num_rows, num_cols)
tile_spec: dict = field(default_factory=dict) # Metadata required for merging
output_dtype: torch.dtype | None = None # Final output dtype
```
```python
```
class TileTask:
tile_id: int # task id
grid_coord: tuple[int, ...] # Tile position in grid
tensor: torch.Tensor | list[torch.Tensor] # The tile tensor
workload: int | float = 1 # Used for load balancing (e.g., tile area)
```
And tiled base split/exec/merge as follow:
```python
```
def tile_split(self, z: torch.Tensor) -> tuple[list[TileTask], GridSpec]:
# mostly copy from AutoencoderKL
_, _, num_frames, height, width = z.shape
@@ -218,14 +210,11 @@ def tile_merge(self, coord_tensor_map: dict[tuple[int, ...], torch.Tensor], grid
```
### Step 3: Override tiled_decode
We need to override tiled_decode, the main logic is:
- check distributed is enabled
- select split/exec/merge
- Invoke self.distributed_executor.execute to decode
```python
+ check distributed is enabled
+ select split/exec/merge
+ Invoke self.distributed_executor.execute to decode
```
def tiled_decode(self, z: torch.Tensor, return_dict: bool = True):
if not self.is_distributed_enabled():
return super().tiled_decode(z, return_dict=return_dict)
@@ -241,14 +230,11 @@ def tiled_decode(self, z: torch.Tensor, return_dict: bool = True):
return DecoderOutput(sample=result)
```
`broadcast_result` is set to True or False depending on the model; when enabled, the result will be used even on ranks other than 0.
### Step 4: Modify Pipeline
Change vae model from AutoencoderKLQwenImage to DistributedAutoencoderKLQwenImage
```diff
```
class YourModelPipeline(nn.Module):
def __init__(
self,
@@ -273,8 +259,8 @@ For models that require VAE encoding (e.g., Image-to-Video), you can also parall
Similar to decode, split the input tensor into tiles. Key considerations:
- **Patchify handling**: If the model uses `patch_size`, scale tile parameters accordingly
- **Temporal chunking**: Video VAEs may have temporal compression (e.g., 4x)
+ **Patchify handling**: If the model uses `patch_size`, scale tile parameters accordingly
+ **Temporal chunking**: Video VAEs may have temporal compression (e.g., 4x)
```python
def encode_tile_split(self, x: torch.Tensor) -> tuple[list[TileTask], GridSpec]:
@@ -420,28 +406,23 @@ def tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
**Key differences from decode parallel:**
| Aspect | Decode Parallel | Encode Parallel |
| -------- | ----------------- | ----------------- |
|--------|-----------------|-----------------|
| `broadcast_result` | Often `False` (only rank 0 needs output) | `True` (all ranks need latents for diffusion) |
| Patchify | Applied in merge (unpatchify) | Handled by parent `_encode()` before `tiled_encode()` |
| Temporal chunking | Frame-by-frame | Chunk-based (e.g., 1 + 4n frames) |
## Testing
Verify numerical consistency between:
+ vae_patch_parallel_size = 1
- vae_patch_parallel_size = 1
- vae_patch_parallel_size = N
+ vae_patch_parallel_size = N
Example:
```python
torch.allclose(output_1, output_n, atol=1e-5)
```
Testing requirements:
- Fix random seed
- Use identical tiling strategy
+ Fix random seed
+ Use identical tiling strategy
```python
m = Omni(
@@ -453,7 +434,6 @@ m = Omni(
),
)
```
When vae_patch_parallel_size is larger than the DiT world size, it will automatically fall back to using the DiT world size instead.
## Reference Implementations
@@ -461,7 +441,7 @@ When vae_patch_parallel_size is larger than the DiT world size, it will automati
Complete examples in the codebase:
| Model | Path | Decode Parallel | Encode Parallel |
| ------- | ------ | ----------------- | ----------------- |
|-------|------|-----------------|-----------------|
| **Z-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl.py` | ✅ | ❌ |
| **Wan2.2** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_wan.py` | ✅ | ✅ |
| **Qwen-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_qwenimage.py` | ✅ | ❌ |
@@ -476,7 +456,7 @@ The tile-parallel executor above assigns independent spatial **tiles** to ranks.
### How it differs from tile parallel
| Aspect | Tile parallel (`"tile"`) | Spatially-sharded (`"spatial_shard_height"`/`"spatial_shard_width"`) |
| -------- | -------------------------- | ----------------------------------------------- |
|--------|--------------------------|-----------------------------------------------|
| Unit of work | Independent overlapping tiles | A single global feature map sharded along H or W |
| Cross-rank communication | Gather tiles to rank 0, stitch + blend | Per-conv **halo exchange** of boundary rows/cols (P2P) |
| Output assembly | Blend overlapping tiles | All-gather shards on rank 0, trim padding (matches `broadcast_result=False`) |
@@ -501,7 +481,7 @@ serve.py (--vae-parallel-mode) / OmniEngineArgs
### Notes
- The decoder is patched **in place** the first time spatial-shard decode runs and is bound to a single split dimension for the lifetime of the VAE instance.
- Numerical correctness vs. single-GPU decode is covered by `tests/diffusion/distributed/test_wan_spatial_shard.py::test_spatial_shard_decode_matches_reference` (multi-GPU, nightly `full_model` + `cards_2`).
- Numerical correctness vs. single-GPU decode is covered by `tests/diffusion/distributed/test_wan_spatial_shard.py::test_spatial_shard_decode_matches_reference` (multi-GPU, nightly `full_model` + `distributed_cuda`).
---
@@ -37,7 +37,6 @@ validation_paths:
- tests/engine/test_async_omni_engine_input.py
- tests/engine/test_async_omni_engine_outputs.py
- tests/engine/test_async_omni_engine_abort.py
- tests/e2e/offline_inference/test_qwen3_omni.py
- tests/engine/test_async_omni_engine_stage_init.py
- tests/engine/test_orchestrator.py
- tests/engine/test_orchestrator_error_handling.py
@@ -114,15 +113,6 @@ Numbers become append-only after normative promotion.
Test routing, output ordering, queue correlation, cancellation, failure
propagation, shutdown ordering, and representative multi-stage execution.
AR abort of a final-stage LLM request must deliver a terminal output with
`finish_reason="abort"` and the generated prefix so collocated training can
resume; diffusion abort remains whole-sample retry. Aborting a parallel-sampling
child must drop `parent_requests` once no children remain. EngineCore control
RPCs (`sleep` / `wake_up` / `pause_scheduler` / `resume_scheduler`) must
propagate worker exceptions rather than returning `{"supported": False}`.
When a request has multiple final output stages, only the last abort message
is request-terminal. Output-processor abort state is committed only after the
physical EngineCore abort succeeds.
Changes that cross into stage runtime or public error behavior require review
from that contract's owners.
+1 -8
View File
@@ -43,7 +43,6 @@ depends_on:
validation_paths:
- tests/entrypoints/test_omni_entrypoints.py
- tests/entrypoints/test_async_omni.py
- tests/entrypoints/test_async_omni_pause_sleep_routing.py
- tests/entrypoints/test_async_omni_duplex.py
- tests/entrypoints/test_serve.py
- tests/entrypoints/test_stream_finish_reason.py
@@ -120,13 +119,7 @@ promotion.
Test request validation, protocol conversion, model-adapter routing,
streaming, session identity, disconnect/cancellation, and error mapping for
each affected offline or serving entrypoint. Sleep must wait for in-flight
`generate()` admission before EngineCore offload; `wake_up` does not resume
admission — callers must `resume_generation()`. Sleeping tags are tracked per
stage so `wake_up(stage_ids=[0])` does not skip a later `wake_up(stage_ids=[1])`.
Streaming input pumps take an admission slot immediately before each EngineCore
ADD or update, not while waiting for the next client chunk. Frontend abort
keeps `request_states` until `generate()` consumes the terminal output.
each affected offline or serving entrypoint.
## Promotion gate
@@ -17,8 +17,6 @@ globally.
| --- | --- | --- |
| `sparsity` | finite, `[0, 1]` | Nominal dropped-key-block fraction; default `0.8`; `0` disables sparsity |
| `start_step` | integer, `>= 0` | Number of early denoise steps kept dense |
| `end_step` | integer, `>= 0` | Number of final denoise steps kept dense (tail fallback) |
| `precision` | `"bf16"`, `"fp8"`, `"mix"` | Kernel precision mode; default `"bf16"`. Requires MindIE-SD with `sparse_attention(precision=...)` |
| `skip_layers` | selector such as `"0-3,38"` | DiT blocks kept dense |
```bash
@@ -54,12 +52,6 @@ platform raises. It is incompatible with ring sequence parallelism because
the kernel needs the complete key sequence for ranking; use Ulysses sequence
parallelism with `ring_degree=1`.
The `precision` knob requires a MindIE-SD release whose `sparse_attention`
accepts `precision=`; older releases accept it through `**kwargs` but
silently ignore it and run the BF16 path. `RAINFUSION_ATTN` raises at runtime
when a non-`bf16` precision is requested without that support, so pin a
compatible MindIE-SD release when using `precision="mix"` or `"fp8"`.
## Geometry handling
RainFusion handles arbitrary video grids by rearranging video spatially and,
+2 -35
View File
@@ -12,17 +12,15 @@ LoRA adapters are lightweight, model-specific fine-tuning weights that can be ap
## LoRA Adapter Format
### PEFT (Parameter-Efficient Fine-Tuning) format (default)
A typical PEFT-format LoRA adapter directory structure:
```text
```
lora_adapter/
├── adapter_config.json
└── adapter_model.safetensors
```
The `adapter_config.json` file contains metadata about the LoRA adapter, including:
- `r`: LoRA rank
- `lora_alpha`: LoRA alpha scaling factor
- `target_modules`: List of module names to apply LoRA to
@@ -69,7 +67,7 @@ For distilled few-step LoRAs, pass `lora_backend="distill"` together with one or
For Qwen-Image and Wan pipelines, the distill backend calls `pipeline.load_lora_weights(...)` during worker initialization.
| Pipeline | Supported distilled LoRA repo | Notes |
| ---------- | -------------------------------- | ------- |
|----------|--------------------------------|-------|
| `QwenImagePipeline` | `lightx2v/Qwen-Image-2512-Lightning` | Used with Qwen-Image-2512 Lightning-style few-step inference. |
| `Wan22Pipeline` | `lightx2v/Wan2.1-Distill-Loras`, `lightx2v/Wan2.2-Distill-Loras` | Wan2.1 uses one LoRA file. For dual-transformer Wan2.2 MoE, pass high-noise then low-noise LoRA files. |
| `Wan22I2VPipeline` | `lightx2v/Wan2.2-Distill-Loras` | For dual-transformer Wan2.2 MoE, pass high-noise then low-noise LoRA files. |
@@ -242,37 +240,6 @@ Notes:
- Output quality and speed depend on the replacement checkpoints and sampling params you choose.
- If you only need to fuse distilled LoRAs into a Wan2.2 checkpoint at load time (without the full LightX2V convert + assemble pipeline), you can instead pass them directly via `--lora-backend distill --lora-path <high>.safetensors <low>.safetensors`. See the [Distill backend](#distill-backend-fuse-distilled-lora-at-init) section above.
## MiniMax-H3 adapter-declared schedules
MiniMax-H3 supports two few-step mechanisms that must not be conflated:
- **Checkpoint-pinned schedule**: a merged release writes `base_schedule` into
`model_index.json`. Requests must pass `num_inference_steps` as the interval
count (for example `4` for `[1.0, 0.7, 0.4, 0.15, 0.0]`).
- **Runtime Turbo LoRA**: LightX2V Turbo artifacts request five sigma points and
enforce `flow_shift=6`, `audio_flow_shift=3`.
- **Runtime native LoRA**: FlashGen-style artifacts declare
`key_format=minimax-h3-native` and embed `base_schedule` in safetensors
metadata. When active, the adapter schedule overrides the base checkpoint and
requests must use the interval-count contract (`num_inference_steps=4`).
Request-mode generation may omit the field and take the count from the adapter
schedule; step execution requires it explicitly, because the step scheduler
derives the total step count from the request before the adapter schedule is
known.
Native artifacts also declare `qkv_layout=grouped`. The H3 loader reorders fused
`qkv_proj` LoRA rows with the same `_reorder_grouped_qkv_to_qkv` helper used for
base-weight loading, then binds the packed Q/K/V slices through the legacy PEFT
manager without modifying `DiffusionLoRAManager`.
Both runtime adapters run with distributed layerwise offload in request-mode
generation, where the manager keeps the LoRA A/B buffers resident on the compute
device while DLO streams the base blocks. MiniMax-H3 step execution still
rejects DLO, so `--step-execution` cannot be combined with
`--enable-distributed-layerwise-offload`. Model-level CPU offload and standard
layerwise offload are rejected in every mode, because the dynamic LoRA tensors
are neither parameters nor registered buffers and therefore do not participate
in those weight lifecycles.
## See Also
+34 -37
View File
@@ -248,11 +248,11 @@ addopts = [
"--dist=loadgroup"
]
markers = [
# ci/cd required — tag with [ci-level] for tests.helpers.mark.get_level_markers().
"core_model: [ci-level] L1&L2 tests (run in each PR)",
"advanced_model: [ci-level] L3 level tests (run on each merge)",
"full_model: [ci-level] L4 level tests (run nightly)",
"local_model: [ci-level] tests (run locally)",
# ci/cd required
"core_model: L1&L2 tests (run in each PR)",
"advanced_model: L3 level tests (run on each merge)",
"full_model: L4 level tests (run nightly)",
"local_model: tests (run locally)",
# function module markers
"diffusion: Diffusion model tests",
"omni: Omni model tests",
@@ -262,41 +262,38 @@ markers = [
"sp: Sequence parallelism tests",
"example: Doc example code tests",
# platform markers
# Tag with [hardware-platform] for tests.helpers.mark.get_supported_platforms().
# Add [gpu] on accelerator platforms that should also get pytest.mark.gpu.
# cpu/gpu are pytest marks; hardware_test(res=...) rejects them as keys.
"cpu: [hardware-platform] Tests that run on CPU",
"gpu: [hardware-platform] Tests that run on GPU (auto-added)",
"cuda: [hardware-platform] [gpu] Tests that run on CUDA (auto-added)",
"rocm: [hardware-platform] [gpu] Tests that run on AMD/ROCm (auto-added)",
"xpu: [hardware-platform] [gpu] Tests that run on XPU (auto-added)",
"npu: [hardware-platform] Tests that run on NPU/Ascend (auto-added)",
"musa: [hardware-platform] [gpu] Tests that run on MUSA/Moore Threads (auto-added)",
"cpu: Tests that run on CPU",
"gpu: Tests that run on GPU (auto-added)",
"cuda: Tests that run on CUDA (auto-added)",
"rocm: Tests that run on AMD/ROCm (auto-added)",
"xpu: Tests that run on XPU (auto-added)",
"npu: Tests that run on NPU/Ascend (auto-added)",
"musa: Tests that run on MUSA/Moore Threads (auto-added)",
# specified computation resources marks (auto-added)
# [hardware-resource] + [cuda]/[rocm]/[xpu]/[npu]/[musa] for hardware_test(res=...).
# Do not put [gpu] on SKU lines: that would make gpu a res key.
"H100: [hardware-resource] [cuda] Tests that require H100 GPU",
"H800: [hardware-resource] [cuda] Tests that require H800 GPU",
"H200: [hardware-resource] [cuda] Tests that require H200 GPU",
"L4: [hardware-resource] [cuda] Tests that require L4 GPU",
"B200: [hardware-resource] [cuda] Tests that require B200 GPU",
"MI325: [hardware-resource] [rocm] Tests that require MI325 GPU (AMD/ROCm)",
"B60: [hardware-resource] [xpu] Tests that require Intel Arc Pro B60 XPU",
"S5000: [hardware-resource] [musa] Tests that require S5000 GPU (Moore Threads/MUSA)",
"A2: [hardware-resource] [npu] Tests that require A2 NPU",
"A3: [hardware-resource] [npu] Tests that require A3 NPU",
# Tag with [hardware-cards] for tests.helpers.mark.get_supported_card_counts().
"cards_1: [hardware-cards] Tests that require 1 accelerator card",
"cards_2: [hardware-cards] Tests that require 2 accelerator cards",
"cards_3: [hardware-cards] Tests that require 3 accelerator cards",
"cards_4: [hardware-cards] Tests that require 4 accelerator cards",
"cards_5: [hardware-cards] Tests that require 5 accelerator cards",
"cards_6: [hardware-cards] Tests that require 6 accelerator cards",
"cards_7: [hardware-cards] Tests that require 7 accelerator cards",
"cards_8: [hardware-cards] Tests that require 8 accelerator cards",
# Tag with [hardware-resource] for tests.helpers.mark.get_hardware_mark_list().
"H100: [hardware-resource] Tests that require H100 GPU",
"H800: [hardware-resource] Tests that require H800 GPU",
"H200: [hardware-resource] Tests that require H200 GPU",
"L4: [hardware-resource] Tests that require L4 GPU",
"B200: [hardware-resource] Tests that require B200 GPU",
"MI325: [hardware-resource] Tests that require MI325 GPU (AMD/ROCm)",
"B60: [hardware-resource] Tests that require Intel Arc Pro B60 XPU",
"S5000: [hardware-resource] Tests that require S5000 GPU (Moore Threads/MUSA)",
"A2: [hardware-resource] Tests that require A2 NPU",
"A3: [hardware-resource] Tests that require A3 NPU",
"distributed_cuda: Tests that require multi cards on CUDA platform",
"distributed_rocm: Tests that require multi cards on ROCm platform",
"distributed_xpu: Tests that require multi cards on XPU platform",
"distributed_npu: Tests that require multi cards on NPU platform",
"distributed_musa: Tests that require multi cards on MUSA platform",
"skipif_cuda: Skip if the num of CUDA cards is less than the required",
"skipif_rocm: Skip if the num of ROCm cards is less than the required",
"skipif_xpu: Skip if the num of XPU cards is less than the required",
"skipif_npu: Skip if the num of NPU cards is less than the required",
"skipif_musa: Skip if the num of MUSA cards is less than the required",
# more detailed markers
"sp: Sequence parallelism tests (multi-GPU)",
"slow: [ci-level] Slow tests (may skip in quick CI)",
"slow: Slow tests (may skip in quick CI)",
"benchmark: Benchmark tests",
"xdist: xdist-parallelizable tests",
]
-57
View File
@@ -860,63 +860,6 @@ fixed HBM usage. Model-level and standard layerwise offload remain unsupported.
The five requested sigma points produce the four denoiser evaluations expected
by the Turbo artifact.
### FlashGen native LoRA
The FlashGen 4-step T2VA artifact uses the native MiniMax-H3 module layout and
declares its distilled sigma schedule in safetensors metadata. It is published on
[ModelScope](https://modelscope.cn/models/FlashGen/Minimax-H3-4step-lora-flashgen):
```text
FlashGen/Minimax-H3-4step-lora-flashgen/minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors
```
Download only that file:
```bash
python -m pip install modelscope
export FLASHGEN_DIR=/path/to/minimax-h3-flashgen-lora
export FLASHGEN_FILE=minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors
modelscope download FlashGen/Minimax-H3-4step-lora-flashgen \
--local_dir "${FLASHGEN_DIR}" \
--include "${FLASHGEN_FILE}"
export FLASHGEN_LORA="${FLASHGEN_DIR}/${FLASHGEN_FILE}"
```
Start from a non-offloaded or DLO FL2VA server command and add
`--task-type fl2va --lora-backend peft --lora-path "${FLASHGEN_LORA}"`.
Each request must use T2VA and the distilled interval-count contract:
```bash
-F 'num_inference_steps=4' \
-F 'extra_params={"task":"t2va","duration":5.2}' \
-F "lora={\"name\":\"h3-flashgen-v1.0\",\"path\":\"${FLASHGEN_LORA}\",\"scale\":1.0}"
```
This path rejects Ref2VA and checkpoints that already pin `base_schedule` in
`model_index.json`. The adapter metadata carries
`base_schedule=1.0,0.7,0.4,0.15,0.0`, so `num_inference_steps=4` means four
denoiser evaluations, not five sigma points. Request-mode generation may omit
the field and take the count from the adapter schedule; `--step-execution`
requires it explicitly, because the step scheduler reads the total step count
off the request at admission, before the adapter schedule is known.
DLO is supported in request-mode generation on the same terms as the Turbo
adapter: the request-switchable LoRA A/B buffers stay resident on the
accelerator while DLO streams only the base blocks, so budget for that
additional fixed HBM usage. The native artifact is rank 64 over 259 target
modules, and its packed `qkv_proj` and `fc1` layers reuse the full-input A
tensor per slice while B carries slice-local output rows, so the resident
footprint exceeds the on-disk payload; measure it for your parallel layout
rather than assuming the checkpoint size. Pure Ulysses replicates the adapter
on every rank, while DiT tensor parallelism shards the B buffers. Model-level
and standard layerwise offload remain unsupported, and `--step-execution`
cannot be combined with `--enable-distributed-layerwise-offload`.
To validate a deployment, post the same fixed-seed T2VA request twice with the
adapter and twice without it, then compare the four output digests. The adapter
is bound and deterministic when each pair matches internally and the two pairs
differ from each other.
## Key parameters
| Parameter | Recommended value | Notes |
+4 -66
View File
@@ -279,79 +279,17 @@ def test_unmeasured_tpot_stays_missing_after_tokenizer_fallback():
assert metrics.request_goodput == 0.0
def test_zero_itl_does_not_create_zero_tpot():
def test_measured_zero_itl_is_not_treated_as_missing():
output = _make_output(100, output_tokens=3)
output.itl = [0.0, 0.0]
output.duplex_session_metrics = {"mean_ttft_ms": 100.0}
metrics = _calculate_test_metrics([output], {"tpot": 1.0})
assert (metrics.num_tpot_samples, metrics.num_itl_samples) == (0, 2)
assert math.isnan(metrics.mean_tpot_ms)
assert (metrics.num_tpot_samples, metrics.num_itl_samples) == (1, 2)
assert metrics.mean_tpot_ms == 0.0
assert metrics.mean_itl_ms == 0.0
assert metrics.request_goodput == 0.0
def test_stage_output_tokens_without_client_timing_omit_tpot(capsys):
output = _make_output(100, output_tokens=5)
output.itl = []
output.text_latency = 0.0
output.ttft = 0.1
metrics, _ = calculate_metrics(
input_requests=[],
outputs=[output],
dur_s=1.0,
tokenizer=None,
selected_percentiles=[50.0, 99.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=["tpot"],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=1.0,
)
printed = capsys.readouterr().out
assert metrics.num_tpot_samples == 0
assert math.isnan(metrics.mean_tpot_ms)
assert "Time per Output Token" not in printed
assert "Mean TPOT" not in printed
def test_consistent_client_latency_can_supply_tpot_fallback():
output = _make_output(100, output_tokens=10)
output.itl = []
output.ttft = 0.1
output.text_latency = 1.0
metrics = _calculate_test_metrics([output])
assert metrics.num_tpot_samples == 1
assert metrics.mean_tpot_ms == pytest.approx(100.0)
def test_single_token_responses_do_not_report_zero_tpot(capsys):
output = _make_output(100, output_tokens=1)
metrics, _ = calculate_metrics(
input_requests=[],
outputs=[output],
dur_s=1.0,
tokenizer=None,
selected_percentiles=[50.0],
goodput_config_dict={},
task_type=TaskType.GENERATION,
selected_percentile_metrics=["tpot"],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=1.0,
)
printed = capsys.readouterr().out
assert metrics.num_tpot_samples == 0
assert math.isnan(metrics.mean_tpot_ms)
assert "Time per Output Token" not in printed
assert metrics.request_goodput == 1.0
def test_duplex_goodput_does_not_pair_measurements_from_different_requests():
+2 -73
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for patch.py
@@ -17,7 +17,6 @@ from vllm.benchmarks.lib.endpoint_request_func import RequestFuncInput
from vllm_omni.benchmarks.patch.patch import (
MixRequestFuncOutput,
_apply_stage0_token_timings,
async_request_openai_chat_omni_completions,
async_request_openai_realtime_duplex,
)
@@ -103,24 +102,7 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
received_at_s=now + 0.03,
)
self.events.add(
{
"type": "response.done",
"response": {
"id": response_id,
"metadata": {
"vllm_omni": {
"stage_metrics": {
"0": {
"num_tokens_out": 3,
"vllm_ttft_ms": 20.0,
"vllm_tpot_ms": 10.0,
"vllm_itls_ms": [10.0, 10.0],
}
}
}
},
},
},
{"type": "response.done", "response": {"id": response_id}},
received_at_s=now + 0.04,
)
@@ -161,7 +143,6 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
assert client.configure_kwargs["native_duplex"] is False
assert client.configure_kwargs["extra_body"] == {
"ref_audio": "data:audio/wav;base64,AAAA",
"return_stage_metrics": True,
}
assert client.sent == [
event
@@ -186,10 +167,6 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
assert output.audio_ttfp > output.ttft
assert output.audio_rtf > 0
assert output.latency > 0
assert output.output_tokens == 12
assert output.itl == [0.01, 0.01] * 4
assert output.text_latency == pytest.approx(output.ttft + 0.08)
assert output.tpot_measured is True
assert output.tts_turn_pcm_bytes == [b"\x00\x00" * 2400] * 4
assert output.tts_output_pcm_bytes == b"\x00\x00" * 9600
session_id = output.duplex_request_metrics[0]["session_id"]
@@ -221,54 +198,6 @@ async def test_seed_tts_realtime_duplex_exports_per_request_metrics(monkeypatch)
}
def test_stage0_token_timings_use_weighted_tpot_when_itls_are_incomplete():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[
{"output_token_count": 3, "itls_ms": [], "tpot_ms": 10.0},
{"output_token_count": 2, "itls_ms": [], "tpot_ms": 40.0},
],
expected_output_tokens=5,
)
assert measured is True
assert output.itl == []
assert output.tpot_measured is True
assert (output.text_latency - output.ttft) / 4 == pytest.approx(0.02)
def test_stage0_zero_itls_fall_back_to_positive_tpot():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[{"output_token_count": 3, "itls_ms": [0.0, 0.0], "tpot_ms": 25.0}],
expected_output_tokens=3,
)
assert measured is True
assert output.itl == []
assert output.tpot_measured is True
assert (output.text_latency - output.ttft) / 2 == pytest.approx(0.025)
def test_stage0_token_count_without_timing_is_not_measured():
output = MixRequestFuncOutput(ttft=0.1)
measured = _apply_stage0_token_timings(
output,
[{"output_token_count": 3, "itls_ms": [], "tpot_ms": None}],
expected_output_tokens=3,
)
assert measured is False
assert output.itl == []
assert output.text_latency == output.ttft
assert output.tpot_measured is False
def create_sse_chunk(data_dict):
"""Helper to create SSE formatted chunk"""
return f"data: {json.dumps(data_dict)}\n\n".encode()
+2 -6
View File
@@ -585,7 +585,7 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
@pytest.mark.asyncio
@pytest.mark.parametrize(
("request_metrics", "expected_itl", "expected_text_latency", "expected_tpot_measured"),
("request_metrics", "expected_itl", "expected_text_latency"),
[
(
[
@@ -594,7 +594,6 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[0.01, 0.02, 0.03],
0.16,
True,
),
(
[
@@ -603,7 +602,6 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[],
0.18,
True,
),
(
[
@@ -612,7 +610,6 @@ async def test_adapter_requires_and_forwards_exact_prepared_payload(tmp_path: Pa
],
[],
0.1,
False,
),
],
)
@@ -622,7 +619,6 @@ async def test_adapter_reports_exact_or_weighted_token_timing(
request_metrics: list[dict[str, object]],
expected_itl: list[float],
expected_text_latency: float,
expected_tpot_measured: bool,
):
case, options = _case(tmp_path), _session_options(tmp_path)
request = _request(case, options)
@@ -647,7 +643,7 @@ async def test_adapter_reports_exact_or_weighted_token_timing(
assert output.success
assert output.itl == expected_itl
assert output.text_latency == pytest.approx(expected_text_latency)
assert output.tpot_measured is expected_tpot_measured
assert output.tpot_measured is True
def test_batch_finalization_publishes_only_measured_results(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
+1 -15
View File
@@ -1159,7 +1159,7 @@ class TestDeployConfigLoading:
stages = merge_pipeline_deploy(pipeline, deploy)
assert deploy.session_mode == "duplex"
assert deploy.active_stream_window == max_sessions
assert deploy.active_stream_window == 1
assert deploy.duplex_session.max_sessions == max_sessions
assert [stage.session_mode for stage in stages] == ["duplex", "duplex", "duplex"]
assert [stage.to_omegaconf().session_mode for stage in stages] == ["duplex", "duplex", "duplex"]
@@ -2399,20 +2399,6 @@ class TestPlatformOverrides:
assert rocm.stages[0].enforce_eager is None
assert rocm.stages[1].enforce_eager is True
def test_higgs_audio_v3_rocm_uses_triton_attention(self):
deploy_path = Path(get_deploy_config_path("higgs_multimodal_qwen3.yaml"))
base = load_deploy_config(deploy_path)
assert base.stages[0].engine_extras["attention_backend"] == "FLASHINFER"
rocm = _apply_platform_overrides(base, platform="rocm")
assert rocm.stages[0].engine_extras["attention_backend"] == "TRITON_ATTN"
pipeline = resolve_pipeline_config("higgs_multimodal_qwen3")
assert isinstance(pipeline, PipelineConfig)
stages = merge_pipeline_deploy(pipeline, rocm)
assert stages[0].yaml_engine_args["attention_backend"] == "TRITON_ATTN"
def test_qwen3_omni_cuda_uses_thinker_rotary_custom_op(self):
deploy_path = Path(get_deploy_config_path("qwen3_omni_moe.yaml"))
pipeline = resolve_pipeline_config(
-18
View File
@@ -653,7 +653,6 @@ def test_sub_config_fields_match_structured_scopes():
"ring_degree",
"allgather_degree",
"ulysses_mode",
"ulysses_a2a_permute",
"cfg_parallel_size",
"vae_patch_parallel_size",
"vae_parallel_mode",
@@ -859,23 +858,6 @@ def test_from_pipeline_config_derives_sequence_parallel_size_from_allgather_degr
assert stage.parallel_config.world_size == 2
def test_from_pipeline_config_preserves_deployed_ulysses_a2a_permute() -> None:
pipeline = _resolve_pipeline_or_skip("dreamzero")
deploy = DeployConfig(
stages=[
StageDeployConfig(
stage_id=0,
ulysses_a2a_permute=True,
)
]
)
stage = VllmOmniConfig.from_pipeline_config(pipeline, user_deploy_config=deploy).stage_by_id(0)
assert isinstance(stage, VllmOmniDiffusionStageConfig)
assert stage.parallel_config.ulysses_a2a_permute is True
def test_diffusion_parallel_config_accepts_four_way_guidance_parallelism():
cfg = OmniStageDiffusionParallelConfig(cfg_parallel_size=4)
+2 -2
View File
@@ -25,9 +25,9 @@ pytest_plugins = (
"tests.helpers.fixtures.clean",
"tests.helpers.fixtures.log",
"tests.helpers.fixtures.media",
"tests.helpers.fixtures.pytest_collection",
"tests.helpers.fixtures.pytest_run_args",
"tests.helpers.fixtures.run_args",
"tests.helpers.fixtures.runtime",
"tests.helpers.fixtures.xdist",
)
@@ -1,89 +0,0 @@
"""Tests for ``OmniARScheduler._drop_aborted_queued_requests``.
``schedule()`` sweeps ``FINISHED_ABORTED`` requests out of the queues before
handing control to upstream, because upstream ``Scheduler.schedule()`` raises
``RuntimeError: Invalid request status: FINISHED_ABORTED`` for any request it
admits in a finished state -- which kills the stage's engine core, not just
the request.
Regression for the sweep missing ``skipped_waiting``, the third queue
upstream admits from. An aborted duplex session parked there was re-selected
by ``_select_waiting_queue_for_scheduling`` on a later tick and crashed the
stage.
"""
from __future__ import annotations
import pytest
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import RequestStatus
import vllm_omni.core.sched.omni_ar_scheduler as ar_sched_mod
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
_POLICIES = [
pytest.param(SchedulingPolicy.FCFS, id="fcfs"),
pytest.param(SchedulingPolicy.PRIORITY, id="priority"),
]
class _StubRequest:
"""Minimal ``Request`` stub with the surface the sweep exercises.
Carries the ``(priority, arrival_time)`` ordering ``PriorityRequestQueue``
relies on so the same stub works under both scheduling policies.
"""
def __init__(self, request_id: str, status: RequestStatus, arrival_time: float = 0.0) -> None:
self.request_id = request_id
self.status = status
self.priority = 0
self.arrival_time = arrival_time
def __lt__(self, other: _StubRequest) -> bool:
return (self.priority, self.arrival_time) < (other.priority, other.arrival_time)
def _make_scheduler(policy: SchedulingPolicy, *, waiting=(), skipped_waiting=(), running=()):
"""Bare scheduler with just the surface the sweep reads."""
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.waiting = create_request_queue(policy)
for req in waiting:
scheduler.waiting.add_request(req)
scheduler.skipped_waiting = create_request_queue(policy)
for req in skipped_waiting:
scheduler.skipped_waiting.add_request(req)
scheduler.running = list(running)
return scheduler
@pytest.mark.parametrize("policy", _POLICIES)
def test_schedule_sweeps_skipped_waiting_before_upstream_selection(
monkeypatch: pytest.MonkeyPatch,
policy: SchedulingPolicy,
) -> None:
"""The public schedule path must remove aborted skipped requests first."""
aborted = _StubRequest("req-aborted", RequestStatus.FINISHED_ABORTED)
scheduler = _make_scheduler(policy, skipped_waiting=[aborted])
scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = None
scheduler.num_waiting_for_streaming_input = 0
scheduler._process_pending_omni_inputs = lambda model_mode: None
scheduler._resync_streaming_input_counter = lambda: None
scheduler._should_defer_waiting_admission = lambda: False
scheduler._restore_omni_wait_queues = lambda: None
scheduler._postprocess_omni_schedule_output = lambda *args, **kwargs: None
scheduler.get_finished_requests_needing_kv_transfer = lambda: {}
scheduler._wrap_omni_scheduler_output = lambda output, **kwargs: output
sentinel = object()
def upstream_schedule(_self, _throttle_prefills: bool = False):
assert list(scheduler.skipped_waiting) == []
return sentinel
monkeypatch.setattr(ar_sched_mod.VLLMScheduler, "schedule", upstream_schedule)
assert OmniARScheduler.schedule(scheduler) is sentinel
@@ -142,16 +142,13 @@ def test_exact_drain_delivers_new_segment_frame() -> None:
session.num_computed_tokens = 6
session.num_output_placeholders = 1
session.num_in_flight_tokens = 1
session.async_tokens_to_discard = 1
_replace_streaming_session(session)
assert session.num_stale_output_tokens == 1
assert session.async_tokens_to_discard == 1
sched = _make_drain_sched(session)
assert _run_step(sched, session, num_scheduled=1, token=42) is False # late frame dropped
assert session.num_stale_output_tokens == 0
assert session.async_tokens_to_discard == 0
assert _run_step(sched, session, num_scheduled=1, token=43) is True # new segment survives
@@ -18,7 +18,7 @@ from vllm.sampling_params import SamplingParams
from vllm.v1.core.sched.output import SchedulerOutput
from vllm.v1.outputs import ModelRunnerOutput
from vllm.v1.request import Request, RequestStatus, StreamingUpdate
from vllm_omni.core.sched.omni_ar_scheduler import OmniARAsyncScheduler, OmniARScheduler
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
# isort: on
@@ -28,9 +28,7 @@ pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _make_scheduler(*, stage_id: int = 0, session_mode: str = "turn") -> OmniARScheduler:
sched = OmniARScheduler.__new__(OmniARScheduler)
sched._new_prompt_len_snapshot = {}
sched.vllm_config = SimpleNamespace(
model_config=SimpleNamespace(stage_id=stage_id, session_mode=session_mode),
)
sched.vllm_config = SimpleNamespace(model_config=SimpleNamespace(stage_id=stage_id, session_mode=session_mode))
sched.num_waiting_for_streaming_input = 0
sched.log_stats = False
sched.chunk_transfer_adapter = None
@@ -187,7 +185,6 @@ def test_resumable_segment_boundary_keeps_pre_transition_send_watermark() -> Non
def replace_with_next_segment(request: Request) -> bool:
request.num_computed_tokens = 0
request._omni_segment_generation = 1
request.status = RequestStatus.WAITING
return False
@@ -204,7 +201,6 @@ def test_resumable_segment_boundary_keeps_pre_transition_send_watermark() -> Non
True,
new_token_ids=[42],
confirmed_num_computed_tokens=26,
segment_generation=0,
)
@@ -290,140 +286,6 @@ def test_queued_streaming_update_on_async_stop_fences_in_flight_once() -> None:
assert session.num_stale_output_tokens == session.num_in_flight_tokens == 1
def test_stale_async_frame_is_dropped_before_output_processing() -> None:
session = _make_request()
session.status = RequestStatus.RUNNING
session.num_in_flight_tokens = 2
session.num_computed_tokens = session.num_tokens + 1
session.num_output_placeholders = 1
session.async_tokens_to_discard = 1
session.sampling_params = SimpleNamespace(num_logprobs=1)
num_computed_tokens = session.num_computed_tokens
num_output_placeholders = session.num_output_placeholders
sched = MagicMock()
sched.requests = {session.request_id: session}
sched.perf_metrics = None
sched.structured_output_manager.should_advance.return_value = False
def discard_stale_output(request: Request, token_ids: list[int]) -> tuple[list[int], bool]:
request.async_tokens_to_discard = 0
return token_ids, False
sched._update_request_with_output.side_effect = discard_stale_output
sched._process_kv_transfer_trigger.return_value = False
sched.chunk_transfer_adapter = MagicMock()
sched.running = [session]
sched.waiting_for_transfer_free = set()
sched.transfer_triggered_requests = set()
sched.active_kv_transfers = set()
sched.pending_stop_after_extraction = set()
sched.connector = None
sched.kv_cache_manager.take_events.return_value = None
sched.kv_cache_manager.estimate_cached_tokens.return_value = 0
sched.finished_req_ids_dict = {}
sched.make_stats.return_value = None
scheduler_output = MagicMock(spec=SchedulerOutput)
scheduler_output.num_scheduled_tokens = {session.request_id: 1}
scheduler_output.scheduled_spec_decode_tokens = {}
scheduler_output.num_invalid_spec_tokens = 0
model_runner_output = MagicMock(spec=ModelRunnerOutput)
model_runner_output.sampled_token_ids = [[42]]
model_runner_output.logprobs = None
model_runner_output.prompt_logprobs_dict = {}
model_runner_output.pooler_output = None
model_runner_output.multimodal_outputs = None
model_runner_output.inter_stage_outputs = [{"hidden": object()}]
model_runner_output.num_nans_in_logits = None
model_runner_output.kv_connector_output = None
model_runner_output.cudagraph_stats = None
model_runner_output.req_id_to_index = {session.request_id: 0}
model_runner_output.routed_experts = None
OmniARScheduler.update_from_output(sched, scheduler_output, model_runner_output)
assert session.async_tokens_to_discard == 0
assert session.status == RequestStatus.RUNNING
assert session.num_computed_tokens == num_computed_tokens
assert session.num_output_placeholders == num_output_placeholders
sched.chunk_transfer_adapter.save_async.assert_not_called()
session.sampling_params = SamplingParams(max_tokens=8)
next_payload = {"hidden": object()}
sched._update_request_with_output.side_effect = None
sched._update_request_with_output.return_value = ([43], False)
model_runner_output.sampled_token_ids = [[43]]
model_runner_output.inter_stage_outputs = [next_payload]
OmniARScheduler.update_from_output(sched, scheduler_output, model_runner_output)
sched.chunk_transfer_adapter.save_async.assert_called_once_with(
next_payload,
session,
False,
new_token_ids=[43],
confirmed_num_computed_tokens=None,
)
def test_legacy_stale_async_marker_bypasses_real_placeholder_accounting() -> None:
request = _make_request()
request.status = RequestStatus.RUNNING
request.num_in_flight_tokens = 1
request.num_output_placeholders = 0
request.async_tokens_to_discard = 1
request.num_stale_output_tokens = 0
sched = OmniARAsyncScheduler.__new__(OmniARAsyncScheduler)
sched.requests = {request.request_id: request}
sched.perf_metrics = None
sched.chunk_transfer_adapter = None
sched.connector = None
sched.ec_connector = None
sched.waiting_for_transfer_free = set()
sched.transfer_triggered_requests = set()
sched.active_kv_transfers = set()
sched.pending_stop_after_extraction = set()
sched.finished_req_ids_dict = {}
sched._new_prompt_len_snapshot = {}
sched.kv_cache_manager = MagicMock()
sched.kv_cache_manager.take_events.return_value = None
sched._remove_stopped_requests_from_queues = MagicMock()
sched._handle_failed_kv_load_outputs = MagicMock(return_value=[])
sched._cleanup_kv_tracking = MagicMock()
sched._aggregate_kv_connector_stats = MagicMock(return_value=None)
sched._publish_kv_cache_events = MagicMock()
sched._attach_finished_request_sets = MagicMock()
sched._attach_scheduler_stats = MagicMock()
sched._capture_omni_connector_output = MagicMock()
scheduler_output = MagicMock(spec=SchedulerOutput)
scheduler_output.num_scheduled_tokens = {request.request_id: 1}
scheduler_output.scheduled_spec_decode_tokens = {}
scheduler_output.num_invalid_spec_tokens = 0
model_runner_output = MagicMock(spec=ModelRunnerOutput)
model_runner_output.sampled_token_ids = [[42]]
model_runner_output.logprobs = None
model_runner_output.prompt_logprobs_dict = {}
model_runner_output.pooler_output = None
model_runner_output.multimodal_outputs = None
model_runner_output.inter_stage_outputs = None
model_runner_output.num_nans_in_logits = None
model_runner_output.kv_connector_output = None
model_runner_output.cudagraph_stats = None
model_runner_output.req_id_to_index = {request.request_id: 0}
model_runner_output.routed_experts = None
OmniARScheduler.update_from_output(sched, scheduler_output, model_runner_output)
assert request.async_tokens_to_discard == 0
assert request.num_output_placeholders == 0
assert request._output_token_ids == []
def test_stage0_streaming_update_discards_outstanding_async_placeholder_token() -> None:
sched = _make_scheduler(stage_id=0)
session = _make_request()
@@ -76,9 +76,6 @@ def _make_scheduler(
else:
hf_config.voxcpm2_runtime_config = runtime_config
sched.vllm_config = SimpleNamespace(model_config=model_config)
# Upstream ``Scheduler.__init__`` always creates ``skipped_waiting``; the
# abort sweep at the top of ``schedule()`` reads it.
sched.skipped_waiting = _MockQueue()
return sched
@@ -166,15 +166,3 @@ def test_fast_path_schedule_defer_disabled_leaves_fence() -> None:
OmniGenerationScheduler.schedule(sched)
assert sched.sched_step_seq == 0
def test_generation_schedule_sweeps_and_resyncs_around_pending_inputs() -> None:
sched, request = _make_fast_path_sched(defer_block_free=False)
calls: list[str] = []
sched._drop_aborted_queued_requests.side_effect = lambda: calls.append("sweep")
sched._process_pending_omni_inputs.side_effect = lambda model_mode: calls.append(model_mode)
sched._resync_streaming_input_counter.side_effect = lambda: calls.append("resync")
OmniGenerationScheduler.schedule(sched)
assert calls == ["sweep", "generation", "sweep", "resync"]
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Unit coverage for the two stage-input timeout hooks.
``_process_pending_input_timeouts`` covers the full-payload transport;
@@ -253,38 +253,6 @@ def test_log_failed_chunk_sends_noop_without_adapter():
scheduler._log_failed_chunk_sends()
class _FakeReceiveFailureAdapter:
def __init__(self, failures):
self._failures = dict(failures)
def collect_failed_receive_request_ids(self):
failures, self._failures = self._failures, {}
return failures
class _FakeReceiveFailureScheduler(OmniSchedulerMixin):
def __init__(self, requests, adapter):
self.requests = requests
self.chunk_transfer_adapter = adapter
self.finish_calls = []
def finish_requests(self, req_ids, status):
self.finish_calls.append((set(req_ids), status))
def test_process_chunk_receive_failures_delegates_to_finish_requests():
request_id = "invalid-chunk"
adapter = _FakeReceiveFailureAdapter({request_id: "prompt exceeds limit"})
scheduler = _FakeReceiveFailureScheduler({request_id: SimpleNamespace(request_id=request_id)}, adapter)
scheduler._process_chunk_receive_failures()
assert len(scheduler.finish_calls) == 1
finished_ids, status = scheduler.finish_calls[0]
assert finished_ids == {request_id}
assert getattr(status, "name", str(status)).endswith("FINISHED_ERROR")
# --- R1.4: VLLM_OMNI_INPUT_WAIT_TIMEOUT_S is parsed at import time, so each of
# these reloads the module under a different environment. One knob now gates the
# deadline on both transports, which is why a silent 0 matters more than it did
@@ -1,168 +0,0 @@
"""Tests for the ``num_waiting_for_streaming_input`` resync placed in
``OmniARScheduler.finish_requests`` / ``OmniGenerationScheduler.finish_requests``
and in ``OmniARScheduler.schedule``.
Upstream keeps that counter incrementally (``+1`` when a resumable request
parks in a waiting queue as ``WAITING_FOR_STREAMING_REQ``, ``-1`` when the
next update arrives or the request is finished) and
``Scheduler.get_num_unfinished_requests`` *subtracts* it from the waiting
queues. Omni rewrites ``request.status`` outside both hooks -- the
chunk-transfer adapter's park/restore and
``_realign_request_status_to_queues`` -- so a counted request can leave
``WAITING_FOR_STREAMING_REQ`` without the matching decrement and inflate the
counter for the lifetime of the process.
An inflated counter does not degrade throughput, it hangs the stage:
``EngineCore.has_work()`` reads false while a live request sits in
``waiting``, so the engine blocks in ``input_queue.get()`` and never calls
``schedule()`` again. Regression for the Qwen3-Omni duplex symptom where the
server answered the first conversation after a boot and was silent from the
second on while still reporting healthy.
"""
from __future__ import annotations
import pytest
from vllm.v1.core.sched.request_queue import SchedulingPolicy, create_request_queue
from vllm.v1.request import RequestStatus
import vllm_omni.core.sched.omni_ar_scheduler as ar_sched_mod
import vllm_omni.core.sched.omni_generation_scheduler as gen_sched_mod
from vllm_omni.core.sched.omni_ar_scheduler import OmniARScheduler
from vllm_omni.core.sched.omni_generation_scheduler import OmniGenerationScheduler
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
class _StubRequest:
def __init__(self, request_id: str, status: RequestStatus) -> None:
self.request_id = request_id
self.status = status
def is_finished(self) -> bool:
return RequestStatus.is_finished(self.status)
_SCHEDULER_PARAMS = [
pytest.param(OmniARScheduler, ar_sched_mod, id="ar"),
pytest.param(OmniGenerationScheduler, gen_sched_mod, id="generation"),
]
def _make_scheduler(scheduler_cls, *, requests, running, waiting, counter, skipped=None):
scheduler = scheduler_cls.__new__(scheduler_cls)
scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = None
scheduler.requests = requests
scheduler.running = running
scheduler.waiting = waiting
scheduler.skipped_waiting = skipped if skipped is not None else []
scheduler.num_waiting_for_streaming_input = counter
return scheduler
@pytest.mark.parametrize(("scheduler_cls", "scheduler_mod"), _SCHEDULER_PARAMS)
def test_finish_requests_clears_counter_left_by_a_stomped_status(
monkeypatch: pytest.MonkeyPatch,
scheduler_cls,
scheduler_mod,
) -> None:
"""The abort case that wedged the talker.
The request was counted while parked for streaming input, then had its
status rewritten to ``RUNNING`` by omni's queue surgery, so upstream's
``finish_requests`` took the running branch and never decremented. With
no parked requests left, the counter must land at 0.
"""
aborted = _StubRequest("req-aborted", RequestStatus.RUNNING)
scheduler = _make_scheduler(
scheduler_cls,
requests={}, # upstream already dropped it
running=[],
waiting=[],
counter=1,
)
monkeypatch.setattr(
scheduler_mod.VLLMScheduler,
"finish_requests",
lambda self, request_ids, finished_status: [aborted],
)
scheduler_cls.finish_requests(scheduler, [aborted.request_id], RequestStatus.FINISHED_ABORTED)
assert scheduler.num_waiting_for_streaming_input == 0
def test_resync_counts_parked_requests_in_skipped_waiting() -> None:
"""``get_num_unfinished_requests`` sums ``waiting`` and ``skipped_waiting``,
so the resync has to look at both or it under-counts and reintroduces the
spin the counter exists to prevent."""
parked = _StubRequest("req-skipped", RequestStatus.WAITING_FOR_STREAMING_REQ)
scheduler = _make_scheduler(
OmniARScheduler,
requests={parked.request_id: parked},
running=[],
waiting=[],
counter=0,
skipped=[parked],
)
scheduler._resync_streaming_input_counter()
assert scheduler.num_waiting_for_streaming_input == 1
def test_ar_schedule_resyncs_before_delegating_upstream(monkeypatch: pytest.MonkeyPatch) -> None:
"""Pins placement in ``schedule``: the chunk-transfer adapter rewrites
status during ``process_pending_chunks``, so the resync has to run after
that and before upstream schedules, while ``self.waiting`` is still the
real queue."""
parked = _StubRequest("req-resumed", RequestStatus.RUNNING)
scheduler = OmniARScheduler.__new__(OmniARScheduler)
scheduler.chunk_transfer_adapter = None
scheduler.input_coordinator = None
scheduler.requests = {parked.request_id: parked}
scheduler.running = [parked]
scheduler.waiting = []
scheduler.skipped_waiting = []
scheduler.num_waiting_for_streaming_input = 1 # leaked by the status rewrite
scheduler.policy = "fcfs"
seen: dict[str, int] = {}
monkeypatch.setattr(
ar_sched_mod.OmniSchedulerMixin, "_consume_pending_connector_output", lambda self, model_mode: None
)
monkeypatch.setattr(ar_sched_mod.OmniSchedulerMixin, "_process_pending_input_timeouts", lambda self: None)
monkeypatch.setattr(OmniARScheduler, "_should_defer_waiting_admission", lambda self: False)
def fake_schedule(self, throttle_prefills=False):
seen["counter"] = self.num_waiting_for_streaming_input
raise _StopScheduleError
class _StopScheduleError(Exception):
pass
monkeypatch.setattr(ar_sched_mod.VLLMScheduler, "schedule", fake_schedule)
with pytest.raises(_StopScheduleError):
OmniARScheduler.schedule(scheduler)
assert seen["counter"] == 0
def test_generation_scheduler_sweep_removes_aborted_requests_from_all_queues() -> None:
aborted = _StubRequest("req-generation-aborted", RequestStatus.FINISHED_ABORTED)
scheduler = OmniGenerationScheduler.__new__(OmniGenerationScheduler)
scheduler.waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.waiting.add_request(aborted)
scheduler.skipped_waiting = create_request_queue(SchedulingPolicy.FCFS)
scheduler.skipped_waiting.add_request(aborted)
scheduler.running = [aborted]
scheduler._drop_aborted_queued_requests()
assert list(scheduler.waiting) == []
assert list(scheduler.skipped_waiting) == []
assert scheduler.running == []
-16
View File
@@ -1,8 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import json
import math
import os
import threading
from collections.abc import Callable
@@ -163,18 +159,6 @@ def benchmark_params(request):
def assert_result(result, params, num_prompt) -> None:
assert result["completed"] == num_prompt, "Request failures exist"
baseline = params.get("baseline")
hardware = result.get("Hardware")
hardware_baseline = baseline.get(hardware) if isinstance(baseline, dict) and isinstance(hardware, str) else None
if isinstance(hardware_baseline, dict) and "mean_tpot_ms" in hardware_baseline:
num_tpot_samples = result.get("num_tpot_samples")
mean_tpot_ms = result.get("mean_tpot_ms")
assert isinstance(num_tpot_samples, int) and not isinstance(num_tpot_samples, bool) and num_tpot_samples > 0, (
"TPOT baseline is configured, but no measurable TPOT samples were produced"
)
assert (
isinstance(mean_tpot_ms, int | float) and not isinstance(mean_tpot_ms, bool) and math.isfinite(mean_tpot_ms)
), "TPOT baseline is configured, but mean_tpot_ms is not finite"
expected_audio_turns = params.get("expected_duplex_audio_turns_per_session")
if expected_audio_turns is not None:
session_metrics = result.get("duplex_session_metrics")
@@ -43,7 +43,7 @@
"extra_body": {
"save_duplex_request_metrics": true
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_ttfp,audio_rtf,audio_duration",
"percentile-metrics": "ttft,e2el,audio_ttfp,audio_rtf,audio_duration",
"baseline": {
"H100": {
"request_throughput": [
+1 -42
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Tests for DFX runner metadata field exclusion."""
import json
@@ -108,7 +105,7 @@ def test_resolve_pytest_marks_hardware_dict_with_extra():
assert "H100" in names
assert "cuda" in names
assert "gpu" in names
assert "cards_2" in names
assert "distributed_cuda" in names
assert "full_model" in names
assert "diffusion" in names
assert "local_model" in names
@@ -515,41 +512,3 @@ def test_omni_duplex_expected_audio_turns_rejects_incomplete_session():
{"expected_duplex_audio_turns_per_session": 4},
1,
)
def test_omni_tpot_baseline_accepts_measured_finite_sample():
from tests.dfx.perf.scripts.run_benchmark import assert_result
assert_result(
{
"completed": 1,
"Hardware": "H100",
"num_tpot_samples": 1,
"mean_tpot_ms": 10.0,
},
{"baseline": {"H100": {"mean_tpot_ms": 20.0}}},
1,
)
@pytest.mark.parametrize(
("num_tpot_samples", "mean_tpot_ms", "match"),
[
(0, float("nan"), "no measurable TPOT samples"),
(1, float("nan"), "mean_tpot_ms is not finite"),
],
)
def test_omni_tpot_baseline_rejects_missing_or_nonfinite_sample(num_tpot_samples, mean_tpot_ms, match):
from tests.dfx.perf.scripts.run_benchmark import assert_result
with pytest.raises(AssertionError, match=match):
assert_result(
{
"completed": 1,
"Hardware": "H100",
"num_tpot_samples": num_tpot_samples,
"mean_tpot_ms": mean_tpot_ms,
},
{"baseline": {"H100": {"mean_tpot_ms": 20.0}}},
1,
)
@@ -101,12 +101,7 @@ class TestAttentionSpec:
def test_block_sparse_defaults_applied_when_backend_selected(self):
spec = AttentionSpec(backend="RAINFUSION_ATTN")
assert spec.block_sparse.sparsity == 0.8
assert spec.backend_kwargs() == {
"sparsity": 0.8,
"start_step": 0,
"end_step": 0,
"precision": "bf16",
}
assert spec.backend_kwargs() == {"sparsity": 0.8, "start_step": 0}
def test_block_sparse_skip_layers_selector_expanded(self):
spec = AttentionSpec(
@@ -117,8 +112,6 @@ class TestAttentionSpec:
assert spec.backend_kwargs() == {
"sparsity": 0.9,
"start_step": 12,
"end_step": 0,
"precision": "bf16",
"skip_layers": [0, 1, 2, 38],
}
@@ -128,44 +121,12 @@ class TestAttentionSpec:
@pytest.mark.parametrize(
"block_sparse",
[{"sparsity": 1.5}, {"start_step": -1}, {"end_step": -1}, {"precision": "bogus"}],
[{"sparsity": 1.5}, {"start_step": -1}],
)
def test_block_sparse_invalid_values(self, block_sparse):
with pytest.raises(ValueError):
AttentionSpec(backend="RAINFUSION_ATTN", block_sparse=block_sparse)
def test_block_sparse_end_step_precision_forwarded(self):
spec = AttentionSpec(
backend="RAINFUSION_ATTN",
block_sparse={"sparsity": 0.9, "start_step": 5, "end_step": 3, "precision": "fp8"},
)
assert spec.backend_kwargs() == {
"sparsity": 0.9,
"start_step": 5,
"end_step": 3,
"precision": "fp8",
}
def test_block_sparse_precision_enum_input_normalized(self):
"""RainFusionPrecision enum members must normalize to their string value.
Regression: the str,Enum members passed the value check but str() later
produced "RainFusionPrecision.MIX" instead of "mix" (StrEnum fixes it).
"""
from vllm_omni.diffusion.attention.backends.rainfusion_attn import RainFusionConfig
from vllm_omni.diffusion.data import RainFusionPrecision
spec = AttentionSpec(
backend="RAINFUSION_ATTN",
block_sparse={"sparsity": 0.8, "precision": RainFusionPrecision.MIX},
)
kwargs = spec.backend_kwargs()
assert kwargs["precision"] == "mix"
# The config path is where str(value) used to mangle enum members.
cfg = RainFusionConfig.from_backend_kwargs(kwargs)
assert cfg.precision == "mix"
assert type(cfg.precision) is str
class TestAttentionConfig:
def test_empty_config(self):
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Geometry contract RAINFUSION_ATTN enforces before handing a forward to rf_v2.
@@ -13,12 +13,10 @@ to the always-kept prefix segment before generating the block mask.
import dataclasses
import sys
import types
from unittest import mock
import pytest
import torch
from vllm_omni.diffusion.attention.backends import rainfusion_attn
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata, VideoTokenLayout, VideoTokenSpan
from vllm_omni.diffusion.attention.backends.rainfusion_attn import (
_BLOCK_SIZE,
@@ -28,8 +26,6 @@ from vllm_omni.diffusion.attention.backends.rainfusion_attn import (
)
from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.npu]
PREFIX_ROWS = 710 # 14 text rows + 696 audio rows
ALIGNED_GRID = (62, 24, 40) # 1280x768 -> 59520 video rows, 465 blocks
MISALIGNED_GRID = (62, 24, 42) # 1344x768 -> 62496 video rows, 488 blocks + 32 rows
@@ -232,9 +228,7 @@ def test_irregular_video_tail_reproduces_dense_attention_with_updated_mindiesd()
heads, head_dim = 4, 128
q, k, v = (torch.randn(1, used, heads, head_dim, dtype=torch.bfloat16, device="npu") for _ in range(3))
# Explicit bf16: the dense comparison exercises the plain sparse kernel,
# and older MindIE-SD releases do not accept precision= (gate would raise).
impl = make_impl(precision="bf16")
impl = make_impl()
impl.rainfusion = dataclasses.replace(impl.rainfusion, sparsity=0.0)
plan = RainFusionPlan(prefix_len=prefix_len, used_len=used, latent_shape=list(grid))
out = impl._forward_sparse_npu(q, k, v, plan)
@@ -247,90 +241,3 @@ def test_irregular_video_tail_reproduces_dense_attention_with_updated_mindiesd()
).transpose(1, 2)
error = (out.float() - reference.float()).abs().mean() / reference.float().abs().mean()
assert error < 2e-3, f"mean relative error {error:.4%} against dense attention"
# end_step tail fallback: the last ``end_step`` denoise steps must stay dense.
def test_end_step_tail_window_stays_dense():
"""step_idx inside the last end_step window must resolve no sparse plan."""
impl = make_impl(end_step=3)
fc = mock.Mock()
fc.denoise_step_idx = 47
fc.total_denoise_steps = 50
with (
mock.patch.object(rainfusion_attn, "is_forward_context_available", return_value=True),
mock.patch.object(rainfusion_attn, "get_forward_context", return_value=fc),
):
assert impl._resolve_plan(make_metadata(ALIGNED_GRID)) is None
def test_end_step_before_tail_window_still_sparse():
"""step_idx before the tail window must still resolve a sparse plan."""
impl = make_impl(end_step=3)
fc = mock.Mock()
fc.denoise_step_idx = 46
fc.total_denoise_steps = 50
with (
mock.patch.object(rainfusion_attn, "is_forward_context_available", return_value=True),
mock.patch.object(rainfusion_attn, "get_forward_context", return_value=fc),
):
assert impl._resolve_plan(make_metadata(ALIGNED_GRID)) is not None
def test_end_step_zero_never_triggers_tail_fallback():
"""end_step=0 must not fall back even on the final denoise step."""
impl = make_impl(end_step=0)
fc = mock.Mock()
fc.denoise_step_idx = 49
fc.total_denoise_steps = 50
with (
mock.patch.object(rainfusion_attn, "is_forward_context_available", return_value=True),
mock.patch.object(rainfusion_attn, "get_forward_context", return_value=fc),
):
assert impl._resolve_plan(make_metadata(ALIGNED_GRID)) is not None
# precision capability gate: a mindiesd without sparse_attention(precision=)
# must fail fast instead of silently running the BF16 path.
def _fake_mindiesd_module():
"""Install a minimal stand-in so the import inside _forward_sparse_npu succeeds."""
import types
fake = types.ModuleType("mindiesd")
fake.sparse_attention = lambda *args, **kwargs: None
return fake
def test_precision_non_bf16_requires_mindiesd_support():
"""precision != bf16 against a mindiesd lacking the kwarg must raise RuntimeError."""
import sys
impl = make_impl(precision="mix")
sys.modules["mindiesd"] = _fake_mindiesd_module()
try:
with mock.patch.object(rainfusion_attn, "_mindiesd_supports_precision", return_value=False):
with pytest.raises(RuntimeError, match="requires MindIE-SD"):
impl._forward_sparse_npu(None, None, None, None)
finally:
sys.modules.pop("mindiesd", None)
def test_precision_non_bf16_passes_gate_when_supported():
"""precision != bf16 with a capable mindiesd must not raise from the gate."""
import sys
impl = make_impl(precision="mix")
sys.modules["mindiesd"] = _fake_mindiesd_module()
try:
with mock.patch.object(rainfusion_attn, "_mindiesd_supports_precision", return_value=True):
# q/k/v shapes: [B, S, N, D]; plan geometry must match S.
q = torch.randn(1, 8, 4, 128)
plan = RainFusionPlan(prefix_len=0, used_len=8, latent_shape=[2, 2, 2])
# The gate must pass; the fake mindiesd returns None so no crash.
out = impl._forward_sparse_npu(q, q, q, plan)
assert out is None
finally:
sys.modules.pop("mindiesd", None)
+1 -241
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for TeaCache extractor functions.
@@ -18,7 +18,6 @@ Currently implemented:
- TestMiniMaxH3Extractor: MiniMaxH3DiTModel extractor
"""
import math
from abc import ABC, abstractmethod
from types import SimpleNamespace
from unittest.mock import MagicMock, Mock, patch
@@ -651,29 +650,6 @@ class TestMiniMaxH3Extractor(BaseExtractorTest):
torch.testing.assert_close(actual_video, expected_video)
torch.testing.assert_close(actual_audio, expected_audio)
def test_extractor_uses_prepared_rope_table(
self,
minimax_h3_module,
sample_inputs,
monkeypatch,
):
"""TeaCache must consume the denoise-loop RoPE table without rebuilding it."""
rope_table = minimax_h3_module.prepare_rope_table(
sample_inputs["img_position_ids"],
seq_len=sample_inputs["x"].shape[1],
)
def unexpected_rope_build(*args, **kwargs):
raise AssertionError("TeaCache extractor rebuilt the prepared RoPE table")
monkeypatch.setattr(minimax_h3_module, "prepare_rope_table", unexpected_rope_build)
monkeypatch.setattr(minimax_h3_module.rope, "forward", unexpected_rope_build)
context = extract_minimax_h3_context(
minimax_h3_module,
**{**sample_inputs, "rope_table": rope_table},
)
context.run_transformer_blocks()
def test_run_transformer_blocks_forwards_packed_layout(
self,
minimax_h3_module,
@@ -729,222 +705,6 @@ class TestMiniMaxH3Extractor(BaseExtractorTest):
assert video_logits.shape[0] == sample_inputs["img_pos_info"]["position_ids"].shape[0]
assert audio_logits.shape[0] == sample_inputs["audio_pos_info"]["position_ids"].shape[0]
@pytest.mark.sp
def test_teacache_strict_sp_keeps_cache_state_rank_local(
self,
minimax_h3_module,
sample_inputs,
monkeypatch,
):
"""Strict SP must not mix local TeaCache state with gathered block rows."""
from vllm_omni.diffusion.attention.ops import minimax_h3_modulation
from vllm_omni.diffusion.cache.teacache.config import TeaCacheConfig
from vllm_omni.diffusion.cache.teacache.hook import TeaCacheHook
from vllm_omni.diffusion.distributed import parallel_state
seq_len = sample_inputs["x"].shape[1]
local_len = seq_len // 2
hidden_size = minimax_h3_module.hidden_size
video_width = minimax_h3_module.arch.latents_dim * math.prod(minimax_h3_module.arch.patch_size)
audio_width = minimax_h3_module.arch.audio_latents_dim
captured_modulation_indices: list[torch.Tensor] = []
class _StrictSPPrepare(nn.Module):
def __init__(self):
super().__init__()
self.global_indices: list[torch.Tensor] = []
def forward(self, hidden, rope_table, combined_indices):
assert hidden.shape[0] == local_len
assert rope_table.shape[0] == local_len
self.global_indices.append(combined_indices.clone())
return hidden, rope_table, combined_indices.narrow(0, 0, local_len)
class _StrictSPGather(nn.Module):
def __init__(self):
super().__init__()
self.inputs: list[torch.Tensor] = []
self.outputs: list[torch.Tensor] = []
def forward(self, tensor):
self.inputs.append(tensor.clone())
if tensor.shape[-1] == hidden_size:
raise AssertionError("TeaCache must not gather hidden states in strict SP")
assert tensor.shape == (local_len, video_width + audio_width)
# Model the all-gather of compact logits from every SP rank.
# postprocess() indexes the resulting global layout with
# infer_out_pos/audio_pos, including rows owned by another rank.
gathered = tensor.repeat(seq_len // local_len, 1)
assert gathered.shape == (seq_len, video_width + audio_width)
self.outputs.append(gathered)
return gathered
class _StrictSPGroup:
def __init__(self):
super().__init__()
self.local_decisions: list[int] = []
self.ops = []
def all_reduce(self, decision, op):
self.local_decisions.append(int(decision.item()))
self.ops.append(op)
# Simulate another rank requiring the second step to execute
# the collective block path even though this rank would cache.
if len(self.local_decisions) == 2:
decision.fill_(1)
return decision
class _StrictSPBlock(nn.Module):
def __init__(self):
super().__init__()
self.block_indices: list[torch.Tensor] = []
def adaln_proj(self, t_emb):
del t_emb
zeros = torch.zeros(3, hidden_size, dtype=torch.float32)
return zeros, zeros
def norm1(self, hidden):
return hidden.clone()
def forward(self, hidden, *, combined_indices, rope_table, **kwargs):
del kwargs
assert hidden.shape[0] == local_len
assert rope_table.shape[0] == local_len
self.block_indices.append(combined_indices.clone())
return hidden + 1
class _StrictSPFinalLayer(nn.Module):
def forward(self, hidden, *, t_emb, inverse_indices):
del t_emb, inverse_indices
assert hidden.shape == (local_len, hidden_size)
return hidden[:, :video_width].float(), hidden[:, :audio_width].float()
local_prepare = _StrictSPPrepare()
gather = _StrictSPGather()
sp_group = _StrictSPGroup()
block = _StrictSPBlock()
minimax_h3_module.local_sp_prepare = local_prepare
minimax_h3_module.sp_gather = gather
minimax_h3_module.blocks = nn.ModuleList([block])
minimax_h3_module.final_layer = _StrictSPFinalLayer()
monkeypatch.setattr(minimax_h3_module, "_rope_local_span", lambda _seq_len: (0, local_len))
monkeypatch.setattr(
minimax_h3_module,
"_embed",
lambda **kwargs: (
torch.arange(local_len * hidden_size, dtype=torch.float32).reshape(local_len, hidden_size),
torch.zeros(3, hidden_size, dtype=torch.float32),
),
)
monkeypatch.setattr(
minimax_h3_module,
"prepare_rope_table",
lambda *args, **kwargs: torch.zeros(local_len, 6, dtype=torch.float32),
)
monkeypatch.setattr(parallel_state, "get_sp_group", lambda: sp_group)
original_indexed_scale_shift = minimax_h3_modulation.indexed_scale_shift_
def capture_indexed_scale_shift(hidden, shift, scale, indices):
captured_modulation_indices.append(indices.clone())
return original_indexed_scale_shift(hidden, shift, scale, indices)
monkeypatch.setattr(minimax_h3_modulation, "indexed_scale_shift_", capture_indexed_scale_shift)
local_img_pos = torch.tensor([0, 1])
local_audio_pos = torch.tensor([2, 3])
token_tags = torch.full((seq_len,), -1, dtype=torch.long)
token_tags[local_img_pos] = 0
token_tags[local_audio_pos] = 2
inverse_indices = torch.zeros(seq_len, dtype=torch.long)
inputs = {
**sample_inputs,
"inverse_indices": inverse_indices,
"token_tags": token_tags,
"update_mask": torch.ones(local_img_pos.numel(), dtype=torch.bool),
"prompt_embeds": torch.empty(0, 6),
"img_pos_info": {"position_ids": local_img_pos},
"audio_pos_info": {"position_ids": local_audio_pos},
"text_pos_info": {"position_ids": torch.empty(0, dtype=torch.long)},
"img_pos_for_infer_output_info": {"position_ids": local_img_pos},
"refiner_packed_seq_params": {
"cu_seqlens_q": torch.tensor([0, 0], dtype=torch.int32),
"max_seqlen_q": 0,
},
}
expected_local_indices = (inverse_indices * 3 + token_tags.clamp(min=0))[:local_len]
hook = TeaCacheHook(
TeaCacheConfig(
transformer_type="MiniMaxH3DiTModel",
rel_l1_thresh=0.3,
)
)
hook.initialize_hook(minimax_h3_module)
hook.new_forward(minimax_h3_module, **inputs)
hook.new_forward(minimax_h3_module, **inputs)
video_logits, audio_logits = hook.new_forward(minimax_h3_module, **inputs)
state = hook.state_manager.get_state()
assert state.previous_residual is not None
assert state.previous_residual.shape == (local_len, hidden_size)
assert len(captured_modulation_indices) == 3
assert all(torch.equal(indices, expected_local_indices) for indices in captured_modulation_indices)
assert sp_group.local_decisions == [1, 0, 0]
assert len(sp_group.ops) == 3
assert all(op == torch.distributed.ReduceOp.MAX for op in sp_group.ops)
assert len(local_prepare.global_indices) == 2
assert all(indices.shape == (seq_len,) for indices in local_prepare.global_indices)
assert len(block.block_indices) == 2
assert all(torch.equal(indices, expected_local_indices) for indices in block.block_indices)
assert len(gather.inputs) == 3
assert video_logits.shape == (local_img_pos.numel(), video_width)
assert audio_logits.shape == (local_audio_pos.numel(), audio_width)
@pytest.mark.cpu
def test_teacache_non_strict_sp_gathers_hidden_once(
self,
minimax_h3_module,
sample_inputs,
monkeypatch,
):
"""The non-strict SP cache path must not gather global hidden rows twice."""
from vllm_omni.diffusion.cache.teacache.config import TeaCacheConfig
from vllm_omni.diffusion.cache.teacache.hook import TeaCacheHook
class _Prepare(nn.Module):
def forward(self, hidden, rope_table, combined_indices):
return hidden, rope_table, combined_indices
class _Gather(nn.Module):
def __init__(self):
super().__init__()
self.inputs: list[torch.Tensor] = []
def forward(self, tensor):
self.inputs.append(tensor.clone())
return tensor
seq_len = sample_inputs["x"].shape[1]
gather = _Gather()
minimax_h3_module.sp_prepare = _Prepare()
minimax_h3_module.sp_gather = gather
monkeypatch.setattr(minimax_h3_module, "_rope_local_span", lambda _seq_len: (0, seq_len))
hook = TeaCacheHook(
TeaCacheConfig(
transformer_type="MiniMaxH3DiTModel",
rel_l1_thresh=0.3,
)
)
hook.initialize_hook(minimax_h3_module)
hook.new_forward(minimax_h3_module, **sample_inputs)
assert len(gather.inputs) == 1
assert gather.inputs[0].shape == (seq_len, minimax_h3_module.hidden_size)
def test_invalid_module_raises_error(self):
"""Test that invalid module without blocks raises ValueError."""
invalid_module = Mock()
@@ -9,7 +9,6 @@ import torch
from diffusers import DiffusionPipeline # pyright: ignore[reportPrivateImportUsage]
from PIL import Image
import vllm_omni.diffusion.data as diffusion_data
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniServer, OmniServerParams, OnlineOmniClient, dummy_messages_from_mix_data
from vllm_omni.diffusion.data import (
@@ -630,46 +629,6 @@ class TestPipelineArgumentsHandling:
with pytest.raises(ValueError):
pipeline.forward(DiffusionRequestBatch(requests=[problematic_request]))
interpolation_request = _make_request(
sampling_params=OmniDiffusionSamplingParams(
enable_frame_interpolation=True,
),
)
with pytest.raises(ValueError, match="Frame interpolation is not supported with the Diffusers backend"):
pipeline.forward(DiffusionRequestBatch(requests=[interpolation_request]))
def test_diffusers_config_preserves_checkpoint_class_for_metadata(self, mocker):
class MockWanImageToVideoPipeline:
pass
od_config = _make_od_config(model_class_name=None)
mocker.patch(
"vllm_omni.diffusion.utils.hf_utils.get_diffusion_model_index",
return_value={"_class_name": "WanImageToVideoPipeline"},
)
mocker.patch.object(
diffusion_data,
"diffusers",
SimpleNamespace(WanImageToVideoPipeline=MockWanImageToVideoPipeline),
)
mocker.patch.object(OmniDiffusionConfig, "update_multimodal_support")
od_config.enrich_config()
assert od_config.model_class_name == "WanImageToVideoPipeline"
assert od_config.diffusers_pipeline_cls is MockWanImageToVideoPipeline
def test_diffusers_config_falls_back_to_adapter_without_pipeline_index(self, mocker):
od_config = _make_od_config(model_class_name=None)
mocker.patch(
"vllm_omni.diffusion.utils.hf_utils.get_diffusion_model_index",
return_value=None,
)
od_config.enrich_config()
assert od_config.model_class_name == "DiffusersAdapterPipeline"
@pytest.mark.advanced_model
@hardware_test(res={"cuda": "L4"}, num_cards=1)
@@ -1,117 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
from dataclasses import dataclass
from types import SimpleNamespace
import pytest
import torch
from vllm_omni.diffusion.distributed import a2a_permute
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def test_jit_build_includes_cuda_headers_from_nvidia_wheels(tmp_path, monkeypatch) -> None:
cu13_include = tmp_path / "nvidia" / "cu13" / "include"
nccl_include = tmp_path / "nvidia" / "nccl" / "include"
nccl_lib = tmp_path / "nvidia" / "nccl" / "lib" / "libnccl.so.2"
cu13_include.mkdir(parents=True)
nccl_include.mkdir(parents=True)
nccl_lib.parent.mkdir(parents=True)
(cu13_include / "cusparse.h").touch()
(nccl_include / "nccl.h").touch()
nccl_lib.touch()
load_kwargs = {}
monkeypatch.setattr(a2a_permute.sysconfig, "get_paths", lambda: {"purelib": str(tmp_path)})
monkeypatch.setattr(
torch.utils.cpp_extension,
"load",
lambda **kwargs: load_kwargs.update(kwargs),
)
monkeypatch.setattr(a2a_permute.symm_mem, "set_backend", lambda _backend: None)
monkeypatch.setattr(a2a_permute, "_BUILT", False)
a2a_permute.ensure_a2a_permute_available()
assert set(load_kwargs["extra_include_paths"]) == {str(cu13_include), str(nccl_include)}
assert load_kwargs["extra_ldflags"] == [str(nccl_lib)]
@dataclass
class _FakeAllocation:
size: int
device: torch.device
class _FakeHandle:
rank = 0
def __init__(self, allocation: _FakeAllocation) -> None:
self.allocation = allocation
self.views: list[tuple[tuple[int, ...], torch.dtype]] = []
def get_buffer(self, rank, shape, dtype):
assert rank == self.rank
self.views.append((shape, dtype))
return self.allocation, shape, dtype
def test_workspace_reuses_peak_capacity_across_shapes(monkeypatch) -> None:
allocations: list[_FakeAllocation] = []
all_reduce_calls: list[object] = []
synchronize_calls: list[torch.device] = []
stream_id = [1]
a2a_permute._SYMM_WORKSPACES.clear()
monkeypatch.setattr(a2a_permute.torch.cuda, "is_current_stream_capturing", lambda: False)
monkeypatch.setattr(
a2a_permute.torch.cuda,
"current_stream",
lambda _device: SimpleNamespace(cuda_stream=stream_id[0]),
)
monkeypatch.setattr(a2a_permute, "_resolve_process_group", lambda _name: object())
monkeypatch.setattr(a2a_permute.torch, "ones", lambda *_args, **_kwargs: object())
monkeypatch.setattr(
a2a_permute.dist,
"all_reduce",
lambda _tensor, group: all_reduce_calls.append(group),
)
monkeypatch.setattr(
a2a_permute.torch.accelerator,
"synchronize",
lambda device: synchronize_calls.append(device),
)
def empty(size, *, dtype, device):
assert dtype == torch.uint8
allocation = _FakeAllocation(size=size, device=torch.device(device))
allocations.append(allocation)
return allocation
monkeypatch.setattr(a2a_permute.symm_mem, "empty", empty)
monkeypatch.setattr(a2a_permute.symm_mem, "rendezvous", lambda allocation, _group: _FakeHandle(allocation))
device = torch.device("cuda:0")
first = a2a_permute._get_symm_buffer((2, 3), torch.float16, device, "group")
smaller = a2a_permute._get_symm_buffer((1, 4), torch.float16, device, "group")
larger = a2a_permute._get_symm_buffer((4, 3), torch.float16, device, "group")
assert first[0].size == 12
assert smaller[0] is first[0]
assert larger[0].size == 24
assert [allocation.size for allocation in allocations] == [12, 24]
assert len(a2a_permute._SYMM_WORKSPACES) == 1
assert len(all_reduce_calls) == 1
assert synchronize_calls == [device, device]
stream_id[0] = 2
with pytest.raises(RuntimeError, match="single CUDA stream"):
a2a_permute._get_symm_buffer((1, 4), torch.float16, device, "group")
a2a_permute.clear_a2a_permute_workspaces()
assert not a2a_permute._SYMM_WORKSPACES
assert synchronize_calls == [device, device, device]
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for the Sequence Parallelism (SP) framework.
These tests verify the SP plan mechanism and hooks work correctly without
@@ -17,7 +17,6 @@ import pytest
import torch
import torch.nn as nn
from tests.helpers.mark import hardware_marks
from vllm_omni.diffusion.distributed.sp_plan import (
SequenceParallelInput,
SequenceParallelOutput,
@@ -420,6 +419,7 @@ class TestHookRegistration:
# =============================================================================
@pytest.mark.L4
class TestModelSpPlans:
"""Test that model _sp_plan definitions are valid.
@@ -427,8 +427,6 @@ class TestModelSpPlans:
May require GPU for model imports.
"""
pytestmark = hardware_marks(res={"cuda": "L4"})
def test_zimage_transformer_sp_plan(self):
"""Test ZImageTransformer2DModel _sp_plan structure.
-32
View File
@@ -1,32 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
import torch
import torch.nn.functional as F
from tests.helpers.mark import hardware_test
from vllm_omni.diffusion.layers.activation import SiluAndMul
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
@pytest.mark.cpu
def test_silu_and_mul_native_matches_packed_reference() -> None:
packed = torch.randn(257, 256, dtype=torch.float32)
gate, up = packed.chunk(2, dim=-1)
torch.testing.assert_close(SiluAndMul().forward_native(packed), F.silu(gate) * up)
@hardware_test(res={"npu": "A3"}, num_cards=1)
def test_silu_and_mul_npu_matches_packed_reference() -> None:
packed = torch.randn(257, 256, device="npu", dtype=torch.bfloat16)
gate, up = packed.chunk(2, dim=-1)
torch.testing.assert_close(
SiluAndMul()(packed),
F.silu(gate) * up,
atol=2e-2,
rtol=2e-2,
)
@@ -1,194 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import sys
import types
import pytest
import torch
import torch.nn.functional as F
from tests.helpers.mark import hardware_test
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion]
_HEAD_DIM = 128
_ROTARY_DIM = 96
_EPS = 1e-5
def _reference(
q: torch.Tensor,
k: torch.Tensor,
q_weight: torch.Tensor,
k_weight: torch.Tensor,
rope_table: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor]:
half = _ROTARY_DIM // 2
def apply(x: torch.Tensor, weight: torch.Tensor) -> torch.Tensor:
x = F.rms_norm(x, (_HEAD_DIM,), weight, _EPS)
cos = rope_table[..., :half].unsqueeze(1)
sin = rope_table[..., half:].unsqueeze(1)
return torch.cat(
(
x[..., :half] * cos - x[..., half:_ROTARY_DIM] * sin,
x[..., half:_ROTARY_DIM] * cos + x[..., :half] * sin,
x[..., _ROTARY_DIM:],
),
dim=-1,
)
return apply(q, q_weight), apply(k, k_weight)
def _inputs() -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
torch.manual_seed(7)
tokens, heads = 11, 4
q = torch.randn(tokens, heads, _HEAD_DIM, dtype=torch.bfloat16)
k = torch.randn(tokens, heads, _HEAD_DIM, dtype=torch.bfloat16)
q_weight = torch.randn(_HEAD_DIM, dtype=torch.bfloat16)
k_weight = torch.randn(_HEAD_DIM, dtype=torch.bfloat16)
freqs = torch.randn(tokens, _ROTARY_DIM // 2)
rope_table = torch.cat((torch.cos(freqs), torch.sin(freqs)), dim=-1).to(torch.bfloat16)
return q, k, q_weight, k_weight, rope_table
@pytest.mark.cpu
def test_npu_qk_norm_rope_uses_torch_npu_fused_primitives_without_mindiesd(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from vllm_omni.diffusion.layers import fused_qk_norm_rope as fused
calls: dict[str, int] = {"rms_norm": 0, "rotary_mul": 0}
def npu_rms_norm(x: torch.Tensor, gamma: torch.Tensor, epsilon: float):
calls["rms_norm"] += 1
return (F.rms_norm(x, (_HEAD_DIM,), gamma, epsilon),)
def npu_rotary_mul(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor):
calls["rotary_mul"] += 1
assert x.shape[-1] == _ROTARY_DIM
assert cos.shape == (1, x.shape[1], 1, _ROTARY_DIM)
assert sin.shape == cos.shape
half = _ROTARY_DIM // 2
return torch.cat(
(
x[..., :half] * cos[..., :half] - x[..., half:] * sin[..., :half],
x[..., half:] * cos[..., half:] + x[..., :half] * sin[..., half:],
),
dim=-1,
)
monkeypatch.setitem(
sys.modules,
"torch_npu",
types.SimpleNamespace(npu_rms_norm=npu_rms_norm, npu_rotary_mul=npu_rotary_mul),
)
monkeypatch.setattr(fused, "find_spec", lambda _: None)
q, k, q_weight, k_weight, rope_table = _inputs()
actual_q, actual_k = fused._npu_qk_norm_rope(
q,
k,
q_weight,
k_weight,
rope_table,
_EPS,
_ROTARY_DIM,
)
expected_q, expected_k = _reference(q, k, q_weight, k_weight, rope_table)
assert calls == {"rms_norm": 2, "rotary_mul": 2}
torch.testing.assert_close(actual_q, expected_q, atol=0, rtol=0)
torch.testing.assert_close(actual_k, expected_k, atol=0, rtol=0)
@pytest.mark.cpu
def test_npu_qk_norm_rope_uses_mindiesd_and_normalizes_packed_layout(
monkeypatch: pytest.MonkeyPatch,
) -> None:
from vllm_omni.diffusion.layers import fused_qk_norm_rope as fused
calls: dict[str, int] = {"rms_norm": 0, "mindiesd_rope": 0}
def npu_rms_norm(x: torch.Tensor, gamma: torch.Tensor, epsilon: float):
calls["rms_norm"] += 1
return (F.rms_norm(x, (_HEAD_DIM,), gamma, epsilon),)
def rotary_position_embedding(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor, **kwargs):
calls["mindiesd_rope"] += 1
assert x.dim() == 4
assert x.shape[0] == 1
assert x.shape[-1] == _ROTARY_DIM
assert cos.shape == (1, x.shape[1], 1, _ROTARY_DIM)
assert sin.shape == cos.shape
assert kwargs == {
"rotated_mode": "rotated_half",
"head_first": False,
"fused": True,
}
half = _ROTARY_DIM // 2
return torch.cat(
(
x[..., :half] * cos[..., :half] - x[..., half:] * sin[..., :half],
x[..., half:] * cos[..., half:] + x[..., :half] * sin[..., half:],
),
dim=-1,
)
monkeypatch.setitem(sys.modules, "torch_npu", types.SimpleNamespace(npu_rms_norm=npu_rms_norm))
monkeypatch.setitem(
sys.modules,
"mindiesd",
types.SimpleNamespace(rotary_position_embedding=rotary_position_embedding),
)
monkeypatch.setattr(fused, "find_spec", lambda _: object())
q, k, q_weight, k_weight, rope_table = _inputs()
actual_q, actual_k = fused._npu_qk_norm_rope(
q,
k,
q_weight,
k_weight,
rope_table,
_EPS,
_ROTARY_DIM,
)
expected_q, expected_k = _reference(q, k, q_weight, k_weight, rope_table)
assert calls == {"rms_norm": 2, "mindiesd_rope": 2}
torch.testing.assert_close(actual_q, expected_q, atol=0, rtol=0)
torch.testing.assert_close(actual_k, expected_k, atol=0, rtol=0)
@hardware_test(res={"npu": "A3"}, num_cards=1)
def test_fused_qk_norm_rope_npu_matches_reference(monkeypatch: pytest.MonkeyPatch) -> None:
"""Exercise the public Ascend dispatch with real torch_npu/MindIE operators."""
from vllm_omni.diffusion.layers import fused_qk_norm_rope as fused
q, k, q_weight, k_weight, rope_table = (tensor.to("npu") for tensor in _inputs())
calls = 0
original_npu_impl = fused._npu_qk_norm_rope
def capture_npu_impl(*args, **kwargs):
nonlocal calls
calls += 1
return original_npu_impl(*args, **kwargs)
monkeypatch.setattr(fused, "_npu_qk_norm_rope", capture_npu_impl)
actual_q, actual_k = fused.fused_qk_norm_rope(
q,
k,
q_weight,
k_weight,
rope_table,
_EPS,
head_dim=_HEAD_DIM,
rotary_dim=_ROTARY_DIM,
)
expected_q, expected_k = _reference(q, k, q_weight, k_weight, rope_table)
assert calls == 1
torch.testing.assert_close(actual_q, expected_q, atol=2e-2, rtol=2e-2)
torch.testing.assert_close(actual_k, expected_k, atol=2e-2, rtol=2e-2)
@@ -273,15 +273,12 @@ def test_required_hwr_miss_fails_before_ordinary_loading_or_publication(
assert loader.take_host_weight_plan() is None
def _make_dlo_online_quant_config(dp_size: int = 2) -> OmniDiffusionConfig:
def _make_dlo_online_quant_config() -> OmniDiffusionConfig:
return OmniDiffusionConfig(
model="",
dtype=torch.float32,
quantization_config="fp8",
parallel_config=DiffusionParallelConfig(
data_parallel_size=dp_size,
sequence_parallel_size=1,
),
parallel_config=DiffusionParallelConfig(data_parallel_size=2),
enable_distributed_layerwise_offload=True,
dlo_use_allgather=True,
)
@@ -827,93 +824,6 @@ def test_dlo_allgather_online_fp8_uses_ordinary_loader(monkeypatch):
assert loader.take_host_weight_plan() is None
def test_dlo_allgather_online_int8_uses_ordinary_loader(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
from vllm_omni.quantization.int8_config import NPUInt8OnlineLinearMethod
od_config = _make_dlo_online_quant_config()
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
model.transformer.quant_method = object.__new__(NPUInt8OnlineLinearMethod)
model.transformer.quant_method.uses_meta_device = True
calls: list[object] = []
allowlist_models: list[nn.Module] = []
original_allowlist_check = loader._unsupported_dlo_allgather_online_quant_methods
def check_allowlist(candidate: nn.Module) -> tuple[str, ...]:
allowlist_models.append(candidate)
return original_allowlist_check(candidate)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
monkeypatch.setattr(loader, "_unsupported_dlo_allgather_online_quant_methods", check_allowlist)
loader._request_offload_after_quant = lambda _model: 1 # type: ignore[method-assign]
loader.load_weights = ( # type: ignore[method-assign]
lambda _model, *, stream_online_quant_to_cpu=False: calls.append(("load", stream_online_quant_to_cpu))
)
loader._process_weights_after_loading = lambda *_args: calls.append("process") # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(
None,
"online quantization requires the ordinary loader",
),
)
assert loader.load_model(load_device="cpu", device=torch.device("cpu")) is model
assert allowlist_models == [model]
assert calls == [("load", True), "process"]
assert loader.take_host_weight_plan() is None
def test_dlo_online_quant_group_size_one_skips_allgather_gate(monkeypatch):
"""A DLO group of one runs no weight collective, so an otherwise
unvalidated online method must not be rejected by the AllGather gate."""
class UnsupportedOnlineMethod:
uses_meta_device = True
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
od_config = _make_dlo_online_quant_config(dp_size=1)
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
model.transformer.quant_method = UnsupportedOnlineMethod()
calls: list[object] = []
allowlist_models: list[nn.Module] = []
original_allowlist_check = loader._unsupported_dlo_allgather_online_quant_methods
def check_allowlist(candidate: nn.Module) -> tuple[str, ...]:
allowlist_models.append(candidate)
return original_allowlist_check(candidate)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
monkeypatch.setattr(loader, "_unsupported_dlo_allgather_online_quant_methods", check_allowlist)
loader._request_offload_after_quant = lambda _model: 1 # type: ignore[method-assign]
loader.load_weights = ( # type: ignore[method-assign]
lambda _model, *, stream_online_quant_to_cpu=False: calls.append(("load", stream_online_quant_to_cpu))
)
loader._process_weights_after_loading = lambda *_args: calls.append("process") # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(
None,
"online quantization requires the ordinary loader",
),
)
assert loader.load_model(load_device="cpu", device=torch.device("cpu")) is model
assert allowlist_models == []
assert calls == [("load", True), "process"]
def test_dlo_allgather_rejects_unvalidated_online_quant_method(monkeypatch):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
@@ -937,54 +847,10 @@ def test_dlo_allgather_rejects_unvalidated_online_quant_method(monkeypatch):
),
)
with pytest.raises(ValueError, match="per-tensor FP8, INT8, and MXFP8 linears"):
with pytest.raises(ValueError, match="per-tensor FP8 linears"):
loader.load_model(load_device="cpu")
def test_dlo_allgather_online_mxfp8_uses_ordinary_loader(monkeypatch):
mxfp8_config = pytest.importorskip("vllm_omni.quantization.mxfp8_config")
NPUMxfp8OnlineLinearMethod = mxfp8_config.NPUMxfp8OnlineLinearMethod
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
od_config = _make_dlo_online_quant_config()
loader = DiffusersPipelineLoader(LoadConfig(), od_config)
model = nn.Module()
model.transformer = nn.Linear(2, 2, bias=False)
model.transformer.quant_method = object.__new__(NPUMxfp8OnlineLinearMethod)
model.transformer.quant_method.uses_meta_device = True
calls: list[object] = []
allowlist_models: list[nn.Module] = []
original_allowlist_check = loader._unsupported_dlo_allgather_online_quant_methods
def check_allowlist(candidate: nn.Module) -> tuple[str, ...]:
allowlist_models.append(candidate)
return original_allowlist_check(candidate)
loader._init_from_load_format = lambda *_args, **_kwargs: model # type: ignore[method-assign]
monkeypatch.setattr(loader, "_unsupported_dlo_allgather_online_quant_methods", check_allowlist)
loader._request_offload_after_quant = lambda _model: 1 # type: ignore[method-assign]
loader.load_weights = ( # type: ignore[method-assign]
lambda _model, *, stream_online_quant_to_cpu=False: calls.append(("load", stream_online_quant_to_cpu))
)
loader._process_weights_after_loading = lambda *_args: calls.append("process") # type: ignore[method-assign]
loader._apply_skip_softmax_calibration = lambda _model: None # type: ignore[method-assign]
monkeypatch.setattr(
loader_mod,
"build_checkpoint_mmap_plan",
lambda *_args, **_kwargs: HostWeightPlanResult(
None,
"online quantization requires the ordinary loader",
),
)
assert loader.load_model(load_device="cpu", device=torch.device("cpu")) is model
assert allowlist_models == [model]
assert calls == [("load", True), "process"]
assert loader.take_host_weight_plan() is None
def test_hsdp_processes_quantized_weights_before_sharding(mocker):
import vllm_omni.diffusion.model_loader.diffusers_loader as loader_mod
from vllm_omni.diffusion.offloader.module_collector import PipelineModules
@@ -223,8 +223,6 @@ def test_h3_turbo_allows_distributed_layerwise_offload(monkeypatch):
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
pipeline.od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
@@ -323,8 +321,6 @@ def test_only_an_active_recognized_turbo_adapter_restricts_ref2va(monkeypatch):
pipeline.partition = "combined"
pipeline.supported_tasks = frozenset({"t2va", "fl2va", "ref2va"})
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
request = _request("generic-peft")
def load():
@@ -1,860 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
from types import SimpleNamespace
import pytest
import torch
from safetensors.torch import save_file
from vllm.lora.lora_weights import PackedLoRALayerWeights
from vllm_omni.diffusion.lora.manager import DiffusionLoRAManager
from vllm_omni.diffusion.models.minimax_h3.npu import lora as lora_module
from vllm_omni.diffusion.models.minimax_h3.npu.lora import load_minimax_h3_native_lora
from vllm_omni.diffusion.sched.sigma_schedule import DMD2SigmaSchedule
from vllm_omni.errors import OmniClientError
from vllm_omni.lora.request import LoRARequest
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
_TINY_HIDDEN = 2
_TINY_FFN = 2
_TINY_TIME = 2
_TINY_BLOCK_ADALN = 4
_TINY_FINAL_ADALN = 3
_TINY_RANK = 4
_TINY_NUM_QUERY_GROUPS = 2
_TINY_HEADS_PER_GROUP = 1
_TINY_HEAD_DIM = 1
_TINY_QKV_SLICE = _TINY_NUM_QUERY_GROUPS * _TINY_HEADS_PER_GROUP * _TINY_HEAD_DIM
# Grouped qkv stores (heads_per_group + 2) * head_dim rows per query group, so
# the attention inner size follows from the head layout and cannot be picked
# independently: 3 * inner must equal the grouped row count.
_TINY_INNER = _TINY_QKV_SLICE
@pytest.fixture(autouse=True)
def _use_tiny_native_dimensions(monkeypatch):
monkeypatch.setattr(lora_module, "_NATIVE_RANK", _TINY_RANK)
monkeypatch.setattr(lora_module, "_NATIVE_ALPHA", _TINY_RANK)
monkeypatch.setattr(lora_module, "_NATIVE_HIDDEN_SIZE", _TINY_HIDDEN)
monkeypatch.setattr(lora_module, "_NATIVE_ATTENTION_INNER_SIZE", _TINY_INNER)
monkeypatch.setattr(lora_module, "_NATIVE_FFN_HIDDEN_SIZE", _TINY_FFN)
monkeypatch.setattr(lora_module, "_NATIVE_TIME_EMBED_DIM", _TINY_TIME)
monkeypatch.setattr(lora_module, "_NATIVE_BLOCK_ADALN_OUT", _TINY_BLOCK_ADALN)
monkeypatch.setattr(lora_module, "_NATIVE_FINAL_ADALN_OUT", _TINY_FINAL_ADALN)
monkeypatch.setattr(lora_module, "_NATIVE_NUM_QUERY_GROUPS", _TINY_NUM_QUERY_GROUPS)
monkeypatch.setattr(lora_module, "_NATIVE_HEADS_PER_GROUP", _TINY_HEADS_PER_GROUP)
monkeypatch.setattr(lora_module, "_NATIVE_HEAD_DIM", _TINY_HEAD_DIM)
monkeypatch.setattr(lora_module, "_NATIVE_QKV_SLICE", _TINY_QKV_SLICE)
monkeypatch.setattr(
lora_module,
"_NATIVE_TARGET_DIMS",
{
"attn.qkv_proj": (_TINY_HIDDEN, 3 * _TINY_INNER),
"attn.out_proj": (_TINY_INNER, _TINY_HIDDEN),
"mlp.fc1": (_TINY_HIDDEN, 2 * _TINY_FFN),
"mlp.fc2": (_TINY_FFN, _TINY_HIDDEN),
"adaln_proj.linear": (_TINY_TIME, _TINY_BLOCK_ADALN),
},
)
monkeypatch.setattr(lora_module, "_NATIVE_FINAL_ADALN_DIMS", (_TINY_TIME, _TINY_FINAL_ADALN))
monkeypatch.setattr(
lora_module,
"_NATIVE_EXPECTED_TARGETS",
frozenset(
[
*(
f"blocks.{block_index}.{suffix}"
for block_index in range(50)
for suffix in lora_module._NATIVE_TARGET_SUFFIXES
)
]
+ [
*(
f"token_refiner.blocks.{block_index}.{suffix}"
for block_index in range(2)
for suffix in lora_module._NATIVE_TOKEN_REFINER_SUFFIXES
)
]
+ ["final_layer.adaln_proj.linear"]
),
)
def _request(path) -> LoRARequest:
return LoRARequest(
lora_name="flashgen",
lora_int_id=7,
lora_path=str(path),
)
def _write_tiny_native(
path,
*,
omit_target: str | None = None,
shape_overrides: dict[str, tuple[int, int]] | None = None,
metadata: dict[str, str] | None = None,
) -> None:
rank = _TINY_RANK
tensors = {}
overrides = shape_overrides or {}
for target in sorted(lora_module._NATIVE_EXPECTED_TARGETS):
if target == omit_target:
continue
input_dim, output_dim = lora_module._native_target_dims(target)
raw_target = f"transformer.{target}"
a_name = f"{raw_target}.lora_A.default.weight"
b_name = f"{raw_target}.lora_B.default.weight"
tensors[a_name] = torch.ones(overrides.get(a_name, (rank, input_dim)))
if target.endswith(".attn.qkv_proj"):
grouped_rows = []
for group in range(_TINY_NUM_QUERY_GROUPS):
grouped_rows.extend(
[
torch.full((1, rank), float(group * 10 + 1)),
torch.full((1, rank), float(group * 10 + 2)),
torch.full((1, rank), float(group * 10 + 3)),
]
)
tensors[b_name] = torch.cat(grouped_rows, dim=0)
elif target.endswith(".mlp.fc1"):
tensors[b_name] = torch.cat(
(
torch.full((output_dim // 2, rank), 2.0),
torch.full((output_dim // 2, rank), 1.0),
),
dim=0,
)
else:
tensors[b_name] = torch.ones(overrides.get(b_name, (output_dim, rank)))
save_file(
tensors,
str(path),
metadata=metadata
or {
"format": "pt",
"key_format": "minimax-h3-native",
"qkv_layout": "grouped",
"lora_rank": str(rank),
"lora_alpha": str(rank),
"base_schedule": "1.0,0.7,0.4,0.15,0.0",
"tasks": "t2va",
},
)
def test_h3_native_loads_and_packs_qkv_and_fc1(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
loaded = load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
assert loaded is not None
lora_model, peft_helper, schedule = loaded
assert peft_helper.r == _TINY_RANK
assert schedule.num_inference_steps == 4
assert len(lora_model.loras) == 259
assert "blocks.0.adaln_proj.linear" in lora_model.loras
assert "final_layer.adaln_proj.linear" in lora_model.loras
# The writer tags group g with q=g*10+1, k=g*10+2, v=g*10+3, so a correct
# grouped -> q/k/v permutation yields slices that are uniform per role.
qkv = lora_model.get_lora("blocks.0.attn.qkv_proj")
assert isinstance(qkv, PackedLoRALayerWeights)
torch.testing.assert_close(qkv.lora_b[0], torch.tensor([[1.0] * _TINY_RANK, [11.0] * _TINY_RANK]))
torch.testing.assert_close(qkv.lora_b[1], torch.tensor([[2.0] * _TINY_RANK, [12.0] * _TINY_RANK]))
torch.testing.assert_close(qkv.lora_b[2], torch.tensor([[3.0] * _TINY_RANK, [13.0] * _TINY_RANK]))
fc1 = lora_model.get_lora("blocks.0.mlp.fc1")
assert isinstance(fc1, PackedLoRALayerWeights)
torch.testing.assert_close(fc1.lora_b[0], torch.full((_TINY_FFN, _TINY_RANK), 2.0))
torch.testing.assert_close(fc1.lora_b[1], torch.full((_TINY_FFN, _TINY_RANK), 1.0))
def test_h3_native_rejects_bad_metadata_and_ref2va(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path, metadata={"key_format": "minimax-h3-native", "qkv_layout": "runtime"})
with pytest.raises(ValueError, match="qkv_layout='grouped'"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
# The exact-filename guard runs before the partition check, so the artifact
# has to keep the published name for this to reach the Ref2VA rejection.
valid_dir = tmp_path / "valid"
valid_dir.mkdir()
valid = valid_dir / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(valid)
with pytest.raises(ValueError, match="supports T2VA only"):
load_minimax_h3_native_lora(
partition="ref2va",
lora_request=_request(valid),
lora_path=valid,
dtype=torch.float32,
)
renamed = tmp_path / "renamed.safetensors"
_write_tiny_native(renamed)
with pytest.raises(ValueError, match="supports only"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(renamed),
lora_path=renamed,
dtype=torch.float32,
)
def test_h3_native_rejects_truncated_artifact(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path, omit_target="blocks.49.mlp.fc2")
with pytest.raises(ValueError, match="target set does not match"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
def test_h3_native_rejects_schedule_with_wrong_interval_count(tmp_path):
"""A mislabeled schedule must fail closed instead of silently changing steps."""
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(
path,
metadata={
"format": "pt",
"key_format": "minimax-h3-native",
"qkv_layout": "grouped",
"lora_rank": str(_TINY_RANK),
"lora_alpha": str(_TINY_RANK),
"base_schedule": "1.0,0.5,0.0",
"tasks": "t2va",
},
)
with pytest.raises(ValueError, match="4-interval base_schedule"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
@pytest.mark.parametrize(
("raw_schedule", "message"),
[
(None, "requires safetensors metadata base_schedule"),
("", "must not be empty"),
(",,", "must not be empty"),
("1.0,oops,0.15,0.0", "is malformed"),
("0.9,0.7,0.4,0.15,0.0", "is malformed"),
],
)
def test_h3_native_rejects_malformed_schedule_metadata(tmp_path, raw_schedule, message):
"""safetensors metadata is string-only, so the loader owns the parse contract."""
metadata = {
"format": "pt",
"key_format": "minimax-h3-native",
"qkv_layout": "grouped",
"lora_rank": str(_TINY_RANK),
"lora_alpha": str(_TINY_RANK),
"tasks": "t2va",
}
if raw_schedule is not None:
metadata["base_schedule"] = raw_schedule
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path, metadata=metadata)
with pytest.raises(ValueError, match=message):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
def test_h3_native_declared_file_fails_closed_on_invalid_metadata(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path, metadata={"key_format": "other"})
with pytest.raises(ValueError, match="requires safetensors metadata"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
def test_pipeline_native_schedule_and_task_validation(monkeypatch):
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline.supported_tasks = frozenset({"t2va", "fl2va", "ref2va"})
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = {7}
pipeline._lora_sigma_schedules = {7: DMD2SigmaSchedule.from_positions([1.0, 0.7, 0.4, 0.15, 0.0])}
pipeline._base_schedule_by_partition = {"fl2va": None}
sampling = SimpleNamespace(
lora_request=LoRARequest("flashgen", 7, "/tmp/native.safetensors"),
lora_scale=1.0,
num_inference_steps=4,
extra_args={},
)
assert pipeline._sigma_schedule_for_request(sampling, "t2va").num_inference_steps == 4
with pytest.raises(OmniClientError, match="num_inference_steps must be 4"):
pipeline._validate_native_sampling(
SimpleNamespace(lora_request=sampling.lora_request, num_inference_steps=5),
task="t2va",
)
with pytest.raises(OmniClientError, match="supports T2VA requests only"):
pipeline._resolve_task(
"fl2va",
{},
has_turbo_lora=False,
has_native_lora=True,
)
pipeline._base_schedule_by_partition = {"fl2va": DMD2SigmaSchedule.from_positions([1.0, 0.5, 0.0])}
with pytest.raises(OmniClientError, match="already pins base_schedule"):
pipeline._sigma_schedule_for_request(sampling, "t2va")
def test_pipeline_native_step_count_follows_adapter_schedule():
"""The accepted step count is the adapter's interval count, never a literal."""
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline._native_lora_adapter_ids = {7}
pipeline._lora_sigma_schedules = {7: DMD2SigmaSchedule.from_positions([1.0, 0.5, 0.0])}
request = LoRARequest("flashgen", 7, "/tmp/native.safetensors")
pipeline._validate_native_sampling(
SimpleNamespace(lora_request=request, num_inference_steps=2),
task="t2va",
)
with pytest.raises(OmniClientError, match="num_inference_steps must be 2 or omitted, not 3"):
pipeline._validate_native_sampling(
SimpleNamespace(lora_request=request, num_inference_steps=3),
task="t2va",
)
with pytest.raises(OmniClientError, match="requires num_inference_steps=2"):
pipeline._validate_native_sampling(
SimpleNamespace(lora_request=request, num_inference_steps=4),
task="t2va",
)
@pytest.mark.parametrize("step_execution", [False, True])
def test_pipeline_native_omitted_step_count_follows_execution_mode(step_execution):
"""Only request mode can resolve an omitted count from the adapter schedule.
``StepScheduler`` reads ``num_inference_steps`` off the request at admission,
before this pipeline sees it, so under step execution an omitted value can
only disagree with the denoise loop. The advertised contract has to match.
"""
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline._native_lora_adapter_ids = {7}
pipeline._lora_sigma_schedules = {7: DMD2SigmaSchedule.from_positions([1.0, 0.7, 0.4, 0.15, 0.0])}
pipeline.od_config = SimpleNamespace(step_execution=step_execution)
sampling = SimpleNamespace(
lora_request=LoRARequest("flashgen", 7, "/tmp/native.safetensors"),
num_inference_steps=None,
)
if not step_execution:
# Request mode defaults to the adapter's own interval count.
pipeline._validate_native_sampling(sampling, task="t2va")
return
with pytest.raises(OmniClientError, match="explicit num_inference_steps=4 under step execution"):
pipeline._validate_native_sampling(sampling, task="t2va")
# The step-mode message must not offer omission it cannot accept.
with pytest.raises(OmniClientError, match=r"must be 4, not 5$"):
pipeline._validate_native_sampling(
SimpleNamespace(lora_request=sampling.lora_request, num_inference_steps=5),
task="t2va",
)
def test_pipeline_replaces_native_classification_after_reload(monkeypatch, tmp_path):
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as pipeline_module
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
request = _request(path)
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline.od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
enable_distributed_layerwise_offload=False,
)
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
loaded = pipeline._load_diffusion_lora_adapter(
lora_request=request,
lora_path=path,
dtype=torch.float32,
)
assert loaded is not None
assert request.lora_int_id in pipeline._native_lora_adapter_ids
assert request.lora_int_id in pipeline._lora_sigma_schedules
monkeypatch.setattr(pipeline_module, "load_minimax_h3_turbo_lora", lambda **_: None)
monkeypatch.setattr(pipeline_module, "load_minimax_h3_native_lora", lambda **_: None)
assert pipeline._load_diffusion_lora_adapter(lora_request=request, lora_path=path, dtype=torch.float32) is None
assert request.lora_int_id not in pipeline._native_lora_adapter_ids
assert request.lora_int_id not in pipeline._lora_sigma_schedules
@pytest.mark.parametrize(
"offload_mode",
[
"model-level CPU offload (--enable-cpu-offload)",
"layerwise offload (--enable-layerwise-offload)",
],
)
def test_h3_native_rejects_offload_modes(tmp_path, offload_mode):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
with pytest.raises(ValueError, match="does not support"):
load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
unsupported_offload_mode=offload_mode,
)
def test_h3_native_allows_distributed_layerwise_offload(monkeypatch):
"""DLO keeps LoRA A/B buffers resident, so native must not fail closed."""
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as pipeline_module
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline.od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
enable_distributed_layerwise_offload=True,
)
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
captured: dict[str, object] = {}
schedule = DMD2SigmaSchedule.from_positions([1.0, 0.7, 0.4, 0.15, 0.0])
def load_native(**kwargs):
captured.update(kwargs)
return object(), object(), schedule
monkeypatch.setattr(pipeline_module, "load_minimax_h3_turbo_lora", lambda **_: None)
monkeypatch.setattr(pipeline_module, "load_minimax_h3_native_lora", load_native)
request = _request("flashgen")
loaded = pipeline._load_diffusion_lora_adapter(
lora_request=request,
lora_path="flashgen",
dtype=torch.bfloat16,
)
assert loaded is not None
assert captured["unsupported_offload_mode"] is None
assert request.lora_int_id in pipeline._native_lora_adapter_ids
assert pipeline._lora_sigma_schedules[request.lora_int_id] is schedule
def test_h3_native_qkv_reorder_matches_base_loader_contract():
from vllm_omni.diffusion.models.minimax_h3.minimax_h3_transformer import _reorder_grouped_qkv_to_qkv
num_query_groups = 56
heads_per_group = 1
head_dim = 128
grouped = torch.arange(num_query_groups * (heads_per_group + 2) * head_dim, dtype=torch.float32)
grouped = grouped.reshape(num_query_groups, (heads_per_group + 2) * head_dim)
reordered = _reorder_grouped_qkv_to_qkv(
grouped.reshape(-1, 1),
num_query_groups=num_query_groups,
heads_per_group=heads_per_group,
head_dim=head_dim,
).reshape(-1)
q_size = num_query_groups * heads_per_group * head_dim
k_size = num_query_groups * head_dim
q, k, v = torch.split(reordered, [q_size, k_size, k_size])
assert q.reshape(num_query_groups, head_dim)[0, 0] == grouped[0, 0]
assert k.reshape(num_query_groups, head_dim)[0, 0] == grouped[0, head_dim]
assert v.reshape(num_query_groups, head_dim)[0, 0] == grouped[0, 2 * head_dim]
def test_native_target_pattern_matches_component_qualified_names():
"""The manager matches "<component>.<module>", so the pattern needs the prefix."""
import regex as re
pattern = lora_module._NATIVE_TARGET_PATTERN
matched = [
target
for target in lora_module._NATIVE_EXPECTED_TARGETS
if re.search(pattern, f"transformer.{target}") is not None
]
assert len(matched) == 259
assert re.search(pattern, "transformer.blocks.0.attn.q_proj") is None
assert re.search(pattern, "transformer.token_refiner.blocks.0.adaln_proj.linear") is None
def test_replace_layers_binds_every_native_target(monkeypatch):
"""Regression for bound=0/259: layer replacement must reach all 259 modules."""
from vllm.lora.layers import BaseLayerWithLoRA
from vllm.lora.peft_helper import PEFTHelper
from vllm_omni.diffusion.lora import manager as manager_module
class _StubLoRALayer(BaseLayerWithLoRA):
def __init__(self, base_layer):
super().__init__()
self.base_layer = base_layer
def _linear() -> torch.nn.Module:
return torch.nn.Linear(_TINY_HIDDEN, _TINY_HIDDEN)
def _block(*, with_adaln: bool) -> torch.nn.Module:
block = torch.nn.Module()
block.attn = torch.nn.Module()
block.attn.qkv_proj = _linear()
block.attn.out_proj = _linear()
block.mlp = torch.nn.Module()
block.mlp.fc1 = _linear()
block.mlp.fc2 = _linear()
if with_adaln:
block.adaln_proj = torch.nn.Module()
block.adaln_proj.linear = _linear()
return block
transformer = torch.nn.Module()
transformer.blocks = torch.nn.ModuleList([_block(with_adaln=True) for _ in range(50)])
transformer.token_refiner = torch.nn.Module()
transformer.token_refiner.blocks = torch.nn.ModuleList([_block(with_adaln=False) for _ in range(2)])
transformer.final_layer = torch.nn.Module()
transformer.final_layer.adaln_proj = torch.nn.Module()
transformer.final_layer.adaln_proj.linear = _linear()
pipeline = torch.nn.Module()
pipeline.transformer = transformer
monkeypatch.setattr(manager_module, "from_layer_diffusion", lambda *, layer, **_: _StubLoRALayer(layer))
manager = object.__new__(DiffusionLoRAManager)
manager.pipeline = pipeline
manager.dtype = torch.float32
manager.max_cached_adapters = 1
manager._lora_modules = {}
manager._max_lora_rank = 64
manager._resident_lora_device = None
peft_helper = PEFTHelper.from_dict(
{
"r": _TINY_RANK,
"lora_alpha": _TINY_RANK,
"target_modules": lora_module._NATIVE_TARGET_PATTERN,
}
)
manager._replace_layers_with_lora(peft_helper)
assert set(manager._lora_modules) == {f"transformer.{target}" for target in lora_module._NATIVE_EXPECTED_TARGETS}
def test_legacy_manager_uses_native_loader(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
class _Pipeline:
def _load_diffusion_lora_adapter(self, **kwargs):
from vllm_omni.diffusion.models.minimax_h3.pipeline_minimax_h3 import (
MiniMaxH3Pipeline,
)
pipeline = object.__new__(MiniMaxH3Pipeline)
pipeline.partition = "fl2va"
pipeline.od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
enable_distributed_layerwise_offload=False,
)
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
return pipeline._load_diffusion_lora_adapter(**kwargs)
manager = object.__new__(DiffusionLoRAManager)
manager.pipeline = _Pipeline()
manager.dtype = torch.float32
manager._expected_lora_modules = {"qkv_proj", "fc1", "adaln_proj.linear"}
lora_model, peft_helper = manager._load_adapter(_request(path))
assert lora_model.id == 7
assert peft_helper.lora_alpha == _TINY_RANK
assert len(lora_model.loras) == 259
def test_pipeline_schedule_inactive_when_scale_zero():
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = {7}
pipeline._lora_sigma_schedules = {7: DMD2SigmaSchedule.from_positions([1.0, 0.7, 0.4, 0.15, 0.0])}
pipeline._base_schedule_by_partition = {"fl2va": None}
sampling = SimpleNamespace(
lora_request=LoRARequest("flashgen", 7, "/tmp/native.safetensors"),
lora_scale=0.0,
num_inference_steps=4,
)
assert pipeline._sigma_schedule_for_request(sampling, "t2va") is None
assert not pipeline._has_active_native_lora(sampling)
def test_pipeline_schedule_falls_back_after_eviction(monkeypatch, tmp_path):
from vllm_omni.diffusion.models.minimax_h3 import MiniMaxH3Pipeline
from vllm_omni.diffusion.models.minimax_h3 import pipeline_minimax_h3 as pipeline_module
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
request = _request(path)
pipeline = object.__new__(MiniMaxH3Pipeline)
torch.nn.Module.__init__(pipeline)
pipeline.partition = "fl2va"
pipeline.od_config = SimpleNamespace(
enable_cpu_offload=False,
enable_layerwise_offload=False,
enable_distributed_layerwise_offload=False,
)
pipeline._turbo_lora_adapter_ids = set()
pipeline._native_lora_adapter_ids = set()
pipeline._lora_sigma_schedules = {}
pipeline._base_schedule_by_partition = {"fl2va": None}
loaded = pipeline._load_diffusion_lora_adapter(
lora_request=request,
lora_path=path,
dtype=torch.float32,
)
assert loaded is not None
assert request.lora_int_id in pipeline._lora_sigma_schedules
monkeypatch.setattr(pipeline_module, "load_minimax_h3_turbo_lora", lambda **_: None)
monkeypatch.setattr(pipeline_module, "load_minimax_h3_native_lora", lambda **_: None)
assert pipeline._load_diffusion_lora_adapter(lora_request=request, lora_path=path, dtype=torch.float32) is None
assert request.lora_int_id not in pipeline._lora_sigma_schedules
sampling = SimpleNamespace(
lora_request=request,
lora_scale=1.0,
num_inference_steps=4,
)
assert pipeline._sigma_schedule_for_request(sampling, "t2va") is None
def test_native_packed_qkv_slices_are_tp2_divisible():
assert lora_module._NATIVE_QKV_SLICE % 2 == 0
assert lora_module._NATIVE_FFN_HIDDEN_SIZE % 2 == 0
def test_lora_manager_activates_native_packed_qkv(tmp_path):
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
loaded = load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
assert loaded is not None
lora_model, _, _ = loaded
packed = lora_model.get_lora("blocks.0.attn.qkv_proj")
assert isinstance(packed, PackedLoRALayerWeights)
assert len(packed.lora_b) == 3
assert all(b.shape[0] == _TINY_QKV_SLICE for b in packed.lora_b)
class _DummyLoRALayer:
def __init__(self):
self.n_slices = 3
self.output_slices = (_TINY_QKV_SLICE, _TINY_QKV_SLICE, _TINY_QKV_SLICE)
self.set_calls: list[tuple[list, list]] = []
self.reset_calls = 0
def reset_lora(self, index: int):
self.reset_calls += 1
def set_lora(self, index: int, lora_a, lora_b):
self.set_calls.append((lora_a, lora_b))
layer = _DummyLoRALayer()
manager = object.__new__(DiffusionLoRAManager)
manager.pipeline = torch.nn.Module()
manager._lora_modules = {"blocks.0.attn.qkv_proj": layer}
manager._registered_adapters = {
lora_model.id: lora_model,
}
manager._active_adapter_id = None
manager._adapter_scales = {}
manager._activate_adapter(lora_model.id, scale=0.5)
assert manager._active_adapter_id == lora_model.id
assert len(layer.set_calls) == 1
lora_a_list, lora_b_list = layer.set_calls[0]
assert len(lora_a_list) == 3
assert len(lora_b_list) == 3
torch.testing.assert_close(lora_b_list[0], packed.lora_b[0] * 0.5)
torch.testing.assert_close(lora_b_list[1], packed.lora_b[1] * 0.5)
torch.testing.assert_close(lora_b_list[2], packed.lora_b[2] * 0.5)
def test_lora_manager_tp2_splits_native_packed_qkv_per_rank():
"""Each Q/K/V slice must be divisible by TP2 so rank-local layers can shard rows."""
tp_size = 2
slice_rows = lora_module._NATIVE_QKV_SLICE
assert slice_rows % tp_size == 0
local_rows = slice_rows // tp_size
class _TpQkvLoRALayer:
def __init__(self, tp_rank: int):
self.tp_rank = tp_rank
self.n_slices = 3
self.output_slices = (local_rows, local_rows, local_rows)
self.set_calls: list[tuple[list, list]] = []
def reset_lora(self, index: int):
return
def set_lora(self, index: int, lora_a, lora_b):
self.set_calls.append((lora_a, lora_b))
for tp_rank in range(tp_size):
layer = _TpQkvLoRALayer(tp_rank)
start = tp_rank * local_rows
end = start + local_rows
full_packed = PackedLoRALayerWeights(
module_name="blocks.0.attn.qkv_proj",
rank=lora_module._NATIVE_RANK,
lora_alphas=[64, 64, 64],
lora_a=[torch.ones(lora_module._NATIVE_RANK, lora_module._NATIVE_HIDDEN_SIZE)] * 3,
lora_b=[torch.full((slice_rows, lora_module._NATIVE_RANK), float(i + 1)) for i in range(3)],
scaling=[1.0, 1.0, 1.0],
)
local_b = [b[start:end].contiguous() for b in full_packed.lora_b]
layer.set_lora(0, full_packed.lora_a, local_b)
assert len(layer.set_calls) == 1
_, lora_b_list = layer.set_calls[0]
assert all(b.shape[0] == local_rows for b in lora_b_list)
assert lora_b_list[0][0, 0].item() == 1.0
assert lora_b_list[1][0, 0].item() == 2.0
assert lora_b_list[2][0, 0].item() == 3.0
# 259 modules, of which 52 qkv expand to 3 slices and 52 fc1 expand to 2.
_EXPECTED_NATIVE_SLICE_PAIRS = 415
def _iter_native_slice_pairs(lora_model):
"""Yield ``(module_name, lora_a, lora_b)`` per slice, flattening packed weights."""
for module_name, weights in lora_model.loras.items():
lora_a = weights.lora_a if isinstance(weights.lora_a, list) else [weights.lora_a]
lora_b = weights.lora_b if isinstance(weights.lora_b, list) else [weights.lora_b]
for a, b in zip(lora_a, lora_b, strict=True):
yield module_name, a, b
@pytest.mark.parametrize("dtype", [torch.float32, torch.float16, torch.bfloat16])
def test_native_lora_load_is_platform_agnostic(tmp_path, dtype):
"""The artifact is trained on Ascend, but loading it is not Ascend-only.
This whole module runs in the CPU job with no ``torch_npu`` present, so a
passing run is itself the evidence. The assertions pin the property that
makes it portable: tensors land on CPU in exactly the requested dtype and
the loader never picks a device, leaving placement to the pipeline.
"""
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
loaded = load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=dtype,
)
assert loaded is not None
lora_model, _, _ = loaded
tensors = {
f"{module_name}.{side}": tensor
for module_name, a, b in _iter_native_slice_pairs(lora_model)
for side, tensor in (("lora_a", a), ("lora_b", b))
}
assert sum(1 for _ in _iter_native_slice_pairs(lora_model)) == _EXPECTED_NATIVE_SLICE_PAIRS
assert {name: tensor.device.type for name, tensor in tensors.items() if tensor.device.type != "cpu"} == {}
assert {name: tensor.dtype for name, tensor in tensors.items() if tensor.dtype != dtype} == {}
@pytest.mark.cuda
@pytest.mark.skipif(not torch.cuda.is_available(), reason="requires a CUDA device")
def test_native_lora_delta_matches_between_cpu_and_cuda(tmp_path):
"""The packed adapter produces the same delta weight on CUDA as on CPU.
Covers the GPU side of the same artifact: every ``lora_b @ lora_a`` product,
including the reordered q/k/v slices, is recomputed on CUDA and compared
against the CPU result, so a GPU deployment cannot silently diverge.
"""
path = tmp_path / "minimax_h3_t2va_flashgen_4step_v1.0_768p_bf16.safetensors"
_write_tiny_native(path)
loaded = load_minimax_h3_native_lora(
partition="fl2va",
lora_request=_request(path),
lora_path=path,
dtype=torch.float32,
)
assert loaded is not None
lora_model, _, _ = loaded
checked = 0
for module_name, a, b in _iter_native_slice_pairs(lora_model):
cpu_delta = b @ a
cuda_delta = b.cuda() @ a.cuda()
assert cuda_delta.device.type == "cuda"
torch.testing.assert_close(cuda_delta.cpu(), cpu_delta, msg=f"delta mismatch for {module_name}")
checked += 1
assert checked == _EXPECTED_NATIVE_SLICE_PAIRS
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import pytest
import torch
@@ -113,146 +113,6 @@ def test_h3_rope_table_materializes_local_rows_in_fused_kernel_layout():
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
def test_h3_prepared_rope_table_is_rank_local_and_validated(monkeypatch):
from vllm_omni.diffusion.models.minimax_h3 import minimax_h3_transformer as h3
class Rope(nn.Module):
def forward(self, position_ids):
freqs_half = position_ids[0].float()
return torch.cat((freqs_half, freqs_half), dim=-1)
model = object.__new__(h3.MiniMaxH3DiTModel)
nn.Module.__init__(model)
model.arch = h3.MiniMaxH3DiTArchConfig(rope_inv_freq_len=1)
model.rope = Rope()
model.local_sp_prepare = nn.Identity()
monkeypatch.setattr(h3, "_sequence_parallel_local_span", lambda *args, **kwargs: (1, 2))
position_ids = torch.arange(12, dtype=torch.long).reshape(1, 4, 3)
actual = model.prepare_rope_table(position_ids, seq_len=4)
expected = h3._build_rope_table(model.rope(position_ids[:, 1:3]))
torch.testing.assert_close(actual, expected, atol=0, rtol=0)
model._validate_prepared_rope_table(actual, local_len=2, device=torch.device("cpu"))
with pytest.raises(ValueError, match="local_seq_len"):
model._validate_prepared_rope_table(actual[:1], local_len=2, device=torch.device("cpu"))
def test_denoise_branch_prepares_rope_table_once_per_npu_run(monkeypatch: pytest.MonkeyPatch):
from vllm_omni.diffusion.models.minimax_h3 import denoise_loop
from vllm_omni.diffusion.models.minimax_h3.denoise_loop import (
MiniMaxH3DenoiseBranch,
minimax_h3_denoise_loop,
)
monkeypatch.setattr(denoise_loop.current_omni_platform, "is_npu", lambda: True)
packed = {
"seq_len": torch.tensor(3),
"img_pos": torch.tensor([1]),
"audio_pos": torch.tensor([2]),
"text_pos": torch.tensor([0]),
"update_mask": torch.tensor([True]),
"cu_seqlens": torch.tensor([0, 3, 3], dtype=torch.int32),
"img_position_ids": torch.zeros(3, 3, dtype=torch.long),
"latent_grid": torch.tensor([1, 1, 1]),
"video_row_start": torch.tensor(1),
}
branch = MiniMaxH3DenoiseBranch(
packed=packed,
text_embeddings=torch.zeros(1, 2),
token_tags=torch.zeros(3, dtype=torch.long),
device=torch.device("cpu"),
)
class Model:
def __init__(self):
self.rope_table = torch.zeros(3, 6, dtype=torch.bfloat16)
self.prepare_calls = 0
self.forward_calls = 0
def prepare_rope_table(self, img_position_ids, *, seq_len):
assert img_position_ids is branch.static_kwargs["img_position_ids"]
assert seq_len == 3
self.prepare_calls += 1
return self.rope_table
def __call__(self, **kwargs):
assert kwargs["rope_table"] is self.rope_table
self.forward_calls += 1
return torch.zeros(1, 96), torch.zeros(1, 32)
model = Model()
minimax_h3_denoise_loop(
model=model,
positive=branch,
initial_video_rows=torch.zeros(1, 96),
initial_audio_rows=torch.zeros(1, 32),
keyframe_cond_rows=None,
sigmas_video=[1.0, 0.5, 0.0],
sigmas_audio=[1.0, 0.5, 0.0],
device=torch.device("cpu"),
)
assert model.prepare_calls == 1
assert model.forward_calls == 2
def test_denoise_branch_keeps_rope_construction_on_the_reference_path_off_npu(
monkeypatch: pytest.MonkeyPatch,
):
from vllm_omni.diffusion.models.minimax_h3 import denoise_loop
from vllm_omni.diffusion.models.minimax_h3.denoise_loop import (
MiniMaxH3DenoiseBranch,
minimax_h3_denoise_loop,
)
monkeypatch.setattr(denoise_loop.current_omni_platform, "is_npu", lambda: False)
packed = {
"seq_len": torch.tensor(3),
"img_pos": torch.tensor([1]),
"audio_pos": torch.tensor([2]),
"text_pos": torch.tensor([0]),
"update_mask": torch.tensor([True]),
"cu_seqlens": torch.tensor([0, 3, 3], dtype=torch.int32),
"img_position_ids": torch.zeros(3, 3, dtype=torch.long),
"latent_grid": torch.tensor([1, 1, 1]),
"video_row_start": torch.tensor(1),
}
branch = MiniMaxH3DenoiseBranch(
packed=packed,
text_embeddings=torch.zeros(1, 2),
token_tags=torch.zeros(3, dtype=torch.long),
device=torch.device("cpu"),
)
class Model:
def __init__(self):
self.forward_calls = 0
def prepare_rope_table(self, *_args, **_kwargs):
raise AssertionError("the reference path must not pre-build a rope_table")
def __call__(self, **kwargs):
assert "rope_table" not in kwargs
self.forward_calls += 1
return torch.zeros(1, 96), torch.zeros(1, 32)
model = Model()
minimax_h3_denoise_loop(
model=model,
positive=branch,
initial_video_rows=torch.zeros(1, 96),
initial_audio_rows=torch.zeros(1, 32),
keyframe_cond_rows=None,
sigmas_video=[1.0, 0.5, 0.0],
sigmas_audio=[1.0, 0.5, 0.0],
device=torch.device("cpu"),
)
assert model.forward_calls == 2
def test_strict_sp_local_span_uses_rank_owned_rows(monkeypatch):
from vllm_omni.diffusion import forward_context
from vllm_omni.diffusion.distributed import parallel_state
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import json
from types import SimpleNamespace
@@ -245,160 +245,6 @@ class TestDistributedLayerwiseOffloadHook:
)
torch.testing.assert_close(rank0_block.weight_scale, expected_scale)
def test_allgather_reconstructs_online_int8_weight_and_scale(
self,
patched_offload_runtime,
monkeypatch,
):
"""DLO must reconstruct finalized online-INT8 tensors byte-for-byte.
One block carries both finalized online-INT8 layouts: the NPU method
leaves a contiguous pre-transposed (K, N) int8 weight with a 1-D fp32
per-channel scale, while the CUDA method leaves a transposed-view
weight (stride (1, K)) with a scalar fp32 scale. The int8 and fp32
dtype groups must survive the flat-shard AllGather independently.
"""
class OnlineInt8Block(nn.Module):
def __init__(self):
super().__init__()
logical = torch.arange(1.0, 13.0).reshape(3, 4)
# NPU online int8: qweight.t().contiguous() -> (K, N), 1-D scale
self.npu_weight = nn.Parameter(logical.t().contiguous().to(torch.int8), requires_grad=False)
self.npu_weight_scale = nn.Parameter(torch.tensor([2.0, 3.0, 4.0]), requires_grad=False)
# CUDA online int8: Parameter(qweight.t().data) -> transposed
# view (stride (1, K)) over the (N, K) storage, scalar scale
self.cuda_weight = nn.Parameter((logical + 100).to(torch.int8).t(), requires_grad=False)
self.cuda_weight_scale = nn.Parameter(torch.tensor([0.5]), requires_grad=False)
current_block = OnlineInt8Block()
rank0_block = OnlineInt8Block()
rank1_block = OnlineInt8Block()
expected = {name: tensor.detach().clone() for name, tensor in rank0_block.named_parameters()}
expected_strides = {name: tensor.stride() for name, tensor in rank0_block.named_parameters()}
def make_hook(block: OnlineInt8Block, rank: int) -> DistributedLayerwiseOffloadHook:
return DistributedLayerwiseOffloadHook(
next_block=block,
device=torch.device("cpu"),
dp_group=object(),
dp_size=2,
rank=rank,
copy_stream=DummyStream(),
comm_stream=DummyStream(),
pin_memory=False,
)
rank0_hook = make_hook(rank0_block, 0)
rank1_hook = make_hook(rank1_block, 1)
rank0_hook.initialize_hook(current_block)
rank1_hook.initialize_hook(OnlineInt8Block())
rank0_hook.gpu_shard_buffers = [
{dtype: torch.empty_like(shard) for dtype, shard in rank0_hook.cpu_shards.items()} for _ in range(2)
]
def fake_allgather(output, local_shard, *, group):
del group
remote_shard = rank1_hook.cpu_shards[local_shard.dtype]
shard_size = local_shard.numel()
output[:shard_size].copy_(local_shard)
output[shard_size : 2 * shard_size].copy_(remote_shard)
monkeypatch.setattr(torch.distributed, "all_gather_into_tensor", fake_allgather)
rank0_hook.prefetch_layer(slot=0, non_blocking=False)
for name, tensor in rank0_block.named_parameters():
assert tensor.stride() == expected_strides[name]
torch.testing.assert_close(
tensor.float(),
expected[name].float(),
rtol=0,
atol=0,
)
def test_allgather_reconstructs_online_mxfp8_weight_and_scale(
self,
patched_offload_runtime,
monkeypatch,
):
"""DLO must reconstruct finalized online-MXFP8 tensors byte-for-byte.
One block carries both finalized online-MXFP8 layouts: the NPU method
leaves a contiguous pre-transposed (K, N) fp8 weight with a contiguous
(K_groups/2, N, 2) e8m0 block scale, while the vLLM-kernel method
(XPU) leaves an (N, K) fp8 weight whose e8m0 scale is a transposed
view over a contiguous (K/32, N) buffer. The fp8 and e8m0 dtype
groups must survive the flat-shard AllGather independently.
"""
class OnlineMxfp8Block(nn.Module):
def __init__(self):
super().__init__()
logical = torch.arange(1.0, 13.0).reshape(3, 4)
# Build scales from raw e8m0 bit patterns (1 byte/elem).
npu_scale_bits = torch.arange(1, 13, dtype=torch.uint8).reshape(2, 3, 2)
kernel_scale_bits = torch.arange(1, 7, dtype=torch.uint8).reshape(2, 3)
# NPU online mxfp8: (N, K).t().contiguous() -> (K, N) fp8
# weight, contiguous (K_groups/2, N, 2) e8m0 scale
self.npu_weight = nn.Parameter(logical.t().contiguous().to(torch.float8_e4m3fn), requires_grad=False)
self.npu_weight_scale = nn.Parameter(npu_scale_bits.view(torch.float8_e8m0fnu), requires_grad=False)
# vLLM-kernel online mxfp8 (XPU): (N, K) fp8 weight, e8m0
# scale kept as a transposed view (stride (1, K/32)) over the
# contiguous (K/32, N) storage
self.kernel_weight = nn.Parameter((logical + 100).to(torch.float8_e4m3fn), requires_grad=False)
self.kernel_weight_scale = nn.Parameter(
kernel_scale_bits.view(torch.float8_e8m0fnu).t(), requires_grad=False
)
current_block = OnlineMxfp8Block()
rank0_block = OnlineMxfp8Block()
rank1_block = OnlineMxfp8Block()
expected = {name: tensor.detach().clone() for name, tensor in rank0_block.named_parameters()}
expected_strides = {name: tensor.stride() for name, tensor in rank0_block.named_parameters()}
def make_hook(block: OnlineMxfp8Block, rank: int) -> DistributedLayerwiseOffloadHook:
return DistributedLayerwiseOffloadHook(
next_block=block,
device=torch.device("cpu"),
dp_group=object(),
dp_size=2,
rank=rank,
copy_stream=DummyStream(),
comm_stream=DummyStream(),
pin_memory=False,
)
rank0_hook = make_hook(rank0_block, 0)
rank1_hook = make_hook(rank1_block, 1)
rank0_hook.initialize_hook(current_block)
rank1_hook.initialize_hook(OnlineMxfp8Block())
rank0_hook.gpu_shard_buffers = [
{dtype: torch.empty_like(shard) for dtype, shard in rank0_hook.cpu_shards.items()} for _ in range(2)
]
def fake_allgather(output, local_shard, *, group):
del group
remote_shard = rank1_hook.cpu_shards[local_shard.dtype]
shard_size = local_shard.numel()
output[:shard_size].copy_(local_shard)
output[shard_size : 2 * shard_size].copy_(remote_shard)
monkeypatch.setattr(torch.distributed, "all_gather_into_tensor", fake_allgather)
rank0_hook.prefetch_layer(slot=0, non_blocking=False)
for name, tensor in rank0_block.named_parameters():
assert tensor.stride() == expected_strides[name]
# Compare raw bytes in logical order: float8/e8m0 CPU upcasts are
# version-sensitive, while contiguous().view(uint8) is an exact
# byte-level comparison for both contiguous and transposed-view
# parameters.
torch.testing.assert_close(
tensor.detach().contiguous().view(torch.uint8),
expected[name].contiguous().view(torch.uint8),
rtol=0,
atol=0,
)
def test_dtensor_wrapper_preserved_across_prefetch_and_offload(self, dist_group, patched_offload_runtime):
"""DTensor wrapper should be preserved through prefetch/offload cycle."""
current_block = TinyBlock(_make_values(1.0))
+1 -54
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import asyncio
import queue
@@ -197,59 +197,6 @@ class TestRequestBatchCapability:
assert diffusion_engine_module.supports_request_batch(od_config) is True
def test_engine_rejects_multi_seq_diffusers_using_adapter_capability(
self,
monkeypatch: pytest.MonkeyPatch,
mocker: MockerFixture,
) -> None:
od_config = SimpleNamespace(
model_class_name="QwenImagePipeline",
custom_pipeline_args=None,
diffusion_load_format="diffusers",
streaming_output=False,
max_num_seqs=2,
)
registry_load = mocker.Mock(
side_effect=lambda model_class_name: (
_SingleRequestPipeline if model_class_name == "DiffusersAdapterPipeline" else _BatchCapablePipeline
)
)
monkeypatch.setattr(
diffusion_engine_module.DiffusionModelRegistry,
"_try_load_model_cls",
registry_load,
)
engine = DiffusionEngine.__new__(DiffusionEngine)
with pytest.raises(ValueError, match="max_num_seqs=1"):
engine._resolve_execution_mode(od_config)
registry_load.assert_called_once_with("DiffusersAdapterPipeline")
def test_engine_prefers_batch_capable_custom_pipeline_over_diffusers_adapter(
self,
monkeypatch: pytest.MonkeyPatch,
mocker: MockerFixture,
) -> None:
od_config = SimpleNamespace(
model_class_name="QwenImagePipeline",
custom_pipeline_args={"pipeline_class": _BatchCapablePipeline},
diffusion_load_format="diffusers",
streaming_output=False,
max_num_seqs=2,
)
registry_load = mocker.Mock(return_value=_SingleRequestPipeline)
monkeypatch.setattr(
diffusion_engine_module.DiffusionModelRegistry,
"_try_load_model_cls",
registry_load,
)
engine = DiffusionEngine.__new__(DiffusionEngine)
assert engine._resolve_execution_mode(od_config) is DiffusionExecutionMode.REQUEST_BATCH
registry_load.assert_not_called()
def test_supports_request_batch_uses_custom_pipeline_class(self, monkeypatch: pytest.MonkeyPatch) -> None:
od_config = SimpleNamespace(
model_class_name="SinglePipeline",
@@ -328,25 +328,6 @@ def test_formatter_preserves_audio_model_video_audio_and_actions(
}
def test_formatter_uses_native_model_identity_for_diffusers_video_frames(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setattr(output_formatter, "supports_audio_output", lambda _: False)
frames = [["frame-0", "frame-1"]]
postprocess_output = normalize_diffusion_postprocess_output(frames)
[result] = format_diffusion_outputs(
request=_request("make a video"),
od_config=_config("WanImageToVideoPipeline"),
diffusion_output=DiffusionOutput(output=frames),
output_data=frames,
postprocess_output=postprocess_output,
)
assert result.images == frames
assert result.final_output_type == "video"
def test_formatter_preserves_audio_only_postprocess_dict(
monkeypatch: pytest.MonkeyPatch,
) -> None:
+1 -46
View File
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""
Unit tests for diffusion engine plugin extensibility hooks.
@@ -20,8 +20,6 @@ from vllm_omni.diffusion.registry import (
_DIFFUSION_MODELS,
_DIFFUSION_POST_PROCESS_FUNCS,
_DIFFUSION_PRE_PROCESS_FUNCS,
get_diffusion_post_process_func,
get_diffusion_pre_process_func,
register_diffusion_model,
)
from vllm_omni.platforms.interface import OmniPlatform, OmniPlatformEnum
@@ -114,49 +112,6 @@ class TestRegisterDiffusionModel:
assert _DIFFUSION_POST_PROCESS_FUNCS["LegacyActionPipeline"] == "test_post_process"
@pytest.mark.parametrize(
("model_class_name", "get_process_func"),
[
("QwenImagePipeline", get_diffusion_post_process_func),
("WanImageToVideoPipeline", get_diffusion_post_process_func),
("WanImageToVideoPipeline", get_diffusion_pre_process_func),
],
)
def test_diffusers_backend_skips_native_process_hooks(model_class_name, get_process_func):
od_config = SimpleNamespace(
model_class_name=model_class_name,
custom_pipeline_args=None,
diffusion_load_format="diffusers",
)
with patch("vllm_omni.diffusion.registry._load_process_func") as load_process_func:
assert get_process_func(od_config) is None
load_process_func.assert_not_called()
@pytest.mark.parametrize(
("model_class_name", "get_process_func"),
[
("QwenImagePipeline", get_diffusion_post_process_func),
("WanImageToVideoPipeline", get_diffusion_post_process_func),
("WanImageToVideoPipeline", get_diffusion_pre_process_func),
],
)
def test_custom_pipeline_preserves_native_process_hooks(model_class_name, get_process_func):
process_func = Mock()
od_config = SimpleNamespace(
model_class_name=model_class_name,
custom_pipeline_args={"pipeline_class": "custom.Pipeline"},
diffusion_load_format="diffusers",
)
with patch("vllm_omni.diffusion.registry._load_process_func", return_value=process_func) as load_process_func:
assert get_process_func(od_config) is process_func
load_process_func.assert_called_once()
class TestWorkerUsesHook:
"""Test that DiffusionWorker resolves model runner via platform hook."""
@@ -57,19 +57,6 @@ def _req(req_id: str, status: RequestStatus, external_req_id: str | None = None)
return request
def _streaming_request(mocker: MockerFixture, num_computed_tokens: int) -> SimpleNamespace:
prompt_token_ids = [0] * num_computed_tokens
return SimpleNamespace(
_all_token_ids=prompt_token_ids.copy(),
_output_token_ids=[],
prompt_token_ids=prompt_token_ids,
num_computed_tokens=num_computed_tokens,
num_prompt_tokens=num_computed_tokens,
num_output_placeholders=0,
update_block_hashes=mocker.Mock(),
)
def test_streaming_payload_can_replace_placeholder_prompt(mocker: MockerFixture) -> None:
request = SimpleNamespace(
_all_token_ids=[0, 0, 7, 8],
@@ -97,219 +84,30 @@ def test_streaming_payload_can_replace_placeholder_prompt(mocker: MockerFixture)
request.update_block_hashes.assert_called_once_with()
def test_turn_start_replacement_ignores_accumulated_prompt_capacity(mocker: MockerFixture) -> None:
request = _streaming_request(mocker, num_computed_tokens=4064)
payload = {
"ids": {"prompt": [1]},
"meta": {
"replace_streaming_prompt": True,
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
},
}
replaced = construct_next_stage_streaming_input_prompt(
payload,
request,
max_model_len=4096,
previous_condition_len=10,
previous_condition_seq=7,
condition_seq=8,
recompute_previous_chunks=1,
)
assert replaced is True
assert request.prompt_token_ids == [0] * 10
assert payload["meta"]["streaming_prompt_recompute"] is False
assert "streaming_prompt_previous_codes" not in payload["ids"]
@pytest.mark.parametrize("reserve", [True, 0, -1, 1.5, "26", None])
def test_streaming_prompt_rejects_invalid_generation_reserve(
mocker: MockerFixture,
reserve: object,
) -> None:
request = _streaming_request(mocker, num_computed_tokens=10)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": reserve,
},
}
with pytest.raises(ValueError, match="next_stage_generation_tokens must be a positive integer"):
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
def test_capacity_managed_streaming_prompt_at_limit_still_appends(mocker: MockerFixture) -> None:
request = _streaming_request(mocker, num_computed_tokens=4060)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
},
}
replaced = construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
assert replaced is False
assert request.num_computed_tokens == 4060
assert request.num_prompt_tokens == 4070
def test_streaming_window_builds_one_chunk_recompute_recipe(mocker: MockerFixture) -> None:
previous_condition_len = 10
previous_codes = list(range(25))
def test_streaming_payload_can_append_exact_prompt_length(mocker: MockerFixture) -> None:
request = SimpleNamespace(
_all_token_ids=[0] * 4039 + previous_codes + [999],
_output_token_ids=previous_codes + [999],
prompt_token_ids=[0] * 4039,
num_computed_tokens=4065,
num_prompt_tokens=4039,
num_output_placeholders=1,
_all_token_ids=[0, 0, 7, 8],
_output_token_ids=[7, 8],
prompt_token_ids=[0, 0],
num_computed_tokens=4,
num_prompt_tokens=2,
update_block_hashes=mocker.Mock(),
)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
},
"ids": {"prompt": [1, 2, 3]},
"meta": {"next_stage_prompt_len": 3},
}
replaced = construct_next_stage_streaming_input_prompt(
payload,
request,
max_model_len=4096,
previous_condition_len=previous_condition_len,
previous_condition_seq=7,
condition_seq=8,
recompute_previous_chunks=1,
)
construct_next_stage_streaming_input_prompt(payload, request)
assert replaced is True
assert request.prompt_token_ids == [0] * 45
assert request.num_prompt_tokens == 45
assert request.num_computed_tokens == 0
assert payload["ids"]["streaming_prompt_previous_codes"] == previous_codes
assert payload["meta"] == {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
"streaming_prompt_recompute": True,
"streaming_condition_seq": 8,
}
assert request.prompt_token_ids == [0, 0, 7, 8, 0, 0, 0]
assert request._all_token_ids == [0, 0, 7, 8, 0, 0, 0]
assert request._output_token_ids == []
assert request.num_computed_tokens == 4
assert request.num_prompt_tokens == 7
request.update_block_hashes.assert_called_once_with()
def test_streaming_window_recomputes_each_condition_without_accumulating(mocker: MockerFixture) -> None:
request = SimpleNamespace(
_all_token_ids=[0] * 10 + [101, 102],
_output_token_ids=[101, 102],
prompt_token_ids=[0] * 10,
num_computed_tokens=12,
num_prompt_tokens=10,
num_output_placeholders=0,
update_block_hashes=mocker.Mock(),
)
second = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 12,
"next_stage_generation_tokens": 26,
},
}
assert construct_next_stage_streaming_input_prompt(
second,
request,
max_model_len=4096,
previous_condition_len=10,
previous_condition_seq=0,
condition_seq=1,
recompute_previous_chunks=1,
)
assert request.num_prompt_tokens == 24
assert second["ids"]["streaming_prompt_previous_codes"] == [101, 102]
request._all_token_ids.extend([201, 202, 203])
request._output_token_ids.extend([201, 202, 203])
request.num_computed_tokens = 27
third = {
"ids": {"prompt": [2]},
"meta": {
"next_stage_prompt_len": 8,
"next_stage_generation_tokens": 26,
},
}
assert construct_next_stage_streaming_input_prompt(
third,
request,
max_model_len=4096,
previous_condition_len=12,
previous_condition_seq=1,
condition_seq=2,
recompute_previous_chunks=1,
)
assert request.num_prompt_tokens == 23
assert third["ids"]["streaming_prompt_previous_codes"] == [201, 202, 203]
assert third["meta"]["streaming_condition_seq"] == 2
def test_capacity_rollover_requires_explicit_window_contract(mocker: MockerFixture) -> None:
request = _streaming_request(mocker, num_computed_tokens=4064)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
},
}
with pytest.raises(ValueError, match="requires window_size=1"):
construct_next_stage_streaming_input_prompt(
payload,
request,
max_model_len=4096,
previous_condition_len=10,
previous_condition_seq=0,
condition_seq=1,
)
@pytest.mark.parametrize("next_stage_prompt_len", [0, -1])
def test_capacity_managed_streaming_prompt_rejects_nonpositive_length(
mocker: MockerFixture, next_stage_prompt_len: int
) -> None:
request = _streaming_request(mocker, num_computed_tokens=0)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": next_stage_prompt_len,
"next_stage_generation_tokens": 26,
},
}
with pytest.raises(ValueError, match="positive next_stage_prompt_len"):
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
def test_capacity_managed_streaming_prompt_rejects_fresh_overflow(mocker: MockerFixture) -> None:
request = _streaming_request(mocker, num_computed_tokens=0)
payload = {
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 4071,
"next_stage_generation_tokens": 26,
},
}
with pytest.raises(ValueError, match="exceeds max_model_len"):
construct_next_stage_streaming_input_prompt(payload, request, max_model_len=4096)
@pytest.fixture
def build_adapter(monkeypatch, mocker: MockerFixture):
def _build(
@@ -317,10 +115,6 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
stage_id: int = 1,
model_mode: str = "ar",
max_num_seqs: int = 2,
max_model_len: int = 0,
tts_max_model_len: int = 0,
tts_attention_type: str = "full_attention",
flat_tts_config: bool = False,
active_stream_window: int = 0,
connector_extra: dict | None = None,
):
@@ -337,10 +131,6 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
self._cancelled_load_reqs = set()
self._pending_save_reqs = deque()
self._finished_save_reqs = set()
self._send_failures = {}
self._send_failure_lock = threading.Lock()
self._receive_failures = {}
self._receive_failure_lock = threading.Lock()
self.stop_event = threading.Event()
self._recv_cond = threading.Condition()
self._save_cond = threading.Condition()
@@ -352,23 +142,9 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
classmethod(lambda cls, _model_config: connector),
)
hf_config = SimpleNamespace(
model_type="minicpmtts",
max_position_embeddings=tts_max_model_len,
attention_type=tts_attention_type,
)
if not flat_tts_config:
hf_config = SimpleNamespace(
tts_config=SimpleNamespace(
max_position_embeddings=tts_max_model_len,
attention_type=tts_attention_type,
)
)
model_config = SimpleNamespace(
worker_type=model_mode,
max_num_seqs=max_num_seqs,
max_model_len=max_model_len,
hf_config=hf_config,
active_stream_window=active_stream_window,
stage_connector_config={
"name": "SharedMemoryConnector",
@@ -384,27 +160,6 @@ def build_adapter(monkeypatch, mocker: MockerFixture):
return _build
def _dequeue_load_entry(adapter, request):
"""Register and take the entry as the background recv loop would."""
adapter.load_async(request)
entry = adapter._registered_load_entries[request.request_id]
try:
adapter._pending_load_reqs.remove(entry)
except ValueError:
pass
return entry
@pytest.mark.parametrize(
("attention_type", "expected_previous_chunks"),
[("full_attention", 0), ("sliding_recompute", 1)],
)
def test_talker_attention_policy_controls_streaming_recompute(build_adapter, attention_type, expected_previous_chunks):
adapter, _ = build_adapter(tts_attention_type=attention_type)
assert adapter._streaming_prompt_previous_chunks == expected_previous_chunks
@pytest.mark.parametrize(
("raw_cfg", "expected_name", "expected_extra"),
[
@@ -445,78 +200,15 @@ def test_load_poll(build_adapter):
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
connector.get.return_value = (payload, 16)
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
adapter._poll_single_request(request)
assert request.additional_information is None
assert "req-1" in adapter._finished_load_reqs
request.status = RequestStatus.WAITING_FOR_CHUNK
adapter.process_pending_chunks(
DummyWaitingQueue(),
[request],
scheduler_requests={request.request_id: request},
)
assert request.additional_information == payload
assert adapter.get_req_chunk["req-1"] == 1
assert "req-1" not in adapter._finished_load_reqs
assert "req-1" in adapter._finished_load_reqs
assert "req-1" in adapter.upstream_exhausted_requests
assert "req-1" not in adapter._pending_load_reqs
def test_load_async_does_not_requeue_registered_inflight_request(build_adapter):
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
request = _req("req-inflight", RequestStatus.RUNNING, external_req_id="ext-inflight")
entry = _dequeue_load_entry(adapter, request)
# The recv loop owns the popped request until polling completes. A
# scheduler-side duplicate registration must not append a second copy.
adapter.load_async(request)
assert adapter._pending_load_reqs == deque()
connector.get.return_value = (
{
"ids": {"prompt": [1]},
"meta": {"finished": True},
},
1,
)
assert adapter._poll_single_request(entry) is True
# Successful polling releases the registration for the next chunk.
adapter.load_async(request)
assert [queued.request for queued in adapter._pending_load_reqs] == [request]
def test_cleanup_then_reregister_drops_stale_queued_entry(build_adapter):
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
request = _req("req-resume", RequestStatus.WAITING, external_req_id="ext-resume")
adapter.load_async(request)
stale_entry = adapter._pending_load_reqs[0]
adapter.cleanup_receiver(request.request_id)
adapter.load_async(request)
fresh_entry = adapter._pending_load_reqs[-1]
assert len(adapter._pending_load_reqs) == 2
assert adapter._pending_load_reqs.popleft() is stale_entry
assert adapter._pending_load_reqs.popleft() is fresh_entry
connector.get.return_value = (
{
"ids": {"prompt": [1]},
"meta": {"finished": True},
},
1,
)
assert adapter._poll_single_request(stale_entry) is True
connector.get.assert_not_called()
assert adapter._registered_load_entries[request.request_id] is fresh_entry
assert adapter._poll_single_request(fresh_entry) is True
connector.get.assert_called_once()
assert request.request_id not in adapter._registered_load_entries
assert adapter.get_req_chunk[request.request_id] == 1
def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter):
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
request = _req("req-replace", RequestStatus.RUNNING, external_req_id="external-replace")
@@ -542,18 +234,15 @@ def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter)
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
request.status = RequestStatus.WAITING_FOR_CHUNK
running_queue = [request]
waiting_queue = DummyWaitingQueue()
adapter.process_pending_chunks(
waiting_queue,
running_queue,
scheduler_requests={request.request_id: request},
)
assert adapter._poll_single_request(request) is True
assert request.num_computed_tokens == 0
assert request.prompt_token_ids == [0] * 10
assert request.request_id in adapter.replaced_streaming_prompt_ids
request.status = RequestStatus.WAITING_FOR_CHUNK
running_queue = [request]
waiting_queue = DummyWaitingQueue()
adapter.process_pending_chunks(waiting_queue, running_queue)
assert running_queue == []
assert waiting_queue == [request]
assert request.status == RequestStatus.WAITING
@@ -569,134 +258,6 @@ def test_load_poll_ar_requeues_explicitly_replaced_running_prompt(build_adapter)
assert request.external_req_id not in adapter.requests_num_chunks_sent
@pytest.mark.parametrize("flat_tts_config", [False, True])
def test_load_poll_ar_recomputes_native_duplex_prompt_each_condition(build_adapter, flat_tts_config):
adapter, connector = build_adapter(
stage_id=1,
model_mode="ar",
max_model_len=8192,
tts_max_model_len=4096,
tts_attention_type="sliding_recompute",
flat_tts_config=flat_tts_config,
)
request = _req("req-rollover", RequestStatus.RUNNING, external_req_id="external-rollover")
request.resumable = True
previous_codes = list(range(25))
request.prompt_token_ids = [0] * 10
request._all_token_ids = [0] * 10 + previous_codes
request._output_token_ids = previous_codes.copy()
request.num_prompt_tokens = 10
request.num_computed_tokens = 35
request.num_output_placeholders = 0
request.update_block_hashes = Mock()
adapter.get_req_chunk[request.request_id] = 1
adapter.request_ids_mapping[request.request_id] = request.external_req_id
adapter._streaming_condition_lengths[request.request_id] = 10
adapter._streaming_condition_seqs[request.request_id] = 0
connector.get.return_value = (
{
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
"finished": True,
},
},
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
request.status = RequestStatus.WAITING_FOR_CHUNK
running_queue = [request]
waiting_queue = DummyWaitingQueue()
adapter.process_pending_chunks(
waiting_queue,
running_queue,
scheduler_requests={request.request_id: request},
)
assert request.num_computed_tokens == 0
assert request.prompt_token_ids == [0] * 45
assert request.additional_information["ids"]["streaming_prompt_previous_codes"] == previous_codes
assert request.additional_information["meta"]["streaming_prompt_recompute"] is True
assert request.resumable is False
assert request.request_id in adapter.replaced_streaming_prompt_ids
assert adapter._max_model_len == 4096
assert running_queue == []
assert waiting_queue == [request]
def test_window_condition_sequence_ignores_control_only_chunks(build_adapter) -> None:
adapter, connector = build_adapter(
stage_id=1,
model_mode="ar",
tts_attention_type="sliding_recompute",
)
request = _req("req-condition-seq", RequestStatus.RUNNING)
def receive(payload):
connector.get.return_value = (payload, 1)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
adapter.requests_with_ready_chunks.add(request.request_id)
adapter._apply_pending_ar_prompt_updates({request.request_id: request})
adapter.requests_with_ready_chunks.discard(request.request_id)
return request.additional_information
first = receive(
{
"ids": {"prompt": [1]},
"meta": {"next_stage_prompt_len": 10},
}
)
assert first["meta"]["streaming_condition_seq"] == 0
assert adapter._streaming_condition_seqs[request.request_id] == 0
control = receive({"meta": {"is_segment_finished": True}})
assert "streaming_condition_seq" not in control["meta"]
assert adapter._streaming_condition_seqs[request.request_id] == 0
second = receive(
{
"ids": {"prompt": [2]},
"meta": {"next_stage_prompt_len": 12},
}
)
assert second["meta"]["streaming_condition_seq"] == 1
assert adapter._streaming_condition_lengths[request.request_id] == 12
def test_load_poll_ar_reports_invalid_capacity_metadata(build_adapter):
adapter, connector = build_adapter(stage_id=1, model_mode="ar", max_model_len=4096)
request = _req("req-invalid-capacity", RequestStatus.RUNNING)
request.resumable = True
adapter.get_req_chunk[request.request_id] = 1
connector.get.return_value = (
{
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 4096,
"next_stage_generation_tokens": 26,
"finished": False,
},
},
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
request.status = RequestStatus.WAITING_FOR_CHUNK
running_queue = [request]
adapter.process_pending_chunks(
DummyWaitingQueue(),
running_queue,
scheduler_requests={request.request_id: request},
)
assert adapter.collect_failed_receive_request_ids() == {
request.request_id: "fresh streaming prompt plus generation reserve exceeds max_model_len: "
"prompt=4096, reserve=26, limit=4096"
}
assert request.request_id not in adapter._finished_load_reqs
def test_load_poll_generation_tensor_codes_use_placeholder_prompt(build_adapter):
adapter, connector = build_adapter(stage_id=1, model_mode="generation")
request = _req("req-tensor", RequestStatus.WAITING, external_req_id="external-tensor")
@@ -711,7 +272,7 @@ def test_load_poll_generation_tensor_codes_use_placeholder_prompt(build_adapter)
}
connector.get.return_value = (payload, 16)
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
adapter._poll_single_request(request)
assert request.prompt_token_ids == [0]
assert request.num_computed_tokens == 0
@@ -741,13 +302,12 @@ def test_load_poll_generation_empty_nonterminal_chunk_keeps_polling(build_adapte
}
connector.get.side_effect = [(empty_payload, 16), (ready_payload, 16)]
entry = _dequeue_load_entry(adapter, request)
assert adapter._poll_single_request(entry) is False
assert adapter._poll_single_request(request) is False
assert request.request_id not in adapter._finished_load_reqs
assert request.request_id not in adapter.requests_with_ready_chunks
assert adapter.get_req_chunk[request.request_id] == 1
assert adapter._poll_single_request(entry) is True
assert adapter._poll_single_request(request) is True
assert request.request_id in adapter._finished_load_reqs
assert torch.equal(request.additional_information["codes"]["audio"], ready_payload["codes"]["audio"])
assert adapter.get_req_chunk[request.request_id] == 2
@@ -787,7 +347,6 @@ def test_segment_boundary_starts_new_send_watermark_before_background_flush(buil
adapter, _ = build_adapter(stage_id=1)
request = _req("req-stream", RequestStatus.WAITING, external_req_id="ext-stream")
request.resumable = True
request._omni_segment_generation = 1
request.num_computed_tokens = 0
adapter.requests_num_chunks_sent["ext-stream"] = 26
@@ -796,16 +355,13 @@ def test_segment_boundary_starts_new_send_watermark_before_background_flush(buil
request=request,
is_segment_finished=True,
confirmed_num_computed_tokens=26,
segment_generation=0,
)
assert len(adapter._pending_save_reqs) == 1
boundary_task = adapter._pending_save_reqs.popleft()
assert boundary_task["segment_generation"] == 0
assert "ext-stream" not in adapter.requests_num_chunks_sent
request.num_computed_tokens = 3
request._omni_segment_generation = 1
adapter.save_async(
multimodal_output=None,
request=request,
@@ -858,7 +414,6 @@ def test_background_send_uses_enqueued_request_snapshot(build_adapter):
request.additional_information["meta"]["segment"] = "next"
request.prompt_token_ids.append(3)
request.append_output_token_ids([8])
request._omni_segment_generation = 1
adapter.save_async(
multimodal_output=None,
request=request,
@@ -1107,7 +662,7 @@ def test_personaplex_sender_cleanup_drops_delayed_frame_state(build_adapter):
assert second.codes is None
def test_save_async_skips_stale_resumable_chunk_within_segment(build_adapter):
def test_save_async_skips_stale_resumable_chunk_until_dedup_is_reset(build_adapter):
adapter, _ = build_adapter(stage_id=1)
request = _req("req-stream", RequestStatus.WAITING, external_req_id="ext-stream")
request.resumable = True
@@ -1126,51 +681,6 @@ def test_save_async_skips_stale_resumable_chunk_within_segment(build_adapter):
assert adapter.requests_num_chunks_sent["ext-stream"] == 0
def test_save_async_drops_late_previous_segment_after_boundary_reset(build_adapter):
adapter, _ = build_adapter(stage_id=1)
request = _req("req-stream", RequestStatus.WAITING, external_req_id="ext-stream")
request.resumable = True
request._omni_segment_generation = 0
adapter.requests_num_chunks_sent["ext-stream"] = 111
adapter.save_async(multimodal_output=None, request=request, is_segment_finished=True)
late = _req("req-stream-late", RequestStatus.WAITING, external_req_id="ext-stream")
late.resumable = True
late.num_computed_tokens = 111
late._omni_segment_generation = 0
adapter.save_async(multimodal_output=None, request=late, is_segment_finished=False)
next_segment = _req("req-stream-next", RequestStatus.WAITING, external_req_id="ext-stream")
next_segment.resumable = True
next_segment.num_computed_tokens = 3
next_segment._omni_segment_generation = 1
adapter.save_async(multimodal_output=None, request=next_segment, is_segment_finished=False)
assert len(adapter._pending_save_reqs) == 2
assert adapter.requests_num_chunks_sent["ext-stream"] == 3
assert adapter._pending_save_reqs[-1]["request"] is next_segment
def test_send_single_request_preserves_queued_preboundary_chunk(build_adapter):
adapter, connector = build_adapter(stage_id=1)
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct(meta=MetaStruct())
request = _req("req-queued-old", RequestStatus.WAITING, external_req_id="ext-queued-old")
request.resumable = True
request._omni_segment_generation = 0
adapter.save_async(multimodal_output=None, request=request, is_segment_finished=False)
adapter.save_async(multimodal_output=None, request=request, is_segment_finished=True)
old_task = adapter._pending_save_reqs.popleft()
adapter._send_single_request(old_task)
connector.put.assert_called_once()
boundary_task = adapter._pending_save_reqs.popleft()
adapter._send_single_request(boundary_task)
assert connector.put.call_count == 2
def test_send_single_request_cleans_up_after_finished_payload(build_adapter, monkeypatch):
adapter, _ = build_adapter(stage_id=1)
request = _req("req-finished", RequestStatus.FINISHED_STOPPED, external_req_id="ext-finished")
@@ -1191,77 +701,6 @@ def test_send_single_request_cleans_up_after_finished_payload(build_adapter, mon
assert args[1] == "ext-finished"
def test_old_turn_terminal_send_preserves_new_turn_state_and_sender(build_adapter):
adapter, connector = build_adapter(
stage_id=1,
model_mode="ar",
max_model_len=4096,
tts_attention_type="sliding_recompute",
)
request = _req("req-live", RequestStatus.RUNNING, external_req_id="ext-live")
request.resumable = True
request._all_token_ids = []
request._output_token_ids = []
request.update_block_hashes = Mock()
adapter.custom_process_next_stage_input_func = lambda **kwargs: OmniPayloadStruct(
meta=MetaStruct(
finished=torch.tensor(True, dtype=torch.bool),
codec_streaming=True,
)
)
put_started = threading.Event()
release_put = threading.Event()
put_keys = []
def blocking_first_put(*_args, **kwargs):
put_keys.append(kwargs["put_key"])
if len(put_keys) == 1:
put_started.set()
assert release_put.wait(timeout=5)
return True, 1, {}
connector.put.side_effect = blocking_first_put
adapter.save_async(multimodal_output=None, request=request)
first_task = adapter._pending_save_reqs.popleft()
first_sender_token = first_task["sender_token"]
send_thread = threading.Thread(target=lambda: adapter._send_single_request(first_task))
send_thread.start()
assert put_started.wait(timeout=5)
entry = _dequeue_load_entry(adapter, request)
connector.get.return_value = (
{
"ids": {"prompt": [1]},
"meta": {
"replace_streaming_prompt": True,
"next_stage_prompt_len": 10,
"next_stage_generation_tokens": 26,
},
},
1,
)
assert adapter._poll_single_request(entry) is True
adapter.requests_with_ready_chunks.add(request.request_id)
adapter._apply_pending_ar_prompt_updates({request.request_id: request})
assert adapter._streaming_condition_seqs[request.request_id] == 0
request.num_computed_tokens = 1
adapter.save_async(multimodal_output=None, request=request)
second_task = adapter._pending_save_reqs.popleft()
assert second_task["sender_token"] is first_sender_token
release_put.set()
send_thread.join(timeout=5)
assert not send_thread.is_alive()
assert adapter._streaming_condition_seqs[request.request_id] == 0
assert adapter._sender_tokens[request.external_req_id] is first_sender_token
assert first_sender_token.cancelled is False
adapter._send_single_request(second_task)
assert put_keys == ["ext-live_1_0", "ext-live_1_1"]
def test_load_poll_non_ar_merges_into_existing_additional_information(build_adapter):
adapter, connector = build_adapter(stage_id=2, model_mode="diffusion")
request = _req("req-non-ar", RequestStatus.WAITING, external_req_id="ext-non-ar")
@@ -1281,7 +720,7 @@ def test_load_poll_non_ar_merges_into_existing_additional_information(build_adap
}
connector.get.return_value = (payload, 8)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
assert adapter._poll_single_request(request) is True
assert request.prompt_token_ids == [7, 8]
assert request.num_computed_tokens == 0
@@ -1322,7 +761,7 @@ def test_load_poll_generation_segment_marker_replaces_previous_chunk(build_adapt
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
assert adapter._poll_single_request(request) is True
assert request.prompt_token_ids == [7, 8]
assert "audio" not in request.additional_information["codes"]
@@ -1351,7 +790,7 @@ def test_load_poll_generation_empty_replacement_snapshot_is_ready(build_adapter)
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
assert adapter._poll_single_request(request) is True
assert request.prompt_token_ids == [0]
assert "codes" not in request.additional_information
@@ -1377,7 +816,7 @@ def test_load_poll_generation_without_snapshot_marker_keeps_incremental_state(bu
1,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is False
assert adapter._poll_single_request(request) is False
assert torch.equal(request.additional_information["codes"]["audio"], torch.tensor([1, 2]))
assert request.additional_information["meta"]["cache_epoch"] == 3
@@ -1401,17 +840,9 @@ def test_load_poll_ar_request_additional_information_concats_tensors(build_adapt
}
connector.get.return_value = (payload, 8)
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
adapter._poll_single_request(request)
# The recv thread records the latest payload; scheduler-side queue
# restoration publishes it onto the Request.
assert torch.equal(request.additional_information["hidden_states"]["output"], torch.tensor([[1.0]]))
request.status = RequestStatus.WAITING_FOR_CHUNK
adapter.process_pending_chunks(
DummyWaitingQueue(),
[request],
scheduler_requests={request.request_id: request},
)
# AR mode now forwards the latest payload directly.
assert request.additional_information == payload
assert request.additional_information["meta"]["finished"].item() is True
@@ -1429,7 +860,7 @@ def test_non_ar_poll_reinitializes_prefill_stats_for_later_chunks(build_adapter)
},
8,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
assert adapter._poll_single_request(request) is True
OmniGenerationScheduler._record_prefill_stats(request)
first_chunk_stats = request.prefill_stats
request.prefill_stats = None
@@ -1441,7 +872,7 @@ def test_non_ar_poll_reinitializes_prefill_stats_for_later_chunks(build_adapter)
},
8,
)
assert adapter._poll_single_request(_dequeue_load_entry(adapter, request)) is True
assert adapter._poll_single_request(request) is True
assert isinstance(request.prefill_stats, PrefillStats)
OmniGenerationScheduler._record_prefill_stats(request)
second_chunk_stats = request.prefill_stats
@@ -1540,7 +971,7 @@ def test_non_active_waiting_request_is_held_off_scheduler(build_adapter):
assert waiting_queue == []
assert active.status == RequestStatus.WAITING_FOR_CHUNK
assert non_active.status == RequestStatus.WAITING
assert [entry.request for entry in adapter._pending_load_reqs] == [active]
assert list(adapter._pending_load_reqs) == [active]
assert list(adapter.waiting_for_chunk_waiting_requests) == [active, non_active]
@@ -1691,19 +1122,8 @@ def _populate_adapter_state(adapter, req_id="req-1", ext_id="ext-1"):
adapter.get_req_chunk[req_id] = 3
adapter.requests_with_ready_chunks.add(req_id)
adapter.request_ids_mapping[req_id] = ext_id
entry = SimpleNamespace(request_id=req_id)
adapter._pending_load_reqs.append(entry)
adapter._registered_load_entries[req_id] = entry
adapter._pending_load_reqs.append(SimpleNamespace(request_id=req_id))
adapter._finished_load_reqs.add(req_id)
adapter._pending_ar_prompt_updates[req_id] = (
SimpleNamespace(request_id=req_id),
{},
False,
False,
False,
)
adapter._streaming_condition_lengths[req_id] = 10
adapter._streaming_condition_seqs[req_id] = 2
adapter.put_req_chunk[ext_id] = 5
adapter.request_payload[ext_id] = {"hidden": [1, 2]}
@@ -1725,71 +1145,12 @@ def test_cleanup_clears_all_state(build_adapter):
assert req_id not in adapter.request_ids_mapping
assert req_id in adapter._cancelled_load_reqs
assert req_id not in adapter._finished_load_reqs
assert req_id not in adapter._pending_ar_prompt_updates
assert req_id not in adapter._streaming_condition_lengths
assert req_id not in adapter._streaming_condition_seqs
assert req_id not in adapter._registered_load_entries
assert ext_id not in adapter.put_req_chunk
assert ext_id not in adapter.request_payload
assert ext_id not in adapter.code_prompt_token_ids
def test_cleanup_and_reregister_discards_inflight_old_segment_chunk(build_adapter):
"""A late old-segment chunk cannot overwrite a resumed registration."""
adapter, connector = build_adapter(stage_id=1, model_mode="ar")
request = _req("req-late", RequestStatus.RUNNING, external_req_id="ext-late")
request.resumable = True
entry = _dequeue_load_entry(adapter, request)
get_started = threading.Event()
release_get = threading.Event()
def blocking_get(*_args):
get_started.set()
assert release_get.wait(timeout=5)
return (
{
"ids": {"prompt": [1]},
"meta": {
"next_stage_prompt_len": 10,
},
},
1,
)
connector.get.side_effect = blocking_get
poll_results = []
poll_thread = threading.Thread(target=lambda: poll_results.append(adapter._poll_single_request(entry)))
poll_thread.start()
assert get_started.wait(timeout=5)
adapter.cleanup_receiver(request.request_id)
fresh_entry = _dequeue_load_entry(adapter, request)
release_get.set()
poll_thread.join(timeout=5)
assert not poll_thread.is_alive()
assert poll_results == [True]
assert request.additional_information is None
assert request.request_id not in adapter._pending_ar_prompt_updates
assert request.request_id not in adapter._streaming_condition_seqs
assert adapter.get_req_chunk[request.request_id] == 0
assert adapter._registered_load_entries[request.request_id] is fresh_entry
connector.get.side_effect = None
connector.get.return_value = (
{
"ids": {"prompt": [2]},
"meta": {"finished": True},
},
1,
)
assert adapter._poll_single_request(fresh_entry) is True
assert adapter.get_req_chunk[request.request_id] == 1
assert request.request_id not in adapter._registered_load_entries
def test_cleanup_infers_external_id(build_adapter):
"""When external_req_id is None, cleanup should look it up from the mapping."""
adapter, _ = build_adapter(stage_id=1)
@@ -1938,7 +1299,7 @@ def test_cleanup_after_poll_flow(build_adapter):
"meta": {"finished": torch.tensor(True, dtype=torch.bool)},
}
connector.get.return_value = (payload, 8)
adapter._poll_single_request(_dequeue_load_entry(adapter, request))
adapter._poll_single_request(request)
assert "req-flow" in adapter.upstream_exhausted_requests
assert adapter.get_req_chunk["req-flow"] == 1
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""GPU e2e for PersonaPlex elastic batching (bit-exact isolation + recycle).
Greedy decoding makes every cross-slot leak observable as a bit difference:
@@ -24,17 +24,12 @@ from __future__ import annotations
import numpy as np
import pytest
from tests.helpers.mark import hardware_marks
torch = pytest.importorskip("torch")
# slow + H100 + omni + cards_1 routes this module into weekly
# "Omni · H100 · Single-GPU" (test-weekly.yml).
pytestmark = [
pytest.mark.omni,
pytest.mark.slow,
*hardware_marks(res={"cuda": "H100"}),
]
# slow + H100 + omni routes this module into the weekly Buildkite E2E group
# ("Omni · H100" in .buildkite/cuda/test-weekly.yml: -m "slow and H100 and
# omni" --run-level full_model); the ready/merge CPU sweeps exclude it by mark.
pytestmark = [pytest.mark.full_model, pytest.mark.omni, pytest.mark.slow, pytest.mark.H100]
if not torch.cuda.is_available(): # noqa: E402
pytest.skip("needs CUDA", allow_module_level=True)
@@ -1,129 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""E2E tests for dots.tts native AR offline inference."""
from collections.abc import Mapping
import pytest
import torch
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
DOTS_TTS_MODEL = "rednote-hilab/dots.tts-soar"
DEPLOY_CONFIG = get_deploy_config_path("dots_tts.yaml")
SAMPLE_RATE = 48000
# dots.tts ships a custom tokenizer carrying the added ``<|audio_gen_start|>``
# token, so remote code must be explicitly enabled.
_OMNI_RUNNER_PARAM = (DOTS_TTS_MODEL, DEPLOY_CONFIG, {"trust_remote_code": True})
pytestmark = pytest.mark.parametrize("omni_runner", [_OMNI_RUNNER_PARAM], indirect=True)
def _build_prompt(omni_runner: OmniRunner, text: str) -> dict:
"""Wrap ``text`` in the prefix scaffold dots.tts prefill expects.
Unlike most TTS models here, a bare ``{"prompt": ...}`` string does not
work: prefill needs ``[文本]<text>[文本对应语音]<|audio_gen_start|>`` as
real Qwen2 token ids.
"""
from transformers import AutoTokenizer
from vllm_omni.model_executor.models.dots_tts.dots_tts_prompt import (
build_dots_tts_prompt,
)
tokenizer = AutoTokenizer.from_pretrained(omni_runner.model_name, trust_remote_code=True)
return build_dots_tts_prompt(tokenizer=tokenizer, text=text)
def _extract_audio(multimodal_output: dict) -> torch.Tensor:
"""Extract the final complete audio tensor from multimodal output."""
assert isinstance(multimodal_output, (dict, Mapping)), f"Expected dict/Mapping, got {type(multimodal_output)}"
# The talker emits delta chunks; the output processor accumulates them
# under "model_outputs" ("audio" kept for backwards compat).
audio = multimodal_output.get("model_outputs")
if audio is None:
audio = multimodal_output.get("audio")
assert audio is not None, f"No audio key, got {list(multimodal_output.keys())}"
if isinstance(audio, list):
valid = [torch.as_tensor(x).float().cpu().reshape(-1) for x in audio if x is not None]
assert valid, "No valid audio tensors in output list"
audio = torch.cat(valid, dim=0) if len(valid) > 1 else valid[0]
assert isinstance(audio, torch.Tensor), f"Expected Tensor, got {type(audio)}"
return audio.float().cpu().reshape(-1)
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_dots_tts_zero_shot_001(omni_runner: OmniRunner) -> None:
"""Zero-shot synthesis produces audio of a plausible duration."""
prompt = _build_prompt(omni_runner, "Hello, this is a test of dots TTS running on vLLM Omni.")
outputs = omni_runner.omni.generate([prompt])
assert len(outputs) == 1
audio = _extract_audio(outputs[0].outputs[0].multimodal_output)
duration_s = audio.shape[0] / SAMPLE_RATE
assert 0.5 < duration_s < 30.0, f"Audio duration out of range: {duration_s:.2f}s"
assert torch.isfinite(audio).all(), "Audio contains NaN/Inf"
assert audio.abs().max() > 1e-3, "Audio is silent"
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_dots_tts_prefill_decode_mixed_batch_002(omni_runner: OmniRunner) -> None:
"""Mixed prefill+decode batches must keep per-request rows aligned.
``compute_logits`` fills ``logits[i]`` positionally from
``_results_queue``, so every request in the batch has to push exactly one
entry per step, prefills included. When the prefill branch pushed nothing,
a shorter request's stop signal could be read into another request's row.
Regression guard for that pairing (PR #4765 review).
"""
long_text = (
"This is a deliberately long sentence that stays in the decode phase "
"for many steps, so the shorter requests behind it keep entering "
"prefill alongside it and reproduce the mixed batch pattern."
)
short_texts = ["Hello one.", "Hello two.", "Hello three.", "Hello four."]
prompts = [_build_prompt(omni_runner, long_text)] + [_build_prompt(omni_runner, t) for t in short_texts]
outputs = omni_runner.omni.generate(prompts)
assert len(outputs) == len(prompts)
for i, out in enumerate(outputs):
audio = _extract_audio(out.outputs[0].multimodal_output)
duration_s = audio.shape[0] / SAMPLE_RATE
assert 0.1 < duration_s < 30.0, f"Request {i} audio duration out of range: {duration_s:.2f}s"
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_dots_tts_deterministic_flow_matching_noise_003(omni_runner: OmniRunner) -> None:
"""The same text twice must yield the same audio.
The DiT flow-matching noise is drawn from a per-request generator seeded
from ``blake2b(seed:request_key:noise_step)`` rather than the global CUDA
RNG. Drawing from the global RNG made output non-reproducible and let
concurrent requests perturb each other's noise streams (PR #4765 review).
"""
text = "Reading aloud every evening helps children build a lasting love of language."
first = _extract_audio(
omni_runner.omni.generate([_build_prompt(omni_runner, text)])[0].outputs[0].multimodal_output
)
second = _extract_audio(
omni_runner.omni.generate([_build_prompt(omni_runner, text)])[0].outputs[0].multimodal_output
)
assert first.shape == second.shape, f"Length differs across runs: {first.shape} vs {second.shape}"
assert torch.allclose(first, second, atol=1e-4), "Audio differs across runs; flow-matching noise is not seeded"
@@ -1,13 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Smoke test for HunyuanImage-3.0 Image-to-Text (I2T) pipeline."""
from collections.abc import Generator
import pytest
import torch
from PIL import Image
from tests.helpers.mark import hardware_test
from tests.helpers.runtime import OmniRunner
from tests.helpers.stage_config import get_deploy_config_path
from vllm_omni import Omni
@@ -44,7 +44,7 @@ EXPECTED_PREFIX_TOKEN_IDS: list[int] = [
# Decoded form, kept only for human-readable assertion messages.
EXPECTED_PREFIX_TEXT = "The image is a solid, uniform green color with no variations, objects, or details present. It"
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion]
pytestmark = [pytest.mark.full_model, pytest.mark.diffusion, pytest.mark.cuda]
@pytest.fixture(scope="module")
@@ -57,7 +57,7 @@ def omni() -> Generator[Omni, None, None]:
yield runner.omni
@hardware_test(res={"cuda": "H100"}, num_cards=4)
@pytest.mark.skipif(torch.accelerator.device_count() < 4, reason="Need at least 4 CUDA GPUs.")
def test_i2t_generates_text(omni: Omni) -> None:
"""Verify I2T output's first 20 token IDs match the HF greedy baseline."""
# Solid-color image keeps the input self-contained and reproducible.
@@ -91,8 +91,8 @@ def test_krea2_cache_dit(offline_client: OfflineOmniClient) -> 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
shard by ``num_cards=2`` (``not cards_1``); skipped automatically on boxes with fewer than 2 CUDA devices.
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.
"""
offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
@@ -111,7 +111,7 @@ 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 shard by ``num_cards=2`` (``not cards_1``); skipped automatically on boxes with
multi-GPU (distributed_cuda) shard by ``num_cards=2``; skipped automatically on boxes with
fewer than 2 CUDA devices.
"""
offline_client.send_diffusion_request({"model": MODEL, "prompt": PROMPT, "sampling_params": _sampling()})
@@ -3,9 +3,6 @@
"""
E2E offline tests for Omni model with video input and audio output.
Abort / sleep-admission lives in ``test_qwen3_omni_colocate_async.py`` so it
does not overlap this module's OmniRunners.
"""
import os
@@ -1,182 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Isolated AsyncOmni abort / sleep-admission regression for Qwen3-Omni.
This lives in its own module so the L3 merge job can run it *after*
``test_qwen3_omni.py`` (offline) and the online serving file. Those modules keep
module-scoped OmniRunner / OmniServer instances until teardown; overlapping a
CuMem sleep-mode engine with them kills StageEngineCoreProc at handshake
(``Failed core proc(s): {}``).
"""
import asyncio
import os
from contextlib import ExitStack
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
import pytest
from vllm import SamplingParams
from vllm.inputs import TokensPrompt
from tests.helpers.clean import wait_for_gpu_memory_to_clear
from tests.helpers.mark import hardware_test
from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
from vllm_omni.entrypoints.async_omni import AsyncOmni
from vllm_omni.platforms import current_omni_platform
MODEL = "Qwen/Qwen3-Omni-30B-A3B-Instruct"
def _output_token_ids(output) -> list[int]:
if not getattr(output, "outputs", None):
return []
completion = output.outputs[0]
token_ids = list(completion.token_ids or [])
if token_ids:
return token_ids
return list(getattr(completion, "cumulative_token_ids", None) or [])
def _finish_reason(output) -> str | None:
if not getattr(output, "outputs", None):
return None
return getattr(output.outputs[0], "finish_reason", None)
def _colocate_async_deploy() -> str:
"""Thinker-only Instruct deploy with sleep mode on a single GPU.
Abort / sleep admission is a thinker control-plane contract. The 3-stage
CI yaml plus CuMem on every stage fails engine-core startup in the L3
merge job: Stage 1 resolves ``devices: "1"`` while only GPU 0 is visible.
"""
return modify_stage_config(
get_deploy_config_path("qwen3_omni_moe_thinking.yaml"),
updates={
"stages": {
0: {
"enable_sleep_mode": True,
"enforce_eager": True,
"trust_remote_code": True,
"tensor_parallel_size": 1,
"devices": "0",
"gpu_memory_utilization": 0.9,
"max_num_seqs": 1,
"max_model_len": 8192,
"max_num_batched_tokens": 8192,
"enable_prefix_caching": False,
"skip_mm_profiling": True,
},
},
},
)
@pytest.mark.advanced_model
@pytest.mark.omni
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=1)
@pytest.mark.usefixtures("clean_gpu_memory_between_tests")
@pytest.mark.asyncio
async def test_colocate_async_abort_tokens_and_sleep_admission() -> None:
"""**required** regression. Control-plane APIs (``abort`` / ``sleep`` /
``wake_up`` / ``resume_generation``) are not in ``send_omni_request``.
On ``origin/main`` this test fails:
- abort: ``generate()`` never yields ``finish_reason="abort"`` with the
tokens produced so far, so resume has an empty prefix (or the stream
hangs after frontend state is dropped).
- sleep: ``generate()`` is admitted into EngineCore while weights/KV are
offloaded, so the task errors (asleep / corrupted ADD frame) instead of
waiting for ``resume_generation()``.
On this PR both legs pass: abort returns a resumeable prefix, and sleep
holds ``generate()`` until wake + resume.
"""
prompt = "What color is the sky? Write a long, detailed explanation."
device_count = current_omni_platform.device_count()
if device_count > 0:
# Fail closed if a prior module-scoped runner is still on the GPU.
# ``clean_gpu_memory_between_tests`` only logs a note on timeout.
wait_for_gpu_memory_to_clear(
devices=list(range(device_count)),
threshold_ratio=0.15,
timeout_s=120,
)
with ExitStack() as after:
engine = AsyncOmni(
model=MODEL,
deploy_config=_colocate_async_deploy(),
enable_sleep_mode=True,
)
after.callback(engine.shutdown)
request_id = "qwen3-abort-partial"
outputs: list = []
async def _generate(max_tokens: int, req_id: str, gen_prompt) -> None:
async for output in engine.generate(
prompt=gen_prompt,
request_id=req_id,
sampling_params=SamplingParams(temperature=0.0, max_tokens=max_tokens),
output_modalities=["text"],
):
outputs.append(output)
abort_task = asyncio.create_task(_generate(256, request_id, prompt))
prefix: list[int] = []
prompt_token_ids: list[int] = []
for _ in range(600):
if abort_task.done():
break
if outputs:
latest = outputs[-1]
prefix = _output_token_ids(latest)
prompt_token_ids = list(getattr(latest, "prompt_token_ids", None) or [])
if prefix:
break
await asyncio.sleep(0.1)
assert prefix, "generate produced no tokens before abort"
assert not abort_task.done(), "generate finished before abort; raise max_tokens"
await engine.abort(request_id)
await asyncio.wait_for(abort_task, timeout=90)
final = outputs[-1]
assert final.finished
assert _finish_reason(final) == "abort"
abort_prefix = _output_token_ids(final)
assert abort_prefix, "abort dropped the generated prefix (main-branch behavior)"
resume_prompt: str | TokensPrompt = (
TokensPrompt(prompt_token_ids=prompt_token_ids + abort_prefix) if prompt_token_ids else prompt
)
outputs.clear()
async for output in engine.generate(
prompt=resume_prompt,
request_id="qwen3-abort-resume",
sampling_params=SamplingParams(temperature=0.0, max_tokens=8),
output_modalities=["text"],
):
outputs.append(output)
assert outputs
assert any(_output_token_ids(out) for out in outputs)
# Trainer order is pause → abort → sleep. Pause here after the
# resume generate so EngineCore is idle before CuMem offload.
await engine.pause_generation(mode="abort", clear_cache=True)
await engine.sleep(level=1)
outputs.clear()
sleep_task = asyncio.create_task(_generate(8, "qwen3-sleep-admission", prompt))
await asyncio.sleep(1.0)
assert not sleep_task.done(), "generate() ran while EngineCore was sleeping (main-branch admission race)"
await engine.wake_up()
await asyncio.sleep(0.5)
assert not sleep_task.done(), "generate() resumed before resume_generation()"
await engine.resume_generation()
await asyncio.wait_for(sleep_task, timeout=180)
assert outputs
assert _finish_reason(outputs[-1]) != "abort"
await asyncio.sleep(5)
@@ -101,8 +101,7 @@ def duplex_camera_frames(*, seconds: int, cache_dir: Path) -> list[str]:
from tests.helpers.media import generate_synthetic_video
video = generate_synthetic_video(448, 448, 30 * seconds, cache_dir=cache_dir)
frames, _ = _video_frames_from_file(Path(video["file_path"]))
return frames
return _video_frames_from_file(Path(video["file_path"]))
def realtime_url(omni_server) -> str:
@@ -252,19 +252,6 @@ def summarize_artifacts(
(summary["done_indices"][0] for summary in reversed(response_summaries) if summary.get("done_indices")),
None,
)
# A soft interrupt's follow-up response can drain past the commit (residual
# model unit), so its response.done lands after input_audio_buffer.committed.
# Anchor the "listen after last done, before commit" sandwich on the last
# turn that closed its floor strictly before the commit, not the
# globally-last done (which may sit after the commit and leave the interval empty).
last_done_before_commit_index = next(
(
summary["done_indices"][0]
for summary in reversed(response_summaries)
if summary.get("done_indices") and commit_index is not None and summary["done_indices"][0] < commit_index
),
None,
)
listen_indices = [index for index, event in enumerate(events) if event.get("type") == "response.listen"]
effective_min_responses = min_responses if validation_mode == "response-required" else 1
enough_responses = len(response_summaries) >= effective_min_responses
@@ -292,9 +279,9 @@ def summarize_artifacts(
)
listen_after_last_done = last_done_index is not None and any(index > last_done_index for index in listen_indices)
listen_after_response_before_commit = (
last_done_before_commit_index is not None
last_done_index is not None
and commit_index is not None
and any(last_done_before_commit_index < index < commit_index for index in listen_indices)
and any(last_done_index < index < commit_index for index in listen_indices)
)
final_listen_after_commit = commit_index is not None and any(index > commit_index for index in listen_indices)
transcript = "".join(str(summary.get("transcript") or "") for summary in response_summaries)
+27 -20
View File
@@ -48,6 +48,10 @@ DEFAULT_SPEECH_TIMEOUT_S = 180.0
# silence-only outputs without flagging short legitimate clips.
_MIN_AUDIO_BYTES = 20_000
_SKIP_ISSUE_6418 = pytest.mark.skip(
reason="https://github.com/vllm-project/vllm-omni/issues/6418",
)
# 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.
@@ -61,7 +65,7 @@ class TestHiggsAudioV3OnlineHappyPath:
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_wav(self, omni_server, online_client) -> None:
"""Single non-streaming WAV request - canonical TTS happy path."""
online_client.send_audio_speech_request(
@@ -78,9 +82,10 @@ class TestHiggsAudioV3OnlineHappyPath:
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
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."""
online_client.send_audio_speech_request(
@@ -90,14 +95,15 @@ class TestHiggsAudioV3OnlineHappyPath:
"stream": False,
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"max_new_tokens": 1024,
"max_new_tokens": 32,
"min_audio_bytes": _MIN_AUDIO_BYTES,
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
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."""
online_client.send_audio_speech_request(
@@ -113,7 +119,7 @@ class TestHiggsAudioV3OnlineHappyPath:
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_plain_text_pcm_streaming(self, omni_server, online_client) -> None:
"""Streaming PCM via talker2code2wav_async_chunk + the connector's codec_streaming path.
@@ -144,7 +150,7 @@ class TestHiggsAudioV3OnlineHappyPath:
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
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.
@@ -179,13 +185,15 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
tokens (pause, sfx) go inline. SFX tokens must pair with their written
onomatopoeia.
These tests check the serving surface and spoken text. They do not assert
whether the requested control changes audio quality.
These tests check the *serving surface* - the validator accepts the
payload, the engine produces audio, and the WAV has non-trivial size.
They do not assert audio quality (out of scope for CI).
"""
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_emotion_and_expressive(self, omni_server, online_client) -> None:
"""Delivery tokens (emotion + expressive_high) at the start of input."""
online_client.send_audio_speech_request(
@@ -196,13 +204,13 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"transcript_expected_text": "Wait, that was actually hilarious.",
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_style_whispering(self, omni_server, online_client) -> None:
"""Style token at the start - ``whispering``."""
online_client.send_audio_speech_request(
@@ -213,13 +221,13 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"transcript_expected_text": "It is just between you and me, alright?",
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_prosody_speed_and_pitch(self, omni_server, online_client) -> None:
"""Two prosody tokens at the start - slow speed plus low pitch."""
online_client.send_audio_speech_request(
@@ -232,13 +240,13 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"transcript_expected_text": "The radar shows a storm approaching from the east.",
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_pause_mid_text(self, omni_server, online_client) -> None:
"""Positional pause token placed inline between two clauses."""
online_client.send_audio_speech_request(
@@ -249,13 +257,13 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"transcript_expected_text": "Hold on a moment. Let me think about it.",
}
)
@_SKIP_ISSUE_6418
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_inline_sfx_with_onomatopoeia(self, omni_server, online_client) -> None:
"""SFX token paired with its written onomatopoeia (``<|sfx:laughter|>Hehe``)."""
online_client.send_audio_speech_request(
@@ -269,7 +277,6 @@ class TestHiggsAudioV3OnlineInlineControlTokens:
"response_format": "wav",
"timeout": DEFAULT_SPEECH_TIMEOUT_S,
"min_audio_bytes": _MIN_AUDIO_BYTES,
"transcript_expected_text": "I cannot believe that just happened. I am still recovering from it.",
}
)
@@ -280,7 +287,7 @@ class TestHiggsAudioV3OnlineVoiceClone:
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_ref_audio_ref_text(self, omni_server, online_client) -> None:
"""Canonical vllm-omni voice clone via ``ref_audio`` + ``ref_text``."""
online_client.send_audio_speech_request(
@@ -297,7 +304,7 @@ class TestHiggsAudioV3OnlineVoiceClone:
@pytest.mark.slow
@pytest.mark.tts
@hardware_test(res={"cuda": "L4", "rocm": "MI325"}, num_cards=1)
@hardware_test(res={"cuda": "L4"}, num_cards=1)
def test_voice_clone_references_alias(self, omni_server, online_client) -> None:
"""BosonAI cookbook payload: ``references=[{audio_path, text}]``.
@@ -1,7 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
E2E expansion tests for MiMo-Audio online serving (nightly Omni · Function Test with H100 · Single-GPU).
E2E expansion tests for MiMo-Audio online serving (nightly Omni · Function Test with H100).
"""
import os
@@ -540,111 +540,6 @@ def test_realtime_duplex_soft_interrupt_accepts_multi_delta_handoff_sequence(tmp
assert summary["response_summaries"][1]["transcript"] == "一加一等于二。"
def test_realtime_duplex_soft_interrupt_accepts_followup_done_after_commit(tmp_path):
"""A follow-up response can drain a residual model unit past the final commit.
Sequence: ``r1.done -> listen -> r2.created -> ... -> commit -> r2.done -> listen``.
The globally-last ``response.done`` (r2) lands *after* the commit, so anchoring
``listen_after_response_before_commit`` on it leaves the ``(last_done, commit)``
interval empty and fails the contract even though the turn yielded the floor
correctly. The check must anchor on the last done strictly before the commit
(r1.done), with the listen at 3.1 s satisfying the sandwich.
"""
demo = _load_soft_interrupt_demo_module()
output = tmp_path / "soft_interrupt_residual_drain"
output.mkdir()
first_response_id = "resp-first"
second_response_id = "resp-second"
events = [
{"type": "response.listen", "_client_received_at_s": 1.0},
{"type": "response.created", "response": {"id": first_response_id}, "_client_received_at_s": 2.0},
{"type": "response.speak", "response_id": first_response_id, "_client_received_at_s": 2.0},
{
"type": "response.audio.delta",
"response_id": first_response_id,
"delta": "AAAA",
"_client_received_at_s": 2.1,
},
{
"type": "response.audio_transcript.delta",
"response_id": first_response_id,
"delta": "中国古代四大发明",
"_client_received_at_s": 2.1,
},
{
"type": "response.audio.delta",
"response_id": first_response_id,
"delta": "AAAA",
"_client_received_at_s": 2.6,
},
{
"type": "response.audio_transcript.delta",
"response_id": first_response_id,
"delta": "是造纸术。",
"_client_received_at_s": 2.6,
},
{"type": "response.done", "response_id": first_response_id, "_client_received_at_s": 3.0},
{"type": "response.listen", "_client_received_at_s": 3.1},
{"type": "response.created", "response": {"id": second_response_id}, "_client_received_at_s": 3.5},
{"type": "response.speak", "response_id": second_response_id, "_client_received_at_s": 3.5},
{
"type": "response.audio.delta",
"response_id": second_response_id,
"delta": "AAAA",
"_client_received_at_s": 3.6,
},
{
"type": "response.audio_transcript.delta",
"response_id": second_response_id,
"delta": "一加一等于",
"_client_received_at_s": 3.6,
},
{
"type": "response.audio.delta",
"response_id": second_response_id,
"delta": "AAAA",
"_client_received_at_s": 3.7,
},
{
"type": "response.audio_transcript.delta",
"response_id": second_response_id,
"delta": "二。",
"_client_received_at_s": 3.7,
},
# The final commit lands while the follow-up response is still draining.
{"type": "input_audio_buffer.committed", "_client_received_at_s": 4.0},
{"type": "response.done", "response_id": second_response_id, "_client_received_at_s": 4.5},
{"type": "response.listen", "_client_received_at_s": 4.6},
]
(output / "events.jsonl").write_text(
"".join(demo.json.dumps(event) + "\n" for event in events),
encoding="utf-8",
)
(output / "result.json").write_text(
demo.json.dumps({"ok": True, "response_ids": [first_response_id, second_response_id]}),
encoding="utf-8",
)
summary = demo.summarize_artifacts(
output_dir=output,
validation_mode="response-required",
min_responses=2,
min_audio_deltas_per_response=2,
expect_followup_response_substring="一加一等于二",
)
assert summary["ok"] is True
# Anchored on r1.done (the last done before the commit), not the late r2.done.
assert summary["listen_after_response_before_commit"] is True
# listen_after_last_done still keys off the globally-last done, hence the final listen.
assert summary["listen_after_last_done"] is True
assert summary["final_listen_after_commit"] is True
assert summary["second_response_before_final_commit"] is True
assert summary["followup_response_transcript_ok"] is True
assert summary["response_summaries"][1]["transcript"] == "一加一等于二。"
def test_realtime_duplex_soft_interrupt_model_policy_accepts_single_response(tmp_path):
demo = _load_soft_interrupt_demo_module()
output = tmp_path / "model_policy"
@@ -2031,28 +1926,6 @@ def test_realtime_duplex_demo_sends_each_units_stacked_composite_next_to_its_bas
assert _sent_video_frames(messages) == [["f0", "s0"], ["f1", "s1"], ["f2"]]
def test_minicpmo_duplex_camera_fixture_returns_flat_base_frame_track(tmp_path, monkeypatch):
from tests.e2e.online_serving.helpers import minicpmo_4_5_duplex as fixtures
from tests.e2e.online_serving.helpers import minicpmo_realtime_duplex_scenarios as demo
from tests.helpers import media
video_path = tmp_path / "camera.mp4"
monkeypatch.setattr(
media,
"generate_synthetic_video",
lambda *args, **kwargs: {"file_path": str(video_path)},
)
monkeypatch.setattr(
demo,
"_video_frames_from_file",
lambda path: (["frame-0", "frame-1"], [None, None]),
)
frames = fixtures.duplex_camera_frames(seconds=2, cache_dir=tmp_path)
assert frames == ["frame-0", "frame-1"]
def test_realtime_duplex_demo_decodes_a_video_file_into_one_frame_per_second(tmp_path):
pytest.importorskip("cv2")
pytest.importorskip("imageio")
@@ -67,8 +67,7 @@ def test_abort_async_waits_for_ack(mocker: MockerFixture):
assert not task.done()
rpc_q.put(AbortResultMessage(rpc_id="abort-rpc-1", success=True))
result = await task
assert result == []
await task
try:
asyncio.run(_run())
@@ -4,7 +4,6 @@
import concurrent.futures
import contextlib
import importlib
import json
import os
import time
import types
@@ -1094,8 +1093,6 @@ def test_build_engine_args_cli_tokenizer_overrides_inferred_base_tokenizer(tmp_p
default_sampling_params={},
)
(tmp_path / "llm").mkdir()
engine_args = build_engine_args_dict(
stage_cfg,
str(tmp_path),
@@ -1157,9 +1154,6 @@ def test_build_engine_args_keeps_stage_owned_tokenizer_subdir(tmp_path):
default_sampling_params={},
)
(tmp_path / "llm").mkdir()
(tmp_path / "tokenizer").mkdir()
engine_args = build_engine_args_dict(
stage_cfg,
str(tmp_path),
@@ -1170,430 +1164,6 @@ def test_build_engine_args_keeps_stage_owned_tokenizer_subdir(tmp_path):
assert engine_args["tokenizer"] == os.path.join(str(tmp_path), "tokenizer")
def test_build_engine_args_rejects_missing_subdir_in_local_model_dir(tmp_path):
"""A local model directory without the stage subfolder must fail closed.
Forwarding the joined path as ``EngineArgs.model`` sends a non-directory
string to HuggingFace, which reports it as a malformed repo id.
"""
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
(tmp_path / "tokenizer").mkdir()
with pytest.raises(RuntimeError, match="language_model"):
build_engine_args_dict(stage_cfg, str(tmp_path))
# A real snapshot subfolder always carries artifacts; empty directories would
# assert the broken exists-means-complete predicate this suite regresses.
_SUBDIR_ARTIFACT = {"language_model": "model.safetensors", "tokenizer": "tokenizer.json"}
def _make_snapshot(root, subdirs):
for subdir in subdirs:
folder = root / subdir
folder.mkdir(parents=True, exist_ok=True)
(folder / _SUBDIR_ARTIFACT.get(subdir, "data.bin")).write_text("x")
return str(root)
def test_build_engine_args_pulls_stage_subdirs_missing_from_cached_snapshot(monkeypatch, tmp_path):
"""Regression for issue #6638.
``snapshot_download(local_files_only=True)`` hands back a snapshot root as
soon as the repo is partly cached, even when this stage's subfolders were
never materialized. Stage init must fetch exactly those subfolders instead
of joining onto a path that exists nowhere.
"""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
_make_snapshot(snapshot, ["tokenizer"])
calls = []
def fake_snapshot_download(self, repo_id, **kwargs):
calls.append(kwargs)
if kwargs.get("local_files_only"):
return str(snapshot)
_make_snapshot(snapshot, ["language_model"])
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert engine_args["model"] == os.path.join(str(snapshot), "language_model")
assert engine_args["tokenizer"] == os.path.join(str(snapshot), "tokenizer")
assert os.path.isdir(engine_args["model"])
# Only the stage's own subfolders are pulled, not the whole repo.
assert calls[-1]["allow_patterns"] == ["language_model/*", "tokenizer/*"]
def test_build_engine_args_skips_hub_call_when_cached_snapshot_is_complete(monkeypatch, tmp_path):
"""A warm cache stays offline-friendly: no outgoing Hub request."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
_make_snapshot(snapshot, ["language_model", "tokenizer"])
def fake_snapshot_download(self, repo_id, **kwargs):
assert kwargs.get("local_files_only"), "warm cache must not reach the Hub"
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert engine_args["model"] == os.path.join(str(snapshot), "language_model")
def test_build_engine_args_redownloads_a_partial_subdir(monkeypatch, tmp_path):
"""A subfolder holding only config.json is an interrupted download.
The warm-cache early return must not accept it: converting the Hub ID into
that local path leaves vLLM's loader without any fallback for the missing
weights.
"""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
partial = snapshot / "language_model"
partial.mkdir(parents=True)
(partial / "config.json").write_text("{}")
_make_snapshot(snapshot, ["tokenizer"])
calls = []
def fake_snapshot_download(self, repo_id, **kwargs):
calls.append(kwargs)
if kwargs.get("local_files_only"):
return str(snapshot)
_make_snapshot(snapshot, ["language_model"])
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert any(not call.get("local_files_only") for call in calls), "a partial subdir must fall through to the Hub"
assert engine_args["model"] == os.path.join(str(snapshot), "language_model")
def test_build_engine_args_redownloads_when_index_lists_missing_shards(monkeypatch, tmp_path):
"""An index plus a subset of its shards is an interrupted download.
The index downloads early; accepting it as proof of weights converts the
Hub ID into a local path vLLM cannot fetch the remaining shards for.
"""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
partial = snapshot / "language_model"
partial.mkdir(parents=True)
(partial / "model.safetensors.index.json").write_text(
json.dumps(
{
"weight_map": {
"a.weight": "model-00001-of-00002.safetensors",
"b.weight": "model-00002-of-00002.safetensors",
}
}
)
)
(partial / "model-00001-of-00002.safetensors").write_text("x")
_make_snapshot(snapshot, ["tokenizer"])
calls = []
def fake_snapshot_download(self, repo_id, **kwargs):
calls.append(kwargs)
if not kwargs.get("local_files_only"):
(partial / "model-00002-of-00002.safetensors").write_text("x")
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert any(not call.get("local_files_only") for call in calls), "missing shards must fall through to the Hub"
assert engine_args["model"] == os.path.join(str(snapshot), "language_model")
def test_build_engine_args_redownloads_shards_without_their_index(monkeypatch, tmp_path):
"""Shard-named weights always ship an index; one without it is partial."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
partial = snapshot / "language_model"
partial.mkdir(parents=True)
(partial / "model-00001-of-00004.safetensors").write_text("x")
_make_snapshot(snapshot, ["tokenizer"])
calls = []
def fake_snapshot_download(self, repo_id, **kwargs):
calls.append(kwargs)
if not kwargs.get("local_files_only"):
shards = {f"w{i}.weight": f"model-0000{i}-of-00004.safetensors" for i in range(1, 5)}
(partial / "model.safetensors.index.json").write_text(json.dumps({"weight_map": shards}))
for shard in shards.values():
(partial / shard).write_text("x")
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert any(not call.get("local_files_only") for call in calls)
def test_build_engine_args_redownloads_a_tokenizer_folder_without_vocabulary(monkeypatch, tmp_path):
"""Templates and configs download first; alone they are not a tokenizer."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
_make_snapshot(snapshot, ["language_model"])
partial = snapshot / "tokenizer"
partial.mkdir(parents=True)
(partial / "chat_template.jinja").write_text("{{ messages }}")
calls = []
def fake_snapshot_download(self, repo_id, **kwargs):
calls.append(kwargs)
if not kwargs.get("local_files_only"):
_make_snapshot(snapshot, ["tokenizer"])
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert any(not call.get("local_files_only") for call in calls)
assert engine_args["tokenizer"] == os.path.join(str(snapshot), "tokenizer")
def test_build_engine_args_forwards_revision_and_download_dir(monkeypatch, tmp_path):
"""revision/download_dir must shape snapshot selection (they cannot be
corrected downstream once the repo ID is a local path)."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "pinned"
_make_snapshot(snapshot, ["language_model", "tokenizer"])
seen = []
def fake_snapshot_download(self, repo_id, **kwargs):
seen.append(kwargs)
return str(snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={
"model_subdir": "language_model",
"tokenizer_subdir": "tokenizer",
"revision": "v2.0",
"download_dir": str(tmp_path / "cache"),
},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert engine_args["model"] == os.path.join(str(snapshot), "language_model")
assert seen, "snapshot resolution must have been consulted"
for call in seen:
assert call.get("revision") == "v2.0"
assert call.get("cache_dir") == str(tmp_path / "cache")
def test_build_engine_args_resolves_tokenizer_revision_separately(monkeypatch, tmp_path):
"""A tokenizer pinned to a different revision resolves against its own snapshot."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
model_snapshot = tmp_path / "snapshots" / "model-rev"
_make_snapshot(model_snapshot, ["language_model"])
tokenizer_snapshot = tmp_path / "snapshots" / "tok-rev"
_make_snapshot(tokenizer_snapshot, ["tokenizer"])
def fake_snapshot_download(self, repo_id, **kwargs):
if kwargs.get("revision") == "tok-rev":
return str(tokenizer_snapshot)
return str(model_snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={
"model_subdir": "language_model",
"tokenizer_subdir": "tokenizer",
"revision": "model-rev",
"tokenizer_revision": "tok-rev",
},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert engine_args["model"] == os.path.join(str(model_snapshot), "language_model")
assert engine_args["tokenizer"] == os.path.join(str(tokenizer_snapshot), "tokenizer")
def test_build_engine_args_resolves_root_tokenizer_revision_separately(monkeypatch, tmp_path):
"""An empty tokenizer_subdir targets the snapshot root and still honors
its own revision."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
model_snapshot = tmp_path / "snapshots" / "model-rev"
_make_snapshot(model_snapshot, ["language_model"])
# An empty tokenizer_subdir means the tokenizer IS the snapshot root, so
# the vocabulary artifact must sit at the root. Building it under
# ``tokenizer/`` returned a root that holds no loadable tokenizer, which
# the root-tokenizer validation now correctly rejects.
tokenizer_snapshot = tmp_path / "snapshots" / "tok-rev"
tokenizer_snapshot.mkdir(parents=True, exist_ok=True)
(tokenizer_snapshot / _SUBDIR_ARTIFACT["tokenizer"]).write_text("x")
def fake_snapshot_download(self, repo_id, **kwargs):
if kwargs.get("revision") == "tok-rev":
return str(tokenizer_snapshot)
return str(model_snapshot)
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={
"model_subdir": "language_model",
"tokenizer_subdir": "",
"revision": "model-rev",
"tokenizer_revision": "tok-rev",
},
default_sampling_params={},
)
engine_args = build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
assert engine_args["model"] == os.path.join(str(model_snapshot), "language_model")
assert engine_args["tokenizer"] == str(tokenizer_snapshot)
def test_build_engine_args_fails_closed_when_subdir_cannot_be_downloaded(monkeypatch, tmp_path):
"""An undownloadable subfolder raises here instead of reaching HuggingFace."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
snapshot = tmp_path / "snapshots" / "deadbeef"
_make_snapshot(snapshot, ["tokenizer"])
def fake_snapshot_download(self, repo_id, **kwargs):
if kwargs.get("local_files_only"):
return str(snapshot)
raise OSError("offline")
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
with pytest.raises(RuntimeError, match="language_model"):
build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
def test_build_engine_args_fails_closed_on_cold_cache_instead_of_joining_repo_id(monkeypatch):
"""With nothing cached, the stage must not join a subdir onto the repo id."""
from huggingface_hub import HfApi
from vllm_omni.engine.stage_init_utils import build_engine_args_dict
def fake_snapshot_download(self, repo_id, **kwargs):
raise OSError("offline")
monkeypatch.setattr(HfApi, "snapshot_download", fake_snapshot_download)
stage_cfg = types.SimpleNamespace(
stage_id=0,
stage_type="llm",
engine_args={"model_subdir": "language_model", "tokenizer_subdir": "tokenizer"},
default_sampling_params={},
)
with pytest.raises(RuntimeError, match="nothing is cached locally"):
build_engine_args_dict(stage_cfg, "MiniMaxAI/MiniMax-Music3")
def test_model_path_resolver_is_generic_and_model_owned(tmp_path):
from vllm_omni.engine.stage_init_utils import _resolve_model_path
+5 -82
View File
@@ -35,7 +35,6 @@ from vllm_omni.engine.messages import (
from vllm_omni.engine.orchestrator import (
Orchestrator,
OrchestratorRequestState,
StreamingSegmentState,
_build_terminal_empty_output,
)
from vllm_omni.engine.stage_pool import StagePool
@@ -257,32 +256,8 @@ class FakeOutputProcessor:
)
def abort_requests(self, request_ids, internal: bool = False):
aborted_ids, _outputs = self.abort_requests_collecting_outputs(request_ids, internal=internal)
return aborted_ids
def abort_requests_collecting_outputs(self, request_ids, *, internal: bool = False, commit_state: bool = True):
"""Mirror MultimodalOutputProcessor collecting for AR abort-prefix tests."""
del internal, commit_state
ids = list(request_ids)
self.abort_calls.append(ids)
outputs: list[RequestOutput] = []
for rid in ids:
seeded = next(
(ro for ro in self.request_outputs if getattr(ro, "request_id", None) == rid),
None,
)
token_ids = list(seeded.outputs[0].token_ids) if seeded is not None and seeded.outputs else [1, 2]
# Intentionally stamp an internal-looking id on the RequestOutput so
# StagePool must re-key by the orchestrator id it passed in.
outputs.append(
_build_request_output(
f"engine-internal-{rid}",
token_ids=token_ids,
finished=True,
finish_reason="abort",
)
)
return ids, outputs
self.abort_calls.append(request_ids)
return request_ids
def update_scheduler_stats(self, _scheduler_stats) -> None:
return None
@@ -345,7 +320,6 @@ def _build_request_output(
prompt_token_ids: list[int] | None = None,
finished: bool = True,
text: str = "test",
finish_reason: str | None = None,
) -> RequestOutput:
completion = CompletionOutput(
index=0,
@@ -353,7 +327,7 @@ def _build_request_output(
token_ids=list(token_ids or [1, 2]),
cumulative_logprob=0.0,
logprobs=None,
finish_reason=(finish_reason if finish_reason is not None else ("stop" if finished else None)),
finish_reason="stop" if finished else None,
stop_reason=None,
)
return RequestOutput(
@@ -1009,11 +983,7 @@ async def test_run_abort(orchestrator_factory) -> None:
async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> None:
stages = [
FakeStageClient(stage_type="llm", final_output=False),
FakeStageClient(
stage_type="llm",
final_output=True,
next_inputs=[{"prompt_token_ids": [7, 8, 9]}],
),
FakeStageClient(stage_type="llm", final_output=True),
]
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-ack", token_ids=[1], finished=True)]),
@@ -1032,11 +1002,6 @@ async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> N
final_stage_id=1,
)
await _wait_for(lambda: len(stages[0].add_request_calls) == 1)
# Abort outputs come from the final AR stage's output processor, and
# StagePool.abort_requests only collects for requests with a live
# replica binding. Forward off stage-0 first so stage-1 is bound.
stages[0].push_engine_core_outputs(_engine_core_outputs("stage0-raw", 1.0))
await _wait_for(lambda: len(stages[1].add_request_calls) == 1)
orchestrator_fixture.request_sync_q.put_nowait(AbortRequestMessage(request_ids=["req-ack"], rpc_id="abort-1"))
result = await _get_rpc_message(orchestrator_fixture)
@@ -1046,53 +1011,11 @@ async def test_run_abort_emits_result_when_rpc_id_set(orchestrator_factory) -> N
assert result.error is None
assert result.rpc_correlation_key == ("abort", "abort-1")
assert stages[0].abort_calls == [["req-ack"]]
assert stages[1].abort_calls == [["req-ack"]]
assert "req-ack" not in orchestrator_fixture.orchestrator.request_states
assert result.abort_outputs is not None
assert len(result.abort_outputs) == 1
abort_msg = result.abort_outputs[0]
assert abort_msg.request_id == "req-ack"
assert abort_msg.finished is True
assert abort_msg.stage_id == 1
assert list(abort_msg.engine_outputs.outputs[0].token_ids) == [2]
assert abort_msg.engine_outputs.outputs[0].finish_reason == "abort"
finally:
await _shutdown_orchestrator(orchestrator_fixture)
@pytest.mark.asyncio
async def test_abort_marks_only_last_final_stage_output_finished() -> None:
"""A request with two final AR stages must keep earlier abort outputs unfinished."""
stages = [
FakeStageClient(stage_type="llm", final_output=True),
FakeStageClient(stage_type="llm", final_output=True),
]
processors = [
FakeOutputProcessor(request_outputs=[_build_request_output("req-two", token_ids=[1], finished=True)]),
FakeOutputProcessor(request_outputs=[_build_request_output("req-two", token_ids=[2], finished=True)]),
]
pools = _build_stage_pools([[stages[0]], [stages[1]]], output_processors=processors)
pools[0]._request_bindings["req-two"] = 0
pools[1]._request_bindings["req-two"] = 0
orchestrator = Orchestrator(
request_async_queue=asyncio.Queue(),
output_async_queue=asyncio.Queue(),
rpc_async_queue=asyncio.Queue(),
stage_pools=pools,
)
orchestrator.request_states["req-two"] = OrchestratorRequestState(
request_id="req-two",
final_stage_id=1,
final_output_stage_ids={0, 1},
)
outputs = await orchestrator._abort_request_ids(["req-two"])
assert [(msg.stage_id, msg.finished) for msg in outputs] == [(0, False), (1, True)]
assert list(outputs[0].engine_outputs.outputs[0].token_ids) == [1]
assert list(outputs[1].engine_outputs.outputs[0].token_ids) == [2]
@pytest.mark.asyncio
async def test_handle_abort_emits_error_result_on_failure() -> None:
rpc_queue: asyncio.Queue = asyncio.Queue()
@@ -2260,7 +2183,7 @@ async def test_resumable_segment_boundary_builds_stage_metrics() -> None:
final_stage_id=0,
)
req_state.streaming.enabled = True
req_state.streaming.segments[0] = StreamingSegmentState(finished=True)
req_state.streaming.segment_finished = True
req_state.stage_submit_ts[0] = time.time()
orchestrator.request_states = {"req-stream": req_state}
orchestrator.stage_pools = [pool]
@@ -1,130 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Streaming segment-boundary state must be keyed by stage.
``Orchestrator._orchestration_loop`` polls every stage into the same
``OrchestratorRequestState``. While the segment-boundary fields lived in a single
flat slot per request, whichever stage polled most recently overwrote the others,
and the per-stage consumers then read back a different stage's boundary.
"""
from __future__ import annotations
from dataclasses import dataclass, field
import pytest
from vllm.sampling_params import SamplingParams
from vllm_omni.engine.orchestrator import (
Orchestrator,
OrchestratorRequestState,
StreamingSegmentState,
)
from vllm_omni.experimental.fullduplex.engine.contracts import DuplexRequestIdentity
from vllm_omni.experimental.fullduplex.engine.messages import DuplexFence
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
THINKER_STAGE = 0
TALKER_STAGE = 1
@dataclass
class _StageMetrics:
pipeline_timings: dict[str, float] = field(default_factory=dict)
@dataclass
class _StageOutput:
"""Stand-in for a processed stage output, with the fields the router reads."""
request_id: str
finished: bool = False
error: object | None = None
class _RecordingPool:
"""Stage pool that records the requests it was asked to build metrics for."""
def __init__(self) -> None:
self.built_for: list[str] = []
def build_stage_metrics(self, outputs: list[_StageOutput], **_kwargs: object) -> _StageMetrics:
self.built_for.extend(output.request_id for output in outputs)
return _StageMetrics()
def _duplex_req_state(request_id: str = "req-duplex") -> OrchestratorRequestState:
req_state = OrchestratorRequestState(
request_id=request_id,
sampling_params_list=[SamplingParams(max_tokens=1), SamplingParams(max_tokens=1)],
final_stage_id=TALKER_STAGE,
)
req_state.streaming.enabled = True
req_state.duplex_identity = DuplexRequestIdentity(
session_id="session-1",
fence=DuplexFence(session_id="session-1"),
)
return req_state
def test_duplex_output_context_reads_only_its_own_stage_segment() -> None:
req_state = _duplex_req_state()
req_state.streaming.segments[THINKER_STAGE] = StreamingSegmentState(
finished=True,
token_ids=[11, 22],
output_metadata={"stage": "thinker"},
)
# The talker polls after the thinker and is still mid-segment.
req_state.streaming.segments[TALKER_STAGE] = StreamingSegmentState(finished=False)
thinker = Orchestrator._duplex_output_context(req_state, stage_id=THINKER_STAGE)
talker = Orchestrator._duplex_output_context(req_state, stage_id=TALKER_STAGE)
assert thinker is not None
assert talker is not None
# These are the assertions that fail on the flat field: the talker is written
# second, so the thinker loses its boundary to whichever stage polled last.
assert thinker.segment_finished is True
assert thinker.segment_token_ids == (11, 22)
assert thinker.segment_output_metadata == {"stage": "thinker"}
assert talker.segment_finished is False
assert talker.segment_token_ids == ()
assert talker.segment_output_metadata == {}
@pytest.mark.asyncio
async def test_segment_boundary_on_one_stage_does_not_build_metrics_on_another() -> None:
req_state = _duplex_req_state()
req_state.streaming.segments[THINKER_STAGE] = StreamingSegmentState(finished=True)
talker_pool = _RecordingPool()
orchestrator = object.__new__(Orchestrator)
orchestrator.request_states = {req_state.request_id: req_state}
orchestrator.stage_pools = [_RecordingPool(), talker_pool]
routed: list[_StageMetrics | None] = []
async def record_route(
_stage: int,
_replica: int,
_output: _StageOutput,
_state: OrchestratorRequestState,
metrics: _StageMetrics | None,
) -> None:
routed.append(metrics)
orchestrator._route_output = record_route
# The talker's own output is neither finished nor at a segment boundary, so
# it must not be treated as one just because the thinker reached one.
await orchestrator._handle_processed_outputs(
TALKER_STAGE,
0,
[_StageOutput(request_id=req_state.request_id)],
)
assert talker_pool.built_for == []
assert routed == [None]
@@ -17,7 +17,6 @@ from vllm.sampling_params import SamplingParams
from vllm_omni.engine.orchestrator import (
Orchestrator,
OrchestratorRequestState,
StreamingSegmentState,
_OrchestratorDuplexStagePort,
)
from vllm_omni.engine.stage_pool import StagePool
@@ -161,26 +160,6 @@ def _duplex_stage_port_submission():
return port, stage_pools, request_states, prewarm, submission
def test_duplex_bridge_state_catches_up_after_serving_turn_boundary() -> None:
port, _, request_states, _, submission = _duplex_stage_port_submission()
request_state = request_states[submission.context.request_id]
bridge_state = request_state.streaming.bridge_states["duplex"]
bridge_state["model_turn_id"] = 0
next_context = DuplexStageRequestContext(
request_id=submission.context.request_id,
session_id=submission.context.session_id,
fence=DuplexFence(submission.context.session_id, turn_id=1),
stage_id=submission.context.stage_id,
final_stage_id=submission.context.final_stage_id,
config_generation=submission.context.config_generation,
sampling_params=submission.context.sampling_params,
)
port.ensure_request(next_context)
assert bridge_state["model_turn_id"] == 1
def _request_output(request_id: str) -> RequestOutput:
completion = CompletionOutput(
index=0,
@@ -379,7 +358,7 @@ async def test_streaming_segment_does_not_complete_final_output_stage() -> None:
final_output_stage_ids={0},
)
req_state.streaming.enabled = True
req_state.streaming.segments[0] = StreamingSegmentState(finished=True)
req_state.streaming.segment_finished = True
output = SimpleNamespace(
request_id=req_state.request_id,
finished=True,
-59
View File
@@ -700,62 +700,3 @@ def test_num_cache_creation_tokens_reaches_direct_request_output():
assert direct.num_cached_tokens == 3
assert direct.num_cache_creation_tokens == 2
def _abort_processor_with_parent(child_ids: list[str]):
processor = object.__new__(MultimodalOutputProcessor)
processor.request_states = {}
processor.external_req_ids = {}
processor.parent_requests = {}
processor._native_text_metrics_by_request = {}
processor.lora_states = SimpleNamespace(request_finished=lambda *_args, **_kwargs: None)
parent = SimpleNamespace(request_id="parent", child_requests=set(child_ids))
processor.parent_requests["parent"] = parent
for child_id in child_ids:
req_state = SimpleNamespace(
parent_req=parent,
lora_name=None,
output_kind=RequestOutputKind.CUMULATIVE,
detokenizer=SimpleNamespace(output_token_ids=[7, 8]),
queue=None,
external_req_id="parent",
make_request_output=MagicMock(return_value=SimpleNamespace(request_id=child_id)),
)
processor.request_states[child_id] = req_state
return processor, parent
def test_abort_last_parallel_child_drops_parent_request():
processor, parent = _abort_processor_with_parent(["0_parent", "1_parent"])
aborted, _outputs = processor.abort_requests_collecting_outputs(["0_parent"], internal=True)
assert aborted == ["0_parent"]
assert "parent" in processor.parent_requests
assert parent.child_requests == {"1_parent"}
aborted, _outputs = processor.abort_requests_collecting_outputs(["1_parent"], internal=True)
assert aborted == ["1_parent"]
assert "parent" not in processor.parent_requests
assert not parent.child_requests
def test_abort_snapshot_leaves_state_until_commit():
processor, parent = _abort_processor_with_parent(["0_parent"])
processor.external_req_ids["parent"] = ["0_parent"]
aborted, outputs = processor.abort_requests_collecting_outputs(
["parent"],
internal=False,
commit_state=False,
)
assert aborted == ["0_parent"]
assert outputs
assert "0_parent" in processor.request_states
assert processor.external_req_ids["parent"] == ["0_parent"]
assert "parent" in processor.parent_requests
assert parent.child_requests == {"0_parent"}
processor.commit_aborted_request_state(["parent"], internal=False)
assert "0_parent" not in processor.request_states
assert "parent" not in processor.parent_requests
assert "parent" not in processor.external_req_ids
+4 -21
View File
@@ -630,24 +630,8 @@ def test_typed_ming_image_engine_args_defer_diffusion_batch_default():
assert OmniDiffusionConfig(**typed_backend_args).max_num_seqs == 1
def _fake_model_root(pipeline, tmp_path):
"""A model directory carrying every subfolder the pipeline's stages declare.
Stage init fails closed when a declared ``model_subdir``/``tokenizer_subdir``
is not a real directory, because the joined path would otherwise reach
HuggingFace as a malformed repo id (issue #6638).
"""
root = tmp_path / "model"
root.mkdir(exist_ok=True)
for stage in pipeline.stages:
for subdir in (stage.model_subdir, stage.tokenizer_subdir):
if subdir:
(root / subdir).mkdir(parents=True, exist_ok=True)
return str(root)
@pytest.mark.parametrize("model_type", sorted(OMNI_PIPELINES))
def test_typed_engine_args_match_current_registry_backend_semantics(model_type, tmp_path):
def test_typed_engine_args_match_current_registry_backend_semantics(model_type):
pipeline = resolve_pipeline_config(model_type)
if pipeline is None:
pytest.skip(f"Pipeline {model_type!r} requires an HF config to resolve")
@@ -657,19 +641,18 @@ def test_typed_engine_args_match_current_registry_backend_semantics(model_type,
if pipeline.default_deploy_config_name is not None
else DeployConfig()
)
model = _fake_model_root(pipeline, tmp_path)
legacy_stages, omni_config = _legacy_and_typed_stages(
pipeline,
deploy,
model=model,
model="/tmp",
)
for legacy_stage in legacy_stages:
stage_id = legacy_stage.stage_id
legacy_args = build_legacy_engine_args_dict(legacy_stage, model=model)
legacy_args = build_legacy_engine_args_dict(legacy_stage, model="/tmp")
typed_args = build_engine_args_dict_from_omni_stage_config(
omni_config.stage_by_id(stage_id),
model=model,
model="/tmp",
)
if legacy_stage.stage_type == StageType.DIFFUSION:
backend_fields = _DIFFUSION_BACKEND_FIELDS
+2 -55
View File
@@ -64,61 +64,8 @@ def test_collective_rpc_control_helper_honors_timeout():
return "slept"
pool, _client = _make_pool(sleep_async=slow_sleep)
with pytest.raises(asyncio.TimeoutError):
await pool.collective_rpc(0, "sleep", timeout=0.01, args=(1,))
asyncio.run(run())
@pytest.mark.cpu
def test_collective_rpc_control_method_reraises_worker_error():
async def run() -> None:
pool, _client = _make_pool(wake_up_async=AsyncMock(side_effect=RuntimeError("worker died")))
with pytest.raises(RuntimeError, match="worker died"):
await pool.collective_rpc(0, "wake_up")
asyncio.run(run())
@pytest.mark.cpu
def test_collective_rpc_non_control_method_still_returns_error_dict():
async def run() -> None:
pool, _client = _make_pool(collective_rpc_async=AsyncMock(side_effect=RuntimeError("probe failed")))
result = await pool.collective_rpc(0, "reset_prefix_cache")
result = await pool.collective_rpc(0, "sleep", timeout=0.01, args=(1,))
assert result != "slept"
assert result["supported"] is False
assert "probe failed" in result["error"]
asyncio.run(run())
@pytest.mark.cpu
def test_abort_requests_does_not_commit_op_state_when_engine_abort_fails():
async def run() -> None:
class RecordingOutputProcessor:
def __init__(self) -> None:
self.collected = False
self.committed = False
def abort_requests_collecting_outputs(self, request_ids, *, internal=False, commit_state=True):
del internal
self.collected = True
assert commit_state is False
return list(request_ids), [SimpleNamespace(request_id=request_ids[0])]
def commit_aborted_request_state(self, request_ids, *, internal=False):
del request_ids, internal
self.committed = True
abort = AsyncMock(side_effect=RuntimeError("engine abort failed"))
output_processor = RecordingOutputProcessor()
client = SimpleNamespace(stage_type="llm", abort_requests_async=abort)
pool = StagePool(0, [client], output_processor=output_processor) # type: ignore[arg-type]
pool._request_bindings["req-1"] = 0
with pytest.raises(RuntimeError, match="engine abort failed"):
await pool.abort_requests(["req-1"])
assert output_processor.collected is True
assert output_processor.committed is False
abort.assert_awaited_once()
asyncio.run(run())
@@ -462,15 +462,6 @@ def test_minicpmo_native_capabilities_separate_model_state_from_core_kv_lease():
assert caps["stage_handoff_transport"] == "scheduler_data_plane"
def test_duplex_overlap_policy_auto_falls_back_to_listen_only():
from vllm_omni.experimental.fullduplex.openai.protocol import (
DuplexOverlapPolicy,
DuplexSessionConfig,
)
assert DuplexSessionConfig._normalize_overlap_policy("auto") == DuplexOverlapPolicy.LISTEN_ONLY.value
def test_minicpmo_native_capabilities_do_not_overclaim_single_session_deployment():
caps = DuplexCapabilities.minicpmo45_native(max_sessions=1).as_dict()
@@ -1811,27 +1811,6 @@ def test_auto_response_playback_overlap_keeps_model_owned_listen_speak_decision(
assert handler._should_force_listen_for_auto_response_overlap(session, {}, speech_payload) is False
def test_legacy_auto_overlap_policy_falls_back_to_listen_only():
handler = OmniDuplexSessionHandler(chat_service=FakeChatService(FakeEngineClient()))
session = DuplexSession(
session_id="sid-auto-policy-fallback",
config=DuplexSessionConfig(
overlap_policy=DuplexSessionConfig._normalize_overlap_policy("auto"),
),
)
session.capabilities = DuplexCapabilities.minicpmo45_native()
decision = handler._overlap_decision(
session,
{"duration_ms": session.config.overlap_barge_in_ms, "is_speech": True},
_native_audio_payload(is_speech=True),
)
assert session.config.overlap_policy == DuplexOverlapPolicy.LISTEN_ONLY.value
assert decision["action"] == "listen"
assert decision["reason"] == "policy_listen_only"
def test_auto_response_overlap_silence_advances_model_unit_and_preserves_realtime_input():
handler, session = _auto_response_context(
"sid-auto-silent-wire-buffer",
@@ -3271,7 +3250,6 @@ async def test_minicpmo_auto_response_continuation_stops_at_large_safety_boundar
assert ws.sent_types()[-2:] == ["response.listen", "response.done"]
assert session.active_response_id is None
assert session.active_request_id == request_id
assert session.turn_id == 1
@pytest.mark.asyncio
@@ -3315,65 +3293,9 @@ async def test_minicpmo_auto_response_boundary_listen_closes_response():
assert ws.sent_types() == ["response.listen", "response.done"]
assert session.active_response_id is None
assert session.active_request_id == request_id
assert session.turn_id == 1
assert native.continuation_owner_id is None
@pytest.mark.asyncio
async def test_minicpmo_auto_response_boundary_allows_next_model_turn_response():
request_id = "duplex-sid-boundary-next-turn-e0-stage0"
handler = OmniDuplexSessionHandler(chat_service=FakeChatService(FakeEngineClient()))
session = DuplexSession(
session_id="sid-boundary-next-turn",
config=DuplexSessionConfig(extra_body={"auto_response": True}),
)
session.capabilities = DuplexCapabilities.minicpmo45_native()
session.begin_response(turn_id=0)
session.bind_request(request_id)
native = handler._minicpmo_session_state(session)
native.continuation_owner_id = f"response:{session.active_response_id}"
native.continuation_units = handler._NATIVE_AUTO_RESPONSE_MAX_CONTINUATION_UNITS
ws = TimedWebSocket()
await handler._maybe_continue_native_response(
ws.send_json,
session=session,
expected_epoch=session.epoch,
)
assert session.active_response_id is None
assert session.turn_id == 1
close_reason, emitted = await handler._send_one_native_duplex_event(
ws.send_json,
{
"supported": True,
"stage_role": "tts",
"is_listen": False,
"data_plane_request_id": request_id,
"text": "next response",
"audio_data": "next-audio",
"audio_format": "pcm16",
"audio_duration_ms": 100,
"end_of_turn": False,
"model_turn_id": 1,
},
session=session,
expected_epoch=session.epoch,
)
assert close_reason is None
assert emitted is True
assert ws.sent_types() == [
"response.listen",
"response.done",
"response.created",
"response.speak",
"response.output_audio.delta",
]
assert session.active_response_turn_id == 1
@pytest.mark.asyncio
async def test_minicpmo_auto_response_boundary_does_not_close_new_epoch_response():
request_id = "duplex-sid-boundary-race-e0-stage0"
@@ -4099,55 +4021,6 @@ async def test_duplex_chat_audio_stream_uses_output_audio_delta_event():
]
@pytest.mark.asyncio
async def test_duplex_chat_stage_metrics_use_latest_streaming_snapshot():
class SnapshotChatService(FakeChatService):
async def create_chat_completion(self, request, raw_request=None):
del raw_request
self.seen_request_ids.append(request.request_id)
snapshots = [
{"num_tokens_out": 1, "vllm_tpot_ms": 0.0, "vllm_itls_ms": []},
{"num_tokens_out": 2, "vllm_tpot_ms": 10.0, "vllm_itls_ms": [10.0]},
{"num_tokens_out": 3, "vllm_tpot_ms": 11.0, "vllm_itls_ms": [10.0, 12.0]},
]
async def generate():
for index, snapshot in enumerate(snapshots):
payload = {
"modality": "audio",
"choices": [{"delta": {"content": f"audio-{index}"}}],
"metrics": {"stage_metrics": {"0": snapshot}},
}
yield f"data: {json.dumps(payload)}\n\n"
return generate()
handler = OmniDuplexSessionHandler(
chat_service=SnapshotChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(
session_id="sid-chat-snapshots",
config=DuplexSessionConfig(instructions="speak"),
)
sent: list[dict[str, Any]] = []
async def send_json(data: dict[str, Any]) -> None:
sent.append(data)
await handler._run_response(session, send_json)
audio_deltas = [event for event in sent if event.get("type") == "response.output_audio.delta"]
done = next(event for event in sent if event.get("type") == "response.done")
assert all("vllm_omni" not in event for event in audio_deltas)
assert done["vllm_omni"]["stage_metrics"]["0"] == {
"num_tokens_out": 3,
"vllm_tpot_ms": 11.0,
"vllm_itls_ms": [10.0, 12.0],
}
@pytest.mark.asyncio
async def test_duplex_chat_rejects_unknown_output_modality():
handler = OmniDuplexSessionHandler(
@@ -4347,224 +4220,6 @@ async def test_playback_ack_response_id_selects_matching_pending_history_item():
assert second_item_id not in session.pending_history_item_ids
@pytest.mark.asyncio
async def test_playback_ack_recovers_missing_response_history_registration():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(
session_id="sid-playback-missing-registration",
config=DuplexSessionConfig(playback_commit_policy="ack_only"),
)
response_id = session.begin_response()
session.append_assistant_text("response text")
session.mark_audio_sent(1000, text_chars=len("response text"))
session.end_response(commit_text=True, preserve_request=True)
assert not session.history_item_ids
assert f"item_{response_id}" in session.pending_history_item_ids
ws = TimedWebSocket()
await handler._handle_playback_ack(
session,
{
"type": "playback.ack",
"response_id": response_id,
"item_id": f"item_{response_id}",
"played_ms": 1000,
"committed_ms": 1000,
},
ws.send_json,
)
ack = next(message for message in ws.sent if message.get("type") == "playback.acknowledged")
assert ack["history_committed"] is True
assert f"item_{response_id}" in session.history_item_ids
assert session.history[-1] == {"role": "assistant", "content": "response text"}
@pytest.mark.asyncio
async def test_playback_ack_rejects_response_after_followup_user_commit():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(
session_id="sid-playback-after-followup-user",
config=DuplexSessionConfig(playback_commit_policy="ack_only"),
)
session.append_text("user A")
session.commit_user_input()
response_id = session.begin_response()
session.append_assistant_text("assistant A")
session.mark_audio_sent(1000, text_chars=len("assistant A"))
session.end_response(commit_text=False, preserve_request=True)
session.append_text("user B")
session.commit_user_input()
ws = TimedWebSocket()
await handler._handle_playback_ack(
session,
{
"type": "playback.ack",
"response_id": response_id,
"item_id": f"item_{response_id}",
"played_ms": 1000,
"committed_ms": 1000,
},
ws.send_json,
)
assert ws.sent_types() == ["error"]
assert ws.sent[0]["code"] == "playback_ack_too_late"
assert session.history == (
{"role": "user", "content": "user A"},
{"role": "user", "content": "user B"},
)
@pytest.mark.asyncio
async def test_cancel_does_not_append_old_assistant_after_followup_user():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(
session_id="sid-cancel-after-followup-user",
config=DuplexSessionConfig(playback_commit_policy="ack_only"),
)
session.append_text("user A")
session.commit_user_input()
session.begin_response()
session.append_assistant_text("assistant A")
session.mark_audio_sent(1000, text_chars=len("assistant A"))
session.acknowledge_playback(500, 500)
session.append_text("user B")
session.commit_user_input()
ws = TimedWebSocket()
cancelled = await handler._cancel_active_response(session, None, ws.send_json, reason="new_response")
assert cancelled is True
assert session.history == (
{"role": "user", "content": "user A"},
{"role": "user", "content": "user B"},
)
@pytest.mark.asyncio
async def test_playback_ack_rejects_item_from_another_response():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(session_id="sid-playback-mismatch", config=DuplexSessionConfig())
first_response_id = session.begin_response()
session.append_assistant_text("first response")
session.mark_audio_sent(1000)
session.end_response(commit_text=False, preserve_request=True)
session.register_history_item(f"item_{first_response_id}", None)
second_response_id = session.begin_response()
session.mark_audio_sent(1000)
ws = TimedWebSocket()
await handler._handle_playback_ack(
session,
{
"type": "playback.ack",
"response_id": second_response_id,
"item_id": f"item_{first_response_id}",
"played_ms": 500,
"committed_ms": 500,
},
ws.send_json,
)
assert ws.sent_types() == ["error"]
assert ws.sent[0]["code"] == "playback_item_mismatch"
assert session.active_response_id == second_response_id
assert session.playback.as_dict() == {
"generated_ms": 1000,
"sent_ms": 1000,
"played_ms": 0,
"committed_ms": 0,
}
@pytest.mark.asyncio
async def test_playback_ack_rejects_user_history_item():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(session_id="sid-playback-user-item", config=DuplexSessionConfig())
user_message = {"role": "user", "content": "user input"}
session.append_history_message(user_message)
session.register_history_item("item_user", user_message)
ws = TimedWebSocket()
await handler._handle_playback_ack(
session,
{
"type": "playback.ack",
"response_id": "user",
"item_id": "item_user",
"played_ms": 500,
"committed_ms": 500,
},
ws.send_json,
)
assert ws.sent_types() == ["error"]
assert ws.sent[0]["code"] == "playback_item_not_found"
assert session.history == ({"role": "user", "content": "user input"},)
@pytest.mark.asyncio
async def test_late_playback_ack_recovers_response_local_history_after_next_response_begins():
handler = OmniDuplexSessionHandler(
chat_service=FakeChatService(FakeEngineClient()),
config_timeout_s=0.1,
idle_timeout_s=1,
)
session = DuplexSession(session_id="sid-playback-response-local", config=DuplexSessionConfig())
first_response_id = session.begin_response()
session.append_assistant_text("first response")
session.mark_audio_sent(1000, text_chars=len("first response"))
session.end_response(commit_text=False, preserve_request=True)
second_response_id = session.begin_response()
session.mark_audio_sent(2000, text_chars=len("second response"))
ws = TimedWebSocket()
await handler._handle_playback_ack(
session,
{
"type": "playback.ack",
"response_id": first_response_id,
"item_id": f"item_{first_response_id}",
"played_ms": 1000,
"committed_ms": 1000,
},
ws.send_json,
)
ack = next(message for message in ws.sent if message.get("type") == "playback.acknowledged")
assert ack["history_committed"] is True
assert session.history[-1] == {"role": "assistant", "content": "first response"}
assert session.active_response_id == second_response_id
assert session.playback.as_dict() == {
"generated_ms": 2000,
"sent_ms": 2000,
"played_ms": 0,
"committed_ms": 0,
}
@pytest.mark.asyncio
async def test_late_playback_ack_does_not_advance_new_response_cursor():
handler = OmniDuplexSessionHandler(
+3 -91
View File
@@ -180,6 +180,7 @@ def test_abort_handles_internal_request_mapping(req_ids: list[str], cancel_prefi
assert re.fullmatch(rf"{re.escape(cancel_prefix)}-[0-9a-f]+", rid)
assert len(aborted_ids) == expected_cancel_count
assert len(set(aborted_ids)) == expected_cancel_count
assert len(omni.request_states) == len(req_ids) - expected_cancel_count
for t in tasks:
t.cancel()
@@ -189,9 +190,7 @@ def test_abort_handles_internal_request_mapping(req_ids: list[str], cancel_prefi
@pytest.mark.cpu
def test_abort_keeps_request_states_until_generate_cleanup():
"""Frontend abort must not drop request_states; generate() owns cleanup."""
def test_abort_pops_request_states_only_after_ack():
async def run():
release = asyncio.Event()
seen_during_wait: list[int] = []
@@ -216,7 +215,7 @@ def test_abort_keeps_request_states_until_generate_cleanup():
release.set()
await task
assert "req-1-aaaa" in omni.request_states
assert "req-1-aaaa" not in omni.request_states
asyncio.run(run())
@@ -242,93 +241,6 @@ def test_abort_propagates_engine_errors_without_popping_state():
asyncio.run(run())
@pytest.mark.cpu
def test_abort_enqueues_prefix_tokens_from_engine():
"""AR abort must deliver the generated prefix on the request queue (CPU)."""
async def run():
from vllm.outputs import CompletionOutput
from vllm_omni.engine.messages import OutputMessage
queue: asyncio.Queue = asyncio.Queue()
async def abort_with_prefix(request_ids):
rid = request_ids[0]
engine_output = OmniRequestOutput(
request_id=rid,
finished=True,
stage_id=0,
final_output_type="text",
outputs=[
CompletionOutput(
index=0,
text="partial",
token_ids=[7, 8, 9],
cumulative_logprob=None,
logprobs=None,
finish_reason="abort",
stop_reason=None,
)
],
)
return [
OutputMessage(
request_id=rid,
stage_id=0,
replica_id=None,
engine_outputs=engine_output,
metrics=None,
finished=True,
stage_submit_ts=None,
)
]
omni = get_async_omni_instance(fake_abort_request=abort_with_prefix)
omni.request_states["req-1-cccc"] = SimpleNamespace(
request_id="req-1-cccc",
external_request_id="req-1",
input_stream_task=None,
queue=queue,
)
await omni._abort(["req-1-cccc"])
msg = queue.get_nowait()
assert msg.finished is True
assert list(msg.engine_outputs.outputs[0].token_ids) == [7, 8, 9]
assert msg.engine_outputs.outputs[0].finish_reason == "abort"
assert "req-1-cccc" in omni.request_states
asyncio.run(run())
@pytest.mark.cpu
def test_abort_enqueues_synthetic_finished_when_engine_returns_empty():
"""Empty abort_async must still unblock generate() with finish_reason=abort."""
async def run():
queue: asyncio.Queue = asyncio.Queue()
async def empty_abort_async(request_ids):
del request_ids
return []
omni = get_async_omni_instance(fake_abort_request=empty_abort_async)
omni.request_states["req-1-dddd"] = SimpleNamespace(
request_id="req-1-dddd",
external_request_id="req-1",
input_stream_task=None,
queue=queue,
)
await omni._abort(["req-1-dddd"])
msg = queue.get_nowait()
assert msg.finished is True
assert msg.request_id == "req-1-dddd"
assert msg.engine_outputs.outputs[0].finish_reason == "abort"
assert "req-1-dddd" in omni.request_states
asyncio.run(run())
@pytest.mark.cpu
def test_generate_accepts_request_after_repeated_cancellations():
async def run_test():
@@ -33,19 +33,6 @@ def test_default_stage_config_includes_cache_backend():
assert engine_args["model_stage"] == "diffusion"
def test_default_stage_config_preserves_ulysses_a2a_permute() -> None:
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(
{
"ulysses_degree": 4,
"ulysses_a2a_permute": True,
}
)[0]
parallel_config = stage_cfg["engine_args"]["parallel_config"]
assert parallel_config.ulysses_degree == 4
assert parallel_config.ulysses_a2a_permute is True
def test_default_stage_config_preserves_model_extras():
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg({"extras": {"ltx2_use_conv_vae": True}})[0]
@@ -640,36 +627,6 @@ def test_default_stage_resolves_video_output_from_checkpoint(mocker):
assert captured["final_output_type"] == "video"
def test_default_diffusers_stage_preserves_video_model_identity(mocker):
captured = {}
def resolve_with_default(*args, default_stage_cfg_factory, **kwargs):
del args, kwargs
captured.update(default_stage_cfg_factory()[0])
stage = SimpleNamespace(stage_type="diffusion", engine_args=SimpleNamespace())
return ("", [stage], None)
mocker.patch(
"vllm_omni.diffusion.utils.hf_utils.get_diffusion_model_index",
return_value={"_class_name": "WanImageToVideoPipeline"},
)
mocker.patch(
"vllm_omni.engine.async_omni_engine.load_and_resolve_stage_configs",
side_effect=resolve_with_default,
)
engine = AsyncOmniEngine.__new__(AsyncOmniEngine)
engine._strip_single_engine_args = lambda kwargs: kwargs
engine._resolve_stage_configs(
"/models/Wan2.2-I2V",
{"diffusion_load_format": "diffusers"},
trust_remote_code=False,
)
assert captured["engine_args"]["model_class_name"] == "WanImageToVideoPipeline"
assert captured["final_output_type"] == "video"
def test_resolve_stage_configs_injects_additional_config_into_diffusion_stage(mocker):
"""Ensure YAML/deploy stage resolution forwards top-level additional_config."""
fake_diffusion_stage = SimpleNamespace(
@@ -18,14 +18,11 @@ def _make_omni(*, stage_types: list[str]) -> AsyncOmni:
omni = object.__new__(AsyncOmni)
omni._pause_cond = asyncio.Condition()
omni._paused = False
omni._admitting = 0
omni._hold_admission_until_resume = False
omni._sleeping_tags = set()
omni._stage_sleeping_tags = {}
omni._level2_sleeping = False
omni.event_resolver = SimpleNamespace(watch_task=lambda *a, **k: None, resolve=AsyncMock())
omni._final_output_handler = lambda: None
omni.reset_mm_cache = AsyncMock()
stage_clients = [SimpleNamespace(stage_type=stage_type) for stage_type in stage_types]
omni.engine = SimpleNamespace(
@@ -174,33 +171,6 @@ def test_sleep_blocks_admission_before_engine_core_rpc():
asyncio.run(run())
@pytest.mark.cpu
def test_sleep_waits_for_in_flight_generate_admission():
"""Sleep must not offload while generate() is still in add_request."""
async def run() -> None:
omni = _make_omni(stage_types=["llm"])
omni._admitting = 1
rpc_started = asyncio.Event()
async def rpc_side_effect(**kwargs):
if kwargs.get("method") == "sleep":
rpc_started.set()
return [True]
omni.collective_rpc = AsyncMock(side_effect=rpc_side_effect)
sleep_task = asyncio.create_task(omni.sleep(level=1, mode="abort"))
await asyncio.sleep(0.05)
assert not sleep_task.done()
assert not rpc_started.is_set()
await omni._release_generate_admission()
await asyncio.wait_for(sleep_task, timeout=1)
assert rpc_started.is_set()
assert omni._admitting == 0
asyncio.run(run())
@pytest.mark.cpu
def test_wake_up_routes_ar_via_collective_rpc_and_diffusion_to_worker_rpc():
async def run() -> None:
@@ -290,80 +260,6 @@ def test_sleep_diffusion_only_skips_engine_core_collective_rpc():
asyncio.run(run())
@pytest.mark.cpu
def test_streaming_wait_for_first_chunk_does_not_block_sleep():
"""Waiting for the next client chunk must not hold an admission slot."""
async def run() -> None:
omni = _make_omni(stage_types=["llm"])
rpc_started = asyncio.Event()
first_chunk = asyncio.Event()
async def rpc_side_effect(**kwargs):
if kwargs.get("method") == "sleep":
rpc_started.set()
return [True]
omni.collective_rpc = AsyncMock(side_effect=rpc_side_effect)
async def wait_then_submit() -> None:
await first_chunk.wait()
await omni._submit_with_admission(asyncio.sleep(0))
wait_task = asyncio.create_task(wait_then_submit())
sleep_task = asyncio.create_task(omni.sleep(level=1, mode="abort"))
await asyncio.sleep(0.05)
assert not wait_task.done()
assert omni._admitting == 0
await asyncio.wait_for(sleep_task, timeout=1)
assert rpc_started.is_set()
wait_task.cancel()
with pytest.raises(asyncio.CancelledError):
await wait_task
assert omni._admitting == 0
asyncio.run(run())
@pytest.mark.cpu
def test_streaming_in_flight_add_blocks_sleep():
"""Sleep must wait while a streaming ADD/update holds an admission slot."""
async def run() -> None:
omni = _make_omni(stage_types=["llm"])
add_started = asyncio.Event()
add_release = asyncio.Event()
rpc_started = asyncio.Event()
async def rpc_side_effect(**kwargs):
if kwargs.get("method") == "sleep":
rpc_started.set()
return [True]
omni.collective_rpc = AsyncMock(side_effect=rpc_side_effect)
async def slow_add() -> None:
add_started.set()
await add_release.wait()
add_task = asyncio.create_task(omni._submit_with_admission(slow_add()))
await add_started.wait()
sleep_task = asyncio.create_task(omni.sleep(level=1, mode="abort"))
await asyncio.sleep(0.05)
assert not sleep_task.done()
assert not rpc_started.is_set()
assert omni._admitting == 1
add_release.set()
await asyncio.wait_for(add_task, timeout=1)
await asyncio.wait_for(sleep_task, timeout=1)
assert rpc_started.is_set()
assert omni._admitting == 0
asyncio.run(run())
@pytest.mark.cpu
def test_wake_up_restores_admission_for_diffusion_only():
"""Pure diffusion must keep sleep → wake → generate (no resume)."""
@@ -412,30 +308,3 @@ def test_pause_then_sleep_wake_keeps_admission_paused_for_diffusion():
assert omni._paused is False
asyncio.run(run())
@pytest.mark.cpu
def test_partial_wake_does_not_skip_remaining_sleeping_stage():
"""sleep(stage_ids=[0]) then wake([0]) must not skip a later wake([1])."""
async def run() -> None:
omni = _make_omni(stage_types=["llm", "llm"])
await omni.sleep(stage_ids=[0, 1], level=1, mode="abort")
assert omni._stage_sleeping_tags.keys() == {0, 1}
await omni.wake_up(stage_ids=[0])
assert 0 not in omni._stage_sleeping_tags
assert 1 in omni._stage_sleeping_tags
assert CuMemTag.WEIGHTS.value in omni._sleeping_tags
omni.collective_rpc.reset_mock()
acks = await omni.wake_up(stage_ids=[1])
assert acks
omni.collective_rpc.assert_awaited_once()
wake_kwargs = omni.collective_rpc.await_args.kwargs
assert wake_kwargs["method"] == "wake_up"
assert wake_kwargs["stage_ids"] == [1]
assert not omni._sleeping_tags
assert not omni._stage_sleeping_tags
asyncio.run(run())
+1 -2
View File
@@ -109,7 +109,7 @@ def get_dynamic_devices(stage_idx: int, num_stages: int, tp_size: int) -> str:
async def _ensure_awake(engine: AsyncOmni, stage_ids: list[int]) -> None:
"""Best-effort full wake + resume so the next shared-engine test starts active."""
"""Best-effort full wake so the next shared-engine test starts active."""
try:
await engine.wake_up(stage_ids=stage_ids)
except Exception as e:
@@ -532,7 +532,6 @@ async def test_pure_diffusion_sleep_wake():
try:
await engine.sleep(level=1)
await engine.wake_up()
await engine.resume_generation()
async for _ in engine.generate(
"test",
sampling_params=OmniDiffusionSamplingParams(num_inference_steps=2, height=256, width=256),
-14
View File
@@ -1,6 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Unit tests for the Omni serve CLI helpers."""
from __future__ import annotations
@@ -79,17 +76,6 @@ def test_serve_parser_accepts_four_way_cfg_parallelism() -> None:
assert args.cfg_parallel_size == 4
def test_serve_parser_accepts_ulysses_a2a_permute() -> None:
parser = TrackingArgumentParser()
subparsers = parser.add_subparsers(dest="subcommand")
OmniServeCommand().subparser_init(subparsers)
args = parser.parse_args(["serve", "fake-model", "--omni", "--ulysses-a2a-permute"])
assert args.ulysses_a2a_permute is True
assert args.get_explicit_kwargs_dict()["ulysses_a2a_permute"] is True
def _make_headless_args(**kwargs) -> TrackingNamespace:
defaults = {
"model": "fake-model",
-14
View File
@@ -216,20 +216,6 @@ def test_resolve_stage_physical_devices_uses_visible_baseline(monkeypatch: pytes
assert resolve_stage_physical_devices(1, "1", visible_baseline="3,4,5") == "4"
@pytest.mark.core_model
@pytest.mark.cpu
def test_resolve_stage_physical_devices_captured_unset_ignores_live_env(monkeypatch: pytest.MonkeyPatch):
"""Parallel init captures an unset CUDA_VISIBLE_DEVICES as None.
A sibling thread may then narrow the live env for spawn. The captured
None must not fall back to that live value, or logical device 0 would
map onto the other stage's GPU.
"""
monkeypatch.setenv("CUDA_VISIBLE_DEVICES", "6,7")
assert resolve_stage_physical_devices(0, "0", visible_baseline=None) == "0"
assert resolve_stage_physical_devices(1, "1", visible_baseline=None) == "1"
@pytest.mark.core_model
@pytest.mark.cpu
def test_map_device_list_raises_on_non_numeric():
+2 -2
View File
@@ -665,7 +665,7 @@ def _speech_assertion_needs_audio_transcript(request_config: dict[str, Any], run
return False
if request_config.get("response_format") == "pcm":
return False
return bool(request_config.get("transcript_expected_text", request_config.get("input")))
return bool(request_config.get("input"))
def _resolve_audio_transcript(
@@ -880,7 +880,7 @@ def assert_audio_speech_response(response: Any, request_config: dict[str, Any],
transcript = _resolve_audio_transcript(response, request_config, run_level, speech_api=True)
if transcript is not None:
expected_text = request_config.get("transcript_expected_text", request_config.get("input"))
expected_text = request_config.get("input")
if expected_text:
print(f"audio content is: {transcript}")
print(f"input text is: {expected_text}")
-1
View File
@@ -1432,7 +1432,6 @@ class OnlineOmniClient:
- 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``
- transcript_expected_text: local expected spoken text; defaults to ``input``
- timeout: request timeout in seconds (float, optional, default 120.0)
- stream: whether to use streaming API (bool, optional, default False)
@@ -1,30 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Pytest collection-phase hooks.
All ``pytest_collection_modifyitems`` policies live here so their order is
explicit. Add a named helper and call it from the hook; do not register a
second ``pytest_collection_modifyitems`` in another plugin.
"""
from __future__ import annotations
import pytest
def pytest_collection_modifyitems(config, items):
_pin_xdist_serial(config, items)
def _pin_xdist_serial(config, items):
"""Pin unmarked tests to one xdist worker.
Only tests explicitly marked ``pytest.mark.xdist`` run concurrently.
Others are pinned to a single xdist worker.
"""
if not config.pluginmanager.hasplugin("xdist"):
return
for item in items:
if not item.get_closest_marker("xdist"):
item.add_marker(pytest.mark.xdist_group(name="serial"))
@@ -1,10 +1,3 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Pytest CLI options and the fixtures that expose them."""
from __future__ import annotations
import pytest
-12
View File
@@ -24,18 +24,6 @@ if TYPE_CHECKING:
omni_fixture_lock = threading.Lock()
@pytest.fixture
def _normalized_hardware_marks(request: pytest.FixtureRequest):
"""Dummy carrier for mixed-``num_cards`` ``@hardware_test`` mark normalization.
Pytest has no API to clone an item and only change marks. ``@hardware_test``
therefore parametrizes this fixture (``indirect=True``) so each collected
item gets one platform's SKU and ``cards_{n}`` without changing test
signatures. The returned platform id is unused by test bodies.
"""
return request.param
@pytest.fixture(scope="function")
def omni_server_function(
request: pytest.FixtureRequest,
+14
View File
@@ -0,0 +1,14 @@
"""
Only tests explicitly marked ``pytest.mark.xdist`` run concurrently.
Other test are pinned to a single xdist worker.
"""
import pytest
def pytest_collection_modifyitems(config, items):
if not config.pluginmanager.hasplugin("xdist"):
return
for item in items:
if not item.get_closest_marker("xdist"):
item.add_marker(pytest.mark.xdist_group(name="serial"))
+126 -251
View File
@@ -1,33 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Pytest marks and decorators for hardware / resource selection (CUDA, ROCm, …).
Module import is stdlib-only so ``tools/pre_commit/check_test_marks.py`` can load
this file without pytest or vllm. Decorator helpers import pytest lazily.
"""
"""Pytest marks and decorators for hardware / resource selection (CUDA, ROCm, …)."""
from __future__ import annotations
import re
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import pytest
import pytest
from vllm.platforms import current_platform
# Marker description tags in ``pyproject.toml`` ``tool.pytest.ini_options.markers``.
# Example: ``"H100: [hardware-resource] [cuda] Tests that require H100 GPU"``.
# Marker description tag in ``pyproject.toml`` ``tool.pytest.ini_options.markers``.
# Example: ``"H100: [hardware-resource] Tests that require H100 GPU"``.
_HARDWARE_RESOURCE_MARKER_TAG = "[hardware-resource]"
_HARDWARE_CARDS_MARKER_TAG = "[hardware-cards]"
_HARDWARE_PLATFORM_MARKER_TAG = "[hardware-platform]"
_CI_LEVEL_MARKER_TAG = "[ci-level]"
# On ``[hardware-platform]`` lines (not SKUs): also attach ``pytest.mark.gpu``.
_GPU_DEVICE_CLASS_TAG = "[gpu]"
_CARDS_MARK_NAME_RE = re.compile(r"^cards_(\d+)$")
# Indirect fixture in ``tests.helpers.fixtures.runtime``: mixed-count ``@hardware_test``
# parametrizes it so each collected item carries one platform's SKU and ``cards_{n}``.
HARDWARE_MARK_NORMALIZATION_FIXTURE = "_normalized_hardware_marks"
def _repo_root() -> Path:
@@ -48,16 +34,23 @@ def _load_pyproject() -> dict:
return {}
@lru_cache(maxsize=16)
def _marker_names_with_tag(tag: str) -> frozenset[str]:
"""Return pytest marker names whose description contains ``tag``."""
@lru_cache(maxsize=1)
def get_hardware_mark_list() -> frozenset[str]:
"""Return hardware SKU marker names from ``pyproject.toml``.
A marker is treated as a hardware resource marker when its description
contains the ``[hardware-resource]`` tag, e.g.::
"H100: [hardware-resource] Tests that require H100 GPU"
"""
data = _load_pyproject()
entries = data.get("tool", {}).get("pytest", {}).get("ini_options", {}).get("markers", [])
if not entries:
# Fallback when TOML parsing fails: scan tagged marker lines.
text = (_repo_root() / "pyproject.toml").read_text(encoding="utf-8")
return frozenset(
re.findall(
rf'^\s*"([A-Za-z0-9_]+)\s*:.*{re.escape(tag)}',
rf'^\s*"([A-Za-z0-9_]+)\s*:\s*{re.escape(_HARDWARE_RESOURCE_MARKER_TAG)}',
text,
flags=re.M,
)
@@ -66,7 +59,7 @@ def _marker_names_with_tag(tag: str) -> frozenset[str]:
names: set[str] = set()
for entry in entries:
text = str(entry)
if tag not in text:
if _HARDWARE_RESOURCE_MARKER_TAG not in text:
continue
name = text.split(":", 1)[0].strip()
if name:
@@ -74,256 +67,138 @@ def _marker_names_with_tag(tag: str) -> frozenset[str]:
return frozenset(names)
@lru_cache(maxsize=1)
def get_hardware_mark_list() -> frozenset[str]:
"""Return hardware SKU marker names tagged ``[hardware-resource]``."""
return _marker_names_with_tag(_HARDWARE_RESOURCE_MARKER_TAG)
@lru_cache(maxsize=1)
def get_supported_card_counts() -> frozenset[int]:
"""Return ``n`` for each ``cards_{n}`` marker tagged ``[hardware-cards]``."""
counts: set[int] = set()
for name in _marker_names_with_tag(_HARDWARE_CARDS_MARKER_TAG):
matched = _CARDS_MARK_NAME_RE.fullmatch(name)
if matched:
counts.add(int(matched.group(1)))
return frozenset(counts)
@lru_cache(maxsize=1)
def get_supported_platforms() -> frozenset[str]:
"""Return platform marker names tagged ``[hardware-platform]``.
Includes ``cpu`` / ``gpu`` (device-class marks) and accelerator keys.
``hardware_marks`` / ``hardware_test`` ``res`` dicts reject ``cpu`` / ``gpu``.
"""
return _marker_names_with_tag(_HARDWARE_PLATFORM_MARKER_TAG)
@lru_cache(maxsize=1)
def get_level_markers() -> frozenset[str]:
"""Return CI level marker names tagged ``[ci-level]`` (``core_model``, ``slow``, …)."""
return _marker_names_with_tag(_CI_LEVEL_MARKER_TAG)
@lru_cache(maxsize=8)
def _skus_for_platform(platform: str) -> frozenset[str]:
"""SKU names tagged ``[hardware-resource]`` and ``[platform]`` (e.g. ``[cuda]``)."""
return get_hardware_mark_list() & _marker_names_with_tag(f"[{platform}]")
def _require_sku(platform: str, res: str) -> None:
allowed = _skus_for_platform(platform)
if res not in allowed:
supported = ", ".join(sorted(allowed)) or f"(none tagged [hardware-resource] [{platform}])"
raise ValueError(f"Invalid {platform} resource type: {res}. Supported: {supported}")
def _cards_mark(num_cards: int) -> pytest.MarkDecorator:
"""Return ``cards_{n}`` so collection can filter with ``-m cards_2`` (etc.).
Multi-card selection is ``not cards_1`` (or an explicit ``cards_2`` / ``cards_4``).
"""
import pytest
if not isinstance(num_cards, int) or isinstance(num_cards, bool) or num_cards < 1:
raise ValueError(f"num_cards must be a positive int, got {num_cards!r}")
if num_cards not in get_supported_card_counts():
counts = get_supported_card_counts()
supported = ", ".join(f"cards_{n}" for n in sorted(counts))
supported = supported or "(none tagged [hardware-cards])"
raise ValueError(f"num_cards={num_cards} has no registered marker. Supported: {supported}.")
return getattr(pytest.mark, f"cards_{num_cards}")
def _cuda_marks(*, res: str, num_cards: int):
import pytest
from vllm.platforms import current_platform
_require_sku("cuda", res)
test_resource = getattr(pytest.mark, res)
def cuda_marks(*, res: str, num_cards: int):
test_platform_detail = pytest.mark.cuda
marks = [test_resource, test_platform_detail, _cards_mark(num_cards)]
if num_cards == 1 or not current_platform.is_cuda():
if res == "L4":
test_resource = pytest.mark.L4
elif res == "H100":
test_resource = pytest.mark.H100
elif res == "H800":
test_resource = pytest.mark.H800
else:
raise ValueError(f"Invalid CUDA resource type: {res}. Supported: L4, H100, H800")
marks = [test_resource, test_platform_detail]
if num_cards == 1:
return marks
return marks + [
pytest.mark.skipif(
current_platform.device_count() < num_cards,
reason=f"Need at least {num_cards} CUDA GPUs to run the test.",
)
]
test_distributed = pytest.mark.distributed_cuda(num_cards=num_cards)
if not current_platform.is_cuda():
return marks + [test_distributed]
test_skipif = pytest.mark.skipif(
current_platform.device_count() < num_cards,
reason=f"Need at least {num_cards} CUDA GPUs to run the test.",
)
return marks + [test_distributed, test_skipif]
def _rocm_marks(*, res: str, num_cards: int):
import pytest
_require_sku("rocm", res)
return [getattr(pytest.mark, res), pytest.mark.rocm, _cards_mark(num_cards)]
def _xpu_marks(*, res: str, num_cards: int):
import pytest
from vllm.platforms import current_platform
_require_sku("xpu", res)
marks = [getattr(pytest.mark, res), pytest.mark.xpu, _cards_mark(num_cards)]
if num_cards == 1 or not current_platform.is_xpu():
def rocm_marks(*, res: str, num_cards: int):
test_platform_detail = pytest.mark.rocm
if res == "MI325":
test_resource = pytest.mark.MI325
else:
raise ValueError(f"Invalid ROCm resource type: {res}. Supported: MI325")
marks = [test_resource, test_platform_detail]
if num_cards == 1:
return marks
return marks + [
pytest.mark.skipif(
current_platform.device_count() < num_cards,
reason=f"Need at least {num_cards} XPUs to run the test.",
)
]
test_distributed = pytest.mark.distributed_rocm(num_cards=num_cards)
return marks + [test_distributed]
def _musa_marks(*, res: str, num_cards: int):
import pytest
def xpu_marks(*, res: str, num_cards: int):
test_platform_detail = pytest.mark.xpu
if res == "B60":
test_resource = pytest.mark.B60
else:
raise ValueError(f"Invalid XPU resource type: {res}. Supported: B60")
marks = [test_resource, test_platform_detail]
if num_cards == 1:
return marks
test_distributed = pytest.mark.distributed_xpu(num_cards=num_cards)
_require_sku("musa", res)
return [getattr(pytest.mark, res), pytest.mark.musa, _cards_mark(num_cards)]
if not current_platform.is_xpu():
return marks + [test_distributed]
test_skipif = pytest.mark.skipif(
current_platform.device_count() < num_cards,
reason=f"Need at least {num_cards} XPUs to run the test.",
)
return marks + [test_distributed, test_skipif]
@lru_cache(maxsize=1)
def _gpu_res_platforms() -> frozenset[str]:
"""Accelerator platforms tagged ``[hardware-platform] [gpu]`` (not the ``gpu`` mark)."""
return (get_supported_platforms() & _marker_names_with_tag(_GPU_DEVICE_CLASS_TAG)) - {"gpu"}
def musa_marks(*, res: str, num_cards: int):
test_platform_detail = pytest.mark.musa
if res == "S5000":
test_resource = pytest.mark.S5000
else:
raise ValueError(f"Invalid MUSA resource type: {res}. Supported: S5000")
marks = [test_resource, test_platform_detail]
if num_cards == 1:
return marks
test_distributed = pytest.mark.distributed_musa(num_cards=num_cards)
return marks + [test_distributed]
def _npu_marks(*, res: str, num_cards: int):
import pytest
_require_sku("npu", res)
return [pytest.mark.npu, getattr(pytest.mark, res), _cards_mark(num_cards)]
def gpu_marks(*, res: str, num_cards: int):
test_platform = pytest.mark.gpu
if res in ("L4", "H100", "H800"):
return [test_platform] + cuda_marks(res=res, num_cards=num_cards)
if res == "MI325":
return [test_platform] + rocm_marks(res=res, num_cards=num_cards)
if res == "B60":
return [test_platform] + xpu_marks(res=res, num_cards=num_cards)
if res == "S5000":
return [test_platform] + musa_marks(res=res, num_cards=num_cards)
raise ValueError(f"Invalid resource type: {res}. Supported: L4, H100, H800, MI325, B60, S5000")
def _res_platforms() -> frozenset[str]:
"""Accelerator keys allowed in ``hardware_test(res=...)`` / ``hardware_marks(res=...)``.
A ``[hardware-platform]`` name is a ``res`` key only if some SKU is tagged
with that platform (``[cuda]``, ``[npu]``, …). ``cpu`` / ``gpu`` have no SKUs.
"""
return frozenset(p for p in get_supported_platforms() if _skus_for_platform(p))
def _normalize_num_cards(res: dict[str, str], num_cards: int | dict[str, int]) -> dict[str, int]:
allowed = _res_platforms()
device_class = get_supported_platforms() - allowed
for platform in res:
if platform in device_class:
raise ValueError(
f"{platform!r} is not a res dict key; use pytest.mark.{platform} "
f"or an accelerator key ({', '.join(sorted(allowed))})."
)
if platform not in allowed:
supported = ", ".join(sorted(allowed)) or "(none)"
raise ValueError(f"Unsupported platform: {platform}. res keys: {supported}.")
if isinstance(num_cards, int) and not isinstance(num_cards, bool):
return {platform: num_cards for platform in res}
if not isinstance(num_cards, dict):
raise ValueError(f"num_cards must be a positive int or dict, got {num_cards!r}")
num_cards_dict = dict(num_cards)
for platform in num_cards_dict:
if platform not in res:
raise ValueError(f"Platform '{platform}' in num_cards but not in res.")
for platform in res:
num_cards_dict.setdefault(platform, 1)
return num_cards_dict
def _marks_for_platform(platform: str, resource: str, num_cards: int) -> list[pytest.MarkDecorator]:
import pytest
builders = {
"cuda": _cuda_marks,
"rocm": _rocm_marks,
"xpu": _xpu_marks,
"musa": _musa_marks,
"npu": _npu_marks,
}
builder = builders.get(platform)
if builder is None:
raise ValueError(f"Unsupported platform: {platform}")
marks = builder(res=resource, num_cards=num_cards)
if platform in _gpu_res_platforms():
return [pytest.mark.gpu] + marks
return marks
def _skipif_not_platform(platform: str) -> pytest.MarkDecorator:
import pytest
from vllm_omni.platforms import current_omni_platform
checkers = {
"cuda": current_omni_platform.is_cuda,
"rocm": current_omni_platform.is_rocm,
"xpu": current_omni_platform.is_xpu,
"npu": current_omni_platform.is_npu,
"musa": current_omni_platform.is_musa,
}
return pytest.mark.skipif(not checkers[platform](), reason=f"Requires {platform} platform")
def _apply_marks(func, marks: list[pytest.MarkDecorator]):
for mark in reversed(marks):
func = mark(func)
return func
def npu_marks(*, res: str, num_cards: int):
test_platform = pytest.mark.npu
if res == "A2":
test_resource = pytest.mark.A2
elif res == "A3":
test_resource = pytest.mark.A3
else:
test_resource = None
if num_cards == 1:
return [mark for mark in [test_platform, test_resource] if mark is not None]
test_distributed = pytest.mark.distributed_npu(num_cards=num_cards)
return [mark for mark in [test_platform, test_resource, test_distributed] if mark is not None]
def hardware_marks(*, res: dict[str, str], num_cards: int | dict[str, int] = 1):
"""Return marks for a **single** pytest item.
Different per-platform ``num_cards`` cannot share one item: ``cards_2`` and
``cards_4`` would both match ``-m cards_2`` / ``-m cards_4``. Use
``@hardware_test`` (one collected variant per platform) or call this once
per platform and attach each list to its own ``pytest.param``.
"""
num_cards_dict = _normalize_num_cards(res, num_cards)
unique_counts = set(num_cards_dict.values())
if len(res) > 1 and len(unique_counts) > 1:
raise ValueError(
"hardware_marks() cannot attach different per-platform num_cards to one pytest item "
f"(got {num_cards_dict}). Use @hardware_test, which collects one variant per platform, "
"or call hardware_marks() once per platform."
)
for platform, _ in res.items():
if platform not in ("cuda", "rocm", "xpu", "npu", "musa"):
raise ValueError(f"Unsupported platform: {platform}")
if isinstance(num_cards, int):
num_cards_dict = {platform: num_cards for platform in res.keys()}
else:
num_cards_dict = num_cards
for platform in num_cards_dict.keys():
if platform not in res:
raise ValueError(f"Platform '{platform}' in num_cards but not in res.")
for platform in res.keys():
if platform not in num_cards_dict:
num_cards_dict[platform] = 1
all_marks: list[pytest.MarkDecorator] = []
for platform, resource in res.items():
all_marks.extend(_marks_for_platform(platform, resource, num_cards_dict[platform]))
cards = num_cards_dict[platform]
if platform in ("cuda", "rocm", "xpu", "musa"):
marks = gpu_marks(res=resource, num_cards=cards)
elif platform == "npu":
marks = npu_marks(res=resource, num_cards=cards)
else:
raise ValueError(f"Unsupported platform: {platform}")
all_marks.extend(marks)
return all_marks
def hardware_test(*, res: dict[str, str], num_cards: int | dict[str, int] = 1):
"""Apply hardware marks; split collected items when card counts differ by platform.
all_marks = hardware_marks(res=res, num_cards=num_cards)
A single item cannot carry both ``cards_4`` (CUDA) and ``cards_2`` (ROCm):
``-m "H100 and cards_4 and cuda"`` would otherwise also match
``-m "H100 and cards_2 and cuda"``. When ``num_cards`` differs across
platforms, this decorator parametrizes one variant per platform so each
item only has that platform's SKU and ``cards_{n}``.
"""
import pytest
def wrapper(f):
func = f
for mark in reversed(all_marks):
func = mark(func)
return func
num_cards_dict = _normalize_num_cards(res, num_cards)
mixed_counts = len(res) > 1 and len(set(num_cards_dict.values())) > 1
if not mixed_counts:
all_marks = hardware_marks(res=res, num_cards=num_cards)
def apply_union_marks(f):
return _apply_marks(f, all_marks)
return apply_union_marks
params = []
for platform, resource in res.items():
marks = _marks_for_platform(platform, resource, num_cards_dict[platform])
marks.append(_skipif_not_platform(platform))
params.append(pytest.param(platform, marks=marks, id=platform))
def apply_split_params(f):
func = pytest.mark.usefixtures(HARDWARE_MARK_NORMALIZATION_FIXTURE)(f)
return pytest.mark.parametrize(HARDWARE_MARK_NORMALIZATION_FIXTURE, params, indirect=True)(func)
return apply_split_params
return wrapper
+2 -40
View File
@@ -1,16 +1,12 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from types import SimpleNamespace
import pytest
from tests.helpers import assertions
from tests.helpers.assertions import (
_assert_transcript_matches,
_resolve_audio_transcript,
assert_audio_speech_response,
)
from tests.helpers.assertions import _assert_transcript_matches, _resolve_audio_transcript
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
@@ -75,40 +71,6 @@ def test_resolve_transcript_leaves_language_unset_by_default(monkeypatch):
assert captured["model_size"] == "small"
@pytest.mark.parametrize(
("request_config", "expected_text"),
[
({"input": "spoken words"}, "spoken words"),
(
{
"input": "<|style:whispering|>spoken words",
"transcript_expected_text": "spoken words",
},
"spoken words",
),
],
)
def test_speech_transcript_expected_text(monkeypatch, request_config, expected_text):
captured: dict = {}
monkeypatch.setattr(assertions, "convert_audio_bytes_to_text", lambda *_args, **_kwargs: "spoken words")
def capture_match(_transcript, _audio_bytes, expected_text, **_kwargs):
captured["expected_text"] = expected_text
monkeypatch.setattr(assertions, "_assert_transcript_matches", capture_match)
response = SimpleNamespace(success=True, audio_bytes=b"fake-wav", audio_format="audio/wav")
request_config["response_format"] = "wav"
assert_audio_speech_response(
response,
request_config,
"advanced_model",
)
assert captured["expected_text"] == expected_text
def test_escalated_transcript_keeps_declared_language(monkeypatch):
# The escalated pass must honour the same language, otherwise a request that
# pins one silently falls back to auto-detection on retry.
-139
View File
@@ -1,139 +0,0 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import pytest
from tests.helpers.mark import (
_gpu_res_platforms,
hardware_marks,
hardware_test,
)
pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
def _mark_names(marks) -> set[str]:
return {mark.name for mark in marks}
def test_gpu_platforms_come_from_pyproject_gpu_tag():
assert "npu" not in _gpu_res_platforms()
assert "gpu" not in _gpu_res_platforms()
def test_default_num_cards_adds_cards_1():
names = _mark_names(hardware_marks(res={"cuda": "H100"}))
assert "cards_1" in names
assert "H100" in names
assert "cards_2" not in names
def test_h100_two_cards_is_selectable_with_cards_2():
names = _mark_names(hardware_marks(res={"cuda": "H100"}, num_cards=2))
assert names >= {"H100", "cuda", "gpu", "cards_2"}
assert "cards_1" not in names
assert "cards_4" not in names
def test_h100_four_cards_is_selectable_with_cards_4():
names = _mark_names(hardware_marks(res={"cuda": "H100"}, num_cards=4))
assert "cards_4" in names
assert "cards_2" not in names
def test_npu_card_count_mark_is_independent_of_sku():
names = _mark_names(hardware_marks(res={"npu": "A2"}, num_cards=4))
assert "cards_4" in names
assert "npu" in names
assert "gpu" not in names
def test_hardware_marks_rejects_mixed_counts_on_one_item():
with pytest.raises(ValueError, match="different per-platform num_cards"):
hardware_marks(
res={"cuda": "H100", "rocm": "MI325"},
num_cards={"cuda": 4, "rocm": 2},
)
def test_same_count_multi_platform_stays_one_item():
names = _mark_names(hardware_marks(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2))
assert names >= {"H100", "cuda", "MI325", "rocm", "cards_2"}
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards=2)
def _probe():
return None
names = {mark.name for mark in _probe.pytestmark}
assert "parametrize" not in names
assert "cards_2" in names
assert "H100" in names
assert "MI325" in names
def test_hardware_test_decorator_applies_cards_mark():
@hardware_test(res={"cuda": "H100"}, num_cards=2)
def _probe():
return None
names = {mark.name for mark in _probe.pytestmark}
assert "cards_2" in names
assert "H100" in names
def _hardware_platform_params(func):
marks = func.pytestmark if isinstance(func.pytestmark, list) else [func.pytestmark]
for mark in marks:
if mark.name != "parametrize":
continue
argnames = mark.args[0]
if argnames == "_normalized_hardware_marks":
return mark.args[1]
raise AssertionError("expected @hardware_test to parametrize _normalized_hardware_marks")
def test_mixed_card_counts_split_so_cuda_filter_does_not_see_rocm_count():
@hardware_test(res={"cuda": "H100", "rocm": "MI325"}, num_cards={"cuda": 4, "rocm": 2})
def _probe():
return None
variants = {param.id: {mark.name for mark in param.marks} for param in _hardware_platform_params(_probe)}
cuda_names = variants["cuda"]
rocm_names = variants["rocm"]
assert cuda_names >= {"H100", "cuda", "cards_4"}
assert "cards_2" not in cuda_names
assert "rocm" not in cuda_names
assert "MI325" not in cuda_names
assert rocm_names >= {"MI325", "rocm", "cards_2"}
assert "cards_4" not in rocm_names
assert "cuda" not in rocm_names
assert "H100" not in rocm_names
def test_unsupported_card_count_raises():
with pytest.raises(ValueError, match="num_cards=16"):
hardware_marks(res={"cuda": "H100"}, num_cards=16)
@pytest.mark.parametrize("platform", ["cpu", "gpu"])
def test_res_rejects_cpu_and_gpu(platform):
with pytest.raises(ValueError, match="not a res dict key"):
hardware_marks(res={platform: "H100"})
def test_npu_rejects_unsupported_sku():
with pytest.raises(ValueError, match="Invalid npu resource type"):
hardware_marks(res={"npu": "A100"})
@pytest.mark.parametrize("sku", ["H200", "B200", "H800"])
def test_cuda_registered_skus_attach_sku_and_cards(sku):
names = _mark_names(hardware_marks(res={"cuda": sku}, num_cards=2))
assert names >= {sku, "cuda", "gpu", "cards_2"}
def test_cuda_rejects_sku_from_another_platform():
with pytest.raises(ValueError, match="Invalid cuda resource type"):
hardware_marks(res={"cuda": "MI325"})
@@ -1,5 +1,4 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""Deterministic unit tests for higgs-audio v3.
These tests verify AC-1 (config), AC-3 (prompt), AC-4 (fused modules),
@@ -344,22 +343,7 @@ class TestSamplerMethods:
return t
@staticmethod
def _make_state_tracking_talker(num_rows):
from vllm_omni.model_executor.models.higgs_audio_v3 import higgs_audio_v3_talker as mod
talker_cls = mod.HiggsAudioV3TalkerForConditionalGeneration
talker = talker_cls.__new__(talker_cls)
torch.nn.Module.__init__(talker)
talker.num_codebooks = 8
talker._decode_last_codes = torch.zeros(num_rows, 8, dtype=torch.long)
talker._decode_has_codes = torch.zeros(num_rows, dtype=torch.bool)
talker._decode_delay_count = torch.zeros(num_rows, dtype=torch.long)
talker._decode_eoc_countdown = torch.full((num_rows,), -1, dtype=torch.long)
talker._decode_generation_done = torch.zeros(num_rows, dtype=torch.bool)
return talker
@staticmethod
def _sampling_metadata(temperature=1.0, top_k=50, top_p=0.95, generators=None):
def _sampling_metadata(temperature=1.0, top_k=50, top_p=0.95):
return type(
"SamplingMetadata",
(),
@@ -368,16 +352,9 @@ class TestSamplerMethods:
"top_k": top_k,
"top_p": top_p,
"all_greedy": temperature is None,
"generators": generators or {},
},
)()
@staticmethod
def _seeded_generator(seed):
generator = torch.Generator(device="cpu")
generator.manual_seed(seed)
return generator
def test_sample_respects_mask(self):
"""Tokens masked to -inf must never be sampled."""
t = self._make_batched_sampler_talker(num_rows=1)
@@ -465,51 +442,6 @@ class TestSamplerMethods:
assert torch.count_nonzero(seen[0][0]).item() == 1
assert torch.count_nonzero(seen[0][2]).item() > 1
def test_seeded_sampling_is_batch_independent(self):
t = self._make_minimal_talker()
num_codebooks = 8
torch.manual_seed(123)
first_logits = torch.randn(num_codebooks, 64)
second_logits = torch.randn(num_codebooks, 64)
filler_logits = torch.randn(num_codebooks, 64)
reference_generator = self._seeded_generator(42)
batched_generators = {
1: self._seeded_generator(42),
2: self._seeded_generator(42),
}
reference_first = t._sample_audio_codes(
first_logits,
self._sampling_metadata(generators={0: reference_generator}),
num_codebooks=num_codebooks,
)
batched_first = t._sample_audio_codes(
torch.cat([filler_logits, first_logits, first_logits]),
self._sampling_metadata(generators=batched_generators),
num_codebooks=num_codebooks,
)
assert torch.equal(reference_first, batched_first[num_codebooks : 2 * num_codebooks])
assert torch.equal(reference_first, batched_first[2 * num_codebooks :])
reference_second = t._sample_audio_codes(
second_logits,
self._sampling_metadata(generators={0: reference_generator}),
num_codebooks=num_codebooks,
)
condensed_second = t._sample_audio_codes(
torch.cat([second_logits, second_logits]),
self._sampling_metadata(
generators={
0: batched_generators[1],
1: batched_generators[2],
}
),
num_codebooks=num_codebooks,
)
assert torch.equal(reference_second, condensed_second[:num_codebooks])
assert torch.equal(reference_second, condensed_second[num_codebooks:])
def test_delay_masking_forces_boc_during_delay(self):
"""During delay phase, codebooks beyond delay_count must have only BOC allowed."""
from vllm_omni.model_executor.models.higgs_audio_v3.higgs_audio_v3_talker import BOC_ID
@@ -746,58 +678,6 @@ class TestSamplerMethods:
assert not t._decode_generation_done[2:5].any()
assert t._decode_eoc_countdown[2:5].eq(-1).all()
def test_decode_state_follows_request_after_batch_condensation(self):
talker = self._make_state_tracking_talker(3)
talker._sync_decode_state_with_batch(["A", "B", "C"])
c_codes = torch.arange(300, 308)
talker._decode_last_codes[:] = torch.stack((torch.arange(100, 108), torch.zeros(8, dtype=torch.long), c_codes))
talker._decode_has_codes[:] = torch.tensor([True, False, True])
talker._decode_delay_count[:] = torch.tensor([3, 0, 8])
talker.on_requests_finished({"B"})
talker._sync_decode_state_with_batch(["A", "C"])
assert talker._decode_has_codes[1].item() is True
assert talker._decode_delay_count[1].item() == 8
assert torch.equal(talker._decode_last_codes[1], c_codes)
def test_finished_request_state_is_cleared_before_pool_slot_reuse(self):
talker = self._make_state_tracking_talker(1)
talker._sync_decode_state_with_batch(["finished"])
talker._decode_has_codes[0] = True
talker._decode_last_codes[0] = 7
talker._decode_delay_count[0] = 5
talker._decode_eoc_countdown[0] = 0
talker._decode_generation_done[0] = True
talker.on_requests_finished({"finished"})
talker._sync_decode_state_with_batch(["new"])
assert talker._decode_has_codes[0].item() is False
assert talker._decode_last_codes[0].eq(0).all()
assert talker._decode_delay_count[0].item() == 0
assert talker._decode_eoc_countdown[0].item() == -1
assert talker._decode_generation_done[0].item() is False
def test_finished_request_state_is_cleared_when_request_id_is_reused(self):
talker = self._make_state_tracking_talker(1)
talker._sync_decode_state_with_batch(["request"])
talker._decode_has_codes[0] = True
talker._decode_last_codes[0] = 7
talker._decode_delay_count[0] = 5
talker._decode_eoc_countdown[0] = 0
talker._decode_generation_done[0] = True
talker.on_requests_finished({"request"})
talker._sync_decode_state_with_batch(["request"])
assert talker._decode_has_codes[0].item() is False
assert talker._decode_last_codes[0].eq(0).all()
assert talker._decode_delay_count[0].item() == 0
assert talker._decode_eoc_countdown[0].item() == -1
assert talker._decode_generation_done[0].item() is False
def test_mixed_batch_prefill_mask_targets_request_rows(self):
"""Mixed prefill/decode must reset only prefill request rows."""
t = self._make_batched_sampler_talker(num_rows=3)
Loaded 100 of 177 files, more files were not shown because too many files have changed in this diff. Show more