- Chapter 4
- L5: Purpose, Test Content, Directory Location, Example
-
-
Weekly / Days before Release
-
GPU
-
-
-
-
-
----
-
- The folder structure for tests file based on the 5 levels design
-Legend: `✅` = test exists, `⬜` = suggested to add.
-```
-vllm_omni/ tests/
-├── config/ → ├── config/
-│ ├── model.py │ └── test_model.py ⬜
-│ └── lora.py │ └── test_lora.py ⬜
-│
-├── core/ → ├── core/
-│ └── sched/ │ └── sched/
-│ ├── omni_ar_scheduler.py │ ├── test_omni_ar_scheduler.py ⬜
-│ ├── omni_generation_scheduler.py │ ├── test_omni_generation_scheduler.py ⬜
-│ └── output.py │ └── test_output.py ✅ currently in entrypoints/test_omni_new_request_data.py (tests output.OmniNewRequestData)
-│
-├── diffusion/ → ├── diffusion/
-│ ├── diffusion_engine.py │ ├── test_diffusion_engine.py ⬜
-│ ├── attention/ │ ├── attention/
-│ │ ├── layer.py │ │ ├── test_attention_sp.py ✅
-│ │ └── backends/ │ │ └── test_flash_attn.py ✅
-│ ├── distributed/ │ ├── distributed/
-│ │ └── ... │ │ ├── test_comm.py ✅
-│ │ │ │ ├── test_cfg_parallel.py ✅
-│ │ │ │ └── test_sp_plan_hooks.py ✅
-│ ├── lora/ │ ├── lora/
-│ │ └── ... │ │ ├── test_base_linear.py ✅
-│ │ │ │ └── test_lora_manager.py ✅
-│ ├── models/ │ ├── models/
-│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
-│ │ ├── ovis_image/ │ │ ├── ovis_image/
-│ │ │ └── ... │ │ │ └── test_ovis_image.py ✅
-│ │ ├── z_image/ │ │ └── z_image/
-│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
-│ └── worker/ │ └── worker/
-│ ├── diffusion_worker.py │ └── test_diffusion_worker.py ✅ file at diffusion/test_diffusion_worker.py
-│ └── diffusion_model_runner.py │
-│
-├── distributed/ → ├── distributed/
-│ └── omni_connectors/ │ └── omni_connectors/
-│ ├── adapter.py │ ├── test_adapter_and_flow.py ✅
-│ ├── kv_transfer_manager.py │ ├── test_basic_connectors.py ✅
-│ ├── connectors/ │ ├── test_kv_flow.py ✅
-│ └── utils/ │ └── test_omni_connector_configs.py ✅
-│
-├── engine/ → ├── engine/
-│ ├── input_processor.py │ ├── test_input_processor.py ⬜ (no processor.py in source)
-│ ├── output_processor.py │ └── test_output_processor.py ⬜
-│ └── arg_utils.py │ └── test_arg_utils.py ⬜
-│
-├── entrypoints/ → ├── entrypoints/
-│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
-│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
-│ │ └── ... │ │ └── test_*.py ⬜
-│ └── openai/ │ └── openai_api/ # maps to entrypoints/openai/
-│ ├── api_server.py │ ├── test_api_server.py ⬜ (e2e indirect coverage)
-│ ├── serving_chat.py │ ├── test_serving_chat_sampling_params.py ✅
-│ ├── serving_speech.py │ ├── test_serving_speech.py ✅
-│ └── image_api_utils.py │ └── test_image_server.py ✅
-│
-├── inputs/ → ├── inputs/
-│ ├── data.py │ ├── test_data.py ⬜
-│ ├── parse.py │ ├── test_parse.py ⬜
-│ └── preprocess.py │ └── test_preprocess.py ✅ currently in entrypoints/test_omni_input_preprocessor.py
-│
-├── model_executor/ → ├── model_executor/
-│ ├── layers/ │ ├── layers/
-│ │ └── mrope.py │ │ └── test_mrope.py ⬜
-│ ├── model_loader/ │ ├── model_loader/
-│ │ └── weight_utils.py │ │ └── test_weight_utils.py ⬜
-│ ├── models/ │ ├── models/
-│ │ ├── qwen2_5_omni/ │ │ ├── qwen2_5_omni/
-│ │ │ ├── qwen2_5_omni_thinker.py │ │ │ ├── test_audio_length.py ✅
-│ │ │ ├── qwen2_5_omni_talker.py │ │ │ ├── test_qwen2_5_omni_thinker.py ⬜
-│ │ │ └── qwen2_5_omni_token2wav.py │ │ │ ├── test_qwen2_5_omni_talker.py ⬜
-│ │ └── qwen3_omni/ │ │ │ └── test_qwen2_5_omni_token2wav.py ⬜
-│ │ └── ... │ │ └── qwen3_omni/
-│ ├── stage_configs/ │ │ └── test_*.py ⬜
-│ │ └── *.yaml │ └── stage_configs/ (used by e2e, test_*.py can be added) ⬜
-│ └── stage_input_processors/ │ └── stage_input_processors/
-│ └── ... │ └── test_*.py ⬜
-│
-├── sample/ → ├── sample/
-│ └── __init__.py │ └── test_*.py ⬜
-│
-├── utils/ → ├── utils/
-│ └── __init__.py │ └── test_*.py ⬜ (no platform_utils.py currently)
-│
-├── worker/ → ├── worker/
-│ ├── gpu_ar_model_runner.py │ ├── test_gpu_ar_model_runner.py ⬜
-│ ├── gpu_ar_worker.py │ ├── test_gpu_ar_worker.py ⬜
-│ ├── gpu_generation_model_runner.py │ ├── test_gpu_generation_model_runner.py ✅
-│ ├── gpu_generation_worker.py │ ├── test_gpu_generation_worker.py ⬜
-│ ├── gpu_model_runner.py │ ├── test_omni_gpu_model_runner.py ✅
-│ └── mixins.py │ └── (npu under platforms/npu/worker/) # not worker/npu/
-│
-├── platforms/ → (no tests/platforms/, e2e and stage_configs provide indirect coverage)
-│ ├── cuda/
-│ ├── npu/worker/ # NPU worker here, not vllm_omni/worker/npu/
-│ ├── rocm/
-│ └── xpu/worker/
-│
-├── outputs.py → test_outputs.py ✅ (at tests root)
-├── (logger, patch, request, version) → (no corresponding unit test)
-│
-└── e2e (tests side only) → ├── e2e/
- ├── online_serving/ ✅ non-empty
- │ ├── test_qwen2_5_omni.py
- │ ├── test_async_omni.py
- │ ├── test_qwen3_omni.py
- │ ├── test_qwen3_omni_expansion.py
- │ ├── test_mimo_audio.py
- │ └── test_images_generations_lora.py
- └── offline_inference/ ✅
- ├── test_qwen2_5_omni.py
- ├── test_qwen3_omni.py
- ├── test_bagel_text2img.py
- ├── test_z_image.py
- ├── test_wan22.py
- ├── test_zimage_tensor_parallel.py
- ├── test_cache_dit.py
- ├── test_teacache.py
- ├── test_stable_audio_expansion.py
- ├── test_diffusion_cpu_offload.py
- ├── test_diffusion_layerwise_offload.py
- ├── test_diffusion_lora.py
- ├── test_sequence_parallel.py
- └── stage_configs/ (legacy schema, still
- ├── bagel_*.yaml present for unmigrated
- └── npu/, rocm/, etc. models)
-
-# Migrated models (qwen3_omni_moe, qwen2_5_omni, qwen3_tts) live under
-# vllm_omni/deploy/ instead — see docs/configuration/stage_configs.md.
-```
-
-
-
-
-
-## Common Specifications
-
-Before entering specific testing levels, the project establishes two common specifications aimed at standardizing the development process and quickly locating issues.
-
-1. ***PR Checklist ([Tests Style](../ci/tests_style.md))***: This template defines the self-check items that must be completed before submitting a code review (Pull Request). It ensures that each code change meets basic requirements such as code style, dependency updates, and documentation synchronization before entering the automated testing pipeline, serving as the first manual line of defense for quality assurance.
-2. ***CI Failure Explanation ([CI Failures](../ci/failures.md))***: This document archives and explains common failure patterns in the Continuous Integration (CI) pipeline, error log interpretation, and preliminary troubleshooting steps. It helps developers and testers quickly diagnose the causes of automated test failures, improving problem-solving efficiency.
-
-## Notes
-
-### Diff-aware Buildkite uploads (`source_file_dependencies`)
-
-L2 (`.buildkite/test-ready.yml`) and L3 (`.buildkite/test-merge.yml`) pipelines can **skip unrelated GPU jobs at upload time** based on the PR diff. This is implemented by `.buildkite/scripts/upload_pipeline.py`, which filters steps before calling `buildkite-agent pipeline upload`.
-
-#### What `source_file_dependencies` is
-
-- A **uploader-only** YAML key on a Buildkite step or group. **Buildkite does not understand it**; `upload_pipeline.py` always **removes** it from the YAML that is uploaded.
-- A list of path **prefixes** (directories or individual files). If **any** changed file in the diff equals a prefix or starts with `prefix/`, the step is kept; otherwise the step (or entire group) is **omitted** from the uploaded pipeline.
-
-#### When filtering runs
-
-| Build context | Changed files used for matching |
-| --- | --- |
-| Pull request | `git diff --name-only origin/...` |
-| `main` branch push | `git diff --name-only ^..` |
-| Other (e.g. local dry-run, non-PR branch) | Diff cannot be resolved → **no filtering**; all steps are uploaded and `source_file_dependencies` is still stripped |
-
-Docs-only PRs are handled earlier in bootstrap (`pipeline.yml`) via skip-ci logic; `source_file_dependencies` applies only to the **uploaded** L2/L3 test pipelines.
-
-#### Where it is configured
-
-| Level | Pipeline file | Upload entry |
-| --- | --- | --- |
-| L2 | `.buildkite/test-ready.yml` | `upload_pipeline.py --upload .buildkite/test-ready.yml` (from `pipeline.yml`) |
-| L3 | `.buildkite/test-merge.yml` | `upload_pipeline.py --upload .buildkite/test-merge.yml` (from `pipeline.yml`) |
-
-Steps **without** `source_file_dependencies` are always uploaded (subject to the usual label conditions: `ready` for L2, `merge-test` for L3).
-
-#### Current skip policy (L2 / L3)
-
-To balance CI cost and coverage:
-
-- **Always run** (no `source_file_dependencies`): baseline groups outside E2E Test—e.g. Simple Test, Diffusion unit tests, Engine/Model Executor, Distributed, Custom Pipeline, Entrypoints (L2), LoRA / Entrypoints (L3).
-- **Diff-gated** (`source_file_dependencies` set): **every leaf job under the E2E Test group** in `test-ready.yml` and `test-merge.yml`, regardless of queue (`mithril-h100-pool`, `gpu_1_queue`, or `gpu_4_queue`). Each step lists the smallest set of prefixes that should trigger it—typically:
- - pytest file(s) exercised by the job (online and/or offline);
- - model code under `vllm_omni/model_executor/models/` or `vllm_omni/diffusion/models/`;
- - related `vllm_omni/model_executor/stage_input_processors/` and `vllm_omni/deploy/*.yaml` when applicable.
-
-Adding a new E2E step: add `source_file_dependencies` on the leaf job with those prefixes. Prefer **per-step** deps rather than a broad group-level list unless every child shares the same paths.
-
-#### YAML examples
-
-H100 E2E (kubernetes / `mithril-h100-pool`):
-
-```yaml
- - label: "Diffusion · Qwen Image Test"
- source_file_dependencies:
- - tests/e2e/online_serving/test_qwen_image.py
- - vllm_omni/diffusion/models/qwen_image/
- commands:
- - pytest -s -v tests/e2e/online_serving/test_qwen_image.py -m 'core_model' ...
- agents:
- queue: "mithril-h100-pool"
-```
-
-Docker E2E (`gpu_1_queue` / `gpu_4_queue`)—same key, same upload-time filtering:
-
-```yaml
- - label: "TTS · Qwen3-TTS CustomVoice Test"
- source_file_dependencies:
- - tests/e2e/online_serving/test_qwen3_tts_customvoice.py
- - vllm_omni/model_executor/models/qwen3_tts/
- - vllm_omni/model_executor/stage_input_processors/qwen3_tts.py
- - vllm_omni/deploy/qwen3_tts.yaml
- commands:
- - pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py ...
- agents:
- queue: "gpu_4_queue"
-```
-
-A **group** may also define `source_file_dependencies`; nested steps inherit filtering as a unit—the whole group is dropped if no prefix matches.
-
-#### Local dry-run
-
-```bash
-# Render filtered YAML to stdout (no upload)
-python3 .buildkite/scripts/upload_pipeline.py .buildkite/test-ready.yml
-
-# Confirm uploader-only keys are stripped
-python3 .buildkite/scripts/upload_pipeline.py .buildkite/test-merge.yml | grep source_file_dependencies
-# (no output expected)
-```
-
-On a PR build, Buildkite logs from `upload_pipeline.py` include lines such as `skip '…' (no changes under …)` for omitted steps.
-
-#### Related
-
-- Implementation: `.buildkite/scripts/upload_pipeline.py`
-- L2/L3 diff skip does **not** replace label-based triggers (`ready`, `merge-test`); it only reduces which steps appear **after** the pipeline is already scheduled.
-
-### Test helper environment variables
-
-Some shared helpers under `tests/helpers/` honor optional environment variables for local debugging. These are **not** set in CI by default.
-
-| Variable | Accepted values | Description |
-| -------- | --------------- | ----------- |
-| `VLLM_OMNI_KEEP_REQUEST_MEDIA` | `1`, `true`, `yes` (case-insensitive) | When enabled, temporary WAV files created by `tests.helpers.media.convert_audio_bytes_to_text` are **not** deleted when the pytest process exits. By default, each call writes a unique file under the system temp directory via `tempfile.mkstemp` and registers `atexit` cleanup. Use this when debugging audio output validation (Whisper transcription, keyword checks, text–audio similarity). The saved path is logged as `audio data is saved: `. |
-
-Example (Linux / macOS):
-
-```bash
-export VLLM_OMNI_KEEP_REQUEST_MEDIA=1
-pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -k test_mix_to_text_audio
-```
-
-Example (Windows PowerShell):
-
-```powershell
-$env:VLLM_OMNI_KEEP_REQUEST_MEDIA = "1"
-pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -k test_mix_to_text_audio
-```
-
-## Chapter 1: L1 & L2 Level Testing - Unit Testing and Basic End-to-End Verification
-
-### 1.1 Testing Purpose
-
-L1 and L2 level testing form the foundation of the quality assurance system. L1 level testing focuses on verifying the internal logic correctness of code units (e.g., functions, classes), ensuring each independent component behaves as designed.
-
-L2 level testing builds upon L1 by introducing GPU resources and verifying that the end-to-end (E2E) process of the model in basic deployment scenarios is smooth. For example, it uses dummy models to confirm that core interfaces like the inference pipeline, output format, and streaming response work properly. The common goal of these two levels is to provide developers with rapid feedback, discovering and fixing issues early in the development cycle.
-
-
-
-### 1.2 Testing Content and Scope
-
-- ***L1 (Unit & Logic Testing)***:
-- - ***Scope***: Tests internal functions and methods of core components such as `entrypoints`, `models`.
- - ***Focus***: Branch coverage, exception handling, algorithm logic correctness. Does not involve external dependencies or the complete service stack.
- - ***Time Cost***: Execution time is controlled within ***15 minutes*** to ensure fast feedback.
-- ***L2 (Basic End-to-End Testing)***:
-- - ***Scope***: Covers two basic deployment scenarios: `online` (serving) and `offline` (inference).
- - ***Focus***: Uses `dummy` weights (via deploy YAML patching at `core_model`) or lightweight real models to verify that the entire chain from request input to result output works normally, including output data structure, streaming (stream) support, and **cheap payload checks** at `--run-level core_model` (see below). Also includes some unit tests that require launching independent service instances.
- - ***L2 response validation (`core_model`)***: Implemented in `tests/helpers/assertions.py` and invoked by `OpenAIClientHandler` based on `--run-level`. At L2 we require **request success** plus minimal output sanity—not full accuracy:
- - **Speech / TTS** (`assert_audio_speech_response`): decoded audio must be present and non-empty (or exceed `min_audio_bytes` when set in `request_config`); `response_format` must match the returned content-type (e.g. `wav`, `pcm`). Whisper transcript similarity, PCM HNR, and preset-voice gender checks run only at L3+.
- - **Diffusion** (`assert_diffusion_response`): at least one non-empty image, video, or audio artifact. Resolution/frame-count parity and other deep checks run only at L3+.
- - **Omni multimodal** (`assert_omni_response`): L2 asserts successful completion; keyword, transcript, and cross-modal similarity checks run only at L3+.
- - ***Characteristic***: Requires ***GPU*** resources to perform model computations.
-
-### 1.3 Test Directory and Execution Files
-
-A clear directory structure is key to managing test cases efficiently.
-
-- ***L1 Test Directory***: `/tests/{component_name}/test_xxx.py`
-- - Here, `{component_name}` corresponds to modules in the source code, such as `distributed`, `entrypoints`, etc., and `test_xxx.py` is the specific test file.
-- ***L2 Test Directory***:
-- - Online Serving: `/tests/e2e/online_serving/test_{model_name}.py`
- - Offline Inference: `/tests/e2e/offline_inference/test_{model_name}.py`
-
-### 1.4 Execution Method and Example
-
-- ***Trigger Timing***: **`PR with ready label`**. That is, when a developer adds a "ready for review" or similar label to a PR on platforms like GitHub, L1 and L2 tests are automatically triggered.
-- ***Diff-aware step skipping***: On L2, **E2E Test** jobs may be omitted at pipeline upload when the PR diff does not touch their [`source_file_dependencies`](#diff-aware-buildkite-uploads-source_file_dependencies) prefixes (H100 and docker queues alike); non-E2E groups still always upload. See [Diff-aware Buildkite uploads](#diff-aware-buildkite-uploads-source_file_dependencies).
-- ***Execution Environment***: L1 uses ***CPU*** environment; L2 requires ***GPU*** environment.
-- ***Script Example***:
-
-
- L1 Test Examples
-
-Examples from `tests/model_executor/models/qwen2_5_omni/test_audio_length.py`
-```python
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
-
-import pytest
-
-pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
-
-def test_resolve_max_mel_frames_default():
- from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
-
- assert resolve_max_mel_frames(None, default=30000) == 30000
- assert resolve_max_mel_frames(None, default=6000) == 6000
-
-
-def test_resolve_max_mel_frames_explicit():
- from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
-
- # Explicit argument always wins over default
- assert resolve_max_mel_frames(123, default=30000) == 123
- assert resolve_max_mel_frames(6000, default=30000) == 6000
- assert resolve_max_mel_frames(0, default=30000) == 0
-
-
-@pytest.mark.parametrize("repeats", [2, 4])
-@pytest.mark.parametrize("code_len", [0, 1, 32768])
-@pytest.mark.parametrize("max_mel_frames", [None, -1, 0, 1, 6000, 30000])
-def test_cap_and_align_mel_length_no_mismatch(repeats, code_len, max_mel_frames):
- """Guard that any max_mel_frames yields a mel length aligned to repeats, and
- consistent with the truncated code length (prevents concat mismatch).
- """
- from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import cap_and_align_mel_length
-
- target_code_len, target_mel_len = cap_and_align_mel_length(
- code_len=code_len,
- repeats=repeats,
- max_mel_frames=max_mel_frames,
- )
-
- assert isinstance(target_code_len, int)
- assert isinstance(target_mel_len, int)
-
- if code_len == 0:
- assert target_code_len == 0
- assert target_mel_len == 0
- return
-
- assert target_code_len >= 1
- assert target_mel_len >= repeats
- assert target_mel_len % repeats == 0
- assert target_mel_len == target_code_len * repeats
- assert target_code_len <= code_len
-
- if max_mel_frames is not None and int(max_mel_frames) > 0 and int(max_mel_frames) >= repeats:
- assert target_mel_len <= int(max_mel_frames)
-```
-
-
-
- L2 Test Examples
-You can refer to Test Examples in Chapter 2 to see example test cases that incorporate both L2 and L3 testing logic.
-
-
-- - ***Run Command***:
-
- `pytest -s -v /tests/e2e/online_serving/test_{model_name}.py`
- `pytest -s -v -m 'core_model and cpu' --run-level=core_model`
-
-## Chapter 2: L3 Level Testing - Core Integration, Performance, and Accuracy Verification
-
-### 2.1 Testing Purpose
-
-L3 level testing executes after code is merged into the main branch. Its core purpose is to verify the integration effect, key performance indicators, and output accuracy of ***real models*** in ***multiple deployment scenarios***
-
-. It acts as the "quality gatekeeper" for the main branch, ensuring that no merge breaks the core capabilities of the model service. Testing needs to provide clear conclusions within a relatively short time (<30min), balancing test depth with feedback speed.
-
-
-
-### 2.2 Testing Content and Scope
-
-- ***Deployment Scenarios***: Covers richer `online` and `offline` deployment configurations, which may include different hardware configurations, batch sizes, concurrency levels, etc.
-- ***Core Verification***:
-- 1. ***Inference Functionality***: Ensures real models can perform forward computation normally and return results.
- 2. ***Accuracy Compliance***: Verifies that the model's evaluation metrics (e.g., accuracy) meet the expected baseline, preventing code changes from introducing accuracy issues.
- 3. ***Important Performance***: Verifies whether performance (e.g., P99 latency, throughput) in core scenarios meets preset thresholds.
-- ***L3 response validation (`advanced_model`)***: At `--run-level advanced_model`, `tests/helpers/assertions.py` adds semantic checks on top of L2 payload gates (see Chapter 1):
- - **Speech / TTS** (`assert_audio_speech_response`): Whisper transcript of returned audio vs `request_config["input"]` (cosine similarity > 0.9 when input text is set); optional `min_audio_bytes` floor; PCM harmonic-to-noise ratio when `response_format` is `pcm`; **preset voice gender** via `_assert_preset_voice_gender_from_audio` when `voice` matches a known preset in `_PRESET_VOICE_GENDER_MAP` (pitch/F0-based classifier; skipped for unknown voices or `pcm` output).
- - **Omni multimodal** (`assert_omni_response`): non-empty text/audio outputs; `key_words` in transcript or text; text–audio similarity / containment; preset **`speaker`** gender check (same helper as TTS).
- - **Diffusion** (`assert_*_diffusion_response`): image/video dimension and frame-count parity with request parameters where configured.
- - **Accuracy suites**: pixel/video similarity and metric baselines under `/tests/e2e/accuracy/` (separate from inline helper assertions).
-
-### 2.3 Test Directory and Execution Files
-
-- ***Functional Testing***:
-- - Online Serving: `/tests/e2e/online_serving/test_{model_name}_expansion.py`
- - Offline Inference: `/tests/e2e/offline_inference/test_{model_name}_expansion.py`
- - (Note: `_expansion.py` likely means it contains more comprehensive scenario cases compared to L2 tests).
-
-### 2.4 Execution Method and Example
-
-- ***Trigger Timing***: **`PR Merged`**. Automatically triggered after code review is approved and merged into the main branch (typically via `merge-test` label on the PR before merge).
-- ***Diff-aware step skipping***: Same [`source_file_dependencies`](#diff-aware-buildkite-uploads-source_file_dependencies) mechanism as L2—**all E2E Test** leaf steps are diff-gated; other L3 groups always upload. See [Diff-aware Buildkite uploads](#diff-aware-buildkite-uploads-source_file_dependencies).
-- ***Execution Environment***: ***GPU*** servers.
-- ***Script Example***:
-
-???+ example "Test Examples"
-
- **2.4.1 Mark Declaration Section**
-
- ```python
- @pytest.mark.advanced_model
- @pytest.mark.core_model
- @pytest.mark.parametrize("omni_server", test_params, indirect=True)
- ```
-
- **Explanation**:
-
- @pytest.mark.advanced_model: Marks the test as L3 merge level (`--run-level advanced_model`): real weights, Whisper/similarity/keyword checks, diffusion deep checks, and **preset voice/speaker gender** validation where applicable. @pytest.mark.full_model: Marks L4 nightly-only suites (e.g. `test_*_expansion.py`, doc examples).
-
- @pytest.mark.core_model: Marks the test as L1 or L2 level. At `--run-level core_model`, validation is limited to request success plus cheap payload checks (e.g. non-empty audio bytes, response format/content-type, non-empty diffusion outputs)—not Whisper, keyword, or accuracy gates. Deploy YAML may use `load_format: dummy` for fast PR feedback.
-
- @pytest.mark.parametrize: A parameterization decorator that allows abstracting test data into parameters, enabling reuse of the same test logic across different data configurations. indirect=True indicates that parameters will be passed to the fixture for processing.
-
- **Notes**: If you believe the test case only needs to execute basic run logic at the PR-level CI, you can mark it only with @pytest.mark.core_model. If you believe it only needs to execute deep validation at merge (L3), use @pytest.mark.advanced_model. For L4 nightly-only expansion and doc-example tests, use @pytest.mark.full_model with `--run-level full_model`. If the test case needs both basic run and deep validation, mark with @pytest.mark.core_model and the appropriate L3/L4 marker (`advanced_model` and/or `full_model`).
-
- **2.4.2 Test Function Definition and Documentation**
-
- ```python
- def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
- """
- Test multi-modal input processing and text/audio output generation via OpenAI API.
- Deploy Setting: default yaml
- Input Modal: text + audio + video + image
- Output Modal: text + audio
- Input Setting: stream=True
- Datasets: single request
- """
- ```
-
- **Explanation**:
-
- **Function Naming Convention**: Uses the test_ prefix, describes the test scenario mix_to_text_audio, and the number 001 indicates the first test case for this scenario.
-
- **Parameter Explanation**:
-
- omni_server: Omni server instance obtained via fixture, containing model information and configuration.
-
- openai_client: Unified OpenAI client processing instance, encapsulating request sending and response validation logic.
-
- Docstring: Describes the test purpose, deployment settings, input/output modalities, streaming settings, and dataset type in detail, providing clear context for test maintenance.
-
- **2.4.3 Multimodal Data Preparation**
-
- ```python
- video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
- image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
- audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
- ```
-
- **Explanation**:
-
- **Data Generation Functions**: Use the generate_synthetic_* series of functions to generate synthetic test data, avoiding reliance on external resources and ensuring test reproducibility and stability.
-
- **Parameter Explanation**:
-
- Video: width, height, duration_frames
-
- Image: width, height
-
- Audio: duration_seconds, channels
-
- **2.4.4 Request Configuration and Keyword Validation**
-
- ```python
- request_config = {
- "model": omni_server.model,
- "messages": messages,
- "stream": True,
- "key_words": {
- "audio": ["water", "cricket"],
- "video": ["sphere", "globe", "circle", "round"],
- "image": ["square", "quadrate"],
- "text": ["beijing"]
- },
- }
- ```
-
- **Explanation**:
-
- **Model Specification**: Uses omni_server.model to ensure the test aligns with the model configured on the server.
-
- **Keyword Validation Mechanism**: This is an innovative design of the template to address the specific needs of multimodal testing:
-
- Audio Keywords: Validate whether the generated text's description of audio content contains expected elements (e.g., "water" for water sounds, "cricket" for cricket sounds). If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
-
- **Video Keywords**: Validate whether the generated text's description of video content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
-
- Image Keywords: Validate whether the generated text's description of image content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
-
- Text Keywords: Validate whether the generated text contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
-
- **2.4.5 Request Execution**
-
- ```python
- openai_client.send_omni_request(request_config, request_num=1) # for omni-understanding models
- # or
- openai_client.send_diffusion_request(request_config, request_num=1) # for diffusion models
- ```
-
- **Explanation**:
-
- **Unified Client**: Uses the OpenAIClientHandler instance to send requests. This client encapsulates error handling, retry mechanisms, and response validation logic.
-
- **Single Request**: The comment clearly states this is a single-request completion test. For concurrent testing, it can be extended to multiple requests using request_num = n.
-
- **Implicit Validation**: `send_omni_request`, `send_audio_speech_request`, and `send_diffusion_request` call `assert_*_response` helpers with the session `--run-level`. At `core_model`, checks are limited to success plus cheap payload sanity (non-empty audio/media, format/content-type, optional `min_audio_bytes`). At `advanced_model` and `full_model`, deep validation adds Whisper transcripts, keyword/similarity, diffusion dimension checks, PCM HNR, etc.
-
- **Audio output debugging**: Deep validation may transcribe returned audio via `convert_audio_bytes_to_text` (Whisper). If an audio keyword or text–audio similarity assertion fails, set `VLLM_OMNI_KEEP_REQUEST_MEDIA=1` before running pytest to keep the intermediate WAV files for inspection (see [Test helper environment variables](#test-helper-environment-variables)).
-
-- ***Run Command (L3 merge)***: `pytest -s -v /tests/e2e/online_serving/test_{model_name}.py -m advanced_model --run-level=advanced_model`
-
-- ***Run Command (L4 nightly expansion)***: `pytest -s -v /tests/e2e/online_serving/test_{model_name}_expansion.py -m full_model --run-level=full_model`
-
-## Chapter 3: L4 Level Testing - Full Functionality, Performance, and Documentation Testing
-
-### 3.1 Testing Purpose
-
-L4 level testing is a comprehensive quality audit before a version release. It expands upon L3, executing ***full*** functional scenarios, conducting systematic ***performance stress tests***, and simultaneously verifying the correctness of accompanying ***example documentation***. Its purpose is to perform deep validation of the system during off-peak nighttime hours, providing quality trend reports for daytime development and data support for release decisions.
-
-
-
-### 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.
-- ***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`)
-- ***Documentation Example Tests***:
-- - `tests/example/online_serving/test_{model_name}.py`
- - `tests/example/offline_inference/test_{model_name}.py`
-
-### 3.4 Execution Method and Example
-
-- ***Trigger Timing***: **`Nightly`**, automatically executed every night.
-- ***Execution Environment***: ***GPU*** server clusters to meet the resource demands of performance testing.
-- ***Script Example***:
-
-??? example "Test Examples: Documentation Example Tests"
-
- --8<-- "docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md"
-
-??? example "Test Examples: Performance Tests"
-
- --8<-- "docs/contributing/ci/test_examples/l4_performance_tests.inc.md"
-
-??? example "Test Examples: Functionality Tests"
-
- --8<-- "docs/contributing/ci/test_examples/l4_functionality_tests.inc.md"
-
-- ***Run Command***: (Specific commands would depend on the performance testing tool and configuration defined in `nightly.json`).
-
-## Chapter 4: L5 Level Testing - Stability and Reliability Testing
-
-### 4.1 Testing Purpose
-
-L5 level testing focuses on the performance of model services under ***long-running*** and ***abnormal fault*** scenarios. It aims to uncover deep-seated issues that only manifest under sustained pressure or extreme conditions, such as memory leaks, resource contention, gradual performance degradation, and lack of fault tolerance mechanisms. This is the final, yet crucial, line of defense for ensuring service high availability and production environment robustness.
-
-
-
-### 4.2 Testing Content and Scope
-
-- ***Long-term Stability (Stability) Testing***: Uses JSON under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` and `test_wan22.json`) to run the service under moderate load for an extended period (e.g., over 12 hours), monitoring whether metrics like memory/VRAM usage, response time, and throughput degrade over time, and whether the service process remains stable.
-- ***Reliability Testing***: Uses pytest suites under `tests/dfx/reliability/` to inject controlled faults against a **live** `vllm_omni serve` instance (same **`omni_server` / `omni_server_function`** fixture style as E2E). Current suites emphasize **GPU memory pressure** (CUDA sidecar “memory hog”), **worker / runtime process kill** (`SIGKILL` on `VLLM::Worker` for Qwen3-Omni, `multiprocessing.spawn` for Wan2.2 video workers, or `vLLM-Omni::` for HunyuanImage DiT workers), **large multimodal chat**, **`/v1/videos`**, **`/v1/images/generations`**, or **`/v1/audio/speech`** jobs under OOM, **`/health` → 503** and **fast-fail / non-hanging concurrent** requests after kill, and **OpenAI-style 5xx error contracts** (e.g. text vs text+audio under OOM). **Post-fault recovery** checks exist where enabled (some cases may be `skip` while issues are tracked). See the Reliability `` block in Section 4.4 for file-level responsibilities and CI markers (`slow`, `hardware_test`, POSIX-only kill).
-
-### 4.3 Test Directory and Execution Files
-
-- ***Stability Test Configuration***: `tests/dfx/stability/tests/test_qwen3_omni.json`, `tests/dfx/stability/tests/test_wan22.json` (one JSON per model / runner family)
-- ***Reliability Test Suite*** (`tests/dfx/reliability/`):
- - `test_reliability_qwen3_omni.py` — Qwen3-Omni chat / multimodal reliability (GPU OOM, process kill, recovery, error contract under `--async-chunk` vs default).
- - `test_reliability_wan22.py` — Wan2.2 I2V video API reliability (`/v1/videos` under OOM and process kill, recovery).
- - `test_reliability_hunyuan_image.py` — HunyuanImage-3.0-Instruct DiT-only reliability (`/v1/images/generations` under OOM and process kill; deploy `hunyuan_image3_dit.yaml`, H100 × 4).
- - `test_reliability_voxcpm2.py` — VoxCPM2 TTS reliability (`/v1/audio/speech` under OOM and process kill; deploy `voxcpm2.yaml`, L4 × 1).
- - `helpers.py` — Shared primitives used by current suites: raw HTTP probes for `/v1/chat/completions` and `/health`, OpenAI-style error parsing, GPU OOM sidecar (`inject_gpu_oom` / `stop_gpu_oom_hogs`), and `pgrep`-based process-kill injector construction (`make_process_kill_fault_injector`).
- - `conftest.py` — `fault_injector` and `omni_server_after_fault` / `omni_server_after_fault_function` fixtures to run a callable **after** the server is ready.
- - `README.md` — Short local run commands for this directory.
-
-### 4.4 Execution Method and Example
-
-- ***Trigger Timing***: **`Weekly`** (weekly) or **`Days before Release`** (several days before a major release). Due to long execution times, the frequency is lower.
-- ***Execution Environment***: ***GPU*** servers, requiring a stable and exclusive testing environment.
-- ***Script Example***:
-
- Test Examples
-
-When you want to add L5-level stability test cases, add or extend the appropriate JSON file under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` for Omni bench traffic, or `test_wan22.json` for diffusion `/v1/videos` workloads). The following illustrates the Qwen3-Omni shape:
-
-```json
-{
- "test_name": "test_qwen3_omni_stability",
- "server_params": {
- "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
- "stage_config_name": "qwen3_omni.yaml"
- },
- "benchmark_params": [
- {
- "dataset_name": "random",
- "backend": "openai-chat-omni",
- "endpoint": "/v1/chat/completions",
- "duration_sec": 43200,
- "request_rate": 0.5,
- "num_prompts_per_batch": 20,
- "random_input_len": 2500,
- "random_output_len": 900,
- "ignore_eos": true,
- "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration"
- }
- ]
-}
-```
-
-#### Parameter Explanation
-
-***Overview***
-
-| Field | Required | Description |
-| ---------------- | -------- | --------------------------------------------------------------------------- |
-| test_name | Yes | Unique identifier for the stability test case |
-| server_params | Yes | Server-side configuration parameters (model, stage configuration, etc.) |
-| benchmark_params | Yes | Stability benchmark running parameters (supports multiple configurations) |
-
-#### server_params Configuration
-
-##### Basic Parameters
-
-| Parameter | Required | Example | Description |
-| ----------------- | -------- | ---------------------------------- | ----------------------------------- |
-| model | Yes | "Qwen/Qwen3-Omni-30B-A3B-Instruct" | Model name or path |
-| stage_config_name | Yes | "qwen3_omni.yaml" | Stage configuration file name |
-
-##### Dynamic Configuration (update/delete)
-
-Supports incremental modifications based on the basic configuration:
-
-| Operation | Description |
-| --------- | ------------------------------------ |
-| update | Update or add configuration items |
-| delete | Delete specified configuration items |
-
-***Example***:
-You can refer to Test Examples in Chapter 3.4
-
-#### benchmark_params Configuration
-
-For stability testing, the key parameters are:
-
-- **duration_sec**: Total duration (in seconds) during which benchmark traffic is sent. The stability benchmark will keep sending batches until this duration is reached.
-- **request_rate** / **max_concurrency**: Exactly one of them must be specified. They control how the traffic is generated for each batch:
- - `request_rate`: Number of requests per second. The benchmark will send `num_prompts_per_batch` requests at the given rate.
- - `max_concurrency`: Maximum number of concurrent requests. When this is used, `request_rate` is set to `inf` internally.
-- **num_prompts_per_batch**: Number of prompts sent in each batch. Multiple batches will be executed sequentially within `duration_sec`.
-
-All other optional parameters follow the same rules as the in Chapter 3.4.
-
-
-
-
- Reliability test suite (tests/dfx/reliability)
-
-#### Purpose and relationship to stability
-
-Reliability tests are **short fault-injection** integration runs (L5 **(b)** in `tests/dfx/reliability/README.md`). They complement **stability** JSON-driven long runs: instead of hours of steady traffic, they **perturb** the server (GPU OOM sidecar, fatal signals on selected processes) and check **failure mode** and **latency bounds** (e.g. chat or `/v1/videos` must not hang under concurrent fault-time load).
-
-#### Directory layout
-
-| Path | Responsibility |
-| ---- | -------------- |
-| `helpers.py` | Shared helpers used by current reliability suites: raw `POST`/`GET` probes (`/v1/chat/completions`, `/health`), OpenAI error parsing (`extract_openai_error_contract_from_bytes`), GPU OOM sidecar lifecycle (`inject_gpu_oom`, `stop_gpu_oom_hogs`), and process-kill injector builder (`make_process_kill_fault_injector`). |
-| `conftest.py` | Pytest fixtures: indirect `fault_injector`, `omni_server_after_fault` / `omni_server_after_fault_function` (run injector after server is ready, then yield server). |
-| `test_reliability_qwen3_omni.py` | Qwen3-Omni: OOM vs **text vs text+audio** error contract, large multimodal chat under OOM, concurrent pressure, **SIGKILL** on `VLLM::Worker`, `/health` → 503 + fast-fail + concurrent chat; optional OOM recovery scenario (may be skipped while tracked in issues). |
-| `test_reliability_wan22.py` | Wan2.2 I2V: large `/v1/videos` under OOM, **SIGKILL** on `multiprocessing.spawn` chain, health / fast-fail / concurrent video requests; optional recovery test (may be skipped). |
-| `test_reliability_hunyuan_image.py` | HunyuanImage DiT-only: large `/v1/images/generations` under OOM, **SIGKILL** on `vLLM-Omni::` workers and serve/tree targets, health / fast-fail / concurrent image requests; some OOM/recovery cases may be skipped while tracked in issues. |
-| `test_reliability_voxcpm2.py` | VoxCPM2: `/v1/audio/speech` under OOM (error contract), **SIGKILL** on `VLLM::` workers and serve/tree targets, health / fast-fail / concurrent speech requests; some OOM cases may be skipped while tracked in issues. |
-| `README.md` | Minimal run / collect examples. |
-
-#### Parametrization and markers
-
-- Each test module defines a **`RELIABILITY_SCENARIOS`** list (`test_name`, `server_params`: model, `stage_config_name` or diffusion `server_args`, etc.). **`create_reliability_omni_server_params()`** in `tests/dfx/conftest.py` resolves stage paths (including XPU substitutions where applicable) and builds **`OmniServerParams`** lists consumed by **`@pytest.mark.parametrize(..., indirect=True)`** on `omni_server` or `omni_server_function`.
-- Cases are tagged **`@pytest.mark.slow`** for weekly / selective CI. GPU-heavy suites use **`@hardware_test(res={"cuda": "H100"}, num_cards=...)`** or **`@hardware_test(res={"cuda": "L4"}, num_cards=1)`** (Qwen3-Omni **2**× H100; Wan2.2 **1**× H100; HunyuanImage DiT **4**× H100; VoxCPM2 **1**× L4).
-- **Process-kill** tests use **`@pytest.mark.skipif(os.name == "nt", ...)`** because injection uses POSIX **`pgrep` / `kill`**.
-
-#### CI trigger
-
-Weekly Buildkite (`.buildkite/test-weekly.yml`) runs one step per model suite (trigger: `WEEKLY=1` or PR label `weekly-test`), for example:
-
-| Buildkite step | Test file | CI hardware |
-| -------------- | --------- | ----------- |
-| Reliability Test - qwen3-omni | `test_reliability_qwen3_omni.py` | H100 × 2 (`mithril-h100-pool`) |
-| Reliability Test - wan22 | `test_reliability_wan22.py` | H100 × 2 (`mithril-h100-pool`) |
-| Reliability Test - hunyuan-image | `test_reliability_hunyuan_image.py` | H100 × 4 (`mithril-h100-pool`) |
-| Reliability Test - voxcpm2 | `test_reliability_voxcpm2.py` | L4 × 1 (`gpu_1_queue`) |
-
-```bash
-pytest -s -v tests/dfx/reliability/test_reliability_qwen3_omni.py -m "slow"
-pytest -s -v tests/dfx/reliability/test_reliability_wan22.py -m "slow"
-pytest -s -v tests/dfx/reliability/test_reliability_hunyuan_image.py -m "slow"
-pytest -s -v tests/dfx/reliability/test_reliability_voxcpm2.py -m "slow"
-```
-
-#### Local commands
-
-```bash
-pytest --collect-only tests/dfx/reliability
-pytest -s -v tests/dfx/reliability/test_reliability_qwen3_omni.py -m slow
-pytest -s -v tests/dfx/reliability/test_reliability_wan22.py -m slow
-pytest -s -v tests/dfx/reliability/test_reliability_hunyuan_image.py -m slow
-pytest -s -v tests/dfx/reliability/test_reliability_voxcpm2.py -m slow
-```
-
-#### Adding a new model suite
-
-1. Add `test_reliability_.py` under `tests/dfx/reliability/`.
-2. Define **`RELIABILITY_SCENARIOS`** and pass them through **`create_reliability_omni_server_params()`** with the correct deploy or e2e stage-config directory (same pattern as existing files).
-3. Reuse **`helpers`** for OOM / kill / raw HTTP; prefer **`assert_fault_exception()`** and **`resolve_oom_device_spec()`** from `tests/dfx/conftest.py` for consistent device selection vs stage YAML.
-4. Register **`slow`** (and **`hardware_test`** if needed); extend **`.buildkite/test-weekly.yml`** when the suite should run in weekly L5.
-
-
-
-- - ***Stability***: `pytest -s -v tests/dfx/stability/scripts/test_stability_qwen3_omni.py` or `pytest -s -v tests/dfx/stability/scripts/test_stability_wan22.py` (or add `test_stability_.py` alongside a matching JSON config)
- - ***Reliability***: `pytest -s -v tests/dfx/reliability/test_reliability_.py -m slow` (current suites: `qwen3_omni`, `wan22`, `hunyuan_image`, `voxcpm2`; add `test_reliability_.py` and a matching step in `.buildkite/test-weekly.yml` for new models)
-
-## Summary
-
-This multi-level testing system achieves continuous, progressive validation of model service quality by tightly integrating testing activities with the development workflow (commit, review, merge, release). From rapid unit testing to comprehensive end-to-end testing, and further to in-depth performance, stability, and reliability verification, each level has clear objectives, collectively building a robust quality protection net. By following this system, teams can deliver high-quality, highly reliable model services more efficiently.
diff --git a/docs/contributing/ci/ci_settings.md b/docs/contributing/ci/ci_settings.md
new file mode 100644
index 000000000..ff4b8e107
--- /dev/null
+++ b/docs/contributing/ci/ci_settings.md
@@ -0,0 +1,344 @@
+# CI Settings
+
+This document describes **where** Buildkite YAML lives in the repo, **how each platform organizes CI**, and **how to add a new job**. It does not document agent queues, GPU types, or container plugin details—those belong in infra / preset files (for example `.buildkite/common/ci_mirror_hardwares.yml`).
+
+For CI levels (L1–L5) and triggers, see [Test System Overview](./test_system_overview.md). For test authoring, see [Test Writing Guide](./test_writing_guide.md). For running tests locally or replaying CI jobs, see [Test Execution Guide](./test_execution_guide.md).
+
+## Directory layout
+
+Canonical layout (prefer these paths for new changes):
+
+```
+.buildkite/
+├── common/ # Shared across platforms
+│ ├── scripts/
+│ │ ├── skip_ci.py # skip-ci decision (docs / skip-mark / CI YAML paths)
+│ │ ├── upload_pipeline.py # Bootstrap + test-pipeline uploader (CUDA/NPU)
+│ │ └── resolve_skip_ci.sh # Shell helpers for AMD/Intel bootstrap
+│ └── ci_mirror_hardwares.yml # CUDA uploader presets (referenced by name only)
+├── cuda/ # Primary NVIDIA CUDA CI
+│ ├── pipeline.yml # Bootstrap entry (hook upload)
+│ ├── bootstrap-upload-steps.yml # Bootstrap child steps (upload_pipeline --upload)
+│ ├── test-ready.yml # L2
+│ ├── test-merge.yml # L3
+│ ├── test-nightly.yml # L4
+│ ├── test-weekly.yml # L5
+│ └── rebase-pipeline.yml
+├── npu/
+│ ├── pipeline-npu.yml # Bootstrap entry (hook upload)
+│ ├── bootstrap-upload-steps.yml # Bootstrap child steps (upload_pipeline --upload)
+│ ├── pipeline-npu-a3.yml # A3 variant (when used)
+│ ├── test-npu-ready.yml # L2
+│ ├── test-npu-nightly.yml # L4
+│ └── scripts/
+├── amd/
+│ ├── test-amd-ready.yml # L2 job definitions (template input)
+│ ├── test-amd-merge.yml # L3 job definitions
+│ ├── test-template-amd-omni.j2 # Renders final pipeline.yaml
+│ └── scripts/
+│ ├── bootstrap-amd-omni.sh # Entry: skip-ci → Jinja → upload
+│ └── run-amd-test.sh # Wraps pytest inside ROCm docker
+├── intel/
+│ ├── pipeline-intel.yml # Static Intel XPU pipeline
+│ └── scripts/
+│ ├── bootstrap-intel-omni.sh
+│ └── run-xpu-test.sh
+└── release/
+ ├── release-pipeline.yml
+ └── scripts/
+```
+
+**Placement rules**
+
+| Rule | Detail |
+| ---- | ------ |
+| Platform code under platform dir | New CUDA jobs go in `.buildkite/cuda/`; do not add new top-level `.buildkite/test-*.yml` files. |
+| Shared logic in `common/` | Skip-ci and CUDA upload rendering stay in `.buildkite/common/scripts/`. |
+| Bootstrap vs test YAML | **Bootstrap** (`pipeline*.yml`) builds images and uploads **child** test pipelines. **Test** YAML (`test-*.yml`) lists pytest steps only. |
+| Register CI YAML in `skip_ci.py` | If you add a new whitelisted test pipeline file, update `L2_YAML_FILES`, `L3_YAML_FILES`, or `L45_YAML_FILES` in `.buildkite/common/scripts/skip_ci.py` so skip-ci paths stay correct. |
+
+There are still **legacy copies** at `.buildkite/*.yaml` (without the `cuda/` prefix). Treat `.buildkite/cuda/*` as source of truth.
+
+## Platform comparison
+
+| Platform | Bootstrap entry | Test job files | Upload mechanism | Job hardware in YAML |
+| -------- | ---------------- | -------------- | ---------------- | -------------------- |
+| **CUDA** | `cuda/pipeline.yml` | `test-ready.yml`, `test-merge.yml`, `test-nightly.yml`, `test-weekly.yml` | `upload_pipeline.py --upload` (expands uploader-only keys) | `mirror_hardwares: ` (string) |
+| **NPU** | `npu/pipeline-npu.yml` | `test-npu-ready.yml`, `test-npu-nightly.yml` | `upload_pipeline.py --upload` | `mirror_hardwares: a2b3_npu_1` / `a2b3_npu_4` / `a3_npu_2` |
+| **AMD** | `amd/scripts/bootstrap-amd-omni.sh` | `test-amd-ready.yml`, `test-amd-merge.yml` | Jinja (`test-template-amd-omni.j2`) → `pipeline upload` | `agent_pool` + `mirror_hardwares: [amdproduction]` (array, template filter) |
+| **Intel** | `intel/scripts/bootstrap-intel-omni.sh` | `intel/pipeline-intel.yml` (steps inline) | Direct `pipeline upload` | Inline `agents.queue` on each step |
+
+## Platform configuration style
+
+=== "CUDA"
+
+ **Bootstrap:** [`cuda/pipeline.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/pipeline.yml) (hook upload) + [`cuda/bootstrap-upload-steps.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/bootstrap-upload-steps.yml) (child steps). Document 1 runs [`upload_pipeline.py --upload .buildkite/cuda/bootstrap-upload-steps.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/scripts/upload_pipeline.py), which injects `if` by step `key` from skip-ci and uploads image build plus L2–L5 child pipeline upload steps (see [Diff-aware CI — Bootstrap skip](#bootstrap-skip)).
+
+ **Test YAML:** `cuda/test-ready.yml` (L2), `test-merge.yml` (L3), `test-nightly.yml` (L4), `test-weekly.yml` (L5). Each file starts with shared `env:` then `steps:`.
+
+ | CI level | File | Typical trigger |
+ | -------- | ---- | ----------------- |
+ | L2 | `cuda/test-ready.yml` | `ready` label |
+ | L3 | `cuda/test-merge.yml` | `merge-test` label / main merge |
+ | L4 | `cuda/test-nightly.yml` | `nightly-test` label or `NIGHTLY=1` |
+ | L5 | `cuda/test-weekly.yml` | `weekly-test` label or `WEEKLY=1` |
+
+ **Upload:** `upload_pipeline.py --upload` expands uploader-only keys before Buildkite upload.
+
+ **Hardware in YAML:** `mirror_hardwares: ` (string)—preset names in [`common/ci_mirror_hardwares.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/ci_mirror_hardwares.yml). Do **not** set `agents` / `plugins` on the same step.
+
+ **Conventions**
+
+ - **`depends_on`:** leaf jobs depend on `upload-ready-pipeline`, `upload-merge-pipeline`, etc.
+ - **`group` / `label`:** `:card_index_dividers:` groups; labels like `Diffusion · Qwen Image Test`.
+ - **`commands`:** `timeout … pytest …` with markers and `--run-level` for the pipeline level.
+ - **`source_file_dependencies`:** required on **E2E Test** leaf jobs in L2/L3; see [Step filtering](#step-filtering).
+
+ **Adding a job**
+
+ 1. Pick the level file (ready / merge / nightly / weekly).
+ 2. Add a step under the right **group** (usually **E2E Test** for model pytest).
+ 3. Set `label`, `commands`, `mirror_hardwares`, `depends_on: upload--pipeline`.
+ 4. For L2/L3 E2E, add `source_file_dependencies` (pytest + model + deploy YAML prefixes).
+ 5. Dry-run:
+
+ ```bash
+ python3 .buildkite/common/scripts/upload_pipeline.py .buildkite/cuda/test-ready.yml
+ ```
+
+=== "NPU"
+
+ **Bootstrap:** [`npu/pipeline-npu.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/pipeline-npu.yml) + [`npu/bootstrap-upload-steps.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/npu/bootstrap-upload-steps.yml)—same split as CUDA; builds A2/B3 and A3 CI images, then uploads child test pipelines.
+
+ **Test YAML:** `npu/test-npu-ready.yml` (L2), `test-npu-nightly.yml` (L4).
+
+ **Upload:** same as CUDA—`upload_pipeline.py --upload`.
+
+ **Hardware in YAML:** `mirror_hardwares` preset (string), expanded to `agents`, top-level `image`, and `plugins`. Presets: `a2b3_npu_1`, `a2b3_npu_4`, `a3_npu_2` in `common/ci_mirror_hardwares.yml`.
+
+ **Conventions**
+
+ - **`depends_on: upload-ready-pipeline`** (or `upload-nightly-pipeline`) ties jobs to bootstrap upload keys.
+ - Do not duplicate `agents` / `image` / `plugins` when using `mirror_hardwares`.
+
+ **Adding a job**
+
+ 1. Edit `test-npu-ready.yml` (L2) or `test-npu-nightly.yml` (L4).
+ 2. Add a step with `mirror_hardwares` (add a new preset in `ci_mirror_hardwares.yml` first if needed).
+ 3. Set `commands` to your pytest file and markers.
+ 4. Dry-run:
+
+ ```bash
+ python3 .buildkite/common/scripts/upload_pipeline.py .buildkite/npu/test-npu-ready.yml
+ ```
+
+ 5. Register new pipeline paths in `skip_ci.py` (`L2_YAML_FILES` or `L45_YAML_FILES`) when applicable.
+
+=== "AMD"
+
+ **Bootstrap:** [`amd/scripts/bootstrap-amd-omni.sh`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/amd/scripts/bootstrap-amd-omni.sh)—skip-ci, diff filtering, Jinja render, then `buildkite-agent pipeline upload`.
+
+ **Test YAML (data):** `amd/test-amd-ready.yml` (L2 / PR), `test-amd-merge.yml` (L3 / main).
+
+ **Rendering:** [`test-template-amd-omni.j2`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/amd/test-template-amd-omni.j2) wraps data steps with `amd-build` image build and `amd_` queues. Do **not** hand-edit generated `pipeline.yaml`.
+
+ **Hardware in YAML:** `agent_pool` (for example `mi325_1`) plus `mirror_hardwares: [amdproduction]` (array—Buildkite template filter, not the CUDA/NPU uploader preset mechanism).
+
+ **Data file fields**
+
+ | Field | Purpose |
+ | ----- | ------- |
+ | `agent_pool` | ROCm pool; template maps to `queue: amd_`. |
+ | `mirror_hardwares` | Which mirror HW runs the step (for example `[amdproduction]`). |
+ | `commands` | Passed into `run-amd-test.sh` via `TEST_COMMAND`. |
+
+ **Adding a job**
+
+ 1. Edit `test-amd-ready.yml` or `test-amd-merge.yml`.
+ 2. Copy a neighboring block: `label`, `agent_pool`, `mirror_hardwares`, `commands`, optional `grade`.
+ 3. Regenerate via bootstrap / Jinja; update `skip_ci.py` if you add a new YAML path.
+
+=== "Intel"
+
+ **Bootstrap:** [`intel/scripts/bootstrap-intel-omni.sh`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/intel/scripts/bootstrap-intel-omni.sh)—skip-ci, then direct `pipeline upload`.
+
+ **Test YAML:** steps live inline in [`intel/pipeline-intel.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/intel/pipeline-intel.yml); runners under `intel/scripts/` (for example `run-xpu-test.sh`).
+
+ **Upload:** `buildkite-agent pipeline upload` (no `upload_pipeline.py` mirror expansion).
+
+ **Hardware in YAML:** inline `agents.queue` (for example `intel-gpu-omni`) on each step.
+
+ **Adding a job**
+
+ 1. Add a step to `pipeline-intel.yml`, or extend an existing runner script.
+ 2. Match `agents`, `env`, `timeout_in_minutes`, and `command`/`commands` with sibling steps.
+ 3. Keep `pipeline-intel.yml` listed in `L2_YAML_FILES` in `skip_ci.py`.
+
+## Cross-cutting conventions
+
+### Diff-aware CI {#diff-aware-ci}
+
+PR diffs drive **two independent skip layers**. Both read changed files from git, but at different pipeline stages and with different granularity:
+
+| Layer | Script | When | What is skipped | Where you configure |
+| ----- | ------ | ---- | --------------- | ------------------- |
+| **Bootstrap** | [`skip_ci.py`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/scripts/skip_ci.py) + [`upload_pipeline.py`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/scripts/upload_pipeline.py) | Before child test pipelines upload (`cuda/pipeline.yml`, `npu/pipeline-npu.yml`, AMD/Intel bootstraps) | Entire default CI, or whole L2/L3 upload for a platform | Whitelists in `skip_ci.py`; bootstrap `if` injected by step `key` in `upload_pipeline.py` |
+| **Step filter** | [`upload_pipeline.py`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/scripts/upload_pipeline.py) | While uploading CUDA L2/L3 YAML | Individual Buildkite steps inside `test-ready.yml` / `test-merge.yml` | `source_file_dependencies` on each step or group |
+
+**Changed files** (both layers):
+
+| Build context | Diff command |
+| --- | --- |
+| Pull request | `git diff --name-only origin/...` |
+| `main` push | `git diff --name-only ^..` |
+| Local dry-run / non-PR | Diff unavailable → bootstrap and step filter both keep all steps; uploader-only keys are still stripped |
+
+Label triggers (`ready`, `merge-test`) are unchanged—diff-aware logic only reduces what runs **after** a pipeline is already scheduled.
+
+#### Bootstrap skip {#bootstrap-skip}
+
+[`skip_ci.py`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/common/scripts/skip_ci.py) classifies the git diff into buckets (docs / qualifying skip-mark / whitelisted L2·L3·L4/L5 YAML / other), then picks one decision path. CUDA/NPU apply that decision by injecting Buildkite `if` on `bootstrap-upload-steps.yml` step keys; AMD/Intel call `gate_bootstrap_ci `.
+
+##### Decision overview
+
+| Diff shape | Path | Default L2/L3 |
+| --- | --- | --- |
+| Product code / non-whitelisted paths | normal CI | all on |
+| Docs and/or qualifying skip-mark only | `skip_all` | all off (scheduled `main` NIGHTLY/WEEKLY exceptions below) |
+| Whitelisted CI YAML only | yaml-gated (`skip_l2_l3`) | per platform/level matrix |
+| Docs/skip-mark **+** whitelisted CI YAML | yaml-gated (`skip_l2_l3`) | same as CI-YAML-only (does **not** widen to normal CI) |
+| Non-qualifying skip-mark and no CI YAML | normal CI | all on |
+| Diff unavailable | normal CI | all on |
+
+**`skip_all` exceptions (CUDA/NPU bootstrap only):** PR labels (`nightly-test`, `merge-test`, `npu-test`, `weekly-test`, …) do **not** revive jobs. On `main`, scheduled `NIGHTLY=1` still builds the image and uploads L4 plus L2/L3 (with `--e2e`); `WEEKLY=1` still builds the image and uploads L5 (CUDA).
+
+**Yaml-gated nightly/weekly:** L4/L5 upload steps keep their normal label / `NIGHTLY` / `WEEKLY` conditions (for example PR `nightly-test` still uploads nightly). Only L2/L3 upload steps are matrix-gated.
+
+##### Whitelisted CI YAML → platform
+
+Register new files in `L2_YAML_FILES`, `L3_YAML_FILES`, or `L45_YAML_FILES` in `skip_ci.py`.
+
+| Level | File | Platform |
+| --- | --- | --- |
+| L2 | `.buildkite/cuda/test-ready.yml` | cuda |
+| L2 | `.buildkite/npu/test-npu-ready.yml` | npu |
+| L2 | `.buildkite/amd/test-amd-ready.yml` | amd |
+| L2 | `.buildkite/intel/pipeline-intel.yml` | intel |
+| L3 | `.buildkite/cuda/test-merge.yml` | cuda |
+| L3 | `.buildkite/amd/test-amd-merge.yml` | amd |
+| L4/L5 | `.buildkite/cuda/test-nightly.yml`, `test-weekly.yml` | cuda |
+| L4/L5 | `.buildkite/npu/test-npu-nightly.yml` | npu |
+
+##### Yaml-gated category branches
+
+`L2?` / `L3?` / `L45?` = whether any file in that whitelist changed. Untouched platforms/levels end up off.
+
+| ID | L2? | L3? | L45? | Enabled L2/L3 |
+| --- | --- | --- | --- | --- |
+| G1 | | | ✓ | none (all L2/L3 off) |
+| G2 | ✓ | | | L2 only on platforms whose L2 YAML changed |
+| G3 | | ✓ | | L3 only on platforms whose L3 YAML changed |
+| G4 | ✓ | ✓ | | union of G2 and G3 (same platform ready+merge → both on) |
+| G5 | ✓ | | ✓ | same as G2 (L45 rescued by L2 YAML) |
+| G6 | | ✓ | ✓ | same as G3 (L45 rescued by L3 YAML) |
+| G7 | ✓ | ✓ | ✓ | same as G4 |
+
+##### Yaml-gated examples (platform-level)
+
+Docs/skip-mark mixed with any row below follows the same matrix.
+
+| Diff (whitelist) | Branch | Enabled L2/L3 |
+| --- | --- | --- |
+| Only `cuda/test-ready.yml` | G2 | `cuda/l2` |
+| Only `amd/test-amd-ready.yml` | G2 | `amd/l2` |
+| Only `npu/test-npu-ready.yml` | G2 | `npu/l2` |
+| Only `intel/pipeline-intel.yml` | G2 | `intel/l2` |
+| Only `cuda/test-merge.yml` | G3 | `cuda/l3` |
+| Only `amd/test-amd-merge.yml` | G3 | `amd/l3` |
+| CUDA ready + CUDA merge | G4 | `cuda/l2` + `cuda/l3` |
+| CUDA ready + AMD merge | G4 | `cuda/l2` + `amd/l3` |
+| CUDA ready + AMD ready | G2 | `cuda/l2` + `amd/l2` |
+| CUDA merge + AMD merge | G3 | `cuda/l3` + `amd/l3` |
+| Only nightly / weekly / npu-nightly YAML | G1 | (none) |
+| CUDA ready + nightly | G5 | `cuda/l2` |
+| CUDA merge + nightly | G6 | `cuda/l3` |
+| CUDA ready + merge + nightly | G7 | `cuda/l2` + `cuda/l3` |
+
+##### How platforms consume the decision
+
+| Platform | Mechanism |
+| --- | --- |
+| **CUDA** | `upload_pipeline.py` injects `if` by step `key` (`image-build`, `upload-ready-pipeline`, `upload-merge-pipeline`, …) from `is_run(cuda, l2/l3)` |
+| **NPU** | Same injection; no merge upload step (L3 always off in bootstrap) |
+| **AMD / Intel** | `gate_bootstrap_ci ` exits 0 when `skip_all` or that target is off |
+
+Unit coverage: `tests/buildkite/test_skip_ci.py`.
+
+#### Step filtering {#step-filtering}
+
+CUDA **L2** (`.buildkite/cuda/test-ready.yml`) and **L3** (`.buildkite/cuda/test-merge.yml`) only. Bootstrap upload entry: `upload_pipeline.py --upload .buildkite/cuda/bootstrap-upload-steps.yml`.
+
+**Uploader-only keys** — removed before Buildkite sees the YAML; never used at runtime on agents:
+
+| Key | Purpose |
+| --- | ------- |
+| `source_file_dependencies` | List of path **prefixes**. If any changed file equals a prefix or starts with `prefix/`, keep the step (or group); otherwise omit it. |
+| `mirror_hardwares` | Expand to `agents` + `plugins` (+ optional `image`) from `ci_mirror_hardwares.yml`. |
+
+**Policy**
+
+- **Always uploaded** (no key): groups outside **E2E Test**—Simple Test, Diffusion unit tests, Engine/Model Executor, Distributed, Custom Pipeline, Entrypoints (L2), LoRA / Entrypoints (L3).
+- **Diff-gated**: every **E2E Test** leaf job. List the smallest prefix set per step—pytest file(s), model code under `vllm_omni/model_executor/models/` or `vllm_omni/diffusion/models/`, plus `stage_input_processors/` and `vllm_omni/deploy/*.yaml` when applicable. A **group** may define the key instead; the whole group drops if no prefix matches.
+
+**YAML examples**
+
+```yaml
+ - label: "Diffusion · Qwen Image Test"
+ source_file_dependencies:
+ - tests/e2e/online_serving/test_qwen_image.py
+ - vllm_omni/diffusion/models/qwen_image/
+ commands:
+ - pytest -s -v tests/e2e/online_serving/test_qwen_image.py -m 'core_model' ...
+ mirror_hardwares: h100_1
+
+ - label: "TTS · Qwen3-TTS CustomVoice Test"
+ source_file_dependencies:
+ - tests/e2e/online_serving/test_qwen3_tts_customvoice.py
+ - vllm_omni/model_executor/models/qwen3_tts/
+ - vllm_omni/model_executor/stage_input_processors/qwen3_tts.py
+ - vllm_omni/deploy/qwen3_tts.yaml
+ commands:
+ - pytest -s -v tests/e2e/online_serving/test_qwen3_tts_customvoice.py ...
+ mirror_hardwares: l4_4
+```
+
+**Local dry-run**
+
+```bash
+python3 .buildkite/common/scripts/upload_pipeline.py .buildkite/cuda/test-ready.yml
+python3 .buildkite/common/scripts/upload_pipeline.py .buildkite/cuda/test-merge.yml | grep source_file_dependencies
+# (no output expected)
+```
+
+On PR builds, `upload_pipeline.py` logs `skip '…' (no changes under …)` for omitted steps.
+
+### Labels and grouping
+
+- Use **groups** for dashboard readability; keep **E2E Test** as the group name CUDA diff filtering expects for `--e2e` nightly runs on main.
+- Prefix labels by model domain: **Omni ·**, **TTS ·**, **Diffusion ·**, **Simple ·**, etc., matching existing steps.
+
+### Validation checklist
+
+| Check | Command / location |
+| ----- | ------------------ |
+| CUDA render | `python3 .buildkite/common/scripts/upload_pipeline.py .buildkite/cuda/test-.yml` |
+| No leaked uploader keys | `… \| grep -E 'mirror_hardwares|source_file_dependencies'` → empty |
+| Skip-ci / upload unit tests | `pytest tests/buildkite/` |
+| Local job replay (CUDA L2+) | `tools/run_ready_jobs.sh`, `tools/run_merge_jobs.sh`, `tools/nightly/run_nightly_jobs.sh` (read YAML from `cuda/`) |
+
+## Related documentation
+
+- [Test System Overview](./test_system_overview.md) — L1–L5 scope, triggers, test pyramid
+- [Test Writing Guide](./test_writing_guide.md) — markers, directories, L1–L5 examples
+- [Test Execution Guide](./test_execution_guide.md) — running CI-aligned jobs locally
+- Implementation: `.buildkite/common/scripts/upload_pipeline.py`, `.buildkite/common/scripts/skip_ci.py`
diff --git a/docs/contributing/ci/failures.md b/docs/contributing/ci/failures.md
index 80699e6a5..9743097cf 100644
--- a/docs/contributing/ci/failures.md
+++ b/docs/contributing/ci/failures.md
@@ -79,7 +79,7 @@ For resilience behavior under injected process signals/OOM pressure (validation
### Running Tests Locally
-For instructions on running tests locally (including specific test files, functions, and markers), see the [Running Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/test_guide/#running-tests) section in the Test Guide.
+For instructions on running tests locally (including specific test files, functions, and markers), see the [Running Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/test_execution_guide/#running-tests) section in the Test Execution guide.
## Step 4: Raise an Issue or Fix It
diff --git a/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md b/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md
index 688933f13..5e65d2f48 100644
--- a/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md
+++ b/docs/contributing/ci/test_examples/l4_functionality_tests.inc.md
@@ -1,6 +1,6 @@
**Scope**
-For diffusion models, the L4 functionality test covers all or common *diffusion features* that are supported by this model, including several [parallelism acceleration methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism_acceleration/), [CPU offloading](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cpu_offload_diffusion/), [TeaCache](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/teacache/) and [Cache-DiT](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cache_dit_acceleration/) cache backends, [quantization methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/quantization/overview/).
+For diffusion models, the L4 functionality test covers all or common *diffusion features* that are supported by this model, including several [parallelism acceleration methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/parallelism_acceleration/), [CPU offloading](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cpu_offload/), [TeaCache](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/teacache/) and [Cache-DiT](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/diffusion/cache_dit_acceleration/) cache backends, [quantization methods](https://docs.vllm.ai/projects/vllm-omni/en/latest/user_guide/quantization/overview/).
**Test Case Design**
@@ -37,7 +37,7 @@ Currently all the features are available in online serving mode. Hence, only nee
**Code Style**
- Validation: test that the multimodal output files of your model have the correct shapes. `OpenAIClientHandler.send_diffusion_request` should have taken care of this.
-- Test marks: always add `full_model` and `diffusion` for L4 nightly `test_*_expansion.py` cases. Add GPU-related marks if needed. Ref: [Markers for Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/tests_markers/).
+- Test marks: always add `full_model` and `diffusion` for L4 nightly `test_*_expansion.py` cases. Add GPU-related marks if needed. Ref: [Markers for Tests](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/ci/test_writing_guide/#markers-for-tests).
- To maximize code reuse, you may refer to
- `tests/conftest.py` for `omni_server` (running server in subprocess) and `openai_client` fixtures (sending requests and validating output), `generate_synthetic_image` and `assert_XXX_valid` helper.
- `tests/helpers/mark.py` for `@hardware_test(...)` and `hardware_marks`.
diff --git a/docs/contributing/ci/test_examples/l4_performance_tests.inc.md b/docs/contributing/ci/test_examples/l4_performance_tests.inc.md
index f1f3073dc..8ff27ee08 100644
--- a/docs/contributing/ci/test_examples/l4_performance_tests.inc.md
+++ b/docs/contributing/ci/test_examples/l4_performance_tests.inc.md
@@ -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 `):
+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],
@@ -17,9 +44,11 @@ When you want to add L4-level ***performance test*** cases, you can refer to the
"ignore_eos": true,
"percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration",
"baseline": {
- "mean_ttft_ms": [500, 800],
- "mean_audio_ttfp_ms": [2000, 3500],
- "mean_audio_rtf": [0.25, 0.35]
+ "H100": {
+ "mean_ttft_ms": [500, 800],
+ "mean_audio_ttfp_ms": [2000, 3500],
+ "mean_audio_rtf": [0.25, 0.35]
+ }
}
}
]
@@ -33,8 +62,91 @@ 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
+```
+
+See also [Markers for Tests](#markers-for-tests) for registered hardware markers (`H100`, `L4`, `cuda`, `distributed_cuda`, …).
**`server_params` Configuration**
@@ -79,11 +191,13 @@ 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). |
-| baseline | object | No | see above | Optional per-metric thresholds; keys must match benchmark output fields. Scalar, list (per sweep step), or object (keyed by concurrency or QPS string).
+| baseline | object | No | see below | Optional thresholds. **Hardware-nested only**: top-level keys must be `[hardware-resource]` pytest markers from `pyproject.toml` (`get_hardware_mark_list()`). Under each hardware key, metric names are free-form; values are scalars or sweep-aligned lists. Example: `{"H100": {"throughput_qps": […], "custom_stage_ms": 1.0}, "A3": {…}}`. Runtime filename aliases (`_RUNTIME_DEVICE_ALIASES`) are separate and do not expand this allowlist. |
diff --git a/docs/contributing/ci/test_guide.md b/docs/contributing/ci/test_execution_guide.md
similarity index 74%
rename from docs/contributing/ci/test_guide.md
rename to docs/contributing/ci/test_execution_guide.md
index 26996129e..d1c2c560f 100644
--- a/docs/contributing/ci/test_guide.md
+++ b/docs/contributing/ci/test_execution_guide.md
@@ -1,4 +1,4 @@
-# Test Guide
+# Test Execution Guide
## Setting Up the Test Environment
### Creating a Container
vLLM-Omni provides an official Docker image for deployment. These images are built upon vLLM Docker images and are available on [Docker Hub](https://hub.docker.com/r/vllm/vllm-omni/tags). The version of vLLM-Omni indicates which vLLM release it is based on.
@@ -49,11 +49,11 @@ Failed jobs: 1/2
cd tests
pytest -s -v -m "core_model and cpu"
```
- The latest test command is available in the "Simple Unit Test" step of this [pipeline](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-ready.yml).
+ The latest test command is available in the "Simple Unit Test" step of this [pipeline](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-ready.yml).
=== "L2 level"
- **Recommended:** run CI-aligned jobs from the repo root via [`tools/run_ready_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/run_ready_jobs.sh). The script reads [`.buildkite/test-ready.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-ready.yml), generates per-step bash wrappers, runs pytest, and tees logs under `logs/ready_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts) for timing and timeout behavior). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
+ **Recommended:** run CI-aligned jobs from the repo root via [`tools/run_ready_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/run_ready_jobs.sh). The script reads [`.buildkite/cuda/test-ready.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-ready.yml), generates per-step bash wrappers, runs pytest, and tees logs under `logs/ready_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts) for timing and timeout behavior). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
```bash
# All L2 and L1 jobs (default: every pytest step in test-ready.yml)
@@ -85,7 +85,7 @@ Failed jobs: 1/2
=== "L3 level"
- **Recommended:** run CI-aligned jobs from the repo root via [`tools/run_merge_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/run_merge_jobs.sh). The script reads [`.buildkite/test-merge.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-merge.yml), generates per-step bash wrappers, runs pytest, and tees logs under `logs/merge_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts)). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
+ **Recommended:** run CI-aligned jobs from the repo root via [`tools/run_merge_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/run_merge_jobs.sh). The script reads [`.buildkite/cuda/test-merge.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-merge.yml), generates per-step bash wrappers, runs pytest, and tees logs under `logs/merge_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts)). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
```bash
# All L3 and L1 jobs (default: every pytest step in test-merge.yml)
@@ -117,7 +117,7 @@ Failed jobs: 1/2
=== "L4 level"
- **Recommended:** run CI-aligned nightly jobs from the repo root via [`tools/nightly/run_nightly_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/nightly/run_nightly_jobs.sh). The script reads [`.buildkite/test-nightly.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-nightly.yml), generates per-step bash wrappers, runs pytest (perf jobs first, then aggregates perf JSON into Excel), and tees logs under `logs/nightly_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts); `generate_nightly_perf_excel` is included in the timing summary). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
+ **Recommended:** run CI-aligned nightly jobs from the repo root via [`tools/nightly/run_nightly_jobs.sh`](https://github.com/vllm-project/vllm-omni/blob/main/tools/nightly/run_nightly_jobs.sh). The script reads [`.buildkite/cuda/test-nightly.yml`](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-nightly.yml), generates per-step bash wrappers, runs pytest (perf jobs first, then aggregates perf JSON into Excel), and tees logs under `logs/nightly_jobs/` (see [CI job runners](#ci-job-runners-l2l4-logs-timing-and-timeouts); `generate_nightly_perf_excel` is included in the timing summary). Requires `bash`, `python3`, and PyYAML (`pip install pyyaml`).
```bash
# All nightly jobs (default: test-type all, model-type all)
@@ -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"
@@ -183,9 +189,9 @@ Failed jobs: 1/2
```
- The latest L5 CI jobs (reliability + invalid-parameter weekly steps) are in [test-weekly.yml](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/test-weekly.yml).
+ The latest L5 CI jobs (reliability + invalid-parameter weekly steps) are in [test-weekly.yml](https://github.com/vllm-project/vllm-omni/blob/main/.buildkite/cuda/test-weekly.yml).
-You can find more information about markers in the documentation: [marker doc](./tests_markers.md)
+You can find more information about markers in [Test Writing Guide — Markers for Tests](./test_writing_guide.md#markers-for-tests).
## Adding New Test Cases
-Please refer to the [L5 Layering Specification document](./CI_5levels.md).
+Please refer to the [Test System Overview](./test_system_overview.md) and [Test Writing Guide](./test_writing_guide.md).
diff --git a/docs/contributing/ci/test_system_overview.md b/docs/contributing/ci/test_system_overview.md
new file mode 100644
index 000000000..335ed5c53
--- /dev/null
+++ b/docs/contributing/ci/test_system_overview.md
@@ -0,0 +1,214 @@
+# Multi-Level Automated Testing System Overview
+
+## Document Overview
+
+This testing system aims to build a complete, efficient, and well-structured quality assurance framework for the development, integration, and release of model services. It draws on the concept of the test pyramid from modern software engineering, progressively expanding testing activities from basic code logic verification to complex end-to-end (E2E) functionality, performance, accuracy, and even long-term stability validation.
+
+Through five levels (L1-L5) and common (Common) specifications, the system clarifies the testing objectives, scope, execution frequency, and required resources for different development stages (e.g., each commit, PR merge, daily build, pre-release). This ensures that models meet high standards for functionality, performance, and reliability across various deployment scenarios (online serving and offline inference).
+
+
Unit tests for components like entrypoints, models
+
/
+
/
+
/
+
core_model and cpu
+
<15min
+
+ tests/{component}/… mirroring vllm_omni/{component}/
+ (e.g. tests/diffusion/, tests/engine/, tests/entrypoints/)
+ Do not add new top-level tests/ dirs unrelated to a component.
+
+
+ L1 & L2
+ Section 1 L1&L2: Purpose, Test Content, Directory Location, Example
+
+
PR with ready label (also can run locally)
+
CPU
+
+
+
L2 (E2E across models & GPU-required UT)
+
Online (basic deployment scenarios): dummy, normal inference function (output format, stream), some instance startup UT
+
High-priority models + online basic scenarios; request success, non-empty output, format match (no Whisper/accuracy)
+
High-priority features and component tests that require launching instances
+
High-priority interfaces (using random lightweight models)
+
core_model and hardware_test(H100, L4, etc.) and omni/tts/diffusion
+ L5
+ L5: Purpose, Test Content, Directory Location, Example
+
+
Weekly / Days before Release
+
GPU
+
+
+
+
+**Test Dir placement (summary):** component / unit under `tests/{component}/` mirroring `vllm_omni/`; model E2E under `tests/e2e/online_serving/`, `tests/e2e/offline_inference/`, and `tests/e2e/accuracy/`; feature integration under `tests/e2e/features//`; doc example tests under `tests/examples/online_serving/` and `tests/examples/offline_inference/`; performance under `tests/dfx/perf/`; stability under `tests/dfx/stability/`; reliability under `tests/dfx/reliability/`. Do **not** add new top-level directories under `tests/` that are unrelated to a `vllm_omni` component (or to the established `e2e` / `dfx` / `helpers` / `examples` / `buildkite` layout).
+
+For per-level test authoring (markers, examples), see [Test Writing Guide](./test_writing_guide.md).
+
+## Common Specifications
+
+Before entering specific testing levels, the project establishes two common specifications aimed at standardizing the development process and quickly locating issues.
+
+1. ***PR Checklist ([`.github/PULL_REQUEST_TEMPLATE.md`](https://github.com/vllm-project/vllm-omni/blob/main/.github/PULL_REQUEST_TEMPLATE.md))***: This template defines the self-check items that must be completed before submitting a code review (Pull Request). It ensures that each code change meets basic requirements such as code style, dependency updates, and documentation synchronization before entering the automated testing pipeline, serving as the first manual line of defense for quality assurance.
+2. ***CI Failure Explanation ([CI Failures](./failures.md))***: This document archives and explains common failure patterns in the Continuous Integration (CI) pipeline, error log interpretation, and preliminary troubleshooting steps. It helps developers and testers quickly diagnose the causes of automated test failures, improving problem-solving efficiency.
+
+## Notes
+
+### L2 / L3 diff-aware CI (CUDA)
+
+On CUDA **L2** and **L3**, **E2E Test** Buildkite jobs may be omitted at pipeline upload when the PR diff does not touch their path prefixes; other groups still always upload. Full two-layer mechanics and YAML examples: [CI Settings — Diff-aware CI](./ci_settings.md#diff-aware-ci) ([step filtering](./ci_settings.md#step-filtering)).
+
+### Test helper environment variables
+
+Some shared helpers under `tests/helpers/` honor optional environment variables for local debugging. These are **not** set in CI by default.
+
+| Variable | Accepted values | Description |
+| -------- | --------------- | ----------- |
+| `VLLM_OMNI_KEEP_REQUEST_MEDIA` | `1`, `true`, `yes` (case-insensitive) | When enabled, temporary WAV files created by `tests.helpers.media.convert_audio_bytes_to_text` are **not** deleted when the pytest process exits. By default, each call writes a unique file under the system temp directory via `tempfile.mkstemp` and registers `atexit` cleanup. Use this when debugging audio output validation (Whisper transcription, keyword checks, text–audio similarity). The saved path is logged as `audio data is saved: `. |
+
+Example (Linux / macOS):
+
+```bash
+export VLLM_OMNI_KEEP_REQUEST_MEDIA=1
+pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -k test_mix_to_text_audio
+```
+
+Example (Windows PowerShell):
+
+```powershell
+$env:VLLM_OMNI_KEEP_REQUEST_MEDIA = "1"
+pytest -s -v tests/e2e/online_serving/test_qwen3_omni.py -k test_mix_to_text_audio
+```
+
+## Summary
+
+This multi-level testing system achieves continuous, progressive validation of model service quality by tightly integrating testing activities with the development workflow (commit, review, merge, release). From rapid unit testing to comprehensive end-to-end testing, and further to in-depth performance, stability, and reliability verification, each level has clear objectives, collectively building a robust quality protection net. By following this system, teams can deliver high-quality, highly reliable model services more efficiently.
diff --git a/docs/contributing/ci/test_writing_guide.md b/docs/contributing/ci/test_writing_guide.md
new file mode 100644
index 000000000..397f5e83c
--- /dev/null
+++ b/docs/contributing/ci/test_writing_guide.md
@@ -0,0 +1,712 @@
+# Test Writing Guide
+
+This document describes how to author tests for each CI level (L1–L5). For the level matrix, CI triggers, and Buildkite upload behavior, see [Test System Overview](./test_system_overview.md).
+
+## Test Directory Structure
+
+Canonical placement by CI level is in the **Test Dir** column of the level matrix in [Test System Overview](./test_system_overview.md).
+
+Summary:
+
+- **Component / unit:** `tests/{component}/…` mirroring `vllm_omni/{component}/`
+- **Model E2E:** `tests/e2e/online_serving/`, `tests/e2e/offline_inference/`, `tests/e2e/accuracy/`
+- **Feature integration:** `tests/e2e/features//`
+- **Doc example tests:** `tests/examples/online_serving/`, `tests/examples/offline_inference/`
+- **Performance:** `tests/dfx/perf/tests/` (JSON configs; runners under `tests/dfx/perf/scripts/`)
+- **Stability:** `tests/dfx/stability/tests/`
+- **Reliability:** `tests/dfx/reliability/`
+- Do **not** add new top-level `tests/` directories unrelated to a component (or to `e2e` / `dfx` / `helpers` / `examples` / `buildkite`)
+
+Deploy YAMLs: `vllm_omni/deploy/` via `tests.helpers.stage_config.get_deploy_config_path` (see [stage configs](../../configuration/stage_configs.md)).
+
+## Markers for Tests
+
+By adding markers before test functions, tests can later be executed uniformly by simply declaring the corresponding marker type.
+
+### Current Markers
+Defined in `pyproject.toml`:
+
+| Marker | Description |
+| ------------------ | --------------------------------------------------------- |
+| `core_model` | L1&L2 tests (run in each PR) |
+| `advanced_model` | L3 tests (run on each merge to main) |
+| `full_model` | L4 tests (run nightly) |
+| `diffusion` | Diffusion model tests |
+| `omni` | Omni multimodal model tests |
+| `tts` | TTS model tests |
+| `cache` | Cache backend tests |
+| `parallel` | Parallelism/distributed tests |
+| `cpu` | Tests that run on CPU |
+| `gpu` | Tests that run on GPU * |
+| `cuda` | Tests that run on CUDA * |
+| `rocm` | Tests that run on AMD/ROCm * |
+| `xpu` | Tests that run on Intel XPU * |
+| `npu` | Tests that run on NPU/Ascend * |
+| `H100` | Tests that require H100 GPU * |
+| `L4` | Tests that require L4 GPU * |
+| `MI325` | Tests that require MI325 GPU (AMD/ROCm) * |
+| `A2` | Tests that require A2 NPU * |
+| `A3` | Tests that require A3 NPU * |
+| `distributed_cuda` | Tests that require multi cards on CUDA platform * |
+| `distributed_rocm` | Tests that require multi cards on ROCm platform * |
+| `distributed_npu` | Tests that require multi cards on NPU platform * |
+| `skipif_cuda` | Skip if the num of CUDA cards is less than the required * |
+| `skipif_rocm` | Skip if the num of ROCm cards is less than the required * |
+| `skipif_npu` | Skip if the num of NPU cards is less than the required * |
+| `slow` | Slow tests (may skip in quick CI) |
+| `benchmark` | Benchmark tests (decorator on runner test functions; perf JSON uses `full_model` + type marker instead) |
+| `local_model` | Tests requiring local / non-HF-hub model weights |
+
+\* Means those markers are auto-added by `@hardware_test` (parametrization decorator) or `hardware_marks` (only returning the list of marks for flexibility).
+
+#### Example usage for markers
+
+```python
+from tests.helpers.mark import hardware_test
+
+@pytest.mark.core_model
+@pytest.mark.omni
+@hardware_test(
+ res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
+ num_cards=2,
+)
+@pytest.mark.parametrize("omni_server", test_params, indirect=True)
+def test_video_to_audio()
+ ...
+```
+
+#### Decorator: `@hardware_test`
+
+This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/helpers/mark.py` performs the following actions:
+
+1. **Applies platform and resource markers**
+ Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `MI325`, `B60`, `A2`, `A3`).
+ ```
+ @pytest.mark.cuda
+ @pytest.mark.L4
+ ```
+2. **Handles multi-card (distributed) scenarios**
+ For tests requiring multiple cards, it automatically adds distributed markers such as `distributed_cuda`, `distributed_rocm`, or `distributed_npu`.
+ ```
+ @pytest.mark.distributed_cuda(num_cards=num_cards)
+ ```
+3. **Supports flexible card requirements**
+ Accepts `num_cards` as either a single integer for all platforms or as a dictionary with per-platform values. If not specified, defaults to 1 card per platform.
+
+4. **Integrates resource validation**
+ On CUDA, adds a skip marker (`skipif_cuda`) if the system does not have the required number of devices.
+ Support for `skipif_rocm` and `skipif_npu` will be implemented later.
+
+
+5. **Works with pytest filtering**
+ Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "distributed_cuda and L4"`).
+
+##### Example usage for decorator
+- Single call for multiple platforms:
+ ```python
+ @hardware_test(
+ res={"cuda": "L4", "rocm": "MI325", "xpu": "B60", "npu": "A2"},
+ num_cards={"cuda": 2, "rocm": 2, "xpu": 2, "npu": 2},
+ )
+ ```
+ or
+ ```python
+ @hardware_test(
+ res={"cuda": "L4", "rocm": "MI325", "xpu": "B60", "npu": "A2"},
+ num_cards=2,
+ )
+ ```
+- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), XPU (B60), MUSA (S5000), NPU (A2/A3)
+- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
+- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
+- Filtering examples:
+ - CUDA only: `pytest -m "distributed_cuda and L4"`
+ - ROCm only: `pytest -m "distributed_rocm and MI325"`
+ - NPU only: `pytest -m "distributed_npu"`
+
+#### Function: `hardware_marks`
+
+`hardware_marks` returns a list of pytest mark objects with the same signature as `@hardware_test`. Use it when you need more flexibility, such as attaching hardware marks to individual `pytest.param` entries rather than an entire test function.
+
+```python
+from tests.helpers.mark import hardware_marks
+
+MULTI_CARD_MARKS = hardware_marks(
+ res={"cuda": "H100", "rocm": "MI325", "npu": "A2"}, num_cards=2
+)
+
+@pytest.mark.parametrize("omni_server", [
+ pytest.param(OmniServerParams(...), id="case_001", marks=MULTI_CARD_MARKS),
+], indirect=True)
+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__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:
+
+1. **Extend the marker list in your pytest config** so that platform/resource markers are defined:
+ ```toml
+ # In pyproject.toml or pytest.ini
+ [tool.pytest.ini_options]
+ markers = [
+ # ... existing markers ...
+ "tpu: Tests that require TPU device",
+ "TPU_V3: Tests that require TPU v3 hardware",
+ "distributed_tpu: Tests that require multiple TPU devices",
+ ]
+ ```
+2. **Implement a marker construction function for your platform** in `vllm-omni/tests/helpers/mark.py`:
+ ```python
+ # In vllm-omni/tests/helpers/mark.py
+
+ def tpu_marks(*, res: str, num_cards: int):
+ test_platform = pytest.mark.tpu
+ if res == "TPU_V3":
+ test_resource = pytest.mark.TPU_V3
+ else:
+ raise ValueError(
+ f"Invalid TPU resource type: {res}. Supported: TPU_V3")
+
+ if num_cards == 1:
+ return [test_platform, test_resource]
+ else:
+ test_distributed = pytest.mark.distributed_tpu(num_cards=num_cards)
+ # Optionally: add skipif_tpu when implemented
+ return [test_platform, test_resource, test_distributed]
+ ```
+3. **Update `hardware_marks` to recognize your new platform**:
+ In the relevant place (see the `hardware_marks` implementation), add:
+ ```python
+ if platform == "tpu":
+ marks = tpu_marks(res=resource, num_cards=cards)
+ ```
+ (`hardware_test` calls `hardware_marks` internally, so both will pick up the change.)
+4. **(Recommended) Add a test using your new markers**:
+ ```python
+ @hardware_test(
+ res={"tpu": "TPU_V3"},
+ num_cards=2,
+ )
+ def test_my_tpu_feature():
+ ...
+ ```
+
+**Summary**:
+- Add pytest markers for your new platform/resources
+- Implement a marker function (`xxx_marks`)
+- Plug into `hardware_marks`
+- You're done: tests using `@hardware_test` or `hardware_marks` with your platform now automatically get the correct markers, distribution, and isolation!
+
+See code in `vllm-omni/tests/helpers/mark.py` for existing examples (`cuda_marks`, `rocm_marks`, `npu_marks`).
+
+## Test case style
+
+### L1 & L2 Level Testing - Unit Testing and Basic End-to-End Verification
+
+#### 1.1 Testing Purpose
+
+L1 and L2 level testing form the foundation of the quality assurance system. L1 level testing focuses on verifying the internal logic correctness of code units (e.g., functions, classes), ensuring each independent component behaves as designed.
+
+L2 level testing builds upon L1 by introducing GPU resources and verifying that the end-to-end (E2E) process of the model in basic deployment scenarios is smooth. For example, it uses dummy models to confirm that core interfaces like the inference pipeline, output format, and streaming response work properly. The common goal of these two levels is to provide developers with rapid feedback, discovering and fixing issues early in the development cycle.
+
+
+
+#### 1.2 Testing Content and Scope
+
+- ***L1 (Unit & Logic Testing)***:
+- - ***Scope***: Tests internal functions and methods of core components such as `entrypoints`, `models`.
+ - ***Focus***: Branch coverage, exception handling, algorithm logic correctness. Does not involve external dependencies or the complete service stack.
+ - ***Time Cost***: Execution time is controlled within ***15 minutes*** to ensure fast feedback.
+- ***L2 (Basic End-to-End Testing)***:
+- - ***Scope***: Covers two basic deployment scenarios: `online` (serving) and `offline` (inference).
+ - ***Focus***: Uses `dummy` weights (via deploy YAML patching at `core_model`) or lightweight real models to verify that the entire chain from request input to result output works normally, including output data structure, streaming (stream) support, and **cheap payload checks** at `--run-level core_model` (see below). Also includes some unit tests that require launching independent service instances.
+ - ***L2 response validation (`core_model`)***: Implemented in `tests/helpers/assertions.py` and invoked by `OpenAIClientHandler` based on `--run-level`. At L2 we require **request success** plus minimal output sanity—not full accuracy:
+ - **Speech / TTS** (`assert_audio_speech_response`): decoded audio must be present and non-empty (or exceed `min_audio_bytes` when set in `request_config`); `response_format` must match the returned content-type (e.g. `wav`, `pcm`). Whisper transcript similarity, PCM HNR, and preset-voice gender checks run only at L3+.
+ - **Diffusion** (`assert_diffusion_response`): at least one non-empty image, video, or audio artifact. Resolution/frame-count parity and other deep checks run only at L3+.
+ - **Omni multimodal** (`assert_omni_response`): L2 asserts successful completion; keyword, transcript, and cross-modal similarity checks run only at L3+.
+ - ***Characteristic***: Requires ***GPU*** resources to perform model computations.
+
+#### 1.3 Test Directory and Execution Files
+
+A clear directory structure is key to managing test cases efficiently. See the **Test Dir** column in [Test System Overview](./test_system_overview.md) for the canonical placement rules.
+
+- ***L1 (component / unit)***: `tests/{component_name}/…` mirroring `vllm_omni/{component_name}/` (e.g. `tests/diffusion/`, `tests/entrypoints/`). Do **not** invent new top-level `tests/` folders unrelated to a component.
+- ***L2 (basic model E2E)***:
+ - Online serving: `tests/e2e/online_serving/test_{model_name}.py`
+ - Offline inference: `tests/e2e/offline_inference/test_{model_name}.py`
+- ***Feature-level integration*** (cross-cutting, not a single-model smoke): `tests/e2e/features//` (e.g. `fullduplex/`, `custom_pipeline/`, `rlhf_test/`).
+
+#### 1.4 Execution Method and Example
+
+- ***Trigger Timing***: **`PR with ready label`**. That is, when a developer adds a "ready for review" or similar label to a PR on platforms like GitHub, L1 and L2 tests are automatically triggered.
+- ***Diff-aware step skipping***: On L2, **E2E Test** jobs may be omitted at pipeline upload when the PR diff does not touch their [`source_file_dependencies`](ci_settings.md#step-filtering) prefixes; non-E2E groups still always upload. See [CI Settings — Diff-aware CI](ci_settings.md#diff-aware-ci).
+- ***Execution Environment***: L1 uses ***CPU*** environment; L2 requires ***GPU*** environment.
+- ***Script Example***:
+
+
+ L1 Test Examples
+
+Examples from `tests/model_executor/models/qwen2_5_omni/test_audio_length.py`
+```python
+# SPDX-License-Identifier: Apache-2.0
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
+
+import pytest
+
+pytestmark = [pytest.mark.core_model, pytest.mark.cpu]
+
+def test_resolve_max_mel_frames_default():
+ from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
+
+ assert resolve_max_mel_frames(None, default=30000) == 30000
+ assert resolve_max_mel_frames(None, default=6000) == 6000
+
+
+def test_resolve_max_mel_frames_explicit():
+ from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import resolve_max_mel_frames
+
+ # Explicit argument always wins over default
+ assert resolve_max_mel_frames(123, default=30000) == 123
+ assert resolve_max_mel_frames(6000, default=30000) == 6000
+ assert resolve_max_mel_frames(0, default=30000) == 0
+
+
+@pytest.mark.parametrize("repeats", [2, 4])
+@pytest.mark.parametrize("code_len", [0, 1, 32768])
+@pytest.mark.parametrize("max_mel_frames", [None, -1, 0, 1, 6000, 30000])
+def test_cap_and_align_mel_length_no_mismatch(repeats, code_len, max_mel_frames):
+ """Guard that any max_mel_frames yields a mel length aligned to repeats, and
+ consistent with the truncated code length (prevents concat mismatch).
+ """
+ from vllm_omni.model_executor.models.qwen2_5_omni.audio_length import cap_and_align_mel_length
+
+ target_code_len, target_mel_len = cap_and_align_mel_length(
+ code_len=code_len,
+ repeats=repeats,
+ max_mel_frames=max_mel_frames,
+ )
+
+ assert isinstance(target_code_len, int)
+ assert isinstance(target_mel_len, int)
+
+ if code_len == 0:
+ assert target_code_len == 0
+ assert target_mel_len == 0
+ return
+
+ assert target_code_len >= 1
+ assert target_mel_len >= repeats
+ assert target_mel_len % repeats == 0
+ assert target_mel_len == target_code_len * repeats
+ assert target_code_len <= code_len
+
+ if max_mel_frames is not None and int(max_mel_frames) > 0 and int(max_mel_frames) >= repeats:
+ assert target_mel_len <= int(max_mel_frames)
+```
+
+
+
+ L2 Test Examples
+You can refer to Test Examples in the L3 section to see example test cases that incorporate both L2 and L3 testing logic.
+
+
+- - ***Run Command***:
+
+ `pytest -s -v /tests/e2e/online_serving/test_{model_name}.py`
+ `pytest -s -v -m 'core_model and cpu' --run-level=core_model`
+
+### L3 Level Testing - Core Integration, Performance, and Accuracy Verification
+
+#### 2.1 Testing Purpose
+
+L3 level testing executes after code is merged into the main branch. Its core purpose is to verify the integration effect, key performance indicators, and output accuracy of ***real models*** in ***multiple deployment scenarios***
+
+. It acts as the "quality gatekeeper" for the main branch, ensuring that no merge breaks the core capabilities of the model service. Testing needs to provide clear conclusions within a relatively short time (<30min), balancing test depth with feedback speed.
+
+
+
+#### 2.2 Testing Content and Scope
+
+- ***Deployment Scenarios***: Covers richer `online` and `offline` deployment configurations, which may include different hardware configurations, batch sizes, concurrency levels, etc.
+- ***Core Verification***:
+- 1. ***Inference Functionality***: Ensures real models can perform forward computation normally and return results.
+ 2. ***Accuracy Compliance***: Verifies that the model's evaluation metrics (e.g., accuracy) meet the expected baseline, preventing code changes from introducing accuracy issues.
+ 3. ***Important Performance***: Verifies whether performance (e.g., P99 latency, throughput) in core scenarios meets preset thresholds.
+- ***L3 response validation (`advanced_model`)***: At `--run-level advanced_model`, `tests/helpers/assertions.py` adds semantic checks on top of L2 payload gates (see L1 & L2 above):
+ - **Speech / TTS** (`assert_audio_speech_response`): Whisper transcript of returned audio vs `request_config["input"]` (cosine similarity > 0.9 when input text is set); optional `min_audio_bytes` floor; PCM harmonic-to-noise ratio when `response_format` is `pcm`; **preset voice gender** via `_assert_preset_voice_gender_from_audio` when `voice` matches a known preset in `_PRESET_VOICE_GENDER_MAP` (pitch/F0-based classifier; skipped for unknown voices or `pcm` output).
+ - **Omni multimodal** (`assert_omni_response`): non-empty text/audio outputs; `key_words` in transcript or text; text–audio similarity / containment; preset **`speaker`** gender check (same helper as TTS).
+ - **Diffusion** (`assert_*_diffusion_response`): image/video dimension and frame-count parity with request parameters where configured.
+ - **Accuracy suites**: pixel/video similarity and metric baselines under `/tests/e2e/accuracy/` (separate from inline helper assertions).
+
+#### 2.3 Test Directory and Execution Files
+
+- ***Functional / model E2E***:
+ - Online serving: `tests/e2e/online_serving/test_{model_name}_expansion.py`
+ - Offline inference: `tests/e2e/offline_inference/test_{model_name}_expansion.py`
+ - Accuracy / similarity: `tests/e2e/accuracy/`
+ - (`_expansion.py` holds broader scenario coverage than the L2 smoke file.)
+- ***Feature-level integration***: `tests/e2e/features//` (not under a new top-level `tests/` directory).
+
+#### 2.4 Execution Method and Example
+
+- ***Trigger Timing***: **`PR Merged`**. Automatically triggered after code review is approved and merged into the main branch (typically via `merge-test` label on the PR before merge).
+- ***Diff-aware step skipping***: Same [`source_file_dependencies`](ci_settings.md#step-filtering) mechanism as L2—**all E2E Test** leaf steps are diff-gated; other L3 groups always upload. See [CI Settings — Diff-aware CI](ci_settings.md#diff-aware-ci).
+- ***Execution Environment***: ***GPU*** servers.
+- ***Script Example***:
+
+???+ example "Test Examples"
+
+ **2.4.1 Mark Declaration Section**
+
+ ```python
+ @pytest.mark.advanced_model
+ @pytest.mark.core_model
+ @pytest.mark.parametrize("omni_server", test_params, indirect=True)
+ ```
+
+ **Explanation**:
+
+ @pytest.mark.advanced_model: Marks the test as L3 merge level (`--run-level advanced_model`): real weights, Whisper/similarity/keyword checks, diffusion deep checks, and **preset voice/speaker gender** validation where applicable. @pytest.mark.full_model: Marks L4 nightly-only suites (e.g. `test_*_expansion.py`, doc examples).
+
+ @pytest.mark.core_model: Marks the test as L1 or L2 level. At `--run-level core_model`, validation is limited to request success plus cheap payload checks (e.g. non-empty audio bytes, response format/content-type, non-empty diffusion outputs)—not Whisper, keyword, or accuracy gates. Deploy YAML may use `load_format: dummy` for fast PR feedback.
+
+ @pytest.mark.parametrize: A parameterization decorator that allows abstracting test data into parameters, enabling reuse of the same test logic across different data configurations. indirect=True indicates that parameters will be passed to the fixture for processing.
+
+ **Notes**: If you believe the test case only needs to execute basic run logic at the PR-level CI, you can mark it only with @pytest.mark.core_model. If you believe it only needs to execute deep validation at merge (L3), use @pytest.mark.advanced_model. For L4 nightly-only expansion and doc-example tests, use @pytest.mark.full_model with `--run-level full_model`. If the test case needs both basic run and deep validation, mark with @pytest.mark.core_model and the appropriate L3/L4 marker (`advanced_model` and/or `full_model`).
+
+ **2.4.2 Test Function Definition and Documentation**
+
+ ```python
+ def test_mix_to_text_audio_001(omni_server, openai_client) -> None:
+ """
+ Test multi-modal input processing and text/audio output generation via OpenAI API.
+ Deploy Setting: default yaml
+ Input Modal: text + audio + video + image
+ Output Modal: text + audio
+ Input Setting: stream=True
+ Datasets: single request
+ """
+ ```
+
+ **Explanation**:
+
+ **Function Naming Convention**: Uses the test_ prefix, describes the test scenario mix_to_text_audio, and the number 001 indicates the first test case for this scenario.
+
+ **Parameter Explanation**:
+
+ omni_server: Omni server instance obtained via fixture, containing model information and configuration.
+
+ openai_client: Unified OpenAI client processing instance, encapsulating request sending and response validation logic.
+
+ Docstring: Describes the test purpose, deployment settings, input/output modalities, streaming settings, and dataset type in detail, providing clear context for test maintenance.
+
+ **2.4.3 Multimodal Data Preparation**
+
+ ```python
+ video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
+ image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
+ audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
+ ```
+
+ **Explanation**:
+
+ **Data Generation Functions**: Use the generate_synthetic_* series of functions to generate synthetic test data, avoiding reliance on external resources and ensuring test reproducibility and stability.
+
+ **Parameter Explanation**:
+
+ Video: width, height, duration_frames
+
+ Image: width, height
+
+ Audio: duration_seconds, channels
+
+ **2.4.4 Request Configuration and Keyword Validation**
+
+ ```python
+ request_config = {
+ "model": omni_server.model,
+ "messages": messages,
+ "stream": True,
+ "key_words": {
+ "audio": ["water", "cricket"],
+ "video": ["sphere", "globe", "circle", "round"],
+ "image": ["square", "quadrate"],
+ "text": ["beijing"]
+ },
+ }
+ ```
+
+ **Explanation**:
+
+ **Model Specification**: Uses omni_server.model to ensure the test aligns with the model configured on the server.
+
+ **Keyword Validation Mechanism**: This is an innovative design of the template to address the specific needs of multimodal testing:
+
+ Audio Keywords: Validate whether the generated text's description of audio content contains expected elements (e.g., "water" for water sounds, "cricket" for cricket sounds). If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
+
+ **Video Keywords**: Validate whether the generated text's description of video content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
+
+ Image Keywords: Validate whether the generated text's description of image content contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
+
+ Text Keywords: Validate whether the generated text contains expected elements. If you provide multiple keywords, the validation is considered successful if at least one keyword is present.
+
+ **2.4.5 Request Execution**
+
+ ```python
+ openai_client.send_omni_request(request_config, request_num=1) # for omni-understanding models
+ # or
+ openai_client.send_diffusion_request(request_config, request_num=1) # for diffusion models
+ ```
+
+ **Explanation**:
+
+ **Unified Client**: Uses the OpenAIClientHandler instance to send requests. This client encapsulates error handling, retry mechanisms, and response validation logic.
+
+ **Single Request**: The comment clearly states this is a single-request completion test. For concurrent testing, it can be extended to multiple requests using request_num = n.
+
+ **Implicit Validation**: `send_omni_request`, `send_audio_speech_request`, and `send_diffusion_request` call `assert_*_response` helpers with the session `--run-level`. At `core_model`, checks are limited to success plus cheap payload sanity (non-empty audio/media, format/content-type, optional `min_audio_bytes`). At `advanced_model` and `full_model`, deep validation adds Whisper transcripts, keyword/similarity, diffusion dimension checks, PCM HNR, etc.
+
+ **Audio output debugging**: Deep validation may transcribe returned audio via `convert_audio_bytes_to_text` (Whisper). If an audio keyword or text–audio similarity assertion fails, set `VLLM_OMNI_KEEP_REQUEST_MEDIA=1` before running pytest to keep the intermediate WAV files for inspection (see [Test helper environment variables](test_system_overview.md#test-helper-environment-variables)).
+
+- ***Run Command (L3 merge)***: `pytest -s -v /tests/e2e/online_serving/test_{model_name}.py -m advanced_model --run-level=advanced_model`
+
+- ***Run Command (L4 nightly expansion)***: `pytest -s -v /tests/e2e/online_serving/test_{model_name}_expansion.py -m full_model --run-level=full_model`
+
+### L4 Level Testing - Full Functionality, Performance, and Documentation Testing
+
+#### 3.1 Testing Purpose
+
+L4 level testing is a comprehensive quality audit before a version release. It expands upon L3, executing ***full*** functional scenarios, conducting systematic ***performance stress tests***, and simultaneously verifying the correctness of accompanying ***example documentation***. Its purpose is to perform deep validation of the system during off-peak nighttime hours, providing quality trend reports for daytime development and data support for release decisions.
+
+
+
+#### 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_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_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/examples/online_serving/test_{model_name}.py`
+ - `tests/examples/offline_inference/test_{model_name}.py`
+
+#### 3.4 Execution Method and Example
+
+- ***Trigger Timing***: **`Nightly`**, automatically executed every night.
+- ***Execution Environment***: ***GPU*** server clusters to meet the resource demands of performance testing.
+- ***Script Example***:
+
+??? example "Test Examples: Documentation Example Tests"
+
+ --8<-- "docs/contributing/ci/test_examples/l4_doc_example_tests.inc.md"
+
+??? example "Test Examples: Performance Tests"
+
+ --8<-- "docs/contributing/ci/test_examples/l4_performance_tests.inc.md"
+
+??? example "Test Examples: Functionality Tests"
+
+ --8<-- "docs/contributing/ci/test_examples/l4_functionality_tests.inc.md"
+
+- ***Run Command***: (Specific commands would depend on the performance testing tool and configuration defined in `nightly.json`).
+
+### L5 Level Testing - Stability and Reliability Testing
+
+#### 4.1 Testing Purpose
+
+L5 level testing focuses on the performance of model services under ***long-running*** and ***abnormal fault*** scenarios. It aims to uncover deep-seated issues that only manifest under sustained pressure or extreme conditions, such as memory leaks, resource contention, gradual performance degradation, and lack of fault tolerance mechanisms. This is the final, yet crucial, line of defense for ensuring service high availability and production environment robustness.
+
+
+
+#### 4.2 Testing Content and Scope
+
+- ***Long-term Stability (Stability) Testing***: Uses JSON under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` and `test_wan22.json`) to run the service under moderate load for an extended period (e.g., over 12 hours), monitoring whether metrics like memory/VRAM usage, response time, and throughput degrade over time, and whether the service process remains stable.
+- ***Reliability Testing***: Uses pytest suites under `tests/dfx/reliability/` to inject controlled faults against a **live** `vllm_omni serve` instance (same **`omni_server` / `omni_server_function`** fixture style as E2E). Current suites emphasize **GPU memory pressure** (CUDA sidecar “memory hog”), **worker / runtime process kill** (`SIGKILL` on `VLLM::Worker` for Qwen3-Omni, `multiprocessing.spawn` for Wan2.2 video workers, or `vLLM-Omni::` for HunyuanImage DiT workers), **large multimodal chat**, **`/v1/videos`**, **`/v1/images/generations`**, or **`/v1/audio/speech`** jobs under OOM, **`/health` → 503** and **fast-fail / non-hanging concurrent** requests after kill, and **OpenAI-style 5xx error contracts** (e.g. text vs text+audio under OOM). **Post-fault recovery** checks exist where enabled (some cases may be `skip` while issues are tracked). See the Reliability `` block in Section 4.4 for file-level responsibilities and CI markers (`slow`, `hardware_test`, POSIX-only kill).
+
+#### 4.3 Test Directory and Execution Files
+
+- ***Stability Test Configuration***: `tests/dfx/stability/tests/test_qwen3_omni.json`, `tests/dfx/stability/tests/test_wan22.json` (one JSON per model / runner family)
+- ***Reliability Test Suite*** (`tests/dfx/reliability/`):
+ - `test_reliability_qwen3_omni.py` — Qwen3-Omni chat / multimodal reliability (GPU OOM, process kill, recovery, error contract under `--async-chunk` vs default).
+ - `test_reliability_wan22.py` — Wan2.2 I2V video API reliability (`/v1/videos` under OOM and process kill, recovery).
+ - `test_reliability_hunyuan_image.py` — HunyuanImage-3.0-Instruct DiT-only reliability (`/v1/images/generations` under OOM and process kill; deploy `hunyuan_image3_dit.yaml`, H100 × 4).
+ - `test_reliability_voxcpm2.py` — VoxCPM2 TTS reliability (`/v1/audio/speech` under OOM and process kill; deploy `voxcpm2.yaml`, L4 × 1).
+ - `helpers.py` — Shared primitives used by current suites: raw HTTP probes for `/v1/chat/completions` and `/health`, OpenAI-style error parsing, GPU OOM sidecar (`inject_gpu_oom` / `stop_gpu_oom_hogs`), and `pgrep`-based process-kill injector construction (`make_process_kill_fault_injector`).
+ - `conftest.py` — `fault_injector` and `omni_server_after_fault` / `omni_server_after_fault_function` fixtures to run a callable **after** the server is ready.
+ - `README.md` — Short local run commands for this directory.
+
+#### 4.4 Execution Method and Example
+
+- ***Trigger Timing***: **`Weekly`** (weekly) or **`Days before Release`** (several days before a major release). Due to long execution times, the frequency is lower.
+- ***Execution Environment***: ***GPU*** servers, requiring a stable and exclusive testing environment.
+- ***Script Example***:
+
+ Test Examples
+
+When you want to add L5-level stability test cases, add or extend the appropriate JSON file under `tests/dfx/stability/tests/` (for example `test_qwen3_omni.json` for Omni bench traffic, or `test_wan22.json` for diffusion `/v1/videos` workloads). The following illustrates the Qwen3-Omni shape:
+
+```json
+{
+ "test_name": "test_qwen3_omni_stability",
+ "server_params": {
+ "model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
+ "stage_config_name": "qwen3_omni.yaml"
+ },
+ "benchmark_params": [
+ {
+ "dataset_name": "random",
+ "backend": "openai-chat-omni",
+ "endpoint": "/v1/chat/completions",
+ "duration_sec": 43200,
+ "request_rate": 0.5,
+ "num_prompts_per_batch": 20,
+ "random_input_len": 2500,
+ "random_output_len": 900,
+ "ignore_eos": true,
+ "percentile-metrics": "ttft,tpot,itl,e2el,audio_rtf,audio_ttfp,audio_duration"
+ }
+ ]
+}
+```
+
+##### Parameter Explanation
+
+***Overview***
+
+| Field | Required | Description |
+| ---------------- | -------- | --------------------------------------------------------------------------- |
+| test_name | Yes | Unique identifier for the stability test case |
+| server_params | Yes | Server-side configuration parameters (model, stage configuration, etc.) |
+| benchmark_params | Yes | Stability benchmark running parameters (supports multiple configurations) |
+
+##### server_params Configuration
+
+###### Basic Parameters
+
+| Parameter | Required | Example | Description |
+| ----------------- | -------- | ---------------------------------- | ----------------------------------- |
+| model | Yes | "Qwen/Qwen3-Omni-30B-A3B-Instruct" | Model name or path |
+| stage_config_name | Yes | "qwen3_omni.yaml" | Stage configuration file name |
+
+###### Dynamic Configuration (update/delete)
+
+Supports incremental modifications based on the basic configuration:
+
+| Operation | Description |
+| --------- | ------------------------------------ |
+| update | Update or add configuration items |
+| delete | Delete specified configuration items |
+
+***Example***:
+You can refer to Test Examples in L4 §3.4
+
+##### benchmark_params Configuration
+
+For stability testing, the key parameters are:
+
+- **duration_sec**: Total duration (in seconds) during which benchmark traffic is sent. The stability benchmark will keep sending batches until this duration is reached.
+- **request_rate** / **max_concurrency**: Exactly one of them must be specified. They control how the traffic is generated for each batch:
+ - `request_rate`: Number of requests per second. The benchmark will send `num_prompts_per_batch` requests at the given rate.
+ - `max_concurrency`: Maximum number of concurrent requests. When this is used, `request_rate` is set to `inf` internally.
+- **num_prompts_per_batch**: Number of prompts sent in each batch. Multiple batches will be executed sequentially within `duration_sec`.
+
+All other optional parameters follow the same rules as in L4 §3.4.
+
+
+
+
+ Reliability test suite (tests/dfx/reliability)
+
+##### Purpose and relationship to stability
+
+Reliability tests are **short fault-injection** integration runs (L5 **(b)** in `tests/dfx/reliability/README.md`). They complement **stability** JSON-driven long runs: instead of hours of steady traffic, they **perturb** the server (GPU OOM sidecar, fatal signals on selected processes) and check **failure mode** and **latency bounds** (e.g. chat or `/v1/videos` must not hang under concurrent fault-time load).
+
+##### Directory layout
+
+| Path | Responsibility |
+| ---- | -------------- |
+| `helpers.py` | Shared helpers used by current reliability suites: raw `POST`/`GET` probes (`/v1/chat/completions`, `/health`), OpenAI error parsing (`extract_openai_error_contract_from_bytes`), GPU OOM sidecar lifecycle (`inject_gpu_oom`, `stop_gpu_oom_hogs`), and process-kill injector builder (`make_process_kill_fault_injector`). |
+| `conftest.py` | Pytest fixtures: indirect `fault_injector`, `omni_server_after_fault` / `omni_server_after_fault_function` (run injector after server is ready, then yield server). |
+| `test_reliability_qwen3_omni.py` | Qwen3-Omni: OOM vs **text vs text+audio** error contract, large multimodal chat under OOM, concurrent pressure, **SIGKILL** on `VLLM::Worker`, `/health` → 503 + fast-fail + concurrent chat; optional OOM recovery scenario (may be skipped while tracked in issues). |
+| `test_reliability_wan22.py` | Wan2.2 I2V: large `/v1/videos` under OOM, **SIGKILL** on `multiprocessing.spawn` chain, health / fast-fail / concurrent video requests; optional recovery test (may be skipped). |
+| `test_reliability_hunyuan_image.py` | HunyuanImage DiT-only: large `/v1/images/generations` under OOM, **SIGKILL** on `vLLM-Omni::` workers and serve/tree targets, health / fast-fail / concurrent image requests; some OOM/recovery cases may be skipped while tracked in issues. |
+| `test_reliability_voxcpm2.py` | VoxCPM2: `/v1/audio/speech` under OOM (error contract), **SIGKILL** on `VLLM::` workers and serve/tree targets, health / fast-fail / concurrent speech requests; some OOM cases may be skipped while tracked in issues. |
+| `README.md` | Minimal run / collect examples. |
+
+##### Parametrization and markers
+
+- Each test module defines a **`RELIABILITY_SCENARIOS`** list (`test_name`, `server_params`: model, `stage_config_name` or diffusion `server_args`, etc.). **`create_reliability_omni_server_params()`** in `tests/dfx/conftest.py` resolves stage paths (including XPU substitutions where applicable) and builds **`OmniServerParams`** lists consumed by **`@pytest.mark.parametrize(..., indirect=True)`** on `omni_server` or `omni_server_function`.
+- Cases are tagged **`@pytest.mark.slow`** for weekly / selective CI. GPU-heavy suites use **`@hardware_test(res={"cuda": "H100"}, num_cards=...)`** or **`@hardware_test(res={"cuda": "L4"}, num_cards=1)`** (Qwen3-Omni **2**× H100; Wan2.2 **1**× H100; HunyuanImage DiT **4**× H100; VoxCPM2 **1**× L4).
+- **Process-kill** tests use **`@pytest.mark.skipif(os.name == "nt", ...)`** because injection uses POSIX **`pgrep` / `kill`**.
+
+##### CI trigger
+
+Weekly Buildkite (`.buildkite/cuda/test-weekly.yml`) runs one step per model suite (trigger: `WEEKLY=1` or PR label `weekly-test`), for example:
+
+| Buildkite step | Test file | CI hardware |
+| -------------- | --------- | ----------- |
+| Reliability Test - qwen3-omni | `test_reliability_qwen3_omni.py` | H100 × 2 (`mithril-h100-pool`) |
+| Reliability Test - wan22 | `test_reliability_wan22.py` | H100 × 2 (`mithril-h100-pool`) |
+| Reliability Test - hunyuan-image | `test_reliability_hunyuan_image.py` | H100 × 4 (`mithril-h100-pool`) |
+| Reliability Test - voxcpm2 | `test_reliability_voxcpm2.py` | L4 × 1 (`gpu_1_queue`) |
+
+```bash
+pytest -s -v tests/dfx/reliability/test_reliability_qwen3_omni.py -m "slow"
+pytest -s -v tests/dfx/reliability/test_reliability_wan22.py -m "slow"
+pytest -s -v tests/dfx/reliability/test_reliability_hunyuan_image.py -m "slow"
+pytest -s -v tests/dfx/reliability/test_reliability_voxcpm2.py -m "slow"
+```
+
+##### Local commands
+
+```bash
+pytest --collect-only tests/dfx/reliability
+pytest -s -v tests/dfx/reliability/test_reliability_qwen3_omni.py -m slow
+pytest -s -v tests/dfx/reliability/test_reliability_wan22.py -m slow
+pytest -s -v tests/dfx/reliability/test_reliability_hunyuan_image.py -m slow
+pytest -s -v tests/dfx/reliability/test_reliability_voxcpm2.py -m slow
+```
+
+##### Adding a new model suite
+
+1. Add `test_reliability_.py` under `tests/dfx/reliability/`.
+2. Define **`RELIABILITY_SCENARIOS`** and pass them through **`create_reliability_omni_server_params()`** with the correct deploy or e2e stage-config directory (same pattern as existing files).
+3. Reuse **`helpers`** for OOM / kill / raw HTTP; prefer **`assert_fault_exception()`** and **`resolve_oom_device_spec()`** from `tests/dfx/conftest.py` for consistent device selection vs stage YAML.
+4. Register **`slow`** (and **`hardware_test`** if needed); extend **`.buildkite/cuda/test-weekly.yml`** when the suite should run in weekly L5.
+
+
+
+- - ***Stability***: `pytest -s -v tests/dfx/stability/scripts/test_stability_qwen3_omni.py` or `pytest -s -v tests/dfx/stability/scripts/test_stability_wan22.py` (or add `test_stability_.py` alongside a matching JSON config)
+ - ***Reliability***: `pytest -s -v tests/dfx/reliability/test_reliability_.py -m slow` (current suites: `qwen3_omni`, `wan22`, `hunyuan_image`, `voxcpm2`; add `test_reliability_.py` and a matching step in `.buildkite/cuda/test-weekly.yml` for new models)
diff --git a/docs/contributing/ci/tests_markers.md b/docs/contributing/ci/tests_markers.md
deleted file mode 100644
index cc89cee47..000000000
--- a/docs/contributing/ci/tests_markers.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# Markers for Tests
-
-By adding markers before test functions, tests can later be executed uniformly by simply declaring the corresponding marker type.
-
-## Current Markers
-Defined in `pyproject.toml`:
-
-| Marker | Description |
-| ------------------ | --------------------------------------------------------- |
-| `core_model` | L1&L2 tests (run in each PR) |
-| `advanced_model` | L3 tests (run on each merge to main) |
-| `full_model` | L4 tests (run nightly) |
-| `diffusion` | Diffusion model tests |
-| `omni` | Omni multimodal model tests |
-| `tts` | TTS model tests |
-| `cache` | Cache backend tests |
-| `parallel` | Parallelism/distributed tests |
-| `cpu` | Tests that run on CPU |
-| `gpu` | Tests that run on GPU * |
-| `cuda` | Tests that run on CUDA * |
-| `rocm` | Tests that run on AMD/ROCm * |
-| `xpu` | Tests that run on Intel XPU * |
-| `npu` | Tests that run on NPU/Ascend * |
-| `H100` | Tests that require H100 GPU * |
-| `L4` | Tests that require L4 GPU * |
-| `MI325` | Tests that require MI325 GPU (AMD/ROCm) * |
-| `A2` | Tests that require A2 NPU * |
-| `A3` | Tests that require A3 NPU * |
-| `distributed_cuda` | Tests that require multi cards on CUDA platform * |
-| `distributed_rocm` | Tests that require multi cards on ROCm platform * |
-| `distributed_npu` | Tests that require multi cards on NPU platform * |
-| `skipif_cuda` | Skip if the num of CUDA cards is less than the required * |
-| `skipif_rocm` | Skip if the num of ROCm cards is less than the required * |
-| `skipif_npu` | Skip if the num of NPU cards is less than the required * |
-| `slow` | Slow tests (may skip in quick CI) |
-| `benchmark` | Benchmark tests |
-
-\* Means those markers are auto-added by `@hardware_test` (parametrization decorator) or `hardware_marks` (only returning the list of marks for flexibility).
-
-### Example usage for markers
-
-```python
-from tests.helpers.mark import hardware_test
-
-@pytest.mark.core_model
-@pytest.mark.omni
-@hardware_test(
- res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
- num_cards=2,
-)
-@pytest.mark.parametrize("omni_server", test_params, indirect=True)
-def test_video_to_audio()
- ...
-```
-
-### Decorator: `@hardware_test`
-
-This decorator is intended to make hardware-aware, cross-platform test authoring easier and more robust for CI/CD environments. The `hardware_test` decorator in `vllm-omni/tests/helpers/mark.py` performs the following actions:
-
-1. **Applies platform and resource markers**
- Adds the appropriate pytest markers for each specified hardware platform (e.g., `cuda`, `rocm`, `xpu`, `npu`) and resource type (e.g., `L4`, `H100`, `MI325`, `B60`, `A2`, `A3`).
- ```
- @pytest.mark.cuda
- @pytest.mark.L4
- ```
-2. **Handles multi-card (distributed) scenarios**
- For tests requiring multiple cards, it automatically adds distributed markers such as `distributed_cuda`, `distributed_rocm`, or `distributed_npu`.
- ```
- @pytest.mark.distributed_cuda(num_cards=num_cards)
- ```
-3. **Supports flexible card requirements**
- Accepts `num_cards` as either a single integer for all platforms or as a dictionary with per-platform values. If not specified, defaults to 1 card per platform.
-
-4. **Integrates resource validation**
- On CUDA, adds a skip marker (`skipif_cuda`) if the system does not have the required number of devices.
- Support for `skipif_rocm` and `skipif_npu` will be implemented later.
-
-
-5. **Works with pytest filtering**
- Allows tests to be filtered and selected at runtime using standard pytest marker expressions (e.g., `-m "distributed_cuda and L4"`).
-
-#### Example usage for decorator
-- Single call for multiple platforms:
- ```python
- @hardware_test(
- res={"cuda": "L4", "rocm": "MI325", "xpu": "B60", "npu": "A2"},
- num_cards={"cuda": 2, "rocm": 2, "xpu": 2, "npu": 2},
- )
- ```
- or
- ```python
- @hardware_test(
- res={"cuda": "L4", "rocm": "MI325", "xpu": "B60", "npu": "A2"},
- num_cards=2,
- )
- ```
-- `res` must be a dict; supported resources: CUDA (L4/H100), ROCm (MI325), NPU (A2/A3)
-- `num_cards` can be int (all platforms) or dict (per platform); defaults to 1 when missing
-- Distributed markers (`distributed_cuda`, `distributed_rocm`, `distributed_npu`) are auto-added for multi-card cases
-- Filtering examples:
- - CUDA only: `pytest -m "distributed_cuda and L4"`
- - ROCm only: `pytest -m "distributed_rocm and MI325"`
- - NPU only: `pytest -m "distributed_npu"`
-
-### Function: `hardware_marks`
-
-`hardware_marks` returns a list of pytest mark objects with the same signature as `@hardware_test`. Use it when you need more flexibility, such as attaching hardware marks to individual `pytest.param` entries rather than an entire test function.
-
-```python
-from tests.helpers.mark import hardware_marks
-
-MULTI_CARD_MARKS = hardware_marks(
- res={"cuda": "H100", "rocm": "MI325", "npu": "A2"}, num_cards=2
-)
-
-@pytest.mark.parametrize("omni_server", [
- pytest.param(OmniServerParams(...), id="case_001", marks=MULTI_CARD_MARKS),
-], indirect=True)
-def test_feature(omni_server):
- ...
-```
-
-## 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:
-
-1. **Extend the marker list in your pytest config** so that platform/resource markers are defined:
- ```toml
- # In pyproject.toml or pytest.ini
- [tool.pytest.ini_options]
- markers = [
- # ... existing markers ...
- "tpu: Tests that require TPU device",
- "TPU_V3: Tests that require TPU v3 hardware",
- "distributed_tpu: Tests that require multiple TPU devices",
- ]
- ```
-2. **Implement a marker construction function for your platform** in `vllm-omni/tests/helpers/mark.py`:
- ```python
- # In vllm-omni/tests/helpers/mark.py
-
- def tpu_marks(*, res: str, num_cards: int):
- test_platform = pytest.mark.tpu
- if res == "TPU_V3":
- test_resource = pytest.mark.TPU_V3
- else:
- raise ValueError(
- f"Invalid TPU resource type: {res}. Supported: TPU_V3")
-
- if num_cards == 1:
- return [test_platform, test_resource]
- else:
- test_distributed = pytest.mark.distributed_tpu(num_cards=num_cards)
- # Optionally: add skipif_tpu when implemented
- return [test_platform, test_resource, test_distributed]
- ```
-3. **Update `hardware_marks` to recognize your new platform**:
- In the relevant place (see the `hardware_marks` implementation), add:
- ```python
- if platform == "tpu":
- marks = tpu_marks(res=resource, num_cards=cards)
- ```
- (`hardware_test` calls `hardware_marks` internally, so both will pick up the change.)
-4. **(Recommended) Add a test using your new markers**:
- ```python
- @hardware_test(
- res={"tpu": "TPU_V3"},
- num_cards=2,
- )
- def test_my_tpu_feature():
- ...
- ```
-
-**Summary**:
-- Add pytest markers for your new platform/resources
-- Implement a marker function (`xxx_marks`)
-- Plug into `hardware_marks`
-- You're done: tests using `@hardware_test` or `hardware_marks` with your platform now automatically get the correct markers, distribution, and isolation!
-
-See code in `vllm-omni/tests/helpers/mark.py` for existing examples (`cuda_marks`, `rocm_marks`, `npu_marks`).
diff --git a/docs/contributing/ci/tests_style.md b/docs/contributing/ci/tests_style.md
deleted file mode 100644
index 420a8821d..000000000
--- a/docs/contributing/ci/tests_style.md
+++ /dev/null
@@ -1,466 +0,0 @@
-# Test File Structure and Style Guide
-
-To ensure project maintainability and sustainable development, we encourage contributors to submit test code (unit tests, system tests, or end-to-end tests) alongside their code changes. This document outlines the guidelines for organizing and naming test files.
-
-## Checklist before submitting your test files
-
-1. The file is saved in an appropriate place and the file name is clear.
-2. The coding style follows the requirements outlined below.
-3. All test functions have appropriate pytest markers.
-4. For tests that need run in CI, please ensure it labeled as ``@pytest.mark.core_model` the test is configured under the `./buildkite/` folder.
-
-
-## Test Types
-
-For more details about our [Five Levels Tests design](../ci/CI_5levels.md).
-
-### Unit Tests and System Tests
-For unit tests and system tests, we strongly recommend placing test files in the same directory structure as the source code being tested, using the naming convention `test_*.py`.
-
-### End-to-End (E2E) Tests for Models
-End-to-end tests verify the complete functionality of a system or component. For our project, the E2E tests for different omni models are organized into two subdirectories:
-
-- **`tests/e2e/offline_inference/`**: Tests for offline inference modes (e.g., Qwen3Omni offline inference)
-
-- **`tests/e2e/online_serving/`**: Tests for online serving scenarios (e.g., API server tests)
-
-## Test Directory Structure
-
-The ideal directory structure mirrors the source code organization. Legend: `✅` = test exists, `⬜` = suggested to add.
-
-```
-vllm_omni/ tests/
-├── config/ → ├── config/
-│ ├── model.py │ └── test_model.py ⬜
-│ └── lora.py │ └── test_lora.py ⬜
-│
-├── core/ → ├── core/
-│ └── sched/ │ └── sched/
-│ ├── omni_ar_scheduler.py │ ├── test_omni_ar_scheduler.py ⬜
-│ ├── omni_generation_scheduler.py │ ├── test_omni_generation_scheduler.py ⬜
-│ └── output.py │ └── test_output.py ✅ currently in entrypoints/test_omni_new_request_data.py (tests output.OmniNewRequestData)
-│
-├── diffusion/ → ├── diffusion/
-│ ├── diffusion_engine.py │ ├── test_diffusion_engine.py ⬜
-│ ├── attention/ │ ├── attention/
-│ │ ├── layer.py │ │ ├── test_attention_sp.py ✅
-│ │ └── backends/ │ │ └── test_flash_attn.py ✅
-│ ├── distributed/ │ ├── distributed/
-│ │ └── ... │ │ ├── test_comm.py ✅
-│ │ │ │ ├── test_cfg_parallel.py ✅
-│ │ │ │ └── test_sp_plan_hooks.py ✅
-│ ├── lora/ │ ├── lora/
-│ │ └── ... │ │ ├── test_base_linear.py ✅
-│ │ │ │ └── test_lora_manager.py ✅
-│ ├── models/ │ ├── models/
-│ │ ├── qwen_image/ │ │ ├── qwen_image/ (e2e coverage)
-│ │ ├── ovis_image/ │ │ ├── ovis_image/
-│ │ │ └── ... │ │ │ └── test_ovis_image.py ✅
-│ │ ├── z_image/ │ │ └── z_image/
-│ │ └── ... │ │ └── test_zimage_tp_constraints.py ✅
-│ └── worker/ │ └── worker/
-│ ├── diffusion_worker.py │ └── test_diffusion_worker.py ✅ file at diffusion/test_diffusion_worker.py
-│ └── diffusion_model_runner.py │
-│
-├── distributed/ → ├── distributed/
-│ └── omni_connectors/ │ └── omni_connectors/
-│ ├── adapter.py │ ├── test_adapter_and_flow.py ✅
-│ ├── kv_transfer_manager.py │ ├── test_basic_connectors.py ✅
-│ ├── connectors/ │ ├── test_kv_flow.py ✅
-│ └── utils/ │ └── test_omni_connector_configs.py ✅
-│
-├── engine/ → ├── engine/
-│ ├── input_processor.py │ ├── test_input_processor.py ⬜ (no processor.py in source)
-│ ├── output_processor.py │ └── test_output_processor.py ⬜
-│ └── arg_utils.py │ └── test_arg_utils.py ⬜
-│
-├── entrypoints/ → ├── entrypoints/
-│ ├── stage_utils.py │ ├── test_stage_utils.py ✅
-│ ├── cli/ │ ├── cli/ (benchmarks/test_serve_cli.py covers CLI serve)
-│ │ └── ... │ │ └── test_*.py ⬜
-│ └── openai/ │ └── openai_api/ # maps to entrypoints/openai/
-│ ├── api_server.py │ ├── test_api_server.py ⬜ (e2e indirect coverage)
-│ ├── serving_chat.py │ ├── test_serving_chat_sampling_params.py ✅
-│ ├── serving_speech.py │ ├── test_serving_speech.py ✅
-│ └── image_api_utils.py │ └── test_image_server.py ✅
-│
-├── inputs/ → ├── inputs/
-│ ├── data.py │ ├── test_data.py ⬜
-│ ├── parse.py │ ├── test_parse.py ⬜
-│ └── preprocess.py │ └── test_preprocess.py ✅ currently in entrypoints/test_omni_input_preprocessor.py
-│
-├── model_executor/ → ├── model_executor/
-│ ├── layers/ │ ├── layers/
-│ │ └── mrope.py │ │ └── test_mrope.py ⬜
-│ ├── model_loader/ │ ├── model_loader/
-│ │ └── weight_utils.py │ │ └── test_weight_utils.py ⬜
-│ ├── models/ │ ├── models/
-│ │ ├── qwen2_5_omni/ │ │ ├── qwen2_5_omni/
-│ │ │ ├── qwen2_5_omni_thinker.py │ │ │ ├── test_audio_length.py ✅
-│ │ │ ├── qwen2_5_omni_talker.py │ │ │ ├── test_qwen2_5_omni_thinker.py ⬜
-│ │ │ └── qwen2_5_omni_token2wav.py │ │ │ ├── test_qwen2_5_omni_talker.py ⬜
-│ │ └── qwen3_omni/ │ │ │ └── test_qwen2_5_omni_token2wav.py ⬜
-│ │ └── ... │ │ └── qwen3_omni/
-│ ├── stage_configs/ │ │ └── test_*.py ⬜
-│ │ └── *.yaml │ └── stage_configs/ (used by e2e, test_*.py can be added) ⬜
-│ └── stage_input_processors/ │ └── stage_input_processors/
-│ └── ... │ └── test_*.py ⬜
-│
-├── sample/ → ├── sample/
-│ └── __init__.py │ └── test_*.py ⬜
-│
-├── utils/ → ├── utils/
-│ └── __init__.py │ └── test_*.py ⬜ (no platform_utils.py currently)
-│
-├── worker/ → ├── worker/
-│ ├── gpu_ar_model_runner.py │ ├── test_gpu_ar_model_runner.py ⬜
-│ ├── gpu_ar_worker.py │ ├── test_gpu_ar_worker.py ⬜
-│ ├── gpu_generation_model_runner.py │ ├── test_gpu_generation_model_runner.py ✅
-│ ├── gpu_generation_worker.py │ ├── test_gpu_generation_worker.py ⬜
-│ ├── gpu_model_runner.py │ ├── test_omni_gpu_model_runner.py ✅
-│ └── mixins.py │ └── (npu under platforms/npu/worker/) # not worker/npu/
-│
-├── platforms/ → (no tests/platforms/, e2e and stage_configs provide indirect coverage)
-│ ├── cuda/
-│ ├── npu/worker/ # NPU worker here, not vllm_omni/worker/npu/
-│ ├── rocm/
-│ └── xpu/worker/
-│
-├── outputs.py → test_outputs.py ✅ (at tests root)
-├── (logger, patch, request, version) → (no corresponding unit test)
-│
-└── e2e (tests side only) → ├── e2e/
- ├── online_serving/ ✅ non-empty
- │ ├── test_qwen2_5_omni.py
- │ ├── test_async_omni.py
- │ ├── test_qwen3_omni.py
- │ ├── test_qwen3_omni_expansion.py
- │ ├── test_mimo_audio.py
- │ └── test_images_generations_lora.py
- └── offline_inference/ ✅
- ├── test_qwen2_5_omni.py
- ├── test_qwen3_omni.py
- ├── test_bagel_text2img.py
- ├── test_z_image.py
- ├── test_wan22.py
- ├── test_zimage_tensor_parallel.py
- ├── test_cache_dit.py
- ├── test_teacache.py
- ├── test_stable_audio_expansion.py
- ├── test_diffusion_cpu_offload.py
- ├── test_diffusion_layerwise_offload.py
- ├── test_diffusion_lora.py
- ├── test_sequence_parallel.py
- ├── test_qwen_image_edit_expansion.py
- └── stage_configs/ (legacy schema, still present
- ├── bagel_*.yaml for unmigrated models)
- └── npu/, rocm/, etc.
-
-# Migrated models (qwen3_omni_moe, qwen2_5_omni, qwen3_tts) live under
-# vllm_omni/deploy/ instead — see docs/configuration/stage_configs.md.
-examples/ tests
-│ └── examples
-├── online_serving/ → ├── online_serving/
-│ └── {doc_page_title}/README.md │ └── test_{doc_page_title}.py ⬜
-└── offline_inference/ → └── offline_inference/
- └── {doc_page_title}/README.md └── test_{doc_page_title}.py ⬜
-```
-
-
-
-### Naming Conventions
-
-- **Unit Tests**: Use `test_.py` format. Example: `stage_utils.py` → `test_stage_utils.py`
-
-- **E2E Tests**: Place in `tests/e2e/offline_inference/` or `tests/e2e/online_serving/` with descriptive names. Example: `tests/e2e/offline_inference/test_qwen3_omni.py`, `tests/e2e/offline_inference/test_diffusion_model.py`
-
-- **Expansion Tests**
-
-### Best Practices
-
-1. **Mirror Source Structure**: Test directories should mirror the source code structure
-2. **Test Type Indicators**: Use comments to indicate test types (UT for unit tests, E2E for end-to-end tests)
-3. **Shared Resources**: Place shared test configurations (e.g., CI configs) in appropriate subdirectories
-4. **Consistent Naming**: Follow the `test_*.py` naming convention consistently across all test files
-
-
-## Test codes requirements
-
-### Coding style
-
-1. **File header**: Add SPDX license header to all test files
-2. **Imports**: Pls don't use manual `sys.path` modifications, use standard imports instead.
-3. **Test type differentiation**:
-
- - Unit tests: Maintain mock style
- - E2E tests for models: Consider using OmniRunner uniformly, avoid decorators
-
-4. **Documentation**: Add docstrings to all test functions
-5. **Environment variables**: Set uniformly in `conftest.py` or at the top of files
-6. **Type annotations**: Add type annotations to all test function parameters
-7. **Pytest Markers**: Add necessary markers like `@pytest.mark.core_model` and use `@hardware_test` to declare hardware requirements (check detailed in [Markers for Tests](../ci/tests_markers.md)).
-
-### Template
-#### E2E - Online serving
-
-E2E Online tests for Qwen3-Omni model with mix input and audio+text output. Based on `tests/e2e/online_serving/test_qwen3_omni.py`.
-
-```python
-"""
-E2E Online tests for Qwen3-Omni model with mix input and audio+text output.
-"""
-
-import os
-
-os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
-
-import threading
-from pathlib import Path
-
-import openai
-import pytest
-
-from tests.helpers.media import (
- convert_audio_bytes_to_text,
- cosine_similarity_text,
- generate_synthetic_video,
-)
-from tests.helpers.runtime import OmniServer, dummy_messages_from_mix_data
-from tests.helpers.stage_config import get_deploy_config_path, modify_stage_config
-from vllm_omni.platforms import current_omni_platform
-
-# Edit: model name and stage config path
-models = ["Qwen/Qwen3-Omni-30B-A3B-Instruct"]
-
-#If you use the default configuration file, you can directly use the following address.
-def get_default_config():
- return get_deploy_config_path("ci/qwen3_omni_moe.yaml")
-
-#If you need to modify the configuration file, you can use modify_stage_config.
-def get_chunk_config():
- path = modify_stage_config(
- get_default_config(),
- updates={
- "async_chunk": True,
- "stage_args": {
- 0: {
- "engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.thinker2talker_async_chunk"
- },
- 1: {
- "engine_args.custom_process_next_stage_input_func": "vllm_omni.model_executor.stage_input_processors.qwen3_omni.talker2code2wav_async_chunk"
- },
- },
- },
- deletes={"stage_args": {2: ["custom_process_input_func"]}},
- )
- return path
-
-stage_configs = [get_default_config(), CHUNK_CONFIG_PATH]
-
-test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
-
-
-#Please use this method to launch the online instance.
-_omni_server_lock = threading.Lock()
-
-@pytest.fixture(scope="module")
-def omni_server(request):
- """Start vLLM-Omni server as a subprocess. Use module scope for multi-stage init (10-20+ min)."""
- with _omni_server_lock:
- model, stage_config_path = request.param
- with OmniServer(
- model,
- ["--stage-configs-path", stage_config_path, "--stage-init-timeout", "120"],
- ) as server:
- yield server
-
-
-@pytest.fixture
-def client(omni_server):
- """OpenAI client for the running vLLM-Omni server."""
- return openai.OpenAI(
- base_url=f"http://{omni_server.host}:{omni_server.port}/v1",
- api_key="EMPTY",
- )
-
-#Please use function definitions above the test function to define the prompts and other parameters you need.
-def get_system_prompt():
- return {
- "role": "system",
- "content": [
- {
- "type": "text",
- "text": (
- "You are Qwen, a virtual human developed by the Qwen Team, "
- "Alibaba Group, capable of perceiving auditory and visual inputs, "
- "as well as generating text and speech."
- ),
- }
- ],
- }
-
-...
-
-#Please define test case tags according to the instructions in the marker documentation.
-@pytest.mark.core_model
-@pytest.mark.omni
-@pytest.mark.parametrize("omni_server", test_params, indirect=True)
-def test_mix_to_text_audio_001(client: openai.OpenAI, omni_server, request) -> None:
- # PLEASE FOLLOW THESE TEMPLATE INSTRUCTIONS:
- # ============================================================================
- # TEMPLATE USAGE GUIDE:
- # 1. Copy this entire function as a starting point for multi-modal tests
- # 2. Update the test name to reflect your specific test scenario
- # 3. Modify input/output modalities as needed (see OPTIONS section below)
- # 4. Adjust assertions based on your expected outcomes
- # 5. Add custom validation logic for your specific use case
- # ============================================================================
-
- #Please list the relevant test points.
- """
- Test multi-modal input processing and text/audio output generation via OpenAI API.
- Deploy Setting: default yaml
- Input Modal: text + audio + video + image
- Output Modal: text + audio
- Input Setting: stream=True
- Datasets: single request
- """
- # SECTION 1: TEST SETUP AND INITIALIZATION
- # =========================================
- # INSTRUCTIONS: Initialize test variables and prepare test environment
- # MODIFY: Add any additional test setup required for your scenario
- e2e_list = list()
- # SECTION 2: TEST DATA GENERATION
- # ================================
- # INSTRUCTIONS: Generate or load test data for each input modality
- # MODIFY: Replace synthetic generators with your actual data sources
- # VIDEO DATA - Generate synthetic video for testing
- # FORMAT: data:video/mp4;base64,{base64_encoded_video}
- # PARAMETERS: width, height, duration_frames
- video_data_url = f"data:video/mp4;base64,{generate_synthetic_video(224, 224, 300)['base64']}"
- # IMAGE DATA - Generate synthetic image for testing
- # FORMAT: data:image/jpeg;base64,{base64_encoded_image}
- # PARAMETERS: width, height
- image_data_url = f"data:image/jpeg;base64,{generate_synthetic_image(224, 224)['base64']}"
- # AUDIO DATA - Generate synthetic audio for testing
- # FORMAT: data:audio/wav;base64,{base64_encoded_audio}
- # PARAMETERS: duration_seconds, channels
- audio_data_url = f"data:audio/wav;base64,{generate_synthetic_audio(5, 1)['base64']}"
-
- # SECTION 3: MESSAGE CONSTRUCTION
- # ================================
- # INSTRUCTIONS: Assemble the complete message payload for API request
- # MODIFY: Add/remove modalities or change prompt structure as needed
-
- # USAGE: Construct a message containing all input modalities
- # IMPORTANT: Ensure the message structure matches OpenAI API expectations
- # CUSTOMIZATION POINTS:
- # - system_prompt: Controls the assistant's behavior
- # - content_text: The user's text prompt/question
- # - *_data_url: URLs for media content (video/image/audio)
- messages = dummy_messages_from_mix_data(
- system_prompt=get_system_prompt(),
- video_data_url=video_data_url,
- image_data_url=image_data_url,
- audio_data_url=audio_data_url,
- content_text=get_prompt("mix"),
- )
-
- # SECTION 4: API REQUEST EXECUTION
- # =================================
- # INSTRUCTIONS: Make the API call and measure performance
- # MODIFY: Add timeout, retry logic, or additional parameters
- start_time = time.perf_counter()
- chat_completion = client.chat.completions.create(model=omni_server.model, messages=messages, stream=True)
-
- #Call using your preferred method and obtain the final audio and text outputs.
- ...
-
- # SECTION 5: OUTPUT VALIDATION
- # =============================
- # INSTRUCTIONS: Verify that outputs meet expected criteria
- # MODIFY: Adjust validation logic for your specific requirements
-
- # ASSERTION 1: E2E Validation
- # PURPOSE: Verify that the E2E latency is less than the baseline.
- current_e2e = time.perf_counter() - start_time
- print(f"the request e2e is: {current_e2e}")
- e2e_list.append(current_e2e)
-
- print(f"the avg e2e is: {sum(e2e_list) / len(e2e_list)}")
-
-
-
- # ASSERTION 2: Text Output Validation
- # PURPOSE: Verify that text output was generated with keyword content
- assert text_content is not None and len(text_content) >= 2, "No text output is generated"
- assert any(
- keyword in text_content.lower() for keyword in ["square", "quadrate", "sphere", "globe", "circle", "round"]
- ), "The output does not contain any of the keywords."
-
-
- # ASSERTION 3: Cross-Modal Consistency
- # PURPOSE: Verify text and audio outputs convey the same information
- # CUSTOMIZATION: Adjust similarity threshold (0.9) based on accuracy requirements
- assert audio_data is not None, "No audio output is generated"
- audio_content = convert_audio_bytes_to_text(audio_data)
- print(f"text content is: {text_content}")
- print(f"audio content is: {audio_content}")
- similarity = cosine_similarity_text(audio_content.lower(), text_content.lower())
- print(f"similarity is: {similarity}")
- assert similarity > 0.9, "The audio content is not same as the text"
-```
-
-
-#### E2E - Offline inference
-```python
-# SPDX-License-Identifier: Apache-2.0
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
-"""
-Offline E2E smoke test for an omni model (video → audio).
-"""
-
-import os
-from pathlib import Path
-
-import pytest
-from vllm.assets.video import VideoAsset
-
-from tests.helpers.mark import hardware_test
-from ..multi_stages.conftest import OmniRunner
-
-# Optional: set process start method for workers
-os.environ["VLLM_WORKER_MULTIPROC_METHOD"] = "spawn"
-
-models = ["{your model name}"] #Edit here to load your model
-stage_configs = [str(Path(__file__).parent / "stage_configs" / {your model yaml})] #Edit here to load your model yaml
-
-# Create parameter combinations for model and stage config
-test_params = [(model, stage_config) for model in models for stage_config in stage_configs]
-
-# function name: test_{input_modality}_to_{output_modality}
-# modality candidate: text, image, audio, video, mixed_modalities
-@pytest.mark.core_model
-@pytest.mark.omni
-@hardware_test(
- res={"cuda": "L4", "rocm": "MI325", "npu": "A2"},
- num_cards=2,
-)
-@pytest.mark.parametrize("test_config", test_params)
-def test_video_to_audio(omni_runner: type[OmniRunner], model: str) -> None:
- """Offline inference: video input, audio output."""
- model, stage_config_path = test_config
- with omni_runner(model, seed=42, stage_configs_path=stage_config_path) as runner:
- # Prepare inputs
- video = VideoAsset(name="sample", num_frames=4).np_ndarrays
-
- outputs = runner.generate_multimodal(
- prompts="Describe this video briefly.",
- videos=video,
- )
-
- # Minimal assertions: got outputs and at least one audio result
- assert outputs
- has_audio = any(o.final_output_type == "audio" for o in outputs)
- assert has_audio
-```
diff --git a/docs/contributing/model/adding_diffusion_model.md b/docs/contributing/model/adding_diffusion_model.md
index b38805112..678797652 100644
--- a/docs/contributing/model/adding_diffusion_model.md
+++ b/docs/contributing/model/adding_diffusion_model.md
@@ -653,7 +653,7 @@ python end2end.py --model your-org/your-model-name --modality text2img --prompts
Mirror BAGEL’s online serving setup:
- Server launcher: `examples/online_serving/your_model_name/run_server.sh`
- - Wrap `vllm serve ... --omni --port ...` (and `--stage-configs-path ...` if needed)
+ - Wrap `vllm serve ... --omni --port ...` (and `--deploy-config ...` if needed)
- Client: `examples/online_serving/your_model_name/openai_chat_client.py`
- Send requests to `POST /v1/chat/completions`
- Support multimodal inputs (e.g., base64 image) if your model needs it
@@ -685,7 +685,7 @@ For a fair comparison, keep the same **prompt**, **seed**, **resolution**, **num
To ensure project maintainability and sustainable development, please submit test code (unit tests, system tests, or end-to-end tests) alongside their code changes.
-For comprehensive testing guidelines and the definition of test levels (L1-L5), please refer to the [Multi-Level Automated Testing System Documentation](../ci/CI_5levels.md). You are at least required to add an L4 *functionality* test described in that document.
+For the definition of test levels (L1-L5), see [Test System Overview](../ci/test_system_overview.md). You are at least required to add an L4 *functionality* test as described in [Test Writing Guide](../ci/test_writing_guide.md#l4-level-testing-full-functionality-performance-and-documentation-testing).
---
@@ -755,7 +755,8 @@ omni = Omni(model="your-model", ulysses_degree=2, ring_degree=2)
### Step Execution
-See detailed design guide: [How to add step execution support](../../design/feature/diffusion_step_execution.md)
+See the detailed
+[Diffusion Continuous Batching design guide](../../design/feature/diffusion_continuous_batching.md).
Use this only when your pipeline can be split into stable request-scoped and
step-scoped phases. The reference implementation is
@@ -769,9 +770,8 @@ step-scoped phases. The reference implementation is
Do not enable `step_execution=True` until those four methods are implemented
and validated against the request-level path.
-If you want the pipeline to work with the experimental batched step-wise path
-(`max_num_seqs > 1`), also see:
-[Continuous Batching for Step-Wise Diffusion](../../design/feature/diffusion_continuous_batching.md).
+The same design guide covers the experimental batched step-wise path used when
+`max_num_seqs > 1`.
If you expose this in example scripts or recipes, keep it opt-in. Surface
runtime features like `step_execution` as optional flags instead of silently
@@ -824,7 +824,7 @@ omni = Omni(model="your-model",
### CPU Offload
-See detailed guide: [CPU Offloading for Diffusion Models](../../user_guide/diffusion/cpu_offload_diffusion.md)
+See detailed guide: [CPU Offloading for Diffusion Models](../../user_guide/diffusion/cpu_offload.md)
vLLM-Omni provides two offloading strategies to reduce GPU memory usage:
diff --git a/docs/contributing/model/adding_omni_model.md b/docs/contributing/model/adding_omni_model.md
index c20e94eda..4698caf9c 100644
--- a/docs/contributing/model/adding_omni_model.md
+++ b/docs/contributing/model/adding_omni_model.md
@@ -34,6 +34,7 @@ vllm_omni/model_executor/models/
└── your_model_name/ # Model directory (e.g., qwen3_omni)
├── __init__.py # Exports main model class
├── your_model.py # Main unified model class
+ ├── pipeline.py # Pipeline topology registry entry
├── your_model_stage1_implementation.py # Stage 1 implementation (e.g., thinker)
├── your_model_stage2_implementation.py # Stage 2 implementation (e.g., talker)
└── your_model_stage3_implementation.py # Stage 3 implementation (e.g., code2wav)
@@ -42,10 +43,14 @@ vllm_omni/model_executor/models/
vllm_omni/model_executor/stage_input_processors/
└── your_model_name.py # Stage transition processors
-vllm_omni/model_executor/stage_configs/
-└── your_model_name.yaml # Stage configuration file
+vllm_omni/deploy/
+└── your_model_name.yaml # Deployment configuration file
```
+New in-tree models should register their `PipelineConfig` in
+`vllm_omni/config/pipeline_registry.py`; use `--deploy-config` for custom
+deployment YAMLs.
+
## Step-by-Step Implementation
### Step 1: Create the Model Directory
@@ -535,7 +540,7 @@ Do **not** add a no-suffix `thinker2talker` when a `sync_process_input_func` is
## Testing
-For comprehensive testing guidelines, please refer to the [Test File Structure and Style Guide](../ci/tests_style.md).
+For comprehensive testing guidelines, please refer to the [Test Writing Guide](../ci/test_writing_guide.md).
## Adding a Model Recipe
diff --git a/docs/contributing/model/adding_tts_model.md b/docs/contributing/model/adding_tts_model.md
index 3e5ae30df..6b70fdb1e 100644
--- a/docs/contributing/model/adding_tts_model.md
+++ b/docs/contributing/model/adding_tts_model.md
@@ -175,15 +175,16 @@ vllm_omni/model_executor/models/
your_model_name/
__init__.py
your_model.py # Unified class (stage dispatch)
+ pipeline.py # Pipeline topology registry entry
your_model_ar_stage.py # Stage 0: AR stage
your_model_decoder.py # Stage 1: audio decoder
vllm_omni/model_executor/stage_input_processors/
your_model_name.py # Stage 0 -> Stage 1 transition
-vllm_omni/model_executor/stage_configs/
- your_model_name.yaml # Batch mode config
- your_model_name_async_chunk.yaml # Streaming mode config
+vllm_omni/deploy/
+ your_model_name.yaml # Batch/deploy config
+ your_model_name_async_chunk.yaml # Streaming/deploy config
```
### Example placement
@@ -445,6 +446,12 @@ Key configuration fields:
| `final_output` | Whether this stage produces the final user-facing output |
| `final_output_type` | Type of final output (`audio`, `text`, etc.) |
+!!! note
+ New in-tree models should define frozen topology in `models//pipeline.py`,
+ register it in `vllm_omni/config/pipeline_registry.py`, and put deployment
+ knobs in `vllm_omni/deploy/.yaml`. Use `--deploy-config` to load a
+ custom deployment YAML.
+
### Batch mode
```yaml
@@ -629,7 +636,7 @@ Key points:
## Testing
-For general testing conventions, see [tests_style.md](../ci/tests_style.md).
+For general testing conventions, see [Test Writing Guide](../ci/test_writing_guide.md).
Recommended test cases for a new TTS model:
@@ -771,8 +778,9 @@ directly from the model's own generator.
vllm_omni/model_executor/models/your_model_name/
__init__.py
modeling_your_model_name.py # unified class: load_weights + forward + streaming
+ pipeline.py # single-stage pipeline topology
-vllm_omni/model_executor/stage_configs/your_model_name.yaml
+vllm_omni/deploy/your_model_name.yaml
```
No stage input processor is needed.
diff --git a/docs/design/ar_diffusion_pipeline_capability.md b/docs/design/ar_diffusion_pipeline_capability.md
new file mode 100644
index 000000000..c9e05c8e8
--- /dev/null
+++ b/docs/design/ar_diffusion_pipeline_capability.md
@@ -0,0 +1,165 @@
+# AR-Diffusion pipeline capability
+
+> **Status:** experimental and non-normative. This note documents the current
+> `experimental/ar_diffusion` contract. It follows the ownership and lifecycle
+> direction in [#5137](https://github.com/vllm-project/vllm-omni/issues/5137),
+> but it is not yet part of the stable module-design hierarchy.
+
+`ARDiffusionEngine` is an opt-in diffusion backend for pipelines that keep
+autoregressive attention KV across requests. The engine only selects
+`ARDiffusionModelRunner`; the runner owns paged KV pools, session lookup, LRU
+eviction, and failure cleanup.
+
+A pipeline opts in by implementing `SupportsARDiffusionPipeline`. Its immutable
+`ARDiffusionKVCacheSpec` reports worker-local KV geometry and policy:
+
+- self-attention layers, TP-local KV heads, head size, and tokens per latent frame;
+- latent frames committed by one model block, sliding window, and attention sink;
+- logical KV branches mapped to worker-local storage slots;
+- named fixed-length cross-attention KV, model-specific scratch geometry, and
+ the pipeline's requested session-capacity upper bound.
+
+The runner binds an `ARDiffusionKVState` only around one `forward()` call. The
+pipeline may use it during that context but must not retain it. The session
+survives subsequent requests with the same `session_id`. Request reset, explicit
+close, LRU eviction, and forward exceptions all release runner-owned KV and call
+the pipeline's reset/close lifecycle hook. Cross-attention KV is allocated lazily
+per session and logical KV branch, and is released on the same path. A pipeline
+publishes one named cache transactionally by yielding exactly one `(k, v)` pair
+per layer:
+
+```python
+state.populate_cross_attention("main", "text", project_text_kv_by_layer())
+```
+
+The cache becomes readable only after the iterable completes successfully.
+Logical branches remain independent even when they share a worker-local
+`local_index`; that index only maps lazily executed self-attention and scratch
+work to worker-local capacity slots. It does not define cross-attention cache
+ownership.
+
+Requests select the session with `extra_args["session_id"]`; setting `reset=True`
+replaces it before the forward, while `close_session=True` releases it after a
+successful forward. The runner also exposes `reset_session()` and
+`close_session()` for an owning serving layer.
+
+## Current execution limits
+
+The experimental runner supports request-mode execution with
+`max_num_seqs=1`. It rejects step execution and request-batch execution at load
+time, and its inherited batch/step entry points also fail explicitly. Supporting
+those paths requires a batch-aware state-binding contract rather than silently
+bypassing `bind_ar_diffusion_state()`.
+
+The current implementation retains one session per model instance. A request
+for a different `session_id` closes the resident session through the same LRU
+lifecycle used by explicit close and failure cleanup before allocating the new
+one. Self-attention and cross-attention memory are therefore sized for one
+resident session even if the pipeline declares a larger `session_capacity`.
+This prioritizes rollout throughput and prevents block-pool exhaustion before a
+count-based eviction can run. Multi-session residency can be added later with a
+memory-backed capacity calculation or allocation-pressure eviction.
+
+Scratch storage is also capability-driven. `frames_per_block` reserves space for
+an uncommitted current video block, while `max_scratch_tokens_per_branch`
+declares the maximum model-specific tokens, such as action/state registers, that
+must coexist with it. `AR_DIFFUSION_KV_SCRATCH_BLOCKS_PER_BRANCH` may increase
+that derived minimum for deployment experiments but cannot reduce it.
+
+## DreamZero adapter
+
+DreamZero implements the capability directly on `DreamZeroPipeline`. Its adapter
+reads DreamZero transformer geometry, describes the positive and negative CFG
+branches, binds the state with a context manager, and translates the existing
+boolean CFG calls to the explicit `"positive"` and `"negative"` KV branch names.
+Its optional `ar_diffusion_warmup_requests()` provider owns the robot observation
+schema and the one-frame/four-frame warmup convention; none of those details are
+present in the generic runner.
+
+Code migrating from the former experimental KV state API should use the
+mechanical mapping below:
+
+```python
+# Before: state.pos, state.neg, state.get_kv_caches(is_negative, ...)
+positive = state.adapter("positive")
+negative = state.adapter("negative")
+contexts = state.get_kv_caches("negative", seq_len=frame_tokens, commit_current=True)
+```
+
+Direct experimental `ARDiffusionKVCache` construction also migrates
+mechanically: replace `local_branches` with explicit `kv_branches`,
+`num_frame_per_block` with `frames_per_block`, and the fixed text/image length
+arguments with a named `cross_attention_lengths` mapping plus
+`session_capacity`. Normal engine users do not construct this object directly.
+
+Cross-attention writers migrate from incremental publication:
+
+```python
+# Before
+for layer_index, (key, value) in enumerate(project_text_kv_by_layer()):
+ state.write_cross_attention_kv("text", layer_index, branch, key, value)
+state.mark_cross_attention_populated(branch, "text")
+
+# After
+state.populate_cross_attention(branch, "text", project_text_kv_by_layer())
+```
+
+The iterable is consumed once and must yield exactly `num_layers` pairs. Failed
+or incomplete projection leaves the previous complete cache unchanged, or
+leaves the cache absent on its first population.
+
+## Single-KV-branch pipeline sketch
+
+A future single-branch causal DMD pipeline can opt in without changes to the
+runner or KV core:
+
+```python
+class WorldPipeline:
+ def ar_diffusion_kv_cache_spec(self):
+ return ARDiffusionKVCacheSpec(
+ num_layers=self.transformer.num_layers,
+ num_kv_heads=self.transformer.tp_local_kv_heads,
+ head_size=self.transformer.head_size,
+ tokens_per_frame=self.transformer.tokens_per_latent_frame,
+ frames_per_block=3,
+ window_frames=self.transformer.sliding_window_frames,
+ sink_frames=self.transformer.sink_frames,
+ kv_branches=(ARDiffusionKVBranchSpec("main", 0),),
+ cross_attention=(ARDiffusionCrossAttentionKVSpec("text", self.text_length),),
+ max_scratch_tokens_per_branch=self.transformer.max_register_tokens,
+ session_capacity=1,
+ )
+
+ @contextmanager
+ def bind_ar_diffusion_state(self, session_id, state):
+ self.current_ar_state = state
+ try:
+ yield
+ finally:
+ self.current_ar_state = None
+
+ def reset_ar_diffusion_session(self, session_id):
+ self.model_states.pop(session_id, None)
+
+ def close_ar_diffusion_session(self, session_id):
+ self.model_states.pop(session_id, None)
+```
+
+The model writes/reads named cross-attention KV through the bound state and uses
+the `"main"` KV branch for paged self-attention. Pipelines without a warmup
+provider are loaded normally and skip AR rollout warmup.
+
+## Relationship to the multi-stage KV manager RFC
+
+[#5244](https://github.com/vllm-project/vllm-omni/issues/5244) describes a
+future multi-stage KV transfer manager, connector integration, and cache-aware
+scheduling. This capability has a narrower scope: it owns model-local paged KV
+and session lifecycle inside one AR-Diffusion runner. It does not define
+cross-stage transfer, connector behavior, or scheduler-visible cache affinity.
+
+When the experimental runtime migrates toward #5244, the implementation should
+reconcile its current full `KVCacheManager` use with the RFC's proposed
+`BlockPool`-oriented DiT manager. The pipeline capability should remain the
+model-facing geometry and state-binding boundary where possible, while the
+experimental `ARDiffusionKVCache` class itself should not be treated as a stable
+public API.
diff --git a/docs/design/feature/async_diffusion_output.md b/docs/design/feature/async_diffusion_output.md
new file mode 100644
index 000000000..a81ed6811
--- /dev/null
+++ b/docs/design/feature/async_diffusion_output.md
@@ -0,0 +1,174 @@
+# Async Diffusion Output
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Performance](#performance)
+3. [Architecture](#architecture)
+4. [Model Coverage](#model-coverage)
+5. [Configuration](#configuration)
+6. [Related Files](#related-files)
+
+## Overview
+
+The async diffusion output feature moves the D2H (Device→Host) copy and SHM packing from the Worker main thread to a background daemon thread, so the GPU default stream can immediately start the next request's forward pass. This eliminates the GPU bubble caused by synchronous D2H/packing.
+
+**Core benefit**: the previous request's output D2H/packing overlaps with the next request's forward pass.
+
+**Automatically enabled** when `step_execution=False` (default, request-mode). No extra configuration needed. When `step_execution=True` (step-mode), the original synchronous path is used — pump thread and Worker background thread are not started.
+
+## Performance
+
+### HunyuanImage-3.0 (TP4)
+
+| Resolution | Async Off (QPS) | Async On (QPS) | Change |
+|---|---|---|---|
+| 1024×1024 | 0.4773 | 0.4802 | +0.60% |
+| 768×768 | 0.8370 | 0.8533 | +1.95% |
+
+**Analysis**: smaller resolution → larger QPS gain. D2H/packing is relatively fixed overhead; forward is variable (scales with resolution). Smaller images → shorter forward → D2H/packing occupies a larger fraction of total time → more benefit from overlapping it.
+
+## Architecture
+
+### Execution Timeline
+
+```
+Before (synchronous D2H):
+[forward req1] [D2H+SHM pack req1] [forward req2] [D2H+SHM pack req2]
+ ^^^^^^^^^^^^^^^^ GPU bubble
+
+After (async D2H):
+[forward req1] [forward req2]
+[D2H+SHM pack req1 (side stream)] [D2H+SHM pack req2 (side stream)]
+ ^^^^^^^^^^^^^^^^ bubble eliminated
+```
+
+### Data Flow
+
+```
+ ┌─────────────────────────────────────────────────────┐
+ │ Worker Process │
+ │ │
+ execute_model ──> │ WorkerBusyLoop ──> forward() ──> DiffusionOutput │
+ │ │ │ (GPU tensor)│
+ │ │ ┌─────────┴──────────┐ │
+ │ │ │ async output path │ │
+ │ │ │ │ │
+ │ │ compute_done ──> result_mq │ │
+ │ │ │ │ │
+ │ │ ▼ │ │
+ │ │ AsyncOutputThread │ │
+ │ │ (side CUDA stream) │ │
+ │ │ wait_event(gpu_event) │ │
+ │ │ pack_diffusion_output_shm() │ │
+ │ │ D2H + SHM write │ │
+ │ │ │ │ │
+ │ │ output_ready ──> result_mq │ │
+ │ │ │ │ │
+ │ ▼ ▼ │ │
+ │ (dequeue next request) │ │
+ └─────────────────────────────────────────────────────┘
+ │
+ ▼
+ ┌─────────────────────────────────────────────────────┐
+ │ Executor (main process) │
+ │ │
+ │ ResultPumpThread (sole reader of result_mq) │
+ │ │ │
+ │ ├── AsyncDiffusionOutput(COMPUTE_DONE) │
+ │ │ → resolve _rpc_futures[rpc_id] │
+ │ │ → RunnerOutput(async_output_id=...) │
+ │ │ │
+ │ ├── AsyncDiffusionOutput(OUTPUT_READY) │
+ │ │ → resolve _output_futures[async_output_id]│
+ │ │ → or batch split via _batch_split_map │
+ │ │ │
+ │ └── Non-async message │
+ │ → _sync_result_buffer (for other RPCs) │
+ │ │
+ │ wait_output_ready(async_output_id) → Future │
+ └─────────────────────────────────────────────────────┘
+```
+
+### Key Components
+
+1. **AsyncDiffusionOutput** (`data.py`): Protocol envelope for `result_mq`. `kind` field routes messages:
+ - `COMPUTE_DONE` — forward finished, GPU can start next request
+ - `OUTPUT_READY` — D2H/SHM packing finished, final output available
+ - `RPC_RESULT` — ordinary RPC return (including error propagation)
+
+2. **Worker AsyncOutputThread** (`diffusion_worker.py`): Background daemon thread that:
+ - Waits for `gpu_event` (cross-stream sync ensuring forward wrote the tensor)
+ - Runs `pack_diffusion_output_shm()` on a side CUDA stream
+ - Enqueues `OUTPUT_READY` to `result_mq` when packing completes
+
+3. **ResultPumpThread** (`multiproc_executor.py`): Sole reader of `result_mq` in request-mode. Dispatches messages:
+ - `COMPUTE_DONE` / `RPC_RESULT` → resolves `_rpc_futures[rpc_id]`
+ - `OUTPUT_READY` → resolves `_output_futures[async_output_id]` (with batch split via `_batch_split_map`)
+ - Non-async messages → `_sync_result_buffer` for other RPCs
+
+4. **collective_rpc() two-path dispatch** (`multiproc_executor.py`):
+ - **Path 1**: `execute_model` / `execute_model_batch` → generates `rpc_id`, registers Future, waits for pump to deliver `compute_done`
+ - **Path 2**: All other RPCs → `_sync_result_buffer` (pump-fed) or `_result_mq` (step-mode, no pump)
+
+5. **record_device_event** (`platforms/`): Cross-platform GPU event recording for side-stream synchronization. Implemented for CUDA, NPU, ROCm, XPU, MUSA; base class returns `None` (safe no-op).
+
+6. **IPC side-stream D2H** (`ipc.py`): `pack_diffusion_output_shm()` and helpers accept `d2h_stream` parameter; use `pin_memory` + `copy_(non_blocking=True)` on the side stream instead of synchronous `.cpu()`.
+
+### Batch Split
+
+When `execute_model_batch` returns a `COMPUTE_DONE` with a single `async_output_id` for the entire batch, the executor splits it into per-request `async_output_id`s (format `{batch_id}/{request_id}`) via `_batch_split_map`. When `OUTPUT_READY` arrives, the pump extracts per-request results from the batch output and resolves each request's output future independently.
+
+## Model Coverage
+
+### Currently Supported (request-mode, `step_execution=False`)
+
+All models running in request-mode (`step_execution=False`, the default) automatically use async output.
+
+**Verified models**:
+
+| Model | Type | `supports_request_batch` |
+|---|---|---|
+| **HunyuanImage-3.0** | Image | `False` |
+| **Qwen-Image** | Image | `True` |
+
+Other models with `step_execution=False` are also supported but not yet verified.
+
+### Not Yet Supported (step-mode, `step_execution=True`)
+
+When `step_execution=True` (or `streaming_output=True`, which auto-enables step-mode), models use `execute_stepwise` instead of `execute_model`, which is not in the async Path 1 whitelist. Async output is not applicable.
+
+**Verified**: Helios (`step_execution=True`, does not benefit from this feature).
+
+Other models with `step_execution=True` are also not applicable.
+
+Adapting step-mode requires additional design because:
+
+- Each step's `RunnerOutput` must be available synchronously for the scheduler to advance
+- Intermediate latent tensors may not need D2H (only the final step needs D2H + postprocess)
+- Steps are serial (step N+1 depends on step N output), so the overlap benefit is limited to "final step's D2H overlaps with the next request's forward"
+
+**Planned approach**: first cover only the final step's async D2H, keep intermediate steps synchronous. This matches the request-mode benefit pattern.
+
+## Configuration
+
+No configuration needed. Async output is automatically enabled when `step_execution=False` (default).
+
+| `step_execution` | Mode | Async Output | Pump Thread | Worker BG Thread |
+|---|---|---|---|---|
+| `False` (default) | request-mode | ✅ enabled | ✅ started | ✅ started |
+| `True` | step-mode | ❌ disabled | ❌ not started | ❌ not started |
+
+## Related Files
+
+- `vllm_omni/diffusion/data.py`: `AsyncDiffusionOutput`, `AsyncOutputKind`, `DiffusionOutput.async_output_id`
+- `vllm_omni/diffusion/worker/diffusion_worker.py`: `WorkerProc._return_result()`, `_async_output_loop()`, `_generate_async_output_id()`
+- `vllm_omni/diffusion/executor/multiproc_executor.py`: `ResultPumpThread`, `collective_rpc()` two-path dispatch, `wait_output_ready()`, `_batch_split_map`
+- `vllm_omni/diffusion/diffusion_engine.py`: `step()` / `step_streaming()` / `add_req_and_wait_for_response()` async output waiting
+- `vllm_omni/diffusion/sched/request_scheduler.py`: `update_from_output()` — `async_output_id` → `FINISHED_COMPLETED`
+- `vllm_omni/diffusion/ipc.py`: `pack_diffusion_output_shm()` side-stream D2H path
+- `vllm_omni/platforms/interface.py`: `OmniPlatform.record_device_event()` base class
+- `vllm_omni/platforms/cuda/platform.py`: `CudaOmniPlatform.record_device_event()`
+- `vllm_omni/platforms/npu/platform.py`: `NPUOmniPlatform.record_device_event()`
+- `vllm_omni/diffusion/models/hunyuan_image3/pipeline_hunyuan_image3.py`: `get_hunyuan_image3_post_process_func()`, postprocess removal
+- `vllm_omni/diffusion/registry.py`: `_DIFFUSION_POST_PROCESS_FUNCS` postprocess registry
diff --git a/docs/design/feature/cache_dit.md b/docs/design/feature/cache_dit.md
index 237a95877..38b597ca3 100644
--- a/docs/design/feature/cache_dit.md
+++ b/docs/design/feature/cache_dit.md
@@ -32,10 +32,13 @@ The library supports three main caching strategies:
vLLM-omni integrates cache-dit through the `CacheDiTBackend` class, which provides a unified interface for managing cache-dit acceleration on diffusion models.
+Code outside `vllm_omni/diffusion/cache` must import Cache-DiT symbols from the
+package-level API: `vllm_omni.diffusion.cache.cachedit`.
+
| Method/Class | Purpose | Behavior |
|--------------|---------|----------|
-| [`CacheDiTBackend`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/#vllm_omni.diffusion.cache.CacheBackend) | Unified backend interface | Automatically handles enabler selection and cache refresh |
-| [`enable_cache_for_dit()`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_dit) | Apply caching to transformer | Configures DBCache on transformer blocks |
+| `CacheDiTBackend` | Unified backend interface | Automatically handles enabler selection and cache refresh |
+| `enable_cache_for_dit()` | Apply caching to transformer | Configures DBCache on transformer blocks |
**Key APIs from Cache-DiT:**
@@ -47,7 +50,7 @@ vLLM-omni integrates cache-dit through the `CacheDiTBackend` class, which provid
| `ForwardPattern` | Defines block forward signature patterns: `Pattern_0`, `Pattern_1`, `Pattern_2` |
| `ParamsModifier` | Per-transformer or per-block-list cache configuration customization |
| `DBCacheConfig` | Configuration for DBCache parameters (warmup steps, cached steps, thresholds) |
-| `refresh_context()` | Update cache context | Called when `num_inference_steps` changes |
+| `refresh_context()` | Update cache context | Called at every generation boundary |
---
@@ -179,7 +182,8 @@ cache_dit.enable_cache(
### Registering Custom Implementations
-After writing your custom enabler, register it in `CUSTOM_DIT_ENABLERS` in `vllm_omni/diffusion/cache/cache_dit_backend.py`:
+After writing your custom enabler, register it in `CUSTOM_DIT_ENABLERS` in
+`vllm_omni/diffusion/cache/cachedit/model_specific.py`:
```python
CUSTOM_DIT_ENABLERS = {
@@ -266,10 +270,10 @@ Complete examples in the codebase:
| Model | Path | Pattern | Notes |
|-------|------|---------|-------|
-| **Standard DiT** | [`cache_dit_backend.py::enable_cache_for_dit`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_dit) | Default enabler | Single transformer, automatic |
-| **Wan2.2** | [`cache_dit_backend.py::enable_cache_for_wan22`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_wan22) | Single or dual-transformer | Auto-detects mode based on transformer_2 presence |
-| **LongCat** | [`cache_dit_backend.py::enable_cache_for_longcat_image`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_longcat_image) | Multi-block-list | Two block lists in one transformer |
-| **BAGEL** | [`cache_dit_backend.py::enable_cache_for_bagel`](https://docs.vllm.ai/projects/vllm-omni/en/latest/api/vllm_omni/diffusion/cache/cache_dit_backend/#vllm_omni.diffusion.cache.cache_dit_backend.enable_cache_for_bagel) | Omni model | Complex architecture |
+| **Standard DiT** | `cachedit.backend::enable_cache_for_dit` | Default enabler | Single transformer, automatic |
+| **Wan2.2** | `cachedit.model_specific::enable_cache_for_wan22` | Single or dual-transformer | Auto-detects mode based on transformer_2 presence |
+| **LongCat** | `cachedit.config::CacheDiTAdapterConfig` | Declarative block adapter | Two block lists in one transformer |
+| **BAGEL** | `cachedit.model_specific::BagelCachedAdapter` | Custom cached adapter | Complex architecture |
---
diff --git a/docs/design/feature/diffusion_continuous_batching.md b/docs/design/feature/diffusion_continuous_batching.md
index f90969369..2ec89b9d1 100644
--- a/docs/design/feature/diffusion_continuous_batching.md
+++ b/docs/design/feature/diffusion_continuous_batching.md
@@ -1,163 +1,232 @@
-# Continuous Batching for Step-Wise Diffusion
+# Diffusion Continuous Batching
-!!! warning "Experimental Feature"
- This feature is experimental. It currently applies only to native
- diffusion pipelines running with `step_execution=True`.
-
-This document describes the batching extension built on top of
-[Diffusion Step Execution](diffusion_step_execution.md). The base
-step-execution contract is unchanged. The batching work is mainly in the
-scheduler and runner layers.
-
-## Why It Helps
-
-Step-wise execution breaks a long denoise loop into scheduler-visible units.
-That gives the runtime a place to admit other compatible requests between
-steps instead of waiting for an entire request to finish.
-
-This matters most in low-MFU or bursty serving scenarios:
-
-- one request's denoise step may not fully saturate the GPU
-- several compatible requests can share the same denoise forward pass
-- throughput and device utilization can improve without changing request-local
- scheduler state
-
-This is **not** a guaranteed single-request latency win. The main benefit is
-usually higher utilization and better throughput when the workload contains
-multiple in-flight compatible requests.
+This document describes the unified diffusion execution architecture, including
+request execution, request-level batching, step execution, continuous batching,
+and output delivery. For user-facing configuration and CLI examples, see
+[Diffusion Execution Modes](../../user_guide/diffusion/execution_modes.md).
## Overview
-With continuous batching enabled:
+`DiffusionEngine` selects an execution mode from `step_execution` and uses
+`max_num_seqs` as that mode's scheduler capacity:
-- the scheduler may keep multiple compatible requests active at the same time
-- the runner packs request-local step state into one `InputBatch`
-- `denoise_step()` runs on that batch
-- `step_scheduler()` and `post_decode()` still run per request
+| Configuration | Engine mode | Scheduler | Execution |
+|---|---|---|---|
+| `step_execution=False`, `max_num_seqs=1` | `REQUEST_BATCH` | `RequestScheduler` | One complete request-level `forward()` |
+| `step_execution=False`, `max_num_seqs>1` | `REQUEST_BATCH` | `RequestScheduler` | One fused `forward()` over compatible requests |
+| `step_execution=True`, `max_num_seqs=1` | `STEP_BATCH` | `StepScheduler` | One request advanced one denoise step per scheduler tick |
+| `step_execution=True`, `max_num_seqs>1` | `STEP_BATCH` | `StepScheduler` | Compatible requests advanced together in step waves |
-The current implementation is conservative:
+Serial request execution is handled inside `REQUEST_BATCH`; it is not a
+separate engine mode. Similarly, continuous batching is the multi-request
+configuration of `STEP_BATCH`, not an independent execution mode.
-- only compatible requests are batched together
-- per-request progress and completion remain independent
+The engine performs the following initialization:
-Here, "continuous batching" means the step-wise path enabled by
-`step_execution=True`. Request-mode `DiffusionRequestBatch` is static
-request-level batching for one full pipeline `forward()` call; it does not
-admit or remove requests between denoise steps.
+1. Resolve model-specific pre- and post-processing hooks.
+2. Select `REQUEST_BATCH` or `STEP_BATCH`.
+3. Construct the configured `DiffusionExecutor`.
+4. Construct `RequestScheduler` or `StepScheduler`, unless a scheduler was
+ explicitly injected.
+5. Bind `execute_batch` or `execute_step`.
-## Enablement
+`DiffusionEngine.make_engine()` owns startup warmup after engine construction.
+This keeps construction, backend selection, and warmup failure cleanup in one
+lifecycle.
-Use `--step-execution` as the feature gate, then increase `--max-num-seqs`
-above `1` if you want batching:
+## Unified Output Stream
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --step-execution \
- --max-num-seqs 8
+All asynchronous diffusion requests use the same per-request output stream,
+regardless of whether the caller exposes streaming results to the user.
+
+```python
+async for output in engine.step_streaming(request):
+ ...
```
-`--max-num-seqs 1` keeps the step-wise path without enabling batching.
+When a request is admitted, the engine creates an
+`asyncio.Queue[DiffusionOutput]` keyed by its request ID. The busy loop sends
+both intermediate chunks and final results through that queue:
-For a reproducible replay flow using the bundled serving benchmark, see the
-Qwen-Image replay commands in
-[`benchmarks/diffusion/README.md`](gh-file:benchmarks/diffusion/README.md)
-and
-[`benchmarks/diffusion/performance_dashboard/qwen_image_serving_performance.md`](gh-file:benchmarks/diffusion/performance_dashboard/qwen_image_serving_performance.md).
+```text
+request
+ -> scheduler.add_request()
+ -> scheduler.schedule()
+ -> execute_batch() or execute_step()
+ -> scheduler.update_from_output()
+ -> per-request output queue
+ -> step_streaming()
+```
-## Scheduler
+Streaming callers forward every yielded output. Non-streaming callers consume
+the same generator to completion and return only its final output. This avoids
+maintaining separate future-based and queue-based result paths.
-The scheduler derives its batch capacity from `max_num_seqs` through
-`max_num_running_reqs`.
+`DiffusionEngine.step()` and
+`async_add_req_and_wait_for_response()` remain deprecated compatibility
+wrappers. New integrations should consume `step_streaming()` or
+`async_add_req_and_stream_response()`.
-Batch admission is gated by
-[`SamplingParamsKey`](gh-file:vllm_omni/diffusion/sched/interface.py),
-which is built from shape-sensitive and CFG-sensitive sampling fields. This is
-the core correctness rule for batching: requests are only co-batched when they
-share the same denoise tensor contract.
+### Completion and Cancellation
-There are three important details:
+Output delivery and scheduler lifecycle are related but separate:
-- `num_inference_steps` is not part of the key, so requests with different
- total step counts can still share a batch
-- requests also do not need to be at the same current denoise progress; active
- requests can continue batching even when their current step indices diverge
-- admission is still FIFO, so an incompatible request at the head of the
- waiting queue blocks later compatible requests
+- A terminal scheduler result always finalizes request state.
+- A disconnected or cancelled consumer removes only its delivery queue.
+- The scheduler may still finish and clean up the request after its queue has
+ been removed.
+- Engine shutdown sends an error output to all remaining streams.
-Today that compatibility rule is still shape-sensitive. `height`, `width`,
-`num_frames`, and CFG-related fields remain part of the key, so different
-resolutions or incompatible guidance settings do **not** co-batch yet. The
-key also covers LoRA identity (`lora_int_id`, `lora_scale`), so requests
-targeting different adapters or scales run in separate batches and the
-worker can activate exactly one adapter per step.
+This separation prevents cancelled consumers from leaving active request IDs in
+the scheduler.
-The scheduler batching unit is one logical `OmniDiffusionRequest`. In the
-step-wise path, runtime tensor batching is represented as `StepInputBatch`. For
-request-mode prompt semantics, see
-[Request-Level Batching](../../user_guide/diffusion/request_batching.md).
+## Request-Batch Execution
-## Runner
+Request-batch execution runs the complete diffusion pipeline for each scheduler
+wave. Each `OmniDiffusionRequest` remains an independent logical request with
+its own prompt, sampling parameters, seed, request ID, output, error, and abort
+state.
-The runner keeps persistent per-request execution state in
-[`DiffusionRequestState`](gh-file:vllm_omni/diffusion/worker/utils.py),
-while the scheduler owns a separate lightweight request state for queueing and
-lifecycle tracking.
+With `max_num_seqs=1`, `execute_batch()` handles the serial request path. With a
+larger value, a pipeline can opt into fused execution by declaring:
-For each step, the runner builds an
-[`InputBatch`](gh-file:vllm_omni/diffusion/worker/input_batch.py) from the
-active request states:
+```python
+supports_request_batch = True
+```
-- prompt embeddings and masks are normalized and padded
-- dynamic tensors such as `latents` and `timesteps` are gathered each step
-- buffers are reused when batch composition stays the same
+Its `forward()` method must accept `DiffusionRequestBatch` and return one
+`DiffusionOutput` per request. The runner validates the result count and maps
+outputs back to their original request IDs with `BatchRunnerOutput`.
-The step-wise batched path is:
+Current in-tree request-batch implementations include the Flux, LTX-2, SD3, and
+Qwen-Image pipelines. Treat the pipeline capability flag in the source as
+authoritative because support continues to expand.
-1. Run `prepare_encode()` for newly admitted requests.
+### Request-Batch Data Flow
+
+- `DiffusionSchedulerOutput` carries scheduled request IDs and payloads.
+- `DiffusionRequestBatch` provides the pipeline-facing static batch.
+- `BatchRunnerOutput` carries per-request results back to the engine.
+
+Request-local setup, such as seed handling and output/error mapping, remains
+per request. Cache refresh, homogeneous LoRA activation, and
+`pipeline.forward(req_batch)` can be shared by the batch.
+
+### Compatibility and Admission
+
+`RequestBatchSamplingParamsKey` contains fields that affect the tensor contract,
+including shape, guidance, output count, and LoRA identity. Only compatible
+requests can share a fused forward call.
+
+Admission is FIFO and conservative. An incompatible request at the front of the
+waiting queue can prevent later compatible requests from joining the current
+wave.
+
+`request_batch_max_wait_ms` optionally delays the first schedule operation of a
+new wave so bursty arrivals can accumulate. It applies only when:
+
+- the pipeline supports request batching;
+- step execution is disabled;
+- no requests are currently running; and
+- the configured wait is greater than zero.
+
+The wait ends when the queue reaches capacity, stabilizes briefly, reaches its
+deadline, or the engine stops.
+
+## Step-Batch Execution
+
+Step execution exposes denoising progress to the scheduler. A supporting
+pipeline implements four stateful operations:
+
+| Operation | Responsibility |
+|---|---|
+| `prepare_encode(state)` | Validate input, encode prompts, initialize latents and timesteps, and create request-local scheduler state |
+| `denoise_step(input_batch, *, states=...)` | Run one denoise forward for the scheduler-provided request states |
+| `step_scheduler(state, noise_pred)` | Update latents and advance request progress |
+| `post_decode(state)` | Decode and postprocess a completed request or output boundary |
+
+Persistent request state lives in `StepRequestState`. Pipeline-specific fields
+that do not belong in the shared contract should be stored in `state.extra`.
+Queueing and lifecycle metadata remain in the scheduler's request state.
+
+Current native pipelines that explicitly enable step execution include
+Qwen-Image, HunyuanImage3, and Helios. Step execution alone does not imply
+continuous-batching support: Qwen-Image accepts batched step states.
+HunyuanImage3 accepts batched step states only when its resolved self-attention
+backend is `TORCH_SDPA`; otherwise it rejects groups larger than one request.
+Configure `DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA` or
+`diffusion_attention_config.default.backend=TORCH_SDPA` when
+`max_num_seqs>1`. See the
+[HunyuanImage-3.0 recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/HunyuanImage-3.0-Instruct.md)
+for its validated configuration. Helios supports only a single active step
+request and must use `max_num_seqs=1`.
+
+### Continuous Batching
+
+When `max_num_seqs>1`, `StepScheduler` can keep multiple compatible requests
+active. The runner gathers their state into `InputBatch`, performs one batched
+denoise forward, then applies scheduler updates per request:
+
+1. Receive transferred KV payloads, then run `prepare_encode()` for newly
+ admitted requests.
2. Build or refresh `InputBatch`.
-3. Run one batched `denoise_step(input_batch)`.
-4. Slice the batched `noise_pred` back per request.
-5. Run per-request `step_scheduler()`.
-6. Run `post_decode()` only for requests that finished denoising.
-7. Scatter updated latents back into persistent request state with
- [`scatter_latents()`](gh-file:vllm_omni/diffusion/worker/input_batch.py).
+3. Run one batched `denoise_step(input_batch, states=states)`.
+4. Slice noise predictions back to each request.
+5. Run each request's `step_scheduler()`.
+6. Run `post_decode()` at a chunk boundary or request completion.
+7. Scatter updated latents back into persistent request state.
-This keeps the shared work limited to the denoise forward pass while preserving
-request-local scheduler state and outputs.
+`StepBatchSamplingParamsKey` protects the batched tensor contract. Requests can
+have different total step counts and current step indices, but shape-sensitive,
+CFG-sensitive, and LoRA fields must be compatible. FIFO head-of-line blocking
+also applies here.
-## Engine
+Chunk-capable pipelines may emit intermediate results. Final-only step
+pipelines emit when the request finishes even if `streaming_output=True`.
-[`DiffusionEngine`](gh-file:vllm_omni/diffusion/diffusion_engine.py) provides
-the background loop and async add-request path needed for multiple requests to
-accumulate in the scheduler.
+## Model Author Guidelines
-When `step_execution=True`, the engine routes work through the step-wise
-executor path. The continuous batching behavior is defined by scheduler-side
-compatibility gating and runner-side `StepInputBatch` packing.
+When converting a request-level pipeline:
-## Current Limitations
+- Reuse the same helpers as `forward()` to avoid behavior drift.
+- Copy request-scoped scheduler state into `state.scheduler`.
+- Advance `state.step_index` only in `step_scheduler()`.
+- Keep model forward work in `denoise_step()`.
+- Keep latent mutation in `step_scheduler()`.
+- Keep final decoding equivalent to the tail of `forward()`.
+- Store masks, condition latents, and other request-local tensors in the
+ request state rather than on the shared pipeline.
-- Experimental feature; use `max_num_seqs=1` for the older conservative path.
-- Only native pipelines that already support `step_execution=True`.
-- Only homogeneous batches keyed by `SamplingParamsKey` are supported.
-- `cache_backend`, KV transfer, and other request-mode extras are not wired
- into the batched step-wise path yet.
-- Future work can relax the current same-shape restriction with richer
- heterogeneous batching policies such as bucketing or padded execution for
- different resolutions.
+Before setting `supports_step_execution = True`, validate output parity for the
+same seed and sampling parameters, scheduler isolation across concurrent
+requests, abort cleanup, progress reporting, and all supported CFG paths.
-## Related Files
+Before setting `supports_request_batch = True`, validate the batch input
+contract, one-output-per-request result shape, request identity and error
+mapping, seeded concurrency, LoRA compatibility, and tensor IPC.
-- Scheduler base:
- [`vllm_omni/diffusion/sched/base_scheduler.py`](gh-file:vllm_omni/diffusion/sched/base_scheduler.py)
-- Scheduler interface:
- [`vllm_omni/diffusion/sched/interface.py`](gh-file:vllm_omni/diffusion/sched/interface.py)
-- Step scheduler:
- [`vllm_omni/diffusion/sched/step_scheduler.py`](gh-file:vllm_omni/diffusion/sched/step_scheduler.py)
-- Runner:
- [`vllm_omni/diffusion/worker/diffusion_model_runner.py`](gh-file:vllm_omni/diffusion/worker/diffusion_model_runner.py)
-- Input batch:
- [`vllm_omni/diffusion/worker/input_batch.py`](gh-file:vllm_omni/diffusion/worker/input_batch.py)
-- Tests:
- [`tests/diffusion/test_diffusion_scheduler.py`](gh-file:tests/diffusion/test_diffusion_scheduler.py)
+## Limitations
+
+- Request batching requires an explicit pipeline capability when
+ `max_num_seqs>1`.
+- Step execution requires the four-method stateful pipeline contract.
+- Both batching paths currently require homogeneous compatibility keys.
+- FIFO scheduling can reduce batching opportunities.
+- `request_batch_max_wait_ms` trades first-request latency for burst
+ coalescing.
+- All diffusion cache backends are currently unsupported in step mode.
+- KV transfer is supported for newly admitted step requests; some other
+ request-mode extras remain unsupported.
+- Step continuous batching remains experimental; use `max_num_seqs=1` for the
+ conservative step path.
+
+## Related Implementation
+
+- Engine: [`vllm_omni/diffusion/diffusion_engine.py`](gh-file:vllm_omni/diffusion/diffusion_engine.py)
+- Model contract: [`vllm_omni/diffusion/models/interface.py`](gh-file:vllm_omni/diffusion/models/interface.py)
+- Scheduler interface: [`vllm_omni/diffusion/sched/interface.py`](gh-file:vllm_omni/diffusion/sched/interface.py)
+- Request scheduler: [`vllm_omni/diffusion/sched/request_scheduler.py`](gh-file:vllm_omni/diffusion/sched/request_scheduler.py)
+- Step scheduler: [`vllm_omni/diffusion/sched/step_scheduler.py`](gh-file:vllm_omni/diffusion/sched/step_scheduler.py)
+- Runner: [`vllm_omni/diffusion/worker/diffusion_model_runner.py`](gh-file:vllm_omni/diffusion/worker/diffusion_model_runner.py)
+- Request batch: [`vllm_omni/diffusion/worker/request_batch.py`](gh-file:vllm_omni/diffusion/worker/request_batch.py)
+- Step input batch: [`vllm_omni/diffusion/worker/input_batch.py`](gh-file:vllm_omni/diffusion/worker/input_batch.py)
diff --git a/docs/design/feature/diffusion_request_level_batching.md b/docs/design/feature/diffusion_request_level_batching.md
deleted file mode 100644
index 57287ada4..000000000
--- a/docs/design/feature/diffusion_request_level_batching.md
+++ /dev/null
@@ -1,175 +0,0 @@
-# Request-Level Batching for Diffusion
-
-This document describes the request-mode batching path for diffusion pipelines.
-For end-user enablement and tuning, see
-[Request-Level Batching](../../user_guide/diffusion/request_batching.md).
-
-This is separate from
-[Continuous Batching for Step-Wise Diffusion](diffusion_continuous_batching.md).
-Request-level batching runs one full pipeline `forward()` over a static batch of
-compatible requests. Step-wise continuous batching admits work between denoise
-steps when `step_execution=True`.
-
-## Why It Helps
-
-The request-level design avoids coupling several logical requests to one request
-object. This keeps request identity, abort/error handling, and per-request
-metadata unambiguous while still allowing one fused pipeline forward pass for
-bursty or concurrent traffic.
-
-## Overview
-
-With request-level batching enabled:
-
-- each `OmniDiffusionRequest` contains one `prompt` and one `request_id`
-- the scheduler groups compatible waiting requests into one scheduler wave
-- `DiffusionRequestBatch` wraps the scheduled requests for pipeline `forward()`
-- batch-capable pipelines return `list[DiffusionOutput]`, one output per
- request
-- `BatchRunnerOutput` maps each result back to its original `request_id`
-
-Pipelines opt in with `supports_request_batch = True` and a `forward()` method
-that accepts `DiffusionRequestBatch` and returns `list[DiffusionOutput]`.
-Pipelines that do not opt in keep the existing per-request execution path.
-
-## Enablement
-
-Request-level batching is the request-mode path, so `step_execution` must remain
-disabled. Increase `max_num_seqs` above `1` to let the scheduler keep multiple
-compatible requests active:
-
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --max-num-seqs 4
-```
-
-For bursty online ingress, `request_batch_max_wait_ms` can add a bounded
-admission wait before the first `schedule()` of a scheduler wave:
-
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --max-num-seqs 4 \
- --request-batch-max-wait-ms 20
-```
-
-`request_batch_max_wait_ms=0` disables this wait and is the default.
-
-## Request Contract
-
-`OmniDiffusionRequest` represents one logical request. It owns one prompt,
-sampling parameters, request id, and request-local metadata. Runtime batches are
-formed by the scheduler and represented separately from the request payload.
-
-Runtime batching is represented by:
-
-- [`DiffusionSchedulerOutput`](gh-file:vllm_omni/diffusion/sched/interface.py)
- for scheduled request ids and request payloads
-- [`DiffusionRequestBatch`](gh-file:vllm_omni/diffusion/worker/request_batch.py)
- for the pipeline-facing request batch
-- [`BatchRunnerOutput`](gh-file:vllm_omni/diffusion/worker/utils.py) for
- per-request results
-
-`DiffusionRequestBatch` intentionally exposes compatibility properties such as
-`prompts`, `sampling_params`, `request_id`, and `kv_sender_info` so migrated
-pipelines can stay close to upstream code while using a batch-aware contract.
-
-## Scheduler
-
-The scheduler derives its capacity from `max_num_seqs` through
-`max_num_running_reqs`. It exposes waiting/running queue counters so the engine
-can decide whether admission wait is useful before scheduling a new wave.
-
-Batch compatibility is controlled by
-[`SamplingParamsKey`](gh-file:vllm_omni/diffusion/sched/interface.py). The key
-contains shape-sensitive and guidance-sensitive fields, including output count
-and LoRA identity. Requests with incompatible shapes, CFG settings, output
-counts, LoRA adapters, or LoRA scales are kept in separate batches.
-
-Admission is conservative:
-
-- the scheduler only batches compatible requests
-- FIFO ordering is preserved
-- an incompatible request at the head of the waiting queue blocks later
- compatible requests
-
-## Engine
-
-[`DiffusionEngine`](gh-file:vllm_omni/diffusion/diffusion_engine.py) resolves
-request-batch capability during initialization from the configured pipeline
-class, including custom pipeline classes.
-
-The capability check uses the pipeline class attribute
-`supports_request_batch = True`. Pipelines that set this attribute must implement
-a request-batch-compatible `forward()` contract and return one
-`DiffusionOutput` per request; the runner validates that return shape at runtime.
-
-When the selected pipeline is batch-capable and `step_execution=False`, request
-mode routes scheduler waves through the batch executor path. Otherwise it keeps
-the per-request executor path.
-
-The optional admission wait runs only when:
-
-- request batching is supported
-- `step_execution=False`
-- `request_batch_max_wait_ms > 0`
-- no requests are currently running
-
-The wait exits early when the waiting queue reaches capacity, when the queue is
-stable for a short window, when the deadline expires, or when the engine stops.
-
-## Executor And Runner
-
-The executor exposes two request-mode entries:
-
-- `execute_request`: one worker call per scheduled request
-- `execute_batch`: one worker call for the whole `DiffusionSchedulerOutput`
-
-On the batch path, the worker builds a `DiffusionRequestBatch` and runs the
-pipeline once. Request-local setup remains per request:
-
-- KV transfer metadata
-- random generator and seed handling
-- request output/error/abort mapping
-
-Shared batch setup happens once per batch when possible:
-
-- cache refresh
-- LoRA activation for the homogeneous adapter key
-- pipeline `forward(req_batch)`
-
-Large tensor IPC still uses the shared-memory packing path. The packer traverses
-both normal `RunnerOutput.result` wrappers and nested batch results so batched
-outputs do not fall back to pickle IPC for tensor payloads.
-
-## Current Limitations
-
-- Only pipelines that declare the request-batch contract use fused batch
- execution.
-- Batches are homogeneous under `SamplingParamsKey`; heterogeneous resolution or
- incompatible guidance settings do not co-batch yet.
-- FIFO scheduling can reduce batching opportunities when an incompatible
- request is at the front of the queue.
-- `request_batch_max_wait_ms` improves burst coalescing but can add latency to
- the first request in a scheduler wave. Keep it small for latency-sensitive
- serving.
-- Step-wise continuous batching is documented separately and only applies when
- `step_execution=True`.
-
-## Related Files
-
-- Request object and request batch:
- [`vllm_omni/diffusion/request.py`](gh-file:vllm_omni/diffusion/request.py)
-- Scheduler interface:
- [`vllm_omni/diffusion/sched/interface.py`](gh-file:vllm_omni/diffusion/sched/interface.py)
-- Scheduler base:
- [`vllm_omni/diffusion/sched/base_scheduler.py`](gh-file:vllm_omni/diffusion/sched/base_scheduler.py)
-- Engine:
- [`vllm_omni/diffusion/diffusion_engine.py`](gh-file:vllm_omni/diffusion/diffusion_engine.py)
-- Worker runner:
- [`vllm_omni/diffusion/worker/diffusion_model_runner.py`](gh-file:vllm_omni/diffusion/worker/diffusion_model_runner.py)
-- Executor interface:
- [`vllm_omni/diffusion/executor/abstract.py`](gh-file:vllm_omni/diffusion/executor/abstract.py)
-- Tests:
- [`tests/diffusion/test_diffusion_engine.py`](gh-file:tests/diffusion/test_diffusion_engine.py)
diff --git a/docs/design/feature/diffusion_step_execution.md b/docs/design/feature/diffusion_step_execution.md
deleted file mode 100644
index 797a1cb48..000000000
--- a/docs/design/feature/diffusion_step_execution.md
+++ /dev/null
@@ -1,131 +0,0 @@
-# Adding Step Execution Support for Diffusion Pipelines
-
-This guide documents vLLM-Omni's stepwise diffusion contract for model authors
-and contributors implementing `step_execution=True` support for a diffusion
-pipeline.
-
-For end-user enablement, supported models, and current limitations, see
-[Step Execution](../../user_guide/diffusion/step_execution.md).
-
-This document describes the base step-execution contract only. For the
-experimental batching policy layered on top of the step-wise path, see
-[Continuous Batching for Step-Wise Diffusion](diffusion_continuous_batching.md).
-
-## Current Support Scope
-
-`step_execution` is **not** a generic diffusion toggle. It only works for
-pipelines that implement the segmented stateful contract in
-[`vllm_omni/diffusion/models/interface.py`](gh-file:vllm_omni/diffusion/models/interface.py).
-
-This page is intentionally author-facing. Treat runtime enablement
-(`step_execution=True` in Python or `--step-execution` in serving) as an
-opt-in user knob layered on top of the implementation contract below.
-
-Current in-tree support:
-
-| Pipeline | Example models | Step execution |
-|----------|----------------|----------------|
-| `QwenImagePipeline` | `Qwen/Qwen-Image`, `Qwen/Qwen-Image-2512` | Yes |
-| All other diffusion pipelines | `QwenImageEditPipeline`, `QwenImageEditPlusPipeline`, `QwenImageLayeredPipeline`, GLM-Image, Wan, Flux, etc. | No |
-
-Current engine/runtime limitations:
-
-- Continuous batching with `max_num_seqs > 1` is experimental and documented in
- [Continuous Batching for Step-Wise Diffusion](diffusion_continuous_batching.md).
- Keep `max_num_seqs=1` if you want the older conservative behavior.
-- `cache_backend` is not supported in step mode.
-- Request-mode extras such as KV transfer are not wired into step mode yet.
-- Unsupported pipelines now fail early during model loading instead of failing on the first request.
-
-## Execution Contract
-
-Step mode is driven by four pipeline methods plus the shared mutable request
-state object:
-
-- `prepare_encode(state)`: one-time request preparation.
-- `denoise_step(state)`: compute the noise prediction for the current step.
-- `step_scheduler(state, noise_pred)`: mutate latents and advance step state.
-- `post_decode(state)`: decode the final output after denoising is complete.
-
-The state lives in
-[`vllm_omni/diffusion/worker/utils.py`](gh-file:vllm_omni/diffusion/worker/utils.py)
-as `DiffusionRequestState`. Store request-scoped tensors there, or use
-`state.extra` for model-specific fields that do not justify extending the core
-dataclass.
-
-The worker-side step loop lives in
-[`vllm_omni/diffusion/worker/diffusion_model_runner.py`](gh-file:vllm_omni/diffusion/worker/diffusion_model_runner.py):
-
-1. `prepare_encode()` runs once for a new request.
-2. `denoise_step()` runs every scheduler tick.
-3. `step_scheduler()` mutates `state.latents` and advances `state.step_index`.
-4. `post_decode()` runs exactly once after `state.denoise_completed` becomes true.
-
-## Recommended Split
-
-When converting an existing request-level `forward()` pipeline, keep the split
-strict and mechanical:
-
-| Request-level phase | Stepwise method | What belongs there |
-|---------------------|-----------------|--------------------|
-| Input validation, prompt encoding, latent init, timestep prep, per-request scheduler creation | `prepare_encode()` | Anything that should happen once per request |
-| Transformer forward / noise prediction | `denoise_step()` | Pure denoise computation for the current timestep |
-| `scheduler.step(...)` and `step_index += 1` | `step_scheduler()` | Only latent/state mutation for one step |
-| VAE decode / postprocess | `post_decode()` | Final decode only |
-
-Keep the stepwise path reusing the same helpers as the request-level path
-whenever possible. Reimplementing the denoise loop from scratch is the easiest
-way to introduce behavioral drift.
-
-## Qwen-Image Reference
-
-[`pipeline_qwen_image.py`](gh-file:vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py)
-is the reference implementation and is split correctly for the current
-contract:
-
-- `prepare_encode()` reuses `_prepare_generation_context()` so prompt encoding,
- latent init, timestep creation, CFG setup, and shape bookkeeping stay aligned
- with `forward()`.
-- `prepare_encode()` deep-copies `self.scheduler` **after**
- `prepare_timesteps()` so request-specific scheduler state is isolated.
-- `denoise_step()` reuses `_build_denoise_kwargs()` plus
- `predict_noise_maybe_with_cfg()`, so sequential CFG, CFG-parallel, and
- non-CFG behavior stay identical to the request-level path.
-- `step_scheduler()` only calls
- `scheduler_step_maybe_with_cfg(..., per_request_scheduler=state.scheduler)`
- and increments `state.step_index`.
-- `post_decode()` reuses `_decode_latents()`, so the final image decode matches
- the normal `forward()` path.
-
-That decomposition is the target pattern for future models.
-
-## Rules For New Pipelines
-
-- Do not keep request-scoped scheduler state on `self.scheduler`. Copy it into
- `state.scheduler` during `prepare_encode()`.
-- Do not mutate `state.step_index` inside `denoise_step()`. Only
- `step_scheduler()` should advance the step.
-- Do not decode partial outputs in `denoise_step()` or `step_scheduler()`.
-- If the request-level pipeline has condition latents, masks, or edit-specific
- tensors, store them in `state` or `state.extra`, not in global pipeline
- attributes.
-- Preserve CFG behavior by sharing the same helper path used by `forward()`.
-- Keep `post_decode()` equivalent to the tail of `forward()`.
-
-## Validation Checklist
-
-Before marking a pipeline as `supports_step_execution = True`, verify:
-
-- Stepwise output matches request-level output for the same seed and sampling params.
-- Per-request scheduler state is isolated across concurrent requests.
-- Abort during denoise does not leak cached state.
-- `step_index` reported by `RunnerOutput` matches the scheduler progress.
-- CFG-parallel and non-CFG paths both work if the request-level pipeline supports them.
-
-## Related Files
-
-- Contract: [`vllm_omni/diffusion/models/interface.py`](gh-file:vllm_omni/diffusion/models/interface.py)
-- State: [`vllm_omni/diffusion/worker/utils.py`](gh-file:vllm_omni/diffusion/worker/utils.py)
-- Runner loop: [`vllm_omni/diffusion/worker/diffusion_model_runner.py`](gh-file:vllm_omni/diffusion/worker/diffusion_model_runner.py)
-- Scheduler transport: [`vllm_omni/diffusion/sched/interface.py`](gh-file:vllm_omni/diffusion/sched/interface.py)
-- Reference pipeline: [`vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py`](gh-file:vllm_omni/diffusion/models/qwen_image/pipeline_qwen_image.py)
diff --git a/docs/design/feature/disaggregated_inference.md b/docs/design/feature/disaggregated_inference.md
index 15d9e09df..c31086c0d 100644
--- a/docs/design/feature/disaggregated_inference.md
+++ b/docs/design/feature/disaggregated_inference.md
@@ -66,8 +66,6 @@ runtime:
connectors:
connector_of_shared_memory:
name: SharedMemoryConnector
- extra:
- shm_threshold_bytes: 65536
```
Wire stages to connectors:
diff --git a/docs/design/feature/distributed_layerwise_offload.md b/docs/design/feature/distributed_layerwise_offload.md
new file mode 100644
index 000000000..da735d60b
--- /dev/null
+++ b/docs/design/feature/distributed_layerwise_offload.md
@@ -0,0 +1,156 @@
+# Distributed Layerwise Offload
+
+This document describes distributed layerwise offload (DLO) for diffusion
+models. DLO keeps only a small number of DiT blocks on the accelerator and
+streams the remaining blocks from host memory. The distributed backend can
+either shard those host-side weights across an existing parallel group or keep
+the standard loader's rank-local weights and avoid an additional collective.
+
+For user-facing commands, see the
+[distributed layerwise offloading guide](../../user_guide/diffusion/cpu_offload.md)
+and the [Cosmos3 recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md).
+
+## Status
+
+DLO is implemented for multi-device diffusion execution. The default
+AllGather path is the primary path for DP and SP deployments. The
+`--dlo-no-use-allgather` path is a rank-local compatibility mode: it is useful
+for standard-loader sharding, workstation bring-up, and systems where an
+additional DLO collective is undesirable, but it does not reduce host weight
+storage across ranks.
+
+The compatibility matrix below describes the current implementation. The
+unit-level guards are covered, but not every parallelism combination has a
+full model-and-hardware end-to-end test.
+
+## Design
+
+### DLO consumes the existing parallel topology
+
+DLO does not create a new DP, TP, or SP topology. It reads the configured
+`DiffusionParallelConfig` and attaches offload hooks to the DiT blocks after the
+standard distributed groups have been initialized.
+
+The DLO weight-sharding group is selected as follows:
+
+1. Use the existing DP group when `data_parallel_size > 1`.
+2. When DP is one and SP is greater than one, use the SP group.
+3. Otherwise, run rank-locally without a DLO process group.
+
+TP is deliberately not used as DLO's AllGather group. HSDP has its own
+parameter-sharding lifecycle and is not allowed to be sharded a second time by
+DLO's AllGather path.
+
+### AllGather path
+
+With the default `dlo_use_allgather=True`, each rank stores approximately
+`1 / group_size` of each streamable block in pinned host memory. The next
+block's shard is copied to a device buffer and reconstructed with
+`all_gather_into_tensor` on a communication stream while the current block is
+executing.
+
+```text
+Compute: [Block N] [Block N+1] [Block N+2]
+H2D: [shard N+1] [shard N+2]
+AllGather: [full N+1] [full N+2]
+Buffers: [current slot] [prefetch slot] [current slot]
+```
+
+
+
+The backend uses two shared device buffers, so accelerator weight residency is
+bounded by the largest streamed blocks rather than the complete model.
+
+When DP is greater than one, the engine can process one request per DP rank in
+the same denoising wave. Because AllGather is a collective, all participating
+requests must take the same execution path at every denoising step.
+
+### Rank-local path without DLO AllGather
+
+With `--dlo-no-use-allgather`, DLO forces its internal offload shard size to
+one. The regular model loader remains responsible for preparing each rank's
+weights, including TP-local tensors or HSDP-managed parameters. DLO then
+streams those rank-local tensors block by block using H2D copies only.
+
+This mode means:
+
+- DP still provides independent replicas, but DLO does not shard weights
+ across DP ranks.
+- SP still performs its normal activation/attention collectives, but DLO does
+ not shard weights across SP ranks.
+- TP/HSDP/SP collectives, if configured, are not disabled by this flag; only
+ DLO's additional weight AllGather is disabled.
+- Pure DP deployments keep a full host-side model copy per rank, subject to
+ shared OS page-cache behavior.
+- The scheduler does not require a synchronized DP request wave for DLO.
+
+This path is intentionally not implemented by reusing the AllGather mmap
+loader. It relies on the standard loader so model-specific TP/HSDP transforms
+remain intact.
+
+## Parallelism compatibility
+
+| Parallelism | DLO + AllGather | DLO without AllGather |
+|---|---|---|
+| **DP** | Supported primary path. DLO shards host weights across the DP group and can run DP multi-concurrency. | Supported rank-local path. DP replicas remain independent; no DLO weight sharding or cross-DP DLO collective. |
+| **SP** | Supported in the implementation. With DP=1, DLO uses the SP group for host-weight sharding; SP still shards sequence/activation work. | SP remains active, but DLO keeps standard-loader rank-local weights and adds no SP weight collective. |
+| **TP > 1** | Unsupported in the DLO mmap path. TP-aware loader callbacks are bypassed, so the backend rejects this configuration when it enters that path. | Standard loading is retained and TP-local tensors can be streamed. This is the intended compatibility path, but it still needs broader model and hardware validation. |
+| **HSDP** | Rejected. HSDP has already sharded parameters, so DLO AllGather would double-shard them. | Accepted by configuration. HSDP owns parameter sharding and its own gathers; DLO only stages rank-local parameters. End-to-end coverage is limited. |
+
+### Combined dimensions
+
+- **DP + SP:** DLO uses the DP group for weight sharding when DP is greater
+ than one; SP continues to use its own sequence-parallel group. If DP is one,
+ the SP group becomes DLO's sharding group in AllGather mode.
+- **DP + TP/SP without AllGather:** standard model loading defines the
+ rank-local tensor layout. DLO adds no cross-DP, cross-TP, or cross-SP weight
+ collective.
+- **HSDP + SP:** the general parallel configuration permits HSDP over SP, but
+ DLO must use `--dlo-no-use-allgather`. HSDP remains responsible for weight
+ materialization and synchronization.
+- **HSDP + DP or TP:** rejected independently by the diffusion parallel
+ configuration.
+
+## Request and loading constraints
+
+AllGather DP multi-concurrency requires:
+
+- explicit `num_inference_steps`;
+- the same `num_inference_steps` for all requests in a wave; and
+- identical request arguments that affect the collective execution path.
+
+The no-AllGather path does not impose these DLO-specific synchronized-wave
+requirements.
+
+The mmap loader is used only by the supported DLO+AllGather path when the
+model has a compatible checkpoint layout. Online quantization is incompatible
+with that sharded mmap path; use `--dlo-no-use-allgather` or disable online
+quantization. Models that do not meet the mmap requirements use the regular
+loader path.
+
+## Validation coverage
+
+Current source-level validation includes:
+
+- HSDP + DLO + AllGather rejection;
+- HSDP + DLO without AllGather acceptance at configuration level;
+- TP rejection in the DLO+AllGather mmap path;
+- resident-layer requests requiring no-AllGather;
+- DP request-wave validation for denoising-step compatibility;
+- sharding, double-buffer, AllGather-size, and heterogeneous-block regression
+ tests.
+
+The highest-value missing coverage is end-to-end numerical comparison against
+ordinary layerwise offload for DP+SP, TP+no-AllGather, and HSDP+SP+no-AllGather
+on the target CUDA/NCCL or CANN/HCCL hardware.
+
+## Recommendations
+
+- Use **DP + DLO AllGather** for the supported throughput and host-memory
+ scaling path.
+- Use **SP + DLO AllGather** for long-sequence workloads when DP concurrency is
+ not the goal.
+- Use **no-AllGather** to bring up TP or workstation PCIe configurations, with
+ the expectation of higher host-memory use and lower validation confidence.
+- Prefer **HSDP alone** for production HSDP deployments until the combined
+ HSDP + DLO no-AllGather path has broader end-to-end coverage.
diff --git a/docs/design/feature/omni_async_output_materialization.md b/docs/design/feature/omni_async_output_materialization.md
new file mode 100644
index 000000000..50a2e7c52
--- /dev/null
+++ b/docs/design/feature/omni_async_output_materialization.md
@@ -0,0 +1,386 @@
+# Async Omni Output Materialization
+
+## Table of Contents
+
+1. [Overview](#overview)
+2. [Performance](#performance)
+3. [Architecture](#architecture)
+4. [Enabling the Feature](#enabling-the-feature)
+5. [Compatibility and Fallbacks](#compatibility-and-fallbacks)
+6. [Related Files](#related-files)
+
+## Overview
+
+Async Omni output materialization moves CPU-side construction of
+`OmniModelRunnerOutput` out of the autoregressive (AR) decode critical path.
+It complements [Async Chunk](async_chunk.md): async chunk pipelines partial
+outputs between stages, while async output materialization prevents the
+construction of those partial outputs from blocking the next decode step.
+
+An AR stage must return sampled token IDs quickly because the scheduler needs
+them for the next decode step. However, an Omni stage can also produce hidden
+states, multimodal tensors, streaming inter-stage payloads, and connector
+metadata. Building these payloads can require device-to-host (D2H) copies,
+tensor slicing, flattening, and Python object construction.
+
+Without this feature, all of that work runs inline in `sample_tokens()`:
+
+```text
+sample tokens
+ -> update decode state
+ -> copy hidden and multimodal outputs to CPU
+ -> build per-request payloads
+ -> build the streaming wire payload
+ -> collect connector signals
+ -> return sampled tokens
+ -> launch the next decode step
+```
+
+With async output materialization, only work required by the next decode step
+stays on the critical path:
+
+```text
+sample tokens
+ -> update decode state
+ -> snapshot output state and start asynchronous D2H copies
+ -> register sampled tokens
+ -> launch the next decode step
+
+background output path
+ -> wait for the payload snapshot
+ -> build per-request payloads
+ -> build the streaming wire payload
+ -> collect connector signals
+ -> construct OmniModelRunnerOutput
+```
+
+This allows payload construction for step `N` to overlap with GPU decode work
+for step `N + 1`. It does not change model computation or generated output.
+
+!!! note "Platform and validation scope"
+ The performance and correctness validation in this document covers CUDA
+ and ROCm. This is a validation scope, not a runtime platform guard: XPU
+ inherits `GPUARModelRunner`, and MUSA selects `GPUARWorker`, so those
+ backends may enter the asynchronous path when its other guards pass. XPU
+ and MUSA have not been validated for this optimization. Ascend NPU uses a
+ separate `NPUARModelRunner` path that still constructs
+ `OmniModelRunnerOutput` and drains connector output synchronously before
+ wrapping the result as an asynchronous runner output.
+
+
+
+
+
+
+
+ Figure 1: Moving payload construction off the decode path reduces the
+ observed gap between consecutive Talker steps from about 2.8 ms to 41 µs.
+ Adapted from the
+
+ Qwen3-Omni optimization blog
+ .
+
+
+
+For Qwen3-Omni, the feature is enabled for both AR stages:
+
+- **Thinker**: Defers hidden-state and multimodal payload materialization.
+- **Talker**: Runs its decode-state postprocess eagerly, then defers codec
+ payload materialization. Because Code2Wav consumes codec codes rather than
+ Talker hidden states, the Talker also skips unnecessary hidden-state D2H.
+- **Code2Wav**: Does not use this path because it is a generation stage rather
+ than an AR stage.
+
+Qwen3-TTS uses the same mechanism in its AR Talker stage. The Talker updates
+the decode state it needs for the next step before returning, then constructs
+the codec payload for Code2Wav in the background.
+
+## Performance
+
+In the controlled Qwen3-Omni optimization sweep, enabling async output
+materialization on top of CUDA Graph and async chunk produced the following
+results at concurrency 64:
+
+| Configuration | Request throughput | Mean audio TTFP | Mean audio RTF |
+|---|---:|---:|---:|
+| CUDA Graph + async chunk | 9.3 req/s | 655 ms | 0.63 |
+| + async output materialization | 11.3 req/s | 631 ms | 0.47 |
+| Change | **+22%** | **-4%** | **-25%** |
+
+The sweep used `Qwen/Qwen3-Omni-30B-A3B-Instruct`, 640 Seed-TTS English
+prompts, and one replica each for Thinker, Talker, and Code2Wav. Async output
+materialization recovers throughput by removing CPU payload construction from
+the decode critical path without regressing time to first audio packet (TTFP).
+See the
+[Qwen3-Omni optimization blog](https://vllm.ai/blog/2026-07-01-qwen3-omni-optimization)
+for the complete benchmark methodology and results.
+
+The implementation was also validated against the synchronous safe-copy path.
+The text and audio output hashes matched, confirming that overlap does not
+change the generated result. See
+[PR #4476](https://github.com/vllm-project/vllm-omni/pull/4476) for the
+correctness and profiling details.
+
+## Architecture
+
+### Output Lifecycle
+
+`OmniAsyncGPUModelRunnerOutput` extends vLLM's
+`AsyncGPUModelRunnerOutput`. It preserves the existing asynchronous
+sampled-token feedback while adding a background builder for the complete Omni
+output.
+
+The output lifecycle is:
+
+1. `GPUARModelRunner.sample_tokens()` samples tokens and performs the
+ bookkeeping required by the next decode step.
+2. The runner snapshots step-local metadata such as request IDs, token spans,
+ scheduler output, and query start locations.
+3. CUDA payload tensors are cloned before reusable model or CUDA Graph buffers
+ can be overwritten.
+4. The cloned payload is copied to pinned CPU memory on a dedicated CUDA
+ stream. A CUDA event records when the copy is ready.
+5. `OmniAsyncGPUModelRunnerOutput` starts a background thread that waits for
+ the payload event and builds `OmniModelRunnerOutput`.
+6. The AR runner immediately registers the sampled-token CPU copy with the
+ input batch and returns, allowing async scheduling to advance.
+7. When the engine calls `get_output()`, it joins the background builder,
+ propagates any builder exception, and finalizes sampled tokens and logprobs
+ through the upstream output implementation.
+
+```text
+GPU / decode path Background output path
+
+forward + sample
+ |
+ +-- clone output tensors
+ +-- enqueue D2H copy --------------> wait for D2H event
+ +-- register sampled tokens |
+ +-- return async output +-- slice per request
+ | +-- build multimodal payloads
+next decode step +-- partition streaming payloads
+ +-- build the wire payload
+ +-- drain connector signals
+ +-- build OmniModelRunnerOutput
+```
+
+### Safe State Snapshots
+
+The next scheduler step can mutate runner state while the background builder is
+still active. Most step-local builder inputs are therefore detached snapshots.
+The snapshotted state includes:
+
+- scheduler token counts and speculative-token metadata
+- request IDs and request-to-batch-index mappings
+- sampled token IDs, logprobs, and prompt logprobs
+- query start locations and scheduled-token spans
+- hidden states and multimodal output tensors
+- KV-connector and encoder-cache outputs captured for the step
+
+CUDA tensor cloning is required because CUDA Graph and model output buffers can
+be reused by the next forward pass. The dedicated copy stream and pinned host
+buffers make the D2H transfer asynchronous; the snapshot retains its cloned
+CUDA sources until the transfer event completes.
+
+### Live Runner State and Ownership
+
+The complete builder input is not a detached snapshot. Connector output state
+remains runner-owned: `get_omni_connector_output()` drains live, per-cycle
+connector signals after the streaming payload is built. Correctness therefore
+depends on that builder being the only consumer that drains connector signals
+for its output cycle. Receive-side connector state written by the background
+receiver is coordinated by the connector mixin's `_lock`.
+`OmniAsyncGPUModelRunnerOutput.get_output()` joins the builder and is the
+completion and exception boundary before the materialized output is consumed.
+
+The shared output builder also contains a full-payload accumulation branch
+that reads `self.requests`. That branch is not reachable when async Omni output
+materialization is enabled: this feature requires `async_chunk`, while
+`should_accumulate_full_payload_output()` returns `False` whenever
+`async_chunk` is enabled. The feature path partitions each step's payload into
+streaming inter-stage and client handoffs instead.
+
+### Output Builder
+
+The background builder performs the work that previously ran inline:
+
+- resolves which requests need downstream payloads
+- converts hidden and multimodal tensors into per-request CPU payloads
+- applies model-specific payload processing
+- partitions per-step payloads into inter-stage and client streams
+- creates the tensor-only `multimodal_outputs` wire payload
+- drains live connector readiness signals after building the streaming payload
+- constructs the final `OmniModelRunnerOutput`
+
+Input-side connector operations remain synchronous with model execution. In
+particular, receiving inputs and flushing previously completed connector
+outputs are not deferred because they affect scheduler-visible request state.
+
+### Qwen3-Omni Stage Behavior
+
+| Stage | Async output behavior | Reason |
+|---|---|---|
+| Thinker | Snapshots hidden states and multimodal outputs; builds the downstream payload in the background | The Talker needs the payload, but the next Thinker decode step only needs sampled-token feedback |
+| Talker | Runs lightweight postprocess eagerly; snapshots codec outputs; omits hidden states from the downstream payload | `hidden_states.last` is needed by the next Talker step, while Code2Wav only needs codec codes |
+| Code2Wav | Uses the normal generation-stage output path | Code2Wav is not executed by `GPUARModelRunner` |
+
+The Qwen3-TTS Talker follows the same pattern as the Qwen3-Omni Talker:
+postprocess runs eagerly because the next decode step needs its updated state,
+while codec payload construction runs asynchronously. Its Code2Wav stage also
+uses the normal generation-stage path.
+
+## Enabling the Feature
+
+There is no separate `async_omni_output` command-line or YAML option. The
+feature is selected automatically for model stages that opt in when the runtime
+conditions are safe. On the validated CUDA and ROCm configurations, using the
+bundled deployment profile is enough to turn it on for the supported
+Qwen3-Omni and Qwen3-TTS pipelines. The selection logic has no CUDA/ROCm-only
+guard; see [Compatibility and Fallbacks](#compatibility-and-fallbacks) for the
+other platform paths. The same profile does not enable background Omni
+materialization on Ascend NPU.
+
+### Example 1: Qwen3-Omni
+
+Start the full Qwen3-Omni pipeline:
+
+```bash
+vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct \
+ --omni \
+ --port 8091
+```
+
+The model registry automatically loads
+`vllm_omni/deploy/qwen3_omni_moe.yaml`. Its relevant settings are:
+
+```yaml
+async_chunk: true
+
+stages:
+ - stage_id: 0 # Thinker
+ enable_prefix_caching: false
+ # async_scheduling defaults to true for AR stages
+
+ - stage_id: 1 # Talker
+ enable_prefix_caching: false
+ # async_scheduling defaults to true for AR stages
+
+ - stage_id: 2 # Code2Wav
+ enable_prefix_caching: false
+ async_scheduling: false
+```
+
+With this profile, async output materialization activates automatically for:
+
+- **Stage 0, Thinker**: Defers hidden-state and multimodal payload
+ construction.
+- **Stage 1, Talker**: Defers codec payload construction after eagerly
+ updating its decode state.
+
+Stage 2 does not use this feature because Code2Wav is not an AR stage.
+
+### Example 2: Qwen3-TTS
+
+Start Qwen3-TTS with any checkpoint that uses the Qwen3-TTS Talker pipeline.
+For example:
+
+```bash
+vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
+ --omni \
+ --port 8091
+```
+
+The model registry automatically loads `vllm_omni/deploy/qwen3_tts.yaml`. Its
+relevant settings are:
+
+```yaml
+async_chunk: true
+
+stages:
+ - stage_id: 0 # Talker
+ async_scheduling: true
+ enable_prefix_caching: false
+
+ - stage_id: 1 # Code2Wav
+ enable_prefix_caching: false
+```
+
+Async output materialization activates only for Stage 0, the AR Talker. The
+Talker keeps the state required by its next decode step on the GPU, skips an
+unnecessary hidden-state D2H copy, and builds the codec payload for Code2Wav in
+the background. Stage 1 uses the generation-stage output path.
+
+The same behavior applies to the Base and VoiceDesign Qwen3-TTS checkpoints
+because they use the same Talker implementation.
+
+!!! warning
+ Do not pass `--no-async-chunk` or enable prefix caching when you want this
+ optimization. Either change causes the runner to fall back to synchronous
+ output construction. Enabling async chunk alone cannot activate the feature
+ for a model that has not opted into async Omni output.
+
+!!! note "Prefix cache compatibility"
+ Async Omni output materialization and Omni prefix caching cannot currently
+ run together. `_should_use_async_omni_output()` returns `False` whenever
+ `self.omni_prefix_cache` is present, so prefix caching selects synchronous
+ output materialization. Supporting both features requires snapshotting or
+ otherwise synchronizing prefix-cache merge and update state before the
+ background output path can consume it safely.
+
+## Compatibility and Fallbacks
+
+`GPUARModelRunner` uses the async output path only when all of the following
+runtime conditions hold:
+
+| Requirement | Reason |
+|---|---|
+| AR async scheduling is enabled | The optimization relies on the scheduler advancing while the prior output is materialized |
+| `async_chunk` is enabled | The feature targets incremental downstream Omni payloads |
+| The model stage opts in with `use_async_omni_output` | Models must declare that their output lifecycle is safe to defer |
+| Omni prefix cache is disabled | Prefix-cache merge and update ordering currently requires synchronous materialization |
+| Speculative decoding is disabled | Speculative output state is not included in this deferred path |
+| Routed-expert output is disabled | Routed-expert extraction currently requires the synchronous path |
+| Postprocess is absent or explicitly runs eagerly | State needed by the next decode step must be updated before the runner returns |
+
+These checks are evaluated per stage on runners based on `GPUARModelRunner`.
+An unsupported combination does not prevent serving; it only falls back to
+synchronous output construction for that stage. CUDA and ROCm are the validated
+platforms. XPU inherits `GPUARModelRunner`, and MUSA selects `GPUARWorker`, so
+they may select the async path when these checks pass, but this optimization is
+not yet validated on either backend.
+
+Ascend NPU does not use this selection path. `NPUARModelRunner.sample_tokens()`
+fully constructs `OmniModelRunnerOutput`, calls
+`get_omni_connector_output()`, and only then creates the asynchronous wrapper,
+so Omni payload materialization remains synchronous.
+
+!!! note
+ `use_async_omni_output`,
+ `eager_omni_postprocess_before_async_output`, and
+ `omni_pooler_payload_include_hidden` are model implementation contracts,
+ not user-facing configuration fields.
+
+## Related Files
+
+- `vllm_omni/worker/gpu_ar_model_runner.py`: Async output object, safe tensor
+ snapshots, compatibility guards, and deferred Omni output builder.
+- `vllm_omni/platforms/npu/worker/npu_ar_model_runner.py`: Separate Ascend NPU
+ runner, where Omni output materialization remains synchronous.
+- `vllm_omni/model_executor/models/qwen3_omni/qwen3_omni.py`: Thinker and
+ Talker opt-in behavior.
+- `vllm_omni/deploy/qwen3_omni_moe.yaml`: Default Qwen3-Omni deployment
+ settings.
+- `vllm_omni/model_executor/models/qwen3_tts/qwen3_tts_talker.py`: Qwen3-TTS
+ Talker opt-in and eager postprocess behavior.
+- `vllm_omni/deploy/qwen3_tts.yaml`: Default Qwen3-TTS deployment settings.
+- `tests/worker/test_gpu_ar_model_runner.py`: Snapshot, guard, connector
+ ordering, and background error-propagation tests.
+- [Async Chunk](async_chunk.md): Inter-stage chunking and scheduling design.
+- [Qwen3-Omni optimization blog](https://vllm.ai/blog/2026-07-01-qwen3-omni-optimization):
+ Optimization context and controlled performance results.
+- [PR #4476](https://github.com/vllm-project/vllm-omni/pull/4476): Feature
+ implementation, validation, and review history.
diff --git a/docs/design/feature/omni_connectors/shared_memory_connector.md b/docs/design/feature/omni_connectors/shared_memory_connector.md
index 5b35014f2..62d77cd6d 100644
--- a/docs/design/feature/omni_connectors/shared_memory_connector.md
+++ b/docs/design/feature/omni_connectors/shared_memory_connector.md
@@ -8,9 +8,7 @@ auto-configured when no explicit connector is specified for an edge.
## How It Works
All payloads are serialized and stored in shared memory (`/dev/shm`); the SHM
-segment name is returned in metadata. The configuration exposes a
-`shm_threshold_bytes` field for a future inline-vs-SHM split, but the current
-implementation always uses shared memory regardless of payload size.
+segment name is returned in metadata.
## Configuration
@@ -19,8 +17,6 @@ runtime:
connectors:
connector_of_shared_memory:
name: SharedMemoryConnector
- extra:
- shm_threshold_bytes: 65536
```
## Notes
@@ -166,27 +162,10 @@ This path is mainly for older code paths and is not the preferred mode for the c
### 6. Key Implementation Characteristics
-#### 6.1 Threshold Exists, but the Current Code Always Uses SHM
+#### 6.1 All Payloads Use Shared Memory
-The class keeps a `shm_threshold_bytes` field and still exposes metrics for inline writes. However, the current implementation uses:
-
-```python
-if True:
- ...
-```
-
-inside `put()`, which means the current code path always writes to shared memory.
-
-So the design still suggests a future split between:
-
-- small payloads inline
-- large payloads in shared memory
-
-but the current behavior is effectively:
-
-- all payloads go through shared memory
-
-This should be documented because it affects real runtime behavior.
+`put()` writes every serialized payload to shared memory. The connector has no
+inline-payload path or size threshold.
#### 6.2 Cleanup Is Currently Passive
diff --git a/docs/design/feature/quantization.md b/docs/design/feature/quantization.md
new file mode 100644
index 000000000..37dd5e26b
--- /dev/null
+++ b/docs/design/feature/quantization.md
@@ -0,0 +1,249 @@
+# Quantization
+
+This document describes the quantization architecture in vLLM-Omni and the
+extension points for contributors. For user-facing configuration, supported
+hardware, and method-specific instructions, see the
+[quantization user guide](../../user_guide/quantization/overview.md).
+
+## Goals and scope
+
+The design has four goals:
+
+1. Provide one `quantization_config` entry point for diffusion-only,
+ multi-stage omni/TTS, and multi-stage diffusion models.
+2. Reuse vLLM's quantization registry and `QuantizationConfig` contract where
+ a vLLM backend already provides the required behavior.
+3. Allow different components of one pipeline to use different quantization
+ configurations.
+4. Preserve the distinction between online quantization of a BF16/FP16
+ checkpoint and loading a pre-quantized checkpoint with serialized scales.
+
+This document covers configuration resolution, component routing, model
+integration, backend extension, and validation. Kernel implementations and
+user-facing support matrices are outside its scope.
+
+## Architecture
+
+The quantization path is split across the following layers:
+
+| Layer | Responsibility | Source |
+| --- | --- | --- |
+| Public API | Exposes `build_quant_config`, component routing, and backend registration | `vllm_omni/quantization/__init__.py` |
+| Factory | Normalizes method names, selects Omni overrides or vLLM registry backends, detects ModelOpt configs, and reconciles checkpoint metadata | `vllm_omni/quantization/factory.py` |
+| Component router | Resolves a quantization config from a layer prefix using longest-prefix matching | `vllm_omni/quantization/component_config.py` |
+| Method configs | Implement `QuantizationConfig` and select the platform-specific linear method | `vllm_omni/quantization/*_config.py` |
+| Diffusion configuration | Stores the canonical runtime config and propagates checkpoint configuration | `vllm_omni/diffusion/data.py` |
+| Worker and model integration | Passes the resolved config into vLLM's model configuration and model layers | `vllm_omni/diffusion/worker/diffusion_worker.py` and `vllm_omni/diffusion/models/` |
+
+The runtime contract remains vLLM's `QuantizationConfig`:
+
+```text
+user/CLI or model config
+ |
+ v
+OmniDiffusionConfig.quantization_config
+ |
+ +--> build_quant_config()
+ | |
+ | +--> Omni override
+ | +--> vLLM registry backend
+ | +--> ComponentQuantizationConfig
+ |
+ +--> checkpoint metadata reconciliation
+ |
+ +--> diffusion worker / vLLM config
+ |
+ +--> model layers call get_quant_method(layer, prefix)
+```
+
+The model layer receives either a method implementation or `None`. Returning
+`None` leaves that layer unquantized.
+
+## Configuration resolution
+
+`build_quant_config()` accepts the following forms:
+
+| Input | Resolution |
+| --- | --- |
+| `None` or `"none"` | Disable quantization. |
+| Method string, such as `"fp8"` | Resolve an Omni override first, then the vLLM quantization registry. |
+| Flat dictionary with `method` or `quant_method` | Normalize the method and construct its `QuantizationConfig`. |
+| Per-component dictionary | Construct a `ComponentQuantizationConfig`. |
+| Existing `QuantizationConfig` | Pass through without rebuilding it. |
+
+Method aliases are normalized case-insensitively, with `-` and `_` treated as
+equivalent. `auto-round` and `auto_round` use the Omni INC/AutoRound adapter.
+ModelOpt checkpoint metadata is detected from its method, producer metadata, or
+`quant_algo`, then mapped to the corresponding vLLM ModelOpt configuration.
+
+The factory uses Omni-specific overrides for methods whose diffusion behavior
+or platform dispatch is not provided by the vLLM registry. The current override
+set includes INT8, BitsAndBytes, MXFP8, MXFP4, MXFP4 dual-scale, and INC/
+AutoRound. Other methods, such as FP8, GGUF, and ModelOpt, are resolved through
+vLLM's registry when their configuration is compatible.
+
+## Per-component routing
+
+Multi-stage models do not necessarily want one quantization policy for every
+module. A component map expresses that policy without requiring each pipeline
+to construct separate vLLM configurations:
+
+```python
+quantization_config = {
+ "transformer": {"method": "fp8"},
+ "vae": None,
+ "default": None,
+}
+```
+
+`ComponentQuantizationConfig` applies the following rules:
+
+1. Component keys are interpreted as runtime layer-prefixes.
+2. The longest matching prefix wins.
+3. `None` disables quantization for the matching component.
+4. `default`, when present, handles prefixes with no explicit match.
+5. A matching child config delegates to its normal
+ `get_quant_method(layer, prefix)` implementation.
+
+Prefix matching happens after the model's weight-name mapping. New model
+integrations must verify the actual runtime prefixes, especially when a
+`WeightsMapper` changes checkpoint names. A prefix mismatch can silently route
+a layer to the default configuration, so component-routing tests should cover
+both positive matches and fall-through behavior.
+
+## Online and pre-quantized checkpoints
+
+The same method name can represent two different loading paths:
+
+- **Online**: the source checkpoint contains BF16/FP16 weights; the method
+ creates quantized weights or scales while the model is loaded.
+- **Pre-quantized**: the checkpoint contains quantized weights and the metadata
+ required by the method; the loader consumes those serialized tensors.
+
+Method configuration owns the mode flag, for example
+`is_checkpoint_int8_serialized` or `is_checkpoint_mxfp8_serialized`. The
+configuration's `get_quant_method()` selects the corresponding linear method.
+
+For pipelines with transformer-specific `config.json` files, such as cascade
+models, `resolve_quant_config_from_disk()` reconciles the active configuration
+with each transformer's checkpoint metadata. Its invariants are:
+
+- If no active configuration exists, valid checkpoint metadata can be used for
+ auto-detection.
+- A method mismatch is an error rather than an implicit conversion.
+- Serialized-checkpoint flags rebuild an online configuration into the
+ matching offline configuration.
+- Checkpoint-specific `ignored_layers` are preserved by rebuilding the active
+ configuration when they differ.
+- AutoRound MXFP8 metadata (`data_type="mx_fp"`) selects its offline path.
+
+This prevents a model from interpreting serialized weights with an online
+loader or silently applying the wrong method to one transformer in a
+multi-transformer pipeline.
+
+## Model and layer integration
+
+Diffusion model constructors receive the resolved configuration and pass it to
+quantizable vLLM layers. Each method configuration is responsible for:
+
+- identifying supported layers in `get_quant_method()`;
+- creating the correct parameter shapes and loaders;
+- processing weights after loading when conversion is required; and
+- applying the platform-specific quantized operation at runtime.
+
+Model code must keep precision-sensitive or unsupported modules unquantized.
+The shared `safe_quant_config()` helper is used by model integrations for
+normalization and modulation layers where a generic FP8 configuration would be
+unsafe. Text encoders, audio/vision encoders, VAEs, schedulers, and tokenizers
+are not implicitly quantized just because the main transformer is quantized;
+they require explicit component routing and method support.
+
+Weight quantization and quantized KV cache are separate paths. The former is
+resolved through `quantization_config`; runtime attention/KV-cache quantization
+is configured by the diffusion attention configuration and must be validated
+independently.
+
+## Platform and parallelism boundaries
+
+Platform checks belong in the method implementation rather than in the common
+factory. This keeps configuration parsing portable while allowing CUDA, NPU,
+and XPU methods to select different kernels or reject unsupported execution at
+model construction time. The user guide is the source of truth for the current
+hardware support matrix.
+
+Quantization methods must also respect the weight layout produced by the
+configured parallelism strategy:
+
+- Tensor-parallel layers may receive partitioned weight shapes and loaders.
+- Sequence-parallel execution changes activation movement but does not by
+ itself change the quantization configuration.
+- Data-parallel replicas build their own method state; quantization metadata
+ must be available consistently on every rank.
+- `WeightsMapper` transformations must be applied before matching ignored-layer
+ lists or component prefixes.
+
+Distributed layerwise offload has an additional boundary: its sharded mmap
+AllGather path is not compatible with online quantization because the path
+expects already materialized checkpoint weights. Use the rank-local
+`--dlo-no-use-allgather` path or a compatible pre-quantized checkpoint when
+combining these features. See the [DLO design document](distributed_layerwise_offload.md)
+for the full compatibility matrix.
+
+## Adding a quantization backend
+
+Contributors should follow this sequence:
+
+1. Check whether an existing vLLM backend can provide the required weight
+ format and kernel behavior. Prefer reuse over a duplicate Omni backend.
+2. If Omni-specific behavior is required, implement a `QuantizationConfig` and
+ its `QuantizeMethodBase`/linear method in `vllm_omni/quantization/`.
+3. Implement `get_name()`, supported activation dtypes, minimum capability,
+ checkpoint parsing, ignored-layer handling, and `get_quant_method()`.
+4. Add a lazy factory override in `factory.py`. Keep optional platform
+ dependencies out of package import paths and normalize aliases to one
+ canonical method name.
+5. If the method is pre-quantized, define the checkpoint metadata and the
+ serialized loading path before adding an online path.
+6. Add model-specific component routing only when the model contains stages
+ that must use different policies.
+7. Document user-visible configuration in the quantization user guide and keep
+ implementation rationale here.
+
+The public factory registration hook,
+`register_quantization_override(method, builder)`, is intended for integrations
+that need to register an Omni-compatible builder without changing the vLLM
+registry.
+
+## Validation
+
+Validation should cover the complete path, not only whether a config object can
+be instantiated:
+
+| Area | Required checks |
+| --- | --- |
+| Factory | Strings, aliases, flat dictionaries, invalid inputs, passthrough, and component maps |
+| Method config | Online/offline selection, ignored layers, platform guards, and weight loading |
+| Checkpoint metadata | Auto-detection, method mismatch errors, serialized flags, and per-transformer metadata |
+| Model integration | Correct quantization scope, unquantized components, and parallel weight layouts |
+| Numerical quality | Same-seed BF16 versus quantized output comparison at representative resolutions and step counts |
+| Performance | Peak device memory, generation latency, and any backend-specific throughput metric |
+
+The unit tests live under `tests/diffusion/quantization/`. The trajectory
+similarity tool in
+`vllm_omni/quantization/tools/compare_diffusion_trajectory_similarity.py`
+provides a repeatable quality and memory comparison for diffusion models.
+Method support and feature combinations remain model- and hardware-dependent;
+passing factory tests alone does not establish production support.
+
+## Design invariants
+
+- `quantization_config` is the single runtime source of truth after config
+ normalization.
+- A checkpoint method mismatch fails loudly instead of risking weight
+ corruption.
+- Components without an explicit supported quantization path remain in their
+ original precision.
+- Optional backend dependencies are imported lazily.
+- Method implementations own platform-specific capability checks and kernels.
+- User-facing support claims require model, hardware, numerical, and memory
+ validation in addition to configuration-level tests.
diff --git a/docs/design/feature/ray_based_execution.md b/docs/design/feature/ray_based_execution.md
deleted file mode 100644
index ae10d661f..000000000
--- a/docs/design/feature/ray_based_execution.md
+++ /dev/null
@@ -1,59 +0,0 @@
-# Distributed utils
-
-This directory (vllm_omni/distributed/ray_utils) contains utilities for distributed execution in vllm-omni, supporting both **Ray** and **Multiprocessing** backends.
-## 1. Installation
-```bash
-pip install "ray[default]"
-```
-## 2. Ray Utils
-
-The `ray_utils` module provides helper functions for managing Ray clusters and actors, which is essential for:
-* **Multi-node deployment**: Running pipeline stages across different physical machines.
-* **Resource management**: Efficient GPU/CPU allocation.
-
-### 2.1 Basic Usage
-
-To use the Ray backend, specify `worker_backend="ray"` when initializing the engine.
-
-**Command Line Example:**
-```bash
-vllm serve Qwen/Qwen2.5-Omni-7B \
- --omni \
- --port 8091 \
- --worker-backend ray \
- --ray-address auto
-```
-
-### 2.2 Cluster Setup
-
-**Step 1: Start Head Node**
-Run this on your primary machine:
-```bash
-ray start --head --port=6399
-```
-
-**Step 2: Connect Worker Nodes**
-Run this on each worker machine:
-```bash
-ray start --address=:6399
-```
-
-> **Tip**: For a complete cluster setup script, refer to the vLLM example:
-> [run_cluster.sh](https://github.com/vllm-project/vllm/blob/main/examples/online_serving/run_cluster.sh)
-
-### 2.3 Distributed Connector Support
-
-When running on Ray, the system automatically adapts its communication strategy:
-
-* **Cross-Node**: Recommended to use `MooncakeTransferEngineConnector` (RDMA, fastest) or `MooncakeStoreConnector` (TCP fallback).
-* **Same-Node**: Can still use `SharedMemoryConnector` for efficiency, or Ray's native object store (plasma).
-* **SHM threshold default differs**: when `worker_backend="ray"`, the SharedMemoryConnector default threshold is set to `sys.maxsize`, which forces payloads to go inline (no SHM). Override `shm_threshold_bytes` in the connector config if you want SHM for Ray runs.
-
-### 2.4 Internal Helpers
-
-* **`initialize_ray_cluster`**: Connects to an existing Ray cluster or starts a local one.
-
-## 3. Troubleshooting
-
-* **Connection Issues**: Ensure the Ray head node is accessible and ports (default 6399 in this example) are open.
-* **Version Mismatch**: Ensure all nodes run the same version of Ray and Python.
diff --git a/docs/design/feature/skip_softmax.md b/docs/design/feature/skip_softmax.md
new file mode 100644
index 000000000..8e6375ee8
--- /dev/null
+++ b/docs/design/feature/skip_softmax.md
@@ -0,0 +1,88 @@
+# Skip-Softmax
+
+Skip-Softmax is the sparse-attention mode of the `TRTLLM_ATTN` backend. Usage — the config keys
+and how to pick an operating point — is in
+[Diffusion Attention Backends](../../user_guide/diffusion/attention_backends.md). This page explains
+the algorithm.
+
+## Motivation
+
+In a long attention row (a video DiT can have tens of thousands of keys), the softmax weight
+concentrates on a small fraction of the keys; the rest receive near-zero weight and barely move the
+output. Computing softmax and the value-weighted sum over those keys is wasted work. Skip-Softmax
+detects, per block of keys, when a block cannot matter and skips its softmax and its value
+multiply.
+
+It is approximate: a skipped block still carries a small non-zero contribution, so the mode is
+opt-in and off by default.
+
+## The online-softmax pass
+
+Attention is computed in a single streaming pass over the keys, in tiles of 128. Per query row the
+kernel maintains three running values:
+
+- `m` — the largest score seen so far,
+- `l` — the running denominator `Σ exp(sⱼ − m)`,
+- `O` — the running numerator `Σ exp(sⱼ − m)·vⱼ`,
+
+and returns `O / l` at the end. For each key tile it computes the tile's scores `Q · K_jᵀ · scale`,
+updates `m`, and accumulates that tile's contribution into `l` and `O`. Rescaling `l` and `O` when
+`m` grows keeps the single pass numerically exact.
+
+## The skip test
+
+Once a tile's scores are known, its largest score `tile_max` is compared against the running
+maximum:
+
+```text
+if exp(tile_max − running_max) < threshold:
+ skip this tile # do not compute its softmax weights or its P·V accumulation
+```
+
+`exp(tile_max − running_max)` is an upper bound on the softmax weight any key in the tile can
+receive: if even the tile's best key is far below the current maximum, every key in the tile is
+negligible, and both the softmax (the exponentials) and the `P·V` accumulation for that tile can be
+dropped. The tile's contribution to `l` and `O` is simply skipped.
+
+## What this bounds
+
+Two properties of the test shape the achievable speedup:
+
+- **`Q · K_jᵀ` always runs.** The test needs `tile_max`, which comes from the tile's scores, so the
+ score matmul is never skipped — only the softmax and the `P·V` accumulation are. The score matmul
+ and the value matmul are comparable in cost, so skipping every eligible tile removes at most
+ roughly half the attention work; the kernel-level speedup is bounded well under 2×, not unbounded.
+
+- **The decision is per tile, not per key.** A tile is skipped only when *all* of its keys are
+ collectively negligible; a single important key keeps the whole tile. How many tiles actually
+ qualify depends on the data and rounds down to tile granularity, so `target_sparsity` selects an
+ operating point on a calibrated curve — it is not a promise that a fixed fraction of tiles is
+ skipped.
+
+## The threshold
+
+The per-tile threshold is normalized by sequence length:
+
+```text
+threshold = factor / seqlen
+```
+
+`factor` comes from one of two sources:
+
+- **Calibrated** — `factor = a · exp(b · target_sparsity)`, where `a` and `b` are fit per model (and
+ per expert, for a multi-expert model) so that a given `target_sparsity` lands near that fraction of
+ skipped tiles on the calibration data.
+- **Direct** — `factor = skip_softmax.threshold · seqlen`, the calibration-free path; the user sets
+ the threshold themselves.
+
+## Timestep gating
+
+`disabled_until_timestep = D` keeps the mode off during the early, high-noise denoise steps and
+turns it on once the normalized timestep `t` drops to `t ≤ D` (`t` runs `1.0` → `0.0` over the
+schedule). The early steps set the global structure of the output and their errors propagate through
+every later step, so keeping them dense costs a few skipped-tile opportunities but protects fidelity.
+
+`t` is the scheduler's own timestep divided by `num_train_timesteps`, published by the pipeline via
+`DenoiseProgressMixin.record_denoise_step`. It is deliberately not derived from the step index,
+because schedulers space their steps non-uniformly. A pipeline that does not publish a timestep
+stays dense when `disabled_until_timestep` is set.
diff --git a/docs/design/feature/tensor_parallel.md b/docs/design/feature/tensor_parallel.md
index bcafde7e7..a01e2a0e6 100644
--- a/docs/design/feature/tensor_parallel.md
+++ b/docs/design/feature/tensor_parallel.md
@@ -264,7 +264,7 @@ Complete examples in the codebase:
| **Z-Image** | `vllm_omni/diffusion/models/z_image/z_image_transformer.py` | Standard TP | Full implementation with validation |
| **FLUX** | `vllm_omni/diffusion/models/flux/flux_transformer.py` | Dual-stream | Image + text streams |
| **Qwen-Image** | `vllm_omni/diffusion/models/qwen_image/qwen_image_transformer.py` | Standard TP | With RoPE |
-| **TP Tests** | `tests/e2e/offline_inference/test_zimage_parallelism.py` | E2E testing | TP correctness and performance |
+| **TP Tests** | `tests/diffusion/distributed/test_tensor_parallel.py` | E2E testing | TP correctness and performance |
| **Constraint Tests** | `tests/diffusion/models/z_image/test_zimage_tp_constraints.py` | Unit testing | Validation logic |
---
diff --git a/docs/design/feature/vae_parallel.md b/docs/design/feature/vae_parallel.md
index 00f41378a..c465449f8 100644
--- a/docs/design/feature/vae_parallel.md
+++ b/docs/design/feature/vae_parallel.md
@@ -445,6 +445,7 @@ Complete examples in the codebase:
| **Z-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl.py` | ✅ | ❌ |
| **Wan2.2** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_wan.py` | ✅ | ✅ |
| **Qwen-Image** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_qwenimage.py` | ✅ | ❌ |
+| **FLUX.2-dev** | `vllm_omni/diffusion/distributed/autoencoders/autoencoder_kl_flux2.py` | ✅ | ✅ |
---
diff --git a/docs/design/figures/dlo/dlo_pipeline.gif b/docs/design/figures/dlo/dlo_pipeline.gif
new file mode 100644
index 000000000..cc206d03b
Binary files /dev/null and b/docs/design/figures/dlo/dlo_pipeline.gif differ
diff --git a/docs/design/index.md b/docs/design/index.md
index 0a59af4cf..c98af378d 100644
--- a/docs/design/index.md
+++ b/docs/design/index.md
@@ -1,6 +1,9 @@
# Design Documents
-This section contains design documents and architecture specifications for vLLM-Omni.
+This section contains design documents and architecture specifications for
+vLLM-Omni. The sidebar groups documents by the system concern they describe;
+retired module pages are preserved in the legacy archive instead of remaining
+in the active navigation.
## Architecture Documents
@@ -8,22 +11,63 @@ This section contains design documents and architecture specifications for vLLM-
## Feature Design Documents
-- [Disaggregated Inference](feature/disaggregated_inference.md)
-- [Ray-based Execution](feature/ray_based_execution.md)
-- [Adding Step Execution Support for Diffusion Pipelines](feature/diffusion_step_execution.md)
-- [Request-Level Batching for Diffusion](feature/diffusion_request_level_batching.md)
-- [Continuous Batching for Step-Wise Diffusion](feature/diffusion_continuous_batching.md)
+### Runtime and stage execution
-## Infrastructure Design Documents
+- [Disaggregated Inference](feature/disaggregated_inference.md)
+- [Async Chunk](feature/async_chunk.md)
+- [Async Diffusion Output](feature/async_diffusion_output.md)
+- [Async Omni Output Materialization](feature/omni_async_output_materialization.md)
+- [Automatic Prefix Caching in Omni Models](feature/prefix_caching.md)
+
+### Communication
+
+#### OmniConnector implementations
+
+- [Mooncake Store Connector](feature/omni_connectors/mooncake_store_connector.md)
+- [Mooncake Transfer Engine Connector](feature/omni_connectors/mooncake_transfer_engine_connector.md)
+- [Mori Transfer Engine Connector](feature/omni_connectors/mori_transfer_engine_connector.md)
+- [Shared Memory Connector](feature/omni_connectors/shared_memory_connector.md)
+- [Yuanrong Store Connector](feature/omni_connectors/yuanrong_connector.md)
+- [Yuanrong Transfer Engine Connector](feature/omni_connectors/yuanrong_transfer_engine_connector.md)
+
+### Diffusion acceleration
+
+#### Parallelism
+
+- [CFG-Parallel](feature/cfg_parallel.md)
+- [Expert Parallel](feature/expert_parallel.md)
+- [Hybrid Sharded Data Parallel (HSDP)](feature/hsdp.md)
+- [Pipeline Parallel](feature/pipeline_parallel.md)
+- [Sequence Parallel](feature/sequence_parallel.md)
+- [Tensor Parallel](feature/tensor_parallel.md)
+- [VAE Patch Parallelism](feature/vae_parallel.md)
+
+#### Attention Backends
+
+- [Skip-Softmax](feature/skip_softmax.md)
+
+#### Quantization
+
+- [Quantization](feature/quantization.md)
+
+- [Cache-DiT](feature/cache_dit.md)
+- [TeaCache](feature/teacache.md)
+- [Diffusion Continuous Batching](feature/diffusion_continuous_batching.md)
+- [Distributed Layerwise Offload](feature/distributed_layerwise_offload.md)
+
+## Infrastructure and Performance
- [Prometheus Metrics](metrics.md)
+- [Speech Generation Performance Optimizations](qwen3_omni_tts_performance_optimization.md)
## Module Design Documents
-- [Entrypoints](module/entrypoints.md)
+- [Entrypoints and Serving Boundaries](module/entrypoints.md)
- [vLLM-Omni Configuration](module/vllm_omni_config.md)
- [Input, Output, and Modality Contracts](module/input_output_modality_contracts.md)
+- [Error Classification, Propagation, and Rendering](module/error_contracts.md)
- [Engine Orchestration](module/engine_orchestration.md)
+- [Stage Runtime and Replica Lifecycle](module/stage_runtime.md)
- [OmniConnector](module/omni_connector.md)
- [Model Integration](module/model_integration.md)
- [Autoregressive Runtime](module/ar_runtime.md)
@@ -40,3 +84,7 @@ This section contains design documents and architecture specifications for vLLM-
- [Observability](module/observability.md)
- [Profiling](module/profiling.md)
- [Benchmarking](module/benchmarking.md)
+
+The pre-#5137 pages are preserved in the
+[legacy module archive](module/archive/README.md) for historical reference and
+are not active design contracts.
diff --git a/docs/design/metrics.md b/docs/design/metrics.md
index 09f0907df..6e4946f4e 100644
--- a/docs/design/metrics.md
+++ b/docs/design/metrics.md
@@ -148,7 +148,13 @@ Upstream vLLM's `Scheduler.make_stats()` runs on every AR generation step, retur
`OmniSchedulerMixin.make_stats()` (in `vllm_omni/core/sched/omni_scheduler_mixin.py`) throttles stats emission to at most once per second **per scheduler** — i.e. per `(stage, replica)` since each replica owns its own scheduler instance. Between intervals it returns `None`, which the engine core skips serializing. This keeps gauges fresh enough for Prometheus scrapes (typically 15-30s intervals) while eliminating the per-step overhead.
-The orchestrator side does not add its own throttle on top: the per-replica recording loop gates only on `raw_outputs.scheduler_stats is not None` (i.e. this replica's scheduler passed its own 1 Hz gate). A previous global `_last_stats_ts` on the orchestrator was removed because it starved every replica other than the first to emit within each second.
+The orchestrator side does not add its own throttle on top. Scheduler gauges are
+recorded when `raw_outputs.scheduler_stats is not None` (i.e. this replica's
+scheduler passed its own 1 Hz gate), while output-batch request/token metrics
+are recorded from `IterationStats` whenever a non-empty output batch is
+processed. A previous global `_last_stats_ts` on the orchestrator was removed
+because it starved every replica other than the first to emit within each
+second.
## Metric Definitions
diff --git a/docs/design/module/archive/README.md b/docs/design/module/archive/README.md
new file mode 100644
index 000000000..6afc76947
--- /dev/null
+++ b/docs/design/module/archive/README.md
@@ -0,0 +1,18 @@
+# Archived module design pages
+
+This directory preserves module-design pages that predate the structure
+proposed in [issue #5137](https://github.com/vllm-project/vllm-omni/issues/5137).
+They are historical snapshots, not active architectural contracts, and are
+intentionally excluded from the documentation navigation.
+
+| Archived page | Active destination |
+| --- | --- |
+| [AR Module](ar_module.md) | [Autoregressive Runtime](../ar_runtime.md) |
+| [AsyncOmni Architecture](async_omni_architecture.md) | [Engine Orchestration](../engine_orchestration.md) and [Stage Runtime](../stage_runtime.md) |
+| [DiT Module](dit_module.md) | [Diffusion module overview](../diffusion/index.md) |
+| [Entrypoint Module](entrypoint_module.md) | [Entrypoints and Serving Boundaries](../entrypoints.md) |
+
+Do not update these pages with current design guidance. Move still-useful
+material into the active destination and review it against current code and
+tests instead. No compatibility redirect pages are provided at the former
+paths.
diff --git a/docs/design/module/archive/ar_module.md b/docs/design/module/archive/ar_module.md
new file mode 100644
index 000000000..f0085fa42
--- /dev/null
+++ b/docs/design/module/archive/ar_module.md
@@ -0,0 +1,387 @@
+# AutoRegressive (AR) Module
+
+## 1. Overview
+
+The AutoRegressive (AR) module in vLLM-Omni handles autoregressive generation stages, primarily used for text, chain-of-thought(COT), and audio latent tokens generation stages in multi-stage models like Qwen2.5-Omni, Qwen3-Omni, BAGEL, .etc. Unlike some representative non-autoregressive generation stages (e.g., Diffusion), AR stages generate tokens sequentially, one at a time, following the standard transformer decoder pattern.
+
+The AR module of vLLM-Omni extends vLLM's core components to support:
+
+- **Multimodal inputs/outputs**: Processing images, videos, and audio alongside text
+- **Direct embedding transfer**: Passing pre-computed prompt embeddings between pipeline stages via serialized payloads
+- **Additional information flow**: Carrying per-request metadata (tensors, lists) through the pipeline
+- **Hidden state exposure**: Exposing per-request hidden representations for downstream stages
+- **Basic generator support**: Support some basic heterogeneous architecture such as Convolution, LSTM, etc.
+
+As shown in the [end2end example](../../../user_guide/examples/offline_inference/qwen3_omni.md), AR module can be widely applied across multiple stages, generating text tokens in thinker(AR), audio latent tokens in talker(AR) and audio wave in code2wav(Convolution).
+
+## 2. Relationship with vLLM
+
+The AR module builds upon vLLM main framework through inheritance, extending core classes while preserving compatibility with vLLM's scheduling, batching, KV cache management, and execution mechanisms.
+
+### Inheritance Hierarchy
+- Scheduler
+
+```mermaid
+classDiagram
+ class VLLMScheduler {
+ +schedule() SchedulerOutput
+ +update_from_output() EngineCoreOutputs
+ }
+ class OmniARScheduler {
+ +schedule() SchedulerOutput
+ }
+ class OmniGenerationScheduler {
+ +schedule() SchedulerOutput
+ +update_from_output() EngineCoreOutputs
+ }
+ VLLMScheduler <|-- OmniARScheduler
+ VLLMScheduler <|-- OmniGenerationScheduler
+```
+- Worker
+
+```mermaid
+classDiagram
+ class GPUWorker {
+ +init_device()
+ +model_runner
+ }
+ class GPUARWorker {
+ +init_device()
+ }
+ class GPUGenerationWorker {
+ +init_device()
+ }
+ GPUWorker <|-- GPUARWorker
+ GPUWorker <|-- GPUGenerationWorker
+```
+- ModelRunner
+
+```mermaid
+classDiagram
+ class GPUModelRunner {
+ +execute_model()
+ +sample_tokens()
+ }
+ class OmniGPUModelRunner {
+ +_update_states()
+ +_preprocess()
+ +_model_forward()
+ }
+ class GPUARModelRunner {
+ +execute_model()
+ +sample_tokens()
+ }
+ class GPUGenerationModelRunner {
+ +execute_model()
+ }
+ GPUModelRunner <|-- OmniGPUModelRunner
+ OmniGPUModelRunner <|-- GPUARModelRunner
+ OmniGPUModelRunner <|-- GPUGenerationModelRunner
+```
+- InputProcessor/OutputProcessor
+
+```mermaid
+classDiagram
+ class InputProcessor {
+ +process_inputs() EngineCoreRequest
+ }
+
+ class VLLMOutputProcessor {
+ +process_outputs() OutputProcessorOutput
+ }
+ class MultimodalOutputProcessor {
+ +process_outputs() OutputProcessorOutput
+ +_route_and_normalize()
+ }
+ VLLMOutputProcessor <|-- MultimodalOutputProcessor
+```
+
+### Key Extensions
+
+- **Scheduler**: `OmniARScheduler` extends `vllm.v1.core.sched.scheduler.Scheduler` to enrich scheduled requests with omni-specific payloads
+- **Worker**: `GPUARWorker` extends `vllm.v1.worker.gpu_worker.Worker` to initialize AR-specific model runners
+- **ModelRunner**: `GPUARModelRunner` extends `OmniGPUModelRunner` → `vllm.v1.worker.gpu_model_runner.GPUModelRunner` to expose hidden states and handle multimodal outputs
+- **InputProcessor**: Stage-0 uses upstream `vllm.v1.engine.input_processor.InputProcessor`; `AsyncOmniEngine` then restores omni-specific payloads (for example `additional_information` and `prompt_embeds`) when building `OmniEngineCoreRequest`
+- **OutputProcessor**: `MultimodalOutputProcessor` extends `vllm.v1.engine.output_processor.OutputProcessor` to route and accumulate multimodal outputs
+
+## 3. Scheduler Design
+
+The AR module provides two scheduler implementations: one for standard autoregressive generation and one for basic heterogeneous architectures.
+
+### Request Flow
+
+The following diagram illustrates the request flow through the AR module components:
+
+```mermaid
+flowchart TD
+ A[InputProcessor stage-0 in AsyncOmniEngine] -->|EngineCoreRequest then upgraded to OmniEngineCoreRequest| B[OmniARScheduler]
+ B -->|schedule: OmniNewRequestData| C[GPUARWorker]
+ C -->|SchedulerOutput| D[GPUARModelRunner]
+ D -->|execute_model: None| E[Model Forward Pass]
+ E -->|hidden_states, logits| D
+ D -->|sample_tokens: OmniModelRunnerOutput| F[OmniARScheduler]
+ F -->|update_from_output| G[MultimodalOutputProcessor]
+ G -->|RequestOutput| H[Client/Downstream Stage]
+
+ style A fill:#e1f5ff
+ style B fill:#fff4e1
+ style C fill:#e8f5e9
+ style D fill:#f3e5f5
+ style G fill:#fce4ec
+```
+
+The flow follows vLLM's standard pattern: input processing → scheduling → worker execution → output processing, with omni-specific enrichments at each stage.
+
+### OmniARScheduler
+
+`OmniARScheduler` extends the base vLLM scheduler with minimal modifications, focusing on enriching scheduled requests with omni-specific payloads.
+
+#### Modified API: `schedule()`
+
+The scheduler wraps base `NewRequestData` entries with `OmniNewRequestData` to include prompt embeddings and additional information:
+
+```python
+def schedule(self) -> SchedulerOutput:
+ scheduler_output = super().schedule()
+ # Rewrap base NewRequestData entries with OmniNewRequestData
+ new_list = []
+ for nr in scheduler_output.scheduled_new_reqs:
+ request = self.requests.get(nr.req_id)
+ omni_nr = OmniNewRequestData(
+ req_id=nr.req_id,
+ prompt_token_ids=nr.prompt_token_ids,
+ # ... other base fields ...
+ prompt_embeds=getattr(request, "prompt_embeds", None),
+ additional_information=getattr(request, "additional_information", None),
+ )
+ new_list.append(omni_nr)
+ scheduler_output.scheduled_new_reqs = new_list
+ return scheduler_output
+```
+
+The `update_from_output()` method remains unchanged, inheriting standard request lifecycle management from the base scheduler.
+
+### OmniGenerationScheduler
+
+`OmniGenerationScheduler` implements a fast-path scheduling strategy for basic heterogeneous architectures that process all input tokens in a single step.
+
+#### Modified API: `schedule()`
+
+Allocates all input tokens for a request at once (or 1 placeholder if zero), falling back to default scheduling if budget is insufficient:
+
+```python
+def schedule(self) -> SchedulerOutput:
+ # Fast path: allocate all input tokens at once
+ while self.waiting and token_budget > 0:
+ request = self.waiting.peek_request()
+ required_tokens = max(getattr(request, "num_prompt_tokens", 0), 1)
+ if required_tokens > token_budget:
+ break # Fall back to default scheduling
+ # Allocate and schedule...
+```
+
+#### Modified API: `update_from_output()`
+
+Marks requests as finished immediately after one step, since generation models complete in a single forward pass:
+
+```python
+def update_from_output(self, ...) -> dict[int, EngineCoreOutputs]:
+ # ...
+ # Diffusion request: completes in one step
+ request.status = RequestStatus.FINISHED_STOPPED
+ kv_transfer_params = self._free_request(request)
+ # ...
+```
+
+## 4. Worker and ModelRunner Design
+
+### GPUARWorker
+
+`GPUARWorker` initializes the AR-specific model runner while maintaining standard device initialization:
+
+```python
+class GPUARWorker(GPUWorker):
+ def init_device(self):
+ # ... standard device initialization ...
+ self.model_runner = GPUARModelRunner(self.vllm_config, self.device)
+```
+
+### GPUARModelRunner
+
+`GPUARModelRunner` follows vLLM's two-phase execute/sample flow while exposing hidden states and multimodal outputs.
+
+#### Two-Phase Execution
+
+**Phase 1: `execute_model()`** - Runs forward pass and stores state:
+- Computes logits from hidden states
+- Stores `ExecuteModelState` with hidden states, logits, and multimodal outputs
+- Returns `None` to defer sampling
+
+**Phase 2: `sample_tokens()`** - Samples tokens and builds output:
+- Retrieves stored state from `execute_model()`
+- Samples tokens using logits
+- Extracts per-request hidden states and multimodal outputs
+- Builds `OmniModelRunnerOutput` with `pooler_output` containing hidden states
+
+```python
+def sample_tokens(self, grammar_output) -> OmniModelRunnerOutput:
+ # Retrieve stored state
+ hidden_states, multimodal_outputs = self.execute_model_state
+
+ # Sample tokens
+ sampler_output = self._sample(logits, spec_decode_metadata)
+
+ # Extract per-request hidden states
+ pooler_output = []
+ for rid in req_ids:
+ hidden_slice = hidden_states_cpu[start:end]
+ payload = {"hidden": hidden_slice}
+ # Add multimodal outputs if present
+ pooler_output.append(payload)
+
+ return OmniModelRunnerOutput(
+ pooler_output=pooler_output,
+ # ... other fields ...
+ )
+```
+
+### GPUGenerationModelRunner
+
+`GPUGenerationModelRunner` implements a simplified single-phase execution for basic heterogeneous architectures:
+
+- No logits computation or token sampling
+- Direct generation from forward pass in model implementation
+- Returns outputs via `pooler_output` immediately after forward pass
+
+### OmniGPUModelRunner
+
+`OmniGPUModelRunner` provides shared functionality for both AR and Generation runners:
+
+#### Prompt Embeddings Overlay
+
+During prefill, overlays custom `prompt_embeds` from request state onto `inputs_embeds`:
+
+```python
+def _collect_additional_information_for_prefill(self, num_scheduled_tokens_np):
+ for req_index, req_id in enumerate(self.input_batch.req_ids):
+ req_state = self.requests[req_id]
+ pe_cpu = getattr(req_state, "prompt_embeds_cpu", None)
+ # Overlay prompt_embeds for prefill portion
+ if pe_cpu is not None:
+ src = pe_cpu[num_computed_tokens:num_computed_tokens + overlay_len]
+ self.inputs_embeds[start_offset:start_offset + overlay_len].copy_(src)
+```
+
+#### Additional Information Processing
+
+Decodes and manages `additional_information` payloads:
+- Decodes serialized payloads → CPU tensors in request state
+- Passes runtime information to model via `runtime_additional_information` kwarg
+- Processes model-provided updates via `postprocess()` hook
+- Merges updates back into request state
+
+#### M-RoPE Position Initialization
+
+For multimodal models using M-RoPE (e.g., Qwen2-VL), computes position encodings from multimodal feature metadata (image grids, video grids, audio features).
+
+## 5. Input/Output Processing
+
+### Processing Pipeline
+
+The input/output processing pipeline handles serialization, routing, and accumulation of multimodal data:
+
+```mermaid
+sequenceDiagram
+ participant Client
+ participant AsyncOmniEngine
+ participant InputProcessor
+ participant Scheduler
+ participant ModelRunner
+ participant MultimodalOutputProcessor
+ participant Client
+
+ Client->>AsyncOmniEngine: prompt + prompt_embeds + additional_info
+ AsyncOmniEngine->>InputProcessor: process_inputs()
+ InputProcessor->>Scheduler: EngineCoreRequest
+ AsyncOmniEngine->>AsyncOmniEngine: _upgrade_to_omni_request() + serialize_additional_information()
+ Scheduler->>ModelRunner: OmniNewRequestData (with payloads)
+ ModelRunner->>ModelRunner: Decode payloads → CPU tensors
+ ModelRunner->>ModelRunner: Overlay prompt_embeds on inputs_embeds
+ ModelRunner->>ModelRunner: Forward pass with runtime_additional_information
+ ModelRunner->>ModelRunner: Extract hidden states + multimodal outputs
+ ModelRunner->>MultimodalOutputProcessor: OmniModelRunnerOutput (pooler_output)
+ MultimodalOutputProcessor->>MultimodalOutputProcessor: Route by output_type
+ MultimodalOutputProcessor->>MultimodalOutputProcessor: Accumulate tensors in OmniRequestState
+ MultimodalOutputProcessor->>MultimodalOutputProcessor: Consolidate tensor lists
+ MultimodalOutputProcessor->>Client: RequestOutput (with multimodal_output)
+```
+
+### Stage-0 Input Processing
+
+Stage-0 now uses upstream `InputProcessor` directly, and `AsyncOmniEngine` upgrades the request to `OmniEngineCoreRequest` while restoring omni-specific payloads.
+
+```python
+request = self.input_processor.process_inputs(
+ request_id=request_id,
+ prompt=prompt,
+ params=params,
+ supported_tasks=self.supported_tasks,
+)
+request = _upgrade_to_omni_request(request, prompt)
+```
+
+### MultimodalOutputProcessor
+
+`MultimodalOutputProcessor` routes outputs by modality type and accumulates multimodal tensors.
+
+#### Output Routing
+
+Routes `EngineCoreOutput` by `output_type` attribute:
+- `"text"`: Standard text generation path
+- `"image"`, `"audio"`, `"latents"`: Extract from `pooling_output` or `multimodal_outputs`
+- Fallback: Heuristic based on presence of `pooling_output`
+
+#### Tensor Accumulation
+
+`OmniRequestState` accumulates multimodal tensors across multiple steps:
+
+```python
+def add_multimodal_tensor(self, payload, mm_type):
+ # Normalize payload to dict
+ incoming = {mm_type or "hidden": payload}
+
+ # Accumulate: convert tensors to lists for deferred concatenation
+ if isinstance(v, torch.Tensor) and isinstance(existing, torch.Tensor):
+ self.mm_accumulated[k] = [existing, v] # List accumulation
+```
+
+Before final output, consolidates tensor lists via concatenation:
+
+```python
+def _consolidate_multimodal_tensors(self):
+ for k, v in self.mm_accumulated.items():
+ if isinstance(v, list) and isinstance(v[0], torch.Tensor):
+ self.mm_accumulated[k] = torch.cat(v, dim=0) # Concatenate
+```
+
+The consolidated tensors are attached to `RequestOutput.multimodal_output` for consumption by downstream stages or clients.
+
+## 6. Summary
+
+The AR module of vLLM-Omni extends vLLM through strategic inheritance and minimal API modifications:
+
+### Key Design Patterns
+
+1. **Inheritance over composition**: Extends vLLM classes to preserve compatibility with existing scheduling, batching, and execution mechanisms
+2. **Payload serialization**: Uses serialized `additional_information` payloads together with prompt-embedding handoff for efficient inter-stage transfer
+3. **Two-phase execution**: Maintains vLLM's execute/sample separation for AR models while supporting single-phase execution for generation models
+4. **Multimodal routing**: Routes outputs by `output_type` and accumulates tensors incrementally to support streaming
+
+### Differences from vLLM
+
+- **Payload support**: Serialized additional information and prompt embeddings enable direct transfer between pipeline stages
+- **Multimodal handling**: Extended input/output processors support images, audio, and other modalities alongside text
+- **Hidden state exposure**: AR model runners expose per-request hidden states via `pooler_output` for downstream consumption
+- **Generation scheduler**: Fast-path scheduling for basic heterogeneous architectures that complete in one step
+
+The AR module seamlessly integrates with vLLM's existing infrastructure while adding the necessary extensions for multi-stage, multimodal generation pipelines.
diff --git a/docs/design/module/archive/async_omni_architecture.md b/docs/design/module/archive/async_omni_architecture.md
new file mode 100644
index 000000000..92b13a3da
--- /dev/null
+++ b/docs/design/module/archive/async_omni_architecture.md
@@ -0,0 +1,203 @@
+# AsyncOmni Architecture (Qwen3-Omni Example)
+
+## 1. System Architecture
+
+```text
+• ┌─────────────────────────────────────────────────────────────────────────────────┐
+ │ API Layer │
+ │ ┌─────────────────────────────────────┐ ┌──────────────────────────────────┐ │
+ │ │ AsyncOmni (EngineClient) │ │ Omni │ │
+ │ │ • generate() / abort() / shutdown() │ │ • generate() │ │
+ │ │ • _final_output_handler() │ │ | │
+ │ └─────────────────────────────────────┘ └──────────────────────────────────┘ │
+ ├─────────────────────────────────────────────────────────────────────────────────┤
+ │ Engine Layer (Proxy) │
+ │ ┌───────────────────────────────────────────────────────────────────────────┐ │
+ │ │ AsyncOmniEngine │ │
+ │ │ • _bootstrap_orchestrator() & _initialize_stages() │ │
+ │ │ • add_request() / add_request_async() -> input_processor.process_inputs() │ │
+ │ │ • try_get_output() / try_get_output_async() │ │
+ │ └───────────────────┬─────────────────────────────────▲─────────────────────┘ │
+ │ request_queue (janus.Queue) output_queue (janus.Queue) │
+ ├──────────────────────┼─────────────────────────────────┼────────────────────────┤
+ │ ▼ Orchestration Layer │ │
+ │ ┌───────────────────────────────────────────────────────────────────────────┐ │
+ │ │ Orchestrator [background thread] │ │
+ │ │ • _request_handler() │ │
+ │ │ - stage_client.add_request_async() & _prewarm_async_chunk_stages() │ │
+ │ │ • _orchestration_output_handler() │ │
+ │ │ - _process_stage_outputs() -> output_processors[i].process_outputs() │ │
+ │ │ - _route_output() & _forward_to_next_stage() │ │
+ │ └──────────┬─────────────────────────┬────────────────────────┬─────────────┘ │
+ ├─────────────┼─────────────────────────┼────────────────────────┼────────────────┤
+ │ │ Communication Layer │ │
+ │ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ │
+ │ │ StageEngineCoreClient │ │ StageEngineCoreClient │ │ StageDiffusionClient │ │
+ │ │ • ZMQ ROUTER / PULL │ │ • ZMQ ROUTER / PULL │ │ • ZMQ ROUTER / PULL │ │
+ │ │ • Msgpack codec │ │ • Msgpack codec │ │ • Msgpack codec │ │
+ │ └──────────┬────────────┘ └──────────┬────────────┘ └──────────┬────────────┘ │
+ │ ▼ ZMQ IPC ▼ ZMQ IPC ▼ ZMQ IPC │
+ ├─────────────────────────────────────────────────────────────────────────────────┤
+ │ Execution Layer │
+ │ ┌───────────────────────┐ ┌───────────────────────┐ ┌───────────────────────┐ │
+ │ │ StageCoreProc │ │ StageCoreProc │ │ DiffusionEngine │ │
+ │ │ [background process] │ │ [background process] │ │ [background process] │ │
+ │ └───────────────────────┘ └───────────────────────┘ └───────────────────────┘ │
+ └─────────────────────────────────────────────────────────────────────────────────┘
+```
+
+## 2. Execution Flow (Arrow Steps, one generate request)
+
+```text
+[1] App
+ -> AsyncOmni.generate(prompt, request_id)
+
+[2] AsyncOmni
+ -> _final_output_handler() (started on first request)
+ -> AsyncOmniEngine.add_request(stage_id=0, ...)
+
+[3] AsyncOmniEngine.add_request
+ -> (if stage-0 is llm and input is not EngineCoreRequest)
+ InputProcessor.process_inputs()
+ OutputProcessor[0].add_request()
+ -> request_queue.put(add_request_msg)
+
+[4] Orchestrator._request_handler
+ -> _handle_add_request(msg)
+ -> stage_clients[0].add_request_async(...)
+
+[5] Orchestrator._orchestration_loop (loop)
+ -> poll stage output
+ - llm stage: await get_output_async()
+ - diffusion stage: get_diffusion_output_nowait()
+ -> (llm stage) output_processors[i].process_outputs(...)
+ -> _route_output(...)
+ -> if finished and not final_stage and non-async-chunk:
+ _forward_to_next_stage(...)
+ -> next_stage.add_request_async(...)
+ -> output_queue.put(output)
+
+[6] AsyncOmni._final_output_loop (background coroutine)
+ -> AsyncOmniEngine.try_get_output_async()
+ -> route by request_id to ClientRequestState.queue
+
+[7] AsyncOmni._process_orchestrator_results
+ -> read from ClientRequestState.queue
+ -> _process_single_result(...)
+ -> yield OmniRequestOutput
+
+[8] Exit condition
+ -> receive result["finished"] == True
+ -> generate() ends
+```
+
+## 3. Runtime Sequence (one generate request)
+
+```mermaid
+sequenceDiagram
+ participant APP as App
+ participant AO as AsyncOmni
+ participant ENG as AsyncOmniEngine
+ participant ORCH as Orchestrator
+ participant S0 as Stage-0 Client
+ participant SN as Next Stage Client
+
+ APP->>AO: generate
+ AO->>AO: start output_handler once
+ AO->>ENG: add_request(stage_id=0, ...)
+ ENG->>ENG: input_processor.process_inputs()
+ ENG->>ORCH: request_queue.put(add_request)
+
+ ORCH->>ORCH: _handle_add_request
+ ORCH->>S0: add_request_async
+
+ loop poll route forward
+ ORCH->>S0: get_output_async / get_diffusion_output_nowait
+ ORCH->>ORCH: _route_output
+ alt need forward to next stage
+ ORCH->>SN: add_request_async
+ end
+ ORCH-->>ENG: output_queue.put
+ end
+
+ AO->>ENG: try_get_output_async
+ ENG-->>AO: message
+ AO-->>APP: yield OmniRequestOutput
+```
+
+## 4. Comparison
+
+Previous topology (reference):
+
+```text
+┌────────────────────────────────────────────────────────────────────────────┐
+│ Main Process │
+│ ┌──────────────────────┐ ┌────────────────────────────────────────────┐ │
+│ │ generate() │ │ final_output_handler() │ │
+│ └──────────────────────┘ └────────────────────────────────────────────┘ │
+└──────────┬─────────────────────────┬─────────────────────────┬─────────────┘
+ mp.Queue (in_q/out_q) mp.Queue (in_q/out_q) mp.Queue (in_q/out_q)
+ ▼▲ ▼▲ ▼▲
+┌───────────────────────┐ ┌───────────────────────┐ ┌──────────────────────┐
+│ Worker Proc-0 │ │ Worker Proc-1 │ │ Worker Proc-2 │
+│ (Thinker LLM) │ │ (Talker LLM) │ │ (Vocoder) │
+│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
+│ │_stage_worker │ │ │ │_stage_worker │ │ │ │_stage_worker │ │
+│ │_async() │ │ │ │_async() │ │ │ │_async() │ │
+│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
+│ ┌────────────────┐ │ │ ┌────────────────┐ │ │ ┌────────────────┐ │
+│ │output_handler()│ │ │ │output_handler()│ │ │ │output_handler()│ │
+│ └────────────────┘ │ │ └────────────────┘ │ │ └────────────────┘ │
+└──────────┬────────────┘ └──────────┬────────────┘ └──────────┬───────────┘
+ ZMQ ▼ ▲ ZMQ ZMQ ▼ ▲ ZMQ ZMQ ▼ ▲ ZMQ
+┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
+│ EngineCore Proc-0 │ │ EngineCore Proc-1 │ │ EngineCore Proc-2 │
+│ (Thinker) │ │ (Talker) │ │ (Vocoder) │
+└──────────────────────┘ └──────────────────────┘ └──────────────────────┘
+```
+
+Current topology:
+
+```text
+┌────────────────────────────────────────────────────────────────────────────┐
+│ Main Process │
+│ ┌──────────────────────────────────────────────────────────────────────┐ │
+│ │ Main Thread │ │
+│ │ ┌──────────────────────┐ ┌─────────────────────────────────────┐ │ │
+│ │ │ generate() │ │ final_output_handler() │ │ │
+│ │ └──────────────────────┘ └─────────────────────────────────────┘ │ │
+│ └──────────────────────────────────────────────────────────────────────┘ │
+│ janus.Queue (request_queue) ▼ ▲ janus.Queue (output_queue) │
+│ ┌──────────────────────────────────────────────────────────────────────┐ │
+│ │ Orchestrator Thread │ │
+│ │ ┌──────────────────────┐ ┌──────────────────────────────────────┐ │ │
+│ │ │ _request_handler() │ │ _orchestration_output_handler() │ │ │
+│ │ └──────────────────────┘ └──────────────────────────────────────┘ │ │
+│ │ ┌────────────────────────────────────────────────────────────────┐ │ │
+│ │ │ _orchestration_loop(): poll/process/route outputs for all stages│ │ │
+│ │ └────────────────────────────────────────────────────────────────┘ │ │
+│ └───────┬─────────────────────────┬─────────────────────────┬──────────┘ │
+└──────────┬─────────────────────────┬─────────────────────────┬─────────────┘
+ ZMQ ▼ ▲ ZMQ ZMQ ▼ ▲ ZMQ ZMQ ▼ ▲ ZMQ
+ ┌──────────────────────┐ ┌──────────────────────┐ ┌──────────────────────┐
+ │ EngineCore Proc-0 │ │ EngineCore Proc-1 │ │ EngineCore Proc-2 │
+ │ (Thinker) │ │ (Talker) │ │ (Vocoder) │
+ └──────────────────────┘ └──────────────────────┘ └──────────────────────┘
+```
+
+
+Test scripts:
+```bash
+# enter offline inference folder.
+cd examples/offline_inference/qwen2_5_omni
+python end2end.py --output-dir output_audio --query-type use_mixed_modalities
+
+cd ../qwen3_omni
+python end2end.py --output-dir output_audio --query-type text --async-chunk --enable-stats
+
+cd ../bagel
+python end2end.py --prompts "A cute cat"
+
+cd ../text_to_image
+python text_to_image.py --prompt "a cup of coffee on the table" --output output.png
+```
diff --git a/docs/design/module/archive/dit_module.md b/docs/design/module/archive/dit_module.md
new file mode 100644
index 000000000..f505d1e67
--- /dev/null
+++ b/docs/design/module/archive/dit_module.md
@@ -0,0 +1,978 @@
+---
+toc_depth: 4
+---
+
+# Diffusion Module Architecture Design
+
+The vLLM-Omni diffusion module (`vllm_omni/diffusion`) is a high-performance inference engine for diffusion models, designed with a modular architecture that separates concerns across multiple components. It provides efficient execution for non-autoregressive generation tasks such as image and video generation.
+
+This document describes the architecture design of the diffusion module, including the diffusion engine, scheduler, worker, diffusion pipeline, and acceleration components.
+
+
+
+
+---
+
+## 1. Diffusion Engine
+
+**Location**: `vllm_omni/diffusion/diffusion_engine.py`
+
+### Responsibilities
+
+The `DiffusionEngine` is the **orchestrator** of the diffusion inference system. It owns request admission, scheduler/executor coordination, output-stream delivery, cancellation cleanup, and startup warmup.
+
+### Key Components
+
+#### 1.1 Initialization
+
+```python
+class DiffusionEngine:
+ def __init__(
+ self,
+ od_config: OmniDiffusionConfig,
+ scheduler: SchedulerInterface | None = None,
+ ):
+ self.od_config = od_config
+ self._init_process_hooks(od_config)
+ self.execution_mode = self._resolve_execution_mode(od_config)
+ self._init_executor(od_config)
+ self._init_scheduler(od_config, scheduler)
+ self._init_runtime_state()
+ self._init_execute_fn()
+```
+
+**Key Features**:
+
+- **Pre/Post Processing**: Registers model-specific pre-processing and post-processing functions via registry pattern
+
+- **Execution Mode Selection**: Selects one of two execution modes:
+ - `REQUEST_BATCH`: complete requests are scheduled through request-level execution. `max_num_seqs=1` is the serial request path; values above `1` allow compatible requests to use fused `pipeline.forward(batch)` when the pipeline supports it.
+ - `STEP_BATCH`: requests are advanced one denoising step at a time through `prepare_encode()`, `denoise_step()`, `step_scheduler()`, and `post_decode()`. `max_num_seqs` controls the maximum number of compatible active requests in one step wave.
+
+- **Scheduler Selection**: Uses `StepScheduler` for step-batch execution and `RequestScheduler` for request-batch execution. Tests and custom integrations may inject a `SchedulerInterface` instance explicitly.
+
+- **Executor Selection**: Builds the configured `DiffusionExecutor`, then routes scheduled work through `execute_batch` or `execute_step` according to the execution mode. `execute_batch` handles the serial `max_num_seqs=1` request path internally.
+
+- **Factory-Owned Warmup**: `DiffusionEngine.make_engine()` constructs the selected engine backend and then runs startup warmup through `run_startup_warmup()`.
+
+#### 1.2 Execution Modes
+
+```python
+class DiffusionExecutionMode(str, Enum):
+ REQUEST_BATCH = "request_batch"
+ STEP_BATCH = "step_batch"
+```
+
+`streaming_output=True` requires step execution. If streaming is enabled without `step_execution=True`, the engine enables step execution so the output stream can receive intermediate chunks or the final step output.
+
+#### 1.3 Output Stream Lifecycle
+
+```python
+request_id = engine.add_request(request)
+async for output in engine.get_output_stream(request_id):
+ yield output
+```
+
+**Design Decisions**:
+
+- **Unified Stream Semantics**: `step_streaming()` is the public async engine path. Streaming callers forward every yielded output. Non-streaming callers consume the same stream to completion and return only the final output.
+
+- **Queue Delivery**: Each admitted request gets an `asyncio.Queue[DiffusionOutput]`. The busy loop emits chunks/final outputs into that queue.
+
+- **Cancellation Cleanup**: If a client disconnects or cancels stream consumption, `get_output_stream()` removes only the queue. Scheduler terminal state is still finalized when the scheduler reports the request as finished, so request IDs do not remain active indefinitely.
+
+#### 1.4 Request Processing Flow
+
+```python
+async def step_streaming(self, request: OmniDiffusionRequest):
+ # 1. Pre-process requests
+ request = self.pre_process_func(request)
+
+ # 2. Add request and consume the unified output stream
+ async for output in self.async_add_req_and_stream_response(request):
+ # 3. Post-process each delivered output
+ yield self.postprocess_output(request, output, ...)
+```
+
+**Flow**:
+
+1. **Pre-processing**: Applies model-specific transformations
+
+2. **Scheduling**: Delegates request state transitions to the selected scheduler
+
+3. **Execution**: Calls the selected executor function for request, request-batch, or step execution
+
+4. **Output Delivery**: Emits intermediate chunks and final outputs through the request output stream
+
+5. **Post-processing**: Converts raw outputs to final format (e.g., PIL images)
+
+---
+
+## 2. Scheduler
+
+**Location**: `vllm_omni/diffusion/sched/`
+
+### Architecture
+
+The scheduler is a **request-state scheduler**. It owns request lifecycle management and scheduling decisions, while execution stays in `DiffusionEngine` and the executor.
+
+### Key Components
+
+#### 2.1 Request State Model
+
+```python
+class DiffusionRequestStatus(enum.IntEnum):
+ WAITING = ...
+ RUNNING = ...
+ PREEMPTED = ...
+ FINISHED_COMPLETED = ...
+ FINISHED_ABORTED = ...
+ FINISHED_ERROR = ...
+
+@dataclass
+class SchedulerRequestState:
+ request_id: str
+ req: OmniDiffusionRequest
+ sampling_params_key: StepBatchSamplingParamsKey | RequestBatchSamplingParamsKey | None = None
+ status: DiffusionRequestStatus = DiffusionRequestStatus.WAITING
+ error: str | None = None
+```
+
+**Design Features**:
+
+- **Explicit lifecycle**: Requests move through waiting, running, optional preemption, and terminal states.
+
+- **Centralized error handling**: Completion, abort, and error states are all normalized in the scheduler layer.
+
+#### 2.2 Shared Bookkeeping in `BaseScheduler`
+
+```python
+class BaseScheduler:
+ def __init__(self) -> None:
+ self._request_states = {}
+ self._waiting = deque()
+ self._running = []
+ self._finished_req_ids = set()
+ self.max_num_running_reqs = 1
+
+ def add_request(self, request: OmniDiffusionRequest) -> str: ...
+ def schedule(self) -> DiffusionSchedulerOutput: ...
+ def update_from_output(
+ self,
+ sched_output: DiffusionSchedulerOutput,
+ output: BaseRunnerOutput,
+ ) -> set[str]: ...
+```
+
+**Responsibilities**:
+
+- **Lifecycle contract**: Defines how the engine adds requests, triggers one scheduling cycle, and feeds executor results back.
+
+- **Stable boundary**: `DiffusionSchedulerOutput` is the only scheduling result consumed by `DiffusionEngine`.
+
+- **Pluggability**: Different scheduler policies can reuse the same engine integration path.
+
+**Design Features**:
+
+- **Common state storage**: Shared request maps and waiting/running sets live in the base class.
+
+- **Shared cleanup logic**: Duplicate-request checks, finish handling, and state removal are centralized instead of
+ duplicated in each policy.
+
+- **Current constraint boundary**: `BaseScheduler` derives `max_num_running_reqs` from `max_num_seqs`. Request-mode diffusion can use that capacity for compatible independent requests when the configured pipeline declares `supports_request_batch = True`; step-wise diffusion uses the same scheduler capacity for compatible step batches.
+
+#### 2.3 Current `RequestScheduler` Policy
+
+```python
+class RequestScheduler(BaseScheduler):
+ def schedule(self) -> DiffusionSchedulerOutput:
+ # 1. keep existing RUNNING requests in the scheduling result
+ # 2. pull WAITING requests while capacity remains
+ # 3. move newly admitted requests into RUNNING
+```
+
+**Behavior**:
+
+- **FIFO request scheduling**: Waiting requests are promoted in queue order.
+
+- **Compatible request admission**: `RequestScheduler` admits waiting requests while capacity remains and the request's `RequestBatchSamplingParamsKey` is compatible with the active batch. Request-mode execution keeps each logical request independent, while batch-capable pipelines receive the scheduled requests as a runner-side `DiffusionRequestBatch`.
+
+- **Executor result feedback**: `update_from_output()` converts executor output into `FINISHED_COMPLETED` or `FINISHED_ERROR` and returns finished request ids.
+
+#### 2.4 Engine-Driven Execution Loop
+
+```python
+request_id = scheduler.add_request(request)
+while True:
+ sched_output = scheduler.schedule()
+ output = executor.add_req(req)
+ finished_req_ids = scheduler.update_from_output(sched_output, output)
+```
+
+**Design Decisions**:
+
+- **Separation of concerns**: Scheduler manages state and policy; executor handles runtime execution.
+
+- **No scheduler-owned IPC**: Scheduler no longer talks to workers directly.
+
+- **Split concurrency model**: Request-mode diffusion can schedule a static
+ batch of compatible independent requests for one full pipeline forward when
+ the pipeline supports request-level batching; the step-wise path can also
+ admit or remove compatible requests between denoise steps. See
+ [Diffusion Continuous Batching](../../feature/diffusion_continuous_batching.md).
+
+---
+
+## 3. Worker
+
+**Location**: `vllm_omni/diffusion/worker/gpu_worker.py`
+
+### Architecture
+
+Workers are **independent processes** that execute the actual model inference. Each worker runs on a dedicated GPU and participates in distributed inference.
+
+### Key Components
+
+#### 3.1 Worker Process Structure
+
+```python
+class WorkerProc:
+ def __init__(self, od_config, gpu_id, broadcast_handle):
+ # Initialize ZMQ context for IPC
+ self.context = zmq.Context(io_threads=2)
+
+ # Connect to broadcast queue (receive requests)
+ self.mq = MessageQueue.create_from_handle(broadcast_handle, gpu_id)
+
+ # Create result queue (only rank 0)
+ if gpu_id == 0:
+ self.result_mq = MessageQueue(n_reader=1, ...)
+
+ # Initialize GPU worker
+ self.worker = self._create_worker(gpu_id, od_config, ...)
+```
+
+**Initialization Steps**:
+
+1. **IPC Setup**: Creates ZMQ context and message queues
+
+2. **Distributed Environment Setup**: Initializes PyTorch distributed communication
+
+ - For CUDA GPUs: Uses NCCL (fast GPU communication)
+
+ - For NPU: Uses HCCL (Huawei Collective Communications Library)
+
+ - For other devices: Uses appropriate backend (GLOO, MCCL, etc.)
+
+3. **Model Loading**: Loads diffusion pipeline on assigned GPU
+
+4. **Cache Setup**: Enables cache backend if configured.
+
+#### 3.2 GPU Worker
+
+```python
+class DiffusionWorker:
+ def init_device(self):
+ # Set distributed environment variables
+ os.environ["RANK"] = str(rank)
+ os.environ["WORLD_SIZE"] = str(world_size)
+
+ # Select the device and initialize distributed execution
+ self.device = current_omni_platform.get_torch_device(rank)
+ current_omni_platform.set_device(self.device)
+ self.vllm_config = _create_diffusion_worker_vllm_config(
+ self.device, self.od_config
+ )
+ init_distributed_environment(world_size, rank)
+ parallel_config = self.od_config.parallel_config
+ initialize_model_parallel(
+ data_parallel_size=parallel_config.data_parallel_size,
+ cfg_parallel_size=parallel_config.cfg_parallel_size,
+ sequence_parallel_size=parallel_config.sequence_parallel_size,
+ tensor_parallel_size=parallel_config.tensor_parallel_size,
+ pipeline_parallel_size=parallel_config.pipeline_parallel_size,
+ )
+
+ def load_model(self, load_format="default", custom_pipeline_name=None):
+ self.model_runner.load_model(
+ load_format=load_format,
+ custom_pipeline_name=custom_pipeline_name,
+ )
+
+ def init_lora_manager(self):
+ self.lora_manager = DiffusionLoRAManager(
+ pipeline=self.model_runner.pipeline,
+ device=self.device,
+ dtype=self.od_config.dtype,
+ )
+```
+
+**Key Features**:
+
+- **Tensor Parallelism**: Supports multi-GPU tensor parallelism via PyTorch distributed
+
+- **Model Loading**: Uses `DiffusersPipelineLoader` for efficient weight loading
+
+- **Cache Integration**: Enables cache backends (TeaCache, cache-dit, etc.) transparently
+
+#### 3.3 Worker Busy Loop
+
+```python
+def _worker_busy_loop(self):
+ while self._running:
+ # 1. Receive unified message (generation request, RPC request, or shutdown)
+ msg = self.mq.dequeue(timeout=1.0)
+
+ # 2. Route message based on type
+ if isinstance(msg, dict) and msg.get("type") == "rpc":
+ # Handle RPC request
+ result, should_reply = self._execute_rpc(msg)
+ if should_reply:
+ self._return_result(result)
+
+ elif isinstance(msg, dict) and msg.get("type") == "shutdown":
+ # Handle shutdown message
+ self._running = False
+
+ else:
+ # Handle generation request (OmniDiffusionRequest list)
+ output = self.worker.execute_model(msg, self.od_config)
+ self._return_result(output)
+```
+
+**Execution Flow**:
+
+1. **Receive**: Dequeues unified messages from shared memory queue
+
+2. **Route**: Handles different message types (generation, RPC, shutdown)
+
+3. **Execute**: Runs forward pass through pipeline for generation requests
+
+4. **Respond**: Sends results back (rank 0 for generation, specified rank for RPC)
+
+#### 3.4 Model Execution
+
+```python
+@torch.inference_mode()
+def execute_model(self, reqs: list[OmniDiffusionRequest], od_config):
+ req = reqs[0] # TODO: support batching
+
+ # Refresh cache backend if enabled
+ if self.cache_backend is not None and self.cache_backend.is_enabled():
+ self.cache_backend.refresh(self.pipeline, req.num_inference_steps)
+
+ # Set forward context for parallelism
+ with set_forward_context(
+ vllm_config=self.vllm_config,
+ omni_diffusion_config=self.od_config
+ ):
+ output = self.pipeline.forward(req)
+ return output
+```
+
+The model execution leverages multiple parallelism strategies that are transparently applied during the forward pass. The `set_forward_context()` context manager makes parallel group information available throughout the forward pass:
+
+```python
+# Inside transformer layers, parallel groups are accessed via:
+from vllm_omni.diffusion.distributed.parallel_state import (
+ get_sp_group, get_dp_group, get_cfg_group, get_pp_group
+)
+```
+
+**Optimizations**:
+
+- **Cache Refresh**: Clears cache state before each generation for clean state
+
+- **Context Management**: Forward context ensures parallel groups are available during execution
+
+- **Single Request**: Currently processes one request at a time (batching TODO)
+
+---
+
+## 4. Diffusion Pipeline
+
+**Location**: `vllm_omni/diffusion/models/*/pipeline_*.py`
+
+The pipeline is the **model-specific implementation** that orchestrates the diffusion process. Different models (QwenImage, Wan2.2, Z-Image) have their own pipeline implementations.
+
+Most pipeline implementation are referred from `diffusers`. The multi-step diffusion loop is usually the most time-consuming part during the overall inference process, which is defined by the `diffuse` function in the pipeline class. An example is as follows:
+
+```python
+def diffuse(self, ...):
+ for i, t in enumerate(timesteps):
+ # Forward pass for positive prompt
+ transformer_kwargs = {
+ "hidden_states": latents,
+ "timestep": timestep / 1000,
+ "encoder_hidden_states": prompt_embeds,
+ }
+ noise_pred = self.transformer(**transformer_kwargs)[0]
+
+ # Forward pass for negative prompt (CFG)
+ if do_true_cfg:
+ neg_transformer_kwargs = {...}
+ neg_transformer_kwargs["cache_branch"] = "negative"
+ neg_noise_pred = self.transformer(**neg_transformer_kwargs)[0]
+
+ # Combine predictions
+ comb_pred = neg_noise_pred + true_cfg_scale * (noise_pred - neg_noise_pred)
+ noise_pred = comb_pred * (cond_norm / noise_norm)
+
+ # Scheduler step
+ latents = self.scheduler.step(noise_pred, t, latents)[0]
+
+ return latents
+```
+
+**Key Features**:
+
+- **CFG Support**: Handles classifier-free guidance with separate forward passes
+
+- **Cache Branching**: Uses `cache_branch` parameter for cache-aware execution
+
+- **True CFG**: Implements advanced CFG with norm preservation
+
+To learn more about the diffusion pipeline and how to add a new diffusion pipeline, please view [Adding Diffusion Model](https://docs.vllm.ai/projects/vllm-omni/en/latest/contributing/model/adding_diffusion_model)
+
+---
+
+## 5. Acceleration Components
+
+### 5.1 Attention Backends
+
+**Location**: `vllm_omni/diffusion/attention/`
+
+#### Architecture
+
+The attention system uses a **role-aware backend selector pattern**. Each `Attention` site declares a semantic `role` (`"self"`, `"cross"`, or a model-specific string). The selector consults the user's `AttentionConfig` to pick a backend per role and falls back to the platform default when nothing is configured.
+
+#### Backend Selection
+
+**Location**: `vllm_omni/diffusion/attention/selector.py`
+
+```python
+class Attention(nn.Module):
+ def __init__(self, num_heads, head_size, causal, softmax_scale, *,
+ role="self", role_category=None, ...):
+ # Resolve backend for this role from the active OmniDiffusionConfig
+ config = get_current_diffusion_config_or_none()
+ attention_config = config.diffusion_attention_config if config is not None else None
+ attn_backend_cls, spec = get_attn_backend_for_role(
+ role=role,
+ head_size=head_size,
+ attention_config=attention_config,
+ role_category=role_category,
+ )
+ self.attn_impl_cls = attn_backend_cls.get_impl_cls()
+ self.attention = self.attn_impl_cls(..., backend_kwargs=spec.backend_kwargs() if spec else None)
+```
+
+**Available Backends**:
+
+- **FlashAttention** (`FLASH_ATTN`): Memory-efficient kernel that dispatches to FA2/FA3 on CUDA, AITER on ROCm, and `mindiesd` on Ascend NPU.
+
+- **SDPA** (`TORCH_SDPA`): PyTorch's scaled dot-product attention — cross-platform fallback.
+
+- **SageAttention** (`SAGE_ATTN`): Quantized attention implementation from the SageAttention library.
+
+These backends provide the **kernel implementations** for attention computation. For attention-level sequence parallelism strategies (Ring Attention, Ulysses), see [Parallel Attention](#52-parallel-attention).
+
+#### Backend Selection Mechanism
+
+Selection is driven by `OmniDiffusionConfig.diffusion_attention_config`, which carries a global `default` plus a `per_role` map of `AttentionSpec` entries. A role string is resolved in this order:
+
+```python
+def get_attn_backend_for_role(role, head_size, attention_config=None, role_category=None):
+ # 1. attention_config.per_role[role] — exact match
+ # 2. attention_config.per_role[role_category] — category fallback
+ # 3. attention_config.default — global default
+ # 4. Platform default — hardware-specific
+ if attention_config is not None:
+ spec, source = attention_config.resolve_with_source(
+ role=role, role_category=role_category,
+ )
+ if spec is not None:
+ return load_backend(spec.backend), spec
+ return current_omni_platform.get_diffusion_attn_backend_cls(...), None
+```
+
+**Selection Priority**:
+
+1. **Per-role override** (`--diffusion-attention-config.per_role..backend`) — finest control; matched on the layer's exact `role` string.
+
+2. **Role category fallback** (`--diffusion-attention-config.per_role..backend`) — used when an exact match is missing and the layer declared `role_category` (e.g. a `"mymodel.audio_to_video"` site can fall back to `"cross"`).
+
+3. **Global default** (`--diffusion-attention-backend FLASH_ATTN`, or `--diffusion-attention-config.default.backend`, or `DIFFUSION_ATTENTION_BACKEND` env var).
+
+4. **Platform default** — `current_omni_platform.get_diffusion_attn_backend_cls(...)` picks the best-available kernel for the hardware.
+
+A backend that needs configuration exposes it as a typed field on `AttentionSpec`; `AttentionSpec.backend_kwargs()` serializes it into the `backend_kwargs` the impl consumes. Today those fields are `skip_softmax` (TRTLLM_ATTN) and `block_sparse` (any block-sparse backend, currently RAINFUSION_ATTN), so e.g. `target_sparsity` or `sparsity` can be set without changing model code.
+
+For the user-facing CLI surface, see [Diffusion Attention Backends](../../../user_guide/diffusion/attention_backends.md). For the role declaration contract on the model side, see [Adding a Diffusion Model](../../../contributing/model/adding_diffusion_model.md).
+
+**Backend Availability**:
+
+- **SDPA**: Always available (PyTorch built-in)
+
+- **FlashAttention**: Requires `flash-attn` on CUDA, `aiter` on ROCm, or `mindiesd` on Ascend NPU
+
+- **SageAttention**: Requires `sage-attention` package (from THU-ML GitHub)
+
+#### Attention Backend Registry
+
+**Location**: `vllm_omni/diffusion/attention/selector.py` + `vllm_omni/platforms//platform.py`
+
+Backend resolution is delegated to the active platform: `current_omni_platform.get_diffusion_attn_backend_cls(selected_backend, head_size)` returns a fully-qualified class path that `_load_backend_cls` imports lazily, with the result cached by `(backend_name, head_size)`.
+
+```python
+@cache
+def _cached_get_backend_cls(backend_name: str | None, head_size: int) -> type[AttentionBackend]:
+ backend_cls_path = current_omni_platform.get_diffusion_attn_backend_cls(
+ selected_backend=backend_name,
+ head_size=head_size,
+ )
+ return _load_backend_cls(backend_cls_path)
+```
+
+Each platform (`cuda`, `rocm`, `xpu`, `musa`, `npu`) maps backend names like `"FLASH_ATTN"`, `"TORCH_SDPA"`, `"SAGE_ATTN"` to the right backend class path for that hardware, including head-size compatibility checks. Passing `selected_backend=None` lets the platform pick its own default.
+
+#### Attention Backend Integration
+
+The `Attention` layer integrates backends through a unified interface. Here's how **FlashAttentionBackend** is integrated as an example:
+
+```python
+# attention/backends/flash_attn.py
+
+class FlashAttentionBackend(AttentionBackend):
+ @staticmethod
+ def get_name() -> str:
+ return "FLASH_ATTN"
+
+ @staticmethod
+ def get_impl_cls() -> type["FlashAttentionImpl"]:
+ return FlashAttentionImpl
+
+ @staticmethod
+ def get_supported_head_sizes() -> list[int]:
+ return [64, 96, 128, 192, 256] # FlashAttention supports these head sizes
+
+
+class FlashAttentionImpl(AttentionImpl):
+ def __init__(self, num_heads, head_size, softmax_scale, causal, ...):
+ self.num_heads = num_heads
+ self.causal = causal
+ self.softmax_scale = softmax_scale
+
+ def forward(self, query, key, value, attn_metadata=None):
+ # Call FlashAttention kernel
+ out = flash_attn_func(
+ query, key, value,
+ causal=self.causal,
+ softmax_scale=self.softmax_scale,
+ )
+ return out
+```
+
+---
+
+### 5.2 Parallel Attention
+
+**Location**: `vllm_omni/diffusion/attention/parallel/`
+
+#### Architecture
+
+Parallel attention strategies implement **Sequence Parallelism (SP) at the attention layer level**. These strategies distribute attention computation across multiple GPUs by splitting the sequence dimension, using different communication patterns. They work **on top of** AttentionBackend implementations (FlashAttention, SDPA, etc.), handling the parallelization/communication while the backends handle the actual attention computation.
+
+**Key Distinction**: Unlike AttentionBackend (which provides kernel implementations), ParallelAttentionStrategy provides communication patterns for multi-GPU attention parallelism. These strategies implement the `ParallelAttentionStrategy` interface and use AttentionBackend implementations internally.
+
+Both Ring Attention and Ulysses are forms of Sequence Parallelism (SP) that:
+
+- Split the sequence dimension across GPUs
+
+- Contribute to `sequence_parallel_size` (via `ring_degree` and `ulysses_degree`)
+
+- Work at the attention layer level (not model/pipeline level)
+
+#### Ulysses Sequence Parallelism (USP)
+
+**Location**: `vllm_omni/diffusion/attention/parallel/ulysses.py`
+
+USP is a sequence-parallel attention strategy that splits attention computation across multiple GPUs by distributing both the sequence dimension and attention heads. It uses **all-to-all communication** to efficiently parallelize attention for very long sequences. Specifically, it uses **all-to-all** collective operations to redistribute Q/K/V tensors before attention computation and gather results afterward.
+
+Ulysses splits attention computation in two dimensions:
+
+1. **Sequence Dimension**: Splits the sequence length across GPUs
+
+2. **Head Dimension**: Splits attention heads across GPUs
+
+**Configuration**: `ulysses_degree` contributes to `sequence_parallel_size`
+
+#### Ring Sequence Parallelism
+
+**Location**: `vllm_omni/diffusion/attention/parallel/ring.py`
+
+Ring Attention is a **parallel attention strategy** that implements sequence parallelism using ring-based point-to-point (P2P) communication. Unlike attention backends that provide the attention kernel implementation, Ring Attention is a **communication pattern** that works on top of attention backends (FlashAttention or SDPA).
+
+Ring Attention splits sequence dimension across GPUs in a ring topology, implemented via the `ParallelAttentionStrategy` interface, instead of `AttentionBackend`. P2P ring communication is applied to circulate Key/Value blocks across GPUs. Internally, `ring_flash_attn_func` or `ring_pytorch_attn_func` is used depending on available backends.
+
+**Architecture**:
+```python
+class RingParallelAttention:
+ """Ring sequence-parallel strategy."""
+
+ def run_attention(self, query, key, value, attn_metadata, ...):
+ # Selects underlying attention kernel (FlashAttention or SDPA)
+ if backend_pref == "sdpa":
+ return ring_pytorch_attn_func(...) # Uses SDPA kernel
+ else:
+ return ring_flash_attn_func(...) # Uses FlashAttention kernel
+```
+
+**Integration**:
+
+- Ring Attention is activated when `ring_degree > 1` in parallel config
+
+- It's selected by `build_parallel_attention_strategy()` in the attention layer
+
+- The `Attention` layer routes to `_run_ring_attention()` when Ring is enabled
+
+- Works alongside attention backends: Ring handles communication, backends handle computation
+
+**Configuration**: `ring_degree` contributes to `sequence_parallel_size`
+
+#### Relationship with AttentionBackend
+
+Parallel attention strategies (Ring, Ulysses) work **on top of** AttentionBackend implementations:
+
+- They use AttentionBackend for the actual attention computation (FlashAttention, SDPA, etc.)
+
+- They handle the multi-GPU communication/parallelization layer
+
+- They implement `ParallelAttentionStrategy` interface (not `AttentionBackend`)
+
+For general parallelism strategies (Data Parallelism, Tensor Parallelism, Pipeline Parallelism), see [Parallel Strategies](#54-parallel-strategies).
+
+---
+
+### 5.3 Cache Backends
+
+**Location**: `vllm_omni/diffusion/cache/`
+
+#### Architecture
+
+Cache backends provide a **unified interface** for applying different caching strategies to accelerate diffusion inference. The system supports multiple backends (TeaCache, cache-dit) with a consistent API for enabling and refreshing cache state.
+
+#### Cache Backend Interface
+
+```python
+class CacheBackend(ABC):
+ def __init__(self, config: DiffusionCacheConfig):
+ self.config = config
+ self.enabled = False
+
+ @abstractmethod
+ def enable(self, pipeline: Any) -> None:
+ """Enable cache on the pipeline."""
+ raise NotImplementedError
+
+ @abstractmethod
+ def refresh(self, pipeline: Any, num_inference_steps: int, verbose: bool = True) -> None:
+ """Refresh cache state for new generation."""
+ raise NotImplementedError
+
+ def is_enabled(self) -> bool:
+ """Check if cache is enabled."""
+ return self.enabled
+```
+
+**Design Pattern**:
+
+- **Abstract Base Class**: Defines contract for all cache backends
+
+- **Pipeline-based**: Works with pipeline instances (not just transformers)
+
+- **State Management**: Provides refresh mechanism for clean state between generations
+
+#### Available Backends
+
+**1. TeaCache Backend**
+
+**Location**: `vllm_omni/diffusion/cache/teacache/backend.py`
+
+```python
+class TeaCacheBackend(CacheBackend):
+ def enable(self, pipeline: Any):
+ # Extract transformer from pipeline
+ transformer = pipeline.transformer
+ transformer_type = transformer.__class__.__name__
+
+ # Create TeaCacheConfig from DiffusionCacheConfig
+ teacache_config = TeaCacheConfig(
+ transformer_type=transformer_type,
+ rel_l1_thresh=self.config.rel_l1_thresh,
+ coefficients=self.config.coefficients,
+ )
+
+ # Apply hooks to transformer
+ apply_teacache_hook(transformer, teacache_config)
+ self.enabled = True
+
+ def refresh(self, pipeline: Any, num_inference_steps: int, verbose: bool = True):
+ transformer = pipeline.transformer
+ if hasattr(transformer, "_hook_registry"):
+ transformer._hook_registry.reset_hook(TeaCacheHook._HOOK_NAME)
+```
+
+**TeaCache Features**:
+
+- **Timestep-aware**: Caches based on timestep embedding similarity
+
+- **Adaptive**: Dynamically decides when to reuse cached computations
+
+- **CFG-aware**: Handles positive/negative branches separately
+
+- **Custom Hook System**: Uses a custom forward interception mechanism (via `HookRegistry`) that wraps the module's `forward` method, allowing transparent integration without modifying model code
+
+**2. Cache-DiT Backend**
+
+**Location**: `vllm_omni/diffusion/cache/cachedit/`
+
+```python
+class CacheDiTBackend(CacheBackend):
+ def enable(self, pipeline: Any):
+ # Uses cache-dit library for acceleration
+ # Supports DBCache, SCM (Step Computation Masking), TaylorSeer
+ # Works with single and dual-transformer architectures
+ ...
+ self.enabled = True
+
+ def refresh(self, pipeline: Any, num_inference_steps: int, verbose: bool = True):
+ # Updates cache context with new num_inference_steps
+ ...
+```
+
+**Cache-DiT Features**:
+
+- **DBCache**: Dynamic block caching with configurable compute blocks
+
+- **SCM**: Step Computation Masking for additional speedup
+
+- **TaylorSeer**: Advanced calibration for cache accuracy
+
+- **Dual-transformer Support**: Handles models like Wan2.2 with two transformers
+
+#### Cache Backend Selector
+
+**Location**: `vllm_omni/diffusion/cache/selector.py`
+
+```python
+def get_cache_backend(
+ cache_backend: str | None,
+ cache_config: dict | DiffusionCacheConfig
+) -> CacheBackend | None:
+ """Get cache backend instance based on cache_backend string.
+
+ Args:
+ cache_backend: Cache backend name ("cache_dit", "tea_cache", or None)
+ cache_config: Cache configuration (dict or DiffusionCacheConfig)
+
+ Returns:
+ Cache backend instance or None if cache_backend is "none"
+ """
+ if cache_backend is None or cache_backend == "none":
+ return None
+
+ if isinstance(cache_config, dict):
+ cache_config = DiffusionCacheConfig.from_dict(cache_config)
+
+ if cache_backend == "cache_dit":
+ return CacheDiTBackend(cache_config)
+ elif cache_backend == "tea_cache":
+ return TeaCacheBackend(cache_config)
+ else:
+ raise ValueError(f"Unsupported cache backend: {cache_backend}")
+```
+
+**Usage Flow**:
+
+1. **Selection**: `get_cache_backend()` returns appropriate backend instance
+
+2. **Enable**: `backend.enable(pipeline)` called during worker initialization
+
+3. **Refresh**: `backend.refresh(pipeline, num_inference_steps)` called before each generation
+
+4. **Check**: `backend.is_enabled()` verifies cache is active
+
+### 5.4 Parallel Strategies
+
+**Location**: `vllm_omni/diffusion/distributed/parallel_state.py`
+
+#### Parallelism Types
+
+The system supports multiple orthogonal parallelism strategies:
+
+**Sequence Parallelism (SP)**
+
+- **Purpose**: Split sequence dimension across GPUs
+
+- **Attention-level SP**: Ring Attention and Ulysses (USP) implement SP at the attention layer level
+
+ - See [Parallel Attention](#52-parallel-attention) for details
+
+ - Configuration: `ulysses_degree` × `ring_degree` = `sequence_parallel_size`
+
+- **Use Case**: Very long sequences (e.g., high-resolution images)
+
+**Data Parallelism (DP)**
+
+- **Purpose**: Replicate model across GPUs, split batch
+
+- **Use Case**: Batch processing, throughput optimization
+
+**Tensor Parallelism (TP)** (Experimental)
+
+- **Purpose**: Split model weights across GPUs
+
+- **Implementation**: Uses vLLM's tensor parallel groups
+
+- **Use Case**: Large models that don't fit on single GPU
+
+**CFG Parallelism** (under development)
+
+- **Purpose**: Parallelize Classifier-Free Guidance (positive/negative prompts)
+
+- **Infrastructure**: CFG parallel groups are initialized and available via `get_cfg_group()`
+
+#### Parallel Group Management
+
+```python
+def initialize_model_parallel(
+ data_parallel_size: int = 1,
+ cfg_parallel_size: int = 1,
+ sequence_parallel_size: int | None = None,
+ ulysses_degree: int = 1,
+ ring_degree: int = 1,
+ tensor_parallel_size: int = 1,
+ pipeline_parallel_size: int = 1,
+ vae_parallel_size: int = 0,
+):
+ # Generate orthogonal parallel groups
+ rank_generator = RankGenerator(
+ tensor_parallel_size,
+ sequence_parallel_size,
+ pipeline_parallel_size,
+ cfg_parallel_size,
+ data_parallel_size,
+ "tp-sp-pp-cfg-dp",
+ )
+
+ # Initialize each parallel group
+ _DP = init_model_parallel_group(rank_generator.get_ranks("dp"), ...)
+ _CFG = init_model_parallel_group(rank_generator.get_ranks("cfg"), ...)
+ _SP = init_model_parallel_group(rank_generator.get_ranks("sp"), ...)
+ _PP = init_model_parallel_group(rank_generator.get_ranks("pp"), ...)
+ _TP = init_model_parallel_group(rank_generator.get_ranks("tp"), ...)
+```
+
+**Rank Order**: `tp-sp-pp-cfg-dp` (tensor → sequence → pipeline → cfg → data)
+
+**Note**: For attention-level Sequence Parallelism implementations (Ring Attention and Ulysses), see [Parallel Attention](#52-parallel-attention). This section covers higher-level parallelism strategies.
+
+
+---
+
+## 6. Data Flow
+
+### Complete Request Flow
+
+
+
+
+
+ End-to-end Data Flow in the vLLM-Omni Diffusion Module
+
+
+
+```
+1. User Request
+ └─> Omni.generate(prompt)
+ └─> Prepare OmniDiffusionRequest
+ └─> DiffusionEngine.step_streaming(request)
+
+2. Pre-processing
+ └─> pre_process_func(request)
+ └─> Model-specific transformations
+
+3. Scheduling
+ └─> scheduler.add_request(request)
+ └─> scheduler.schedule()
+ └─> DiffusionEngine selects execute_batch / execute_step
+
+4. Worker Execution
+ └─> WorkerProc._worker_busy_loop()
+ └─> DiffusionWorker.execute_model(req)
+ └─> DiffusionModelRunner selects execution mode
+ ├─> REQUEST_BATCH
+ │ └─> Pipeline.forward(req_or_batch)
+ │ ├─> encode_prompt()
+ │ ├─> prepare_latents()
+ │ ├─> diffuse() [loop]
+ │ │ ├─> transformer.forward() [with cache backend hooks]
+ │ │ └─> scheduler.step()
+ │ └─> vae.decode()
+ └─> STEP
+ ├─> pipeline.prepare_encode(state) [new requests]
+ ├─> pipeline.denoise_step(input_batch)
+ ├─> pipeline.step_scheduler(state, noise_pred)
+ └─> pipeline.post_decode(state) [chunk boundary or final step]
+
+5. Result Collection
+ └─> Executor returns RunnerOutput / BatchRunnerOutput
+ └─> scheduler.update_from_output(...)
+ ├─> DiffusionEngine emits chunk/final output to request queue
+ └─> DiffusionEngine finalizes terminal scheduler state
+
+6. Post-processing
+ └─> post_process_func(output)
+ └─> Convert to PIL images / final format
+
+7. Stream Completion / Cancellation
+ ├─> Streaming callers receive every yielded output
+ ├─> Non-streaming callers drain the stream and return the final output
+ └─> Cancelled streams drop their output queue but still allow scheduler state cleanup
+```
+
+---
diff --git a/docs/design/module/archive/entrypoint_module.md b/docs/design/module/archive/entrypoint_module.md
new file mode 100644
index 000000000..7a26fbb7f
--- /dev/null
+++ b/docs/design/module/archive/entrypoint_module.md
@@ -0,0 +1 @@
+Architecture design of the entrypoint (update soon)
diff --git a/docs/design/module/engine_orchestration.md b/docs/design/module/engine_orchestration.md
index cf8ef1fd0..5bb0ea0df 100644
--- a/docs/design/module/engine_orchestration.md
+++ b/docs/design/module/engine_orchestration.md
@@ -2,26 +2,56 @@
title: Engine Orchestration
kind: module
status: draft
+architecture_state: current-plus-in-flight
owners:
- "@tzhouam"
- - "@yinpeiqi"
- "@fake0fan"
+document_stewards:
+ - "@hsliuustc0106"
+ - "@Gaohan123"
+ - "@david6666666"
+required_reviewers:
+ - "@yinpeiqi"
- "@Sy0307"
+ - "@yenuo26"
+ - "@NickCao"
primary_code_paths:
- - vllm_omni/engine/**
+ - vllm_omni/engine/async_omni_engine.py
+ - vllm_omni/engine/async_engine_utils.py
+ - vllm_omni/engine/orchestrator.py
+ - vllm_omni/engine/cfg_companion_tracker.py
+ - vllm_omni/engine/rpc_result_router.py
related_code_paths:
+ - vllm_omni/engine/messages.py
+ - vllm_omni/engine/orchestrator_monitor.py
+ - vllm_omni/engine/stage_client.py
+ - vllm_omni/engine/stage_pool.py
+ - vllm_omni/outputs/output_processor.py
- vllm_omni/distributed/omni_coordinator/**
- - vllm_omni/distributed/ray_utils/**
depends_on:
- - vllm_omni_config.md
- input_output_modality_contracts.md
+ - error_contracts.md
+ - stage_runtime.md
- omni_connector.md
validation_paths:
- - tests/engine/**
- - tests/distributed/omni_coordinator/**
+ - tests/engine/test_async_omni_engine_input.py
+ - tests/engine/test_async_omni_engine_outputs.py
+ - tests/engine/test_async_omni_engine_abort.py
+ - tests/engine/test_async_omni_engine_stage_init.py
+ - tests/engine/test_orchestrator.py
+ - tests/engine/test_orchestrator_error_handling.py
+ - tests/engine/test_orchestrator_stage_input_bridge.py
+ - tests/engine/test_cfg_companion_lifecycle.py
+ - tests/engine/test_rpc_result_router.py
+ - tests/e2e/features/fullduplex/engine/**
upstream_refs:
- - vllm.v1.engine
-last_reviewed: 2026-07-16
+ - vllm.v1.engine.EngineCoreRequest
+ - vllm.v1.engine.EngineCoreOutputs
+ - vllm.v1.engine.exceptions.EngineDeadError
+ - vllm.outputs.RequestOutput
+invariant_namespace: ORCH-INV
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
---
# Engine orchestration
@@ -29,24 +59,64 @@ last_reviewed: 2026-07-16
Engine orchestration coordinates configured AR and diffusion stages while
keeping public clients independent from stage processes and transports.
+## Contract status
+
+This document is a draft description of current behavior. It also identifies
+the boundary affected by the in-flight stage client/process refactor in
+[#5441](https://github.com/vllm-project/vllm-omni/pull/5441). Names and
+responsibilities proposed only by that PR are not current contracts.
+
+## Ownership boundary
+
+This document owns `AsyncOmniEngine`, `Orchestrator`, request-state creation,
+cross-stage routing, output ordering, companion tracking, control/RPC
+correlation, cancellation propagation, and terminal-state convergence.
+
+It does not own stage placement, replica selection, stage-process startup,
+payload schemas, public protocol rendering, connector transport, or semantic
+error classification. Those responsibilities belong to the stage runtime,
+I/O, entrypoint, connector, and error contracts.
+
## Candidate invariants
+These identifiers are proposals while the document is `draft`.
+
### ORCH-INV-001: The orchestrator owns cross-stage routing
**Rule:** Entrypoints and stage clients MUST NOT independently forward a
-request to a downstream stage.
+request to a downstream logical stage.
### ORCH-INV-002: Stage clients do not own routing policy
**Rule:** Stage clients MUST implement communication and lifecycle operations
without selecting the next logical stage.
-### ORCH-INV-003: Terminal state is monotonic
+### ORCH-INV-100: Terminal state is monotonic
**Rule:** Once a request reaches a terminal state, orchestration MUST NOT
forward new work for that request.
+## Invariant namespace
+
+`ORCH-INV` reserves `001-099` for dependency direction, `100-199` for
+request state and ordering, `200-299` for failure/cancellation/cleanup, and
+`300-399` for extension points, shutdown ordering, and upstream alignment.
+Numbers become append-only after normative promotion.
+
## Safe-change guide
-Test routing, output ordering, cancellation, failure propagation, shutdown, and
-representative multi-stage execution.
+Test routing, output ordering, queue correlation, cancellation, failure
+propagation, shutdown ordering, and representative multi-stage execution.
+Changes that cross into stage runtime or public error behavior require review
+from that contract's owners.
+
+## Promotion gate
+
+- Reconcile current names and boundaries after #5441 merges, closes, or is
+ replaced.
+- Demonstrate one terminal outcome per request across success, abort,
+ request-scoped failure, and fatal engine failure.
+- Verify queue correlation and orchestrator-before-runtime shutdown ordering
+ in the cited validation paths.
+- Obtain approval from a technical owner and an independent validation
+ reviewer.
diff --git a/docs/design/module/entrypoints.md b/docs/design/module/entrypoints.md
index 8d4da8434..80b576f96 100644
--- a/docs/design/module/entrypoints.md
+++ b/docs/design/module/entrypoints.md
@@ -1,51 +1,134 @@
---
-title: Entrypoints
+title: Entrypoints and Serving Boundaries
kind: module
status: draft
+architecture_state: current-plus-in-flight
owners:
- - "@tzhouam"
- - "@yinpeiqi"
- - "@fake0fan"
- "@alex-jw-brooks"
+ - "@linyueqian"
+document_stewards:
+ - "@hsliuustc0106"
+ - "@Gaohan123"
+ - "@david6666666"
+required_reviewers:
+ - "@tzhouam"
+ - "@herotai214"
+ - "@yenuo26"
+ - "@NickCao"
primary_code_paths:
- - vllm_omni/entrypoints/**
+ - vllm_omni/entrypoints/omni.py
+ - vllm_omni/entrypoints/async_omni.py
+ - vllm_omni/entrypoints/omni_base.py
+ - vllm_omni/entrypoints/cli/**
+ - vllm_omni/entrypoints/openai/**
+ - vllm_omni/entrypoints/openpi/**
+ - vllm_omni/entrypoints/client_request_state.py
+ - vllm_omni/entrypoints/stage_utils.py
+ - vllm_omni/entrypoints/utils.py
+primary_path_exceptions:
+ - path: vllm_omni/entrypoints/openai/errors.py
+ owner: error_contracts.md
related_code_paths:
- - vllm_omni/reasoning/**
+ - vllm_omni/errors.py
+ - vllm_omni/inputs/**
+ - vllm_omni/outputs/**
+ - vllm_omni/engine/async_omni_engine.py
+ - vllm_omni/config/**
+ - vllm_omni/deploy/**
depends_on:
- - vllm_omni_config.md
- input_output_modality_contracts.md
+ - error_contracts.md
- engine_orchestration.md
validation_paths:
- - tests/entrypoints/**
- - tests/reasoning/**
+ - tests/entrypoints/test_omni_entrypoints.py
+ - tests/entrypoints/test_async_omni.py
+ - tests/entrypoints/test_async_omni_duplex.py
+ - tests/entrypoints/test_serve.py
+ - tests/entrypoints/test_stream_finish_reason.py
+ - tests/entrypoints/openai/**
+ - tests/entrypoints/openai_api/**
+ - tests/e2e/online_serving/**
upstream_refs:
- - vllm.entrypoints
-last_reviewed: 2026-07-16
+ - vllm.engine.protocol.EngineClient
+ - vllm.entrypoints.openai/**
+ - vllm.entrypoints.serve/**
+ - vllm.renderers/**
+ - vllm.v1.engine.exceptions/**
+invariant_namespace: ENTRY-INV
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
---
-# Entrypoints
+# Entrypoints and serving boundaries
Entrypoints translate offline, CLI, and serving requests into stable engine
operations and translate engine outputs into public responses.
+## Contract status
+
+This document describes current entrypoint responsibilities plus the boundary
+under review in the open roadmap
+[#5227](https://github.com/vllm-project/vllm-omni/issues/5227) and helper-move
+PR [#5453](https://github.com/vllm-project/vllm-omni/pull/5453). In-flight
+helper locations are not treated as current paths.
+
+## Ownership boundary
+
+This document owns offline API semantics, CLI and serve composition,
+supported OpenAI-compatible routes, request validation and normalization,
+response conversion, streaming/session behavior, and engine handoff.
+
+It does not own configuration precedence, cross-stage routing, stage
+placement, payload implementation, or semantic error classification.
+`entrypoints/openai/errors.py` is an explicit primary-path exception owned by
+`error_contracts.md`.
+
## Candidate invariants
+These identifiers are proposals while the document is `draft`.
+
### ENTRY-INV-001: Entrypoints adapt but do not orchestrate
**Rule:** Entrypoints MUST NOT implement cross-stage routing or stage lifecycle
policy.
-### ENTRY-INV-002: Public requests are normalized once
+### ENTRY-INV-100: Public requests are normalized once
**Rule:** Public protocol values MUST be validated and converted to internal
request contracts before engine submission.
-### ENTRY-INV-003: Streaming preserves request identity
+### ENTRY-INV-101: Streaming preserves request identity
**Rule:** Every streamed response MUST remain associated with the request and
output modality that produced it.
+The reviewer-proposed rule that model-specific behavior should stay behind a
+common adapter or processor abstraction remains an unnumbered candidate until
+the entrypoint refactor makes that boundary enforceable.
+
+## Invariant namespace
+
+`ENTRY-INV` reserves `001-099` for boundary and dependency direction,
+`100-199` for normalization, sessions, and streaming, `200-299` for
+disconnect, cancellation, rendering, and cleanup, and `300-399` for upstream
+route and renderer compatibility. Numbers become append-only after normative
+promotion.
+
## Safe-change guide
-Test request validation, protocol conversion, streaming, cancellation, and
-error mapping for each affected entrypoint.
+Test request validation, protocol conversion, model-adapter routing,
+streaming, session identity, disconnect/cancellation, and error mapping for
+each affected offline or serving entrypoint.
+
+## Promotion gate
+
+- Reconcile the page after #5227's P0 ownership work and #5453's final
+ disposition.
+- Publish a supported route/transport matrix rather than claiming blanket
+ OpenAI parity.
+- Verify one normalization and handoff path per route family and one terminal
+ outcome per streaming transport.
+- Promote the `api_server.py` composition-root rule only after helper moves and
+ import boundaries enforce it.
+- Obtain approval from a technical owner and an independent validation
+ reviewer.
diff --git a/docs/design/module/error_contracts.md b/docs/design/module/error_contracts.md
new file mode 100644
index 000000000..02469d075
--- /dev/null
+++ b/docs/design/module/error_contracts.md
@@ -0,0 +1,109 @@
+---
+title: Error Classification, Propagation, and Rendering
+kind: module
+status: draft
+architecture_state: current-plus-rfc
+owners:
+ - "@alex-jw-brooks"
+ - "@NickCao"
+document_stewards:
+ - "@hsliuustc0106"
+ - "@Gaohan123"
+ - "@david6666666"
+required_reviewers:
+ - "@yinpeiqi"
+ - "@tzhouam"
+ - "@yenuo26"
+primary_code_paths:
+ - vllm_omni/errors.py
+ - vllm_omni/entrypoints/openai/errors.py
+related_code_paths:
+ - vllm_omni/engine/messages.py
+ - vllm_omni/engine/orchestrator.py
+ - vllm_omni/entrypoints/omni_base.py
+ - vllm_omni/entrypoints/async_omni.py
+ - vllm_omni/entrypoints/openai/api_server.py
+ - vllm_omni/entrypoints/openai/serving_chat.py
+ - vllm_omni/entrypoints/openai/serving_speech.py
+ - vllm_omni/entrypoints/openai/serving_audio_generate.py
+ - vllm_omni/entrypoints/openai/protocol/images.py
+ - vllm_omni/entrypoints/openai/protocol/videos.py
+depends_on: []
+validation_paths:
+ - tests/engine/test_orchestrator_error_handling.py
+ - tests/engine/test_async_omni_engine_outputs.py
+ - tests/entrypoints/test_omni_entrypoints.py
+ - tests/entrypoints/test_stream_finish_reason.py
+ - tests/entrypoints/openai_api/test_image_server.py
+ - tests/entrypoints/openai_api/test_video_server.py
+ - tests/entrypoints/openai_api/test_serving_audio_generate.py
+ - tests/entrypoints/openai_api/test_serving_speech.py
+ - tests/metrics/test_prometheus.py
+upstream_refs:
+ - vllm.v1.engine.exceptions.EngineDeadError
+ - vllm.v1.engine.exceptions.EngineGenerateError
+ - vllm.entrypoints.serve.utils.error_response
+ - vLLM RFC #48227
+ - vLLM PR #49665
+invariant_namespace: ERR-INV
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
+---
+
+# Error classification, propagation, and rendering
+
+This contract separates the meaning of a failure from the transport used to
+carry it and the public protocol used to render it.
+
+## Contract status
+
+Current behavior is distributed across engine, message, and entrypoint code.
+The semantic hierarchy proposed by
+[#5570](https://github.com/vllm-project/vllm-omni/issues/5570) remains an open
+RFC. Proposed types and wire records from that RFC are not current contracts.
+
+## Ownership boundary
+
+This document owns semantic classification, fatal versus request-scoped
+failure, cross-process error context, compatibility policy, sanitization, and
+offline, HTTP, SSE, WebSocket, and asynchronous-job rendering rules.
+
+`engine/messages.py` remains primarily owned by the I/O contract; this page
+owns the semantics of its error fields. Route and serving files remain
+primarily owned by `entrypoints.md`; this page owns their rendering
+constraints.
+
+## Candidate invariants
+
+The following topics require owner review before receiving stable IDs:
+
+- A request-scoped failure terminates only the affected request unless the
+ underlying engine is no longer usable.
+- Every public rendering path preserves a stable classification while
+ removing sensitive internal context.
+- Cancellation remains distinguishable from internal failure across process
+ and protocol boundaries.
+
+## Invariant namespace
+
+`ERR-INV` reserves `001-099` for classification ownership, `100-199` for wire
+context and compatibility, `200-299` for fatality, termination, sanitization,
+and rendering, and `300-399` for upstream alignment and transport extension
+points. Numbers become append-only after normative promotion.
+
+## Safe-change guide
+
+Exercise exception classification, stage wire round trips, fatal and
+request-scoped propagation, cancellation, sanitization, offline exceptions,
+HTTP status/body consistency, streaming terminal events, asynchronous-job
+persistence, and metrics classification.
+
+## Promotion gate
+
+- Resolve #5570's hierarchy, wire record, sanitization, and mixed-version
+ questions.
+- Add table-driven exception/status/type tests and stage wire round trips.
+- Verify all public transports terminate once and do not leak sensitive
+ payload data.
+- Obtain approval from both technical owners and an independent validation
+ reviewer.
diff --git a/docs/design/module/input_output_modality_contracts.md b/docs/design/module/input_output_modality_contracts.md
index 10d803d26..1083b09b0 100644
--- a/docs/design/module/input_output_modality_contracts.md
+++ b/docs/design/module/input_output_modality_contracts.md
@@ -2,46 +2,132 @@
title: Input, Output, and Modality Contracts
kind: module
status: draft
+architecture_state: current
owners:
+ - "@Sy0307"
+ - "@amy-why-3459"
+document_stewards:
+ - "@hsliuustc0106"
+ - "@Gaohan123"
+ - "@david6666666"
+required_reviewers:
- "@tzhouam"
- - "@gcanlin"
+ - "@linyueqian"
+ - "@david6666666"
+ - "@yenuo26"
+ - "@NickCao"
primary_code_paths:
- vllm_omni/inputs/**
- vllm_omni/outputs/**
+ - vllm_omni/request.py
+ - vllm_omni/data_entry_keys.py
+ - vllm_omni/engine/messages.py
+ - vllm_omni/engine/serialization.py
+ - vllm_omni/engine/mm_outputs.py
+ - vllm_omni/engine/output_modality.py
+ - vllm_omni/engine/output_processor.py
related_code_paths:
- - vllm_omni/tokenizers/**
-depends_on: []
+ - vllm_omni/model_executor/stage_input_processors/**
+ - vllm_omni/core/sched/**
+ - vllm_omni/distributed/omni_connectors/**
+ - vllm_omni/diffusion/request.py
+ - vllm_omni/diffusion/worker/request_batch.py
+ - vllm_omni/errors.py
+depends_on:
+ - error_contracts.md
validation_paths:
- tests/inputs/**
+ - tests/engine/test_data_entry_keys.py
+ - tests/engine/test_omni_request.py
+ - tests/engine/test_omni_request_output.py
+ - tests/engine/test_multimodal_accumulation.py
+ - tests/engine/test_output_modality.py
+ - tests/engine/test_output_processor.py
+ - tests/engine/test_wire_multimodal_output.py
+ - tests/utils/test_mm_outputs.py
+ - tests/utils/test_mm_outputs_partition.py
+ - tests/diffusion/test_diffusion_output_metadata.py
upstream_refs:
- - vllm.inputs
- - vllm.outputs
-last_reviewed: 2026-07-16
+ - vllm.inputs/**
+ - vllm.outputs/**
+ - vllm.v1.request.Request
+ - vllm.v1.engine.EngineCoreRequest
+ - vllm.v1.engine.EngineCoreOutput
+ - vllm.v1.engine.output_processor.OutputProcessor
+invariant_namespace: IO-INV
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
---
# Input, output, and modality contracts
These contracts define the data that may cross entrypoint, orchestration,
-stage, and model boundaries.
+stage, connector, and model boundaries.
+
+## Contract status
+
+This document is a draft description of the current request, message,
+serialization, modality, accumulation, and output types. It does not assign
+semantic error ownership to the wire schema.
+
+## Ownership boundary
+
+This document owns request identity, prompt/input types, modality keys and
+metadata, queue and wire message schemas, serialization, output types,
+accumulation, completion semantics, and compatibility shims.
+
+It does not own route-specific validation or rendering, model-specific
+interpretation, connector transport mechanics, scheduling policy, or semantic
+error classification. The `ErrorMessage` schema belongs here; the meaning and
+public rendering of its error fields belong to `error_contracts.md`.
## Candidate invariants
+These identifiers are proposals while the document is `draft`.
+
### IO-INV-001: Boundary data has an explicit modality
**Rule:** Data crossing a module or stage boundary MUST identify its modality
and use the corresponding validated contract.
-### IO-INV-002: Request identity is stable
+### IO-INV-100: Request identity is stable
**Rule:** Request identity MUST be preserved across conversions, stages,
streaming updates, cancellation, and errors.
-### IO-INV-003: Internal objects do not leak into public protocols
+### IO-INV-101: Completion is explicit
+
+**Rule:** Producers MUST distinguish partial updates from the terminal update
+for every output modality.
+
+### IO-INV-300: Internal objects do not leak into public protocols
**Rule:** Entrypoints MUST explicitly translate internal output objects into
public response types.
+## Invariant namespace
+
+`IO-INV` reserves `001-099` for schema ownership and dependency direction,
+`100-199` for identity, modality, accumulation, ordering, and completion,
+`200-299` for serialization failure and payload cleanup, and `300-399` for
+upstream extension, deprecation, and compatibility. Numbers become
+append-only after normative promotion.
+
## Safe-change guide
-Test construction, validation, serialization, streaming, optional fields, and
-compatibility at every affected producer-consumer boundary.
+Test construction, validation, serialization, round trips, streaming,
+accumulation, completion, optional fields, unknown fields, and compatibility
+at every affected producer-consumer boundary.
+
+## Promotion gate
+
+- Add explicit CODEOWNERS for the primary I/O paths or confirm that fallback
+ ownership is intentional.
+- Document the compatibility window for deprecated `vllm_omni.engine`
+ I/O-related imports.
+- Verify wire round trips and rejection of unknown or incompatible schema
+ fields.
+- Define payload-versus-metadata rules and completion semantics for streaming
+ and batched outputs.
+- Obtain approval from a technical owner and independent AR, diffusion,
+ connector, entrypoint, and validation reviewers as applicable.
diff --git a/docs/design/module/stage_runtime.md b/docs/design/module/stage_runtime.md
new file mode 100644
index 000000000..aefba5a40
--- /dev/null
+++ b/docs/design/module/stage_runtime.md
@@ -0,0 +1,126 @@
+---
+title: Stage Runtime and Replica Lifecycle
+kind: module
+status: draft
+architecture_state: current-plus-in-flight
+owners:
+ - "@tzhouam"
+ - "@fake0fan"
+document_stewards:
+ - "@hsliuustc0106"
+ - "@Gaohan123"
+ - "@david6666666"
+required_reviewers:
+ - "@Sy0307"
+ - "@princepride"
+ - "@chickeyton"
+ - "@yenuo26"
+ - "@NickCao"
+primary_code_paths:
+ - vllm_omni/engine/stage_runtime.py
+ - vllm_omni/engine/stage_pool.py
+ - vllm_omni/engine/stage_client.py
+ - vllm_omni/engine/membership_controller.py
+ - vllm_omni/engine/stage_engine_startup.py
+ - vllm_omni/engine/stage_engine_core_client.py
+ - vllm_omni/engine/stage_engine_core_proc.py
+ - vllm_omni/engine/stage_engine_core_proc_manager.py
+ - vllm_omni/engine/stage_init_utils.py
+related_code_paths:
+ - vllm_omni/distributed/omni_coordinator/**
+ - vllm_omni/distributed/ray_utils/**
+ - vllm_omni/diffusion/inline_stage_diffusion_client.py
+ - vllm_omni/diffusion/stage_diffusion_client.py
+ - vllm_omni/diffusion/stage_diffusion_proc.py
+ - vllm_omni/config/**
+depends_on:
+ - input_output_modality_contracts.md
+ - error_contracts.md
+ - omni_connector.md
+validation_paths:
+ - tests/engine/test_async_omni_engine_stage_init.py
+ - tests/engine/test_membership_controller.py
+ - tests/engine/test_single_stage_mode.py
+ - tests/engine/test_stage_engine_core_client.py
+ - tests/engine/test_stage_engine_core_proc.py
+ - tests/diffusion/test_inline_stage_diffusion_client.py
+ - tests/diffusion/test_stage_diffusion_proc.py
+ - tests/distributed/omni_coordinator/**
+upstream_refs:
+ - vllm.v1.engine.core_client.AsyncMPClient
+ - vllm.v1.engine.core_client.DPLBAsyncMPClient
+ - vllm.v1.engine.core.EngineCoreProc
+ - vllm.v1.engine.utils.CoreEngineProcManager
+ - vllm.v1.engine.coordinator.DPCoordinator
+invariant_namespace: STAGE-INV
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
+---
+
+# Stage runtime and replica lifecycle
+
+The stage runtime turns a logical stage definition into ready local or
+distributed replicas and provides the lifecycle boundary used by the
+orchestrator.
+
+## Contract status
+
+This document describes the current `StageRuntime`, `StagePool`, stage-client,
+and stage-process layout. The unified LLM/diffusion direction in
+[#5441](https://github.com/vllm-project/vllm-omni/pull/5441) remains in flight
+and is not presented as current behavior.
+
+## Ownership boundary
+
+This document owns local and distributed stage placement, startup and
+readiness, one pool per logical stage, replica identity and membership,
+selection and affinity, stage-client/process lifecycle, liveness, draining,
+and shutdown.
+
+It does not own cross-stage request policy, model scheduler policy, connector
+transfer semantics, configuration precedence, or public error rendering.
+
+## Candidate invariants
+
+These identifiers are proposals while the document is `draft`.
+
+### STAGE-INV-001: The runtime owns replica lifecycle
+
+**Rule:** Orchestration MUST acquire stage capacity through the stage runtime
+and MUST NOT construct or retire backend replicas directly.
+
+### STAGE-INV-100: Affinity uses stable replica identity
+
+**Rule:** Follow-up operations for a request with replica affinity MUST resolve
+the same logical replica until the request terminates or the replica is
+declared lost.
+
+### STAGE-INV-200: Shutdown is idempotent
+
+**Rule:** Repeated shutdown or cleanup signals MUST NOT leak stage clients,
+process managers, or membership registrations.
+
+## Invariant namespace
+
+`STAGE-INV` reserves `001-099` for placement and ownership, `100-199` for
+replica identity/readiness/affinity, `200-299` for liveness loss, draining and
+cleanup, and `300-399` for local/distributed and upstream compatibility.
+Numbers become append-only after normative promotion.
+
+## Safe-change guide
+
+Exercise local and distributed startup, readiness, replica selection,
+affinity, membership changes, abort routing, process failure, draining, and
+repeated shutdown. Cross-stage policy changes belong in
+`engine_orchestration.md`.
+
+## Promotion gate
+
+- Reconcile terminology and ownership after #5441 reaches a final state.
+- Add or identify a dedicated StageRuntime/StagePool lifecycle suite; current
+ evidence is distributed across the cited tests.
+- Demonstrate affinity preservation for update, interaction, and abort paths.
+- Demonstrate membership loss and repeated shutdown without leaked clients or
+ processes.
+- Obtain approval from a technical owner and an independent validation
+ reviewer.
diff --git a/docs/design/module/vllm_omni_config.md b/docs/design/module/vllm_omni_config.md
index 5df0cdcb3..085e4cee6 100644
--- a/docs/design/module/vllm_omni_config.md
+++ b/docs/design/module/vllm_omni_config.md
@@ -2,6 +2,8 @@
title: vLLM-Omni Configuration
kind: module
status: draft
+architecture_state: deferred-pending-refactor
+ownership_status: provisional
owners:
- "@lishunyang12"
- "@alex-jw-brooks"
@@ -16,32 +18,39 @@ validation_paths:
- tests/config/**
upstream_refs:
- vllm.config
-last_reviewed: 2026-07-16
+last_reviewed: 2026-08-07
+last_verified_commit: 3d7fc3b9ba3cac88d579d4dc35b78b0b641675fc
---
# vLLM-Omni configuration
-Configuration defines how user input becomes validated engine, stage,
-deployment, and platform settings.
+This page intentionally records discovery paths only. Substantive
+configuration ownership and contract work is deferred until the active
+configuration refactoring has settled.
-## Candidate invariants
+## Deferred contract scope
-### CONFIG-INV-001: Configuration is validated before startup
+The current code and tests remain authoritative for parsing, defaults,
+overrides, deployment topology, stage construction, and runtime projection.
+This draft does not define a stable precedence model, environment-variable
+capture rule, canonical configuration object, or invariant namespace.
-**Rule:** Invalid stage graphs, resource assignments, and incompatible options
-MUST fail before managed runtime processes start.
+The PR review proposal to capture environment-derived values in configuration
+objects at initialization is an open design question, not a current
+invariant.
-### CONFIG-INV-002: Precedence is deterministic
+## Safe-change guide while deferred
-**Rule:** Defaults, files, environment variables, and CLI overrides MUST have a
-documented and deterministic precedence.
+Changes should trace every affected producer to its runtime consumer and test
+the structured, legacy, CLI, and deployment paths that are actually supported.
+Do not infer a stable contract from this placeholder page.
-### CONFIG-INV-003: Runtime modules consume validated configuration
+## Promotion gate
-**Rule:** Runtime modules MUST NOT independently reinterpret raw user
-configuration.
-
-## Safe-change guide
-
-Test defaults, parsing, override precedence, serialization, invalid
-combinations, and representative multi-stage configurations.
+- Finish or stabilize the configuration refactor and identify the canonical
+ runtime configuration representation.
+- Confirm technical owners, primary path exceptions, and validation owners.
+- Document parsing and override precedence, including when environment values
+ are captured.
+- Allocate an invariant namespace only after those behaviors are enforced and
+ owner-approved.
diff --git a/docs/design/qwen3_omni_tts_performance_optimization.md b/docs/design/qwen3_omni_tts_performance_optimization.md
index e4548a17c..bc1e29a7f 100644
--- a/docs/design/qwen3_omni_tts_performance_optimization.md
+++ b/docs/design/qwen3_omni_tts_performance_optimization.md
@@ -419,8 +419,7 @@ Notes:
```bash
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct \
--omni \
- --port 8091 \
- --stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_omni_moe_async_chunk.yaml
+ --port 8091
```
#### 3) Key config knobs
@@ -494,7 +493,7 @@ The default config (`qwen3_tts.yaml`) enables the full optimization stack:
vllm-omni serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
--omni \
--port 8000 \
- --stage-configs-path vllm_omni/model_executor/stage_configs/qwen3_tts_no_async_chunk.yaml
+ --no-async-chunk
```
#### 3) Key config knobs
diff --git a/docs/features/session_state_manager.md b/docs/features/session_state_manager.md
new file mode 100644
index 000000000..c0c055fcb
--- /dev/null
+++ b/docs/features/session_state_manager.md
@@ -0,0 +1,160 @@
+# Session State Manager (experimental)
+
+!!! warning
+ This feature is **experimental** (RFC [#4480](https://github.com/vllm-project/vllm-omni/issues/4480)).
+ The code lives under `vllm_omni/experimental/world_models/` and its APIs may
+ change without notice. It is **off by default**; when disabled, model
+ behavior is byte-for-byte unchanged.
+
+Autoregressive diffusion world models (DreamZero, Cosmos3, and similar) keep
+per-session state across `forward()` calls: accumulated video latents, frame
+stitching buffers, encode-once conditioning, counters, and reset heuristics.
+Historically each model hand-rolled this state. The session state manager
+extracts it into a shared, typed contract:
+
+- `StateObject` — one typed unit of session state (e.g. `LatentBuffer` for
+ accumulated latents and bounded rings), with a uniform lifecycle
+ (`allocate` / `commit` / `view` / `reset` / `evict` / `nbytes_by_device`).
+- `SessionStateManager` — maps `session_id -> {name: StateObject}`, caps the
+ number of retained sessions by evicting the least recently used one first
+ (matching the bespoke caches it replaces), and reports per-device byte usage
+ via `stats()` for observability.
+
+Attention KV is *not* managed here: for DreamZero it is owned by the
+AR-Diffusion engine's paged KV pool (PR
+[#4534](https://github.com/vllm-project/vllm-omni/pull/4534)). The manager
+covers the model's non-KV session state; integrating the engine's per-session
+KV handle into the same byte accounting is RFC #4480 Phase 1.
+
+## Enabling
+
+Via config:
+
+```yaml
+# deploy config
+enable_session_state_manager: true
+```
+
+or programmatically with `OmniDiffusionConfig(enable_session_state_manager=True)`.
+
+Via environment variables (no config change needed):
+
+```bash
+export OMNI_DIFFUSION_SESSION_STATE_MANAGER=1 # 1/0/true/false/yes/no/on/off
+export OMNI_DIFFUSION_SESSION_STATE_MANAGER_MAX_SESSIONS=64 # optional, positive int
+```
+
+**Precedence:** a *set* environment variable overrides the config value, in
+both directions — `OMNI_DIFFUSION_SESSION_STATE_MANAGER=0` force-disables the
+manager even if the config enables it. Unset or unparsable values fall back
+to the config (an unparsable `MAX_SESSIONS` logs a warning and is ignored).
+
+**Why both a config field and an environment variable?** The config field is
+the API. The environment override exists for A/B equivalence validation: the
+manager-backed path must be bit-identical to the bespoke path, and the way to
+verify that is to run the *same* deploy config twice with only the flag
+flipped per process — no config-file edits, so the two runs cannot drift
+apart. (Same pattern as the `DIFFUSION_CACHE_BACKEND` fallback.)
+
+**Why is `MAX_SESSIONS` not a config field?** The retained-session cap is not
+a public tuning knob: it mirrors the bespoke `MAX_DREAMZERO_SESSIONS = 64`
+constant (which is not configurable either), so the manager-backed path evicts
+exactly like the path it replaces. The environment variable exists only to
+stress eviction in tests and experiments. It is deliberately not promoted to
+the config, because RFC #4480 Phase 1 replaces count-based capping with a byte
+budget — that budget, not this cap, is the knob that deserves a config field.
+
+## Choosing between an attribute and a `StateObject`
+
+A session holds its values in two places. Named `StateObject`s live in the
+session's object map; everything else lives in `SessionState.attrs`, declared
+one line per value with the `SessionAttr` descriptor. The rule for choosing is:
+
+> A value belongs in a `StateObject` when the **manager** must be able to act on
+> it — release it under budget pressure, accumulate into it, stage and commit
+> it, or rebuild it from a recorded source. It belongs in `attrs` when the
+> manager only needs to **carry** it: the model assigns it wholesale, reads it
+> back, and clears it, and no one but the model can decide its fate.
+
+Byte accounting is independent of that choice. A session reports every byte it
+holds in either bucket, because the number describes what the process is
+holding, not what the manager may do about it.
+
+The consequence is worth stating plainly: **a value left in `attrs` is a value
+the byte-budget planner cannot free.** In Phase 0, which accounts but does not
+enforce, that is legitimate — but it should be a recorded decision rather than
+an accident, which is what the table below records.
+
+Note what the rule does *not* say. "Is it big" is not the criterion, and
+neither is "does it grow". The largest allocation a DreamZero session holds —
+`vae_enc_feat_map`, 603 MiB — is an *attribute*, because nothing the manager
+can do to it is useful: it is replaced wholesale, it has no recompute source,
+and freeing it mid-session breaks the encoder stream. Ending the session is the
+only release available, and that reclaims attributes anyway. Size alone argues
+for counting a value, which the accounting does regardless of bucket; it does
+not argue for a lifecycle.
+
+What would change that answer is a release the manager can perform without
+losing the value — offloading it to host memory and copying it back is
+transparent, needs no recomputation, and suits a large contiguous cache. The
+contract has no such operation: `evict()` frees storage, it does not park it.
+So the bucket follows the operations that exist, and moves if they do.
+
+### Byte accounting
+
+`SessionStateManager.stats()` reports byte totals keyed by device
+(`nbytes:cuda:0`, `nbytes:cpu`) alongside `total_nbytes`. The split is not
+cosmetic: device memory is the contended resource and host memory generally is
+not, so a single figure covering both answers no question. Which pool a budget
+applies to is a question for whoever enforces it, and is not decided here.
+
+Two details make the numbers honest. Bytes are counted per **storage**, not as
+`numel() * element_size()`, so a narrow slice reports the whole allocation it
+keeps alive rather than the part it exposes. And storages are **deduplicated**
+across the whole session, so two views of one buffer — or an alias between an
+attribute and a state object — are counted once.
+
+## Scope and guarantees
+
+- **Opt-in and equivalent.** With the flag off, models use their bespoke state
+ paths unchanged. With the flag on, the manager-backed path is validated
+ bit-identical to the bespoke path (CPU equivalence tests under
+ `tests/dreamzero/test_session_state_equivalence.py`, plus GPU A/B runs).
+- **Session eviction is count-based (LRU).** Evicting a session drops it from
+ the lookup table only; an adapter still holding the session keeps its state
+ (matching bespoke behavior, where the caller holds the state object).
+- **Byte budget is recorded, not enforced.** `SessionStateManager.stats()`
+ reports per-manager, per-device byte totals; budget *enforcement* (and
+ eviction driven by it) is a later phase of RFC #4480.
+
+## Supported models
+
+### DreamZero
+
+Sizes measured on one A100 at the shipped deploy config (`180x320` per camera,
+three cameras stitched to `352x640`, bfloat16), over a 400-step session.
+
+| Value | Bucket | Size | Notes |
+|---|---|---|---|
+| `vae_enc_feat_map` | `attrs` | **603 MiB** | Wan encoder causal-conv cache, 24 entries. Constant from ~step 50. The largest thing a session holds: 64 sessions is 37.7 GiB. Counted, not releasable — see the rule above. |
+| `video_latents_across_time` | `LatentBuffer` | grows | AR video latent chunks, host memory (`.cpu()`), kept for decode. |
+| `stitched_buffer` | `LatentBuffer` | bounded | Stitched pixel frames, host memory, ring of `FRAMES_PER_CHUNK`. |
+| `vae_encoder_out` | `LatentBuffer` | 0.43 MiB | Bounded ring of `num_frame_per_block` latent frames — see below. |
+| `clip_feas`, `ys`, `language`, `prompt_embeds` | `attrs` | small | Conditioning tensors, written once per session and reread. |
+| `vae_pending_body_frames` | `attrs` | ≤2.6 MiB | Raw frames awaiting a 4-frame body chunk; bounded by construction. |
+| `call_count`, `current_start_frame`, `vae_stream_initialized` | `attrs` | — | Counters and flags. |
+
+Attention KV does not appear here: it is engine-owned (PR #4534).
+
+**The bounded VAE encoder ring.** The bespoke state concatenates every encoded
+chunk into one tensor and clears it only on a *session* reset — a window reset
+deliberately keeps it — so it grows for as long as the session lives, at
+0.215 MiB per latent frame (one per four steps). Its only reader,
+`_vae_stream_get_observation_latents`, quantises the whole history and then
+keeps the last `num_frame_per_block` latent frames. Since `quant_conv` is a
+`WanCausalConv3d` of kernel size 1, pointwise in time, those frames do not
+depend on the discarded ones. The manager-backed state therefore retains a ring
+of `num_frame_per_block` frames instead: identical output, bounded memory, and
+the per-step quantisation no longer scales with session length. Asking for more
+frames than the ring retains raises rather than silently returning a shorter
+window. The bespoke path is unchanged.
diff --git a/docs/getting_started/installation/README.md b/docs/getting_started/installation/README.md
index 89562c53c..b49cee794 100644
--- a/docs/getting_started/installation/README.md
+++ b/docs/getting_started/installation/README.md
@@ -1,5 +1,8 @@
# Installation
+!!! important
+ vLLM-Omni is released against the matching upstream vLLM major/minor version. vLLM-Omni 0.26.x requires vLLM 0.26.x; the stable v0.26.0 instructions pin vLLM 0.26.0.
+
vLLM-Omni supports the following hardware platforms:
- [GPU](gpu.md)
diff --git a/docs/getting_started/installation/gpu.md b/docs/getting_started/installation/gpu.md
index e21a11cee..59d2aa482 100644
--- a/docs/getting_started/installation/gpu.md
+++ b/docs/getting_started/installation/gpu.md
@@ -34,7 +34,7 @@ vLLM-Omni is a Python library that supports the following GPU variants. The libr
### Pre-built wheels
-Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, and 0.25.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
+Note: Pre-built wheels are currently available for vLLM-Omni 0.11.0rc1, 0.12.0rc1, 0.14.0rc1, 0.14.0, 0.16.0, 0.18.0, 0.20.0, 0.21.0, 0.22.0, 0.23.0, 0.24.0, 0.25.0, and 0.26.0. If you need a newer unreleased revision, please [build from source](https://docs.vllm.ai/projects/vllm-omni/en/latest/getting_started/installation/gpu/#build-wheel-from-source).
=== "NVIDIA CUDA"
diff --git a/docs/getting_started/installation/gpu/cuda.inc.md b/docs/getting_started/installation/gpu/cuda.inc.md
index d4808139e..a68bfdb58 100644
--- a/docs/getting_started/installation/gpu/cuda.inc.md
+++ b/docs/getting_started/installation/gpu/cuda.inc.md
@@ -5,7 +5,7 @@
# --8<-- [end:requirements]
# --8<-- [start:set-up-using-python]
-vLLM-Omni depends vLLM. So please follow instructions below mainly for vLLM.
+vLLM-Omni depends on the matching major/minor release of vLLM. The vLLM-Omni 0.26.x release line uses vLLM 0.26.x.
!!! note
PyTorch installed via `conda` will statically link `NCCL` library, which can cause issues when vLLM tries to use `NCCL`. See for more details.
@@ -20,7 +20,7 @@ Therefore, it is recommended to install vLLM and vLLM-Omni with a **fresh new**
vLLM-Omni is built based on vLLM. Please install it with command below.
```bash
-uv pip install vllm==0.25.0 --torch-backend=auto
+uv pip install vllm==0.26.0 --torch-backend=auto
```
#### Installation of vLLM-Omni
@@ -39,13 +39,13 @@ uv pip install 'vllm-omni[demo]'
# --8<-- [start:build-wheel-from-source]
#### Installation of vLLM
-If you do not need to modify source code of vLLM, you can directly install the stable 0.25.0 release version of the library
+If you do not need to modify source code of vLLM, you can directly install the stable 0.26.0 release version of the library
```bash
-uv pip install vllm==0.25.0 --torch-backend=auto
+uv pip install vllm==0.26.0 --torch-backend=auto
```
-The 0.25.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
+The 0.26.0 release of vLLM ships CUDA 13.0-compatible binaries by default. If you need a different CUDA variant or want to reuse an existing PyTorch installation, build vLLM from source instead.
#### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -66,12 +66,12 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash
git clone https://github.com/vllm-project/vllm.git
cd vllm
-git checkout v0.25.0
+git checkout v0.26.0
```
Set up environment variables to get pre-built wheels. If there are internet problems, just download the whl file manually. And set `VLLM_PRECOMPILED_WHEEL_LOCATION` as your local absolute path of whl file.
```bash
-#For CUDA 13.0 (the default for v0.25.0; the wheel filename has no `+cu130` suffix)
-export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.25.0/vllm-0.25.0-cp38-abi3-manylinux_2_28_x86_64.whl
+#For CUDA 13.0 (the default for v0.26.0; the wheel filename has no `+cu130` suffix)
+export VLLM_PRECOMPILED_WHEEL_LOCATION=https://github.com/vllm-project/vllm/releases/download/v0.26.0/vllm-0.26.0-cp38-abi3-manylinux_2_28_x86_64.whl
```
Install vllm with command below (If you have no existing PyTorch).
```bash
@@ -102,7 +102,7 @@ docker run --runtime nvidia --gpus 2 \
--env "HF_TOKEN=$HF_TOKEN" \
-p 8091:8091 \
--ipc=host \
- vllm/vllm-omni:v0.25.0 \
+ vllm/vllm-omni:v0.26.0 \
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
@@ -124,7 +124,7 @@ If you want to specify the base vLLM version:
```bash
DOCKER_BUILDKIT=1 docker build \
-f docker/Dockerfile.cuda \
- --build-arg BASE_IMAGE=vllm/vllm-openai:v0.22.1 \
+ --build-arg BASE_IMAGE=vllm/vllm-openai:v0.26.0 \
-t vllm-omni-cuda .
```
diff --git a/docs/getting_started/installation/gpu/rocm.inc.md b/docs/getting_started/installation/gpu/rocm.inc.md
index 4a64e8723..c44407299 100644
--- a/docs/getting_started/installation/gpu/rocm.inc.md
+++ b/docs/getting_started/installation/gpu/rocm.inc.md
@@ -13,7 +13,7 @@ vLLM-Omni current recommends the steps in under setup through Docker Images.
vLLM-Omni is built based on vLLM. Please install it with command below.
```bash
-uv pip install vllm==0.25.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.25.0/rocm723
+uv pip install vllm==0.26.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.26.0/rocm723
```
#### Installation of vLLM-Omni
@@ -34,13 +34,13 @@ uv pip install onnxruntime-rocm
# --8<-- [start:build-wheel-from-source]
#### Installation of vLLM
-If you do not need to modify source code of vLLM, you can directly install the stable 0.25.0 release version of the library
+If you do not need to modify source code of vLLM, you can directly install the stable 0.26.0 release version of the library
```bash
-uv pip install vllm==0.25.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.25.0/rocm723
+uv pip install vllm==0.26.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.26.0/rocm723
```
-The pre-built 0.25.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
+The pre-built 0.26.0 vLLM wheel targets ROCm 7.2.3. If you need a different ROCm stack or want to reuse an existing PyTorch installation, build vLLM from source instead.
#### Installation of vLLM-Omni
Since vllm-omni is rapidly evolving, it's recommended to install it from source
@@ -58,7 +58,7 @@ If you want to check, modify or debug with source code of vLLM, install the libr
```bash
git clone https://github.com/vllm-project/vllm.git
cd vllm
-git checkout v0.25.0
+git checkout v0.26.0
python3 -m pip install -r requirements/rocm.txt
python3 setup.py develop
```
@@ -130,7 +130,7 @@ docker run --rm \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HF_TOKEN=$HF_TOKEN" \
-p 8091:8091 \
- vllm/vllm-omni-rocm:v0.25.0 \
+ vllm/vllm-omni-rocm:v0.26.0 \
--model Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
```
@@ -149,7 +149,7 @@ docker run --rm -it \
-v ~/.cache/huggingface:/root/.cache/huggingface \
--env "HF_TOKEN=$HF_TOKEN" \
--entrypoint bash \
- vllm/vllm-omni-rocm:v0.25.0
+ vllm/vllm-omni-rocm:v0.26.0
```
# --8<-- [end:pre-built-images]
diff --git a/docs/getting_started/installation/npu/npu.inc.md b/docs/getting_started/installation/npu/npu.inc.md
index 7381f5b10..a0d4c86b0 100644
--- a/docs/getting_started/installation/npu/npu.inc.md
+++ b/docs/getting_started/installation/npu/npu.inc.md
@@ -9,8 +9,8 @@ The recommended way to use vLLM-Omni on NPU is through the vLLM-Ascend pre-built
```bash
# vLLM-Ascend image
-# Atlas A2: quay.io/atlas-ci/vllm-ascend:v0.25.0
-# Atlas A3: quay.io/atlas-ci/vllm-ascend:v0.25.0-a3
+# Atlas A2: quay.io/atlas-ci/vllm-ascend:v0.26.0
+# Atlas A3: quay.io/atlas-ci/vllm-ascend:v0.26.0-a3
docker run --rm \
--name vllm-omni-npu \
--shm-size=1g \
@@ -28,11 +28,11 @@ docker run --rm \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v /root/.cache:/root/.cache \
-p 8000:8000 \
- -it quay.io/atlas-ci/vllm-ascend:0.25.0-a3 bash
+ -it quay.io/atlas-ci/vllm-ascend:v0.26.0-a3 bash
# Inside the container, install vLLM-Omni from source
cd /vllm-workspace
-git clone -b v0.25.0 https://github.com/vllm-project/vllm-omni.git
+git clone -b v0.26.0 https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
pip install -v -e . --no-build-isolation
# or VLLM_OMNI_TARGET_DEVICE=npu pip install -v -e .
@@ -53,16 +53,20 @@ We are keeping [issue #886](https://github.com/vllm-project/vllm-omni/issues/886
You can also build vLLM-Omni from the latest main branch if you want to use the latest features or bug fixes. (But sometimes it will break for a while. You can check [issue #886](https://github.com/vllm-project/vllm-omni/issues/886) for the status of the latest commit of vLLM-Omni main branch on NPU.)
```bash
-# Pin vLLM version to 0.18.0
-git clone -b v0.18.0 https://github.com/vllm-project/vllm.git
+# Pin vLLM and vLLM-Ascend to the v0.26 release line
+git clone -b v0.26.0 https://github.com/vllm-project/vllm.git
+cd vllm
VLLM_TARGET_DEVICE=empty pip install -v -e .
+cd ..
-git clone -b v0.18.0rc1 https://github.com/vllm-project/vllm-ascend.git
+git clone -b releases/v0.26.0rc https://github.com/vllm-project/vllm-ascend.git
+cd vllm-ascend
pip install -v -e .
+cd ..
# Install vLLM-Omni from the latest main branch
git clone https://github.com/vllm-project/vllm-omni.git
-cd /vllm-workspace/vllm-omni
+cd vllm-omni
pip install -v -e . --no-build-isolation
# or VLLM_OMNI_TARGET_DEVICE=npu pip install -v -e .
export VLLM_WORKER_MULTIPROC_METHOD=spawn
@@ -84,8 +88,8 @@ Supported images as following.
Here's an example deployment command that has been verified on 4 x NPUs:
```bash
-# Atlas A2: quay.io/ascend/vllm-omni:v0.25.0
-# Atlas A3: quay.io/ascend/vllm-omni:v0.25.0-a3
+# Atlas A2: quay.io/ascend/vllm-omni:v0.26.0
+# Atlas A3: quay.io/ascend/vllm-omni:v0.26.0-a3
docker run --rm \
--name vllm-omni-a3 \
--shm-size=4g \
@@ -110,7 +114,7 @@ docker run --rm \
-v /etc/ascend_install.info:/etc/ascend_install.info \
-v ~/.cache:/root/.cache \
-p 8091:8091 \
- -it quay.io/ascend/vllm-omni:v0.25.0-a3 bash
+ -it quay.io/ascend/vllm-omni:v0.26.0-a3 bash
```
!!! tip
@@ -122,9 +126,9 @@ Or build IMAGE from **source code**:
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
# A2
-docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu .
+# docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu .
# A3
-# docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu.a3 .
+docker build -t vllm-omni-dev-image:latest -f ./docker/Dockerfile.npu.a3 .
```
# --8<-- [end:pre-built-images]
diff --git a/docs/getting_started/quickstart.md b/docs/getting_started/quickstart.md
index 914f1f7b4..f9673eac7 100644
--- a/docs/getting_started/quickstart.md
+++ b/docs/getting_started/quickstart.md
@@ -19,10 +19,10 @@ uv venv --python 3.12 --seed
source .venv/bin/activate
# On CUDA
-uv pip install vllm==0.25.0 --torch-backend=auto
+uv pip install vllm==0.26.0 --torch-backend=auto
# On ROCm
-uv pip install vllm==0.25.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.25.0/rocm723
+uv pip install vllm==0.26.0+rocm723 --extra-index-url https://wheels.vllm.ai/rocm/0.26.0/rocm723
git clone https://github.com/vllm-project/vllm-omni.git
cd vllm-omni
@@ -34,7 +34,7 @@ For additional installation methods — please see the [installation guide](inst
!!! note
It is important to install the same major & minor version of vLLM and vLLM Omni, otherwise things may not work as expected. If the versions are misaligned, you will see a warning when you import vLLM Omni.
- If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.25.0` and vLLM Omni `0.25.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
+ If you are seeing strange behavior with the `vllm` command not handling the `--omni` flag correctly, you most likely have a version mismatch with vLLM < `0.26.0` and vLLM Omni `0.26.0`, as vLLM Omni no longer hijacks the vLLM entrypoint. Updating vLLM should resolve this issue.
## Offline Inference
@@ -87,9 +87,8 @@ if __name__ == "__main__":
!!! info
- For diffusion request-level batching controls such as `max_num_seqs` and
- `request_batch_max_wait_ms`, see
- [Request-Level Batching](../user_guide/diffusion/request_batching.md).
+ For diffusion request batching, step execution, and streaming controls, see
+ [Diffusion Execution Modes](../user_guide/diffusion/execution_modes.md).
For more usages, please refer to [offline inference](../user_guide/examples/offline_inference/qwen2_5_omni.md)
diff --git a/docs/models/supported_models.md b/docs/models/supported_models.md
index 99b17d529..d1d4e051d 100644
--- a/docs/models/supported_models.md
+++ b/docs/models/supported_models.md
@@ -41,24 +41,24 @@ th {
| `Wan22S2VPipeline` | Wan2.2-S2V | `Wan-AI/Wan2.2-S2V-14B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22VACEPipeline` | Wan2.1-VACE | `Wan-AI/Wan2.1-VACE-1.3B-diffusers`, `Wan-AI/Wan2.1-VACE-14B-diffusers` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `Wan22VACEPipeline` | Wan2.2-VACE | `Pyros13/Wan2.2-VACE-Fun-A14B-Diffusers` | ✅︎ | | | |
-| `LTX2Pipeline` | LTX-2-T2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
-| `LTX2ImageToVideoPipeline` | LTX-2-I2V | `Lightricks/LTX-2` | ✅︎ | ✅︎ | | |
-| `LTX2TwoStagesPipeline` | LTX-2-T2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
-| `LTX2ImageToVideoTwoStagesPipeline` | LTX-2-I2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
-| `LTX23Pipeline` | LTX-2.3-T2V | `dg845/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | |
-| `LTX23ImageToVideoPipeline` | LTX-2.3-I2V | `dg845/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | |
+| `LTX2Pipeline` | LTX-2 / LTX-2.3 one-stage T2V and I2V | `Lightricks/LTX-2`, `diffusers/LTX-2.3-Diffusers` | ✅︎ | ✅︎ | | |
+| `LTX2DistilledPipeline` | LTX-2 distilled two-stage T2V and I2V | `rootonchair/LTX-2-19b-distilled` | ✅︎ | ✅︎ | | |
+| `LingBotVideoPipeline` | LingBot-Video dense and MoE T2V | `robbyant/lingbot-video-dense-1.3b`, `robbyant/lingbot-video-moe-30b-a3b` | ✅︎ | | | |
+| `MiniMaxH3Pipeline` | MiniMax H3 T2VA, first/last-keyframe FL2VA, and mixed-reference Ref2VA | `MiniMaxAI/MiniMax-H3` | ✅︎ | | | |
| `DreamZeroPipeline` | DreamZero-DROID | `GEAR-Dreams/DreamZero-DROID` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `HeliosPipeline`, `HeliosPyramidPipeline` | Helios | `BestWishYsh/Helios-Base`, `BestWishYsh/Helios-Mid`, `BestWishYsh/Helios-Distilled` | ✅︎ | ✅︎ | ✅︎ | |
| `MagiHumanPipeline` | MagiHuman | `SII-GAIR/daVinci-MagiHuman-Base-1080p` | ✅︎ | ✅︎ | | |
| `OvisImagePipeline` | Ovis-Image | `OvisAI/Ovis-Image` | ✅︎ | ✅︎ | | ✅︎ |
| `LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `LongCatImageEditPipeline` | LongCat-Image-Edit | `meituan-longcat/LongCat-Image-Edit` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
+| `BooguImagePipeline` | Boogu-Image | `Boogu/Boogu-Image-0.1-Base`, `Boogu/Boogu-Image-0.1-Edit` | ✅︎ | | | |
| `StableDiffusionXLPipeline` | Stable-Diffusion-XL | `stabilityai/stable-diffusion-xl-base-1.0` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `StableDiffusion3Pipeline` | Stable-Diffusion-3 | `stabilityai/stable-diffusion-3.5-medium` | ✅︎ | ✅︎ | | ✅︎ |
| `CosyVoice3Model` | CosyVoice3 | `FunAudioLLM/Fun-CosyVoice3-0.5B-2512` | ✅︎ | ✅︎ | | ✅︎ |
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | |
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | |
| `MammothModa2ForConditionalGeneration` | MammothModa2-Preview | `bytedance-research/MammothModa2-Preview` | ✅︎ | ✅︎ | | |
+| `MammothModa2ForConditionalGeneration` | MammothModa2-Dev (AR-only image understanding) | `bytedance-research/MammothModa2-Dev` | ✅︎ | | | |
| `Flux2KleinPipeline` | FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B`, `black-forest-labs/FLUX.2-klein-9B` | ✅︎ | ✅︎ | ✅︎ | ✅︎ |
| `FluxKontextPipeline` | FLUX.1-Kontext-dev | `black-forest-labs/FLUX.1-Kontext-dev` | ✅︎ | ✅︎ | | |
| `FluxPipeline` | FLUX.1-dev | `black-forest-labs/FLUX.1-dev` | ✅︎ | ✅︎ | | ✅︎ |
@@ -83,7 +83,7 @@ th {
| `MiMoAudioModel` | MiMo-Audio-7B-Instruct | `XiaomiMiMo/MiMo-Audio-7B-Instruct` | ✅︎ | ✅︎ | | |
| `MiMoV2ASRForCausalLM` | MiMo-V2.5-ASR | `XiaomiMiMo/MiMo-V2.5-ASR` | ✅︎ | ✅︎ | | |
| `Flux2Pipeline` | FLUX.2-dev | `black-forest-labs/FLUX.2-dev` | ✅︎ | ✅︎ | | |
-| `FishSpeechSlowARForConditionalGeneration` | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✅︎ | ✅︎ | | |
+| `FishSpeechSlowARForConditionalGeneration` | Fish Speech S2 Pro | `fishaudio/s2-pro` | ✅︎ | ✅︎ | | ✅︎ |
| `DreamIDOmniPipeline` | DreamID-Omni | `XuGuo699/DreamID-Omni` | ✅︎ | ✅︎ | | |
| `SenseNovaU1Pipeline` | SenseNova-U1 (DiT-only) | `SenseNova/SenseNova-U1-8B-MoT` | ✅︎ | | | |
| `LancePipeline` | Lance | `bytedance-research/Lance` | ✅︎ | | | |
diff --git a/docs/serving/audio_generate_api.md b/docs/serving/audio_generate_api.md
index afe693ae4..3109446c1 100644
--- a/docs/serving/audio_generate_api.md
+++ b/docs/serving/audio_generate_api.md
@@ -66,7 +66,7 @@ Content-Type: application/json
|-----------|------|---------|-------------|
| `input` | string | **required** | Text prompt describing the audio to generate |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
-| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
+| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, opus |
| `speed` | float | 1.0 | Playback speed (0.25 - 4.0) |
#### Diffusion Parameters
@@ -90,7 +90,6 @@ Returns binary audio data with the appropriate `Content-Type` header:
| `mp3` | `audio/mpeg` |
| `flac` | `audio/flac` |
| `pcm` | `audio/pcm` |
-| `aac` | `audio/aac` |
| `opus` | `audio/opus` |
## Examples
@@ -285,7 +284,7 @@ Pydantic validation failure (e.g. invalid `response_format`, `speed` out of rang
"detail": [
{
"type": "literal_error",
- "msg": "Input should be 'wav', 'pcm', 'flac', 'mp3', 'aac' or 'opus'",
+ "msg": "Input should be 'wav', 'pcm', 'flac', 'mp3' or 'opus'",
...
}
]
diff --git a/docs/serving/speech_api.md b/docs/serving/speech_api.md
index 27dfda95f..0deea7f8c 100644
--- a/docs/serving/speech_api.md
+++ b/docs/serving/speech_api.md
@@ -109,7 +109,7 @@ Content-Type: application/json
| `input` | string | **required** | The text to synthesize into speech |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
| `voice` | string | "vivian" | Speaker name (e.g., vivian, ryan, aiden) |
-| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
+| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, opus |
| `speed` | float | 1.0 | Playback speed (0.25-4.0) |
#### vLLM-Omni Extension Parameters
@@ -276,20 +276,42 @@ Client -> Server:
| Message | Description |
|---------|-------------|
-| `{"type": "session.config", ...}` | Session configuration (sent once, first message) |
+| `{"type": "session.config", ...}` | Session configuration (first message; may be resent between utterances to change it) |
| `{"type": "input.text", "text": "..."}` | Text chunk |
-| `{"type": "input.done"}` | End of input, flushes remaining buffer |
+| `{"type": "input.done"}` | End of utterance: flushes the buffer and keeps the connection open |
+| `{"type": "session.close"}` | End of connection |
Server -> Client:
| Message | Description |
|---------|-------------|
-| `{"type": "audio.start", "sentence_index": 0, "sentence_text": "...", "format": "pcm", "sample_rate": 24000}` | Audio generation starting for the buffered input |
+| `{"type": "audio.start", "utterance_index": 0, "sentence_index": 0, "sentence_text": "...", "format": "pcm", "sample_rate": 24000}` | Audio generation starting for the buffered input |
| Binary frame | Raw audio bytes (one or more PCM chunks when `stream_audio=true`) |
-| `{"type": "audio.done", "sentence_index": 0, "total_bytes": 96000, "error": false}` | Audio complete for the buffered input |
-| `{"type": "session.done", "total_sentences": N}` | Session complete |
+| `{"type": "audio.done", "utterance_index": 0, "sentence_index": 0, "total_bytes": 96000, "error": false}` | Audio complete for the buffered input |
+| `{"type": "session.done", "utterance_index": 0, "total_sentences": N}` | Flushed utterance complete |
| `{"type": "error", "message": "..."}` | Non-fatal error |
+### Flushing vs. Closing
+
+`input.done` is a flush, not a disconnect. The server synthesizes the buffered
+text, emits `session.done`, and then waits on the same connection for the next
+utterance, so a client that speaks repeatedly (for example one driven by an
+upstream LLM) pays the WebSocket handshake once instead of once per utterance.
+
+* The session config is sticky. Send `input.text` again straight after
+ `session.done` to reuse it, or send another `session.config` first to change
+ voice, format, or reference audio. A `session.config` sent while text is
+ still buffered is rejected so no pending input is silently dropped.
+* An utterance is the flush unit, not a linguistic one: it is whatever text was
+ buffered when `input.done` arrived, of any length, synthesized as one request.
+ `utterance_index` counts those flushes across the connection, so it tells you
+ which `input.done` a frame belongs to. `sentence_index` counts within one
+ flush and so pairs with `total_sentences`, which means every utterance reports
+ `sentence_index: 0` of `total_sentences: 1` (or `0` for an empty buffer).
+* End the connection with `session.close`, or by closing the socket. An idle
+ connection is still closed after the server's idle timeout, which now also
+ applies to the gap between utterances.
+
### Session Config Parameters
All REST API parameters are supported, plus:
diff --git a/docs/serving/videos_api.md b/docs/serving/videos_api.md
index 72d79c304..6a94f9dab 100644
--- a/docs/serving/videos_api.md
+++ b/docs/serving/videos_api.md
@@ -73,7 +73,7 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
| `audio_reference` | string | null | JSON-encoded audio reference for speech-to-video: `{"audio_url": "..."}` — supports HTTP(s) URLs or base64 data URLs |
| `width` | integer | model default | Output video width |
| `height` | integer | model default | Output video height |
-| `num_frames` | integer | model default | Number of generated frames |
+| `num_frames` | integer | 1 | Number of generated frames |
| `fps` | integer | model default | Output frames per second |
| `num_inference_steps` | integer | model default | Number of diffusion steps |
| `guidance_scale` | number | null | CFG guidance scale for the low-noise stage |
diff --git a/docs/source/architecture/qwen3-omni-async-output-step-gap.svg b/docs/source/architecture/qwen3-omni-async-output-step-gap.svg
new file mode 100644
index 000000000..f1ccea6ee
--- /dev/null
+++ b/docs/source/architecture/qwen3-omni-async-output-step-gap.svg
@@ -0,0 +1,150 @@
+
diff --git a/docs/user_guide/diffusion/attention_backends.md b/docs/user_guide/diffusion/attention_backends.md
index 50e02971f..7dc35b367 100644
--- a/docs/user_guide/diffusion/attention_backends.md
+++ b/docs/user_guide/diffusion/attention_backends.md
@@ -14,7 +14,8 @@ The full set of backends and their platform defaults is in the **Backend Options
| Value | Notes |
|---|---|
-| `FLASH_ATTN` | Wraps FlashAttention 2. Default on Hopper / Ada / Ampere when `flash-attn` is installed. |
+| `TRTLLM_ATTN` | FlashInfer's trtllm-gen FMHA (TensorRT-LLM's generated kernels, vendored by FlashInfer). Dense BF16, GQA native, `head_dim=128`. Datacenter Blackwell only (sm_100 / sm_103). Packed paths can provide `cu_seqlens` directly. Supports optional **Skip-Softmax** sparse attention — see below. Requires `flashinfer`. |
+| `FLASH_ATTN` | Wraps FlashAttention 4 on Blackwell when `flash-attn-4` is installed, then falls back to FlashAttention 3/2. Default on Hopper / Ada / Ampere when a compatible FlashAttention package is installed. |
| `CUDNN_ATTN` | Pins `sdpa_kernel([CUDNN_ATTENTION])`. Default on Blackwell (sm_10x / sm_12x) with cuDNN ≥ 9.5. Wins on mask-heavy DiTs (HunyuanVideo-1.5: 2× e2e vs SDPA). |
| `FLASHINFER_ATTN` | Calls FlashInfer's dense `single_prefill_with_kv_cache` directly with `custom_mask` for non-causal masked attention. Used as Blackwell fallback when cuDNN is unavailable. Requires `flashinfer`. |
| `TORCH_SDPA` | PyTorch `scaled_dot_product_attention` with the default backend dispatcher. Most conservative; always available. |
@@ -22,6 +23,7 @@ The full set of backends and their platform defaults is in the **Backend Options
| `SAGE_ATTN_3` | Requires `sageattn3` from `SageAttention/sageattention3_blackwell`. CUDA only, intended for Blackwell GPUs, with GQA/MQA requests falling back to PyTorch SDPA. |
| `FLASH_ATTN_HUB` | FlashAttention 2 from HuggingFace `kernels` library. Useful for train/rollout alignment. |
| `FLASH_ATTN_3_HUB` | FlashAttention 3 from HuggingFace `kernels` library. CUDA Hopper (sm_90+) only; falls back to `FLASH_ATTN_HUB` on older GPUs. |
+| `RAINFUSION_ATTN` | MindIE-SD **RainFusion** block-sparse video attention — see [below](#rainfusion_attn-backend-and-block-sparse-video-attention). Ascend NPU only; requires `mindiesd`. Delegates to `FLASH_ATTN` for anything that is not a packed video sequence. |
## Configuration
@@ -68,11 +70,13 @@ vllm-omni serve \
--diffusion-attention-config '{"default":{"backend":"FLASH_ATTN"},"per_role":{"cross":{"backend":"TORCH_SDPA"}}}'
```
-Backends may also accept backend-specific parameters via `extra`:
+A backend that needs configuration exposes it as a typed field on the spec:
+`TRTLLM_ATTN`'s Skip-Softmax (see [below](#trtllm_attn-backend-and-skip-softmax)) and
+`RAINFUSION_ATTN`'s RainFusion block (see [below](#rainfusion_attn-backend-and-block-sparse-video-attention)):
```bash
---diffusion-attention-config.per_role.self.backend SPARSE_BLOCK \
---diffusion-attention-config.per_role.self.extra.block_size 128
+--diffusion-attention-config.default.backend TRTLLM_ATTN \
+--diffusion-attention-config.default.skip_softmax.target_sparsity 0.5
```
### Programmatic API
@@ -101,10 +105,15 @@ A plain dict is also accepted and normalized to `AttentionConfig`.
Auto-route preference, in order:
-1. `CUDNN_ATTN` — when cuDNN ≥ 9.5 is available (ships in PyTorch 2.5+ wheels)
-2. `FLASHINFER_ATTN` — when `flashinfer` is installed but cuDNN < 9.5
-3. `FLASH_ATTN` — when `flash-attn` is installed with the Blackwell CUTE kernel
-4. `TORCH_SDPA` — last resort
+1. `TRTLLM_ATTN` — on **datacenter** Blackwell (sm_100 / sm_103) when `flashinfer` is installed, the model's `head_dim` is 128, **and the model declares a compatible packed/mask-free path**
+2. `CUDNN_ATTN` — when cuDNN ≥ 9.5 is available (ships in PyTorch 2.5+ wheels)
+3. `FLASHINFER_ATTN` — when `flashinfer` is installed but cuDNN < 9.5
+4. `FLASH_ATTN` — when `flash-attn` is installed with the Blackwell CUTE kernel
+5. `TORCH_SDPA` — last resort
+
+`TRTLLM_ATTN` is skipped on workstation Blackwell (sm_120 / sm_121) and for any `head_dim != 128`, so those GPUs keep the `CUDNN_ATTN` route described below.
+
+`TRTLLM_ATTN` outranks `CUDNN_ATTN` on datacenter Blackwell for compatible packed/mask-free pipelines. Workstation Blackwell (sm_120 / sm_121) and pipelines that require attention masks retain their normal fallback.
The startup log line `Defaulting to diffusion attention backend CUDNN_ATTN (Blackwell sm_120, cuDNN 91002)` confirms the route.
@@ -119,6 +128,158 @@ Auto-route preference:
`CUDNN_ATTN` and `FLASHINFER_ATTN` are still selectable via env var on these GPUs but are not in the auto-route — FlashAttention 2 is the well-tuned path on pre-Blackwell hardware.
+## TRTLLM_ATTN Backend and Skip-Softmax
+
+`TRTLLM_ATTN` runs FlashInfer's trtllm-gen FMHA. Dense it is a BF16 backend like the others; on top of
+that it can enable **Skip-Softmax**, a sparse-attention mode that trades a little fidelity for speed
+(algorithm: [design doc](../../design/feature/skip_softmax.md)).
+
+Enable it through the typed `skip_softmax` block on the attention spec:
+
+| Key | Valid values | Meaning |
+|---|---|---|
+| `target_sparsity` | finite, `[0, 1]` | Operating point on a calibrated curve. Needs a calibration for the model. |
+| `threshold` | finite, `≥ 0` | Direct threshold, no calibration needed. Mutually exclusive with `target_sparsity`. |
+| `disabled_until_timestep` | finite, `[0, 1]` | Holds early, high-noise steps dense; skip turns on once the normalized timestep `t ≤ D`. |
+
+```bash
+vllm-omni serve Wan-AI/Wan2.2-T2V-A14B-Diffusers \
+ --diffusion-attention-config '{"default": {"backend": "TRTLLM_ATTN",
+ "skip_softmax": {"target_sparsity": 0.65, "disabled_until_timestep": 0.86}}}'
+```
+
+Programmatically the same block is a typed `SkipSoftmaxSpec` (values validated at construction):
+
+```python
+from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, SkipSoftmaxSpec
+
+AttentionConfig(
+ default=AttentionSpec(
+ backend="TRTLLM_ATTN",
+ skip_softmax=SkipSoftmaxSpec(target_sparsity=0.65, disabled_until_timestep=0.86),
+ ),
+)
+```
+
+**Start at `target_sparsity=0.65, disabled_until_timestep=0.86`** and raise `target_sparsity` only
+as far as your quality bar allows — the output diverges from dense as it climbs. The gain grows with
+sequence length, since Skip-Softmax only accelerates attention. Dense BF16 is `TRTLLM_ATTN` with no
+`skip_softmax` block.
+
+Requires datacenter Blackwell with `head_dim == 128`; elsewhere, or without FlashInfer, selecting
+`TRTLLM_ATTN` raises rather than silently degrading.
+
+## TRTLLM_ATTN SAGE Quantization
+
+On top of dense/Skip-Softmax, `TRTLLM_ATTN` can run **SAGE** (SageAttention) — the trtllm-gen
+FMHA kernel with per-block quantized Q/K and per-channel fp8 V, so the two attention matmuls run
+in low precision. Enable it through the typed `quant` block on the attention spec:
+
+| Key | Valid values | Meaning |
+|---|---|---|
+| `dtype_qk` | `int8`, `fp8_e4m3` | Quantization dtype for Q and K. Absent ⇒ dense (SAGE off). |
+| `q_block_size` | `1`, `4`, `16` | Per-token block size for Q scales (default `1`). |
+| `k_block_size` | `1`, `4`, `16` | Per-token block size for K scales (default `16`). |
+
+V is always quantized per-channel to fp8_e4m3 and K-smoothing is applied inside the routine, so
+neither is a user knob. Only block sizes `1`, `4`, `16` have compiled kernels.
+
+The `quant` block is a shared `AttnQuantSpec` consumed by more than one backend; each reads the
+fields that apply and rejects the rest. `TRTLLM_ATTN` (SAGE) reads `dtype_qk` (`int8` / `fp8_e4m3`),
+`q_block_size`, and `k_block_size`; `FLASHINFER_ATTN` reads `dtype_qk` (`float16` / `bfloat16`),
+`dtype_vo`, and `flashinfer_backend` (see its section). A `dtype_qk` from the wrong set raises.
+
+```bash
+vllm-omni serve Wan-AI/Wan2.2-T2V-A14B-Diffusers \
+ --diffusion-attention-config '{"default": {"backend": "TRTLLM_ATTN",
+ "quant": {"dtype_qk": "fp8_e4m3", "q_block_size": 1, "k_block_size": 16}}}'
+```
+
+```python
+from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, AttnQuantSpec
+
+AttentionConfig(
+ default=AttentionSpec(
+ backend="TRTLLM_ATTN",
+ quant=AttnQuantSpec(dtype_qk="fp8_e4m3", q_block_size=1, k_block_size=16),
+ ),
+)
+```
+
+**Requirements.** Needs FlashInfer ≥ 0.6.16rc1. Kernel
+availability is arch-dependent: `fp8_e4m3` QK has kernels on both **SM100** (B200) and **SM103**
+(B300); `int8` QK kernels are compiled for **SM100 only**.
+
+## RAINFUSION_ATTN Backend and Block-Sparse Video Attention
+
+`RAINFUSION_ATTN` runs MindIE-SD's RainFusion (`rf_v2`) kernel on Ascend NPU. It pools each
+128-token block of keys, ranks them per query block, and computes attention against only the
+top-scoring ones. Video tokens are rearranged into `(t, h, w)` order first, so the kept blocks are
+spatiotemporal neighbours rather than arbitrary rows of the packed sequence.
+
+Only the video segment is sparsified. The prefix rows (text, visual conditions, audio) and the
+first-frame blocks are always kept dense, which is why the realized sparsity is lower than the
+nominal `sparsity` you configure. Anything the kernel cannot handle — a warmup denoise step, an
+exempt layer, a sequence with no published video geometry, a video segment under 32 blocks —
+delegates to `FLASH_ATTN`, so a model can select this backend unconditionally.
+
+Enable it through the typed `block_sparse` block on the attention spec, shared by every
+block-sparse backend:
+
+| Key | Valid values | Meaning |
+|---|---|---|
+| `sparsity` | finite, `[0, 1]` | Nominal fraction of key blocks dropped per query block. `0` disables sparsity. Defaults to `0.8`. |
+| `start_step` | `≥ 0` | Keep the first N denoise steps dense. Layout is decided early, so these steps dominate structural fidelity. |
+| `skip_layers` | index selector, e.g. `"0-3,38"` | DiT blocks that always stay dense. |
+
+```bash
+vllm-omni serve MiniMaxAI/MiniMax-H3 \
+ --diffusion-attention-config '{"default": {"backend": "RAINFUSION_ATTN",
+ "block_sparse": {"sparsity": 0.8, "start_step": 0}}}'
+```
+
+Programmatically the same block is a typed `BlockSparseSpec` (values validated at construction):
+
+```python
+from vllm_omni.diffusion.data import AttentionConfig, AttentionSpec, BlockSparseSpec
+
+AttentionConfig(
+ default=AttentionSpec(
+ backend="RAINFUSION_ATTN",
+ block_sparse=BlockSparseSpec(sparsity=0.8, start_step=0, skip_layers="0-1"),
+ ),
+)
+```
+
+**Tune in the order `start_step` → `sparsity` → `skip_layers`.** Raise `start_step` first: it is the
+cheapest way to recover structure, because the early high-noise steps decide global layout while the
+later steps only refine texture. Then walk `sparsity` up to your quality bar. Reach for
+`skip_layers` last, once an A/B against dense at the same seed points at specific blocks.
+
+Requires Ascend NPU with `mindiesd`; selecting it on any other platform raises. It is also
+incompatible with ring sequence parallelism, since `rf_v2` needs the whole key sequence to rank
+blocks — use Ulysses SP (`ring_degree=1`).
+
+### Which geometries run sparse
+
+Sparsity is only applied when the video segment is a **multiple of 128 rows**, where the row count
+is `latent_t × (height / 32) × (width / 32)`. Otherwise `rf_v2`'s block mask and the kernel's own
+tiling disagree on the block count, and the block straddling the seam mixes video and prefix rows
+that selection may then drop. Any resolution still runs — an unaligned geometry falls back to dense
+attention and logs `RAINFUSION_ATTN staying dense` with the row count it computed — but it gets no
+speedup, so pick an aligned geometry when you want one.
+
+Alignment is necessary, not sufficient, for good quality. `rf_v2` groups video positions into 8x8
+spatial tiles (two tiles fill one 128-row block), which is what makes a selected block a compact
+patch of the frame. When the latent `h` or `w` is not a multiple of 8, the leftover rows or columns
+are peeled off and appended as a flat run instead of being tiled, so they get pooled with spatially
+distant positions and selection can no longer rank them meaningfully. This is silent, and invisible
+to a `sparsity=0` check, because a fully populated mask does not care how blocks are grouped.
+
+"Multiple of 8" on the latent grid means **width and height that are multiples of 256**. Aligned
+resolutions off that grid still run sparse; they just lose more fidelity at the same `sparsity`,
+which you can buy back with `start_step`.
+
## End-to-End Benchmark (BF16, sm_120 RTX Pro 6000 Blackwell)
Same prompt and seed across runs. `Total generation time` from `text_to_video.py` / `text_to_image.py`.
@@ -145,9 +306,17 @@ DIFFUSION_ATTENTION_BACKEND=FLASHINFER_ATTN python examples/offline_inference/te
--model Lightricks/LTX-2 ...
```
-### FA4 not yet integrated
+### FlashAttention-4 on Blackwell
-FlashAttention-4 (released March 2026) targets Blackwell natively and reportedly beats cuDNN by ~20% on B200. As of this writing the `flash-attn-4 4.0.0b10` wheel crashes with `AttributeError: 'NoneType' object has no attribute '_trait'` during JIT on sm_120. Not yet wired into vLLM-Omni; revisit when stable lands.
+Install the optional CUDA 13 extra to use the CuTe-based FA4 path:
+
+```bash
+pip install 'vllm-omni[fa4]'
+```
+
+`FLASH_ATTN` prefers `flash_attn.cute` on Blackwell and falls back to FA3/FA2
+when it is unavailable. Version `4.0.0b18` is required; earlier beta
+wheels had known JIT failures on Blackwell.
## Choosing a Backend Manually
@@ -305,64 +474,3 @@ DIFFUSION_ATTENTION_BACKEND=SAGE_ATTN_3 python examples/offline_inference/text_t
--tensor-parallel-size 2 \
--output outputs/hv15_sage3.mp4
```
-
-### Mixed backends across roles
-
-Use `FLASH_ATTN` for self-attention and `TORCH_SDPA` for cross-attention:
-
-```bash
-python examples/offline_inference/text_to_video/text_to_video.py \
- --model Wan-AI/Wan2.2-TI2V-5B-Diffusers \
- --prompt "A dog running across a field of golden wheat." \
- --diffusion-attention-config.per_role.self.backend FLASH_ATTN \
- --diffusion-attention-config.per_role.cross.backend TORCH_SDPA \
- --tensor-parallel-size 2 \
- --output outputs/wan22_mixed.mp4
-```
-
-### Compare against FlashAttention
-
-Unset the backend override, or explicitly use `FLASH_ATTN`:
-
-```bash
-python examples/offline_inference/text_to_video/text_to_video.py \
- --model Wan-AI/Wan2.2-TI2V-5B-Diffusers \
- --prompt "A dog running across a field of golden wheat." \
- --height 704 --width 1280 --num-frames 49 \
- --num-inference-steps 30 --seed 42 --guidance-scale 5.0 \
- --tensor-parallel-size 2 \
- --output outputs/wan22_fa3.mp4
-```
-
-## Validation Guidance
-
-Don't assume a faster attention backend is numerically interchangeable with `TORCH_SDPA`.
-
-Always compare:
-
-- End-to-end runtime
-- Diffusion-stage runtime (`add_req_and_wait` line in DiffusionEngine.step breakdown)
-- Output quality against a known-good baseline (CLIP similarity, frame-level diff, or visual review)
-
-At minimum, keep the same:
-
-- model
-- prompt
-- seed
-- resolution
-- frame count / step count
-- parallel config (TP / CFG-parallel / Ulysses degrees)
-
-## Reproducing the Benchmark Table
-
-The end-to-end numbers above were collected by running `text_to_video.py` /
-`text_to_image.py` with the same prompt and seed while varying
-`DIFFUSION_ATTENTION_BACKEND`. For a quick kernel-level comparison of the
-backends without loading a model:
-
-```bash
-python benchmarks/diffusion/bench_attention_backends.py --preset hv15
-```
-
-It runs all three BF16 backends on representative DiT attention shapes and
-prints a ranking table at the end.
diff --git a/docs/user_guide/diffusion/cpu_offload_diffusion.md b/docs/user_guide/diffusion/cpu_offload.md
similarity index 56%
rename from docs/user_guide/diffusion/cpu_offload_diffusion.md
rename to docs/user_guide/diffusion/cpu_offload.md
index d6fcd8944..02ab9f074 100644
--- a/docs/user_guide/diffusion/cpu_offload_diffusion.md
+++ b/docs/user_guide/diffusion/cpu_offload.md
@@ -179,7 +179,145 @@ class Flux2Transformer2DModel(nn.Module):
- Support single GPU only for now
-### Implementation Notes
+## Distributed Layerwise Offloading
+
+### How It Works
+
+Distributed layerwise offloading extends single-GPU layerwise offloading to
+multi-device deployments. In the default mode, each rank stores only
+**1/dp_size** of the model weights in host memory; full layer weights are
+reconstructed at runtime via **AllGather** on a dedicated communication stream,
+overlapped with computation via a fixed double-buffer scheme. The
+`--dlo-no-use-allgather` mode instead streams standard-loader rank-local
+weights independently and does not shard weights across ranks.
+
+For the implementation design and compatibility matrix, see
+[Distributed Layerwise Offload](../../design/feature/distributed_layerwise_offload.md).
+
+**Key features:**
+- **Weight sharding + AllGather**: each rank stores 1/dp_size of weights,
+ reconstructed per-layer via `all_gather_into_tensor`
+- **Fixed double-buffer**: exactly 2 transformer blocks on each device at any
+ time, regardless of model size
+- **DP multi-concurrency**: N concurrent requests processed in parallel
+ (AllGather only gathers weight shards, which are request-independent)
+- **mmap weight loading**: weights loaded as mmap views pointing to shared OS
+ page cache, eliminating O(dp_size × model_size) RSS during model creation
+- **Platform-agnostic**: works on NVIDIA GPU (CUDA/NCCL) and Ascend NPU
+ (CANN/HCCL) via vLLM-Omni's platform abstraction
+
+**Execution Flow (per device):**
+
+```
+Compute Stream: [Layer N] [Layer N+1] [Layer N+2]
+H2D Stream: [H2D Shard N+1] [H2D Shard N+2]
+AllGather: [AG N+1] [AG N+2]
+Slot Usage: Slot0: Layer N Slot1: Layer N+1
+```
+
+### Usage
+
+**CLI:**
+```bash
+# 4× GPU/NPU with AllGather (recommended)
+vllm serve /path/to/model --omni \
+ --enable-distributed-layerwise-offload \
+ --data-parallel-size 4
+
+# Without DLO AllGather (each rank streams standard-loader rank-local weights)
+vllm serve /path/to/model --omni \
+ --enable-distributed-layerwise-offload \
+ --data-parallel-size 4 \
+ --dlo-no-use-allgather
+
+# With SP instead of DP (long sequences)
+vllm serve /path/to/model --omni \
+ --enable-distributed-layerwise-offload \
+ --usp 4
+```
+
+**Python API:**
+```python
+from vllm_omni import Omni
+
+m = Omni(
+ model="/path/to/model",
+ enable_distributed_layerwise_offload=True,
+ dlo_use_allgather=True, # default
+)
+```
+
+### CLI Flags
+
+| Flag | Description | Default |
+|------|-------------|---------|
+| `--enable-distributed-layerwise-offload` | Enable DLO | `false` |
+| `--data-parallel-size N` | Number of DP ranks; DLO weight-sharding group when AllGather is enabled | `1` |
+| `--dlo-use-allgather` | Shard + AllGather (saves CPU, requires concurrent requests) | `true` |
+| `--dlo-no-use-allgather` | Use standard-loader rank-local weights; no DLO sharding or AllGather | `false` |
+
+### How model weights are loaded (mmap path)
+
+When DLO + AllGather is active, the offloader:
+
+1. **Saves non-persistent buffers** (e.g. RoPE `inv_freq`, timestep `freqs`)
+ from the normally-created transformer
+2. **Converts to meta device** via `to_empty(device="meta")`, releasing random
+ initialization weights
+3. **Loads checkpoint weights as mmap views** via `safe_open().get_tensor()`,
+ which return views into the OS page cache (shared across all ranks, 0 RSS)
+4. **Calls `post_load_weights()`** to apply model-specific dtype conversions
+ (e.g. Cosmos3's `time_embedder` → FP32)
+5. **Restores non-persistent buffers** from saved copies
+
+This approach requires **zero model-specific code changes** — no pipeline or
+transformer modifications are needed.
+
+### OffloadPlan (declarative topology metadata)
+
+Models can optionally declare an `OffloadPlan` class variable to provide
+topology metadata (block attribute names, submodules to offload) without
+any offload-specific logic:
+
+```python
+from vllm_omni.diffusion.offloader import OffloadPlan
+
+class MyPipeline(nn.Module):
+ _dit_modules = ["transformer"]
+ _offload_plan = OffloadPlan(
+ block_attrs={"transformer": ("blocks",)},
+ offload_submodules={"context_encoder": "layers"},
+ )
+```
+
+When not declared, the offloader falls back to `_layerwise_offload_blocks_attrs`
+and heuristic attribute search. This is backward-compatible — existing models
+work without any changes.
+
+### DP Multi-concurrency
+
+When `--data-parallel-size > 1` and AllGather is enabled, the scheduler batches
+up to `dp_size` requests per denoise step. Each DP rank processes a different
+request while AllGather synchronizes only weight shards (request-independent).
+
+**Requirements for concurrent requests:**
+- `num_inference_steps` must be specified explicitly (None is not allowed)
+- All concurrent requests must have the same `num_inference_steps` value
+- These constraints exist because AllGather is a collective that requires
+ every rank to participate at each denoise step
+
+### Limitations
+- Online quantization (FP8) is incompatible with mmap loading — falls back to
+ regular loading only when the model is outside the DLO+AllGather mmap path;
+ online quantization with DLO+AllGather is rejected. Use
+ `--dlo-no-use-allgather` or disable online quantization.
+- Tensor Parallel > 1 is rejected in the DLO+AllGather mmap path because TP-aware
+ weight-loader callbacks are bypassed. The no-AllGather path retains the
+ standard TP loader and is experimental.
+- HSDP + AllGather is rejected because it would double-shard parameters. HSDP
+ with no-AllGather is accepted at configuration level, but has limited
+ end-to-end validation.
+- `num_inference_steps=None` is not allowed in DP multi-concurrency mode
**Module Discovery**
@@ -208,7 +346,10 @@ OffloadBackend (base class)
├── ModelLevelOffloadBackend → uses SequentialOffloadHook (.to() swap)
│ (delegates to a pipeline's enable_omni_model_cpu_offload
│ for split models like Cosmos3)
-└── LayerWiseOffloadBackend → uses LayerwiseOffloadHook
+├── LayerWiseOffloadBackend → uses LayerwiseOffloadHook
+│ (single-GPU, full weights on host)
+└── DistributedLayerwiseOffloadBackend → uses DistributedLayerwiseOffloadHook
+ (multi-GPU, sharded AllGather or rank-local mode)
```
Factory function `get_offload_backend()` selects the appropriate backend based on
@@ -221,19 +362,28 @@ reasoner/generator components inside the model forward pass.
## Supported Models
-| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Blocks Attrs (Layerwise specific) |
-|--------------|----------------|-----------|---------------------|-------------------|-----------------------------------|
-| LongCatImagePipeline | `meituan-longcat/LongCat-Image` | `LongCatImageTransformer2DModel` | - | ✓ | `"transformer_blocks"`, `"single_transformer_blocks"` |
-| NextStep11Pipeline | `stepfun-ai/NextStep-1.1` | `NextStepModel` | - | ✓ | `"layers"` |
-| OvisImagePipeline | `AIDC-AI/Ovis-Image-7B` | `OvisImageTransformer2DModel` | - | ✓ | `"transformer"` |
-| QwenImagePipeline | `Qwen/Qwen-Image` | `QwenImageTransformer2DModel` | ✓ | ✓ | `"transformer_blocks"` |
-| StableDiffusionXLPipeline | `stabilityai/stable-diffusion-xl-base-1.0` | `SDXLUNet2DConditionModel` | ✓ | ✓ | `"down_blocks"`, `"up_blocks"` |
-| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | `"transformer_blocks"` |
-| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
-| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | `"blocks"` |
-| SoulXSingerPipeline / SoulXSingerSVCPipeline | `Soul-AILab/SoulX-Singer` | `DiffLlama` (`cfm_decoder.model.diff_estimator`) | ✓ | ✓ | `"layers"` |
-| BagelPipeline | `ByteDance-Seed/BAGEL-7B-MoT` | `Qwen2MoTModel` | - | ✓ | `"layers"`, `"customized modules"` |
+| Architecture | Example Models | DiT Class | Model-Level Offload | Layerwise Offload | Distributed Layerwise Offload | Blocks Attrs (Layerwise specific) |
+|--------------|----------------|-----------|---------------------|-------------------|-------------------------------|-----------------------------------|
+| Flux2Pipeline | `black-forest-labs/FLUX.2-dev` | `Flux2Transformer2DModel` | ✓ | ✓ | - | `"transformer_blocks"`, `"single_transformer_blocks"` |
+| LongCatImagePipeline | `meituan-longcat/LongCat-Image` | `LongCatImageTransformer2DModel` | - | ✓ | - | `"transformer_blocks"`, `"single_transformer_blocks"` |
+| NextStep11Pipeline | `stepfun-ai/NextStep-1.1` | `NextStepModel` | - | ✓ | - | `"layers"` |
+| OvisImagePipeline | `AIDC-AI/Ovis-Image-7B` | `OvisImageTransformer2DModel` | - | ✓ | - | `"transformer"` |
+| QwenImagePipeline | `Qwen/Qwen-Image` | `QwenImageTransformer2DModel` | ✓ | ✓ | - | `"transformer_blocks"` |
+| StableDiffusionXLPipeline | `stabilityai/stable-diffusion-xl-base-1.0` | `SDXLUNet2DConditionModel` | ✓ | ✓ | - | `"down_blocks"`, `"up_blocks"` |
+| StableDiffusion3Pipeline | `stabilityai/stable-diffusion-3.5-medium` | `SD3Transformer2DModel` | - | ✓ | - | `"transformer_blocks"` |
+| Wan22I2VPipeline | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
+| Wan22Pipeline | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | `WanTransformer3DModel` | ✓ | ✓ | - | `"blocks"` |
+| SoulXSingerPipeline / SoulXSingerSVCPipeline | `Soul-AILab/SoulX-Singer` | `DiffLlama` (`cfm_decoder.model.diff_estimator`) | ✓ | ✓ | - | `"layers"` |
+| BagelPipeline | `ByteDance-Seed/BAGEL-7B-MoT` | `Qwen2MoTModel` | - | ✓ | - | `"layers"`, `"customized modules"` |
+| Cosmos3OmniDiffusersPipeline | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | `Cosmos3VFMTransformer`, `Cosmos3LanguageModel` | ✓ | ✓ | ✓ | `"layers"`, `"gen_layers"` |
**Notes:**
- Model-Level Offloading is expected to be supported by all common diffusion models (DiT and encoders) naturally
- Layerwise Offloading requires DiT class to define `_layerwise_offload_blocks_attrs` pointing to transformer blocks
+- Distributed Layerwise Offloading reuses the layerwise block topology, but the
+ model and parallelism combination still needs validation. AllGather mode
+ additionally requires a compatible checkpoint/mmap layout; no-AllGather mode
+ retains the standard loader's rank-local tensors. See the
+ [DLO design](../../design/feature/distributed_layerwise_offload.md) and
+ [Cosmos3 DistOffload recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/cosmos3/Cosmos3-DistOffload.md)
+ for usage examples.
diff --git a/docs/user_guide/diffusion/execution_modes.md b/docs/user_guide/diffusion/execution_modes.md
new file mode 100644
index 000000000..9611a8210
--- /dev/null
+++ b/docs/user_guide/diffusion/execution_modes.md
@@ -0,0 +1,252 @@
+# Diffusion Execution Modes
+
+vLLM-Omni supports complete-request execution and step-wise diffusion
+execution. Both modes use the same asynchronous engine output stream; the
+configuration controls how work is scheduled and whether intermediate outputs
+are exposed.
+
+## Choose a Mode
+
+| Goal | CLI configuration |
+|---|---|
+| Serial request execution | `--max-num-seqs 1` |
+| Fused request-level batching | `--max-num-seqs N` |
+| Single-request step execution | `--step-execution --max-num-seqs 1` |
+| Step-wise continuous batching | `--step-execution --max-num-seqs N` |
+| Chunked diffusion output | `--diffusion-streaming-output` |
+
+`N` must be greater than `1` to allow batching. The selected pipeline must
+support request-level batching or batched-step execution as appropriate;
+single-request step support alone is not sufficient for step-wise continuous
+batching.
+
+## Request Execution
+
+Request execution is the default when `--step-execution` is omitted. The
+pipeline performs a complete `forward()` for each scheduler wave.
+
+### Serial Requests
+
+Use the conservative serial path with any request-mode pipeline:
+
+```bash
+vllm serve MODEL --omni \
+ --port 8091 \
+ --max-num-seqs 1
+```
+
+### Request-Level Batching
+
+Set `max_num_seqs` above one to combine compatible independent requests into a
+single pipeline forward:
+
+```bash
+vllm serve Qwen/Qwen-Image --omni \
+ --port 8091 \
+ --max-num-seqs 4
+```
+
+For bursty traffic, add a small admission window:
+
+```bash
+vllm serve Qwen/Qwen-Image --omni \
+ --port 8091 \
+ --max-num-seqs 4 \
+ --request-batch-max-wait-ms 20
+```
+
+`--request-batch-max-wait-ms 0` is the default. A nonzero value can improve
+batch formation but adds up to that much latency before a new scheduler wave.
+It has no effect in step mode.
+
+Each prompt remains a separate logical request. Do not submit a top-level list
+as one packed prompt. Concurrent serving requests are batched internally when
+their shapes, guidance settings, output counts, and LoRA settings are
+compatible.
+
+Only pipelines that declare request-batch support accept
+`max_num_seqs>1` in request mode. Unsupported pipelines fail during engine
+initialization; use `max_num_seqs=1` instead.
+
+## Step Execution
+
+Step execution lets the scheduler advance and abort requests between denoise
+steps:
+
+```bash
+vllm serve Qwen/Qwen-Image --omni \
+ --port 8091 \
+ --step-execution \
+ --max-num-seqs 1
+```
+
+Set a larger capacity to allow compatible requests to share denoise waves:
+
+```bash
+vllm serve Qwen/Qwen-Image --omni \
+ --port 8091 \
+ --step-execution \
+ --max-num-seqs 8
+```
+
+Step continuous batching is experimental. Start with `max_num_seqs=1` when
+validating a model or debugging correctness, then increase it for
+multi-request throughput.
+
+Step execution is capability-based, not a generic switch for every diffusion
+model. Qwen-Image supports step-wise continuous batching. HunyuanImage3 also
+supports it, but only when its resolved self-attention backend is `TORCH_SDPA`;
+set `DIFFUSION_ATTENTION_BACKEND=TORCH_SDPA` or configure
+`diffusion_attention_config.default.backend=TORCH_SDPA` before using
+`--max-num-seqs >1`. See the
+[HunyuanImage-3.0 recipe](https://github.com/vllm-project/vllm-omni/blob/main/recipes/Tencent/HunyuanImage-3.0-Instruct.md)
+for its validated configuration. Helios supports single-request step execution only: use
+`--step-execution --max-num-seqs 1` for Helios. Consult the selected pipeline's
+documentation and source for the latest support status.
+
+## Streaming Output
+
+Use `--diffusion-streaming-output` for a pipeline that can produce intermediate
+diffusion outputs:
+
+```bash
+vllm serve BestWishYsh/Helios-Distilled --omni \
+ --port 8000 \
+ --diffusion-streaming-output
+```
+
+Streaming output requires step execution. If
+`--diffusion-streaming-output` is set without `--step-execution`, the engine
+enables step execution automatically. Model initialization fails if the
+pipeline does not implement step execution.
+
+Chunk-capable pipelines emit intermediate and final outputs through the same
+request stream. Final-only step pipelines emit only the final result even when
+streaming output is enabled.
+
+The normal non-streaming serving path uses the same internal stream but drains
+it and returns only the final output. Users do not need to enable
+`--diffusion-streaming-output` to benefit from the unified engine lifecycle.
+
+## Send Requests
+
+Execution-mode flags configure the server. They do not change the
+OpenAI-compatible client request format. For example, after starting any of
+the non-streaming configurations above:
+
+```bash
+curl -X POST http://localhost:8091/v1/images/generations \
+ -H "Content-Type: application/json" \
+ -d '{
+ "prompt": "a cat sitting on a windowsill",
+ "size": "1024x1024",
+ "num_inference_steps": 50,
+ "seed": 42
+ }' | jq -r '.data[0].b64_json' | base64 -d > cat.png
+```
+
+To create a request-level batch, send multiple independent requests
+concurrently. Do not put several prompts into one `prompt` field:
+
+```bash
+for prompt in "a red fox" "a blue bird" "a green frog"; do
+ curl -s -X POST http://localhost:8091/v1/images/generations \
+ -H "Content-Type: application/json" \
+ -d "{\"prompt\":\"${prompt}\",\"size\":\"1024x1024\",\"seed\":42}" \
+ > /dev/null &
+done
+wait
+```
+
+The scheduler may batch these requests when their sampling parameters are
+compatible. See the
+[Image Generation API](../../serving/image_generation_api.md) for response
+formats and additional client examples. Streaming models can use
+model-specific streaming endpoints documented by their serving guide.
+
+## Python API
+
+The Python arguments mirror the CLI flags:
+
+```python
+from vllm_omni import Omni
+
+omni = Omni(
+ model="Qwen/Qwen-Image",
+ max_num_seqs=4,
+ request_batch_max_wait_ms=20.0,
+)
+
+outputs = omni.generate(
+ [
+ "a cup of coffee on a table",
+ "a toy dinosaur on a sandy beach",
+ "a fox waking up in bed and yawning",
+ ]
+)
+```
+
+Each list item passed to `Omni.generate()` becomes an independent logical
+request that the scheduler may batch with compatible items.
+
+For step execution:
+
+```python
+from vllm_omni import Omni
+from vllm_omni.inputs.data import OmniDiffusionSamplingParams
+
+omni = Omni(
+ model="Qwen/Qwen-Image",
+ step_execution=True,
+ max_num_seqs=1,
+)
+
+outputs = omni.generate(
+ "A cat sitting on a windowsill",
+ OmniDiffusionSamplingParams(num_inference_steps=50),
+)
+```
+
+## Stage Configuration
+
+Use the equivalent engine arguments in a deployment YAML:
+
+```yaml
+stage_args:
+ - stage_id: 0
+ stage_type: diffusion
+ engine_args:
+ step_execution: false
+ max_num_seqs: 4
+ request_batch_max_wait_ms: 20
+```
+
+For step execution, set `step_execution: true` and remove
+`request_batch_max_wait_ms`.
+
+## CLI Reference
+
+| Flag | Default | Effect |
+|---|---:|---|
+| `--step-execution` | disabled | Select step-wise scheduling |
+| `--max-num-seqs` | `1` for diffusion stages | Set request- or step-scheduler capacity |
+| `--request-batch-max-wait-ms` | `0` | Wait for burst coalescing in request mode |
+| `--diffusion-streaming-output` | disabled | Expose supported intermediate diffusion outputs and require step execution |
+
+## Limitations and Troubleshooting
+
+- Requests batch only when their compatibility-sensitive parameters match.
+- Different LoRA adapters or scales run in separate batches.
+- FIFO scheduling can cause an incompatible request to block later compatible
+ requests.
+- All diffusion cache backends are unsupported in step mode. KV transfer is
+ supported for newly admitted step requests; some other request-mode extras
+ remain unsupported.
+- If request-mode startup reports that the pipeline does not support batching,
+ use `--max-num-seqs 1`.
+- If step-mode startup mentions `prepare_encode()`, `denoise_step()`,
+ `step_scheduler()`, or `post_decode()`, the pipeline does not implement the
+ required step contract.
+
+For implementation details and model-author guidance, see
+[Diffusion Continuous Batching](../../design/feature/diffusion_continuous_batching.md).
diff --git a/docs/user_guide/diffusion/regional_compilation.md b/docs/user_guide/diffusion/regional_compilation.md
new file mode 100644
index 000000000..469a62b2c
--- /dev/null
+++ b/docs/user_guide/diffusion/regional_compilation.md
@@ -0,0 +1,38 @@
+# Regional Compilation
+
+Regional compilation applies `torch.compile` to the repeated transformer blocks
+declared by a diffusion model. It is the default compilation scope when
+diffusion inference runs without `--enforce-eager`.
+
+## Configuration
+
+Dynamic compilation is enabled by default so the compiled regions can handle
+mixed resolutions. For a fixed-shape workload, disable it explicitly:
+
+```bash
+vllm serve --omni --no-diffusion-compile-dynamic
+```
+
+The equivalent per-stage deploy configuration is:
+
+```yaml
+stages:
+ - stage_id: 0
+ diffusion_compile_granularity: regional
+ diffusion_compile_dynamic: false
+```
+
+For an experimental whole-transformer compile scope, set
+`--diffusion-compile-granularity full` or use
+`diffusion_compile_granularity: full` in the stage configuration. Full scope may
+still contain graph breaks; it does not force one graph. It is rejected when
+HSDP, sequence parallelism, CPU offload, or layerwise offload is enabled. Use
+regional scope with those features.
+
+These settings control the generic model-runner compilation path. Pipelines
+that provide their own `setup_compile()` implementation manage their compilation
+policy independently. Compilation is lazy, so backend or graph errors can first
+surface on the initial request.
+
+Use `--enforce-eager` to disable the model runner's generic compile setup.
+Pipelines that compile internally define their own eager-mode behavior.
diff --git a/docs/user_guide/diffusion/request_batching.md b/docs/user_guide/diffusion/request_batching.md
deleted file mode 100644
index 5cd24802b..000000000
--- a/docs/user_guide/diffusion/request_batching.md
+++ /dev/null
@@ -1,110 +0,0 @@
-# Request-Level Batching
-
-Request-level batching lets diffusion serving combine multiple compatible
-logical requests into one pipeline `forward()` call. Each prompt remains a
-separate request with its own `request_id`, sampling parameters, seed, output,
-error, and abort state. The scheduler decides which requests can run together.
-
-!!! warning "Prompt List Semantics"
-
- Diffusion request-level batching does not support a top-level packed
- list-prompt request. Submit multiple prompts as independent requests and let
- the scheduler batch compatible in-flight requests. Multimodal payloads stay
- inside a single prompt dict, for example
- `{"prompt": "...", "multi_modal_data": {"image": image}}`.
-
-## Enablement
-
-Increase `max_num_seqs` above `1` to allow the request scheduler to keep more
-than one compatible request active:
-
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --max-num-seqs 4
-```
-
-For bursty online traffic, you can also set a small admission wait window. This
-lets the engine wait briefly before the first `schedule()` of a new wave so
-nearby compatible requests can arrive and share the same fused forward pass:
-
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --max-num-seqs 4 \
- --request-batch-max-wait-ms 20
-```
-
-`--request-batch-max-wait-ms 0` is the default and disables admission waiting,
-so there is no added wait latency.
-
-For deploy YAMLs, configure the diffusion stage engine args:
-
-```yaml
-stage_args:
- - stage_id: 0
- stage_type: diffusion
- engine_args:
- max_num_seqs: 4
- request_batch_max_wait_ms: 20
-```
-
-## Compatibility
-
-Only pipelines that declare request-batch support use the fused request-batch
-path. The engine validates that the pipeline `forward()` uses the request-batch
-contract and returns `list[DiffusionOutput]`. Pipelines that do not support this
-contract do not use fused `pipeline.forward(batch)`; scheduled requests are
-executed through per-request worker calls.
-
-The scheduler batches only compatible requests. Compatibility is based on
-shape-sensitive and guidance-sensitive sampling fields, including resolution,
-frame count, CFG settings, output count, and LoRA identity. Requests with
-different LoRA adapters or scales are kept in separate batches so the worker
-activates one adapter per batch.
-
-Request-level batching applies when `step_execution=False`. For the separate
-step-wise runtime, see [Step Execution](step_execution.md).
-
-## Tuning
-
-- `max_num_seqs` caps the number of active compatible requests in one scheduler
- wave.
-- `request_batch_max_wait_ms` is an upper bound on extra admission wait before a
- new wave starts. Keep it small for latency-sensitive serving; values such as
- `10` to `50` ms are a practical starting range for bursty HTTP ingress.
-- `0` disables admission waiting and preserves the lowest first-request latency.
-- FIFO ordering is conservative: an incompatible request at the front of the
- waiting queue can block later compatible requests from joining the current
- batch.
-
-## Python API
-
-When constructing `Omni`, pass the same engine arguments:
-
-```python
-from vllm_omni.entrypoints.omni import Omni
-
-omni = Omni(
- model="Qwen/Qwen-Image",
- max_num_seqs=4,
- request_batch_max_wait_ms=20.0,
-)
-
-outputs = omni.generate(
- [
- "a cup of coffee on a table",
- "a toy dinosaur on a sandy beach",
- "a fox waking up in bed and yawning",
- ]
-)
-```
-
-`Omni.generate([...])` submits each list item as its own logical diffusion
-request. The runtime may batch those requests internally when their sampling
-parameters are compatible.
-
-## For Contributors
-
-For implementation details and model-author guidance, see
-[Request-Level Batching for Diffusion](../../design/feature/diffusion_request_level_batching.md).
diff --git a/docs/user_guide/diffusion/step_execution.md b/docs/user_guide/diffusion/step_execution.md
deleted file mode 100644
index 07184a787..000000000
--- a/docs/user_guide/diffusion/step_execution.md
+++ /dev/null
@@ -1,95 +0,0 @@
-# Step Execution
-
-Step execution is an opt-in diffusion execution mode enabled with
-`step_execution=True` when constructing `Omni`.
-
-It is not a generic diffusion toggle for every pipeline. Only pipelines that
-implement the stepwise contract support it today.
-
-## Quick Start
-
-### Python API
-
-```python
-from vllm_omni import Omni
-from vllm_omni.inputs.data import OmniDiffusionSamplingParams
-
-omni = Omni(
- model="Qwen/Qwen-Image",
- step_execution=True,
-)
-
-outputs = omni.generate(
- "A cat sitting on a windowsill",
- OmniDiffusionSamplingParams(
- num_inference_steps=50,
- ),
-)
-```
-
-### Serving
-
-```bash
-vllm serve Qwen/Qwen-Image --omni \
- --port 8091 \
- --step-execution \
- --max-num-seqs 8
-```
-
-For serving, `--step-execution` enables the step-wise runtime. Continuous
-batching only becomes relevant when `--max-num-seqs > 1`.
-
-## Supported Pipelines
-
-| Pipeline | Example models | Step execution |
-|----------|----------------|----------------|
-| `QwenImagePipeline` | `Qwen/Qwen-Image`, `Qwen/Qwen-Image-2512` | Yes |
-| All other diffusion pipelines | `QwenImageEditPipeline`, `QwenImageEditPlusPipeline`, `QwenImageLayeredPipeline`, GLM-Image, Wan, Flux, etc. | No |
-
-!!! warning "Experimental continuous batching"
- When `--step-execution` is enabled and `max_num_seqs > 1` is configured,
- the step-wise path can batch
- compatible requests together. This is experimental. Requests with
- incompatible sampling parameters are intentionally kept in separate batches,
- and `max_num_seqs=1` remains the conservative default.
-
-## Current Limitations
-
-- Continuous batching under `step_execution` is experimental and only batches
- compatible requests.
-- `cache_backend` is not supported together with step execution.
-- Unsupported pipelines fail early during model loading.
-- Request-mode extras such as KV transfer are not wired into step mode yet.
-- LoRA is supported in step mode, but each batch must use a single adapter:
- requests with different `lora_request` or `lora_scale` are scheduled into
- separate batches.
-
-## When To Use It
-
-Use step execution only when you specifically need the pipeline to run through
-its stepwise request state machine. For normal diffusion inference, leave it
-disabled unless your workflow depends on this mode.
-
-For Qwen-Image online serving, the usual progression is:
-
-- start with `--step-execution --max-num-seqs 1` if you only need the step-wise path
-- increase `--max-num-seqs` after that if you want the experimental compatible-request batching behavior
-
-If you are looking for general diffusion speedups, see
-[Diffusion Features Overview](../diffusion_features.md).
-
-## Troubleshooting
-
-If model loading fails with a message mentioning `prepare_encode()`,
-`denoise_step()`, `step_scheduler()`, and `post_decode()`, the selected
-pipeline does not support step execution.
-
-## For Model Authors
-
-If you want to add step execution support to a new diffusion pipeline, see the
-implementation guide:
-[Diffusion Step Execution Design](../../design/feature/diffusion_step_execution.md).
-
-If you also want that pipeline to participate in the experimental batched
-step-wise path, see:
-[Continuous Batching for Step-Wise Diffusion](../../design/feature/diffusion_continuous_batching.md).
diff --git a/docs/user_guide/diffusion_features.md b/docs/user_guide/diffusion_features.md
index d3ed3bd67..83bc3643e 100644
--- a/docs/user_guide/diffusion_features.md
+++ b/docs/user_guide/diffusion_features.md
@@ -59,7 +59,7 @@ Memory optimization methods help reduce GPU memory usage, enabling inference on
| Method | Description | Best For |
|--------|-------------|----------|
-| **[CPU Offload](diffusion/cpu_offload_diffusion.md)** | Offloads model components to CPU memory | Limited VRAM, large models on consumer GPUs |
+| **[CPU Offload](diffusion/cpu_offload.md)** | Offloads model components to CPU memory | Limited VRAM, large models on consumer GPUs |
| **[Quantization](quantization/overview.md)** | Reduces transformer stages from BF16 to FP8/INT8/etc. | Limited VRAM, minimal accuracy loss |
| **[VAE Parallelism](diffusion/parallelism/vae_parallelism.md)** | Distributes VAE decode work across GPUs | High-resolution generation with reduced VAE memory peak |
@@ -80,12 +80,11 @@ denoise steps.
| Method | Description | Best For |
|--------|-------------|----------|
-| **[Request-Level Batching](diffusion/request_batching.md)** | Scheduler batches compatible independent diffusion requests into one pipeline forward pass | Bursty online serving and multi-request throughput |
-| **[Step Execution](diffusion/step_execution.md)** | Per-step denoise execution with mid-request abort support | Request cancellation between denoise steps, fine-grained execution control |
+| **[Diffusion Execution Modes](diffusion/execution_modes.md)** | Configures serial requests, request batching, step execution, continuous batching, and streaming output | Matching latency, throughput, cancellation, and output-delivery requirements |
-**Note:** Request-level batching is available for pipelines that declare the
-request-batch forward contract. Step execution is currently supported by
-QwenImagePipeline only. See [Supported Models](#supported-models) for details.
+**Note:** Request-level batching and step execution are capability-based.
+Consult the execution guide and the selected pipeline's documentation for
+current support.
### Quantization Methods
@@ -105,8 +104,8 @@ The following tables show which models support each feature:
> Notes:
-> 1. CPU Offload has two methods: Module-wise (default for models with DiT + text encoder) and Layerwise. The tables below show **Layerwise support** only. Split models like Cosmos3 (no separate text encoder) swap their reasoner/generator components for module-wise offload; see the [CPU Offload Guide](diffusion/cpu_offload_diffusion.md).
-> 2. The **💾Quantization** column is collapsed for readability. See [Quantization Overview](quantization/overview.md) for per-method and per-model support details.
+> 1. CPU Offload has two methods: Module-wise (default for models with DiT + text encoder) and Layerwise. The tables below show **Layerwise support** only. Split models like Cosmos3 (no separate text encoder) swap their reasoner/generator components for module-wise offload; see the [CPU Offload Guide](diffusion/cpu_offload.md).
+> 2. The **💾Quantization** column is collapsed for readability. See [Quantization](quantization/overview.md) for per-method and per-model support details.
### ImageGen
@@ -116,8 +115,8 @@ The following tables show which models support each feature:
| **FLUX.1-dev** | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
| **FLUX.1-schnell** | ❌ | ✅ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
| **FLUX.2-klein** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
-| **FLUX.1-Kontext-dev** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
-| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
+| **FLUX.1-Kontext-dev** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
+| **FLUX.2-dev** | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ | ❌ | ❌ |
| **GLM-Image** | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| **Hidream-I1-Full** | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| **HunyuanImage3** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ |
@@ -154,13 +153,13 @@ The following tables show which models support each feature:
| **Wan2.2** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (encode/decode) | ❌ | ❌ |
| **Wan2.2-S2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ❌ | ❌ |
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
-| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
-| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ | ✅ (decode) | ❌ | ❌ |
+| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
+| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **Helios** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **HunyuanVideo-1.5 T2V I2V** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
| **DreamID-Omni** | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **Cosmos3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (encode/decode) | ✅ | ❌ |
-
+| **MiniMax-H3** | ❌ | ✅ | ✅ | ❌ | ✅ (DiT/TE) | ❌ | ✅ | ✅ | ✅ (tile) | ✅ (DiT) | ❌ |
**Frame Interpolation Support**
@@ -206,7 +205,7 @@ The following tables show which models support each feature:
3. CPU Offloading (Layerwise) and CPU Offloading (Module-wise) are not compatible.
4. CPU Offloading (Layerwise) supports single-card for now.
5. Using FP8-Quant as an example of qunatization methods.
- 6. Step Execution is not compatible with cache backends (TeaCache, Cache-DiT). LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
+ 6. Step Execution is not compatible with any diffusion cache backend. LoRA is supported, but each scheduled batch must use a single adapter (requests with different `lora_request` or `lora_scale` are kept in separate batches).
## Multi-Thread Weight Loading
@@ -275,9 +274,9 @@ Measured on NVIDIA H800:
**Memory Optimization:**
-- **[CPU Offload Guide](diffusion/cpu_offload_diffusion.md)** - Offload model components to CPU, reduce GPU memory usage
+- **[CPU Offload Guide](diffusion/cpu_offload.md)** - Offload model components to CPU, reduce GPU memory usage
- **[VAE Parallelism Guide](diffusion/parallelism/vae_parallelism.md)** - Distribute VAE decode work across GPUs for high-resolution images and videos
-- **[Quantization Overview](quantization/overview.md)** - Overview of quantization methods for diffusion, multi-stage omni/TTS, and multi-stage diffusion models
+- **[Quantization](quantization/overview.md)** - Quantization methods for diffusion, multi-stage omni/TTS, and multi-stage diffusion models
**Extensions:**
@@ -286,7 +285,7 @@ Measured on NVIDIA H800:
**Execution Modes:**
-- **[Step Execution Guide](diffusion/step_execution.md)** - Per-step denoise execution with mid-request abort support
+- **[Diffusion Execution Modes](diffusion/execution_modes.md)** - Configure request batching, step execution, continuous batching, and streaming output
**Startup Optimization:**
diff --git a/docs/user_guide/examples/offline_inference/bagel.md b/docs/user_guide/examples/offline_inference/bagel.md
deleted file mode 100644
index 391c41b35..000000000
--- a/docs/user_guide/examples/offline_inference/bagel.md
+++ /dev/null
@@ -1,287 +0,0 @@
-# BAGEL-7B-MoT
-
-Source .
-
-## Setup
-
-Please refer to the [stage configuration documentation](https://docs.vllm.ai/projects/vllm-omni/en/latest/configuration/stage_configs/) to configure memory allocation appropriately for your hardware setup.
-
-## Architecture
-
-BAGEL-7B-MoT is a Mixture-of-Transformers (MoT) model supporting both image generation and understanding. It offers two deployment topologies:
-
-| Topology | Stages | Description |
-| :------- | :----- | :---------- |
-| **Two-stage** (default) | Stage 0 (Thinker, AR) + Stage 1 (DiT, Diffusion) | Thinker handles text/understanding via vLLM AR engine; DiT handles image generation. KV cache is transferred between stages. |
-| **Single-stage** | Stage 0 (DiT, Diffusion) only | The DiT stage contains a full LLM, ViT, VAE, and tokenizer internally. All modalities are handled within a single diffusion process. |
-
-Both topologies support all four modalities: `text2img`, `img2img`, `img2text`, `text2text`.
-
-## Quick Start
-
-```bash
-cd examples/offline_inference/bagel
-
-# Default two-stage mode (auto-detected)
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat"
-
-# Single-stage mode
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat" \
- --deploy-config vllm_omni/deploy/bagel_single_stage.yaml
-```
-
-> **Note**: These examples work with the default configuration on an **NVIDIA A100 (80GB)**. For dual-GPU setups, modify the deploy YAML to distribute stages across devices.
-
-## Modality Control
-
-Control the mode using the `--modality` argument:
-
-| Modality | Input | Output | Description |
-| :------- | :---- | :----- | :---------- |
-| `text2img` | Text | Image | Generate images from text prompts |
-| `img2img` | Image + Text | Image | Transform images using text guidance |
-| `img2text` | Image + Text | Text | Generate text descriptions from images |
-| `text2text` | Text | Text | Pure text generation (language model mode) |
-
-### Text to Image (text2img)
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat" \
- --steps 50
-```
-
-### Image to Image (img2img)
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality img2img \
- --image-path /path/to/image.jpg \
- --prompts "Let the woman wear a blue dress"
-```
-
-### Image to Text (img2text)
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality img2text \
- --image-path /path/to/image.jpg \
- --prompts "Describe this image in detail"
-```
-
-### Text to Text (text2text)
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2text \
- --prompts "What is the capital of France?"
-
-# Load prompts from a text file (one prompt per line):
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2text \
- --txt-prompts /path/to/prompts.txt
-```
-
-## Think Mode
-
-Think mode enables the model to generate `...` planning/reasoning tokens before producing the final output. This improves generation quality for complex prompts.
-
-- **Two-stage**: The Thinker (AR) stage decodes think tokens, then transfers the augmented KV cache to the DiT stage for image generation.
-- **Single-stage**: The DiT's internal LLM generates think tokens in-place before proceeding to denoise.
-
-```bash
-# Think + text2img: plan before generating
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A futuristic city with flying cars" \
- --think \
- --max-think-tokens 1000
-
-# Think + img2img: reason about the edit
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality img2img \
- --image-path /path/to/image.jpg \
- --prompts "Make it look like a watercolor painting" \
- --think
-
-# Think + img2text: reason before describing
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality img2text \
- --image-path /path/to/image.jpg \
- --prompts "What is happening in this image?" \
- --think
-
-# Think + text2text: chain-of-thought reasoning
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2text \
- --prompts "Solve: 23 * 47" \
- --think
-```
-
-Think mode parameters:
-
-| Argument | Default | Description |
-| :------- | :------ | :---------- |
-| `--think` | `False` | Enable thinking mode |
-| `--max-think-tokens` | `1000` | Maximum tokens for think generation |
-| `--do-sample` | `False` | Enable sampling (vs. greedy) for text generation |
-| `--text-temperature` | `0.3` | Temperature for text generation sampling |
-
-## Classifier-Free Guidance (CFG)
-
-CFG controls the trade-off between prompt fidelity and diversity. These parameters apply to image generation modalities (`text2img`, `img2img`).
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A photorealistic portrait" \
- --cfg-text-scale 6.0 \
- --cfg-img-scale 2.0 \
- --negative-prompt "blurry, low quality, distorted" \
- --cfg-interval 0.4 1.0 \
- --cfg-renorm-type global \
- --cfg-renorm-min 0.0
-```
-
-| Argument | Default | Description |
-| :------- | :------ | :---------- |
-| `--cfg-text-scale` | `4.0` | Text CFG scale (higher = more prompt-adherent) |
-| `--cfg-img-scale` | `1.5` | Image CFG scale (for img2img) |
-| `--negative-prompt` | `None` | Negative prompt for CFG conditioning |
-| `--cfg-interval` | pipeline default | CFG active interval `[start, end]` as fractions of total timesteps |
-| `--cfg-renorm-type` | `None` | Renormalization type: `global`, `text_channel`, `channel` |
-| `--cfg-renorm-min` | `None` | Minimum renormalization value |
-| `--cfg-parallel-size` | `1` | CFG parallel size: `1` = batched (single GPU), `2` = 2-branch parallel, `3` = full 3-GPU parallel |
-
-## Deployment Topologies
-
-### Two-Stage (Default)
-
-The default topology auto-detected from the model. No extra flags needed.
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat"
-```
-
-The pipeline is defined in [`bagel.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel.yaml). Stage 0 (Thinker) and Stage 1 (DiT) share GPU 0 by default. For dual-GPU setups, customize the deploy YAML and set `devices: "1"` for stage 1.
-
-### Single-Stage
-
-Pass the single-stage deploy config via `--deploy-config`:
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat" \
- --deploy-config vllm_omni/deploy/bagel_single_stage.yaml
-```
-
-See [`bagel_single_stage.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel_single_stage.yaml) for configuration details. The `pipeline: bagel_single_stage` field selects the single-stage topology from the pipeline registry.
-
-### Tensor Parallelism (TP)
-
-For larger models or multi-GPU environments, customize the deploy YAML (see [`bagel.yaml`](https://github.com/vllm-project/vllm-omni/tree/main/vllm_omni/deploy/bagel.yaml)) and set per-stage `tensor_parallel_size` and `devices`:
-
-```yaml
-# Example: TP=2 on GPUs 0,1 for the Thinker stage
-stages:
- - stage_id: 0
- tensor_parallel_size: 2
- devices: "0,1"
-```
-
-Then pass the custom deploy YAML:
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat" \
- --deploy-config /path/to/custom_bagel.yaml
-```
-
-### FP8 Quantization
-
-```bash
-python end2end.py --model ByteDance-Seed/BAGEL-7B-MoT \
- --modality text2img \
- --prompts "A cute cat" \
- --quantization fp8
-```
-
-## Command Line Reference
-
-### Core Arguments
-
-| Argument | Type | Default | Description |
-| :------- | :--- | :------ | :---------- |
-| `--model` | string | `ByteDance-Seed/BAGEL-7B-MoT` | Model path or HuggingFace name |
-| `--modality` | choice | `text2img` | `text2img`, `img2img`, `img2text`, `text2text` |
-| `--prompts` | list | `None` | Input text prompts |
-| `--txt-prompts` | string | `None` | Path to text file with one prompt per line |
-| `--image-path` | string | `None` | Input image path (required for `img2img`/`img2text`) |
-| `--output` | string | `.` | Output directory for saved images |
-| `--steps` | int | `50` | Number of diffusion inference steps |
-| `--seed` | int | `None` | Random seed for reproducibility |
-
-### Think Mode Arguments
-
-| Argument | Type | Default | Description |
-| :------- | :--- | :------ | :---------- |
-| `--think` | flag | `False` | Enable `...` planning/reasoning |
-| `--max-think-tokens` | int | `1000` | Maximum tokens for think generation |
-| `--do-sample` | flag | `False` | Use sampling instead of greedy decoding |
-| `--text-temperature` | float | `0.3` | Sampling temperature for text generation |
-
-### CFG Arguments
-
-| Argument | Type | Default | Description |
-| :------- | :--- | :------ | :---------- |
-| `--cfg-text-scale` | float | `4.0` | Text CFG guidance scale |
-| `--cfg-img-scale` | float | `1.5` | Image CFG guidance scale |
-| `--negative-prompt` | string | `None` | Negative prompt for CFG |
-| `--cfg-parallel-size` | int | `1` | CFG parallel GPU count (1, 2, or 3) |
-| `--cfg-interval` | float[2] | pipeline default | CFG active window `[start, end]` |
-| `--cfg-renorm-type` | string | `None` | `global`, `text_channel`, or `channel` |
-| `--cfg-renorm-min` | float | `None` | Minimum renormalization value |
-
-### Engine Arguments
-
-| Argument | Type | Default | Description |
-| :------- | :--- | :------ | :---------- |
-| `--deploy-config` | string | `None` | Path to deploy YAML (auto-detected if omitted) |
-| `--stage-configs-path` | string | `None` | [Deprecated] Legacy path to `stage_args` YAML; prefer `--deploy-config` |
-| `--worker-backend` | choice | `multi_process` | `multi_process` or `ray` |
-| `--ray-address` | string | `None` | Ray cluster address |
-| `--quantization` | string | `None` | Quantization method (e.g. `fp8`) |
-| `--log-stats` | flag | `False` | Enable statistics logging |
-| `--init-timeout` | int | `300` | Initialization timeout (seconds) |
-| `--batch-timeout` | int | `5` | Batch timeout (seconds) |
-| `--enable-diffusion-pipeline-profiler` | flag | `False` | Profile diffusion stage durations |
-
-## FAQ
-
-- If you encounter OOM errors, try decreasing `max_model_len` or `gpu_memory_utilization` in the deploy YAML.
-
-**Two-stage VRAM usage:**
-
-| Stage | VRAM |
-| :---- | :--- |
-| Stage 0 (Thinker) | **15.04 GiB + KV Cache** |
-| Stage 1 (DiT) | **26.50 GiB** |
-| Total | **~42 GiB + KV Cache** |
-
-**Single-stage VRAM usage:** The DiT loads the full model (~42 GiB) in one process.
-
-## Example materials
-
-??? abstract "end2end.py"
- ``````py
- --8<-- "examples/offline_inference/bagel/end2end.py"
- ``````
diff --git a/docs/user_guide/examples/offline_inference/image_to_video.md b/docs/user_guide/examples/offline_inference/image_to_video.md
index dc1d3d6b2..db7a31ce5 100644
--- a/docs/user_guide/examples/offline_inference/image_to_video.md
+++ b/docs/user_guide/examples/offline_inference/image_to_video.md
@@ -4,7 +4,7 @@ Source .
-
-
-## Run examples (MammothModa2-Preview)
-
-Download model
-```bash
-hf download bytedance-research/MammothModa2-Preview --local-dir ./MammothModa2-Preview
-```
-
-### Text-to-Image (T2I)
-
-Text-to-image now runs through the shared offline image example
-(`examples/offline_inference/text_to_image/text_to_image.py`). See the recipe
-`recipes/MammothModa2/MammothModa2-Preview.md` for the full command and the
-`extra_body` knobs (`text_guidance_scale`, `cfg_range`, `num_inference_steps`).
-
-### Image Summary
-
-```bash
-python examples/offline_inference/mammothmodal2_preview/run_mammothmoda2_image_summarize.py \
- --model ./MammothModa2-Preview \
- --deploy-config ./vllm_omni/deploy/mammoth_moda2_ar.yaml \
- --question "Summarize this image." \
- --image ./image.png
-```
-
-## Example materials
-
-??? abstract "run_mammothmoda2_image_summarize.py"
- ``````py
- --8<-- "examples/offline_inference/mammothmodal2_preview/run_mammothmoda2_image_summarize.py"
- ``````
diff --git a/docs/user_guide/examples/offline_inference/text_to_image.md b/docs/user_guide/examples/offline_inference/text_to_image.md
index 7cd508d29..f1cdb9293 100644
--- a/docs/user_guide/examples/offline_inference/text_to_image.md
+++ b/docs/user_guide/examples/offline_inference/text_to_image.md
@@ -184,7 +184,7 @@ if __name__ == "__main__":
!!! info
For diffusion request-level batching controls such as `max_num_seqs`, see
- [Request-Level Batching](../../diffusion/request_batching.md).
+ [Diffusion Execution Modes](../../diffusion/execution_modes.md).
### Negative Prompts
diff --git a/docs/user_guide/examples/offline_inference/text_to_video.md b/docs/user_guide/examples/offline_inference/text_to_video.md
index 5d89eac34..fcd3e7337 100644
--- a/docs/user_guide/examples/offline_inference/text_to_video.md
+++ b/docs/user_guide/examples/offline_inference/text_to_video.md
@@ -12,7 +12,7 @@ For backend selection and SageAttention usage, see the [Diffusion Attention Back
| Model | Default Resolution | Default Frames | Default Steps | Guidance | VRAM (BF16) |
|---|---|---|---|---|---|
| `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 720x1280 | 81 | 40 | 4.0 | ~60 GiB |
-| `dg845/LTX-2.3-Diffusers` | 384x512 | 25 | 20 | 4.0 | 96GB-class GPU |
+| `Lightricks/LTX-2` | 512x768 | 121 | 40 | video 3.0 / audio 7.0 | Model-dependent |
| `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v` | 480x832 | 121 | 50 | 6.0 | 1×A100 80GB |
| `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_t2v` | 720x1280 | 121 | 50 | 6.0 | FP8 + VAE tiling required |
@@ -35,45 +35,17 @@ python text_to_video.py \
--output t2v_out.mp4
```
-LTX2 example:
+### LTX-2
```bash
python text_to_video.py \
- --model "Lightricks/LTX-2" \
- --prompt "A cinematic close-up of ocean waves at golden hour." \
- --negative-prompt "worst quality, inconsistent motion, blurry, jittery, distorted" \
- --height 512 \
- --width 768 \
- --num-frames 121 \
- --num-inference-steps 40 \
- --guidance-scale 4.0 \
- --frame-rate 24 \
- --output ltx2_out.mp4
-```
-
-### LTX-2.3
-
-```bash
-python text_to_video.py \
- --model dg845/LTX-2.3-Diffusers \
- --model-class-name LTX23Pipeline \
+ --model Lightricks/LTX-2 \
--prompt "Cherry blossoms swaying gently in the breeze with synchronized ambient sound" \
- --negative-prompt "worst quality, inconsistent motion, blurry, jittery, distorted" \
- --height 384 \
- --width 512 \
- --num-frames 25 \
- --num-inference-steps 20 \
- --guidance-scale 4.0 \
- --frame-rate 24 \
- --fps 24 \
- --audio-sample-rate 48000 \
- --output ltx23_t2v_output.mp4
+ --output ltx2_output.mp4
```
-Use a Diffusers-format checkpoint such as `dg845/LTX-2.3-Diffusers`; the
-upstream `Lightricks/LTX-2.3` raw safetensors repo is not directly loadable by
-this pipeline. Pass `--model-class-name LTX23Pipeline` to select the LTX-2.3
-text-to-video pipeline explicitly.
+See the [LTX-2 recipe](../../../../recipes/LTX/LTX-2.md) for all checkpoints,
+pipeline selection, I2V, defaults, and advanced options.
### HunyuanVideo-1.5 (480p)
@@ -135,8 +107,7 @@ python text_to_video.py \
### Common
- `--model`: Diffusers model ID or local path.
-- `--model-class-name`: Optional explicit pipeline class. Use `LTX23Pipeline`
- for LTX-2.3 text-to-video.
+- `--model-class-name`: Optional explicit pipeline override.
- `--prompt`: text description (string).
- `--height/--width`: output resolution. Default depends on model.
- `--num-frames`: number of frames. Default depends on model.
@@ -151,8 +122,7 @@ python text_to_video.py \
- `--enable-cpu-offload`: enable CPU offloading for diffusion models.
- `--enable-layerwise-offload`: enable layerwise offloading on DiT modules.
- `--frame-rate`: generation FPS for pipelines that require it (e.g., LTX2).
-- `--audio-sample-rate`: audio sample rate for embedded audio (when the
- pipeline returns audio; LTX-2.3 outputs 48kHz audio).
+- `--audio-sample-rate`: fallback audio sample rate when the pipeline returns audio.
- `--quantization`: quantization method (`fp8` for FP8, `gguf` for GGUF).
- `--flow-shift`: scheduler flow_shift parameter.
diff --git a/docs/user_guide/examples/online_serving/bagel.md b/docs/user_guide/examples/online_serving/bagel.md
index aa8b33de8..4b5c6dde6 100644
--- a/docs/user_guide/examples/online_serving/bagel.md
+++ b/docs/user_guide/examples/online_serving/bagel.md
@@ -40,7 +40,7 @@ bash run_server_stage_cli.sh --stage 0
bash run_server_stage_cli.sh --stage 1
```
-To use a custom deploy YAML (note: `--stage-configs-path` is deprecated in favor of `--deploy-config`):
+To use a custom deploy YAML:
```bash
vllm serve ByteDance-Seed/BAGEL-7B-MoT --omni --port 8091 \
diff --git a/docs/user_guide/examples/online_serving/glm_image.md b/docs/user_guide/examples/online_serving/glm_image.md
index ea404faa9..8d41b26fd 100644
--- a/docs/user_guide/examples/online_serving/glm_image.md
+++ b/docs/user_guide/examples/online_serving/glm_image.md
@@ -11,8 +11,8 @@ memory, sampling params) live in `vllm_omni/deploy/glm_image.yaml`.
vllm serve zai-org/GLM-Image --omni --port 8091
```
-The config system auto-detects the pipeline from the model's `model_index.json` — no
-manual `--stage-configs-path` or `--deploy-config` needed.
+The config system auto-detects the pipeline from the model's `model_index.json`, so
+no manual `--deploy-config` is needed.
By default, stage 0 (AR) runs on GPU 0 and stage 1 (Diffusion) on GPU 1. To colocate
both stages on a single GPU, override per stage:
diff --git a/docs/user_guide/examples/online_serving/qwen2_5_omni.md b/docs/user_guide/examples/online_serving/qwen2_5_omni.md
index d8c48a403..a06e6d7a0 100644
--- a/docs/user_guide/examples/online_serving/qwen2_5_omni.md
+++ b/docs/user_guide/examples/online_serving/qwen2_5_omni.md
@@ -15,9 +15,9 @@ Please refer to [README.md](https://github.com/vllm-project/vllm-omni/tree/main/
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091
```
-If you have custom stage configs file, launch the server with command below
+To use a custom deploy config, launch the server with the command below:
```bash
-vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
+vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --deploy-config /path/to/deploy_config.yaml
```
### Send Multi-modal Request
@@ -182,7 +182,7 @@ The script supports the following arguments:
- `--model`: Model name/path (default: Qwen/Qwen2.5-Omni-7B)
- `--server-port`: Port for vLLM server (default: 8091)
- `--gradio-port`: Port for Gradio demo (default: 7861)
-- `--stage-configs-path`: Path to custom stage configs YAML file (optional)
+- `--deploy-config`: Path to a custom deploy config YAML file (optional)
- `--server-host`: Host for vLLM server (default: 0.0.0.0)
- `--gradio-ip`: IP for Gradio demo (default: 127.0.0.1)
- `--share`: Share Gradio demo publicly (creates a public link)
@@ -195,9 +195,9 @@ The script supports the following arguments:
vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091
```
-If you have custom stage configs file:
+To use a custom deploy config:
```bash
-vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --stage-configs-path /path/to/stage_configs_file
+vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --deploy-config /path/to/deploy_config.yaml
```
**Step 2: Run the Gradio demo**
diff --git a/docs/user_guide/examples/online_serving/qwen3_omni.md b/docs/user_guide/examples/online_serving/qwen3_omni.md
index ec509d597..b4c05bdbe 100644
--- a/docs/user_guide/examples/online_serving/qwen3_omni.md
+++ b/docs/user_guide/examples/online_serving/qwen3_omni.md
@@ -140,7 +140,7 @@ You can control output modalities to specify which types of output the model sho
| Modalities | Output |
|------------|--------|
| `["text"]` | Text only |
-| `["audio"]` | Text + Audio |
+| `["audio"]` | Audio only |
| `["text", "audio"]` | Text + Audio |
| Not specified | Text + Audio (default) |
@@ -161,13 +161,16 @@ curl http://localhost:8091/v1/chat/completions \
#### Text + Audio
```bash
-curl http://localhost:8091/v1/chat/completions \
+response=$(curl -s http://localhost:8091/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-Omni-30B-A3B-Instruct",
"messages": [{"role": "user", "content": "Describe vLLM in brief."}],
- "modalities": ["audio"]
- }'
+ "modalities": ["text", "audio"]
+ }')
+
+echo "$response" | jq -r '.choices[0].message.content'
+echo "$response" | jq -r '.choices[1].message.audio.data' | base64 -d > output.wav
```
### Using Python client
@@ -205,7 +208,7 @@ client = OpenAI(base_url="http://localhost:8091/v1", api_key="EMPTY")
response = client.chat.completions.create(
model="Qwen/Qwen3-Omni-30B-A3B-Instruct",
messages=[{"role": "user", "content": "Describe vLLM in brief."}],
- modalities=["audio"]
+ modalities=["text", "audio"]
)
# Response contains two choices: one with text, one with audio
print(response.choices[0].message.content) # Text response
@@ -303,7 +306,7 @@ The gradio script supports the following arguments:
``````
??? abstract "qwen3_omni_moe_thinking.yaml"
``````yaml
- --8<-- "examples/online_serving/qwen3_omni/qwen3_omni_moe_thinking.yaml"
+ --8<-- "vllm_omni/deploy/qwen3_omni_moe_thinking.yaml"
``````
??? abstract "run_curl_multimodal_generation.sh"
``````sh
diff --git a/docs/user_guide/examples/online_serving/text_to_audio.md b/docs/user_guide/examples/online_serving/text_to_audio.md
index b1ec33a20..a4833aef6 100644
--- a/docs/user_guide/examples/online_serving/text_to_audio.md
+++ b/docs/user_guide/examples/online_serving/text_to_audio.md
@@ -139,7 +139,7 @@ Content-Type: application/json
|-----------|------|---------|-------------|
| `input` | string | **required** | Text prompt describing the audio to generate |
| `model` | string | server's model | Model to use (optional, should match server if specified) |
-| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, aac, opus |
+| `response_format` | string | "wav" | Audio format: wav, mp3, flac, pcm, opus |
| `speed` | float | 1.0 | Playback speed (0.25 - 4.0) |
| `audio_length` | float | null | Audio duration in seconds (max ~47s for `stable-audio-open-1.0`) |
| `audio_start` | float | 0.0 | Audio start time in seconds |
diff --git a/docs/user_guide/examples/online_serving/text_to_speech.md b/docs/user_guide/examples/online_serving/text_to_speech.md
index 5640845d8..c6a613723 100644
--- a/docs/user_guide/examples/online_serving/text_to_speech.md
+++ b/docs/user_guide/examples/online_serving/text_to_speech.md
@@ -422,12 +422,19 @@ curl -X POST http://localhost:8091/v1/audio/speech \
Raw PCM streaming requires `stream_format="audio"`, `response_format="pcm"`, and `async_chunk: true` on the stage config (default in `qwen3_tts.yaml`). `speed` is not supported when streaming.
### Streaming WebSocket
-The `/v1/audio/speech/stream` endpoint accepts text incrementally, splits it at sentence boundaries, and emits one PCM stream per sentence:
+The `/v1/audio/speech/stream` endpoint accepts text incrementally and synthesizes the buffered text as one continuous request on `input.done`:
```bash
python qwen3_tts/streaming_speech_client.py --text "Hello world. How are you? I am fine."
python qwen3_tts/streaming_speech_client.py --text "..." --simulate-stt --stt-delay 0.1
```
+`input.done` flushes without closing, so repeating `--text` synthesizes several utterances over one connection:
+```bash
+python qwen3_tts/streaming_speech_client.py \
+ --text "First utterance." \
+ --text "Second utterance, same connection."
+```
+
### Gradio demos
```bash
./qwen3_tts/run_gradio_demo.sh # CustomVoice (default)
diff --git a/docs/user_guide/examples/online_serving/text_to_video.md b/docs/user_guide/examples/online_serving/text_to_video.md
index d4d520d5e..2f00d386b 100644
--- a/docs/user_guide/examples/online_serving/text_to_video.md
+++ b/docs/user_guide/examples/online_serving/text_to_video.md
@@ -191,7 +191,7 @@ curl -X POST http://localhost:8091/v1/videos \
| `negative_prompt` | str | None | Negative prompt |
| `width` | int | None | Video width in pixels |
| `height` | int | None | Video height in pixels |
-| `num_frames` | int | None | Number of frames to generate |
+| `num_frames` | int | 1 | Number of frames to generate; set explicitly for video generation |
| `fps` | int | None | Frames per second for output video |
| `num_inference_steps` | int | None | Number of denoising steps |
| `guidance_scale` | float | None | CFG guidance scale (low-noise stage) |
@@ -289,90 +289,12 @@ done
## LTX-2
-### Start Server
-
-#### Basic Start
-
```bash
-vllm serve Lightricks/LTX-2 --omni --port 8098 \
- --enforce-eager --flow-shift 1.0 --boundary-ratio 1.0
+vllm serve Lightricks/LTX-2 --omni --port 8098
```
-For multi-GPU memory reduction, you can enable HSDP:
-
-```bash
-vllm serve Lightricks/LTX-2 --omni --port 8098 \
- --enforce-eager --flow-shift 1.0 --boundary-ratio 1.0 \
- --use-hsdp --hsdp-shard-size 2
-```
-
-#### Start with Optimization Presets
-
-Use the LTX-2 startup script with built-in optimization presets:
-
-```bash
-# Baseline (1 GPU, eager)
-bash run_server_ltx2.sh baseline
-
-# 4-GPU Ulysses sequence parallelism (lossless)
-bash run_server_ltx2.sh ulysses4
-
-# Cache-DiT lossy acceleration (1 GPU, ~1.4× speedup)
-bash run_server_ltx2.sh cache-dit
-
-# Best combo: 4-GPU Ulysses SP + Cache-DiT (~2.2× speedup)
-bash run_server_ltx2.sh best-combo
-```
-
-#### Optimization Benchmarks
-
-Benchmarked on H800, online serving (480×768, 41 frames, 20 steps, `seed=42`).
-"Inference" is the server-reported inference time; excludes HTTP/poll overhead.
-
-| Preset | Server Command | Inference (s) | Speedup | Type |
-|--------|---------------|---------------|---------|------|
-| `baseline` | `--enforce-eager` | 10.3 | 1.00× | — |
-| `compile` | *(default, no --enforce-eager)* | ~10.3 (warm) | ~1.00× | Lossless |
-| `ulysses4` | `--enforce-eager --usp 4` | ~10.3 | ~1.00× | Lossless |
-| `cache-dit` | `--enforce-eager --cache-backend cache_dit` | 7.4 avg | ~1.4× | Lossy |
-| `best-combo` | `--enforce-eager --usp 4 --cache-backend cache_dit` | 4.7 avg | **~2.2×** | Lossless + Lossy |
-
-**Observations**:
-- **torch.compile**: On H800, warm-request inference time matches the eager baseline (~10.3s).
- The first request pays ~6s compilation overhead. Benefit depends on model architecture and GPU.
-- **Ulysses SP (4 GPU)**: No measurable speedup alone for 41-frame generation at this resolution.
- Communication overhead outweighs gains at this sequence length.
-- **Cache-DiT**: Inference varies per request (6–10s) due to dynamic caching decisions.
- Average is ~7.4s (~1.4× speedup) with slight quality tradeoff.
-- **Best combo**: 4-GPU Ulysses SP + Cache-DiT synergize well — Cache-DiT reduces per-step
- computation, making the communication overhead of Ulysses SP worthwhile. Average ~4.7s
- (~2.2× speedup).
-- **FP8 quantization**: Reduces VRAM but does not speed up LTX-2 on H800 (compute-bound).
-
-**Deployment Recommendations**:
-- For **production with quality priority**: use `baseline` with `--enforce-eager`
-- For **maximum throughput** (4 GPUs, quality tradeoff): use `best-combo` (~2.2× speedup)
-- For **single-GPU throughput**: use `cache-dit` (~1.4× speedup)
-- `--enforce-eager` is recommended to avoid torch.compile warmup latency on first request
-
-### Send Requests (curl)
-
-```bash
-# Using the provided script
-bash run_curl_ltx2.sh
-
-# Or directly
-curl -sS -X POST http://localhost:8098/v1/videos \
- -H "Accept: application/json" \
- -F "prompt=A serene lakeside sunrise with mist over the water." \
- -F "width=768" \
- -F "height=480" \
- -F "num_frames=41" \
- -F "fps=24" \
- -F "num_inference_steps=20" \
- -F "guidance_scale=3.0" \
- -F "seed=42"
-```
+See the [LTX-2 recipe](../../../../recipes/LTX/LTX-2.md) for all checkpoints,
+pipeline selection, requests, defaults, and advanced options.
## Example materials
diff --git a/docs/user_guide/fault_injection_reliability_matrix.md b/docs/user_guide/fault_injection_reliability_matrix.md
index 974982b3b..28271affa 100644
--- a/docs/user_guide/fault_injection_reliability_matrix.md
+++ b/docs/user_guide/fault_injection_reliability_matrix.md
@@ -65,23 +65,23 @@ More failure modes (for example, network jitter and network interruption) will b
## Source of Conclusions
-The behaviors and conclusions above are summarized from current fault injection validation results on **`Qwen3-Omni`**, **`Wan2.2`**, **`HunyuanImage-3.0-Instruct`** (DiT-only, `/v1/images/generations`), and **`VoxCPM2`** (`/v1/audio/speech`). Automated coverage lives under `tests/dfx/reliability/` (`test_reliability_qwen3_omni.py`, `test_reliability_wan22.py`, `test_reliability_hunyuan_image.py`, `test_reliability_voxcpm2.py`) and runs weekly via `.buildkite/test-weekly.yml`.
+The behaviors and conclusions above are summarized from current fault injection validation results on **`Qwen3-Omni`**, **`Wan2.2`**, **`HunyuanImage-3.0-Instruct`** (DiT-only, `/v1/images/generations`), and **`VoxCPM2`** (`/v1/audio/speech`). Automated coverage lives under `tests/dfx/reliability/` (`test_reliability_qwen3_omni.py`, `test_reliability_wan22.py`, `test_reliability_hunyuan_image.py`, `test_reliability_voxcpm2.py`) and runs weekly via `.buildkite/cuda/test-weekly.yml`.
### Example: `SIGTERM` fault injection log (Qwen3-Omni)
-The following full log is from a real run where a `SIGTERM` signal was sent to the serve root process.
+The following log is based on a real run where a `SIGTERM` signal was sent to the serve root process. Its command line has been normalized to the current `--deploy-config` option.
```text
-Launching OmniServer with: /workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --stage-configs-path vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
+Launching OmniServer with: /workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --deploy-config vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
Server ready on 127.0.0.1:60675 (OmniServer startup took 206.023s)
OmniServer started successfully
-[reliability][process-kill] current_server_proc pid=556855 name=python3 cmdline=/workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --stage-configs-path vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
+[reliability][process-kill] current_server_proc pid=556855 name=python3 cmdline=/workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --deploy-config vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
[reliability][process-kill] current_server_proc pid=557118 name=python3 cmdline=/workspace/.venv/bin/python3 -c from multiprocessing.resource_tracker import main;main(57)
[reliability][process-kill] current_server_proc pid=557119 name=VLLM::StageEngineCoreProc_noid_replica0_DP0 cmdline=VLLM::StageEngineCoreProc_noid_replica0_DP0
[reliability][process-kill] current_server_proc pid=557122 name=VLLM::StageEngineCoreProc_noid_replica0_DP0 cmdline=VLLM::StageEngineCoreProc_noid_replica0_DP0
[reliability][process-kill] current_server_proc pid=558201 name=VLLM::StageEngineCoreProc_noid_replica0_DP0 cmdline=VLLM::StageEngineCoreProc_noid_replica0_DP0
-[reliability][process-kill] root-kill pid=556855 name=python3 signal=SIGTERM cmdline=/workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --stage-configs-path vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
+[reliability][process-kill] root-kill pid=556855 name=python3 signal=SIGTERM cmdline=/workspace/.venv/bin/python3 -m vllm_omni.entrypoints.cli.main serve Qwen/Qwen3-Omni-30B-A3B-Instruct --host 127.0.0.1 --port 60675 --omni --async-chunk --stage-init-timeout 600 --init-timeout 900 --log-stats --deploy-config vllm-omni/vllm_omni/deploy/qwen3_omni_moe.yaml
[0;36m(APIServer pid=556855)[0;0m INFO 05-19 12:24:51 [omni_base.py:463] [AsyncOmni] Shutting down
[0;36m(APIServer pid=556855)[0;0m INFO 05-19 12:24:51 [async_omni_engine.py:2397] [AsyncOmniEngine] Shutting down Orchestrator
[0;36m(APIServer pid=556855)[0;0m INFO 05-19 12:24:51 [orchestrator.py:351] [Orchestrator] Received shutdown signal
diff --git a/docs/user_guide/quantization/fp8.md b/docs/user_guide/quantization/fp8.md
index 6114812db..ddca3ea19 100644
--- a/docs/user_guide/quantization/fp8.md
+++ b/docs/user_guide/quantization/fp8.md
@@ -88,10 +88,12 @@ warmup_quack_fp8([(14040, 2048, 6144), (14040, 2048, 2048)])
| Wan2.2 | Wan2.2 diffusion pipelines | Not validated | Not validated | Validate against BF16 before documenting as supported | TBD | |
| Z-Image | `Tongyi-MAI/Z-Image-Turbo` | Yes | Yes | All layers | None | ✅︎ |
| FLUX.1 | `black-forest-labs/FLUX.1-dev`, `black-forest-labs/FLUX.1-schnell` | Yes | Yes | All layers | None | |
+| FLUX.2-dev | `black-forest-labs/FLUX.2-dev` | Yes | Not validated | All layers | None | ✅︎ |
| FLUX.2-klein | `black-forest-labs/FLUX.2-klein-4B` | Yes | Yes | All layers | None | |
| HunyuanImage-3.0 | `tencent/HunyuanImage-3.0`, `tencent/HunyuanImage-3.0-Instruct` | Yes | Yes | All layers; use the Hunyuan stage config for multi-stage runs | None | |
| HunyuanVideo-1.5 | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v`, `720p_t2v`, `480p_i2v` | Yes | Yes | All layers | None | |
| Cosmos3 | `nvidia/Cosmos3-Nano`, `nvidia/Cosmos3-Super` | Yes | Not validated | All layers | None | |
+| MiniMax H3 | `MiniMaxAI/MiniMax-H3` | Yes | Not validated | DiT linears except patch, timestep, and final projections; not compatible with layerwise offload | None | |
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
diff --git a/docs/user_guide/quantization/overview.md b/docs/user_guide/quantization/overview.md
index 3a4a45573..ee36f5b60 100644
--- a/docs/user_guide/quantization/overview.md
+++ b/docs/user_guide/quantization/overview.md
@@ -5,6 +5,9 @@ path. The same configuration entrypoint is used across diffusion-only models,
multi-stage omni/TTS models, and multi-stage diffusion models, but each model
type has a different quantization scope.
+For the internal architecture and backend extension points, see the
+[quantization design document](../../design/feature/quantization.md).
+
## Quantization Modes
| Mode | Guide | Description | Methods |
diff --git a/examples/model_display_names.yml b/examples/model_display_names.yml
index be3f8e381..7a04ff8fa 100644
--- a/examples/model_display_names.yml
+++ b/examples/model_display_names.yml
@@ -8,5 +8,4 @@ mimo_audio: MiMo-Audio
ming_flash_omni: Ming-flash-omni 2.0
qwen2_5_omni: Qwen2.5-Omni
qwen3_omni: Qwen3-Omni
-sensenova_u1: SenseNova-U1
step_audio2: Step-Audio2
diff --git a/examples/offline_inference/audex/README.md b/examples/offline_inference/audex/README.md
new file mode 100644
index 000000000..793b69eb9
--- /dev/null
+++ b/examples/offline_inference/audex/README.md
@@ -0,0 +1,114 @@
+# Audex (Nemotron-Labs-Audex-2B) offline inference
+
+One checkpoint, four tasks — each script builds its own `Omni` engine with
+the right deploy yaml, so they run as plain `python
+
+