[XPU][S2V] Optimize Wan2.2 S2V: RoPE refactor + cache_dit enabling (#4062)

Signed-off-by: Chendi Xue <chendi.xue@intel.com>
Co-authored-by: Mengkejiergeli Ba <mengkejiergeli.ba@intel.com>
This commit is contained in:
Chendi.XueandMengkejiergeli Ba authored and GitHub committed 2026-06-05 06:20:16 +08:00
1 parent fccd95b12b
commit 31f882935d
7 files changed
+841 -514

No files matched your search

+1
View File
@@ -145,6 +145,7 @@ The following tables show which models support each feature:
| Model | ⚡TeaCache | ⚡Cache-DiT | 🔀SP (Ulysses & Ring) | 🔀CFG-Parallel | 🔀Tensor-Parallel | Pipeline-Parallel | 🔀HSDP | 💾CPU Offload (Layerwise) | 💾VAE-Patch-Parallel | 💾Quantization | 🔄Step Execution |
|------------------------------|:---------:|:----------:|:---------------------:|:--------------:|:-----------------:|:-----------------:|:------:|:-------------------------:|:--------------------:|:--------------:|:----------------:|
| **Wan2.2** | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ (encode/decode) | ❌ | ❌ |
| **Wan2.2-S2V** | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ (encode/decode) | ❌ | ❌ |
| **Wan2.1-VACE** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ (decode) | ❌ | ❌ |
| **LTX-2** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| **LTX-2.3** | ❌ | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
@@ -16,9 +16,10 @@ Usage:
"""
import argparse
import os
import json
import time
from pathlib import Path
from typing import Any
import torch
@@ -29,6 +30,16 @@ from vllm_omni.outputs import OmniRequestOutput
from vllm_omni.platforms import current_omni_platform
def parse_profiler_config(value: str) -> dict[str, Any]:
try:
config = json.loads(value)
except json.JSONDecodeError as e:
raise argparse.ArgumentTypeError(f"--profiler-config must be valid JSON: {e}") from e
if not isinstance(config, dict):
raise argparse.ArgumentTypeError("--profiler-config must be a JSON object")
return config
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="Generate a talking-head video from a reference image and audio (Wan2.2 S2V)."
@@ -87,6 +98,12 @@ def parse_args() -> argparse.Namespace:
default=3.0,
help="Scheduler flow shift (default: 3.0).",
)
parser.add_argument(
"--boundary-ratio",
type=float,
default=None,
help="Boundary split ratio for low/high DiT stages (S2V). Default varies by model.",
)
parser.add_argument(
"--output",
type=str,
@@ -178,30 +195,10 @@ def parse_args() -> argparse.Namespace:
help="Enable cache-dit summary logging after diffusion forward passes.",
)
parser.add_argument(
"--profile-dir",
type=str,
"--profiler-config",
type=parse_profiler_config,
default=None,
help="Enable torch profiler and save traces to this directory.",
)
parser.add_argument(
"--profile-record-shapes",
action="store_true",
help="Record tensor shapes in profiler (increases trace size).",
)
parser.add_argument(
"--profile-with-stack",
action="store_true",
help="Record stack traces in profiler (increases overhead).",
)
parser.add_argument(
"--profile-with-memory",
action="store_true",
help="Profile memory usage.",
)
parser.add_argument(
"--profile-with-flops",
action="store_true",
help="Estimate FLOPs for operations.",
help='JSON profiler config for torch/cuda profiling, e.g. \'{"profiler":"torch","torch_profiler_dir":"./perf"}\'.',
)
return parser.parse_args()
@@ -239,23 +236,9 @@ def main():
ulysses_degree=args.ulysses_degree,
)
# Check if profiling is requested via CLI args or environment variable
profile_dir = args.profile_dir or os.getenv("VLLM_TORCH_PROFILER_DIR")
profiler_enabled = bool(profile_dir)
profiler_config = None
if profiler_enabled:
from vllm.config import ProfilerConfig
profiler_enabled = args.profiler_config is not None
profiler_config = ProfilerConfig(
profiler="torch",
torch_profiler_dir=profile_dir,
torch_profiler_record_shapes=args.profile_record_shapes,
torch_profiler_with_stack=args.profile_with_stack,
torch_profiler_with_memory=args.profile_with_memory,
torch_profiler_with_flops=args.profile_with_flops,
)
omni = Omni(
omni_kwargs = dict(
model=args.model,
model_class_name="WanS2VPipeline",
flow_shift=args.flow_shift,
@@ -269,8 +252,12 @@ def main():
cache_backend=args.cache_backend,
cache_config=cache_config,
enable_cache_dit_summary=args.enable_cache_dit_summary,
profiler_config=profiler_config,
profiler_config=args.profiler_config,
)
if args.boundary_ratio is not None:
omni_kwargs["boundary_ratio"] = args.boundary_ratio
omni = Omni(**omni_kwargs)
# Print generation configuration
print(f"\n{'=' * 60}")
@@ -325,13 +312,16 @@ def main():
if profiler_enabled:
print("\n[Profiler] Stopping profiler and collecting results...")
profile_results = omni.stop_profile()
if profile_results and isinstance(profile_results, list):
if profile_results and isinstance(profile_results, dict):
traces = profile_results.get("traces", [])
print("\n" + "=" * 60)
print("PROFILING RESULTS:")
for i, result in enumerate(profile_results):
print(f"\nStage {i}:")
if result:
print(f" {result}")
for rank, trace in enumerate(traces):
print(f"\nRank {rank}:")
if trace:
print(f" • Trace: {trace}")
if not traces:
print(" No traces collected.")
print("=" * 60)
else:
print("[Profiler] No valid profiling data returned.")
+220
View File
@@ -242,6 +242,80 @@ def enable_cache_for_wan22(pipeline: Any, cache_config: Any) -> Callable[[int],
return refresh_cache_context
def enable_cache_for_wan22_s2v(pipeline: Any, cache_config: Any) -> Callable[[int], None]:
"""Enable cache-dit for Wan2.2 S2V.
S2V uses a single transformer, but unlike the other Wan2.2 variants its
block loop calls each block as ``block(hidden_states, **kwargs)`` and keeps
the timestep modulation state in ``e`` rather than a second positional
tensor. CacheDiT Pattern_3 matches that contract: cache hidden states only
and pass the remaining conditioning through kwargs unchanged.
The S2V transformer has an ``after_transformer_block`` method that injects
audio embeddings after specific layers. The cached blocks wrapper
(Wan22S2VCachedBlocks._run_block) calls the original internally, so we
permanently replace it with a no-op on the transformer to prevent double
injection from the main forward loop.
"""
db_cache_config = _build_db_cache_config(cache_config)
calibrator_config = None
if cache_config.enable_taylorseer:
taylorseer_order = cache_config.taylorseer_order
calibrator_config = TaylorSeerCalibratorConfig(taylorseer_order=taylorseer_order)
logger.info(f"TaylorSeer enabled with order={taylorseer_order}")
# Save the original after_transformer_block before cache-dit wrapping
transformer = pipeline.transformer
if hasattr(transformer, "after_transformer_block"):
transformer._cache_dit_original_after_transformer_block = transformer.after_transformer_block
Wan22S2VCachedAdapter.apply(
BlockAdapter(
transformer=transformer,
blocks=[transformer.blocks],
forward_pattern=[ForwardPattern.Pattern_3],
params_modifiers=[
ParamsModifier(cache_config=db_cache_config, calibrator_config=calibrator_config),
],
has_separate_cfg=True,
),
cache_config=db_cache_config,
calibrator_config=calibrator_config,
)
# Permanently replace after_transformer_block with a no-op.
# The cached blocks wrapper (Wan22S2VCachedBlocks._run_block) already calls
# the original via _cache_dit_original_after_transformer_block.
def _noop_after_transformer_block(block_idx: int, hidden_states: torch.Tensor) -> torch.Tensor:
return hidden_states
transformer.after_transformer_block = _noop_after_transformer_block
def refresh_cache_context(pipeline: Any, num_inference_steps: int, verbose: bool = True) -> None:
"""Refresh cache context for the S2V transformer."""
if cache_config.scm_steps_mask_policy is None:
cache_dit.refresh_context(
pipeline.transformer,
num_inference_steps=num_inference_steps,
verbose=verbose,
)
else:
cache_dit.refresh_context(
pipeline.transformer,
cache_config=DBCacheConfig().reset(
num_inference_steps=num_inference_steps,
steps_computation_mask=cache_dit.steps_mask(
mask_policy=cache_config.scm_steps_mask_policy,
total_steps=num_inference_steps,
),
steps_computation_policy=cache_config.scm_steps_policy,
),
verbose=verbose,
)
return refresh_cache_context
def enable_cache_for_longcat_image(pipeline: Any, cache_config: Any) -> Callable[[int], None]:
"""Enable cache-dit for LongCatImage pipeline.
@@ -986,6 +1060,151 @@ class BagelCachedBlocks(CachedBlocks_Pattern_0_1_2):
return hidden_states, encoder_hidden_states
class Wan22S2VCachedBlocks(CachedBlocks_Pattern_3_4_5):
"""CacheDiT blocks wrapper that preserves S2V per-layer audio injection."""
def _run_block(self, block_id: int, block: torch.nn.Module, hidden_states: torch.Tensor, *args, **kwargs):
hidden_states = block(hidden_states, *args, **kwargs)
hidden_states, new_encoder_hidden_states = self._process_block_outputs(hidden_states)
original_after_transformer_block = getattr(
self.transformer,
"_cache_dit_original_after_transformer_block",
getattr(self.transformer, "after_transformer_block", None),
)
if original_after_transformer_block is not None:
hidden_states = original_after_transformer_block(block_id, hidden_states)
return hidden_states, new_encoder_hidden_states
def call_blocks(
self,
hidden_states: torch.Tensor,
*args,
**kwargs,
):
new_encoder_hidden_states = None
for block_id, block in enumerate(self.transformer_blocks):
hidden_states, new_encoder_hidden_states = self._run_block(
block_id,
block,
hidden_states,
*args,
**kwargs,
)
return hidden_states, new_encoder_hidden_states
def call_Fn_blocks(
self,
hidden_states: torch.Tensor,
*args,
**kwargs,
):
new_encoder_hidden_states = None
for block_id, block in enumerate(self._Fn_blocks()):
hidden_states, new_encoder_hidden_states = self._run_block(
block_id,
block,
hidden_states,
*args,
**kwargs,
)
return hidden_states, new_encoder_hidden_states
def call_Mn_blocks(
self,
hidden_states: torch.Tensor,
*args,
**kwargs,
):
original_hidden_states = hidden_states
new_encoder_hidden_states = None
start_idx = self.context_manager.Fn_compute_blocks()
for block_id, block in enumerate(self._Mn_blocks(), start=start_idx):
hidden_states, new_encoder_hidden_states = self._run_block(
block_id,
block,
hidden_states,
*args,
**kwargs,
)
hidden_states = hidden_states.contiguous()
hidden_states_residual = hidden_states - original_hidden_states.to(hidden_states.device)
return (
hidden_states,
new_encoder_hidden_states,
hidden_states_residual,
)
def call_Bn_blocks(
self,
hidden_states: torch.Tensor,
*args,
**kwargs,
):
new_encoder_hidden_states = None
if self.context_manager.Bn_compute_blocks() == 0:
return hidden_states, new_encoder_hidden_states
start_idx = len(self.transformer_blocks) - self.context_manager.Bn_compute_blocks()
for block_id, block in enumerate(self._Bn_blocks(), start=start_idx):
hidden_states, new_encoder_hidden_states = self._run_block(
block_id,
block,
hidden_states,
*args,
**kwargs,
)
return hidden_states, new_encoder_hidden_states
class Wan22S2VCachedAdapter(CachedAdapter):
"""CacheDiT adapter that uses Wan22S2VCachedBlocks for S2V audio injection.
Only overrides collect_unified_blocks to use Wan22S2VCachedBlocks (which
calls after_transformer_block per-layer internally). The base class
mock_transformer handles the forward wrapping — after_transformer_block is
permanently replaced with a no-op in enable_cache_for_wan22_s2v() to prevent
double injection.
"""
@classmethod
def collect_unified_blocks(
cls,
block_adapter: BlockAdapter,
contexts_kwargs: list[dict],
) -> list[dict[str, torch.nn.ModuleList]]:
BlockAdapter.assert_normalized(block_adapter)
total_cached_blocks: list[dict[str, torch.nn.ModuleList]] = []
assert hasattr(block_adapter.pipe, "_context_manager")
for i in range(len(block_adapter.transformer)):
unified_blocks_bind_context = {}
for j in range(len(block_adapter.blocks[i])):
cache_config: BasicCacheConfig = contexts_kwargs[i * len(block_adapter.blocks[i]) + j]["cache_config"]
unified_blocks_bind_context[block_adapter.unique_blocks_name[i][j]] = torch.nn.ModuleList(
[
Wan22S2VCachedBlocks(
block_adapter.blocks[i][j],
transformer=block_adapter.transformer[i],
forward_pattern=block_adapter.forward_pattern[i][j],
check_forward_pattern=block_adapter.check_forward_pattern,
check_num_outputs=block_adapter.check_num_outputs,
cache_prefix=block_adapter.blocks_name[i][j],
cache_context=block_adapter.unique_blocks_name[i][j],
context_manager=block_adapter.pipe._context_manager,
cache_type=cache_config.cache_type,
)
]
)
total_cached_blocks.append(unified_blocks_bind_context)
return total_cached_blocks
class BagelCachedAdapter(CachedAdapter):
"""
Custom CachedAdapter for Bagel that uses BagelCachedContextManager and BagelCachedBlocks.
@@ -1808,6 +2027,7 @@ CUSTOM_DIT_ENABLERS.update(
{
"Wan22Pipeline": enable_cache_for_wan22,
"Wan22I2VPipeline": enable_cache_for_wan22,
"Wan22S2VPipeline": enable_cache_for_wan22_s2v,
"HunyuanImage3Pipeline": enable_cache_for_hunyuan_image3,
"FluxPipeline": enable_cache_for_flux,
"Flux2KleinPipeline": enable_cache_for_flux2_klein,
+5
View File
@@ -96,6 +96,8 @@ def _move_tensor_tree_to_cpu(value: object) -> object:
def get_dummy_run_num_frames(model_class_name: str, supports_audio_input: bool) -> int:
"""Get num_frames for the dummy warmup run. Returns 0 to skip warmup."""
model_cls = DiffusionModelRegistry._try_load_model_cls(model_class_name)
if model_cls is not None and hasattr(model_cls, "dummy_run_num_frames"):
return int(getattr(model_cls, "dummy_run_num_frames"))
@@ -722,6 +724,9 @@ class DiffusionEngine:
prompt.setdefault("multi_modal_data", {})["audio"] = dummy_audio
num_frames = get_dummy_run_num_frames(self.od_config.model_class_name, supports_audio_input)
if num_frames <= 0:
logger.info("Skipping dummy warmup run (num_frames=0)")
return
req = OmniDiffusionRequest(
prompts=[prompt],
request_id=DUMMY_DIFFUSION_REQUEST_ID,
+232
View File
@@ -5,6 +5,7 @@ from einops import rearrange, repeat
from vllm.logger import init_logger
from vllm_omni.diffusion.layers.custom_op import CustomOp
from vllm_omni.platforms import current_omni_platform
logger = init_logger(__name__)
@@ -300,3 +301,234 @@ def apply_rope_to_qk(
query = rope(query, cos, sin)
key = rope(key, cos, sin)
return query, key
class WanS2VRotaryPosEmbed(torch.nn.Module):
"""Precompute complex-valued RoPE embeddings for S2V multi-grid positions.
Owns the base frequency buffer and provides forward() to compute position
embeddings given hidden_states (for shape) and grid_sizes.
"""
def __init__(self, num_heads: int, head_dim: int, max_seq_len: int = 1024):
super().__init__()
self.num_heads = num_heads
self.head_dim = head_dim
d = head_dim
freqs = torch.cat(
[
self._rope_params(max_seq_len, d - 4 * (d // 6)),
self._rope_params(max_seq_len, 2 * (d // 6)),
self._rope_params(max_seq_len, 2 * (d // 6)),
],
dim=1,
)
self.register_buffer("freqs", freqs.to(torch.complex64), persistent=False)
@staticmethod
@torch.amp.autocast(current_omni_platform.device_type, enabled=False)
def _rope_params(max_seq_len, dim, theta=10000):
if dim % 2 != 0:
raise ValueError(f"dim ({dim}) must be even")
freqs = torch.outer(
torch.arange(max_seq_len), 1.0 / torch.pow(theta, torch.arange(0, dim, 2).to(torch.float64).div(dim))
)
return torch.polar(torch.ones_like(freqs), freqs)
def forward(
self,
hidden_states: torch.Tensor,
grid_sizes: list,
trainable_freqs: torch.Tensor | None = None,
) -> torch.Tensor:
"""Precompute RoPE embeddings for the given grid layout.
Args:
hidden_states: Tensor [B, S, ...] (used for batch/seq shape and device)
grid_sizes: Grid specification (list of [offsets, sizes, totals])
trainable_freqs: Optional trainable frequency overrides for t_f < 0
Returns:
Complex tensor [B, S, 1, head_dim//2] of precomputed position embeddings
"""
b, s = hidden_states.shape[0], hidden_states.shape[1]
c = self.head_dim // 2
device = hidden_states.device
freqs = self.freqs.to(device)
if trainable_freqs is not None:
freqs_input = [freqs, trainable_freqs]
else:
freqs_input = freqs
if isinstance(freqs_input, list):
trainable_f = freqs_input[1]
freqs_split = freqs_input[0]
else:
trainable_f = None
freqs_split = freqs_input
freqs_split = freqs_split.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
output = torch.empty((b, s, 1, c), device=device, dtype=torch.complex64)
seq_bucket = [0]
if not isinstance(grid_sizes, list):
grid_sizes = [grid_sizes]
for g in grid_sizes:
if not isinstance(g, list):
g = [torch.zeros_like(g), g]
batch_size = g[0].shape[0]
for i in range(batch_size):
f_o, h_o, w_o = g[0][i]
f, h, w = g[1][i]
t_f, t_h, t_w = g[2][i]
seq_f, seq_h, seq_w = f - f_o, h - h_o, w - w_o
seq_len = int(seq_f * seq_h * seq_w)
if seq_len > 0:
if t_f > 0:
assert f_o * f >= 0 and h_o * h >= 0 and w_o * w >= 0
seq_f_int = int(seq_f)
seq_h_int = int(seq_h)
seq_w_int = int(seq_w)
if f_o >= 0:
f_sam = torch.linspace(int(f_o), int(t_f + f_o) - 1, seq_f_int, device=device).long()
else:
f_sam = torch.linspace(int(-f_o), int(-t_f - f_o) + 1, seq_f_int, device=device).long()
h_sam = torch.linspace(int(h_o), int(t_h + h_o) - 1, seq_h_int, device=device).long()
w_sam = torch.linspace(int(w_o), int(t_w + w_o) - 1, seq_w_int, device=device).long()
freqs_0 = freqs_split[0][f_sam] if f_o >= 0 else freqs_split[0][f_sam].conj()
freqs_0 = freqs_0.view(seq_f_int, 1, 1, -1)
freqs_i = torch.cat(
[
freqs_0.expand(seq_f_int, seq_h_int, seq_w_int, -1),
freqs_split[1][h_sam]
.view(1, seq_h_int, 1, -1)
.expand(seq_f_int, seq_h_int, seq_w_int, -1),
freqs_split[2][w_sam]
.view(1, 1, seq_w_int, -1)
.expand(seq_f_int, seq_h_int, seq_w_int, -1),
],
dim=-1,
).reshape(seq_len, 1, -1)
elif t_f < 0:
freqs_i = trainable_f.unsqueeze(1)
output[i, seq_bucket[-1] : seq_bucket[-1] + seq_len] = freqs_i
seq_bucket.append(seq_bucket[-1] + seq_len)
return output
class RotaryEmbeddingWanS2V(RotaryEmbeddingWan):
"""Apply RoPE using precomputed complex freqs for Wan S2V main transformer.
Converts complex freqs (from WanS2VRotaryPosEmbed) to cos/sin and delegates
to RotaryEmbeddingWan for platform-optimized application (float32 kernel).
Under TP, freqs has 1 head — broadcasts automatically via cos/sin.
"""
def __init__(self) -> None:
super().__init__(is_neox_style=False, half_head_dim=True)
def forward(self, x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
freqs_sliced = freqs[:, : x.size(1)]
cos = freqs_sliced.real.to(x.dtype)
sin = freqs_sliced.imag.to(x.dtype)
return super().forward(x, cos, sin)
class RotaryEmbeddingS2VGrid(torch.nn.Module):
"""Grid-based RoPE for S2V motioner/init attention.
Applies complex-valued rotary embeddings using 3D grid sampling
(frame, height, width). Used by SimpleSelfAttention, SwinSelfAttention,
CausalSelfAttention in motioner blocks.
"""
@staticmethod
@torch.amp.autocast(current_omni_platform.device_type, enabled=False)
def precompute(
seq_len: int, num_heads: int, head_dim: int, grid_sizes, freqs: torch.Tensor, device: torch.device, start=None
) -> torch.Tensor:
"""Precompute position frequency tensor from grid specification.
Returns a complex tensor that can be reused across layers via apply_precomputed().
"""
c = head_dim // 2
trainable_freqs = None
if isinstance(freqs, list):
trainable_freqs = freqs[1]
freqs = freqs[0]
freqs = freqs.to(device)
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
if not isinstance(grid_sizes, list):
grid_sizes = [grid_sizes]
batch_size = grid_sizes[0][0].shape[0] if isinstance(grid_sizes[0], list) else grid_sizes[0].shape[0]
precomputed = torch.empty((batch_size, seq_len, 1, c), device=device, dtype=torch.complex64)
seq_bucket = [0]
for g in grid_sizes:
if not isinstance(g, list):
g = [torch.zeros_like(g), g]
g_batch_size = g[0].shape[0]
for i in range(g_batch_size):
if start is None:
f_o, h_o, w_o = g[0][i]
else:
f_o, h_o, w_o = start[i]
f, h, w = g[1][i]
t_f, t_h, t_w = g[2][i]
seq_f, seq_h, seq_w = f - f_o, h - h_o, w - w_o
seg_len = int(seq_f * seq_h * seq_w)
if seg_len > 0:
if t_f > 0:
seq_f_int = int(seq_f)
seq_h_int = int(seq_h)
seq_w_int = int(seq_w)
if f_o >= 0:
f_sam = torch.linspace(int(f_o), int(t_f + f_o) - 1, seq_f_int, device=device).long()
else:
f_sam = torch.linspace(int(-f_o), int(-t_f - f_o) + 1, seq_f_int, device=device).long()
h_sam = torch.linspace(int(h_o), int(t_h + h_o) - 1, seq_h_int, device=device).long()
w_sam = torch.linspace(int(w_o), int(t_w + w_o) - 1, seq_w_int, device=device).long()
freqs_0 = freqs[0][f_sam] if f_o >= 0 else freqs[0][f_sam].conj()
freqs_0 = freqs_0.view(seq_f_int, 1, 1, -1)
freqs_i = torch.cat(
[
freqs_0.expand(seq_f_int, seq_h_int, seq_w_int, -1),
freqs[1][h_sam].view(1, seq_h_int, 1, -1).expand(seq_f_int, seq_h_int, seq_w_int, -1),
freqs[2][w_sam].view(1, 1, seq_w_int, -1).expand(seq_f_int, seq_h_int, seq_w_int, -1),
],
dim=-1,
).reshape(seg_len, 1, -1)
elif t_f < 0:
freqs_i = trainable_freqs.unsqueeze(1)
precomputed[i, seq_bucket[-1] : seq_bucket[-1] + seg_len] = freqs_i
seq_bucket.append(seq_bucket[-1] + seg_len)
return precomputed
@staticmethod
@torch.amp.autocast(current_omni_platform.device_type, enabled=False)
def apply_precomputed(x: torch.Tensor, precomputed_freqs: torch.Tensor) -> torch.Tensor:
"""Apply precomputed position frequencies to input tensor."""
n = x.size(2)
input_dtype = x.dtype
seq_len = x.size(1)
precomputed_freqs = precomputed_freqs[:, :seq_len]
x_c = torch.view_as_complex(x.to(torch.float64).reshape(x.size(0), seq_len, n, -1, 2))
x_c = torch.view_as_real(x_c * precomputed_freqs).flatten(3)
return x_c.reshape(x.size(0), seq_len, n, -1).to(input_dtype)
@staticmethod
@torch.amp.autocast(current_omni_platform.device_type, enabled=False)
def forward(x: torch.Tensor, grid_sizes, freqs: torch.Tensor, start=None) -> torch.Tensor:
n, d = x.size(2), x.size(3)
precomputed = RotaryEmbeddingS2VGrid.precompute(x.size(1), n, d, grid_sizes, freqs, x.device, start=start)
return RotaryEmbeddingS2VGrid.apply_precomputed(x, precomputed)
@@ -13,7 +13,7 @@ import logging
import math
import os
from collections.abc import Iterable
from typing import Any
from typing import Any, ClassVar
import numpy as np
import PIL.Image
@@ -482,6 +482,7 @@ class Wan22S2VPipeline(
_DEFAULT_MOTION_FRAMES = 73
_DEFAULT_INFER_FRAMES = 80
_DEFAULT_FPS = 16
dummy_run_num_frames: ClassVar[int] = 0
def __init__(
self,
@@ -929,21 +930,19 @@ class Wan22S2VPipeline(
def predict_noise(self, current_model: nn.Module | None = None, **kwargs: Any) -> torch.Tensor:
"""Forward pass through the S2V transformer to predict noise.
The S2V model returns a list of tensors (one per batch element).
We take the first element since S2V processes one sample at a time.
With return_dict=False, the model returns (output,) where output is [B, C, T, H, W].
We squeeze batch dim since S2V processes one sample at a time.
"""
if current_model is None:
current_model = self.transformer
# WanModel_S2V's norm layers compute in float32; autocast ensures
# the float32→bfloat16 casts happen automatically (matching the
# original Wan2.2 inference path).
param_dtype = next(current_model.parameters()).dtype
with torch.amp.autocast(self.device.type, dtype=param_dtype):
result = current_model(**kwargs)
# WanModel_S2V.forward returns a list of tensors
if isinstance(result, list):
return result[0]
return result[0] if isinstance(result, tuple) else result
if isinstance(result, tuple):
return result[0].squeeze(0)
return result.sample.squeeze(0)
def diffuse(
self,
@@ -996,32 +995,29 @@ class Wan22S2VPipeline(
latent_model_input = [latents.to(device)]
timestep = t.unsqueeze(0).to(device) if t.dim() == 0 else t.to(device)
# -- Positive (conditional) prediction kwargs --
positive_kwargs = {
"x": latent_model_input,
"t": timestep,
"context": prompt_embeds[0:1],
"seq_len": max_seq_len,
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": prompt_embeds[0:1],
"cond_states": cond_latents,
"motion_latents": input_motion_latents,
"ref_latents": ref_latents,
"motion_frames": motion_frames,
"drop_motion_frames": drop_first_motion,
"audio_emb": positive_audio_emb,
"encoder_hidden_states_audio": positive_audio_emb,
"return_dict": False,
}
if do_true_cfg:
negative_kwargs = {
"x": latent_model_input,
"t": timestep,
"context": negative_prompt_embeds[0:1],
"seq_len": max_seq_len,
"hidden_states": latent_model_input,
"timestep": timestep,
"encoder_hidden_states": negative_prompt_embeds[0:1],
"cond_states": cond_latents,
"motion_latents": input_motion_latents,
"ref_latents": ref_latents,
"motion_frames": motion_frames,
"drop_motion_frames": drop_first_motion,
"audio_emb": negative_audio_emb,
"encoder_hidden_states_audio": negative_audio_emb,
"return_dict": False,
}
else:
negative_kwargs = None
@@ -1174,7 +1170,7 @@ class Wan22S2VPipeline(
ref_latents = self.encode_ref_image(image, height, width, device=device).to(dtype=dtype)
# ---- 4. Initial motion latents (zeros) ----
motion_frames = self.motion_frames
motion_frames = min(self.motion_frames, infer_frames - 1)
motion_pixels = torch.zeros(
[1, 3, motion_frames, height, width],
dtype=dtype,
@@ -1288,6 +1284,10 @@ class Wan22S2VPipeline(
)
# ---- Decode this clip ----
if self.od_config.enable_cpu_offload:
self.transformer.to("cpu")
current_omni_platform.empty_cache()
latents_for_decode = latents.unsqueeze(0) # [1, C, T, H, W]
if not (drop_first_motion and r == 0):
decode_latents = torch.cat([motion_latents, latents_for_decode], dim=2)
File diff suppressed because it is too large. Load diff