[CI][Perf] migrate diffusion DFX benches to vllm bench serve --omni (#7737)

Signed-off-by: wangyu <410167048@qq.com>
This commit is contained in:
wangyu authored and GitHub committed 2026-09-21 19:16:39 -07:00
1 parent a4ad73e08b
commit 0570b0da7e
30 files changed
+2040 -729

No files matched your search

@@ -319,7 +319,7 @@ source_file_dependencies:
diffusion_wan22_perf:
- *wan22
- tests/dfx/perf/scripts/run_diffusion_benchmark.py
- tests/dfx/perf/scripts/run_benchmark.py
- tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json
diffusion_wan22_reliability:
@@ -345,7 +345,7 @@ source_file_dependencies:
diffusion_cosmos3_perf:
- *cosmos3
- tests/dfx/perf/scripts/run_diffusion_benchmark.py
- tests/dfx/perf/scripts/run_benchmark.py
- tests/dfx/perf/tests/test_cosmos3_vllm_omni.json
diffusion_qwen_image_function:
@@ -418,7 +418,7 @@ source_file_dependencies:
diffusion_minimax_h3_perf:
- *minimax_h3
- tests/dfx/perf/scripts/run_diffusion_benchmark.py
- tests/dfx/perf/scripts/run_benchmark.py
- tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json
diffusion_hunyuan_video_function:
@@ -432,7 +432,7 @@ source_file_dependencies:
diffusion_hunyuan_video_perf:
- *hunyuan_video
- tests/dfx/perf/scripts/run_diffusion_benchmark.py
- tests/dfx/perf/scripts/run_benchmark.py
- tests/dfx/perf/tests/test_hunyuanvideo15_t2v_vllm_omni.json
- tests/dfx/perf/tests/test_hunyuanvideo15_i2v_vllm_omni.json
@@ -459,6 +459,11 @@ source_file_dependencies:
- tests/e2e/online_serving/test_lingbot_video_moe.py
- tests/e2e/offline_inference/test_lingbot_world_v2.py
diffusion_lingbot_perf:
- *lingbot_video
- tests/dfx/perf/scripts/run_benchmark.py
- tests/dfx/perf/tests/test_lingbot_video_vllm_omni.json
# Invalid-param jobs: model business code only, plus the reliability scripts.
reliability_invalid_param_h100:
+63 -90
View File
@@ -53,80 +53,62 @@ steps:
- label: ":full_moon: Omni · Accuracy Test"
source_file_dependencies: omni_qwen3_omni_accuracy
timeout_in_minutes: 180
artifact_paths:
- tests/e2e/accuracy/qwen3_omni/results/qwen_omni_acc/*.json
commands:
- export SEED_TTS_WER_EVAL=1
- export SEED_TTS_EVAL_DEVICE=cuda:1
- |
set +e
pytest -s -v tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py -m "full_model and H100 and B200 and cards_2" --run-level full_model
EXIT=$$?
buildkite-agent artifact upload "tests/e2e/accuracy/qwen3_omni/results/qwen_omni_acc/*.json"
exit $$EXIT
- pytest -s -v tests/e2e/accuracy/qwen3_omni/test_qwen3_omni.py -m "full_model and H100 and B200 and cards_2" --run-level full_model
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Accuracy Test"
source_file_dependencies: omni_minicpmo_4_5_accuracy
timeout_in_minutes: 180
artifact_paths:
- tests/e2e/accuracy/minicpmo_4_5/results/*.json
commands:
- export SEED_TTS_WER_EVAL=1
- export SEED_TTS_EVAL_DEVICE=cuda:1
- |
set +e
pytest -s -v tests/e2e/accuracy/minicpmo_4_5/test_minicpmo_4_5.py -m "full_model and H100 and B200 and cuda and (cards_1 or cards_2)" --run-level full_model
EXIT=$$?
buildkite-agent artifact upload "tests/e2e/accuracy/minicpmo_4_5/results/*.json"
exit $$EXIT
- pytest -s -v tests/e2e/accuracy/minicpmo_4_5/test_minicpmo_4_5.py -m "full_model and H100 and B200 and cuda and (cards_1 or cards_2)" --run-level full_model
- label: ":full_moon: Omni · Perf Test · No Async Chunk"
source_file_dependencies: omni_qwen3_omni_perf
key: nightly-omni-performance-no-async-chunk
timeout_in_minutes: 300
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_no_async_chunk.json -m "H100 and B200 and cards_2"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_no_async_chunk.json -m "H100 and B200 and cards_2"
- label: ":full_moon: Omni · Perf Test · Async Chunk"
source_file_dependencies: omni_qwen3_omni_perf
key: nightly-omni-performance-async-chunk
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_async_chunk.json -m "H100 and B200 and full_model and cards_2"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_async_chunk.json -m "H100 and B200 and full_model and cards_2"
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Perf Test"
source_file_dependencies: omni_minicpmo_4_5_perf
key: nightly-omni-performance-minicpmo-4-5
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5.json -m "H100 and B200 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5.json -m "H100 and B200 and cards_1"
- label: ":full_moon: Omni · MiniCPM-o 4.5 · Duplex Seed-TTS Perf Test"
source_file_dependencies: omni_minicpmo_4_5_duplex_perf
key: nightly-omni-performance-minicpmo-4-5-duplex-seed-tts
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5_duplex_seed_tts.json -m "H100 and B200 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minicpmo_4_5_duplex_seed_tts.json -m "H100 and B200 and cards_1"
- label: ":full_moon: Omni · Multi-Replica Startup Test with 4x H100"
source_file_dependencies: omni_qwen3_omni_function
@@ -153,29 +135,23 @@ steps:
source_file_dependencies: tts_qwen3_tts_perf
key: nightly-tts-performance-single
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
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 B200 and cards_1"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json -m "H100 and B200 and cards_1"
- label: ":full_moon: TTS · Perf Test · 2-GPU"
source_file_dependencies: tts_qwen3_tts_perf
key: nightly-tts-performance-2gpu
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
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 B200 and cards_2"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json -m "H100 and B200 and cards_2"
- group: ":card_index_dividers: Tiny Model Tests [Multi-GPU]"
key: nightly-tiny-model-test-group
@@ -277,33 +253,27 @@ steps:
source_file_dependencies: diffusion_qwen_image_perf
key: nightly-diffusion-x2iat-performance-qwen-image-single
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/diffusion_result_*.json
- tests/dfx/perf/results/logs/*.log
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- 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 B200 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
- 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 B200 and cards_1"
- label: ":full_moon: Diffusion X2I(&A&T) · Perf Test · Qwen-Image · 4-GPU"
source_file_dependencies: diffusion_qwen_image_perf
key: nightly-diffusion-x2iat-performance-qwen-image-4gpu
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/diffusion_result_*.json
- tests/dfx/perf/results/logs/*.log
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export CACHE_DIT_VERSION=1.5.0
# Do not pin DIFFUSION_ATTENTION_BACKEND: H100 auto-selects FLASH_ATTN;
# B200 rejects explicit FLASH_ATTN without FA4 and uses CUDNN/TRTLLM.
- |
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 B200 and cards_4"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
exit $$EXIT
- 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 B200 and cards_4"
- group: ":card_index_dividers: Diffusion X2V Model Test"
key: nightly-diffusion-x2v-group
@@ -399,15 +369,11 @@ steps:
source_file_dependencies: diffusion_wan22_perf
key: nightly-diffusion-x2v-performance-single
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- |
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 B200 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
- export BENCHMARK_DIR=tests/dfx/perf/results
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and B200 and cards_1"
- label: ":full_moon: Diffusion X2V · Perf Test · 2-GPU"
source_file_dependencies:
@@ -415,16 +381,16 @@ steps:
- diffusion_cosmos3_perf
key: nightly-diffusion-x2v-performance-2gpu
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
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 B200 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json -m "H100 and B200 and cards_2"
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 B200 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json -m "H100 and B200 and cards_2"
EXIT2=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
if [ $$EXIT1 -ne 0 ]; then exit $$EXIT1; fi
exit $$EXIT2
@@ -432,16 +398,16 @@ steps:
source_file_dependencies: diffusion_hunyuan_video_perf
key: nightly-diffusion-x2v-performance-hunyuanvideo15
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_hunyuanvideo15_t2v_vllm_omni.json -m "H100 and B200 and (cards_1 or cards_2)"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_hunyuanvideo15_t2v_vllm_omni.json -m "H100 and B200 and (cards_1 or cards_2)"
EXIT1=$$?
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_hunyuanvideo15_i2v_vllm_omni.json -m "H100 and B200 and (cards_1 or cards_2)"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_hunyuanvideo15_i2v_vllm_omni.json -m "H100 and B200 and (cards_1 or cards_2)"
EXIT2=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
if [ $$EXIT1 -ne 0 ]; then exit $$EXIT1; fi
exit $$EXIT2
@@ -449,15 +415,21 @@ steps:
source_file_dependencies: diffusion_minimax_h3_perf
key: nightly-diffusion-x2v-performance-minimax-h3
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json -m "H100 and B200 and cards_4"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/diffusion_result_*.json"
buildkite-agent artifact upload "tests/dfx/perf/results/logs/*.log"
exit $$EXIT
- export BENCHMARK_DIR=tests/dfx/perf/results
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_minimax_h3_vllm_omni.json -m "H100 and B200 and cards_4"
- label: ":full_moon: Diffusion X2V · Perf Test · LingBot"
source_file_dependencies: diffusion_lingbot_perf
key: nightly-diffusion-x2v-performance-lingbot
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_lingbot_video_vllm_omni.json -m "H100 and B200 and cards_1"
- group: ":card_index_dividers: Diffusion Test"
key: nightly-diffusion-test-group
@@ -533,6 +505,9 @@ steps:
- nightly-diffusion-x2v-performance-single
- nightly-diffusion-x2v-performance-2gpu
if: build.env("NIGHTLY") == "1" && build.env("EMAIL_DISTRIBUTION") == "1"
artifact_paths:
- tests/dfx/perf/results/*.xlsx
- tests/dfx/perf/results/*.html
commands:
- pip install openpyxl
- export DEFAULT_INPUT_DIR=tests/dfx/perf/results
@@ -548,7 +523,5 @@ steps:
- 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"
- buildkite-agent artifact upload "tests/dfx/perf/results/*.xlsx"
- buildkite-agent artifact upload "tests/dfx/perf/results/*.html"
agents:
queue: "cpu_queue_premerge"
+11 -19
View File
@@ -12,6 +12,8 @@ steps:
depends_on: upload-weekly-pipeline
if: build.env("WEEKLY") == "1"
timeout_in_minutes: 60
artifact_paths:
- coverage-core-model-cpu-*.xml.gz
commands:
- |
set +e
@@ -19,7 +21,6 @@ steps:
pytest -sv tests/ -m 'core_model and cpu' --cov=vllm_omni --cov-report=term-missing:skip-covered --cov-report=xml:$${REPORT}
EXIT=$$?
gzip -9 -f "$${REPORT}"
buildkite-agent artifact upload "$${REPORT}.gz"
exit $$EXIT
mirror_hardwares: l4_1
@@ -79,41 +80,32 @@ steps:
source_file_dependencies: omni_qwen3_omni_perf
key: weekly-omni-performance-vllm-text
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_vllm_text.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_vllm_text.json
mirror_hardwares: h100_2
- label: ":full_moon: Omni · Perf Test · Async Chunk · Random"
key: weekly-omni-performance-async-chunk-random
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_async_chunk.json -m "H100 and slow"
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_async_chunk.json -m "H100 and slow"
mirror_hardwares: h100_2
- label: ":full_moon: Omni · Perf Test · Multi-Replica"
source_file_dependencies: omni_qwen3_omni_perf
key: weekly-omni-performance-multi-replicas
timeout_in_minutes: 180
artifact_paths:
- tests/dfx/perf/results/*.json
commands:
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json
EXIT=$$?
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
- pytest -s -v tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen3_omni_multi_replicas.json
mirror_hardwares: h100_3
- group: ":card_index_dividers: E2E Tests"
+3 -4
View File
@@ -286,11 +286,10 @@ steps:
HF_TOKEN: "${HF_TOKEN}"
DIFFUSION_ATTENTION_BACKEND: TORCH_SDPA
commands:
- export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
- export BENCHMARK_DIR=tests/dfx/perf/results
- |
set +e
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py -m npu --test-config-file tests/dfx/perf/tests/test_wan22_i2v_vllm_omni.json
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py -m npu --test-config-file tests/dfx/perf/tests/test_wan22_i2v_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"
buildkite-agent artifact upload "tests/dfx/perf/results/*.json"
exit $$EXIT
@@ -7,16 +7,17 @@ When you want to add L4-level ***performance test*** cases, add entries to JSON
| Omni (nightly) | `run_benchmark.py` | `test_qwen3_omni_no_async_chunk.json`, `test_qwen3_omni_async_chunk.json` (`full_model` without `slow` in `mark`) |
| Omni (weekly) | `run_benchmark.py` | `test_qwen3_omni_async_chunk.json` (CUDA only), `test_qwen3_omni_vllm_text.json`, `test_qwen3_omni_multi_replicas.json` (`slow` in `mark`; **Perf Test** in `test-weekly.yml`) |
| TTS | `run_benchmark.py` | `test_tts.json`, `test_voxcpm2.json`, `test_higgs_audio_v3.json` |
| Diffusion | `run_diffusion_benchmark.py` | `test_qwen_image_vllm_omni.json`, `test_bagel_vllm_omni.json`, `test_wan22_i2v_vllm_omni.json`, `test_cosmos3_vllm_omni.json`, … |
| Diffusion (`/v1/chat/completions`) | `run_diffusion_benchmark.py` | `test_qwen_image_vllm_omni.json`, `test_bagel_vllm_omni.json`, … |
| Diffusion (`/v1/images/generations`, `/v1/images/edits`, `/v1/videos`) | `run_benchmark.py` | `test_wan22_i2v_vllm_omni.json`, `test_cosmos3_vllm_omni.json`, `test_lingbot_video_vllm_omni.json`, … |
#### How runners pick cases
Without **`--test-config-file`**, each runner scans all `*.json` under `tests/dfx/perf/tests/` but only keeps its own model type:
- **`run_benchmark.py`**: omni and TTS cases only (skips diffusion JSON).
- **`run_diffusion_benchmark.py`**: diffusion cases only (skips omni / TTS JSON).
- **`run_benchmark.py`**: cases whose `benchmark_params` use ``dataset_name`` (omni, TTS, and image/video OpenAI generation via `vllm bench serve --omni`).
- **`run_diffusion_benchmark.py`**: cases whose `benchmark_params` use ``dataset`` (diffusion client schema, including custom jsonl).
Diffusion cases are detected when the JSON has `server_type` (typically `"vllm-omni"`) or `"diffusion"` in the `mark` array. Omni / TTS JSON has neither.
Diffusion-script cases are detected by `is_diffusion_perf_config()`: presence of ``dataset`` (without ``dataset_name``) in `benchmark_params`. Marks / `server_type` / endpoint values are not used for this split.
#### Running perf cases
@@ -60,16 +61,16 @@ Pass **`--test-config-file`** to load one JSON file, or omit it for the bulk sca
##### Overview
| Field | Required | Description |
| ------------------ | -------------- | -------------------------------------------- |
| test_name | Yes | Unique identifier for the test case |
| mark | No | Pytest marks; see **`mark` field** below |
| server_params | Yes | Server-side configuration parameters |
| benchmark_params | Yes | Benchmark running parameters |
| server_type | Diffusion only | Routes case to run_diffusion_benchmark.py |
| benchmark_endpoint | Diffusion only | Benchmark API path |
| Field | Required | Description |
| ------------------ | --------- | ---------------------------------------------------------------------------------------- |
| test_name | Yes | Unique identifier for the test case |
| mark | No | Pytest marks; see **`mark` field** below |
| server_params | Yes | Server-side configuration parameters |
| benchmark_params | Yes | Benchmark running parameters |
| server_type | Diffusion | Only for diffusion-script JSON; omit on omni-bench generation cases |
| benchmark_endpoint | Optional | Legacy diffusion custom-jsonl alias; prefer `benchmark_params[].endpoint` |
Omit `mark` only for configs not meant to be filtered by `-m`. `server_type` is typically `"vllm-omni"`. `benchmark_endpoint` examples: `/v1/videos`, `/v1/images/generations`.
Omit `mark` only for configs not meant to be filtered by `-m`. Cases that call `/v1/images/edits`, `/v1/images/generations`, or `/v1/videos` use the same `benchmark_params` schema as Omni/TTS (`dataset_name`, `endpoint`, `extra_body`) and are executed by `run_benchmark.py` — do not set `server_type` or `task` on those cases. Remaining diffusion cases (usually `/v1/chat/completions`, or custom jsonl) stay on `run_diffusion_benchmark.py` and may keep `server_type`.
#### `mark` field
@@ -100,7 +101,7 @@ Recommended for L4 perf cases:
}
```
Multi-GPU diffusion (example: Cosmos3 with `cfg-parallel-size=2`):
Multi-GPU generation via omni bench (example: Cosmos3 with `cfg-parallel-size=2`):
```JSON
{
@@ -110,10 +111,17 @@ Multi-GPU diffusion (example: Cosmos3 with `cfg-parallel-size=2`):
"full_model",
"diffusion"
],
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/generations",
"server_params": { "...": "..." },
"benchmark_params": [ { "name": "1024x1024_steps4", "...": "..." } ]
"benchmark_params": [
{
"name": "1024x1024_steps4",
"dataset_name": "random",
"endpoint": "/v1/images/generations",
"num_prompts": 3,
"max_concurrency": 1,
"extra_body": { "width": 1024, "height": 1024, "num_inference_steps": 4 }
}
]
}
```
@@ -132,8 +140,8 @@ Result files use the **runtime** hardware label from `get_runtime_resource_label
Examples:
- Omni/TTS: `result_{test_name}_{optional_hw}_{dataset}_....json` under `BENCHMARK_DIR`
- Diffusion: one aggregate `diffusion_result_{config_stem}_{optional_hw}_{timestamp}.json` per source JSON file (array of all runs from that file)
- Omni/TTS and OpenAI generation endpoints (`/v1/images/*`, `/v1/videos`): `result_{test_name}_{optional_hw}_{dataset}_....json` under `BENCHMARK_DIR`
- Remaining diffusion (`run_diffusion_benchmark.py`): one aggregate `diffusion_result_{config_stem}_{optional_hw}_{timestamp}.json` per source JSON file
#### Local commands
@@ -145,6 +153,9 @@ pytest -s -v tests/dfx/perf/scripts/run_benchmark.py -m "full_model and omni and
# Single file (same selectors as the CI Perf steps)
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py \
--test-config-file tests/dfx/perf/tests/test_bagel_vllm_omni.json
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py \
--test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json \
-m "H100 and B200 and cards_2"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py \
--test-config-file tests/dfx/perf/tests/test_qwen3_omni_async_chunk.json \
-m "H100 and full_model and not slow"
@@ -203,7 +214,8 @@ You can add any benchmark running parameters you need here. For all optional par
2. For boolean variables in the running parameters, modify them to forms such as ignore_eos: true/false and fill them into the JSON file.
3. Optionally add a `baseline` object (see **Baseline thresholds** below). If you omit `baseline` or leave it empty, the performance test still runs but does not assert metric thresholds from this field.
4. Set `"name"` on each `benchmark_params` entry for stable pytest ids and readable result keys.
5. The qps and concurrency modes are recommended to be mutually exclusive. For detailed explanations, see the table below:
5. Image/video generation cases (`/v1/images/generations`, `/v1/images/edits`, `/v1/videos`) use this same schema: set `endpoint` to the API path, put width/height/steps/frames in `extra_body`, use `dataset_name: random` for text-only inputs or `random-mm` when the request needs a synthetic image/video. Do not set `server_type` or `task`, and do not use `random-request-config` or kebab-case diffusion client fields.
6. The qps and concurrency modes are recommended to be mutually exclusive. For detailed explanations, see the table below:
| Parameter | Type | Required | Example/Values | Description |
| --------------- | ------------- | -------- | -------------------- | ------------------------------------ |
+1 -1
View File
@@ -165,7 +165,7 @@ Failed jobs: 1/2
pytest -sv tests/dfx/perf/scripts/run_benchmark.py -m "full_model and tts and H100"
pytest -sv tests/dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"
pytest -sv tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_tts.json
pytest -sv tests/dfx/perf/scripts/run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json
pytest -sv tests/dfx/perf/scripts/run_benchmark.py --test-config-file tests/dfx/perf/tests/test_cosmos3_vllm_omni.json
```
Nightly **Perf Test** jobs in [``test-nightly.yml``](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-nightly.yml) use ``--test-config-file`` only (no ``-m``). Weekly **Perf Test** in [``test-weekly.yml``](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-weekly.yml) runs ``test_qwen3_omni_vllm_text.json`` and ``test_qwen3_omni_multi_replicas.json`` (JSON ``mark`` includes ``slow``). E2e L4 function tests use ``full_model`` + ``--run-level full_model``. Example:
+2 -1
View File
@@ -159,7 +159,8 @@ When `mark` is present, it must be an **array** with exactly one ``hardware_mark
}
```
- Local bulk load: `pytest -sv tests/dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"`
- Local bulk load: `pytest -sv tests/dfx/perf/scripts/run_benchmark.py -m "full_model and H100"` (omni/TTS and `/v1/images/*` + `/v1/videos` diffusion)
- Diffusion chat-completions remaining cases: `pytest -sv tests/dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"`
- Nightly CI perf steps: `--test-config-file tests/dfx/perf/tests/test_<model>_vllm_omni.json` (file selects cases; no `-m`)
- Result filenames use **runtime** GPU detection (`get_runtime_resource_label`); `H100` is omitted on the default CI pool
+9 -9
View File
@@ -5,7 +5,7 @@
## Pipelines
| `--model-class-name` | Task | Required checkpoint repositories |
|---|---|---|
| --- | --- | --- |
| `LTX2Pipeline` | LTX-2 one-stage T2V/I2V | `Lightricks/LTX-2` |
| `LTX2TwoStagePipeline` | LTX-2 ordinary two-stage T2V/I2V | `Lightricks/LTX-2` |
| `LTX2DistilledOneStagePipeline` | LTX-2 merged-distilled one-stage T2V/I2V | `rootonchair/LTX-2-19b-distilled` |
@@ -53,7 +53,7 @@ pipe(req, image=image, prompt=prompt)
The consolidation also removes these registry names without aliases:
| Removed name | Replacement |
|---|---|
| --- | --- |
| `LTX23Pipeline` | `LTX2Pipeline`; checkpoint metadata selects LTX-2.3 |
| `LTX2ImageToVideoPipeline` | `LTX2Pipeline` with `image=` |
| `LTX23ImageToVideoPipeline` | `LTX2Pipeline` with `image=`; checkpoint metadata selects LTX-2.3 |
@@ -67,7 +67,7 @@ offline and serving entrypoints already use named fields and are unaffected.
## One-Stage Defaults
| Parameter | LTX-2 | LTX-2.3 |
|---|---:|---:|
| --- | ---: | ---: |
| Width × height | 768 × 512 | 768 × 512 |
| Frames / frame rate | 121 / 24 | 121 / 24 |
| Denoise steps | 40 | 30 |
@@ -85,7 +85,7 @@ default to `121`.
## Two-Stage Defaults
| Parameter | Ordinary | Full-distilled |
|---|---:|---:|
| --- | ---: | ---: |
| Final width × height | 1536 × 1024 | 1536 × 1024 |
| Stage 1 width × height | 768 × 512 | 768 × 512 |
| Frames / frame rate | 121 / 24 | 121 / 24 |
@@ -172,7 +172,7 @@ spatio-temporal guidance (STG), cross-modality guidance, and rescaling.
Distilled stages and ordinary Stage 2 are fixed positive-only.
| Parameter | Default | Effect | Alias |
|---|---:|---|---|
| --- | ---: | --- | --- |
| `video_cfg_scale` | 3.0 | Video text CFG; `1.0` disables it | `video_cfg_guidance_scale` |
| `audio_cfg_scale` | 7.0 | Audio text CFG; `1.0` disables it | `audio_cfg_guidance_scale` |
| `video_stg_scale` | 1.0 | Video STG; `0.0` disables it | `video_stg_guidance_scale` |
@@ -210,7 +210,7 @@ per denoise step: `cond`, `uncond`, `ptb` (STG), and `mod`
(cross-modality). The useful balanced configurations are therefore:
| `--cfg-parallel-size` | Passes per rank | Guidance-slot utilization | Notes |
|---:|---:|---:|---|
| ---: | ---: | ---: | --- |
| `1` | 4 | 100% | Single-rank fused guidance batch |
| `2` | 2 | 100% | Recommended two-rank configuration |
| `4` | 1 | 100% | One guidance pass per rank |
@@ -269,7 +269,7 @@ noted below.
### Complete `forward` Surface
| Argument | Type/default | Meaning and constraints |
|---|---|---|
| --- | --- | --- |
| `req` | `DiffusionRequestBatch`, required | Only positional argument; contains prompts and per-request sampling parameters. |
| `image` | image or batch, `None` | Direct value wins over request images; no image selects T2V. I2V accepts one image per prompt, and a batch cannot mix T2V/I2V. |
| `prompt` | string or list, `None` | Positive-text fallback; request prompts win. Mutually exclusive with `prompt_embeds`. |
@@ -306,7 +306,7 @@ request prompt payload; LTX guidance fields live in sampling `extra_args`.
### Recipe-Specific Request Capabilities
| Override | One-stage | Ordinary two-stage | Distilled two-stage |
|---|---|---|---|
| --- | --- | --- | --- |
| Guidance | Supported | Stage 1 only; Stage 2 is positive-only | Fixed positive-only |
| Negative prompt/embeddings | Supported | Supported by Stage 1 | Rejected |
| `num_inference_steps` | Supported | Controls Stage 1; Stage 2 uses 3 | Fixed at 8 for Stage 1; Stage 2 uses 3 |
@@ -357,7 +357,7 @@ bundled offline CLI do not currently expose `sigmas`.
- The output audio sample rate comes from the loaded components and is not a
request parameter.
- For benchmarks, use `tests/dfx/perf/tests/test_ltx2_vllm_omni.json` with
`tests/dfx/perf/scripts/run_diffusion_benchmark.py`.
`tests/dfx/perf/scripts/run_benchmark.py`.
- Ordinary and full-distilled two-stage T2V/I2V are supported; HQ execution
remains out of scope.
+82 -7
View File
@@ -6,11 +6,16 @@ Unit tests for metrics.py
"""
import math
from types import SimpleNamespace
import pytest
from vllm.benchmarks.serve import TaskType
from vllm_omni.benchmarks.metrics.metrics import calculate_metrics
from vllm_omni.benchmarks.metrics.metrics import (
aggregate_stage_durations,
calculate_metrics,
print_stage_durations_metrics,
)
from vllm_omni.benchmarks.patch.patch import MixRequestFuncOutput
pytestmark = [pytest.mark.core_model, pytest.mark.benchmark, pytest.mark.cpu]
@@ -379,12 +384,7 @@ class _EmptyAwareTokenizer:
"""
def __call__(self, text, add_special_tokens=False):
class _R:
pass
r = _R()
r.input_ids = [0] * len(text)
return r
return SimpleNamespace(input_ids=[0] * len(text))
def _make_tts_output(prompt_len: int) -> MixRequestFuncOutput:
@@ -588,5 +588,80 @@ def test_image_with_generated_text_still_reports_text_result(capsys):
assert " Image Result " in out
def test_aggregate_stage_durations_mean_p50_p99() -> None:
ok_a = MixRequestFuncOutput()
ok_a.success = True
ok_a.stage_durations = {"diffuse": 1.0, "vae.decode": 0.2, "stage_0_gen_ms": 50.0}
ok_b = MixRequestFuncOutput()
ok_b.success = True
ok_b.stage_durations = {"diffuse": 3.0, "vae.decode": 0.4, "stage_1_gen_ms": 80.0}
failed = MixRequestFuncOutput()
failed.success = False
failed.stage_durations = {"diffuse": 99.0}
summaries = aggregate_stage_durations([ok_a, ok_b, failed])
assert summaries["stage_durations_mean"]["diffuse"] == pytest.approx(2.0)
assert summaries["stage_durations_p50"]["diffuse"] == pytest.approx(2.0)
assert summaries["stage_durations_mean"]["vae.decode"] == pytest.approx(0.3)
assert "stage_durations_p99" in summaries
assert "stage_0_gen_ms" not in summaries["stage_durations_mean"]
assert "stage_1_gen_ms" not in summaries["stage_durations_mean"]
def test_print_stage_durations_metrics(capsys) -> None:
output = MixRequestFuncOutput()
output.success = True
output.stage_durations = {
"Wan22I2VPipeline.diffuse": 1.25,
"Wan22I2VPipeline.text_encoder.forward": 0.4,
"queue_wait_ms": 0.5,
"stage_0_gen_ms": 1000.0,
}
summaries = aggregate_stage_durations([output])
metrics = SimpleNamespace(**summaries)
print_stage_durations_metrics([99.0], metrics)
out = capsys.readouterr().out
assert "Wan22I2VPipeline" not in out
assert "Mean Diffuse (s):" in out
assert "Median Diffuse (s):" in out
assert "P99 Diffuse (s):" in out
assert "Mean Text Encoder Forward (s):" in out
assert "Mean Queue Wait (ms):" in out
assert "Stage 0 Gen" not in out
assert "stage_0_gen" not in out
def test_profiler_stage_durations_print_without_print_stage(capsys) -> None:
output = MixRequestFuncOutput()
output.success = True
output.prompt_len = 8
output.latency = 1.0
output.stage_durations = {
"Wan22I2VPipeline.diffuse": 1.25,
"queue_wait_ms": 0.5,
"stage_0_gen_ms": 1000.0,
}
common = dict(
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=["e2el"],
max_concurrency=None,
request_rate=float("inf"),
benchmark_duration=1.0,
)
metrics, _ = calculate_metrics(**common, print_stage=False)
shown = capsys.readouterr().out
assert "Mean Diffuse (s):" in shown
assert "Mean Queue Wait (ms):" in shown
assert "Stage 0 Gen" not in shown
assert metrics.stage_durations_mean["Wan22I2VPipeline.diffuse"] == pytest.approx(1.25)
assert "stage_0_gen_ms" not in metrics.stage_durations_mean
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+388 -8
View File
@@ -22,12 +22,17 @@ from vllm_omni.benchmarks.data_modules.seed_tts_dataset import (
)
from vllm_omni.benchmarks.patch.patch import (
MixRequestFuncOutput,
_add_combined_video_form_references,
_add_video_extra_body_to_form,
_add_video_reference_to_form,
_apply_image_metrics_from_payload,
_apply_stage0_token_timings,
_apply_video_metrics_from_payload,
_attach_seed_tts_to_request_func_input,
_build_benchmark_session,
_extract_stage_durations_from_payload,
_iter_image_reference_inputs,
_iter_video_reference_inputs,
_omni_request_timeout_s,
async_request_openai_chat_omni_completions,
async_request_openai_image_edits_omni,
@@ -1359,16 +1364,15 @@ async def test_image_edits_defaults_to_non_streaming_json(mocker: MockerFixture)
return None
captured_stream: list[str] = []
real_add_field = None
import aiohttp
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
if name == "stream":
captured_stream.append(str(value))
return real_add_field(self, name, value, **kwargs)
import aiohttp
real_add_field = aiohttp.FormData.add_field
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
mock_session = mocker.AsyncMock()
@@ -1512,8 +1516,7 @@ def test_video_local_image_reference_not_forwarded_as_raw_extra_field(tmp_path,
"reference",
[
{"image_url": "https://example.com/ref.png"},
{"file_id": "file-abc"},
[{"image_url": "https://example.com/a.png"}, {"file_id": "file-xyz"}],
[{"image_url": "https://example.com/a.png"}, {"image_url": "https://example.com/b.png"}],
],
)
def test_video_structured_image_reference_serialized_to_form(reference: object, mocker: MockerFixture) -> None:
@@ -1541,18 +1544,395 @@ def test_video_structured_image_reference_serialized_to_form(reference: object,
assert field_names.count("image_reference") == 1
assert "input_reference" not in field_names
payload = next(value for name, value in captured if name == "image_reference")
assert isinstance(payload, (str, bytes, bytearray))
assert json.loads(payload) == reference
def test_video_file_id_reference_is_rejected() -> None:
"""file_id is unsupported on the server and must not be sent as image_reference."""
import aiohttp
form = aiohttp.FormData()
with pytest.raises(ValueError, match="file_id is not supported yet"):
_add_video_reference_to_form(form, {"file_id": "file-abc"})
with pytest.raises(ValueError, match="file_id is not supported yet"):
_add_video_reference_to_form(form, [{"image_url": "https://example.com/a.png"}, {"file_id": "file-xyz"}])
with pytest.raises(ValueError, match="file_id is not supported yet"):
_add_combined_video_form_references(form, None, {"video_reference": {"file_id": "file-vid"}})
def test_image_reference_urls_from_random_mm_content(mocker: MockerFixture) -> None:
"""random-mm image_url parts keep an explicit image_reference type."""
import aiohttp
content = [
{
"type": "image_url",
"image_url": {"url": "https://example.com/ref.png"},
},
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{_MIN_PNG_B64}"},
},
]
refs = list(_iter_image_reference_inputs(content))
assert refs == [
{"image_url": "https://example.com/ref.png"},
{"image_url": f"data:image/png;base64,{_MIN_PNG_B64}"},
]
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
assert _add_video_reference_to_form(form, refs[0]) is True
assert _add_video_reference_to_form(form, refs[1]) is True
field_names = [name for name, _ in captured]
assert field_names.count("image_reference") == 2
assert "video_reference" not in field_names
payloads = []
for name, value in captured:
if name != "image_reference":
continue
assert isinstance(value, (str, bytes, bytearray))
payloads.append(json.loads(value))
assert payloads == refs
def test_video_reference_urls_from_random_mm_content(mocker: MockerFixture) -> None:
"""random-mm data:video_url parts stay on video_reference."""
import aiohttp
content = [
{
"type": "video_url",
"video_url": {"url": "data:video/mp4;base64,AAAA"},
}
]
refs = list(_iter_video_reference_inputs(content))
assert refs == [{"video_url": "data:video/mp4;base64,AAAA"}]
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
assert _add_video_reference_to_form(form, refs[0]) is True
field_names = [name for name, _ in captured]
assert field_names.count("video_reference") == 1
assert "input_references" not in field_names
assert "input_reference" not in field_names
assert "image_reference" not in field_names
payload = next(value for name, value in captured if name == "video_reference")
assert isinstance(payload, (str, bytes, bytearray))
assert json.loads(payload) == {"video_url": "data:video/mp4;base64,AAAA"}
def test_video_reference_https_url_from_random_mm_content(mocker: MockerFixture) -> None:
"""HTTP(S) video_url parts must stay on video_reference, not image_reference."""
import aiohttp
content = [
{
"type": "video_url",
"video_url": {"url": "https://example.com/ref.mp4"},
}
]
refs = list(_iter_video_reference_inputs(content))
assert refs == [{"video_url": "https://example.com/ref.mp4"}]
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
assert _add_video_reference_to_form(form, refs[0]) is True
field_names = [name for name, _ in captured]
assert field_names.count("video_reference") == 1
assert "image_reference" not in field_names
assert "input_references" not in field_names
payload = next(value for name, value in captured if name == "video_reference")
assert isinstance(payload, (str, bytes, bytearray))
assert json.loads(payload) == {"video_url": "https://example.com/ref.mp4"}
@pytest.mark.parametrize(
("reference", "field_name", "expected"),
[
("data:image/png;base64,AAAA", "image_reference", {"image_url": "data:image/png;base64,AAAA"}),
("data:video/mp4;base64,AAAA", "video_reference", {"video_url": "data:video/mp4;base64,AAAA"}),
("https://example.com/ref.png", "image_reference", {"image_url": "https://example.com/ref.png"}),
("https://example.com/ref.mp4", "video_reference", {"video_url": "https://example.com/ref.mp4"}),
],
)
def test_bare_reference_string_uses_matching_json_field(
reference: str,
field_name: str,
expected: dict[str, str],
mocker: MockerFixture,
) -> None:
"""Image and video URLs use matching JSON fields; neither is a file upload."""
import aiohttp
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
assert _add_video_reference_to_form(aiohttp.FormData(), reference) is True
assert [name for name, _ in captured] == [field_name]
payload = captured[0][1]
assert isinstance(payload, (str, bytes, bytearray))
assert json.loads(payload) == expected
def test_bare_video_string_is_not_collected_as_image() -> None:
content = ["https://example.com/ref.mp4", "data:video/mp4;base64,AAAA"]
assert list(_iter_image_reference_inputs(content)) == []
assert list(_iter_video_reference_inputs(content)) == [
{"video_url": "https://example.com/ref.mp4"},
{"video_url": "data:video/mp4;base64,AAAA"},
]
def test_bare_http_without_media_extension_is_rejected() -> None:
import aiohttp
with pytest.raises(ValueError, match="image or video extension"):
_add_video_reference_to_form(aiohttp.FormData(), "https://example.com/ref")
def test_image_and_inline_video_use_combined_reference_fields(mocker: MockerFixture) -> None:
"""Image plus data:video must be image_reference + video_reference, not input_references."""
import aiohttp
content = [
{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}},
{"type": "video_url", "video_url": {"url": "data:video/mp4;base64,AAAA"}},
]
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
_add_combined_video_form_references(form, content)
field_names = [name for name, _ in captured]
assert field_names.count("image_reference") == 1
assert field_names.count("video_reference") == 1
assert "input_references" not in field_names
assert "input_reference" not in field_names
image_payload = next(value for name, value in captured if name == "image_reference")
video_payload = next(value for name, value in captured if name == "video_reference")
assert isinstance(image_payload, (str, bytes, bytearray))
assert isinstance(video_payload, (str, bytes, bytearray))
assert json.loads(image_payload) == {"image_url": "https://example.com/ref.png"}
assert json.loads(video_payload) == {"video_url": "data:video/mp4;base64,AAAA"}
def test_image_url_and_bare_data_video_string_use_json_fields(mocker: MockerFixture) -> None:
"""Image URL plus a bare data:video string must not upload input_references."""
import aiohttp
content = [{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}]
extra_body = {"video_reference": "data:video/mp4;base64,AAAA"}
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
_add_combined_video_form_references(form, content, extra_body)
field_names = [name for name, _ in captured]
assert field_names.count("image_reference") == 1
assert field_names.count("video_reference") == 1
assert "input_references" not in field_names
assert "input_reference" not in field_names
image_payload = next(value for name, value in captured if name == "image_reference")
video_payload = next(value for name, value in captured if name == "video_reference")
assert isinstance(image_payload, (str, bytes, bytearray))
assert isinstance(video_payload, (str, bytes, bytearray))
assert json.loads(image_payload) == {"image_url": "https://example.com/ref.png"}
assert json.loads(video_payload) == {"video_url": "data:video/mp4;base64,AAAA"}
captured.clear()
assert _add_video_reference_to_form(form, "data:video/mp4;base64,AAAA") is True
assert [name for name, _ in captured] == ["video_reference"]
def _oversized_data_video_url() -> str:
"""data:video whose JSON text part is larger than 1MB."""
return "data:video/mp4;base64," + ("A" * (1024 * 1024))
def test_oversized_data_video_uploads_as_input_references(mocker: MockerFixture) -> None:
"""A lone data:video over the 1MB text limit is uploaded, not sent as JSON."""
import aiohttp
video_url = _oversized_data_video_url()
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
assert _add_video_reference_to_form(aiohttp.FormData(), {"video_url": video_url}) is True
uploaded = next(value for name, value in captured if name == "input_references")
assert uploaded == base64.b64decode("A" * (1024 * 1024))
field_names = [name for name, _ in captured]
assert "video_reference" not in field_names
assert "image_reference" not in field_names
def test_oversized_data_video_with_image_stays_json(mocker: MockerFixture) -> None:
"""input_references cannot be combined with image_reference, even for a large video."""
import aiohttp
content = [{"type": "image_url", "image_url": {"url": "https://example.com/ref.png"}}]
extra_body = {"video_reference": _oversized_data_video_url()}
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
_add_combined_video_form_references(aiohttp.FormData(), content, extra_body)
field_names = [name for name, _ in captured]
assert "image_reference" in field_names
assert "video_reference" in field_names
assert "input_references" not in field_names
def test_image_upload_bytes_and_structured_video_use_json_fields(mocker: MockerFixture) -> None:
"""Image upload bytes plus a video URL must not use singular input_reference."""
import aiohttp
image_bytes = base64.b64decode(_MIN_PNG_B64)
extra_body = {
"image_reference": {"bytes": image_bytes, "content_type": "image/png"},
"video_reference": {"video_url": "https://example.com/ref.mp4"},
}
captured: list[tuple[str, object]] = []
real_add_field = aiohttp.FormData.add_field
def tracking_add_field(self, name, value=None, **kwargs):
captured.append((str(name), value))
return real_add_field(self, name, value, **kwargs)
mocker.patch.object(aiohttp.FormData, "add_field", tracking_add_field)
form = aiohttp.FormData()
_add_combined_video_form_references(form, None, extra_body)
field_names = [name for name, _ in captured]
assert field_names.count("image_reference") == 1
assert field_names.count("video_reference") == 1
assert "input_reference" not in field_names
assert "input_references" not in field_names
image_payload = next(value for name, value in captured if name == "image_reference")
video_payload = next(value for name, value in captured if name == "video_reference")
assert isinstance(image_payload, (str, bytes, bytearray))
assert isinstance(video_payload, (str, bytes, bytearray))
assert json.loads(image_payload) == {"image_url": f"data:image/png;base64,{_MIN_PNG_B64}"}
assert json.loads(video_payload) == {"video_url": "https://example.com/ref.mp4"}
def test_video_unsupported_image_reference_raises() -> None:
import aiohttp
form = aiohttp.FormData()
with pytest.raises(ValueError, match="Unsupported image_reference"):
with pytest.raises(ValueError, match="Unsupported reference"):
_add_video_reference_to_form(form, {"not_a_supported_key": "x"})
with pytest.raises(ValueError, match="Unsupported image_reference"):
with pytest.raises(ValueError, match="Unsupported reference"):
_add_video_reference_to_form(form, "/tmp/does-not-exist-ref.png")
def test_extract_stage_durations_from_video_and_image_shapes() -> None:
video_payload = {
"stage_durations": {
"diffuse": 1.5,
"text_encoder.forward": 0.2,
"vae.decode": 0.1,
"stage_0_gen_ms": 4000.0,
}
}
assert _extract_stage_durations_from_payload(video_payload) == video_payload["stage_durations"]
image_stage_durations = {
"diffuse": 2.0,
"vae.decode": 0.3,
}
image_payload = {
"metrics": {"stage_durations": image_stage_durations},
"data": [{"b64_json": "x"}],
}
assert _extract_stage_durations_from_payload(image_payload) == image_stage_durations
def test_video_metrics_persist_full_stage_durations() -> None:
output = MixRequestFuncOutput()
output.latency = 4.2
stage_durations = {
"diffuse": 1.5,
"vae.decode": 0.25,
"stage_0_gen_ms": 4000.0,
}
payload = {
"duration_s": 2.0,
"num_frames": 48,
"fps": 24.0,
"stage_durations": stage_durations,
}
_apply_video_metrics_from_payload(output, payload, {})
assert output.stage_durations == stage_durations
assert output.video_generation_time_ms == pytest.approx(4000.0)
def test_image_metrics_persist_stage_durations_from_metrics() -> None:
output = MixRequestFuncOutput()
stage_durations = {
"diffuse": 1.1,
"text_encoder.forward": 0.4,
"vae.decode": 0.2,
}
payload = {
"created": 1,
"data": [{"b64_json": _MIN_PNG_B64}],
"metrics": {"stage_durations": stage_durations},
}
assert _apply_image_metrics_from_payload(output, payload) == 1
assert output.stage_durations == stage_durations
if __name__ == "__main__":
pytest.main([__file__, "-v", "-s"])
+43 -13
View File
@@ -1,3 +1,6 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
import json
import os
import re
@@ -84,17 +87,20 @@ def resolve_pytest_marks(mark_field: Any) -> list[pytest.MarkDecorator]:
raise ValueError(f"mark must be a list; got {type(mark_field).__name__}")
def _mark_names(mark_field: Any) -> set[str]:
if isinstance(mark_field, list):
return {str(item) for item in mark_field if isinstance(item, str)}
return set()
def is_diffusion_perf_config(cfg: dict[str, Any]) -> bool:
"""True for perf JSON cases intended for ``run_diffusion_benchmark.py``."""
if cfg.get("server_type") is not None:
return True
return "diffusion" in _mark_names(cfg.get("mark"))
"""True for perf JSON cases intended for ``run_diffusion_benchmark.py``.
Schema split (not marks / endpoints):
- ``benchmark_params[].dataset`` → diffusion client (``run_diffusion_benchmark.py``)
- ``benchmark_params[].dataset_name`` → ``vllm bench serve --omni`` (``run_benchmark.py``)
"""
for params in cfg.get("benchmark_params") or []:
if not isinstance(params, dict):
continue
if "dataset" in params and "dataset_name" not in params:
return True
return False
def _marks_by_test_name(configs: list[dict[str, Any]]) -> dict[str, list[pytest.MarkDecorator]]:
@@ -654,6 +660,7 @@ def run_benchmark(
random_output_len: Any | None = None,
resource_label: str | None = None,
num_warmups: int = 2,
benchmark_params_name: str | None = None,
) -> dict[str, Any]:
"""Run one ``vllm bench serve --omni`` iteration and return parsed metrics.
@@ -662,15 +669,35 @@ def run_benchmark(
maps keep every hardware bucket, but each metric is reduced to the value for
this concurrency / request-rate step. If the benchmark exits without writing a
result file, ``result_omni_template.json`` is used as a fallback.
``benchmark_params_name`` (the ``name`` field from ``benchmark_params``) is
persisted in the result JSON and filename so multiple generation configs
under one ``test_name`` stay distinguishable (e.g. Wan USP2 832x480 vs
1280x720).
"""
current_dt = datetime.now().strftime("%Y%m%d-%H%M%S")
ri = _safe_filename_token(random_input_len)
ro = _safe_filename_token(random_output_len)
hw = resource_label_for_filename(resource_label)
name_token = _safe_filename_token(benchmark_params_name) if benchmark_params_name else ""
if name_token == "na":
name_token = ""
name_parts = [f"result_{test_name}"]
if hw:
result_filename = f"result_{test_name}_{hw}_{dataset_name}_{flow}_{num_prompt}_in{ri}_out{ro}_{current_dt}.json"
else:
result_filename = f"result_{test_name}_{dataset_name}_{flow}_{num_prompt}_in{ri}_out{ro}_{current_dt}.json"
name_parts.append(hw)
if name_token:
name_parts.append(name_token)
name_parts.extend(
[
str(dataset_name),
str(flow),
str(num_prompt),
f"in{ri}",
f"out{ro}",
current_dt,
]
)
result_filename = "_".join(name_parts) + ".json"
if "--result-filename" in args:
print(f"The result file will be overwritten by {result_filename}")
command = (
@@ -740,6 +767,9 @@ def run_benchmark(
if random_output_len is not None:
result["random_output_len"] = random_output_len
result["Hardware"] = hardware_json_value(resource_label)
result["test_name"] = test_name
if benchmark_params_name:
result["name"] = benchmark_params_name
with open(result_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
return result
+113 -8
View File
@@ -50,12 +50,19 @@ if CONFIG_FILE_PATH is None:
_all_configs = load_benchmark_configs(config_dir=_PERF_TESTS_DIR)
BENCHMARK_CONFIGS = [cfg for cfg in _all_configs if not is_diffusion_perf_config(cfg)]
print(
f"No --test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts case(s) from "
f"No --test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts/generation case(s) from "
f"{_PERF_TESTS_DIR}/*.json (skipped {len(_all_configs) - len(BENCHMARK_CONFIGS)} diffusion; "
f"use -m to filter, e.g. -m tts)"
)
else:
BENCHMARK_CONFIGS = load_benchmark_configs(CONFIG_FILE_PATH)
_loaded = load_benchmark_configs(CONFIG_FILE_PATH)
BENCHMARK_CONFIGS = [cfg for cfg in _loaded if not is_diffusion_perf_config(cfg)]
skipped = len(_loaded) - len(BENCHMARK_CONFIGS)
if skipped:
print(
f"--test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts/generation case(s); "
f"skipped {skipped} remaining diffusion case(s) (chat completions / custom jsonl)"
)
DEPLOY_CONFIGS_DIR = Path(__file__).parent.parent / "deploy"
server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS)
@@ -93,24 +100,107 @@ class _SingleActiveContext:
stack.close()
# OmniServer defaults for flags not already present in JSON ``serve_args`` /
# ``extra_cli_args``. Add new (flag, value) pairs here rather than special-casing.
_OMNI_DEFAULT_SERVER_ARGS: tuple[tuple[str, str], ...] = (
("--stage-init-timeout", "600"),
("--init-timeout", "900"),
)
def _cli_flag_names(cli_args: tuple[str, ...] | list[str]) -> set[str]:
"""Return long-option names present in a flat CLI argv list."""
names: set[str] = set()
for item in cli_args:
token = str(item)
if not token.startswith("--"):
continue
names.add(token.split("=", 1)[0])
return names
def _merge_omni_default_server_args(
extra_cli_args: tuple[str, ...] | list[str],
*,
use_omni: bool,
defaults: tuple[tuple[str, str], ...] = _OMNI_DEFAULT_SERVER_ARGS,
) -> list[str]:
"""Fill Omni defaults for flags not already set in JSON-derived CLI args.
JSON ``serve_args`` / ``extra_cli_args`` win; only missing flags are appended.
"""
if not use_omni:
return []
present = _cli_flag_names(extra_cli_args)
args: list[str] = []
for flag, value in defaults:
if flag not in present:
args += [flag, value]
return args
def _omni_server_env() -> dict[str, str]:
"""Writable video/image storage for ``/v1/videos`` and related generation APIs."""
result_dir = Path(os.environ.get("BENCHMARK_DIR", "tests/dfx/perf/results"))
storage_path = Path(os.environ.get("VLLM_OMNI_STORAGE_PATH", str(result_dir / "storage")))
storage_path.mkdir(parents=True, exist_ok=True)
return {"VLLM_OMNI_STORAGE_PATH": str(storage_path)}
def _resolve_offline_model(model: str) -> str:
"""Resolve HF ids / MiniMax env overrides the same way as the diffusion runner."""
import huggingface_hub
from vllm_omni.transformers_utils.repo_utils import hf_api
if not model or os.path.isdir(model):
return model
model_env_overrides = {
"MiniMaxAI/MiniMax-H3": "VLLM_TEST_MINIMAX_H3_MODEL",
"MiniMaxAI/MiniMax-H3/FL2VA": "VLLM_TEST_MINIMAX_H3_FL2VA_MODEL",
"MiniMaxAI/MiniMax-H3/Ref2VA": "VLLM_TEST_MINIMAX_H3_REF2VA_MODEL",
}
env_name = model_env_overrides.get(model)
if env_name:
env_model = os.environ.get(env_name)
if env_model:
return env_model
parts = model.split("/")
if len(parts) >= 3:
repo_id = "/".join(parts[:2])
subfolder = "/".join(parts[2:])
snapshot_root = hf_api().snapshot_download(
repo_id,
allow_patterns=[f"{subfolder}/**"],
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
)
return str(Path(snapshot_root) / subfolder)
if not huggingface_hub.constants.HF_HUB_OFFLINE:
return model
return hf_api().snapshot_download(model, local_files_only=True)
@contextmanager
def _start_omni_server(server_param):
test_name, model, stage_config_path, stage_overrides, extra_cli_args, use_omni = server_param
extra = tuple(extra_cli_args or ())
model = _resolve_offline_model(model)
print(f"Starting OmniServer with test: {test_name}, model: {model}")
server_args: list[str] = []
if use_omni:
server_args += ["--stage-init-timeout", "600", "--init-timeout", "900"]
server_args: list[str] = _merge_omni_default_server_args(extra, use_omni=use_omni)
# --deploy-config and --stage-overrides compose at the CLI (see vllm_omni/entrypoints/utils.py):
# deploy-config sets the base; stage-overrides are applied on top. Both can be set.
if stage_config_path:
server_args = ["--deploy-config", stage_config_path] + server_args
if stage_overrides:
server_args = ["--stage-overrides", stage_overrides] + server_args
if extra_cli_args:
server_args = list(extra_cli_args) + server_args
with OmniServer(model, server_args, use_omni=use_omni) as server:
if extra:
server_args = list(extra) + server_args
with OmniServer(model, server_args, use_omni=use_omni, env_dict=_omni_server_env()) as server:
server.test_name = test_name
print("OmniServer started successfully")
yield server
@@ -259,11 +349,24 @@ def test_performance_benchmark(omni_server, benchmark_params):
"eval_phase",
"trust_remote_code",
"expected_duplex_audio_turns_per_session",
"name",
"enable_negative_prompt",
"random_request_config",
"num_input_images",
"warmup_requests",
"warmup_concurrency",
"warmup_num_inference_steps",
}
param_keys = {str(key).replace("-", "_") for key in params}
if "model" not in param_keys:
args.extend(["--model", str(model)])
for key, value in params.items():
if key in exclude_keys or value is None:
continue
if key in {"extra_body", "extra-body"} and value == {}:
continue
arg_name = f"--{key.replace('_', '-')}"
@@ -298,6 +401,7 @@ def test_performance_benchmark(omni_server, benchmark_params):
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
num_warmups=_resolve_num_warmups(params, default=2),
benchmark_params_name=params.get("name") if isinstance(params.get("name"), str) else None,
)
assert_result(result, params, num_prompt)
@@ -316,5 +420,6 @@ def test_performance_benchmark(omni_server, benchmark_params):
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
num_warmups=_resolve_num_warmups(params, default=max(2, int(concurrency))),
benchmark_params_name=params.get("name") if isinstance(params.get("name"), str) else None,
)
assert_result(result, params, num_prompt)
@@ -204,11 +204,11 @@ def load_diffusion_benchmark_configs(
) -> list[dict[str, Any]]:
"""Load one diffusion benchmark JSON, or merge all ``*.json`` under *config_dir*."""
if config_path is not None:
configs = load_configs(config_path)
loaded = load_configs(config_path)
source = str(Path(config_path).resolve())
for cfg in configs:
for cfg in loaded:
cfg.setdefault(_DIFFUSION_SOURCE_CONFIG_KEY, source)
return configs
return loaded
if config_dir is None:
raise ValueError("load_diffusion_benchmark_configs requires config_path or config_dir")
configs: list[dict[str, Any]] = []
@@ -231,7 +231,15 @@ if CONFIG_FILE_PATH is None:
f"use -m to filter, e.g. -m diffusion)"
)
else:
BENCHMARK_CONFIGS = load_diffusion_benchmark_configs(CONFIG_FILE_PATH)
_loaded = load_diffusion_benchmark_configs(CONFIG_FILE_PATH)
BENCHMARK_CONFIGS = [cfg for cfg in _loaded if is_diffusion_perf_config(cfg)]
skipped = len(_loaded) - len(BENCHMARK_CONFIGS)
if skipped:
print(
f"--test-config-file: loaded {len(BENCHMARK_CONFIGS)} diffusion case(s); "
f"skipped {skipped} omni-bench generation case(s) "
f"(/v1/images/edits, /v1/images/generations, /v1/videos → run_benchmark.py)"
)
_AGGREGATED_RESULT_FILES_BY_SOURCE: dict[str, Path] = {}
+104 -85
View File
@@ -5,7 +5,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 2
}
@@ -14,8 +17,6 @@
"diffusion"
],
"description": "Cosmos3-Nano text-to-image official demo workload on 2 GPUs",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/generations",
"server_params": {
"model": "nvidia/Cosmos3-Nano",
"serve_args": {
@@ -34,24 +35,28 @@
"benchmark_params": [
{
"name": "1024x1024_steps4",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 4,
"num-prompts": 3,
"max-concurrency": 1,
"seed": 42,
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/images/generations",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 1024,
"height": 1024,
"num_inference_steps": 4,
"seed": 42,
"guidance_scale": 7.0,
"flow_shift": 3.0,
"guardrails": false
},
"baseline": {
"H100": {
"throughput_qps": 1.1958,
"latency_mean": 0.843,
"peak_memory_mb_mean": 80000.0
"request_throughput": 1.1958,
"mean_e2el_ms": 843.0,
"mean_peak_memory_mb": 80000.0
}
}
}
@@ -63,7 +68,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 2
}
@@ -72,8 +80,6 @@
"diffusion"
],
"description": "Cosmos3-Nano text-to-video official demo workload on 2 GPUs",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "nvidia/Cosmos3-Nano",
"serve_args": {
@@ -92,23 +98,21 @@
"benchmark_params": [
{
"name": "1280x720_frames189_steps4",
"dataset": "random",
"task": "t2v",
"num-prompts": 3,
"max-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1280,
"height": 720,
"prompt": "A robot arm is cleaning a plate in the kitchen.",
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 1280,
"height": 720,
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
@@ -118,9 +122,9 @@
},
"baseline": {
"H100": {
"throughput_qps": 0.0307,
"latency_mean": 32.7665,
"peak_memory_mb_mean": 28762.4762
"request_throughput": 0.0307,
"mean_e2el_ms": 32766.5,
"mean_peak_memory_mb": 28762.4762
}
}
}
@@ -132,7 +136,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 2
}
@@ -141,8 +148,6 @@
"diffusion"
],
"description": "Cosmos3-Nano image-to-video official demo workload on 2 GPUs",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "nvidia/Cosmos3-Nano",
"serve_args": {
@@ -161,24 +166,30 @@
"benchmark_params": [
{
"name": "1280x720_frames189_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 3,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"random-request-config": [
{
"width": 1280,
"height": 720,
"prompt": "The scene comes to life with smooth, natural motion.",
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(720, 1280, 1)": 1.0
},
"extra_body": {
"width": 1280,
"height": 720,
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
@@ -186,9 +197,9 @@
},
"baseline": {
"H100": {
"throughput_qps": 0.0305,
"latency_mean": 33.1009,
"peak_memory_mb_mean": 29441.1429
"request_throughput": 0.0305,
"mean_e2el_ms": 33100.9,
"mean_peak_memory_mb": 29441.1429
}
}
}
@@ -200,7 +211,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 2
}
@@ -209,8 +223,6 @@
"diffusion"
],
"description": "Cosmos3-Nano video-to-video official demo workload on 2 GPUs",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "nvidia/Cosmos3-Nano",
"serve_args": {
@@ -229,23 +241,30 @@
"benchmark_params": [
{
"name": "1280x720_frames189_steps4",
"dataset": "random",
"task": "v2v",
"num-prompts": 3,
"max-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1280,
"height": 720,
"prompt": "Continue the same scene with smooth natural motion.",
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"video": 1
},
"random_mm_bucket_config": {
"(720, 1280, 189)": 1.0
},
"extra_body": {
"width": 1280,
"height": 720,
"num_inference_steps": 4,
"num_frames": 189,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
@@ -258,9 +277,9 @@
},
"baseline": {
"H100": {
"throughput_qps": 0.0299,
"latency_mean": 33.925,
"peak_memory_mb_mean": 29443.1429
"request_throughput": 0.0299,
"mean_e2el_ms": 33925.0,
"mean_peak_memory_mb": 29443.1429
}
}
}
@@ -5,7 +5,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 1
}
@@ -14,7 +17,6 @@
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v",
"serve_args": {
@@ -25,26 +27,33 @@
"benchmark_params": [
{
"name": "832x480_frames33_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(480, 832, 1)": 1.0
},
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 5.0
"flow_shift": 5.0,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
@@ -55,7 +64,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 2
}
@@ -64,7 +76,6 @@
"diffusion"
],
"description": "CacheDiT + TP=2 + VAE patch parallel=2 + VAE tiling",
"server_type": "vllm-omni",
"server_params": {
"model": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v",
"serve_args": {
@@ -79,26 +90,33 @@
"benchmark_params": [
{
"name": "832x480_frames33_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(480, 832, 1)": 1.0
},
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 5.0
"flow_shift": 5.0,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
@@ -5,7 +5,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 1
}
@@ -14,7 +17,6 @@
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
"serve_args": {
@@ -25,25 +27,24 @@
"benchmark_params": [
{
"name": "832x480_frames33_steps4",
"dataset": "random",
"task": "t2v",
"num-prompts": 10,
"max-concurrency": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 5.0
"flow_shift": 5.0,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
@@ -64,7 +65,6 @@
"diffusion"
],
"description": "CacheDiT + TP=2 + VAE patch parallel=2 + VAE tiling",
"server_type": "vllm-omni",
"server_params": {
"model": "hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
"serve_args": {
@@ -79,25 +79,24 @@
"benchmark_params": [
{
"name": "832x480_frames33_steps4",
"dataset": "random",
"task": "t2v",
"num-prompts": 10,
"max-concurrency": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 33,
"fps": 24,
"seed": 42,
"guidance_scale": 6.0,
"flow_shift": 5.0
"flow_shift": 5.0,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
@@ -1,33 +1,50 @@
[
{
"test_name": "test_lingbot_video_single_device",
"description": "Single-device LingBot dense T2V baseline at 320x192, 9 frames, 2 steps.",
"server_type": "vllm-omni",
"server_params": {
"model": "robbyant/lingbot-video-dense-1.3b",
"serve_args": {
"model-class-name": "LingBotVideoPipeline",
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "320x192_frames9_steps2",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 320,
"height": 192,
"num-frames": 9,
"fps": 24,
"num-inference-steps": 2,
"num-prompts": 3,
"max-concurrency": 1,
"warmup-requests": 1,
"warmup-concurrency": 1,
"warmup-num-inference-steps": 2,
"enable-negative-prompt": true
}
]
}
{
"test_name": "test_lingbot_video_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": [
"H100",
"B200"
]
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device LingBot dense T2V baseline at 320x192, 9 frames, 2 steps.",
"server_params": {
"model": "robbyant/lingbot-video-dense-1.3b",
"serve_args": {
"model-class-name": "LingBotVideoPipeline",
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "320x192_frames9_steps2",
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"num_warmups": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 320,
"height": 192,
"num_frames": 9,
"fps": 24,
"num_inference_steps": 2,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
}
]
+160 -115
View File
@@ -14,7 +14,6 @@
"diffusion"
],
"description": "Single-device baseline with enforce-eager (no torch.compile)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
@@ -25,31 +24,41 @@
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 256,
"height": 256,
"num_frames": 145,
"fps": 24,
"num_inference_steps": 6,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 768,
"height": 480,
"num_frames": 41,
"fps": 24,
"num_inference_steps": 20,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
},
@@ -68,7 +77,6 @@
"diffusion"
],
"description": "Single-device with torch.compile (default, no enforce-eager)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
@@ -78,31 +86,41 @@
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 256,
"height": 256,
"num_frames": 145,
"fps": 24,
"num_inference_steps": 6,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 768,
"height": 480,
"num_frames": 41,
"fps": 24,
"num_inference_steps": 20,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
},
@@ -121,7 +139,6 @@
"diffusion"
],
"description": "CFG-parallel=2 with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
@@ -133,31 +150,41 @@
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 256,
"height": 256,
"num_frames": 145,
"fps": 24,
"num_inference_steps": 6,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 768,
"height": 480,
"num_frames": 41,
"fps": 24,
"num_inference_steps": 20,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
},
@@ -176,7 +203,6 @@
"diffusion"
],
"description": "CFG-parallel=2 with torch.compile",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
@@ -187,31 +213,41 @@
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 256,
"height": 256,
"num_frames": 145,
"fps": 24,
"num_inference_steps": 6,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 768,
"height": 480,
"num_frames": 41,
"fps": 24,
"num_inference_steps": 20,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
},
@@ -230,7 +266,6 @@
"diffusion"
],
"description": "CacheDiT with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
@@ -242,31 +277,41 @@
"benchmark_params": [
{
"name": "256x256_145f_steps6",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 256,
"height": 256,
"num-frames": 145,
"fps": 24,
"num-inference-steps": 6,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 256,
"height": 256,
"num_frames": 145,
"fps": 24,
"num_inference_steps": 6,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
},
{
"name": "480x768_41f_steps20",
"dataset": "random",
"task": "t2v",
"backend": "v1/videos",
"width": 768,
"height": 480,
"num-frames": 41,
"fps": 24,
"num-inference-steps": 20,
"num-prompts": 3,
"max-concurrency": 1,
"enable-negative-prompt": true
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 768,
"height": 480,
"num_frames": 41,
"fps": 24,
"num_inference_steps": 20,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
}
}
]
}
@@ -5,7 +5,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 4
}
@@ -14,8 +17,6 @@
"diffusion"
],
"description": "MiniMax-H3 T2VA text-to-video+audio on 4 GPUs (USP4 + HSDP4, encoder-TP4, VAE-PP4 tile)",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "MiniMaxAI/MiniMax-H3",
"serve_args": {
@@ -38,33 +39,30 @@
"benchmark_params": [
{
"name": "1344x768_frames209_steps8",
"dataset": "random",
"task": "t2v",
"num-prompts": 3,
"max-concurrency": 1,
"warmup-requests": 1,
"warmup-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1344,
"height": 768,
"prompt": "Cinematic drone shot pushing through misty mountain ridges at sunrise, golden light catching the clouds, slow and smooth.",
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"num_warmups": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 1344,
"height": 768,
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"seed": 42,
"flow_shift": 12.0,
"extra_params": "{\"task\":\"t2va\",\"duration\":8.7,\"aspect_ratio\":\"16:9\",\"audio_flow_shift\":3.0}"
},
"baseline": {
"H100": {
"throughput_qps": 0.0211,
"latency_mean": 38.3252,
"peak_memory_mb_mean": 54219.6
"request_throughput": 0.0211,
"mean_e2el_ms": 38325.2,
"mean_peak_memory_mb": 54219.6
}
}
}
@@ -76,7 +74,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 4
}
@@ -85,8 +86,6 @@
"diffusion"
],
"description": "MiniMax-H3 FL2VA first-frame image-to-video+audio on 4 GPUs (USP4 + HSDP4, encoder-TP4, VAE-PP4 tile)",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "MiniMaxAI/MiniMax-H3",
"serve_args": {
@@ -109,34 +108,39 @@
"benchmark_params": [
{
"name": "1344x768_frames209_steps8",
"dataset": "random",
"task": "ti2v",
"num-prompts": 3,
"max-concurrency": 1,
"warmup-requests": 1,
"warmup-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1344,
"height": 768,
"prompt": "Continue the scene: the camera slowly pushes in on the subject as the background city lights drift and blur, with the actor singing softly and emotionally.",
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"num_warmups": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(768, 1344, 1)": 1.0
},
"extra_body": {
"width": 1344,
"height": 768,
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"seed": 42,
"flow_shift": 12.0,
"extra_params": "{\"task\":\"fl2va\",\"duration\":8.7,\"audio_flow_shift\":3.0}"
},
"num-input-images": 1,
"baseline": {
"H100": {
"throughput_qps": 0.0212,
"latency_mean": 38.1385,
"peak_memory_mb_mean": 54622.8
"request_throughput": 0.0212,
"mean_e2el_ms": 38138.5,
"mean_peak_memory_mb": 54622.8
}
}
}
@@ -148,7 +152,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 4
}
@@ -157,8 +164,6 @@
"diffusion"
],
"description": "MiniMax-H3 Ref2VA video-reference-to-video+audio on 4 GPUs (USP4 + HSDP4, encoder-TP4, VAE-PP4 tile)",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "MiniMaxAI/MiniMax-H3",
"serve_args": {
@@ -181,33 +186,39 @@
"benchmark_params": [
{
"name": "1344x768_frames209_steps8",
"dataset": "random",
"task": "v2v",
"num-prompts": 3,
"max-concurrency": 1,
"warmup-requests": 1,
"warmup-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1344,
"height": 768,
"prompt": "Restyle the reference footage into a cinematic night scene with neon reflections, keeping the same camera motion and subject.",
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"num_warmups": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"video": 1
},
"random_mm_bucket_config": {
"(480, 832, 209)": 1.0
},
"extra_body": {
"width": 1344,
"height": 768,
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"seed": 42,
"flow_shift": 12.0,
"extra_params": "{\"task\":\"ref2va\",\"duration\":8.7,\"aspect_ratio\":\"16:9\",\"audio_flow_shift\":3.0}"
},
"baseline": {
"H100": {
"throughput_qps": 0.0077,
"latency_mean": 105.7508,
"peak_memory_mb_mean": 55180.8
"request_throughput": 0.0077,
"mean_e2el_ms": 105750.8,
"mean_peak_memory_mb": 55180.8
}
}
}
@@ -219,7 +230,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 4
}
@@ -228,8 +242,6 @@
"diffusion"
],
"description": "MiniMax-H3 T2VA with 4-card distributed layerwise offload (DLO), 8 denoise steps (USP4 + HSDP4, encoder-TP4, VAE-PP4 tile)",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/videos",
"server_params": {
"model": "MiniMaxAI/MiniMax-H3",
"serve_args": {
@@ -256,33 +268,30 @@
"benchmark_params": [
{
"name": "1344x768_frames209_steps8",
"dataset": "random",
"task": "t2v",
"num-prompts": 3,
"max-concurrency": 1,
"warmup-requests": 1,
"warmup-concurrency": 1,
"seed": 42,
"random-request-config": [
{
"width": 1344,
"height": 768,
"prompt": "Cinematic drone shot pushing through misty mountain ridges at sunrise, golden light catching the clouds, slow and smooth.",
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"weight": 1
}
],
"extra-body": {
"dataset_name": "random",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 3,
"max_concurrency": 1,
"num_warmups": 1,
"random_input_len": 8,
"random_output_len": 1,
"percentile_metrics": "e2el",
"extra_body": {
"width": 1344,
"height": 768,
"num_inference_steps": 8,
"num_frames": 209,
"fps": 24,
"seed": 42,
"flow_shift": 12.0,
"extra_params": "{\"task\":\"t2va\",\"duration\":8.7,\"aspect_ratio\":\"16:9\",\"audio_flow_shift\":3.0}"
},
"baseline": {
"H100": {
"throughput_qps": 0.0212,
"latency_mean": 38.2089,
"peak_memory_mb_mean": 36218.4
"request_throughput": 0.0212,
"mean_e2el_ms": 38208.9,
"mean_peak_memory_mb": 36218.4
}
}
}
+117 -7
View File
@@ -205,15 +205,53 @@ def test_is_diffusion_perf_config():
from tests.dfx.conftest import is_diffusion_perf_config
assert not is_diffusion_perf_config(
{"test_name": "omni_a", "mark": [{"hardware_marks": {"res": {"cuda": "H100"}}}, "omni"]}
{
"test_name": "omni_a",
"mark": [{"hardware_marks": {"res": {"cuda": "H100"}}}, "omni"],
"benchmark_params": [{"dataset_name": "random", "endpoint": "/v1/chat/completions"}],
}
)
assert is_diffusion_perf_config(
{
"test_name": "diff_a",
"server_type": "vllm-omni",
"mark": [{"hardware_marks": {"res": {"cuda": "H100"}}}, "diffusion"],
"benchmark_params": [{"task": "t2i", "dataset": "random"}],
}
)
videos_cfg = {
"test_name": "diff_videos",
"server_type": "vllm-omni",
"mark": [{"hardware_marks": {"res": {"cuda": "H100"}}}, "diffusion"],
"benchmark_params": [{"task": "t2v", "dataset_name": "random", "endpoint": "/v1/videos"}],
}
assert not is_diffusion_perf_config(videos_cfg)
custom_edits_cfg = {
"test_name": "diff_custom_edits",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/edits",
"benchmark_params": [{"dataset": "custom", "task": "ti2i"}],
}
assert is_diffusion_perf_config(custom_edits_cfg)
def test_merge_omni_default_server_args_respects_json():
from tests.dfx.perf.scripts.run_benchmark import _merge_omni_default_server_args
extra = ("--stage-init-timeout", "1800", "--init-timeout", "1800", "--usp", "4")
assert _merge_omni_default_server_args(extra, use_omni=True) == []
assert _merge_omni_default_server_args((), use_omni=True) == [
"--stage-init-timeout",
"600",
"--init-timeout",
"900",
]
assert _merge_omni_default_server_args((), use_omni=False) == []
# Only fill the missing default; keep JSON's other timeouts.
assert _merge_omni_default_server_args(("--stage-init-timeout=1800",), use_omni=True) == [
"--init-timeout",
"900",
]
def test_benchmark_param_id_suffix_from_task_eval_phase():
@@ -303,12 +341,84 @@ def test_paired_omni_benchmark_reuses_server_and_preserves_case_metadata(tmp_pat
finally:
active_context.close()
assert events == [
("start", omni_p0_server),
("stop", omni_p0_server),
("start", tts_server),
("stop", tts_server),
]
def test_run_benchmark_persists_distinct_benchmark_params_name(tmp_path, monkeypatch):
"""Two benchmark_params under one test_name must keep distinct saved identity."""
import io
from pathlib import Path
from tests.dfx import conftest as dfx_conftest
result_dir = tmp_path / "results"
result_dir.mkdir()
monkeypatch.setenv("BENCHMARK_DIR", str(result_dir))
class _FakePopen:
def __init__(self, command, **kwargs):
self.stdout = io.StringIO("")
self.stderr = io.StringIO("")
result_dir_idx = command.index("--result-dir")
filename_idx = command.index("--result-filename")
out = Path(command[result_dir_idx + 1]) / command[filename_idx + 1]
out.parent.mkdir(parents=True, exist_ok=True)
out.write_text(json.dumps({"completed": 3, "request_throughput": 0.01}), encoding="utf-8")
def wait(self):
return 0
monkeypatch.setattr(dfx_conftest.subprocess, "Popen", _FakePopen)
shared_test = "test_wan22_i2v_usp2"
name_a = "832x480_frames81_steps4"
name_b = "1280x720_frames121_steps4"
result_a = dfx_conftest.run_benchmark(
args=["--host", "127.0.0.1", "--port", "8000"],
test_name=shared_test,
flow=1,
dataset_name="random-mm",
num_prompt=10,
random_input_len=8,
random_output_len=1,
resource_label="H800",
benchmark_params_name=name_a,
)
result_b = dfx_conftest.run_benchmark(
args=["--host", "127.0.0.1", "--port", "8000"],
test_name=shared_test,
flow=1,
dataset_name="random-mm",
num_prompt=10,
random_input_len=8,
random_output_len=1,
resource_label="H800",
benchmark_params_name=name_b,
)
assert result_a["test_name"] == shared_test
assert result_b["test_name"] == shared_test
assert result_a["name"] == name_a
assert result_b["name"] == name_b
assert "benchmark_params" not in result_a
assert "benchmark_params" not in result_b
files = sorted(p.name for p in result_dir.glob("result_*.json"))
assert len(files) == 2
assert any(name_a in name for name in files)
assert any(name_b in name for name in files)
assert files[0] != files[1]
def omni_group_key(record: dict) -> tuple:
return (
record.get("model_id") or "",
record.get("test_name") or "",
record.get("name") or "",
record.get("dataset_name") or "",
record.get("max_concurrency") if record.get("max_concurrency") is not None else 0,
record.get("num_prompts") if record.get("num_prompts") is not None else 0,
)
assert omni_group_key(result_a) != omni_group_key(result_b)
def test_is_hardware_nested_baseline():
@@ -5,7 +5,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"]
"cuda": [
"H100",
"B200"
]
},
"num_cards": 1
}
@@ -14,7 +17,6 @@
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"serve_args": {
@@ -24,28 +26,37 @@
"benchmark_params": [
{
"name": "832x480_frames81_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 81,
"fps": 16,
"weight": 1
}
],
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(480, 832, 1)": 1.0
},
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 81,
"fps": 16,
"seed": 42,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
},
"baseline": {
"H100": {
"throughput_qps": 0.0361,
"latency_mean": 27.6206,
"peak_memory_mb_mean": 80548.0
"request_throughput": 0.0361,
"mean_e2el_ms": 27620.6,
"mean_peak_memory_mb": 80548.0
}
}
}
@@ -57,7 +68,10 @@
{
"hardware_marks": {
"res": {
"cuda": ["H100", "B200"],
"cuda": [
"H100",
"B200"
],
"npu": "A3"
},
"num_cards": 2
@@ -67,7 +81,6 @@
"diffusion"
],
"description": "USP=2 + VAE patch parallel=2 + HSDP + VAE slicing",
"server_type": "vllm-omni",
"server_params": {
"model": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"serve_args": {
@@ -81,55 +94,73 @@
"benchmark_params": [
{
"name": "832x480_frames81_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 81,
"fps": 16,
"weight": 1
}
],
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(480, 832, 1)": 1.0
},
"extra_body": {
"width": 832,
"height": 480,
"num_inference_steps": 4,
"num_frames": 81,
"fps": 16,
"seed": 42,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
},
"baseline": {
"H100": {
"throughput_qps": 0.05263,
"latency_mean": 18.9415,
"peak_memory_mb_mean": 50053.7
"request_throughput": 0.05263,
"mean_e2el_ms": 18941.5,
"mean_peak_memory_mb": 50053.7
}
}
},
{
"name": "1280x720_frames121_steps4",
"dataset": "random",
"task": "i2v",
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 1,
"seed": 42,
"enable-negative-prompt": true,
"random-request-config": [
{
"width": 1280,
"height": 720,
"num_inference_steps": 4,
"num_frames": 121,
"fps": 16,
"weight": 1
}
],
"dataset_name": "random-mm",
"tokenizer": "gpt2",
"endpoint": "/v1/videos",
"num_prompts": 10,
"max_concurrency": 1,
"random_input_len": 8,
"random_output_len": 1,
"random_range_ratio": 0.0,
"percentile_metrics": "e2el",
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0,
"random_mm_limit_mm_per_prompt": {
"image": 1
},
"random_mm_bucket_config": {
"(720, 1280, 1)": 1.0
},
"extra_body": {
"width": 1280,
"height": 720,
"num_inference_steps": 4,
"num_frames": 121,
"fps": 16,
"seed": 42,
"negative_prompt": "Negative prompt for benchmarking diffusion models"
},
"baseline": {
"H100": {
"throughput_qps": 0.009595,
"latency_mean": 104.1058,
"peak_memory_mb_mean": 59718.0
"request_throughput": 0.009595,
"mean_e2el_ms": 104105.8,
"mean_peak_memory_mb": 59718.0
}
}
}
+17 -2
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Generate a nightly Excel performance report from JSON results.
@@ -137,6 +140,7 @@ def _omni_group_key(record: dict[str, Any]) -> tuple[Any, ...]:
return (
record.get("model_id") or "",
record.get("test_name") or "",
record.get("name") or "",
record.get("dataset_name") or "",
record.get("max_concurrency") if record.get("max_concurrency") is not None else 0,
record.get("num_prompts") if record.get("num_prompts") is not None else 0,
@@ -171,6 +175,7 @@ def _load_summary_columns(script_dir: str) -> list[str]:
"model_id",
"tokenizer_id",
"test_name",
"name",
"dataset_name",
"num_prompts",
"request_rate",
@@ -218,7 +223,7 @@ def _load_summary_columns(script_dir: str) -> list[str]:
def _ensure_omni_summary_columns(summary_columns: list[str]) -> list[str]:
"""Ensure omni summary contains required columns, even when a custom columns file exists."""
required = ("test_name", "dataset_name", "source_file")
required = ("test_name", "name", "dataset_name", "source_file")
existing = set(summary_columns)
if all(c in existing for c in required):
return summary_columns
@@ -232,8 +237,18 @@ def _ensure_omni_summary_columns(summary_columns: list[str]) -> list[str]:
else:
out.append("test_name")
existing = set(out)
if "dataset_name" not in existing:
if "name" not in existing:
if "test_name" in out:
idx = out.index("test_name") + 1
out.insert(idx, "name")
else:
out.append("name")
existing = set(out)
if "dataset_name" not in existing:
if "name" in out:
idx = out.index("name") + 1
out.insert(idx, "dataset_name")
elif "test_name" in out:
idx = out.index("test_name") + 1
out.insert(idx, "dataset_name")
else:
+8 -3
View File
@@ -1,4 +1,7 @@
#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
"""
Generate an improved nightly HTML performance dashboard without modifying the
existing generator.
@@ -256,6 +259,7 @@ def _sort_omni_records(records: list[dict[str, Any]]) -> list[dict[str, Any]]:
key=lambda r: (
r.get("model_id") or "",
r.get("test_name") or "",
r.get("name") or "",
r.get("dataset_name") or "",
r.get("max_concurrency") or 0,
r.get("num_prompts") or 0,
@@ -1236,7 +1240,7 @@ function initSection(prefix, columns, data, numericCols, groups) {{
const extraKey = prefix === "diff" ? "" : "tokenizer_id";
const metaKeys = prefix === "diff"
? ["test_name"]
: ["test_name", "max_concurrency", "num_prompts"];
: ["test_name", "name", "max_concurrency", "num_prompts"];
const configFields = prefix === "diff"
? ["test_name", "model", "endpoint", "dataset"]
: [
@@ -1245,13 +1249,14 @@ function initSection(prefix, columns, data, numericCols, groups) {{
"model_id",
"tokenizer_id",
"test_name",
"name",
"dataset_name",
"max_concurrency",
"num_prompts",
];
const labelFields = prefix === "diff"
? ["test_name", "dataset"]
: ["test_name", "dataset_name"];
: ["test_name", "name", "dataset_name"];
const latestMetric = prefix === "diff" ? "throughput_qps" : "output_throughput";
const secondaryMetric = prefix === "diff" ? "latency_mean" : "mean_e2el_ms";
const modelList = document.getElementById(`${{prefix}}-model-list`);
@@ -1650,7 +1655,7 @@ window.addEventListener("load", () => {{
'<div class="section-subtitle">Chat/audio benchmark history with '
"trend charts grouped by throughput, latency, and audio metrics."
'</div></div><div class="section-pills"><div class="pill"><strong>'
"Grouping</strong> test_name + dataset + concurrency + prompts</div>"
"Grouping</strong> test_name + name + dataset + concurrency + prompts</div>"
'<div class="pill"><strong>Snapshots</strong> single-point series '
"stay readable</div></div></div>"
),
+13 -2
View File
@@ -41,7 +41,7 @@
# From repo root: pytest -sv -m "<markers> and local_model" (markers from MODEL_TYPE: omni, tts,
# diffusion; all → "(omni or tts or diffusion) and local_model"). Not filtered by nightly YAML.
# LABEL_SUBSTR: if set, restrict to tests/**/test_*.py basenames and tests/dfx/perf/tests/*.json
# whose filename contains the substring (benchmark runner chosen by JSON family).
# whose filename contains the substring (benchmark runner via is_diffusion_perf_config).
#
# stability (when included in TEST_TYPE):
# From repo root: pytest -s -v --run-level full_model -m "<mark>" tests/dfx/stability/scripts/...
@@ -711,8 +711,19 @@ def perf_json_model_family(json_basename: str) -> str:
def perf_json_runner(json_basename: str) -> Path:
if perf_json_model_family(json_basename) in ("omni", "tts"):
"""Pick benchmark runner from JSON schema (``dataset`` vs ``dataset_name``)."""
json_path = REPO_ROOT / PERF_TESTS_REL / json_basename
try:
if str(REPO_ROOT) not in sys.path:
sys.path.insert(0, str(REPO_ROOT))
from tests.dfx.conftest import is_diffusion_perf_config, load_configs
configs = load_configs(str(json_path))
if configs and any(is_diffusion_perf_config(cfg) for cfg in configs):
return RUN_DIFFUSION_BENCHMARK_REL
return RUN_BENCHMARK_REL
except Exception as exc:
print(f"# warn: could not classify {json_basename}: {exc}", file=sys.stderr)
return RUN_DIFFUSION_BENCHMARK_REL
+113 -7
View File
@@ -5,6 +5,7 @@ import warnings
from collections import defaultdict
from collections.abc import Sequence
from dataclasses import field, make_dataclass
from typing import Any
import numpy as np
from vllm.benchmarks.datasets import SampleRequest
@@ -15,6 +16,7 @@ from vllm.tokenizers import TokenizerLike
from vllm_omni.metrics import definitions as defs
_PERCENTILE_ROWS_TYPE = list[tuple[float, float]] | None
_STAGE_DURATION_MAP_TYPE = dict[str, float] | None
_FLOAT_LIST_TYPE = list[float]
_INT_LIST_TYPE = list[int]
@@ -67,9 +69,14 @@ _MULTIMODAL_BENCHMARK_FIELDS = [
(defs.MEDIAN_PEAK_MEMORY_MB, float, field(default=0.0)),
(defs.STD_PEAK_MEMORY_MB, float, field(default=0.0)),
(defs.PERCENTILES_PEAK_MEMORY_MB, _PERCENTILE_ROWS_TYPE, field(default=None)),
(defs.STAGE_DURATIONS_MEAN, _STAGE_DURATION_MAP_TYPE, field(default=None)),
(defs.STAGE_DURATIONS_P50, _STAGE_DURATION_MAP_TYPE, field(default=None)),
(defs.STAGE_DURATIONS_P99, _STAGE_DURATION_MAP_TYPE, field(default=None)),
]
MultiModalsBenchmarkMetrics = make_dataclass(
# ``make_dataclass`` returns a runtime class that mypy treats as a variable, not
# a type. Annotate as Any so call sites can use these names in annotations.
MultiModalsBenchmarkMetrics: Any = make_dataclass(
"MultiModalsBenchmarkMetrics",
_MULTIMODAL_BENCHMARK_FIELDS,
bases=(BenchmarkMetrics,),
@@ -99,7 +106,7 @@ _STAGE_BENCHMARK_FIELDS = [
(defs.INTER_OUTPUT_LATENCIES_MS, _FLOAT_LIST_TYPE, field(default_factory=list)),
]
StageBenchmarkMetrics = make_dataclass(
StageBenchmarkMetrics: Any = make_dataclass(
"StageBenchmarkMetrics",
_STAGE_BENCHMARK_FIELDS,
namespace={"__doc__": "Aggregated metrics for one pipeline stage (for printing only)."},
@@ -240,6 +247,7 @@ def print_metrics(
if isinstance(metrics, MultiModalsBenchmarkMetrics):
print("{:<40} {:<10.2f}".format("Peak concurrent requests:", metrics.max_concurrent_requests))
print_peak_memory_metrics(metrics)
print_stage_durations_metrics(selected_percentiles or [], metrics)
if task_type != TaskType.GENERATION or "e2el" in selected_percentile_metrics:
process_one_metric("e2el", metrics)
print_text_metrics(task_type, selected_percentile_metrics, metrics)
@@ -338,6 +346,92 @@ def print_peak_memory_metrics(metrics: MultiModalsBenchmarkMetrics):
print("{:<40} {:<10.2f}".format(f"P{_p_label(p)} PEAK_MEMORY_MB (MB):", value))
def aggregate_stage_durations(outputs: Sequence[RequestFuncOutput]) -> dict[str, dict[str, float]]:
"""Aggregate pipeline profiler timings into mean/p50/p99 maps.
``stage_N_gen_ms`` is omitted. That clock already belongs to
``stage_gen_time`` / image / video generation, not to this profiler block.
The maps are stored on ``MultiModalsBenchmarkMetrics`` and printed with the
other run-level metrics. ``--print-stage`` does not gate them.
"""
stage_duration_lists: dict[str, list[float]] = {}
for output in outputs:
if not getattr(output, "success", False):
continue
stage_durations = getattr(output, "stage_durations", None)
if not isinstance(stage_durations, dict):
continue
for stage, duration in stage_durations.items():
if _is_printed_stage_gen_key(str(stage)):
continue
if isinstance(duration, bool) or not isinstance(duration, (int, float)) or not np.isfinite(duration):
continue
stage_duration_lists.setdefault(str(stage), []).append(float(duration))
if not stage_duration_lists:
return {}
return {
defs.STAGE_DURATIONS_MEAN: {stage: float(np.mean(values)) for stage, values in stage_duration_lists.items()},
defs.STAGE_DURATIONS_P50: {
stage: float(np.percentile(values, 50)) for stage, values in stage_duration_lists.items()
},
defs.STAGE_DURATIONS_P99: {
stage: float(np.percentile(values, 99)) for stage, values in stage_duration_lists.items()
},
}
def _stage_duration_display_name(stage: str) -> str:
"""Strip pipeline-class prefix and prettify profiler keys for printing.
``Wan22I2VPipeline.diffuse`` -> ``Diffuse``
``Wan22I2VPipeline.text_encoder.forward`` -> ``Text Encoder Forward``
``queue_wait_ms`` -> ``Queue Wait``
"""
name = str(stage)
head, sep, tail = name.partition(".")
if sep and head.endswith("Pipeline"):
name = tail
if name.endswith("_ms"):
name = name[: -len("_ms")]
return name.replace("_", " ").replace(".", " ").title()
def _is_printed_stage_gen_key(stage: str) -> bool:
"""True for ``stage_0_gen_ms``, which duplicates ``--print-stage`` stage timing."""
name = str(stage)
prefix, suffix = "stage_", "_gen_ms"
if not name.startswith(prefix) or not name.endswith(suffix):
return False
return name[len(prefix) : -len(suffix)].isdigit()
def print_stage_durations_metrics(
selected_percentiles: list[float],
metrics: MultiModalsBenchmarkMetrics,
) -> None:
"""Print profiler timings already stored on ``metrics``.
Mean and median always print, matching :func:`print_image_metrics`. P99 prints
only when 99 is in ``selected_percentiles``. ``stage_N_gen_ms`` is not here;
that clock stays on stage_gen_time / image / video generation.
"""
mean = getattr(metrics, defs.STAGE_DURATIONS_MEAN, None) or {}
if not mean:
return
p50 = getattr(metrics, defs.STAGE_DURATIONS_P50, None) or {}
p99 = getattr(metrics, defs.STAGE_DURATIONS_P99, None) or {}
show_p99 = any(float(p) == 99.0 for p in selected_percentiles)
print("{s:{c}^{n}}".format(s=" Stage Durations ", n=50, c="-"))
for stage in mean:
label = _stage_duration_display_name(stage)
unit = " (ms)" if str(stage).endswith("_ms") else " (s)"
print("{:<40} {:<10.2f}".format(f"Mean {label}{unit}:", mean[stage]))
if stage in p50:
print("{:<40} {:<10.2f}".format(f"Median {label}{unit}:", p50[stage]))
if show_p99 and stage in p99:
print("{:<40} {:<10.2f}".format(f"P99 {label}{unit}:", p99[stage]))
def print_image_metrics(selected_percentiles: list[float], metrics: MultiModalsBenchmarkMetrics):
print("{s:{c}^{n}}".format(s=" Image Result ", n=50, c="="))
print("{:<40} {:<10}".format("Total images generated:", getattr(metrics, defs.TOTAL_IMAGES)))
@@ -818,7 +912,7 @@ def calculate_metrics(
request_rate,
benchmark_duration,
print_stage: bool = False,
) -> tuple[BenchmarkMetrics, list[int]]:
) -> tuple[MultiModalsBenchmarkMetrics, list[int]]:
"""Calculate the metrics for the benchmark.
Args:
@@ -928,7 +1022,7 @@ def calculate_metrics(
goodput_ttfts.append(outputs[i].ttft if ttft_measured else None)
goodput_audio_ttfps.append(getattr(outputs[i], defs.AUDIO_TTFP, 0.0) if audio_ttfp_measured else None)
audio_duration.append(getattr(outputs[i], defs.AUDIO_DURATION, 0.0))
audio_frames.append(getattr(outputs[i], defs.AUDIO_FRAMES, 0.0))
audio_frames.append(getattr(outputs[i], defs.AUDIO_FRAMES, 0))
image_count = int(getattr(outputs[i], defs.IMAGE_COUNT, 0) or 0)
total_images += image_count
image_generation_time_ms = float(getattr(outputs[i], defs.IMAGE_GENERATION_TIME_MS, 0.0) or 0.0)
@@ -982,9 +1076,17 @@ def calculate_metrics(
good_completed += 1
if completed == 0:
warnings.formatwarning = lambda msg, category, filename, lineno, line=None: (
f"{filename}:{lineno}: {category.__name__}: {msg}\n"
)
def _formatwarning(
message: Warning | str,
category: type[Warning],
filename: str,
lineno: int,
line: str | None = None,
) -> str:
return f"{filename}:{lineno}: {category.__name__}: {message}\n"
warnings.formatwarning = _formatwarning
warnings.warn(
"All requests failed. This is likely due to a misconfiguration on the benchmark arguments.",
stacklevel=2,
@@ -1074,6 +1176,7 @@ def calculate_metrics(
isinstance(getattr(output, "duplex_session_metrics", None), dict) for output in outputs
)
missing_duplex_value = float("nan") if duplex_metrics_present else 0
stage_duration_summaries = aggregate_stage_durations(outputs)
metrics = MultiModalsBenchmarkMetrics(
completed=completed,
failed=len(failed_outputs),
@@ -1167,6 +1270,9 @@ def calculate_metrics(
defs.AUDIO_CONTINUITY_OK_RATE: (
(sum(audio_continuity_ok) / len(audio_continuity_ok)) if audio_continuity_ok else 1.0
),
defs.STAGE_DURATIONS_MEAN: stage_duration_summaries.get(defs.STAGE_DURATIONS_MEAN) or {},
defs.STAGE_DURATIONS_P50: stage_duration_summaries.get(defs.STAGE_DURATIONS_P50) or {},
defs.STAGE_DURATIONS_P99: stage_duration_summaries.get(defs.STAGE_DURATIONS_P99) or {},
},
)
print_metrics(
+360 -36
View File
@@ -19,6 +19,7 @@ from dataclasses import dataclass, replace
from datetime import datetime
from pathlib import Path
from typing import TYPE_CHECKING, Any, Literal
from urllib.parse import urlparse
import aiohttp
import numpy as np
@@ -873,6 +874,9 @@ class MixRequestFuncOutput(RequestFuncOutput):
tts_turn_pcm_bytes: list[bytes] | None = None
#: Per-stage snapshot from orchestrator ``metrics["stage_metrics"]`` (merged across SSE chunks).
stage_metrics: dict[str, dict] | None = None
#: Diffusion pipeline profiler timings from response ``stage_durations``
#: (e.g. diffuse / text_encoder.forward / vae.decode), when present.
stage_durations: dict[str, float] | None = None
stage_id: int | None = None
final_output_type: str | None = None
duplex_request_metrics: list[dict[str, object]] | None = None
@@ -883,6 +887,7 @@ _IMAGE_EDITS_EXTRA_BODY_FORM_FIELDS = (
"negative_prompt",
"num_inference_steps",
"guidance_scale",
"guidance_scale_2",
"strength",
"true_cfg_scale",
"seed",
@@ -902,15 +907,51 @@ def _guess_mime_type(path: str) -> str:
return mime or "application/octet-stream"
def _iter_image_edit_inputs(value: Any) -> Iterable[Any]:
"""Yield image references from benchmark multimodal content."""
_IMAGE_REFERENCE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".heic", ".heif"})
_VIDEO_REFERENCE_SUFFIXES = frozenset({".mp4", ".mov", ".webm", ".mkv", ".m4v"})
def _string_reference_kind(reference: str) -> str | None:
"""Classify a bare reference string as image, video, or file.
``data:image`` / ``data:video`` carry their type in the URL. Bare http(s)
URLs use the path extension. An existing local path is a file upload, not
an image or a video URL.
"""
if reference.startswith("data:image"):
return "image"
if reference.startswith("data:video"):
return "video"
if reference.startswith(("http://", "https://")):
suffix = Path(urlparse(reference).path).suffix.lower()
if suffix in _VIDEO_REFERENCE_SUFFIXES:
return "video"
if suffix in _IMAGE_REFERENCE_SUFFIXES:
return "image"
return None
local_path = reference.removeprefix("file://")
if local_path and os.path.exists(local_path):
return "file"
return None
def _iter_image_reference_inputs(value: Any) -> Iterable[Any]:
"""Yield image references from benchmark multimodal content.
``random-mm`` image buckets arrive as OpenAI chat parts
``{"type": "image_url", "image_url": {"url": ...}}``. Yield
``{"image_url": url}`` so the form helper keeps an explicit image type.
Bare video strings are left for ``_iter_video_reference_inputs``.
"""
if value is None:
return
if isinstance(value, list):
for item in value:
yield from _iter_image_edit_inputs(item)
yield from _iter_image_reference_inputs(item)
return
if not isinstance(value, dict):
if isinstance(value, str) and _string_reference_kind(value) == "video":
return
yield value
return
@@ -919,15 +960,49 @@ def _iter_image_edit_inputs(value: Any) -> Iterable[Any]:
image_url = value.get("image_url")
if isinstance(image_url, dict):
url = image_url.get("url")
if url:
yield url
elif image_url:
yield image_url
if isinstance(url, str) and url:
yield {"image_url": url}
elif isinstance(image_url, str) and image_url:
yield {"image_url": image_url}
return
for key in ("image", "images"):
if key in value:
yield from _iter_image_edit_inputs(value[key])
yield from _iter_image_reference_inputs(value[key])
def _iter_video_reference_inputs(value: Any) -> Iterable[dict[str, str]]:
"""Yield structured video references from benchmark multimodal content.
``random-mm`` video buckets arrive as OpenAI chat parts
``{"type": "video_url", "video_url": {"url": ...}}``, or as a bare
``data:video`` / video http(s) string. Yield ``{"video_url": url}`` so the
form helper keeps the video branch.
"""
if value is None:
return
if isinstance(value, list):
for item in value:
yield from _iter_video_reference_inputs(item)
return
if not isinstance(value, dict):
if isinstance(value, str) and _string_reference_kind(value) == "video":
yield {"video_url": value}
return
if value.get("type") == "video_url":
video_url = value.get("video_url")
if isinstance(video_url, dict):
url = video_url.get("url")
if isinstance(url, str) and url:
yield {"video_url": url}
elif isinstance(video_url, str) and video_url:
yield {"video_url": video_url}
return
for key in ("video", "videos"):
if key in value:
yield from _iter_video_reference_inputs(value[key])
def _add_image_edit_input_to_form(form: aiohttp.FormData, image_input: Any) -> None:
@@ -940,6 +1015,12 @@ def _add_image_edit_input_to_form(form: aiohttp.FormData, image_input: Any) -> N
)
return
if isinstance(image_input, Mapping) and _is_structured_image_reference(image_input):
image_url = image_input.get("image_url")
if isinstance(image_url, str) and image_url:
_add_image_edit_input_to_form(form, image_url)
return
if isinstance(image_input, str):
if image_input.startswith(("data:image", "http://", "https://")):
form.add_field("url", image_input)
@@ -1137,6 +1218,68 @@ def _update_output_peak_memory_from_payload(output: MixRequestFuncOutput, data:
output.peak_memory_mb = peak_memory_mb
def _coerce_stage_durations_dict(raw: object) -> dict[str, float] | None:
"""Normalize a stage_durations mapping to ``dict[str, float]``."""
if not isinstance(raw, dict) or not raw:
return None
coerced: dict[str, float] = {}
for key, value in raw.items():
if isinstance(value, bool) or not isinstance(value, (int, float)) or not np.isfinite(value):
continue
coerced[str(key)] = float(value)
return coerced or None
def _extract_stage_durations_from_payload(data: Mapping[str, object]) -> dict[str, float] | None:
"""Pull pipeline profiler timings from video/image/chat response shapes."""
found = _coerce_stage_durations_dict(data.get("stage_durations"))
if found:
return found
metrics = data.get("metrics")
if isinstance(metrics, dict):
found = _coerce_stage_durations_dict(metrics.get("stage_durations"))
if found:
return found
response_data = data.get("data")
if isinstance(response_data, list):
for item in response_data:
if isinstance(item, dict):
found = _coerce_stage_durations_dict(item.get("stage_durations"))
if found:
return found
choices = data.get("choices")
if isinstance(choices, list):
for choice in choices:
if not isinstance(choice, dict):
continue
for message_key in ("message", "delta"):
message = choice.get(message_key)
if not isinstance(message, dict):
continue
content = message.get("content")
if isinstance(content, list):
for item in content:
if isinstance(item, dict):
found = _coerce_stage_durations_dict(item.get("stage_durations"))
if found:
return found
elif isinstance(content, dict):
found = _coerce_stage_durations_dict(content.get("stage_durations"))
if found:
return found
return None
def _update_output_stage_durations_from_payload(output: MixRequestFuncOutput, data: Mapping[str, object]) -> None:
"""Persist the full profiler ``stage_durations`` map when the response has one."""
found = _extract_stage_durations_from_payload(data)
if found:
output.stage_durations = found
def _image_metrics_from_stage_metrics(metrics: object) -> tuple[int, float, int, float]:
if not isinstance(metrics, dict):
return 0, 0.0, 0, 0.0
@@ -1209,6 +1352,7 @@ def _apply_image_metrics_from_payload(output: MixRequestFuncOutput, data: Mappin
"""Populate image benchmark fields from an OpenAI-compatible image payload."""
_update_output_stage_metrics_from_payload(output, data, update_output_tokens=False)
_update_output_peak_memory_from_payload(output, data)
_update_output_stage_durations_from_payload(output, data)
payload_image_count = 0
response_data = data.get("data")
@@ -1321,15 +1465,129 @@ def _video_frames_from_payload(data: Mapping[str, object], request_body: Mapping
def _is_structured_image_reference(reference: Mapping[str, object]) -> bool:
"""True for API image_reference objects ({image_url}/{file_id})."""
"""True for API image_reference objects ({"image_url": "..."})."""
image_url = reference.get("image_url")
file_id = reference.get("file_id")
has_url = isinstance(image_url, str) and bool(image_url)
has_file_id = isinstance(file_id, str) and bool(file_id)
return has_url or has_file_id
return isinstance(image_url, str) and bool(image_url)
def _add_video_reference_to_form(form: aiohttp.FormData, reference: object) -> bool:
def _is_structured_video_reference(reference: Mapping[str, object]) -> bool:
"""True for API video_reference objects ({"video_url": "..."})."""
video_url = reference.get("video_url")
return isinstance(video_url, str) and bool(video_url)
_VIDEO_REFERENCE_JSON_MAX_BYTES = 1024 * 1024
def _data_video_json_exceeds_text_limit(video_url: str) -> bool:
"""True when a data:video URL would exceed the ~1MB multipart text-part limit."""
if not video_url.startswith("data:video"):
return False
encoded = json.dumps({"video_url": video_url}).encode("utf-8")
return len(encoded) > _VIDEO_REFERENCE_JSON_MAX_BYTES
def _add_data_video_upload(form: aiohttp.FormData, video_url: str) -> bool:
"""Upload one inline video as ``input_references`` instead of a JSON text part."""
header, _, payload = video_url.partition(",")
if not payload:
raise ValueError(f"Unsupported video data URL: {video_url[:64]!r}")
try:
video_bytes = base64.b64decode(payload)
except (ValueError, TypeError) as exc:
raise ValueError("video data URL is not valid base64") from exc
mime = header[len("data:") :].split(";", 1)[0] or "video/mp4"
suffix = ".mp4" if mime.endswith("mp4") else ".bin"
form.add_field(
"input_references",
video_bytes,
filename=f"benchmark-reference{suffix}",
content_type=mime,
)
return True
def _file_bytes_as_data_url(raw: bytes, mime: str) -> str:
encoded = base64.b64encode(raw).decode("ascii")
return f"data:{mime};base64,{encoded}"
def _image_reference_json_value(reference: object) -> object:
"""Turn an image file into a JSON ``image_reference`` when it must share a form.
``input_reference`` cannot be combined with ``video_reference``. Image URLs
stay URLs. Upload bytes and local image files become ``data:image`` URLs.
"""
if isinstance(reference, Mapping) and "bytes" in reference and not _is_structured_image_reference(reference):
raw = reference["bytes"]
if not isinstance(raw, (bytes, bytearray)):
raise ValueError(f"image reference bytes must be bytes (got {type(raw).__name__}).")
content_type = reference.get("content_type", "image/png")
if not isinstance(content_type, str) or not content_type.startswith("image/"):
content_type = "image/png"
return {"image_url": _file_bytes_as_data_url(bytes(raw), content_type.split(";", 1)[0])}
if isinstance(reference, str):
kind = _string_reference_kind(reference)
if kind == "image":
return {"image_url": reference}
if kind == "file":
local_path = reference.removeprefix("file://")
mime = _guess_mime_type(local_path)
if not mime.startswith("image/"):
mime = "image/png"
with open(local_path, "rb") as handle:
return {"image_url": _file_bytes_as_data_url(handle.read(), mime)}
return reference
def _video_reference_json_value(reference: object) -> object:
"""Turn a video file into a JSON ``video_reference`` when it must share a form.
``input_reference`` cannot be combined with ``image_reference``. Video URLs
stay URLs. Local video files become ``data:video`` URLs.
"""
if isinstance(reference, Mapping) and "bytes" in reference and not _is_structured_video_reference(reference):
raw = reference["bytes"]
if not isinstance(raw, (bytes, bytearray)):
raise ValueError(f"video reference bytes must be bytes (got {type(raw).__name__}).")
content_type = reference.get("content_type", "video/mp4")
if not isinstance(content_type, str) or not content_type.startswith("video/"):
content_type = "video/mp4"
return {"video_url": _file_bytes_as_data_url(bytes(raw), content_type.split(";", 1)[0])}
if isinstance(reference, str):
kind = _string_reference_kind(reference)
if kind == "video":
return {"video_url": reference}
if kind == "file":
local_path = reference.removeprefix("file://")
mime = _guess_mime_type(local_path)
if not mime.startswith("video/"):
mime = "video/mp4"
with open(local_path, "rb") as handle:
return {"video_url": _file_bytes_as_data_url(handle.read(), mime)}
return reference
def _add_video_reference_to_form(
form: aiohttp.FormData,
reference: object,
*,
upload_inline_video: bool = True,
) -> bool:
"""Encode one reference: image URL, video URL, or file upload.
Image URLs use ``image_reference``. Video URLs use ``video_reference``.
A lone ``data:video`` whose JSON text exceeds 1MB is uploaded as
``input_references``. Local paths and raw bytes use ``input_reference``.
``upload_inline_video`` must be false when an image is on the same form:
``input_references`` cannot be combined with ``image_reference``.
"""
candidates = reference if isinstance(reference, list) else [reference]
for item in candidates:
if isinstance(item, Mapping):
file_id = item.get("file_id")
if isinstance(file_id, str) and file_id:
raise ValueError("file_id is not supported yet")
if isinstance(reference, dict) and "bytes" in reference:
form.add_field(
"input_reference",
@@ -1343,23 +1601,39 @@ def _add_video_reference_to_form(form: aiohttp.FormData, reference: object) -> b
form.add_field("image_reference", json.dumps(dict(reference)))
return True
if isinstance(reference, Mapping) and _is_structured_video_reference(reference):
video_url = reference.get("video_url")
if upload_inline_video and isinstance(video_url, str) and _data_video_json_exceeds_text_limit(video_url):
return _add_data_video_upload(form, video_url)
form.add_field("video_reference", json.dumps(dict(reference)))
return True
if isinstance(reference, list):
if reference and all(isinstance(item, Mapping) and _is_structured_image_reference(item) for item in reference):
form.add_field("image_reference", json.dumps([dict(item) for item in reference]))
return True
if reference and all(isinstance(item, Mapping) and _is_structured_video_reference(item) for item in reference):
form.add_field("video_reference", json.dumps([dict(item) for item in reference]))
return True
raise ValueError(
"Unsupported image_reference list; expected non-empty list of "
'{"image_url": "..."} and/or {"file_id": "..."} objects.'
"Unsupported reference list; expected non-empty list of "
'{"image_url": "..."} or {"video_url": "..."} objects.'
)
if isinstance(reference, str):
if reference.startswith(("data:image", "http://", "https://")):
kind = _string_reference_kind(reference)
if kind == "image":
form.add_field("image_reference", json.dumps({"image_url": reference}))
return True
local_path = reference.removeprefix("file://")
if os.path.exists(local_path):
with open(local_path, "rb") as f:
reference_bytes = f.read()
if kind == "video":
if upload_inline_video and _data_video_json_exceeds_text_limit(reference):
return _add_data_video_upload(form, reference)
form.add_field("video_reference", json.dumps({"video_url": reference}))
return True
if kind == "file":
local_path = reference.removeprefix("file://")
with open(local_path, "rb") as handle:
reference_bytes = handle.read()
form.add_field(
"input_reference",
reference_bytes,
@@ -1367,15 +1641,64 @@ def _add_video_reference_to_form(form: aiohttp.FormData, reference: object) -> b
content_type=_guess_mime_type(local_path),
)
return True
raise ValueError(f"Unsupported image_reference path or URL: {reference!r}")
if reference.startswith(("http://", "https://")):
raise ValueError(
"Bare http(s) reference needs an image or video extension "
f"({', '.join(sorted(_IMAGE_REFERENCE_SUFFIXES | _VIDEO_REFERENCE_SUFFIXES))}); "
f"got {reference!r}."
)
raise ValueError(f"Unsupported reference path or URL: {reference!r}")
raise ValueError(
"Unsupported image_reference; expected upload bytes, local path/URL string, "
'or {"image_url": "..."} / {"file_id": "..."} object '
"Unsupported reference; expected image URL, video URL, upload bytes, or a local file "
f"(got {type(reference).__name__})."
)
def _add_combined_video_form_references(
form: aiohttp.FormData,
multi_modal_content: Any,
extra_body: Mapping[str, Any] | None = None,
) -> None:
"""Serialize image and video refs using a server-accepted field pair.
Alone, each reference uses its own field: image URL → ``image_reference``,
video URL → ``video_reference``, file → ``input_reference``. A lone inline
video whose JSON text exceeds 1MB is uploaded as ``input_references``.
Together, that upload cannot be combined with ``image_reference``, so both
sides stay on the JSON fields. A file paired with the other media is rewritten
as a data URL of the matching type.
"""
extra_body = extra_body or {}
image_refs = list(_iter_image_reference_inputs(multi_modal_content))
video_refs = list(_iter_video_reference_inputs(multi_modal_content))
if not image_refs and extra_body.get("image_reference") is not None:
image_refs = [extra_body["image_reference"]]
if not video_refs and extra_body.get("video_reference") is not None:
video_refs = [extra_body["video_reference"]]
if image_refs and video_refs:
for raw in (image_refs[0], video_refs[0]):
candidates = raw if isinstance(raw, list) else [raw]
for item in candidates:
if isinstance(item, Mapping):
file_id = item.get("file_id")
if isinstance(file_id, str) and file_id:
raise ValueError("file_id is not supported yet")
_add_video_reference_to_form(form, _image_reference_json_value(image_refs[0]))
_add_video_reference_to_form(
form,
_video_reference_json_value(video_refs[0]),
upload_inline_video=False,
)
return
if image_refs:
_add_video_reference_to_form(form, image_refs[0])
if video_refs:
_add_video_reference_to_form(form, video_refs[0])
def _add_video_extra_body_to_form(
form: aiohttp.FormData,
extra_body: Mapping[str, object],
@@ -1398,9 +1721,11 @@ def _add_video_extra_body_to_form(
"height",
"poll_interval_s",
"poll_timeout_s",
# Handled only by _add_video_reference_to_form (upload / JSON image_url).
# Handled only by _add_video_reference_to_form (upload / JSON image_url / video_url).
"image_reference",
"video_reference",
"input_reference",
"input_references",
*_VIDEO_FORM_FIELDS,
}
for key, value in extra_body.items():
@@ -1421,8 +1746,9 @@ def _apply_video_metrics_from_payload(
output.video_frames = _video_frames_from_payload(data, request_body)
_update_output_stage_metrics_from_payload(output, data, update_output_tokens=False)
_update_output_peak_memory_from_payload(output, data)
_update_output_stage_durations_from_payload(output, data)
stage_durations = data.get("stage_durations")
stage_durations = output.stage_durations if output.stage_durations is not None else data.get("stage_durations")
stage_gen_ms = _video_generation_ms_from_stage_durations(stage_durations)
if stage_gen_ms <= 0:
inference_time_s = coerce_positive_float_scalar(data.get("inference_time_s"))
@@ -1534,6 +1860,7 @@ async def async_request_openai_chat_omni_completions(
output.image_pixels = 0
output.denoise_step_latency_ms = 0.0
output.peak_memory_mb = 0.0
output.stage_durations = None
completion_tokens_seen = 0
try:
async with session.post(url=api_url, json=payload, headers=headers) as response:
@@ -1564,6 +1891,7 @@ async def async_request_openai_chat_omni_completions(
data = json.loads(chunk)
_update_output_stage_metrics_from_payload(output, data)
_update_output_peak_memory_from_payload(output, data)
_update_output_stage_durations_from_payload(output, data)
usage = data.get("usage")
completion_tokens = None
if isinstance(usage, dict):
@@ -1926,15 +2254,7 @@ async def async_request_openai_videos_omni(
form.add_field("size", str(request_body["size"]))
_add_video_extra_body_to_form(form, extra_body, request_body)
reference_added = False
for reference in _iter_image_edit_inputs(request_func_input.multi_modal_content):
if _add_video_reference_to_form(form, reference):
reference_added = True
break
if not reference_added:
image_reference = extra_body.get("image_reference")
if image_reference is not None:
_add_video_reference_to_form(form, image_reference)
_add_combined_video_form_references(form, request_func_input.multi_modal_content, extra_body)
headers = {
"Authorization": f"Bearer {os.environ.get('OPENAI_API_KEY')}",
@@ -2044,7 +2364,7 @@ async def async_request_openai_image_edits_omni(
_add_image_edit_extra_body_to_form(form, extra_body)
try:
image_inputs = list(_iter_image_edit_inputs(request_func_input.multi_modal_content))
image_inputs = list(_iter_image_reference_inputs(request_func_input.multi_modal_content))
if not image_inputs:
raise ValueError(
"openai-image-edits-omni requires image multimodal content. "
@@ -2102,6 +2422,7 @@ async def async_request_openai_image_edits_omni(
update_output_tokens=(data.get("type") == "ar_delta"),
)
_update_output_peak_memory_from_payload(output, data)
_update_output_stage_durations_from_payload(output, data)
chunk_type = data.get("type")
if chunk_type == "ar_delta":
@@ -3201,6 +3522,9 @@ async def benchmark(
defs.MEAN_PEAK_MEMORY_MB: getattr(mm_metrics, defs.MEAN_PEAK_MEMORY_MB),
defs.MEDIAN_PEAK_MEMORY_MB: getattr(mm_metrics, defs.MEDIAN_PEAK_MEMORY_MB),
defs.PERCENTILES_PEAK_MEMORY_MB: getattr(mm_metrics, defs.PERCENTILES_PEAK_MEMORY_MB),
defs.STAGE_DURATIONS_MEAN: getattr(mm_metrics, defs.STAGE_DURATIONS_MEAN) or {},
defs.STAGE_DURATIONS_P50: getattr(mm_metrics, defs.STAGE_DURATIONS_P50) or {},
defs.STAGE_DURATIONS_P99: getattr(mm_metrics, defs.STAGE_DURATIONS_P99) or {},
"input_lens": [output.prompt_len for output in outputs],
"start_times": [output.start_time for output in outputs],
"output_lens": actual_output_lens,
@@ -1730,7 +1730,10 @@ class Cosmos3OmniDiffusersPipeline(
},
},
}
return DiffusionOutput(output=action_output)
return DiffusionOutput(
output=action_output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
@staticmethod
def _truthy(value) -> bool:
@@ -3610,6 +3613,7 @@ class Cosmos3OmniDiffusersPipeline(
"payload": {"video": output_video},
"metadata": {"video": {"fps": frame_rate}},
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
full_output = torch.cat(output_chunks, dim=2)[:, :, :total_frames]
@@ -3636,6 +3640,7 @@ class Cosmos3OmniDiffusersPipeline(
"video": {"fps": frame_rate},
},
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
# -- Forward (main generation entry point) -------------------------------
@@ -4072,7 +4077,10 @@ class Cosmos3OmniDiffusersPipeline(
if _is_rank_zero():
logger.info("Sound tokenizer decoded in %.2fs", time.time() - sound_decode_start)
logger.info("Total pipeline time: %.2fs", time.time() - pipeline_start)
return DiffusionOutput(output={"video": video, "audio": audio, "audio_sample_rate": sound_sample_rate})
return DiffusionOutput(
output={"video": video, "audio": audio, "audio_sample_rate": sound_sample_rate},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
if action_enabled:
if action_latents is None or raw_action_dim is None or domain_id is None:
@@ -4092,6 +4100,10 @@ class Cosmos3OmniDiffusersPipeline(
},
},
},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
return DiffusionOutput(output={"image": video} if is_t2i else {"video": video})
return DiffusionOutput(
output={"image": video} if is_t2i else {"video": video},
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
@@ -548,7 +548,10 @@ class HunyuanVideo15Pipeline(
latents = latents.to(self.vae.dtype) / self.vae.config.scaling_factor
output = self.vae.decode(latents, return_dict=False)[0]
return DiffusionOutput(output=output)
return DiffusionOutput(
output=output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
@@ -1,5 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
from __future__ import annotations
@@ -643,7 +643,10 @@ class HunyuanVideo15I2VPipeline(
latents = latents.to(self.vae.dtype) / self.vae.config.scaling_factor
output = self.vae.decode(latents, return_dict=False)[0]
return DiffusionOutput(output=output)
return DiffusionOutput(
output=output,
stage_durations=self.stage_durations if hasattr(self, "stage_durations") else None,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
+4
View File
@@ -99,6 +99,10 @@ MEDIAN_PEAK_MEMORY_MB = f"median_{PEAK_MEMORY_MB}"
STD_PEAK_MEMORY_MB = f"std_{PEAK_MEMORY_MB}"
PERCENTILES_PEAK_MEMORY_MB = f"percentiles_{PEAK_MEMORY_MB}"
STAGE_DURATIONS_MEAN = "stage_durations_mean"
STAGE_DURATIONS_P50 = "stage_durations_p50"
STAGE_DURATIONS_P99 = "stage_durations_p99"
# Stage snapshot / StageBenchmarkMetrics field names.
TOTAL_OUTPUT = "total_output"
TTFTS = "ttfts"