[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:
bjf-frzandHongsheng Liu authored and GitHub committed 2026-08-24 14:48:31 +08:00
1 parent 3d035bfa19
commit 072bfc02dd
28 files changed
+1316 -107

No files matched your search

@@ -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"
+35 -7
View File
@@ -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]
+10 -1
View File
@@ -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)