Compare commits

..
60 changed files with 6088 additions and 2614 deletions

No files matched your search

+1 -1
View File
@@ -334,7 +334,7 @@ Classify the model's **CI priority** first:
| **L1** | `tests/diffusion/models/{slug}/`, `tests/diffusion/cache/`, transformer unit tests | `core_model` + `cpu` | `test-ready.yml` | Weight remap, `_sp_plan`, cache enabler registration, shape contracts |
| **L2** | `tests/e2e/online_serving/test_{slug}.py` (and offline if the category is offline-first) | **`core_model` + `advanced_model`** (both on baseline smoke) + `diffusion` + `@hardware_test` / `hardware_marks` | `test-ready.yml` | Default deploy smoke — minimal `num_inference_steps`, single prompt |
| **L3** | `tests/e2e/online_serving/test_{slug}.py` **and** `tests/e2e/offline_inference/test_{slug}.py` when offline matters | Baseline smoke: **`core_model` + `advanced_model`**; heavier cases: `advanced_model` only (+ `diffusion`) | `test-merge.yml` **or** merged into nightly diffusion function job | Real weights, streaming/API paths, LoRA/offload smoke |
| **L4** | `tests/e2e/online_serving/test_{slug}_expansion.py` (+ offline expansion if needed) | `full_model` + `diffusion` | `test-nightly.yml` (X2I / X2V / X2A function groups) | Feature combos per [#1832](https://github.com/vllm-project/vllm-omni/issues/1832); perf → `tests/dfx/perf/tests/test_{model}_vllm_omni.json` |
| **L4** | `tests/e2e/online_serving/test_{slug}_expansion.py` (+ offline expansion if needed) | `full_model` + `diffusion` | `test-nightly.yml` (X2I / X2V / X2A function groups) | Feature combos per [#1832](https://github.com/vllm-project/vllm-omni/issues/1832); perf → `tests/dfx/perf/tests/test_{model}_vllm_omni.json` with per-case **`mark`** (`hardware_marks` + `full_model` + `diffusion`) |
**L2 & L3 online — same file, dual marks on the baseline smoke:** The **first / simplest** case in `test_{slug}.py` (default deploy, minimal steps, single prompt) should carry **both** `@pytest.mark.core_model` and `@pytest.mark.advanced_model` on the **same** function so L2 (`test-ready.yml`) and L3 (`test-merge.yml`) share one smoke test. Heavier deploy variants or API paths in the same file use **`advanced_model` only**. When L3 moves to nightly, migrate those heavier cases into `test_{slug}_expansion.py` with `full_model` and remove the dedicated `test-merge.yml` job (see `test_longcat_image_expansion.py`, `test_qwen_image_expansion.py`).
+11 -7
View File
@@ -97,7 +97,7 @@ Existing references: `tests/e2e/offline_inference/test_qwen2_5_omni.py` (L2-styl
| **Offline inference e2e** | `tests/e2e/offline_inference/` | **Module (default):** `omni_runner` + `omni_runner_handler`. **Function (isolation only):** `omni_runner_function` + `omni_runner_handler_function`. Diffusion/TTS may use `Omni(...).generate` directly | L2: `core_model` + **one of** `omni` / `tts` / `diffusion`; `@hardware_test(...)` when GPU/NPU is required |
| **Online serving e2e** | `tests/e2e/online_serving/` | **Module (default):** `omni_server` + `openai_client`. **Function (isolation only):** `omni_server_function` + `openai_client_function`. Clients: `send_omni_request` (omni), `send_audio_speech_request` (tts), `send_diffusion_request` / `send_video_diffusion_request` / `send_images_generations_request` (diffusion) | Baseline smoke: **`core_model` + `advanced_model`**; heavier paths: `advanced_model` only; L4 expansion: `full_model` |
| **Documentation / runnable examples** | `tests/examples/offline_inference/`, `tests/examples/online_serving/` | **Offline docs (preferred):** extract Python/Bash blocks from the doc README (e.g. `ReadmeSnippet.extract_readme_snippets`), `pytest.mark.parametrize` each snippet, run via `example_runner.run` with a stable `output_subfolder`. **Online docs:** copy client/request scripts into dedicated tests and keep them in sync with the doc page. | Usually **L4**: `advanced_model`, often `example` plus hardware marks matching the nightly docs-example job (see `.buildkite/test-nightly.yml`). Full conventions: [docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md](../../../docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md) (introduced in [PR #1910](https://github.com/vllm-project/vllm-omni/pull/1910): naming, output directory layout, skip rules, avoid trimming `num_inference_steps` without a strong CI reason). |
| **Performance / benchmark** | `tests/dfx/perf/tests/*.json` + `run_*_benchmark.py` | JSON or script-driven server + load config; assert explicit metrics / baselines | L4 Perf: `full_model` + `benchmark`; wire `test-nightly.yml` Perf steps |
| **Performance / benchmark** | `tests/dfx/perf/tests/*.json` + `run_*_benchmark.py` | JSON or script-driven server + load config; assert explicit metrics / baselines | L4 Perf: JSON `mark` with `full_model` + `omni`/`tts`/`diffusion`; wire `test-nightly.yml` Perf steps |
| **Invalid parameter / negative HTTP validation** | `tests/dfx/reliability/invalid_param_test/` | Live `omni_server` + low-level `send_*_http_request` with `err_code` / `err_message` | `pytest.mark.slow` + `omni` / `tts` / `diffusion` + `@hardware_marks` (`H100` or `L4`); CI in **`test-weekly.yml`** (not ready/merge/nightly) |
**If the user’s test plan includes invalid parameter validation / invalid params / negative HTTP / 400 validation:** do **not** add those `test_*` functions to `tests/e2e/online_serving/test_*.py` or `*_expansion.py`. **Move or author them** under `tests/dfx/reliability/invalid_param_test/` in the **endpoint-matching script** (see **Invalid parameter validation** below). Success-path e2e and invalid-param dfx tests must stay in separate modules.
@@ -270,7 +270,7 @@ def test_text_to_video_001(omni_server, openai_client) -> None:
*Documentation example tests:* follow the **Preferred Test Strategy** in [l4_doc_example_tests.inc.md](../../../docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md): dynamic extraction for offline READMEs; explicit copied client code for online pages until extraction is justified; use the documented **naming**, **output directory** (page folder + case id), and **skipping** rules (e.g. Gradio-only scripts).
*Performance tests:* add or extend entries under `tests/perf/` (and JSON configs where the project uses them), with **explicit baselines** and the same marker/run-level pairing as the CI step that will execute them.
*Performance tests:* add or extend entries under `tests/dfx/perf/tests/` (and JSON configs where the project uses them), with **explicit baselines**, **`mark`** on each case (`hardware_marks` + `full_model` + type marker), and the same nightly Perf step pattern as in-tree configs.
**3. Cross-cutting rules**
@@ -592,27 +592,31 @@ Implementation strategy:
**When the user explicitly asks for L4 perf (or full L4 including perf):**
1. **Do not** put throughput/latency assertions inside `test_*_expansion.py` — perf uses the **dfx benchmark harness**, not `omni_server` e2e fixtures.
2. Add or extend a **JSON config** under `tests/dfx/perf/tests/` (mirror in-tree names: `test_qwen_image_vllm_omni.json`, `test_bagel_vllm_omni.json`).
3. Each JSON entry: `test_name`, `server_params` (`model`, `serve_args`), `benchmark_params[]` with explicit **`baseline`** (`throughput_qps`, `latency_mean`, `peak_memory_mb_mean`, …).
2. Add or extend a **JSON config** under `tests/dfx/perf/tests/` (mirror in-tree names: `test_qwen_image_vllm_omni.json`, `test_cosmos3_vllm_omni.json`, `test_qwen3_omni_async_chunk.json`).
3. Each JSON **case**: `test_name`, optional **`mark`** (`hardware_marks` required when present; `marks`: `full_model` + `omni`/`tts`/`diffusion`), `server_params` (`model`, `serve_args` or `stage_config_name`), `benchmark_params[]` with **`name`** and explicit **`baseline`** (`throughput_qps`, `latency_mean`, `peak_memory_mb_mean`, …). Diffusion cases also set `server_type` (e.g. `"vllm-omni"`) and usually `benchmark_endpoint`.
4. Run locally via the matching script (model type → runner):
| Model type | Runner script | Example config |
|------------|---------------|----------------|
| **Diffusion X2I/X2V** | `tests/dfx/perf/scripts/run_diffusion_benchmark.py` | `tests/dfx/perf/tests/test_qwen_image_vllm_omni.json` |
| **TTS** | `tests/dfx/perf/scripts/run_benchmark.py` | `tests/dfx/perf/tests/test_tts.json` (shared) or `test_{slug}.json` (dedicated; use when the model must not join the shared nightly matrix before integration — e.g. `test_voxcpm2.json`, `test_coqui_tts.json`) |
| **Omni** | (see existing Omni Perf steps — `run_benchmark.py` / dedicated omni configs in-tree) | `tests/dfx/perf/tests/test_omni*.json` |
| **Omni** | `tests/dfx/perf/scripts/run_benchmark.py` | `test_qwen3_omni_no_async_chunk.json`, `test_qwen3_omni_async_chunk.json`, `test_qwen3_omni_vllm_text.json`, `test_qwen3_omni_multi_replicas.json` |
5. Wire **`test-nightly.yml` · Perf Test** step for that model (separate from Function Test): export `DIFFUSION_BENCHMARK_DIR` / `BENCHMARK_DIR`, run pytest on the script with `--test-config-file`, **upload artifacts** (`buildkite-agent artifact upload`), often **multi-GPU H100** for diffusion perf.
5. Wire **`test-nightly.yml` · Perf Test** step for that model (separate from Function Test): export `DIFFUSION_BENCHMARK_DIR` / `BENCHMARK_DIR`, run pytest on the script with **`--test-config-file`** (nightly perf steps do not use `-m`), **upload artifacts** (`buildkite-agent artifact upload`), often **multi-GPU H100** for diffusion perf.
6. **One benchmark scenario → one `test_name` block** in JSON (same spirit as one `test_*` per function case). Combine server `serve_args` + workload in one entry; use multiple `benchmark_params` rows for size/step sweeps under the same server config.
**Perf local command (diffusion example):**
**Perf local commands:**
```bash
cd tests
export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
# Single file (CI-like)
pytest -s -v dfx/perf/scripts/run_diffusion_benchmark.py \
--test-config-file dfx/perf/tests/test_<model>_vllm_omni.json
# Bulk load + filter by JSON mark
pytest -sv dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"
pytest -sv dfx/perf/scripts/run_benchmark.py -m "full_model and omni and H100"
```
**Clarify in the test plan** which L4 pillars you deliver: `Function only` | `Function + Perf` | `Function + Accuracy + Perf`.
@@ -127,8 +127,12 @@ pytest -s -v e2e/online_serving/test_qwen_image_expansion.py -m "full_model and
cd tests
export DIFFUSION_BENCHMARK_DIR=tests/dfx/perf/results
export DIFFUSION_ATTENTION_BACKEND=FLASH_ATTN
# CI-like: single JSON file (nightly Perf Test steps)
pytest -s -v dfx/perf/scripts/run_diffusion_benchmark.py \
--test-config-file dfx/perf/tests/test_qwen_image_vllm_omni.json
# Local bulk load: all *.json under tests/dfx/perf/tests/, filter by JSON mark
pytest -sv dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and diffusion and H100"
pytest -sv dfx/perf/scripts/run_benchmark.py -m "full_model and omni and H100"
```
Broad marker sweep (when no explicit file shard):
@@ -150,7 +154,26 @@ Nightly **L4** for a model is often **multiple jobs** in `test-nightly.yml`, not
**Rule:** “L4 functional cases” → **Function only** by default. State that Perf/Accuracy are separate pillars; add them only when requested.
**Diffusion perf JSON** (`tests/dfx/perf/tests/test_<slug>_vllm_omni.json`): array of objects with `test_name`, `server_params` (`model`, `serve_args`), `benchmark_params[]`, and per-workload **`baseline`** (`throughput_qps`, `latency_mean`, `peak_memory_mb_mean`). Copy structure from `test_qwen_image_vllm_omni.json` in-tree.
**Diffusion perf JSON** (`tests/dfx/perf/tests/test_<slug>_vllm_omni.json`): array of **case objects**, each with:
| Field | Required | Notes |
|-------|----------|-------|
| `test_name` | Yes | Unique server/workload id |
| `mark` | Recommended | When present: **`hardware_marks` required** + optional `marks` (`full_model`, `diffusion`, `local_model`, …). Parsed by `resolve_pytest_marks`; enables local `-m` filtering. |
| `server_type` | Diffusion | e.g. `"vllm-omni"` — routes case to `run_diffusion_benchmark.py` |
| `benchmark_endpoint` | Diffusion | e.g. `/v1/videos`, `/v1/images/generations` |
| `server_params` | Yes | `model`, `serve_args`, … |
| `benchmark_params[]` | Yes | Each row: **`name`** (pytest id suffix), workload fields, **`baseline`** |
Copy structure from `test_qwen_image_vllm_omni.json` or `test_cosmos3_vllm_omni.json` (2-GPU mark example).
**Omni perf JSON**: `test_qwen3_omni_*.json` — same `mark` shape with `"marks": ["full_model", "omni"]`; run via `run_benchmark.py`.
**Runner split**: `is_diffusion_perf_config()` → diffusion when `server_type` is set or `"diffusion"` ∈ `mark.marks`; otherwise omni/tts → `run_benchmark.py`.
**Param ids**: `{test_name}-{benchmark_params.name}` (e.g. `test_tts-p0`, not `test_tts-0`).
**Result filenames**: runtime hardware from `get_runtime_resource_label()`; `H100` omitted on default CI pool.
**Diffusion perf nightly step** (under **Diffusion X2I(&A&T)**; copy full `kubernetes` plugins from `· Perf Test · Qwen-Image`):
@@ -177,9 +200,9 @@ Nightly **L4** for a model is often **multiple jobs** in `test-nightly.yml`, not
| Model type | Perf runner | Config example |
|------------|-------------|----------------|
| Diffusion X2I/X2V | `dfx/perf/scripts/run_diffusion_benchmark.py` | `test_qwen_image_vllm_omni.json` |
| TTS | `dfx/perf/scripts/run_benchmark.py` | `test_tts.json` (`BENCHMARK_DIR`) |
| Omni | `run_benchmark.py` / omni configs in-tree | `test_omni*.json` |
| Diffusion X2I/X2V | `dfx/perf/scripts/run_diffusion_benchmark.py` | `test_qwen_image_vllm_omni.json`, `test_cosmos3_vllm_omni.json` |
| TTS | `dfx/perf/scripts/run_benchmark.py` | `test_tts.json`, `test_voxcpm2.json`, `test_higgs_audio_v3.json` |
| Omni | `dfx/perf/scripts/run_benchmark.py` | `test_qwen3_omni_async_chunk.json`, `test_qwen3_omni_no_async_chunk.json`, … |
Do **not** put throughput/latency baselines inside `test_*_expansion.py` — that belongs in the dfx perf JSON + nightly Perf job.
+180
View File
@@ -0,0 +1,180 @@
name: Daily PR labeling
on:
schedule:
# Run away from the top of the hour to reduce scheduler contention.
- cron: "17 1 * * *"
workflow_dispatch:
permissions:
contents: read
issues: write
pull-requests: write
concurrency:
group: daily-pr-labeler
cancel-in-progress: true
jobs:
label-open-prs:
if: github.repository == 'vllm-project/vllm-omni'
runs-on: ubuntu-latest
steps:
- name: Add relevant labels to open PRs
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { owner, repo } = context.repo;
// These labels are reserved for CI control and must never be added automatically.
const forbiddenLabels = new Set([
"ci-failure",
"diffusion-x2iat-test",
"diffusion-x2v-test",
"merge-test",
"nightly-test",
"npu-test",
"omni-test",
"ready",
"tts-test",
"weekly-test",
]);
const isForbiddenLabel = (label) =>
forbiddenLabels.has(label) || /\btest\b/i.test(label);
const prs = await github.paginate(github.rest.pulls.list, {
owner,
repo,
state: "open",
per_page: 100,
});
const changed = [];
const failures = [];
for (const pr of prs) {
// PR titles in this repository consistently carry the change area/type.
// Avoid body matching because the PR template contains generic keywords.
const title = (pr.title || "").toLowerCase();
const has = (pattern) => pattern.test(title);
const labels = new Set();
const add = (label) => {
if (!isForbiddenLabel(label)) labels.add(label);
};
const documentation = has(
/\bdocs?\b|\bdocumentation\b|\brecipe\b|\bskills?\b|\bguide\b|\breadme\b/
);
const test = has(
/\btests?\b|\bpytest\b|\be2e\b|\bcoverage\b|\bmock\b|\btestcase\b|\bci\b|\bcd\b|\bbuildkite\b/
);
const bug = has(
/\bbugfix\b|\bbug fix\b|\bbug\b|\bfix(?:es|ed|ing)?\b|\bcorrect(?:s|ing)?\b|\brepair\b|\bresolve\b|\bprevent\b|\bguard\b|\bharden\b|\brestore\b|\bsuppress\b|\bavoid\b|\breject\b|\bprotect\b|\bensure\b|\baddress\b/
);
const refactor = has(
/\brefactor\b|\bcleanup\b|\bchore\b|\breorganiz|\bunify\b|\bcentralize\b|\bsplit\b|\bmigrat|\brename\b|\bconsolidat|\bre-land\b/
);
if (documentation) add("documentation");
if (bug) add("bug");
if (refactor) add("refactor");
if (test) add("CI/CD");
const explicitFeature = has(
/\bfeature\b|\bfeat\b|\badd\b|\benable\b|\ballow\b|\bimplement\b|\bintroduce\b|\boptimi[sz]\w*\b|\bperf(?:ormance)?\b|\bbenchmark\b|\bprofil(?:er|ing)?\b|\btracing\b|\bstreaming\b/
);
if (
!documentation &&
!test &&
!refactor &&
(explicitFeature || (!bug && has(/\bsupport\b/)))
) {
add("enhancement");
}
if (
has(
/\bquant(?:ization|ize|ized)?\b|\bfp8\b|\bfp4\b|\bnvfp\b|\bw4a4\b|\bmodelopt\b|\bawq\b|\bgptq\b|\bint8\b/
)
) {
add("quantization");
}
if (
has(
/\bdiffusion\b|\bdit\b|\bflux\b|\bwan\b|\bltx\b|\bhelios\b|\bhunyuanimage\b|\bqwen[- ]image\b|\bz-image\b|\bcosmos\b|\blingbot\b|\bimage\b|\bvideo\b|\btext-to-(?:video|image)\b/
)
) {
add("diffusion");
}
if (
has(
/\btts\b|\btext-to-speech\b|\bspeech\b|\bvocoder\b|\bvoice\b|\bcode2wav\b|\bcosyvoice\b|\bvoxcpm\b|\bmoss\b|\bfish speech\b|\baudiox\b|\bstep audio\b/
)
) {
add("tts");
}
if (
has(
/\bomni\b|\bmultimodal\b|\bmimo-audio\b|\bomnivoice\b|\bminicpm-o\b|\bqwen3-omni\b|\bqwen2\.5-omni\b/
)
) {
add("omni");
}
if (
has(
/\b(core|connector|engine|scheduler|worker|modelrunner|model runner|orchestrat|runtime|stage|transfer|cache|outputprocessor|output processor|entrypoint|config|ipc|shared memory|shm|serialization|requestoutput|request output|pipeline registry)\b/
)
) {
add("core");
}
if (
has(
/\bfrontend\b|\bapi\b|\bserver\b|\bcli\b|\bopenai\b|\bcomfyui\b|\bchat\/completions\b|\baudio\/(?:generate|speech)\b|\bimages\/generations\b/
)
) {
add("frontend");
}
const newModel =
has(/\bnew model\b|\[new model\]/) ||
(!bug && has(/\[model\]/) && has(/\badd\b|\binitial\b|\bintegration\b|\bport\b/)) ||
has(/\badd .* model support\b|\binitial .* model integration\b|\bport .* model\b/);
if (newModel) add("new model");
if (has(/\bnpu\b|\bascend\b/)) add("NPU");
if (has(/\brocm\b|\bamd\b|\bgfx\d{3,4}\b|\bmi\d{2,4}[a-z]*\b/)) add("ROCm");
if (has(/\bxpu\b|\bintel\b/)) add("xpu");
if (has(/\bhardware plugin\b/)) add("Hardware Plugin");
if (has(/\bvla\b|\binternvla\b|\bopenpi\b|\bpi0(?:\.5)?\b|\bgr00t\b|\balpamayo\b/)) add("VLA");
if (has(/\bworld[- ]model\b|\blingbot[- ]world\b/)) add("world model");
if (has(/\brl\b|\breinforcement\b|\brollout\b/)) add("RL");
if (has(/\brfc\b/)) add("RFC");
const existing = new Set((pr.labels || []).map((label) => label.name));
const labelsToAdd = [...labels].filter((label) => !existing.has(label));
if (labelsToAdd.length === 0) continue;
try {
await github.rest.issues.addLabels({
owner,
repo,
issue_number: pr.number,
labels: labelsToAdd,
});
changed.push({ number: pr.number, labels: labelsToAdd });
} catch (error) {
failures.push({ number: pr.number, message: error.message });
core.warning(`PR #${pr.number}: ${error.message}`);
}
}
console.log(
JSON.stringify({
prsReviewed: prs.length,
prsChanged: changed.length,
labelsAdded: changed.reduce((count, pr) => count + pr.labels.length, 0),
failures,
})
);
+9 -8
View File
@@ -126,11 +126,12 @@ Through five levels (L1-L5) and common (Common) specifications, the system clari
<strong>Interface tests:</strong><br>
/tests/entrypoints/test_xxx<br>
<strong>Performance:</strong><br>
/tests/dfx/perf/tests/test_qwen_omni.json (Omni), test_tts.json (TTS),<br>
and /tests/dfx/perf/tests/test_{diffusion_model}_vllm_omni.json (Diffusion)<br>
/tests/dfx/perf/tests/test_qwen3_omni_*.json (Omni), test_tts.json (TTS),<br>
test_voxcpm2.json, test_higgs_audio_v3.json, and<br>
/tests/dfx/perf/tests/test_{diffusion_model}_vllm_omni.json (Diffusion)<br>
<strong>Doc Test:</strong><br>
tests/example/online_serving/test_{model_name}.py<br>
tests/example/offline_inference/test_{model_name}.py<br>
tests/examples/online_serving/test_{model_name}.py<br>
tests/examples/offline_inference/test_{model_name}.py<br>
<strong>Accuracy Test:</strong><br>
/tests/e2e/accuracy/test_{model_name}.py
</td>
@@ -708,16 +709,16 @@ L4 level testing is a comprehensive quality audit before a version release. It e
### 3.2 Testing Content and Scope
- ***Full Functionality Testing***: Executes all test cases defined in `test_{model_name}_expansion.py`, covering all implemented features, positive flows, boundary conditions, and exception handling.
- ***Performance Testing***: Uses `tests/dfx/perf/tests/test_qwen_omni.json`, `tests/dfx/perf/tests/test_tts.json`, and diffusion configs in the form `tests/dfx/perf/tests/test_*_vllm_omni.json` (passed to `run_benchmark.py` via `--test-config-file`) to drive performance testing tools for stress, load, and endurance tests, collecting metrics like throughput, response time, and resource utilization.
- ***Performance Testing***: Uses `tests/dfx/perf/tests/test_qwen3_omni_*.json` (Omni), `test_tts.json` / `test_voxcpm2.json` / `test_higgs_audio_v3.json` (TTS), and diffusion configs `tests/dfx/perf/tests/test_*_vllm_omni.json` (passed to `run_benchmark.py` or `run_diffusion_benchmark.py` via `--test-config-file` in nightly **Perf Test** steps) to drive throughput, latency, and memory benchmarks. Each JSON **case** may declare an optional top-level **`mark`** array: exactly one ``hardware_marks`` object plus pytest marker name strings (`full_model`, `omni` / `tts` / `diffusion`, …). Runners attach those marks to each parametrized `(server, benchmark index)` pair so **local** bulk runs can filter with `-m` (for example `-m "full_model and H100 and diffusion"`). Nightly CI perf jobs select workloads by **`--test-config-file`**, not `-m`. Details are in the Performance Tests example below.
- ***Documentation Testing***: Verifies whether the example code provided to users is runnable and its results match the description.
### 3.3 Test Directory and Execution Files
- ***Functional Testing***: Same directories as L3.
- ***Performance Test Configuration***: `tests/dfx/perf/tests/test_qwen_omni.json`, `tests/dfx/perf/tests/test_tts.json`, and diffusion configs `tests/dfx/perf/tests/test_*_vllm_omni.json` (e.g. `test_qwen_image_vllm_omni.json`)
- ***Performance Test Configuration***: `tests/dfx/perf/tests/test_qwen3_omni_*.json`, `test_tts.json`, `test_voxcpm2.json`, `test_higgs_audio_v3.json`, and diffusion configs `tests/dfx/perf/tests/test_*_vllm_omni.json` (e.g. `test_qwen_image_vllm_omni.json`, `test_cosmos3_vllm_omni.json`). Optional per-case `mark` for local `-m` filtering is documented in Section 3.4 Performance Tests.
- ***Documentation Example Tests***:
- - `tests/example/online_serving/test_{model_name}.py`
- `tests/example/offline_inference/test_{model_name}.py`
- `tests/examples/online_serving/test_{model_name}.py`
- `tests/examples/offline_inference/test_{model_name}.py`
### 3.4 Execution Method and Example
@@ -1,14 +1,41 @@
When you want to add L4-level ***performance test*** cases, you can refer to the following format for case addition in `tests/dfx/perf/tests/test_qwen_omni.json`, `tests/dfx/perf/tests/test_tts.json`, or diffusion configs such as `tests/dfx/perf/tests/test_*_vllm_omni.json` (selected via `pytest ... run_benchmark.py --test-config-file <path>`):
When you want to add L4-level ***performance test*** cases, add entries to JSON files under `tests/dfx/perf/tests/` and run them via `tests/dfx/perf/scripts/run_benchmark.py` (omni / TTS) or `run_diffusion_benchmark.py` (diffusion).
**Config file layout (in-tree examples)**
| Model type | Runner | Example JSON files |
| ---------- | ------ | ------------------ |
| Omni | `run_benchmark.py` | `test_qwen3_omni_no_async_chunk.json`, `test_qwen3_omni_async_chunk.json`, `test_qwen3_omni_vllm_text.json`, `test_qwen3_omni_multi_replicas.json` |
| 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`, … |
**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).
Diffusion cases are detected when the JSON has `server_type` (typically `"vllm-omni"`) or `"diffusion"` in the `mark` array. Omni / TTS JSON has neither.
**Running perf cases**
Pass **`--test-config-file`** to run one JSON file as-is, or omit it for the bulk scan above and filter with pytest **`-m`** on each case's JSON `mark` (for example `pytest … run_benchmark.py -m "full_model and tts and H100"`).
```JSON
{
"test_name": "test_qwen3_omni",
"test_name": "test_qwen3_omni_async_chunk",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}},
"full_model",
"omni"
],
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"stage_config_name": "qwen3_omni.yaml"
},
"benchmark_params": [
{
"name": "random_10p",
"dataset_name": "random",
"num_prompts": [10, 20],
"max_concurrency": [1, 4],
@@ -33,8 +60,95 @@ When you want to add L4-level ***performance test*** cases, you can refer to the
| Field | Required | Description |
| ---------------- | -------- | --------------------------------------------------------------- |
| test_name | Yes | Unique identifier for the test case |
| mark | No | Pytest marks for this case (see **`mark` field** below). Omit only for configs not meant to be filtered by `-m`. |
| server_params | Yes | Server-side configuration parameters |
| benchmark_params | Yes | Benchmark running parameters (supports multiple configurations) |
| server_type | Diffusion only | When set (e.g. `"vllm-omni"`), routes the case to `run_diffusion_benchmark.py`. |
| benchmark_endpoint | Diffusion only | API path for the benchmark client (e.g. `/v1/videos`, `/v1/images/generations`). |
**`mark` field**
Optional top-level field on each perf JSON **case object** (one per `test_name`). `run_benchmark.py` and `run_diffusion_benchmark.py` read it via `tests.dfx.conftest.resolve_pytest_marks` and attach the marks to the corresponding `pytest.param`, so you can filter locally with `-m`.
When `mark` is present, it must be an **array** with exactly one ``hardware_marks`` object (same shape as `@hardware_test` / `hardware_marks()` in `tests/helpers/mark.py`), followed by registered pytest marker name strings such as `full_model`, `omni`, `tts`, `diffusion`, or `local_model`.
Supported form:
| Form | Example | Effect |
| ---- | ------- | ------ |
| Array (single platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}}, "full_model", "diffusion"]` | Hardware marks from `hardware_marks(...)`; `num_cards` > 1 adds `distributed_*` (+ CUDA `skipif_cuda` when applicable). String entries become extra pytest marks. |
| Array (multi-platform) | `"mark": [{"hardware_marks": {"res": {"cuda": "H100", "rocm": "MI325", "npu": "A2"}, "num_cards": {"cuda": 2, "rocm": 2, "npu": 1}}}, "full_model", "omni"]` | Same as above, but declares **multiple platforms** in one case. Filter examples: `-m "full_model and H100 and cuda"`, `-m "full_model and MI325 and rocm"`. |
Recommended for L4 perf cases:
```JSON
{
"test_name": "test_bagel_single_device_single_stage_t2i",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}},
"full_model",
"diffusion"
],
"server_type": "vllm-omni",
"server_params": { "...": "..." },
"benchmark_params": [ { "name": "1024x1024_steps20", "...": "..." } ]
}
```
Multi-GPU diffusion (example: Cosmos3 with `cfg-parallel-size=2`):
```JSON
{
"test_name": "test_cosmos3_t2i_official_demo_2gpu",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}},
"full_model",
"diffusion"
],
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/generations",
"server_params": { "...": "..." },
"benchmark_params": [ { "name": "1024x1024_steps4", "...": "..." } ]
}
```
- Omni perf: include `"omni"` in the `mark` array
- TTS perf: include `"tts"` in the `mark` array
- Diffusion perf: include `"diffusion"` in the `mark` array
- HunyuanImage local-weight cases: add `"local_model"` to the `mark` array
**Parametrization IDs**
Each `(server, benchmark index)` pair becomes one `pytest.param` with id `{test_name}-{suffix}`. The suffix comes from `benchmark_params[].name` when set (for example `test_tts-p0`, `test_omni-p1`); otherwise it is derived from fields like `task` / `eval_phase`.
**Benchmark result filenames**
Result files use the **runtime** hardware label from `get_runtime_resource_label()` (detected GPU/NPU name on the machine that ran the job), **not** `mark.hardware_marks.res`. On the default H100 CI pool, `H100` is **omitted** from filenames (`resource_label_for_filename`).
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)
**Local commands**
```bash
# Bulk load + filter by JSON mark
pytest -s -v tests/dfx/perf/scripts/run_diffusion_benchmark.py -m "full_model and H100 and diffusion"
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py -m "full_model and omni and H100"
# Single file (same as nightly 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_qwen3_omni_async_chunk.json
# Optional baseline assertion (default off)
pytest -sv tests/dfx/perf/scripts/run_diffusion_benchmark.py --assert-baseline \
--test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
```
See also [Markers for Tests](./tests_markers.md) for registered hardware markers (`H100`, `L4`, `cuda`, `distributed_cuda`, …).
**`server_params` Configuration**
@@ -79,10 +193,12 @@ You can add any benchmark running parameters you need here. For all optional par
1. Change the --xxx-xx-xx running parameters to xxx_xx_xx format and fill them as keys in the JSON file.
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. The qps and concurrency modes are recommended to be mutually exclusive. For detailed explanations, see the table below:
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:
| Parameter | Type | Required | Example/Values | Description |
| --------------- | ----------- | -------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| name | string | No | `"1024x1024_steps4"` | Stable suffix for pytest param id and logs. |
| num_prompts | int / array | Yes | 10,[10, 20, 30] | Number of requests. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of qps or max_concurrency, e.g., [10,10,10]. If an array is used, its length must match the number of qps or max_concurrency. |
| request_rate | float / array | No | 0.5, [0.5, 1, inf] | Queries per second. Supports single values or arrays. If a single value is used, it will be automatically expanded to match the number of num_prompts, e.g., [1,1,1]. If an array is used, its length must match the number of num_prompts. |
| max_concurrency | int / array | No | 1, [1, 2, 3] | Maximum concurrent in-flight requests. Same array / expansion rules as `request_rate` (mutually exclusive with QPS mode). |
+10 -4
View File
@@ -149,12 +149,18 @@ Failed jobs: 1/2
pytest -s -v -m "full_model and distributed_cuda and L4" --run-level=full_model
pytest -s -v -m "full_model and (omni or tts) and H100" --run-level=full_model
```
To run a standalone perf benchmark (defaults to ``test_qwen_omni.json``; use ``--test-config-file tests/dfx/perf/tests/test_tts.json`` for TTS):
If you only want to run specific test cases on a particular platform, you can use:
```bash
pytest -s -v tests/dfx/perf/scripts/run_benchmark.py
pytest -s -v -m "full_model and distributed_cuda and L4" --run-level=full_model
```
Note: ``run_benchmark.py`` and ``run_diffusion_benchmark.py`` accept an optional ``--test-config-file``. If omitted, each loads every ``*.json`` under ``tests/dfx/perf/tests/`` (omni/tts vs diffusion split by ``is_diffusion_perf_config``) and pytest ``-m`` filters by each case's JSON ``mark``:
```bash
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
```
Nightly **Perf Test** jobs in ``test-nightly.yml`` use ``--test-config-file`` only (no ``-m``). E2e L4 function tests still use ``full_model`` + ``--run-level full_model`` (see [test-nightly.yml](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-nightly.yml)). Example:
=== "L5 level"
+29 -2
View File
@@ -33,7 +33,8 @@ Defined in `pyproject.toml`:
| `skipif_rocm` | Skip if the num of ROCm cards is less than the required * |
| `skipif_npu` | Skip if the num of NPU cards is less than the required * |
| `slow` | Slow tests (may skip in quick CI) |
| `benchmark` | Benchmark tests |
| `benchmark` | Benchmark tests (decorator on runner test functions; perf JSON uses `full_model` + type marker instead) |
| `local_model` | Tests requiring local / non-HF-hub model weights |
\* Means those markers are auto-added by `@hardware_test` (parametrization decorator) or `hardware_marks` (only returning the list of marks for flexibility).
@@ -94,7 +95,7 @@ This decorator is intended to make hardware-aware, cross-platform test authoring
num_cards=2,
)
```
- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), NPU (A2/A3)
- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), XPU (B60), MUSA (S5000), NPU (A2/A3)
- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
- Filtering examples:
@@ -120,6 +121,32 @@ def test_feature(omni_server):
...
```
### JSON `mark` field (L4 perf configs)
Perf JSON under `tests/dfx/perf/tests/` can attach pytest marks per **case** (`test_name` block). Parsed by `tests.dfx.conftest.resolve_pytest_marks` and applied to each parametrized run in `run_benchmark.py` / `run_diffusion_benchmark.py`.
When `mark` is present, it must be an **array** with exactly one ``hardware_marks`` object (same semantics as `hardware_marks()` above), followed by registered pytest marker name strings such as `full_model`, `omni`, `tts`, or `diffusion`.
```json
{
"test_name": "test_cosmos3_t2i_official_demo_2gpu",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}},
"full_model",
"diffusion"
],
"server_type": "vllm-omni",
"server_params": { "...": "..." },
"benchmark_params": [{ "name": "1024x1024_steps4", "...": "..." }]
}
```
- Local bulk load: `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
See [L4 performance test examples](./test_examples/l4_performance_tests.inc.md) for the full schema.
## Add Support for a New Platform
If you want to add support for a new platform (e.g., "tpu" for a new accelerator), follow these steps:
@@ -0,0 +1,91 @@
# Replica Data Parallelism for Video Generation
A runnable recipe + benchmark for **replica data parallelism** (replica-DP) on a
video DiT. Replica-DP runs `N` independent diffusion engine replicas, one per GPU,
and routes each request to a single replica. It scales **request throughput**
near-linearly; it does **not** change single-request latency (for that, use an
intra-request axis such as tensor/sequence/CFG parallelism).
This example uses `Wan-AI/Wan2.2-TI2V-5B-Diffusers` with one GPU per replica.
## Files
| File | Purpose |
|------|---------|
| `wan2_2_ti2v_dp.yaml` | Diffusion stage config; `runtime.num_replicas` + `runtime.devices` drive the fan-out. |
| `run_server.sh` | Substitutes `NUM_REPLICAS` / `DEVICES` into the config and serves. |
| `bench_replica_dp.py` | Replica-agnostic load driver; reports throughput, latency, and a per-input isolation check. |
## Run
Start the server with the replica count you want (one GPU per replica):
```bash
# baseline
NUM_REPLICAS=1 DEVICES=0 ./run_server.sh
# 2 replicas
NUM_REPLICAS=2 DEVICES=0,1 ./run_server.sh
# 4 replicas
NUM_REPLICAS=4 DEVICES=0,1,2,3 ./run_server.sh
```
In another shell, drive a fixed workload and read the throughput:
```bash
python bench_replica_dp.py --url http://127.0.0.1:8098 \
--num-requests 28 --concurrency 8 --label "replicas=4"
```
Re-run the server at `NUM_REPLICAS=1,2,4` (same client workload each time) and
compare `videos/min`. Pass `--baseline-tpm <N=1 value>` to print a scaling factor.
### Isolation check
Identical outputs under a *shared* prompt+seed prove nothing -- every request
would be identical regardless of isolation. Instead each request gets a distinct
input (a prompt from the pool + a unique seed `base_seed+i`), and we check that a
request's output is unchanged whether it runs alone or concurrently amid other
distinct requests. Capture a per-input baseline on `N=1`, then verify it under
`N>=2` concurrency:
```bash
# 1) against NUM_REPLICAS=1 (serial), record each input's output hash
python bench_replica_dp.py --num-requests 8 --concurrency 1 \
--write-baseline baseline.json
# 2) against NUM_REPLICAS>=2, replay the SAME inputs concurrently and verify
python bench_replica_dp.py --num-requests 8 --concurrency 8 \
--check-baseline baseline.json
```
Step 2 exits non-zero if any request's output differs from its own baseline (a
mismatch means cross-request state bleed or misrouting). Keep `--num-requests`,
`--base-seed`, and the prompt source identical between the two runs so the input
sets match.
## Measured scaling
Wan2.2-TI2V-5B (832×480, 33 frames, 30 steps), 4× A800-80GB (NVLink), one GPU per replica:
| Replicas | Throughput (videos/min) | Scaling | Efficiency |
|----------|-------------------------|---------|------------|
| 1 | 4.71 | 1.00× | — |
| 2 | 9.20 | 1.95× | 98% |
| 4 | 18.02 | 3.83× | 96% |
Per-request latency stayed flat (~13–14 s) across all replica counts. Isolation
is checked per-input via the baseline/verify protocol above: each request's
output under `N>=2` concurrency must match its own single-replica baseline.
## Notes on the config surface
- `runtime.num_replicas: N` fans out `N` replicas; `runtime.devices` assigns their
GPUs. With `tensor_parallel_size = 1`, list one GPU per replica
(`num_replicas * tensor_parallel_size` entries, pool mode).
- Replica fan-out here comes from the stage config, not a CLI flag. (The headless /
multi-runtime launch path uses the process-local `--omni-dp-size-local`, which
requires `--stage-id`.)
- This recipe serves via `--stage-configs-path`. Expressing replica-DP under the
newer `--deploy-config` schema is part of the ongoing serving-config alignment
(see the diffusion parallelism guide and the `#4707` discussion); `num_replicas`
is accepted by both paths.
@@ -0,0 +1,241 @@
#!/usr/bin/env python3
"""Throughput-scaling + isolation benchmark for diffusion replica data parallelism.
Sends `--num-requests` text-to-video requests (up to `--concurrency` in flight)
to a running vLLM-Omni video server via `POST /v1/videos/sync` (blocks until the
clip is produced) and reports:
completed / failed, makespan (wall clock), throughput (videos/min),
per-request latency p50/p90/max (nearest-rank), and -- when a baseline
manifest is supplied -- a per-request isolation check.
Each request gets a *distinct* input (a prompt from the pool + a unique seed),
so an output is a deterministic function of its own input and nothing else.
Measuring replica scaling
-------------------------
This client is replica-agnostic. Vary the *server's* replica count
(`runtime.num_replicas` in the stage config; see run_server.sh) and keep the
client workload fixed, then compare throughput:
N=1 -> baseline
N=2 -> expect ~2x
N=4 -> expect ~4x (near-linear is the goal)
Pass the single-replica videos/min via `--baseline-tpm` to print a scaling
factor for the current run.
Isolation / correctness
-----------------------
Byte-identical outputs under a *shared* prompt+seed prove nothing -- every
request would be identical regardless of isolation. Instead, give each request a
distinct input and check that its output is unchanged whether it runs alone or
concurrently amid other distinct requests:
1. Against an N=1 server, capture a per-input baseline:
bench_replica_dp.py --num-requests 8 --concurrency 1 \
--write-baseline baseline.json
2. Against an N>=2 server, replay the *same* inputs concurrently and verify
each output matches its own baseline:
bench_replica_dp.py --num-requests 8 --concurrency 8 \
--check-baseline baseline.json
A mismatch means a request's output changed under concurrency -- cross-request
state bleed or misrouting -- and the run exits non-zero.
Requires: requests (`pip install requests`).
"""
import argparse
import concurrent.futures as cf
import hashlib
import json
import math
import sys
import time
from pathlib import Path
import requests
# Distinct prompts, cycled across requests; combined with a per-request unique
# seed this makes every request a distinct input (see input_key()).
DEFAULT_PROMPTS = [
"A cat playing piano, cinematic, high detail",
"A drone shot over a snowy mountain range at sunrise",
"A red sports car driving through a neon-lit city at night",
"Ocean waves crashing on a rocky shore, slow motion",
"A hot air balloon floating over green rolling hills",
"Timelapse of clouds moving over a desert canyon at dusk",
"A steam train crossing a stone viaduct in autumn",
"A jellyfish drifting through deep blue water, backlit",
]
def input_key(prompt, seed, args):
"""Stable identity of a request's input; matches baseline<->verify by content, not order."""
return "\x1f".join([prompt, str(seed), args.size, str(args.num_frames), str(args.steps)])
def nearest_rank(xs, q):
"""Nearest-rank percentile; q in [0, 1]. Correct for tiny samples (unlike int(q*n)-1)."""
if not xs:
return float("nan")
s = sorted(xs)
rank = max(1, math.ceil(q * len(s)))
return s[rank - 1]
def one_request(args, spec, idx, save):
"""Fire one /v1/videos/sync request; return (ok, latency, nbytes, md5, key, err)."""
prompt, seed = spec["prompt"], spec["seed"]
key = input_key(prompt, seed, args)
# multipart form fields match POST /v1/videos/sync in the omni API server
form = {
"prompt": (None, prompt),
"size": (None, args.size), # e.g. "832x480"
"num_frames": (None, str(args.num_frames)),
"num_inference_steps": (None, str(args.steps)),
"seed": (None, str(seed)), # per-request unique -> distinct, reproducible input
}
if args.model:
form["model"] = (None, args.model)
if args.negative_prompt:
form["negative_prompt"] = (None, args.negative_prompt)
t0 = time.perf_counter()
try:
r = requests.post(f"{args.url}/v1/videos/sync", files=form, timeout=args.timeout)
dt = time.perf_counter() - t0
if r.status_code != 200:
return (False, dt, 0, "", key, f"HTTP {r.status_code}: {r.text[:160]}")
body = r.content
md5 = hashlib.md5(body).hexdigest()
if save and args.save_dir:
Path(args.save_dir).mkdir(parents=True, exist_ok=True)
(Path(args.save_dir) / f"req{idx:03d}_seed{seed}.mp4").write_bytes(body)
return (True, dt, len(body), md5, key, "")
except Exception as e: # noqa: BLE001 - report any client-side failure
return (False, time.perf_counter() - t0, 0, "", key, repr(e))
def build_specs(args):
"""Deterministic list of distinct (prompt, seed) inputs; identical across baseline/verify runs."""
prompts = DEFAULT_PROMPTS
if args.prompts_file:
prompts = [ln.strip() for ln in Path(args.prompts_file).read_text().splitlines() if ln.strip()]
return [{"prompt": prompts[i % len(prompts)], "seed": args.base_seed + i} for i in range(args.num_requests)]
def run_batch(args, specs, save):
"""Fire specs concurrently; return (results, makespan). results = list of one_request tuples."""
results = []
wall0 = time.perf_counter()
with cf.ThreadPoolExecutor(max_workers=args.concurrency) as ex:
futs = {ex.submit(one_request, args, spec, i, save): i for i, spec in enumerate(specs)}
for done, f in enumerate(cf.as_completed(futs), 1):
res = f.result()
results.append(res)
good, dt, nbytes, _md5, _key, err = res
print(f" [{done}/{len(specs)}] {'ok' if good else 'FAIL'} {dt:6.1f}s {(nbytes / 1e6):5.1f}MB {err}")
return results, time.perf_counter() - wall0
def main():
ap = argparse.ArgumentParser(description="Diffusion replica-DP throughput + isolation benchmark")
ap.add_argument("--url", default="http://127.0.0.1:8098", help="server base URL (no path)")
ap.add_argument("--num-requests", type=int, default=16, help="total requests to send")
ap.add_argument("--concurrency", type=int, default=4, help="max in-flight (set >= replica count)")
ap.add_argument("--warmup", type=int, default=-1, help="warmup requests before timing (default: =concurrency)")
ap.add_argument("--base-seed", type=int, default=42, help="request i uses seed base_seed+i (distinct inputs)")
ap.add_argument("--prompts-file", default="", help="optional file, one prompt per line (else a built-in pool)")
ap.add_argument("--negative-prompt", default="")
ap.add_argument("--size", default="832x480", help="WIDTHxHEIGHT")
ap.add_argument("--num-frames", type=int, default=33)
ap.add_argument("--steps", type=int, default=30, help="num_inference_steps")
ap.add_argument("--model", default="", help="optional; defaults to the served model")
ap.add_argument("--timeout", type=float, default=1800, help="per-request timeout (s)")
ap.add_argument("--save-dir", default="", help="save returned clips")
ap.add_argument("--label", default="", help="run label, e.g. 'replicas=2'")
ap.add_argument(
"--baseline-tpm", type=float, default=0.0, help="single-replica videos/min, to print a scaling factor"
)
ap.add_argument("--write-baseline", default="", help="run against N=1: write per-input md5 manifest to this path")
ap.add_argument("--check-baseline", default="", help="run against N>=2: verify each output matches this manifest")
args = ap.parse_args()
if args.write_baseline and args.check_baseline:
ap.error("--write-baseline and --check-baseline are mutually exclusive")
specs = build_specs(args)
warmup = args.concurrency if args.warmup < 0 else args.warmup
print(
f"-> {args.url}/v1/videos/sync | {args.num_requests} reqs "
f"| concurrency {args.concurrency} | warmup {warmup} | {args.size} {args.num_frames}f {args.steps}steps "
f"| {args.label}"
)
# Warmup: absorb each replica's first-call compile/init cost so it does not
# skew the measured makespan. Results are discarded and never saved.
if warmup > 0:
print(f"warming up ({warmup} reqs, not timed)...")
run_batch(args, [specs[i % len(specs)] for i in range(warmup)], save=False)
results, makespan = run_batch(args, specs, save=True)
ok = sum(1 for r in results if r[0])
fail = len(results) - ok
lat = [r[1] for r in results if r[0]]
errs = [r[5] for r in results if not r[0]]
got = {r[4]: r[3] for r in results if r[0]} # input_key -> md5
print(f"\n===== result {args.label} =====")
print(f"completed {ok} / failed {fail} | makespan {makespan:.1f}s")
if ok:
tpm = ok / makespan * 60.0
print(f"throughput: {tpm:.2f} videos/min ({ok / makespan:.4f} videos/s)")
print(
f"latency (s): p50={nearest_rank(lat, 0.50):.1f} "
f"p90={nearest_rank(lat, 0.90):.1f} "
f"min={min(lat):.1f} max={max(lat):.1f}"
)
if args.baseline_tpm > 0:
print(f"scaling vs baseline ({args.baseline_tpm:.2f}/min): {tpm / args.baseline_tpm:.2f}x")
if errs:
print("sample failures:", errs[:3])
# Isolation check: compare each request's output against its own single-replica baseline.
isolation_failed = False
if args.write_baseline:
Path(args.write_baseline).write_text(json.dumps(got, indent=2))
print(f"baseline: wrote {len(got)} per-input hashes -> {args.write_baseline}")
elif args.check_baseline:
baseline = json.loads(Path(args.check_baseline).read_text())
matched = mismatched = unknown = 0
for key, md5 in got.items():
if key not in baseline:
unknown += 1
print(f" isolation: NO BASELINE for input {key!r} (not in the baseline manifest)")
elif baseline[key] == md5:
matched += 1
else:
mismatched += 1
print(f" isolation: MISMATCH for input {key!r} (output changed under concurrency)")
# PASS requires the run to cover the *whole* baseline: a subset (fewer requests, or
# some failed) must not pass just because the inputs it did run happened to match.
unverified = sorted(set(baseline) - set(got))
for key in unverified:
print(f" isolation: NOT VERIFIED for baseline input {key!r} (absent from this run)")
isolation_failed = mismatched > 0 or unknown > 0 or len(unverified) > 0
verdict = "PASS (isolated)" if not isolation_failed else "FAIL"
print(
f"isolation: {matched} matched / {mismatched} mismatched / {unknown} unknown / "
f"{len(unverified)} unverified vs baseline({len(baseline)}) -> {verdict}"
)
if fail > 0 or ok == 0 or isolation_failed:
sys.exit(1)
if __name__ == "__main__":
main()
@@ -0,0 +1,33 @@
#!/usr/bin/env bash
# Serve N independent Wan2.2 TI2V-5B replicas (replica data parallelism).
#
# Usage:
# NUM_REPLICAS=1 DEVICES=0 ./run_server.sh # baseline
# NUM_REPLICAS=2 DEVICES=0,1 ./run_server.sh
# NUM_REPLICAS=4 DEVICES=0,1,2,3 ./run_server.sh
#
# Then drive load with bench_replica_dp.py (see README.md) and compare
# throughput across replica counts.
set -euo pipefail
MODEL="${MODEL:-Wan-AI/Wan2.2-TI2V-5B-Diffusers}"
HOST="${HOST:-127.0.0.1}"
PORT="${PORT:-8098}"
NUM_REPLICAS="${NUM_REPLICAS:-1}"
DEVICES="${DEVICES:-0}" # one GPU per replica for tensor_parallel_size=1
HERE="$(cd "$(dirname "$0")" && pwd)"
TEMPLATE="$HERE/wan2_2_ti2v_dp.yaml"
CFG="$(mktemp --suffix=_wan2_2_dp.yaml)"
trap 'rm -f "$CFG"' EXIT
sed -e "s/__NUM_REPLICAS__/${NUM_REPLICAS}/" \
-e "s/__DEVICES__/${DEVICES}/" \
"$TEMPLATE" > "$CFG"
echo "Serving ${MODEL}: num_replicas=${NUM_REPLICAS} devices=${DEVICES} (config ${CFG})"
vllm serve "$MODEL" --omni \
--host "$HOST" \
--port "$PORT" \
--stage-configs-path "$CFG"
@@ -0,0 +1,43 @@
# Replica data-parallel recipe for Wan2.2 TI2V-5B (video DiT).
#
# Fans out `num_replicas` independent DiffusionEngine replicas, one per GPU,
# and lets the serving layer route each request to a single replica. This
# scales request throughput near-linearly; it does not change single-request
# latency. See ./README.md for measured numbers and the `run_server.sh` driver.
#
# run_server.sh substitutes __NUM_REPLICAS__ / __DEVICES__ from env before serving.
# For tensor_parallel_size = 1, `devices` lists one GPU per replica (pool mode):
# num_replicas=4 -> devices "0,1,2,3".
stage_args:
- stage_id: 0
stage_type: diffusion
runtime:
num_replicas: __NUM_REPLICAS__
devices: "__DEVICES__"
max_batch_size: 1
engine_args:
model_stage: dit
# `model_class_name` takes the diffusion registry KEY (WanPipeline), which
# resolves to the Wan22Pipeline class. Wan2.2 TI2V-5B is served by the base
# WanPipeline, which auto-detects TI2V-5B mode from model_index.json.
model_class_name: WanPipeline
max_num_seqs: 1
enforce_eager: true
trust_remote_code: true
distributed_executor_backend: "mp"
vae_use_tiling: true
parallel_config:
tensor_parallel_size: 1
final_output: true
final_output_type: video
is_comprehension: false
default_sampling_params:
seed: 42
runtime:
enabled: true
defaults:
window_size: -1
max_inflight: 1
+2 -2
View File
@@ -148,7 +148,7 @@ Do not copy latency or VRAM numbers between commits or machines. Record them
only after running a latest-head sweep on the target hardware.
For formal PR benchmarking, reuse
`tests/dfx/perf/tests/test_ltx2_3_vllm_omni.json` with
`tests/dfx/perf/scripts/run_diffusion_benchmark.py`. That config captures the
`tests/dfx/perf/tests/test_ltx2_vllm_omni.json` with
`tests/dfx/perf/scripts/run_diffusion_benchmark.py`. Each case in that file includes a JSON **`mark`** (`hardware_marks` + `full_model` + `diffusion`) for local `-m` filtering. That config captures the
single-device eager baseline and CFG-parallel=2 case for a small 384x512,
25-frame, 20-step workload.
+351 -1
View File
@@ -11,11 +11,180 @@ from typing import Any
import pytest
from tests.dfx.reliability.helpers import list_remote_process_pids_by_pattern, post_chat_completions_raw
from tests.helpers.mark import hardware_marks
from tests.helpers.runtime import OmniServerParams
from tests.helpers.stage_config import modify_stage_config
from vllm_omni.platforms import current_omni_platform
def _named_pytest_marks(names: list[str]) -> list[pytest.MarkDecorator]:
marks: list[pytest.MarkDecorator] = []
for name in names:
name = name.strip()
if not name:
raise ValueError("mark name must be a non-empty string")
marks.append(getattr(pytest.mark, name))
return marks
def _hardware_marks_from_dict(hw: Any) -> list[pytest.MarkDecorator]:
if not isinstance(hw, dict):
raise ValueError(f"mark.hardware_marks must be a dict, got {type(hw).__name__}")
res = hw.get("res")
if not isinstance(res, dict):
raise ValueError(f"mark.hardware_marks.res must be a dict, got {type(res).__name__}")
num_cards = hw.get("num_cards", 1)
return list(hardware_marks(res=res, num_cards=num_cards))
def resolve_pytest_marks(mark_field: Any) -> list[pytest.MarkDecorator]:
"""Convert a JSON ``mark`` field into pytest mark decorators.
Supported form (per test-case object in perf/stability JSON)::
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}},
"full_model",
"diffusion"
]
Exactly one ``hardware_marks`` object is required when ``mark`` is present.
``hardware_marks`` delegates to :func:`tests.helpers.mark.hardware_marks`
(same shape as ``@hardware_test``). Additional array entries are registered
pytest marker names (strings).
"""
if mark_field is None:
return []
if isinstance(mark_field, list):
marks: list[pytest.MarkDecorator] = []
hw_seen = False
for item in mark_field:
if isinstance(item, dict) and "hardware_marks" in item:
if hw_seen:
raise ValueError("mark array must contain at most one hardware_marks object")
hw_seen = True
marks.extend(_hardware_marks_from_dict(item["hardware_marks"]))
unknown_keys = set(item) - {"hardware_marks"}
if unknown_keys:
raise ValueError(
f"mark hardware_marks object only allows hardware_marks; unknown keys: {sorted(unknown_keys)}"
)
elif isinstance(item, str):
item = item.strip()
if not item:
raise ValueError("mark name must be a non-empty string")
marks.extend(_named_pytest_marks([item]))
else:
raise ValueError(
f"mark array entries must be hardware_marks objects or marker name strings; got {type(item).__name__}"
)
if not hw_seen:
raise ValueError("mark array must contain a hardware_marks object")
return marks
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"))
def _marks_by_test_name(configs: list[dict[str, Any]]) -> dict[str, list[pytest.MarkDecorator]]:
return {str(cfg["test_name"]): resolve_pytest_marks(cfg.get("mark")) for cfg in configs}
def create_unique_server_pytest_params(
configs: list[dict[str, Any]],
stage_configs_dir: Path,
) -> list[Any]:
"""Like :func:`create_unique_server_params`, but wrap each row in ``pytest.param`` with JSON marks."""
marks_by_name = _marks_by_test_name(configs)
return [
pytest.param(
row,
marks=marks_by_name.get(row[0], []),
id=row[0],
)
for row in create_unique_server_params(configs, stage_configs_dir)
]
def create_benchmark_pytest_params(
benchmark_configs: list[dict[str, Any]],
server_to_benchmark_mapping: dict[str, dict],
) -> list[Any]:
"""Like :func:`create_benchmark_indices`, but wrap each index in ``pytest.param`` with JSON marks."""
marks_by_name = _marks_by_test_name(benchmark_configs)
params: list[Any] = []
seen: set[str] = set()
for config in benchmark_configs:
test_name = config["test_name"]
if test_name in seen:
continue
seen.add(test_name)
params_list = get_benchmark_params_for_server(test_name, server_to_benchmark_mapping)
id_suffixes = _unique_benchmark_param_id_suffixes(params_list)
for idx, id_suffix in enumerate(id_suffixes):
params.append(
pytest.param(
(test_name, idx),
marks=marks_by_name.get(test_name, []),
id=f"{test_name}-{id_suffix}",
)
)
return params
def create_paired_benchmark_pytest_params(
server_entries: list[tuple[Any, str]],
params_by_test_name: dict[str, list[Any]],
marks_by_name: dict[str, list[pytest.MarkDecorator]],
) -> list[Any]:
"""One ``pytest.param`` per ``(server entry, benchmark index)`` pair.
Pass ``server_entry`` and ``(test_name, idx)`` as separate ``pytest.param``
arguments for ``@pytest.mark.parametrize("server_fixture,benchmark_params", ...)``.
"""
pairs: list[Any] = []
for server_entry, test_name in server_entries:
params_list = params_by_test_name.get(test_name, [])
id_suffixes = _unique_benchmark_param_id_suffixes(params_list)
for idx, id_suffix in enumerate(id_suffixes):
pairs.append(
pytest.param(
server_entry,
(test_name, idx),
marks=marks_by_name.get(test_name, []),
id=f"{test_name}-{id_suffix}",
)
)
return pairs
def create_paired_omni_benchmark_pytest_params(
configs: list[dict[str, Any]],
stage_configs_dir: Path,
) -> list[Any]:
"""Paired params for ``run_benchmark.py`` (omni/tts)."""
mapping = create_test_parameter_mapping(configs)
marks_by_name = _marks_by_test_name(configs)
server_entries = [(row, row[0]) for row in create_unique_server_params(configs, stage_configs_dir)]
params_by_test_name = {
test_name: get_benchmark_params_for_server(test_name, mapping) for _, test_name in server_entries
}
return create_paired_benchmark_pytest_params(server_entries, params_by_test_name, marks_by_name)
def load_configs(config_path: str) -> list[dict[str, Any]]:
try:
abs_path = Path(config_path).resolve()
@@ -32,6 +201,29 @@ def load_configs(config_path: str) -> list[dict[str, Any]]:
raise RuntimeError(f"Failed to load configuration file: {str(e)}")
def load_benchmark_configs(
config_path: str | None = None,
*,
config_dir: Path | None = None,
) -> list[dict[str, Any]]:
"""Load one benchmark JSON file, or merge all ``*.json`` under *config_dir*.
When *config_path* is omitted, every ``*.json`` in *config_dir* is loaded and
concatenated. Pytest ``-m`` expressions then select cases via each entry's
``mark`` field (e.g. ``-m tts`` after bulk load).
"""
if config_path is not None:
return load_configs(config_path)
if config_dir is None:
raise ValueError("load_benchmark_configs requires config_path or config_dir")
configs: list[dict[str, Any]] = []
for path in sorted(config_dir.glob("*.json")):
configs.extend(load_configs(str(path)))
if not configs:
raise ValueError(f"No benchmark JSON files found under {config_dir}")
return configs
def modify_stage(default_path: str, updates: dict[str, Any] | None, deletes: dict[str, Any] | None) -> str:
kwargs: dict[str, Any] = {}
if updates is not None:
@@ -293,6 +485,158 @@ def _safe_filename_token(value: Any | None, *, default: str = "na") -> str:
return s if s else default
def _benchmark_param_id_suffix(param: dict[str, Any], *, idx: int) -> str:
"""Derive a readable pytest id suffix from one ``benchmark_params`` entry."""
name = param.get("name")
if isinstance(name, str) and name.strip():
return _safe_filename_token(name.strip())
parts: list[str] = []
for key in ("task", "eval_phase", "dataset_name", "dataset"):
value = param.get(key)
if value is None or value == "":
continue
token = _safe_filename_token(str(value))
if token != "na":
parts.append(token)
if parts:
return "_".join(parts)
return f"case{idx}"
def _unique_benchmark_param_id_suffixes(params_list: list[dict[str, Any]]) -> list[str]:
"""Return unique pytest id suffixes for a server's benchmark param list."""
raw = [_benchmark_param_id_suffix(param, idx=idx) for idx, param in enumerate(params_list)]
seen: dict[str, int] = {}
unique: list[str] = []
for suffix in raw:
count = seen.get(suffix, 0)
seen[suffix] = count + 1
if count == 0:
unique.append(suffix)
else:
unique.append(f"{suffix}_{count}")
return unique
def extract_mark_resource_label(mark_field: Any) -> str:
"""Return a filename-safe hardware label from ``mark.hardware_marks.res`` values.
Example: ``{"cuda": "H100"}`` -> ``"H100"``; multiple platforms join with ``-``.
Prefer :func:`get_runtime_resource_label` for perf result filenames so labels
reflect the machine that actually ran the benchmark.
"""
if isinstance(mark_field, list):
for item in mark_field:
if isinstance(item, dict) and "hardware_marks" in item:
return extract_mark_resource_label(item)
return "na"
if not isinstance(mark_field, dict):
return "na"
hw = mark_field.get("hardware_marks")
if not isinstance(hw, dict):
return "na"
res = hw.get("res")
if not isinstance(res, dict) or not res:
return "na"
labels = [_safe_filename_token(value) for value in res.values()]
return "-".join(labels) if labels else "na"
_KNOWN_RUNTIME_RESOURCE_TOKENS: tuple[str, ...] = (
"H100",
"H800",
"H200",
"H20",
"L40S",
"L40",
"L4",
"A100",
"A800",
"A10G",
"A10",
"A30",
"MI325",
"MI300",
"MI250",
"B60",
"S5000",
"910B4",
"910B",
"910",
"310P",
"A2",
"A3",
)
_RUNTIME_RESOURCE_LABEL: str | None = None
def _normalize_runtime_device_label(raw: str) -> str:
"""Map a platform device name to a short filename-safe resource token."""
if not raw or not str(raw).strip():
return "na"
upper = str(raw).upper()
for token in _KNOWN_RUNTIME_RESOURCE_TOKENS:
if token.upper() in upper:
return _safe_filename_token(token)
compact = re.sub(r"[^a-zA-Z0-9]+", "", str(raw))
for prefix in ("NVIDIA", "AMD", "ASCEND", "HUAWEI"):
if compact.upper().startswith(prefix):
compact = compact[len(prefix) :]
break
return _safe_filename_token(compact[:48]) if compact else "na"
def _read_runtime_device_name(*, device_id: int = 0) -> str | None:
"""Device name from the active Omni platform."""
if current_omni_platform.device_count() <= device_id:
return None
get_name = getattr(current_omni_platform, "get_device_name", None)
if not callable(get_name):
return None
raw = get_name(device_id)
if isinstance(raw, str) and raw.strip():
return raw.strip()
return None
def get_runtime_resource_label(*, device_id: int = 0, refresh: bool = False) -> str:
"""Return a filename-safe hardware label detected on the running machine."""
global _RUNTIME_RESOURCE_LABEL
if not refresh and _RUNTIME_RESOURCE_LABEL is not None:
return _RUNTIME_RESOURCE_LABEL
raw = _read_runtime_device_name(device_id=device_id)
label = _normalize_runtime_device_label(raw) if raw else "na"
if not refresh:
_RUNTIME_RESOURCE_LABEL = label
return label
_FILENAME_OMIT_RESOURCE_LABELS = frozenset({"H100"})
def hardware_json_value(resource_label: str | None) -> str:
"""Hardware token stored in perf result JSON (empty when unknown)."""
token = _safe_filename_token(resource_label)
return "" if token == "na" else token
def resource_label_for_filename(resource_label: str | None) -> str:
"""Hardware token embedded in result filenames (H100 omitted on default CI pool)."""
token = _safe_filename_token(resource_label)
if token in _FILENAME_OMIT_RESOURCE_LABELS:
return ""
return token
def extract_configs_resource_label(configs: list[dict[str, Any]]) -> str:
"""Return runtime hardware label for perf result filenames."""
del configs
return get_runtime_resource_label()
def resolve_baseline_value(
baseline_raw: Any,
*,
@@ -357,6 +701,7 @@ def run_benchmark(
max_concurrency: Any | None = None,
random_input_len: Any | None = None,
random_output_len: Any | None = None,
resource_label: str | None = None,
) -> dict[str, Any]:
"""Run one ``vllm bench serve --omni`` iteration and return parsed metrics.
@@ -367,7 +712,11 @@ def run_benchmark(
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)
result_filename = f"result_{test_name}_{dataset_name}_{flow}_{num_prompt}_in{ri}_out{ro}_{current_dt}.json"
hw = resource_label_for_filename(resource_label)
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"
if "--result-filename" in args:
print(f"The result file will be overwritten by {result_filename}")
command = (
@@ -437,6 +786,7 @@ def run_benchmark(
result["random_input_len"] = random_input_len
if random_output_len is not None:
result["random_output_len"] = random_output_len
result["Hardware"] = hardware_json_value(resource_label)
with open(result_path, "w", encoding="utf-8") as f:
json.dump(result, f, ensure_ascii=False, indent=2)
return result
+26 -23
View File
@@ -7,21 +7,24 @@ from typing import Any
import pytest
from tests.dfx.conftest import (
create_benchmark_indices,
create_paired_omni_benchmark_pytest_params,
create_test_parameter_mapping,
create_unique_server_params,
get_benchmark_params_for_server,
load_configs,
get_runtime_resource_label,
is_diffusion_perf_config,
load_benchmark_configs,
resolve_baseline_value,
run_benchmark,
)
from tests.helpers.runtime import OmniServer
pytestmark = [pytest.mark.full_model]
# Compare metrics to each test JSON ``baseline`` block only when pytest is run with ``--assert-baseline``
# (registered in ``tests/dfx/conftest.py``; default: off). ``run_benchmark`` and ``_resolve_baseline_value`` are
# defined in the same module.
#
# Optional JSON field ``mark`` is applied as pytest marks via
# ``create_paired_omni_benchmark_pytest_params`` (e.g. ``"mark": [{"hardware_marks":
# {"res": {"cuda": "H100"}, "num_cards": 2}}, "full_model", "omni"]``).
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
@@ -40,22 +43,22 @@ def _get_config_file_from_argv() -> str | None:
_PERF_TESTS_DIR = Path(__file__).resolve().parent.parent / "tests"
_DEFAULT_CONFIG_FILE = str(_PERF_TESTS_DIR / "test_qwen_omni.json")
CONFIG_FILE_PATH = _get_config_file_from_argv()
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(
"No --test-config-file in argv, using default: tests/dfx/perf/tests/test_qwen_omni.json "
"(override with e.g. --test-config-file tests/dfx/perf/tests/test_tts.json)"
f"No --test-config-file: loaded {len(BENCHMARK_CONFIGS)} omni/tts 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)"
)
CONFIG_FILE_PATH = _DEFAULT_CONFIG_FILE
BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH)
else:
BENCHMARK_CONFIGS = load_benchmark_configs(CONFIG_FILE_PATH)
DEPLOY_CONFIGS_DIR = Path(__file__).parent.parent / "deploy"
test_params = create_unique_server_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR)
server_to_benchmark_mapping = create_test_parameter_mapping(BENCHMARK_CONFIGS)
paired_benchmark_params = create_paired_omni_benchmark_pytest_params(BENCHMARK_CONFIGS, DEPLOY_CONFIGS_DIR)
_omni_server_lock = threading.Lock()
@@ -91,17 +94,11 @@ def omni_server(request):
print("OmniServer stopped")
benchmark_indices = create_benchmark_indices(BENCHMARK_CONFIGS, server_to_benchmark_mapping)
@pytest.fixture
def benchmark_params(request, omni_server):
"""Benchmark parameters fixture with proper parametrization"""
def benchmark_params(request):
"""Benchmark parameters fixture; paired with ``omni_server`` via parametrization."""
test_name, param_index = request.param
if test_name != omni_server.test_name:
pytest.skip(f"Skipping parameter for {test_name} - current server is {omni_server.test_name}")
all_params = get_benchmark_params_for_server(test_name, server_to_benchmark_mapping)
if not all_params:
@@ -153,8 +150,11 @@ def assert_result(
@pytest.mark.benchmark
@pytest.mark.parametrize("omni_server", test_params, indirect=True)
@pytest.mark.parametrize("benchmark_params", benchmark_indices, indirect=True)
@pytest.mark.parametrize(
"omni_server,benchmark_params",
paired_benchmark_params,
indirect=["omni_server", "benchmark_params"],
)
def test_performance_benchmark(omni_server, benchmark_params, request):
test_name = benchmark_params["test_name"]
params = benchmark_params["params"]
@@ -168,6 +168,7 @@ def test_performance_benchmark(omni_server, benchmark_params, request):
print(f"Benchmark parameters: {benchmark_params}")
assert_baseline = request.config.getoption("--assert-baseline", default=False)
resource_label = get_runtime_resource_label()
def to_list(value, default=None):
if value is None:
@@ -239,6 +240,7 @@ def test_performance_benchmark(omni_server, benchmark_params, request):
max_concurrency=None,
random_input_len=params.get("random_input_len"),
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
)
assert_result(
result,
@@ -264,6 +266,7 @@ def test_performance_benchmark(omni_server, benchmark_params, request):
max_concurrency=concurrency,
random_input_len=params.get("random_input_len"),
random_output_len=params.get("random_output_len"),
resource_label=resource_label,
)
assert_result(
result,
+134 -55
View File
@@ -8,15 +8,21 @@ This runner separates two concepts:
2. ``benchmark_endpoint``: which serving API the benchmark client calls.
Examples: ``/v1/chat/completions`` and ``/v1/videos``.
A config JSON file is REQUIRED via --test-config-file:
A config JSON file may be passed via --test-config-file. If omitted, every ``*.json`` under
``tests/dfx/perf/tests/`` is loaded and pytest ``-m`` filters by each case's ``mark``:
pytest run_diffusion_benchmark.py -m "diffusion"
pytest run_diffusion_benchmark.py --test-config-file tests/dfx/perf/tests/test_qwen_image_vllm_omni.json
Optional: ``--assert-baseline`` compares metrics to the ``baseline`` block in each benchmark entry (default: off).
All benchmark results for a session are consolidated into a single JSON file under
BENCHMARK_RESULT_DIR (override via the DIFFUSION_BENCHMARK_DIR environment variable).
Each entry in the file contains the test metadata (test_name, endpoint, benchmark_params,
timestamp) together with the raw metrics returned by the benchmark script.
Optional JSON field ``mark`` is applied as pytest marks on that case via
``pytest.param`` (e.g. ``"mark": [{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}}, "full_model", "diffusion"]``).
All benchmark results are written under BENCHMARK_RESULT_DIR (override via the
DIFFUSION_BENCHMARK_DIR environment variable). Each source JSON file gets one
aggregated ``diffusion_result_{config_stem}_{hardware}_{timestamp}.json`` (JSON array
of all runs from cases in that file). Bulk load without ``--test-config-file`` uses
the same per-file aggregation; ``-m`` only selects which cases run.
"""
import json
@@ -35,8 +41,15 @@ import psutil
import pytest
from benchmarks.diffusion.backends import endpoint_filename_token, normalize_endpoint
pytestmark = [pytest.mark.diffusion, pytest.mark.full_model, pytest.mark.local_model]
from tests.dfx.conftest import (
create_paired_benchmark_pytest_params,
get_runtime_resource_label,
hardware_json_value,
is_diffusion_perf_config,
resolve_pytest_marks,
resource_label_for_filename,
)
from tests.helpers.runtime import get_open_port
os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
os.environ.setdefault("DIFFUSION_ATTENTION_BACKEND", "FLASH_ATTN")
@@ -126,6 +139,10 @@ _BRANCHPOINT_COMMIT_SHA: str | None = None
DIFFUSION_RESULT_TEMPLATE_PATH = Path(__file__).parent / "diffusion_result_template.json"
_DIFFUSION_SOURCE_CONFIG_KEY = "_source_config_file"
_PERF_TESTS_DIR = Path(__file__).resolve().parent.parent / "tests"
def _get_config_file_from_argv() -> str | None:
"""Read --test-config-file from sys.argv at import time so pytest parametrize can use it.
@@ -142,9 +159,6 @@ def _get_config_file_from_argv() -> str | None:
CONFIG_FILE_PATH = _get_config_file_from_argv()
if CONFIG_FILE_PATH is None:
print("No config file provided, using default config file: tests/dfx/perf/tests/test_qwen_image_vllm_omni.json")
CONFIG_FILE_PATH = "tests/dfx/perf/tests/test_qwen_image_vllm_omni.json"
# ---------------------------------------------------------------------------
# Config loading
@@ -184,28 +198,85 @@ def load_configs(config_path: str) -> list[dict[str, Any]]:
raise RuntimeError(f"Failed to load configuration file: {str(e)}")
BENCHMARK_CONFIGS = load_configs(CONFIG_FILE_PATH)
_config_stem = Path(CONFIG_FILE_PATH).stem # e.g. "test_qwen_image_vllm_omni"
AGGREGATED_RESULT_FILE = BENCHMARK_RESULT_DIR / f"diffusion_result_{_config_stem}_{_SESSION_TIMESTAMP}.json"
def load_diffusion_benchmark_configs(
config_path: str | None = None,
*,
config_dir: Path | None = None,
) -> 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)
source = str(Path(config_path).resolve())
for cfg in configs:
cfg.setdefault(_DIFFUSION_SOURCE_CONFIG_KEY, source)
return configs
if config_dir is None:
raise ValueError("load_diffusion_benchmark_configs requires config_path or config_dir")
configs: list[dict[str, Any]] = []
for path in sorted(config_dir.glob("*.json")):
source = str(path.resolve())
for cfg in load_configs(str(path)):
cfg[_DIFFUSION_SOURCE_CONFIG_KEY] = source
configs.append(cfg)
if not configs:
raise ValueError(f"No benchmark JSON files found under {config_dir}")
return configs
def _append_to_aggregated_file(record: dict[str, Any]) -> None:
"""Thread-safe append of *record* to the session-level aggregated JSON file.
if CONFIG_FILE_PATH is None:
_all_configs = load_diffusion_benchmark_configs(config_dir=_PERF_TESTS_DIR)
BENCHMARK_CONFIGS = [cfg for cfg in _all_configs if is_diffusion_perf_config(cfg)]
print(
f"No --test-config-file: loaded {len(BENCHMARK_CONFIGS)} diffusion case(s) from "
f"{_PERF_TESTS_DIR}/*.json (skipped {len(_all_configs) - len(BENCHMARK_CONFIGS)} omni/tts; "
f"use -m to filter, e.g. -m diffusion)"
)
else:
BENCHMARK_CONFIGS = load_diffusion_benchmark_configs(CONFIG_FILE_PATH)
The file contains a JSON array; each call loads the existing array (or
starts a new one), appends the record, and writes the file back atomically.
"""
_AGGREGATED_RESULT_FILES_BY_SOURCE: dict[str, Path] = {}
def _normalized_source_path(source_file: str) -> str:
return str(Path(source_file).resolve())
def _aggregated_result_file_for_source(source_file: str) -> Path:
"""One session aggregate per source JSON (same naming as single ``--test-config-file``)."""
key = _normalized_source_path(source_file)
if key not in _AGGREGATED_RESULT_FILES_BY_SOURCE:
stem = Path(key).stem
resource = resource_label_for_filename(get_runtime_resource_label())
if resource:
result_name = f"diffusion_result_{stem}_{resource}_{_SESSION_TIMESTAMP}.json"
else:
result_name = f"diffusion_result_{stem}_{_SESSION_TIMESTAMP}.json"
_AGGREGATED_RESULT_FILES_BY_SOURCE[key] = BENCHMARK_RESULT_DIR / result_name
return _AGGREGATED_RESULT_FILES_BY_SOURCE[key]
def _write_result_record(record: dict[str, Any]) -> Path:
"""Append one benchmark record to the aggregate file for its source JSON."""
source_file = record.get("source_file") or CONFIG_FILE_PATH
if not source_file:
raise ValueError("benchmark record missing source_file")
target = _aggregated_result_file_for_source(str(source_file))
with _RESULT_LOCK:
BENCHMARK_RESULT_DIR.mkdir(parents=True, exist_ok=True)
if AGGREGATED_RESULT_FILE.exists():
with open(AGGREGATED_RESULT_FILE, encoding="utf-8") as f:
if target.exists():
with open(target, encoding="utf-8") as f:
records: list[dict] = json.load(f)
else:
records = []
records.append(record)
with open(AGGREGATED_RESULT_FILE, "w", encoding="utf-8") as f:
with open(target, "w", encoding="utf-8") as f:
json.dump(records, f, indent=2, ensure_ascii=False)
return target
def _append_to_aggregated_file(record: dict[str, Any]) -> None:
"""Backward-compatible wrapper; prefer :func:`_write_result_record`."""
_write_result_record(record)
_server_lock = threading.Lock()
@@ -215,14 +286,6 @@ _server_lock = threading.Lock()
# ---------------------------------------------------------------------------
def _get_open_port() -> int:
"""Return an available TCP port on localhost."""
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
return s.getsockname()[1]
def _wait_for_port(host: str, port: int, timeout: int = 1200, proc: subprocess.Popen | None = None) -> None:
"""Block until the given host:port accepts connections or timeout expires.
@@ -312,7 +375,7 @@ class DiffusionServer:
self.model = server_cfg["model"]
self.serve_args = server_cfg["serve_args"]
self.host = "127.0.0.1"
self.port = port if port is not None else _get_open_port()
self.port = port if port is not None else get_open_port(self.host)
self.proc: subprocess.Popen | None = None
self.test_name: str = ""
@@ -482,6 +545,7 @@ def _unique_server_params(configs: list[dict[str, Any]]) -> list[dict[str, Any]]
"serve_args": _build_serve_args(serve_args_dict),
"benchmark_endpoint": cfg.get("benchmark_endpoint", cfg.get("benchmark_backend")),
"server_params": cfg["server_params"],
"mark": cfg.get("mark"),
}
)
return result
@@ -496,19 +560,29 @@ def _test_param_mapping(configs: list[dict[str, Any]]) -> dict[str, list[dict]]:
return mapping
def _make_server(server_cfg: dict[str, Any]) -> DiffusionServer:
"""Factory: return a vLLM-Omni diffusion server instance for the config."""
return DiffusionServer(server_cfg=server_cfg)
def _marks_by_test_name(configs: list[dict[str, Any]]) -> dict[str, list[pytest.MarkDecorator]]:
return {str(cfg["test_name"]): resolve_pytest_marks(cfg.get("mark")) for cfg in configs}
def _paired_diffusion_benchmark_pytest_params(configs: list[dict[str, Any]]) -> list[Any]:
"""Paired params for ``run_diffusion_benchmark.py``; same shape as omni runner."""
test_param_map = _test_param_mapping(configs)
server_entries = [(cfg, cfg["test_name"]) for cfg in _unique_server_params(configs)]
return create_paired_benchmark_pytest_params(server_entries, test_param_map, _marks_by_test_name(configs))
# ---------------------------------------------------------------------------
# Parametrize data
# ---------------------------------------------------------------------------
server_params = _unique_server_params(BENCHMARK_CONFIGS)
test_param_map = _test_param_mapping(BENCHMARK_CONFIGS)
paired_benchmark_params = _paired_diffusion_benchmark_pytest_params(BENCHMARK_CONFIGS)
def _make_server(server_cfg: dict[str, Any]) -> DiffusionServer:
"""Factory: return a vLLM-Omni diffusion server instance for the config."""
return DiffusionServer(server_cfg=server_cfg)
benchmark_indices: list[int] = list(range(max(len(v) for v in test_param_map.values())))
# ---------------------------------------------------------------------------
# Pytest fixtures
@@ -534,16 +608,13 @@ def diffusion_server(request):
@pytest.fixture
def benchmark_params(request, diffusion_server):
"""Yield the benchmark params dict for the current (server, index) pair."""
param_index: int = request.param
test_name = diffusion_server.test_name
def benchmark_params(request):
"""Benchmark params for the paired server/index parametrization."""
test_name, param_index = request.param
params_list = test_param_map.get(test_name, [])
if not params_list:
raise ValueError(f"No benchmark params for test: {test_name}")
if param_index >= len(params_list):
pytest.skip(f"Param index {param_index} out of range for {test_name} (has {len(params_list)} params)")
current = param_index + 1
total = len(params_list)
@@ -575,8 +646,8 @@ def run_benchmark(
The raw metrics are written to a temporary file by the subprocess. After
the run completes the metrics are merged with full metadata (test_name,
endpoint, benchmark_params, timestamp, flat reporting fields) and appended
to the session-wide aggregated JSON file (AGGREGATED_RESULT_FILE). The
temporary file is removed afterwards. Subprocess stdout/stderr are tee'd
to ``diffusion_result_{config_stem}_{hardware}_{timestamp}.json`` for the
source JSON file. The temporary file is removed afterwards. Subprocess stdout/stderr are tee'd
to a .log file under BENCHMARK_RESULT_DIR/logs/; its path is stored in
the record.
"""
@@ -586,7 +657,12 @@ def run_benchmark(
log_dir = BENCHMARK_RESULT_DIR / "logs"
log_dir.mkdir(parents=True, exist_ok=True)
endpoint_label = endpoint_filename_token(endpoint)
log_file = log_dir / f"{test_name}_{endpoint_label}_{timestamp}.log"
resource_label = get_runtime_resource_label()
hw_for_filename = resource_label_for_filename(resource_label)
if hw_for_filename:
log_file = log_dir / f"{test_name}_{hw_for_filename}_{endpoint_label}_{timestamp}.log"
else:
log_file = log_dir / f"{test_name}_{endpoint_label}_{timestamp}.log"
with tempfile.NamedTemporaryFile(mode="w", suffix=".json", prefix="diffusion_bench_tmp_", delete=False) as tmp:
tmp_result_file = Path(tmp.name)
@@ -696,7 +772,7 @@ def run_benchmark(
"Model": model,
"Framework": server_type,
"API Endpoint": endpoint,
"Hardware": "",
"Hardware": hardware_json_value(resource_label),
"Deployment": "",
"Task": params.get("task", "t2i"),
"Dataset": params.get("dataset", "random"),
@@ -728,8 +804,8 @@ def run_benchmark(
"build_url": os.environ.get("BUILDKITE_BUILD_URL", ""),
"source_file": source_file,
}
_append_to_aggregated_file(record)
print(f"\n Result appended to: {AGGREGATED_RESULT_FILE}")
result_path = _write_result_record(record)
print(f"\n Result saved to: {result_path}")
print(f" Log saved to: {log_file}")
return metrics
@@ -932,12 +1008,10 @@ def _iter_sweep_runs(params: dict[str, Any]) -> list[dict[str, Any]]:
# ---------------------------------------------------------------------------
@pytest.mark.benchmark
@pytest.mark.parametrize(
"diffusion_server",
server_params,
ids=[p["test_name"] for p in server_params],
indirect=True,
"diffusion_server,benchmark_params",
paired_benchmark_params,
indirect=["diffusion_server", "benchmark_params"],
)
@pytest.mark.parametrize("benchmark_params", benchmark_indices, indirect=True)
def test_diffusion_performance_benchmark(diffusion_server, benchmark_params, request):
"""Run the diffusion performance benchmark and verify request completion.
@@ -962,7 +1036,10 @@ def test_diffusion_performance_benchmark(diffusion_server, benchmark_params, req
test_name=test_name,
endpoint=endpoint,
server_cfg=server_cfg,
source_file=cast(str, CONFIG_FILE_PATH),
source_file=server_cfg.get(
_DIFFUSION_SOURCE_CONFIG_KEY,
CONFIG_FILE_PATH or f"{_PERF_TESTS_DIR}/*.json",
),
)
print(f"\n{'=' * 60}")
@@ -980,7 +1057,9 @@ def test_diffusion_performance_benchmark(diffusion_server, benchmark_params, req
if key in result:
print(f" {key}: {result[key]:.4f}")
print(f"\n Aggregated results: {AGGREGATED_RESULT_FILE}")
source = server_cfg.get(_DIFFUSION_SOURCE_CONFIG_KEY) or CONFIG_FILE_PATH
if source:
print(f"\n Aggregated results: {_aggregated_result_file_for_source(str(source))}")
print("=" * 60)
assert_baseline = request.config.getoption("--assert-baseline", default=False)
@@ -1,6 +1,18 @@
[
{
"test_name": "test_bagel_single_device_single_stage_t2i",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-stage BAGEL (TP=1, CFG=1), t2i Task",
"server_type": "vllm-omni",
"server_params": {
@@ -38,6 +50,18 @@
},
{
"test_name": "test_bagel_single_device_single_stage_i2i",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-stage BAGEL (TP=1, CFG=1), i2i Task",
"server_type": "vllm-omni",
"server_params": {
@@ -76,6 +100,18 @@
},
{
"test_name": "test_bagel_single_device_multi_stage_t2i",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Two-stage BAGEL (bagel.yaml), TP=1, CFG=1, t2i Task",
"server_type": "vllm-omni",
"server_params": {
@@ -113,6 +149,18 @@
},
{
"test_name": "test_bagel_single_device_multi_stage_i2i",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Two-stage BAGEL (bagel.yaml), TP=1, CFG=1, i2i Task",
"server_type": "vllm-omni",
"server_params": {
+253 -205
View File
@@ -1,213 +1,261 @@
[
{
"test_name": "test_cosmos3_t2i_official_demo_2gpu",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
},
"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": {
"guidance_scale": 7.0,
"flow_shift": 3.0,
"guardrails": false
},
"baseline": {
"throughput_qps": 1.107,
"latency_mean": 0.8929,
"peak_memory_mb_mean": 80000.0
}
}
]
{
"test_name": "test_cosmos3_t2i_official_demo_2gpu",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_cosmos3_t2v_official_demo_2gpu",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
"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": {
"guidance_scale": 7.0,
"flow_shift": 3.0,
"guardrails": false
},
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false,
"use_resolution_template": false,
"use_duration_template": false
},
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 36.6824,
"peak_memory_mb_mean": 30155.4
}
}
]
"baseline": {
"throughput_qps": 1.107,
"latency_mean": 0.8929,
"peak_memory_mb_mean": 80000.0
}
}
]
},
{
"test_name": "test_cosmos3_t2v_official_demo_2gpu",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_cosmos3_i2v_official_demo_2gpu",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false,
"use_resolution_template": false,
"use_duration_template": false
},
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false
},
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 36.968,
"peak_memory_mb_mean": 30443.6
}
}
]
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 36.6824,
"peak_memory_mb_mean": 30155.4
}
}
]
},
{
"test_name": "test_cosmos3_i2v_official_demo_2gpu",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_cosmos3_v2v_official_demo_2gpu",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false
},
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false,
"condition_frame_indexes_vision": [
0,
1
],
"condition_video_keep": "first"
},
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 37.2117,
"peak_memory_mb_mean": 30135.6
}
}
]
}
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 36.968,
"peak_memory_mb_mean": 30443.6
}
}
]
},
{
"test_name": "test_cosmos3_v2v_official_demo_2gpu",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"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": {
"cfg-parallel-size": 2,
"use-hsdp": true,
"hsdp-shard-size": 2,
"vae-patch-parallel-size": 2,
"vae-use-tiling": true,
"model-class-name": "Cosmos3OmniDiffusersPipeline",
"no-guardrails": true,
"stage-init-timeout": 900,
"init-timeout": 1200,
"enable-diffusion-pipeline-profiler": true
}
},
"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": {
"guidance_scale": 6.0,
"flow_shift": 10.0,
"max_sequence_length": 4096,
"guardrails": false,
"condition_frame_indexes_vision": [
0,
1
],
"condition_video_keep": "first"
},
"baseline": {
"throughput_qps": 0.027,
"latency_mean": 37.2117,
"peak_memory_mb_mean": 30135.6
}
}
]
}
]
@@ -1,6 +1,18 @@
[
{
"test_name": "test_higgs_audio_v3",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"tts"
],
"server_params": {
"model": "bosonai/higgs-audio-v3-tts-4b",
"stage_overrides": {
+148 -136
View File
@@ -1,140 +1,152 @@
[
{
"test_name": "test_hunyuan_image3_ti2i_inline",
"description": "AR (TP=2) + 2 * DiT (TP=2) - using inline dataset and deploy-config",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/edits",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"trust-remote-code": true,
"deploy-config-inline": {
"type": "yaml",
"content": {
"pipeline": "hunyuan_image_3_moe",
"async_chunk": false,
"trust_remote_code": true,
"connectors": {
"rdma_connector": {
"name": "MooncakeTransferEngineConnector",
"extra": {
"host": "auto",
"zmq_port": 50051,
"protocol": "rdma",
"device_name": "",
"memory_pool_size": 17179869184,
"memory_pool_device": "cpu"
}
}
},
"stages": [
{
"stage_id": 0,
"is_comprehension": true,
"final_output": true,
"final_output_type": "text",
"max_num_seqs": 16,
"gpu_memory_utilization": 0.95,
"enforce_eager": false,
"enable_prefix_caching": false,
"max_num_batched_tokens": 32768,
"devices": "0,1",
"tensor_parallel_size": 2,
"hf_overrides": {
"rope_parameters": {
"mrope_section": [
0,
32,
32
],
"rope_type": "default"
}
},
"omni_kv_config": {
"need_send_cache": true
},
"output_connectors": {
"to_stage_1": "rdma_connector"
},
"default_sampling_params": {
"temperature": 0.0,
"top_p": 1,
"top_k": -1,
"max_tokens": 8192,
"detokenize": true,
"skip_special_tokens": false,
"stop_token_ids": [
128025
]
}
},
{
"stage_id": 1,
"max_num_seqs": 1,
"enforce_eager": true,
"devices": "2,3",
"distributed_executor_backend": "mp",
"omni_kv_config": {
"need_recv_cache": true
},
"parallel_config": {
"tensor_parallel_size": 2,
"vae_patch_parallel_size": 2,
"enable_expert_parallel": true
},
"input_connectors": {
"from_stage_0": "rdma_connector"
},
"default_sampling_params": {
"num_inference_steps": 50,
"guidance_scale": 0
}
}
],
"edges": [
{
"from": 0,
"to": 1,
"window_size": -1,
"max_inflight": 1
}
]
}
},
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "inline_dataset_5_requests",
"dataset": "custom",
"dataset-path-inline": {
"type": "jsonl",
"content": [
{
"prompt": "新年宠物海报,Q版圆润的可爱标题“新年快乐汪”,副标题“HAPPY NEW YEAR”。 鱼眼镜头,背景是房间门口,近景,上传的主体歪头笑,围着红色围巾,戴着红色毛线帽,高清,绒毛细节,面部特写。 宝丽莱相纸,超现实主义,写实主义,胶片摄影,打印颗粒感肌理。肌理,超写实,复古感。",
"image_urls": [
"https://raw.githubusercontent.com/Tencent-Hunyuan/HunyuanImage-3.0/main/assets/demo_instruct_imgs/input_0_0.png"
],
"num_inference_steps": 8,
"seed": 42,
"guidance_scale": 1.0
}
]
},
"task": "ti2i",
"bot-task": "think_recaption",
"max-concurrency": 16,
"num-prompts": 96,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.1874,
"latency_p99": 101.852,
"latency_mean": 79.0385,
"peak_memory_mb_max": 70000,
"peak_memory_mb_mean": 70000
{
"test_name": "test_hunyuan_image3_ti2i_inline",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"diffusion",
"local_model"
],
"description": "AR (TP=2) + 2 * DiT (TP=2) - using inline dataset and deploy-config",
"server_type": "vllm-omni",
"benchmark_endpoint": "/v1/images/edits",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"trust-remote-code": true,
"deploy-config-inline": {
"type": "yaml",
"content": {
"pipeline": "hunyuan_image_3_moe",
"async_chunk": false,
"trust_remote_code": true,
"connectors": {
"rdma_connector": {
"name": "MooncakeTransferEngineConnector",
"extra": {
"host": "auto",
"zmq_port": 50051,
"protocol": "rdma",
"device_name": "",
"memory_pool_size": 17179869184,
"memory_pool_device": "cpu"
}
}
},
"stages": [
{
"stage_id": 0,
"is_comprehension": true,
"final_output": true,
"final_output_type": "text",
"max_num_seqs": 16,
"gpu_memory_utilization": 0.95,
"enforce_eager": false,
"enable_prefix_caching": false,
"max_num_batched_tokens": 32768,
"devices": "0,1",
"tensor_parallel_size": 2,
"hf_overrides": {
"rope_parameters": {
"mrope_section": [
0,
32,
32
],
"rope_type": "default"
}
},
"omni_kv_config": {
"need_send_cache": true
},
"output_connectors": {
"to_stage_1": "rdma_connector"
},
"default_sampling_params": {
"temperature": 0.0,
"top_p": 1,
"top_k": -1,
"max_tokens": 8192,
"detokenize": true,
"skip_special_tokens": false,
"stop_token_ids": [
128025
]
}
},
{
"stage_id": 1,
"max_num_seqs": 1,
"enforce_eager": true,
"devices": "2,3",
"distributed_executor_backend": "mp",
"omni_kv_config": {
"need_recv_cache": true
},
"parallel_config": {
"tensor_parallel_size": 2,
"vae_patch_parallel_size": 2,
"enable_expert_parallel": true
},
"input_connectors": {
"from_stage_0": "rdma_connector"
},
"default_sampling_params": {
"num_inference_steps": 50,
"guidance_scale": 0
}
}
],
"edges": [
{
"from": 0,
"to": 1,
"window_size": -1,
"max_inflight": 1
}
]
}
},
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "inline_dataset_5_requests",
"dataset": "custom",
"dataset-path-inline": {
"type": "jsonl",
"content": [
{
"prompt": "新年宠物海报,Q版圆润的可爱标题“新年快乐汪”,副标题“HAPPY NEW YEAR”。 鱼眼镜头,背景是房间门口,近景,上传的主体歪头笑,围着红色围巾,戴着红色毛线帽,高清,绒毛细节,面部特写。 宝丽莱相纸,超现实主义,写实主义,胶片摄影,打印颗粒感肌理。肌理,超写实,复古感。",
"image_urls": [
"https://raw.githubusercontent.com/Tencent-Hunyuan/HunyuanImage-3.0/main/assets/demo_instruct_imgs/input_0_0.png"
],
"num_inference_steps": 8,
"seed": 42,
"guidance_scale": 1.0
}
]
}
]
},
"task": "ti2i",
"bot-task": "think_recaption",
"max-concurrency": 16,
"num-prompts": 96,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.1874,
"latency_p99": 101.852,
"latency_mean": 79.0385,
"peak_memory_mb_max": 70000,
"peak_memory_mb_mean": 70000
}
}
]
}
]
@@ -1,35 +1,47 @@
[
{
"test_name": "test_hunyuan_image_tp2_cfgp2",
"description": "TP=2 CfgP=2 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy_config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 2,
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.21,
"latency_p99": 4.7469,
"peak_memory_mb_max": 101100,
"peak_memory_mb_mean": 101100
}
}
]
}
{
"test_name": "test_hunyuan_image_tp2_cfgp2",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"diffusion",
"local_model"
],
"description": "TP=2 CfgP=2 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy_config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 2,
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.21,
"latency_p99": 4.7469,
"peak_memory_mb_max": 101100,
"peak_memory_mb_mean": 101100
}
}
]
}
]
@@ -1,35 +1,47 @@
[
{
"test_name": "test_hunyuan_image_tp2_sp2",
"description": "TP=2 SP=2 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy-config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 2,
"ulysses-degree": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.20,
"latency_p99": 5.1025,
"peak_memory_mb_max": 97402,
"peak_memory_mb_mean": 97402
}
}
]
}
{
"test_name": "test_hunyuan_image_tp2_sp2",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"diffusion",
"local_model"
],
"description": "TP=2 SP=2 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy-config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 2,
"ulysses-degree": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.20,
"latency_p99": 5.1025,
"peak_memory_mb_max": 97402,
"peak_memory_mb_mean": 97402
}
}
]
}
]
@@ -1,35 +1,47 @@
[
{
"test_name": "test_hunyuan_image_tp4",
"description": "TP=4 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy-config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 4,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.213,
"latency_mean": 4.6953,
"latency_p99": 4.8601,
"peak_memory_mb_max": 56912.0,
"peak_memory_mb_mean": 56912.0
}
}
]
}
{
"test_name": "test_hunyuan_image_tp4",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"diffusion",
"local_model"
],
"description": "TP=4 baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "tencent/HunyuanImage-3.0-Instruct",
"serve_args": {
"deploy-config": "../vllm_omni/deploy/hunyuan_image3_dit.yaml",
"tensor-parallel-size": 4,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1024x1024_steps8",
"dataset": "random",
"task": "t2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 8,
"num-prompts": 10,
"max-concurrency": 1,
"skip-performance-assertion": true,
"baseline": {
"throughput_qps": 0.213,
"latency_mean": 4.6953,
"latency_p99": 4.8601,
"peak_memory_mb_max": 56912.0,
"peak_memory_mb_mean": 56912.0
}
}
]
}
]
+267 -211
View File
@@ -1,217 +1,273 @@
[
{
"test_name": "test_ltx2_baseline_eager",
"description": "Single-device baseline with enforce-eager (no torch.compile)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
{
"test_name": "test_ltx2_baseline_eager",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline with enforce-eager (no torch.compile)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_ltx2_torch_compile",
"description": "Single-device with torch.compile (default, no enforce-eager)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
"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
},
{
"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
}
]
},
{
"test_name": "test_ltx2_torch_compile",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device with torch.compile (default, no enforce-eager)",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_ltx2_cfg2_eager",
"description": "CFG-parallel=2 with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
"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
},
{
"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
}
]
},
{
"test_name": "test_ltx2_cfg2_eager",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"diffusion"
],
"description": "CFG-parallel=2 with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_ltx2_cfg2_compile",
"description": "CFG-parallel=2 with torch.compile",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
"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
},
{
"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
}
]
},
{
"test_name": "test_ltx2_cfg2_compile",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"diffusion"
],
"description": "CFG-parallel=2 with torch.compile",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cfg-parallel-size": 2,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_ltx2_cache_dit_eager",
"description": "CacheDiT with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cache-backend": "cache_dit",
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
}
"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
},
{
"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
}
]
},
{
"test_name": "test_ltx2_cache_dit_eager",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "CacheDiT with enforce-eager",
"server_type": "vllm-omni",
"server_params": {
"model": "Lightricks/LTX-2",
"serve_args": {
"cache-backend": "cache_dit",
"enforce-eager": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
},
{
"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
}
]
}
]
@@ -1,331 +1,343 @@
[
{
"test_name": "test_qwen3_omni_chunk",
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"extra_cli_args": [
"--async-chunk"
]
{
"test_name": "test_qwen3_omni_chunk",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"omni"
],
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"extra_cli_args": [
"--async-chunk"
]
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
135.4379,
187.0896,
400,
435.675,
724.6836
],
"mean_e2el_ms": [
17619.9851,
34811.8335,
43656.9501,
56657.1548,
85097.6016
],
"mean_audio_rtf": [
0.1279,
0.2238,
0.2735,
0.3712,
0.5634
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
135.4379,
187.0896,
400,
435.675,
724.6836
],
"mean_e2el_ms": [
17619.9851,
34811.8335,
43656.9501,
56657.1548,
85097.6016
],
"mean_audio_rtf": [
0.1279,
0.2238,
0.2735,
0.3712,
0.5634
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
340.329
],
"mean_e2el_ms": [
4305.477
],
"mean_audio_rtf": [
0.1305
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
220.0909
],
"mean_e2el_ms": [
5475.9565
],
"mean_audio_rtf": [
0.1818
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
450.0155
],
"mean_e2el_ms": [
8516.8316
],
"mean_audio_rtf": [
0.2717
]
}
},
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
78.4278,
195.8037,
333.7318,
561.805,
1038.043
],
"mean_e2el_ms": [
4288.3029,
7429.9429,
9451.6184,
12607.5292,
18153.2547
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
275.3875
],
"mean_e2el_ms": [
730.6995
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
166.2201
],
"mean_e2el_ms": [
673.5789
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
357.7781
],
"mean_e2el_ms": [
953.3493
]
}
}
]
}
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
340.329
],
"mean_e2el_ms": [
4305.477
],
"mean_audio_rtf": [
0.1305
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
220.0909
],
"mean_e2el_ms": [
5475.9565
],
"mean_audio_rtf": [
0.1818
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
450.0155
],
"mean_e2el_ms": [
8516.8316
],
"mean_audio_rtf": [
0.2717
]
}
},
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
78.4278,
195.8037,
333.7318,
561.805,
1038.043
],
"mean_e2el_ms": [
4288.3029,
7429.9429,
9451.6184,
12607.5292,
18153.2547
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
275.3875
],
"mean_e2el_ms": [
730.6995
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
166.2201
],
"mean_e2el_ms": [
673.5789
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
357.7781
],
"mean_e2el_ms": [
953.3493
]
}
}
]
}
]
@@ -1,115 +1,127 @@
[
{
"test_name": "test_qwen3_omni_3gpu_replica2",
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"stage_overrides": {
"1": {"num_replicas": 2, "devices": "1,2"},
"2": {"num_replicas": 2, "devices": "1,2"}
},
"extra_cli_args": [
"--async-chunk"
]
{
"test_name": "test_qwen3_omni_3gpu_replica2",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 3
}
},
"full_model",
"omni"
],
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"stage_overrides": {
"1": {"num_replicas": 2, "devices": "1,2"},
"2": {"num_replicas": 2, "devices": "1,2"}
},
"extra_cli_args": [
"--async-chunk"
]
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [128, 128, 128, 128],
"max_concurrency": [8, 16, 24, 32],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [203, 283, 470, 869],
"mean_tpot_ms": [28, 36, 41, 45],
"mean_audio_ttfp_ms": [861, 1200, 1528, 2100],
"mean_audio_rtf": [0.24, 0.30, 0.35, 0.39]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [10],
"request_rate": [0.1],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [128, 128, 128, 128],
"max_concurrency": [8, 16, 24, 32],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [203, 283, 470, 869],
"mean_tpot_ms": [28, 36, 41, 45],
"mean_audio_ttfp_ms": [861, 1200, 1528, 2100],
"mean_audio_rtf": [0.24, 0.30, 0.35, 0.39]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [10],
"request_rate": [0.1],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [377],
"mean_audio_ttfp_ms": [684],
"mean_audio_rtf": [0.14]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [40],
"request_rate": [0.5],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [228],
"mean_audio_ttfp_ms": [686],
"mean_audio_rtf": [0.16]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [100],
"request_rate": [1.0],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [522],
"mean_audio_ttfp_ms": [1119],
"mean_audio_rtf": [0.21]
}
}
]
}
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [377],
"mean_audio_ttfp_ms": [684],
"mean_audio_rtf": [0.14]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [40],
"request_rate": [0.5],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [228],
"mean_audio_ttfp_ms": [686],
"mean_audio_rtf": [0.16]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [100],
"request_rate": [1.0],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [522],
"mean_audio_ttfp_ms": [1119],
"mean_audio_rtf": [0.21]
}
}
]
}
]
@@ -1,170 +1,182 @@
[
{
"test_name": "test_qwen3_omni",
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"extra_cli_args": [
"--no-async-chunk"
]
{
"test_name": "test_qwen3_omni",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"omni"
],
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"extra_cli_args": [
"--no-async-chunk"
]
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
109.1218,
247.4411,
354.9384,
447.7885,
694.7487
],
"mean_e2el_ms": [
22413.667,
30808.9715,
35677.844,
46345.8978,
68350.4853
],
"mean_audio_rtf": [
0.1695,
0.2033,
0.2467,
0.3234,
0.4438
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
109.1218,
247.4411,
354.9384,
447.7885,
694.7487
],
"mean_e2el_ms": [
22413.667,
30808.9715,
35677.844,
46345.8978,
68350.4853
],
"mean_audio_rtf": [
0.1695,
0.2033,
0.2467,
0.3234,
0.4438
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
323.3573
],
"mean_e2el_ms": [
4623.4173
],
"mean_audio_rtf": [
0.1406
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
215.4502
],
"mean_e2el_ms": [
4924.149
],
"mean_audio_rtf": [
0.1637
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
442.5815
],
"mean_e2el_ms": [
6441.0986
],
"mean_audio_rtf": [
0.2091
]
}
}
]
}
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
323.3573
],
"mean_e2el_ms": [
4623.4173
],
"mean_audio_rtf": [
0.1406
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
215.4502
],
"mean_e2el_ms": [
4924.149
],
"mean_audio_rtf": [
0.1637
]
}
},
{
"dataset_name": "random-mm",
"backend": "openai-chat-omni",
"endpoint": "/v1/chat/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
"mean_ttft_ms": [
442.5815
],
"mean_e2el_ms": [
6441.0986
],
"mean_audio_rtf": [
0.2091
]
}
}
]
}
]
@@ -1,180 +1,192 @@
[
{
"test_name": "test_qwen3_omni_vllm_text",
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"use_omni": false,
"extra_cli_args": [
"--no-enable-prefix-caching",
"--skip-mm-profiling"
]
{
"test_name": "test_qwen3_omni_vllm_text",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"omni"
],
"server_params": {
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"use_omni": false,
"extra_cli_args": [
"--no-enable-prefix-caching",
"--skip-mm-profiling"
]
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"benchmark_params": [
{
"dataset_name": "random",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
4,
16,
32,
64,
128
],
"max_concurrency": [
1,
4,
8,
16,
32
],
"random_input_len": 2500,
"random_output_len": 900,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
69.1276,
255.1882,
310.639,
422.0337,
532.0582
],
"mean_e2el_ms": [
4382.7211,
7510.3815,
9440.4854,
12553.0186,
16932.6065
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
26.3381
],
"mean_e2el_ms": [
485.2436
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
26.4517
],
"mean_e2el_ms": [
517.2017
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
27.6972
],
"mean_e2el_ms": [
561.3684
]
}
}
]
}
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
69.1276,
255.1882,
310.639,
422.0337,
532.0582
],
"mean_e2el_ms": [
4382.7211,
7510.3815,
9440.4854,
12553.0186,
16932.6065
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
10
],
"request_rate": [
0.1
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 1,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"audio": 1
},
"random_mm_bucket_config": {
"(0, 60, 3)": 1.0
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
26.3381
],
"mean_e2el_ms": [
485.2436
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
40
],
"request_rate": [
0.5
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 2,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.5,
"(720, 1280, 2)": 0.5
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
26.4517
],
"mean_e2el_ms": [
517.2017
]
}
},
{
"dataset_name": "random-mm",
"backend": "vllm",
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"endpoint": "/v1/completions",
"num_prompts": [
100
],
"request_rate": [
1.0
],
"random_input_len": 100,
"random_output_len": 100,
"random_range_ratio": 0.0,
"ignore_eos": true,
"extra_body": {
"modalities": [
"text"
]
},
"random_mm_base_items_per_request": 3,
"random_mm_num_mm_items_range_ratio": 0.5,
"random_mm_limit_mm_per_prompt": {
"image": 1,
"video": 1,
"audio": 1
},
"random_mm_bucket_config": {
"(256, 256, 1)": 0.34,
"(720, 1280, 2)": 0.33,
"(0, 60, 3)": 0.33
},
"percentile-metrics": "ttft,tpot,itl,e2el",
"baseline": {
"mean_ttft_ms": [
27.6972
],
"mean_e2el_ms": [
561.3684
]
}
}
]
}
]
@@ -1,149 +1,185 @@
[
{
"test_name": "test_qwen_image_edit_2511_single_device",
"description": "Single-device baseline (two input images)",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0721,
"latency_mean": 13.8398,
"peak_memory_mb_max": 54731.4,
"peak_memory_mb_mean": 54731.4
}
},
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0186,
"latency_mean": 53.6942,
"peak_memory_mb_max": 64402.4,
"peak_memory_mb_mean": 64402.4
}
}
]
{
"test_name": "test_qwen_image_edit_2511_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline (two input images)",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_vae_patch4",
"description": "Ulysses SP=2 + CFG=2 + VAE patch parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0538,
"latency_mean": 18.5591,
"peak_memory_mb_max": 53485.0,
"peak_memory_mb_mean": 53485.0
}
}
]
"benchmark_params": [
{
"name": "512x512_steps20_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0721,
"latency_mean": 13.8398,
"peak_memory_mb_max": 54731.4,
"peak_memory_mb_mean": 54731.4
}
},
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0186,
"latency_mean": 53.6942,
"peak_memory_mb_max": 64402.4,
"peak_memory_mb_mean": 64402.4
}
}
]
},
{
"test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_vae_patch4",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG=2 + VAE patch parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_cache_dit",
"description": "Ulysses SP=2 + CFG=2 + CacheDiT",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"enable-diffusion-pipeline-profiler": true
}
"benchmark_params": [
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0538,
"latency_mean": 18.5591,
"peak_memory_mb_max": 53485.0,
"peak_memory_mb_mean": 53485.0
}
}
]
},
{
"test_name": "test_qwen_image_edit_2511_ulysses2_cfg2_cache_dit",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG=2 + CacheDiT",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit-2511",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.2776,
"latency_mean": 3.5938,
"peak_memory_mb_max": 55214.0,
"peak_memory_mb_mean": 55214.0
}
},
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1051,
"latency_mean": 9.4918,
"peak_memory_mb_max": 64265.6,
"peak_memory_mb_mean": 64265.6
}
}
]
}
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.2776,
"latency_mean": 3.5938,
"peak_memory_mb_max": 55214.0,
"peak_memory_mb_mean": 55214.0
}
},
{
"name": "1536x1536_steps35_i2i_2img",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"num-input-images": 2,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1051,
"latency_mean": 9.4918,
"peak_memory_mb_max": 64265.6,
"peak_memory_mb_mean": 64265.6
}
}
]
}
]
@@ -1,144 +1,180 @@
[
{
"test_name": "test_qwen_image_edit_single_device",
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1129,
"latency_mean": 8.8352,
"peak_memory_mb_max": 55356.5,
"peak_memory_mb_mean": 55356.5
}
},
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0240,
"latency_mean": 41.5686,
"peak_memory_mb_max": 62762.7,
"peak_memory_mb_mean": 62762.7
}
}
]
{
"test_name": "test_qwen_image_edit_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_edit_ulysses2_cfg2_vae_patch4",
"description": "Ulysses SP=2 + CFG=2 + VAE patch parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0672,
"latency_mean": 14.8517,
"peak_memory_mb_max": 53931.5,
"peak_memory_mb_mean": 53931.5
}
}
]
"benchmark_params": [
{
"name": "512x512_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1129,
"latency_mean": 8.8352,
"peak_memory_mb_max": 55356.5,
"peak_memory_mb_mean": 55356.5
}
},
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0240,
"latency_mean": 41.5686,
"peak_memory_mb_max": 62762.7,
"peak_memory_mb_mean": 62762.7
}
}
]
},
{
"test_name": "test_qwen_image_edit_ulysses2_cfg2_vae_patch4",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG=2 + VAE patch parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_edit_ulysses2_cfg2_cache_dit",
"description": "Ulysses SP=2 + CFG=2 + CacheDiT",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"enable-diffusion-pipeline-profiler": true
}
"benchmark_params": [
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0672,
"latency_mean": 14.8517,
"peak_memory_mb_max": 53931.5,
"peak_memory_mb_mean": 53931.5
}
}
]
},
{
"test_name": "test_qwen_image_edit_ulysses2_cfg2_cache_dit",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG=2 + CacheDiT",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Edit",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.2879,
"latency_mean": 3.4701,
"peak_memory_mb_max": 55525.6,
"peak_memory_mb_mean": 55525.6
}
},
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1113,
"latency_mean": 8.9656,
"peak_memory_mb_max": 63758.3,
"peak_memory_mb_mean": 63758.3
}
}
]
}
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.2879,
"latency_mean": 3.4701,
"peak_memory_mb_max": 55525.6,
"peak_memory_mb_mean": 55525.6
}
},
{
"name": "1536x1536_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1113,
"latency_mean": 8.9656,
"peak_memory_mb_max": 63758.3,
"peak_memory_mb_mean": 63758.3
}
}
]
}
]
@@ -1,49 +1,61 @@
[
{
"test_name": "test_qwen_image_layered_single_device",
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Layered",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "640x640_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 640,
"height": 640,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0703,
"latency_mean": 14.208,
"peak_memory_mb_max": 60543.5,
"peak_memory_mb_mean": 60543.5
}
},
{
"name": "1024x1024_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0417,
"latency_mean": 23.914,
"peak_memory_mb_max": 60543.5,
"peak_memory_mb_mean": 60543.5
}
}
]
}
{
"test_name": "test_qwen_image_layered_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image-Layered",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "640x640_steps20_i2i",
"dataset": "random",
"task": "i2i",
"width": 640,
"height": 640,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0703,
"latency_mean": 14.208,
"peak_memory_mb_max": 60543.5,
"peak_memory_mb_mean": 60543.5
}
},
{
"name": "1024x1024_steps35_i2i",
"dataset": "random",
"task": "i2i",
"width": 1024,
"height": 1024,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0417,
"latency_mean": 23.914,
"peak_memory_mb_max": 60543.5,
"peak_memory_mb_mean": 60543.5
}
}
]
}
]
@@ -1,219 +1,279 @@
[
{
"test_name": "test_qwen_image_single_device",
"description": "Single-device baseline (no parallelism)",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.4153,
"latency_mean": 2.4065,
"peak_memory_mb_mean": 52289.9
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0423,
"latency_mean": 23.5991,
"peak_memory_mb_mean": 61360.5
}
}
]
{
"test_name": "test_qwen_image_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline (no parallelism)",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_single_device_step_execution",
"description": "Single-device baseline (no parallelism) with step execution",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true,
"step-execution": true
}
},
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.4175,
"latency_mean": 2.3953,
"peak_memory_mb_mean": 56000
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0423,
"latency_mean": 23.5768,
"peak_memory_mb_mean": 65000
}
}
]
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.4153,
"latency_mean": 2.4065,
"peak_memory_mb_mean": 52289.9
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0423,
"latency_mean": 23.5991,
"peak_memory_mb_mean": 61360.5
}
}
]
},
{
"test_name": "test_qwen_image_single_device_step_execution",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline (no parallelism) with step execution",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true,
"step-execution": true
}
},
{
"test_name": "test_qwen_image_ulysses2_cfg2_vae_patch4",
"description": "Ulysses SP=2 + CFG-parallel=2 + VAE Patch Parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
"benchmark_params": [
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1214,
"latency_mean": 8.2211,
"peak_memory_mb_mean": 51704.7
}
}
]
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.4175,
"latency_mean": 2.3953,
"peak_memory_mb_mean": 56000
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.0423,
"latency_mean": 23.5768,
"peak_memory_mb_mean": 65000
}
}
]
},
{
"test_name": "test_qwen_image_ulysses2_cfg2_vae_patch4",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG-parallel=2 + VAE Patch Parallel=4",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"vae-patch-parallel-size": 4,
"vae-use-tiling": true,
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_ulysses2_cfg2_cache_dit",
"description": "Ulysses SP=2 + CFG-parallel=2 + CacheDiT acceleration",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"enable-diffusion-pipeline-profiler": true
}
"benchmark_params": [
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1214,
"latency_mean": 8.2211,
"peak_memory_mb_mean": 51704.7
}
}
]
},
{
"test_name": "test_qwen_image_ulysses2_cfg2_cache_dit",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 4
}
},
"full_model",
"diffusion"
],
"description": "Ulysses SP=2 + CFG-parallel=2 + CacheDiT acceleration",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"ulysses-degree": 2,
"cfg-parallel-size": 2,
"cache-backend": "cache_dit",
"cache-config": {
"Fn_compute_blocks": 1,
"Bn_compute_blocks": 0,
"max_warmup_steps": 4,
"residual_diff_threshold": 0.24,
"max_continuous_cached_steps": 3,
"enable_taylorseer": false,
"taylorseer_order": 1,
"scm_steps_mask_policy": null,
"scm_steps_policy": "dynamic"
},
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.559,
"latency_mean": 1.794,
"peak_memory_mb_mean": 52397.6775
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1918,
"latency_mean": 5.2022,
"peak_memory_mb_mean": 61468.8
}
}
]
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_qwen_image_single_device_step_execution_high_concurrency",
"description": "Single-device Qwen-Image step-level execution benchmark at 512x512, 20 steps, concurrency sweep",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true,
"step-execution": true,
"max-num-seqs": 8
}
},
"benchmark_params": [
{
"name": "512x512_steps20_high_concurrency",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": [20, 40, 80, 160],
"max-concurrency": [1, 2, 4, 8],
"warmup-requests": 8,
"warmup-concurrency": 8,
"warmup-num-inference-steps": 20,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": [0.4125, 0.6714, 0.6674, 0.8159],
"latency_mean": [2.4215, 2.992, 5.9293, 9.656],
"peak_memory_mb_mean": [67000, 67000, 67000, 67000]
}
}
]
}
"benchmark_params": [
{
"name": "512x512_steps20",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.559,
"latency_mean": 1.794,
"peak_memory_mb_mean": 52397.6775
}
},
{
"name": "1536x1536_steps35",
"dataset": "random",
"task": "t2i",
"width": 1536,
"height": 1536,
"num-inference-steps": 35,
"num-prompts": 10,
"max-concurrency": 1,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": 0.1918,
"latency_mean": 5.2022,
"peak_memory_mb_mean": 61468.8
}
}
]
},
{
"test_name": "test_qwen_image_single_device_step_execution_high_concurrency",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device Qwen-Image step-level execution benchmark at 512x512, 20 steps, concurrency sweep",
"server_type": "vllm-omni",
"server_params": {
"model": "Qwen/Qwen-Image",
"serve_args": {
"enable-diffusion-pipeline-profiler": true,
"step-execution": true,
"max-num-seqs": 8
}
},
"benchmark_params": [
{
"name": "512x512_steps20_high_concurrency",
"dataset": "random",
"task": "t2i",
"width": 512,
"height": 512,
"num-inference-steps": 20,
"num-prompts": [20, 40, 80, 160],
"max-concurrency": [1, 2, 4, 8],
"warmup-requests": 8,
"warmup-concurrency": 8,
"warmup-num-inference-steps": 20,
"enable-negative-prompt": true,
"baseline": {
"throughput_qps": [0.4125, 0.6714, 0.6674, 0.8159],
"latency_mean": [2.4215, 2.992, 5.9293, 9.656],
"peak_memory_mb_mean": [67000, 67000, 67000, 67000]
}
}
]
}
]
@@ -86,3 +86,202 @@ def test_enabled_false_entry_is_skipped():
# Only the enabled=True entry should appear
assert len(params) == 1
assert params[0].get("task") == "voice_clone"
def test_resolve_pytest_marks_hardware_dict_with_extra():
from tests.dfx.conftest import resolve_pytest_marks
marks = resolve_pytest_marks(
[
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 2}},
"full_model",
"diffusion",
"local_model",
]
)
names = {m.name for m in marks}
assert "H100" in names
assert "cuda" in names
assert "gpu" in names
assert "distributed_cuda" in names
assert "full_model" in names
assert "diffusion" in names
assert "local_model" in names
def test_resolve_pytest_marks_rejects_legacy_object_format():
from tests.dfx.conftest import resolve_pytest_marks
with pytest.raises(ValueError, match="mark must be a list"):
resolve_pytest_marks(
{
"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1},
"marks": ["full_model"],
}
)
def test_extract_mark_resource_label():
from tests.dfx.conftest import extract_mark_resource_label
assert extract_mark_resource_label(None) == "na"
assert (
extract_mark_resource_label(
[
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}},
"full_model",
]
)
== "H100"
)
assert (
extract_mark_resource_label(
[
{
"hardware_marks": {
"res": {"cuda": "H100", "rocm": "MI325"},
"num_cards": 2,
}
},
"diffusion",
]
)
== "H100-MI325"
)
def test_resource_label_for_filename():
from tests.dfx.conftest import resource_label_for_filename
assert resource_label_for_filename("H100") == ""
assert resource_label_for_filename("L4") == "L4"
assert resource_label_for_filename("910B") == "910B"
assert resource_label_for_filename("na") == "na"
def test_hardware_json_value():
from tests.dfx.conftest import hardware_json_value
assert hardware_json_value("H100") == "H100"
assert hardware_json_value("na") == ""
assert hardware_json_value(None) == ""
def test_extract_configs_resource_label(monkeypatch):
from tests.dfx.conftest import extract_configs_resource_label, get_runtime_resource_label
monkeypatch.setattr(
"tests.dfx.conftest._read_runtime_device_name",
lambda *, device_id=0: "NVIDIA H100 80GB HBM3",
)
get_runtime_resource_label(refresh=True)
assert extract_configs_resource_label([]) == "H100"
get_runtime_resource_label(refresh=True)
monkeypatch.setattr(
"tests.dfx.conftest._read_runtime_device_name",
lambda *, device_id=0: "Ascend910B2",
)
assert get_runtime_resource_label(refresh=True) == "910B"
def test_load_benchmark_configs_from_dir(tmp_path):
from tests.dfx.conftest import load_benchmark_configs
(tmp_path / "a.json").write_text(
json.dumps([{"test_name": "test_a", "server_params": {"model": "m/a"}, "benchmark_params": []}]),
encoding="utf-8",
)
(tmp_path / "b.json").write_text(
json.dumps([{"test_name": "test_b", "server_params": {"model": "m/b"}, "benchmark_params": []}]),
encoding="utf-8",
)
configs = load_benchmark_configs(config_dir=tmp_path)
assert [c["test_name"] for c in configs] == ["test_a", "test_b"]
def test_create_unique_server_pytest_params_applies_marks(tmp_path):
from tests.dfx.conftest import create_unique_server_pytest_params
configs = [
{
"test_name": "test_with_mark",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}},
"full_model",
],
"server_params": {"model": "some/model"},
"benchmark_params": [{"name": "p0", "num_prompts": 1}],
},
{
"test_name": "test_without_mark",
"server_params": {"model": "other/model"},
"benchmark_params": [{"name": "p0", "num_prompts": 1}],
},
]
params = create_unique_server_pytest_params(configs, tmp_path)
by_id = {p.id: p for p in params}
assert len(by_id["test_with_mark"].values) == 1
assert isinstance(by_id["test_with_mark"].values[0], tuple)
assert any(m.name == "H100" for m in by_id["test_with_mark"].marks)
assert not any(m.name == "H100" for m in by_id["test_without_mark"].marks)
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"]}
)
assert is_diffusion_perf_config(
{
"test_name": "diff_a",
"server_type": "vllm-omni",
"mark": [{"hardware_marks": {"res": {"cuda": "H100"}}}, "diffusion"],
}
)
def test_benchmark_param_id_suffix_from_task_eval_phase():
from tests.dfx.conftest import _unique_benchmark_param_id_suffixes
params = [
{"task": "default_voice", "eval_phase": "latency"},
{"task": "default_voice", "eval_phase": "throughput"},
]
assert _unique_benchmark_param_id_suffixes(params) == [
"default_voice_latency",
"default_voice_throughput",
]
def test_create_paired_omni_benchmark_pytest_params(tmp_path):
from tests.dfx.conftest import create_paired_omni_benchmark_pytest_params
configs = [
{
"test_name": "test_omni",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}},
"omni",
],
"server_params": {"model": "m/omni"},
"benchmark_params": [{"name": "p0", "num_prompts": 1}, {"name": "p1", "num_prompts": 2}],
},
{
"test_name": "test_tts",
"mark": [
{"hardware_marks": {"res": {"cuda": "H100"}, "num_cards": 1}},
"tts",
],
"server_params": {"model": "m/tts"},
"benchmark_params": [{"name": "p0", "num_prompts": 1}],
},
]
params = create_paired_omni_benchmark_pytest_params(configs, tmp_path)
by_id = {p.id: p for p in params}
assert set(by_id) == {"test_omni-p0", "test_omni-p1", "test_tts-p0"}
omni_row, bench_row = by_id["test_tts-p0"].values
assert omni_row[0] == "test_tts"
assert bench_row == ("test_tts", 0)
assert any(m.name == "tts" for m in by_id["test_tts-p0"].marks)
assert not any(m.name == "tts" for m in by_id["test_omni-p0"].marks)
+36
View File
@@ -1,6 +1,18 @@
[
{
"test_name": "test_qwen3_tts_base",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"tts"
],
"server_params": {
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-Base",
"stage_overrides": {
@@ -98,6 +110,18 @@
},
{
"test_name": "test_qwen3_tts_customvoice",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"tts"
],
"server_params": {
"model": "Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice",
"stage_overrides": {
@@ -342,6 +366,18 @@
},
{
"test_name": "test_voxtral_tts",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"tts"
],
"server_params": {
"model": "mistralai/Voxtral-4B-TTS-2603"
},
+12
View File
@@ -1,6 +1,18 @@
[
{
"test_name": "test_voxcpm2",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"tts"
],
"server_params": {
"model": "openbmb/VoxCPM2",
"trust_remote_code": true,
+128 -104
View File
@@ -1,107 +1,131 @@
[
{
"test_name": "test_wan22_i2v_single_device",
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
"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
}
],
"baseline": {
"throughput_qps": 0.0465,
"latency_mean": 21.4354,
"peak_memory_mb_mean": 72557.58
}
}
]
{
"test_name": "test_wan22_i2v_single_device",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 1
}
},
"full_model",
"diffusion"
],
"description": "Single-device baseline",
"server_type": "vllm-omni",
"server_params": {
"model": "Wan-AI/Wan2.2-I2V-A14B-Diffusers",
"serve_args": {
"enable-diffusion-pipeline-profiler": true
}
},
{
"test_name": "test_wan22_i2v_usp2_vae_patch2_hsdp_slicing",
"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": {
"usp": 2,
"vae-patch-parallel-size": 2,
"use-hsdp": true,
"vae-use-slicing": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
}
],
"baseline": {
"throughput_qps": 0.0642,
"latency_mean": 15.5589,
"peak_memory_mb_mean": 45742.88
}
},
{
"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
}
],
"baseline": {
"throughput_qps": 0.0128,
"latency_mean": 77.8565,
"peak_memory_mb_mean": 53947.84
}
}
]
}
"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
}
],
"baseline": {
"throughput_qps": 0.0465,
"latency_mean": 21.4354,
"peak_memory_mb_mean": 72557.58
}
}
]
},
{
"test_name": "test_wan22_i2v_usp2_vae_patch2_hsdp_slicing",
"mark": [
{
"hardware_marks": {
"res": {
"cuda": "H100"
},
"num_cards": 2
}
},
"full_model",
"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": {
"usp": 2,
"vae-patch-parallel-size": 2,
"use-hsdp": true,
"vae-use-slicing": true,
"enable-diffusion-pipeline-profiler": true
}
},
"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
}
],
"baseline": {
"throughput_qps": 0.0642,
"latency_mean": 15.5589,
"peak_memory_mb_mean": 45742.88
}
},
{
"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
}
],
"baseline": {
"throughput_qps": 0.0128,
"latency_mean": 77.8565,
"peak_memory_mb_mean": 53947.84
}
}
]
}
]
@@ -9,6 +9,7 @@ import torch.nn as nn
from vllm_omni.diffusion.model_loader.checkpoint_adapters import (
ModelOptFp8CheckpointAdapter,
ModelOptNvFp4CheckpointAdapter,
)
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
@@ -89,6 +90,23 @@ class _QuantizedRemappedModelOptModel(nn.Module):
return name
class _RemappedNvFp4Model(nn.Module):
def __init__(self) -> None:
super().__init__()
self.runtime = nn.Module()
self.runtime.proj = nn.Module()
self.runtime.proj.register_parameter(
"weight",
nn.Parameter(torch.empty(1, 1, dtype=torch.uint8), requires_grad=False),
)
for name in ("input_scale", "weight_scale", "weight_scale_2"):
self.runtime.proj.register_parameter(name, nn.Parameter(torch.empty(1), requires_grad=False))
@staticmethod
def remap_checkpoint_key(name: str) -> str:
return name.replace("transformer.orig.proj.", "runtime.proj.")
def _make_source() -> SimpleNamespace:
return SimpleNamespace(
subfolder="transformer",
@@ -188,13 +206,36 @@ def test_modelopt_adapter_remaps_and_keeps_scales_for_quantized_target():
emitted = dict(adapted)
assert len(adapted) == len(emitted) == 3
# Both scales are retained (the dropped-scale regression). They keep their
# source-checkpoint names; the model's load_weights remaps them downstream.
assert torch.equal(emitted["transformer.orig.proj.weight_scale"], weight_scale)
assert torch.equal(emitted["transformer.orig.proj.input_scale"], input_scale)
# Both scales are retained and emitted under the remapped runtime names.
assert torch.equal(emitted["runtime.proj.weight_scale"], weight_scale)
assert torch.equal(emitted["runtime.proj.input_scale"], input_scale)
# The FP8 weight is passed through unchanged under the remapped target name
# (the quantized param is FP8, so it must NOT be dequantized). Compare via
# float32 since torch.equal has no FP8 CPU kernel.
assert emitted["runtime.proj.weight"].dtype == torch.float8_e4m3fn
assert torch.equal(emitted["runtime.proj.weight"].to(torch.float32), fp8_weight.to(torch.float32))
def test_modelopt_nvfp4_adapter_remaps_quantized_weights_and_scales():
adapter = ModelOptNvFp4CheckpointAdapter(_RemappedNvFp4Model(), _make_source())
prefix = "transformer.orig.proj"
checkpoint_tensors = [
(f"{prefix}.input_scale", torch.tensor([1.0])),
(f"{prefix}.weight_scale", torch.tensor([0.5])),
(f"{prefix}.weight_scale_2", torch.tensor([0.25])),
(f"{prefix}.weight", torch.tensor([[1]], dtype=torch.uint8)),
]
adapted = list(adapter.adapt(iter(checkpoint_tensors)))
assert [name for name, _ in adapted] == [
name.replace(f"{prefix}.", "runtime.proj.") for name, _ in checkpoint_tensors
]
def test_modelopt_nvfp4_adapter_rejects_unconsumed_pre_quant_scale():
adapter = ModelOptNvFp4CheckpointAdapter(nn.Module(), _make_source())
with pytest.raises(ValueError, match="does not consume pre_quant_scale"):
list(adapter.adapt(iter([("transformer.proj.pre_quant_scale", torch.tensor([1.0]))])))
@@ -36,20 +36,36 @@ class StubScheduler:
timesteps: list[int] | None = None,
*,
flow_shift: float = 1.0,
use_karras_sigmas: bool = True,
) -> None:
self.timesteps = torch.tensor(timesteps or [9, 3], dtype=torch.int64)
self.config = SimpleNamespace(
num_train_timesteps=1000,
flow_shift=flow_shift,
use_karras_sigmas=use_karras_sigmas,
)
self.set_timesteps_calls: list[tuple[int, torch.device]] = []
self.set_timesteps_calls: list[dict[str, Any]] = []
self.step_calls: list[tuple[torch.Tensor, torch.Tensor, torch.Tensor]] = []
def set_timesteps(self, num_steps: int, device: torch.device) -> None:
self.set_timesteps_calls.append((num_steps, device))
self.timesteps = torch.arange(num_steps, 0, -1, dtype=torch.int64, device=device)
def set_timesteps(
self,
num_inference_steps: int | None = None,
device: str | torch.device | None = None,
*,
shift: float | None = None,
sigmas: list[float] | None = None,
) -> None:
self.set_timesteps_calls.append(
{
"num_inference_steps": num_inference_steps,
"device": device,
"shift": shift,
"sigmas": sigmas,
}
)
if sigmas is not None:
self.timesteps = torch.tensor(sigmas, device=device)
else:
assert num_inference_steps is not None
self.timesteps = torch.arange(num_inference_steps, 0, -1, dtype=torch.int64, device=device)
def step(self, noise_pred: torch.Tensor, timestep: torch.Tensor, latents: torch.Tensor, **kwargs):
del kwargs
@@ -200,6 +216,7 @@ def fake_cosmos3_guardrails(monkeypatch: pytest.MonkeyPatch):
def make_cosmos3_pipeline():
def _make():
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT,
Cosmos3OmniDiffusersPipeline,
)
@@ -213,11 +230,10 @@ def make_cosmos3_pipeline():
pipeline.vae_scale_factor_temporal = 4
pipeline.vae_scale_factor_spatial = 8
pipeline.scheduler = StubScheduler([9, 3], flow_shift=1.0)
pipeline._base_scheduler_config = pipeline.scheduler.config
pipeline._engine_init_flow_shift = 1.0
pipeline._current_flow_shift = 1.0
pipeline._base_scheduler_use_karras_sigmas = True
pipeline._current_scheduler_use_karras_sigmas = True
pipeline._engine_init_flow_shift = COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT
pipeline._current_flow_shift = COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT
pipeline.is_distilled_model = False
pipeline.is_edge_model = False
pipeline._guidance_scale = None
pipeline._num_timesteps = None
pipeline._cosmos3_branch_caches = None
@@ -243,6 +259,7 @@ def make_sampling_params(**overrides: Any) -> SimpleNamespace:
"num_frames": None,
"num_inference_steps": None,
"guidance_scale": None,
"guidance_scale_provided": False,
"generator": None,
"seed": 123,
"num_outputs_per_prompt": 1,
@@ -288,6 +305,165 @@ def _mask() -> torch.Tensor:
return torch.ones(1, 1, dtype=torch.long)
def _capture_tokenize_calls(pipeline: Any) -> list[dict[str, Any]]:
calls: list[dict[str, Any]] = []
def _tokenize(
text: str,
max_sequence_length: int,
use_system_prompt: bool = False,
system_prompt: str | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
index = len(calls) + 1
calls.append(
{
"text": text,
"max_sequence_length": max_sequence_length,
"use_system_prompt": use_system_prompt,
"system_prompt": system_prompt,
}
)
return _ids(index), torch.full((1, 1), index, dtype=torch.long)
pipeline._tokenize_prompt = _tokenize
return calls
@pytest.mark.parametrize(
("provided", "value", "default", "is_distilled", "expected"),
[
(False, 1.0, 7.0, False, 7.0),
(True, 1.0, 7.0, False, 1.0),
(True, 4.5, 7.0, False, 4.5),
(False, 1.0, 7.0, True, 1.0),
(True, 4.5, 7.0, True, 1.0),
],
)
def test_resolve_guidance_scale(
make_cosmos3_pipeline,
provided: bool,
value: float,
default: float,
is_distilled: bool,
expected: float,
) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_distilled_model = is_distilled
sp = make_sampling_params(
guidance_scale=value,
guidance_scale_provided=provided,
)
assert pipeline._resolve_guidance_scale(sp, default) == expected
def test_distilled_generation_accepts_t2i_and_i2v(make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_distilled_model = True
common = {
"action_enabled": False,
"transfer_config": None,
"is_v2v": False,
"sound_enabled": False,
}
pipeline._validate_distilled_generation_mode(
is_t2i=True,
image_tensor=None,
**common,
)
pipeline._validate_distilled_generation_mode(
is_t2i=False,
image_tensor=torch.zeros(1),
**common,
)
@pytest.mark.parametrize(
("overrides", "message"),
[
({"action_enabled": True}, "action requests are unsupported"),
({"transfer_config": SimpleNamespace()}, "transfer requests are unsupported"),
({"is_v2v": True}, "video-to-video requests are unsupported"),
({"sound_enabled": True}, "sound generation is unsupported"),
(
{"is_t2i": False, "image_tensor": None},
"text-to-video requests are unsupported",
),
(
{"is_t2i": True, "image_tensor": torch.zeros(1)},
"image-conditioned image generation is unsupported",
),
],
)
def test_distilled_generation_rejects_unsupported_modes(
make_cosmos3_pipeline,
overrides: dict[str, Any],
message: str,
) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_distilled_model = True
kwargs = {
"is_t2i": True,
"image_tensor": None,
"action_enabled": False,
"transfer_config": None,
"is_v2v": False,
"sound_enabled": False,
}
kwargs.update(overrides)
with pytest.raises(ValueError, match=message):
pipeline._validate_distilled_generation_mode(**kwargs)
def test_distilled_generation_rejects_robolab_policy(make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_distilled_model = True
with pytest.raises(ValueError, match="RoboLab/action policy requests are unsupported"):
pipeline._forward_robolab_policy(make_sampling_params(), None, 0.0)
@pytest.mark.parametrize(
("prompt", "sampling_params", "message"),
[
(
{"prompt": "x", "modalities": ["video"], "generate_sound": True},
make_sampling_params(),
"do not support sound generation",
),
(
{"prompt": "x", "modalities": ["video"]},
make_sampling_params(extra_args={"edge": {"control_path": "/tmp/control.mp4"}}),
"do not support transfer inference",
),
(
{
"prompt": "x",
"modalities": ["video"],
"additional_information": {
"preprocessed_video": torch.zeros(1, 3, 5, 16, 16),
},
},
make_sampling_params(height=16, width=16, num_frames=5),
"do not support video-to-video generation",
),
],
)
def test_edge_forward_rejects_unsupported_modes(
make_cosmos3_pipeline,
prompt: dict[str, Any],
sampling_params: SimpleNamespace,
message: str,
) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_edge_model = True
with pytest.raises(ValueError, match=message):
pipeline.forward(SimpleNamespace(prompts=[prompt], sampling_params=sampling_params))
def test_pipeline_registered_and_exported() -> None:
from vllm_omni.diffusion.cache.cache_dit_backend import CUSTOM_DIT_ENABLERS
from vllm_omni.diffusion.models import cosmos3
@@ -303,17 +479,45 @@ def test_pipeline_registered_and_exported() -> None:
assert issubclass(Cosmos3OmniDiffusersPipeline, nn.Module)
assert issubclass(Cosmos3OmniDiffusersPipeline, ProgressBarMixin)
assert Cosmos3OmniDiffusersPipeline.support_image_input is True
assert _DIFFUSION_MODELS["Cosmos3OmniDiffusersPipeline"] == (
"cosmos3",
"pipeline_cosmos3",
"Cosmos3OmniDiffusersPipeline",
)
assert _DIFFUSION_PRE_PROCESS_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_pre_process_func"
assert _DIFFUSION_POST_PROCESS_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_post_process_func"
assert _DIFFUSION_IR_OP_PRIORITY_FUNCS["Cosmos3OmniDiffusersPipeline"] == "get_cosmos3_ir_op_priority_func"
assert "Cosmos3OmniDiffusersPipeline" in CUSTOM_DIT_ENABLERS
assert "Cosmos3OmniDiffusersPipeline" in cosmos3.__all__
for pipeline_name in ("Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"):
assert _DIFFUSION_MODELS[pipeline_name] == (
"cosmos3",
"pipeline_cosmos3",
"Cosmos3OmniDiffusersPipeline",
)
assert _DIFFUSION_PRE_PROCESS_FUNCS[pipeline_name] == "get_cosmos3_pre_process_func"
assert _DIFFUSION_POST_PROCESS_FUNCS[pipeline_name] == "get_cosmos3_post_process_func"
assert _DIFFUSION_IR_OP_PRIORITY_FUNCS[pipeline_name] == "get_cosmos3_ir_op_priority_func"
assert pipeline_name in CUSTOM_DIT_ENABLERS
@pytest.mark.parametrize(
"pipeline_name",
["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"],
)
def test_cosmos3_model_index_resolves_pipeline(
tmp_path,
pipeline_name: str,
) -> None:
import json
from vllm_omni.diffusion.data import OmniDiffusionConfig
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
Cosmos3OmniDiffusersPipeline,
)
from vllm_omni.diffusion.registry import DiffusionModelRegistry
(tmp_path / "model_index.json").write_text(json.dumps({"_class_name": pipeline_name}))
config = OmniDiffusionConfig(model=str(tmp_path))
config.enrich_config()
assert config.model_class_name == pipeline_name
resolved_pipeline_cls = DiffusionModelRegistry._try_load_model_cls(config.model_class_name)
assert resolved_pipeline_cls is Cosmos3OmniDiffusersPipeline
@pytest.fixture
def stub_real_pipeline_init(monkeypatch: pytest.MonkeyPatch):
@@ -335,25 +539,24 @@ def stub_real_pipeline_init(monkeypatch: pytest.MonkeyPatch):
return self
class _StubDiffusersScheduler:
load_config_calls: list[dict[str, Any]] = []
from_config_calls: list[dict[str, Any]] = []
def __init__(self, *, flow_shift: float = 1.0, use_karras_sigmas: bool = True) -> None:
self.config = SimpleNamespace(flow_shift=flow_shift, use_karras_sigmas=use_karras_sigmas)
def __init__(self, *, flow_shift: float = 1.0) -> None:
self.config = SimpleNamespace(flow_shift=flow_shift)
@classmethod
def from_pretrained(cls, *args, **kwargs):
return cls()
def load_config(cls, *args, **kwargs):
cls.load_config_calls.append({"args": args, "kwargs": dict(kwargs)})
return {
"_class_name": "UniPCMultistepScheduler",
"flow_shift": 1.0,
}
@classmethod
def from_config(cls, config, **kwargs):
cls.from_config_calls.append({"config": config, "kwargs": dict(kwargs)})
return cls(
flow_shift=kwargs.get("flow_shift", getattr(config, "flow_shift", 1.0)),
use_karras_sigmas=kwargs.get(
"use_karras_sigmas",
getattr(config, "use_karras_sigmas", True),
),
)
return cls(flow_shift=kwargs.get("shift", config.get("flow_shift", 1.0)))
class _StubVideoProcessor:
def __init__(self, *args, **kwargs) -> None:
@@ -361,13 +564,18 @@ def stub_real_pipeline_init(monkeypatch: pytest.MonkeyPatch):
monkeypatch.setattr(pipeline_cosmos3, "AutoTokenizer", _StubAutoTokenizer)
monkeypatch.setattr(pipeline_cosmos3, "DistributedAutoencoderKLWan", _StubDiffusersVAE)
monkeypatch.setattr(pipeline_cosmos3, "UniPCMultistepScheduler", _StubDiffusersScheduler)
monkeypatch.setattr(pipeline_cosmos3, "FlowUniPCMultistepScheduler", _StubDiffusersScheduler)
monkeypatch.setattr(pipeline_cosmos3, "VideoProcessor", _StubVideoProcessor)
monkeypatch.setattr(pipeline_cosmos3, "get_local_device", lambda: torch.device("cpu"))
return _StubDiffusersScheduler
def _make_od_config(*, sound_gen: bool) -> SimpleNamespace:
def _make_od_config(
*,
sound_gen: bool,
tf_model_config_overrides: dict[str, Any] | None = None,
model_config: dict[str, Any] | None = None,
) -> SimpleNamespace:
tf_model_config = {
"hidden_size": 8,
"num_hidden_layers": 0,
@@ -382,6 +590,8 @@ def _make_od_config(*, sound_gen: bool) -> SimpleNamespace:
}
if sound_gen:
tf_model_config["sound_gen"] = True
if tf_model_config_overrides:
tf_model_config.update(tf_model_config_overrides)
return SimpleNamespace(
enable_cpu_offload=False,
enable_diffusion_pipeline_profiler=False,
@@ -390,7 +600,7 @@ def _make_od_config(*, sound_gen: bool) -> SimpleNamespace:
flow_shift=None,
quantization_config=None,
custom_pipeline_args={},
model_config={},
model_config=model_config or {},
tf_model_config=tf_model_config,
)
@@ -400,16 +610,13 @@ def test_pipeline_init_skips_tokenizer_when_sound_disabled(stub_real_pipeline_in
pipeline = Cosmos3OmniDiffusersPipeline(od_config=_make_od_config(sound_gen=False))
assert stub_real_pipeline_init.from_config_calls == []
assert pipeline._sound_tokenizer is None
assert pipeline.transformer.sound_gen is False
assert not hasattr(pipeline.transformer, "audio_proj_in")
assert not hasattr(pipeline.transformer, "audio_proj_out")
assert pipeline._base_scheduler_use_karras_sigmas is True
assert pipeline._current_scheduler_use_karras_sigmas is True
def test_pipeline_init_rebuilds_scheduler_with_cosmos3_defaults(stub_real_pipeline_init) -> None:
def test_pipeline_init_uses_flow_unipc_with_cosmos3_defaults(stub_real_pipeline_init) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
od_config = _make_od_config(sound_gen=False)
@@ -417,55 +624,302 @@ def test_pipeline_init_rebuilds_scheduler_with_cosmos3_defaults(stub_real_pipeli
pipeline = Cosmos3OmniDiffusersPipeline(od_config=od_config)
assert stub_real_pipeline_init.from_config_calls
call = stub_real_pipeline_init.from_config_calls[-1]
assert getattr(call["config"], "flow_shift") == 1.0
assert call["kwargs"] == {"flow_shift": 2.5}
assert len(stub_real_pipeline_init.load_config_calls) == 1
assert len(stub_real_pipeline_init.from_config_calls) == 1
call = stub_real_pipeline_init.from_config_calls[0]
assert call["config"]["_class_name"] == "UniPCMultistepScheduler"
assert call["kwargs"] == {
"shift": 1.0,
"use_dynamic_shifting": False,
"prediction_type": "flow_prediction",
}
assert pipeline.is_distilled_model is False
assert pipeline._engine_init_flow_shift == 2.5
assert pipeline._base_scheduler_use_karras_sigmas is True
def test_set_flow_shift_restores_checkpoint_scheduler_mode(
make_cosmos3_pipeline,
@pytest.mark.parametrize(
("scheduler_class_name", "expected_distilled"),
[
("FlowMatchEulerDiscreteScheduler", True),
("UniPCMultistepScheduler", False),
],
)
def test_pipeline_resolves_scheduler_class_from_checkpoint_file(
tmp_path,
stub_real_pipeline_init,
monkeypatch: pytest.MonkeyPatch,
scheduler_class_name: str,
expected_distilled: bool,
) -> None:
import json
from vllm_omni.diffusion.models.cosmos3 import pipeline_cosmos3
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
from vllm_omni.diffusion.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler as RealFlowUniPCMultistepScheduler,
)
class RecordingScheduler:
from_config_calls: list[dict[str, Any]] = []
t_list = [1.0, 0.75, 0.5, 0.25]
scheduler_dir = tmp_path / "scheduler"
scheduler_dir.mkdir()
scheduler_config = {"_class_name": scheduler_class_name}
if expected_distilled:
scheduler_config["fixed_step_sampler_config"] = {"sample_type": "sde", "t_list": t_list}
(scheduler_dir / "scheduler_config.json").write_text(json.dumps(scheduler_config))
def __init__(self, *, flow_shift: float, use_karras_sigmas: bool) -> None:
self.config = SimpleNamespace(flow_shift=flow_shift, use_karras_sigmas=use_karras_sigmas)
class StubFlowUniPCScheduler(StubScheduler):
from_config_calls: list[tuple[Any, dict[str, Any]]] = []
@classmethod
def load_config(cls, *args, **kwargs):
return RealFlowUniPCMultistepScheduler.load_config(*args, **kwargs)
@classmethod
def from_config(cls, config, **kwargs):
cls.from_config_calls.append({"config": config, "kwargs": dict(kwargs)})
return cls(
flow_shift=kwargs.get("flow_shift", getattr(config, "flow_shift", 1.0)),
use_karras_sigmas=kwargs.get(
"use_karras_sigmas",
getattr(config, "use_karras_sigmas", True),
),
)
cls.from_config_calls.append((config, dict(kwargs)))
return cls()
monkeypatch.setattr(pipeline_cosmos3, "UniPCMultistepScheduler", RecordingScheduler)
pipeline = make_cosmos3_pipeline()
class StubFlowMatchScheduler(StubScheduler):
from_config_calls: list[tuple[Any, dict[str, Any]]] = []
pipeline._set_flow_shift(10.0, use_karras_sigmas=False)
assert RecordingScheduler.from_config_calls[-1]["kwargs"] == {
"flow_shift": 10.0,
"use_karras_sigmas": False,
@classmethod
def from_config(cls, config, **kwargs):
cls.from_config_calls.append((config, dict(kwargs)))
scheduler = cls()
scheduler.config.fixed_step_sampler_config = config["fixed_step_sampler_config"]
return scheduler
monkeypatch.setattr(pipeline_cosmos3, "FlowUniPCMultistepScheduler", StubFlowUniPCScheduler)
monkeypatch.setattr(
pipeline_cosmos3,
"FlowMatchEulerDiscreteScheduler",
StubFlowMatchScheduler,
)
od_config = _make_od_config(sound_gen=False)
od_config.model = str(tmp_path)
pipeline = Cosmos3OmniDiffusersPipeline(od_config=od_config)
assert pipeline.is_distilled_model is expected_distilled
if expected_distilled:
assert len(StubFlowMatchScheduler.from_config_calls) == 1
assert StubFlowMatchScheduler.from_config_calls[0][1] == {"stochastic_sampling": True}
assert StubFlowUniPCScheduler.from_config_calls == []
assert pipeline._scheduler_init_t_list == t_list
else:
assert len(StubFlowUniPCScheduler.from_config_calls) == 1
assert StubFlowMatchScheduler.from_config_calls == []
assert not hasattr(pipeline, "_scheduler_init_t_list")
def test_distilled_pipeline_initializes_sde_scheduler(
stub_real_pipeline_init,
monkeypatch: pytest.MonkeyPatch,
) -> None:
from diffusers import FlowMatchEulerDiscreteScheduler
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
Cosmos3OmniDiffusersPipeline,
)
t_list = [1.0, 0.9375, 0.8333333333333334, 0.625]
scheduler_config = {
"_class_name": "FlowMatchEulerDiscreteScheduler",
"shift": 1.0,
"stochastic_sampling": False,
"fixed_step_sampler_config": {
"sample_type": "sde",
"t_list": t_list,
},
}
assert pipeline._current_flow_shift == 10.0
assert pipeline._current_scheduler_use_karras_sigmas is False
monkeypatch.setattr(
stub_real_pipeline_init,
"load_config",
classmethod(lambda cls, *args, **kwargs: scheduler_config),
)
pipeline._set_flow_shift(10.0)
assert RecordingScheduler.from_config_calls[-1]["kwargs"] == {"flow_shift": 10.0}
assert pipeline._current_flow_shift == 10.0
assert pipeline._current_scheduler_use_karras_sigmas is True
pipeline = Cosmos3OmniDiffusersPipeline(od_config=_make_od_config(sound_gen=False))
assert pipeline.is_distilled_model is True
assert isinstance(pipeline.scheduler, FlowMatchEulerDiscreteScheduler)
assert pipeline.scheduler.config.stochastic_sampling is True
assert pipeline._scheduler_init_t_list == t_list
pipeline._set_flow_shift(10.0)
assert len(RecordingScheduler.from_config_calls) == 2
@pytest.mark.parametrize(
("fixed_step_config", "error_pattern"),
[
pytest.param(
{"t_list": [1.0, 0.5]},
r"fixed_step_sampler_config\.sample_type=sde",
id="missing-sample-type",
),
pytest.param(
{"sample_type": "ode", "t_list": [1.0, 0.5]},
r"fixed_step_sampler_config\.sample_type=sde",
id="unsupported-sample-type",
),
pytest.param(
{"sample_type": "sde"},
r"non-empty fixed_step_sampler_config\.t_list",
id="missing-t-list",
),
pytest.param(
{"sample_type": "sde", "t_list": []},
r"non-empty fixed_step_sampler_config\.t_list",
id="empty-t-list",
),
],
)
def test_distilled_scheduler_validates_fixed_step_config(
stub_real_pipeline_init,
monkeypatch: pytest.MonkeyPatch,
fixed_step_config: dict[str, Any],
error_pattern: str,
) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
Cosmos3OmniDiffusersPipeline,
)
scheduler_config = {
"_class_name": "FlowMatchEulerDiscreteScheduler",
"fixed_step_sampler_config": fixed_step_config,
}
monkeypatch.setattr(
stub_real_pipeline_init,
"load_config",
classmethod(lambda cls, *args, **kwargs: scheduler_config),
)
with pytest.raises(ValueError, match=error_pattern):
Cosmos3OmniDiffusersPipeline(od_config=_make_od_config(sound_gen=False))
def test_pipeline_init_selects_edge_transformer_from_backbone_type(stub_real_pipeline_init) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import (
COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT,
Cosmos3OmniDiffusersPipeline,
)
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
COSMOS3_EDGE_BACKBONE_TYPE,
Cosmos3EdgeVFMTransformer,
)
pipeline = Cosmos3OmniDiffusersPipeline(
od_config=_make_od_config(
sound_gen=False,
tf_model_config_overrides={
"backbone_type": COSMOS3_EDGE_BACKBONE_TYPE,
"qk_norm_for_text": False,
"latent_channel": 48,
"latent_patch_size": 2,
"temporal_compression_factor": 4,
"layer_norm_epsilon": 1e-5,
},
)
)
assert isinstance(pipeline.transformer, Cosmos3EdgeVFMTransformer)
assert pipeline.is_edge_model is True
assert pipeline._engine_init_flow_shift == COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT
def test_pipeline_init_does_not_select_edge_from_model_index_class_name(stub_real_pipeline_init) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3VFMTransformer
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import Cosmos3EdgeVFMTransformer
pipeline = Cosmos3OmniDiffusersPipeline(
od_config=_make_od_config(
sound_gen=False,
model_config={"_class_name": "Cosmos3EdgeOmniDiffusersPipeline"},
)
)
assert isinstance(pipeline.transformer, Cosmos3VFMTransformer)
assert not isinstance(pipeline.transformer, Cosmos3EdgeVFMTransformer)
def test_flow_unipc_reuses_scheduler_and_forwards_each_request_shift(make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
scheduler = pipeline.scheduler
assert pipeline._engine_init_flow_shift == 10.0
assert pipeline._current_flow_shift == 10.0
assert scheduler.config.flow_shift == 1.0
for shift in (3.0, 10.0):
pipeline._set_flow_shift(shift)
pipeline._set_timesteps(4, torch.device("cpu"), shift=pipeline._current_flow_shift)
assert pipeline.scheduler is scheduler
assert pipeline._current_flow_shift == 10.0
assert [call["shift"] for call in scheduler.set_timesteps_calls] == [3.0, 10.0]
assert all(call["num_inference_steps"] == 4 for call in scheduler.set_timesteps_calls)
assert all(call["sigmas"] is None for call in scheduler.set_timesteps_calls)
def test_flow_unipc_reproducible_with_same_seed(make_cosmos3_pipeline) -> None:
from vllm_omni.diffusion.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler,
)
pipeline = make_cosmos3_pipeline()
pipeline.scheduler = FlowUniPCMultistepScheduler(
shift=1.0,
use_dynamic_shifting=False,
prediction_type="flow_prediction",
)
pipeline._format_and_tokenize_prompts = lambda *args, **kwargs: (
_ids(1),
_mask(),
_ids(0),
_mask(),
)
def run(seed: int) -> torch.Tensor:
request = make_request_batch(
{"prompt": "A test video.", "modalities": ["video"]},
make_sampling_params(
seed=seed,
guidance_scale=1.0,
guidance_scale_provided=True,
num_inference_steps=4,
num_frames=5,
height=16,
width=16,
extra_args={"flow_shift": 3.0},
),
)
output = pipeline.forward(request)
return output.output["video"]
first = run(123)
second = run(123)
different_seed = run(456)
torch.testing.assert_close(first, second)
assert not torch.equal(first, different_seed)
def test_distilled_set_timesteps_uses_fixed_sigma_schedule(make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
pipeline.is_distilled_model = True
pipeline._scheduler_init_t_list = [1.0, 0.75, 0.5, 0.25]
pipeline._set_timesteps(
num_inference_steps=99,
device=torch.device("cpu"),
shift=7.0,
)
assert pipeline.scheduler.set_timesteps_calls == [
{
"num_inference_steps": None,
"device": torch.device("cpu"),
"shift": None,
"sigmas": pipeline._scheduler_init_t_list,
}
]
def test_pipeline_init_passes_tokenizer_attrs_into_transformer(
@@ -806,12 +1260,35 @@ def test_ir_op_priority_hook_preserves_platform_fields(monkeypatch: pytest.Monke
assert merged.custom_op == ["platform_kernel", "native"]
def test_prompt_formatting_and_checkpoint_key_remap(make_cosmos3_pipeline) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
def test_format_and_tokenize_prompts_leaves_plain_prompts_unchanged_by_default(make_cosmos3_pipeline) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import COSMOS3_SYSTEM_PROMPT
pipeline = make_cosmos3_pipeline()
captured: list[str] = []
pipeline._tokenize_prompt = lambda text, *args, **kwargs: (captured.append(text) or _ids(len(captured)), _mask())
calls = _capture_tokenize_calls(pipeline)
result = pipeline._format_and_tokenize_prompts(
" A robot. ",
" bad. ",
num_frames=48,
frame_rate=24,
height=720,
width=1280,
max_sequence_length=32,
sp=SimpleNamespace(extra_args={}),
use_system_prompt=False,
is_t2i=False,
)
assert [call["text"] for call in calls] == ["A robot.", "bad."]
assert all(call["max_sequence_length"] == 32 for call in calls)
assert all(call["use_system_prompt"] is False for call in calls)
assert all(call["system_prompt"] == COSMOS3_SYSTEM_PROMPT for call in calls)
assert [tensor.item() for tensor in result] == [1, 1, 2, 2]
def test_format_and_tokenize_prompts_applies_video_templates_and_system_override(make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
calls = _capture_tokenize_calls(pipeline)
pipeline._format_and_tokenize_prompts(
"A robot",
@@ -821,21 +1298,167 @@ def test_prompt_formatting_and_checkpoint_key_remap(make_cosmos3_pipeline) -> No
height=720,
width=1280,
max_sequence_length=32,
sp=SimpleNamespace(
system_prompt="direct system prompt",
extra_args={
"use_duration_template": True,
"use_resolution_template": True,
"system_prompt": "API system prompt",
},
),
use_system_prompt=True,
is_t2i=False,
)
assert calls == [
{
"text": ("A robot. The video is 2.0 seconds long and is of 24 FPS. This video is of 720x1280 resolution."),
"max_sequence_length": 32,
"use_system_prompt": True,
"system_prompt": "API system prompt",
},
{
"text": (
"bad. The video is not 2.0 seconds long and is not of 24 FPS. This video is not of 720x1280 resolution."
),
"max_sequence_length": 32,
"use_system_prompt": True,
"system_prompt": "API system prompt",
},
]
def test_format_and_tokenize_prompts_uses_image_templates_for_t2i(make_cosmos3_pipeline) -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import COSMOS3_T2I_SYSTEM_PROMPT
pipeline = make_cosmos3_pipeline()
calls = _capture_tokenize_calls(pipeline)
pipeline._format_and_tokenize_prompts(
"A robot",
"bad",
num_frames=1,
frame_rate=24,
height=1024,
width=768,
max_sequence_length=64,
sp=SimpleNamespace(
extra_args={
"negative_metadata_mode": "inverse",
# Duration/resolution metadata templates are off by default
# (commit "Change resolution and duration templates to off by
# default"); enable them explicitly to exercise the formatting.
"use_duration_template": True,
"use_resolution_template": True,
}
),
use_system_prompt=True,
is_t2i=True,
)
assert [call["text"] for call in calls] == [
"A robot. This image is of 1024x768 resolution.",
"bad. This image is not of 1024x768 resolution.",
]
assert all("seconds" not in call["text"] and "FPS" not in call["text"] for call in calls)
assert all(call["system_prompt"] == COSMOS3_T2I_SYSTEM_PROMPT for call in calls)
def test_format_and_tokenize_prompts_rewrites_json_object_metadata(make_cosmos3_pipeline) -> None:
import json
pipeline = make_cosmos3_pipeline()
calls = _capture_tokenize_calls(pipeline)
pipeline._format_and_tokenize_prompts(
('{"caption": "A robot", "duration": "old", "fps": 1, "resolution": {"H": 1, "W": 2}, "aspect_ratio": "1:1"}'),
"bad",
num_frames=48,
frame_rate=24,
height=720,
width=1280,
max_sequence_length=32,
sp=SimpleNamespace(
extra_args={
"aspect_ratio": "16:9",
"use_duration_template": True,
"use_resolution_template": True,
}
),
is_t2i=False,
)
assert "The video is 2.0 seconds long" in captured[0]
assert "The video is not 2.0 seconds long" in captured[1]
assert json.loads(calls[0]["text"]) == {
"caption": "A robot",
"duration": "2s",
"fps": 24.0,
"resolution": {"H": 720, "W": 1280},
"aspect_ratio": "16:9",
}
assert "The video is 2.0 seconds long" not in calls[0]["text"]
assert calls[1]["text"] == (
"bad. The video is not 2.0 seconds long and is not of 24 FPS. This video is not of 720x1280 resolution."
)
def test_format_and_tokenize_prompts_removes_video_metadata_from_t2i_json(make_cosmos3_pipeline) -> None:
import json
pipeline = make_cosmos3_pipeline()
calls = _capture_tokenize_calls(pipeline)
pipeline._format_and_tokenize_prompts(
'{"caption": "A robot", "duration": "2s", "fps": 24}',
"",
num_frames=1,
frame_rate=24,
height=1024,
width=768,
max_sequence_length=32,
sp=SimpleNamespace(extra_args={}),
is_t2i=True,
)
assert json.loads(calls[0]["text"]) == {
"caption": "A robot",
"resolution": {"H": 1024, "W": 768},
}
@pytest.mark.parametrize(
"prompt",
[
"{malformed",
'["A robot"]',
],
)
def test_format_and_tokenize_prompts_falls_back_for_non_object_json(
make_cosmos3_pipeline,
prompt: str,
) -> None:
pipeline = make_cosmos3_pipeline()
calls = _capture_tokenize_calls(pipeline)
pipeline._format_and_tokenize_prompts(
prompt,
"",
num_frames=48,
frame_rate=24,
height=720,
width=1280,
max_sequence_length=32,
sp=SimpleNamespace(
extra_args={
"use_duration_template": True,
"use_resolution_template": True,
}
),
is_t2i=False,
)
assert calls[0]["text"] == (
f"{prompt}. The video is 2.0 seconds long and is of 24 FPS. This video is of 720x1280 resolution."
)
def test_checkpoint_key_remap() -> None:
from vllm_omni.diffusion.models.cosmos3.pipeline_cosmos3 import Cosmos3OmniDiffusersPipeline
remaps = {
"embed_tokens.weight": "transformer.language_model.embed_tokens.weight",
@@ -847,9 +1470,14 @@ def test_prompt_formatting_and_checkpoint_key_remap(make_cosmos3_pipeline) -> No
"layers.3.self_attn.to_q.weight": "transformer.language_model.layers.3.self_attn.to_q.weight",
"layers.3.self_attn.to_out.weight": "transformer.language_model.layers.3.self_attn.to_out.weight",
"layers.3.self_attn.norm_q.weight": "transformer.language_model.layers.3.self_attn.norm_q.weight",
"layers.3.self_attn.k_norm_und_for_gen.weight": (
"transformer.language_model.layers.3.self_attn.k_norm_und_for_gen.weight"
),
"layers.3.self_attn.add_q_proj.weight": "transformer.gen_layers.3.cross_attention.to_q.weight",
"layers.3.self_attn.to_add_out.weight": "transformer.gen_layers.3.cross_attention.to_out.weight",
"layers.3.self_attn.norm_added_q.weight": "transformer.gen_layers.3.cross_attention.norm_q.weight",
"layers.3.mlp_moe_gen.up_proj.weight": "transformer.gen_layers.3.mlp.up_proj.weight",
"layers.3.mlp_moe_gen.down_proj.weight": "transformer.gen_layers.3.mlp.down_proj.weight",
"transformer.model.layers.3.self_attn.add_k_proj.weight": (
"transformer.gen_layers.3.cross_attention.to_k.weight"
),
@@ -1193,9 +1821,9 @@ def test_forward_transfer_uses_source_fps_except_wsm(make_cosmos3_pipeline, hint
pipeline._prepare_transfer_latents = fake_prepare
pipeline.diffuse_transfer = fake_diffuse_transfer
def fake_set_flow_shift(target, *, use_karras_sigmas=None):
def fake_set_flow_shift(target):
captured.setdefault("flow_shifts", []).append(target)
captured.setdefault("scheduler_use_karras_sigmas", []).append(use_karras_sigmas)
pipeline._current_flow_shift = float(target)
pipeline._set_flow_shift = fake_set_flow_shift
pipeline._decode_latents = lambda latents: torch.zeros(1, 3, 5, 16, 16, device="meta")
@@ -1230,8 +1858,8 @@ def test_forward_transfer_uses_source_fps_except_wsm(make_cosmos3_pipeline, hint
assert captured["format_frame_rate"] == expected_fps
assert captured["shared_kwargs"]["fps"] == expected_fps
assert captured["flow_shifts"] == [10.0]
# Transfer uses the V2V flow-sigma schedule (karras off) so flow_shift applies.
assert captured["scheduler_use_karras_sigmas"] == [False]
# Transfer applies the V2V flow shift when building its timestep schedule.
assert [call["shift"] for call in pipeline.scheduler.set_timesteps_calls] == [10.0]
assert output.output["metadata"]["video"]["fps"] == expected_fps
assert output.output["payload"]["video"].device.type == "meta"
@@ -1391,9 +2019,9 @@ class TestForwardRouting:
pipeline._format_and_tokenize_prompts = fake_format
pipeline._prepare_latents = fake_prepare
def fake_set_flow_shift(target, *, use_karras_sigmas=None):
def fake_set_flow_shift(target):
captured.setdefault("flow_shifts", []).append(target)
captured.setdefault("scheduler_use_karras_sigmas", []).append(use_karras_sigmas)
pipeline._current_flow_shift = float(target)
pipeline._set_flow_shift = fake_set_flow_shift
pipeline.diffuse = fake_diffuse
@@ -1420,7 +2048,7 @@ class TestForwardRouting:
{
"key": "video",
"is_t2i": False,
"flow": [1.0],
"flow": [10.0],
"steps": [35],
"frames": 189,
},
@@ -1443,8 +2071,8 @@ class TestForwardRouting:
assert captured["format"]["is_t2i"] is expected["is_t2i"]
assert captured["format"]["num_frames"] == expected["frames"]
assert captured["flow_shifts"] == expected["flow"]
assert captured["scheduler_use_karras_sigmas"] == [None]
assert [call[0] for call in pipeline.scheduler.set_timesteps_calls] == expected["steps"]
assert [call["num_inference_steps"] for call in pipeline.scheduler.set_timesteps_calls] == expected["steps"]
assert all(call["shift"] == expected["flow"][0] for call in pipeline.scheduler.set_timesteps_calls)
def test_forward_i2v_sound_and_action_routes(self, make_cosmos3_pipeline) -> None:
pipeline = make_cosmos3_pipeline()
@@ -1491,7 +2119,6 @@ class TestForwardRouting:
)
)
assert captured["flow_shifts"][-1] == 10.0
assert captured["scheduler_use_karras_sigmas"][-1] is False
assert captured["format"]["negative_prompt"] == ""
assert captured["diffuse_calls"][-1]["shared_kwargs"]["noisy_frame_mask"] is v2v_mask
assert captured["diffuse_calls"][-1]["condition_latents"] is v2v_condition
@@ -15,6 +15,25 @@ from vllm_omni.platforms import current_omni_platform
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
@pytest.fixture(autouse=True)
def _single_rank_tensor_parallel(monkeypatch: pytest.MonkeyPatch) -> None:
"""Provide the TP metadata required by vLLM parallel linear layers."""
from vllm.model_executor import parameter
from vllm.model_executor.layers import linear
from vllm_omni.diffusion.models.cosmos3 import (
transformer_cosmos3,
transformer_cosmos3_edge,
)
monkeypatch.setattr(linear, "get_tensor_model_parallel_rank", lambda: 0)
monkeypatch.setattr(linear, "get_tensor_model_parallel_world_size", lambda: 1)
monkeypatch.setattr(parameter, "get_tensor_model_parallel_rank", lambda: 0)
monkeypatch.setattr(parameter, "get_tensor_model_parallel_world_size", lambda: 1)
monkeypatch.setattr(transformer_cosmos3, "get_tensor_model_parallel_world_size", lambda: 1)
monkeypatch.setattr(transformer_cosmos3_edge, "get_tensor_model_parallel_world_size", lambda: 1)
@pytest.fixture
def accelerator_device() -> torch.device:
"""Provide an accelerator device, skipping when none is available."""
@@ -40,6 +59,22 @@ def _tiny_cosmos3_config(**overrides):
return config
def _tiny_cosmos3_edge_config(**overrides):
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
COSMOS3_EDGE_BACKBONE_TYPE,
)
config = _tiny_cosmos3_config(
latent_patch_size=2,
latent_channel=48,
temporal_compression_factor=4,
backbone_type=COSMOS3_EDGE_BACKBONE_TYPE,
qk_norm_for_text=False,
)
config.update(overrides)
return config
def test_mrope_position_ids_cover_text_video_sound_and_action() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import (
compute_mrope_position_ids_action,
@@ -78,6 +113,16 @@ def test_mrope_position_ids_cover_text_video_sound_and_action() -> None:
assert action_offset == 14
def test_timestep_embedder_stores_frequencies_in_fp32() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import (
TimestepEmbedder,
)
embedder = TimestepEmbedder(hidden_size=8, frequency_embedding_size=16)
assert embedder.freqs.dtype == torch.float32
@pytest.mark.parametrize(
("key", "value"),
[
@@ -97,30 +142,245 @@ def test_validate_supported_config_rejects_unsupported_flags(key: str, value) ->
Cosmos3VFMTransformer._validate_supported_config(None)
def test_edge_config_resolves_nemotron_defaults() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
)
model = Cosmos3EdgeVFMTransformer(
SimpleNamespace(
tf_model_config=_tiny_cosmos3_edge_config(
layer_norm_epsilon=1e-5,
rope_scaling={},
rope_parameters={"mrope_section": [24, 20, 20]},
),
dtype=torch.float32,
)
)
assert model.rms_norm_eps == 1e-5
assert model.rope_theta == 100_000_000
assert model.mrope_section == [24, 20, 20]
assert model.latent_channel_size == 48
assert model.latent_patch_size == 2
assert model.temporal_compression_factor == 4
def test_edge_config_requires_backbone_type() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
)
config = _tiny_cosmos3_edge_config()
config.pop("backbone_type")
with pytest.raises(ValueError, match="must declare backbone_type"):
Cosmos3EdgeVFMTransformer(
SimpleNamespace(
tf_model_config=config,
dtype=torch.float32,
)
)
def test_edge_und_and_gen_use_relu2_and_no_und_qk_norm() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
Cosmos3Relu2MLP,
)
model = Cosmos3EdgeVFMTransformer(
SimpleNamespace(
tf_model_config=_tiny_cosmos3_edge_config(
num_hidden_layers=1,
use_k_norm_und_for_gen=False,
),
dtype=torch.float32,
)
)
layer = model.language_model.layers[0]
gen_layer = model.gen_layers[0]
assert isinstance(layer.mlp, Cosmos3Relu2MLP)
assert not hasattr(layer.mlp, "gate_proj")
assert isinstance(gen_layer.mlp, Cosmos3Relu2MLP)
assert not hasattr(gen_layer.mlp, "gate_proj")
assert not hasattr(layer.self_attn, "norm_q")
assert not hasattr(layer.self_attn, "norm_k")
assert layer.self_attn.k_norm_und_for_gen is None
def test_edge_creates_gen_k_norm_when_enabled() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import RMSNorm
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
)
model = Cosmos3EdgeVFMTransformer(
SimpleNamespace(
tf_model_config=_tiny_cosmos3_edge_config(
num_hidden_layers=1,
qk_norm_for_diffusion=True,
use_k_norm_und_for_gen=True,
),
dtype=torch.float32,
)
)
k_norm = model.language_model.layers[0].self_attn.k_norm_und_for_gen
assert model.use_k_norm_und_for_gen is True
assert isinstance(k_norm, RMSNorm)
assert isinstance(model.gen_layers[0].cross_attention.norm_q, RMSNorm)
assert isinstance(model.gen_layers[0].cross_attention.norm_k, RMSNorm)
def test_edge_supports_no_gen_qk_norm_variant() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
)
model = Cosmos3EdgeVFMTransformer(
SimpleNamespace(
tf_model_config=_tiny_cosmos3_edge_config(
num_hidden_layers=1,
qk_norm_for_diffusion=False,
use_k_norm_und_for_gen=False,
),
dtype=torch.float32,
)
)
assert model.qk_norm_for_diffusion is False
assert model.language_model.layers[0].self_attn.k_norm_und_for_gen is None
assert model.gen_layers[0].cross_attention.qk_norm is False
assert not hasattr(model.gen_layers[0].cross_attention, "norm_q")
assert not hasattr(model.gen_layers[0].cross_attention, "norm_k")
def test_edge_validates_required_relu2_weights() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeVFMTransformer,
)
model = object.__new__(Cosmos3EdgeVFMTransformer)
nn.Module.__init__(model)
model.num_hidden_layers = 1
model.qk_norm_for_diffusion = True
model.use_k_norm_und_for_gen = True
complete = {
"transformer.language_model.layers.0.mlp.up_proj.weight",
"transformer.language_model.layers.0.mlp.down_proj.weight",
"transformer.language_model.layers.0.self_attn.k_norm_und_for_gen.weight",
"transformer.gen_layers.0.mlp.up_proj.weight",
"transformer.gen_layers.0.mlp.down_proj.weight",
}
model.validate_loaded_weights(complete)
mlp_weights = complete - {
"transformer.language_model.layers.0.self_attn.k_norm_und_for_gen.weight",
}
for missing_weight in mlp_weights:
with pytest.raises(ValueError, match="missing required weights"):
model.validate_loaded_weights(complete - {missing_weight})
missing_k_norm = complete - {"transformer.language_model.layers.0.self_attn.k_norm_und_for_gen.weight"}
with pytest.raises(ValueError, match=r"self_attn\.k_norm_und_for_gen"):
model.validate_loaded_weights(missing_k_norm)
model.use_k_norm_und_for_gen = False
model.validate_loaded_weights(missing_k_norm)
def test_edge_gen_cached_k_is_normalized_but_reasoner_uses_raw_k() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import RMSNorm
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeCausalAttention,
)
class ScaleProjection(nn.Module):
def __init__(self, scale: float) -> None:
super().__init__()
self.scale = scale
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x * self.scale
class IdentityOutput(nn.Module):
def forward(self, x: torch.Tensor) -> torch.Tensor:
return x
class RawKAttention(nn.Module):
def forward(self, q: torch.Tensor, k: torch.Tensor, v: torch.Tensor) -> torch.Tensor:
del v
return q + k
attn = object.__new__(Cosmos3EdgeCausalAttention)
nn.Module.__init__(attn)
attn.hidden_size = 4
attn.num_heads = 2
attn.num_kv_heads = 2
attn.head_dim = 2
attn.num_heads_local = 2
attn.num_kv_heads_local = 2
attn.to_q = ScaleProjection(1.0)
attn.to_k = ScaleProjection(1.0)
attn.to_v = ScaleProjection(1.0)
attn.to_out = IdentityOutput()
attn.k_norm_und_for_gen = RMSNorm(2, eps=1e-5)
attn.attn = RawKAttention()
hidden_states = torch.tensor(
[[[1.0, 2.0, 3.0, 4.0], [2.0, 1.0, 4.0, 3.0]]],
dtype=torch.float32,
)
freqs_cos = torch.ones(1, 2, 1, 2)
freqs_sin = torch.zeros(1, 2, 1, 2)
reasoner_out, gen_k, gen_v = attn(hidden_states, freqs_cos, freqs_sin)
attn.to_k.scale = 3.0
scaled_reasoner_out, scaled_gen_k, scaled_gen_v = attn(hidden_states, freqs_cos, freqs_sin)
torch.testing.assert_close(gen_k, scaled_gen_k)
torch.testing.assert_close(gen_v, scaled_gen_v)
assert not torch.allclose(reasoner_out, scaled_reasoner_out)
def test_transformer_sharding_offload_and_patch_round_trip_contracts() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3LanguageModel, Cosmos3VFMTransformer
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3_edge import (
Cosmos3EdgeLanguageModel,
Cosmos3EdgeVFMTransformer,
)
model = object.__new__(Cosmos3VFMTransformer)
nn.Module.__init__(model)
model.language_model = nn.Module()
model.language_model.layers = nn.ModuleList([nn.Linear(2, 2) for _ in range(2)])
model.gen_layers = nn.ModuleList([nn.Linear(2, 2)])
model.norm_moe_gen = nn.LayerNorm(2)
for transformer_cls, language_model_cls in (
(Cosmos3VFMTransformer, Cosmos3LanguageModel),
(Cosmos3EdgeVFMTransformer, Cosmos3EdgeLanguageModel),
):
model = object.__new__(transformer_cls)
nn.Module.__init__(model)
model.language_model = nn.Module()
model.language_model.layers = nn.ModuleList([nn.Linear(2, 2) for _ in range(2)])
model.gen_layers = nn.ModuleList([nn.Linear(2, 2)])
model.norm_moe_gen = nn.LayerNorm(2)
matched = [
name
for name, module in model.named_modules()
if any(condition(name, module) for condition in model._hsdp_shard_conditions)
]
assert matched == ["language_model.layers.0", "language_model.layers.1", "gen_layers.0"]
assert Cosmos3VFMTransformer._layerwise_offload_blocks_attrs == ["gen_layers"]
assert Cosmos3LanguageModel._layerwise_offload_blocks_attrs == ["layers"]
assert Cosmos3VFMTransformer._repeated_blocks == ["Cosmos3GenDecoderLayer"]
matched = [
name
for name, module in model.named_modules()
if any(condition(name, module) for condition in model._hsdp_shard_conditions)
]
assert matched == ["language_model.layers.0", "language_model.layers.1", "gen_layers.0"]
assert transformer_cls._layerwise_offload_blocks_attrs == ["gen_layers"]
assert language_model_cls._layerwise_offload_blocks_attrs == ["layers"]
assert transformer_cls._repeated_blocks == ["Cosmos3GenDecoderLayer"]
model.latent_patch_size = 2
model.latent_channel_size = 3
latents = torch.arange(1 * 3 * 1 * 3 * 5, dtype=torch.float32).reshape(1, 3, 1, 3, 5)
torch.testing.assert_close(model.unpatchify(model.patchify(latents, t=1, h=3, w=5), t=1, h=3, w=5), latents)
model.latent_patch_size = 2
model.latent_channel_size = 3
latents = torch.arange(1 * 3 * 1 * 3 * 5, dtype=torch.float32).reshape(1, 3, 1, 3, 5)
torch.testing.assert_close(
model.unpatchify(model.patchify(latents, t=1, h=3, w=5), t=1, h=3, w=5),
latents,
)
def test_forward_returns_video_prediction(monkeypatch: pytest.MonkeyPatch) -> None:
@@ -142,6 +402,17 @@ def test_forward_returns_video_prediction(monkeypatch: pytest.MonkeyPatch) -> No
assert tuple(output.shape) == (1, 2, 1, 2, 2)
def test_qwen_gen_mlp_remains_gated() -> None:
from vllm_omni.diffusion.models.cosmos3.transformer_cosmos3 import Cosmos3GatedMLP, Cosmos3VFMTransformer
model = Cosmos3VFMTransformer(
SimpleNamespace(tf_model_config=_tiny_cosmos3_config(num_hidden_layers=1), dtype=torch.float32)
)
assert isinstance(model.gen_layers[0].mlp, Cosmos3GatedMLP)
assert hasattr(model.gen_layers[0].mlp, "gate_proj")
def test_model_cpu_offload_swaps_back_to_generator(monkeypatch: pytest.MonkeyPatch) -> None:
from vllm_omni.diffusion.models.cosmos3 import transformer_cosmos3
@@ -42,8 +42,6 @@ def _server_args() -> list[str]:
return [
"--num-gpus",
"1",
"--model-class-name",
"Cosmos3OmniDiffusersPipeline",
"--stage-init-timeout",
"900",
"--init-timeout",
@@ -48,6 +48,7 @@ _DEPLOY_CONFIG = {
"trust_remote_code": True,
"devices": "0,1,2,3", # set dynamically by _write_deploy_config
"vae_use_slicing": False,
"moe_backend": "flashinfer_cutlass",
"vae_use_tiling": False,
"parallel_config": {
"pipeline_parallel_size": 1,
+9 -7
View File
@@ -60,8 +60,9 @@ def test_sensenova_extra_registry_declares_request_and_response_params() -> None
@pytest.mark.core_model
@pytest.mark.cpu
def test_cosmos3_extra_registry_declares_request_and_response_params() -> None:
assert get_extra_body_params("Cosmos3OmniDiffusersPipeline") == frozenset(
@pytest.mark.parametrize("pipeline_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
def test_cosmos3_extra_registry_declares_request_and_response_params(pipeline_name: str) -> None:
assert get_extra_body_params(pipeline_name) == frozenset(
{
"flow_shift",
"max_sequence_length",
@@ -98,7 +99,7 @@ def test_cosmos3_extra_registry_declares_request_and_response_params() -> None:
"session_id",
}
)
assert get_extra_output_params("Cosmos3OmniDiffusersPipeline") == frozenset(
assert get_extra_output_params(pipeline_name) == frozenset(
{
"action",
"raw_action_dim",
@@ -106,7 +107,7 @@ def test_cosmos3_extra_registry_declares_request_and_response_params() -> None:
"action_mode",
}
)
assert should_init_extra_args_for_non_diffusion_stages("Cosmos3OmniDiffusersPipeline") is False
assert should_init_extra_args_for_non_diffusion_stages(pipeline_name) is False
@pytest.mark.core_model
@@ -224,9 +225,10 @@ def test_ming_flash_omni_image_to_image_prompt_builder() -> None:
@pytest.mark.core_model
@pytest.mark.cpu
def test_cosmos3_text_to_image_prompt_builder_selects_image_modality() -> None:
@pytest.mark.parametrize("pipeline_name", ["Cosmos3OmniDiffusersPipeline", "Cosmos3OmniPipeline"])
def test_cosmos3_text_to_image_prompt_builder_selects_image_modality(pipeline_name: str) -> None:
assert build_text_to_image_prompt(
"Cosmos3OmniDiffusersPipeline",
pipeline_name,
prompt="a red sports car at golden hour",
negative_prompt="blurry, distorted",
height=1024,
@@ -237,7 +239,7 @@ def test_cosmos3_text_to_image_prompt_builder_selects_image_modality() -> None:
"negative_prompt": "blurry, distorted",
}
assert build_text_to_image_prompt(
"Cosmos3OmniDiffusersPipeline",
pipeline_name,
prompt="a red sports car",
negative_prompt=None,
) == {"prompt": "a red sports car", "modalities": ["image"]}
@@ -71,6 +71,7 @@ EXCLUDED_MODELS = [
"OmniVoicePipeline",
"OmniVoice",
"Cosmos3OmniDiffusersPipeline",
"Cosmos3OmniPipeline",
"DiffusersAdapterPipeline",
"HiDreamImagePipeline",
"DreamZeroPipeline",
+1
View File
@@ -469,6 +469,7 @@ class _DiffusionConfigProjection:
diffusion_kv_cache_skip_layers: str | list[int] | tuple[int, ...] | set[int] | None = None
diffusion_kv_cache_skip_step_indices: set[int] | None = None
diffusion_kv_cache_skip_layer_indices: set[int] | None = None
moe_backend: str = "auto"
force_cutlass_fp8: bool = False
enable_diffusion_pipeline_profiler: bool = False
step_execution: bool = False
+1
View File
@@ -1016,6 +1016,7 @@ CUSTOM_DIT_ENABLERS.update(
"Wan22VACEPipeline": enable_cache_for_wan22,
"Wan22S2VPipeline": enable_cache_for_wan22_s2v,
"Cosmos3OmniDiffusersPipeline": enable_cache_for_cosmos3,
"Cosmos3OmniPipeline": enable_cache_for_cosmos3,
"Krea2Pipeline": enable_cache_for_krea2,
}
)
+3
View File
@@ -664,6 +664,9 @@ class OmniDiffusionConfig:
# STA_mode: STA_Mode = STA_Mode.STA_INFERENCE
skip_time_steps: int = 15
# MoE kernel backend selection
moe_backend: str = "auto"
# Compilation
enforce_eager: bool = False
@@ -11,7 +11,12 @@ from vllm.model_executor.utils import get_packed_modules_mapping
logger = init_logger(__name__)
MODEL_OPT_SCALE_SUFFIXES = (".input_scale", ".weight_scale", ".weight_scale_2", ".weight_scale_inv")
MODEL_OPT_SCALE_SUFFIXES = (
".input_scale",
".weight_scale",
".weight_scale_2",
".weight_scale_inv",
)
DEFAULT_PACKED_MODULES_MAPPING = {
"to_qkv": ("to_q", "to_k", "to_v"),
"add_kv_proj": ("add_q_proj", "add_k_proj", "add_v_proj"),
@@ -122,6 +127,7 @@ class ModelOptFp8CheckpointAdapter:
for candidate in self._weights_mapper.apply_list([name]):
if candidate != name and candidate in self._loadable_tensors:
# Preserve shard names so downstream packed-weight loaders can route them.
return candidate, name
return None, name
@@ -178,6 +184,7 @@ class ModelOptFp8CheckpointAdapter:
def _handle_scale_tensor(
self,
name: str,
output_name: str,
tensor: torch.Tensor,
target_name: str | None,
state: _AdaptState,
@@ -186,7 +193,7 @@ class ModelOptFp8CheckpointAdapter:
if target_name is None:
state.skipped_scales += 1
else:
yield name, tensor
yield output_name, tensor
yield from self._flush_pending_weights(name, state)
def _target_dtype_for_dequantization(
@@ -249,7 +256,7 @@ class ModelOptFp8CheckpointAdapter:
for name, tensor in weights:
target_name, output_name = self._resolve_target_and_output_names(name)
if self._is_scale(name):
yield from self._handle_scale_tensor(name, tensor, target_name, state)
yield from self._handle_scale_tensor(name, output_name, tensor, target_name, state)
continue
target_dtype = self._target_dtype_for_dequantization(tensor, target_name)
@@ -264,6 +271,8 @@ class ModelOptFp8CheckpointAdapter:
class ModelOptNvFp4CheckpointAdapter(ModelOptFp8CheckpointAdapter):
_PRE_QUANT_SCALE_SUFFIX = ".pre_quant_scale"
@staticmethod
def _is_checkpoint_quant_config(quant_config: object | None) -> bool:
return (
@@ -273,6 +282,22 @@ class ModelOptNvFp4CheckpointAdapter(ModelOptFp8CheckpointAdapter):
and bool(getattr(quant_config, "is_checkpoint_nvfp4_serialized", False))
)
def adapt(
self,
weights: Iterable[tuple[str, torch.Tensor]],
) -> Generator[tuple[str, torch.Tensor], None, None]:
def validated_weights() -> Generator[tuple[str, torch.Tensor], None, None]:
for name, tensor in weights:
if name.endswith(self._PRE_QUANT_SCALE_SUFFIX):
raise ValueError(
f"ModelOpt NVFP4 checkpoint tensor {name!r} is unsupported: "
"vLLM 0.25.0 does not consume pre_quant_scale. Export the checkpoint "
"with pre-quant scales folded into the weights."
)
yield name, tensor
yield from super().adapt(validated_weights())
class ModelOptMixedPrecisionCheckpointAdapter(ModelOptFp8CheckpointAdapter):
@staticmethod
@@ -7,10 +7,12 @@ from .pipeline_cosmos3 import (
get_cosmos3_pre_process_func,
)
from .transformer_cosmos3 import Cosmos3VFMTransformer
from .transformer_cosmos3_edge import Cosmos3EdgeVFMTransformer
__all__ = [
"Cosmos3OmniDiffusersPipeline",
"get_cosmos3_post_process_func",
"get_cosmos3_pre_process_func",
"Cosmos3VFMTransformer",
"Cosmos3EdgeVFMTransformer",
]
@@ -29,6 +29,7 @@ is produced from sound latents rather than from ``multi_modal_data["audio"]``.
from __future__ import annotations
import json
import math
import os
import time
@@ -39,7 +40,7 @@ from typing import Any, ClassVar
import numpy as np
import PIL.Image
import torch
from diffusers import UniPCMultistepScheduler
from diffusers import FlowMatchEulerDiscreteScheduler
from diffusers.utils.torch_utils import randn_tensor
from diffusers.video_processor import VideoProcessor
from torch import nn
@@ -61,6 +62,9 @@ from vllm_omni.diffusion.models.interface import (
SupportsComponentDiscovery,
)
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin, _is_rank_zero
from vllm_omni.diffusion.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler,
)
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
from vllm_omni.diffusion.request import OmniDiffusionRequest
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
@@ -94,7 +98,8 @@ from .transfer import (
transfer_max_frames_from_extra_args,
uint8_cthw_to_normalized_5d,
)
from .transformer_cosmos3 import Cosmos3VFMTransformer, resolve_sound_gen
from .transformer_cosmos3 import Cosmos3VFMTransformer, _tf_config_get, resolve_sound_gen
from .transformer_cosmos3_edge import COSMOS3_EDGE_BACKBONE_TYPE, Cosmos3EdgeVFMTransformer
from .utils import (
COSMOS3_DEFAULT_CONDITION_FRAME_INDEXES_VISION,
COSMOS3_VAE_TEMPORAL_COMPRESSION,
@@ -154,6 +159,7 @@ COSMOS3_T2V_DEFAULT_WIDTH = 1280
COSMOS3_T2V_DEFAULT_NUM_FRAMES = 189
COSMOS3_T2V_DEFAULT_NUM_INFERENCE_STEPS = 35
COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE = 6.0
COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT = 10.0
COSMOS3_T2I_DEFAULT_HEIGHT = 1024
COSMOS3_T2I_DEFAULT_WIDTH = 1024
@@ -162,12 +168,73 @@ COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE = 7.0
COSMOS3_T2I_DEFAULT_FLOW_SHIFT = 3.0
COSMOS3_T2I_DEFAULT_GUIDANCE_INTERVAL: tuple[float, float] = (400.0, 1000.0)
COSMOS3_EDGE_T2V_DEFAULT_HEIGHT = 480
COSMOS3_EDGE_T2V_DEFAULT_WIDTH = 832
COSMOS3_EDGE_T2V_DEFAULT_GUIDANCE_SCALE = 5.0
COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT = 3.0
COSMOS3_EDGE_T2I_DEFAULT_HEIGHT = 640
COSMOS3_EDGE_T2I_DEFAULT_WIDTH = 640
COSMOS3_DISTILLED_CHECKPOINT_SCHEDULER_CLASS = "FlowMatchEulerDiscreteScheduler"
# Truncation cap on the prompt token count (shared by T2I and T2V). Prompts
# are tokenized to their natural length (no padding); this only bounds the
# UND pathway / GEN cross-attention cost for pathologically long prompts.
COSMOS3_DEFAULT_MAX_SEQUENCE_LENGTH = 4096
def _ceil_video_num_frames(num_frames: int, temporal_compression_factor: int) -> int:
"""Round a video length up to the causal VAE's ``factor * k + 1`` grid."""
if temporal_compression_factor <= 0:
raise ValueError(f"Cosmos3 temporal_compression_factor must be positive, got {temporal_compression_factor}.")
if num_frames <= 1:
return num_frames
latent_frames = math.ceil((num_frames - 1) / temporal_compression_factor)
return latent_frames * temporal_compression_factor + 1
def _format_json_object_prompt(
prompt: str,
*,
num_frames: int,
frame_rate: float,
height: int,
width: int,
aspect_ratio: str | None,
) -> str | None:
"""Match Imaginaire4 metadata injection for JSON-object prompts."""
try:
prompt_obj = json.loads(prompt)
except (json.JSONDecodeError, TypeError, ValueError):
return None
if not isinstance(prompt_obj, dict):
return None
metadata: dict[str, Any] = {}
if num_frames > 1:
duration_seconds = int(num_frames / frame_rate) if frame_rate > 0 else 0
metadata.update({"duration": f"{duration_seconds}s", "fps": float(frame_rate)})
else:
prompt_obj.pop("duration", None)
prompt_obj.pop("fps", None)
metadata["resolution"] = {"H": int(height), "W": int(width)}
if aspect_ratio is not None:
metadata["aspect_ratio"] = aspect_ratio
prompt_obj.update(metadata)
return json.dumps(prompt_obj)
def resolve_cosmos3_transformer_cls(model_config: Any) -> type[Cosmos3VFMTransformer]:
"""Select the Cosmos3 transformer implementation from transformer/config.json."""
backbone_type = _tf_config_get(model_config, "backbone_type", None)
if backbone_type is None:
return Cosmos3VFMTransformer
if backbone_type == COSMOS3_EDGE_BACKBONE_TYPE:
return Cosmos3EdgeVFMTransformer
raise ValueError(f"Unsupported Cosmos3 transformer backbone_type={backbone_type!r}.")
# ---------------------------------------------------------------------------
# Post-process function (registered in registry.py)
# ---------------------------------------------------------------------------
@@ -190,6 +257,9 @@ def get_cosmos3_pre_process_func(od_config: OmniDiffusionConfig):
from .guardrails import check_text_safety, ensure_initialized, is_guardrails_enabled
video_processor = VideoProcessor(vae_scale_factor=16)
is_edge_model = (
_tf_config_get(getattr(od_config, "tf_model_config", None), "backbone_type", None) == COSMOS3_EDGE_BACKBONE_TYPE
)
# Eager-load guardrail models at pipeline build time when the server-level
# gate is on. Per-request overrides only decide whether the loaded models
# are *invoked* — they cannot turn checks on without a server-side preload.
@@ -410,12 +480,17 @@ def get_cosmos3_pre_process_func(od_config: OmniDiffusionConfig):
extra = _extra_args(request)
transfer_requested = action_mode is None and has_transfer_hints(extra)
# Auto-calculate H/W from aspect ratio (720p max area)
# Resolve missing H/W.
if transfer_requested:
_set_transfer_size_from_image(request, image)
elif request.sampling_params.height is None or request.sampling_params.width is None:
if action_mode is not None:
_set_action_size_from_image(request, image)
elif is_edge_model:
if request.sampling_params.height is None:
request.sampling_params.height = COSMOS3_EDGE_T2V_DEFAULT_HEIGHT
if request.sampling_params.width is None:
request.sampling_params.width = COSMOS3_EDGE_T2V_DEFAULT_WIDTH
else:
max_area = 720 * 1280
aspect_ratio = image.height / image.width
@@ -823,27 +898,52 @@ class Cosmos3OmniDiffusersPipeline(
sound_latent_fps = self._sound_tokenizer.latent_fps
# --- Transformer (weights loaded later via weights_sources) ---
self.transformer = Cosmos3VFMTransformer(
transformer_cls = resolve_cosmos3_transformer_cls(od_config.tf_model_config)
self.transformer = transformer_cls(
od_config=od_config,
temporal_compression_factor=self.vae_scale_factor_temporal,
sound_gen=sound_gen,
sound_dim=sound_dim,
sound_latent_fps=sound_latent_fps,
)
self.is_edge_model = transformer_cls is Cosmos3EdgeVFMTransformer
# --- Scheduler ---
# Load from checkpoint to preserve solver_order, timestep_spacing,
# beta_schedule, sigma bounds, flow_shift, etc. Only override
# flow_shift when explicitly requested by the user.
self.scheduler = UniPCMultistepScheduler.from_pretrained(
# Distilled model differs from regular one only by scheduler,
# distilled one uses FlowMatchEulerDiscreteScheduler, while
# regular should use FlowUniPCMultistepScheduler
scheduler_config = FlowUniPCMultistepScheduler.load_config(
model_path,
subfolder="scheduler",
local_files_only=local_files_only,
)
if od_config.flow_shift is not None:
self.scheduler = UniPCMultistepScheduler.from_config(
self.scheduler.config,
flow_shift=od_config.flow_shift,
scheduler_class_name = scheduler_config.get("_class_name")
self.is_distilled_model = False
if scheduler_class_name == COSMOS3_DISTILLED_CHECKPOINT_SCHEDULER_CLASS:
fixed_step_config = scheduler_config.get("fixed_step_sampler_config")
if not isinstance(fixed_step_config, dict) or fixed_step_config.get("sample_type") != "sde":
raise ValueError("Cosmos3 distilled scheduler requires fixed_step_sampler_config.sample_type=sde.")
t_list = fixed_step_config.get("t_list")
if not isinstance(t_list, list) or not t_list:
raise ValueError("Cosmos3 distilled scheduler requires a non-empty fixed_step_sampler_config.t_list.")
self.scheduler = FlowMatchEulerDiscreteScheduler.from_config(
scheduler_config,
stochastic_sampling=True,
)
self._scheduler_init_t_list = list(t_list)
self.is_distilled_model = True
else:
# Preserve compatible solver settings from the checkpoint, but keep
# the base shift neutral. The concrete request shift is applied when
# FlowUniPC builds its timesteps.
self.scheduler = FlowUniPCMultistepScheduler.from_config(
scheduler_config,
shift=1.0,
use_dynamic_shifting=False,
prediction_type="flow_prediction",
)
self._cpu_scheduler_state()
@@ -862,14 +962,16 @@ class Cosmos3OmniDiffusersPipeline(
),
]
# Snapshot the loaded scheduler config so we can rebuild the
# scheduler at request time when a per-request flow_shift override
# is supplied (T2I uses shift=3.0; T2V/I2V use the engine default).
self._base_scheduler_config = self.scheduler.config
self._engine_init_flow_shift = float(getattr(self.scheduler.config, "flow_shift", 1.0) or 1.0)
# An engine-level override becomes the video default; otherwise use
# the matching regular/Edge default. Per-request values still take
# precedence.
default_video_flow_shift = (
COSMOS3_EDGE_VIDEO_DEFAULT_FLOW_SHIFT if self.is_edge_model else COSMOS3_VIDEO_DEFAULT_FLOW_SHIFT
)
self._engine_init_flow_shift = float(
od_config.flow_shift if od_config.flow_shift is not None else default_video_flow_shift
)
self._current_flow_shift = self._engine_init_flow_shift
self._base_scheduler_use_karras_sigmas = self._scheduler_use_karras_sigmas(self.scheduler.config)
self._current_scheduler_use_karras_sigmas = self._base_scheduler_use_karras_sigmas
self._guidance_scale = None
self._num_timesteps = None
@@ -989,6 +1091,7 @@ class Cosmos3OmniDiffusersPipeline(
"self_attn.to_out.": f"{und_lp}.self_attn.to_out.",
"self_attn.norm_q.": f"{und_lp}.self_attn.norm_q.",
"self_attn.norm_k.": f"{und_lp}.self_attn.norm_k.",
"self_attn.k_norm_und_for_gen.": f"{und_lp}.self_attn.k_norm_und_for_gen.",
# GEN attention
"self_attn.add_q_proj.": f"{gen_lp}.cross_attention.to_q.",
"self_attn.add_k_proj.": f"{gen_lp}.cross_attention.to_k.",
@@ -1054,6 +1157,7 @@ class Cosmos3OmniDiffusersPipeline(
loaded = loader.load_weights(_remapped_weights())
self.transformer.post_load_weights()
self.transformer.eval()
self.transformer.validate_loaded_weights(loaded)
if getattr(self.transformer, "sound_gen", False):
sound_markers = ("audio_proj_in.", "audio_proj_out.", "audio_modality_embed")
missing = [marker.rstrip(".") for marker in sound_markers if not any(marker in name for name in loaded)]
@@ -1359,6 +1463,8 @@ class Cosmos3OmniDiffusersPipeline(
inputs: RoboLabPolicyInputs,
pipeline_start: float,
) -> DiffusionOutput:
if self.is_distilled_model:
raise self._distilled_unsupported_error("RoboLab/action policy requests are unsupported.")
if not getattr(self.transformer, "action_gen", False):
raise ValueError(
"Cosmos3 RoboLab policy serving was requested, but the transformer "
@@ -1469,6 +1575,7 @@ class Cosmos3OmniDiffusersPipeline(
guidance_interval=None,
raw_action_dim=raw_action_dim,
scheduler=scheduler,
generator=generator,
)
if _is_rank_zero():
@@ -1560,38 +1667,34 @@ class Cosmos3OmniDiffusersPipeline(
raise ValueError(f"Incorrect modality value in {modalities}, expected one of {accepted_modalities}.")
return "image" in modalities
def _set_timesteps(self, num_inference_steps: int, device: str | torch.device, shift: float) -> None:
if self.is_distilled_model:
self.scheduler.set_timesteps(sigmas=self._scheduler_init_t_list, device=device)
else:
self.scheduler.set_timesteps(
num_inference_steps,
device=device,
shift=shift,
)
def _set_flow_shift(self, target_shift: float) -> None:
"""Select the shift applied by FlowUniPC when timesteps are built."""
self._current_flow_shift = float(target_shift)
@staticmethod
def _scheduler_use_karras_sigmas(config: Any) -> bool | None:
value = getattr(config, "use_karras_sigmas", None)
return None if value is None else bool(value)
def _resolve_seed(sp: OmniDiffusionSamplingParams, generator: torch.Generator | None, default: int = 42) -> int:
if sp.seed is not None:
return int(sp.seed)
if isinstance(generator, torch.Generator):
return int(generator.initial_seed())
return int(default)
def _set_flow_shift(self, target_shift: float, *, use_karras_sigmas: bool | None = None) -> None:
"""Set UniPC scheduler mode for a concrete request.
The scheduler is rebuilt from the saved base config if
the target differs from the current shift or Karras-sigma mode.
Tracking explicit scheduler state is required because the previous
mode may have rebuilt the scheduler - we cannot rely on
``self.scheduler.config`` reflecting the last requested target if a
rebuild was skipped via the equality check.
"""
target = float(target_shift)
target_use_karras_sigmas = (
self._base_scheduler_use_karras_sigmas if use_karras_sigmas is None else bool(use_karras_sigmas)
)
if (
target == float(self._current_flow_shift)
and target_use_karras_sigmas == self._current_scheduler_use_karras_sigmas
):
return
scheduler_kwargs: dict[str, Any] = {"flow_shift": target}
if use_karras_sigmas is not None:
scheduler_kwargs["use_karras_sigmas"] = bool(use_karras_sigmas)
self.scheduler = UniPCMultistepScheduler.from_config(self._base_scheduler_config, **scheduler_kwargs)
self._cpu_scheduler_state()
self._current_flow_shift = target
self._current_scheduler_use_karras_sigmas = self._scheduler_use_karras_sigmas(self.scheduler.config)
def _resolve_guidance_scale(self, sp: OmniDiffusionSamplingParams, default: float) -> float:
if self.is_distilled_model:
return 1.0
if sp.guidance_scale_provided:
return float(sp.guidance_scale)
return float(default)
def _cpu_scheduler_state(self) -> None:
# We need to move scheduler tensors to CPU, as unipc from diffusers assumes they are on CPU.
@@ -1612,6 +1715,53 @@ class Cosmos3OmniDiffusersPipeline(
def num_timesteps(self):
return self._num_timesteps
@staticmethod
def _distilled_unsupported_error(detail: str) -> ValueError:
return ValueError(
f"Cosmos3 distilled checkpoints support only text-to-image and image-to-video generation; {detail}"
)
def _validate_distilled_generation_mode(
self,
*,
is_t2i: bool,
image_tensor: torch.Tensor | None,
action_enabled: bool,
transfer_config: Cosmos3TransferConfig | None,
is_v2v: bool,
sound_enabled: bool,
) -> None:
if not self.is_distilled_model:
return
if action_enabled:
raise self._distilled_unsupported_error("action requests are unsupported.")
if transfer_config is not None:
raise self._distilled_unsupported_error("transfer requests are unsupported.")
if is_v2v:
raise self._distilled_unsupported_error("video-to-video requests are unsupported.")
if sound_enabled:
raise self._distilled_unsupported_error("sound generation is unsupported.")
if not is_t2i and image_tensor is None:
raise self._distilled_unsupported_error("text-to-video requests are unsupported.")
if is_t2i and image_tensor is not None:
raise self._distilled_unsupported_error("image-conditioned image generation is unsupported.")
def _validate_edge_generation_mode(
self,
*,
transfer_config: Cosmos3TransferConfig | None,
is_v2v: bool,
sound_enabled: bool,
) -> None:
if not self.is_edge_model:
return
if sound_enabled:
raise ValueError("Cosmos3 Edge checkpoints do not support sound generation.")
if transfer_config is not None:
raise ValueError("Cosmos3 Edge checkpoints do not support transfer inference.")
if is_v2v:
raise ValueError("Cosmos3 Edge checkpoints do not support video-to-video generation.")
# -- Prompt formatting -----------------------------------------------------
@staticmethod
@@ -1628,6 +1778,10 @@ class Cosmos3OmniDiffusersPipeline(
"""
Append duration and resolution metadata to a prompt.
"""
prompt = prompt.strip()
if duration_template is None and resolution_template is None:
return prompt
parts: list[str] = []
head = prompt.rstrip(".").strip()
if head:
@@ -1903,15 +2057,26 @@ class Cosmos3OmniDiffusersPipeline(
res_tmpl = COSMOS3_IMAGE_RESOLUTION_TEMPLATE if is_t2i else COSMOS3_RESOLUTION_TEMPLATE
else:
res_tmpl = None
prompt = self._apply_metadata_templates(
json_prompt = _format_json_object_prompt(
prompt,
num_frames,
frame_rate,
height,
width,
duration_template=dur_tmpl,
resolution_template=res_tmpl,
num_frames=num_frames,
frame_rate=frame_rate,
height=height,
width=width,
aspect_ratio=self._get_sp_param(sp, "aspect_ratio", None),
)
if json_prompt is not None:
prompt = json_prompt
else:
prompt = self._apply_metadata_templates(
prompt,
num_frames,
frame_rate,
height,
width,
duration_template=dur_tmpl,
resolution_template=res_tmpl,
)
if _is_rank_zero():
logger.info("Final prompt: '%s'", prompt)
@@ -2272,6 +2437,7 @@ class Cosmos3OmniDiffusersPipeline(
guidance_interval: tuple[float, float] | None = None,
raw_action_dim: int | None = None,
scheduler: Any | None = None,
generator: torch.Generator | None = None,
) -> torch.Tensor | tuple[torch.Tensor, ...]:
"""Denoising loop with 3-mode CFG support (parallel, sequential, none).
@@ -2383,11 +2549,23 @@ class Cosmos3OmniDiffusersPipeline(
if raw_action_dim is not None and 0 < raw_action_dim < action_pred.shape[-1]:
action_pred[..., raw_action_dim:] = 0
if action_latents is None and sound_latents is None:
latents = step_scheduler.step(video_pred, t, latents, return_dict=False)[0]
latents = step_scheduler.step(
video_pred,
t,
latents,
generator=generator,
return_dict=False,
)[0]
else:
packed_noise, shapes, numels = _pack_joint(video_pred, action_pred, sound_pred)
packed_latents, _, _ = _pack_joint(latents, action_latents, sound_latents)
packed_next = step_scheduler.step(packed_noise, t, packed_latents, return_dict=False)[0]
packed_next = step_scheduler.step(
packed_noise,
t,
packed_latents,
generator=generator,
return_dict=False,
)[0]
unpacked = _unpack_joint(packed_next, shapes, numels)
latents = unpacked[0]
idx = 1
@@ -2613,6 +2791,7 @@ class Cosmos3OmniDiffusersPipeline(
velocity_mask: torch.Tensor,
condition_latents: torch.Tensor,
guidance_interval: tuple[float, float] | None = None,
generator: torch.Generator | None = None,
) -> torch.Tensor:
def _active_at(t: torch.Tensor, interval: tuple[float, float] | None) -> bool:
if interval is None:
@@ -2724,7 +2903,13 @@ class Cosmos3OmniDiffusersPipeline(
if isinstance(noise_pred, tuple):
raise ValueError("Cosmos3 transfer diffusion expects video-only tensor predictions.")
noise_pred = noise_pred * velocity_mask
latents = self.scheduler.step(noise_pred, t, latents, return_dict=False)[0]
latents = self.scheduler.step(
noise_pred,
t,
latents,
generator=generator,
return_dict=False,
)[0]
latents = velocity_mask * latents + (1.0 - velocity_mask) * condition_latents
finally:
self._cosmos3_branch_caches = None
@@ -2741,6 +2926,8 @@ class Cosmos3OmniDiffusersPipeline(
transfer_video_tensor: torch.Tensor | None,
transfer_input_fps: float | None,
) -> DiffusionOutput:
if self.is_distilled_model:
raise self._distilled_unsupported_error("transfer requests are unsupported.")
input_frames = None
if transfer_video_tensor is not None:
input_frames = normalized_video_to_uint8_cthw(transfer_video_tensor)
@@ -2806,7 +2993,7 @@ class Cosmos3OmniDiffusersPipeline(
guidance_scale = (
float(transfer_config.guidance_scale)
if transfer_config.guidance_scale is not None
else float(sp.guidance_scale or COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE)
else self._resolve_guidance_scale(sp, COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE)
)
flow_shift_target = float(
transfer_config.flow_shift
@@ -2821,11 +3008,11 @@ class Cosmos3OmniDiffusersPipeline(
self._guidance_scale = guidance_scale
self._num_timesteps = num_inference_steps
self._set_flow_shift(flow_shift_target, use_karras_sigmas=False)
self._set_flow_shift(flow_shift_target)
generator = sp.generator
seed = self._resolve_seed(sp, generator)
if generator is None:
seed = sp.seed if sp.seed is not None else 42
generator = torch.Generator(device=self.device).manual_seed(seed)
cond_ids, cond_mask, uncond_ids, uncond_mask = self._format_and_tokenize_prompts(
@@ -2915,7 +3102,11 @@ class Cosmos3OmniDiffusersPipeline(
transfer_share_vision_temporal_positions=transfer_config.share_vision_temporal_positions,
)
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
self._set_timesteps(
num_inference_steps,
device=self.device,
shift=self._current_flow_shift,
)
latents = self.diffuse_transfer(
latents=latents,
timesteps=self.scheduler.timesteps,
@@ -2930,6 +3121,7 @@ class Cosmos3OmniDiffusersPipeline(
shared_kwargs=shared_kwargs,
velocity_mask=velocity_mask,
condition_latents=condition_latents,
generator=generator,
)
output_video = self._decode_latents(latents).clamp(-1, 1)
previous_output = output_video
@@ -3009,6 +3201,20 @@ class Cosmos3OmniDiffusersPipeline(
action_enabled = action_mode is not None
transfer_config = resolve_transfer_config(sp, prompt_data)
action_video_tensor = video_tensor if action_enabled else None
is_v2v = video_tensor is not None and not is_t2i and not action_enabled
self._validate_distilled_generation_mode(
is_t2i=is_t2i,
image_tensor=image_tensor,
action_enabled=action_enabled,
transfer_config=transfer_config,
is_v2v=is_v2v,
sound_enabled=sound_enabled,
)
self._validate_edge_generation_mode(
transfer_config=transfer_config,
is_v2v=is_v2v,
sound_enabled=sound_enabled,
)
if transfer_config is not None:
if is_t2i:
raise ValueError("Cosmos3 transfer inference is supported only for video outputs.")
@@ -3056,31 +3262,36 @@ class Cosmos3OmniDiffusersPipeline(
raise ValueError("Cosmos3 non-action generation accepts either image or video input, not both.")
if video_tensor is not None and is_t2i:
raise ValueError("Cosmos3 video-to-video generation is supported only for video outputs.")
is_v2v = video_tensor is not None and not is_t2i and not action_enabled
# T2I and T2V share the same model + forward path; only defaults
# differ:
# T2I: 1024x1024, 50 steps, shift=3.0, guidance_interval=[400, 1000]
# T2V: 720x1280, 35 steps, shift=engine-init, no interval
# T2I and T2V share the same model and forward path; their defaults are:
# T2I: regular 1024x1024, Edge 640x640; 50 steps, shift=3.0,
# guidance_interval=[400, 1000]
# T2V: 189 frames and 35 steps; regular guidance=6, shift=10;
# Edge guidance=5, shift=3;
# no guidance interval
if is_t2i:
height = sp.height or COSMOS3_T2I_DEFAULT_HEIGHT
width = sp.width or COSMOS3_T2I_DEFAULT_WIDTH
height = sp.height or (
COSMOS3_EDGE_T2I_DEFAULT_HEIGHT if self.is_edge_model else COSMOS3_T2I_DEFAULT_HEIGHT
)
width = sp.width or (COSMOS3_EDGE_T2I_DEFAULT_WIDTH if self.is_edge_model else COSMOS3_T2I_DEFAULT_WIDTH)
num_frames = 1
num_inference_steps = sp.num_inference_steps or COSMOS3_T2I_DEFAULT_NUM_INFERENCE_STEPS
guidance_scale = sp.guidance_scale if sp.guidance_scale else COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE
guidance_scale = self._resolve_guidance_scale(sp, COSMOS3_T2I_DEFAULT_GUIDANCE_SCALE)
default_flow_shift = COSMOS3_T2I_DEFAULT_FLOW_SHIFT
default_guidance_interval: tuple[float, float] | None = COSMOS3_T2I_DEFAULT_GUIDANCE_INTERVAL
batch_size = max(1, int(sp.num_outputs_per_prompt or 1))
else:
height = sp.height or COSMOS3_T2V_DEFAULT_HEIGHT
width = sp.width or COSMOS3_T2V_DEFAULT_WIDTH
height = sp.height or (
COSMOS3_EDGE_T2V_DEFAULT_HEIGHT if self.is_edge_model else COSMOS3_T2V_DEFAULT_HEIGHT
)
width = sp.width or (COSMOS3_EDGE_T2V_DEFAULT_WIDTH if self.is_edge_model else COSMOS3_T2V_DEFAULT_WIDTH)
default_guidance_scale = (
COSMOS3_EDGE_T2V_DEFAULT_GUIDANCE_SCALE if self.is_edge_model else COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE
)
num_frames = sp.num_frames or COSMOS3_T2V_DEFAULT_NUM_FRAMES
num_inference_steps = sp.num_inference_steps or COSMOS3_T2V_DEFAULT_NUM_INFERENCE_STEPS
guidance_scale = sp.guidance_scale if sp.guidance_scale else COSMOS3_T2V_DEFAULT_GUIDANCE_SCALE
# Fall back to the engine-init shift, NOT None: passing None
# to ``_set_flow_shift`` would leak a prior T2I rebuild
# (shift=3.0) into a subsequent video request.
default_flow_shift = COSMOS3_V2V_DEFAULT_FLOW_SHIFT if is_v2v else self._engine_init_flow_shift
guidance_scale = self._resolve_guidance_scale(sp, default_guidance_scale)
default_flow_shift = self._engine_init_flow_shift
default_guidance_interval = None
batch_size = 1 # Existing video pipeline assumes B=1.
@@ -3103,9 +3314,23 @@ class Cosmos3OmniDiffusersPipeline(
f"or action_chunk_size + 1; got num_frames={num_frames}, action_chunk_size={action_chunk_size}."
)
num_inference_steps = sp.num_inference_steps or 30
guidance_scale = sp.guidance_scale if sp.guidance_scale is not None else 1.0
guidance_scale = self._resolve_guidance_scale(sp, 1.0)
default_flow_shift = 5.0
if not is_t2i and not action_enabled:
requested_num_frames = int(num_frames)
num_frames = _ceil_video_num_frames(
requested_num_frames,
self.vae_scale_factor_temporal,
)
if num_frames != requested_num_frames and _is_rank_zero():
logger.info(
"Rounded Cosmos3 num_frames from %d to %d for temporal compression factor %d.",
requested_num_frames,
num_frames,
self.vae_scale_factor_temporal,
)
domain_id = None
if action_enabled:
domain_id = resolve_domain_id(
@@ -3146,13 +3371,12 @@ class Cosmos3OmniDiffusersPipeline(
self._guidance_scale = guidance_scale
self._num_timesteps = num_inference_steps
# Always resolve to a concrete target shift for this request, then
# update the shared Diffusers scheduler.
self._set_flow_shift(flow_shift_target, use_karras_sigmas=False if is_v2v else None)
# Always resolve to a concrete target shift for this request.
self._set_flow_shift(flow_shift_target)
generator = sp.generator
seed = self._resolve_seed(sp, generator)
if generator is None:
seed = sp.seed if sp.seed is not None else 42
generator = torch.Generator(device=self.device).manual_seed(seed)
# --- Format prompts & tokenize (B=1; reused across loop iterations
@@ -3293,7 +3517,11 @@ class Cosmos3OmniDiffusersPipeline(
)
def _run_diffusion(start_latents):
self.scheduler.set_timesteps(num_inference_steps, device=self.device)
self._set_timesteps(
num_inference_steps,
device=self.device,
shift=self._current_flow_shift,
)
scheduler = self.scheduler
return self.diffuse(
latents=start_latents,
@@ -3314,6 +3542,7 @@ class Cosmos3OmniDiffusersPipeline(
guidance_interval=guidance_interval,
raw_action_dim=raw_action_dim,
scheduler=scheduler,
generator=generator,
)
if is_t2i and batch_size > 1:
@@ -396,7 +396,6 @@ class TimestepEmbedder(nn.Module):
hidden_size: int,
frequency_embedding_size: int = 256,
max_period: int = 10000,
target_dtype: torch.dtype = torch.bfloat16,
) -> None:
super().__init__()
# Following diffusers naming pattern here for checkpoint compatibility.
@@ -407,7 +406,7 @@ class TimestepEmbedder(nn.Module):
self.hidden_size = hidden_size
half = frequency_embedding_size // 2
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=target_dtype) / half)
freqs = torch.exp(-math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half)
self.register_buffer("freqs", freqs, persistent=False)
def forward(self, t: torch.Tensor) -> torch.Tensor:
@@ -587,6 +586,7 @@ class Cosmos3CrossAttention(nn.Module):
head_dim: int,
rms_norm_eps: float,
quant_config: QuantizationConfig | None = None,
qk_norm: bool = True,
prefix: str = "",
) -> None:
super().__init__()
@@ -636,8 +636,10 @@ class Cosmos3CrossAttention(nn.Module):
prefix=f"{prefix}.to_out",
)
self.norm_q = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.norm_k = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.qk_norm = qk_norm
if self.qk_norm:
self.norm_q = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.norm_k = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.attn = FrameworkAttention(
num_heads=self.num_heads,
@@ -721,8 +723,9 @@ class Cosmos3CrossAttention(nn.Module):
v = self.to_v(hidden_states).view(B, S_gen, self.num_kv_heads_local, self.head_dim)
# Per-head QK norm
q = F.rms_norm(q, (self.head_dim,), self.norm_q.weight, eps=self.norm_q.variance_epsilon)
k = F.rms_norm(k, (self.head_dim,), self.norm_k.weight, eps=self.norm_k.variance_epsilon)
if self.qk_norm:
q = F.rms_norm(q, (self.head_dim,), self.norm_q.weight, eps=self.norm_q.variance_epsilon)
k = F.rms_norm(k, (self.head_dim,), self.norm_k.weight, eps=self.norm_k.variance_epsilon)
# Qwen3-style RoPE
q, k = _apply_rotary_pos_emb(q, k, freqs_cos, freqs_sin)
@@ -806,6 +809,8 @@ class Cosmos3GenDecoderLayer(nn.Module):
head_dim: int,
rms_norm_eps: float,
quant_config: QuantizationConfig | None = None,
mlp_cls: type[nn.Module] = Cosmos3GatedMLP,
qk_norm: bool = True,
prefix: str = "",
) -> None:
super().__init__()
@@ -817,11 +822,12 @@ class Cosmos3GenDecoderLayer(nn.Module):
head_dim=head_dim,
rms_norm_eps=rms_norm_eps,
quant_config=quant_config,
qk_norm=qk_norm,
prefix=f"{prefix}.cross_attention",
)
self.input_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.post_attention_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.mlp = Cosmos3GatedMLP(
self.mlp = mlp_cls(
hidden_size=hidden_size,
intermediate_size=intermediate_size,
quant_config=quant_config,
@@ -996,6 +1002,9 @@ class Cosmos3VFMTransformer(nn.Module):
packed_modules_mapping = {}
_language_model_cls = Cosmos3LanguageModel
_gen_mlp_cls = Cosmos3GatedMLP
@staticmethod
def _is_transformer_block(name: str, module) -> bool:
return ("gen_layers" in name or "language_model.layers" in name) and name.split(".")[-1].isdigit()
@@ -1032,6 +1041,25 @@ class Cosmos3VFMTransformer(nn.Module):
if actual != expected:
raise ValueError(f"Unsupported Cosmos3 transformer config: {key}={actual!r}; expected {expected!r}.")
@classmethod
def _resolve_rms_norm_eps(cls, model_config: Any) -> float:
return float(_tf_config_get(model_config, "rms_norm_eps", 1e-6))
@classmethod
def _resolve_rope_theta(cls, model_config: Any) -> float:
return float(_tf_config_get(model_config, "rope_theta", 5_000_000))
@classmethod
def _resolve_mrope_section(cls, model_config: Any) -> list[int]:
rope_scaling = _tf_config_get(model_config, "rope_scaling", {}) or {}
return list(rope_scaling.get("mrope_section", [24, 20, 20]))
def _language_model_kwargs(self) -> dict[str, Any]:
return {}
def validate_loaded_weights(self, loaded: set[str]) -> None:
del loaded
def __init__(
self,
od_config: OmniDiffusionConfig,
@@ -1044,7 +1072,6 @@ class Cosmos3VFMTransformer(nn.Module):
super().__init__()
model_config = od_config.tf_model_config
self._validate_supported_config(model_config)
rope_scaling = _tf_config_get(model_config, "rope_scaling", {}) or {}
self.hidden_size = int(_tf_config_get(model_config, "hidden_size", 4096))
self.num_hidden_layers = int(_tf_config_get(model_config, "num_hidden_layers", 36))
@@ -1053,9 +1080,10 @@ class Cosmos3VFMTransformer(nn.Module):
self.head_dim = int(_tf_config_get(model_config, "head_dim", 128))
self.intermediate_size = int(_tf_config_get(model_config, "intermediate_size", 12288))
self.vocab_size = int(_tf_config_get(model_config, "vocab_size", 151936))
self.rms_norm_eps = float(_tf_config_get(model_config, "rms_norm_eps", 1e-6))
self.rope_theta = float(_tf_config_get(model_config, "rope_theta", 5_000_000))
self.mrope_section = list(rope_scaling.get("mrope_section", [24, 20, 20]))
self.rms_norm_eps = self._resolve_rms_norm_eps(model_config)
self.rope_theta = self._resolve_rope_theta(model_config)
self.mrope_section = self._resolve_mrope_section(model_config)
self.qk_norm_for_diffusion = bool(_tf_config_get(model_config, "qk_norm_for_diffusion", True))
self.latent_patch_size = int(_tf_config_get(model_config, "latent_patch_size", 2))
self.latent_channel_size = int(_tf_config_get(model_config, "latent_channel", 48))
self.timestep_scale = float(_tf_config_get(model_config, "timestep_scale", 0.001))
@@ -1095,10 +1123,12 @@ class Cosmos3VFMTransformer(nn.Module):
)
self.patch_latent_dim = (self.latent_patch_size**2) * self.latent_channel_size
self.use_k_norm_und_for_gen = _tf_config_get(model_config, "use_k_norm_und_for_gen", None)
dtype = od_config.dtype
quant_config = getattr(od_config, "quantization_config", None) if od_config else None
self.language_model = Cosmos3LanguageModel(
self.language_model = self._language_model_cls(
hidden_size=self.hidden_size,
intermediate_size=self.intermediate_size,
num_hidden_layers=self.num_hidden_layers,
@@ -1111,12 +1141,13 @@ class Cosmos3VFMTransformer(nn.Module):
mrope_section=self.mrope_section,
quant_config=quant_config,
prefix="language_model",
**self._language_model_kwargs(),
)
# Video projection layers are small; not worth quantizing.
self.proj_in = nn.Linear(self.patch_latent_dim, self.hidden_size)
self.proj_out = nn.Linear(self.hidden_size, self.patch_latent_dim)
self.time_embedder = TimestepEmbedder(self.hidden_size, target_dtype=dtype)
self.time_embedder = TimestepEmbedder(self.hidden_size)
if self.action_gen:
self.action_proj_in = DomainAwareLinear(
self.action_dim,
@@ -1147,6 +1178,8 @@ class Cosmos3VFMTransformer(nn.Module):
head_dim=self.head_dim,
rms_norm_eps=self.rms_norm_eps,
quant_config=quant_config,
mlp_cls=self._gen_mlp_cls,
qk_norm=self.qk_norm_for_diffusion,
prefix=f"gen_layers.{i}",
)
for i in range(self.num_hidden_layers)
@@ -0,0 +1,396 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Cosmos3 Edge transformer variant with a Nemotron dense UND backbone."""
from __future__ import annotations
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.model_executor.layers.linear import ColumnParallelLinear, RowParallelLinear
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
from vllm_omni.diffusion.attention.layer import Attention as FrameworkAttention
from .transformer_cosmos3 import (
Cosmos3VFMTransformer,
Qwen3VLTextRotaryEmbedding,
RMSNorm,
_apply_rotary_pos_emb,
_nested_get,
_tf_config_get,
)
COSMOS3_EDGE_BACKBONE_TYPE = "cosmos3_edge_nemotron_dense"
def _tf_config_nested_get(config: Any, key: str, default: Any = None) -> Any:
if config is None:
return default
if isinstance(config, dict):
value = _nested_get(config, key)
elif hasattr(config, "to_dict"):
value = _nested_get(config.to_dict(), key)
elif hasattr(config, "params"):
value = _nested_get(config.params, key)
else:
value = getattr(config, key, None)
return default if value is None else value
class Cosmos3Relu2MLP(nn.Module):
"""Nemotron dense MLP: down_proj(relu(up_proj(x)) ** 2)."""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.up_proj = ColumnParallelLinear(
hidden_size,
intermediate_size,
bias=False,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.up_proj",
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
hidden = F.relu(self.up_proj(x))
return self.down_proj(hidden * hidden)
class Cosmos3EdgeCausalAttention(nn.Module):
"""Edge UND causal attention.
Reasoner self-attention uses raw UND Q/K with RoPE because
``qk_norm_for_text=false``. The GEN-facing UND key cache is a separate
normalized-then-RoPE'd view of the raw K tensor when GEN QK norm is enabled;
no-GEN-QK-norm checkpoints reuse the raw RoPE'd UND K.
"""
def __init__(
self,
*,
hidden_size: int,
num_attention_heads: int,
num_key_value_heads: int,
head_dim: int,
rms_norm_eps: float,
use_k_norm_und_for_gen: bool,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_attention_heads
self.num_kv_heads = num_key_value_heads
self.head_dim = head_dim
tp_size = get_tensor_model_parallel_world_size()
self.num_heads_local = self.num_heads // tp_size
self.num_kv_heads_local = self.num_kv_heads // tp_size
self.to_q = ColumnParallelLinear(
hidden_size,
self.num_heads * self.head_dim,
bias=False,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_q",
)
self.to_k = ColumnParallelLinear(
hidden_size,
self.num_kv_heads * self.head_dim,
bias=False,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_k",
)
self.to_v = ColumnParallelLinear(
hidden_size,
self.num_kv_heads * self.head_dim,
bias=False,
gather_output=False,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_v",
)
self.to_out = RowParallelLinear(
self.num_heads * self.head_dim,
hidden_size,
bias=False,
input_is_parallel=True,
return_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.to_out",
)
self.k_norm_und_for_gen = RMSNorm(self.head_dim, eps=rms_norm_eps) if use_k_norm_und_for_gen else None
self.attn = FrameworkAttention(
num_heads=self.num_heads,
head_size=self.head_dim,
causal=True,
softmax_scale=1.0 / (self.head_dim**0.5),
num_kv_heads=self.num_kv_heads,
skip_sequence_parallel=True,
)
def forward(
self,
hidden_states: torch.Tensor,
freqs_cos: torch.Tensor,
freqs_sin: torch.Tensor,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
B, S, _ = hidden_states.shape
q = self.to_q(hidden_states).view(B, S, self.num_heads_local, self.head_dim)
k = self.to_k(hidden_states).view(B, S, self.num_kv_heads_local, self.head_dim)
v = self.to_v(hidden_states).view(B, S, self.num_kv_heads_local, self.head_dim)
q_reasoner, k_reasoner = _apply_rotary_pos_emb(q, k, freqs_cos, freqs_sin)
if self.k_norm_und_for_gen is not None:
k_gen = F.rms_norm(
k,
(self.head_dim,),
self.k_norm_und_for_gen.weight,
eps=self.k_norm_und_for_gen.variance_epsilon,
)
_, k_gen = _apply_rotary_pos_emb(q, k_gen, freqs_cos, freqs_sin)
else:
k_gen = k_reasoner
out = self.attn(q_reasoner, k_reasoner, v).reshape(B, S, -1)
return self.to_out(out), k_gen, v
class Cosmos3EdgeUndDecoderLayer(nn.Module):
"""Edge UND decoder layer: causal self-attention + ReLU2 MLP."""
def __init__(
self,
*,
hidden_size: int,
intermediate_size: int,
num_attention_heads: int,
num_key_value_heads: int,
head_dim: int,
rms_norm_eps: float,
use_k_norm_und_for_gen: bool,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.self_attn = Cosmos3EdgeCausalAttention(
hidden_size=hidden_size,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
head_dim=head_dim,
rms_norm_eps=rms_norm_eps,
use_k_norm_und_for_gen=use_k_norm_und_for_gen,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
)
self.input_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.post_attention_layernorm = RMSNorm(hidden_size, eps=rms_norm_eps)
self.mlp = Cosmos3Relu2MLP(
hidden_size=hidden_size,
intermediate_size=intermediate_size,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
def forward(
self,
hidden_states: torch.Tensor,
freqs: tuple[torch.Tensor, torch.Tensor],
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
cos, sin = freqs
attn_out, k, v = self.self_attn(hidden_states, cos, sin)
hidden_states = residual + attn_out
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = residual + self.mlp(hidden_states)
return hidden_states, k, v
class Cosmos3EdgeLanguageModel(nn.Module):
"""Nemotron dense UND tower that returns GEN-facing K plus raw V per layer."""
_layerwise_offload_blocks_attrs = ["layers"]
def __init__(
self,
*,
hidden_size: int,
intermediate_size: int,
num_hidden_layers: int,
num_attention_heads: int,
num_key_value_heads: int,
head_dim: int,
vocab_size: int,
rms_norm_eps: float,
rope_theta: float,
mrope_section: list[int],
use_k_norm_und_for_gen: bool,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.embed_tokens = nn.Embedding(vocab_size, hidden_size)
self.rotary_emb = Qwen3VLTextRotaryEmbedding(
head_dim=head_dim,
rope_theta=rope_theta,
mrope_section=mrope_section,
)
self.layers = nn.ModuleList(
[
Cosmos3EdgeUndDecoderLayer(
hidden_size=hidden_size,
intermediate_size=intermediate_size,
num_attention_heads=num_attention_heads,
num_key_value_heads=num_key_value_heads,
head_dim=head_dim,
rms_norm_eps=rms_norm_eps,
use_k_norm_und_for_gen=use_k_norm_und_for_gen,
quant_config=quant_config,
prefix=f"{prefix}.layers.{i}",
)
for i in range(num_hidden_layers)
]
)
self.norm = RMSNorm(hidden_size, eps=rms_norm_eps)
def forward(
self,
text_ids: torch.Tensor,
freqs: tuple[torch.Tensor, torch.Tensor],
) -> list[tuple[torch.Tensor, torch.Tensor]]:
hidden = self.embed_tokens(text_ids)
cached_kv: list[tuple[torch.Tensor, torch.Tensor]] = []
for layer in self.layers:
hidden, k, v = layer(hidden, freqs)
cached_kv.append((k, v))
return cached_kv
class Cosmos3EdgeVFMTransformer(Cosmos3VFMTransformer):
"""Cosmos3 Edge variant with Nemotron dense UND and shared GEN diffusion."""
_language_model_cls = Cosmos3EdgeLanguageModel
_gen_mlp_cls = Cosmos3Relu2MLP
@staticmethod
def _validate_supported_config(model_config: Any) -> None:
expected_values = {
"qk_norm_for_text": False,
"position_embedding_type": "unified_3d_mrope",
"unified_3d_mrope_reset_spatial_ids": True,
"joint_attn_implementation": "two_way",
}
for key, expected in expected_values.items():
actual = _tf_config_get(model_config, key, expected)
if actual != expected:
raise ValueError(
f"Unsupported Cosmos3 Edge transformer config: {key}={actual!r}; expected {expected!r}."
)
backbone_type = _tf_config_get(model_config, "backbone_type", None)
if backbone_type is None:
raise ValueError(
f"Cosmos3 Edge transformer config must declare backbone_type={COSMOS3_EDGE_BACKBONE_TYPE!r}."
)
if backbone_type != COSMOS3_EDGE_BACKBONE_TYPE:
raise ValueError(
"Unsupported Cosmos3 Edge transformer config: "
f"backbone_type={backbone_type!r}; expected {COSMOS3_EDGE_BACKBONE_TYPE!r}."
)
required_values = {
"latent_channel": 48,
"latent_patch_size": 2,
"temporal_compression_factor": 4,
}
for key, expected in required_values.items():
actual = _tf_config_get(model_config, key, None)
if actual is None:
raise ValueError(f"Cosmos3 Edge transformer config must declare {key}={expected}.")
if int(actual) != expected:
raise ValueError(
f"Unsupported Cosmos3 Edge transformer config: {key}={actual!r}; expected {expected!r}."
)
@classmethod
def _resolve_rms_norm_eps(cls, model_config: Any) -> float:
return float(
_tf_config_get(
model_config,
"rms_norm_eps",
_tf_config_get(model_config, "layer_norm_epsilon", 1e-5),
)
)
@classmethod
def _resolve_rope_theta(cls, model_config: Any) -> float:
return float(_tf_config_get(model_config, "rope_theta", 100_000_000))
@classmethod
def _resolve_mrope_section(cls, model_config: Any) -> list[int]:
mrope_section = _tf_config_get(model_config, "mrope_section", None)
if mrope_section is None:
mrope_section = _tf_config_nested_get(model_config, "mrope_section", [24, 20, 20])
return list(mrope_section)
def _language_model_kwargs(self) -> dict[str, Any]:
return {"use_k_norm_und_for_gen": self.use_k_norm_und_for_gen}
def validate_loaded_weights(self, loaded: set[str]) -> None:
missing: list[str] = []
for layer_idx in range(self.num_hidden_layers):
required_markers = (
f"language_model.layers.{layer_idx}.mlp.up_proj.",
f"language_model.layers.{layer_idx}.mlp.down_proj.",
f"gen_layers.{layer_idx}.mlp.up_proj.",
f"gen_layers.{layer_idx}.mlp.down_proj.",
)
if self.use_k_norm_und_for_gen:
required_markers = (
*required_markers,
f"language_model.layers.{layer_idx}.self_attn.k_norm_und_for_gen.",
)
missing.extend(
marker.rstrip(".") for marker in required_markers if not any(marker in name for name in loaded)
)
if missing:
preview = ", ".join(missing[:10])
suffix = " ..." if len(missing) > 10 else ""
raise ValueError(
"Cosmos3 Edge transformer checkpoint is missing required weights for "
f"{preview}{suffix}. Use a converted Edge checkpoint with ReLU2 UND/GEN MLP weights."
)
+8
View File
@@ -276,6 +276,11 @@ _DIFFUSION_MODELS = {
"pipeline_cosmos3",
"Cosmos3OmniDiffusersPipeline",
),
"Cosmos3OmniPipeline": (
"cosmos3",
"pipeline_cosmos3",
"Cosmos3OmniDiffusersPipeline",
),
"SoulXSingerPipeline": (
"soulx_singer",
"pipeline_soulx_singer_svs",
@@ -536,6 +541,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
"DreamIDOmniPipeline": "get_dreamid_omni_post_process_func",
"SenseNovaU1Pipeline": "get_sensenova_u1_post_process_func",
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_post_process_func",
"Cosmos3OmniPipeline": "get_cosmos3_post_process_func",
"HiDreamImagePipeline": "get_hidream_image_post_process_func",
"StableDiffusionXLPipeline": "get_sdxl_image_post_process_func",
"Krea2Pipeline": "get_krea2_post_process_func",
@@ -546,6 +552,7 @@ _DIFFUSION_IR_OP_PRIORITY_FUNCS = {
# `ir_op_priority_func` function must be placed in {mod_folder}/{mod_relname}.py,
# where mod_folder and mod_relname are defined and mapped using `_DIFFUSION_MODELS` via the `arch` key.
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_ir_op_priority_func",
"Cosmos3OmniPipeline": "get_cosmos3_ir_op_priority_func",
}
_DIFFUSION_PRE_PROCESS_FUNCS = {
@@ -570,6 +577,7 @@ _DIFFUSION_PRE_PROCESS_FUNCS = {
"HunyuanImage3ForCausalMM": "get_hunyuan_image_3_pre_process_func",
"MagiHumanPipeline": "get_magi_human_pre_process_func",
"Cosmos3OmniDiffusersPipeline": "get_cosmos3_pre_process_func",
"Cosmos3OmniPipeline": "get_cosmos3_pre_process_func",
"SoulXSingerPipeline": "get_soulxsinger_pre_process_func",
"SoulXSingerSVCPipeline": "get_soulxsinger_svc_pre_process_func",
}
@@ -288,6 +288,12 @@ class DiffusionWorker:
# Since vLLM v0.20.0, IR wraps GPU ops. Set IR op priority preference to enforce GPU op fusion during wrapping.
# Also need to log, because vLLM internally logs another line in VllmConfig.__post_init__. Avoid confusion.
vllm_config.kernel_config.ir_op_priority = _resolve_ir_op_priority(self.od_config, vllm_config)
if self.od_config.moe_backend != "auto":
logger.warning(
"Overriding MoE backend from default 'auto' to '%s' per deploy config.",
self.od_config.moe_backend,
)
vllm_config.kernel_config.moe_backend = self.od_config.moe_backend
logger.info(
"Final IR op priority after setting vLLM-Omni overrides: %s", vllm_config.kernel_config.ir_op_priority
)
@@ -2,7 +2,7 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from collections.abc import Mapping
from dataclasses import asdict, fields, is_dataclass
from dataclasses import asdict, is_dataclass
from typing import Any
import msgspec
@@ -59,10 +59,7 @@ class OmniMsgpackEncoder:
if isinstance(obj, Image.Image):
return self._encode_pil_image(obj)
# RequestOutput (not a dataclass, needs special handling).
# Note: OmniRequestOutput subclasses RequestOutput but is a dataclass,
# so msgspec encodes it natively (field map) and it never reaches
# this hook.
# RequestOutput (not a dataclass, needs special handling)
if isinstance(obj, RequestOutput):
return self._encode_request_output(obj)
@@ -255,30 +252,23 @@ class OmniMsgpackDecoder:
def _decode_omni_request_output(self, obj: dict[str, Any]) -> Any:
"""Decode dict to OmniRequestOutput.
By the time this runs, ``_post_process`` has already reconstructed
nested values bottom-up (CompletionOutput, tensors, images, ...), so
the object is built directly from the known dataclass fields.
OmniRequestOutput is a dataclass, so we can use msgspec.convert
or construct it directly.
"""
from vllm_omni.outputs import OmniRequestOutput
data = dict(obj)
# Legacy wire format (pre-inheritance): the stage output was nested
# under "request_output". Merge its content via the compat setter.
legacy_inner = data.pop("request_output", None)
multi_modal_placeholders = data.pop("multi_modal_placeholders", None)
field_names = {f.name for f in fields(OmniRequestOutput)}
known = {k: v for k, v in data.items() if k in field_names}
try:
out = OmniRequestOutput(**known)
# Use msgspec.convert for dataclass reconstruction
return msgspec.convert(obj, OmniRequestOutput)
except Exception:
# Defensive fallback; should rarely happen if
# _is_omni_request_output is correct.
return obj
if legacy_inner is not None and not isinstance(legacy_inner, dict):
out.request_output = legacy_inner
if multi_modal_placeholders is not None:
setattr(out, "multi_modal_placeholders", multi_modal_placeholders)
return out
try:
# Fallback: construct directly if msgspec.convert fails
# (e.g., if some fields are missing or have wrong types)
return OmniRequestOutput(**obj)
except Exception:
# If both attempts fail, return dict as-is (defensive fallback)
# This should rarely happen if _is_omni_request_output is correct
return obj
def _decode_tensor(self, obj: dict[str, Any]) -> torch.Tensor:
"""Decode dict to torch.Tensor."""
+2 -1
View File
@@ -148,7 +148,8 @@ class OmniServeCommand(CLISubcommand):
"The following CLI args are not supported under --omni: "
f"{', '.join(offenders)}. Configure parallelism through the "
"per-stage YAML (`--deploy-config` / `--stage-configs-path`) "
"and replica count via `--omni-dp-size-local`."
"and replica count via the per-stage `num_replicas` config field "
"(single-runtime) or `--omni-dp-size-local` (headless / multi-runtime)."
)
# --omni-lb-policy is validated against the LoadBalancingPolicy enum.
+7 -3
View File
@@ -577,6 +577,7 @@ class OmniBase(PDDisaggregationMixin):
self.prom_metrics.set_running(running)
self.prom_metrics.set_waiting(max(0, total - running))
images = getattr(engine_outputs, "images", []) if output_type == "image" else []
response_metrics: dict[str, Any] = {}
stage_metrics: dict[str, dict[str, Any]] = {}
rid_key = str(req_id)
@@ -601,14 +602,17 @@ class OmniBase(PDDisaggregationMixin):
response_metrics["final_output_type"] = current_stage_metrics["final_output_type"]
response_metrics["num_tokens_in"] = current_stage_metrics["num_tokens_in"]
response_metrics["num_tokens_out"] = current_stage_metrics["num_tokens_out"]
# Generation content (outputs, prompt, images, trajectory_*, ...) is
# copied from engine_outputs onto the returned object by
# OmniRequestOutput.__post_init__.
return OmniRequestOutput(
request_id=req_id or "",
stage_id=stage_id,
final_output_type=output_type,
request_output=engine_outputs,
images=images,
trajectory_latents=getattr(engine_outputs, "trajectory_latents", None),
trajectory_timesteps=getattr(engine_outputs, "trajectory_timesteps", None),
trajectory_log_probs=getattr(engine_outputs, "trajectory_log_probs", None),
trajectory_decoded=getattr(engine_outputs, "trajectory_decoded", None),
_custom_output=getattr(engine_outputs, "_custom_output", {}),
metrics=response_metrics,
stage_durations=stage_durations,
peak_memory_mb=peak_memory_mb,
+4 -3
View File
@@ -3113,7 +3113,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
request_id=request_id,
)
images = getattr(result, "images", [])
images = getattr(result.request_output, "images", [])
stage_durations = result.stage_durations
peak_memory_mb = result.peak_memory_mb
cot_output = None
@@ -3184,7 +3184,7 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
)
yield f"data: {chunk.model_dump_json()}\n\n"
elif final_output_type == "image":
images = self._flatten_diffusion_images(getattr(output, "images", []))
images = self._flatten_diffusion_images(getattr(output.request_output, "images", []))
if not images:
raise RuntimeError("Streaming image edit produced an empty final image output.")
image_data = [
@@ -3488,7 +3488,8 @@ class OmniOpenAIServingChat(OpenAIServingChat, AudioMixin):
# Image output path (text2img / img2img)
final_output_type = getattr(result, "final_output_type", "image")
images = getattr(result, "images", [])
# Handle nested OmniRequestOutput structure where images might be in request_output
images = getattr(result.request_output, "images", [])
multimodal_output = getattr(result, "multimodal_output", {}) or {}
stage_durations = result.stage_durations
peak_memory_mb = result.peak_memory_mb
+5
View File
@@ -153,6 +153,11 @@ _EXTRA_SPECS: dict[str, dict[str, Any]] = {
"extra_output_params": COSMOS3_EXTRA_OUTPUT_PARAMS,
"text_to_image_prompt_builder": build_cosmos3_text_to_image_prompt,
},
"Cosmos3OmniPipeline": {
"extra_body_params": COSMOS3_EXTRA_BODY_PARAMS,
"extra_output_params": COSMOS3_EXTRA_OUTPUT_PARAMS,
"text_to_image_prompt_builder": build_cosmos3_text_to_image_prompt,
},
"MagiHumanPipeline": {
"extra_body_params": MAGI_HUMAN_EXTRA_BODY_PARAMS,
"extra_output_params": MAGI_HUMAN_EXTRA_OUTPUT_PARAMS,
+124 -168
View File
@@ -3,7 +3,7 @@ from typing import Any
import torch
from PIL import Image
from vllm.outputs import CompletionOutput, RequestOutput
from vllm.outputs import RequestOutput
from vllm.v1.outputs import ModelRunnerOutput
from vllm_omni.inputs.data import OmniPromptType
@@ -59,58 +59,12 @@ class OmniModelRunnerOutput(ModelRunnerOutput):
omni_connector_output: OmniConnectorOutput | None = None
# RequestOutput attributes that hold generation content. When wrapping a
# stage output, these are copied onto the OmniRequestOutput itself so that
# the object IS the RequestOutput instead of holding one.
_REQUEST_OUTPUT_CONTENT_ATTRS = (
"prompt",
"prompt_token_ids",
"prompt_logprobs",
"outputs",
"finished",
"lora_request",
"encoder_prompt",
"encoder_prompt_token_ids",
"num_cached_tokens",
"kv_transfer_params",
)
# Omni-specific content copied when the wrapped stage output is itself an
# OmniRequestOutput (e.g. a diffusion stage inside a multi-stage pipeline).
_OMNI_CONTENT_ATTRS = (
"images",
"latents",
"trajectory_latents",
"trajectory_timesteps",
"trajectory_log_probs",
"trajectory_decoded",
"_multimodal_output",
"_custom_output",
)
# Dynamic attributes that must not be copied verbatim onto the wrapper:
# they collide with properties defined on OmniRequestOutput.
_COPY_SKIP_ATTRS = frozenset({"request_output", "multimodal_output", "custom_output"})
@dataclass
class OmniRequestOutput(RequestOutput):
class OmniRequestOutput:
"""Unified request output for both pipeline stages and diffusion models.
Extends vLLM's ``RequestOutput`` so that omni outputs can flow directly
through vLLM serving codepaths (which expect ``prompt_token_ids``,
``outputs``, etc. as real attributes). The inherited fields store the
LLM generation content; omni-specific fields store pipeline/diffusion
extras.
Note: ``RequestOutput`` is a plain class (not a dataclass), so all of its
attributes are redeclared below as dataclass fields with defaults — the
dataclass-generated ``__init__`` replaces ``RequestOutput.__init__`` and
must set them itself.
This class handles outputs from:
1. Multi-stage LLM pipelines (with stage_id, final_output_type, and the
inherited RequestOutput fields carrying the stage's generation content)
1. Multi-stage LLM pipelines (with stage_id, final_output_type, request_output)
2. Diffusion models (with images, prompt, metrics)
Attributes:
@@ -118,38 +72,30 @@ class OmniRequestOutput(RequestOutput):
finished: Whether generation is complete
stage_id: Identifier of the stage that produced this output (pipeline mode)
final_output_type: Type of output ("text", "image", "audio", "latents")
request_output: The underlying RequestOutput from the stage (pipeline mode)
images: List of generated PIL images (diffusion mode)
prompt: The prompt used for generation
prompt: The prompt used for generation (diffusion mode)
latents: Optional tensor of latent representations (diffusion mode)
metrics: Generation metrics. A plain dict for omni outputs; may carry
vLLM's request stats object when copied from a raw RequestOutput.
metrics: Optional dictionary of generation metrics
"""
# --- Inherited RequestOutput attributes (redeclared as fields) ---
request_id: str = ""
prompt: OmniPromptType | None = None
prompt_token_ids: list[int] | None = None
prompt_logprobs: Any = None
outputs: list[CompletionOutput] = field(default_factory=list)
finished: bool = True
metrics: Any = field(default_factory=dict)
lora_request: Any = None
encoder_prompt: str | None = None
encoder_prompt_token_ids: list[int] | None = None
num_cached_tokens: int | None = None
kv_transfer_params: dict[str, Any] | None = None
# --- Pipeline stage fields ---
# Pipeline stage fields
stage_id: int | None = None
final_output_type: str = "text"
request_output: RequestOutput | None = None
# --- Diffusion model fields ---
# Diffusion model fields
images: list[Image.Image] = field(default_factory=list)
prompt: OmniPromptType | None = None
latents: torch.Tensor | None = None
trajectory_latents: torch.Tensor | None = None
trajectory_timesteps: torch.Tensor | None = None
trajectory_log_probs: torch.Tensor | None = None
trajectory_decoded: list | None = None
metrics: dict[str, Any] = field(default_factory=dict)
_multimodal_output: dict[str, Any] = field(default_factory=dict)
_custom_output: dict[str, Any] = field(default_factory=dict)
@@ -164,44 +110,6 @@ class OmniRequestOutput(RequestOutput):
error_status_code: int | None = None
error_type: str | None = None
def _copy_content_from(self, source: Any) -> None:
"""Copy generation content from a stage output into this object.
``source`` may be a vLLM ``RequestOutput``, another
``OmniRequestOutput``, or a duck-typed stand-in (tests use mocks and
SimpleNamespace). Attribute presence is checked against the source's
instance ``__dict__`` so that auto-created Mock attributes are not
copied. Control fields set by the wrapper (stage_id,
final_output_type, metrics, stage_durations, ...) are intentionally
not copied.
"""
source_vars = getattr(source, "__dict__", None)
def _has(name: str) -> bool:
if source_vars is not None:
return name in source_vars
return hasattr(source, name)
for name in _REQUEST_OUTPUT_CONTENT_ATTRS + _OMNI_CONTENT_ATTRS:
if _has(name):
setattr(self, name, getattr(source, name))
if not self.request_id:
self.request_id = getattr(source, "request_id", "") or ""
# Dynamic attribute that vLLM code attaches to RequestOutput; it is a
# read-only property on this class, so store it in _multimodal_output.
if not isinstance(source, OmniRequestOutput) and _has("multimodal_output"):
mm = source.multimodal_output
if mm is not None:
self._multimodal_output = mm
# Preserve remaining dynamic attributes (e.g. dynamically annotated
# stage outputs) so they stay reachable through `request_output`.
if source_vars:
known = type(self).__dataclass_fields__.keys()
for name, value in source_vars.items():
if name.startswith("_") or name in known or name in _COPY_SKIP_ATTRS:
continue
setattr(self, name, value)
@classmethod
def from_error(
cls,
@@ -235,10 +143,7 @@ class OmniRequestOutput(RequestOutput):
final_output_type: str,
request_output: RequestOutput,
) -> "OmniRequestOutput":
"""Create output from a pipeline stage.
The stage output's content is copied into the new object's own
(inherited) fields.
"""Create output from pipeline stage.
Args:
stage_id: Stage identifier
@@ -248,13 +153,13 @@ class OmniRequestOutput(RequestOutput):
Returns:
OmniRequestOutput configured for pipeline mode
"""
out = cls(
return cls(
request_id=getattr(request_output, "request_id", ""),
stage_id=stage_id,
final_output_type=final_output_type,
request_output=request_output,
finished=True,
)
out.finished = True
return out
@classmethod
def from_diffusion(
@@ -315,21 +220,35 @@ class OmniRequestOutput(RequestOutput):
@property
def multimodal_output(self) -> Any:
"""Return the multimodal output payload.
"""Return multimodal output from the underlying request output or local field.
Checks completion outputs first (where multimodal_output is attached
by AR stages), then the local _multimodal_output field.
For pipeline outputs, this checks completion outputs first, then request_output.
For diffusion outputs, this returns the local _multimodal_output field.
Returns either a MultimodalPayload (Phase 3+) or a plain dict (legacy).
"""
for output in self.outputs:
if self.request_output is None:
return self._multimodal_output
# Check completion outputs first (where multimodal_output is attached)
for output in getattr(self.request_output, "outputs", []):
if mm := getattr(output, "multimodal_output", None):
return mm
if mm := getattr(self.request_output, "multimodal_output", None):
return mm
return self._multimodal_output
@property
def custom_output(self) -> dict[str, Any]:
"""Return custom output data from diffusion pipelines."""
"""Return custom output data from diffusion pipelines.
For diffusion outputs, returns the local _custom_output field.
For pipeline outputs with an inner OmniRequestOutput, forwards
the custom_output from the inner request output.
"""
if self.request_output is not None:
if isinstance(self.request_output, OmniRequestOutput):
return self.request_output._custom_output
return self._custom_output
@custom_output.setter
@@ -341,6 +260,58 @@ class OmniRequestOutput(RequestOutput):
"""Return the number of generated images."""
return len(self.images)
# Pass-through properties keep vLLM serving codepaths compatible with
# OmniRequestOutput for pipeline outputs (Issue #345).
@property
def prompt_token_ids(self) -> list[int] | None:
"""Return prompt token IDs from the underlying request output.
This property is required for compatibility with vLLM's streaming
chat completion generator which checks res.prompt_token_ids.
"""
if self.request_output is not None:
return getattr(self.request_output, "prompt_token_ids", None)
return None
@property
def outputs(self) -> list[Any]:
"""Return outputs from the underlying request output.
This property is required for compatibility with vLLM's streaming
and non-streaming chat completion generators.
"""
if self.request_output is not None:
return getattr(self.request_output, "outputs", [])
return []
@property
def encoder_prompt_token_ids(self) -> list[int] | None:
"""Return encoder prompt token IDs from the underlying request output."""
if self.request_output is not None:
return getattr(self.request_output, "encoder_prompt_token_ids", None)
return None
@property
def prompt_logprobs(self) -> Any:
"""Return prompt logprobs from the underlying request output."""
if self.request_output is not None:
return getattr(self.request_output, "prompt_logprobs", None)
return None
@property
def num_cached_tokens(self) -> int | None:
"""Return number of cached tokens from the underlying request output."""
if self.request_output is not None:
return getattr(self.request_output, "num_cached_tokens", None)
return None
@property
def kv_transfer_params(self) -> Any:
"""Return KV transfer params from the underlying request output."""
if self.request_output is not None:
return getattr(self.request_output, "kv_transfer_params", None)
return None
@property
def is_diffusion_output(self) -> bool:
"""Check if this is a diffusion model output."""
@@ -349,33 +320,60 @@ class OmniRequestOutput(RequestOutput):
@property
def is_pipeline_output(self) -> bool:
"""Check if this is a pipeline stage output."""
return self.stage_id is not None
return self.stage_id is not None and self.request_output is not None
def unwrap(self) -> "OmniRequestOutput":
"""Return the output itself.
"""Unwrap nested OmniRequestOutput to get the innermost result.
Kept for backward compatibility: outputs used to nest a stage's
OmniRequestOutput inside ``request_output``; content is now flattened
onto the object itself, so there is nothing to unwrap.
This helper handles the common pattern where pipeline outputs may wrap
other OmniRequestOutput instances. It recursively unwraps until it reaches
the final output with actual content (images, text, etc.).
Returns:
The innermost OmniRequestOutput containing the actual generation results.
Example:
```python
result = omni.generate(...)
output = OmniRequestOutput.unwrap_result(result)
if output.images:
# Access images directly
video_frames = output.images
```
"""
return self
current = self
# Unwrap nested pipeline outputs
while current.is_pipeline_output and current.request_output is not None:
if isinstance(current.request_output, OmniRequestOutput):
current = current.request_output
else:
break
return current
@staticmethod
def unwrap_result(result: Any) -> "OmniRequestOutput":
"""Extract the final OmniRequestOutput from omni.generate() results.
"""Unwrap result from omni.generate() to get the final OmniRequestOutput.
Handles:
This static helper handles the full unwrapping pattern including:
1. Extracting from list if needed
2. Type validation
3. Recursive unwrapping of nested pipeline outputs
Args:
result: The result from omni.generate() - may be a list or OmniRequestOutput
Returns:
The OmniRequestOutput with actual content
The innermost OmniRequestOutput with actual content
Raises:
ValueError: If result is not an OmniRequestOutput or list containing one
Example:
```python
result = omni.generate(...)
output = OmniRequestOutput.unwrap_result(result)
# output is guaranteed to be the final OmniRequestOutput
```
"""
# Handle list wrapper
if isinstance(result, list):
@@ -387,6 +385,7 @@ class OmniRequestOutput(RequestOutput):
if not isinstance(result, OmniRequestOutput):
raise ValueError(f"Expected OmniRequestOutput, got {type(result)}")
# Unwrap nested outputs
return result.unwrap()
def to_dict(self) -> dict[str, Any]:
@@ -426,8 +425,7 @@ class OmniRequestOutput(RequestOutput):
f"finished={self.finished}",
f"stage_id={self.stage_id}",
f"final_output_type={self.final_output_type!r}",
f"prompt_token_ids={self.prompt_token_ids}",
f"outputs={self.outputs}",
f"request_output={self.request_output}",
f"images={images_repr}",
f"prompt={self.prompt!r}",
f"latents={self.latents}",
@@ -439,45 +437,3 @@ class OmniRequestOutput(RequestOutput):
]
return f"OmniRequestOutput({', '.join(parts)})"
def _request_output_get(self: OmniRequestOutput) -> RequestOutput | None:
"""Backward-compat view of the wrapped stage output.
The omni output now IS the RequestOutput, so pipeline/LLM outputs return
``self``; pure diffusion or error outputs return None (matching the old
composition semantics where ``request_output`` was unset for them).
"""
if self.stage_id is not None or self.outputs:
return self
return None
def _request_output_set(self: OmniRequestOutput, value: Any) -> None:
if value is not None:
self._copy_content_from(value)
# `request_output` is attached after class creation, NOT declared as a field
# or InitVar: msgspec serializes dataclasses natively via __dataclass_fields__
# (which includes InitVar pseudo-fields), so a request_output entry would be
# read through the property during encoding — returning `self` and recursing
# forever. As a plain property it is invisible to both dataclasses and
# msgspec.
OmniRequestOutput.request_output = property( # type: ignore[assignment]
_request_output_get, _request_output_set
)
# Keep `request_output=` working as a constructor keyword for
# backward compatibility: the wrapped stage output's content is copied onto
# the new object (see _copy_content_from).
_dataclass_init = OmniRequestOutput.__init__
def _omni_request_output_init(self: OmniRequestOutput, *args: Any, request_output: Any = None, **kwargs: Any) -> None:
_dataclass_init(self, *args, **kwargs)
if request_output is not None:
self._copy_content_from(request_output)
OmniRequestOutput.__init__ = _omni_request_output_init # type: ignore[method-assign]
+1 -1
View File
@@ -94,7 +94,7 @@ def quack_scaled_fp8_mm(
return None
out = torch.empty(a.shape[0], b.shape[1], device=a.device, dtype=out_dtype)
alpha = scale_a.reshape(1).float() * scale_b.reshape(1).float()
gemm.gemm_tuned(a, b, out, bias=bias, alpha=alpha)
gemm.gemm(a, b, out=out, bias=bias, alpha=alpha, tuned=True)
return out