Files
nvlabs--longlive/pipeline/self_forcing_training.py
T
2026-07-13 12:31:40 +08:00

775 lines
36 KiB
Python

# Adopted from https://github.com/guandeh17/Self-Forcing
# SPDX-License-Identifier: Apache-2.0
from utils.wan_5b_wrapper import WanDiffusionWrapper
from utils.scheduler import SchedulerInterface
from utils.i2v_conditioning import (
_overwrite_i2v_context,
_zero_i2v_context_timestep,
)
from typing import List, Optional, Tuple
import torch
import torch.distributed as dist
from torchvision.io import write_video
class SelfForcingTrainingPipeline:
def __init__(self,
scheduler: SchedulerInterface,
generator: WanDiffusionWrapper,
denoising_step_list: Optional[List[int]] = None,
num_frame_per_block=3,
independent_first_frame: bool = False,
same_step_across_blocks: bool = False,
last_step_only: bool = False,
num_max_frames: int = 21,
context_noise: int = 0,
sampling_steps: Optional[int] = None,
local_attn_size: int = -1,
sink_size: int = 0,
multi_shot_sink: bool = False,
scene_cut_prefix: str = "[SCENE_CUT]",
multi_shot_rope_offset: float = 0.0,
frame_seq_length: Optional[int] = None,
**kwargs):
super().__init__()
self.scheduler = scheduler
self.generator = generator
if denoising_step_list is None:
if sampling_steps is None:
raise ValueError("sampling_steps is required when denoising_step_list is not provided")
denoising_step_list = self._build_default_denoising_step_list(sampling_steps)
self.denoising_step_list = torch.as_tensor(denoising_step_list, dtype=torch.long)
if self.denoising_step_list[-1] == 0:
self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
# Wan specific hyperparameters
self.num_transformer_blocks = self.generator.model.num_layers
if not dist.is_initialized() or dist.get_rank() == 0:
print(f"num_transformer_blocks: {self.num_transformer_blocks}")
if frame_seq_length is not None:
self.frame_seq_length = frame_seq_length
else:
self.frame_seq_length = 880
if not dist.is_initialized() or dist.get_rank() == 0:
print(f"frame_seq_length: {self.frame_seq_length}")
self.num_frame_per_block = num_frame_per_block
self.context_noise = context_noise
self.i2v = False
self.kv_cache1 = None
self.kv_cache2 = None
self.crossattn_cache = None
self.independent_first_frame = independent_first_frame
self.same_step_across_blocks = same_step_across_blocks
self.last_step_only = last_step_only
self.sampling_steps = sampling_steps
self.local_attn_size = local_attn_size
self.sink_size = sink_size
self.multi_shot_sink = multi_shot_sink
self.global_sink_size = sink_size if multi_shot_sink else 0
self.scene_cut_prefix = scene_cut_prefix
self.multi_shot_rope_offset = multi_shot_rope_offset
self.kv_cache_size = num_max_frames * self.frame_seq_length
if not dist.is_initialized() or dist.get_rank() == 0:
print(
f"[SelfForcingTrainingPipeline] kv_cache_size={self.kv_cache_size}, "
f"local_attn_size={local_attn_size}, sink_size={sink_size}, "
f"auto_global_sink_size={self.global_sink_size}, multi_shot_sink={multi_shot_sink}"
)
def _build_default_denoising_step_list(self, sampling_steps):
shift = getattr(self.scheduler, "shift", 1.0)
num_train_timesteps = getattr(self.scheduler, "num_train_timesteps", 1000)
sigmas = torch.linspace(1.0, 0.0, int(sampling_steps) + 1, dtype=torch.float32)[:-1]
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
return torch.cat([
(sigmas * num_train_timesteps).to(torch.long),
torch.zeros(1, dtype=torch.long),
])
def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
rank = dist.get_rank() if dist.is_initialized() else 0
if rank == 0:
# Generate random indices
indices = torch.randint(
low=0,
high=num_denoising_steps,
size=(num_blocks,),
device=device
)
if self.last_step_only:
indices = torch.ones_like(indices) * (num_denoising_steps - 1)
else:
indices = torch.empty(num_blocks, dtype=torch.long, device=device)
if dist.is_initialized():
dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
return indices.tolist()
def generate_chunk_with_cache(
self,
noise: torch.Tensor,
conditional_dict: dict,
*,
current_start_frame: int = 0,
requires_grad: bool = True,
return_sim_step: bool = False,
) -> Tuple[torch.Tensor, Optional[int], Optional[int]]:
"""
Chunk generation method tailored for sequential training
Args:
noise: noise tensor for a single chunk [batch_size, chunk_frames, C, H, W]
conditional_dict: dictionary of conditional information
kv_cache: externally provided KV cache (defaults to self.kv_cache1 if None)
crossattn_cache: externally provided cross-attention cache (defaults to self.crossattn_cache if None)
current_start_frame: start frame index of the chunk in the full sequence
requires_grad: whether gradients are required
return_sim_step: whether to return simulation step info
Returns:
output: generated chunk [batch_size, chunk_frames, C, H, W]
denoised_timestep_from: starting denoise timestep
denoised_timestep_to: ending denoise timestep
"""
batch_size, chunk_frames, num_channels, height, width = noise.shape
# Compute block configuration
if not self.independent_first_frame or chunk_frames % self.num_frame_per_block == 0:
assert chunk_frames % self.num_frame_per_block == 0
num_blocks = chunk_frames // self.num_frame_per_block
all_num_frames = [self.num_frame_per_block] * num_blocks
else:
# Handle the case of an independent first frame
assert (chunk_frames - 1) % self.num_frame_per_block == 0
num_blocks = (chunk_frames - 1) // self.num_frame_per_block
all_num_frames = [1] + [self.num_frame_per_block] * num_blocks
# Prepare output tensor
output = torch.zeros_like(noise)
# Build per-block conditional dicts for prompt switching
prompt_embeds = conditional_dict["prompt_embeds"]
num_prompts = prompt_embeds.shape[0]
num_segments = num_prompts // batch_size
if num_segments > 1:
prompt_embeds_per_block = prompt_embeds.reshape(
batch_size, num_segments, *prompt_embeds.shape[1:])
conditional_dict_list = [
{"prompt_embeds": prompt_embeds_per_block[:, i]}
for i in range(num_segments)
]
else:
conditional_dict_list = None
# Randomly select denoising steps (synced across ranks)
num_denoising_steps = len(self.denoising_step_list)
exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
# Determine gradient-enabled range — disable everywhere when requires_grad=False
if not requires_grad:
start_gradient_frame_index = chunk_frames # Out of range: no gradients anywhere
else:
start_gradient_frame_index = 0
if not dist.is_initialized() or dist.get_rank() == 0:
print(f"[SeqTrain-Pipeline] start_gradient_frame_index={start_gradient_frame_index}, chunk_frames={chunk_frames}")
# Generate block by block
local_start_frame = 0
for block_index, current_num_frames in enumerate(all_num_frames):
if conditional_dict_list is not None:
block_cond = conditional_dict_list[min(block_index, len(conditional_dict_list) - 1)]
for cache_idx in range(self.num_transformer_blocks):
self.crossattn_cache[cache_idx]["is_init"] = False
else:
block_cond = conditional_dict
noisy_input = noise[:, local_start_frame:local_start_frame + current_num_frames]
# Spatial denoising loop
for step_idx, current_timestep in enumerate(self.denoising_step_list):
exit_flag = (
step_idx == exit_flags[0]
if self.same_step_across_blocks
else step_idx == exit_flags[block_index]
)
timestep = torch.ones(
[batch_size, current_num_frames],
device=noise.device,
dtype=torch.int64
) * current_timestep
if not exit_flag:
# Intermediate steps: no gradients
with torch.no_grad():
_, denoised_pred = self.generator(
noisy_image_or_video=noisy_input,
conditional_dict=block_cond,
timestep=timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=(current_start_frame + local_start_frame) * self.frame_seq_length,
)
# Add noise for the next step
if step_idx < len(self.denoising_step_list) - 1:
next_timestep = self.denoising_step_list[step_idx + 1]
noisy_input = self.scheduler.add_noise(
denoised_pred.flatten(0, 1),
torch.randn_like(denoised_pred.flatten(0, 1)),
next_timestep * torch.ones(
[batch_size * current_num_frames], device=noise.device, dtype=torch.long
),
).unflatten(0, denoised_pred.shape[:2])
else:
# Final step may require gradients
enable_grad = local_start_frame >= start_gradient_frame_index
context_manager = torch.enable_grad() if enable_grad else torch.no_grad()
with context_manager:
_, denoised_pred = self.generator(
noisy_image_or_video=noisy_input,
conditional_dict=block_cond,
timestep=timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=(current_start_frame + local_start_frame) * self.frame_seq_length,
)
break
# Record output
output[:, local_start_frame:local_start_frame + current_num_frames] = denoised_pred
# Update cache with context noise
context_timestep = torch.ones_like(timestep) * self.context_noise
context_noisy = self.scheduler.add_noise(
denoised_pred.flatten(0, 1),
torch.randn_like(denoised_pred.flatten(0, 1)),
context_timestep.flatten(0, 1),
).unflatten(0, denoised_pred.shape[:2])
with torch.no_grad():
self.generator(
noisy_image_or_video=context_noisy,
conditional_dict=block_cond,
timestep=context_timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=(current_start_frame + local_start_frame) * self.frame_seq_length,
)
local_start_frame += current_num_frames
# Compute returned timestep information
if not self.same_step_across_blocks:
denoised_timestep_from, denoised_timestep_to = None, None
elif exit_flags[0] == len(self.denoising_step_list) - 1:
denoised_timestep_to = 0
denoised_timestep_from = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0
).item()
else:
denoised_timestep_to = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0
).item()
denoised_timestep_from = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0
).item()
if return_sim_step:
return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
return output, denoised_timestep_from, denoised_timestep_to
def inference_with_trajectory(
self,
noise: torch.Tensor,
initial_latent: Optional[torch.Tensor] = None,
return_sim_step: bool = False,
slice_last_frames: int = 21,
sampling_steps: Optional[int] = None,
**conditional_dict
) -> torch.Tensor:
# Apply local_attn_size / sink_size overrides before inference,
# matching what CausalDiffusionInferencePipeline does.
prev_state = self._apply_attn_overrides()
try:
return self._inference_with_trajectory_inner(
noise=noise,
initial_latent=initial_latent,
return_sim_step=return_sim_step,
slice_last_frames=slice_last_frames,
sampling_steps=sampling_steps,
**conditional_dict,
)
finally:
self._restore_attn_overrides(prev_state)
def _apply_attn_overrides(self):
"""Save current model attention state and apply pipeline overrides."""
model = self.generator.model
prev = {
"local_attn_size": getattr(model, "local_attn_size", -1),
"rope_temporal_offset": getattr(model, "rope_temporal_offset", 0.0),
"max_attention_sizes": {},
"sink_sizes": {},
"global_sink_sizes": {},
}
for name, module in model.named_modules():
if hasattr(module, "max_attention_size"):
prev["max_attention_sizes"][name] = module.max_attention_size
if hasattr(module, "sink_size"):
prev["sink_sizes"][name] = module.sink_size
if hasattr(module, "global_sink_size"):
prev["global_sink_sizes"][name] = module.global_sink_size
model.local_attn_size = self.local_attn_size
model.rope_temporal_offset = 0.0
self._set_all_modules_max_attention_size(self.local_attn_size)
if self.sink_size is not None and self.sink_size >= 0:
self._set_all_modules_sink_size(self.sink_size)
self._set_all_modules_global_sink_size(self.global_sink_size)
return prev
def _restore_attn_overrides(self, prev):
"""Restore model attention state saved by _apply_attn_overrides."""
model = self.generator.model
model.local_attn_size = prev["local_attn_size"]
model.rope_temporal_offset = prev["rope_temporal_offset"]
for name, module in model.named_modules():
if name in prev["max_attention_sizes"]:
try:
module.max_attention_size = prev["max_attention_sizes"][name]
except Exception:
pass
if name in prev["sink_sizes"]:
try:
module.sink_size = prev["sink_sizes"][name]
except Exception:
pass
if name in prev["global_sink_sizes"]:
try:
module.global_sink_size = prev["global_sink_sizes"][name]
except Exception:
pass
@staticmethod
def _is_scene_cut_from_mask(scene_cut_mask, block_index: int) -> bool:
if scene_cut_mask is None or block_index <= 0:
return False
if block_index >= len(scene_cut_mask):
return False
value = scene_cut_mask[block_index]
if torch.is_tensor(value):
return bool(value.item())
return bool(value)
def _set_all_modules_sink_size(self, sink_size_value: int):
"""Override sink_size on all submodules that define it."""
model = self.generator.model
if hasattr(model, "sink_size"):
model.sink_size = sink_size_value
for _name, module in model.named_modules():
if hasattr(module, "sink_size"):
try:
module.sink_size = sink_size_value
except Exception:
pass
def _set_all_modules_global_sink_size(self, value: int):
"""Override global_sink_size on all submodules; create the attribute if missing."""
setattr(self.generator.model, "global_sink_size", value)
for _, module in self.generator.model.named_modules():
try:
setattr(module, "global_sink_size", value)
except Exception:
pass
def _pin_current_chunk(self, kv_cache, current_num_frames):
"""Mark the current chunk's buffer position as pinned for multi-shot sink.
The pinned region REPLACES the original sink on the next rolling event.
No data is copied here — relocation happens inside the attention layer
during rolling, ensuring zero duplication.
"""
chunk_tokens = current_num_frames * self.frame_seq_length
pin_len = min(self.sink_size * self.frame_seq_length, chunk_tokens)
for block_cache in kv_cache:
local_end = block_cache["local_end_index"].item()
chunk_start = local_end - chunk_tokens
block_cache["pinned_start"].fill_(chunk_start)
block_cache["pinned_len"].fill_(pin_len)
def _inference_with_trajectory_inner(
self,
noise: torch.Tensor,
initial_latent: Optional[torch.Tensor] = None,
return_sim_step: bool = False,
slice_last_frames: int = 21,
sampling_steps: Optional[int] = None,
**conditional_dict
) -> torch.Tensor:
from wan_5b.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
batch_size, num_frames, num_channels, height, width = noise.shape
num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
clamp_i2v_first_chunk = self.independent_first_frame and initial_latent is not None
if clamp_i2v_first_chunk and num_input_frames != 1:
raise ValueError(
f"i2v first-chunk clamp expects one conditioning latent frame, got {num_input_frames}."
)
if not self.independent_first_frame or clamp_i2v_first_chunk:
# If the first frame is independent and the first frame is provided, then the number of frames in the
# noise should still be a multiple of num_frame_per_block
assert num_frames % self.num_frame_per_block == 0
num_blocks = num_frames // self.num_frame_per_block
else:
# Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
assert (num_frames - 1) % self.num_frame_per_block == 0
num_blocks = (num_frames - 1) // self.num_frame_per_block
num_output_frames = (
num_frames if clamp_i2v_first_chunk else num_frames + num_input_frames
)
output = torch.zeros(
[batch_size, num_output_frames, num_channels, height, width],
device=noise.device,
dtype=noise.dtype
)
# Step 1: Initialize KV cache to all zeros
self._initialize_kv_cache(
batch_size=batch_size, dtype=noise.dtype, device=noise.device
)
self._initialize_crossattn_cache(
batch_size=batch_size, dtype=noise.dtype, device=noise.device
)
# Build per-block conditional dicts for prompt switching
prompt_embeds = conditional_dict["prompt_embeds"]
num_prompts = prompt_embeds.shape[0]
num_segments = num_prompts // batch_size
if num_segments > 1:
prompt_embeds_per_block = prompt_embeds.reshape(
batch_size, num_segments, *prompt_embeds.shape[1:])
conditional_dict_list = [
{"prompt_embeds": prompt_embeds_per_block[:, i]}
for i in range(num_segments)
]
else:
conditional_dict_list = None
# Step 2: Cache context feature
current_start_frame = 0
if initial_latent is not None and not clamp_i2v_first_chunk:
timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
# Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
output[:, :1] = initial_latent
init_cond = conditional_dict_list[0] if conditional_dict_list is not None else conditional_dict
with torch.no_grad():
self.generator(
noisy_image_or_video=initial_latent,
conditional_dict=init_cond,
timestep=timestep * 0,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length
)
current_start_frame += 1
# Step 3: Temporal denoising loop
all_num_frames = [self.num_frame_per_block] * num_blocks
if self.independent_first_frame and initial_latent is None:
all_num_frames = [1] + all_num_frames
# --- UniPC scheduler setup ---
# Priority: function arg > pipeline attribute > len(denoising_step_list)
if sampling_steps is None:
sampling_steps = self.sampling_steps if self.sampling_steps is not None else len(self.denoising_step_list)
shift = self.scheduler.shift
num_train_timesteps = self.scheduler.num_train_timesteps
ref_scheduler = FlowUniPCMultistepScheduler(
num_train_timesteps=num_train_timesteps, shift=1, use_dynamic_shifting=False)
ref_scheduler.set_timesteps(sampling_steps, device=noise.device, shift=shift)
unipc_timesteps = ref_scheduler.timesteps
num_denoising_steps = len(unipc_timesteps)
exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
if slice_last_frames == -1:
# -1 means keep full sequence trainable.
start_gradient_frame_index = 0
else:
start_gradient_frame_index = num_output_frames - slice_last_frames
scene_cut_mask = conditional_dict.pop("scene_cut_mask", None)
current_shot_index = 0
phi = self.multi_shot_rope_offset
self.generator.model.rope_temporal_offset = 0.0
grad_enable_mask = torch.zeros((batch_size, sum(all_num_frames)), dtype=torch.bool)
for block_index, current_num_frames in enumerate(all_num_frames):
if phi != 0.0 and self._is_scene_cut_from_mask(scene_cut_mask, block_index):
current_shot_index += 1
self.generator.model.rope_temporal_offset = current_shot_index * phi
if not dist.is_initialized() or dist.get_rank() == 0:
print(
f"[training] multi-shot RoPE: shot_index={current_shot_index}, "
f"temporal_offset={self.generator.model.rope_temporal_offset:.4f}"
)
if conditional_dict_list is not None:
block_cond = conditional_dict_list[min(block_index, len(conditional_dict_list) - 1)]
for cache_idx in range(self.num_transformer_blocks):
self.crossattn_cache[cache_idx]["is_init"] = False
else:
block_cond = conditional_dict
first_i2v_block = clamp_i2v_first_chunk and block_index == 0
noise_start_frame = (
current_start_frame
if clamp_i2v_first_chunk
else current_start_frame - num_input_frames
)
latents = noise[
:,
noise_start_frame:noise_start_frame + current_num_frames,
]
# re-init scheduler per chunk (internal state is consumed during stepping)
sample_scheduler = FlowUniPCMultistepScheduler(
num_train_timesteps=num_train_timesteps, shift=1, use_dynamic_shifting=False)
sample_scheduler.set_timesteps(sampling_steps, device=noise.device, shift=shift)
# Step 3.1: Spatial denoising loop (UniPC multi-step)
for index, t in enumerate(sample_scheduler.timesteps):
if self.same_step_across_blocks:
exit_flag = (index == exit_flags[0])
else:
exit_flag = (index == exit_flags[block_index])
timestep = t * torch.ones(
[batch_size, current_num_frames],
device=noise.device,
dtype=torch.float32)
if first_i2v_block:
latents = _overwrite_i2v_context(
latents, initial_latent, num_input_frames
)
timestep = _zero_i2v_context_timestep(
timestep, num_input_frames
)
if not exit_flag:
with torch.no_grad():
flow_pred, _ = self.generator(
noisy_image_or_video=latents,
conditional_dict=block_cond,
timestep=timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length
)
latents = sample_scheduler.step(
flow_pred, t, latents, return_dict=False)[0]
if first_i2v_block:
latents = _overwrite_i2v_context(
latents, initial_latent, num_input_frames
)
else:
if current_start_frame < start_gradient_frame_index:
grad_enable_mask[:, current_start_frame:current_start_frame + current_num_frames] = False
with torch.no_grad():
flow_pred, denoised_pred = self.generator(
noisy_image_or_video=latents,
conditional_dict=block_cond,
timestep=timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length
)
else:
grad_enable_mask[:, current_start_frame:current_start_frame + current_num_frames] = True
flow_pred, denoised_pred = self.generator(
noisy_image_or_video=latents,
conditional_dict=block_cond,
timestep=timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length
)
if first_i2v_block:
denoised_pred = _overwrite_i2v_context(
denoised_pred, initial_latent, num_input_frames
)
break
# Step 3.2: record the model's output
output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
# Step 3.3: rerun with context noise to update the cache
context_timestep = torch.ones(
[batch_size, current_num_frames], device=noise.device, dtype=torch.long) * self.context_noise
if first_i2v_block:
context_timestep = _zero_i2v_context_timestep(
context_timestep, num_input_frames
)
# add context noise
context_noise = torch.randn_like(denoised_pred.flatten(0, 1))
if first_i2v_block:
context_noise = context_noise.unflatten(0, denoised_pred.shape[:2])
context_noise[:, :num_input_frames] = 0
context_noise = context_noise.flatten(0, 1)
denoised_pred = self.scheduler.add_noise(
denoised_pred.flatten(0, 1),
context_noise,
context_timestep.reshape(1, -1) * torch.ones(
[batch_size * current_num_frames], device=noise.device, dtype=torch.long)
).unflatten(0, denoised_pred.shape[:2])
if first_i2v_block:
denoised_pred = _overwrite_i2v_context(
denoised_pred, initial_latent, num_input_frames
)
with torch.no_grad():
self.generator(
noisy_image_or_video=denoised_pred,
conditional_dict=block_cond,
timestep=context_timestep,
kv_cache=self.kv_cache1,
crossattn_cache=self.crossattn_cache,
current_start=current_start_frame * self.frame_seq_length
)
# Step 3.3b: pin KV on scene cut for multi-shot sink.
if self.multi_shot_sink and scene_cut_mask is not None:
is_cut = (
block_index > 0
and block_index < len(scene_cut_mask)
and scene_cut_mask[block_index]
)
if is_cut:
self._pin_current_chunk(self.kv_cache1, current_num_frames)
# Step 3.4: update the start and end frame indices
current_start_frame += current_num_frames
# Step 3.5: Return the denoised timestep
if not self.same_step_across_blocks:
denoised_timestep_from, denoised_timestep_to = None, None
elif exit_flags[0] == num_denoising_steps - 1:
denoised_timestep_to = 0
denoised_timestep_from = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - unipc_timesteps[exit_flags[0]].cuda()).abs(), dim=0).item()
else:
denoised_timestep_to = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - unipc_timesteps[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
denoised_timestep_from = 1000 - torch.argmin(
(self.scheduler.timesteps.cuda() - unipc_timesteps[exit_flags[0]].cuda()).abs(), dim=0).item()
if return_sim_step:
return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
return output, denoised_timestep_from, denoised_timestep_to
def _initialize_kv_cache(self, batch_size, dtype, device):
"""
Initialize a Per-GPU KV cache for the Wan model.
"""
kv_cache1 = []
# Get the actual number of heads and head dimension from model
num_heads = self.generator.model.num_heads
head_dim = self.generator.model.dim // num_heads
for _ in range(self.num_transformer_blocks):
kv_cache1.append({
"k": torch.zeros([batch_size, self.kv_cache_size, num_heads, head_dim], dtype=dtype, device=device),
"v": torch.zeros([batch_size, self.kv_cache_size, num_heads, head_dim], dtype=dtype, device=device),
"global_end_index": torch.tensor([0], dtype=torch.long, device=device),
"local_end_index": torch.tensor([0], dtype=torch.long, device=device),
"pinned_start": torch.tensor([0], dtype=torch.long, device=device),
"pinned_len": torch.tensor([0], dtype=torch.long, device=device),
})
self.kv_cache1 = kv_cache1
def _initialize_crossattn_cache(self, batch_size, dtype, device):
"""
Initialize a Per-GPU cross-attention cache for the Wan model.
"""
crossattn_cache = []
# Get the actual number of heads and head dimension from model
num_heads = self.generator.model.num_heads
head_dim = self.generator.model.dim // num_heads
for _ in range(self.num_transformer_blocks):
crossattn_cache.append({
"k": torch.zeros([batch_size, 512, num_heads, head_dim], dtype=dtype, device=device),
"v": torch.zeros([batch_size, 512, num_heads, head_dim], dtype=dtype, device=device),
"is_init": False
})
self.crossattn_cache = crossattn_cache
def clear_kv_cache(self):
"""
Zero out all tensors in KV cache and cross-attention cache instead of setting them to None.
This preserves memory allocation while clearing old information, avoiding reallocation overhead.
"""
# Clear KV cache
if getattr(self, "kv_cache1", None) is not None:
for blk in self.kv_cache1:
blk["k"].zero_()
blk["v"].zero_()
if "global_end_index" in blk:
blk["global_end_index"].zero_()
if "local_end_index" in blk:
blk["local_end_index"].zero_()
if "pinned_start" in blk:
blk["pinned_start"].zero_()
if "pinned_len" in blk:
blk["pinned_len"].zero_()
# Clear cross-attention cache
if getattr(self, "crossattn_cache", None) is not None:
for blk in self.crossattn_cache:
blk["k"].zero_()
blk["v"].zero_()
blk["is_init"] = False
def _set_all_modules_max_attention_size(self, local_attn_size_value: int):
"""
Set a unified upper bound for all submodules that contain the max_attention_size attribute.
local_attn_size_value == -1 indicates global attention (use Wan's default token limit 32760).
Otherwise set to local_attn_size_value * frame_seq_length.
"""
if isinstance(local_attn_size_value, (list, tuple)):
raise ValueError("_set_all_modules_max_attention_size expects an int, got list/tuple.")
if int(local_attn_size_value) == -1:
target_size = 32760
policy = "global"
else:
target_size = int(local_attn_size_value) * self.frame_seq_length
policy = "local"
# Root module
if hasattr(self.generator.model, "max_attention_size"):
try:
_ = getattr(self.generator.model, "max_attention_size")
except Exception:
pass
setattr(self.generator.model, "max_attention_size", target_size)
# Child modules
for name, module in self.generator.model.named_modules():
if hasattr(module, "max_attention_size"):
try:
setattr(module, "max_attention_size", target_size)
except Exception:
pass