Merge main and preserve dispatch backlog across monitor windows
Signed-off-by: Sy03 <1370724210@qq.com>
This commit is contained in:
commit
ea7de7e3de
769 files changed
+73802
-22805
No files matched your search
@@ -161,6 +161,7 @@ nav:
|
||||
- Cache-DiT: design/feature/cache_dit.md
|
||||
- TeaCache: design/feature/teacache.md
|
||||
- Diffusion Continuous Batching: design/feature/diffusion_continuous_batching.md
|
||||
- Mooncake KV Transfer: design/feature/mooncake-kv-transfer.md
|
||||
- Infrastructure and Performance:
|
||||
- Prometheus Metrics: design/metrics.md
|
||||
- Speech Generation Performance Optimizations: design/qwen3_omni_tts_performance_optimization.md
|
||||
|
||||
+1
-1
@@ -56,7 +56,7 @@ vLLM-Omni is flexible and easy to use with:
|
||||
vLLM-Omni seamlessly supports most popular open-source models on HuggingFace, including:
|
||||
|
||||
- Omni-modality models (e.g. Qwen3-Omni, MiniCPM-o 4.5, Cosmos3, HunyuanImage, BAGEL)
|
||||
- TTS models (e.g. Qwen3-TTS, IndexTTS 2.5, dots.tts, CosyVoice3)
|
||||
- TTS models (e.g. Qwen3-TTS, IndexTTS 2.5, CosyVoice3)
|
||||
- Diffusion models — image, video, and audio generation (e.g. MiniMax H3, LTX-2.5, SANA-Video, Wan2.2)
|
||||
- Robot-policy and action models (e.g. π0, GR00T-N1.7, DreamZero-DROID, InternVLA-A1)
|
||||
|
||||
|
||||
+54
-1
@@ -23,7 +23,9 @@ You can use `vllm bench serve --omni --help=all` to get descriptions of all para
|
||||
The API endpoint exposed externally, to which clients send their requests.
|
||||
|
||||
- `--dataset-name`
|
||||
The name of the dataset used; random-mm indicates generating random multimodal inputs (images, videos, audio), while `omniinteract` replays official OmniInteract videos.
|
||||
The name of the dataset used; `random-mm` generates random multimodal inputs (images, videos, audio),
|
||||
`omniinteract` replays official OmniInteract videos, and `videomme` loads Video-MME MCQ videos
|
||||
(`lmms-eval/Video-MME`, MiniCPM OmniEvalKit packing).
|
||||
|
||||
- `--num-prompts`
|
||||
The total number of requests to send, an integer.
|
||||
@@ -343,6 +345,57 @@ transcript, event, and result artifacts without errors. A valid LISTEN-only case
|
||||
chunks. Official-manifest eligibility is reported separately because clipped or cancelled output is a benchmark-quality
|
||||
signal, not a transport failure. This local performance test does not score answer accuracy.
|
||||
|
||||
### Video-MME Benchmark
|
||||
|
||||
Video-MME (`--dataset-name videomme`) scores multiple-choice video QA. Default packing is
|
||||
OmniEvalKit MiniCPM `minicpm-frames` (up to 96 sampled frames as `image_url`). Pass a local
|
||||
mirror with `--dataset-path` / `--videomme-parquet` + `--videomme-video-dir`, or a Hugging
|
||||
Face dataset id (`lmms-eval/Video-MME` by default; any `org/name` is accepted when
|
||||
`--dataset-name videomme` is explicit). Relative `--videomme-video-dir` values are resolved
|
||||
to absolute `file://` URLs. For those URLs, start the server with
|
||||
`--allowed-local-media-path` covering the video root; otherwise use `--videomme-inline-local-video`.
|
||||
|
||||
For MiniCPM-o 4.5, the default deployment allows 64 images per request. To run the
|
||||
96-frame recipe, start the server with a stage 0 override (replace `/path/to/Video-MME`
|
||||
with the local video root shared by the benchmark client and server):
|
||||
|
||||
```bash
|
||||
vllm serve openbmb/MiniCPM-o-4_5 --omni \
|
||||
--allowed-local-media-path /path/to/Video-MME \
|
||||
--stage-overrides '{"0":{"limit_mm_per_prompt":{"image":96,"audio":64,"video":1}}}'
|
||||
```
|
||||
|
||||
The override applies to this server invocation only. Alternatively, use
|
||||
`--videomme-max-frames 64` in the benchmark to stay within the default image limit;
|
||||
this evaluates a different frame-sampling setting from the 96-frame recipe.
|
||||
|
||||
Run the benchmark against that server:
|
||||
|
||||
```bash
|
||||
vllm bench serve --omni \
|
||||
--backend openai-chat-omni \
|
||||
--dataset-name videomme \
|
||||
--dataset-path /path/to/Video-MME \
|
||||
--videomme-pack-mode minicpm-frames \
|
||||
--videomme-max-frames 96 \
|
||||
--model openbmb/MiniCPM-o-4_5 \
|
||||
--endpoint /v1/chat/completions \
|
||||
--num-prompts 8 \
|
||||
--max-concurrency 1 \
|
||||
--save-result
|
||||
```
|
||||
|
||||
Accuracy keys (`videomme_accuracy`, per-duration / domain / task breakdowns) are written into
|
||||
the saved JSON. `videomme_accuracy` excludes HTTP failures; `videomme_accuracy_incl_http_fail`
|
||||
counts them as wrong. `videomme_submitted` / `videomme_unique_question_ids` show when
|
||||
oversampling made the request count differ from unique-question coverage.
|
||||
`videomme_skipped_rows` counts rows skipped during sampling because of missing or unreadable
|
||||
media or invalid question fields. The opt-in accuracy runner rejects skipped rows, HTTP
|
||||
failures, duplicate question IDs, and missing gold answers. This checks the sampled subset;
|
||||
it does not establish that a supplied local mirror contains the entire official dataset.
|
||||
Use `--videomme-save-eval-items` (or `VIDEOMME_SAVE_EVAL_ITEMS=1`) for per-request
|
||||
`videomme_eval_items` rows.
|
||||
|
||||
### Multi-Modal Benchmark
|
||||
|
||||
<details class="admonition abstract" markdown="1">
|
||||
|
||||
@@ -1,5 +1,67 @@
|
||||
# vllm-omni serve
|
||||
|
||||
## Multiple API frontends
|
||||
|
||||
For local EngineCore pipelines with one or more stages, `--api-server-count` starts
|
||||
multiple API frontend processes that share one parent-owned set of stage
|
||||
engines:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
--api-server-count 2 \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
This mode is intended for workloads whose request parsing, multimodal input
|
||||
processing, or response encoding can bottleneck a single frontend. It
|
||||
currently requires local multi-process EngineCore stages with one local
|
||||
process group per replica. A stage may still use multiple `num_replicas`.
|
||||
It cannot be combined with diffusion, headless or
|
||||
remote stages, intra-stage data parallelism, Ray, fault tolerance, elastic
|
||||
expert parallelism, sleep mode, or runtime LoRA updating. Runtime voice upload
|
||||
and deletion are also disabled because those mutations are process-local;
|
||||
built-in voices, inline reference audio, and voices restored at startup remain
|
||||
available. The `/v1/omni/sleep` and `/v1/omni/wakeup` control routes return HTTP
|
||||
409 for the same reason: their bookkeeping is process-local while stage engines
|
||||
are shared.
|
||||
|
||||
A single-stage model pipeline is supported through the normal launch command;
|
||||
this is distinct from the distributed `--stage-id` / `--headless` launch mode.
|
||||
Every stage in the pipeline must be an EngineCore stage. Mixed AR-to-DiT
|
||||
pipelines, including MiniMax-H3, remain unsupported even when their first
|
||||
stage is an EngineCore stage. Diffusion support requires per-frontend channels
|
||||
and shared-engine lifecycle handling in the diffusion backend.
|
||||
|
||||
### Workloads and performance validation
|
||||
|
||||
Each API process owns its frontend processor, Orchestrator, request state,
|
||||
and per-replica EngineCore channels. Requests return through the channel of
|
||||
the originating API process; model weights and GPU stage engines are shared.
|
||||
This distributes frontend and orchestration work across CPU processes.
|
||||
|
||||
| Workload | Potential benefit | Limitation |
|
||||
| ---------- | ------------------- | ------------ |
|
||||
| Concurrent cold image/video inputs on supported EngineCore pipelines | Parallel frontend processing when one API process is CPU-limited | A single cold request and GPU computation may be unchanged |
|
||||
| Many replicas producing frequent text/audio outputs | Less API/Orchestrator work and GIL contention per process | Routing remains serial within each Orchestrator |
|
||||
| Warm inputs or low-concurrency, GPU-limited generation | Additional frontend capacity | More API processes may add overhead without improving throughput |
|
||||
|
||||
Here, a cold input means media or preprocessing caches miss, not that model
|
||||
weights must be loaded or GPU kernels compiled. Each frontend has its own
|
||||
processor/cache state, so cache duplication, CPU thread contention, and memory
|
||||
usage also matter. Multi-API serving does not itself change the output queue,
|
||||
move response encoding to an executor, or change the model's numerical scheduler.
|
||||
|
||||
Compare `--api-server-count 1`, `2`, and higher counts with the same hardware,
|
||||
model/stage configuration, media, sampling parameters, output lengths, and
|
||||
offered load. Keep cold-cache and warmed-cache runs separate and report repeated
|
||||
measurements. Record CPU and request counts for every API worker, input and
|
||||
output queue delay, GPU utilization, first-output latency, and end-to-end latency.
|
||||
Confirm requests reach every worker: connection reuse can concentrate traffic
|
||||
on one process. Queue-only or single-API experiments do not establish a
|
||||
multi-API throughput gain. The multi-replica workload in
|
||||
[#4680](https://github.com/vllm-project/vllm-omni/issues/4680) remains a relevant
|
||||
validation target.
|
||||
|
||||
## Stage-based CLI quickstart
|
||||
|
||||
The stage-based CLI is designed for deployments that require launching each pipeline stage in an isolated process
|
||||
@@ -61,3 +123,10 @@ CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
## Arguments
|
||||
|
||||
--8<-- "docs/generated/argparse_omni/omni_serve.inc.md"
|
||||
|
||||
Async video jobs use process-local metadata and task handles. With multiple API
|
||||
workers, creating, listing, retrieving, downloading, and deleting these jobs
|
||||
returns HTTP 409. This restriction is independent of the current diffusion
|
||||
stage launch restriction: supporting multi-worker diffusion also requires a
|
||||
shared job lifecycle. Missing frontend topology state returns HTTP 503 for
|
||||
operations that require process-local state protection.
|
||||
@@ -70,6 +70,7 @@ The lifecycle labels used below are:
|
||||
| `OMNI_DIFFUSION_PROMPT_EMBED_CACHE_SIZE` | Positive integer; default `32` entries | Each diffusion runner; resolved during model setup | A valid environment value overrides the explicit cache size. A non-integer logs a warning; a non-positive value is ignored. | Experimental |
|
||||
| `OMNI_DIFFUSION_SESSION_STATE_MANAGER` | Boolean with the same accepted spellings as the prompt cache; default disabled | Experimental diffusion session manager; model setup | A recognized environment value overrides the explicit enable setting. An unrecognized value is ignored. | Experimental |
|
||||
| `OMNI_DIFFUSION_SESSION_STATE_MANAGER_MAX_SESSIONS` | Positive integer; default `64` | Experimental diffusion session manager; model setup | A valid environment value overrides the explicit maximum. A non-integer logs a warning; a non-positive value is ignored. | Experimental |
|
||||
| `VLLM_OMNI_AR_DIFFUSION_KV_GATHER` | `1` enables; default `0` (off) | AR-diffusion KV manager at allocation and attention at dispatch; set before worker startup | Only the exact value `1` enables contiguous KV gathering. History staging additionally requires `ARDiffusionKVConfig.reuse_history_staging`. | Experimental |
|
||||
|
||||
Backend names for `DIFFUSION_ATTENTION_BACKEND` are the members of
|
||||
`DiffusionAttentionBackendEnum`, such as `FLASH_ATTN`, `TORCH_SDPA`,
|
||||
|
||||
@@ -53,7 +53,7 @@ The new deploy schema lives under `vllm_omni/deploy/` and is paired with a froze
|
||||
| ------- | ------ | ---------- | --------- | ------------- |
|
||||
| `base_config` | str (path) | optional | — | Overlay parent (relative or absolute). `stages:` / `platforms:` deep-merged by stage_id; other scalars overlay-wins. Intended for user-authored overlays; prod yamls stay flat. |
|
||||
| `async_chunk` | bool | optional | `true` | Enable chunked streaming between stages. Pin to `false` if the pipeline runs end-to-end. |
|
||||
| `session_mode` | str | optional | `"turn"` | Session behavior. Duplex models (pipelines with a `duplex_plugin`, e.g. MiniCPM-o 4.5) set `"duplex"`; `vllm-omni serve` then serves it over `/v1/realtime?duplex=1` and `/v1/chat/completions`, both on duplex sessions. |
|
||||
| `session_mode` | str | optional | `"turn"` | Session behavior. For pipelines with a `duplex_plugin`, explicitly select `"duplex"` for the duplex engine or `"turn"` for the ordinary online serving stack. The shipped MiniCPM-o 4.5 default remains `"duplex"`. See [Full Duplex](../serving/full_duplex_api.md#enable-full-duplex). |
|
||||
| `active_stream_window` | int | optional | `0` | Number of active downstream stream slots; `0` preserves all-stream cycling. |
|
||||
| `duplex_session` | dict | optional | runtime defaults | Full-duplex session lifecycle, buffering, replay, and capacity limits. |
|
||||
| `connectors` | dict | optional | `null` | Named connector specs (`{name, extra}`). Referenced by each stage's `input_connectors` / `output_connectors`. See [Connector schema](#connector-schema). |
|
||||
@@ -290,3 +290,90 @@ vllm serve Qwen/Qwen2.5-Omni-7B --omni --port 8091 --deploy-config /path/to/depl
|
||||
|
||||
!!! important
|
||||
We are actively iterating on the definition of deployment configurations, and we welcome feedback from users and developers.
|
||||
|
||||
## Qwen3-TTS with Model Runner V2
|
||||
|
||||
Qwen3-TTS can opt into the native CUDA Model Runner V2 pipeline on vLLM
|
||||
0.29.0. Select one of these deployment profiles:
|
||||
|
||||
| Profile | Runner | Code2Wav graph batches | Intended use |
|
||||
| --- | --- | --- | --- |
|
||||
| `qwen3_tts.yaml` | V1 | Existing defaults | Existing deployment / regression control |
|
||||
| `qwen3_tts_mrv2.yaml` | V2 | B1 | Native runner validation |
|
||||
| `qwen3_tts_high_concurrency_mrv2.yaml` | V2 | B1, B2 | Opt-in throughput tuning |
|
||||
| `qwen3_tts_high_concurrency_mrv2_b4.yaml` | V2 | B1, B2, B3, B4 | Experimental throughput / buffered playback |
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-Base --omni \
|
||||
--deploy-config vllm_omni/deploy/qwen3_tts_mrv2.yaml
|
||||
```
|
||||
|
||||
The V2 profiles bound Talker prefill to 512 tokens per step and select the
|
||||
Talker AR runner and the Code2Wav generation
|
||||
runner together. Native inter-stage delivery carries codec payloads directly;
|
||||
request-owned snapshots preserve buffers through asynchronous completion and
|
||||
CUDA graph reuse. Terminal completion waits for upstream stage metrics before
|
||||
releasing request state. Platform sections retain V1 on NPU, XPU, ROCm and MUSA;
|
||||
this change does not qualify MRV2 on those backends or enable other model families.
|
||||
Selecting V2 for a stage without `supports_native_mrv2_data_plane` emits a warning
|
||||
when pipeline and deployment settings are merged. That stage retains the legacy
|
||||
transport path; the warning does not establish support for that combination.
|
||||
|
||||
MRV2 model hooks use capability declarations rather than architecture or stage
|
||||
names. Omni lifecycle flags select the model state, and `_returns_tuple` declares
|
||||
the capture output contract. The optional batched predictor hook is `mtp`, with
|
||||
an explicit `mtp_output_key` (a string or two-part payload key), optional
|
||||
`mtp_validity_key`, and `mtp_graph_safe`/`mtp_disable_graph` capture controls.
|
||||
Its inputs are token IDs, embeddings, previous hidden states and per-row
|
||||
conditioning; it returns updated embeddings and prediction codes. Models may
|
||||
supply `mtp_sampling_params` and `get_mtp_seed(sampling_params)` for model-local explicit seeds, and declare
|
||||
`mtp_accepts_per_row_generators`, `mtp_accepts_req_infos`, or `mtp_sample_uniforms`
|
||||
(with `mtp_sample_steps` and `mtp_sample_vocab_size`) as needed. Qwen3-TTS retains
|
||||
its existing `talker_mtp` entry point for V1.
|
||||
|
||||
### Included performance work
|
||||
|
||||
- Request snapshots have a fast path for immutable scalar leaves, including
|
||||
waveform lists. Mutable containers, aliases and cycles keep deep-copy
|
||||
semantics. This is not the historical shallow `dict(prompt)` experiment.
|
||||
- The API reference cache holds owned float32 arrays with a default capacity
|
||||
of 1024 entries and a 512 MiB waveform-payload budget. Cache hits return
|
||||
independent lists. Qwen3-TTS model artifact caches also default to 1024
|
||||
entries. These caches have different owners and eviction policies; equal
|
||||
entry limits do not make them a single coherent cache.
|
||||
- Talker state stays on GPU where its lifecycle allows it. BOS/EOS projections
|
||||
are cached in their projection dtype and invalidated on weight loading.
|
||||
- Code2Wav packs CPU codec inputs before device transfer. Optional B2/B4 graph
|
||||
buckets batch compatible requests without sharing their per-request state.
|
||||
- Native output materialization runs in a bounded worker and drains before
|
||||
closing the data plane. The existing control-only shortcut avoids launching
|
||||
model work for a step that has only lifecycle events.
|
||||
|
||||
MTP prefix re-prefill remains disabled in the high-concurrency V2 profile.
|
||||
Adding more graph shapes increases compilation cost and has not established a
|
||||
stable end-to-end gain for this extracted version.
|
||||
|
||||
### Decoder batches and playback
|
||||
|
||||
Changing `decode_cudagraph_batch_sizes` selects the captured batch buckets.
|
||||
Keep `decode_batch_max_size` consistent with the intended maximum too; the
|
||||
stateful graph path currently groups according to the captured buckets, while
|
||||
the stateless path also uses the explicit maximum.
|
||||
|
||||
B4 remains experimental. First-packet latency alone does not establish
|
||||
uninterrupted playback. Validate inter-chunk arrival times, buffering, WER and
|
||||
speaker similarity before adopting either batching preset for a production
|
||||
workload. Floating-point decoder outputs can differ across batch sizes; this PR
|
||||
does not claim bitwise or quality equivalence.
|
||||
|
||||
### Optional MPS deployment
|
||||
|
||||
NVIDIA MPS is an optional operator setting for colocated CUDA processes, not a
|
||||
YAML option or a library default. This PR does not establish a throughput or
|
||||
first-packet latency benefit from MPS. Measure the exact deployment with and
|
||||
without MPS before enabling it.
|
||||
|
||||
Use only assigned GPUs and an independent MPS pipe directory. A private MPS
|
||||
server does not provide exclusive GPU ownership or MIG isolation. For a
|
||||
single-GPU deployment, explicitly place both stages on that GPU; the supplied
|
||||
high-concurrency profile places its two stages on different GPUs by default.
|
||||
@@ -323,7 +323,7 @@ inputs, fields, and ownership rules are described below.
|
||||
flowchart TB
|
||||
layer1["Layer 1 · Authoring inputs<br/>PipelineConfig + DeployConfig"]
|
||||
layer2["Layer 2 · Production resolution boundary<br/>resolve_omni_config()<br/>(StageConfigFactory + VllmOmniConfig internally)"]
|
||||
layer3["Layer 3 · Startup hand-off<br/>OmniConfigResolution<br/>PipelineConfig + temporary OmegaConf stage bridge"]
|
||||
layer3["Layer 3 · Startup hand-off<br/>OmniConfigResolution<br/>PipelineConfig + typed stage configs"]
|
||||
layer4["Layer 4 · Runtime launch planning<br/>StageRuntime"]
|
||||
layer5["Layer 5 · Engine materialization<br/>VllmConfig / OmniDiffusionConfig"]
|
||||
|
||||
@@ -335,10 +335,10 @@ The production resolution boundary is `resolve_omni_config()` in
|
||||
It delegates typed construction to `StageConfigFactory.create_from_model()` and
|
||||
`VllmOmniConfig.from_pipeline_config()`, then returns an
|
||||
`OmniConfigResolution` consumed by both `AsyncOmniEngine` and headless startup.
|
||||
Until `StageRuntime` consumes typed stage configs directly, this envelope carries
|
||||
the effective `PipelineConfig` alongside OmegaConf-compatible `stage_configs` as
|
||||
a temporary runtime bridge; both views describe the same resolved topology,
|
||||
including injected stages.
|
||||
This envelope carries the effective `PipelineConfig` alongside typed
|
||||
`stage_configs`, which `StageRuntime` consumes directly. Both views describe the
|
||||
same resolved topology, including injected stages. Backend arguments are
|
||||
projected from these typed stages when each engine is initialized.
|
||||
|
||||
The legacy `stage_args` YAML path has been removed. Model topology now resolves
|
||||
through `PipelineConfig`, with runtime overrides supplied by `DeployConfig`.
|
||||
@@ -360,7 +360,7 @@ The important ownership rules are:
|
||||
3. CLI and Python overrides are applied at the resolution boundary, with
|
||||
per-stage overrides taking precedence over global values where supported.
|
||||
4. `OmniConfigResolution` is the sole startup hand-off. Its `pipeline_config`
|
||||
and temporary `stage_configs` compatibility view must describe the same
|
||||
and typed `stage_configs` must describe the same
|
||||
topology.
|
||||
5. `StageRuntime` owns launch planning and replica lifecycle. `ReplicaInitPlan`
|
||||
is runtime-private state, not a user configuration object.
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
# Mooncake KV Transfer
|
||||
|
||||
## PR summary
|
||||
|
||||
This change adds an opt-in AR-to-DiT KV handoff using vLLM's native
|
||||
`KVTransferConfig`, Scheduler-owned diffusion KV pages, and
|
||||
`MooncakeConnector`. The existing `OmniKVTransferManager` path remains the
|
||||
fallback when `kv_transfer_config` is absent.
|
||||
|
||||
The orchestrator creates one transfer ticket per request and resolves the
|
||||
bound AR replica's Mooncake endpoint after generation. DiT allocates the
|
||||
destination pages, waits for every worker rank to finish loading, and releases
|
||||
the source and destination pages only after the connector lifecycle completes.
|
||||
Request registration failures are rolled back before Worker dispatch. Receive
|
||||
timeouts and cancellation terminate the request without recycling in-flight
|
||||
pages: the scheduler retains them until every CFG row has completed on every
|
||||
rank. Late completion is polled only while such requests remain. If a peer
|
||||
never completes, its pages remain reserved until shutdown; other requests can
|
||||
continue using the remaining capacity. Invalid pages, missing Worker ranks,
|
||||
and failures without a proven rollback still stop the replica before teardown.
|
||||
The example sets `transfer_timeout: 60.0` seconds explicitly. Cancellation
|
||||
queued during a synchronous receive is processed when that receive completes
|
||||
or times out; it does not interrupt the remote write.
|
||||
|
||||
## Reference configuration
|
||||
|
||||
| Stage | Replicas | Devices | Parallelism | KV cache |
|
||||
| --- | ---: | --- | --- | ---: |
|
||||
| AR | 2 | 0-3 | TP=2 per replica | 2 GiB per worker |
|
||||
| DiT | 2 | 4-7 | TP=2 per replica, EP enabled | 1 GiB per worker |
|
||||
|
||||
The acceptance workload uses 512x512 images, two denoising steps, guidance
|
||||
scale 2.0, and `max_num_seqs=1`.
|
||||
|
||||
For a four-GPU debug run, set both stages to one replica and assign AR TP=2 to
|
||||
devices `0,1` and DiT TP=2 to devices `2,3`.
|
||||
|
||||
## Experiment
|
||||
|
||||
The final comparison used one node with eight NVIDIA RTX PRO 6000 Blackwell
|
||||
Server Edition GPUs, TCP Mooncake transport, the same 50-prompt/seed T2I
|
||||
dataset, concurrency 8, and two independent 100-request runs per
|
||||
implementation. Each side completed 200/200 requests without an OOM or request
|
||||
failure.
|
||||
|
||||
| Implementation | Run 1 (req/s) | Run 2 (req/s) | Mean (req/s) |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| `OmniKVTransferManager` | 4.4103 | 4.4497 | 4.4300 |
|
||||
| Native `MooncakeConnector` | 4.4457 | 4.4699 | 4.4578 |
|
||||
|
||||
Native throughput is **0.63% higher** than the legacy mean. The absolute
|
||||
performance difference is below the 1% acceptance threshold.
|
||||
|
||||
For accuracy, a non-zero transferred prefix and the resulting attention output
|
||||
were compared bit-for-bit with the legacy manager path:
|
||||
|
||||
```text
|
||||
prefix_bits_equal True
|
||||
attention_bits_equal True
|
||||
max_abs_diff 0.0
|
||||
```
|
||||
|
||||
The one-AR/one-DiT TP=2 debug topology also completed 20/20 sequential
|
||||
requests. A separate real-TCP transport check transferred both CFG rows from a
|
||||
TP=1 producer to a strict-Ulysses SP=2 consumer with exact values on both
|
||||
target ranks.
|
||||
|
||||
## Scope and limitations
|
||||
|
||||
- The fully validated end-to-end topology is 2x AR TP=2 plus 2x DiT TP=2.
|
||||
- Heterogeneous TP=1 to SP=2 reshaping is transport-tested, but arbitrary mixed
|
||||
TP/CFG/SP end-to-end combinations have not been exhaustively validated.
|
||||
- CFG-parallel end-to-end deployment is not part of this acceptance run.
|
||||
- Native AR-to-DiT transfer currently requires `async_chunk: false`.
|
||||
- TCP is covered by the acceptance run. The supplementary single-node RDMA
|
||||
measurements use a separate short workload; they do not establish RDMA
|
||||
support for multi-node deployment, which remains out of scope.
|
||||
- The native path currently validates the two-stage `AR(0) -> DiT(1)` graph. Other stage graphs are rejected during startup until
|
||||
their producer/consumer edge mapping is implemented.
|
||||
- Sleep mode is disabled for the native paged path because registered KV pages
|
||||
must remain mapped for the connector lifetime.
|
||||
|
||||
- Ascend NPU adaptation and its performance validation are maintained in a
|
||||
separate follow-up PR; the GPU acceptance results above do not validate NPU.
|
||||
- The homogeneous piecewise CUDA batching optimization is retained. A native
|
||||
paged-attention CUDA graph capture/replay regression covers two CFG rows,
|
||||
strided QKV, mixed causal/image spans, changed inputs, and output stability.
|
||||
It must be run on CUDA hardware; CPU unit tests do not establish graph or
|
||||
end-to-end performance acceptance.
|
||||
@@ -84,10 +84,22 @@ Block topology comes from the pipeline `OffloadPlan` first: `block_attrs` maps
|
||||
each DiT path to its ordered block containers, `encoder_block_attrs` declares
|
||||
streamable encoder stacks, `on_demand_component_paths` marks pipeline-managed
|
||||
residency, `resident_dit_paths` marks the DiTs that may hold resident layers,
|
||||
and `encoder_dlo_weight_replication` marks the encoders whose loader-produced
|
||||
weights are safe for AllGather. DiTs absent from the plan fall back to
|
||||
`_layerwise_offload_blocks_attrs` (including the deprecated singular-name
|
||||
compatibility path).
|
||||
`encoder_dlo_weight_replication` marks the encoders whose loader-produced
|
||||
weights are safe for AllGather, and `offload_submodules` maps a DiT submodule
|
||||
that owns its residency to its own block container. DiTs absent from the plan
|
||||
fall back to `_layerwise_offload_blocks_attrs` (including the deprecated
|
||||
singular-name compatibility path).
|
||||
|
||||
An undeclared DiT submodule joins the plan only when it is large enough that
|
||||
keeping it resident would defeat streaming; its block container is then found
|
||||
by scanning well-known attribute names, which warns once per submodule class.
|
||||
Size is computed from parameter shapes and dtypes, including meta parameters,
|
||||
so resolving before mmap loading preserves the same residency decision.
|
||||
Block ownership validation includes nested components: a block shared with a
|
||||
parent, sibling, or encoder is rejected before any hooks or storage are changed.
|
||||
A submodule with no block container must implement the `load_to_device` /
|
||||
`offload_to_cpu` lifecycle, and the resolver rejects it before any component of
|
||||
the pipeline is placed or hooked.
|
||||
|
||||
The resolver is pure: it moves no tensor, installs no hook, writes no module
|
||||
attribute, and reads no process group — multi-rank facts come from
|
||||
|
||||
@@ -318,18 +318,18 @@ 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.
|
||||
Do not pass `--no-async-chunk` when you want this optimization. Disabling
|
||||
async chunk 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.
|
||||
Prefix cache and async Omni output can run together. `save_outputs` returns
|
||||
a `step_id`; the output builder later calls `materialize(sid, req_ids)` with
|
||||
the save-time request list, which may be a step late relative to the live
|
||||
`input_batch`. `_should_use_async_omni_output()` does not disable itself
|
||||
when the cache is present. See
|
||||
[Automatic Prefix Caching](prefix_caching.md#implementation).
|
||||
|
||||
## Compatibility and Fallbacks
|
||||
|
||||
@@ -341,10 +341,9 @@ runtime conditions hold:
|
||||
| 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 |
|
||||
| Postprocess is absent or explicitly runs eagerly | State needed by the next decode step must be updated before the runner returns. Eager postprocess sees the scheduled slice (`None, None` combined tensors), not the prefix-cache-merged full prompt; it must only consume the tail |
|
||||
|
||||
These checks are evaluated per stage on runners based on `GPUARModelRunner`.
|
||||
An unsupported combination does not prevent serving; it only falls back to
|
||||
@@ -380,6 +379,8 @@ so Omni payload materialization remains synchronous.
|
||||
- `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.
|
||||
- [Automatic Prefix Caching](prefix_caching.md#implementation): Prefix-cache
|
||||
`step_id` / `materialize` contract used by the deferred output builder.
|
||||
- [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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# Automatic Prefix Caching in Omni Models
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Table of Contents
|
||||
@@ -9,6 +8,8 @@
|
||||
- [High-Level Approach](#high-level-approach)
|
||||
- [Example](#example)
|
||||
- [What About Multimodal Inputs?](#what-about-multimodal-inputs)
|
||||
- [Implementation](#implementation)
|
||||
- [Related Files](#related-files)
|
||||
|
||||
---
|
||||
|
||||
@@ -27,6 +28,7 @@ vLLM implements automatic prefix caching for managing its kv-cache, which is bes
|
||||
This document describes vLLM-Omni's mechanism for caching tensor outputs that are meant to be passed between stages, when requests have common prefixes, similar to the way in which vLLM has prefix caching for the kv-cache. This works in conjunction with vLLM's multimodal encoder caching, but is distinct. See the final section for a concrete example for how they tie together in practice.
|
||||
|
||||
### High-Level Approach
|
||||
|
||||
!!! note "Note 2"
|
||||
Prior to reading this section, it's recommended to take a look at the design documents in vLLM for [Automatic Prefix Caching](https://docs.vllm.ai/en/latest/features/automatic_prefix_caching/), which will make some of the concepts more clear.
|
||||
|
||||
@@ -38,8 +40,10 @@ The main focus of vLLM-Omni's approach to prefix caching stage outputs is to bui
|
||||
|
||||
With this in mind, consider the set of blocks in a 2D layout, where the row represents the index of blocks being considered, and the columns represent the slots corresponding to tokens within each block. Since we know the `num_blocks` and `block_size` from our kv cache config, if we want to cache a tensor with feature size `D`, we can preallocate a CPU tensor of size `(num_blocks, block_size, D)`, and use the same block index and slot mapping to retrieve the corresponding feature vector.
|
||||
|
||||
Host footprint: each cached key costs `num_blocks × block_size × D × dtype_bytes` of **pinned** CPU memory (pinned so device→host can overlap compute), allocated on the first `save_outputs` that sees the key — the first real request pays the `cudaHostAlloc`. Measured on a Qwen3-Omni deployment: thinker `__hidden_states__` `[15092, 16, 2048]` bf16 ≈ 0.92 GiB (plus the same again for each `hidden_states.layer_*` key a model exposes), Qwen3-TTS talker `codes.audio` `[16180, 16, 16]` ≈ 33 MiB. Budget host RAM for the stage accordingly.
|
||||
|
||||
### Example
|
||||
|
||||
!!! note "Note 3"
|
||||
Prefix caching in vLLM-Omni currently is only supported on AutoRegressive stages with one kv-cache group. Configure it with the pipeline-wide `enable_prefix_caching` field in the deploy config.
|
||||
|
||||
@@ -56,7 +60,7 @@ The prefix cache flow is then outlined below.
|
||||
|
||||
2. Say we process the request `The quick brown fox was tired and slept beneath the shady tree`, which is 12 tokens and evenly divides into 3 blocks as shown below.
|
||||
|
||||
```
|
||||
```text
|
||||
[ The quick brown fox ] [ was tired and slept ] [beneath the shady tree ]
|
||||
Block 1: |<--- block tokens ---->|
|
||||
Block 2: |<------- prefix ------>| |<--- block tokens --->|
|
||||
@@ -65,10 +69,9 @@ Block 3: |<------------------ prefix -------------------->| |<--- block tokens -
|
||||
|
||||
When the request processes, we inspect the multimodal outputs and identify the `mm_feature` tensor, which will be of shape `(seq_len, feature_dim)`, i.e., `(12, 16)` in this example. We note that the first axis is dependent on the `seq_len` and add a new cache_tensor of shape `(num_blocks, block_size, feature_dim)` to our multimodal cache for tensors.
|
||||
|
||||
1. If we lay out the cache as a 2D tensor of shape (`num_blocks`, `block_size`), we'll have something like the following:
|
||||
|
||||
3. If we lay out the cache as a 2D tensor of shape (`num_blocks`, `block_size`), we'll have something like the following:
|
||||
|
||||
```
|
||||
```text
|
||||
0: [ The quick brown fox ]
|
||||
1: [ was tired and slept ]
|
||||
2: [beneath the shady tree ]
|
||||
@@ -78,7 +81,8 @@ When the request processes, we inspect the multimodal outputs and identify the `
|
||||
```
|
||||
|
||||
Or, if we flatten it down to 1D,
|
||||
```
|
||||
|
||||
```text
|
||||
0: The
|
||||
1: quick
|
||||
2: brown
|
||||
@@ -90,7 +94,8 @@ Or, if we flatten it down to 1D,
|
||||
```
|
||||
|
||||
which we can think of as row indices into the hidden states tensor if we view it as the 2D shape `(num_blocks x block_size, feature_dim)`. That is, the analogous flattened (from 3D -> 2D) mapping of the cache for hidden states becomes the following.
|
||||
```
|
||||
|
||||
```text
|
||||
0: <hidden states vector of len 2 corresponding to 'The'>
|
||||
1: <hidden states vector of len 2 corresponding to 'quick'>
|
||||
2: <hidden states vector of len 2 corresponding to 'brown'>
|
||||
@@ -103,10 +108,9 @@ which we can think of as row indices into the hidden states tensor if we view it
|
||||
|
||||
Similarly, for the multimodal outputs cache, the flattened coordinates are the same, but the `mm_feature` maps to vectors of length `16` instead of the hidden size of `2`. Note that in practice, we may have multiple multimodal output tensors per forward pass, which may have different names and different feature dimensions.
|
||||
|
||||
1. Now, say that we receive a new request `The quick brown fox jumped over the dog`.
|
||||
|
||||
4. Now, say that we receive a new request `The quick brown fox jumped over the dog`.
|
||||
|
||||
```
|
||||
```text
|
||||
[ The quick brown fox ] [ jumped over the dog ]
|
||||
Block 1: |<--- block tokens ---->|
|
||||
Block 2: |<------- prefix ------>| |<--- block tokens --->|
|
||||
@@ -116,7 +120,7 @@ Here, we will have a cache hit for `Block 1` which will be detected by vLLM base
|
||||
|
||||
Since we have the block indices / slot mappings from the kv cache manager, we can simply mirror the mappings and leverage the same indices for the cached hidden states and multimodal outputs. This allows us to look up the correct tensors from our externally maintained 3D caches.
|
||||
|
||||
```
|
||||
```text
|
||||
0: [ The quick brown fox ] < already in the cache
|
||||
1: [ was tired and slept ]
|
||||
2: [beneath the shady tree ]
|
||||
@@ -129,11 +133,11 @@ Since we have the block indices / slot mappings from the kv cache manager, we ca
|
||||
|
||||
Finally, to pass the full hidden states and multimodal outputs to the next stage, we simply concatenate the cached contents with the corresponding new tensors computed from the current forward call.
|
||||
|
||||
|
||||
### What About Multimodal Inputs?
|
||||
|
||||
It's also useful to consider the case about how Omni prefix caching is handled when we have multimodal inputs that don't cleanly end on block boundaries, as well as how this works with multimodal encoder caching in vLLM. For example:
|
||||
|
||||
```
|
||||
```text
|
||||
[ Im0 Im1 Im2 Im3 ] [ Im4 Im5 foo <empty> ]
|
||||
Block 1: |<--- block tokens ---->|
|
||||
Block 2: |<------- prefix ------>| |<--- block tokens --->|
|
||||
@@ -147,9 +151,9 @@ In reality, this isn't a big problem for correctness, because vLLM also maintain
|
||||
- The hash describing the image data starting at position 0 and with length 6
|
||||
- In vLLM's encoder cache, a mapping from the image hash above to the encoder output
|
||||
|
||||
|
||||
To understand what happens, say we get the following input as a second request:
|
||||
```
|
||||
|
||||
```text
|
||||
[ Im0 Im1 Im2 Im3 ] [ Im4 Im5 bar baz ]
|
||||
Block 1: |<--- block tokens ---->|
|
||||
Block 2: |<------- prefix ------>| |<--- block tokens --->|
|
||||
@@ -162,3 +166,162 @@ Because we have multimodal data in a scheduled span that isn't fully precomputed
|
||||
When we pass our multimodal tensors to the language model component in the same stage, we'll then expect the same outputs, because the prefix caching behaviors in vLLM-Omni / vLLM match, so the LLM will use vLLM's KV cache manager's prefix caching to correctly handle the attention information for `Block 1` while calculating the outputs for `Block 2`, giving us the correct results for processing `Block 2` with the context of `Block 1`.
|
||||
|
||||
Finally, we look up the output hidden states/multimodal tensors corresponding to the prefix cache hit `Block 1` and concatenate it with the forward pass result to get the final result, which is expected to be identical to the full hidden states when prefix caching is disabled.
|
||||
|
||||
### Implementation
|
||||
|
||||
The block/slot model is `vllm_omni/core/prefix_cache/`.
|
||||
`OmniPrefixCacheManager` owns slot occupancy, the request-task table,
|
||||
hit spans, and merge.
|
||||
`OmniPrefixCacheController` moves data: a reusable `StagingBufferPool` for
|
||||
this step's device→host copy, and writes into the durable `PrefixBlockPool`.
|
||||
The state lock covers those tables only.
|
||||
|
||||
Miss is not an error (this step's forward slice only). A hit span whose
|
||||
hidden rows are absent is fatal. For mm keys the rule is looser: a model may
|
||||
emit a key only for some requests, so a hit span with no rows behind an mm key
|
||||
reads zeros from the pool for those positions rather than raising. Abort still
|
||||
writes: once a hash entered this step's batch it must land in the cache.
|
||||
|
||||
`enable_prefix_caching` is refused on KV-consumer stages (`kv_role` of
|
||||
`kv_consumer` or `kv_both`). KV received from a producer is reported as
|
||||
`num_computed_tokens` too, and the manager cannot tell it from a local hit.
|
||||
Producer-only stages are unaffected. Pooling stages never save, so they get
|
||||
no cache. This gate is one function, `stage_prefix_cache_config`, called by
|
||||
both the GPU and the NPU model runner at kv-cache init.
|
||||
|
||||
Which stages may set `enable_prefix_caching: true`:
|
||||
|
||||
| Stage | `enable_prefix_caching` | Why |
|
||||
| --- | --- | --- |
|
||||
| AR stage with one full-attention kv group whose hidden states / per-token mm feed the next stage (Qwen3-Omni thinker and talker) | supported | The case the cache is built for: full-prompt hidden states are merged from the pool on a hit. |
|
||||
| AR stage that sets `requires_full_prefix_cached_hidden_states = False`, optionally with `deferred_prefix_cache_mm_keys` (Qwen3-TTS talker, Higgs v3 talker) | supported | Hidden is not cached; deferred codec rows are written once on finish. |
|
||||
| Pooling stage | ignored | Never saves; the gate returns no config. |
|
||||
| `kv_role: kv_consumer` / `kv_both` | refused at kv-cache init (`OmniPrefixCacheUnmatchError`) | Producer KV shows up as `num_computed_tokens` and is indistinguishable from a local hit. |
|
||||
| Speculative decoding on the stage | refused at kv-cache init | Under async scheduling vLLM keeps `num_computed_tokens_cpu` optimistic (all drafts accepted) during the forward and corrects it afterwards; `step_slots_cpu` would mirror rows at the wrong slots. Not verified; refused as a whole. |
|
||||
| `prefix_match_unit` smaller than `block_size` | refused at kv-cache init | Sub-block hits make `num_computed_tokens` unaligned; the hit registry only mirrors whole blocks. |
|
||||
| Hybrid / sliding-window / multi-group kv cache (e.g. a talker with `attention_type: sliding_recompute`) | refused at kv-cache init | The cache mirrors exactly one full-attention block table. |
|
||||
| Attention backend whose kernel block size differs from `--block-size` (FlashInfer / FlashMLA / CutlassMLA with a block size they do not list natively), or decode context parallel | refused at first step (`FullAttentionGroupView`) | `step_slots_cpu` computes `table[req, pos // block_size] * block_size + pos % block_size` over allocator block ids; hybrid kernel blocks and DCP token striping change that row layout. FlashAttention / Triton accept any multiple of 16 and never split blocks. |
|
||||
| Codec decoder / Code2Wav stages (Qwen3-Omni stage 2, Qwen3-TTS stage 1) | keep `false` | Nothing downstream consumes their hidden states; the cache would only add device→host copies. Not validated. |
|
||||
| Diffusion stages | n/a | No vLLM KV cache to mirror. |
|
||||
|
||||
Hit spans come from `scheduled_new_reqs` only, as in the pre-refactor cache:
|
||||
|
||||
- A new request with a (partial) prefix hit is the normal path: the hit
|
||||
blocks are read from the pool, the rest is this step's rows, and the
|
||||
divergent tail is written under its own blocks. The gather starts on the
|
||||
prefetch thread at `new_step_starts`; a same-step hit (vLLM hashes blocks
|
||||
at schedule time, so `b` can hit blocks `a` computes in the same forward)
|
||||
cannot plan until `a`'s write is registered and starts at `save_outputs`
|
||||
instead. Either way it runs before the next step can hand those blocks
|
||||
to a new tenant. vLLM frees blocks one step before `finished_req_ids`
|
||||
arrives, so a finished request whose last step materializes late may
|
||||
still find a hit slot reassigned. Each `(slot, key)` carries a write
|
||||
version, bumped whenever a new write claims it. A planned read captures
|
||||
that version and is registered in `_pending_reads`; a later write that
|
||||
reclaims those slots copy-on-writes the still-`COMMITTED` rows into the
|
||||
ref before it overwrites the pool, so a delayed fetch serves the original
|
||||
tenant. A version mismatch with no preserved copy raises for live and
|
||||
finished alike (the pool rows are a newer tenant's). Production defaults
|
||||
stay opt-in until preempt/resume hit spans are reconstructed.
|
||||
- `async_chunk` continuation: when the next upstream chunk arrives, the same
|
||||
request id re-enters `scheduled_new_reqs` with `num_computed_tokens` equal
|
||||
to what it already computed itself. Ids already in `live_reqs` are skipped
|
||||
for hit marking: those rows were delivered in earlier steps and re-emitting
|
||||
them would duplicate output. A `delivered_upto` span for this case is
|
||||
Phase 2.
|
||||
- Preemption + reschedule: vLLM resets `num_computed_tokens` to 0 on
|
||||
preemption and re-runs prefix matching on resume, so the resumed request
|
||||
can come back with a fresh hit. With the V1 model runner it arrives
|
||||
through `scheduled_cached_reqs` (id in `resumed_req_ids`, `new_block_ids`
|
||||
replaces the table); with the V2 runner it re-enters `scheduled_new_reqs`
|
||||
while still in `live_reqs`. Neither path marks an omni hit span: the
|
||||
resumed request gets only the rows it recomputes, and its still-open
|
||||
deferred write keeps appending (a slot written twice keeps the later
|
||||
chunk). Cache integrity holds either way — the hit blocks already have
|
||||
rows, from this request or the one it hit. Stages that need full prompt
|
||||
hidden states should be sized so preemption does not occur while prefix
|
||||
caching is on. Same as before this refactor; tracked for Phase 2.
|
||||
|
||||
Two write paths, split by `ModelCachePolicy.deferred_keys`:
|
||||
|
||||
- Immediate (`JOIN_NEXT_STEP`): save launches a whole-step device→host into
|
||||
a staging slot; the committer waits that event and writes the CPU pool.
|
||||
The next save waits `done` (the pool write): a reused slot must never
|
||||
leave a pending pool write behind, or a delayed hit read of the old rows
|
||||
would find nothing recoverable.
|
||||
- Deferred (`JOIN_ON_FINISH`): mm whose first dim is this step's token count
|
||||
stays on a per-request GPU clone; `_WriteChunk`s append across steps;
|
||||
finish/abort (or GPU-byte-budget pressure) forces the copy. One open
|
||||
WriteTask per request; a budget flush may close it mid-request, in which
|
||||
case the next save opens a new one (`write_n` + 1) and a hit reads both,
|
||||
so one long request cannot pin the whole budget.
|
||||
|
||||
A `WriteTask` moves through `TaskState` only via `transition()`, one step at
|
||||
a time along a strict chain:
|
||||
`PENDING` (registered, not queued) → `QUEUED` (on a copy queue) →
|
||||
`COPYING` (one thread owns the copy stage) → `HOST_READY` (host rows ready) →
|
||||
`WRITTEN` (in the CPU pool); `FAILED` is reachable from any non-terminal
|
||||
state. Skipping a step raises. The worker claims `COPYING` in the same `_wake`
|
||||
critical section as the queue pop, so `escalate` never re-queues a task it can
|
||||
see is already claimed. `host_ready` / `done` are wait primitives set by the
|
||||
transitions into `HOST_READY` / `WRITTEN` / `FAILED`. GPU-clone bytes are
|
||||
charged once per clone on a `_BudgetTicket` pinned by every task that views
|
||||
it, and uncharged when the last holder releases (idempotent per tid).
|
||||
|
||||
Mm whose first dim equals the unpadded scheduled length *or* the CUDA-graph
|
||||
padded length is registered on first sighting. Talker `codes.audio` is a
|
||||
cat of scheduled rows and stays unpadded while hidden is padded; both must
|
||||
open a pool key, whether immediate or deferred.
|
||||
Leftover mm (lists, `codes.ref`, any tensor whose first dim is not this
|
||||
step's token count) is copied to CPU at save without truncating that first
|
||||
dim. That leftover copy is the async-builder read replica for this step; it
|
||||
does not write the pool or carry abort/preempt occupancy.
|
||||
|
||||
```python
|
||||
cache.register_policy(ModelCachePolicy.from_model(model)) # load_model
|
||||
cache.new_step_starts(scheduler_output) # before _update_states
|
||||
sid = cache.save_outputs(hidden, mm_outputs, num_tokens_unpadded=n,
|
||||
num_tokens_padded=n_pad)
|
||||
outs = cache.materialize(sid, req_ids) # or discard_step(sid)
|
||||
```
|
||||
|
||||
Each step id is consumed exactly once. `req_ids` must be a subset of the save
|
||||
snapshot. At most `staging_depth` unused step ids may exist at once: every
|
||||
`save_outputs` claims one staging slot, including saves with only leftover
|
||||
mm that copy no device→host page. A slot is also held by each immediate
|
||||
write that views it, until the committer's pool write. A later save waits
|
||||
for `materialize`/`discard_step` or that pool write to free a slot;
|
||||
`staging_claim_timeout_s` then errors with the unused ids and the task count.
|
||||
`join`/`join_host_ready` use the same bound and raise with the stuck task's
|
||||
id and state instead of hanging the caller.
|
||||
`materialize` may run on the async output builder after the engine has
|
||||
entered the next step; leftover mm (not written to the pool) is copied
|
||||
to CPU at `save_outputs` so the builder never reads live CUDA-graph
|
||||
buffers. See
|
||||
[Async Omni Output Materialization](omni_async_output_materialization.md).
|
||||
|
||||
The cache is constructed only on the last pipeline-parallel rank
|
||||
(`_ensure_omni_prefix_cache`). Other ranks skip it: they never call
|
||||
`save_outputs`, so a hit table there would resolve to absent slots.
|
||||
|
||||
Threads, locks, and what each may block on:
|
||||
|
||||
| Thread | Role | May block on | Must not hold while blocked |
|
||||
| --- | --- | --- | --- |
|
||||
| Engine | `new_step_starts`, `save_outputs` | previous-step `join` (`done`) / finished-write `join_host_ready`; `reserve()` GPU-byte flush; staging-slot claim; `dispatch()` / finish-abort `escalate()` (eager mode: the copy + pool write run inline) | `_state_lock` |
|
||||
| Async output builder | `materialize` (may overlap the next engine step) | this step's `step_d2h_event`; `join` (`done`); deferred `fetch_host` | `_state_lock` |
|
||||
| Committer | `_worker_loop`: wait device→host / deferred copy / pool write | `_wake.wait`; `step_d2h_event` or copy-stream sync | never takes `_state_lock` |
|
||||
| Prefetch pool | hit-span gather during forward | `join` (`done`); deferred `fetch_host` | `_state_lock` |
|
||||
|
||||
| Lock | Covers | Does not cover |
|
||||
| --- | --- | --- |
|
||||
| manager `_state_lock` | occupancy tables, step contexts, hit spans, task registration, pool-key publish (`install_key`) | join, GPU-byte flush, copy, `step_d2h_event` wait, pool-key allocation, eager `dispatch()` / `escalate()` |
|
||||
| controller `_lock` / `_wake` | task registry, queues, GPU-clone byte budget | device→host / pool-write body (released before `synchronize`) |
|
||||
| `WriteTask.lock` | `state`, `reassigned`, `append_chunk` | waiting on `host_ready` / `done` (those are events) |
|
||||
|
||||
### Related Files
|
||||
|
||||
- `vllm_omni/core/prefix_cache/`
|
||||
- `vllm_omni/worker/gpu_model_runner.py` (`_ensure_omni_prefix_cache`)
|
||||
- `tests/core/test_prefix_cache.py`
|
||||
- [Async Omni Output Materialization](omni_async_output_materialization.md)
|
||||
@@ -271,3 +271,111 @@ The required evidence is:
|
||||
|
||||
Audio that is empty, all zero, non-finite, or only a protocol `listen` event is
|
||||
not a successful end-to-end result.
|
||||
|
||||
### Paced multi-session load driver
|
||||
|
||||
The existing driver keeps its two-session admission and slot-reuse validation
|
||||
when `--sessions` is omitted. Passing `--sessions N` selects a separate,
|
||||
bounded load mode; it does not replace the lifecycle test or change server
|
||||
admission capacity. Start an existing PersonaPlex server configured to admit
|
||||
the requested number of sessions, then run from a source checkout with the
|
||||
vLLM-Omni client dependencies installed:
|
||||
|
||||
```bash
|
||||
python -m tests.e2e.online_serving.personaplex_realtime_duplex \
|
||||
--model nvidia/personaplex-7b-v1 \
|
||||
--input-wav /path/to/input.wav \
|
||||
--sessions 2 --load-frames 1000 --drain-s 2 \
|
||||
--server-revision SERVER_COMMIT --server-hardware A100-80GB \
|
||||
--output-dir /tmp/personaplex-load-n2
|
||||
```
|
||||
|
||||
Use a new output directory for every run. The driver refuses to overwrite an
|
||||
existing `load-result.json`. The server labels are supplied by the operator,
|
||||
not detected or verified by the client. Record the server command, package
|
||||
versions, checkpoint revision and hardware alongside a published measurement.
|
||||
|
||||
All attempted sessions finish admission before admitted sessions share a
|
||||
common input start. A rejected session remains a failed row in the report;
|
||||
it is not removed from the denominator. Each session receives at most
|
||||
`--load-frames` input frames, without looping the WAV, and uses 80 ms pacing.
|
||||
A stalled send cannot cause a catch-up burst. The timeline records send
|
||||
schedule lateness so an overloaded client is visible rather than mistaken
|
||||
for a faster server. This mode measures steady concurrency, not join/leave
|
||||
interference; the original lifecycle mode still checks slot reuse.
|
||||
Load mode does not probe an additional N+1 session for `resource_exhausted`;
|
||||
the lifecycle mode owns that admission-limit check. A per-session
|
||||
`client_pacing_warning` and the summary's `client_pacing_warning_sessions`
|
||||
identify send-lateness p99 above one nominal frame (80 ms). This is a warning,
|
||||
not an extra acceptance gate: read it alongside server-facing measurements
|
||||
so a slow client is not mistaken for a slow server.
|
||||
|
||||
The fixed drain window is part of the measurement configuration. Audio
|
||||
arriving during cleanup is excluded, so closing cannot rescue a session that
|
||||
missed its observation deadline. Cleanup is attempted for admitted sessions
|
||||
even when audio validation fails. Protocol errors, missing/invalid audio,
|
||||
shared response IDs and cleanup failures make the report fail and the CLI
|
||||
exit nonzero. Cancellation closes transports and propagates to the caller.
|
||||
`--cleanup-timeout-s` (default 5 s) bounds session-close send and acknowledgement
|
||||
together, independently of `--timeout-s`. The subsequent WebSocket close
|
||||
handshake has the same separate bound; an unresponsive close acknowledgement
|
||||
cannot hold cancellation for the full inference timeout.
|
||||
|
||||
`load-result.json` retains per-session send and audio-packet timelines using
|
||||
the client's monotonic clock. It does not export handshake credentials or
|
||||
raw audio payloads. Metric definitions are explicit:
|
||||
|
||||
Both modes validate each nonempty audio packet's strict base64 encoding, PCM16
|
||||
sample alignment, 24 kHz sample rate and response identity. Valid aggregate
|
||||
audio cannot hide a malformed packet. In load mode, rejected packets make the session fail
|
||||
and appear in `invalid_audio_packets` with their event index, receive time and
|
||||
fixed diagnostic code; they do not contribute samples or timing intervals.
|
||||
Empty audio deltas remain legal and contribute no samples. This check does
|
||||
not change the public client's decoding. The lifecycle scenario and dispatch
|
||||
remain the same, but malformed packets that aggregate checks once tolerated
|
||||
now fail that mode as well.
|
||||
|
||||
The probe accepts both the current OpenAI Realtime audio event name
|
||||
`response.output_audio.delta` and the legacy `response.audio.delta`. Validation
|
||||
uses the wire payload directly rather than relying on a particular client
|
||||
library's alias table, so a server-side event-name migration cannot silently
|
||||
turn real audio into a zero-output measurement.
|
||||
|
||||
| Field | Meaning |
|
||||
| --- | --- |
|
||||
| `client_audio_packet_interval_ms` | Consecutive nonempty packet arrivals: median, linear p99, max, and zero-based index of the first maximal interval. Interval index 0 ends at packet 1. One packet can contain multiple codec frames; this is not server tick latency. |
|
||||
| `client_first_audio_after_stream_start_ms` | First audio receipt minus first input send start; admission is excluded. |
|
||||
| `client_stream_rtf` | First input send start to last audio receipt, divided by received audio duration. This includes real-time input pacing and is not an inference-only RTF. |
|
||||
| `client_send_lateness_ms` | Send start minus its original shared schedule, clipped at zero. |
|
||||
| `frame_deficit` | Integer input-frame minus output-frame count from the shared session validation; missing when valid whole-frame accounting is unavailable. |
|
||||
| `output_frames`, `voiced_frames`, `silent_frames`, `frame_coverage_ratio`, `audio_rms`, `audio_chunks` | Session-validation statistics retained on success and on acceptance failure once computed. |
|
||||
| `acceptance_check` | Failed local check, with numeric diagnostics in `error`; other exceptions retain phase/type only, without remote handshake or metadata text. |
|
||||
| `audio_underrun_s`, `audio_underrun_event_count`, `audio_continuity_ok` | Shared `compute_continuity_stats` on valid PCM16 packet arrivals, with its default 100 ms underrun threshold; absent audio gives missing values rather than a continuity pass. |
|
||||
|
||||
No output gives missing latency/RTF values, not zero. By default RTF is
|
||||
diagnostic; `--max-client-rtf` adds an explicit ceiling. The existing frame
|
||||
coverage, audible-frame and scheduler-data-plane checks still apply. A longer
|
||||
`--drain-s` can demonstrate eventual completion under backlog, but it must not
|
||||
be reported as real-time capacity when the fixed-window run failed or client
|
||||
RTF exceeded the stated target. Likewise, the first live session can pay
|
||||
one-time model/codec/JIT costs; either use the server's configured duplex
|
||||
warmup path or label that run as cold-start evidence before comparing steady
|
||||
capacity. Passing client-only metrics does not certify model quality or a
|
||||
real-time capacity target on another GPU. Begin with N=1 and N=2; higher N is a
|
||||
capacity experiment, not a promised property of an A100 or of this patch.
|
||||
Continuity is a client-arrival diagnostic, not proof of speaker playback and
|
||||
not an additional pass/fail gate. Packet-interval and send-lateness summaries
|
||||
retain their documented linear p99; the older client interval helper reports
|
||||
rounded nearest-rank p95 instead and is not substituted for that schema.
|
||||
|
||||
Deterministic driver and localhost WebSocket checks need no model or GPU:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/e2e/online_serving/test_personaplex_load_driver.py \
|
||||
-m 'core_model and cpu' --run-level core_model -q
|
||||
```
|
||||
|
||||
These checks test the driver against controlled protocol responses. Run the
|
||||
load-driver command against real weights separately before reporting model-serving
|
||||
performance. Server tick timestamps and generic TTFT/TPOT aggregation remain
|
||||
the responsibility of the existing duplex metrics paths, not this driver.
|
||||
+82
-14
@@ -9,6 +9,12 @@ the owner document for `vllm_omni/engine/duplex/**`,
|
||||
`vllm_omni/entrypoints/duplex_omni.py`, `vllm_omni/entrypoints/duplex/**`,
|
||||
`vllm_omni/clients/**` and the per-model `duplex/` packages.
|
||||
|
||||
`vllm_omni/protocol/realtime/**` (the OpenAI Realtime API) and
|
||||
`vllm_omni/protocol/duplex/**` (vLLM-Omni's extension of it) are the shared
|
||||
wire codec duplex is built on. Duplex is its first consumer but not its owner: the codec
|
||||
is deliberately runtime-agnostic so a second Realtime surface can reuse it
|
||||
(RFC #6592 P0a).
|
||||
|
||||
The public wire contract lives in
|
||||
[`docs/serving/realtime_duplex_api.md`](../serving/realtime_duplex_api.md);
|
||||
this page describes how it is produced.
|
||||
@@ -108,23 +114,56 @@ plugin and the session runtime config to `DuplexOrchestrator` directly.
|
||||
`AsyncOmniEngine`, `OrchestratorBase` and `Orchestrator` carry only
|
||||
template seams; `tests/engine/test_duplex_import_boundary.py` checks that
|
||||
importing the turn-based stack loads no duplex module.
|
||||
4. **Every surface a duplex server exposes is backed by a session.**
|
||||
4. **Deployment selects the serving stack.**
|
||||
`vllm-omni serve` constructs `DuplexOmni` when the pipeline declares
|
||||
`duplex_plugin`; the server exposes `/v1/realtime?duplex=1` (alias
|
||||
`duplex_plugin` and the deploy configuration sets `session_mode: duplex`.
|
||||
An explicit `session_mode: turn` selects `AsyncOmni` instead, without
|
||||
changing the pipeline's duplex capability. In duplex mode the server exposes `/v1/realtime?duplex=1` (alias
|
||||
`/v1/duplex`), `POST /v1/chat/completions`, `/v1/models` and `/health`, and
|
||||
every other turn-based route reports "not available". The chat route is
|
||||
served by `DuplexChatCompletionsAdapter`, a Realtime client that runs one
|
||||
short-lived session per request: speech becomes a committed turn, text is
|
||||
seeded as the session's opening turn because a model-native model takes a
|
||||
turn only when it hears speech. Whether a model can be reached with text is
|
||||
`DuplexCapabilities.supports_chat_completions`; one that cannot is refused
|
||||
at once rather than left to idle out. Turn-based use of the same model stays
|
||||
available offline through `Omni` / `AsyncOmni`.
|
||||
every other turn-based route reports "not available". The chat route uses
|
||||
the ordinary chat service on the duplex engine, without a session, when
|
||||
the plugin declares `DuplexCapabilities.supports_chat_completions`.
|
||||
Turn-based deployments use the ordinary API initialization and retain
|
||||
model capability checks and endpoint restrictions.
|
||||
5. **Serving is transport only.** The websocket handler does socket I/O,
|
||||
wire-envelope validation, command translation, event rendering and the
|
||||
attachment/resume/replay bookkeeping; it holds no session state.
|
||||
6. **No `typing.Protocol`** in the duplex surfaces: the plugin, data plane,
|
||||
session state, PCM buffer, stage port and client transport seams are ABCs.
|
||||
7. **The wire codec is not duplex, and a wire type is not a mailbox message.**
|
||||
The event and command *vocabulary* plus its wire rendering, the audio format
|
||||
negotiation, the conversation-item rules and the error envelope are model-
|
||||
and runtime-agnostic, and split along the three tiers
|
||||
[`docs/serving/realtime_duplex_api.md`](../serving/realtime_duplex_api.md)
|
||||
already defines: **Tier 1** (identical to OpenAI) is
|
||||
`vllm_omni/protocol/realtime/**`; **Tier 2** (OpenAI names carrying our
|
||||
extensions) and **Tier 3** (ours alone) are `vllm_omni/protocol/duplex/**`.
|
||||
A Tier 2 class *subclasses* its Tier 1 twin and declares only what it adds,
|
||||
so the OpenAI surface stays honestly pure --- `session.created` without
|
||||
`resume_token`, `input_audio_buffer.append` without `video_frames`. Our
|
||||
error **codes** are Tier 3 (`protocol/duplex/errors.py`); only the envelope
|
||||
shape and OpenAI's three `error.type` classes are Tier 1.
|
||||
What stays engine-side is everything that is *internal representation*
|
||||
rather than contract: `DuplexCommand.payload()` renders the session runner's
|
||||
mailbox dictionary, whose channel genuinely differs from the client event
|
||||
(`session.update` and the three `conversation.item.*` commands all travel on
|
||||
`turn.signal`); `engine/duplex/realtime_commands.py` decides which command a
|
||||
decoded event becomes; `engine/duplex/realtime_events.py` holds the
|
||||
session's projection state. The codec may not import the engine, the
|
||||
entrypoints, `model_executor` or the clients
|
||||
(`tests/protocol/realtime/test_protocol_import_boundary.py`), and there is
|
||||
exactly one implementation of each codec behaviour
|
||||
(`tests/protocol/realtime/test_realtime_codec_single_source.py`).
|
||||
The dependency chain is `protocol/realtime` <- `protocol/duplex` <- engine /
|
||||
entrypoints / clients, and a duplex consumer uses **only** the middle link:
|
||||
`protocol/duplex/**` re-exports the Tier 1 names it does not extend, so a
|
||||
helper that later needs a duplex-specific version (`convert_input_audio_with_rate`
|
||||
resamples to MiniCPM-o's 16 kHz rather than the client's rate) is overridden
|
||||
in one file instead of at every call site
|
||||
(`tests/protocol/duplex/test_duplex_protocol_facade.py`).
|
||||
`DuplexEvent` is therefore a plain alias of `RealtimeEvent` --- an event has
|
||||
no engine-internal half --- while `DuplexCommand` is a real class, because a
|
||||
command does.
|
||||
|
||||
## Package layout
|
||||
|
||||
@@ -143,6 +182,31 @@ vllm_omni/
|
||||
│ │ ├── chat_completions.py DuplexChatCompletionsAdapter (/v1/chat/completions on a session per request)
|
||||
│ │ └── websocket.py websocket send/close/receive helpers
|
||||
│ └── openai/api_server.py builds DuplexOmni for duplex models; session-backed app state
|
||||
├── protocol/ SHARED WIRE CODEC (no engine / no model / no transport)
|
||||
│ ├── realtime/ the OpenAI Realtime API itself
|
||||
│ │ ├── events.py RealtimeEvent + OpenAI's 30 server events (22 ship as Tier 1;
|
||||
│ │ │ 8 have a Tier 2 twin in protocol/duplex)
|
||||
│ │ ├── commands.py RealtimeCommand + OpenAI's 10 client commands (8 Tier 1,
|
||||
│ │ │ 2 with a Tier 2 twin) --- wire_type + fields, no payload()
|
||||
│ │ ├── formats.py audio format spellings, what is supported, per-session validation
|
||||
│ │ ├── session.py session-object readers; RealtimeInputDefaults (append defaults)
|
||||
│ │ ├── items.py conversation item shape, truncation, transcripts, camera frames
|
||||
│ │ ├── audio_input.py decode_audio_append -> RealtimeAudioAppend; client speech hints
|
||||
│ │ ├── audio.py PCM / G.711 / WAV decode, resample, re-encode
|
||||
│ │ ├── errors.py RealtimeProtocolError (the envelope's exception type)
|
||||
│ │ └── capabilities.py RealtimeProtocolCapabilities: what one consumer can serve
|
||||
│ └── duplex/ Tier 2 + Tier 3, and the one door a duplex consumer uses
|
||||
│ ├── __init__.py re-exports the Tier 1 helper functions (audio, formats,
|
||||
│ │ items, session, capabilities) so the engine never
|
||||
│ │ imports protocol/realtime directly
|
||||
│ ├── events.py the whole 42-event vocabulary: 22 Tier-1 re-exports,
|
||||
│ │ 8 Tier-2 subclasses (session.created + resume_token, ...),
|
||||
│ │ 12 Tier-3 events (listen/speak, playback, resume, ...);
|
||||
│ │ DuplexEvent = RealtimeEvent
|
||||
│ ├── commands.py the whole 17-command vocabulary: 8 Tier-1 re-exports,
|
||||
│ │ 2 Tier-2 subclasses (append + hints, commit + final),
|
||||
│ │ 7 Tier-3 commands (barge_in, playback.ack, ...)
|
||||
│ └── errors.py our error-code vocabulary (Tier 3) + RealtimeProtocolError
|
||||
├── engine/
|
||||
│ ├── omni_engine_base.py OmniEngineBase (stage processes, orchestrator thread, queues, RPC)
|
||||
│ ├── async_omni_engine.py AsyncOmniEngine (turn-based request building)
|
||||
@@ -150,16 +214,20 @@ vllm_omni/
|
||||
│ ├── orchestrator.py OrchestratorBase + Orchestrator
|
||||
│ ├── duplex_orchestrator.py DuplexOrchestrator (+ DuplexOrchestratorRequestState; implements DuplexStagePort)
|
||||
│ └── duplex/
|
||||
│ ├── commands.py DuplexCommand dataclasses, command_from_realtime
|
||||
│ ├── realtime_commands.py Realtime client event -> DuplexCommand translation
|
||||
│ ├── events.py DuplexEvent dataclasses (+ to_realtime), REALTIME_ERROR_TYPES_BY_CODE
|
||||
│ ├── commands.py mailbox half: DuplexCommand.payload() + the mailbox `type`,
|
||||
│ │ paired with each protocol command; command_from_realtime
|
||||
│ ├── realtime_commands.py duplex binding of the codec: decoded event -> DuplexCommand,
|
||||
│ │ DUPLEX_REALTIME_CAPABILITIES, duplex_response_format
|
||||
│ ├── events.py re-export shim (DuplexEvent = RealtimeEvent) + the runner's
|
||||
│ │ epoch-filter sets DOMAIN_TERMINAL_EVENTS / MODEL_OUTPUT_EVENTS
|
||||
│ ├── realtime_events.py RealtimeProjectionState: internal event -> typed events
|
||||
│ ├── messages.py queue envelopes (Open/Close/Resume/Touch/Command/Result/Event), DuplexSessionError
|
||||
│ ├── config.py DuplexSessionConfig, DuplexCapabilities, ResponseCreateOptions
|
||||
│ ├── contracts.py DuplexFence (session_id, epoch, turn_id), stage request records, DuplexStagePort
|
||||
│ ├── plugin.py DuplexModelPlugin, DuplexModelSessionState, DuplexDataPlane, PcmAppendBuffer ABCs
|
||||
│ ├── turn_detection.py server-side VAD turn detector used by the session
|
||||
│ ├── audio.py / vad.py / intermediate.py
|
||||
│ ├── audio.py compatibility re-export, via protocol/duplex
|
||||
│ ├── vad.py / intermediate.py
|
||||
│ └── session/ one engine-resident session and everything that runs it
|
||||
│ ├── engine_session.py DuplexEngineSession: ledgers, lease, fence, stage resources, append sequencing
|
||||
│ ├── runner.py DuplexSessionRunner (per-session mailbox on the orchestrator loop)
|
||||
|
||||
@@ -30,6 +30,7 @@ primary_code_paths:
|
||||
- vllm_omni/entrypoints/utils.py
|
||||
- vllm_omni/entrypoints/duplex/**
|
||||
- vllm_omni/clients/**
|
||||
- vllm_omni/protocol/**
|
||||
primary_path_exceptions:
|
||||
- path: vllm_omni/entrypoints/openai/errors.py
|
||||
owner: error_contracts.md
|
||||
@@ -53,6 +54,7 @@ depends_on:
|
||||
- error_contracts.md
|
||||
- engine_orchestration.md
|
||||
validation_paths:
|
||||
- tests/protocol/**
|
||||
- tests/entrypoints/test_omni_entrypoints.py
|
||||
- tests/entrypoints/test_async_omni.py
|
||||
- tests/entrypoints/test_async_omni_pause_sleep_routing.py
|
||||
@@ -94,11 +96,43 @@ 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.
|
||||
|
||||
When `--api-server-count` is greater than one, the serve composition root
|
||||
starts the frontend process manager while a parent-owned `StageRuntime`
|
||||
launches the shared local stage engines. Each frontend receives only its own
|
||||
stage-client channel configuration and does not launch or retire backend
|
||||
processes. CLI validation rejects unsupported distributed, diffusion,
|
||||
fault-tolerant, elastic-EP, Ray, and runtime-LoRA combinations before startup.
|
||||
|
||||
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`.
|
||||
|
||||
It also owns `vllm_omni/protocol/**`, the shared wire codec promoted by RFC
|
||||
[#6592](https://github.com/vllm-project/vllm-omni/issues/6592) P0a. It follows the three tiers
|
||||
`docs/serving/realtime_duplex_api.md` already defines. `protocol/realtime/**`
|
||||
is Tier 1, identical to OpenAI: 30 server events, 10 client commands, their
|
||||
typed fields and pure wire rendering, audio-format negotiation,
|
||||
conversation-item rules and the error-envelope shape. `protocol/duplex/**` is
|
||||
Tier 2 (OpenAI names carrying vLLM-Omni extensions, each a subclass of its
|
||||
Tier 1 twin declaring only what it adds) and Tier 3 (ours alone, including our
|
||||
error-code vocabulary), kept separate so a GA-only consumer is not handed a
|
||||
vocabulary its clients never send. That package sits
|
||||
outside `entrypoints/` on purpose --- the engine depends on it, so it belongs
|
||||
to neither layer --- but deciding what a Realtime client may put on the wire is
|
||||
this document's subject, and the same reasoning already places
|
||||
`vllm_omni/errors.py`, `inputs/` and `outputs/` under contract documents rather
|
||||
than under a consumer.
|
||||
|
||||
It is deliberately **not** owned by `../fullduplex.md`, even though duplex is
|
||||
the codec's only consumer today. RFC #6592 asks for a named owner precisely so
|
||||
a second Realtime consumer does not need duplex review to change the codec
|
||||
(#6592 open question 8). Ownership here is review scope, not a dependency: the
|
||||
codec may not import the engine, the entrypoints or the model code, which
|
||||
`tests/protocol/realtime/test_protocol_import_boundary.py` enforces. When a
|
||||
second consumer lands (#6592 P0b) the package is expected to graduate to its
|
||||
own module document with owners drawn from both consumers.
|
||||
|
||||
## Candidate invariants
|
||||
|
||||
These identifiers are proposals while the document is `draft`.
|
||||
@@ -108,6 +142,22 @@ These identifiers are proposals while the document is `draft`.
|
||||
**Rule:** Entrypoints MUST NOT implement cross-stage routing or stage lifecycle
|
||||
policy.
|
||||
|
||||
### ENTRY-INV-002: The shared Realtime codec does not depend on its consumers
|
||||
|
||||
**Rule:** `vllm_omni/protocol/**` MUST NOT import `vllm_omni.engine`,
|
||||
`vllm_omni.entrypoints`, `vllm_omni.model_executor`, `vllm_omni.worker` or
|
||||
`vllm_omni.clients`; `protocol/duplex/**` MAY depend on `protocol/realtime/**`
|
||||
but never the reverse, and a Tier 1 class MUST NOT carry a vLLM-Omni extension
|
||||
field; and each protocol/codec behavior MUST have exactly one
|
||||
implementation that every consumer uses. A wire type MUST NOT carry a
|
||||
consumer's internal representation: the duplex mailbox rendering
|
||||
(`DuplexCommand.payload()`, whose channel differs from the client event for
|
||||
`session.update` and the `conversation.item.*` commands) stays engine-side.
|
||||
A duplex consumer (`engine/duplex/**`, `entrypoints/duplex/**`, the duplex
|
||||
clients) MUST import `vllm_omni.protocol.duplex` and MUST NOT import
|
||||
`vllm_omni.protocol.realtime` directly, so the tier boundary has exactly one
|
||||
extension point.
|
||||
|
||||
### ENTRY-INV-100: Public requests are normalized once
|
||||
|
||||
**Rule:** Public protocol values MUST be validated and converted to internal
|
||||
@@ -141,6 +191,9 @@ stage so `wake_up(stage_ids=[0])` does not skip a later `wake_up(stage_ids=[1])`
|
||||
Streaming input pumps take an admission slot immediately before each EngineCore
|
||||
ADD or update, not while waiting for the next client chunk. Frontend abort
|
||||
keeps `request_states` until `generate()` consumes the terminal output.
|
||||
Multi-API serving must preserve one client rank and one channel set per
|
||||
frontend, include every frontend in readiness/failure observation, and keep
|
||||
stage lifecycle ownership in the parent composition root.
|
||||
|
||||
## Promotion gate
|
||||
|
||||
|
||||
@@ -77,6 +77,22 @@ readiness, one pool per logical stage, replica identity and membership,
|
||||
selection and affinity, stage-client/process lifecycle, liveness, draining,
|
||||
and shutdown.
|
||||
|
||||
Local EngineCore initialization and multi-API serving use
|
||||
`StageRuntime.launch_stage_engines(num_api_servers=1)` as their common launch
|
||||
entry. It resolves plans when not supplied by the runtime's device-group
|
||||
scheduler, coordinates device locks and environment overlays, starts engines,
|
||||
and rolls back resources on startup or attachment failure. Single-API clients
|
||||
attach locally after readiness and take ownership of the engine resources;
|
||||
there is no additional API subprocess. Diffusion and remote attachment retain
|
||||
their backend-specific initialization.
|
||||
|
||||
For local multi-API serving, the parent runtime owns and launches each stage
|
||||
engine once. It supplies a distinct input/output channel for every API
|
||||
frontend and stage replica; frontend workers attach clients to those engines
|
||||
without acquiring stage-process ownership. Readiness observation and shutdown
|
||||
remain parent-owned. This topology currently excludes diffusion and
|
||||
remote/headless stages, intra-stage data parallelism, and Ray backends.
|
||||
|
||||
It does not own cross-stage request policy, model scheduler policy, connector
|
||||
transfer semantics, configuration precedence, or public error rendering.
|
||||
|
||||
@@ -114,6 +130,10 @@ affinity, membership changes, abort routing, process failure, draining, and
|
||||
repeated shutdown. Cross-stage policy changes belong in
|
||||
`engine_orchestration.md`.
|
||||
|
||||
For multi-API changes, additionally exercise per-frontend channel assignment,
|
||||
shared-engine startup handshakes, frontend failure observation, and parent
|
||||
cleanup without duplicate stage shutdown.
|
||||
|
||||
## Promotion gate
|
||||
|
||||
- Reconcile terminology and ownership after #5441 reaches a final state.
|
||||
|
||||
@@ -51,6 +51,7 @@ This extension offers the following nodes based on the output modalities:
|
||||
- **Generate Video** for text-to-video and image-to-video tasks
|
||||
- **Multimodality Understanding** for multimodality-to-text and multimodality-to-audio tasks
|
||||
- **TTS** and **TTS Voice Clone** for TTS tasks
|
||||
- **Generate Music** for text-to-music tasks
|
||||
|
||||
This extension also offers example workflows (at **ComfyUI sidebar -> Templates -> vLLM-Omni**)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# Custom Pipeline Extension Guide
|
||||
|
||||
Transformer already support Custom Pipeline via
|
||||
https://github.com/huggingface/diffusers/blob/main/docs/source/en/using-diffusers/custom_pipeline_overview.md
|
||||
<https://github.com/huggingface/diffusers/blob/main/docs/source/en/using-diffusers/custom_pipeline_overview.md>
|
||||
|
||||
This guide demonstrates how to use the newly added features for extending vLLM-Omni's diffusion pipeline with custom functionality.
|
||||
|
||||
@@ -20,8 +20,9 @@ Three main features enable custom pipeline extension:
|
||||
`WorkerWrapperBase` is a wrapper class that creates `DiffusionWorker` instances with optional extension support. It enables dynamic inheritance, allowing you to add custom methods and functionality to workers without modifying the base worker class.
|
||||
|
||||
**Key capabilities:**
|
||||
|
||||
- Dynamic worker class extension via `worker_extension_cls`
|
||||
- Support for custom pipeline initialization via `custom_pipeline_args`
|
||||
- Support for custom pipeline initialization via `custom_pipeline_args["pipeline_class"]`
|
||||
- Method delegation to underlying worker
|
||||
- Attribute access forwarding
|
||||
|
||||
@@ -42,6 +43,7 @@ The `diffusion_load_format` parameter controls the initial diffusion model load.
|
||||
`CustomPipelineWorkerExtension` is a mixin class that extends `DiffusionWorker` with the ability to re-initialize the pipeline with a custom implementation.
|
||||
|
||||
**Key method:**
|
||||
|
||||
- `re_init_pipeline(custom_pipeline_args)`: Re-initializes the pipeline with custom arguments, properly cleaning up the old pipeline first
|
||||
|
||||
**Location:** `vllm_omni/diffusion/worker/diffusion_worker.py`
|
||||
@@ -150,6 +152,7 @@ omni = Omni(
|
||||
},
|
||||
worker_extension_cls=MyCustomExtension,
|
||||
# Note: worker_extension_cls is an internal parameter
|
||||
# CustomPipelineWorkerExtension will automatically init pipeline when custom_pipeline_args is provided
|
||||
# CustomPipelineWorkerExtension will automatically init pipeline when
|
||||
# custom_pipeline_args includes "pipeline_class".
|
||||
)
|
||||
```
|
||||
@@ -58,6 +58,7 @@ th {
|
||||
| `Magi2Pipeline` | MAGI-2 native Preview-stage T2VA and I2VA (272p/540p) | `sand-ai/MAGI-2-preview` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/SandAI/MAGI-2-preview-L20X.md) |
|
||||
| `DreamZeroPipeline` | DreamZero-DROID | `GEAR-Dreams/DreamZero-DROID` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `Pi0Pipeline` | π0 (Pi-Zero) VLA | `lerobot/pi0_base` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/lerobot/Pi0.md) |
|
||||
| `Pi05Pipeline` | π0.5 VLA | `lerobot/pi05_base` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/lerobot/Pi05.md) |
|
||||
| `HeliosPipeline`, `HeliosPyramidPipeline` | Helios | `BestWishYsh/Helios-Base`, `BestWishYsh/Helios-Mid`, `BestWishYsh/Helios-Distilled` | ✅︎ | ✅︎ | ✅︎ | | — |
|
||||
| `OvisImagePipeline` | Ovis-Image | `OvisAI/Ovis-Image` | ✅︎ | ✅︎ | | ✅︎ | — |
|
||||
| `LongcatImagePipeline` | LongCat-Image | `meituan-longcat/LongCat-Image` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -72,7 +73,6 @@ th {
|
||||
| `OmniVoiceModel` | OmniVoice | `k2-fsa/OmniVoice` | ✅︎ | | | ✅︎ | — |
|
||||
| `VoxCPM2TalkerForConditionalGeneration` | VoxCPM2 | `openbmb/VoxCPM2` | ✅︎ | | | ✅︎ | — |
|
||||
| `BreezeForConditionalGeneration` | Breeze-TTS-2 (TTS, voice clone, voice direction) | `BreezeBlue/Breeze-TTS-2` | ✅︎ | | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/BreezeBlue/Breeze-TTS-2.md) |
|
||||
| `DotsTTSForConditionalGeneration` | dots.tts | `dots-studio/dots.tts-soar` | ✅︎ | | | | — |
|
||||
| `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` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
@@ -106,12 +106,12 @@ th {
|
||||
| `HunyuanVideo15ImageToVideoPipeline` | HunyuanVideo-1.5-I2V | `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_i2v`, `hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-720p_i2v` | ✅︎ | ✅︎ | | | — |
|
||||
| `VoxtralTTSForConditionalGeneration` | Voxtral TTS | `mistralai/Voxtral-4B-TTS-2603` | ✅︎ | ✅︎ | | | — |
|
||||
| `CovoAudioForConditionalGeneration` | Covo-Audio-Chat | `tencent/Covo-Audio-Chat` | ✅︎ | | | | — |
|
||||
| `DyninOmniForConditionalGeneration` | Dynin-Omni | `snu-aidas/Dynin-Omni` | ✅︎ | | | ✅︎ | — |
|
||||
| `MiniCPMO45OmniForConditionalGeneration` | MiniCPM-o 4.5 | `openbmb/MiniCPM-o-4_5` | ✅︎ | | ✅︎ | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/OpenBMB/MiniCPM-o-4_5.md) |
|
||||
| `ErnieImagePipeline` | ERNIE-Image | `baidu/ERNIE-Image`, `baidu/ERNIE-Image-Turbo` | ✅︎ | ✅︎ | ✅︎ | ✅︎ | — |
|
||||
| `GepardTalkerForConditionalGeneration` | Gepard-1.0 | `nineninesix/gepard-1.0` | ✅︎ | | | ✅︎ | — |
|
||||
| `HiDreamImagePipeline` | HiDream-I1-Full | `HiDream-ai/HiDream-I1-Full` | ✅︎ | ✅︎ | | | — |
|
||||
| `HiDreamO1ImagePipeline` | HiDream-O1-Image | `HiDream-ai/HiDream-O1-Image` | ✅︎ | | | | — |
|
||||
| `AnimaPipeline` | Anima | `circlestone-labs/Anima` | ✅︎ | ✅︎ | | | [Repository](https://github.com/vllm-project/vllm-omni/blob/main/recipes/circlestone-labs/Anima.md) |
|
||||
|
||||
The `Recipe` column links the corresponding deployment recipe. `Published` is
|
||||
preferred when a corresponding `recipes.vllm.ai` page exists; otherwise the
|
||||
|
||||
@@ -22,14 +22,14 @@ capability gates, see the [Realtime Duplex API guide](realtime_duplex_api.md).
|
||||
## Enable Full Duplex
|
||||
|
||||
A model is served full duplex when its registered pipeline declares a
|
||||
`duplex_plugin` (the model's `DuplexModelPlugin`). That declaration alone
|
||||
decides it: `vllm-omni serve` constructs `DuplexOmni` instead of `AsyncOmni`,
|
||||
and every surface the server exposes is backed by a duplex session. It serves
|
||||
`duplex_plugin` (the model's `DuplexModelPlugin`) and its deploy configuration
|
||||
sets `session_mode: duplex`. `vllm serve --omni` constructs `DuplexOmni`
|
||||
instead of `AsyncOmni`. It serves
|
||||
`/v1/realtime?duplex=1` (and its alias `/v1/duplex`),
|
||||
`POST /v1/chat/completions`, `/v1/models` and `/health`; every other
|
||||
turn-based HTTP route (speech, batch, embeddings, video, ...) answers "not
|
||||
available". Turn-based use of such a model stays available offline through the
|
||||
Python API (`Omni` / `AsyncOmni`).
|
||||
available". Set `session_mode: turn` to select the ordinary online serving
|
||||
stack instead. The model's supported tasks and endpoint restrictions still apply.
|
||||
|
||||
`/v1/chat/completions` is the ordinary chat service running on the duplex
|
||||
engine: a request is a turn-based generation on the same stages, served
|
||||
@@ -45,8 +45,24 @@ The deploy configuration of such a model must agree:
|
||||
session_mode: duplex
|
||||
```
|
||||
|
||||
A duplex model started with a deploy configuration that does not set it fails
|
||||
at startup rather than falling back to turn-based serving.
|
||||
A duplex-capable model must explicitly set `session_mode` to `duplex` or `turn`
|
||||
in its deploy configuration, possibly through `base_config` inheritance.
|
||||
Missing or invalid values fail at startup. The default MiniCPM-o deployment
|
||||
continues to use duplex mode.
|
||||
|
||||
To run MiniCPM-o 4.5 with the turn-based engine:
|
||||
|
||||
```bash
|
||||
vllm serve openbmb/MiniCPM-o-4_5 --omni \
|
||||
--deploy-config vllm_omni/deploy/minicpmo_4_5_turn.yaml \
|
||||
--trust-remote-code \
|
||||
--port 8091
|
||||
```
|
||||
|
||||
This profile inherits the default model and stage settings and overrides only
|
||||
`session_mode`. It supports ordinary HTTP requests without creating a duplex
|
||||
session handler. Mode selection happens at startup; a WebSocket query parameter
|
||||
does not switch engines. The Python `Omni` / `AsyncOmni` APIs are unchanged.
|
||||
|
||||
!!! warning
|
||||
|
||||
|
||||
@@ -8,12 +8,41 @@ history stays honest, and a dropped connection can resume the same session.
|
||||
This page covers how to run a duplex deployment, how to drive it from Python
|
||||
with `vllm_omni.clients.duplex.DuplexClient`, and the complete wire contract.
|
||||
|
||||
The endpoint is served for models that ship a duplex plugin — currently
|
||||
MiniCPM-o 4.5 only — and just for deploy configurations that declare
|
||||
The endpoint is served for models that ship a duplex plugin — including
|
||||
MiniCPM-o 4.5 and Qwen3-Omni — and just for deploy configurations that declare
|
||||
`session_mode: duplex`. PersonaPlex and Nemotron VoiceChat are ported to the
|
||||
plugin contract in follow-up PRs and are not served over this endpoint yet. The runtime architecture
|
||||
is described in [Full-Duplex Runtime (MiniCPM-o 4.5)](../design/fullduplex.md).
|
||||
|
||||
## Qwen3-Omni conversation history
|
||||
|
||||
Qwen3-Omni keeps incoming conversation items independently addressable by item
|
||||
ID. Before applying the model's chat template, it merges consecutive user
|
||||
items into one multimodal user message. An assistant message, including an
|
||||
empty message reserved for an interrupted or unheard answer, separates turns.
|
||||
The final user message places media before text instructions, following the
|
||||
[official Qwen3-Omni demo's `format_history()`](https://github.com/QwenLM/Qwen3-Omni/blob/e4235853125589c789f06a2dd83e9f4126df5e9d/web_demo.py#L84-L179).
|
||||
|
||||
A camera image or text item may arrive after an audio commit and before
|
||||
`response.create`. Those items belong to the same pending user turn: the
|
||||
committed audio remains associated with its history item even when that item
|
||||
is no longer last. Prompt preparation snapshots the selected inputs; later
|
||||
items do not modify an already prepared request.
|
||||
|
||||
The duplex adapter retains its existing budgets: at most four audio payloads
|
||||
(including the current input), an 8 MiB encoded-audio history budget (keeping
|
||||
at least the newest payload), and at most eight prompt images. The recent
|
||||
16-message window is rounded outwards to a complete turn. If any audio in an
|
||||
older turn is no longer retained, the entire turn's model context, including
|
||||
its images/text and assistant replies, is omitted. Images from that turn are
|
||||
therefore **not** permanent session-wide model context. These limits differ
|
||||
from the demo's one image-bearing turn and five audio-bearing turns; the
|
||||
shared rule is grouping before pruning, rather than copying its UI budgets.
|
||||
|
||||
Grouping and pruning affect only the model-input view. They do not merge or
|
||||
delete the source conversation items exposed to clients, and do not change
|
||||
playback acknowledgement or interruption handling.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Start the Server
|
||||
@@ -32,33 +61,19 @@ vllm-omni serve openbmb/MiniCPM-o-4_5 \
|
||||
mounts `ws://<host>:8099/v1/realtime?duplex=1` (this page; `ws://<host>:8099/v1/duplex`
|
||||
is an alias of the same route), `POST /v1/chat/completions`, `/v1/models` and
|
||||
`/health`, and nothing else. The remaining turn-based HTTP routes (speech,
|
||||
batch, embeddings, video, ...) are not served by a duplex model.
|
||||
batch, embeddings, video, ...) are not served in duplex mode.
|
||||
|
||||
`/v1/chat/completions` is not a turn-based path on a duplex server: each
|
||||
request opens a short-lived duplex session and reads the answer off it. Audio
|
||||
content becomes an ordinary committed turn; text is seeded as the session's
|
||||
opening turn, because a model-native model takes a turn only when it hears
|
||||
speech. A model that cannot be seeded that way declares so, and a text request
|
||||
to it is refused with 400. Two consequences are worth sizing for:
|
||||
`/v1/chat/completions` uses the ordinary chat service on the same engine
|
||||
when the plugin declares `supports_chat_completions`. HTTP chat requests do
|
||||
not create duplex sessions or consume session admission slots. Their options
|
||||
follow the [Chat Completions API](chat_completions_api.md) and the model's
|
||||
capabilities and endpoint restrictions.
|
||||
|
||||
- **Every request holds an admission slot** for its lifetime, so
|
||||
`duplex_session.max_sessions` caps HTTP concurrency as well as websocket
|
||||
sessions. Beyond it a request is refused with HTTP 503.
|
||||
- **Latency follows the model's own clock.** A model-native session generates
|
||||
per audio unit, so even a short answer costs what that many units cost --
|
||||
noticeably more than the same model would take turn-based.
|
||||
|
||||
`n > 1`, `logprobs`, `tools`, and image or video content parts have no
|
||||
representation in one duplex turn and are refused with HTTP 400 -- a Realtime
|
||||
conversation item carries text and audio only. `temperature`,
|
||||
`max_completion_tokens`, `modalities` and
|
||||
`chat_template_kwargs.use_tts_template` apply; the other
|
||||
`chat_template_kwargs` keys are logged as ignored. `extra_body` is passed to
|
||||
the session unchanged, exactly as a websocket client's session `extra_body`
|
||||
is, which is how a request asking for `modalities: ["text", "audio"]` supplies
|
||||
the `ref_audio` MiniCPM-o requires for audio output. A model that should not
|
||||
serve the route lists it in the deploy config's `endpoint_restrictions`,
|
||||
exactly as a turn-based model does.
|
||||
For a turn-based online deployment, use `minicpmo_4_5_turn.yaml` or explicitly
|
||||
set `session_mode: turn` in a deploy configuration derived from the model's
|
||||
profile. This selects `AsyncOmni` and the ordinary API initialization instead
|
||||
of the duplex handler. See [Full Duplex](full_duplex_api.md#enable-full-duplex)
|
||||
for the command and configuration requirements.
|
||||
|
||||
### Run the Example Client
|
||||
|
||||
@@ -782,7 +797,10 @@ events with `server_event_seq > 40` are replayed in order)
|
||||
```
|
||||
|
||||
with camera frames (omni video; rides the append that closes a 1 s model
|
||||
unit; each entry is a **bare** base64 JPEG/PNG — no `data:` URL prefix)
|
||||
unit; each entry is a **bare** base64 JPEG/PNG — no `data:` URL prefix).
|
||||
Only a model whose Stage 0 interleaves a frame track at unit boundaries takes
|
||||
this field; a turn model has no such boundary and rejects it, expecting
|
||||
`conversation.item.create` with `input_image` content instead.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -959,6 +977,10 @@ deferred commit during an active response (`event.response_create_deferred`)
|
||||
{"type": "conversation.item.truncated", "item_id": "item_resp_01", "content_index": 0, "audio_end_ms": 1850, "event": {"…": "…"}}
|
||||
```
|
||||
|
||||
After this acknowledgement, `conversation.item.retrieve` returns the truncated
|
||||
transcript. Without text/audio alignment marks, its prefix is estimated from
|
||||
the requested position and audio duration; this is not exact word alignment.
|
||||
|
||||
#### Response lifecycle
|
||||
|
||||
`C→S response.create`
|
||||
@@ -1345,8 +1367,9 @@ them out into typed events before they reach a client.
|
||||
- Session capacity is bounded by `duplex_session.max_sessions` in the deploy
|
||||
configuration; admission beyond it fails with `resource_exhausted`
|
||||
(`rate_limit_error`, retryable).
|
||||
- A duplex deployment serves the duplex session surfaces only: the websocket
|
||||
route and `/v1/chat/completions`, the latter on a session per request (see
|
||||
[Start the Server](#start-the-server)). The other turn-based HTTP routes
|
||||
report "not available" on a MiniCPM-o 4.5 server; turn-based use of the
|
||||
model stays available offline through `Omni` / `AsyncOmni`.
|
||||
- A duplex deployment serves the websocket route and, when supported by the
|
||||
model, ordinary `/v1/chat/completions` requests on the same engine without a
|
||||
session per request (see [Start the Server](#start-the-server)). Other
|
||||
turn-based HTTP routes report "not available" in duplex mode. To use the
|
||||
ordinary online serving stack, select `session_mode: turn` as described in
|
||||
[Full Duplex](full_duplex_api.md#enable-full-duplex).
|
||||
@@ -521,6 +521,15 @@ are cached in-process with a shared LRU so repeated requests with the same
|
||||
all TTS model types; deleting a voice invalidates every model-type slot at
|
||||
once.
|
||||
|
||||
Decoded reference waveforms use a separate LRU cache of owned, contiguous
|
||||
float32 arrays at the source sampling rate. Its byte budget counts numeric
|
||||
buffers (four bytes per mono sample), excluding cache metadata. Numeric storage
|
||||
avoids retaining a Python float object for every sample during garbage
|
||||
collection. MOSS reference encoding consumes these arrays directly; other
|
||||
list-based interfaces, including MOSS Nano, receive temporary lists that are
|
||||
not retained in the resolve cache. The MOSS reference encoder also releases
|
||||
completed batch inputs before waiting for more work.
|
||||
|
||||
### Precomputed Custom Voices
|
||||
|
||||
Qwen3-TTS Base and VoxCPM2 can load offline-precomputed voices at startup.
|
||||
|
||||
@@ -41,7 +41,7 @@ bytes.
|
||||
| Server to client | `video.start` | Confirms the request ID, format, and accepted configuration |
|
||||
| Server to client | `video.chunk_metadata` | Describes the next binary media frame |
|
||||
| Server to client | Binary frame | Carries one fragmented MP4 chunk |
|
||||
| Client to server | `session.interaction` | Queues an optional prompt update during generation |
|
||||
| Client to server | `session.interaction` | Queues an optional interaction (e.g., prompt. camera action) during generation |
|
||||
| Server to client | `session.interaction.queued` | Confirms that an interaction was queued |
|
||||
| Client to server | `session.ping` | Refreshes the stall clock |
|
||||
| Server to client | `session.pong` | Acknowledges a ping |
|
||||
@@ -83,10 +83,39 @@ Models that implement interaction updates can change the active prompt:
|
||||
}
|
||||
```
|
||||
|
||||
Models that register the camera modality (for example LingBot-World) also
|
||||
accept structural SE3 payloads on `event.multi_modal_data.camera`. Coordinates
|
||||
follow Unity semantics (`+X` right, `+Y` up, `+Z` forward; same as LingBot's
|
||||
native camera frame). WASD key tokens are not admitted by the engine:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "session.interaction",
|
||||
"interaction": {
|
||||
"event_id": "forward-1",
|
||||
"event": {
|
||||
"multi_modal_data": {
|
||||
"camera": {
|
||||
"mode": "velocity",
|
||||
"data": {
|
||||
"translation": [0.0, 0.0, 0.05],
|
||||
"rotation": [0.0, 0.0, 0.0, 1.0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`mode` is `target` (lerp to an absolute pose over `transition_chunks`) or
|
||||
`velocity` (hold a per-frame SE3 delta until replaced). Prompt and camera may
|
||||
be combined in one interaction event.
|
||||
|
||||
This feature is pipeline-dependent. An enabled endpoint does not imply that
|
||||
the loaded model supports midway prompt updates.
|
||||
the loaded model supports midway prompt or camera updates.
|
||||
|
||||
The default start timeout is 10 seconds and the server reports a stall after
|
||||
about 60 seconds without generation progress or a `session.ping`. See the
|
||||
[streaming video generation example](https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/streaming_video_generation)
|
||||
for prompt-update scheduling and browser playback.
|
||||
for prompt/camera-update scheduling and browser playback.
|
||||
@@ -73,6 +73,10 @@ curl -L "http://localhost:8091/v1/videos/${video_id}/content" -o output.mp4
|
||||
| `image_reference` | string | null | JSON-encoded reference image payload; do not combine with `input_reference` or `video_reference` |
|
||||
| `video_reference` | string | null | JSON-encoded reference video payload; do not combine with `input_reference` or `image_reference` |
|
||||
| `audio_reference` | string | null | JSON-encoded audio reference for speech-to-video: `{"audio_url": "..."}` — supports HTTP(s) URLs or base64 data URLs |
|
||||
| `source_video` | file | null | MiniMax H3 latent-edit source video (`.mp4` or `.mov`, up to 512 MiB) |
|
||||
| `source_audio` | file | null | Optional MiniMax H3 latent-edit source audio (`.wav` or `.mp3`, up to 512 MiB) |
|
||||
| `video_noise_mask` | file | null | UTF-8 JSON MiniMax H3 video mask; `0` preserves and `1` regenerates a token |
|
||||
| `audio_noise_mask` | file | null | UTF-8 JSON MiniMax H3 audio mask; `0` preserves and `1` regenerates a token |
|
||||
| `width` | integer | model default | Output video width |
|
||||
| `height` | integer | model default | Output video height |
|
||||
| `num_frames` | integer | 1 | Number of generated frames |
|
||||
@@ -214,6 +218,49 @@ upstream vLLM's media URL allowlist protection. Deployments should therefore
|
||||
treat remote media URLs as untrusted and choose this setting as part of their
|
||||
URL access policy.
|
||||
|
||||
### MiniMax H3 Latent-Mask Editing
|
||||
|
||||
MiniMax H3 accepts request-scoped source media and video/audio noise masks.
|
||||
At least one mask is required. A nontrivial `video_noise_mask` requires
|
||||
`source_video`, while a nontrivial `audio_noise_mask` requires either
|
||||
`source_audio` or a `source_video` with an audio stream. Mask values are in
|
||||
`[0, 1]`: `0` preserves the source, `1` regenerates it, and fractional values
|
||||
blend the two behaviors. Exact all-one masks are no-ops and do not require a
|
||||
source. Source uploads without a mask are rejected. Masks may be a JSON scalar
|
||||
or arrays matching the H3 latent/token grid; pixel-resolution masks must be
|
||||
resized or pooled by the client before upload.
|
||||
|
||||
For an aligned output of `F` frames at `W x H`, the video latent grid is
|
||||
`[Tv, H/16, W/16]`, where `Tv = 2 + 5 * ((F - 5) / 17)`. The video mask may be
|
||||
a scalar, a flat token vector, `[Tv, H/32, W/32]`, or the full latent grid. For
|
||||
the model input, timestep, and velocity, a full-grid mask is max-pooled over
|
||||
each 2x2 spatial token and fractional values are rounded upward to 1/256
|
||||
levels. The final x0 restore uses the original, unquantized mask, so full-grid
|
||||
masks retain cell-level preservation inside a model token. The audio length is
|
||||
`Ta = round(F * 40 / 24)`, and its mask may be a scalar, `[Ta]`, `[2, Ta]`, or
|
||||
a flat `2 * Ta` vector. If source audio is short, its missing latent tail is
|
||||
forced to mask value `1` so H3 generates that portion. Each mask must be sent
|
||||
as a UTF-8 JSON file part and is limited to 8 MiB. A file may contain either a
|
||||
JSON scalar or an array.
|
||||
|
||||
```bash
|
||||
curl -s http://localhost:8091/v1/videos/sync \
|
||||
-F "prompt=Partially restyle the complete clip and soundtrack" \
|
||||
-F 'extra_params={"task":"t2va","duration":4.0,"aspect_ratio":"16:9"}' \
|
||||
-F "source_video=@source.mp4;type=video/mp4" \
|
||||
-F "source_audio=@source.wav;type=audio/wav" \
|
||||
-F "video_noise_mask=@video-mask.json;type=application/json" \
|
||||
-F "audio_noise_mask=@audio-mask.json;type=application/json" \
|
||||
-o edited.mp4
|
||||
```
|
||||
|
||||
For example, each mask file in the request above may contain the JSON scalar
|
||||
`0.5`.
|
||||
|
||||
The server streams source files to temporary request-scoped storage and removes
|
||||
them after synchronous or asynchronous generation finishes. Models that do not
|
||||
declare latent-mask editing support reject these fields.
|
||||
|
||||
### Speech-to-Video
|
||||
|
||||
For models that support audio-driven generation (e.g., Wan2.2-S2V), pass both
|
||||
@@ -251,6 +298,7 @@ These `extra_params` control how the server turns decoded frames into MP4 bytes.
|
||||
| Field | Type | Default | Description |
|
||||
| --- | --- | --- | --- |
|
||||
| `preencode_mp4` | boolean | false | Encode the MP4 on the worker while the VAE is still decoding, instead of after the full video is materialized |
|
||||
| `preencode_batch_frames` | positive integer | 17 (H3, Wan T2V/I2V); 1 (Wan S2V) | Minimum accumulated frames per worker transfer/encoding batch; used only with `preencode_mp4=true` |
|
||||
| `video_codec_options` | object | null | Encoder options passed through to the H.264 encoder, such as `{"preset": "ultrafast", "threads": "0"}` |
|
||||
|
||||
With `preencode_mp4` enabled, each committed VAE chunk leaves the accelerator and
|
||||
@@ -261,16 +309,29 @@ unchanged: the same complete MP4, byte-for-byte equivalent frames.
|
||||
```bash
|
||||
curl -X POST http://localhost:8091/v1/videos/sync \
|
||||
-F "prompt=A small robot walking through a neon city" \
|
||||
-F 'extra_params={"preencode_mp4": true, "video_codec_options": {"preset": "ultrafast"}}' \
|
||||
-F 'extra_params={"preencode_mp4": true, "preencode_batch_frames": 33, "video_codec_options": {"preset": "ultrafast"}}' \
|
||||
-o output.mp4
|
||||
```
|
||||
|
||||
Set `preencode_batch_frames` in `extra_params` (or `extra_args` for offline
|
||||
sampling) to tune batching. The worker accumulates complete VAE chunks until
|
||||
it has at least this many frames, then transfers and encodes them together.
|
||||
It always flushes the final partial batch. This is a threshold, not an exact
|
||||
chunk length: a value of 1 submits every native chunk immediately, and a value
|
||||
smaller than a native chunk does not split it. Larger values reduce transfers
|
||||
but retain more frames on the accelerator and delay encoding. The VAE decode
|
||||
window and output frame count stay unchanged. Wan S2V keeps its existing
|
||||
per-clip behavior by default. Zero, negative, fractional, boolean, string, and
|
||||
null values are rejected when pre-encoding is enabled.
|
||||
|
||||
`preencode_mp4` applies to the complete-MP4 response paths only. The
|
||||
`/v1/realtime/video` WebSocket endpoint rejects it, because that path already
|
||||
overlaps encoding through its own incremental fragmented-MP4 encoder.
|
||||
overlaps encoding through its own incremental fragmented-MP4 encoder. Wan also
|
||||
rejects it together with `enable_frame_interpolation`, which needs the decoded
|
||||
frames the pre-encoded path no longer materializes.
|
||||
|
||||
Support is per model: MiniMax-H3 implements it, and other models ignore the flag
|
||||
and take the full-decode path.
|
||||
Support is per model: MiniMax-H3 and Wan 2.2 (T2V, I2V, and S2V) implement it,
|
||||
and other models ignore the flag and take the full-decode path.
|
||||
|
||||
## Storage
|
||||
|
||||
|
||||
@@ -78,3 +78,35 @@ Protected video tails require a compatible MindIE-SD release.
|
||||
For common configuration and selector behavior, see the
|
||||
[attention backend overview](../attention_backends.md) and the
|
||||
[backend selection design](../../../design/feature/attention_backend_selection.md).
|
||||
|
||||
## Wan2.2 T2V quantization on Ascend
|
||||
|
||||
`RAINFUSION_ATTN` accepts `diffusion_kv_cache_dtype: fp8` or `mxfp4` for
|
||||
Wan2.2 T2V A14B.
|
||||
MindIE-SD must expose `sparse_attention` with `rf_v3` and an explicit `precision`
|
||||
argument. Omni passes the selected precision directly to this API; the installed
|
||||
native operators must support that precision. Unsupported inputs or an
|
||||
incompatible Python API raise an actionable error; Omni does not change BSA
|
||||
precision automatically. Update `diffusion_kv_cache_dtype` to a precision
|
||||
supported by the installed MindIE-SD, or unset it. Native execution errors also
|
||||
propagate without retry.
|
||||
BSA FP8 and MXFP4 retain MindIE-SD's default rotation policy.
|
||||
|
||||
`diffusion_kv_cache_skip_layers`/`diffusion_kv_cache_skip_steps` explicitly
|
||||
select floating-point BSA and keep attention sparse. The existing `block_sparse`
|
||||
warmup/layer exclusions and short-sequence threshold instead select Dense
|
||||
attention, using the configured Dense precision. Leave `block_sparse.precision`
|
||||
at its default when using `diffusion_kv_cache_dtype` as the shared Dense/BSA
|
||||
precision setting.
|
||||
|
||||
Configure BSA quantization in the model's existing deployment YAML with
|
||||
`backend: RAINFUSION_ATTN` and stage-level `diffusion_kv_cache_dtype: fp8` or
|
||||
`mxfp4`. The precision and skip selectors are siblings of
|
||||
`diffusion_attention_config`; they are not per-role `quant` fields.
|
||||
For 40 denoising steps, `diffusion_kv_cache_skip_steps: "0,1,38,39"` and
|
||||
`diffusion_kv_cache_skip_layers: "0,39"` are recommended starting points. These
|
||||
fixed step indices do not automatically follow a changed step count. Selected
|
||||
forwards remain floating-point BSA; unsupported BSA precision is an error rather
|
||||
than an automatic fallback. See
|
||||
[quantized attention](../../quantization/quantized_kvcache.md#wan22-t2v-quantized-attention-on-ascend)
|
||||
for the shared configuration structure and index semantics.
|
||||
@@ -28,7 +28,7 @@ and their optimal Triton tile configurations:
|
||||
|
||||
Config loading order (3-tier, see ops/mot_gemm.py):
|
||||
1. $VLLM_TUNED_CONFIG_FOLDER/<filename> (env override)
|
||||
2. This directory: vllm_omni/diffusion/layers/mot/configs/<filename>
|
||||
2. This directory: vllm_omni/diffusion/models/bagel/mot/configs/<filename>
|
||||
3. Conservative default config (compiles everywhere, sub-optimal perf)
|
||||
|
||||
If no config file matches the current device, a warning is printed with
|
||||
@@ -39,7 +39,7 @@ To generate configs for your hardware:
|
||||
python benchmarks/kernels/mot_linear_benchmarks.py \
|
||||
--model ByteDance-Seed/BAGEL-7B-MoT \
|
||||
--tp-size 1 --dtype w16a16 --tune \
|
||||
--save-dir vllm_omni/diffusion/layers/mot/configs/
|
||||
--save-dir vllm_omni/diffusion/models/bagel/mot/configs/
|
||||
|
||||
For multi-GPU tuning (uses Ray for parallel search):
|
||||
|
||||
|
||||
@@ -353,9 +353,14 @@ modes can resolve the same `None` value to different schedules.
|
||||
Cache-DiT on any selected component. Prompt-embedding cache is also
|
||||
incompatible when the text encoder uses `allgather`. Rank-local cache hits
|
||||
could otherwise make ranks enter different weight collectives.
|
||||
- Per-tensor online FP8 linears use the ordinary loader and can run with either
|
||||
DiT transfer path. With DiT `allgather`, every rank temporarily materializes
|
||||
the complete FP8 model in host memory before DLO retains only its shard.
|
||||
- Per-tensor online FP8, INT8, and MXFP8 linears use the ordinary loader and
|
||||
can run with either DiT transfer path. With DiT `allgather`, every rank
|
||||
temporarily materializes the complete quantized model in host memory before
|
||||
DLO retains only its shard. Layers too wide for the NPU quantization kernel
|
||||
(`npu_quant_matmul` rejects an output dimension past 65535) stay
|
||||
unquantized and load straight into host memory under offload-after-quant —
|
||||
their runtime layout is a plain contiguous bf16 weight, the same layout DLO
|
||||
shards on the ordinary path, so they are allowed under `allgather` too.
|
||||
Other online quantization methods require rank-local transfer for the
|
||||
affected component until their runtime layouts are validated.
|
||||
- Resident leading layers require DiT `rank-local` transfer and a model
|
||||
|
||||
@@ -247,12 +247,18 @@ curl http://localhost:8091/v1/chat/completions \
|
||||
-d @payload.json
|
||||
```
|
||||
|
||||
BAGEL img2img derives its generated image size from the input image. It
|
||||
preserves the input aspect ratio, aligns dimensions to the latent stride, and
|
||||
applies the checkpoint size limit; explicit `height` and `width` values do not
|
||||
override this model-specific resize policy. In step mode, the effective size is
|
||||
resolved before scheduler admission so requests with different resulting
|
||||
shapes are not combined.
|
||||
When no output size is requested (`size=auto` on `/v1/images/edits`, or no
|
||||
`height`/`width` in a chat request), BAGEL img2img derives the canvas from the
|
||||
input image: it preserves the input aspect ratio, aligns dimensions to the
|
||||
latent stride (16 px), and applies the checkpoint size limit. An explicit
|
||||
`size`, `height`, or `width` is honored instead, in both the single-stage and
|
||||
the two-stage deployment; the input image is still resized to a stride-aligned
|
||||
size for the VAE/ViT prefill. The latent grid needs multiples of 16, so a
|
||||
requested side that is not one is floored to the next multiple (1000x700
|
||||
generates 992x688) and the server logs a warning; the response reports the
|
||||
generated size. A side above the checkpoint limit is rejected with HTTP 400.
|
||||
In step mode, the effective size is resolved before scheduler admission so
|
||||
requests with different resulting shapes are not combined.
|
||||
|
||||
### Image to Text (img2text)
|
||||
|
||||
@@ -318,8 +324,8 @@ curl http://localhost:8091/v1/chat/completions \
|
||||
| `--server` / `-s` | `http://localhost:8091` | Server URL |
|
||||
| `--image-url` / `-i` | `None` | Input image URL or local path (img2img/img2text) |
|
||||
| `--modality` / `-m` | `text2img` | `text2img`, `img2img`, `img2text`, `text2text` |
|
||||
| `--height` | `512` | Text-to-image output height; BAGEL img2img derives its size from the input image |
|
||||
| `--width` | `512` | Text-to-image output width; BAGEL img2img derives its size from the input image |
|
||||
| `--height` | `512` | Output height, a multiple of 16; omit both `--height` and `--width` to let img2img derive the size from the input image |
|
||||
| `--width` | `512` | Output width, a multiple of 16; omit both `--height` and `--width` to let img2img derive the size from the input image |
|
||||
| `--steps` | `25` | Number of inference steps |
|
||||
| `--seed` | `42` | Random seed |
|
||||
| `--negative` | `None` | Negative prompt for CFG |
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
Source <https://github.com/vllm-project/vllm-omni/tree/main/examples/online_serving/qwen3_omni>.
|
||||
|
||||
|
||||
## 🛠️ Installation
|
||||
|
||||
Please refer to [README.md](https://github.com/vllm-project/vllm-omni/tree/main/README.md)
|
||||
@@ -15,6 +14,19 @@ Please refer to [README.md](https://github.com/vllm-project/vllm-omni/tree/main/
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
For a local deployment with multiple API frontend processes sharing one set
|
||||
of stage engines, add `--api-server-count`:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
--api-server-count 2 --port 8091
|
||||
```
|
||||
|
||||
This mode supports local EngineCore stages with one local process group per
|
||||
replica, including stages configured with multiple `num_replicas`. Headless,
|
||||
remote, diffusion, intra-replica data-parallel, and Ray stage deployments
|
||||
remain single-frontend.
|
||||
|
||||
The default deployment configuration situated at `vllm_omni/deploy/qwen3_omni_moe.yaml` is resolved and loaded
|
||||
automatically via the model registry, obviating the necessity for the `--deploy-config` flag in standard deployment topologies.
|
||||
The bundled Qwen3-Omni setup explicitly sets `moe_backend: triton` for the Thinker and Talker stages. This keeps
|
||||
@@ -27,6 +39,7 @@ on the Thinker stage. Keep Talker and Code2Wav at `pipeline_parallel_size: 1`. T
|
||||
default asynchronous scheduling and audio chunk streaming can remain enabled.
|
||||
|
||||
To explicitly utilize a custom deployment YAML, specify the configuration path:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 \
|
||||
--deploy-config /path/to/deploy_config_file
|
||||
@@ -50,7 +63,7 @@ Adopt the stage-based CLI architecture to independently instantiate execution pr
|
||||
The example below pins Stage 0 to GPU 0 and Stage 1/2 to GPU 1 via
|
||||
`CUDA_VISIBLE_DEVICES`.
|
||||
|
||||
**1. Stage 0 (Thinker + API server)**
|
||||
#### 1. Stage 0 (Thinker + API server)
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
@@ -60,7 +73,7 @@ CUDA_VISIBLE_DEVICES=0 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
--omni-master-port 26000
|
||||
```
|
||||
|
||||
**2. Stage 1 (Talker)**
|
||||
#### 2. Stage 1 (Talker)
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
@@ -70,7 +83,7 @@ CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
--omni-master-port 26000
|
||||
```
|
||||
|
||||
**3. Stage 2 (Code2Wav)**
|
||||
#### 3. Stage 2 (Code2Wav)
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
@@ -95,6 +108,9 @@ To experiment with another supported MoE backend, set `moe_backend` in the
|
||||
deployment YAML for both the Thinker and Talker stages. For the stage-based
|
||||
CLI, pass `--moe-backend` directly on each stage command.
|
||||
|
||||
For other stage settings, pass the option on that stage command. For example,
|
||||
to change its GPU memory utilization:
|
||||
|
||||
```bash
|
||||
CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
--stage-id 1 \
|
||||
@@ -107,11 +123,12 @@ CUDA_VISIBLE_DEVICES=1 vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni \
|
||||
### Send Multi-modal Request
|
||||
|
||||
Get into the example folder
|
||||
|
||||
```bash
|
||||
cd examples/online_serving/qwen3_omni
|
||||
```
|
||||
|
||||
#### Send request via python
|
||||
#### Send request via python
|
||||
|
||||
```bash
|
||||
python openai_chat_completion_client_for_multimodal_generation.py --query-type use_image --port 8091 --host "localhost"
|
||||
@@ -126,7 +143,6 @@ The Python client supports the following command-line arguments:
|
||||
- `--audio-path` (or `-a`): Path to local audio file or URL. If not provided and query-type is `use_audio`, uses default audio URL. Supports local file paths (automatically encoded to base64) or HTTP/HTTPS URLs and common audio formats: MP3, WAV, OGG, FLAC, M4A. Example: `--audio-path /path/to/audio.wav` or `--audio-path https://example.com/audio.mp3`
|
||||
- `--prompt` (or `-p`): Custom text prompt/question. If not provided, uses default prompt for the selected query type. Example: `--prompt "What are the main activities shown in this video?"`
|
||||
|
||||
|
||||
For example, to use a local video file with custom prompt:
|
||||
|
||||
```bash
|
||||
@@ -136,19 +152,20 @@ python openai_chat_completion_client_for_multimodal_generation.py \
|
||||
--prompt "What are the main activities shown in this video?"
|
||||
```
|
||||
|
||||
#### Send request via curl
|
||||
#### Send request via curl
|
||||
|
||||
```bash
|
||||
bash run_curl_multimodal_generation.sh use_image
|
||||
```
|
||||
|
||||
## Modality control
|
||||
|
||||
You can control output modalities to specify which types of output the model should generate. This is useful when you only need text output and want to skip audio generation stages for better performance.
|
||||
|
||||
### Supported modalities
|
||||
|
||||
| Modalities | Output |
|
||||
|------------|--------|
|
||||
| ------------ | -------- |
|
||||
| `["text"]` | Text only |
|
||||
| `["audio"]` | Audio only |
|
||||
| `["text", "audio"]` | Text + Audio |
|
||||
@@ -226,7 +243,9 @@ print(response.choices[1].message.audio) # Audio response
|
||||
```
|
||||
|
||||
## Streaming Output
|
||||
|
||||
If you want to enable streaming output, please set the argument as below. The final output will be obtained just after generated by corresponding stage. We support both text streaming output and audio streaming output. Other modalities can output normally.
|
||||
|
||||
```bash
|
||||
python openai_chat_completion_client_for_multimodal_generation.py \
|
||||
--query-type use_image \
|
||||
@@ -259,12 +278,14 @@ The convenience script launches both the vLLM server and Gradio demo together:
|
||||
```
|
||||
|
||||
This script will:
|
||||
|
||||
1. Start the vLLM server in the background
|
||||
2. Wait for the server to be ready
|
||||
3. Launch the Gradio demo
|
||||
4. Handle cleanup when you press Ctrl+C
|
||||
|
||||
The script supports the following arguments:
|
||||
|
||||
- `--model`: Model name/path (default: Qwen/Qwen3-Omni-30B-A3B-Instruct)
|
||||
- `--server-port`: Port for vLLM server (default: 8091)
|
||||
- `--gradio-port`: Port for Gradio demo (default: 7861)
|
||||
@@ -275,18 +296,19 @@ The script supports the following arguments:
|
||||
|
||||
#### Option 2: Manual Launch (Two-Step Process)
|
||||
|
||||
**Step 1: Launch the vLLM API server**
|
||||
##### Step 1: Launch the vLLM API server
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091
|
||||
```
|
||||
|
||||
If you have a custom deploy config file:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-Omni-30B-A3B-Instruct --omni --port 8091 --deploy-config /path/to/deploy_config_file
|
||||
```
|
||||
|
||||
**Step 2: Run the Gradio demo**
|
||||
##### Step 2: Run the Gradio demo
|
||||
|
||||
In a separate terminal:
|
||||
|
||||
@@ -299,7 +321,7 @@ Then open `http://localhost:7861/` on your local browser to interact with the we
|
||||
The gradio script supports the following arguments:
|
||||
|
||||
- `--model`: Model name/path (should match the server model)
|
||||
- `--api-base`: Base URL for the vLLM API server (default: http://localhost:8091/v1)
|
||||
- `--api-base`: Base URL for the vLLM API server (default: <http://localhost:8091/v1>)
|
||||
- `--ip`: Host/IP for Gradio server (default: 127.0.0.1)
|
||||
- `--port`: Port for Gradio server (default: 7861)
|
||||
- `--share`: Share the Gradio demo publicly (creates a public link)
|
||||
|
||||
@@ -413,6 +413,22 @@ vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice --omni --port 8091
|
||||
./qwen3_tts/run_server.sh Base
|
||||
```
|
||||
|
||||
For a local deployment with multiple API frontend processes sharing one set
|
||||
of TTS stage engines, add `--api-server-count`:
|
||||
|
||||
```bash
|
||||
vllm serve Qwen/Qwen3-TTS-12Hz-1.7B-CustomVoice \
|
||||
--omni --api-server-count 2 --port 8091
|
||||
```
|
||||
|
||||
This mode supports local EngineCore stages with one local process group per
|
||||
replica, including stages configured with multiple `num_replicas`. Headless,
|
||||
remote, intra-replica data-parallel, and Ray stage deployments are not
|
||||
supported. Runtime voice upload and deletion are disabled with multiple
|
||||
API frontends because their registries are process-local. Built-in voices,
|
||||
inline `ref_audio`, and voices restored from `custom_voice_dir` at startup are
|
||||
supported.
|
||||
|
||||
### Executor backend
|
||||
|
||||
Single-GPU serves now default to the uniproc executor (lower IPC overhead, the Base cloning use case from [#2603](https://github.com/vllm-project/vllm-omni/issues/2603) / [#2604](https://github.com/vllm-project/vllm-omni/pull/2604)). `vllm_omni/deploy/qwen3_tts.yaml` is the only Qwen3-TTS deploy config; pass `--deploy-config <path>` to override.
|
||||
@@ -478,6 +494,10 @@ checkpoint. When a request names an uploaded voice, the server infers
|
||||
`task_type="Base"`. Built-in presets such as `vivian` and `ryan` remain
|
||||
CustomVoice speakers and require a CustomVoice checkpoint.
|
||||
|
||||
The runtime upload and delete routes require a single API frontend; with
|
||||
`--api-server-count > 1`, use inline `ref_audio` or restore precomputed voices
|
||||
from `custom_voice_dir` at startup.
|
||||
|
||||
### Precomputed custom voices
|
||||
|
||||
For reused Base voice-cloning speakers, precompute the reference artifacts once
|
||||
|
||||
@@ -6,22 +6,25 @@ W4A4 MXFP4 (Microscaling FP4) quantizes both weights and activations to FP4
|
||||
(`float4_e2m1fn_x2`, packed 2 values per byte) using the OCP MX format: groups
|
||||
of 32 K-dimension elements share a single `float8_e8m0fnu` exponent scale.
|
||||
|
||||
On Ascend, single-scale MXFP4 also supports Universal Optimal Scaling
|
||||
(UOS, `Qmax=7.25`), described in the single-scale scaling section below.
|
||||
|
||||
vLLM-Omni provides two quantization methods with different scale structures:
|
||||
|
||||
| Method | Scale structure | Mode | Use case |
|
||||
|--------|----------------|------|----------|
|
||||
| `mxfp4` | Single-scale (per-32 fine only) | Online only | Quick accuracy baseline; no checkpoint prep needed |
|
||||
| `mxfp4_dualscale` | Dual-scale (fine per-32 + coarse per-512 + per-channel `mul_scale`) | Online + Offline | Production; better accuracy; offline recommended |
|
||||
| -------- | ---------------- | ------ | ---------- |
|
||||
| `mxfp4` | Single-scale (per-32 fine only) | Online + pre-quantized single-scale weights | Online baseline, or an existing single-scale checkpoint in the layout below |
|
||||
| `mxfp4_dualscale` | Dual-scale (fine per-32 + coarse per-512 + per-channel `mul_scale`) | Online + Offline | W4A4 with dual-level scales; offline mode loads calibrated Smooth tensors |
|
||||
|
||||
!!! tip "Recommended: `mxfp4_dualscale` offline"
|
||||
For production deployments, use the `mxfp4_dualscale` offline mode with a
|
||||
!!! tip "Existing W4A4-only DualScale deployments"
|
||||
For W4A4-only deployments, the existing `mxfp4_dualscale` offline mode uses a
|
||||
pre-quantized checkpoint produced by msModelSlim. Offline checkpoints load
|
||||
calibrated `mul_scale` tensors from disk, providing measurably better accuracy
|
||||
than any online method. The one-time preprocessing cost amortises across all
|
||||
subsequent inference runs.
|
||||
calibrated `mul_scale` tensors from disk. Quality depends on the model,
|
||||
calibration recipe and BF16 fallback policy; evaluate the checkpoint against
|
||||
your quality target before deployment.
|
||||
|
||||
Use `mxfp4` online only for quick experimentation where preprocessing time
|
||||
is not acceptable and accuracy loss is tolerable.
|
||||
W4A8 layer/step fallback requires single-scale `mxfp4`, online or offline.
|
||||
DualScale supports W4A4 and BF16 routing only.
|
||||
|
||||
!!! warning "Online single-scale ≠ Offline dual-scale"
|
||||
`mxfp4_dualscale` offline mode uses `NPUMxfp4DualScaleLinearMethod`:
|
||||
@@ -34,61 +37,244 @@ vLLM-Omni provides two quantization methods with different scale structures:
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | Support |
|
||||
|--------|---------|
|
||||
| NVIDIA Blackwell GPU (SM 100+) | ⭕ |
|
||||
| NVIDIA Ada/Hopper GPU (SM 89+) | ⭕ |
|
||||
| NVIDIA Ampere GPU (SM 80+) | ⭕ |
|
||||
| AMD ROCm (gfx950 / MI355X) | ✅ |
|
||||
| Intel XPU | ⭕ |
|
||||
| Ascend NPU (Atlas 950 A5) | ✅ |
|
||||
The following matrix describes these vLLM-Omni implementations, rather than
|
||||
hardware support for the MXFP4 format in general.
|
||||
|
||||
Legend: `✅` supported, `❌` unsupported, `⭕` not verified in this guide.
|
||||
| Device | Single-scale online OCP | Single-scale offline | UOS / W4A8 fallback | DualScale |
|
||||
| -------- | -------- | -------- | -------- | -------- |
|
||||
| Ascend NPU (Atlas 950 A5) | ✅ | ✅ | ✅ | ✅ |
|
||||
| AMD ROCm (gfx950 / MI355X) | ✅ | ❌ | ❌ | ❌ |
|
||||
| Other AMD architectures | ❌ | ❌ | ❌ | ❌ |
|
||||
| NVIDIA GPU | ❌ | ❌ | ❌ | ❌ |
|
||||
| Intel XPU | ❌ | ❌ | ❌ | ❌ |
|
||||
|
||||
Legend: `✅` supported by the implementation, `❌` unsupported.
|
||||
ROCm uses the existing AITER online path and requires `mxfp4_scale_alg=0`.
|
||||
|
||||
## Model Type Support
|
||||
|
||||
### Diffusion Model (Wan2.2)
|
||||
|
||||
| Model | Online | Offline | Notes |
|
||||
|-------|--------|---------|-------|
|
||||
| Wan2.2-T2V-A14B | `mxfp4` / `mxfp4_dualscale` | `mxfp4_dualscale` | MoE cascade (`transformer` + `transformer_2`); both transformers quantized with the same config |
|
||||
| Wan2.2-I2V-A14B | `mxfp4` / `mxfp4_dualscale` | `mxfp4_dualscale` | MoE cascade; same scheme as T2V-A14B |
|
||||
| Wan2.2-TI2V-5B | ❌ | ❌ | Parameter count too small; W4A4 causes unacceptable accuracy loss |
|
||||
| ------- | -------- | --------- | ------- |
|
||||
| Wan2.2-T2V-A14B | `mxfp4` / `mxfp4_dualscale` | `mxfp4` / `mxfp4_dualscale` | Cascade experts resolve checkpoint metadata independently; explicit global runtime policy applies to both |
|
||||
| Wan2.2-I2V-A14B | `mxfp4` / `mxfp4_dualscale` | `mxfp4` / `mxfp4_dualscale` | Per-expert configuration uses the same interface as T2V; validate I2V checkpoints separately |
|
||||
| Wan2.2-TI2V-5B | Requires validation | Requires validation | This guide does not establish checkpoint compatibility or generation quality for TI2V-5B |
|
||||
|
||||
The choice between `mxfp4` and `mxfp4_dualscale` in **online mode** is about
|
||||
quantization quality, not model compatibility — both work on cascade (A14B) and
|
||||
single-transformer models alike, the same as `mxfp8` online:
|
||||
Both methods can be integrated with single-transformer and cascade models.
|
||||
This alone does not establish support or quality for every model variant:
|
||||
|
||||
- `mxfp4`: single-scale, lower overhead, simpler compute, online only
|
||||
- `mxfp4_dualscale`: dual-scale + optional BF16 fallback, better accuracy, online **and** offline
|
||||
- `mxfp4`: single-scale; online and pre-quantized weights, with optional W4A8 fallback on NPU
|
||||
- `mxfp4_dualscale`: dual-scale; online and offline, with BF16 fallback
|
||||
|
||||
**Offline** checkpoints for A14B are always in `mxfp4_dualscale` format (produced
|
||||
by the merge script); there is no offline `mxfp4` single-scale format.
|
||||
The existing `merge_mxfp4_dualscale_checkpoint.py` produces **offline
|
||||
`mxfp4_dualscale`** checkpoints. Single-scale weights must satisfy the separate
|
||||
layout described below; changing a DualScale checkpoint's method name to
|
||||
`mxfp4` does not convert its weights. Prepare single-scale checkpoints with
|
||||
external tooling. Real exported checkpoint compatibility and model quality
|
||||
require validation of that checkpoint; the synthetic tests do not establish
|
||||
UOS export or video acceptance.
|
||||
|
||||
!!! note "Per-layer BF16 fallback in offline cascade models"
|
||||
The A14B offline checkpoint uses `quant_method: mxfp4_dualscale`. Most
|
||||
An A14B offline DualScale checkpoint uses `quant_method: mxfp4_dualscale`. Most
|
||||
linear layers are stored as W4A4 MXFP4 DualScale; precision-sensitive layers
|
||||
retain their original BF16 weights and are listed in `ignored_layers` inside
|
||||
each transformer's `config.json`. The two transformers may have different
|
||||
`ignored_layers` sets — the pipeline reads each transformer's own `config.json`
|
||||
and rebuilds the config locally when they differ, so routing is always
|
||||
per-transformer-accurate.
|
||||
|
||||
!!! warning "TI2V-5B not supported"
|
||||
Wan2.2-TI2V-5B is excluded from W4A4 quantization. Its smaller parameter
|
||||
count makes it significantly more sensitive to 4-bit quantization noise,
|
||||
resulting in unacceptable accuracy loss. Use [MXFP8](mxfp8.md) for TI2V-5B.
|
||||
and resolves the storage format and BF16 layer policy independently.
|
||||
|
||||
## Configuration
|
||||
|
||||
### W4A8 fallback at selected layers and denoising steps (Ascend NPU)
|
||||
|
||||
For single-scale `mxfp4`, set `w4a8_fallback_steps` to a list of zero-based
|
||||
request denoising loop indices (not scheduler timestep values).
|
||||
Set `w4a8_fallback_layers` to exact runtime Linear paths, for example
|
||||
`["blocks.10.attn1.to_qkv", "blocks.12.ffn.net_2"]`. A selected layer uses W4A8
|
||||
at every step; at a selected step, all other quantized layers also use W4A8.
|
||||
The rules are combined with **OR**. W4A8 uses MXFP8 activations with MXFP4
|
||||
weights. Omitting both lists or passing `[]` keeps the existing W4A4 behavior. Existing
|
||||
`ignored_layers` take priority: **BF16 > (layer OR step) W4A8 > W4A4**.
|
||||
DualScale's existing leading BF16 block rule is unchanged; it does not enable
|
||||
W4A8.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
|
||||
omni = Omni(
|
||||
model="<Wan2.2-T2V-A14B-Diffusers>",
|
||||
quantization_config={
|
||||
"method": "mxfp4",
|
||||
"w4a8_fallback_layers": ["blocks.10.attn1.to_qkv"],
|
||||
"w4a8_fallback_steps": [0, 1, 38, 39],
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
Both policies also work with offline single-scale `mxfp4`. Any nonempty
|
||||
W4A8 list with `mxfp4_dualscale`, online or offline, raises an explicit error
|
||||
at configuration construction. Empty lists preserve its existing W4A4 path.
|
||||
Each expert's checkpoint still determines its serialized
|
||||
format and BF16 layer routing. A serialized checkpoint config is the complete
|
||||
storage contract: `ignored_layers`, or its `modules_to_not_convert` alias,
|
||||
selects the BF16 layers, and omitting both means no explicit BF16 layers. A
|
||||
method-only checkpoint value such as `"mxfp4"` or
|
||||
`{"quant_method": "mxfp4"}` carries no storage policy and leaves a caller's
|
||||
online `ignored_layers` unchanged. An explicitly supplied quantization config
|
||||
retains its W4A8 layer and step lists when the loader rebuilds either expert's
|
||||
config from disk, including empty lists that disable checkpoint policies.
|
||||
Within an explicit runtime config, an omitted W4A8 list defaults to `[]`;
|
||||
individual omitted fields do not inherit saved policies. Without an explicit
|
||||
runtime config, auto-detection uses the saved checkpoint policies.
|
||||
|
||||
Layer names are relative to each expert (`blocks.10...`, without a
|
||||
`transformer.` or `transformer_2.` prefix). A global config applies the same
|
||||
list to both experts; per-component `transformer`/`transformer_2` configs can
|
||||
select different lists. Wan selects these expert names exactly: a `transformer`
|
||||
entry does not match `transformer_2`; an omitted expert uses `default` if set.
|
||||
A component mapped to `null` (or an unmatched component
|
||||
without a default) stays unquantized and requires a BF16 checkpoint; loading a
|
||||
quantized checkpoint for that disabled component raises an error. A top-level
|
||||
`quantization_config=None` retains checkpoint auto-detection.
|
||||
Names are exact, with no regex, wildcard, block expansion,
|
||||
or checkpoint-name remapping. Wan validates them against its runtime Linear
|
||||
modules and logs the selected paths/count and BF16 overrides per PP rank.
|
||||
Use `attn1.to_qkv` for fused self-attention; `attn1.to_q`, `to_k`, or `to_v`
|
||||
checkpoint names raise an error. Cross-attention `attn2.to_q/to_k/to_v` remains
|
||||
separate. Layer-only fallback does not require a denoise ForwardContext.
|
||||
|
||||
The text-to-video example accepts the JSON directly:
|
||||
|
||||
```bash
|
||||
ASCEND_RT_VISIBLE_DEVICES=0,1,2,3 \
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model /path/to/Wan2.2-T2V-A14B-Diffusers \
|
||||
--quantization-config '{"method":"mxfp4","w4a8_fallback_steps":[0,2]}' \
|
||||
--tensor-parallel-size 1 --ulysses-degree 2 --cfg-parallel-size 2 \
|
||||
--enforce-eager --height 480 --width 832 --num-frames 81 \
|
||||
--num-inference-steps 40 --seed 42 --output wan22-w4a8.mp4
|
||||
```
|
||||
|
||||
Use either `--quantization` or `--quantization-config` in this example.
|
||||
Serving accepts the same JSON through `--diffusion-quantization-config`.
|
||||
|
||||
The list uses one request's loop index across both high- and low-noise experts,
|
||||
and both CFG branches see the same index. A new request starts at step zero.
|
||||
Indices outside the request's step range are never selected. A pipeline must
|
||||
publish `ForwardContext.denoise_step_idx`; requesting step fallback without
|
||||
that context raises an error instead of silently ignoring the list.
|
||||
|
||||
Single-scale W4A4 and W4A8 reuse the same prepared `weight` and `weight_scale`.
|
||||
Online loading quantizes the original BF16/FP16 weight once; offline loading
|
||||
packs the numeric FP4 values and reshapes the saved E8M0 scale without
|
||||
requantizing the weight. Only activation precision changes during forward.
|
||||
There is no DualScale decoding, second W4 representation or `w4a8_weight` /
|
||||
`w4a8_weight_scale` cache. No weight quantization runs inside the denoising loop.
|
||||
|
||||
The mixed FP8/FP4 GEMM uses BF16 output and a matching BF16 row bias for
|
||||
compatibility with the target A5 runtime. An FP16 caller receives that result
|
||||
converted back to FP16; its output shape and dtype are preserved.
|
||||
|
||||
The single-scale fallback validation target is eager Wan2.2 T2V A14B on A5.
|
||||
Quality scores, optimal fallback lists, real UOS exports and additional
|
||||
compile/offload combinations require separate evaluation. Existing W4A4-only
|
||||
DualScale still needs complete 512-channel input groups; use TP=1 with
|
||||
sequence/CFG parallelism for its initial multi-card run.
|
||||
|
||||
### `mxfp4` — Pre-Quantized Single-Scale Weights (Ascend NPU)
|
||||
|
||||
Set the following fields in each transformer's `config.json` when a
|
||||
single-scale checkpoint uses calibrated Smooth scaling:
|
||||
|
||||
```json
|
||||
{
|
||||
"quantization_config": {
|
||||
"quant_method": "mxfp4",
|
||||
"is_checkpoint_mxfp4_serialized": true,
|
||||
"require_smooth_scale": true,
|
||||
"ignored_layers": []
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Each quantized Linear must provide tensors under its model parameter name:
|
||||
|
||||
| Tensor | Checkpoint layout | Meaning |
|
||||
| -------- | ------------------- | --------- |
|
||||
| `weight` | `(N, K)` numeric FP4 values | Packed to FP4 after loading |
|
||||
| `weight_scale` | `(N, ceil(K / 32))`, `uint8` | Raw E8M0 exponent bytes, required |
|
||||
| `mul_scale` | `(K,)`, FP32 | Smooth pre-scale, required when declared above |
|
||||
|
||||
Both W4A4 and W4A8 quantize `x * mul_scale`. The Smooth tensor is converted
|
||||
to the layer's activation dtype after loading. Row-parallel layers load the
|
||||
corresponding input-channel shard. Pre-fused QKV weights must use one shared
|
||||
Smooth tensor for the fused projection.
|
||||
|
||||
For backward compatibility, `require_smooth_scale` defaults to `false`:
|
||||
an absent tensor uses identity scaling, while a present tensor is still loaded.
|
||||
Without a declaration, an absent Smooth tensor cannot be distinguished from a
|
||||
legacy unsmoothed checkpoint. Calibrated checkpoints should declare
|
||||
`require_smooth_scale: true` so missing or misnamed Smooth tensors fail loading;
|
||||
incorrect tensor shapes, non-floating tensors, NaN/Inf and nonpositive values
|
||||
also fail. Values that overflow or underflow to zero in the activation dtype
|
||||
are rejected during weight processing. The flag requires serialized single-scale
|
||||
weights and is rejected for online quantization. It does not make DualScale
|
||||
scales optional or change missing-parameter policies for other backends.
|
||||
Either the checkpoint or an explicit caller can require Smooth: a `true`
|
||||
declaration is retained when per-expert metadata rebuilds the configuration,
|
||||
including when the other source omits the flag or sets it to `false`.
|
||||
|
||||
Prepare the canonical numeric FP4 checkpoint before inference using external
|
||||
tooling. Runtime loading does not parse producer-specific export formats.
|
||||
Retain the producer recipe to identify the weight-generation algorithm and
|
||||
Smooth calibration; tensor storage alone does not establish UOS provenance.
|
||||
|
||||
### `mxfp4` — Single-Scale Block Scaling (OCP / UOS)
|
||||
|
||||
`mxfp4_scale_alg` selects the MXFP4 block-scaling method on NPU.
|
||||
UOS is a scaling rule, not a separate activation dtype or an activation-only
|
||||
algorithm. Both choices use FP4 E2M1 values with shared E8M0 scales.
|
||||
|
||||
| Mode | Weight preparation | A4 activation quantization | W4A8 fallback |
|
||||
| --- | --- | --- | --- |
|
||||
| Online | Selected scaling method, once at loading | Selected scaling method | Same W4; OCP MXFP8 A8 |
|
||||
| Offline | Load existing W4 and scales unchanged | Selected scaling method | Same W4; OCP MXFP8 A8 |
|
||||
|
||||
The available settings are:
|
||||
|
||||
| Setting | MXFP4 scaling method | NPU parameters |
|
||||
| --- | --- | --- |
|
||||
| `0` (default) | OCP MX | `scale_alg=0` |
|
||||
| `2` | UOS (`Qmax=7.25`) | `scale_alg=2, dst_type_max=7.25` |
|
||||
|
||||
Universal Optimal Scaling (UOS, `Qmax=7.25`) is derived in
|
||||
[MXAttention: Data-Free Optimal Scaling and Pre-Normalization Quantization for MXFP4 Attention](https://arxiv.org/abs/2607.24377),
|
||||
Section 4.1. The value `7.25` is the boundary used to select the shared
|
||||
power-of-two scale; the maximum finite E2M1 value remains `6`.
|
||||
This option applies UOS to Linear quantization. It does not implement the
|
||||
paper's full MXAttention pipeline or Pre-Normalization Quantization (PNQ).
|
||||
|
||||
Both use FP4 E2M1 with E8M0 scales, `axis=-1`, `block_size=32`,
|
||||
`round_mode="rint"`. W4A8 always quantizes the activation to
|
||||
`float8_e4m3fn` with `scale_alg=0` and the same grouping/rounding, without
|
||||
UOS parameters. A4 and A8 share the prepared W4 and scale. UOS is currently
|
||||
NPU-only; ROCm retains its existing AITER online algorithm.
|
||||
|
||||
An explicit UOS online configuration is
|
||||
`{"method": "mxfp4", "mxfp4_scale_alg": 2}`. For offline checkpoints the
|
||||
setting affects **only A4 activations**. Existing weights are never requantized:
|
||||
a UOS checkpoint can run UOS W4 + UOS A4 or UOS W4 + OCP A4, and both fall back to
|
||||
UOS W4 + OCP A8. Record the weight-generation algorithm separately from the
|
||||
runtime activation setting. Smooth remains active with either setting. The
|
||||
UOS configuration explicitly selects `2`; attention computation is unchanged.
|
||||
|
||||
### `mxfp4` — Single-Scale Online Mode
|
||||
|
||||
Online mode requires no pre-processing. vLLM-Omni quantizes BF16 weights to
|
||||
MXFP4 at load time using `npu_dynamic_mx_quant`. A single block scale
|
||||
(`float8_e8m0fnu`, one per 32 K elements) is computed on the fly; no
|
||||
calibration `mul_scale` is available. Applies equally to single-transformer
|
||||
and cascade (A14B) models — both transformers in a cascade receive the same
|
||||
quantization config automatically.
|
||||
Online mode quantizes BF16/FP16 weights once at loading and computes activation
|
||||
scales during forward. Each block of 32 K elements shares one E8M0 scale;
|
||||
no calibrated `mul_scale` is available. An explicit global quantization config
|
||||
applies to both cascade experts. Per-component configs can select different
|
||||
policies; checkpoint auto-detection resolves each expert independently.
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
@@ -103,7 +289,7 @@ outputs = omni.generate(
|
||||
|
||||
```bash
|
||||
# Single-transformer or cascade model — same command
|
||||
python text_to_video.py --model <your-model> --quantization mxfp4
|
||||
python examples/offline_inference/text_to_video/text_to_video.py --model <your-model> --quantization mxfp4
|
||||
|
||||
# Online serving
|
||||
vllm serve <your-model> --omni --quantization mxfp4
|
||||
@@ -113,19 +299,19 @@ vllm serve <your-model> --omni --quantization mxfp4
|
||||
|
||||
Online DualScale mode computes both fine and coarse scales on the fly from BF16
|
||||
weights using `npu_dynamic_dual_level_mx_quant`. Applies equally to
|
||||
single-transformer and cascade (A14B) models. Compared to `mxfp4` online,
|
||||
DualScale provides better quantization accuracy at higher compute cost.
|
||||
single-transformer and cascade (A14B) models with compatible Linear layers.
|
||||
Compare quality and performance using the intended model and workload.
|
||||
|
||||
The default configuration keeps the leading 5 transformer blocks in BF16
|
||||
(`num_bf16_fallback_layers=5`). Accuracy evaluation on Wan2.2-A14B shows this
|
||||
is sufficient to meet quality requirements and is the recommended setting.
|
||||
(`num_bf16_fallback_layers=5`). This is an implementation default; select and
|
||||
validate the BF16 policy against your own quality target.
|
||||
|
||||
```python
|
||||
omni = Omni(model="<your-model>", quantization="mxfp4_dualscale")
|
||||
```
|
||||
|
||||
```bash
|
||||
python text_to_video.py --model <your-model> --quantization mxfp4_dualscale
|
||||
python examples/offline_inference/text_to_video/text_to_video.py --model <your-model> --quantization mxfp4_dualscale
|
||||
```
|
||||
|
||||
If accuracy debugging identifies additional precision-sensitive layers, they
|
||||
@@ -136,21 +322,22 @@ omni = Omni(
|
||||
model="<your-model>",
|
||||
quantization_config={
|
||||
"method": "mxfp4_dualscale",
|
||||
"ignored_layers": ["blocks.10.attn1.to_q"], # explicit per-layer override
|
||||
"ignored_layers": ["blocks.10.attn1.to_qkv"], # explicit per-layer override
|
||||
},
|
||||
)
|
||||
```
|
||||
|
||||
BF16 fallback routing in online mode applies two rules in priority order:
|
||||
|
||||
1. **`ignored_layers`** (explicit per-layer override): any layer whose prefix
|
||||
matches is kept in BF16 regardless of block index.
|
||||
1. **`ignored_layers`** (explicit per-layer override): a listed runtime Linear
|
||||
module is kept in BF16 regardless of block index. Use exact module paths,
|
||||
not arbitrary block prefixes or names ending in `.weight`.
|
||||
2. **`num_bf16_fallback_layers`** (coarse leading-block rule): the first N
|
||||
transformer blocks (`blocks.0` … `blocks.N-1`) fall back to BF16. Defaults
|
||||
to `5` (recommended). Layers outside `blocks.N.*`
|
||||
(e.g. `condition_embedder`) are always quantized.
|
||||
to `5`. Layers outside `blocks.N.*` are quantized unless explicitly listed
|
||||
in `ignored_layers`.
|
||||
|
||||
### `mxfp4_dualscale` — DualScale Offline Mode (Recommended)
|
||||
### `mxfp4_dualscale` — DualScale Offline Mode
|
||||
|
||||
Offline mode loads a pre-quantized DualScale checkpoint from msModelSlim. A
|
||||
preprocessing step converts the raw quantized output to the diffusers format
|
||||
@@ -169,8 +356,8 @@ inside `config.json`. At runtime, each layer's prefix is matched against
|
||||
Each quantized linear layer stores four tensors:
|
||||
|
||||
| Tensor | Shape | dtype | Description |
|
||||
|--------|-------|-------|-------------|
|
||||
| `weight` | `(N, K)` | float8_e4m3fn | FP4 packed (2 values per byte) |
|
||||
| -------- | ------- | ------- | ------------- |
|
||||
| `weight` | `(N, K)` | float8_e4m3fn | Numeric FP4 values; packed into two values per byte after loading |
|
||||
| `weight_scale` | `(N, K//32)` | uint8 | Fine block scale (`float8_e8m0fnu` bit pattern) |
|
||||
| `weight_dual_scale` | `(N, K//512, 1)` | float32 | Coarse block scale |
|
||||
| `mul_scale` | `(K,)` | float32 | Per-input-channel smooth pre-scale (from calibration) |
|
||||
@@ -223,7 +410,7 @@ python vllm_omni/quantization/tools/merge_mxfp4_dualscale_checkpoint.py \
|
||||
```
|
||||
|
||||
| Argument | Description |
|
||||
|----------|-------------|
|
||||
| ---------- | ------------- |
|
||||
| `--model-type` | Model variant: `Wan2.2-T2V-A14B` or `Wan2.2-I2V-A14B` |
|
||||
| `--original-model` | Root directory of the original BF16 diffusers model |
|
||||
| `--quant-path` | Root directory of the msModelSlim quantized output |
|
||||
@@ -251,14 +438,14 @@ The `quantization_config` injected into `config.json` for each transformer:
|
||||
```
|
||||
|
||||
`ignored_layers` lists every linear layer that retains its original BF16 weight,
|
||||
using vllm-omni model parameter names (QKV-fused, FFN underscored, `to_out`
|
||||
using vllm-omni runtime module paths (QKV-fused, FFN underscored, `to_out`
|
||||
unindexed). The exact entries are determined by the quantization tool (msModelSlim)
|
||||
and may differ between `transformer` and `transformer_2` in a cascade model.
|
||||
|
||||
#### Step 3 — Serve
|
||||
|
||||
```bash
|
||||
python text_to_video.py --model /path/to/Wan2.2-T2V-A14B-MXFP4-DualScale
|
||||
python examples/offline_inference/text_to_video/text_to_video.py --model /path/to/Wan2.2-T2V-A14B-MXFP4-DualScale
|
||||
|
||||
# Online serving
|
||||
vllm serve /path/to/Wan2.2-T2V-A14B-MXFP4-DualScale --omni
|
||||
@@ -275,25 +462,32 @@ omni = Omni(model="/path/to/Wan2.2-T2V-A14B-MXFP4-DualScale")
|
||||
|
||||
## Parameters
|
||||
|
||||
### `mxfp4` (single-scale, online only)
|
||||
### `mxfp4` (single-scale, online + offline)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `method` | str | — | `"mxfp4"` |
|
||||
| `ignored_layers` | list[str] | `[]` | Layer prefixes to keep in BF16 |
|
||||
| `mxfp4_scale_alg` | int | `0` | MXFP4 block scaling: `0` OCP MX, `2` UOS (`Qmax=7.25`, NPU only). Online: W4 and A4; offline: A4 only. A8 remains OCP MXFP8 |
|
||||
| `is_checkpoint_mxfp4_serialized` | bool | `false` | Load pre-quantized single-scale weights and scales; declared in checkpoint `config.json` |
|
||||
| `require_smooth_scale` | bool | `false` | Require calibrated Smooth tensors for offline single-scale weights |
|
||||
| `ignored_layers` | list[str] | `[]` | Exact runtime Linear module paths to keep in BF16 |
|
||||
| `w4a8_fallback_layers` | list[str] | `[]` | Exact runtime Linear paths that always use A8; NPU only |
|
||||
| `w4a8_fallback_steps` | list[int] | `[]` | Zero-based denoise steps that use A8; NPU only |
|
||||
|
||||
### `mxfp4_dualscale` (dual-scale, online + offline)
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| ----------- | ------ | --------- | ------------- |
|
||||
| `method` | str | — | `"mxfp4_dualscale"` |
|
||||
| `is_checkpoint_serialized` | bool | `False` | `True` for offline DualScale checkpoints; auto-set from `config.json` when using the preprocessing script |
|
||||
| `ignored_layers` | list[str] | `[]` | Layer prefixes to keep in BF16. **Works in both modes**: offline — populated by the merge script for interleaved sensitive layers; online — user-supplied for explicit per-layer precision override |
|
||||
| `num_bf16_fallback_layers` | int | `5` | **Online mode only**: leading N transformer blocks (`blocks.0` … `blocks.N-1`) kept in BF16. Applied after `ignored_layers`; ignored in offline mode. Default of `5` is the evaluated recommended value for Wan2.2-A14B |
|
||||
| `ignored_layers` | list[str] | `[]` | Exact runtime Linear module paths to keep in BF16. **Works in both modes**: offline — populated by the merge script for interleaved sensitive layers; online — user-supplied for explicit per-layer precision override |
|
||||
| `num_bf16_fallback_layers` | int | `5` | **Online mode only**: leading N transformer blocks (`blocks.0` … `blocks.N-1`) kept in BF16. Applied after `ignored_layers`; ignored in offline mode. Validate the default of `5` against the intended model and quality target |
|
||||
| `w4a8_fallback_layers` | list[str] | `[]` | Must remain empty; DualScale does not support W4A8 fallback |
|
||||
| `w4a8_fallback_steps` | list[int] | `[]` | Must remain empty; DualScale does not support W4A8 fallback |
|
||||
|
||||
#### BF16 fallback priority (online mode)
|
||||
|
||||
```
|
||||
```text
|
||||
for each linear layer:
|
||||
if prefix in ignored_layers → BF16 (explicit override, highest priority)
|
||||
elif block_idx < num_bf16_fallback_layers → BF16 (coarse leading-block rule)
|
||||
@@ -307,8 +501,9 @@ unless they appear in `ignored_layers`.
|
||||
|
||||
1. **Online single-scale (`mxfp4`)** quantizes BF16 weights at load time using
|
||||
`npu_dynamic_mx_quant` (single-scale). No calibration `mul_scale` is
|
||||
available — all output partitions receive an identity pre-scale. No offline
|
||||
checkpoint format exists for this method.
|
||||
available — all output partitions receive an identity pre-scale. Offline
|
||||
single-scale checkpoints use the canonical numeric FP4 layout
|
||||
described above.
|
||||
|
||||
2. **Online dual-scale (`mxfp4_dualscale`, `is_checkpoint_serialized=False`)**
|
||||
quantizes BF16 weights using `npu_dynamic_dual_level_mx_quant` (fine + coarse
|
||||
@@ -316,7 +511,7 @@ unless they appear in `ignored_layers`.
|
||||
explicit `ignored_layers` stay in BF16 for accuracy.
|
||||
|
||||
3. **Offline dual-scale (`mxfp4_dualscale`, `is_checkpoint_serialized=True`)** —
|
||||
**recommended for production** — loads four tensors per quantized layer: FP4
|
||||
loads four tensors per quantized layer: FP4
|
||||
weight, fine scale (`uint8` reinterpreted as `float8_e8m0fnu`), coarse scale
|
||||
(`float32`), and per-input-channel `mul_scale` (`float32`). BF16 fallback
|
||||
layers have no quantization tensors and are routed via `ignored_layers`.
|
||||
@@ -325,21 +520,21 @@ unless they appear in `ignored_layers`.
|
||||
layout as `float8_e8m0fnu`) and reinterpreted at load time without a lossy
|
||||
float32 round-trip.
|
||||
|
||||
5. **Cascade model config propagation**: in a cascade model (transformer +
|
||||
transformer_2), vLLM-Omni reads each transformer's own `config.json` and
|
||||
rebuilds the quant config locally when `ignored_layers` differs between
|
||||
transformers, ensuring per-layer routing is accurate for each. The first
|
||||
transformer's config is propagated to `od_config` so the second transformer
|
||||
can reuse it as a starting point.
|
||||
5. **Cascade model configuration**: checkpoint auto-detection resolves each
|
||||
expert's own `config.json` independently; the first expert's detected config
|
||||
is not reused as the second expert's policy. An explicit global runtime
|
||||
config applies to both experts, while per-component configs select policies
|
||||
by the exact `transformer` / `transformer_2` names. Serialized checkpoint
|
||||
metadata still determines each expert's storage format and BF16 layers.
|
||||
|
||||
6. **Self-attention QKV fusion**: Q, K, V projection weights are fused into a
|
||||
single `QKVParallelLinear` layer at runtime. `ignored_layers` entries use the
|
||||
fused name (`attn1.to_qkv`), written automatically by the merge script.
|
||||
|
||||
7. W4A4 carries higher quantization noise than W8A8 (16 vs 256 levels). The
|
||||
DualScale offline method mitigates this with calibrated `mul_scale` smooth
|
||||
quantization. Use `ignored_layers` and `num_bf16_fallback_layers` to trade
|
||||
off compression vs. accuracy for precision-sensitive layers.
|
||||
7. FP4 has fewer representable values than FP8, so evaluate quantization
|
||||
error and generation quality for the chosen recipe. Offline Smooth scaling
|
||||
and BF16 fallback can help with sensitive layers.
|
||||
`num_bf16_fallback_layers` applies only to online DualScale.
|
||||
|
||||
## Adapting MXFP4 for a New Model
|
||||
|
||||
@@ -350,27 +545,26 @@ a merge script for offline checkpoints.
|
||||
|
||||
### Step 1 — Discover runtime layer names
|
||||
|
||||
`ignored_layers` entries must match the **runtime parameter names** used inside
|
||||
vllm-omni, which may differ from the names stored in the diffusers checkpoint.
|
||||
The canonical source of truth is the model's own `named_parameters()`.
|
||||
`ignored_layers` entries use exact **runtime Linear module paths**, without
|
||||
`.weight`. Inspect the actual transformer instance in model initialization or
|
||||
a developer harness; `Omni` does not expose an `omni.pipeline.transformer` API.
|
||||
For a transformer instance named `transformer`:
|
||||
|
||||
```python
|
||||
from vllm_omni import Omni
|
||||
from vllm.model_executor.layers.linear import LinearBase
|
||||
|
||||
# Load the model without quantization to inspect parameter names.
|
||||
omni = Omni(model="/path/to/your-model") # no --quantization flag
|
||||
for name, _ in omni.pipeline.transformer.named_parameters():
|
||||
if "weight" in name and "scale" not in name:
|
||||
for name, module in transformer.named_modules():
|
||||
if isinstance(module, LinearBase):
|
||||
print(name)
|
||||
```
|
||||
|
||||
Compare the printed names against the diffusers checkpoint keys
|
||||
Compare these module paths against the corresponding diffusers checkpoint keys
|
||||
(`safetensors.safe_open` or `torch.load`) to identify any renames your model
|
||||
applies. Common patterns that differ in Wan2.2 (and may appear in other
|
||||
models):
|
||||
|
||||
| Diffusers checkpoint name | vllm-omni runtime name | Reason |
|
||||
|---------------------------|------------------------|--------|
|
||||
| --------------------------- | ------------------------ | -------- |
|
||||
| `attn1.to_q`, `attn1.to_k`, `attn1.to_v` | `attn1.to_qkv` | Self-attention Q/K/V fused into `QKVParallelLinear` |
|
||||
| `ffn.net.0.proj` | `ffn.net_0.proj` | Dots in sub-module names replaced with underscores |
|
||||
| `ffn.net.2` | `ffn.net_2` | Same underscore rule |
|
||||
@@ -382,8 +576,9 @@ fused runtime parameters.
|
||||
|
||||
!!! warning "Partial QKV fallback is not allowed"
|
||||
If your model fuses Q, K, V into a single layer, `ignored_layers` must
|
||||
include **all three or none**. A partial fallback (e.g. `to_q` in BF16 but
|
||||
`to_k`, `to_v` quantized) cannot be expressed at runtime because they share
|
||||
name the fused runtime module (for Wan, `attn1.to_qkv`). The source
|
||||
checkpoint must keep **all three or none** in BF16. A partial fallback
|
||||
(e.g. `to_q` in BF16 but `to_k`, `to_v` quantized) cannot be expressed at runtime because they share
|
||||
one `QKVParallelLinear`. The merge script enforces this and raises an error
|
||||
if only some of the trio appear as non-quantized.
|
||||
|
||||
@@ -392,12 +587,13 @@ fused runtime parameters.
|
||||
#### Online mode
|
||||
|
||||
Pass `ignored_layers` directly in the quantization config using the **runtime
|
||||
names** discovered in Step 1. No code changes to the model are required.
|
||||
names** discovered in Step 1. This assumes the model already passes its
|
||||
quantization config and module prefixes to compatible vLLM Linear layers.
|
||||
|
||||
```python
|
||||
omni = Omni(
|
||||
model="/path/to/your-model",
|
||||
quantization={
|
||||
quantization_config={
|
||||
"method": "mxfp4_dualscale",
|
||||
"ignored_layers": [
|
||||
"blocks.0.attn1.to_qkv", # runtime name, not diffusers name
|
||||
@@ -409,9 +605,14 @@ omni = Omni(
|
||||
```
|
||||
|
||||
```bash
|
||||
# CLI does not support list-typed ignored_layers directly.
|
||||
# Use the Python API or set ignored_layers in config.json (offline).
|
||||
python your_script.py --model /path/to/your-model --quantization mxfp4_dualscale
|
||||
# The text-to-video example accepts list-valued fields through JSON.
|
||||
python examples/offline_inference/text_to_video/text_to_video.py \
|
||||
--model /path/to/your-model \
|
||||
--quantization-config '{"method":"mxfp4_dualscale","ignored_layers":["blocks.0.attn1.to_qkv"]}'
|
||||
|
||||
# Serving accepts the same configuration.
|
||||
vllm serve /path/to/your-model --omni \
|
||||
--diffusion-quantization-config '{"method":"mxfp4_dualscale","ignored_layers":["blocks.0.attn1.to_qkv"]}'
|
||||
```
|
||||
|
||||
The `num_bf16_fallback_layers` coarse rule is an alternative to listing layers
|
||||
@@ -436,9 +637,13 @@ the merge script is correct. The injected block:
|
||||
}
|
||||
```
|
||||
|
||||
To add a layer manually (e.g. to pin an additional layer to BF16 without
|
||||
re-running the merge script), edit `config.json` inside the transformer
|
||||
subfolder. Use runtime names, not diffusers checkpoint names.
|
||||
Adding an offline BF16 fallback layer requires both the original BF16 weight
|
||||
from the base model and a matching `ignored_layers` entry. Regenerate the
|
||||
checkpoint with the revised storage policy, restoring the original weight
|
||||
(and bias, if present) and removing that layer's quantization tensors.
|
||||
Editing `config.json` alone does not reconstruct BF16 weights from stored FP4
|
||||
values. For fused QKV, restore all three projections together. Use runtime
|
||||
module paths in `ignored_layers`.
|
||||
|
||||
### Step 3 — Write a merge script for offline mode
|
||||
|
||||
@@ -474,6 +679,7 @@ renaming rules and collect the results.
|
||||
!!! tip "Validate before serving"
|
||||
After producing the offline checkpoint, load it without a `--quantization`
|
||||
flag and verify that vLLM-Omni auto-detects the correct method. Check that
|
||||
the layer count reported in the startup log matches expectations: quantized
|
||||
layer count + `ignored_layers` count should equal total linear layer count.
|
||||
Any mismatch indicates a name-mapping bug in the merge script.
|
||||
each intended runtime Linear module resolves to the expected quantized or
|
||||
BF16 method and that every BF16 entry has original BF16 weights. Account
|
||||
for fused modules and, in online DualScale, the leading-block fallback rule;
|
||||
the raw length of `ignored_layers` alone is not a reliable layer count.
|
||||
@@ -13,7 +13,7 @@ For the internal architecture and backend extension points, see the
|
||||
| Mode | Guide | Description | Methods |
|
||||
| ------ | ------- | ------------- | --------- |
|
||||
| Online quantization | [Online Quantization](online.md) | vLLM-Omni computes quantized weights and scales while loading the model. | FP8 W8A8, Int8 W8A8, BitsAndBytes W4, MXFP8 W8A8, MXFP4 W4A4 |
|
||||
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion Flash Attention tensors during inference. | FP8 FA |
|
||||
| Runtime attention quantization | [Quantized KV Cache](quantized_kvcache.md) | vLLM-Omni dynamically quantizes eligible diffusion attention tensors during inference. | FP8, MXFP8, MXFP4 |
|
||||
| Pre-quantized checkpoints | Method-specific guides | The checkpoint or an offline quantizer provides quantized weights and scales before serving. | ModelOpt, AutoRound, TorchAO, msModelSlim, serialized Int8, offline MXFP8, offline MXFP4 DualScale |
|
||||
|
||||
## Hardware Support
|
||||
|
||||
@@ -2,41 +2,36 @@
|
||||
|
||||
## Overview
|
||||
|
||||
In DiT-based image and video generation, Flash Attention can take a large share
|
||||
of denoising time, especially for high-resolution or long-frame workloads.
|
||||
vLLM-Omni supports online FP8 quantization for eligible diffusion Flash
|
||||
Attention (FA) to reduce FA latency while keeping model weights in their
|
||||
original dtype.
|
||||
In DiT-based image and video generation, attention can take a large share of
|
||||
denoising time, especially for high-resolution or long-frame workloads.
|
||||
vLLM-Omni supports online Q/K/V quantization for eligible diffusion attention
|
||||
paths while keeping model weights in their original dtype.
|
||||
|
||||
This feature is configured through `diffusion_kv_cache_dtype` on
|
||||
`OmniDiffusionConfig` (CLI: `--diffusion-kv-cache-dtype`). It is intentionally
|
||||
**not** the same as vLLM's `--kv-cache-dtype`, which controls autoregressive
|
||||
language-model KV cache storage and defaults to `"auto"`. Diffusion FA
|
||||
quantization uses the dedicated diffusion flags so omni serve does not inherit
|
||||
that default.
|
||||
Runtime attention precision is configured through `diffusion_kv_cache_dtype`
|
||||
on `OmniDiffusionConfig` (CLI: `--diffusion-kv-cache-dtype`). This setting is
|
||||
separate from vLLM's `--kv-cache-dtype`, which controls autoregressive
|
||||
language-model KV cache storage.
|
||||
|
||||
In vLLM-Omni diffusion pipelines, this is a runtime FA path: Q/K/V tensors are
|
||||
dynamically quantized before the attention operator. It does not quantize model
|
||||
weights and is separate from [FP8 W8A8](fp8.md), [Int8 W8A8](int8.md), or
|
||||
pre-quantized checkpoint formats.
|
||||
|
||||
If `diffusion_kv_cache_dtype` is not set, behavior is unchanged and attention
|
||||
runs in the native dtype.
|
||||
If `diffusion_kv_cache_dtype` is not set, attention runs in the native dtype.
|
||||
|
||||
## Hardware Support
|
||||
|
||||
| Device | FP8 FA |
|
||||
|--------|--------|
|
||||
| Ascend NPU | ✅ |
|
||||
| NVIDIA GPU | ❌ |
|
||||
| AMD ROCm | ❌ |
|
||||
| Intel XPU | ❌ |
|
||||
| Device | Dense FP8 | Dense MXFP8/MXFP4 | BSA FP8/MXFP4 |
|
||||
|--------|------------|-------------------|----------------|
|
||||
| Ascend NPU | ✅ | ✅ | ✅ |
|
||||
| NVIDIA GPU | ❌ | ❌ | ❌ |
|
||||
| AMD ROCm | ❌ | ❌ | ❌ |
|
||||
| Intel XPU | ❌ | ❌ | ❌ |
|
||||
|
||||
Legend: `✅` supported, `❌` unsupported.
|
||||
|
||||
FP8 FA is currently implemented only for the NPU Flash Attention backend. Other
|
||||
backends do not support `diffusion_kv_cache_dtype="fp8"` for diffusion attention
|
||||
and fall back to native dtype execution.
|
||||
These runtime methods are implemented only by the NPU Flash Attention and BSA
|
||||
backends. Other backends reject an incompatible explicit configuration.
|
||||
|
||||
## Model Type Support
|
||||
|
||||
@@ -44,7 +39,7 @@ and fall back to native dtype execution.
|
||||
|
||||
| Model | Scope | Status | Notes |
|
||||
|-------|-------|--------|-------|
|
||||
| Wan2.2 | Eligible DiT full-attention FA on Ascend NPU | Tested | Compare quality and latency against a BF16 baseline before production use |
|
||||
| Wan2.2 | Self-attention on Ascend NPU | NPU interface and T2V smoke tested | Compare quality and latency against a BF16 baseline before production use |
|
||||
| Other diffusion models | Eligible DiT full-attention FA on Ascend NPU | Not tested | You can try `diffusion_kv_cache_dtype="fp8"`; tune `diffusion_kv_cache_skip_steps` and `diffusion_kv_cache_skip_layers` when higher precision is needed |
|
||||
|
||||
### Multi-Stage Omni/TTS Model (Qwen3-Omni, Qwen3-TTS)
|
||||
@@ -105,12 +100,13 @@ The legacy keyword aliases `kv_cache_dtype`, `kv_cache_skip_steps`, and
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `diffusion_kv_cache_dtype` | str \| None | `None` | Set to `"fp8"` to enable dynamic FP8 FA on supported attention backends |
|
||||
| `diffusion_kv_cache_dtype` | str \| None | `None` | Runtime attention method. Dense FA supports `fp8`, `mxfp8`, and `mxfp4`; BSA supports `fp8` and `mxfp4` |
|
||||
| `diffusion_kv_cache_skip_steps` | str \| None | `None` | Denoising step selector to keep in native dtype, for example `"0,1,4-6"` |
|
||||
| `diffusion_kv_cache_skip_layers` | str \| None | `None` | Transformer layer selector to keep in native dtype, for example `"0-2,10"` |
|
||||
|
||||
Selectors use comma-separated integers and inclusive ranges. Listed steps or
|
||||
layers skip FP8 FA; all other eligible full-attention forwards use the FP8 path.
|
||||
layers run floating-point attention through the selected Dense or BSA backend;
|
||||
all other eligible forwards use `diffusion_kv_cache_dtype`.
|
||||
|
||||
## Validation and Notes
|
||||
|
||||
@@ -123,3 +119,60 @@ layers skip FP8 FA; all other eligible full-attention forwards use the FP8 path.
|
||||
4. Report both latency and quality results when enabling this option for a new
|
||||
model. For image or video models, include visual comparison and quantitative
|
||||
metrics when available, such as PSNR or SSIM.
|
||||
|
||||
## Wan2.2 T2V quantized attention on Ascend
|
||||
|
||||
Wan2.2 T2V A14B supports quantized self-attention through MindIE-SD. Model
|
||||
weights are unchanged, and model-level opt-out keeps cross-attention in its
|
||||
native dtype.
|
||||
|
||||
| Attention path | self-attention `backend` | Supported `diffusion_kv_cache_dtype` |
|
||||
| --- | --- | --- |
|
||||
| Dense FA | `FLASH_ATTN` | `fp8`, `mxfp8`, `mxfp4` |
|
||||
| BSA | `RAINFUSION_ATTN` | `fp8`, `mxfp4` |
|
||||
|
||||
Configure the method and fallback selectors once at the diffusion stage. The
|
||||
per-role attention config only selects the self-attention backend:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- stage_id: 0
|
||||
diffusion_kv_cache_dtype: "mxfp8"
|
||||
diffusion_kv_cache_skip_layers: "0,39"
|
||||
diffusion_kv_cache_skip_steps: "0,1,38,39"
|
||||
diffusion_attention_config:
|
||||
per_role:
|
||||
self:
|
||||
backend: FLASH_ATTN # Use RAINFUSION_ATTN with fp8 or mxfp4 for BSA.
|
||||
```
|
||||
|
||||
For 40 denoising steps, `diffusion_kv_cache_skip_steps: "0,1,38,39"` and
|
||||
`diffusion_kv_cache_skip_layers: "0,39"` are recommended starting points. Steps
|
||||
are zero-based across the complete request and do not reset when Wan switches
|
||||
transformers; layer indices are zero-based and local to each transformer.
|
||||
Selectors accept comma-separated indices and inclusive ranges such as
|
||||
`"0,3-5"`. A selected forward uses floating-point attention while preserving
|
||||
the Dense or BSA path.
|
||||
|
||||
Unsupported precision/input combinations raise an error and must be corrected
|
||||
in the configuration; operator errors are not retried with another precision.
|
||||
|
||||
For BSA, change the backend to `RAINFUSION_ATTN` and set
|
||||
`diffusion_kv_cache_dtype` to `fp8` or `mxfp4`.
|
||||
See [RainFusion attention](../diffusion/attention_backends/rainfusion.md) for
|
||||
sparse-path configuration and behavior.
|
||||
|
||||
### Requirement and interface test
|
||||
|
||||
Until these APIs are available in a MindIE-SD release, use revision
|
||||
[`8637b5333b0225381b215390fd09a8732e671cc4`](https://gitcode.com/zqxu/MindIE-SD/commit/8637b5333b0225381b215390fd09a8732e671cc4).
|
||||
Build the Python package, PyTorch plugin, and custom operators from that same
|
||||
checkout. With CANN sourced, run the NPU interface tests from the Omni checkout:
|
||||
|
||||
```bash
|
||||
python -m pytest tests/platforms/npu/quant/test_kv_quant_npu.py \
|
||||
-k real_npu -vv -s -o addopts=''
|
||||
```
|
||||
|
||||
A skipped NPU test is not a pass. Validate generated video quality against a
|
||||
floating-point run with the same prompt, seed, resolution, and step count.
|
||||
Reference in new issue
Block a user