[FEAT]Add FastVideo VSA backend for Wan2.2 (#4820)
Signed-off-by: bjf-frz <frz123db@gmail.com> Co-authored-by: Hongsheng Liu <liuhongsheng4@huawei.com>
This commit is contained in:
1 parent
3d035bfa19
commit
072bfc02dd
28 files changed
+1316
-107
No files matched your search
@@ -61,7 +61,8 @@ unstructured keyword dictionaries:
|
||||
- `quant` is consumed by FlashInfer and TRTLLM with backend-specific value
|
||||
validation;
|
||||
- `skip_softmax` is consumed by TRTLLM; and
|
||||
- `block_sparse` is consumed by block-sparse backends such as RainFusion.
|
||||
- `block_sparse` is consumed by block-sparse backends such as RainFusion; and
|
||||
- `fastvideo_vsa_topk` is consumed by FastVideo VSA.
|
||||
|
||||
A backend reads only the fields it owns and rejects incompatible values. New
|
||||
options should be added to a shared typed spec only when more than one backend
|
||||
@@ -87,3 +88,19 @@ An implementation change is complete only when it:
|
||||
corresponding user guide; and
|
||||
6. updates the overview matrix without moving algorithm details back into the
|
||||
landing page.
|
||||
|
||||
## FastVideo VSA model metadata
|
||||
|
||||
FastVideo VSA support is scoped to `FastVideo/FastWan2.2-TI2V-5B-Diffusers`, whose text-to-video and image-to-video modes both use `Wan22Pipeline`. It operates on a flattened DiT sequence but partitions tokens in
|
||||
the original latent video grid. Wan integrations therefore attach the
|
||||
post-patch `(T, H, W)` shape as `vsa_dit_seq_shape` attention metadata. The
|
||||
backend validates that `T * H * W` equals the sequence length, derives the
|
||||
runtime block count, and selects the configured top-k key/value blocks for
|
||||
each query block.
|
||||
|
||||
Checkpoint-specific compensation remains model-owned. Wan layers pass an
|
||||
optional learned `gate_compress` projection through attention metadata when
|
||||
the checkpoint contains those weights. Checkpoints without the projection do
|
||||
not allocate or execute it. When top-k selects every block, native checkpoints
|
||||
route to SDPA, while FastVideo DMD checkpoints preserve the VSA all-block path
|
||||
to retain checkpoint semantics.
|
||||
@@ -20,6 +20,7 @@ For the internal selector, registry, and platform contract, see
|
||||
| Install and use SageAttention 2.2 or SageAttention3 | [SageAttention](attention_backends/sage.md) |
|
||||
| Match training or rollout kernels loaded from Hugging Face | [Hugging Face Hub Backends](attention_backends/huggingface_hub.md) |
|
||||
| Use block-sparse video attention on Ascend NPU | [RainFusion](attention_backends/rainfusion.md) |
|
||||
| Use FastVideo VSA with FastWan2.2-TI2V-5B on CUDA | [FastVideo VSA](attention_backends/fastvideo_vsa.md) |
|
||||
|
||||
## Backend options
|
||||
|
||||
@@ -35,6 +36,7 @@ For the internal selector, registry, and platform contract, see
|
||||
| `FLASH_ATTN_HUB` | Hub kernel | FlashAttention 2 from Hugging Face `kernels` | [Hugging Face Hub Backends](attention_backends/huggingface_hub.md) |
|
||||
| `FLASH_ATTN_3_HUB` | Hub kernel | FlashAttention 3 from Hugging Face `kernels` on Hopper or newer | [Hugging Face Hub Backends](attention_backends/huggingface_hub.md) |
|
||||
| `RAINFUSION_ATTN` | Block sparse | MindIE-SD RainFusion video attention on Ascend NPU | [RainFusion](attention_backends/rainfusion.md) |
|
||||
| `FASTVIDEO_VSA` | Block sparse | FastVideo variable sparse self-attention for FastWan2.2-TI2V-5B on CUDA | [FastVideo VSA](attention_backends/fastvideo_vsa.md) |
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -105,6 +107,7 @@ Backend-specific typed blocks are documented with their consumers:
|
||||
and [TRTLLM SAGE](attention_backends/trtllm.md#sage-quantization).
|
||||
- `skip_softmax`: [TRTLLM Skip-Softmax](attention_backends/trtllm.md#skip-softmax).
|
||||
- `block_sparse`: [RainFusion](attention_backends/rainfusion.md#configuration).
|
||||
- `fastvideo_vsa_topk`: [FastVideo VSA](attention_backends/fastvideo_vsa.md#choose-top-k).
|
||||
|
||||
## Platform defaults
|
||||
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# FastVideo VSA
|
||||
|
||||
FastVideo Variable Sparse Attention (VSA) accelerates the `FastVideo/FastWan2.2-TI2V-5B-Diffusers` model's
|
||||
self-attention by partitioning the post-patch latent grid into spatiotemporal
|
||||
blocks. For every query block, VSA scores the key/value blocks and computes
|
||||
attention only against the selected top-k blocks.
|
||||
|
||||
The supported checkpoint provides both text-to-video and image-to-video modes through `Wan22Pipeline`; the separate Wan I2V-14B, S2V, and VACE pipelines are outside this backend's supported scope.
|
||||
|
||||
VSA is a CUDA-only, explicitly selected backend. It requires the
|
||||
`fastvideo-kernel` package and currently supports non-causal self-attention
|
||||
with equal query and key/value sequence lengths. Unsupported shapes, masks,
|
||||
dtypes, sequence-parallel execution, or kernel failures fall back to
|
||||
`TORCH_SDPA` and emit a warning with the reason.
|
||||
|
||||
## Enable the backend
|
||||
|
||||
For online serving, select the backend with the existing attention backend
|
||||
flag. Use `--fastvideo-vsa-topk` to set the number of key/value blocks retained
|
||||
for every query block:
|
||||
|
||||
```bash
|
||||
vllm-omni serve <model> \
|
||||
--diffusion-attention-backend FASTVIDEO_VSA \
|
||||
--fastvideo-vsa-topk 64
|
||||
```
|
||||
|
||||
The backwards-compatible environment variable selects the backend with the
|
||||
default `topk=64`:
|
||||
|
||||
```bash
|
||||
export DIFFUSION_ATTENTION_BACKEND=FASTVIDEO_VSA
|
||||
vllm-omni serve <model>
|
||||
```
|
||||
|
||||
To tune top-k, pass the CLI backend and top-k flags together as shown above,
|
||||
or use the equivalent structured configuration:
|
||||
|
||||
```bash
|
||||
vllm-omni serve <model> \
|
||||
--diffusion-attention-config \
|
||||
'{"default":{"backend":"FASTVIDEO_VSA","fastvideo_vsa_topk":64}}'
|
||||
```
|
||||
|
||||
Do not combine `--diffusion-attention-backend` with an explicit
|
||||
`diffusion_attention_config.default.backend`. The top-k value must be positive
|
||||
and is valid only when the default backend is `FASTVIDEO_VSA`.
|
||||
|
||||
For a deploy YAML stage, use either the shorthand fields:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- stage_id: 0
|
||||
diffusion_attention_backend: FASTVIDEO_VSA
|
||||
fastvideo_vsa_topk: 64
|
||||
```
|
||||
|
||||
or the structured configuration:
|
||||
|
||||
```yaml
|
||||
stages:
|
||||
- stage_id: 0
|
||||
diffusion_attention_config:
|
||||
default:
|
||||
backend: FASTVIDEO_VSA
|
||||
fastvideo_vsa_topk: 64
|
||||
```
|
||||
|
||||
## Choose top-k
|
||||
|
||||
At runtime the backend logs the sequence shape and derived block count:
|
||||
|
||||
```text
|
||||
FASTVIDEO_VSA routing: seq_len=27280, dit_seq_shape=(31, 22, 40),
|
||||
block_size=(4, 8, 8), num_blocks=120, topk=64, keep_ratio=53.3%,
|
||||
checkpoint_mode=native, route=VSA
|
||||
```
|
||||
|
||||
Use `num_blocks` as the upper bound when tuning top-k. Top-k is the number of
|
||||
key/value blocks retained for each query block, not the number of tokens or
|
||||
the total number of blocks processed by the layer.
|
||||
|
||||
- A smaller top-k increases sparsity and may reduce attention computation, but
|
||||
it can remove relevant blocks and reduce visual quality or temporal
|
||||
consistency. Routing and padding overhead can also make a smaller value
|
||||
slower for some shapes.
|
||||
- A larger top-k retains more context and generally approaches dense-attention
|
||||
quality, but reduces the potential speedup and increases memory traffic.
|
||||
- `topk > num_blocks` is invalid for the runtime shape and falls back to SDPA.
|
||||
- For a native checkpoint, `topk == num_blocks` routes to SDPA because scoring
|
||||
every block provides no sparsity benefit.
|
||||
- For a FastVideo DMD checkpoint, `topk == num_blocks` stays on the VSA
|
||||
all-block path so the checkpoint keeps its trained compensation semantics.
|
||||
|
||||
There is no universally optimal value. Resolution, frame count, GPU, kernel
|
||||
version, and checkpoint all affect both quality and latency. Start from the
|
||||
logged `num_blocks`, test several keep ratios on the target workload, and
|
||||
compare output quality against the same checkpoint running dense attention.
|
||||
|
||||
## Checkpoint behavior
|
||||
|
||||
The backend does not expose a user-selectable gate mode. Wan checkpoints that
|
||||
contain learned `to_gate_compress` weights use that projection automatically.
|
||||
When those weights are absent, the unused projection is removed and VSA uses
|
||||
the sparse branch without learned compensation.
|
||||
|
||||
FastVideo DMD checkpoints use their fixed distilled timestep schedule. Native
|
||||
Wan checkpoints keep their normal scheduler and inference-step configuration;
|
||||
selecting VSA does not turn a native checkpoint into a distilled model.
|
||||
|
||||
## Verify routing and fallback
|
||||
|
||||
Check the startup and first-forward logs instead of assuming that selecting
|
||||
the backend guarantees sparse execution:
|
||||
|
||||
- `route=VSA` means top-k block selection is active.
|
||||
- `route=VSA_ALL_BLOCKS` means the FastVideo DMD checkpoint retained all
|
||||
blocks through the VSA kernel.
|
||||
- `route=SDPA` or `FASTVIDEO_VSA falling back to SDPA: ...` means dense SDPA
|
||||
executed; the warning includes the reason.
|
||||
|
||||
The current kernel requires CUDA tensors in FP16 or BF16, 256-token blocks,
|
||||
standard `head_size**-0.5` scaling, equal Q/K/V head counts, no attention mask,
|
||||
and no active sequence-parallel context. NPU and XPU paths do not execute the
|
||||
FastVideo VSA CUDA kernel.
|
||||
@@ -1304,6 +1304,7 @@ stages:
|
||||
({"model_type": "step_audio_2"}, None, "step_audio_2"),
|
||||
(None, {"_class_name": "HunyuanVideo15Pipeline"}, "hunyuan_video_15"),
|
||||
(None, {"_class_name": "WanPipeline"}, "wan2_2_ti2v"),
|
||||
(None, {"_class_name": "WanDMDPipeline"}, "wan2_2_ti2v"),
|
||||
],
|
||||
)
|
||||
def test_migrated_models_are_discovered_without_explicit_deploy(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Tests for the additive structured Omni config."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -1152,6 +1152,35 @@ def test_from_pipeline_config_normalizes_diffusion_config_aliases_from_engine_ar
|
||||
assert stage.diffusion_config.diffusion_kv_max_rows_per_request == 2
|
||||
|
||||
|
||||
def test_from_pipeline_config_forwards_fastvideo_vsa_topk(tmp_path, monkeypatch):
|
||||
from vllm_omni.platforms import current_omni_platform
|
||||
|
||||
monkeypatch.setattr(current_omni_platform, "is_cuda", lambda: True)
|
||||
deploy_path = tmp_path / "dreamzero_fastvideo_vsa.yaml"
|
||||
deploy_path.write_text(
|
||||
"\n".join(
|
||||
[
|
||||
"pipeline: dreamzero",
|
||||
"async_chunk: false",
|
||||
"stages:",
|
||||
" - stage_id: 0",
|
||||
" diffusion_attention_backend: FASTVIDEO_VSA",
|
||||
" fastvideo_vsa_topk: 96",
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
stage = _from_pipeline_key(
|
||||
"dreamzero",
|
||||
deploy_config_path=str(deploy_path),
|
||||
).stage_by_id(0)
|
||||
|
||||
attention_config = stage.diffusion_config.diffusion_attention_config
|
||||
assert attention_config.default is not None
|
||||
assert attention_config.default.backend == "FASTVIDEO_VSA"
|
||||
assert attention_config.default.fastvideo_vsa_topk == 96
|
||||
|
||||
|
||||
def test_from_pipeline_config_rejects_reserved_diffusion_kv_mode(tmp_path):
|
||||
deploy_path = tmp_path / "dreamzero_reserved_diffusion_kv.yaml"
|
||||
deploy_path.write_text(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""Tests for per-role attention backend configuration (RFC: per-role-attention-backend).
|
||||
|
||||
@@ -90,6 +90,14 @@ class TestAttentionSpec:
|
||||
with pytest.raises(ValueError, match=match):
|
||||
AttentionSpec(**spec)
|
||||
|
||||
def test_fastvideo_vsa_topk_serialized(self):
|
||||
spec = AttentionSpec(backend="FASTVIDEO_VSA", fastvideo_vsa_topk=96)
|
||||
assert spec.backend_kwargs() == {"topk": 96}
|
||||
|
||||
def test_fastvideo_vsa_topk_rejected_for_other_backend(self):
|
||||
with pytest.raises(ValueError, match="only supported by the FASTVIDEO_VSA"):
|
||||
AttentionSpec(backend="TORCH_SDPA", fastvideo_vsa_topk=96)
|
||||
|
||||
def test_block_sparse_defaults_applied_when_backend_selected(self):
|
||||
spec = AttentionSpec(backend="RAINFUSION_ATTN")
|
||||
assert spec.block_sparse.sparsity == 0.8
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import sys
|
||||
import types
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import AttentionMetadata
|
||||
from vllm_omni.diffusion.attention.backends.fastvideo_vsa import (
|
||||
FastVideoVSAImpl,
|
||||
)
|
||||
from vllm_omni.diffusion.attention.backends.registry import (
|
||||
DiffusionAttentionBackendEnum,
|
||||
)
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.diffusion, pytest.mark.cpu]
|
||||
|
||||
|
||||
def test_fastvideo_vsa_backend_is_registered():
|
||||
assert DiffusionAttentionBackendEnum.FASTVIDEO_VSA.get_path().endswith("fastvideo_vsa.FastVideoVSABackend")
|
||||
|
||||
|
||||
def test_fastvideo_vsa_tiles_3d_sequence_and_untiles(monkeypatch):
|
||||
calls: dict[str, Any] = {}
|
||||
fake_module = types.ModuleType("fastvideo_kernel")
|
||||
|
||||
def fake_video_sparse_attn_bshd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
variable_block_sizes,
|
||||
q_variable_block_sizes,
|
||||
compress_attn_weight,
|
||||
topk,
|
||||
block_size,
|
||||
):
|
||||
calls["q_shape"] = tuple(q.shape)
|
||||
calls["vbs"] = variable_block_sizes.detach().cpu().tolist()
|
||||
calls["q_vbs"] = q_variable_block_sizes.detach().cpu().tolist()
|
||||
calls["compress_sum"] = float(compress_attn_weight.detach().cpu().sum())
|
||||
calls["compress_shape"] = tuple(compress_attn_weight.shape)
|
||||
calls["topk"] = topk
|
||||
calls["block_size"] = block_size
|
||||
return q + k + v
|
||||
|
||||
setattr(fake_module, "video_sparse_attn_bshd", fake_video_sparse_attn_bshd)
|
||||
monkeypatch.setitem(sys.modules, "fastvideo_kernel", fake_module)
|
||||
|
||||
impl = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={
|
||||
"topk": 1,
|
||||
"block_size": (4, 8, 8),
|
||||
"min_seq_len": 1,
|
||||
"disable_when_sp_active": False,
|
||||
},
|
||||
)
|
||||
query = torch.randn(1, 300, 2, 8)
|
||||
key = torch.randn_like(query)
|
||||
value = torch.randn_like(query)
|
||||
attn_metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (3, 10, 10)})
|
||||
monkeypatch.setattr(impl, "_fallback_reason", lambda *args, **kwargs: None)
|
||||
|
||||
output = impl.forward_cuda(query, key, value, attn_metadata)
|
||||
|
||||
assert output.shape == query.shape
|
||||
assert calls["q_shape"] == (1, 1024, 2, 8)
|
||||
assert calls["vbs"] == [192, 48, 48, 12]
|
||||
assert calls["q_vbs"] == [192, 48, 48, 12]
|
||||
assert calls["compress_shape"] == (1, 1024, 2, 8)
|
||||
assert calls["compress_sum"] == 0.0
|
||||
assert calls["topk"] == 1
|
||||
assert calls["block_size"] == (4, 8, 8)
|
||||
|
||||
|
||||
def test_fastvideo_vsa_uses_learned_gate_when_provided(monkeypatch):
|
||||
calls: dict[str, Any] = {}
|
||||
fake_module = types.ModuleType("fastvideo_kernel")
|
||||
|
||||
def fake_video_sparse_attn_bshd(
|
||||
q,
|
||||
k,
|
||||
v,
|
||||
variable_block_sizes,
|
||||
q_variable_block_sizes,
|
||||
compress_attn_weight,
|
||||
topk,
|
||||
block_size,
|
||||
):
|
||||
calls["compress_sum"] = float(compress_attn_weight.detach().cpu().sum())
|
||||
calls["compress_shape"] = tuple(compress_attn_weight.shape)
|
||||
return q + k + v
|
||||
|
||||
setattr(fake_module, "video_sparse_attn_bshd", fake_video_sparse_attn_bshd)
|
||||
monkeypatch.setitem(sys.modules, "fastvideo_kernel", fake_module)
|
||||
|
||||
impl = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={
|
||||
"topk": 1,
|
||||
"block_size": (4, 8, 8),
|
||||
"min_seq_len": 1,
|
||||
"disable_when_sp_active": False,
|
||||
},
|
||||
)
|
||||
query = torch.randn(1, 300, 2, 8)
|
||||
gate = torch.ones_like(query)
|
||||
metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (3, 10, 10), "gate_compress": gate})
|
||||
monkeypatch.setattr(impl, "_fallback_reason", lambda *args, **kwargs: None)
|
||||
|
||||
output = impl.forward_cuda(query, query, query, metadata)
|
||||
|
||||
assert output.shape == query.shape
|
||||
assert calls["compress_shape"] == (1, 1024, 2, 8)
|
||||
# Only the 300 real tokens carry ones; padded gate slots stay zero.
|
||||
assert calls["compress_sum"] == gate.numel()
|
||||
|
||||
|
||||
def test_fastvideo_vsa_falls_back_without_dit_shape(monkeypatch):
|
||||
calls: dict[str, Any] = {}
|
||||
|
||||
impl = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={
|
||||
"topk": 1,
|
||||
"block_size": (4, 8, 8),
|
||||
"min_seq_len": 1,
|
||||
"disable_when_sp_active": False,
|
||||
},
|
||||
)
|
||||
|
||||
def fake_fallback(query, key, value, attn_metadata, reason):
|
||||
calls["reason"] = reason
|
||||
return torch.zeros_like(query)
|
||||
|
||||
monkeypatch.setattr(impl, "_fallback", fake_fallback)
|
||||
|
||||
query = torch.randn(1, 512, 2, 8)
|
||||
output = impl.forward_cuda(query, query, query, AttentionMetadata())
|
||||
|
||||
assert output.shape == query.shape
|
||||
assert calls["reason"] == "vsa_dit_seq_shape metadata is required"
|
||||
|
||||
|
||||
def test_fastvideo_vsa_falls_back_for_mask(monkeypatch):
|
||||
calls: dict[str, Any] = {}
|
||||
|
||||
impl = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={"min_seq_len": 1},
|
||||
)
|
||||
|
||||
def fake_fallback(query, key, value, attn_metadata, reason):
|
||||
calls["reason"] = reason
|
||||
return torch.zeros_like(query)
|
||||
|
||||
monkeypatch.setattr(impl, "_fallback", fake_fallback)
|
||||
|
||||
query = torch.randn(1, 512, 2, 8)
|
||||
mask = torch.ones(1, 512, dtype=torch.bool)
|
||||
output = impl.forward_cuda(query, query, query, AttentionMetadata(attn_mask=mask))
|
||||
|
||||
assert output.shape == query.shape
|
||||
assert calls["reason"]
|
||||
|
||||
|
||||
def test_fastvideo_vsa_allows_topk_equal_to_num_blocks():
|
||||
query = torch.randn(1, 512, 2, 8)
|
||||
metadata = AttentionMetadata(extra={"vsa_dit_seq_shape": (4, 8, 16)})
|
||||
|
||||
all_blocks = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={"topk": 2, "block_size": (4, 8, 8), "min_seq_len": 1},
|
||||
)
|
||||
too_many = FastVideoVSAImpl(
|
||||
num_heads=2,
|
||||
head_size=8,
|
||||
softmax_scale=8**-0.5,
|
||||
causal=False,
|
||||
backend_kwargs={"topk": 3, "block_size": (4, 8, 8), "min_seq_len": 1},
|
||||
)
|
||||
|
||||
# CPU/float32 is rejected later, but k=N itself must not trigger fallback.
|
||||
assert all_blocks._fallback_reason(query, query, query, metadata) == "dtype torch.float32 is not supported"
|
||||
assert too_many._fallback_reason(query, query, query, metadata) == "topk 3 > num_blocks 2"
|
||||
@@ -1,6 +1,7 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import importlib
|
||||
from contextlib import contextmanager
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -9,6 +10,7 @@ import torch
|
||||
from torch import nn
|
||||
|
||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import Wan22Pipeline
|
||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanSelfAttention
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
from vllm_omni.diffusion.worker.request_batch import DiffusionRequestBatch
|
||||
from vllm_omni.inputs.data import OmniDiffusionSamplingParams
|
||||
@@ -82,6 +84,7 @@ def _make_pipeline() -> Wan22Pipeline:
|
||||
pipeline.vae_scale_factor_spatial = 8
|
||||
pipeline.boundary_ratio = 0.875
|
||||
pipeline.expand_timesteps = False
|
||||
pipeline.is_dmd = False
|
||||
pipeline._guidance_scale = None
|
||||
pipeline._guidance_scale_2 = None
|
||||
pipeline._num_timesteps = None
|
||||
@@ -94,7 +97,7 @@ def _make_pipeline() -> Wan22Pipeline:
|
||||
|
||||
|
||||
def _make_sampling(**overrides):
|
||||
values = {
|
||||
values: dict[str, object] = {
|
||||
"height": None,
|
||||
"width": None,
|
||||
"num_frames": 1,
|
||||
@@ -338,3 +341,125 @@ def test_diffuse_runs_prediction_and_scheduler_for_each_timestep() -> None:
|
||||
(3.0, 3, 28.0, False),
|
||||
]
|
||||
assert torch.equal(result, torch.full_like(latents, 10.0))
|
||||
|
||||
|
||||
class _StubDMDScheduler:
|
||||
def __init__(self) -> None:
|
||||
self.predict_clean_calls: list[tuple[float, float, float]] = []
|
||||
self.add_noise_calls: list[tuple[float, float, float]] = []
|
||||
|
||||
def predict_clean(self, model_output, sample, timestep):
|
||||
self.predict_clean_calls.append((float(model_output.mean()), float(sample.mean()), float(timestep)))
|
||||
return sample - model_output
|
||||
|
||||
def add_noise(self, clean_sample, noise, timestep):
|
||||
self.add_noise_calls.append((float(clean_sample.mean()), float(noise.mean()), float(timestep)))
|
||||
return clean_sample + 10.0
|
||||
|
||||
|
||||
def test_diffuse_dmd_predicts_clean_and_renoises_between_steps(monkeypatch) -> None:
|
||||
pipeline = _make_pipeline()
|
||||
pipeline.is_dmd = True
|
||||
pipeline.scheduler = _StubDMDScheduler()
|
||||
latents = torch.zeros((1, 1, 1, 1, 1), dtype=torch.float32)
|
||||
timesteps = torch.tensor([1000.0, 757.0, 522.0])
|
||||
|
||||
pipeline.predict_noise_maybe_with_cfg = lambda **kwargs: torch.ones_like(latents) # type: ignore[method-assign]
|
||||
monkeypatch.setattr(
|
||||
"vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2.randn_tensor",
|
||||
lambda *args, **kwargs: torch.full(args[0], 2.0, dtype=kwargs["dtype"]),
|
||||
)
|
||||
|
||||
result = pipeline.diffuse(
|
||||
latents=latents,
|
||||
timesteps=timesteps,
|
||||
prompt_embeds=torch.zeros(1, 8),
|
||||
negative_prompt_embeds=None,
|
||||
guidance_low=1.0,
|
||||
guidance_high=1.0,
|
||||
boundary_timestep=None,
|
||||
dtype=torch.float32,
|
||||
attention_kwargs={},
|
||||
generator=torch.Generator(device="cpu").manual_seed(1),
|
||||
)
|
||||
|
||||
assert pipeline.scheduler.predict_clean_calls == [
|
||||
(1.0, 0.0, 1000.0),
|
||||
(1.0, 9.0, 757.0),
|
||||
(1.0, 18.0, 522.0),
|
||||
]
|
||||
assert pipeline.scheduler.add_noise_calls == [
|
||||
(-1.0, 2.0, 757.0),
|
||||
(8.0, 2.0, 522.0),
|
||||
]
|
||||
torch.testing.assert_close(result, torch.tensor([[[[[17.0]]]]]))
|
||||
|
||||
|
||||
def _make_gate_loading_pipeline():
|
||||
pipeline = Wan22Pipeline.__new__(Wan22Pipeline)
|
||||
nn.Module.__init__(pipeline)
|
||||
gate = WanSelfAttention.__new__(WanSelfAttention)
|
||||
nn.Module.__init__(gate)
|
||||
gate.to_gate_compress = nn.Linear(1, 1)
|
||||
pipeline.gate_holder = gate
|
||||
return pipeline, gate
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("module_name", "class_name"),
|
||||
[
|
||||
("pipeline_wan2_2", "Wan22Pipeline"),
|
||||
("pipeline_wan2_2_i2v", "Wan22I2VPipeline"),
|
||||
("pipeline_wan2_2_s2v", "Wan22S2VPipeline"),
|
||||
("pipeline_wan2_2_vace", "Wan22VACEPipeline"),
|
||||
],
|
||||
)
|
||||
def test_wan_pipeline_loaders_share_optional_gate_cleanup(monkeypatch, module_name, class_name) -> None:
|
||||
module = importlib.import_module(f"vllm_omni.diffusion.models.wan2_2.{module_name}")
|
||||
pipeline_cls = getattr(module, class_name)
|
||||
pipeline = pipeline_cls.__new__(pipeline_cls)
|
||||
expected = {"loaded"}
|
||||
|
||||
def fake_loader(model, weights):
|
||||
assert model is pipeline
|
||||
assert list(weights) == [("weight", torch.ones(1))]
|
||||
return expected
|
||||
|
||||
monkeypatch.setattr(module, "load_wan_weights_with_optional_gate", fake_loader)
|
||||
|
||||
assert pipeline_cls.load_weights(pipeline, iter((("weight", torch.ones(1)),))) is expected
|
||||
|
||||
|
||||
def test_load_weights_removes_unloaded_vsa_gate(monkeypatch) -> None:
|
||||
pipeline, gate = _make_gate_loading_pipeline()
|
||||
|
||||
class _Loader:
|
||||
def __init__(self, model):
|
||||
del model
|
||||
|
||||
def load_weights(self, weights):
|
||||
return {name for name, _ in weights}
|
||||
|
||||
monkeypatch.setattr("vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2.AutoWeightsLoader", _Loader)
|
||||
pipeline.load_weights(iter((("other.weight", torch.ones(1)),)))
|
||||
|
||||
assert pipeline.has_gate_compress_weights is False
|
||||
assert gate.to_gate_compress is None
|
||||
|
||||
|
||||
def test_load_weights_keeps_trained_vsa_gate(monkeypatch) -> None:
|
||||
pipeline, gate = _make_gate_loading_pipeline()
|
||||
original_gate = gate.to_gate_compress
|
||||
|
||||
class _Loader:
|
||||
def __init__(self, model):
|
||||
del model
|
||||
|
||||
def load_weights(self, weights):
|
||||
return {name for name, _ in weights}
|
||||
|
||||
monkeypatch.setattr("vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2.AutoWeightsLoader", _Loader)
|
||||
pipeline.load_weights(iter((("gate_holder.to_gate_compress.weight", torch.ones(1)),)))
|
||||
|
||||
assert pipeline.has_gate_compress_weights is True
|
||||
assert gate.to_gate_compress is original_gate
|
||||
@@ -0,0 +1,32 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
from vllm_omni.diffusion.models.wan2_2.scheduling_wan_euler import WanEulerScheduler
|
||||
|
||||
pytestmark = [pytest.mark.core_model, pytest.mark.cpu, pytest.mark.diffusion]
|
||||
|
||||
|
||||
def test_dmd_predict_clean_and_add_noise_match_flow_formulas() -> None:
|
||||
scheduler = WanEulerScheduler(num_train_timesteps=1000, shift=8.0)
|
||||
timestep = torch.tensor(757.0)
|
||||
sigma = scheduler.sigma_for_timestep(timestep)
|
||||
sample = torch.tensor([2.0, -1.0])
|
||||
model_output = torch.tensor([0.5, 0.25])
|
||||
noise = torch.tensor([-0.5, 1.5])
|
||||
|
||||
clean = scheduler.predict_clean(model_output, sample, timestep)
|
||||
expected_clean = sample - sigma * model_output
|
||||
torch.testing.assert_close(clean, expected_clean)
|
||||
|
||||
renoised = scheduler.add_noise(clean, noise, timestep)
|
||||
expected_renoised = (1.0 - sigma) * expected_clean + sigma * noise
|
||||
torch.testing.assert_close(renoised, expected_renoised)
|
||||
|
||||
|
||||
def test_dmd_sigma_requires_scalar_timestep() -> None:
|
||||
scheduler = WanEulerScheduler(num_train_timesteps=1000, shift=8.0)
|
||||
with pytest.raises(ValueError, match="scalar timestep"):
|
||||
scheduler.sigma_for_timestep(torch.tensor([757.0, 522.0]))
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
@@ -400,7 +400,9 @@ def test_serve_cli_accepts_diffusion_attention_backend():
|
||||
"Qwen/Qwen-Image",
|
||||
"--omni",
|
||||
"--diffusion-attention-backend",
|
||||
"FLASH_ATTN",
|
||||
"FASTVIDEO_VSA",
|
||||
"--fastvideo-vsa-topk",
|
||||
"96",
|
||||
]
|
||||
)
|
||||
|
||||
@@ -408,10 +410,12 @@ def test_serve_cli_accepts_diffusion_attention_backend():
|
||||
stage_cfg = AsyncOmniEngine._create_default_diffusion_stage_cfg(explicit_kwargs)[0]
|
||||
diffusion_attention_config = stage_cfg["engine_args"]["diffusion_attention_config"]
|
||||
|
||||
assert args.diffusion_attention_backend == "FLASH_ATTN"
|
||||
assert args.diffusion_attention_backend == "FASTVIDEO_VSA"
|
||||
assert args.fastvideo_vsa_topk == 96
|
||||
assert isinstance(diffusion_attention_config, AttentionConfig)
|
||||
assert diffusion_attention_config.default is not None
|
||||
assert diffusion_attention_config.default.backend == "FLASH_ATTN"
|
||||
assert diffusion_attention_config.default.backend == "FASTVIDEO_VSA"
|
||||
assert diffusion_attention_config.default.backend_kwargs() == {"topk": 96}
|
||||
|
||||
|
||||
def test_serve_cli_accepts_request_batch_max_wait_ms():
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Config factories for vllm-omni, e.g., StageConfigFactory."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -226,7 +226,10 @@ class StageConfigFactory:
|
||||
# If we have a resolver, call it with the optional hf_config
|
||||
# to get the default pipeline config for this key
|
||||
pipeline_cfg = obj(hf_config) if callable(obj) else obj
|
||||
if pipeline_cfg is not None and pipeline_cfg.diffusers_class_name == class_name:
|
||||
if pipeline_cfg is not None and class_name in (
|
||||
pipeline_cfg.diffusers_class_name,
|
||||
*pipeline_cfg.diffusers_class_aliases,
|
||||
):
|
||||
logger.info(
|
||||
"Detected pipeline %r from model_index.json (_class_name=%r)",
|
||||
pipeline_cfg.model_type,
|
||||
@@ -433,6 +436,7 @@ class StageConfigFactory:
|
||||
load-balancer policy (``None`` when no strategy set one) travels with the
|
||||
stages instead of through a mutable out-param.
|
||||
"""
|
||||
deploy_cfg: DeployConfig | None
|
||||
if user_deploy_config is not None:
|
||||
deploy_cfg = user_deploy_config
|
||||
elif deploy_config_path is not None:
|
||||
@@ -440,9 +444,12 @@ class StageConfigFactory:
|
||||
assert deploy_cfg is not None
|
||||
elif pipeline_cfg.default_deploy_config_name is not None:
|
||||
deploy_cfg = load_deploy_config(_DEPLOY_DIR / pipeline_cfg.default_deploy_config_name)
|
||||
assert deploy_cfg is not None
|
||||
else:
|
||||
deploy_cfg = DeployConfig()
|
||||
|
||||
assert deploy_cfg is not None
|
||||
|
||||
cli_async_chunk = cli_overrides.get("async_chunk")
|
||||
if cli_async_chunk is not None:
|
||||
deploy_cfg.async_chunk = bool(cli_async_chunk)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Structured vLLM-Omni configuration classes.
|
||||
|
||||
This module is additive for Phase 2 of RFC #4021.
|
||||
@@ -126,7 +126,7 @@ def _enforce_keyword_only_init(cls: type[Any]) -> type[Any]:
|
||||
|
||||
cls.__init__ = keyword_only_init
|
||||
cls.__signature__ = keyword_only_signature
|
||||
cls.__match_args__ = ()
|
||||
setattr(cls, "__match_args__", ())
|
||||
return cls
|
||||
|
||||
|
||||
@@ -447,7 +447,7 @@ class OmniStageSchedulerConfig(_TrackExplicitConfigFields, VllmSchedulerConfig):
|
||||
max_num_seqs: int | None = Field(default=None, ge=1)
|
||||
max_num_batched_tokens: int | None = Field(default=None, ge=1)
|
||||
max_model_len: int | None = Field(default=None, ge=-1)
|
||||
is_encoder_decoder: InitVar[bool] = False
|
||||
is_encoder_decoder: InitVar[bool] = False # type: ignore[assignment]
|
||||
enable_chunked_prefill: bool | None = None
|
||||
async_scheduling: bool | None = None
|
||||
|
||||
@@ -519,8 +519,8 @@ class OmniStageParallelConfig(_TrackExplicitConfigFields, VllmParallelConfig):
|
||||
self.data_parallel_rank,
|
||||
self.data_parallel_rpc_port,
|
||||
self.worker_cls,
|
||||
self.all2all_backend,
|
||||
self.disable_custom_all_reduce,
|
||||
getattr(self, "all2all_backend", None),
|
||||
getattr(self, "disable_custom_all_reduce", None),
|
||||
)
|
||||
if self.data_parallel_size_local is not None and self.data_parallel_size_local > self.data_parallel_size:
|
||||
raise ValueError(
|
||||
@@ -741,7 +741,7 @@ class _DiffusionConfigProjection:
|
||||
from vllm_omni.diffusion.data import normalize_omni_diffusion_kwargs
|
||||
|
||||
normalized_kwargs = normalize_omni_diffusion_kwargs(kwargs)
|
||||
valid_fields = {f.name for f in fields(cls)}
|
||||
valid_fields = {f.name for f in fields(cast(Any, cls))}
|
||||
return cls(**{k: v for k, v in normalized_kwargs.items() if k in valid_fields})
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
@@ -893,7 +893,7 @@ class _DiffusionConfigProjection:
|
||||
setattr(self, name, _copy_value(getattr(omni_diffusion_config, name)))
|
||||
|
||||
|
||||
_DIFFUSION_CONFIG_FIELDS = frozenset(f.name for f in fields(_DiffusionConfigProjection))
|
||||
_DIFFUSION_CONFIG_FIELDS = frozenset(f.name for f in fields(cast(Any, _DiffusionConfigProjection)))
|
||||
|
||||
# Current OmniDiffusionConfig still contains a flat mix of shared engine,
|
||||
# runtime, parallel, and diffusion-specific knobs. Keep this classification
|
||||
@@ -956,6 +956,7 @@ _STAGE_DEPLOY_ENGINE_FIELDS: tuple[str, ...] = tuple(_STAGE_DEPLOY_FIELDS)
|
||||
_DIFFUSION_BACKCOMPAT_ENGINE_FIELDS = frozenset(
|
||||
{
|
||||
"diffusion_attention_backend",
|
||||
"fastvideo_vsa_topk",
|
||||
"kv_cache_dtype",
|
||||
"kv_cache_skip_layers",
|
||||
"kv_cache_skip_steps",
|
||||
@@ -1140,7 +1141,7 @@ def _global_stage_cli_fields() -> frozenset[str]:
|
||||
)
|
||||
externally_consumed = (
|
||||
_NON_STAGE_ENGINE_CLI_FIELDS
|
||||
| frozenset(f.name for f in fields(VllmOmniOrchestratorConfig))
|
||||
| frozenset(f.name for f in fields(cast(Any, VllmOmniOrchestratorConfig)))
|
||||
| (orchestrator_field_names() - _STAGE_ENGINE_FIELDS)
|
||||
)
|
||||
return candidates - externally_consumed
|
||||
@@ -1238,7 +1239,7 @@ def _stage_sampling_params(
|
||||
|
||||
def _orchestrator_cli_overrides(cli_overrides: Mapping[str, Any]) -> dict[str, Any]:
|
||||
overrides: dict[str, Any] = {}
|
||||
for config_field in fields(VllmOmniOrchestratorConfig):
|
||||
for config_field in fields(cast(Any, VllmOmniOrchestratorConfig)):
|
||||
name = config_field.name
|
||||
if name == "deploy_config_path":
|
||||
continue
|
||||
@@ -1443,8 +1444,8 @@ def _with_resolved_processors(
|
||||
input_proc: str | None,
|
||||
next_stage_proc: str | None,
|
||||
) -> StageConfigType:
|
||||
stage_config._resolved_custom_process_input_func = input_proc
|
||||
stage_config._resolved_custom_process_next_stage_input_func = next_stage_proc
|
||||
setattr(stage_config, "_resolved_custom_process_input_func", input_proc)
|
||||
setattr(stage_config, "_resolved_custom_process_next_stage_input_func", next_stage_proc)
|
||||
return stage_config
|
||||
|
||||
|
||||
@@ -1558,13 +1559,16 @@ def _build_stage_config(
|
||||
builder = _STAGE_CONFIG_BUILDERS[topology.execution_type]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"Unsupported stage execution type: {topology.execution_type!r}") from exc
|
||||
return builder(
|
||||
pipeline,
|
||||
deploy,
|
||||
topology,
|
||||
stage_deploy,
|
||||
engine,
|
||||
model=model,
|
||||
return cast(
|
||||
StageConfigType,
|
||||
builder(
|
||||
pipeline,
|
||||
deploy,
|
||||
topology,
|
||||
stage_deploy,
|
||||
engine,
|
||||
model=model,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -1608,7 +1612,7 @@ def _build_model_config(
|
||||
kwargs["model_subdir"] = topology.model_subdir
|
||||
if "tokenizer_subdir" not in kwargs and topology.tokenizer_subdir is not None:
|
||||
kwargs["tokenizer_subdir"] = topology.tokenizer_subdir
|
||||
return OmniStageModelConfig(
|
||||
return cast(Any, OmniStageModelConfig)(
|
||||
default_sampling_params=default_sampling_params,
|
||||
duplex_max_sessions=duplex_max_sessions,
|
||||
**kwargs,
|
||||
@@ -1699,7 +1703,7 @@ def _build_connector_config(
|
||||
) -> OmniStageConnectorConfig:
|
||||
output_connectors = stage_deploy.output_connectors if stage_deploy is not None else None
|
||||
input_connectors = stage_deploy.input_connectors if stage_deploy is not None else None
|
||||
return OmniStageConnectorConfig(
|
||||
return cast(Any, OmniStageConnectorConfig)(
|
||||
async_chunk=bool(deploy.async_chunk),
|
||||
omni_kv_config=_copy_value(engine.get("omni_kv_config")),
|
||||
output_connectors=_copy_value(output_connectors) if output_connectors else None,
|
||||
@@ -1848,11 +1852,11 @@ class VllmOmniConfig:
|
||||
for topology in pipeline_cfg.stages
|
||||
)
|
||||
|
||||
orchestrator_config = VllmOmniOrchestratorConfig(
|
||||
orchestrator_config = cast(Any, VllmOmniOrchestratorConfig)(
|
||||
deploy_config_path=loaded_deploy_config_path,
|
||||
**_orchestrator_cli_overrides(cli_overrides),
|
||||
)
|
||||
return cls(
|
||||
return cast(Any, cls)(
|
||||
pipeline_config=pipeline_cfg,
|
||||
stage_configs=stage_configs,
|
||||
orchestrator_config=orchestrator_config,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Stage configuration system for vLLM-Omni."""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -234,6 +234,7 @@ class PipelineConfig:
|
||||
# this value to auto-detect the pipeline. Only needed for diffusers-style
|
||||
# multi-component repos (e.g. GLM-Image). ``None`` = not a diffusers model.
|
||||
diffusers_class_name: str | None = None
|
||||
diffusers_class_aliases: tuple[str, ...] = ()
|
||||
endpoint_restrictions: tuple[EndpointRestriction, ...] = ()
|
||||
# Optional model-owned duplex planner loaded by the stable engine runtime.
|
||||
duplex_runtime_extension: str | None = None
|
||||
@@ -356,6 +357,7 @@ class StageDeployConfig:
|
||||
diffusers_call_kwargs: dict[str, Any] | None = None
|
||||
diffusion_quantization_config: str | None = None
|
||||
diffusion_attention_backend: str | None = None
|
||||
fastvideo_vsa_topk: int | None = None
|
||||
diffusion_attention_config: dict[str, Any] | None = None
|
||||
|
||||
# Diffusion execution, cache, and VAE behavior.
|
||||
|
||||
@@ -0,0 +1,431 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
import math
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
|
||||
from vllm_omni.diffusion.attention.backends.abstract import (
|
||||
AttentionBackend,
|
||||
AttentionImpl,
|
||||
AttentionMetadata,
|
||||
)
|
||||
from vllm_omni.diffusion.attention.backends.sdpa import SDPAImpl
|
||||
from vllm_omni.diffusion.forward_context import get_forward_context, is_forward_context_available
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
# Keep the external FastVideo pybind/CUDA kernel opaque to torch.compile.
|
||||
# This mirrors the SageAttention3 backend pattern: tracing the raw extension
|
||||
# through Dynamo can reach Inductor scheduling with unstable internal op names
|
||||
# (e.g. KeyError: "op12"). The custom op gives Dynamo a single Tensor->Tensor
|
||||
# boundary and lets Inductor schedule the surrounding Wan block normally.
|
||||
if not hasattr(torch.ops.vllm_omni, "fastvideo_vsa_bshd"):
|
||||
|
||||
@torch.library.custom_op("vllm_omni::fastvideo_vsa_bshd", mutates_args=())
|
||||
def _fastvideo_vsa_bshd_op(
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
variable_block_sizes: torch.Tensor,
|
||||
q_variable_block_sizes: torch.Tensor,
|
||||
compress_attn_weight: torch.Tensor,
|
||||
topk: int,
|
||||
block_t: int,
|
||||
block_h: int,
|
||||
block_w: int,
|
||||
) -> torch.Tensor:
|
||||
from fastvideo_kernel import video_sparse_attn_bshd
|
||||
|
||||
return video_sparse_attn_bshd(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
variable_block_sizes=variable_block_sizes,
|
||||
q_variable_block_sizes=q_variable_block_sizes,
|
||||
topk=topk,
|
||||
block_size=(block_t, block_h, block_w),
|
||||
compress_attn_weight=compress_attn_weight if compress_attn_weight.numel() else None,
|
||||
)
|
||||
|
||||
@_fastvideo_vsa_bshd_op.register_fake
|
||||
def _(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
variable_block_sizes,
|
||||
q_variable_block_sizes,
|
||||
compress_attn_weight,
|
||||
topk,
|
||||
block_t,
|
||||
block_h,
|
||||
block_w,
|
||||
):
|
||||
del (
|
||||
key,
|
||||
value,
|
||||
variable_block_sizes,
|
||||
q_variable_block_sizes,
|
||||
compress_attn_weight,
|
||||
topk,
|
||||
block_t,
|
||||
block_h,
|
||||
block_w,
|
||||
)
|
||||
return torch.empty_like(query)
|
||||
|
||||
|
||||
_fastvideo_vsa_bshd_op = torch.ops.vllm_omni.fastvideo_vsa_bshd
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def _get_tile_partition_indices(
|
||||
dit_seq_shape: tuple[int, int, int],
|
||||
tile_size: tuple[int, int, int],
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
t_size, h_size, w_size = dit_seq_shape
|
||||
tile_t, tile_h, tile_w = tile_size
|
||||
indices = torch.arange(t_size * h_size * w_size, device=device, dtype=torch.long).reshape(t_size, h_size, w_size)
|
||||
tiles = []
|
||||
for tile_t_idx in range(math.ceil(t_size / tile_t)):
|
||||
for tile_h_idx in range(math.ceil(h_size / tile_h)):
|
||||
for tile_w_idx in range(math.ceil(w_size / tile_w)):
|
||||
tiles.append(
|
||||
indices[
|
||||
tile_t_idx * tile_t : min((tile_t_idx + 1) * tile_t, t_size),
|
||||
tile_h_idx * tile_h : min((tile_h_idx + 1) * tile_h, h_size),
|
||||
tile_w_idx * tile_w : min((tile_w_idx + 1) * tile_w, w_size),
|
||||
].flatten()
|
||||
)
|
||||
return torch.cat(tiles, dim=0)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def _construct_variable_block_sizes(
|
||||
dit_seq_shape: tuple[int, int, int],
|
||||
tile_size: tuple[int, int, int],
|
||||
device: torch.device,
|
||||
) -> torch.Tensor:
|
||||
num_tiles = tuple(math.ceil(seq_dim / tile_dim) for seq_dim, tile_dim in zip(dit_seq_shape, tile_size))
|
||||
|
||||
def _sizes(dim_len: int, tile: int, n_tiles: int) -> torch.Tensor:
|
||||
sizes = torch.full((n_tiles,), tile, dtype=torch.int32, device=device)
|
||||
remainder = dim_len - (n_tiles - 1) * tile
|
||||
sizes[-1] = remainder if remainder > 0 else tile
|
||||
return sizes
|
||||
|
||||
t_sizes = _sizes(dit_seq_shape[0], tile_size[0], num_tiles[0])
|
||||
h_sizes = _sizes(dit_seq_shape[1], tile_size[1], num_tiles[1])
|
||||
w_sizes = _sizes(dit_seq_shape[2], tile_size[2], num_tiles[2])
|
||||
return (t_sizes[:, None, None] * h_sizes[None, :, None] * w_sizes[None, None, :]).reshape(-1)
|
||||
|
||||
|
||||
@functools.lru_cache(maxsize=32)
|
||||
def _get_non_pad_index(variable_block_sizes: torch.Tensor, max_block_size: int) -> torch.Tensor:
|
||||
num_blocks = variable_block_sizes.shape[0]
|
||||
device = variable_block_sizes.device
|
||||
starts = torch.arange(num_blocks, device=device) * max_block_size
|
||||
padded_index = starts[:, None] + torch.arange(max_block_size, device=device)[None, :]
|
||||
valid = torch.arange(max_block_size, device=device)[None, :] < variable_block_sizes[:, None]
|
||||
return padded_index[valid]
|
||||
|
||||
|
||||
@torch.compiler.disable
|
||||
def _get_tile_metadata(
|
||||
dit_seq_shape: tuple[int, int, int],
|
||||
tile_size: tuple[int, int, int],
|
||||
block_elements: int,
|
||||
device: torch.device,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
tile_partition_indices = _get_tile_partition_indices(dit_seq_shape, tile_size, device)
|
||||
variable_block_sizes = _construct_variable_block_sizes(dit_seq_shape, tile_size, device)
|
||||
non_pad_index = _get_non_pad_index(variable_block_sizes, block_elements)
|
||||
untile_combined_index = non_pad_index[torch.argsort(tile_partition_indices)]
|
||||
return tile_partition_indices, variable_block_sizes, non_pad_index, untile_combined_index
|
||||
|
||||
|
||||
def _get_vsa_dit_seq_shape(attn_metadata: AttentionMetadata | None) -> tuple[int, int, int] | None:
|
||||
if attn_metadata is None:
|
||||
return None
|
||||
value = attn_metadata.extra.get("vsa_dit_seq_shape")
|
||||
if value is None:
|
||||
return None
|
||||
if not isinstance(value, (list, tuple)) or len(value) != 3:
|
||||
return None
|
||||
return (int(value[0]), int(value[1]), int(value[2]))
|
||||
|
||||
|
||||
def _get_gate_compress(attn_metadata: AttentionMetadata | None) -> torch.Tensor | None:
|
||||
if attn_metadata is None:
|
||||
return None
|
||||
value = attn_metadata.extra.get("gate_compress")
|
||||
return value if isinstance(value, torch.Tensor) else None
|
||||
|
||||
|
||||
def _preserve_vsa_all_blocks(attn_metadata: AttentionMetadata | None) -> bool:
|
||||
if attn_metadata is None:
|
||||
return False
|
||||
return attn_metadata.extra.get("preserve_vsa_all_blocks") is True
|
||||
|
||||
|
||||
class FastVideoVSABackend(AttentionBackend):
|
||||
accept_output_buffer: bool = True
|
||||
|
||||
@staticmethod
|
||||
def get_supported_head_sizes() -> list[int]:
|
||||
# FastVideo VSA is intended for video DiT head sizes such as 64/128.
|
||||
# Keep this permissive and let the runtime fallback handle unsupported
|
||||
# cases from the installed fastvideo-kernel build.
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def get_name() -> str:
|
||||
return "FASTVIDEO_VSA"
|
||||
|
||||
@staticmethod
|
||||
def get_impl_cls() -> type[FastVideoVSAImpl]:
|
||||
return FastVideoVSAImpl
|
||||
|
||||
|
||||
class FastVideoVSAImpl(AttentionImpl):
|
||||
def __init__(
|
||||
self,
|
||||
num_heads: int,
|
||||
head_size: int,
|
||||
softmax_scale: float,
|
||||
causal: bool = False,
|
||||
num_kv_heads: int | None = None,
|
||||
prefix: str = "",
|
||||
qkv_layout: str | None = None,
|
||||
backend_kwargs: Mapping[str, Any] | None = None,
|
||||
**extra_impl_args,
|
||||
) -> None:
|
||||
backend_kwargs = backend_kwargs or {}
|
||||
self.num_heads = num_heads
|
||||
self.num_kv_heads = num_kv_heads if num_kv_heads is not None else num_heads
|
||||
self.head_size = head_size
|
||||
self.softmax_scale = softmax_scale
|
||||
self.causal = causal
|
||||
self.qkv_layout = qkv_layout
|
||||
|
||||
self.topk = int(backend_kwargs.get("topk", 64))
|
||||
self.block_size = self._parse_block_size(backend_kwargs.get("block_size", (4, 8, 8)))
|
||||
self.block_elements = self.block_size[0] * self.block_size[1] * self.block_size[2]
|
||||
self.min_seq_len = int(backend_kwargs.get("min_seq_len", self.block_elements * 2))
|
||||
self.fallback_on_error = bool(backend_kwargs.get("fallback_on_error", True))
|
||||
self.disable_when_sp_active = bool(backend_kwargs.get("disable_when_sp_active", True))
|
||||
|
||||
self.sdpa_fallback = SDPAImpl(
|
||||
num_heads=num_heads,
|
||||
head_size=head_size,
|
||||
softmax_scale=softmax_scale,
|
||||
causal=causal,
|
||||
num_kv_heads=num_kv_heads,
|
||||
qkv_layout=qkv_layout,
|
||||
)
|
||||
|
||||
if self.block_elements != 256:
|
||||
logger.warning(
|
||||
"FASTVIDEO_VSA currently uses fastvideo_kernel.video_sparse_attn_bshd, "
|
||||
"which supports only 256-token blocks. Configured block_size=%s "
|
||||
"(product=%d) will fall back to SDPA.",
|
||||
self.block_size,
|
||||
self.block_elements,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _parse_block_size(value: Any) -> tuple[int, int, int]:
|
||||
if isinstance(value, int):
|
||||
return (value, value, value)
|
||||
if isinstance(value, (list, tuple)) and len(value) == 3:
|
||||
return (int(value[0]), int(value[1]), int(value[2]))
|
||||
raise ValueError(f"FASTVIDEO_VSA block_size must be an int or length-3 tuple/list, got {value!r}")
|
||||
|
||||
def _fallback(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None,
|
||||
reason: str,
|
||||
) -> torch.Tensor:
|
||||
logger.warning_once("FASTVIDEO_VSA falling back to SDPA: %s", reason)
|
||||
return self.sdpa_fallback.forward(query, key, value, attn_metadata)
|
||||
|
||||
def _fallback_reason(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None,
|
||||
) -> str | None:
|
||||
if self.causal:
|
||||
return "causal attention is not supported"
|
||||
if self.block_elements != 256:
|
||||
return f"block_elements must be 256, got {self.block_elements}"
|
||||
if self.topk <= 0:
|
||||
return f"topk must be positive, got {self.topk}"
|
||||
if query.ndim != 4 or key.ndim != 4 or value.ndim != 4:
|
||||
return f"expected [B, S, H, D] tensors, got {query.shape}, {key.shape}, {value.shape}"
|
||||
if query.shape[0] != key.shape[0] or query.shape[0] != value.shape[0]:
|
||||
return "batch dimensions must match"
|
||||
if query.shape[2:] != key.shape[2:] or query.shape[2:] != value.shape[2:]:
|
||||
return "head/head_dim dimensions must match"
|
||||
if query.shape[1] != key.shape[1] or query.shape[1] != value.shape[1]:
|
||||
return "initial VSA backend supports self-attention with Sq == Skv only"
|
||||
if query.shape[1] < self.min_seq_len:
|
||||
return f"sequence length {query.shape[1]} is below min_seq_len {self.min_seq_len}"
|
||||
dit_seq_shape = _get_vsa_dit_seq_shape(attn_metadata)
|
||||
if dit_seq_shape is None:
|
||||
return "vsa_dit_seq_shape metadata is required"
|
||||
if math.prod(dit_seq_shape) != query.shape[1]:
|
||||
return f"vsa_dit_seq_shape product {math.prod(dit_seq_shape)} != sequence length {query.shape[1]}"
|
||||
num_blocks = math.prod(
|
||||
math.ceil(seq_dim / tile_dim) for seq_dim, tile_dim in zip(dit_seq_shape, self.block_size)
|
||||
)
|
||||
if self.topk > num_blocks:
|
||||
return f"topk {self.topk} > num_blocks {num_blocks}"
|
||||
if query.dtype not in (torch.float16, torch.bfloat16):
|
||||
return f"dtype {query.dtype} is not supported"
|
||||
if key.dtype != query.dtype or value.dtype != query.dtype:
|
||||
return "q/k/v dtypes must match"
|
||||
if query.device.type != "cuda" or key.device.type != "cuda" or value.device.type != "cuda":
|
||||
return "q/k/v must be CUDA tensors"
|
||||
expected_scale = self.head_size**-0.5
|
||||
if abs(float(self.softmax_scale) - float(expected_scale)) > 1e-6:
|
||||
return f"softmax_scale {self.softmax_scale} differs from FastVideo VSA scale {expected_scale}"
|
||||
if attn_metadata is not None and attn_metadata.attn_mask is not None:
|
||||
return "attention masks are not supported"
|
||||
if attn_metadata is not None and attn_metadata.full_attn_spans is not None:
|
||||
return "piecewise/full attention spans are not supported"
|
||||
if self.num_heads != self.num_kv_heads:
|
||||
return "GQA/MQA is not supported"
|
||||
if self.disable_when_sp_active and is_forward_context_available():
|
||||
ctx = get_forward_context()
|
||||
if getattr(ctx, "sp_active", False):
|
||||
return "sequence parallel context is active"
|
||||
return None
|
||||
|
||||
def forward_cuda(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> torch.Tensor:
|
||||
reason = self._fallback_reason(query, key, value, attn_metadata)
|
||||
if reason is not None:
|
||||
return self._fallback(query, key, value, attn_metadata, reason)
|
||||
|
||||
seq_len = query.shape[1]
|
||||
dit_seq_shape = _get_vsa_dit_seq_shape(attn_metadata)
|
||||
assert dit_seq_shape is not None
|
||||
num_blocks = math.prod(
|
||||
math.ceil(seq_dim / tile_dim) for seq_dim, tile_dim in zip(dit_seq_shape, self.block_size)
|
||||
)
|
||||
preserve_all_blocks = _preserve_vsa_all_blocks(attn_metadata)
|
||||
use_native_sdpa = self.topk == num_blocks and not preserve_all_blocks
|
||||
route = "SDPA" if use_native_sdpa else "VSA_ALL_BLOCKS" if self.topk == num_blocks else "VSA"
|
||||
checkpoint_mode = "fastvideo_dmd" if preserve_all_blocks else "native"
|
||||
logger.info_once(
|
||||
"FASTVIDEO_VSA routing: seq_len=%d, dit_seq_shape=%s, block_size=%s, num_blocks=%d, "
|
||||
"topk=%d, keep_ratio=%.1f%%, checkpoint_mode=%s, route=%s",
|
||||
seq_len,
|
||||
dit_seq_shape,
|
||||
self.block_size,
|
||||
num_blocks,
|
||||
self.topk,
|
||||
100.0 * self.topk / num_blocks,
|
||||
checkpoint_mode,
|
||||
route,
|
||||
)
|
||||
if use_native_sdpa:
|
||||
return self._fallback(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
attn_metadata,
|
||||
f"topk {self.topk} selects all blocks for a native checkpoint",
|
||||
)
|
||||
|
||||
try:
|
||||
tile_partition_indices, variable_block_sizes, non_pad_index, untile_combined_index = _get_tile_metadata(
|
||||
dit_seq_shape,
|
||||
self.block_size,
|
||||
self.block_elements,
|
||||
query.device,
|
||||
)
|
||||
|
||||
padded_len = variable_block_sizes.numel() * self.block_elements
|
||||
target_shape = (query.shape[0], padded_len, query.shape[2], query.shape[3])
|
||||
query_tiled = torch.zeros(target_shape, device=query.device, dtype=query.dtype)
|
||||
key_tiled = torch.zeros_like(query_tiled)
|
||||
value_tiled = torch.zeros_like(query_tiled)
|
||||
query_tiled[:, non_pad_index] = query[:, tile_partition_indices]
|
||||
key_tiled[:, non_pad_index] = key[:, tile_partition_indices]
|
||||
value_tiled[:, non_pad_index] = value[:, tile_partition_indices]
|
||||
# Gate behavior is checkpoint-driven, not user-configured.
|
||||
# Wan VSA layers always provide a gate projection. Its zero
|
||||
# initialization makes checkpoints without gate weights sparse-only;
|
||||
# checkpoints containing to_gate_compress weights use the learned gate.
|
||||
gate_compress = _get_gate_compress(attn_metadata)
|
||||
if gate_compress is None:
|
||||
gate_compress = torch.zeros_like(query)
|
||||
elif gate_compress.shape != query.shape:
|
||||
raise ValueError(f"gate_compress shape {gate_compress.shape} must match query shape {query.shape}")
|
||||
gate_tiled = torch.zeros_like(query_tiled)
|
||||
gate_tiled[:, non_pad_index] = gate_compress[:, tile_partition_indices]
|
||||
compress_attn_weight = gate_tiled
|
||||
|
||||
output = _fastvideo_vsa_bshd_op(
|
||||
query_tiled.contiguous(),
|
||||
key_tiled.contiguous(),
|
||||
value_tiled.contiguous(),
|
||||
variable_block_sizes,
|
||||
variable_block_sizes,
|
||||
compress_attn_weight,
|
||||
self.topk,
|
||||
self.block_size[0],
|
||||
self.block_size[1],
|
||||
self.block_size[2],
|
||||
)
|
||||
return output[:, untile_combined_index].contiguous()
|
||||
except Exception as exc:
|
||||
if not self.fallback_on_error:
|
||||
raise
|
||||
return self._fallback(query, key, value, attn_metadata, f"VSA kernel failed: {exc}")
|
||||
|
||||
def forward_npu(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> torch.Tensor:
|
||||
return self.sdpa_fallback.forward_npu(query, key, value, attn_metadata)
|
||||
|
||||
def forward_xpu(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> torch.Tensor:
|
||||
return self.sdpa_fallback.forward_xpu(query, key, value, attn_metadata)
|
||||
|
||||
def forward_musa(
|
||||
self,
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
value: torch.Tensor,
|
||||
attn_metadata: AttentionMetadata | None = None,
|
||||
) -> torch.Tensor:
|
||||
return self.sdpa_fallback.forward_musa(query, key, value, attn_metadata)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Diffusion attention backend registry.
|
||||
|
||||
This module provides an enum-based registry for diffusion attention backends,
|
||||
@@ -23,7 +23,7 @@ logger = init_logger(__name__)
|
||||
class _DiffusionBackendEnumMeta(EnumMeta):
|
||||
"""Metaclass for DiffusionAttentionBackendEnum to provide better error messages."""
|
||||
|
||||
def __getitem__(cls, name: str) -> "DiffusionAttentionBackendEnum":
|
||||
def __getitem__(cls, name: str) -> "DiffusionAttentionBackendEnum": # type: ignore[override]
|
||||
"""Get backend by name with helpful error messages."""
|
||||
try:
|
||||
return super().__getitem__(name) # type: ignore[return-value]
|
||||
@@ -62,6 +62,7 @@ class DiffusionAttentionBackendEnum(Enum, metaclass=_DiffusionBackendEnumMeta):
|
||||
SAGE_ATTN_3 = "vllm_omni.diffusion.attention.backends.sage_attn3.SageAttention3Backend"
|
||||
CUDNN_ATTN = "vllm_omni.diffusion.attention.backends.cudnn_attn.CuDNNAttentionBackend"
|
||||
FLASHINFER_ATTN = "vllm_omni.diffusion.attention.backends.flashinfer_attn.FlashInferAttentionBackend"
|
||||
FASTVIDEO_VSA = "vllm_omni.diffusion.attention.backends.fastvideo_vsa.FastVideoVSABackend"
|
||||
FLASH_ATTN_HUB = "vllm_omni.diffusion.attention.backends.flash_attn_hub.FlashAttentionHubBackend"
|
||||
FLASH_ATTN_3_HUB = "vllm_omni.diffusion.attention.backends.flash_attn_hub.FlashAttention3HubBackend"
|
||||
TRTLLM_ATTN = "vllm_omni.diffusion.attention.backends.trtllm_attn.TrtllmAttentionBackend"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# adapted from sglang and fastvideo
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
import copy
|
||||
import math
|
||||
import os
|
||||
@@ -77,11 +77,13 @@ def normalize_omni_diffusion_kwargs(kwargs: Mapping[str, Any]) -> dict[str, Any]
|
||||
# Handle "diffusion_attention_backend" shorthand: merge into
|
||||
# diffusion_attention_config before field filtering.
|
||||
diffusion_attn_backend = normalized.pop("diffusion_attention_backend", None)
|
||||
if diffusion_attn_backend is not None:
|
||||
fastvideo_vsa_topk = normalized.pop("fastvideo_vsa_topk", None)
|
||||
if diffusion_attn_backend is not None or fastvideo_vsa_topk is not None:
|
||||
existing = normalized.get("diffusion_attention_config")
|
||||
normalized["diffusion_attention_config"] = parse_attention_config(
|
||||
existing,
|
||||
attention_backend=diffusion_attn_backend,
|
||||
fastvideo_vsa_topk=fastvideo_vsa_topk,
|
||||
)
|
||||
|
||||
# Check environment variable as fallback for cache_backend.
|
||||
@@ -104,6 +106,7 @@ def parse_kv_cache_skip_selector(
|
||||
"""Parse a non-negative index selector such as "0-9,20,25-30"."""
|
||||
if selector is None:
|
||||
return None
|
||||
values: set[int]
|
||||
if isinstance(selector, set):
|
||||
values = selector
|
||||
elif isinstance(selector, (list, tuple)):
|
||||
@@ -112,7 +115,7 @@ def parse_kv_cache_skip_selector(
|
||||
text = selector.strip()
|
||||
if not text:
|
||||
return None
|
||||
values: set[int] = set()
|
||||
values = set()
|
||||
for chunk in text.split(","):
|
||||
token = chunk.strip()
|
||||
if not token:
|
||||
@@ -245,6 +248,7 @@ class DiffusionParallelConfig:
|
||||
assert self.pipeline_parallel_size > 0, "Pipeline parallel size must be > 0"
|
||||
assert self.data_parallel_size is None or self.data_parallel_size > 0, "Data parallel size must be > 0"
|
||||
assert self.tensor_parallel_size > 0, "Tensor parallel size must be > 0"
|
||||
assert self.sequence_parallel_size is not None
|
||||
assert self.sequence_parallel_size > 0, "Sequence parallel size must be > 0"
|
||||
assert self.ulysses_degree > 0, "Ulysses degree must be > 0"
|
||||
assert self.ring_degree > 0, "Ring degree must be > 0"
|
||||
@@ -362,6 +366,7 @@ class DiffusionParallelConfig:
|
||||
self.world_size = world_size
|
||||
return 1
|
||||
|
||||
assert self.sequence_parallel_size is not None
|
||||
non_dp_size = (
|
||||
self.pipeline_parallel_size
|
||||
* self.tensor_parallel_size
|
||||
@@ -1063,6 +1068,7 @@ class OmniDiffusionConfig:
|
||||
|
||||
if self.diffusion_compile_granularity == "full":
|
||||
incompatible_features = []
|
||||
assert self.parallel_config.sequence_parallel_size is not None
|
||||
if self.parallel_config.use_hsdp:
|
||||
incompatible_features.append("HSDP")
|
||||
if self.parallel_config.sequence_parallel_size > 1:
|
||||
@@ -1262,6 +1268,7 @@ class OmniDiffusionConfig:
|
||||
if self.model_class_name is None and self.diffusion_load_format == "diffusers":
|
||||
self.model_class_name = "DiffusersAdapterPipeline"
|
||||
|
||||
assert self.model is not None
|
||||
try:
|
||||
config_dict = get_diffusion_model_index(
|
||||
self.model,
|
||||
@@ -1338,6 +1345,7 @@ class OmniDiffusionConfig:
|
||||
|
||||
from vllm_omni.diffusion.utils.hf_utils import _looks_like_hidream_o1
|
||||
|
||||
assert self.model is not None
|
||||
is_hidream_o1 = _looks_like_hidream_o1(self.model, cfg)
|
||||
if self.model_class_name == "HiDreamO1ImagePipeline" and not is_hidream_o1:
|
||||
raise ValueError(f"Checkpoint {self.model} does not have the HiDream-O1 signature")
|
||||
@@ -1395,6 +1403,7 @@ class OmniDiffusionConfig:
|
||||
elif model_type == "vla":
|
||||
from vllm_omni.diffusion.utils.hf_utils import _looks_like_dreamzero
|
||||
|
||||
assert self.model is not None
|
||||
if _looks_like_dreamzero(self.model):
|
||||
self.model_class_name = "DreamZeroPipeline"
|
||||
self.set_tf_model_config(TransformerConfig())
|
||||
@@ -1593,10 +1602,11 @@ class AttnQuantSpec:
|
||||
for name, v in (("dtype_qk", self.dtype_qk), ("dtype_vo", self.dtype_vo)):
|
||||
if v is not None and v not in self._VALID_DTYPES:
|
||||
raise ValueError(f"quant.{name}={v!r} unsupported; use one of {sorted(self._VALID_DTYPES)}.")
|
||||
for name, v in (("q_block_size", self.q_block_size), ("k_block_size", self.k_block_size)):
|
||||
if v not in self._VALID_BLOCK_SIZES:
|
||||
for block_name, block_value in (("q_block_size", self.q_block_size), ("k_block_size", self.k_block_size)):
|
||||
if block_value not in self._VALID_BLOCK_SIZES:
|
||||
raise ValueError(
|
||||
f"quant.{name}={v!r} unsupported; kernels exist only for {sorted(self._VALID_BLOCK_SIZES)}."
|
||||
f"quant.{block_name}={block_value!r} unsupported; "
|
||||
f"kernels exist only for {sorted(self._VALID_BLOCK_SIZES)}."
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1638,6 +1648,7 @@ class AttentionSpec:
|
||||
backend: str
|
||||
skip_softmax: SkipSoftmaxSpec | None = None
|
||||
quant: AttnQuantSpec | None = None
|
||||
fastvideo_vsa_topk: int | None = None
|
||||
block_sparse: BlockSparseSpec | None = None
|
||||
skip_calibration: dict | None = field(default=None, repr=False)
|
||||
|
||||
@@ -1657,6 +1668,11 @@ class AttentionSpec:
|
||||
f"quant is only supported by the TRTLLM_ATTN and FLASHINFER_ATTN backends, but "
|
||||
f"backend={self.backend!r}. Remove quant or set a supported backend."
|
||||
)
|
||||
if self.fastvideo_vsa_topk is not None:
|
||||
if self.backend.upper() != "FASTVIDEO_VSA":
|
||||
raise ValueError("fastvideo_vsa_topk is only supported by the FASTVIDEO_VSA backend.")
|
||||
if self.fastvideo_vsa_topk <= 0:
|
||||
raise ValueError("fastvideo_vsa_topk must be positive.")
|
||||
if self.backend.upper() in BLOCK_SPARSE_BACKENDS:
|
||||
# Selecting the backend is the opt-in; without an explicit block the
|
||||
# defaults apply rather than silently running dense.
|
||||
@@ -1698,12 +1714,15 @@ class AttentionSpec:
|
||||
if q.flashinfer_backend is not None:
|
||||
quant_kw["flashinfer_backend"] = q.flashinfer_backend
|
||||
kw["quant"] = quant_kw
|
||||
if self.fastvideo_vsa_topk is not None:
|
||||
kw["topk"] = self.fastvideo_vsa_topk
|
||||
if self.block_sparse is not None:
|
||||
bs = self.block_sparse
|
||||
kw["sparsity"] = bs.sparsity
|
||||
kw["start_step"] = bs.start_step
|
||||
if bs.skip_layer_indices:
|
||||
kw["skip_layers"] = sorted(bs.skip_layer_indices)
|
||||
|
||||
return kw or None
|
||||
|
||||
|
||||
@@ -1773,7 +1792,7 @@ class AttentionConfig:
|
||||
normalized[role] = node
|
||||
return
|
||||
|
||||
spec_keys = {"backend", "skip_softmax", "quant", "block_sparse"}
|
||||
spec_keys = {"backend", "skip_softmax", "quant", "fastvideo_vsa_topk", "block_sparse"}
|
||||
node_dict = dict(node)
|
||||
node_keys = set(node_dict)
|
||||
if node_keys & spec_keys:
|
||||
@@ -1812,6 +1831,7 @@ def parse_attention_config(
|
||||
attention_config: AttentionConfig | Mapping[str, Any] | None = None,
|
||||
*,
|
||||
attention_backend: str | None = None,
|
||||
fastvideo_vsa_topk: int | None = None,
|
||||
) -> AttentionConfig:
|
||||
"""Pure type-conversion: coerce *attention_config* to an AttentionConfig.
|
||||
|
||||
@@ -1839,6 +1859,14 @@ def parse_attention_config(
|
||||
if attention_backend.lower() != "auto":
|
||||
normalized.default = AttentionSpec(backend=attention_backend)
|
||||
|
||||
if fastvideo_vsa_topk is not None:
|
||||
if normalized.default is None:
|
||||
raise ValueError("--fastvideo-vsa-topk requires --diffusion-attention-backend FASTVIDEO_VSA.")
|
||||
if normalized.default.backend.upper() != "FASTVIDEO_VSA":
|
||||
raise ValueError("--fastvideo-vsa-topk is only valid with the FASTVIDEO_VSA backend.")
|
||||
normalized.default.fastvideo_vsa_topk = fastvideo_vsa_topk
|
||||
normalized.default.__post_init__()
|
||||
|
||||
return normalized
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -33,7 +33,7 @@ from vllm_omni.diffusion.models.interface import SupportsComponentDiscovery
|
||||
from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin, _is_rank_zero
|
||||
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
|
||||
from vllm_omni.diffusion.models.wan2_2.scheduling_wan_euler import WanEulerScheduler
|
||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanTransformer3DModel
|
||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_transformer import WanSelfAttention, WanTransformer3DModel
|
||||
from vllm_omni.diffusion.postprocess import interpolate_video_tensor
|
||||
from vllm_omni.diffusion.profiler.diffusion_pipeline_profiler import DiffusionPipelineProfilerMixin
|
||||
from vllm_omni.diffusion.request import OmniDiffusionRequest
|
||||
@@ -44,6 +44,8 @@ from vllm_omni.platforms import current_omni_platform
|
||||
logger = logging.getLogger(__name__)
|
||||
DEBUG_PERF = False
|
||||
WAN_SAMPLE_SOLVER_CHOICES = {"unipc", "euler"}
|
||||
FASTWAN_DMD_TIMESTEPS = (1000.0, 757.0, 522.0)
|
||||
FASTWAN_DMD_SCHEDULER_SHIFT = 8.0
|
||||
|
||||
|
||||
def build_wan_scheduler(sample_solver: str, flow_shift: float) -> Any:
|
||||
@@ -64,6 +66,30 @@ def build_wan_scheduler(sample_solver: str, flow_shift: float) -> Any:
|
||||
)
|
||||
|
||||
|
||||
def load_wan_weights_with_optional_gate(model: nn.Module, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load Wan weights and discard optional VSA gates absent from the checkpoint."""
|
||||
gate_param_names = {name for name, _ in model.named_parameters() if ".to_gate_compress." in name}
|
||||
has_gate_compress_weights = False
|
||||
|
||||
def tracked_weights():
|
||||
nonlocal has_gate_compress_weights
|
||||
for name, weight in weights:
|
||||
if ".to_gate_compress." in name:
|
||||
has_gate_compress_weights = True
|
||||
yield name, weight
|
||||
|
||||
loaded_weights = AutoWeightsLoader(model).load_weights(tracked_weights())
|
||||
setattr(model, "has_gate_compress_weights", has_gate_compress_weights)
|
||||
if not has_gate_compress_weights:
|
||||
for module in model.modules():
|
||||
if isinstance(module, WanSelfAttention):
|
||||
module.to_gate_compress = None
|
||||
|
||||
# Optional gate parameters are absent from public FullAttn/DMD checkpoints.
|
||||
loaded_weights.update(gate_param_names)
|
||||
return loaded_weights
|
||||
|
||||
|
||||
def resolve_wan_sample_solver(req: OmniDiffusionRequest, default: str = "unipc") -> str:
|
||||
extra_args = getattr(req.sampling_params, "extra_args", {}) or {}
|
||||
raw = extra_args.get("sample_solver", default)
|
||||
@@ -314,6 +340,7 @@ class Wan22Pipeline(
|
||||
|
||||
# Read model_index.json to detect expand_timesteps mode (for TI2V-5B)
|
||||
self.expand_timesteps = False
|
||||
self.is_dmd = False
|
||||
self.has_transformer_2 = False
|
||||
if local_files_only:
|
||||
model_index_path = os.path.join(model, "model_index.json")
|
||||
@@ -321,6 +348,7 @@ class Wan22Pipeline(
|
||||
with open(model_index_path) as f:
|
||||
model_index = json.load(f)
|
||||
self.expand_timesteps = model_index.get("expand_timesteps", False)
|
||||
self.is_dmd = model_index.get("_class_name") == "WanDMDPipeline"
|
||||
# Check if this is a two-stage model (MoE with transformer_2)
|
||||
transformer_2_path = os.path.join(model, "transformer_2")
|
||||
self.has_transformer_2 = os.path.exists(transformer_2_path)
|
||||
@@ -333,6 +361,7 @@ class Wan22Pipeline(
|
||||
with open(model_index_path) as f:
|
||||
model_index = json.load(f)
|
||||
self.expand_timesteps = model_index.get("expand_timesteps", False)
|
||||
self.is_dmd = model_index.get("_class_name") == "WanDMDPipeline"
|
||||
# Check transformer_2 from model_index
|
||||
transformer_2_info = model_index.get("transformer_2", [None, None])
|
||||
self.has_transformer_2 = transformer_2_info[0] is not None
|
||||
@@ -422,6 +451,10 @@ class Wan22Pipeline(
|
||||
else:
|
||||
self.transformer_2 = None
|
||||
|
||||
for transformer in (self.transformer, self.transformer_2):
|
||||
if transformer is not None:
|
||||
transformer.preserve_vsa_all_blocks = self.is_dmd
|
||||
|
||||
# Store the active transformer config
|
||||
if load_transformer:
|
||||
self.transformer_config = self.transformer.config
|
||||
@@ -430,8 +463,14 @@ class Wan22Pipeline(
|
||||
else:
|
||||
raise RuntimeError("No transformer loaded")
|
||||
|
||||
self._sample_solver = "unipc"
|
||||
self._flow_shift = od_config.flow_shift if od_config.flow_shift is not None else 5.0
|
||||
self._sample_solver = "euler" if self.is_dmd else "unipc"
|
||||
self._flow_shift = (
|
||||
FASTWAN_DMD_SCHEDULER_SHIFT
|
||||
if self.is_dmd
|
||||
else od_config.flow_shift
|
||||
if od_config.flow_shift is not None
|
||||
else 5.0
|
||||
)
|
||||
self.scheduler = build_wan_scheduler(self._sample_solver, self._flow_shift)
|
||||
|
||||
self.vae_scale_factor_temporal = self.vae.config.scale_factor_temporal if getattr(self, "vae", None) else 4
|
||||
@@ -480,6 +519,7 @@ class Wan22Pipeline(
|
||||
attention_kwargs: dict[str, Any],
|
||||
latent_condition: torch.Tensor | None = None,
|
||||
first_frame_mask: torch.Tensor | None = None,
|
||||
generator: torch.Generator | None = None,
|
||||
) -> torch.Tensor | AsyncLatents:
|
||||
if attention_kwargs is None:
|
||||
attention_kwargs = {}
|
||||
@@ -561,7 +601,20 @@ class Wan22Pipeline(
|
||||
cfg_normalize=False,
|
||||
)
|
||||
|
||||
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
|
||||
if self.is_dmd:
|
||||
pred_clean = self.scheduler.predict_clean(noise_pred, latents, t).to(noise_pred.dtype)
|
||||
if step_idx + 1 < len(timesteps):
|
||||
noise = randn_tensor(
|
||||
latents.shape,
|
||||
generator=generator,
|
||||
device=latents.device,
|
||||
dtype=pred_clean.dtype,
|
||||
)
|
||||
latents = self.scheduler.add_noise(pred_clean, noise, timesteps[step_idx + 1])
|
||||
else:
|
||||
latents = pred_clean
|
||||
else:
|
||||
latents = self.scheduler_step_maybe_with_cfg(noise_pred, t, latents, do_true_cfg)
|
||||
pbar.update()
|
||||
|
||||
return latents
|
||||
@@ -600,13 +653,21 @@ class Wan22Pipeline(
|
||||
mod_value = self.vae_scale_factor_spatial * patch_size[1] # 16*2=32 for TI2V, 8*2=16 for I2V
|
||||
height = (height // mod_value) * mod_value
|
||||
width = (width // mod_value) * mod_value
|
||||
num_steps = 40 if common.num_inference_steps is None else common.num_inference_steps
|
||||
if self.is_dmd:
|
||||
# The checkpoint was distilled for these three transitions. Ignore
|
||||
# request-level step counts, including the engine's 1-step warmup.
|
||||
num_steps = len(FASTWAN_DMD_TIMESTEPS)
|
||||
else:
|
||||
num_steps = 40 if common.num_inference_steps is None else common.num_inference_steps
|
||||
|
||||
output_type = common.output_type or "np"
|
||||
num_outputs_per_prompt = common.num_outputs_per_prompt or 1
|
||||
attention_kwargs: dict | None = None
|
||||
|
||||
guidance_low, guidance_high = resolve_wan_guidance_scales(common, default_guidance_scale=4.0)
|
||||
guidance_low, guidance_high = resolve_wan_guidance_scales(
|
||||
common,
|
||||
default_guidance_scale=1.0 if self.is_dmd else 4.0,
|
||||
)
|
||||
|
||||
# record guidance for properties
|
||||
self._guidance_scale = guidance_low
|
||||
@@ -685,17 +746,19 @@ class Wan22Pipeline(
|
||||
current_omni_platform.synchronize()
|
||||
_t_text_enc_ms = (time.perf_counter() - _t_text_enc_start) * 1000
|
||||
|
||||
first_request = req.requests[0]
|
||||
sample_solver = resolve_wan_sample_solver(first_request, default=self._sample_solver)
|
||||
flow_shift = resolve_wan_flow_shift(first_request, self.od_config)
|
||||
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
||||
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
||||
self._sample_solver = sample_solver
|
||||
self._flow_shift = flow_shift
|
||||
if self.is_dmd:
|
||||
timesteps = torch.tensor(FASTWAN_DMD_TIMESTEPS, device=device, dtype=torch.float32)
|
||||
else:
|
||||
first_request = req.requests[0]
|
||||
sample_solver = resolve_wan_sample_solver(first_request, default=self._sample_solver)
|
||||
flow_shift = resolve_wan_flow_shift(first_request, self.od_config)
|
||||
if sample_solver != self._sample_solver or abs(flow_shift - self._flow_shift) > 1e-6:
|
||||
self.scheduler = build_wan_scheduler(sample_solver, flow_shift)
|
||||
self._sample_solver = sample_solver
|
||||
self._flow_shift = flow_shift
|
||||
|
||||
# Timesteps
|
||||
self.scheduler.set_timesteps(num_steps, device=device)
|
||||
timesteps = self.scheduler.timesteps
|
||||
self.scheduler.set_timesteps(num_steps, device=device)
|
||||
timesteps = self.scheduler.timesteps
|
||||
self._num_timesteps = len(timesteps)
|
||||
boundary_timestep = None
|
||||
if boundary_ratio is not None:
|
||||
@@ -817,6 +880,7 @@ class Wan22Pipeline(
|
||||
attention_kwargs=attention_kwargs,
|
||||
latent_condition=latent_condition,
|
||||
first_frame_mask=first_frame_mask,
|
||||
generator=generator,
|
||||
)
|
||||
|
||||
# Wan2.2 is prone to out of memory errors when predicting large videos
|
||||
@@ -1003,9 +1067,7 @@ class Wan22Pipeline(
|
||||
return latents
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights)
|
||||
return load_wan_weights_with_optional_gate(self, weights)
|
||||
|
||||
def check_inputs(
|
||||
self,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -16,7 +16,6 @@ import torchvision.transforms.functional as TF
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
from torch import nn
|
||||
from transformers import AutoTokenizer, CLIPImageProcessor, CLIPVisionModel, UMT5EncoderModel
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
from vllm.sequence import IntermediateTensors
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
@@ -36,6 +35,7 @@ from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
||||
build_wan_scheduler,
|
||||
create_transformer_from_config,
|
||||
load_transformer_config,
|
||||
load_wan_weights_with_optional_gate,
|
||||
resolve_wan_flow_shift,
|
||||
resolve_wan_guidance_scales,
|
||||
resolve_wan_sample_solver,
|
||||
@@ -1014,9 +1014,7 @@ class Wan22I2VPipeline(
|
||||
raise ValueError("`guidance_scale_2` is only supported when `boundary_ratio` is set.")
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights)
|
||||
return load_wan_weights_with_optional_gate(self, weights)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""
|
||||
Wan2.2 Speech-to-Video (S2V) Pipeline for vLLM-Omni.
|
||||
@@ -22,7 +22,6 @@ from diffusers.utils.torch_utils import randn_tensor
|
||||
from torch import nn
|
||||
from torchvision import transforms
|
||||
from transformers import AutoTokenizer, UMT5Config, UMT5EncoderModel, Wav2Vec2ForCTC, Wav2Vec2Processor
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
from vllm.multimodal.media.audio import load_audio
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
@@ -36,6 +35,7 @@ from vllm_omni.diffusion.models.progress_bar import ProgressBarMixin
|
||||
from vllm_omni.diffusion.models.schedulers import FlowUniPCMultistepScheduler
|
||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
||||
load_transformer_config,
|
||||
load_wan_weights_with_optional_gate,
|
||||
retrieve_latents,
|
||||
)
|
||||
from vllm_omni.diffusion.models.wan2_2.wan2_2_s2v_transformer import (
|
||||
@@ -1475,6 +1475,4 @@ class Wan22S2VPipeline(
|
||||
return outputs
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights)
|
||||
return load_wan_weights_with_optional_gate(self, weights)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""
|
||||
VACE (Video Creation and Editing) Pipeline for WAN models.
|
||||
@@ -22,12 +22,12 @@ import PIL.Image
|
||||
import torch
|
||||
from vllm.logger import init_logger
|
||||
from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
|
||||
from vllm.model_executor.models.utils import AutoWeightsLoader
|
||||
|
||||
from vllm_omni.diffusion.data import DiffusionOutput, OmniDiffusionConfig
|
||||
from vllm_omni.diffusion.models.interface import SupportImageInput
|
||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
||||
Wan22Pipeline,
|
||||
load_wan_weights_with_optional_gate,
|
||||
retrieve_latents,
|
||||
)
|
||||
from vllm_omni.diffusion.models.wan2_2.pipeline_wan2_2 import (
|
||||
@@ -747,6 +747,4 @@ class Wan22VACEPipeline(Wan22Pipeline, SupportImageInput):
|
||||
)
|
||||
|
||||
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
|
||||
"""Load weights using AutoWeightsLoader for vLLM integration."""
|
||||
loader = AutoWeightsLoader(self)
|
||||
return loader.load_weights(weights)
|
||||
return load_wan_weights_with_optional_gate(self, weights)
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -110,6 +110,42 @@ class WanEulerScheduler:
|
||||
self._step_index = None
|
||||
self._begin_index = None
|
||||
|
||||
def sigma_for_timestep(
|
||||
self,
|
||||
timestep: float | torch.Tensor,
|
||||
*,
|
||||
device: torch.device | str | None = None,
|
||||
dtype: torch.dtype = torch.float32,
|
||||
) -> torch.Tensor:
|
||||
"""Return the flow sigma nearest to a (possibly distilled) timestep."""
|
||||
timestep_t = torch.as_tensor(timestep, device=self.timesteps.device, dtype=self.timesteps.dtype)
|
||||
if timestep_t.numel() != 1:
|
||||
raise ValueError(f"Expected a scalar timestep, got shape {tuple(timestep_t.shape)}")
|
||||
index = torch.argmin(torch.abs(self.timesteps - timestep_t))
|
||||
return self.sigmas[index].to(device=device or self.timesteps.device, dtype=dtype)
|
||||
|
||||
def predict_clean(
|
||||
self,
|
||||
model_output: torch.Tensor,
|
||||
sample: torch.Tensor,
|
||||
timestep: float | torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Convert a flow/noise prediction into the corresponding clean latent."""
|
||||
sample_fp32 = sample.to(torch.float32)
|
||||
sigma = self.sigma_for_timestep(timestep, device=sample.device, dtype=torch.float32)
|
||||
return sample_fp32 - sigma * model_output.to(torch.float32)
|
||||
|
||||
def add_noise(
|
||||
self,
|
||||
clean_sample: torch.Tensor,
|
||||
noise: torch.Tensor,
|
||||
timestep: float | torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
"""Re-noise a clean latent at a distilled flow timestep."""
|
||||
sigma = self.sigma_for_timestep(timestep, device=noise.device, dtype=torch.float32)
|
||||
noisy = (1.0 - sigma) * clean_sample.to(torch.float32) + sigma * noise.to(torch.float32)
|
||||
return noisy.to(noise.dtype)
|
||||
|
||||
def scale_model_input(self, sample: torch.Tensor, timestep: int | None = None) -> torch.Tensor: # noqa: ARG002
|
||||
return sample
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import math
|
||||
from collections.abc import Iterable
|
||||
@@ -419,6 +419,24 @@ class WanSelfAttention(nn.Module):
|
||||
prefix=prefix,
|
||||
)
|
||||
|
||||
# FastVideo VSA checkpoints may add a learned projection that gates
|
||||
# the compressed global branch. Zero initialization makes checkpoints
|
||||
# without these weights sparse-only, with no user-facing mode switch.
|
||||
self.to_gate_compress: ColumnParallelLinear | None = None
|
||||
if self.attn.attn_backend.get_name() == "FASTVIDEO_VSA":
|
||||
self.to_gate_compress = ColumnParallelLinear(
|
||||
dim,
|
||||
self.inner_dim,
|
||||
bias=True,
|
||||
gather_output=False,
|
||||
return_bias=False,
|
||||
quant_config=quant_config,
|
||||
prefix=f"{prefix}.to_gate_compress" if prefix else "to_gate_compress",
|
||||
)
|
||||
nn.init.zeros_(self.to_gate_compress.weight)
|
||||
if self.to_gate_compress.bias is not None:
|
||||
nn.init.zeros_(self.to_gate_compress.bias)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
@@ -441,6 +459,15 @@ class WanSelfAttention(nn.Module):
|
||||
key = key.unflatten(2, (self.num_kv_heads, self.head_dim))
|
||||
value = value.unflatten(2, (self.num_kv_heads, self.head_dim))
|
||||
|
||||
if self.to_gate_compress is not None:
|
||||
gate_result = self.to_gate_compress(hidden_states)
|
||||
gate_compress = gate_result[0] if isinstance(gate_result, tuple) else gate_result
|
||||
gate_compress = gate_compress.unflatten(2, (self.num_heads, self.head_dim))
|
||||
if attn_metadata is None:
|
||||
attn_metadata = AttentionMetadata(extra={"gate_compress": gate_compress})
|
||||
else:
|
||||
attn_metadata.extra["gate_compress"] = gate_compress
|
||||
|
||||
# Apply rotary embeddings
|
||||
if rotary_emb is not None:
|
||||
freqs_cos, freqs_sin = rotary_emb
|
||||
@@ -709,6 +736,8 @@ class WanTransformerBlock(nn.Module):
|
||||
temb: torch.Tensor,
|
||||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
hidden_states_mask: torch.Tensor | None = None,
|
||||
vsa_dit_seq_shape: tuple[int, int, int] | None = None,
|
||||
preserve_vsa_all_blocks: bool = False,
|
||||
) -> torch.Tensor:
|
||||
if temb.ndim == 4:
|
||||
# temb: batch_size, seq_len, 6, inner_dim (wan2.2 ti2v)
|
||||
@@ -729,7 +758,12 @@ class WanTransformerBlock(nn.Module):
|
||||
|
||||
# 1. Self-attention
|
||||
norm_hidden_states = self.norm1(hidden_states, scale_msa, shift_msa).type_as(hidden_states)
|
||||
self_attn_metadata = AttentionMetadata(attn_mask=hidden_states_mask)
|
||||
self_attn_extra = {}
|
||||
if vsa_dit_seq_shape is not None:
|
||||
self_attn_extra["vsa_dit_seq_shape"] = vsa_dit_seq_shape
|
||||
if preserve_vsa_all_blocks:
|
||||
self_attn_extra["preserve_vsa_all_blocks"] = True
|
||||
self_attn_metadata = AttentionMetadata(attn_mask=hidden_states_mask, extra=self_attn_extra)
|
||||
attn_output = self.attn1(norm_hidden_states, rotary_emb, self_attn_metadata)
|
||||
hidden_states = (hidden_states + attn_output * gate_msa).type_as(hidden_states)
|
||||
|
||||
@@ -910,6 +944,9 @@ class WanTransformer3DModel(nn.Module):
|
||||
pos_embed_seq_len=pos_embed_seq_len,
|
||||
)
|
||||
|
||||
# DMD/FastVideo checkpoints retain VSA semantics when top-k selects every block.
|
||||
self.preserve_vsa_all_blocks = False
|
||||
|
||||
# 3. Transformer blocks — partitioned across PP stages via vLLM's `make_layers`.
|
||||
# It computes the [start_layer, end_layer) slice for this rank and fills the remaining slots
|
||||
# with PPMissingLayer so that weight names stay globally consistent.
|
||||
@@ -1044,8 +1081,19 @@ class WanTransformer3DModel(nn.Module):
|
||||
)
|
||||
|
||||
# Transformer blocks
|
||||
# Preserve the post-patch (T, H, W) grid so VSA can partition
|
||||
# the flattened DiT sequence into spatiotemporal blocks.
|
||||
vsa_dit_seq_shape = (post_patch_num_frames, post_patch_height, post_patch_width)
|
||||
for block in self.blocks[self.start_layer : self.end_layer]:
|
||||
hidden_states = block(hidden_states, encoder_hidden_states, timestep_proj, rotary_emb, hidden_states_mask)
|
||||
hidden_states = block(
|
||||
hidden_states,
|
||||
encoder_hidden_states,
|
||||
timestep_proj,
|
||||
rotary_emb,
|
||||
hidden_states_mask,
|
||||
vsa_dit_seq_shape,
|
||||
self.preserve_vsa_all_blocks,
|
||||
)
|
||||
|
||||
if not is_pipeline_last_stage():
|
||||
# Non-last PP stage: hand the token sequence to the caller via IntermediateTensors.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""VACE variant of WanTransformer3DModel for conditional video generation."""
|
||||
|
||||
@@ -61,6 +61,7 @@ class VaceWanTransformerBlock(WanTransformerBlock):
|
||||
temb: torch.Tensor,
|
||||
rotary_emb: tuple[torch.Tensor, torch.Tensor],
|
||||
hidden_states_mask: torch.Tensor | None = None,
|
||||
vsa_dit_seq_shape: tuple[int, int, int] | None = None,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
if self.proj_in is not None:
|
||||
control_hidden_states = self.proj_in(control_hidden_states)
|
||||
@@ -72,6 +73,7 @@ class VaceWanTransformerBlock(WanTransformerBlock):
|
||||
temb,
|
||||
rotary_emb,
|
||||
hidden_states_mask,
|
||||
vsa_dit_seq_shape,
|
||||
)
|
||||
|
||||
conditioning_states = self.proj_out(control_hidden_states)
|
||||
@@ -250,6 +252,7 @@ class WanVACETransformer3DModel(WanTransformer3DModel):
|
||||
)
|
||||
|
||||
# VACE: embed context and run conditioning blocks
|
||||
vsa_dit_seq_shape = (post_patch_num_frames, post_patch_height, post_patch_width)
|
||||
vace_hints = None
|
||||
if vace_context is not None and self.vace_blocks is not None:
|
||||
full_seq_len = hidden_states.shape[1] * sp_size
|
||||
@@ -263,6 +266,7 @@ class WanVACETransformer3DModel(WanTransformer3DModel):
|
||||
timestep_proj,
|
||||
rotary_emb,
|
||||
hidden_states_mask,
|
||||
vsa_dit_seq_shape,
|
||||
)
|
||||
vace_hints.append(conditioning_states)
|
||||
|
||||
@@ -278,6 +282,7 @@ class WanVACETransformer3DModel(WanTransformer3DModel):
|
||||
timestep_proj,
|
||||
rotary_emb,
|
||||
hidden_states_mask,
|
||||
vsa_dit_seq_shape,
|
||||
)
|
||||
if vace_hints is not None and self.vace_layers_mapping is not None and i in self.vace_layers_mapping:
|
||||
vace_idx = self.vace_layers_mapping[i]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import importlib
|
||||
|
||||
@@ -61,6 +61,11 @@ _DIFFUSION_MODELS = {
|
||||
"pipeline_wan2_2",
|
||||
"Wan22Pipeline",
|
||||
),
|
||||
"WanDMDPipeline": (
|
||||
"wan2_2",
|
||||
"pipeline_wan2_2",
|
||||
"Wan22Pipeline",
|
||||
),
|
||||
"WanVACEPipeline": (
|
||||
"wan2_2",
|
||||
"pipeline_wan2_2_vace",
|
||||
@@ -459,6 +464,7 @@ def _apply_sequence_parallel_if_enabled(model, od_config: OmniDiffusionConfig) -
|
||||
|
||||
try:
|
||||
sp_size = od_config.parallel_config.sequence_parallel_size
|
||||
assert sp_size is not None
|
||||
if sp_size <= 1:
|
||||
return
|
||||
|
||||
@@ -539,6 +545,7 @@ _DIFFUSION_POST_PROCESS_FUNCS = {
|
||||
"OvisImagePipeline": "get_ovis_image_post_process_func",
|
||||
"BooguImagePipeline": "get_boogu_image_post_process_func",
|
||||
"WanPipeline": "get_wan22_post_process_func",
|
||||
"WanDMDPipeline": "get_wan22_post_process_func",
|
||||
"WanVACEPipeline": "get_wan22_vace_post_process_func",
|
||||
"LTX2Pipeline": "get_ltx2_post_process_func",
|
||||
"LTX2TwoStagePipeline": "get_ltx2_post_process_func",
|
||||
@@ -612,6 +619,7 @@ _DIFFUSION_PRE_PROCESS_FUNCS = {
|
||||
"LongCatVideoAvatarPipeline": "get_longcat_video_avatar_pre_process_func",
|
||||
"QwenImageLayeredPipeline": "get_qwen_image_layered_pre_process_func",
|
||||
"WanPipeline": "get_wan22_pre_process_func",
|
||||
"WanDMDPipeline": "get_wan22_pre_process_func",
|
||||
"WanVACEPipeline": "get_wan22_vace_pre_process_func",
|
||||
"WanImageToVideoPipeline": "get_wan22_i2v_pre_process_func",
|
||||
"WanS2VPipeline": "get_wan22_s2v_pre_process_func",
|
||||
@@ -703,6 +711,7 @@ def register_diffusion_model(
|
||||
|
||||
def _load_process_func(od_config: OmniDiffusionConfig, func_name: str):
|
||||
"""Load and return a process function from the appropriate module."""
|
||||
assert od_config.model_class_name is not None
|
||||
mod_folder, mod_relname, _ = _DIFFUSION_MODELS[od_config.model_class_name]
|
||||
if mod_relname == "":
|
||||
# Full module path (registered via register_diffusion_model)
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass, field, fields
|
||||
from typing import Any
|
||||
from typing import Any, cast
|
||||
|
||||
from vllm.engine.arg_utils import AsyncEngineArgs, EngineArgs
|
||||
from vllm.logger import init_logger
|
||||
@@ -298,24 +302,26 @@ class OmniEngineArgs(EngineArgs):
|
||||
}
|
||||
stage_connector_config["extra"]["stage_id"] = self.stage_id
|
||||
|
||||
hf_overrides = cast(dict[str, Any] | Callable[[Any], Any] | None, getattr(self, "hf_overrides", None))
|
||||
# If model_arch is specified, inject it into hf_overrides so vLLM can
|
||||
# resolve the architecture even when config.json lacks 'architectures'.
|
||||
# Also inject model_type so AutoConfig can resolve the correct config
|
||||
# class for models with empty or missing config.json (e.g. CosyVoice3).
|
||||
if self.model_arch:
|
||||
if self.hf_overrides is None:
|
||||
self.hf_overrides = {}
|
||||
if isinstance(self.hf_overrides, dict):
|
||||
self.hf_overrides.setdefault("architectures", [self.model_arch])
|
||||
if "model_type" not in self.hf_overrides:
|
||||
if hf_overrides is None:
|
||||
hf_overrides = {}
|
||||
self.hf_overrides = hf_overrides
|
||||
if isinstance(hf_overrides, dict):
|
||||
hf_overrides.setdefault("architectures", [self.model_arch])
|
||||
if "model_type" not in hf_overrides:
|
||||
model_type = _ARCH_TO_MODEL_TYPE.get(self.model_arch)
|
||||
if model_type is not None:
|
||||
self.hf_overrides.setdefault("model_type", model_type)
|
||||
hf_overrides.setdefault("model_type", model_type)
|
||||
|
||||
# Stage wrappers (e.g. Code2Wav) may need max_model_len larger
|
||||
# than the base checkpoint's text max_position_embeddings.
|
||||
if self.model_arch == "Qwen3TTSCode2Wav" and self.max_model_len is not None:
|
||||
self.hf_overrides.setdefault("talker_config", {}).setdefault(
|
||||
hf_overrides.setdefault("talker_config", {}).setdefault(
|
||||
"max_position_embeddings", int(self.max_model_len)
|
||||
)
|
||||
|
||||
@@ -331,33 +337,37 @@ class OmniEngineArgs(EngineArgs):
|
||||
if model_type is not None:
|
||||
self._patch_empty_hf_config(model_type)
|
||||
|
||||
tokenizer = cast(str | None, getattr(self, "tokenizer", None))
|
||||
|
||||
# NemotronVoiceChat: the checkpoint's only tokenizer subfolder is the
|
||||
# auxiliary rnnt_tokenizer/ (ASR loss vocab), which the generic
|
||||
# subfolder auto-detect below would wrongly pick up. The text tokenizer
|
||||
# is the Nemotron backbone's, resolved from a local override or its
|
||||
# HF id (cache-friendly).
|
||||
if not self.tokenizer and self.model_arch in (
|
||||
if not tokenizer and self.model_arch in (
|
||||
"NemotronVoiceChatThinkerForConditionalGeneration",
|
||||
"NemotronVoiceChatTalkerForConditionalGeneration",
|
||||
"NemotronVoiceChatCode2Wav",
|
||||
):
|
||||
self.tokenizer = os.environ.get("NEMOTRON_VOICECHAT_LLM_PATH") or "nvidia/NVIDIA-Nemotron-Nano-9B-v2"
|
||||
logger.info("NemotronVoiceChat: using text tokenizer from %s", self.tokenizer)
|
||||
tokenizer = self.tokenizer
|
||||
logger.info("NemotronVoiceChat: using text tokenizer from %s", tokenizer)
|
||||
|
||||
# Auto-detect tokenizer for models that store it in a subdirectory
|
||||
# rather than the root (e.g. CosyVoice3 uses CosyVoice-BlankEN/).
|
||||
if not self.tokenizer and self.model:
|
||||
if not tokenizer and self.model:
|
||||
model_path = self.model
|
||||
if os.path.isdir(model_path) and not os.path.isfile(os.path.join(model_path, "tokenizer_config.json")):
|
||||
for subfolder in sorted(os.listdir(model_path)):
|
||||
candidate = os.path.join(model_path, subfolder)
|
||||
if os.path.isdir(candidate) and os.path.isfile(os.path.join(candidate, "tokenizer_config.json")):
|
||||
self.tokenizer = candidate
|
||||
tokenizer = candidate
|
||||
logger.info("Auto-detected tokenizer at %s", candidate)
|
||||
break
|
||||
elif not os.path.isdir(model_path):
|
||||
subfolder = _TOKENIZER_SUBFOLDER_MAP.get(self.model_arch)
|
||||
if subfolder:
|
||||
tokenizer_subfolder = _TOKENIZER_SUBFOLDER_MAP.get(self.model_arch or "")
|
||||
if tokenizer_subfolder:
|
||||
# Download just the tokenizer files from the subfolder
|
||||
try:
|
||||
from huggingface_hub import snapshot_download
|
||||
@@ -365,15 +375,15 @@ class OmniEngineArgs(EngineArgs):
|
||||
local_dir = snapshot_download(
|
||||
model_path,
|
||||
allow_patterns=[
|
||||
f"{subfolder}/tokenizer*",
|
||||
f"{subfolder}/tokenization_*",
|
||||
f"{subfolder}/special_tokens*",
|
||||
f"{subfolder}/vocab*",
|
||||
f"{subfolder}/merges*",
|
||||
f"{subfolder}/added_tokens*",
|
||||
f"{tokenizer_subfolder}/tokenizer*",
|
||||
f"{tokenizer_subfolder}/tokenization_*",
|
||||
f"{tokenizer_subfolder}/special_tokens*",
|
||||
f"{tokenizer_subfolder}/vocab*",
|
||||
f"{tokenizer_subfolder}/merges*",
|
||||
f"{tokenizer_subfolder}/added_tokens*",
|
||||
],
|
||||
)
|
||||
candidate = os.path.join(local_dir, subfolder)
|
||||
candidate = os.path.join(local_dir, tokenizer_subfolder)
|
||||
if os.path.isdir(candidate):
|
||||
self.tokenizer = candidate
|
||||
logger.info("Downloaded tokenizer from %s/%s", model_path, subfolder)
|
||||
@@ -532,6 +542,7 @@ class OrchestratorArgs:
|
||||
hsdp_shard_size: int = -1
|
||||
hsdp_replicate_size: int = 1
|
||||
diffusion_attention_backend: str | None = None
|
||||
fastvideo_vsa_topk: int | None = None
|
||||
diffusion_attention_config: str | None = None
|
||||
diffusion_compile_granularity: str | None = None
|
||||
diffusion_compile_dynamic: bool | None = None
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""
|
||||
Async Omni Engine for vLLM-Omni multi-stage runtime.
|
||||
|
||||
@@ -375,9 +378,13 @@ class AsyncOmniEngine:
|
||||
getattr(self, "_duplex_runtime_extension_path", None)
|
||||
)
|
||||
if duplex_runtime_extension is not None:
|
||||
stage_clients = []
|
||||
for pool in self.stage_pools:
|
||||
assert pool.stage_client is not None
|
||||
stage_clients.append(pool.stage_client)
|
||||
validate_duplex_runtime_extension(
|
||||
duplex_runtime_extension,
|
||||
sampling_defaults=tuple(pool.stage_client.default_sampling_params for pool in self.stage_pools),
|
||||
sampling_defaults=tuple(client.default_sampling_params for client in stage_clients),
|
||||
)
|
||||
|
||||
orchestrator = Orchestrator(
|
||||
@@ -685,6 +692,7 @@ class AsyncOmniEngine:
|
||||
)
|
||||
|
||||
# Full input processing (tokenization, multimodal, etc.)
|
||||
assert self.input_processor is not None
|
||||
_t_preprocess = time.perf_counter()
|
||||
try:
|
||||
request = self.input_processor.process_inputs(
|
||||
@@ -763,11 +771,13 @@ class AsyncOmniEngine:
|
||||
Exception: Whatever prompt expansion or input processing raised.
|
||||
The caller is expected to let it reach the client.
|
||||
"""
|
||||
assert self.prompt_expand_func is not None
|
||||
expanded = self.prompt_expand_func(original_prompt, stage0_params)
|
||||
if not expanded:
|
||||
return []
|
||||
|
||||
companions: list[AddCompanionRequestMessage] = []
|
||||
assert self.input_processor is not None
|
||||
for ep in expanded:
|
||||
cid = f"{parent_id}{ep.request_id_suffix}"
|
||||
companion_prompt = ep.prompt
|
||||
@@ -864,7 +874,7 @@ class AsyncOmniEngine:
|
||||
"mag_max_skip_steps": 5,
|
||||
"mag_retention_ratio": 0.1,
|
||||
}
|
||||
if cache_backend in ("step_cache"):
|
||||
if cache_backend == "step_cache":
|
||||
return {
|
||||
"step_cache_dit_enabled": True,
|
||||
"velocity_sim_thresholds": [0.95, 0.93],
|
||||
@@ -1018,10 +1028,12 @@ class AsyncOmniEngine:
|
||||
if (
|
||||
kwargs.get("diffusion_attention_config") is not None
|
||||
or kwargs.get("diffusion_attention_backend") is not None
|
||||
or kwargs.get("fastvideo_vsa_topk") is not None
|
||||
):
|
||||
attention_config = parse_attention_config(
|
||||
kwargs.get("diffusion_attention_config"),
|
||||
attention_backend=kwargs.get("diffusion_attention_backend"),
|
||||
fastvideo_vsa_topk=kwargs.get("fastvideo_vsa_topk"),
|
||||
)
|
||||
|
||||
stage_engine_args = {
|
||||
@@ -1223,6 +1235,7 @@ class AsyncOmniEngine:
|
||||
if (
|
||||
kwargs.get("diffusion_attention_config") is not None
|
||||
or kwargs.get("diffusion_attention_backend") is not None
|
||||
or kwargs.get("fastvideo_vsa_topk") is not None
|
||||
):
|
||||
has_stage_attention = (
|
||||
hasattr(cfg.engine_args, "diffusion_attention_config")
|
||||
@@ -1232,6 +1245,7 @@ class AsyncOmniEngine:
|
||||
cfg.engine_args.diffusion_attention_config = parse_attention_config(
|
||||
kwargs.get("diffusion_attention_config"),
|
||||
attention_backend=kwargs.get("diffusion_attention_backend"),
|
||||
fastvideo_vsa_topk=kwargs.get("fastvideo_vsa_topk"),
|
||||
)
|
||||
quantization_config = kwargs.get("diffusion_quantization_config") or kwargs.get("quantization_config")
|
||||
if quantization_config is not None:
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
|
||||
"""
|
||||
Omni serve command for vLLM-Omni.
|
||||
|
||||
@@ -606,6 +609,12 @@ class OmniServeCommand(CLISubcommand):
|
||||
"May be combined with --diffusion-attention-config.per_role.* overrides, "
|
||||
"but mutually exclusive with --diffusion-attention-config.default.backend.",
|
||||
)
|
||||
omni_config_group.add_argument(
|
||||
"--fastvideo-vsa-topk",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Number of key/value blocks selected per query block by FASTVIDEO_VSA.",
|
||||
)
|
||||
omni_config_group.add_argument(
|
||||
"--diffusion-attention-config",
|
||||
"-dac",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM-Omni project
|
||||
"""Wan2.2 TI2V pipeline topology."""
|
||||
|
||||
from vllm_omni.config.stage_config import (
|
||||
@@ -12,6 +12,7 @@ WAN2_2_TI2V_PIPELINE = PipelineConfig(
|
||||
model_type="wan2_2_ti2v",
|
||||
model_arch="WanPipeline",
|
||||
diffusers_class_name="WanPipeline",
|
||||
diffusers_class_aliases=("WanDMDPipeline",),
|
||||
stages=(
|
||||
StagePipelineConfig(
|
||||
stage_id=0,
|
||||
|
||||
Reference in new issue
Block a user