looking-ahead-ti2v / src /utils /predictive_causal_model.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
05182f3 verified
Raw History Blame Contribute Delete
19.2 kB
"""FCP with shared source streams and per-offset AdaLN mini DiTs.
The causal backbone/cached preview route is shared with TF. Auxiliary modules
retain checkpoint-compatible serialized names and are used only during teacher-forced training.
"""
import copy
import logging
import os
import torch
from torch import nn
from torch.utils.checkpoint import checkpoint
from src.utils.attention import attention
from src.utils.causal_mask import visualize_causal_mask
from src.utils.causal_model import CausalHead, CausalWanModel
from src.utils.distributed import is_main_process
from src.utils.predictive_multi_chunk_config import build_predictive_chunk_layout
from src.wan22.modules.model import WanLayerNorm, WanRMSNorm, rope_apply, sinusoidal_embedding_1d
class PredictiveSourceAdaLNProjection(nn.Linear):
def reset_parameters(self):
nn.init.zeros_(self.weight)
if self.bias is not None:
nn.init.zeros_(self.bias)
class PredictiveMultiChunkSelfAttention(nn.Module):
def __init__(self, dim, num_heads, qk_norm=True, eps=1e-6):
super().__init__()
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.q = nn.Linear(dim, dim)
self.k = nn.Linear(dim, dim)
self.v = nn.Linear(dim, dim)
self.o = nn.Linear(dim, dim)
self.norm_q = WanRMSNorm(dim, eps) if qk_norm else nn.Identity()
self.norm_k = WanRMSNorm(dim, eps) if qk_norm else nn.Identity()
def forward(self, x, grid_sizes, freqs):
b, s = x.shape[:2]
n, d = self.num_heads, self.head_dim
q = self.norm_q(self.q(x)).view(b, s, n, d)
k = self.norm_k(self.k(x)).view(b, s, n, d)
v = self.v(x).view(b, s, n, d)
out = attention(
rope_apply(q, grid_sizes, freqs).type_as(v),
rope_apply(k, grid_sizes, freqs).type_as(v),
v,
causal=False,
window_size=(-1, -1),
)
return self.o(out.type_as(v).flatten(2))
class PredictiveMultiChunkAttentionBlock(nn.Module):
def __init__(self, dim, ffn_dim, num_heads, qk_norm=True, eps=1e-6):
super().__init__()
self.dim = dim
self.norm1 = WanLayerNorm(dim, eps)
self.self_attn = PredictiveMultiChunkSelfAttention(dim, num_heads, qk_norm, eps)
self.norm2 = WanLayerNorm(dim, eps)
self.ffn = nn.Sequential(
nn.Linear(dim, ffn_dim), nn.GELU(approximate="tanh"), nn.Linear(ffn_dim, dim)
)
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
self.source_adaln_projection = PredictiveSourceAdaLNProjection(dim, 6 * dim)
def forward(self, x, e, grid_sizes, freqs, source_tokens):
if source_tokens.shape != x.shape:
raise ValueError("FCP AdaLN source and future token shapes must match")
frames, seq = e.shape[1], x.shape[1] // e.shape[1]
source_mod = (
self.source_adaln_projection(source_tokens)
.unflatten(1, (frames, seq))
.unflatten(-1, (6, self.dim))
)
e = (self.modulation.view(1, 1, 1, 6, self.dim) + e.unsqueeze(2) + source_mod).unbind(3)
y = self.self_attn(
(self.norm1(x).unflatten(1, (frames, seq)) * (1 + e[1]) + e[0]).flatten(1, 2),
grid_sizes,
freqs,
)
x = x + (y.unflatten(1, (frames, seq)) * e[2]).flatten(1, 2)
y = self.ffn((self.norm2(x).unflatten(1, (frames, seq)) * (1 + e[4]) + e[3]).flatten(1, 2))
return x + (y.unflatten(1, (frames, seq)) * e[5]).flatten(1, 2)
class PredictiveMultiChunkBranch(nn.Module):
def __init__(self, model, config):
super().__init__()
dim = model.dim
self.hidden_adapter = nn.Sequential(
nn.Linear(
len(config["source_layers"]) * dim, config["source_layer_fusion"]["hidden_dim"]
),
nn.GELU(),
nn.Linear(config["source_layer_fusion"]["hidden_dim"], dim),
)
self.blocks = nn.ModuleList(
[
PredictiveMultiChunkAttentionBlock(
dim, model.ffn_dim, model.num_heads, model.qk_norm, model.eps
)
for _ in range(config["mini_dit_depth"])
]
)
_init_predictive_module(self)
if config["init_mode"] == "copy":
for block, index in zip(self.blocks, config["init_source"], strict=True):
main = model.blocks[index].state_dict()
target = block.state_dict()
selected = {k: v for k, v in main.items() if k in target}
missing, unexpected = block.load_state_dict(selected, strict=False)
if unexpected or any(not k.startswith("source_adaln_projection.") for k in missing):
raise RuntimeError("FCP mini DiT copy is incompatible with backbone blocks")
block.source_adaln_projection.reset_parameters()
# Fresh private modules preserve source initialization order. Do not
# recopy these modules after a subsequent TF checkpoint warm start.
for name in ("patch_embedding", "time_embedding", "time_projection", "head"):
original = getattr(model, name)
if name == "patch_embedding":
module = nn.Conv3d(
model.in_dim, dim, kernel_size=model.patch_size, stride=model.patch_size
)
elif name == "time_embedding":
module = nn.Sequential(
nn.Linear(model.freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim)
)
elif name == "time_projection":
module = nn.Sequential(nn.SiLU(), nn.Linear(dim, dim * 6))
else:
module = CausalHead(dim, model.out_dim, model.patch_size, model.eps)
reference = next(original.parameters())
module.to(device=reference.device, dtype=reference.dtype)
if config["init_mode"] == "copy":
module.load_state_dict(original.state_dict(), strict=True)
else:
_init_predictive_module(module)
setattr(self, name, module)
reference = next(model.parameters())
self.to(device=reference.device, dtype=reference.dtype)
self.freq_dim, self.dim = model.freq_dim, dim
def forward(
self,
hidden_tokens,
future_latents,
future_t,
grid_sizes,
freqs,
gradient_checkpointing=False,
):
dtype = hidden_tokens.dtype
patch = self.patch_embedding(future_latents.to(self.patch_embedding.weight.dtype))
if tuple(patch.shape[2:]) != tuple(grid_sizes[0].tolist()):
raise ValueError("FCP future latent patch grid mismatch")
x = patch.flatten(2).transpose(1, 2).to(dtype)
embeds = sinusoidal_embedding_1d(self.freq_dim, future_t.flatten()).to(
self.time_embedding[0].weight.dtype
)
embeds = self.time_embedding(embeds)
e = (
self.time_projection(embeds)
.unflatten(1, (6, self.dim))
.unflatten(0, future_t.shape)
.to(dtype)
)
head_e = embeds.unflatten(0, future_t.shape).unsqueeze(2).to(dtype)
source = self.hidden_adapter(hidden_tokens).to(dtype)
for block in self.blocks:
if torch.is_grad_enabled() and gradient_checkpointing:
x = checkpoint(block, x, e, grid_sizes, freqs, source, use_reentrant=False)
else:
x = block(x, e, grid_sizes, freqs, source)
return self.head(x.to(self.head.head.weight.dtype), head_e.to(self.head.head.weight.dtype))
def _init_predictive_module(module):
for child in module.modules():
if isinstance(child, PredictiveSourceAdaLNProjection):
child.reset_parameters()
elif isinstance(child, nn.Linear):
nn.init.xavier_uniform_(child.weight)
if child.bias is not None:
nn.init.zeros_(child.bias)
class PredictiveCausalWanModel(CausalWanModel):
def configure_predictive_multi_chunk(self, config):
if hasattr(self, "predictive_multi_chunk_branches"):
raise RuntimeError("FCP branches are already configured")
self.predictive_multi_chunk_config = copy.deepcopy(config)
self.predictive_multi_chunk_branches = nn.ModuleDict(
{
f"offset_{offset}": PredictiveMultiChunkBranch(self, config)
for offset in config["future_offsets"]
}
)
self.register_buffer(
"predictive_multi_chunk_source_layers_metadata",
torch.tensor(
config["source_layers"], dtype=torch.int64, device=next(self.parameters()).device
),
)
def _forward_predictive_multi_chunk(
self, sources, grid_sizes, aux_noisy_chunks_by_offset, aux_timestep_chunks_by_offset
):
cfg = self.predictive_multi_chunk_config
batch = sources[cfg["source_layers"][0]].shape[0]
frames = int(grid_sizes[0, 0])
layout = build_predictive_chunk_layout(
num_frames=frames,
block_size=self.num_frame_per_block,
independent_first_frame=self.independent_first_frame,
)
if layout.num_regular_chunks != cfg["num_chunks"]:
raise ValueError("FCP runtime and configured chunk counts differ")
frame_seq = int(grid_sizes[0, 1] * grid_sizes[0, 2])
noisy_tokens = frames * frame_seq
regular_start = layout.regular_frame_start * frame_seq
hidden = torch.cat([sources[layer] for layer in cfg["source_layers"]], dim=-1)
chunk_tokens = self.num_frame_per_block * frame_seq
source_chunks = {
"clean": hidden[:, regular_start:noisy_tokens],
"noisy": hidden[:, noisy_tokens + regular_start : 2 * noisy_tokens],
}
source_chunks = {
name: value.reshape(batch, layout.num_regular_chunks, chunk_tokens, hidden.shape[-1])
for name, value in source_chunks.items()
}
chunk_grid = grid_sizes.clone()
chunk_grid[:, 0] = self.num_frame_per_block
outputs = {}
for offset in cfg["future_offsets"]:
valid = layout.valid_pair_count(offset)
if (
offset not in aux_noisy_chunks_by_offset
or offset not in aux_timestep_chunks_by_offset
):
raise ValueError(f"Missing independent FCP inputs for offset {offset}")
future = aux_noisy_chunks_by_offset[offset]
future_t = aux_timestep_chunks_by_offset[offset]
expected = (batch, self.in_dim, valid, self.num_frame_per_block)
if tuple(future.shape[:4]) != expected or tuple(future_t.shape) != (
batch,
valid,
self.num_frame_per_block,
):
raise ValueError(f"Invalid FCP auxiliary input shapes for offset {offset}")
future = future.permute(0, 2, 1, 3, 4, 5).reshape(
batch * valid, self.in_dim, self.num_frame_per_block, *future.shape[-2:]
)
grids = chunk_grid.repeat_interleave(valid, dim=0)
predictions = {}
branch = self.predictive_multi_chunk_branches[f"offset_{offset}"]
for name in cfg["source_context_streams"]:
current = source_chunks[name][:, :valid].reshape(
batch * valid, chunk_tokens, hidden.shape[-1]
)
head = branch(
current,
future,
future_t.reshape(batch * valid, self.num_frame_per_block),
grids,
self.freqs,
self.gradient_checkpointing,
)
prediction = torch.stack(self.unpatchify(head, grids)).permute(0, 2, 1, 3, 4)
predictions[name] = prediction.unflatten(0, (batch, valid))
outputs[offset] = {"flow_pred_by_source": predictions, "valid_chunks": valid}
return outputs
def _forward_train(
self,
x,
t,
context,
clean_x=None,
return_predictive_aux=False,
aux_noisy_chunks_by_offset=None,
aux_timestep_chunks_by_offset=None,
):
"""
Forward pass through the diffusion model
Args:
x (List[Tensor]):
List of input video tensors, each with shape [C_in, F, H, W]
t (Tensor):
Diffusion timesteps tensor of shape [B, F]
context (List[Tensor]):
List of text embeddings each with shape [L, C]
clip_fea (Tensor, *optional*):
CLIP image features for image-to-video mode
y (List[Tensor], *optional*):
Conditional video inputs for image-to-video mode, same shape as x
Returns:
List[Tensor]:
List of denoised video tensors with original input shapes [C_out, F, H / 8, W / 8]
"""
if clean_x is None:
raise ValueError("Teacher-forcing forward requires clean_x.")
device = self.patch_embedding.weight.device
if self.freqs.device != device:
self.freqs = self.freqs.to(device)
if self.block_mask is None:
self.block_mask = self._prepare_teacher_forcing_mask(
device,
num_frames=x.shape[2],
frame_seqlen=x.shape[-2] * x.shape[-1] // (self.patch_size[1] * self.patch_size[2]),
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
sink_size=self.sink_size,
independent_first_frame=self.independent_first_frame,
)
_vis_flag = os.environ.get("WAN_VIS_CAUSAL_MASK", "0")
if _vis_flag and _vis_flag != "0":
if is_main_process():
if not hasattr(self, "_causal_mask_vis_done"):
self._causal_mask_vis_done = set()
mode = "TF"
vis_key = (mode, int(x.shape[2]), bool(self.independent_first_frame))
if vis_key not in self._causal_mask_vis_done:
out_dir = os.environ.get(
"WAN_VIS_CAUSAL_MASK_DIR", os.path.join("outputs", "causal_mask_vis")
)
try:
saved_path = visualize_causal_mask(
mode=mode,
num_frames=int(x.shape[2]),
num_frame_per_block=self.num_frame_per_block,
local_attn_size=self.local_attn_size,
sink_size=self.sink_size,
independent_first_frame=bool(self.independent_first_frame),
out_dir=out_dir,
)
logging.info(
"[causal_mask_vis] saved %s mask (num_frames=%d, num_frame_per_block=%d, local_attn_size=%d, sink_size=%d, independent_first_frame=%s) -> %s",
mode,
int(x.shape[2]),
self.num_frame_per_block,
self.local_attn_size,
self.sink_size,
bool(self.independent_first_frame),
saved_path,
)
except Exception as _e:
logging.warning("[causal_mask_vis] failed: %s", _e)
self._causal_mask_vis_done.add(vis_key)
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
grid_sizes = torch.stack([torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
x = [u.flatten(2).transpose(1, 2) for u in x]
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
x = torch.cat(
[torch.cat([u, u.new_zeros(1, seq_lens[0] - u.size(1), u.size(2))], dim=1) for u in x]
)
e = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, t.flatten()).type_as(x))
e0 = self.time_projection(e).unflatten(1, (6, self.dim)).unflatten(dim=0, sizes=t.shape)
context_lens = None
context = self.text_embedding(
torch.stack(
[torch.cat([u, u.new_zeros(self.text_len - u.size(0), u.size(1))]) for u in context]
)
)
clean_patch_embedding = self.patch_embedding
clean_x = [clean_patch_embedding(u.unsqueeze(0)) for u in clean_x]
clean_x = [u.flatten(2).transpose(1, 2) for u in clean_x]
seq_lens_clean = torch.tensor([u.size(1) for u in clean_x], dtype=torch.long)
clean_x = torch.cat(
[
torch.cat([u, u.new_zeros(1, seq_lens_clean[0] - u.size(1), u.size(2))], dim=1)
for u in clean_x
]
)
x = torch.cat([clean_x, x], dim=1)
aug_t = torch.zeros_like(t)
e_clean = self.time_embedding(
sinusoidal_embedding_1d(self.freq_dim, aug_t.flatten()).type_as(x)
)
e0_clean = (
self.time_projection(e_clean)
.unflatten(1, (6, self.dim))
.unflatten(dim=0, sizes=t.shape)
)
e0 = torch.cat([e0_clean, e0], dim=1)
kwargs = {
"e": e0,
"seq_lens": seq_lens,
"grid_sizes": grid_sizes,
"freqs": self.freqs,
"context": context,
"context_lens": context_lens,
"block_mask": self.block_mask,
}
def create_custom_forward(module):
def custom_forward(*inputs, **kwargs):
return module(*inputs, **kwargs)
return custom_forward
sources, auxiliary = {}, None
selected = (
self.predictive_multi_chunk_config["source_layers"] if return_predictive_aux else []
)
for index, block in enumerate(self.blocks):
if torch.is_grad_enabled() and self.gradient_checkpointing:
x = torch.utils.checkpoint.checkpoint(
create_custom_forward(block), x, **kwargs, use_reentrant=False
)
else:
x = block(x, **kwargs)
if index in selected:
sources[index] = x
if index == selected[-1]:
auxiliary = self._forward_predictive_multi_chunk(
sources,
grid_sizes,
aux_noisy_chunks_by_offset,
aux_timestep_chunks_by_offset,
)
x = x[:, x.shape[1] // 2 :]
x = self.head(x, e.unflatten(dim=0, sizes=t.shape).unsqueeze(2))
x = self.unpatchify(x, grid_sizes)
output = torch.stack(x)
return (output, auxiliary) if return_predictive_aux else output