LiveEdit / wan /modules /causal_model.py
multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
70d51ed verified
Raw
History Blame Contribute Delete
82.6 kB
from wan.modules.attention import attention
from wan.modules.model import (
WanRMSNorm,
rope_apply,
WanLayerNorm,
WAN_CROSSATTENTION_CLASSES,
rope_params,
MLPProj,
sinusoidal_embedding_1d
)
from torch.nn.attention.flex_attention import create_block_mask, flex_attention
from diffusers.configuration_utils import ConfigMixin, register_to_config
from torch.nn.attention.flex_attention import BlockMask
from diffusers.models.modeling_utils import ModelMixin
import torch.nn as nn
import torch
import math
import torch.distributed as dist
import os
# wan 1.3B model has a weird channel / head configurations and require max-autotune to work with flexattention
# see https://github.com/pytorch/pytorch/issues/133254
# change to default for other models
flex_attention = torch.compile(
flex_attention, dynamic=False, mode="max-autotune-no-cudagraphs")
def causal_rope_apply(x, grid_sizes, freqs, start_frame=0):
n, c = x.size(2), x.size(3) // 2
# split freqs
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
# loop over samples
output = []
for i, (f, h, w) in enumerate(grid_sizes.tolist()):
seq_len = f * h * w
# precompute multipliers
x_i = torch.view_as_complex(x[i, :seq_len].to(torch.float64).reshape(
seq_len, n, -1, 2))
freqs_i = torch.cat([
freqs[0][start_frame:start_frame + f].view(f, 1, 1, -1).expand(f, h, w, -1),
freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1)
],
dim=-1).reshape(seq_len, 1, -1)
# apply rotary embedding
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
x_i = torch.cat([x_i, x[i, seq_len:]])
# append to collection
output.append(x_i)
return torch.stack(output).type_as(x)
def causal_rope_apply_pruned(x, grid_sizes, freqs, pruning_info, start_frame=0):
"""
对pruned tokens应用RoPE,使用kept_indices的原始位置信息
现在每个frame保留相同数量的tokens,可以正常reshape
Args:
x: [B, F*N_kept, num_heads, head_dim] pruned tokens(每个frame N_kept个)
grid_sizes: [B, 3] 原始的(F, H, W)
freqs: RoPE频率参数
pruning_info: dict 包含kept_indices等信息
start_frame: 起始帧索引
Returns:
[B, F*N_kept, num_heads, head_dim] 应用RoPE后的tokens
"""
B, total_len, n, head_dim = x.shape
c = head_dim // 2
kept_indices_list = pruning_info['kept_indices']
F = pruning_info['num_frames']
tokens_per_frame_kept = pruning_info['tokens_per_frame_kept']
tokens_per_frame_original = pruning_info['tokens_per_frame_original']
# split freqs
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
output = []
for i, (f, h, w) in enumerate(grid_sizes.tolist()):
assert f == F and h * w == tokens_per_frame_original
kept_idx = kept_indices_list[i] # [F*tokens_per_frame_kept]
# 将kept_idx转换为3D坐标(frame, height, width)
frame_indices = kept_idx // tokens_per_frame_original
spatial_idx = kept_idx % tokens_per_frame_original
height_indices = spatial_idx // w
width_indices = spatial_idx % w
# 获取对应位置的频率
freqs_f = freqs[0][start_frame + frame_indices] # [F*tokens_per_frame_kept, c1]
freqs_h = freqs[1][height_indices] # [F*tokens_per_frame_kept, c2]
freqs_w = freqs[2][width_indices] # [F*tokens_per_frame_kept, c3]
freqs_i = torch.cat([freqs_f, freqs_h, freqs_w], dim=1) # [F*tokens_per_frame_kept, c]
# 应用RoPE
num_tokens = len(kept_idx) # F*tokens_per_frame_kept
x_i = x[i].to(torch.float64) # [num_tokens, n, head_dim]
x_i = torch.view_as_complex(x_i.reshape(num_tokens, n, c, 2))
freqs_i = freqs_i.unsqueeze(1) # [num_tokens, 1, c]
# 应用旋转
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
output.append(x_i)
return torch.stack(output).type_as(x)
class CausalWanSelfAttention(nn.Module):
def __init__(self,
dim,
num_heads,
local_attn_size=-1,
sink_size=0,
qk_norm=True,
eps=1e-6,
block_id=None):
assert dim % num_heads == 0
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.local_attn_size = local_attn_size
self.sink_size = sink_size
self.qk_norm = qk_norm
self.eps = eps
self.max_attention_size = 32760 if local_attn_size == -1 else local_attn_size * 1560
# layers
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=eps) if qk_norm else nn.Identity()
self.norm_k = WanRMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
self.block_id = block_id
def forward(
self,
x,
seq_lens,
grid_sizes,
freqs,
block_mask,
kv_cache=None,
current_start=0,
cache_start=None,
debug_dict=None,
pruning_info=None # 🆕 接收pruning信息
):
r"""
Args:
x(Tensor): Shape [B, L, num_heads, C / num_heads]
seq_lens(Tensor): Shape [B]
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
block_mask (BlockMask)
"""
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
if cache_start is None:
cache_start = current_start
# query, key, value function
def qkv_fn(x):
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)
return q, k, v
q, k, v = qkv_fn(x)
if kv_cache is None:
# if it is teacher forcing training?
is_tf = (s == seq_lens[0].item() * 2)
if is_tf:
q_chunk = torch.chunk(q, 2, dim=1)
k_chunk = torch.chunk(k, 2, dim=1)
roped_query = []
roped_key = []
# rope should be same for clean and noisy parts
for ii in range(2):
rq = rope_apply(q_chunk[ii], grid_sizes, freqs).type_as(v)
rk = rope_apply(k_chunk[ii], grid_sizes, freqs).type_as(v)
roped_query.append(rq)
roped_key.append(rk)
roped_query = torch.cat(roped_query, dim=1)
roped_key = torch.cat(roped_key, dim=1)
padded_length = math.ceil(q.shape[1] / 128) * 128 - q.shape[1]
padded_roped_query = torch.cat(
[roped_query,
torch.zeros([q.shape[0], padded_length, q.shape[2], q.shape[3]],
device=q.device, dtype=v.dtype)],
dim=1
)
padded_roped_key = torch.cat(
[roped_key, torch.zeros([k.shape[0], padded_length, k.shape[2], k.shape[3]],
device=k.device, dtype=v.dtype)],
dim=1
)
padded_v = torch.cat(
[v, torch.zeros([v.shape[0], padded_length, v.shape[2], v.shape[3]],
device=v.device, dtype=v.dtype)],
dim=1
)
x = flex_attention(
query=padded_roped_query.transpose(2, 1),
key=padded_roped_key.transpose(2, 1),
value=padded_v.transpose(2, 1),
block_mask=block_mask
)[:, :, :-padded_length].transpose(2, 1)
else:
# 🎯 根据pruning_info选择RoPE函数
if pruning_info and pruning_info.get('pruned', False):
# 使用pruned RoPE,保持原始位置信息
roped_query = causal_rope_apply_pruned(
q, grid_sizes, freqs, pruning_info, start_frame=0
).type_as(v)
roped_key = causal_rope_apply_pruned(
k, grid_sizes, freqs, pruning_info, start_frame=0
).type_as(v)
else:
# 正常的RoPE
roped_query = rope_apply(q, grid_sizes, freqs).type_as(v)
roped_key = rope_apply(k, grid_sizes, freqs).type_as(v)
padded_length = math.ceil(q.shape[1] / 128) * 128 - q.shape[1]
padded_roped_query = torch.cat(
[roped_query,
torch.zeros([q.shape[0], padded_length, q.shape[2], q.shape[3]],
device=q.device, dtype=v.dtype)],
dim=1
)
padded_roped_key = torch.cat(
[roped_key, torch.zeros([k.shape[0], padded_length, k.shape[2], k.shape[3]],
device=k.device, dtype=v.dtype)],
dim=1
)
padded_v = torch.cat(
[v, torch.zeros([v.shape[0], padded_length, v.shape[2], v.shape[3]],
device=v.device, dtype=v.dtype)],
dim=1
)
x = flex_attention(
query=padded_roped_query.transpose(2, 1),
key=padded_roped_key.transpose(2, 1),
value=padded_v.transpose(2, 1),
block_mask=block_mask
)[:, :, :-padded_length].transpose(2, 1)
else:
frame_seqlen = math.prod(grid_sizes[0][1:]).item()
current_start_frame = current_start // frame_seqlen
# 🎯 根据pruning_info选择RoPE函数(kv_cache分支)
if pruning_info and pruning_info.get('pruned', False):
roped_query = causal_rope_apply_pruned(
q, grid_sizes, freqs, pruning_info,
start_frame=current_start_frame
).type_as(v)
roped_key = causal_rope_apply_pruned(
k, grid_sizes, freqs, pruning_info,
start_frame=current_start_frame
).type_as(v)
else:
roped_query = causal_rope_apply(
q, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
roped_key = causal_rope_apply(
k, grid_sizes, freqs, start_frame=current_start_frame).type_as(v)
current_end = current_start + roped_query.shape[1]
sink_tokens = self.sink_size * frame_seqlen
# If we are using local attention and the current KV cache size is larger than the local attention size, we need to truncate the KV cache
kv_cache_size = kv_cache["k"].shape[1]
num_new_tokens = roped_query.shape[1]
if self.local_attn_size != -1 and (current_end > kv_cache["global_end_index"].item()) and (
num_new_tokens + kv_cache["local_end_index"].item() > kv_cache_size):
# Calculate the number of new tokens added in this step
# Shift existing cache content left to discard oldest tokens
# Clone the source slice to avoid overlapping memory error
num_evicted_tokens = num_new_tokens + kv_cache["local_end_index"].item() - kv_cache_size
num_rolled_tokens = kv_cache["local_end_index"].item() - num_evicted_tokens - sink_tokens
kv_cache["k"][:, sink_tokens:sink_tokens + num_rolled_tokens] = \
kv_cache["k"][:, sink_tokens + num_evicted_tokens:sink_tokens + num_evicted_tokens + num_rolled_tokens].clone()
kv_cache["v"][:, sink_tokens:sink_tokens + num_rolled_tokens] = \
kv_cache["v"][:, sink_tokens + num_evicted_tokens:sink_tokens + num_evicted_tokens + num_rolled_tokens].clone()
# Insert the new keys/values at the end
local_end_index = kv_cache["local_end_index"].item() + current_end - \
kv_cache["global_end_index"].item() - num_evicted_tokens
local_start_index = local_end_index - num_new_tokens
# if debug_dict["block_index"] == 0 :
# print(f"[KVCache DEBUG] {local_start_index} to {local_end_index}, {sink_tokens=}")
# print(f"[KVCache DEBUG] {roped_key.shape=}")
# print(f"[KVCache DEBUG] {v.shape=}")
kv_cache["k"][:, local_start_index:local_end_index] = roped_key
kv_cache["v"][:, local_start_index:local_end_index] = v
else:
# Assign new keys/values directly up to current_end
local_end_index = kv_cache["local_end_index"].item() + current_end - kv_cache["global_end_index"].item()
local_start_index = local_end_index - num_new_tokens
# if debug_dict is not None and debug_dict["block_index"] == 0 :
# print(f"[KVCache DEBUG] {local_start_index} to {local_end_index}, {sink_tokens=}")
# print(f"[KVCache DEBUG] {roped_key.shape=}")
# print(f"[KVCache DEBUG] {v.shape=}")
kv_cache["k"][:, local_start_index:local_end_index] = roped_key
kv_cache["v"][:, local_start_index:local_end_index] = v
# print(f"{max(0, local_end_index - self.max_attention_size)} to {local_end_index}")
#
x = attention( # 对应wan/modules/attention.py中的attention函数
roped_query,
kv_cache["k"][:, max(0, local_end_index - self.max_attention_size):local_end_index],
kv_cache["v"][:, max(0, local_end_index - self.max_attention_size):local_end_index],
debug_dict=debug_dict
)
kv_cache["global_end_index"].fill_(current_end)
kv_cache["local_end_index"].fill_(local_end_index)
# output
x = x.flatten(2)
x = self.o(x)
return x
class CausalWanAttentionBlock(nn.Module):
def __init__(self,
cross_attn_type,
dim,
ffn_dim,
num_heads,
local_attn_size=-1,
sink_size=0,
qk_norm=True,
cross_attn_norm=False,
eps=1e-6,
block_id=None):
super().__init__()
self.dim = dim
self.ffn_dim = ffn_dim
self.num_heads = num_heads
self.local_attn_size = local_attn_size
self.qk_norm = qk_norm
self.cross_attn_norm = cross_attn_norm
self.eps = eps
# layers
self.norm1 = WanLayerNorm(dim, eps)
self.self_attn = CausalWanSelfAttention(dim, num_heads, local_attn_size, sink_size, qk_norm, eps, block_id)
self.norm3 = WanLayerNorm(
dim, eps,
elementwise_affine=True) if cross_attn_norm else nn.Identity()
self.cross_attn = WAN_CROSSATTENTION_CLASSES[cross_attn_type](dim,
num_heads,
(-1, -1),
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.ffn_hidden_dim = ffn_dim
# modulation
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
self.block_id = block_id
def _block_internal_prune(self, x, kept_indices_per_frame, num_frames):
"""
Block内部pruning:在计算delta时对输入进行prune
Args:
x: [B, F*H*W, C] 完整的输入tokens
kept_indices_per_frame: List[Tensor], 每个frame的kept indices
num_frames: int, 帧数
Returns:
x_pruned: [B, total_kept, C] pruned后的tokens
flat_kept_indices: [total_kept] 扁平化的全局indices
"""
B, L, C = x.shape
tokens_per_frame = L // num_frames
# 收集所有kept indices(转换为全局索引)
flat_kept_indices = []
for f_idx in range(num_frames):
frame_kept = kept_indices_per_frame[f_idx] # [kept_count]
global_indices = frame_kept + f_idx * tokens_per_frame
flat_kept_indices.append(global_indices)
flat_kept_indices = torch.cat(flat_kept_indices, dim=0) # [total_kept]
# Gather pruned tokens
x_pruned = x[:, flat_kept_indices, :] # [B, total_kept, C]
return x_pruned, flat_kept_indices
def _block_internal_restore(self, delta_pruned, x_full, flat_kept_indices, block_idx=None,
first_chunk_cache=None, cache_item="x", fill_strategy="prev_step",
num_frames=None, frame_seqlen=None, prev_step_deltas=None, source_latent=None):
"""
Block内部restore:将pruned delta恢复到完整维度
Args:
delta_pruned: [B, total_kept, C] pruned计算的delta
x_full: [B, F*H*W, C] 完整的x(用于获取shape)
flat_kept_indices: [total_kept] 全局kept indices
block_idx: int, 当前block的索引
first_chunk_cache: dict, first chunk的缓存
cache_item: str, 缓存内容类型(当fill_strategy="first_chunk"时使用)
- "x": 缓存block输入x
- "delta": 缓存delta(y_full)
fill_strategy: str, 未pruning位置的填充策略
- "identity": delta=0,保持当前x不变
- "prev_step": 使用前一个全量计算step的delta(推荐)
- "first_chunk": 使用first chunk的值(根据cache_item)
- "source_latent": 使用source_latent (y_input),delta = source - current_x
- "avg_delta": 使用保留token的平均delta
- "interpolate": 从周围保留token插值
- "zero": 填充0(等同identity)
num_frames: int, 帧数(插值时需要)
frame_seqlen: int, 每帧token数(插值时需要)
prev_step_deltas: dict, 上一个step的delta缓存 {block_idx: delta}
source_latent: Tensor, 源latent (y_input),用于source_latent策略
Returns:
delta_full: [B, F*H*W, C] 完整维度的delta
- 保留位置:使用计算得到的delta
- 未保留位置:根据fill_strategy填充
"""
B, L, C = x_full.shape
delta_full = torch.zeros_like(x_full) # [B, L, C]
# Scatter pruned delta到保留位置
delta_full[:, flat_kept_indices, :] = delta_pruned
# 创建未保留位置的mask
all_indices = torch.arange(L, device=x_full.device)
kept_mask = torch.zeros(L, dtype=torch.bool, device=x_full.device)
kept_mask[flat_kept_indices] = True
unkept_indices = all_indices[~kept_mask]
if len(unkept_indices) == 0:
# 没有未保留位置,直接返回
return delta_full
# 🎨 根据fill_strategy填充未保留位置
if fill_strategy == "zero" or fill_strategy == "identity":
# 策略1&2: delta=0,保持当前x不变(已经是0了,不需要操作)
pass
elif fill_strategy == "prev_step":
# 策略: 使用前一个全量计算step的delta(⭐推荐)
# if block_idx == 0:
# print(f"[Debug Fill] prev_step_deltas is not None: {prev_step_deltas is not None}")
# print(f"[Debug Fill] block_idx is not None: {block_idx is not None}")
# if prev_step_deltas is not None:
# print(f"[Debug Fill] block_idx in prev_step_deltas: {block_idx in prev_step_deltas}")
# print(f"[Debug Fill] prev_step_deltas.keys(): {list(prev_step_deltas.keys())}")
# print(f"[Debug Fill] not self.training: {not self.training}")
if (prev_step_deltas is not None and block_idx is not None and \
block_idx in prev_step_deltas and not self.training):
prev_delta = prev_step_deltas[block_idx]
# 使用上一个step的delta填充未保留位置
delta_full[:, unkept_indices, :] = prev_delta[:, unkept_indices, :]
# if block_idx == 0:
# print(f"[Debug Fill] Successfully filled {len(unkept_indices)} unkept positions from prev_step")
elif fill_strategy == "debug":
# 策略: 使用前一个全量计算step的delta 直接覆盖
if (prev_step_deltas is not None and block_idx is not None and
block_idx in prev_step_deltas and not self.training):
prev_delta = prev_step_deltas[block_idx]
# 使用上一个step的delta填充未保留位置
delta_full = prev_delta
elif fill_strategy == "first_chunk":
# 策略: 使用first chunk的值
if (first_chunk_cache is not None and block_idx is not None and
block_idx in first_chunk_cache and not self.training):
if cache_item == "x":
# 缓存的是x:delta = first_chunk_x - current_x
first_chunk_x = first_chunk_cache[block_idx]
delta_full[:, unkept_indices, :] = first_chunk_x[:, unkept_indices, :] - x_full[:, unkept_indices, :]
elif cache_item == "delta":
# 缓存的是delta:直接使用
first_chunk_delta = first_chunk_cache[block_idx]
delta_full[:, unkept_indices, :] = first_chunk_delta[:, unkept_indices, :]
elif fill_strategy == "source_latent":
# 策略: 使用source_latent (y_input)
# delta = source - current_x
if source_latent is not None:
delta_full[:, unkept_indices, :] = source_latent[:, unkept_indices, :] - x_full[:, unkept_indices, :]
elif fill_strategy == "avg_delta":
# 策略: 使用保留token的平均delta
if len(flat_kept_indices) > 0:
avg_delta = delta_pruned.mean(dim=1, keepdim=True) # [B, 1, C]
delta_full[:, unkept_indices, :] = avg_delta.expand(B, len(unkept_indices), C)
elif fill_strategy == "interpolate":
# 策略: 从周围保留token插值(2D空间插值)
if num_frames is not None and frame_seqlen is not None:
# 需要2D grid信息进行插值
# 这里实现一个简单的最近邻插值
H = int(frame_seqlen ** 0.5)
W = frame_seqlen // H
for b in range(B):
for f_idx in range(num_frames):
# 当前帧的范围
frame_start = f_idx * frame_seqlen
frame_end = (f_idx + 1) * frame_seqlen
# 当前帧的kept和unkept indices(转为局部索引)
frame_kept_global = flat_kept_indices[(flat_kept_indices >= frame_start) & (flat_kept_indices < frame_end)]
frame_kept_local = frame_kept_global - frame_start
frame_unkept_global = unkept_indices[(unkept_indices >= frame_start) & (unkept_indices < frame_end)]
frame_unkept_local = frame_unkept_global - frame_start
if len(frame_kept_local) == 0 or len(frame_unkept_local) == 0:
continue
# 转换为2D坐标
kept_y = frame_kept_local // W
kept_x = frame_kept_local % W
unkept_y = frame_unkept_local // W
unkept_x = frame_unkept_local % W
# 对每个unkept位置,找最近的kept位置
for i, unk_idx in enumerate(frame_unkept_local):
uy, ux = unkept_y[i].item(), unkept_x[i].item()
# 计算距离
dist = (kept_y - uy) ** 2 + (kept_x - ux) ** 2
nearest_idx = dist.argmin()
# 使用最近邻的delta
nearest_global_idx = frame_kept_global[nearest_idx]
delta_full[b, frame_start + unk_idx] = delta_full[b, nearest_global_idx]
return delta_full
def forward(
self,
x,
e,
seq_lens,
grid_sizes,
freqs,
context,
context_lens,
block_mask,
kv_cache=None,
crossattn_cache=None,
current_start=0,
cache_start=None,
debug_dict=None,
pruning_info=None,
block_idx=None # 🆕 用于保存/读取first chunk缓存
):
r"""
Args:
x(Tensor): Shape [B, L, C] - 保持完整维度
e(Tensor): Shape [B, F, 6, C]
seq_lens(Tensor): Shape [B], length of each sequence in batch
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
pruning_info(dict): {'internal_pruning': bool, 'kept_indices_per_frame': List[Tensor]}
"""
num_frames = e.shape[1]
e = (self.modulation.unsqueeze(1) + e).chunk(6, dim=2)
# ✨ Block内部Pruning模式判断
use_block_internal_pruning = (
pruning_info is not None and
pruning_info.get('internal_pruning', False) and
'kept_indices_per_frame' in pruning_info and
pruning_info.get('use_pruning', False) # 🆕 新增:检查是否启用pruning
)
# 🆕 层级控制:检查是否在指定层进行pruning
pruning_layers = pruning_info.get('pruning_layers', set()) if pruning_info else set()
use_self_attn_pruning = use_block_internal_pruning and "self_attn" in pruning_layers
use_ffn_pruning = use_block_internal_pruning and "ffn" in pruning_layers
# 🆕 提前提取first_chunk_cache、cache_item、fill_strategy和prev_step_deltas(供所有分支使用)
first_chunk_cache = pruning_info.get('first_chunk_block_inputs', {}) if pruning_info else {}
cache_item = pruning_info.get('cache_item', 'x') if pruning_info else 'x'
fill_strategy = pruning_info.get('fill_strategy', 'prev_step') if pruning_info else 'prev_step'
use_mean_alignment = pruning_info.get('use_mean_alignment', True) if pruning_info else True # 🆕 均值对齐开关
source_latent = pruning_info.get('source_latent', None) if pruning_info else None # 🆕 源latent (y_input)
# ⚠️ 关键:直接使用pruning_info中的引用,如果不存在则用空字典(会导致保存失败)
# 这样修改局部变量会直接影响pruning_info中的字典
prev_step_self_attn_deltas = pruning_info.get('prev_step_self_attn_deltas', {}) if pruning_info else {}
prev_step_ffn_deltas = pruning_info.get('prev_step_ffn_deltas', {}) if pruning_info else {}
# 🐛 Debug: 打印引用检查
# if block_idx == 0 and pruning_info and 'prev_step_self_attn_deltas' in pruning_info:
# print(f"[Block Debug] prev_step_self_attn_deltas is pruning_info ref: {prev_step_self_attn_deltas is pruning_info['prev_step_self_attn_deltas']}")
# print(f"[Block Debug] prev_step_self_attn_deltas id: {id(prev_step_self_attn_deltas)}, pruning_info id: {id(pruning_info['prev_step_self_attn_deltas'])}")
# 原始frame_seqlen(x始终保持完整)
frame_seqlen = x.shape[1] // num_frames
# ========== Self-Attention with Block Internal Pruning ==========
if use_self_attn_pruning:
# 💾 保存first chunk(根据cache_item决定保存x还是delta)
should_cache = (block_idx is not None and block_idx not in first_chunk_cache and not self.training)
if should_cache and cache_item == "x":
# 缓存x:在计算前保存
first_chunk_cache[block_idx] = x.detach().clone()
# 1. Norm + Modulation (完整维度)
x_norm = (self.norm1(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (1 + e[1]) + e[0]).flatten(1, 2)
# 2. Prune输入
x_pruned, flat_kept_indices = self._block_internal_prune(
x_norm,
pruning_info['kept_indices_per_frame'],
num_frames
)
# 3. Self-Attn计算pruned delta
# 🔑 构造兼容旧格式的pruning_info(用于RoPE)
pruned_info = {
'pruned': True,
'tokens_per_frame_kept': len(pruning_info['kept_indices_per_frame'][0]),
'tokens_per_frame_original': frame_seqlen,
'num_frames': num_frames,
'kept_indices': [flat_kept_indices] # List of [total_kept] for batch
}
y_pruned = self.self_attn(
x_pruned, seq_lens, grid_sizes, freqs, block_mask,
kv_cache, current_start, cache_start, debug_dict, pruned_info
)
# 🔧 对齐y_pruned与prev_step delta的均值(如果使用prev_step填充策略且启用均值对齐)
# print("DEBUG: use_mean_alignment", use_mean_alignment, "fill_strategy", fill_strategy, "block_idx", block_idx)
if use_mean_alignment and (fill_strategy in ["prev_step", "debug"]) and block_idx in prev_step_self_attn_deltas:
prev_delta = prev_step_self_attn_deltas[block_idx] # [B, L, D]
# 计算y_pruned的均值
y_pruned_mean = y_pruned.mean(dim=(0, 1), keepdim=True) # [1, 1, D]
# 计算kept位置对应的prev_delta均值
prev_delta_kept = prev_delta[:, flat_kept_indices, :] # [B, kept, D]
prev_delta_kept_mean = prev_delta_kept.mean(dim=(0, 1), keepdim=True) # [1, 1, D]
# 对齐:y_pruned -= (y_pruned_mean - prev_delta_kept_mean)
y_pruned = y_pruned - (y_pruned_mean - prev_delta_kept_mean)
if block_idx == 0:
print(f"[Mean Align] Self-Attn: y_pruned_mean={y_pruned_mean[0,0,:3].tolist()}, "
f"prev_kept_mean={prev_delta_kept_mean[0,0,:3].tolist()}")
# 4. Restore delta到完整维度(使用first chunk填充未保留位置)
y_full = self._block_internal_restore(
y_pruned, x, flat_kept_indices, block_idx, first_chunk_cache, cache_item, fill_strategy,
num_frames, frame_seqlen, prev_step_self_attn_deltas, source_latent # ✅ 传递source_latent
)
# if block_idx == 0:
# print("[Reuse] Self Attn delta in forward pass")
# 💾 保存当前step的self-attn delta(用于下一个pruning step填充)
# 注意:保存未经modulation的delta,使用时需要应用当前step的modulation
# if block_idx is not None and not self.training:
# # prev_step_self_attn_deltas[block_idx] = y_full.detach().clone()
# if block_idx == 0:
# # 🔍 打印填充后的完整delta统计(包含保留+填充部分)
# filled_mean = y_full.mean().item()
# filled_std = y_full.std().item()
# # 🔍 打印pruned delta统计(仅保留部分,真实计算的delta)
# pruned_mean = y_pruned.mean().item()
# pruned_std = y_pruned.std().item()
# print(f"[Reuse] Filled delta: mean={filled_mean:.6f}, std={filled_std:.6f}")
# print(f"[Reuse] Pruned delta (computed): mean={pruned_mean:.6f}, std={pruned_std:.6f}")
# 5. 残差连接(x保持完整)
x = x + (y_full.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * e[2]).flatten(1, 2)
else:
# 原始流程(无pruning,全量计算)
y = self.self_attn(
(self.norm1(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (1 + e[1]) + e[0]).flatten(1, 2),
seq_lens, grid_sizes, freqs, block_mask, kv_cache, current_start, cache_start, debug_dict, None
)
# print(f"[Shapes] self-attn {y.shape=}")
# 💾 全量计算时也保存self-attn delta(用于下一个pruning step填充)
if block_idx is not None and not self.training:
# y是完整的delta,直接保存
prev_step_self_attn_deltas[block_idx]=y
# if block_idx == 0:
# delta_mean = y.mean().item()
# delta_std = y.std().item()
# print(f"[Cache] Saving Self-Attn delta: mean={delta_mean:.6f}, std={delta_std:.6f} in block {block_idx}")
x = x + (y.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * e[2]).flatten(1, 2)
# ========== Cross-Attention & FFN ==========
def cross_attn_ffn(x, context, context_lens, e, crossattn_cache=None, debug_dict=None,
block_idx=None, first_chunk_cache=None, use_ffn_pruning=False, cache_item="x",
fill_strategy="prev_step", prev_step_ffn_deltas=None):
# Cross-Attention (不prune,因为涉及外部context)
x = x + self.cross_attn(self.norm3(x), context, context_lens,
crossattn_cache=crossattn_cache, debug_dict=debug_dict)
# FFN with Block Internal Pruning
if use_ffn_pruning:
# 💾 保存first chunk(如果cache_item="x"且还没缓存)
# 注意:如果self-attention已经缓存了,这里不重复缓存
should_cache_ffn = (block_idx is not None and block_idx not in first_chunk_cache and not self.training)
if should_cache_ffn and cache_item == "x":
first_chunk_cache[block_idx] = x.detach().clone()
# print(f"[Block {block_idx} FFN] 💾 Saved first chunk x: {x.shape}")
# 1. Norm + Modulation
x_norm = (self.norm2(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (1 + e[4]) + e[3]).flatten(1, 2)
# 2. Prune输入
x_pruned, flat_kept_indices = self._block_internal_prune(
x_norm,
pruning_info['kept_indices_per_frame'],
num_frames
)
# 3. FFN计算pruned delta
x_norm_for_ffn = x_pruned
# 先正常计算
y_pruned = self.ffn(x_norm_for_ffn)
# 🔍 保存FFN中间激活(如果启用debug)
if debug_dict is not None and debug_dict.get('visualize_ffn', False) and block_idx == 0:
with torch.no_grad():
ffn_hidden = self.ffn[0](x_norm_for_ffn)
ffn_act = self.ffn[1](ffn_hidden)
import numpy as np
save_dir = debug_dict.get("save_path", "./visual-output").replace(os.path.basename(debug_dict.get("save_path", "")), "")
os.makedirs(save_dir, exist_ok=True)
chunk_idx = debug_dict.get('current_chunk', 0)
step_idx = debug_dict.get('current_step', 0)
np.save(os.path.join(save_dir, f'ffn_hidden_{chunk_idx}_{step_idx}.npy'),
ffn_hidden.cpu().float().numpy())
np.save(os.path.join(save_dir, f'ffn_act_{chunk_idx}_{step_idx}.npy'),
ffn_act.cpu().float().numpy())
np.save(os.path.join(save_dir, f'ffn_output_{chunk_idx}_{step_idx}.npy'),
y_pruned.cpu().float().numpy())
print(f"[Debug] Saved FFN activations: hidden={ffn_hidden.shape}, act={ffn_act.shape}, output={y_pruned.shape}")
# 🔧 对齐y_pruned与prev_step delta的均值(如果使用prev_step填充策略且启用均值对齐)
if use_mean_alignment and (fill_strategy in ["prev_step", "debug"]) and block_idx in prev_step_ffn_deltas:
prev_delta = prev_step_ffn_deltas[block_idx] # [B, L, D]
# 计算y_pruned的均值
y_pruned_mean = y_pruned.mean(dim=(0, 1), keepdim=True) # [1, 1, D]
# 计算kept位置对应的prev_delta均值
prev_delta_kept = prev_delta[:, flat_kept_indices, :] # [B, kept, D]
prev_delta_kept_mean = prev_delta_kept.mean(dim=(0, 1), keepdim=True) # [1, 1, D]
# 对齐:y_pruned -= (y_pruned_mean - prev_delta_kept_mean)
y_pruned = y_pruned - (y_pruned_mean - prev_delta_kept_mean)
if block_idx == 0:
print(f"[Mean Align] FFN: y_pruned_mean={y_pruned_mean[0,0,:3].tolist()}, "
f"prev_kept_mean={prev_delta_kept_mean[0,0,:3].tolist()}")
# 4. Restore delta(使用first chunk填充未保留位置)
y_full = self._block_internal_restore(
y_pruned, x, flat_kept_indices, block_idx, first_chunk_cache, cache_item, fill_strategy,
num_frames, frame_seqlen, prev_step_ffn_deltas, source_latent # ✅ 传递source_latent
)
# if block_idx == 0:
# print("[Reuse] FFN delta in cross_attn_ffn")
# 5. 残差连接
x = x + (y_full.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * e[5]).flatten(1, 2)
else:
# 原始流程(无pruning,全量计算)
x_norm_input = (self.norm2(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (1 + e[4]) + e[3]).flatten(1, 2)
# 先正常计算
y = self.ffn(x_norm_input)
# 🔍 保存FFN中间激活(如果启用debug)
if debug_dict is not None and debug_dict.get('visualize_ffn', False) and block_idx == 0:
with torch.no_grad():
ffn_hidden = self.ffn[0](x_norm_input)
ffn_act = self.ffn[1](ffn_hidden)
import numpy as np
save_dir = debug_dict.get("save_path", "./visual-output").replace(os.path.basename(debug_dict.get("save_path", "")), "")
os.makedirs(save_dir, exist_ok=True)
chunk_idx = debug_dict.get('current_chunk', 0)
step_idx = debug_dict.get('current_step', 0)
np.save(os.path.join(save_dir, f'ffn_hidden_{chunk_idx}_{step_idx}.npy'),
ffn_hidden.cpu().float().numpy())
np.save(os.path.join(save_dir, f'ffn_act_{chunk_idx}_{step_idx}.npy'),
ffn_act.cpu().float().numpy())
np.save(os.path.join(save_dir, f'ffn_output_{chunk_idx}_{step_idx}.npy'),
y.cpu().float().numpy())
print(f"[Debug] Saved FFN activations: hidden={ffn_hidden.shape}, act={ffn_act.shape}, output={y.shape}")
# print(f"[Shapes] ffn {y.shape=}")
# 💾 全量计算时也保存FFN delta(用于下一个pruning step填充)
if block_idx is not None and not self.training:
prev_step_ffn_deltas[block_idx] = y #.detach().clone()
# if block_idx == 0:
# print("[Cache] Saving FFN delta in cross_attn_ffn")
x = x + (y.unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * e[5]).flatten(1, 2)
return x
x = cross_attn_ffn(x, context, context_lens, e, crossattn_cache, debug_dict,
block_idx, first_chunk_cache, use_ffn_pruning, cache_item, fill_strategy, prev_step_ffn_deltas)
return x
class CausalHead(nn.Module):
def __init__(self, dim, out_dim, patch_size, eps=1e-6):
super().__init__()
self.dim = dim
self.out_dim = out_dim
self.patch_size = patch_size
self.eps = eps
# layers
out_dim = math.prod(patch_size) * out_dim
self.norm = WanLayerNorm(dim, eps)
self.head = nn.Linear(dim, out_dim)
# modulation
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
def forward(self, x, e):
r"""
Args:
x(Tensor): Shape [B, L1, C]
e(Tensor): Shape [B, F, 1, C]
"""
# assert e.dtype == torch.float32
# with amp.autocast(dtype=torch.float32):
num_frames, frame_seqlen = e.shape[1], x.shape[1] // e.shape[1]
e = (self.modulation.unsqueeze(1) + e).chunk(2, dim=2)
x = (self.head(self.norm(x).unflatten(dim=1, sizes=(num_frames, frame_seqlen)) * (1 + e[1]) + e[0]))
return x
class CausalWanModel(ModelMixin, ConfigMixin):
r"""
Wan diffusion backbone supporting both text-to-video and image-to-video.
"""
ignore_for_config = [
'patch_size', 'cross_attn_norm', 'qk_norm', 'text_dim'
]
_no_split_modules = ['WanAttentionBlock']
_supports_gradient_checkpointing = True
@register_to_config
def __init__(self,
model_type='t2v',
patch_size=(1, 2, 2),
text_len=512,
in_dim=16,
dim=2048,
ffn_dim=8192,
freq_dim=256,
text_dim=4096,
out_dim=16,
num_heads=16,
num_layers=32,
local_attn_size=-1,
sink_size=0,
qk_norm=True,
cross_attn_norm=True,
eps=1e-6):
r"""
Initialize the diffusion model backbone.
Args:
model_type (`str`, *optional*, defaults to 't2v'):
Model variant - 't2v' (text-to-video) or 'i2v' (image-to-video)
patch_size (`tuple`, *optional*, defaults to (1, 2, 2)):
3D patch dimensions for video embedding (t_patch, h_patch, w_patch)
text_len (`int`, *optional*, defaults to 512):
Fixed length for text embeddings
in_dim (`int`, *optional*, defaults to 16):
Input video channels (C_in)
dim (`int`, *optional*, defaults to 2048):
Hidden dimension of the transformer
ffn_dim (`int`, *optional*, defaults to 8192):
Intermediate dimension in feed-forward network
freq_dim (`int`, *optional*, defaults to 256):
Dimension for sinusoidal time embeddings
text_dim (`int`, *optional*, defaults to 4096):
Input dimension for text embeddings
out_dim (`int`, *optional*, defaults to 16):
Output video channels (C_out)
num_heads (`int`, *optional*, defaults to 16):
Number of attention heads
num_layers (`int`, *optional*, defaults to 32):
Number of transformer blocks
local_attn_size (`int`, *optional*, defaults to -1):
Window size for temporal local attention (-1 indicates global attention)
sink_size (`int`, *optional*, defaults to 0):
Size of the attention sink, we keep the first `sink_size` frames unchanged when rolling the KV cache
qk_norm (`bool`, *optional*, defaults to True):
Enable query/key normalization
cross_attn_norm (`bool`, *optional*, defaults to False):
Enable cross-attention normalization
eps (`float`, *optional*, defaults to 1e-6):
Epsilon value for normalization layers
"""
super().__init__()
assert model_type in ['t2v', 'i2v']
self.model_type = model_type
self.patch_size = patch_size
self.text_len = text_len
self.in_dim = in_dim
self.dim = dim
self.ffn_dim = ffn_dim
self.freq_dim = freq_dim
self.text_dim = text_dim
self.out_dim = out_dim
self.num_heads = num_heads
self.num_layers = num_layers
self.local_attn_size = local_attn_size
self.qk_norm = qk_norm
self.cross_attn_norm = cross_attn_norm
self.eps = eps
# embeddings
self.patch_embedding = nn.Conv3d(
in_dim, dim, kernel_size=patch_size, stride=patch_size)
self.source_patch_embedding = None
self.text_embedding = nn.Sequential(
nn.Linear(text_dim, dim), nn.GELU(approximate='tanh'),
nn.Linear(dim, dim))
self.time_embedding = nn.Sequential(
nn.Linear(freq_dim, dim), nn.SiLU(), nn.Linear(dim, dim))
self.time_projection = nn.Sequential(
nn.SiLU(), nn.Linear(dim, dim * 6))
# blocks
cross_attn_type = 't2v_cross_attn' if model_type == 't2v' else 'i2v_cross_attn'
self.blocks = nn.ModuleList([
CausalWanAttentionBlock(cross_attn_type, dim, ffn_dim, num_heads,
local_attn_size, sink_size, qk_norm, cross_attn_norm, eps, block_id)
for block_id in range(num_layers)
])
# head
self.head = CausalHead(dim, out_dim, patch_size, eps)
# buffers (don't use register_buffer otherwise dtype will be changed in to())
assert (dim % num_heads) == 0 and (dim // num_heads) % 2 == 0
d = dim // num_heads
self.freqs = torch.cat([
rope_params(1024, d - 4 * (d // 6)),
rope_params(1024, 2 * (d // 6)),
rope_params(1024, 2 * (d // 6))
],
dim=1)
if model_type == 'i2v':
self.img_emb = MLPProj(1280, dim)
# initialize weights
self.init_weights()
self.gradient_checkpointing = False
self.block_mask = None
self.num_frame_per_block = 1
self.independent_first_frame = False
# Token Pruning配置(在patch_embedding后进行)
self.enable_internal_pruning = False
self.internal_pruning_layers = set() # 🆕 控制哪些层进行pruning: {"self_attn", "ffn"}
self.first_chunk_cache_item = "x"
self.unpruned_fill_strategy = "first_chunk"
self.use_mean_alignment = False
self.first_chunk_block_inputs = {}
self.prev_step_self_attn_deltas = {}
for i in range(self.num_layers):
self.prev_step_self_attn_deltas[i] = torch.zeros([1, 4680, 1536])
self.prev_step_ffn_deltas = {} # {block_idx: [B, F*H*W, D]}
for i in range(self.num_layers):
self.prev_step_ffn_deltas[i] = torch.zeros([1, 4680, 1536])
def _initialize_source_patch_embedding(self, model_path=None):
"""
初始化source_patch_embedding(只在unpruned_fill_strategy="source_latent"时调用)
从预训练模型加载patch_embedding权重
Args:
model_path: 模型路径,如果为None则使用默认路径
"""
if self.source_patch_embedding is None:
# 获取主patch_embedding的device和dtype
device = self.patch_embedding.weight.device
dtype = self.patch_embedding.weight.dtype
self.source_patch_embedding = nn.Conv3d(
self.in_dim, self.dim,
kernel_size=self.patch_size,
stride=self.patch_size
).to(device=device, dtype=dtype)
# 如果没有指定路径,使用默认路径
if model_path is None:
model_path = "wan_models/Wan2.1-T2V-1.3B/"
# 从预训练模型加载patch_embedding权重
try:
# 尝试加载safetensors格式
from safetensors.torch import load_file
checkpoint_path = f"{model_path}/diffusion_pytorch_model.safetensors"
state_dict = load_file(checkpoint_path)
print(f"[Source Patch Embedding] Loading from safetensors: {checkpoint_path}")
except Exception as e:
try:
# 如果safetensors失败,尝试.pth格式
checkpoint_path = f"{model_path}/diffusion_pytorch_model.pth"
state_dict = torch.load(checkpoint_path, map_location='cpu', weights_only=False)
print(f"[Source Patch Embedding] Loading from pth: {checkpoint_path}")
except Exception as e2:
# 如果都失败,则复制当前模型的权重
print(f"[Source Patch Embedding] Warning: Failed to load checkpoint, copying from current model")
print(f" Safetensors error: {e}")
print(f" PTH error: {e2}")
with torch.no_grad():
self.source_patch_embedding.weight.copy_(self.patch_embedding.weight)
if self.patch_embedding.bias is not None:
self.source_patch_embedding.bias.copy_(self.patch_embedding.bias)
# 设置为eval模式
self.source_patch_embedding.eval()
self.source_patch_embedding.requires_grad_(False)
print(f"[Source Patch Embedding] Initialized by copying from current model")
return
# 提取patch_embedding的权重
patch_weight_key = 'patch_embedding.weight'
patch_bias_key = 'patch_embedding.bias'
if patch_weight_key in state_dict:
with torch.no_grad():
# 转换到正确的dtype和device
weight = state_dict[patch_weight_key].to(device=device, dtype=dtype)
self.source_patch_embedding.weight.copy_(weight)
if patch_bias_key in state_dict and self.source_patch_embedding.bias is not None:
bias = state_dict[patch_bias_key].to(device=device, dtype=dtype)
self.source_patch_embedding.bias.copy_(bias)
print(f"[Source Patch Embedding] Loaded weights: {self.source_patch_embedding.weight.shape}, dtype={dtype}")
else:
# 如果找不到,则复制当前模型的权重
print(f"[Source Patch Embedding] Warning: 'patch_embedding' not found in checkpoint, copying from current model")
with torch.no_grad():
self.source_patch_embedding.weight.copy_(self.patch_embedding.weight)
if self.patch_embedding.bias is not None:
self.source_patch_embedding.bias.copy_(self.patch_embedding.bias)
# 设置为eval模式,不参与训练
self.source_patch_embedding.eval()
self.source_patch_embedding.requires_grad_(False)
print(f"[Source Patch Embedding] Initialized successfully")
def _set_gradient_checkpointing(self, module, value=False):
self.gradient_checkpointing = value
@staticmethod
def _prepare_blockwise_causal_attn_mask(
device: torch.device | str, num_frames: int = 21,
frame_seqlen: int = 1560, num_frame_per_block=1, local_attn_size=-1
) -> BlockMask:
"""
we will divide the token sequence into the following format
[1 latent frame] [1 latent frame] ... [1 latent frame]
We use flexattention to construct the attention mask
"""
total_length = num_frames * frame_seqlen
# we do right padding to get to a multiple of 128
padded_length = math.ceil(total_length / 128) * 128 - total_length
ends = torch.zeros(total_length + padded_length,
device=device, dtype=torch.long)
# Block-wise causal mask will attend to all elements that are before the end of the current chunk
frame_indices = torch.arange(
start=0,
end=total_length,
step=frame_seqlen * num_frame_per_block,
device=device
)
for tmp in frame_indices:
ends[tmp:tmp + frame_seqlen * num_frame_per_block] = tmp + \
frame_seqlen * num_frame_per_block
def attention_mask(b, h, q_idx, kv_idx):
if local_attn_size == -1:
return (kv_idx < ends[q_idx]) | (q_idx == kv_idx)
else:
return ((kv_idx < ends[q_idx]) & (kv_idx >= (ends[q_idx] - local_attn_size * frame_seqlen))) | (q_idx == kv_idx)
# return ((kv_idx < total_length) & (q_idx < total_length)) | (q_idx == kv_idx) # bidirectional mask
block_mask = create_block_mask(attention_mask, B=None, H=None, Q_LEN=total_length + padded_length,
KV_LEN=total_length + padded_length, _compile=False, device=device)
import torch.distributed as dist
if not dist.is_initialized() or dist.get_rank() == 0:
print(
f" cache a block wise causal mask with block size of {num_frame_per_block} frames")
print(block_mask)
# import imageio
# import numpy as np
# from torch.nn.attention.flex_attention import create_mask
# mask = create_mask(attention_mask, B=None, H=None, Q_LEN=total_length +
# padded_length, KV_LEN=total_length + padded_length, device=device)
# import cv2
# mask = cv2.resize(mask[0, 0].cpu().float().numpy(), (1024, 1024))
# imageio.imwrite("mask_%d.jpg" % (0), np.uint8(255. * mask))
return block_mask
@staticmethod
def _prepare_teacher_forcing_mask(
device: torch.device | str, num_frames: int = 21,
frame_seqlen: int = 1560, num_frame_per_block=1
) -> BlockMask:
"""
we will divide the token sequence into the following format
[1 latent frame] [1 latent frame] ... [1 latent frame]
We use flexattention to construct the attention mask
"""
# debug
DEBUG = False
if DEBUG:
num_frames = 9
frame_seqlen = 256
total_length = num_frames * frame_seqlen * 2
# we do right padding to get to a multiple of 128
padded_length = math.ceil(total_length / 128) * 128 - total_length
clean_ends = num_frames * frame_seqlen
# for clean context frames, we can construct their flex attention mask based on a [start, end] interval
context_ends = torch.zeros(total_length + padded_length, device=device, dtype=torch.long)
# for noisy frames, we need two intervals to construct the flex attention mask [context_start, context_end] [noisy_start, noisy_end]
noise_context_starts = torch.zeros(total_length + padded_length, device=device, dtype=torch.long)
noise_context_ends = torch.zeros(total_length + padded_length, device=device, dtype=torch.long)
noise_noise_starts = torch.zeros(total_length + padded_length, device=device, dtype=torch.long)
noise_noise_ends = torch.zeros(total_length + padded_length, device=device, dtype=torch.long)
# Block-wise causal mask will attend to all elements that are before the end of the current chunk
attention_block_size = frame_seqlen * num_frame_per_block
frame_indices = torch.arange(
start=0,
end=num_frames * frame_seqlen,
step=attention_block_size,
device=device, dtype=torch.long
)
# attention for clean context frames
for start in frame_indices:
context_ends[start:start + attention_block_size] = start + attention_block_size
noisy_image_start_list = torch.arange(
num_frames * frame_seqlen, total_length,
step=attention_block_size,
device=device, dtype=torch.long
)
noisy_image_end_list = noisy_image_start_list + attention_block_size
# attention for noisy frames
for block_index, (start, end) in enumerate(zip(noisy_image_start_list, noisy_image_end_list)):
# attend to noisy tokens within the same block
noise_noise_starts[start:end] = start
noise_noise_ends[start:end] = end
# attend to context tokens in previous blocks
# noise_context_starts[start:end] = 0
noise_context_ends[start:end] = block_index * attention_block_size
def attention_mask(b, h, q_idx, kv_idx):
# first design the mask for clean frames
clean_mask = (q_idx < clean_ends) & (kv_idx < context_ends[q_idx])
# then design the mask for noisy frames
# noisy frames will attend to all clean preceeding clean frames + itself
C1 = (kv_idx < noise_noise_ends[q_idx]) & (kv_idx >= noise_noise_starts[q_idx])
C2 = (kv_idx < noise_context_ends[q_idx]) & (kv_idx >= noise_context_starts[q_idx])
noise_mask = (q_idx >= clean_ends) & (C1 | C2)
eye_mask = q_idx == kv_idx
return eye_mask | clean_mask | noise_mask
block_mask = create_block_mask(attention_mask, B=None, H=None, Q_LEN=total_length + padded_length,
KV_LEN=total_length + padded_length, _compile=False, device=device)
if DEBUG:
print(block_mask)
import imageio
import numpy as np
from torch.nn.attention.flex_attention import create_mask
mask = create_mask(attention_mask, B=None, H=None, Q_LEN=total_length +
padded_length, KV_LEN=total_length + padded_length, device=device)
import cv2
mask = cv2.resize(mask[0, 0].cpu().float().numpy(), (1024, 1024))
imageio.imwrite("mask_%d.jpg" % (0), np.uint8(255. * mask))
return block_mask
@staticmethod
def _prepare_blockwise_causal_attn_mask_i2v(
device: torch.device | str, num_frames: int = 21,
frame_seqlen: int = 1560, num_frame_per_block=4, local_attn_size=-1
) -> BlockMask:
"""
we will divide the token sequence into the following format
[1 latent frame] [N latent frame] ... [N latent frame]
The first frame is separated out to support I2V generation
We use flexattention to construct the attention mask
"""
total_length = num_frames * frame_seqlen
# we do right padding to get to a multiple of 128
padded_length = math.ceil(total_length / 128) * 128 - total_length
ends = torch.zeros(total_length + padded_length,
device=device, dtype=torch.long)
# special handling for the first frame
ends[:frame_seqlen] = frame_seqlen
# Block-wise causal mask will attend to all elements that are before the end of the current chunk
frame_indices = torch.arange(
start=frame_seqlen,
end=total_length,
step=frame_seqlen * num_frame_per_block,
device=device
)
for idx, tmp in enumerate(frame_indices):
ends[tmp:tmp + frame_seqlen * num_frame_per_block] = tmp + \
frame_seqlen * num_frame_per_block
def attention_mask(b, h, q_idx, kv_idx):
if local_attn_size == -1:
return (kv_idx < ends[q_idx]) | (q_idx == kv_idx)
else:
return ((kv_idx < ends[q_idx]) & (kv_idx >= (ends[q_idx] - local_attn_size * frame_seqlen))) | \
(q_idx == kv_idx)
block_mask = create_block_mask(attention_mask, B=None, H=None, Q_LEN=total_length + padded_length,
KV_LEN=total_length + padded_length, _compile=False, device=device)
if not dist.is_initialized() or dist.get_rank() == 0:
print(
f" cache a block wise causal mask with block size of {num_frame_per_block} frames")
print(block_mask)
# import imageio
# import numpy as np
# from torch.nn.attention.flex_attention import create_mask
# mask = create_mask(attention_mask, B=None, H=None, Q_LEN=total_length +
# padded_length, KV_LEN=total_length + padded_length, device=device)
# import cv2
# mask = cv2.resize(mask[0, 0].cpu().float().numpy(), (1024, 1024))
# imageio.imwrite("mask_%d.jpg" % (0), np.uint8(255. * mask))
return block_mask
def _forward_inference(
self,
x,
t,
context,
seq_len,
clip_fea=None,
kv_cache=None,
crossattn_cache=None,
current_start=None,
cache_start=None,
debug_dict=None,
y=None, # 🆕 source latent for unpruned filling
kept_indices_per_frame=None, # 🆕 Block Internal Pruning使用
importance_mask=None, # Old Per-frame Pruning使用(废弃)
use_pruning=False, # 🆕 是否在当前step应用pruning
):
# 🐛 Debug: 检查进入forward时的状态
# if hasattr(self, 'prev_step_self_attn_deltas') and not self.training:
# none_count = sum(1 for v in self.prev_step_self_attn_deltas.values() if v is None)
# total_count = len(self.prev_step_self_attn_deltas)
# print(f"[Forward Entry] prev_step_self_attn_deltas: {none_count}/{total_count} blocks are None, id={id(self.prev_step_self_attn_deltas)}")
device = self.patch_embedding.weight.device
if self.freqs.device != device:
self.freqs = self.freqs.to(device)
# 🆕 如果有y,并且使用source_latent填充策略,单独处理source
source_latent_embedded = None
if y is not None:
# 原有训练逻辑:concat后一起patch_embedding
x = [torch.cat([u, v], dim=0) for u, v in zip(x, y)]
# 只在使用source_latent填充策略时才处理source
if self.unpruned_fill_strategy == "source_latent":
# 初始化source_patch_embedding(如果还没初始化)
if self.source_patch_embedding is None:
self._initialize_source_patch_embedding()
# 对source单独做patch_embedding
with torch.no_grad():
source_embed = [self.source_patch_embedding(v.unsqueeze(0)) for v in y]
source_latent_embedded = [u.flatten(2).transpose(1, 2) for u in source_embed]
source_latent_embedded = torch.cat(source_latent_embedded) # [B, L, C]
# 💾 保存latent空间的grid_sizes(patch_embedding前)
# x: List[Tensor], 每个shape=[C, F, H, W](如果有y则C=32,否则C=16)
latent_grid_sizes = torch.stack(
[torch.tensor(u.shape[1:], dtype=torch.long) for u in x]) # [B, 3] (F, H_latent, W_latent)
# embeddings (保持原有训练逻辑)
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]) # [B, 3] (F, H_patch, W_patch)
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)
assert seq_lens.max() <= seq_len
x = torch.cat(x)
pruning_info = {'pruned': False}
original_seq_lens = seq_lens
original_grid_sizes = grid_sizes
if kept_indices_per_frame is not None:
# print("🎯 Block Internal Pruning (新实现)")
# Pipeline传来的kept_indices是latent空间的(60×104)
# 需要转换到patch_embedding后空间的indices (30×52)
# 1. 使用latent空间的grid_sizes重建mask
B = x.shape[0]
F, H_latent, W_latent = latent_grid_sizes[0].tolist() # ✅ 使用latent空间尺寸
patch_t, patch_h, patch_w = self.patch_size
H_patch = H_latent // patch_h
W_patch = W_latent // patch_w
tokens_per_frame_latent = H_latent * W_latent
tokens_per_frame_patch = H_patch * W_patch
# 重建latent空间mask
latent_mask = torch.zeros(F, H_latent, W_latent, dtype=torch.bool, device=x.device)
for f_idx in range(F):
kept_idx_latent = kept_indices_per_frame[f_idx]
# 🔧 添加边界检查和裁剪
valid_mask = (kept_idx_latent >= 0) & (kept_idx_latent < H_latent * W_latent)
kept_idx_latent = kept_idx_latent[valid_mask]
h_coords = kept_idx_latent // W_latent
w_coords = kept_idx_latent % W_latent
# 再次确保索引在范围内
h_coords = torch.clamp(h_coords, 0, H_latent - 1)
w_coords = torch.clamp(w_coords, 0, W_latent - 1)
latent_mask[f_idx, h_coords, w_coords] = True
# 2. 下采样到patch空间(average pooling + threshold)
# [F, H_latent, W_latent] → [F, H_patch, W_patch]
latent_mask_float = latent_mask.float().unsqueeze(0).unsqueeze(0) # [1, 1, F, H, W]
latent_mask_2d = latent_mask_float.squeeze(1) # [1, F, H, W]
# 对每个frame单独average pooling
patch_masks = []
for f_idx in range(F):
frame_mask = latent_mask_2d[:, f_idx:f_idx+1] # [1, 1, H, W]
# Average pooling: 计算每个patch内True的比例
pooled = torch.nn.functional.avg_pool2d(
frame_mask,
kernel_size=(patch_h, patch_w),
stride=(patch_h, patch_w)
)
# 阈值:patch内>50%的像素为True → 该patch为True
patch_mask_bool = (pooled > 0.5).squeeze() # [H_patch, W_patch]
patch_masks.append(patch_mask_bool)
patch_mask = torch.stack(patch_masks) # [F, H_patch, W_patch]
# 3. 从patch mask提取indices
kept_indices_per_frame_patch = []
for f_idx in range(F):
frame_mask_flat = patch_mask[f_idx].flatten() # [H_patch * W_patch]
kept_idx = torch.nonzero(frame_mask_flat, as_tuple=True)[0]
kept_indices_per_frame_patch.append(kept_idx)
# 4. Per-frame统一长度
min_kept = min(len(idx) for idx in kept_indices_per_frame_patch)
kept_indices_per_frame_patch = [idx[:min_kept] for idx in kept_indices_per_frame_patch]
# x保持完整,pruning在每个Block内部进行
pruning_info = {
'internal_pruning': True,
'use_pruning': use_pruning, # 🆕 是否在当前step应用pruning
'kept_indices_per_frame': kept_indices_per_frame_patch, # ✅ 使用patch空间的indices
'first_chunk_block_inputs': self.first_chunk_block_inputs, # 🆕 传递缓存引用
'pruning_layers': self.internal_pruning_layers, # 🆕 传递层级配置
'cache_item': self.first_chunk_cache_item, # 🆕 传递缓存内容配置: "x" or "delta"
'fill_strategy': self.unpruned_fill_strategy, # 🆕 传递填充策略
'prev_step_self_attn_deltas': self.prev_step_self_attn_deltas, # 🆕 self-attn的delta缓存
'prev_step_ffn_deltas': self.prev_step_ffn_deltas, # 🆕 FFN的delta缓存
'use_mean_alignment': self.use_mean_alignment, # 🆕 均值对齐开关
'source_latent': source_latent_embedded # 🆕 源latent (patch_embedding后的y)
}
total_kept = F * min_kept
reduction = (1 - total_kept / (F * tokens_per_frame_patch)) * 100
# 🆕 显示层级配置
layers_enabled = []
if "self_attn" in self.internal_pruning_layers:
layers_enabled.append("Self-Attn")
if "ffn" in self.internal_pruning_layers:
layers_enabled.append("FFN")
layers_str = " + ".join(layers_enabled) if layers_enabled else "None"
importance_mask = None
"""
torch.cat([
torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))],
dim=1) for u in x
])
"""
# time embeddings
# with amp.autocast(dtype=torch.float32):
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)
# assert e.dtype == torch.float32 and e0.dtype == torch.float32
# context
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
]))
if clip_fea is not None:
context_clip = self.img_emb(clip_fea) # bs x 257 x dim
context = torch.concat([context_clip, context], dim=1)
# arguments
kwargs = dict(
e=e0,
seq_lens=seq_lens,
grid_sizes=grid_sizes,
freqs=self.freqs,
context=context,
context_lens=context_lens,
block_mask=self.block_mask,
pruning_info=pruning_info # 🆕 传递pruning信息给attention层
)
def create_custom_forward(module):
def custom_forward(*inputs, **kwargs):
return module(*inputs, **kwargs)
return custom_forward
for block_index, block in enumerate(self.blocks):
if torch.is_grad_enabled() and self.gradient_checkpointing:
kwargs.update(
{
"kv_cache": kv_cache[block_index],
"current_start": current_start,
"cache_start": cache_start
}
)
x = torch.utils.checkpoint.checkpoint(
create_custom_forward(block),
x, **kwargs,
use_reentrant=False,
)
else:
if debug_dict is not None:
debug_dict["block_index"] = block_index
kwargs.update(
{
"kv_cache": kv_cache[block_index],
"crossattn_cache": crossattn_cache[block_index],
"current_start": current_start,
"cache_start": cache_start,
"debug_dict": debug_dict,
"block_idx": block_index # 🆕 传递block索引
}
)
x = block(x, **kwargs) # 对应CausalWanAttentionBlock
x = self.head(x, e.unflatten(dim=0, sizes=t.shape).unsqueeze(2))
# unpatchify
x = self.unpatchify(x, grid_sizes)
return torch.stack(x)
def _forward_train(
self,
x,
t,
context,
seq_len,
clean_x=None,
aug_t=None,
clip_fea=None,
y=None,
):
r"""
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]
context (List[Tensor]):
List of text embeddings each with shape [L, C]
seq_len (`int`):
Maximum sequence length for positional encoding
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]
"""
# print(f"{[u.shape for u in x]=} {t.shape=} {[u.shape for u in context]=} {seq_len=}")
if self.model_type == 'i2v':
assert clip_fea is not None and y is not None
# params
device = self.patch_embedding.weight.device
if self.freqs.device != device:
self.freqs = self.freqs.to(device)
# Construct blockwise causal attn mask
if self.block_mask is None:
if clean_x is not None:
if self.independent_first_frame:
raise NotImplementedError()
else:
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
)
else:
if self.independent_first_frame:
self.block_mask = self._prepare_blockwise_causal_attn_mask_i2v(
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
)
else:
self.block_mask = self._prepare_blockwise_causal_attn_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
)
if y is not None: # channel-wise
x = [torch.cat([u, v[:, -u.shape[1]:] if u.shape[1] != v.shape[1] else v], dim=0)
for u, v in zip(x, y)]
# embeddings
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
# print(f"after patch embedding, {[u.shape 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]
# print(f"after flatten, {[u.shape for u in x]=}")
seq_lens = torch.tensor([u.size(1) for u in x], dtype=torch.long)
assert seq_lens.max() <= seq_len
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
])
# time embeddings
# with amp.autocast(dtype=torch.float32):
# print(f"{x.shape=} {self.freq_dim=} {t.flatten().shape=}")
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)
# assert e.dtype == torch.float32 and e0.dtype == torch.float32
# print(f"{x.shape=} {e.shape=} {e0.shape=} {seq_lens=} {grid_sizes=}")
# context
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
]))
if clip_fea is not None:
context_clip = self.img_emb(clip_fea) # bs x 257 x dim
context = torch.concat([context_clip, context], dim=1)
if clean_x is not None:
clean_x = [self.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)
assert seq_lens_clean.max() <= seq_len
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)
if aug_t is None:
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)
# arguments
kwargs = dict(
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
for block in 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)
# print(f"in training forward, after block {block}, {x.shape=}")
if clean_x is not None:
x = x[:, x.shape[1] // 2:]
# head
x = self.head(x, e.unflatten(dim=0, sizes=t.shape).unsqueeze(2))
# unpatchify
x = self.unpatchify(x, grid_sizes)
return torch.stack(x)
def forward(
self,
*args,
**kwargs
):
if kwargs.get('kv_cache', None) is not None:
return self._forward_inference(*args, **kwargs)
else:
return self._forward_train(*args, **kwargs)
def unpatchify(self, x, grid_sizes):
r"""
Reconstruct video tensors from patch embeddings.
Args:
x (List[Tensor]):
List of patchified features, each with shape [L, C_out * prod(patch_size)]
grid_sizes (Tensor):
Original spatial-temporal grid dimensions before patching,
shape [B, 3] (3 dimensions correspond to F_patches, H_patches, W_patches)
Returns:
List[Tensor]:
Reconstructed video tensors with shape [C_out, F, H / 8, W / 8]
"""
c = self.out_dim
out = []
for u, v in zip(x, grid_sizes.tolist()):
u = u[:math.prod(v)].view(*v, *self.patch_size, c)
u = torch.einsum('fhwpqrc->cfphqwr', u)
u = u.reshape(c, *[i * j for i, j in zip(v, self.patch_size)])
out.append(u)
return out
def init_weights(self):
r"""
Initialize model parameters using Xavier initialization.
"""
# basic init
for m in self.modules():
if isinstance(m, nn.Linear):
nn.init.xavier_uniform_(m.weight)
if m.bias is not None:
nn.init.zeros_(m.bias)
# init embeddings
nn.init.xavier_uniform_(self.patch_embedding.weight.flatten(1))
for m in self.text_embedding.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=.02)
for m in self.time_embedding.modules():
if isinstance(m, nn.Linear):
nn.init.normal_(m.weight, std=.02)
# init output layer
nn.init.zeros_(self.head.head.weight)