[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:
1 parent
fccd95b12b
commit
31f882935d
7 files changed
+841
-514
No files matched your search
@@ -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
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
Reference in new issue
Block a user