remind-video-generator / utils /wan_wrapper.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
5d51a70 verified
Raw
History Blame Contribute Delete
38.5 kB
# Copyright 2026 Applied Intuition, Inc.
# SPDX-License-Identifier: CC-BY-NC-4.0
import types
from typing import List, Optional, Tuple, Union
import torch
from safetensors.torch import load_file as safe_load_file
from safetensors.torch import save_file as safe_save_file
from utils.scheduler import SchedulerInterface, FlowMatchScheduler
from wan.modules.tokenizers import HuggingfaceTokenizer
from wan.modules.model import WanModel
from wan.modules.vae import _video_vae
from wan.modules.t5 import umt5_xxl
from wan.modules.causal_model import CausalWanModel
import os
import torch.distributed.checkpoint as dcp
from torch.distributed.checkpoint.filesystem import FileSystemReader
# from settings import MODEL_FOLDER
MODEL_FOLDER = None # Set via config: text_encoder_path / vae_path, or wan_model_folder
def _select_chunk_prompt_embeds(
prompt_embeds: torch.Tensor,
conditional_dict: dict,
*,
current_frames: int,
freqs_offset: int,
) -> torch.Tensor:
"""Select chunk-local text contexts for one model invocation.
ReMind keeps one context per chunk. Streaming inference slices the
pre-encoded schedule by absolute RoPE frame position. Partial or
non-contiguous history passes expand to one context per frame so prompt
boundaries remain exact.
"""
if prompt_embeds.ndim != 4:
return prompt_embeds
prompt_chunk_size = int(conditional_dict.get("prompt_chunk_size", 0) or 0)
if prompt_chunk_size <= 0:
raise ValueError(
"4D prompt_embeds require conditional_dict['prompt_chunk_size']"
)
num_prompt_chunks = prompt_embeds.shape[1]
explicit_frames = conditional_dict.get("prompt_frame_indices")
if explicit_frames is not None:
frame_indices = torch.as_tensor(
explicit_frames, device=prompt_embeds.device, dtype=torch.long
)
if frame_indices.ndim != 1 or frame_indices.numel() != current_frames:
raise ValueError(
"prompt_frame_indices must contain one absolute index per "
f"current frame; got {tuple(frame_indices.shape)} for "
f"current_frames={current_frames}"
)
chunk_indices = torch.div(
frame_indices, prompt_chunk_size, rounding_mode="floor"
)
if (
int(chunk_indices.min().item()) < 0
or int(chunk_indices.max().item()) >= num_prompt_chunks
):
raise ValueError(
f"prompt frame indices map outside {num_prompt_chunks} chunks"
)
return prompt_embeds.index_select(1, chunk_indices)
start_frame = int(freqs_offset)
stop_frame = start_frame + int(current_frames)
if start_frame % prompt_chunk_size == 0 and current_frames % prompt_chunk_size == 0:
start_chunk = start_frame // prompt_chunk_size
stop_chunk = stop_frame // prompt_chunk_size
if start_chunk >= 0 and stop_chunk <= num_prompt_chunks:
return prompt_embeds[:, start_chunk:stop_chunk]
frame_indices = torch.arange(
start_frame, stop_frame, device=prompt_embeds.device, dtype=torch.long
)
chunk_indices = torch.div(frame_indices, prompt_chunk_size, rounding_mode="floor")
if (
int(chunk_indices.min().item()) < 0
or int(chunk_indices.max().item()) >= num_prompt_chunks
):
raise ValueError(
f"prompt range [{start_frame}, {stop_frame}) maps outside "
f"{num_prompt_chunks} chunks of size {prompt_chunk_size}"
)
return prompt_embeds.index_select(1, chunk_indices)
class WanTextEncoder(torch.nn.Module):
def __init__(self, model_folder: str) -> None:
super().__init__()
self.text_encoder = (
umt5_xxl(
encoder_only=True,
return_tokenizer=False,
dtype=torch.float32,
device=torch.device("meta"),
)
.eval()
.requires_grad_(False)
)
self.text_encoder.to_empty(device="cpu")
safetensors_path = os.path.join(
model_folder, "models_t5_umt5-xxl-enc-bf16.safetensors"
)
pth_path = os.path.join(model_folder, "models_t5_umt5-xxl-enc-bf16.pth")
if os.path.exists(safetensors_path):
state_dict = safe_load_file(safetensors_path)
elif os.path.exists(pth_path):
state_dict = torch.load(pth_path, map_location="cpu", weights_only=True)
else:
raise FileNotFoundError(
f"Missing T5 weights in {model_folder}: expected "
f"{os.path.basename(safetensors_path)} or {os.path.basename(pth_path)}"
)
self.text_encoder.load_state_dict(state_dict)
self.tokenizer = HuggingfaceTokenizer(
name=os.path.join(model_folder, "google", "umt5-xxl/"),
seq_len=512,
clean="whitespace",
)
@property
def device(self):
# Assume we are always on GPU
return torch.cuda.current_device()
def forward(
self,
text_prompts: Union[List[str], List[List[str]]],
) -> dict:
nested = bool(text_prompts and isinstance(text_prompts[0], (list, tuple)))
if nested:
chunk_counts = [len(prompts) for prompts in text_prompts]
if not chunk_counts or min(chunk_counts) <= 0:
raise ValueError("chunk-local text prompts cannot be empty")
if len(set(chunk_counts)) != 1:
raise ValueError(
"all samples must provide the same number of chunk prompts; "
f"got {chunk_counts}"
)
flat_prompts = [
str(prompt) for prompts in text_prompts for prompt in prompts
]
else:
flat_prompts = [str(prompt) for prompt in text_prompts]
# Chunk-local prompts repeat the base caption on every non-event
# chunk; encode each UNIQUE string once and scatter back (typically
# 7 prompts -> 3-4 unique, ~2x cheaper umt5-xxl pass).
unique_prompts = list(dict.fromkeys(flat_prompts))
index_of = {p: i for i, p in enumerate(unique_prompts)}
gather_idx = [index_of[p] for p in flat_prompts]
ids, mask = self.tokenizer(
unique_prompts, return_mask=True, add_special_tokens=True
)
ids = ids.to(self.device)
mask = mask.to(self.device)
seq_lens = mask.gt(0).sum(dim=1).long()
context = self.text_encoder(ids, mask)
for u, v in zip(context, seq_lens):
u[v:] = 0.0 # set padding to 0.0
if len(unique_prompts) != len(flat_prompts):
context = context[
torch.as_tensor(gather_idx, device=context.device, dtype=torch.long)
]
if nested:
batch_size = len(text_prompts)
num_chunks = chunk_counts[0]
context = context.view(
batch_size, num_chunks, context.shape[1], context.shape[2]
)
result = {"prompt_embeds": context}
return result
class WanVAEWrapper(torch.nn.Module):
def __init__(self, model_folder: str):
super().__init__()
wan22_vae_path = os.path.join(model_folder, "Wan2.2_VAE.pth")
if os.path.exists(wan22_vae_path):
from wan.modules.vae_wan22 import WanVideoVAE38
vae = WanVideoVAE38()
state_dict = torch.load(
wan22_vae_path, map_location="cpu", weights_only=True
)
if state_dict and next(iter(state_dict)).startswith("model."):
vae.load_state_dict(state_dict, strict=True)
else:
vae.model.load_state_dict(state_dict, strict=True)
self.mean = vae.mean.to(dtype=torch.float32)
self.std = vae.std.to(dtype=torch.float32)
self.model = vae.model.eval().requires_grad_(False)
self.z_dim = int(vae.z_dim)
self.upsampling_factor = int(vae.upsampling_factor)
print(
f"WanVAEWrapper loaded {wan22_vae_path} "
f"(z_dim={self.z_dim}, upsampling_factor={self.upsampling_factor})"
)
return
mean = [
-0.7571,
-0.7089,
-0.9113,
0.1075,
-0.1745,
0.9653,
-0.1517,
1.5508,
0.4134,
-0.0715,
0.5517,
-0.3632,
-0.1922,
-0.9497,
0.2503,
-0.2921,
]
std = [
2.8184,
1.4541,
2.3275,
2.6558,
1.2196,
1.7708,
2.6052,
2.0743,
3.2687,
2.1526,
2.8652,
1.5579,
1.6382,
1.1253,
2.8251,
1.9160,
]
self.mean = torch.tensor(mean, dtype=torch.float32)
self.std = torch.tensor(std, dtype=torch.float32)
vae_path = os.path.join(model_folder, "Wan2.1_VAE.pth")
self.model = (
_video_vae(
pretrained_path=vae_path,
z_dim=16,
)
.eval()
.requires_grad_(False)
)
self.z_dim = 16
self.upsampling_factor = 8
print(
f"WanVAEWrapper loaded {vae_path} "
f"(z_dim={self.z_dim}, upsampling_factor={self.upsampling_factor})"
)
def forward(
self, x: torch.Tensor, method: str = "encode", **kwargs
) -> torch.Tensor:
if method == "encode":
return self.encode_to_latent(x)
elif method == "decode":
return self.decode_to_pixel(x, **kwargs)
else:
raise ValueError(f"Unknown method {method}")
def encode_to_latent(self, pixel: torch.Tensor) -> torch.Tensor:
# pixel: [batch_size, num_channels, num_frames, height, width]
device, dtype = pixel.device, pixel.dtype
scale = [
self.mean.to(device=device, dtype=dtype),
1.0 / self.std.to(device=device, dtype=dtype),
]
output = [
self.model.encode(u.unsqueeze(0), scale).float().squeeze(0) for u in pixel
]
output = torch.stack(output, dim=0)
output = output.permute(0, 2, 1, 3, 4)
return output
def decode_to_pixel(
self, latent: torch.Tensor, use_cache: bool = False
) -> torch.Tensor:
# from [batch_size, num_frames, num_channels, height, width]
# to [batch_size, num_channels, num_frames, height, width]
zs = latent.permute(0, 2, 1, 3, 4)
if use_cache:
assert latent.shape[0] == 1, "Batch size must be 1 when using cache"
device, dtype = latent.device, latent.dtype
scale = [
self.mean.to(device=device, dtype=dtype),
1.0 / self.std.to(device=device, dtype=dtype),
]
if use_cache:
decode_function = self.model.cached_decode
else:
decode_function = self.model.decode
output = []
for u in zs:
output.append(
decode_function(u.unsqueeze(0), scale).float().clamp_(-1, 1).squeeze(0)
)
output = torch.stack(output, dim=0)
# from [batch_size, num_channels, num_frames, height, width]
# to [batch_size, num_frames, num_channels, height, width]
output = output.permute(0, 2, 1, 3, 4)
return output
def load_state_dict_from_folder_safetensors(file_path):
state_dict = {}
for file_name in os.listdir(file_path):
if (
"." in file_name
and "diffusion" in file_name
and file_name.split(".")[-1] in ["safetensors"]
):
state_dict.update(safe_load_file(os.path.join(file_path, file_name)))
return state_dict
def _filter_state_dict_keys(state_dict, skip_substrings):
"""
Filter out (do not load) weights whose keys contain any of `skip_substrings`.
Returns (filtered_state_dict, skipped_keys).
"""
if not skip_substrings:
return state_dict, []
skipped = []
filtered = {}
for k, v in state_dict.items():
if any(s in k for s in skip_substrings):
skipped.append(k)
continue
filtered[k] = v
return filtered, skipped
def _slice_prefix_tensor_for_live_shape(state_dict, key, live_tensor, label):
"""Adapt a pretrained tensor to the live module shape when safe.
Wan2.2-TI2V-5B ships a 48-channel input/output head. The ReMind
continuous-latent training target is still 16 Wan VAE channels, so 5B
i2v16 configs instantiate smaller patch/head tensors and keep the prefix
rows/channels from the pretrained checkpoint.
"""
tensor = state_dict.get(key)
if tensor is None or live_tensor is None:
return
live_shape = tuple(live_tensor.shape)
ckpt_shape = tuple(tensor.shape)
if ckpt_shape == live_shape:
return
if tensor.dim() == live_tensor.dim() == 5:
if (
ckpt_shape[0] == live_shape[0]
and ckpt_shape[2:] == live_shape[2:]
and ckpt_shape[1] >= live_shape[1]
):
print(
f"[{label} surgery] slicing {key} {list(ckpt_shape)} "
f"-> {list(live_shape)} on input channels"
)
state_dict[key] = tensor[:, : live_shape[1]].contiguous()
return
if tensor.dim() == live_tensor.dim() == 2:
if ckpt_shape[1] == live_shape[1] and ckpt_shape[0] >= live_shape[0]:
print(
f"[{label} surgery] slicing {key} {list(ckpt_shape)} "
f"-> {list(live_shape)} on output rows"
)
state_dict[key] = tensor[: live_shape[0]].contiguous()
return
if tensor.dim() == live_tensor.dim() == 1:
if ckpt_shape[0] >= live_shape[0]:
print(
f"[{label} surgery] slicing {key} {list(ckpt_shape)} "
f"-> {list(live_shape)}"
)
state_dict[key] = tensor[: live_shape[0]].contiguous()
return
print(
f"[{label} surgery] cannot adapt {key}: ckpt={list(ckpt_shape)} "
f"live={list(live_shape)}"
)
def dcp_load_dict(path):
if path.endswith(".safetensors"):
auto_state_dict = safe_load_file(path)
state_dict = {}
for key, value in auto_state_dict.items():
# Remove FSDP wrapper prefix if present
if "._fsdp_wrapped_module." in key:
key = key.replace("._fsdp_wrapped_module.", ".")
# Remove model. prefix if present
if "model." in key:
key = key.replace("model.", "")
state_dict[key] = value
return state_dict
safe_file_path = path + "/model.safetensors"
if os.path.exists(safe_file_path):
state_dict = safe_load_file(safe_file_path)
else:
reader = FileSystemReader(path)
metadata = reader.read_metadata()
auto_state_dict = {}
for key, entry in metadata.state_dict_metadata.items():
auto_state_dict[key] = torch.empty(
entry.size, dtype=entry.properties.dtype, device=torch.device("meta")
)
dcp.load(state_dict=auto_state_dict, storage_reader=reader, no_dist=True)
state_dict = {}
for key, value in auto_state_dict.items():
# Remove FSDP wrapper prefix if present
if "._fsdp_wrapped_module." in key:
key = key.replace("._fsdp_wrapped_module.", ".")
# Remove model. prefix if present
if "model." in key:
key = key.replace("model.", "")
state_dict[key] = value
safe_save_file(state_dict, safe_file_path)
return state_dict
class WanDiffusionWrapper(torch.nn.Module):
def __init__(
self,
model_name="Wan2.1-T2V-1.3B",
load_path=None,
timestep_shift=5.0,
is_causal=False,
ckpt_path=None,
weight_list=[],
filter_list=[],
in_dim=36,
out_dim=None,
model_type=None,
dual_model=False,
high_noise_threshold=0.5,
prope_temporal_dim=0, # ProPE split: temporal RoPE dims (causal only)
cc_rope_mode="dual_prope", # RoPE variant: standard | dual_prope | cc_basic | cc_output | cc_dual_channel | cc_dual_output | prope_residual | cc_value | cc_full
cc_phase_slots=16, # dual_channel only: # freq slots dedicated to camera
degradation_control_dim=0,
degradation_control_hidden_dim=256,
require_full_weight_coverage=False,
):
super().__init__()
import torch.distributed as dist
rank = dist.get_rank() if dist.is_initialized() else 0
load_generator_on_all_ranks = os.environ.get(
"REMIND_LOAD_GENERATOR_ON_ALL_RANKS", "0"
).strip().lower() in {"1", "true", "yes", "on"}
num_threads = int(os.environ.get("TORCH_NUM_THREADS", "32"))
if torch.get_num_threads() != num_threads:
torch.set_num_threads(num_threads)
# model_path: use the first weight_list path's directory as the model config source,
# or fall back to model_name if weight_list is empty
if weight_list:
model_path = weight_list[0]["path"]
else:
model_path = model_name
# Wan2.2 dual model: config.json is inside high_noise_model/ subdir
config_path = model_path
if dual_model and os.path.isdir(os.path.join(model_path, "high_noise_model")):
config_path = os.path.join(model_path, "high_noise_model")
# Initialize primary model
if is_causal:
config = CausalWanModel.load_config(config_path)
config = dict(config)
config["in_dim"] = in_dim
if out_dim is not None:
config["out_dim"] = out_dim
if model_type is not None:
config["model_type"] = model_type
config["prope_temporal_dim"] = prope_temporal_dim
config["cc_rope_mode"] = cc_rope_mode
config["cc_phase_slots"] = cc_phase_slots
config["degradation_control_dim"] = degradation_control_dim
config["degradation_control_hidden_dim"] = degradation_control_hidden_dim
with torch.device("meta"):
self.model = CausalWanModel(**config)
self.model.to_empty(device="cpu")
self._cc_rope_mode = cc_rope_mode
else:
config = WanModel.load_config(config_path)
config = dict(config)
config["in_dim"] = in_dim
with torch.device("meta"):
self.model = WanModel(**config)
self.model.to_empty(device="cpu")
# Initialize secondary model for dual-model mode (Wan2.2)
self.model_2 = None
self.dual_model = dual_model
self.high_noise_threshold = high_noise_threshold
if dual_model and not is_causal:
# Use same config for model_2
with torch.device("meta"):
self.model_2 = WanModel(**config)
self.model_2.to_empty(device="cpu")
if rank == 0 or load_generator_on_all_ranks:
if rank != 0 and load_generator_on_all_ranks:
print(
f"[Rank {rank}] loading generator weights locally "
"because REMIND_LOAD_GENERATOR_ON_ALL_RANKS=1"
)
state_dict_full = None
state_dict_full_2 = None # For model_2
primary_missing_keys = None
if ckpt_path is not None:
state_dict_full = dcp_load_dict(ckpt_path)
else:
for weight_config in weight_list:
weight_path = weight_config["path"]
is_model_2 = weight_config.get("is_model_2", False)
should_load_weights = weight_config.get("load_weights", True)
if isinstance(should_load_weights, str):
should_load_weights = should_load_weights.lower() not in {
"0",
"false",
"no",
"off",
}
if not should_load_weights:
print(
f"load_model {weight_path}: skipped weight load (load_weights=false)"
)
continue
# For Wan2.2 dual model: automatically determine high/low noise model
# based on directory structure if not explicitly specified
if (
dual_model
and not is_causal
and "is_model_2" not in weight_config
):
# Check if path contains high/low noise indicators
if (
"high_noise" in weight_path.lower()
or "high" in os.path.basename(weight_path).lower()
):
is_model_2 = False # high noise -> primary model
elif (
"low_noise" in weight_path.lower()
or "low" in os.path.basename(weight_path).lower()
):
is_model_2 = True # low noise -> model_2
if os.path.isdir(weight_path):
state_dict = load_state_dict_from_folder_safetensors(
weight_path
)
else:
state_dict = safe_load_file(weight_path)
if is_model_2 and dual_model and not is_causal:
# This weight is for model_2 (low noise model in Wan2.2)
if state_dict_full_2 is None:
state_dict_full_2 = state_dict
else:
state_dict_full_2.update(state_dict)
else:
# This weight is for model (primary/high noise model)
if state_dict_full is None:
state_dict_full = state_dict
else:
state_dict_full.update(state_dict)
# Load primary model
if state_dict_full is not None:
state_dict_full, _ = _filter_state_dict_keys(
state_dict_full, skip_substrings=filter_list
)
# in_dim=16 surgery: the checkpoint's patch_embedding.weight is
# shaped [dim, 36, 1, 2, 2] (16 video + 4 mask + 16 render),
# but when we instantiate the model with in_dim=16 the conv
# expects [dim, 16, 1, 2, 2]. Slice the checkpoint tensor to
# the first 16 input channels (the "video" branch) — those
# weights are the ones we want to keep for pure-latent I2V.
# The dropped 20 channels were already getting zeros fed into
# them at runtime (render_latent_input=None → zero-pad), so
# slicing is bit-exact equivalent to the zero-pad regime at
# init, with the added benefit that gradients no longer drift
# those 20 channels away from zero over training.
# Both causal students and full-attention teachers may use a
# pure 16-channel latent interface with an I2V checkpoint whose
# patch embedding has extra mask/render channels. Keep the
# pretrained video-channel prefix in either case.
pe_key = "patch_embedding.weight"
_slice_prefix_tensor_for_live_shape(
state_dict_full,
pe_key,
self.model.patch_embedding.weight,
"in_dim",
)
if is_causal:
_slice_prefix_tensor_for_live_shape(
state_dict_full,
"head.head.weight",
self.model.head.head.weight,
"out_dim",
)
_slice_prefix_tensor_for_live_shape(
state_dict_full,
"head.head.bias",
self.model.head.head.bias,
"out_dim",
)
missing_keys, unexpected_keys = self.model.load_state_dict(
state_dict_full, strict=False
)
primary_missing_keys = set(missing_keys)
print(
f"load_model {model_path} (primary) missing_keys: {len(missing_keys)} unexpected_keys: {len(unexpected_keys)}"
)
if require_full_weight_coverage and (missing_keys or unexpected_keys):
raise RuntimeError(
f"incomplete pretrained weight coverage for {model_path}: "
f"missing={len(missing_keys)} {missing_keys[:20]} "
f"unexpected={len(unexpected_keys)} "
f"{unexpected_keys[:20]}"
)
elif require_full_weight_coverage:
raise RuntimeError(f"no pretrained weights loaded for {model_path}")
# Causal models are constructed on `meta` then materialized with
# to_empty(), so "zero-init" modules whose keys are absent from the
# source checkpoint must be explicitly reset after materialization.
# Otherwise the camera phase MLP reads uninitialized memory at step
# 0 and breaks the pretrained-identity invariant for cc_* modes.
if is_causal and cc_rope_mode in (
"cc_basic",
"cc_output",
"cc_value",
"cc_full",
"cc_dual_channel",
"cc_dual_output",
):
n_zeroed = 0
n_present = 0
missing = primary_missing_keys or set()
for i, blk in enumerate(self.model.blocks):
mlp = getattr(blk.self_attn, "camera_phase_mlp", None)
if mlp is None:
continue
n_present += 1
key = f"blocks.{i}.self_attn.camera_phase_mlp.proj.weight"
if primary_missing_keys is not None and key not in missing:
continue
with torch.no_grad():
mlp.proj.weight.zero_()
n_zeroed += 1
print(
f"[CC-RoPE {cc_rope_mode}] zeroed camera_phase_mlp on "
f"{n_zeroed}/{n_present} blocks with missing checkpoint keys"
)
control_embedding = getattr(
self.model, "degradation_control_embedding", None
)
if is_causal and control_embedding is not None:
control_key = "degradation_control_embedding.0.weight"
if primary_missing_keys is None or control_key in (
primary_missing_keys or set()
):
self.model.reset_degradation_control_parameters()
print(
"[DegradationControl] initialized missing adapter with "
"a zero output projection"
)
# CC-RoPE modes: pretrained checkpoints (e.g. HY-WorldPlay /
# some adapted checkpoints) may ship non-zero prope_proj
# weights learned for the dual-attention path. For cc_output,
# cc_dual_output, prope_residual, and cc_full they'd be fed a
# differently-distributed input (P·x_std vs x_p_from_2nd_attn),
# so we zero them post-load to guarantee the bit-exact-identity
# invariant at step 0.
# `cc_basic` / `cc_dual_channel` / `cc_value` don't instantiate
# prope_proj at all (set to None in __init__) — stale checkpoint
# keys simply land in `unexpected_keys`, no action needed here.
if is_causal and cc_rope_mode in (
"cc_output",
"cc_dual_output",
"prope_residual",
"cc_full",
):
n_zeroed = 0
for blk in self.model.blocks:
pp = blk.self_attn.prope_proj
if pp is None:
continue
if pp.weight.abs().sum().item() > 0.0:
n_zeroed += 1
with torch.no_grad():
pp.weight.zero_()
if pp.bias is not None:
pp.bias.zero_()
print(
f"[CC-RoPE {cc_rope_mode}] re-zeroed prope_proj on {n_zeroed}/{len(self.model.blocks)} blocks (was non-zero from pretrained ckpt)"
)
# cc_value / cc_full: re-zero `value_proj` for the same
# step-0-bit-exact invariant. Pretrained ckpts won't have this
# key, but if a future ckpt ships value_proj weights, they must
# not contaminate step 0. `cc_basic`/`cc_dual_channel`/
# `dual_prope`/`cc_output` don't instantiate value_proj at all
# (set to None in __init__).
if is_causal and cc_rope_mode in ("cc_value", "cc_full"):
n_zeroed = 0
for blk in self.model.blocks:
vp = blk.self_attn.value_proj
if vp is None:
continue
if vp.weight.abs().sum().item() > 0.0:
n_zeroed += 1
with torch.no_grad():
vp.weight.zero_()
if vp.bias is not None:
vp.bias.zero_()
print(
f"[CC-RoPE {cc_rope_mode}] re-zeroed value_proj on {n_zeroed}/{len(self.model.blocks)} blocks (was non-zero from pretrained ckpt)"
)
# Load secondary model (only for dual_model and non-causal mode)
if dual_model and not is_causal and state_dict_full_2 is not None:
state_dict_full_2, _ = _filter_state_dict_keys(
state_dict_full_2, skip_substrings=filter_list
)
missing_keys_2, unexpected_keys_2 = self.model_2.load_state_dict(
state_dict_full_2, strict=False
)
print(
f"load_model_2 {model_path} (low noise model for Wan2.2) missing_keys: {len(missing_keys_2)} unexpected_keys: {len(unexpected_keys_2)}"
)
if dist.is_initialized():
dist.barrier()
self.uniform_timestep = not is_causal
self.scheduler = FlowMatchScheduler(
shift=timestep_shift, sigma_min=0.0, extra_one_step=True
)
self.scheduler.set_timesteps(1000, training=True)
self.seq_len = 1560 * 24 # [1, 12 * 2, 16, 60, 104]
self.post_init()
def _convert_flow_pred_to_x0(
self, flow_pred: torch.Tensor, xt: torch.Tensor, timestep: torch.Tensor
) -> torch.Tensor:
"""
Convert flow matching's prediction to x0 prediction.
flow_pred: the prediction with shape [B, C, H, W]
xt: the input noisy data with shape [B, C, H, W]
timestep: the timestep with shape [B]
pred = noise - x0
x_t = (1-sigma_t) * x0 + sigma_t * noise
we have x0 = x_t - sigma_t * pred
see derivations https://chatgpt.com/share/67bf8589-3d04-8008-bc6e-4cf1a24e2d0e
"""
# use higher precision for calculations
original_dtype = flow_pred.dtype
flow_pred, xt, sigmas, timesteps = map(
lambda x: x.double().to(flow_pred.device),
[flow_pred, xt, self.scheduler.sigmas, self.scheduler.timesteps],
)
timestep_id = torch.argmin(
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1)
x0_pred = xt - sigma_t * flow_pred
return x0_pred.to(original_dtype)
def forward(
self,
noisy_image_or_video: torch.Tensor,
conditional_dict: dict,
timestep: torch.Tensor,
kv_cache: Optional[List[dict]] = None,
crossattn_cache: Optional[List[dict]] = None,
kv_size: Optional[Tuple[int, int]] = (0, 0),
image_latent_input: Optional[torch.Tensor] = None,
render_latent_input: Optional[torch.Tensor] = None,
freqs_offset: int = 0,
freqs_positions: Optional[torch.Tensor] = None,
viewmats: Optional[torch.Tensor] = None, # [B, F, 4, 4] c2w
Ks: Optional[torch.Tensor] = None, # [B, F, 3, 3] intrinsics
degradation_control: Optional[torch.Tensor] = None,
) -> torch.Tensor:
prompt_embeds = _select_chunk_prompt_embeds(
conditional_dict["prompt_embeds"],
conditional_dict,
current_frames=noisy_image_or_video.shape[1],
freqs_offset=freqs_offset,
)
if degradation_control is None:
degradation_control = conditional_dict.get("degradation_control")
if degradation_control is not None:
current_frames = noisy_image_or_video.shape[1]
if degradation_control.shape[1] != current_frames:
start = int(freqs_offset)
stop = start + current_frames
if degradation_control.shape[1] < stop:
raise ValueError(
"degradation_control does not cover the requested "
f"frame range [{start}, {stop}); shape is "
f"{tuple(degradation_control.shape)}"
)
degradation_control = degradation_control[:, start:stop]
# [B, F] -> [B]
if self.uniform_timestep:
input_timestep = timestep[:, 0]
else:
input_timestep = timestep
# X0 prediction
# Handle None inputs for T2V mode
image_latent_permuted = (
image_latent_input.permute(0, 2, 1, 3, 4).contiguous()
if image_latent_input is not None
else None
)
render_latent_permuted = (
render_latent_input.permute(0, 2, 1, 3, 4).contiguous()
if render_latent_input is not None
else None
)
if kv_cache is None:
raise ValueError("ReMind inference requires an initialized KV cache")
if self.dual_model:
raise ValueError("KV-cache inference does not support dual-model mode")
flow_pred = self.model(
noisy_image_or_video.permute(0, 2, 1, 3, 4).contiguous(),
t=input_timestep,
context=prompt_embeds,
seq_len=self.seq_len,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
kv_size=kv_size,
image_latent_input=image_latent_permuted,
render_latent_input=render_latent_permuted,
freqs_offset=freqs_offset,
freqs_positions=freqs_positions,
viewmats=viewmats,
Ks=Ks,
degradation_control=degradation_control,
).permute(0, 2, 1, 3, 4)
if kv_size[1] < 0:
return flow_pred
pred_x0 = self._convert_flow_pred_to_x0(
flow_pred=flow_pred.flatten(0, 1),
xt=noisy_image_or_video.flatten(0, 1),
timestep=timestep.flatten(0, 1),
).unflatten(0, flow_pred.shape[:2])
return flow_pred, pred_x0
def forward_wan22(
self,
latent_list: List[torch.Tensor],
t: torch.Tensor,
context: torch.Tensor,
seq_len: int,
**kwargs,
) -> List[torch.Tensor]:
"""
Forward method specifically for Wan2.2 dual-model inference.
Compatible with T2VAlignedInferencePipeline's direct model call signature.
Args:
latent_list: List of latent tensors [B, C, F, H, W]
t: Timestep tensor [B]
context: Text embeddings
seq_len: Sequence length
**kwargs: Additional arguments
Returns:
List of flow predictions
"""
if not self.dual_model:
raise ValueError("forward_wan22 is only available for dual-model mode")
# Select model based on timestep
normalized_timestep = t.float() / 1000.0
use_high_noise = (normalized_timestep >= self.high_noise_threshold).all().item()
selected_model = self.model if use_high_noise else self.model_2
# Process each latent in the list
output_list = []
for latent in latent_list:
flow_pred = selected_model(
latent, t=t, context=context, seq_len=seq_len, **kwargs
)
output_list.append(flow_pred)
return output_list
def get_scheduler(self) -> SchedulerInterface:
"""
Update the current scheduler with the interface's static method
"""
scheduler = self.scheduler
scheduler.convert_x0_to_noise = types.MethodType(
SchedulerInterface.convert_x0_to_noise, scheduler
)
scheduler.convert_noise_to_x0 = types.MethodType(
SchedulerInterface.convert_noise_to_x0, scheduler
)
scheduler.convert_velocity_to_x0 = types.MethodType(
SchedulerInterface.convert_velocity_to_x0, scheduler
)
self.scheduler = scheduler
return scheduler
def post_init(self):
"""
A few custom initialization steps that should be called after the object is created.
Currently, the only one we have is to bind a few methods to scheduler.
We can gradually add more methods here if needed.
"""
self.get_scheduler()