add codes of wan2.2-s2v

This commit is contained in:
qi wang
2025-08-26 16:19:11 +08:00
parent 031a9be56c
commit d18200b833
22 changed files with 3242 additions and 7 deletions
+6 -1
View File
@@ -1,2 +1,7 @@
__pycache__/
.DS_Store
.DS_Store
.vscode*
tmp_examples*
new_checkpoint*
batch_test*
nohup*
+38
View File
@@ -34,6 +34,7 @@ We are excited to introduce **Wan2.2**, a major upgrade to our foundational vide
## 🔥 Latest News!!
* Aug 26, 2025: 🎵 We introduce **[Wan2.2-S2V-14B](https://humanaigc.github.io/wan-s2v-webpage)**, an audio-driven cinematic video generation model, including [inference code](#run-speech-to-video-generation), [model weights](#model-download), and [technical report]()! Now you can try it on [wan.video](https://wan.video/), [ModelScope Gradio](https://www.modelscope.cn/studios/Wan-AI/Wan2.2-S2V) or [HuggingFace Gradio](https://huggingface.co/spaces/Wan-AI/Wan2.2-S2V)!
* Jul 28, 2025: 👋 We have open a [HF space](https://huggingface.co/spaces/Wan-AI/Wan-2.2-5B) using the TI2V-5B model. Enjoy!
* Jul 28, 2025: 👋 Wan2.2 has been integrated into ComfyUI ([CN](https://docs.comfy.org/zh-CN/tutorials/video/wan/wan2_2) | [EN](https://docs.comfy.org/tutorials/video/wan/wan2_2)). Enjoy!
* Jul 28, 2025: 👋 Wan2.2's T2V, I2V and TI2V have been integrated into Diffusers ([T2V-A14B](https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B-Diffusers) | [I2V-A14B](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B-Diffusers) | [TI2V-5B](https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B-Diffusers)). Feel free to give it a try!
@@ -63,6 +64,11 @@ If your research or project builds upon [**Wan2.1**](https://github.com/Wan-Vide
- [x] Checkpoints of the 5B model
- [x] ComfyUI integration
- [x] Diffusers integration
- Wan2.2-S2V Speech-to-Video
- [x] Inference code of Wan2.2-S2V
- [x] Checkpoints of Wan2.2-S2V-14B
- [ ] ComfyUI integration
- [ ] Diffusers integration
## Run Wan2.2
@@ -88,6 +94,8 @@ pip install -r requirements.txt
| T2V-A14B | 🤗 [Huggingface](https://huggingface.co/Wan-AI/Wan2.2-T2V-A14B) 🤖 [ModelScope](https://modelscope.cn/models/Wan-AI/Wan2.2-T2V-A14B) | Text-to-Video MoE model, supports 480P & 720P |
| I2V-A14B | 🤗 [Huggingface](https://huggingface.co/Wan-AI/Wan2.2-I2V-A14B) 🤖 [ModelScope](https://modelscope.cn/models/Wan-AI/Wan2.2-I2V-A14B) | Image-to-Video MoE model, supports 480P & 720P |
| TI2V-5B | 🤗 [Huggingface](https://huggingface.co/Wan-AI/Wan2.2-TI2V-5B) 🤖 [ModelScope](https://modelscope.cn/models/Wan-AI/Wan2.2-TI2V-5B) | High-compression VAE, T2V+I2V, supports 720P |
| S2V-14B | 🤗 [Huggingface](https://huggingface.co/Wan-AI/Wan2.2-S2V-14B) 🤖 [ModelScope](https://modelscope.cn/models/Wan-AI/Wan2.2-S2V-14B) | Speech-to-Video model, supports 480P & 720P |
> 💡Note:
@@ -228,8 +236,38 @@ torchrun --nproc_per_node=8 generate.py --task ti2v-5B --size 1280*704 --ckpt_di
> The process of prompt extension can be referenced [here](#2-using-prompt-extention).
#### Run Speech-to-Video Generation
This repository supports the `Wan2.2-S2V-14B` Speech-to-Video model and can simultaneously support video generation at 480P and 720P resolutions.
- Single-GPU Speech-to-Video inference
```sh
python generate.py --task s2v-14B --size 1024*704 --ckpt_dir ./Wan2.2-S2V-14B/ --offload_model True --convert_model_dtype --prompt "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard." --image "examples/i2v_input.JPG" --audio "examples/talk.wav"
# Without setting --num_clip, the generated video length will automatically adjust based on the input audio length
```
> 💡 This command can run on a GPU with at least 80GB VRAM.
- Multi-GPU inference using FSDP + DeepSpeed Ulysses
```sh
torchrun --nproc_per_node=8 generate.py --task s2v-14B --size 1024*704 --ckpt_dir ./Wan2.2-S2V-14B/ --dit_fsdp --t5_fsdp --ulysses_size 8 --prompt "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard." --image "examples/i2v_input.JPG" --audio "examples/talk.wav"
```
- Pose + Audio driven generation
```sh
torchrun --nproc_per_node=8 generate.py --task s2v-14B --size 1024*704 --ckpt_dir ./Wan2.2-S2V-14B/ --dit_fsdp --t5_fsdp --ulysses_size 8 --prompt "a person is singing" --image "examples/pose.png" --audio "examples/sing.MP3" --pose_video "./examples/pose.mp4"
```
> 💡For the Speech-to-Video task, the `size` parameter represents the area of the generated video, with the aspect ratio following that of the original input image.
> 💡The model can generate videos from audio input combined with reference image and optional text prompt.
> 💡The `--pose_video` parameter enables pose-driven generation, allowing the model to follow specific pose sequences while generating videos synchronized with audio input.
> 💡The `--num_clip` parameter controls the number of video clips generated, useful for quick preview with shorter generation time.
## Computational Efficiency on Different GPUs
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 804 KiB

BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
+78 -4
View File
@@ -18,7 +18,7 @@ import wan
from wan.configs import MAX_AREA_CONFIGS, SIZE_CONFIGS, SUPPORTED_SIZES, WAN_CONFIGS
from wan.distributed.util import init_distributed_group
from wan.utils.prompt_extend import DashScopePromptExpander, QwenPromptExpander
from wan.utils.utils import save_video, str2bool
from wan.utils.utils import merge_video_audio, save_video, str2bool
EXAMPLE_PROMPT = {
"t2v-A14B": {
@@ -35,6 +35,14 @@ EXAMPLE_PROMPT = {
"prompt":
"Two anthropomorphic cats in comfy boxing gear and bright gloves fight intensely on a spotlighted stage.",
},
"s2v-14B": {
"prompt":
"Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside.",
"image":
"examples/i2v_input.JPG",
"audio":
"examples/talk.wav",
},
}
@@ -48,6 +56,8 @@ def _validate_args(args):
args.prompt = EXAMPLE_PROMPT[args.task]["prompt"]
if args.image is None and "image" in EXAMPLE_PROMPT[args.task]:
args.image = EXAMPLE_PROMPT[args.task]["image"]
if args.audio is None and "audio" in EXAMPLE_PROMPT[args.task]:
args.audio = EXAMPLE_PROMPT[args.task]["audio"]
if args.task == "i2v-A14B":
assert args.image is not None, "Please specify the image path for i2v."
@@ -69,9 +79,10 @@ def _validate_args(args):
args.base_seed = args.base_seed if args.base_seed >= 0 else random.randint(
0, sys.maxsize)
# Size check
assert args.size in SUPPORTED_SIZES[
args.
task], f"Unsupport size {args.size} for task {args.task}, supported sizes are: {', '.join(SUPPORTED_SIZES[args.task])}"
if not 's2v' in args.task:
assert args.size in SUPPORTED_SIZES[
args.
task], f"Unsupport size {args.size} for task {args.task}, supported sizes are: {', '.join(SUPPORTED_SIZES[args.task])}"
def _parse_args():
@@ -194,6 +205,36 @@ def _parse_args():
default=False,
help="Whether to convert model paramerters dtype.")
# following args only works for s2v
parser.add_argument(
"--num_clip",
type=int,
default=None,
help="Number of video clips to generate, the whole video will not exceed the length of audio."
)
parser.add_argument(
"--audio",
type=str,
default=None,
help="Path to the audio file, e.g. wav, mp3")
parser.add_argument(
"--pose_video",
type=str,
default=None,
help="Provide Dw-pose sequence to do Pose Driven")
parser.add_argument(
"--start_from_ref",
action="store_true",
default=False,
help="whether set the reference image as the starting point for generation"
)
parser.add_argument(
"--infer_frames",
type=int,
default=80,
help="Number of frames per clip, 48 or 80 or others (must be multiple of 4) for 14B s2v"
)
args = parser.parse_args()
_validate_args(args)
@@ -353,6 +394,37 @@ def generate(args):
guide_scale=args.sample_guide_scale,
seed=args.base_seed,
offload_model=args.offload_model)
elif "s2v" in args.task:
logging.info("Creating WanS2V pipeline.")
wan_s2v = wan.WanS2V(
config=cfg,
checkpoint_dir=args.ckpt_dir,
device_id=device,
rank=rank,
t5_fsdp=args.t5_fsdp,
dit_fsdp=args.dit_fsdp,
use_sp=(args.ulysses_size > 1),
t5_cpu=args.t5_cpu,
convert_model_dtype=args.convert_model_dtype,
)
logging.info(f"Generating video ...")
video = wan_s2v.generate(
input_prompt=args.prompt,
ref_image_path=args.image,
audio_path=args.audio,
num_repeat=args.num_clip,
pose_video=args.pose_video,
max_area=MAX_AREA_CONFIGS[args.size],
infer_frames=args.infer_frames,
shift=args.sample_shift,
sample_solver=args.sample_solver,
sampling_steps=args.sample_steps,
guide_scale=args.sample_guide_scale,
seed=args.base_seed,
offload_model=args.offload_model,
init_first_frame=args.start_from_ref,
)
else:
logging.info("Creating WanI2V pipeline.")
wan_i2v = wan.WanI2V(
@@ -396,6 +468,8 @@ def generate(args):
nrow=1,
normalize=True,
value_range=(-1, 1))
if "s2v" in args.task:
merge_video_audio(video_path=args.save_file, audio_path=args.audio)
del video
torch.cuda.synchronize()
+1
View File
@@ -1,5 +1,6 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
from . import configs, distributed, modules
from .image2video import WanI2V
from .speech2video import WanS2V
from .text2video import WanT2V
from .textimage2video import WanTI2V
+9 -1
View File
@@ -5,6 +5,7 @@ import os
os.environ['TOKENIZERS_PARALLELISM'] = 'false'
from .wan_i2v_A14B import i2v_A14B
from .wan_s2v_14B import s2v_14B
from .wan_t2v_A14B import t2v_A14B
from .wan_ti2v_5B import ti2v_5B
@@ -12,6 +13,7 @@ WAN_CONFIGS = {
't2v-A14B': t2v_A14B,
'i2v-A14B': i2v_A14B,
'ti2v-5B': ti2v_5B,
's2v-14B': s2v_14B,
}
SIZE_CONFIGS = {
@@ -20,7 +22,9 @@ SIZE_CONFIGS = {
'480*832': (480, 832),
'832*480': (832, 480),
'704*1280': (704, 1280),
'1280*704': (1280, 704)
'1280*704': (1280, 704),
'1024*704': (1024, 704),
'704*1024': (704, 1024),
}
MAX_AREA_CONFIGS = {
@@ -30,10 +34,14 @@ MAX_AREA_CONFIGS = {
'832*480': 832 * 480,
'704*1280': 704 * 1280,
'1280*704': 1280 * 704,
'1024*704': 1024 * 704,
'704*1024': 704 * 1024,
}
SUPPORTED_SIZES = {
't2v-A14B': ('720*1280', '1280*720', '480*832', '832*480'),
'i2v-A14B': ('720*1280', '1280*720', '480*832', '832*480'),
'ti2v-5B': ('704*1280', '1280*704'),
's2v-14B': ('720*1280', '1280*720', '480*832', '832*480', '1024*704',
'704*1024', '704*1280', '1280*704'),
}
+59
View File
@@ -0,0 +1,59 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
from easydict import EasyDict
from .shared_config import wan_shared_cfg
#------------------------ Wan S2V 14B ------------------------#
s2v_14B = EasyDict(__name__='Config: Wan S2V 14B')
s2v_14B.update(wan_shared_cfg)
# t5
s2v_14B.t5_checkpoint = 'models_t5_umt5-xxl-enc-bf16.pth'
s2v_14B.t5_tokenizer = 'google/umt5-xxl'
# vae
s2v_14B.vae_checkpoint = 'Wan2.1_VAE.pth'
s2v_14B.vae_stride = (4, 8, 8)
# wav2vec
s2v_14B.wav2vec = "wav2vec2-large-xlsr-53-english"
s2v_14B.num_heads = 40
# transformer
s2v_14B.transformer = EasyDict(
__name__="Config: Transformer config for WanModel_S2V")
s2v_14B.transformer.patch_size = (1, 2, 2)
s2v_14B.transformer.dim = 5120
s2v_14B.transformer.ffn_dim = 13824
s2v_14B.transformer.freq_dim = 256
s2v_14B.transformer.num_heads = 40
s2v_14B.transformer.num_layers = 40
s2v_14B.transformer.window_size = (-1, -1)
s2v_14B.transformer.qk_norm = True
s2v_14B.transformer.cross_attn_norm = True
s2v_14B.transformer.eps = 1e-6
s2v_14B.transformer.enable_adain = True
s2v_14B.transformer.adain_mode = "attn_norm"
s2v_14B.transformer.audio_inject_layers = [
0, 4, 8, 12, 16, 20, 24, 27, 30, 33, 36, 39
]
s2v_14B.transformer.zero_init = True
s2v_14B.transformer.zero_timestep = True
s2v_14B.transformer.enable_motioner = False
s2v_14B.transformer.add_last_motion = True
s2v_14B.transformer.trainable_token = False
s2v_14B.transformer.enable_tsm = False
s2v_14B.transformer.enable_framepack = True
s2v_14B.transformer.framepack_drop_mode = 'padd'
s2v_14B.transformer.audio_dim = 1024
s2v_14B.transformer.motion_frames = 73
s2v_14B.transformer.cond_dim = 16
# inference
s2v_14B.sample_neg_prompt = "画面模糊,最差质量,画面模糊,细节模糊不清,情绪激动剧烈,手快速抖动,字幕,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
s2v_14B.drop_first_motion = True
s2v_14B.sample_shift = 3
s2v_14B.sample_steps = 40
s2v_14B.sample_guide_scale = 4.5
+1 -1
View File
@@ -356,7 +356,7 @@ class WanModel(ModelMixin, ConfigMixin):
super().__init__()
assert model_type in ['t2v', 'i2v', 'ti2v']
assert model_type in ['t2v', 'i2v', 'ti2v', 's2v']
self.model_type = model_type
self.patch_size = patch_size
+5
View File
@@ -0,0 +1,5 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
from .audio_encoder import AudioEncoder
from .model_s2v import WanModel_S2V
__all__ = ['WanModel_S2V', 'AudioEncoder']
+189
View File
@@ -0,0 +1,189 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import math
import librosa
import numpy as np
import torch
import torch.nn.functional as F
from transformers import Wav2Vec2ForCTC, Wav2Vec2Processor
def get_sample_indices(original_fps,
total_frames,
target_fps,
num_sample,
fixed_start=None):
required_duration = num_sample / target_fps
required_origin_frames = int(np.ceil(required_duration * original_fps))
if required_duration > total_frames / original_fps:
raise ValueError("required_duration must be less than video length")
if not fixed_start is None and fixed_start >= 0:
start_frame = fixed_start
else:
max_start = total_frames - required_origin_frames
if max_start < 0:
raise ValueError("video length is too short")
start_frame = np.random.randint(0, max_start + 1)
start_time = start_frame / original_fps
end_time = start_time + required_duration
time_points = np.linspace(start_time, end_time, num_sample, endpoint=False)
frame_indices = np.round(np.array(time_points) * original_fps).astype(int)
frame_indices = np.clip(frame_indices, 0, total_frames - 1)
return frame_indices
def linear_interpolation(features, input_fps, output_fps, output_len=None):
"""
features: shape=[1, T, 512]
input_fps: fps for audio, f_a
output_fps: fps for video, f_m
output_len: video length
"""
features = features.transpose(1, 2) # [1, 512, T]
seq_len = features.shape[2] / float(input_fps) # T/f_a
if output_len is None:
output_len = int(seq_len * output_fps) # f_m*T/f_a
output_features = F.interpolate(
features, size=output_len, align_corners=True,
mode='linear') # [1, 512, output_len]
return output_features.transpose(1, 2) # [1, output_len, 512]
class AudioEncoder():
def __init__(self, device='cpu', model_id="facebook/wav2vec2-base-960h"):
# load pretrained model
self.processor = Wav2Vec2Processor.from_pretrained(model_id)
self.model = Wav2Vec2ForCTC.from_pretrained(model_id)
self.model = self.model.to(device)
self.video_rate = 30
def extract_audio_feat(self,
audio_path,
return_all_layers=False,
dtype=torch.float32):
audio_input, sample_rate = librosa.load(audio_path, sr=16000)
input_values = self.processor(
audio_input, sampling_rate=sample_rate,
return_tensors="pt").input_values
# INFERENCE
# retrieve logits & take argmax
res = self.model(
input_values.to(self.model.device), output_hidden_states=True)
if return_all_layers:
feat = torch.cat(res.hidden_states)
else:
feat = res.hidden_states[-1]
feat = linear_interpolation(
feat, input_fps=50, output_fps=self.video_rate)
z = feat.to(dtype) # Encoding for the motion
return z
def get_audio_embed_bucket(self,
audio_embed,
stride=2,
batch_frames=12,
m=2):
num_layers, audio_frame_num, audio_dim = audio_embed.shape
if num_layers > 1:
return_all_layers = True
else:
return_all_layers = False
min_batch_num = int(audio_frame_num / (batch_frames * stride)) + 1
bucket_num = min_batch_num * batch_frames
batch_idx = [stride * i for i in range(bucket_num)]
batch_audio_eb = []
for bi in batch_idx:
if bi < audio_frame_num:
audio_sample_stride = 2
chosen_idx = list(
range(bi - m * audio_sample_stride,
bi + (m + 1) * audio_sample_stride,
audio_sample_stride))
chosen_idx = [0 if c < 0 else c for c in chosen_idx]
chosen_idx = [
audio_frame_num - 1 if c >= audio_frame_num else c
for c in chosen_idx
]
if return_all_layers:
frame_audio_embed = audio_embed[:, chosen_idx].flatten(
start_dim=-2, end_dim=-1)
else:
frame_audio_embed = audio_embed[0][chosen_idx].flatten()
else:
frame_audio_embed = \
torch.zeros([audio_dim * (2 * m + 1)], device=audio_embed.device) if not return_all_layers \
else torch.zeros([num_layers, audio_dim * (2 * m + 1)], device=audio_embed.device)
batch_audio_eb.append(frame_audio_embed)
batch_audio_eb = torch.cat([c.unsqueeze(0) for c in batch_audio_eb],
dim=0)
return batch_audio_eb, min_batch_num
def get_audio_embed_bucket_fps(self,
audio_embed,
fps=16,
batch_frames=81,
m=0):
num_layers, audio_frame_num, audio_dim = audio_embed.shape
if num_layers > 1:
return_all_layers = True
else:
return_all_layers = False
scale = self.video_rate / fps
min_batch_num = int(audio_frame_num / (batch_frames * scale)) + 1
bucket_num = min_batch_num * batch_frames
padd_audio_num = math.ceil(min_batch_num * batch_frames / fps *
self.video_rate) - audio_frame_num
batch_idx = get_sample_indices(
original_fps=self.video_rate,
total_frames=audio_frame_num + padd_audio_num,
target_fps=fps,
num_sample=bucket_num,
fixed_start=0)
batch_audio_eb = []
audio_sample_stride = int(self.video_rate / fps)
for bi in batch_idx:
if bi < audio_frame_num:
chosen_idx = list(
range(bi - m * audio_sample_stride,
bi + (m + 1) * audio_sample_stride,
audio_sample_stride))
chosen_idx = [0 if c < 0 else c for c in chosen_idx]
chosen_idx = [
audio_frame_num - 1 if c >= audio_frame_num else c
for c in chosen_idx
]
if return_all_layers:
frame_audio_embed = audio_embed[:, chosen_idx].flatten(
start_dim=-2, end_dim=-1)
else:
frame_audio_embed = audio_embed[0][chosen_idx].flatten()
else:
frame_audio_embed = \
torch.zeros([audio_dim * (2 * m + 1)], device=audio_embed.device) if not return_all_layers \
else torch.zeros([num_layers, audio_dim * (2 * m + 1)], device=audio_embed.device)
batch_audio_eb.append(frame_audio_embed)
batch_audio_eb = torch.cat([c.unsqueeze(0) for c in batch_audio_eb],
dim=0)
return batch_audio_eb, min_batch_num
+105
View File
@@ -0,0 +1,105 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import math
from typing import Tuple, Union
import torch
import torch.cuda.amp as amp
import torch.nn as nn
from diffusers.models.attention import AdaLayerNorm
from ..model import WanAttentionBlock, WanCrossAttention
from .auxi_blocks import MotionEncoder_tc
class CausalAudioEncoder(nn.Module):
def __init__(self,
dim=5120,
num_layers=25,
out_dim=2048,
video_rate=8,
num_token=4,
need_global=False):
super().__init__()
self.encoder = MotionEncoder_tc(
in_dim=dim,
hidden_dim=out_dim,
num_heads=num_token,
need_global=need_global)
weight = torch.ones((1, num_layers, 1, 1)) * 0.01
self.weights = torch.nn.Parameter(weight)
self.act = torch.nn.SiLU()
def forward(self, features):
with amp.autocast(dtype=torch.float32):
# features B * num_layers * dim * video_length
weights = self.act(self.weights)
weights_sum = weights.sum(dim=1, keepdims=True)
weighted_feat = ((features * weights) / weights_sum).sum(
dim=1) # b dim f
weighted_feat = weighted_feat.permute(0, 2, 1) # b f dim
res = self.encoder(weighted_feat) # b f n dim
return res # b f n dim
class AudioCrossAttention(WanCrossAttention):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
class AudioInjector_WAN(nn.Module):
def __init__(self,
all_modules,
all_modules_names,
dim=2048,
num_heads=32,
inject_layer=[0, 27],
root_net=None,
enable_adain=False,
adain_dim=2048,
need_adain_ont=False):
super().__init__()
num_injector_layers = len(inject_layer)
self.injected_block_id = {}
audio_injector_id = 0
for mod_name, mod in zip(all_modules_names, all_modules):
if isinstance(mod, WanAttentionBlock):
for inject_id in inject_layer:
if f'transformer_blocks.{inject_id}' in mod_name:
self.injected_block_id[inject_id] = audio_injector_id
audio_injector_id += 1
self.injector = nn.ModuleList([
AudioCrossAttention(
dim=dim,
num_heads=num_heads,
qk_norm=True,
) for _ in range(audio_injector_id)
])
self.injector_pre_norm_feat = nn.ModuleList([
nn.LayerNorm(
dim,
elementwise_affine=False,
eps=1e-6,
) for _ in range(audio_injector_id)
])
self.injector_pre_norm_vec = nn.ModuleList([
nn.LayerNorm(
dim,
elementwise_affine=False,
eps=1e-6,
) for _ in range(audio_injector_id)
])
if enable_adain:
self.injector_adain_layers = nn.ModuleList([
AdaLayerNorm(
output_dim=dim * 2, embedding_dim=adain_dim, chunk_dim=1)
for _ in range(audio_injector_id)
])
if need_adain_ont:
self.injector_adain_output_layers = nn.ModuleList(
[nn.Linear(dim, dim) for _ in range(audio_injector_id)])
+242
View File
@@ -0,0 +1,242 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import importlib.metadata
import math
from typing import Any, Dict, List, Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.models import ModelMixin
from diffusers.utils import is_torch_version, logging
from einops import rearrange
try:
from flash_attn import flash_attn_func, flash_attn_qkvpacked_func
except ImportError:
flash_attn_func = None
MEMORY_LAYOUT = {
"flash": (
lambda x: x.view(x.shape[0] * x.shape[1], *x.shape[2:]),
lambda x: x,
),
"torch": (
lambda x: x.transpose(1, 2),
lambda x: x.transpose(1, 2),
),
"vanilla": (
lambda x: x.transpose(1, 2),
lambda x: x.transpose(1, 2),
),
}
def attention(
q,
k,
v,
mode="flash",
drop_rate=0,
attn_mask=None,
causal=False,
max_seqlen_q=None,
batch_size=1,
):
"""
Perform QKV self attention.
Args:
q (torch.Tensor): Query tensor with shape [b, s, a, d], where a is the number of heads.
k (torch.Tensor): Key tensor with shape [b, s1, a, d]
v (torch.Tensor): Value tensor with shape [b, s1, a, d]
mode (str): Attention mode. Choose from 'self_flash', 'cross_flash', 'torch', and 'vanilla'.
drop_rate (float): Dropout rate in attention map. (default: 0)
attn_mask (torch.Tensor): Attention mask with shape [b, s1] (cross_attn), or [b, a, s, s1] (torch or vanilla).
(default: None)
causal (bool): Whether to use causal attention. (default: False)
cu_seqlens_q (torch.Tensor): dtype torch.int32. The cumulative sequence lengths of the sequences in the batch,
used to index into q.
cu_seqlens_kv (torch.Tensor): dtype torch.int32. The cumulative sequence lengths of the sequences in the batch,
used to index into kv.
max_seqlen_q (int): The maximum sequence length in the batch of q.
max_seqlen_kv (int): The maximum sequence length in the batch of k and v.
Returns:
torch.Tensor: Output tensor after self attention with shape [b, s, ad]
"""
pre_attn_layout, post_attn_layout = MEMORY_LAYOUT[mode]
if mode == "torch":
if attn_mask is not None and attn_mask.dtype != torch.bool:
attn_mask = attn_mask.to(q.dtype)
x = F.scaled_dot_product_attention(
q, k, v, attn_mask=attn_mask, dropout_p=drop_rate, is_causal=causal)
elif mode == "flash":
x = flash_attn_func(
q,
k,
v,
)
# x with shape [(bxs), a, d]
x = x.view(batch_size, max_seqlen_q, x.shape[-2],
x.shape[-1]) # reshape x to [b, s, a, d]
elif mode == "vanilla":
scale_factor = 1 / math.sqrt(q.size(-1))
b, a, s, _ = q.shape
s1 = k.size(2)
attn_bias = torch.zeros(b, a, s, s1, dtype=q.dtype, device=q.device)
if causal:
# Only applied to self attention
assert (
attn_mask
is None), "Causal mask and attn_mask cannot be used together"
temp_mask = torch.ones(
b, a, s, s, dtype=torch.bool, device=q.device).tril(diagonal=0)
attn_bias.masked_fill_(temp_mask.logical_not(), float("-inf"))
attn_bias.to(q.dtype)
if attn_mask is not None:
if attn_mask.dtype == torch.bool:
attn_bias.masked_fill_(attn_mask.logical_not(), float("-inf"))
else:
attn_bias += attn_mask
# TODO: Maybe force q and k to be float32 to avoid numerical overflow
attn = (q @ k.transpose(-2, -1)) * scale_factor
attn += attn_bias
attn = attn.softmax(dim=-1)
attn = torch.dropout(attn, p=drop_rate, train=True)
x = attn @ v
else:
raise NotImplementedError(f"Unsupported attention mode: {mode}")
x = post_attn_layout(x)
b, s, a, d = x.shape
out = x.reshape(b, s, -1)
return out
class CausalConv1d(nn.Module):
def __init__(self,
chan_in,
chan_out,
kernel_size=3,
stride=1,
dilation=1,
pad_mode='replicate',
**kwargs):
super().__init__()
self.pad_mode = pad_mode
padding = (kernel_size - 1, 0) # T
self.time_causal_padding = padding
self.conv = nn.Conv1d(
chan_in,
chan_out,
kernel_size,
stride=stride,
dilation=dilation,
**kwargs)
def forward(self, x):
x = F.pad(x, self.time_causal_padding, mode=self.pad_mode)
return self.conv(x)
class MotionEncoder_tc(nn.Module):
def __init__(self,
in_dim: int,
hidden_dim: int,
num_heads=int,
need_global=True,
dtype=None,
device=None):
factory_kwargs = {"dtype": dtype, "device": device}
super().__init__()
self.num_heads = num_heads
self.need_global = need_global
self.conv1_local = CausalConv1d(
in_dim, hidden_dim // 4 * num_heads, 3, stride=1)
if need_global:
self.conv1_global = CausalConv1d(
in_dim, hidden_dim // 4, 3, stride=1)
self.norm1 = nn.LayerNorm(
hidden_dim // 4,
elementwise_affine=False,
eps=1e-6,
**factory_kwargs)
self.act = nn.SiLU()
self.conv2 = CausalConv1d(hidden_dim // 4, hidden_dim // 2, 3, stride=2)
self.conv3 = CausalConv1d(hidden_dim // 2, hidden_dim, 3, stride=2)
if need_global:
self.final_linear = nn.Linear(hidden_dim, hidden_dim,
**factory_kwargs)
self.norm1 = nn.LayerNorm(
hidden_dim // 4,
elementwise_affine=False,
eps=1e-6,
**factory_kwargs)
self.norm2 = nn.LayerNorm(
hidden_dim // 2,
elementwise_affine=False,
eps=1e-6,
**factory_kwargs)
self.norm3 = nn.LayerNorm(
hidden_dim, elementwise_affine=False, eps=1e-6, **factory_kwargs)
self.padding_tokens = nn.Parameter(torch.zeros(1, 1, 1, hidden_dim))
def forward(self, x):
x = rearrange(x, 'b t c -> b c t')
x_ori = x.clone()
b, c, t = x.shape
x = self.conv1_local(x)
x = rearrange(x, 'b (n c) t -> (b n) t c', n=self.num_heads)
x = self.norm1(x)
x = self.act(x)
x = rearrange(x, 'b t c -> b c t')
x = self.conv2(x)
x = rearrange(x, 'b c t -> b t c')
x = self.norm2(x)
x = self.act(x)
x = rearrange(x, 'b t c -> b c t')
x = self.conv3(x)
x = rearrange(x, 'b c t -> b t c')
x = self.norm3(x)
x = self.act(x)
x = rearrange(x, '(b n) t c -> b t n c', b=b)
padding = self.padding_tokens.repeat(b, x.shape[1], 1, 1)
x = torch.cat([x, padding], dim=-2)
x_local = x.clone()
if not self.need_global:
return x_local
x = self.conv1_global(x_ori)
x = rearrange(x, 'b c t -> b t c')
x = self.norm1(x)
x = self.act(x)
x = rearrange(x, 'b t c -> b c t')
x = self.conv2(x)
x = rearrange(x, 'b c t -> b t c')
x = self.norm2(x)
x = self.act(x)
x = rearrange(x, 'b t c -> b c t')
x = self.conv3(x)
x = rearrange(x, 'b c t -> b t c')
x = self.norm3(x)
x = self.act(x)
x = self.final_linear(x)
x = rearrange(x, '(b n) t c -> b t n c', b=b)
return x, x_local
+906
View File
@@ -0,0 +1,906 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import math
import types
from copy import deepcopy
import numpy as np
import torch
import torch.cuda.amp as amp
import torch.nn as nn
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.models.modeling_utils import ModelMixin
from einops import rearrange
from ...distributed.sequence_parallel import (
distributed_attention,
gather_forward,
get_rank,
get_world_size,
)
from ..model import (
Head,
WanAttentionBlock,
WanLayerNorm,
WanModel,
WanSelfAttention,
flash_attention,
rope_params,
sinusoidal_embedding_1d,
)
from .audio_utils import AudioInjector_WAN, CausalAudioEncoder
from .motioner import FramePackMotioner, MotionerTransformers
from .s2v_utils import rope_precompute
def zero_module(module):
"""
Zero out the parameters of a module and return it.
"""
for p in module.parameters():
p.detach().zero_()
return module
def torch_dfs(model: nn.Module, parent_name='root'):
module_names, modules = [], []
current_name = parent_name if parent_name else 'root'
module_names.append(current_name)
modules.append(model)
for name, child in model.named_children():
if parent_name:
child_name = f'{parent_name}.{name}'
else:
child_name = name
child_modules, child_names = torch_dfs(child, child_name)
module_names += child_names
modules += child_modules
return modules, module_names
@amp.autocast(enabled=False)
def rope_apply(x, grid_sizes, freqs, start=None):
n, c = x.size(2), x.size(3) // 2
# loop over samples
output = []
for i, _ in enumerate(x):
s = x.size(1)
x_i = torch.view_as_complex(x[i, :s].to(torch.float64).reshape(
s, n, -1, 2))
freqs_i = freqs[i, :s]
# apply rotary embedding
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
x_i = torch.cat([x_i, x[i, s:]])
# append to collection
output.append(x_i)
return torch.stack(output).float()
@amp.autocast(enabled=False)
def rope_apply_usp(x, grid_sizes, freqs):
s, n, c = x.size(1), x.size(2), x.size(3) // 2
# loop over samples
output = []
for i, _ in enumerate(x):
s = x.size(1)
# precompute multipliers
x_i = torch.view_as_complex(x[i, :s].to(torch.float64).reshape(
s, n, -1, 2))
freqs_i = freqs[i]
freqs_i_rank = freqs_i
x_i = torch.view_as_real(x_i * freqs_i_rank).flatten(2)
x_i = torch.cat([x_i, x[i, s:]])
# append to collection
output.append(x_i)
return torch.stack(output).float()
def sp_attn_forward_s2v(self,
x,
seq_lens,
grid_sizes,
freqs,
dtype=torch.bfloat16):
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
half_dtypes = (torch.float16, torch.bfloat16)
def half(x):
return x if x.dtype in half_dtypes else x.to(dtype)
# 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)
q = rope_apply_usp(q, grid_sizes, freqs)
k = rope_apply_usp(k, grid_sizes, freqs)
x = distributed_attention(
half(q),
half(k),
half(v),
seq_lens,
window_size=self.window_size,
)
# output
x = x.flatten(2)
x = self.o(x)
return x
class Head_S2V(Head):
def forward(self, x, e):
"""
Args:
x(Tensor): Shape [B, L1, C]
e(Tensor): Shape [B, L1, C]
"""
assert e.dtype == torch.float32
with amp.autocast(dtype=torch.float32):
e = (self.modulation + e.unsqueeze(1)).chunk(2, dim=1)
x = (self.head(self.norm(x) * (1 + e[1]) + e[0]))
return x
class WanS2VSelfAttention(WanSelfAttention):
def forward(self, x, seq_lens, grid_sizes, freqs):
"""
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]
"""
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
# 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)
x = flash_attention(
q=rope_apply(q, grid_sizes, freqs),
k=rope_apply(k, grid_sizes, freqs),
v=v,
k_lens=seq_lens,
window_size=self.window_size)
# output
x = x.flatten(2)
x = self.o(x)
return x
class WanS2VAttentionBlock(WanAttentionBlock):
def __init__(self,
dim,
ffn_dim,
num_heads,
window_size=(-1, -1),
qk_norm=True,
cross_attn_norm=False,
eps=1e-6):
super().__init__(dim, ffn_dim, num_heads, window_size, qk_norm,
cross_attn_norm, eps)
self.self_attn = WanS2VSelfAttention(dim, num_heads, window_size,
qk_norm, eps)
def forward(self, x, e, seq_lens, grid_sizes, freqs, context, context_lens):
assert e[0].dtype == torch.float32
seg_idx = e[1].item()
seg_idx = min(max(0, seg_idx), x.size(1))
seg_idx = [0, seg_idx, x.size(1)]
e = e[0]
modulation = self.modulation.unsqueeze(2)
with amp.autocast(dtype=torch.float32):
e = (modulation + e).chunk(6, dim=1)
assert e[0].dtype == torch.float32
e = [element.squeeze(1) for element in e]
norm_x = self.norm1(x).float()
parts = []
for i in range(2):
parts.append(norm_x[:, seg_idx[i]:seg_idx[i + 1]] *
(1 + e[1][:, i:i + 1]) + e[0][:, i:i + 1])
norm_x = torch.cat(parts, dim=1)
# self-attention
y = self.self_attn(norm_x, seq_lens, grid_sizes, freqs)
with amp.autocast(dtype=torch.float32):
z = []
for i in range(2):
z.append(y[:, seg_idx[i]:seg_idx[i + 1]] * e[2][:, i:i + 1])
y = torch.cat(z, dim=1)
x = x + y
# cross-attention & ffn function
def cross_attn_ffn(x, context, context_lens, e):
x = x + self.cross_attn(self.norm3(x), context, context_lens)
norm2_x = self.norm2(x).float()
parts = []
for i in range(2):
parts.append(norm2_x[:, seg_idx[i]:seg_idx[i + 1]] *
(1 + e[4][:, i:i + 1]) + e[3][:, i:i + 1])
norm2_x = torch.cat(parts, dim=1)
y = self.ffn(norm2_x)
with amp.autocast(dtype=torch.float32):
z = []
for i in range(2):
z.append(y[:, seg_idx[i]:seg_idx[i + 1]] * e[5][:, i:i + 1])
y = torch.cat(z, dim=1)
x = x + y
return x
x = cross_attn_ffn(x, context, context_lens, e)
return x
class WanModel_S2V(ModelMixin, ConfigMixin):
ignore_for_config = [
'args', 'kwargs', 'patch_size', 'cross_attn_norm', 'qk_norm',
'text_dim', 'window_size'
]
_no_split_modules = ['WanS2VAttentionBlock']
@register_to_config
def __init__(
self,
cond_dim=0,
audio_dim=5120,
num_audio_token=4,
enable_adain=False,
adain_mode="attn_norm",
audio_inject_layers=[0, 4, 8, 12, 16, 20, 24, 27],
zero_init=False,
zero_timestep=False,
enable_motioner=True,
add_last_motion=True,
enable_tsm=False,
trainable_token_pos_emb=False,
motion_token_num=1024,
enable_framepack=False, # Mutually exclusive with enable_motioner
framepack_drop_mode="drop",
model_type='s2v',
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,
window_size=(-1, -1),
qk_norm=True,
cross_attn_norm=True,
eps=1e-6,
*args,
**kwargs):
super().__init__()
assert model_type == 's2v'
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.window_size = window_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.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
self.blocks = nn.ModuleList([
WanS2VAttentionBlock(dim, ffn_dim, num_heads, window_size, qk_norm,
cross_attn_norm, eps)
for _ in range(num_layers)
])
# head
self.head = Head_S2V(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)
# initialize weights
self.init_weights()
self.use_context_parallel = False # will modify in _configure_model func
if cond_dim > 0:
self.cond_encoder = nn.Conv3d(
cond_dim,
self.dim,
kernel_size=self.patch_size,
stride=self.patch_size)
self.enbale_adain = enable_adain
self.casual_audio_encoder = CausalAudioEncoder(
dim=audio_dim,
out_dim=self.dim,
num_token=num_audio_token,
need_global=enable_adain)
all_modules, all_modules_names = torch_dfs(
self.blocks, parent_name="root.transformer_blocks")
self.audio_injector = AudioInjector_WAN(
all_modules,
all_modules_names,
dim=self.dim,
num_heads=self.num_heads,
inject_layer=audio_inject_layers,
root_net=self,
enable_adain=enable_adain,
adain_dim=self.dim,
need_adain_ont=adain_mode != "attn_norm",
)
self.adain_mode = adain_mode
self.trainable_cond_mask = nn.Embedding(3, self.dim)
if zero_init:
self.zero_init_weights()
self.zero_timestep = zero_timestep # Whether to assign 0 value timestep to ref/motion
# init motioner
if enable_motioner and enable_framepack:
raise ValueError(
"enable_motioner and enable_framepack are mutually exclusive, please set one of them to False"
)
self.enable_motioner = enable_motioner
self.add_last_motion = add_last_motion
if enable_motioner:
motioner_dim = 2048
self.motioner = MotionerTransformers(
patch_size=(2, 4, 4),
dim=motioner_dim,
ffn_dim=motioner_dim,
freq_dim=256,
out_dim=16,
num_heads=16,
num_layers=13,
window_size=(-1, -1),
qk_norm=True,
cross_attn_norm=False,
eps=1e-6,
motion_token_num=motion_token_num,
enable_tsm=enable_tsm,
motion_stride=4,
expand_ratio=2,
trainable_token_pos_emb=trainable_token_pos_emb,
)
self.zip_motion_out = torch.nn.Sequential(
WanLayerNorm(motioner_dim),
zero_module(nn.Linear(motioner_dim, self.dim)))
self.trainable_token_pos_emb = trainable_token_pos_emb
if trainable_token_pos_emb:
d = self.dim // self.num_heads
x = torch.zeros([1, motion_token_num, self.num_heads, d])
x[..., ::2] = 1
gride_sizes = [[
torch.tensor([0, 0, 0]).unsqueeze(0).repeat(1, 1),
torch.tensor([
1, self.motioner.motion_side_len,
self.motioner.motion_side_len
]).unsqueeze(0).repeat(1, 1),
torch.tensor([
1, self.motioner.motion_side_len,
self.motioner.motion_side_len
]).unsqueeze(0).repeat(1, 1),
]]
token_freqs = rope_apply(x, gride_sizes, self.freqs)
token_freqs = token_freqs[0, :,
0].reshape(motion_token_num, -1, 2)
token_freqs = token_freqs * 0.01
self.token_freqs = torch.nn.Parameter(token_freqs)
self.enable_framepack = enable_framepack
if enable_framepack:
self.frame_packer = FramePackMotioner(
inner_dim=self.dim,
num_heads=self.num_heads,
zip_frame_buckets=[1, 2, 16],
drop_mode=framepack_drop_mode)
def zero_init_weights(self):
with torch.no_grad():
self.trainable_cond_mask = zero_module(self.trainable_cond_mask)
if hasattr(self, "cond_encoder"):
self.cond_encoder = zero_module(self.cond_encoder)
for i in range(self.audio_injector.injector.__len__()):
self.audio_injector.injector[i].o = zero_module(
self.audio_injector.injector[i].o)
if self.enbale_adain:
self.audio_injector.injector_adain_layers[
i].linear = zero_module(
self.audio_injector.injector_adain_layers[i].linear)
def process_motion(self, motion_latents, drop_motion_frames=False):
if drop_motion_frames or motion_latents[0].shape[1] == 0:
return [], []
self.lat_motion_frames = motion_latents[0].shape[1]
mot = [self.patch_embedding(m.unsqueeze(0)) for m in motion_latents]
batch_size = len(mot)
mot_remb = []
flattern_mot = []
for bs in range(batch_size):
height, width = mot[bs].shape[3], mot[bs].shape[4]
flat_mot = mot[bs].flatten(2).transpose(1, 2).contiguous()
motion_grid_sizes = [[
torch.tensor([-self.lat_motion_frames, 0,
0]).unsqueeze(0).repeat(1, 1),
torch.tensor([0, height, width]).unsqueeze(0).repeat(1, 1),
torch.tensor([self.lat_motion_frames, height,
width]).unsqueeze(0).repeat(1, 1)
]]
motion_rope_emb = rope_precompute(
flat_mot.detach().view(1, flat_mot.shape[1], self.num_heads,
self.dim // self.num_heads),
motion_grid_sizes,
self.freqs,
start=None)
mot_remb.append(motion_rope_emb)
flattern_mot.append(flat_mot)
return flattern_mot, mot_remb
def process_motion_frame_pack(self,
motion_latents,
drop_motion_frames=False,
add_last_motion=2):
flattern_mot, mot_remb = self.frame_packer(motion_latents,
add_last_motion)
if drop_motion_frames:
return [m[:, :0] for m in flattern_mot
], [m[:, :0] for m in mot_remb]
else:
return flattern_mot, mot_remb
def process_motion_transformer_motioner(self,
motion_latents,
drop_motion_frames=False,
add_last_motion=True):
batch_size, height, width = len(
motion_latents), motion_latents[0].shape[2] // self.patch_size[
1], motion_latents[0].shape[3] // self.patch_size[2]
freqs = self.freqs
device = self.patch_embedding.weight.device
if freqs.device != device:
freqs = freqs.to(device)
if self.trainable_token_pos_emb:
with amp.autocast(dtype=torch.float64):
token_freqs = self.token_freqs.to(torch.float64)
token_freqs = token_freqs / token_freqs.norm(
dim=-1, keepdim=True)
freqs = [freqs, torch.view_as_complex(token_freqs)]
if not drop_motion_frames and add_last_motion:
last_motion_latent = [u[:, -1:] for u in motion_latents]
last_mot = [
self.patch_embedding(m.unsqueeze(0)) for m in last_motion_latent
]
last_mot = [m.flatten(2).transpose(1, 2) for m in last_mot]
last_mot = torch.cat(last_mot)
gride_sizes = [[
torch.tensor([-1, 0, 0]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor([0, height,
width]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor([1, height,
width]).unsqueeze(0).repeat(batch_size, 1)
]]
else:
last_mot = torch.zeros([batch_size, 0, self.dim],
device=motion_latents[0].device,
dtype=motion_latents[0].dtype)
gride_sizes = []
zip_motion = self.motioner(motion_latents)
zip_motion = self.zip_motion_out(zip_motion)
if drop_motion_frames:
zip_motion = zip_motion * 0.0
zip_motion_grid_sizes = [[
torch.tensor([-1, 0, 0]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor([
0, self.motioner.motion_side_len, self.motioner.motion_side_len
]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor(
[1 if not self.trainable_token_pos_emb else -1, height,
width]).unsqueeze(0).repeat(batch_size, 1),
]]
mot = torch.cat([last_mot, zip_motion], dim=1)
gride_sizes = gride_sizes + zip_motion_grid_sizes
motion_rope_emb = rope_precompute(
mot.detach().view(batch_size, mot.shape[1], self.num_heads,
self.dim // self.num_heads),
gride_sizes,
freqs,
start=None)
return [m.unsqueeze(0) for m in mot
], [r.unsqueeze(0) for r in motion_rope_emb]
def inject_motion(self,
x,
seq_lens,
rope_embs,
mask_input,
motion_latents,
drop_motion_frames=False,
add_last_motion=True):
# inject the motion frames token to the hidden states
if self.enable_motioner:
mot, mot_remb = self.process_motion_transformer_motioner(
motion_latents,
drop_motion_frames=drop_motion_frames,
add_last_motion=add_last_motion)
elif self.enable_framepack:
mot, mot_remb = self.process_motion_frame_pack(
motion_latents,
drop_motion_frames=drop_motion_frames,
add_last_motion=add_last_motion)
else:
mot, mot_remb = self.process_motion(
motion_latents, drop_motion_frames=drop_motion_frames)
if len(mot) > 0:
x = [torch.cat([u, m], dim=1) for u, m in zip(x, mot)]
seq_lens = seq_lens + torch.tensor([r.size(1) for r in mot],
dtype=torch.long)
rope_embs = [
torch.cat([u, m], dim=1) for u, m in zip(rope_embs, mot_remb)
]
mask_input = [
torch.cat([
m, 2 * torch.ones([1, u.shape[1] - m.shape[1]],
device=m.device,
dtype=m.dtype)
],
dim=1) for m, u in zip(mask_input, x)
]
return x, seq_lens, rope_embs, mask_input
def after_transformer_block(self, block_idx, hidden_states):
if block_idx in self.audio_injector.injected_block_id.keys():
audio_attn_id = self.audio_injector.injected_block_id[block_idx]
audio_emb = self.merged_audio_emb # b f n c
num_frames = audio_emb.shape[1]
if self.use_context_parallel:
hidden_states = gather_forward(hidden_states, dim=1)
input_hidden_states = hidden_states[:, :self.
original_seq_len].clone(
) # b (f h w) c
input_hidden_states = rearrange(
input_hidden_states, "b (t n) c -> (b t) n c", t=num_frames)
if self.enbale_adain and self.adain_mode == "attn_norm":
audio_emb_global = self.audio_emb_global
audio_emb_global = rearrange(audio_emb_global,
"b t n c -> (b t) n c")
adain_hidden_states = self.audio_injector.injector_adain_layers[
audio_attn_id](
input_hidden_states, temb=audio_emb_global[:, 0])
attn_hidden_states = adain_hidden_states
else:
attn_hidden_states = self.audio_injector.injector_pre_norm_feat[
audio_attn_id](
input_hidden_states)
audio_emb = rearrange(
audio_emb, "b t n c -> (b t) n c", t=num_frames)
attn_audio_emb = audio_emb
residual_out = self.audio_injector.injector[audio_attn_id](
x=attn_hidden_states,
context=attn_audio_emb,
context_lens=torch.ones(
attn_hidden_states.shape[0],
dtype=torch.long,
device=attn_hidden_states.device) * attn_audio_emb.shape[1])
residual_out = rearrange(
residual_out, "(b t) n c -> b (t n) c", t=num_frames)
hidden_states[:, :self.
original_seq_len] = hidden_states[:, :self.
original_seq_len] + residual_out
if self.use_context_parallel:
hidden_states = torch.chunk(
hidden_states, get_world_size(), dim=1)[get_rank()]
return hidden_states
def forward(
self,
x,
t,
context,
seq_len,
ref_latents,
motion_latents,
cond_states,
audio_input=None,
motion_frames=[17, 5],
add_last_motion=2,
drop_motion_frames=False,
*extra_args,
**extra_kwargs):
"""
x: A list of videos each with shape [C, T, H, W].
t: [B].
context: A list of text embeddings each with shape [L, C].
seq_len: A list of video token lens, no need for this model.
ref_latents A list of reference image for each video with shape [C, 1, H, W].
motion_latents A list of motion frames for each video with shape [C, T_m, H, W].
cond_states A list of condition frames (i.e. pose) each with shape [C, T, H, W].
audio_input The input audio embedding [B, num_wav2vec_layer, C_a, T_a].
motion_frames The number of motion frames and motion latents frames encoded by vae, i.e. [17, 5]
add_last_motion For the motioner, if add_last_motion > 0, it means that the most recent frame (i.e., the last frame) will be added.
For frame packing, the behavior depends on the value of add_last_motion:
add_last_motion = 0: Only the farthest part of the latent (i.e., clean_latents_4x) is included.
add_last_motion = 1: Both clean_latents_2x and clean_latents_4x are included.
add_last_motion = 2: All motion-related latents are used.
drop_motion_frames Bool, whether drop the motion frames info
"""
add_last_motion = self.add_last_motion * add_last_motion
audio_input = torch.cat([
audio_input[..., 0:1].repeat(1, 1, 1, motion_frames[0]), audio_input
],
dim=-1)
audio_emb_res = self.casual_audio_encoder(audio_input)
if self.enbale_adain:
audio_emb_global, audio_emb = audio_emb_res
self.audio_emb_global = audio_emb_global[:,
motion_frames[1]:].clone()
else:
audio_emb = audio_emb_res
self.merged_audio_emb = audio_emb[:, motion_frames[1]:, :]
device = self.patch_embedding.weight.device
# embeddings
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
# cond states
cond = [self.cond_encoder(c.unsqueeze(0)) for c in cond_states]
x = [x_ + pose for x_, pose in zip(x, cond)]
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)
original_grid_sizes = deepcopy(grid_sizes)
grid_sizes = [[torch.zeros_like(grid_sizes), grid_sizes, grid_sizes]]
# ref and motion
self.lat_motion_frames = motion_latents[0].shape[1]
ref = [self.patch_embedding(r.unsqueeze(0)) for r in ref_latents]
batch_size = len(ref)
height, width = ref[0].shape[3], ref[0].shape[4]
ref_grid_sizes = [[
torch.tensor([30, 0, 0]).unsqueeze(0).repeat(batch_size,
1), # the start index
torch.tensor([31, height,
width]).unsqueeze(0).repeat(batch_size,
1), # the end index
torch.tensor([1, height, width]).unsqueeze(0).repeat(batch_size, 1),
] # the range
]
ref = [r.flatten(2).transpose(1, 2) for r in ref] # r: 1 c f h w
self.original_seq_len = seq_lens[0]
seq_lens = seq_lens + torch.tensor([r.size(1) for r in ref],
dtype=torch.long)
grid_sizes = grid_sizes + ref_grid_sizes
x = [torch.cat([u, r], dim=1) for u, r in zip(x, ref)]
# Initialize masks to indicate noisy latent, ref latent, and motion latent.
# However, at this point, only the first two (noisy and ref latents) are marked;
# the marking of motion latent will be implemented inside `inject_motion`.
mask_input = [
torch.zeros([1, u.shape[1]], dtype=torch.long, device=x[0].device)
for u in x
]
for i in range(len(mask_input)):
mask_input[i][:, self.original_seq_len:] = 1
# compute the rope embeddings for the input
x = torch.cat(x)
b, s, n, d = x.size(0), x.size(
1), self.num_heads, self.dim // self.num_heads
self.pre_compute_freqs = rope_precompute(
x.detach().view(b, s, n, d), grid_sizes, self.freqs, start=None)
x = [u.unsqueeze(0) for u in x]
self.pre_compute_freqs = [
u.unsqueeze(0) for u in self.pre_compute_freqs
]
x, seq_lens, self.pre_compute_freqs, mask_input = self.inject_motion(
x,
seq_lens,
self.pre_compute_freqs,
mask_input,
motion_latents,
drop_motion_frames=drop_motion_frames,
add_last_motion=add_last_motion)
x = torch.cat(x, dim=0)
self.pre_compute_freqs = torch.cat(self.pre_compute_freqs, dim=0)
mask_input = torch.cat(mask_input, dim=0)
x = x + self.trainable_cond_mask(mask_input).to(x.dtype)
# time embeddings
if self.zero_timestep:
t = torch.cat([t, torch.zeros([1], dtype=t.dtype, device=t.device)])
with amp.autocast(dtype=torch.float32):
e = self.time_embedding(
sinusoidal_embedding_1d(self.freq_dim, t).float())
e0 = self.time_projection(e).unflatten(1, (6, self.dim))
assert e.dtype == torch.float32 and e0.dtype == torch.float32
if self.zero_timestep:
e = e[:-1]
zero_e0 = e0[-1:]
e0 = e0[:-1]
token_len = x.shape[1]
e0 = torch.cat([
e0.unsqueeze(2),
zero_e0.unsqueeze(2).repeat(e0.size(0), 1, 1, 1)
],
dim=2)
e0 = [e0, self.original_seq_len]
else:
e0 = e0.unsqueeze(2).repeat(1, 1, 2, 1)
e0 = [e0, 0]
# 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
]))
# grad ckpt args
def create_custom_forward(module, return_dict=None):
def custom_forward(*inputs, **kwargs):
if return_dict is not None:
return module(*inputs, **kwargs, return_dict=return_dict)
else:
return module(*inputs, **kwargs)
return custom_forward
if self.use_context_parallel:
# sharded tensors for long context attn
sp_rank = get_rank()
x = torch.chunk(x, get_world_size(), dim=1)
sq_size = [u.shape[1] for u in x]
sq_start_size = sum(sq_size[:sp_rank])
x = x[sp_rank]
# Confirm the application range of the time embedding in e0[0] for each sequence:
# - For tokens before seg_id: apply e0[0][:, :, 0]
# - For tokens after seg_id: apply e0[0][:, :, 1]
sp_size = x.shape[1]
seg_idx = e0[1] - sq_start_size
e0[1] = seg_idx
self.pre_compute_freqs = torch.chunk(
self.pre_compute_freqs, get_world_size(), dim=1)
self.pre_compute_freqs = self.pre_compute_freqs[sp_rank]
# arguments
kwargs = dict(
e=e0,
seq_lens=seq_lens,
grid_sizes=grid_sizes,
freqs=self.pre_compute_freqs,
context=context,
context_lens=context_lens)
for idx, block in enumerate(self.blocks):
x = block(x, **kwargs)
x = self.after_transformer_block(idx, x)
# Context Parallel
if self.use_context_parallel:
x = gather_forward(x.contiguous(), dim=1)
# unpatchify
x = x[:, :self.original_seq_len]
# head
x = self.head(x, e)
x = self.unpatchify(x, original_grid_sizes)
return [u.float() for u in x]
def unpatchify(self, x, grid_sizes):
"""
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)
+794
View File
@@ -0,0 +1,794 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import math
from typing import Any, Dict, List, Literal, Optional, Union
import numpy as np
import torch
import torch.cuda.amp as amp
import torch.nn as nn
from diffusers.loaders import FromOriginalModelMixin, PeftAdapterMixin
from diffusers.utils import BaseOutput, is_torch_version
from einops import rearrange, repeat
from ..model import flash_attention
from .s2v_utils import rope_precompute
def sinusoidal_embedding_1d(dim, position):
# preprocess
assert dim % 2 == 0
half = dim // 2
position = position.type(torch.float64)
# calculation
sinusoid = torch.outer(
position, torch.pow(10000, -torch.arange(half).to(position).div(half)))
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
return x
@amp.autocast(enabled=False)
def rope_params(max_seq_len, dim, theta=10000):
assert dim % 2 == 0
freqs = torch.outer(
torch.arange(max_seq_len),
1.0 / torch.pow(theta,
torch.arange(0, dim, 2).to(torch.float64).div(dim)))
freqs = torch.polar(torch.ones_like(freqs), freqs)
return freqs
@amp.autocast(enabled=False)
def rope_apply(x, grid_sizes, freqs, start=None):
n, c = x.size(2), x.size(3) // 2
# split freqs
if type(freqs) is list:
trainable_freqs = freqs[1]
freqs = freqs[0]
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
# loop over samples
output = []
output = x.clone()
seq_bucket = [0]
if not type(grid_sizes) is list:
grid_sizes = [grid_sizes]
for g in grid_sizes:
if not type(g) is list:
g = [torch.zeros_like(g), g]
batch_size = g[0].shape[0]
for i in range(batch_size):
if start is None:
f_o, h_o, w_o = g[0][i]
else:
f_o, h_o, w_o = start[i]
f, h, w = g[1][i]
t_f, t_h, t_w = g[2][i]
seq_f, seq_h, seq_w = f - f_o, h - h_o, w - w_o
seq_len = int(seq_f * seq_h * seq_w)
if seq_len > 0:
if t_f > 0:
factor_f, factor_h, factor_w = (t_f / seq_f).item(), (
t_h / seq_h).item(), (t_w / seq_w).item()
if f_o >= 0:
f_sam = np.linspace(f_o.item(), (t_f + f_o).item() - 1,
seq_f).astype(int).tolist()
else:
f_sam = np.linspace(-f_o.item(),
(-t_f - f_o).item() + 1,
seq_f).astype(int).tolist()
h_sam = np.linspace(h_o.item(), (t_h + h_o).item() - 1,
seq_h).astype(int).tolist()
w_sam = np.linspace(w_o.item(), (t_w + w_o).item() - 1,
seq_w).astype(int).tolist()
assert f_o * f >= 0 and h_o * h >= 0 and w_o * w >= 0
freqs_0 = freqs[0][f_sam] if f_o >= 0 else freqs[0][
f_sam].conj()
freqs_0 = freqs_0.view(seq_f, 1, 1, -1)
freqs_i = torch.cat([
freqs_0.expand(seq_f, seq_h, seq_w, -1),
freqs[1][h_sam].view(1, seq_h, 1, -1).expand(
seq_f, seq_h, seq_w, -1),
freqs[2][w_sam].view(1, 1, seq_w, -1).expand(
seq_f, seq_h, seq_w, -1),
],
dim=-1).reshape(seq_len, 1, -1)
elif t_f < 0:
freqs_i = trainable_freqs.unsqueeze(1)
# apply rotary embedding
# precompute multipliers
x_i = torch.view_as_complex(
x[i, seq_bucket[-1]:seq_bucket[-1] + seq_len].to(
torch.float64).reshape(seq_len, n, -1, 2))
x_i = torch.view_as_real(x_i * freqs_i).flatten(2)
output[i, seq_bucket[-1]:seq_bucket[-1] + seq_len] = x_i
seq_bucket.append(seq_bucket[-1] + seq_len)
return output.float()
class RMSNorm(nn.Module):
def __init__(self, dim, eps=1e-5):
super().__init__()
self.dim = dim
self.eps = eps
self.weight = nn.Parameter(torch.ones(dim))
def forward(self, x):
return self._norm(x.float()).type_as(x) * self.weight
def _norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(dim=-1, keepdim=True) + self.eps)
class LayerNorm(nn.LayerNorm):
def __init__(self, dim, eps=1e-6, elementwise_affine=False):
super().__init__(dim, elementwise_affine=elementwise_affine, eps=eps)
def forward(self, x):
return super().forward(x.float()).type_as(x)
class SelfAttention(nn.Module):
def __init__(self,
dim,
num_heads,
window_size=(-1, -1),
qk_norm=True,
eps=1e-6):
assert dim % num_heads == 0
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.window_size = window_size
self.qk_norm = qk_norm
self.eps = eps
# 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 = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
self.norm_k = RMSNorm(dim, eps=eps) if qk_norm else nn.Identity()
def forward(self, x, seq_lens, grid_sizes, freqs):
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
# 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)
x = flash_attention(
q=rope_apply(q, grid_sizes, freqs),
k=rope_apply(k, grid_sizes, freqs),
v=v,
k_lens=seq_lens,
window_size=self.window_size)
# output
x = x.flatten(2)
x = self.o(x)
return x
class SwinSelfAttention(SelfAttention):
def forward(self, x, seq_lens, grid_sizes, freqs):
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
assert b == 1, 'Only support batch_size 1'
# 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)
q = rope_apply(q, grid_sizes, freqs)
k = rope_apply(k, grid_sizes, freqs)
T, H, W = grid_sizes[0].tolist()
q = rearrange(q, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
k = rearrange(k, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
v = rearrange(v, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
ref_q = q[-1:]
q = q[:-1]
ref_k = repeat(
k[-1:], "1 s n d -> t s n d", t=k.shape[0] - 1) # t hw n d
k = k[:-1]
k = torch.cat([k[:1], k, k[-1:]])
k = torch.cat([k[1:-1], k[2:], k[:-2], ref_k], dim=1) # (bt) (3hw) n d
ref_v = repeat(v[-1:], "1 s n d -> t s n d", t=v.shape[0] - 1)
v = v[:-1]
v = torch.cat([v[:1], v, v[-1:]])
v = torch.cat([v[1:-1], v[2:], v[:-2], ref_v], dim=1)
# q: b (t h w) n d
# k: b (t h w) n d
out = flash_attention(
q=q,
k=k,
v=v,
# k_lens=torch.tensor([k.shape[1]] * k.shape[0], device=x.device, dtype=torch.long),
window_size=self.window_size)
out = torch.cat([out, ref_v[:1]], axis=0)
out = rearrange(out, '(b t) (h w) n d -> b (t h w) n d', t=T, h=H, w=W)
x = out
# output
x = x.flatten(2)
x = self.o(x)
return x
#Fix the reference frame RoPE to 1,H,W.
#Set the current frame RoPE to 1.
#Set the previous frame RoPE to 0.
class CasualSelfAttention(SelfAttention):
def forward(self, x, seq_lens, grid_sizes, freqs):
shifting = 3
b, s, n, d = *x.shape[:2], self.num_heads, self.head_dim
assert b == 1, 'Only support batch_size 1'
# 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)
T, H, W = grid_sizes[0].tolist()
q = rearrange(q, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
k = rearrange(k, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
v = rearrange(v, 'b (t h w) n d -> (b t) (h w) n d', t=T, h=H, w=W)
ref_q = q[-1:]
q = q[:-1]
grid_sizes = torch.tensor([[1, H, W]] * q.shape[0], dtype=torch.long)
start = [[shifting, 0, 0]] * q.shape[0]
q = rope_apply(q, grid_sizes, freqs, start=start)
ref_k = k[-1:]
grid_sizes = torch.tensor([[1, H, W]], dtype=torch.long)
# start = [[shifting, H, W]]
start = [[shifting + 10, 0, 0]]
ref_k = rope_apply(ref_k, grid_sizes, freqs, start)
ref_k = repeat(
ref_k, "1 s n d -> t s n d", t=k.shape[0] - 1) # t hw n d
k = k[:-1]
k = torch.cat([*([k[:1]] * shifting), k])
cat_k = []
for i in range(shifting):
cat_k.append(k[i:i - shifting])
cat_k.append(k[shifting:])
k = torch.cat(cat_k, dim=1) # (bt) (3hw) n d
grid_sizes = torch.tensor(
[[shifting + 1, H, W]] * q.shape[0], dtype=torch.long)
k = rope_apply(k, grid_sizes, freqs)
k = torch.cat([k, ref_k], dim=1)
ref_v = repeat(v[-1:], "1 s n d -> t s n d", t=q.shape[0]) # t hw n d
v = v[:-1]
v = torch.cat([*([v[:1]] * shifting), v])
cat_v = []
for i in range(shifting):
cat_v.append(v[i:i - shifting])
cat_v.append(v[shifting:])
v = torch.cat(cat_v, dim=1) # (bt) (3hw) n d
v = torch.cat([v, ref_v], dim=1)
# q: b (t h w) n d
# k: b (t h w) n d
outs = []
for i in range(q.shape[0]):
out = flash_attention(
q=q[i:i + 1],
k=k[i:i + 1],
v=v[i:i + 1],
window_size=self.window_size)
outs.append(out)
out = torch.cat(outs, dim=0)
out = torch.cat([out, ref_v[:1]], axis=0)
out = rearrange(out, '(b t) (h w) n d -> b (t h w) n d', t=T, h=H, w=W)
x = out
# output
x = x.flatten(2)
x = self.o(x)
return x
class MotionerAttentionBlock(nn.Module):
def __init__(self,
dim,
ffn_dim,
num_heads,
window_size=(-1, -1),
qk_norm=True,
cross_attn_norm=False,
eps=1e-6,
self_attn_block="SelfAttention"):
super().__init__()
self.dim = dim
self.ffn_dim = ffn_dim
self.num_heads = num_heads
self.window_size = window_size
self.qk_norm = qk_norm
self.cross_attn_norm = cross_attn_norm
self.eps = eps
# layers
self.norm1 = LayerNorm(dim, eps)
if self_attn_block == "SelfAttention":
self.self_attn = SelfAttention(dim, num_heads, window_size, qk_norm,
eps)
elif self_attn_block == "SwinSelfAttention":
self.self_attn = SwinSelfAttention(dim, num_heads, window_size,
qk_norm, eps)
elif self_attn_block == "CasualSelfAttention":
self.self_attn = CasualSelfAttention(dim, num_heads, window_size,
qk_norm, eps)
self.norm2 = LayerNorm(dim, eps)
self.ffn = nn.Sequential(
nn.Linear(dim, ffn_dim), nn.GELU(approximate='tanh'),
nn.Linear(ffn_dim, dim))
def forward(
self,
x,
seq_lens,
grid_sizes,
freqs,
):
# self-attention
y = self.self_attn(self.norm1(x).float(), seq_lens, grid_sizes, freqs)
x = x + y
y = self.ffn(self.norm2(x).float())
x = x + y
return x
class Head(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 = LayerNorm(dim, eps)
self.head = nn.Linear(dim, out_dim)
def forward(self, x):
x = self.head(self.norm(x))
return x
class MotionerTransformers(nn.Module, PeftAdapterMixin):
def __init__(
self,
patch_size=(1, 2, 2),
in_dim=16,
dim=2048,
ffn_dim=8192,
freq_dim=256,
out_dim=16,
num_heads=16,
num_layers=32,
window_size=(-1, -1),
qk_norm=True,
cross_attn_norm=False,
eps=1e-6,
self_attn_block="SelfAttention",
motion_token_num=1024,
enable_tsm=False,
motion_stride=4,
expand_ratio=2,
trainable_token_pos_emb=False,
):
super().__init__()
self.patch_size = patch_size
self.in_dim = in_dim
self.dim = dim
self.ffn_dim = ffn_dim
self.freq_dim = freq_dim
self.out_dim = out_dim
self.num_heads = num_heads
self.num_layers = num_layers
self.window_size = window_size
self.qk_norm = qk_norm
self.cross_attn_norm = cross_attn_norm
self.eps = eps
self.enable_tsm = enable_tsm
self.motion_stride = motion_stride
self.expand_ratio = expand_ratio
self.sample_c = self.patch_size[0]
# embeddings
self.patch_embedding = nn.Conv3d(
in_dim, dim, kernel_size=patch_size, stride=patch_size)
# blocks
self.blocks = nn.ModuleList([
MotionerAttentionBlock(
dim,
ffn_dim,
num_heads,
window_size,
qk_norm,
cross_attn_norm,
eps,
self_attn_block=self_attn_block) for _ in range(num_layers)
])
# 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)
self.gradient_checkpointing = False
self.motion_side_len = int(math.sqrt(motion_token_num))
assert self.motion_side_len**2 == motion_token_num
self.token = nn.Parameter(
torch.zeros(1, motion_token_num, dim).contiguous())
self.trainable_token_pos_emb = trainable_token_pos_emb
if trainable_token_pos_emb:
x = torch.zeros([1, motion_token_num, num_heads, d])
x[..., ::2] = 1
gride_sizes = [[
torch.tensor([0, 0, 0]).unsqueeze(0).repeat(1, 1),
torch.tensor([1, self.motion_side_len,
self.motion_side_len]).unsqueeze(0).repeat(1, 1),
torch.tensor([1, self.motion_side_len,
self.motion_side_len]).unsqueeze(0).repeat(1, 1),
]]
token_freqs = rope_apply(x, gride_sizes, self.freqs)
token_freqs = token_freqs[0, :, 0].reshape(motion_token_num, -1, 2)
token_freqs = token_freqs * 0.01
self.token_freqs = torch.nn.Parameter(token_freqs)
def after_patch_embedding(self, x):
return x
def forward(
self,
x,
):
"""
x: A list of videos each with shape [C, T, H, W].
t: [B].
context: A list of text embeddings each with shape [L, C].
"""
# params
motion_frames = x[0].shape[1]
device = self.patch_embedding.weight.device
freqs = self.freqs
if freqs.device != device:
freqs = freqs.to(device)
if self.trainable_token_pos_emb:
with amp.autocast(dtype=torch.float64):
token_freqs = self.token_freqs.to(torch.float64)
token_freqs = token_freqs / token_freqs.norm(
dim=-1, keepdim=True)
freqs = [freqs, torch.view_as_complex(token_freqs)]
if self.enable_tsm:
sample_idx = [
sample_indices(
u.shape[1],
stride=self.motion_stride,
expand_ratio=self.expand_ratio,
c=self.sample_c) for u in x
]
x = [
torch.flip(torch.flip(u, [1])[:, idx], [1])
for idx, u in zip(sample_idx, x)
]
# embeddings
x = [self.patch_embedding(u.unsqueeze(0)) for u in x]
x = self.after_patch_embedding(x)
seq_f, seq_h, seq_w = x[0].shape[-3:]
batch_size = len(x)
if not self.enable_tsm:
grid_sizes = torch.stack(
[torch.tensor(u.shape[2:], dtype=torch.long) for u in x])
grid_sizes = [[
torch.zeros_like(grid_sizes), grid_sizes, grid_sizes
]]
seq_f = 0
else:
grid_sizes = []
for idx in sample_idx[0][::-1][::self.sample_c]:
tsm_frame_grid_sizes = [[
torch.tensor([idx, 0,
0]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor([idx + 1, seq_h,
seq_w]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor([1, seq_h,
seq_w]).unsqueeze(0).repeat(batch_size, 1),
]]
grid_sizes += tsm_frame_grid_sizes
seq_f = sample_idx[0][-1] + 1
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([u for u in x])
batch_size = len(x)
token_grid_sizes = [[
torch.tensor([seq_f, 0, 0]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor(
[seq_f + 1, self.motion_side_len,
self.motion_side_len]).unsqueeze(0).repeat(batch_size, 1),
torch.tensor(
[1 if not self.trainable_token_pos_emb else -1, seq_h,
seq_w]).unsqueeze(0).repeat(batch_size, 1),
] # 第三行代表rope emb的想要覆盖到的范围
]
grid_sizes = grid_sizes + token_grid_sizes
token_unpatch_grid_sizes = torch.stack([
torch.tensor([1, 32, 32], dtype=torch.long)
for b in range(batch_size)
])
token_len = self.token.shape[1]
token = self.token.clone().repeat(x.shape[0], 1, 1).contiguous()
seq_lens = seq_lens + torch.tensor([t.size(0) for t in token],
dtype=torch.long)
x = torch.cat([x, token], dim=1)
# arguments
kwargs = dict(
seq_lens=seq_lens,
grid_sizes=grid_sizes,
freqs=freqs,
)
# grad ckpt args
def create_custom_forward(module, return_dict=None):
def custom_forward(*inputs, **kwargs):
if return_dict is not None:
return module(*inputs, **kwargs, return_dict=return_dict)
else:
return module(*inputs, **kwargs)
return custom_forward
ckpt_kwargs: Dict[str, Any] = ({
"use_reentrant": False
} if is_torch_version(">=", "1.11.0") else {})
for idx, block in enumerate(self.blocks):
if self.training and self.gradient_checkpointing:
x = torch.utils.checkpoint.checkpoint(
create_custom_forward(block),
x,
**kwargs,
**ckpt_kwargs,
)
else:
x = block(x, **kwargs)
# head
out = x[:, -token_len:]
return out
def unpatchify(self, x, grid_sizes):
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):
# 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))
class FramePackMotioner(nn.Module):
def __init__(
self,
inner_dim=1024,
num_heads=16, # Used to indicate the number of heads in the backbone network; unrelated to this module's design
zip_frame_buckets=[
1, 2, 16
], # Three numbers representing the number of frames sampled for patch operations from the nearest to the farthest frames
drop_mode="drop", # If not "drop", it will use "padd", meaning padding instead of deletion
*args,
**kwargs):
super().__init__(*args, **kwargs)
self.proj = nn.Conv3d(
16, inner_dim, kernel_size=(1, 2, 2), stride=(1, 2, 2))
self.proj_2x = nn.Conv3d(
16, inner_dim, kernel_size=(2, 4, 4), stride=(2, 4, 4))
self.proj_4x = nn.Conv3d(
16, inner_dim, kernel_size=(4, 8, 8), stride=(4, 8, 8))
self.zip_frame_buckets = torch.tensor(
zip_frame_buckets, dtype=torch.long)
self.inner_dim = inner_dim
self.num_heads = num_heads
assert (inner_dim %
num_heads) == 0 and (inner_dim // num_heads) % 2 == 0
d = inner_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)
self.drop_mode = drop_mode
def forward(self, motion_latents, add_last_motion=2):
motion_frames = motion_latents[0].shape[1]
mot = []
mot_remb = []
for m in motion_latents:
lat_height, lat_width = m.shape[2], m.shape[3]
padd_lat = torch.zeros(16, self.zip_frame_buckets.sum(), lat_height,
lat_width).to(
device=m.device, dtype=m.dtype)
overlap_frame = min(padd_lat.shape[1], m.shape[1])
if overlap_frame > 0:
padd_lat[:, -overlap_frame:] = m[:, -overlap_frame:]
if add_last_motion < 2 and self.drop_mode != "drop":
zero_end_frame = self.zip_frame_buckets[:self.zip_frame_buckets.
__len__() -
add_last_motion -
1].sum()
padd_lat[:, -zero_end_frame:] = 0
padd_lat = padd_lat.unsqueeze(0)
clean_latents_4x, clean_latents_2x, clean_latents_post = padd_lat[:, :, -self.zip_frame_buckets.sum(
):, :, :].split(
list(self.zip_frame_buckets)[::-1], dim=2) # 16, 2 ,1
# patchfy
clean_latents_post = self.proj(clean_latents_post).flatten(
2).transpose(1, 2)
clean_latents_2x = self.proj_2x(clean_latents_2x).flatten(
2).transpose(1, 2)
clean_latents_4x = self.proj_4x(clean_latents_4x).flatten(
2).transpose(1, 2)
if add_last_motion < 2 and self.drop_mode == "drop":
clean_latents_post = clean_latents_post[:, :
0] if add_last_motion < 2 else clean_latents_post
clean_latents_2x = clean_latents_2x[:, :
0] if add_last_motion < 1 else clean_latents_2x
motion_lat = torch.cat(
[clean_latents_post, clean_latents_2x, clean_latents_4x], dim=1)
# rope
start_time_id = -(self.zip_frame_buckets[:1].sum())
end_time_id = start_time_id + self.zip_frame_buckets[0]
grid_sizes = [] if add_last_motion < 2 and self.drop_mode == "drop" else \
[
[torch.tensor([start_time_id, 0, 0]).unsqueeze(0).repeat(1, 1),
torch.tensor([end_time_id, lat_height // 2, lat_width // 2]).unsqueeze(0).repeat(1, 1),
torch.tensor([self.zip_frame_buckets[0], lat_height // 2, lat_width // 2]).unsqueeze(0).repeat(1, 1), ]
]
start_time_id = -(self.zip_frame_buckets[:2].sum())
end_time_id = start_time_id + self.zip_frame_buckets[1] // 2
grid_sizes_2x = [] if add_last_motion < 1 and self.drop_mode == "drop" else \
[
[torch.tensor([start_time_id, 0, 0]).unsqueeze(0).repeat(1, 1),
torch.tensor([end_time_id, lat_height // 4, lat_width // 4]).unsqueeze(0).repeat(1, 1),
torch.tensor([self.zip_frame_buckets[1], lat_height // 2, lat_width // 2]).unsqueeze(0).repeat(1, 1), ]
]
start_time_id = -(self.zip_frame_buckets[:3].sum())
end_time_id = start_time_id + self.zip_frame_buckets[2] // 4
grid_sizes_4x = [[
torch.tensor([start_time_id, 0, 0]).unsqueeze(0).repeat(1, 1),
torch.tensor([end_time_id, lat_height // 8,
lat_width // 8]).unsqueeze(0).repeat(1, 1),
torch.tensor([
self.zip_frame_buckets[2], lat_height // 2, lat_width // 2
]).unsqueeze(0).repeat(1, 1),
]]
grid_sizes = grid_sizes + grid_sizes_2x + grid_sizes_4x
motion_rope_emb = rope_precompute(
motion_lat.detach().view(1, motion_lat.shape[1], self.num_heads,
self.inner_dim // self.num_heads),
grid_sizes,
self.freqs,
start=None)
mot.append(motion_lat)
mot_remb.append(motion_rope_emb)
return mot, mot_remb
def sample_indices(N, stride, expand_ratio, c):
indices = []
current_start = 0
while current_start < N:
bucket_width = int(stride * (expand_ratio**(len(indices) / stride)))
interval = int(bucket_width / stride * c)
current_end = min(N, current_start + bucket_width)
bucket_samples = []
for i in range(current_end - 1, current_start - 1, -interval):
for near in range(c):
bucket_samples.append(i - near)
indices += bucket_samples[::-1]
current_start += bucket_width
return indices
if __name__ == '__main__':
device = "cuda"
model = FramePackMotioner(inner_dim=1024)
batch_size = 2
num_frame, height, width = (28, 32, 32)
single_input = torch.ones([16, num_frame, height, width], device=device)
for i in range(num_frame):
single_input[:, num_frame - 1 - i] *= i
x = [single_input] * batch_size
model.forward(x)
+70
View File
@@ -0,0 +1,70 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import numpy as np
import torch
def rope_precompute(x, grid_sizes, freqs, start=None):
b, s, n, c = x.size(0), x.size(1), x.size(2), x.size(3) // 2
# split freqs
if type(freqs) is list:
trainable_freqs = freqs[1]
freqs = freqs[0]
freqs = freqs.split([c - 2 * (c // 3), c // 3, c // 3], dim=1)
# loop over samples
output = torch.view_as_complex(x.detach().reshape(b, s, n, -1,
2).to(torch.float64))
seq_bucket = [0]
if not type(grid_sizes) is list:
grid_sizes = [grid_sizes]
for g in grid_sizes:
if not type(g) is list:
g = [torch.zeros_like(g), g]
batch_size = g[0].shape[0]
for i in range(batch_size):
if start is None:
f_o, h_o, w_o = g[0][i]
else:
f_o, h_o, w_o = start[i]
f, h, w = g[1][i]
t_f, t_h, t_w = g[2][i]
seq_f, seq_h, seq_w = f - f_o, h - h_o, w - w_o
seq_len = int(seq_f * seq_h * seq_w)
if seq_len > 0:
if t_f > 0:
factor_f, factor_h, factor_w = (t_f / seq_f).item(), (
t_h / seq_h).item(), (t_w / seq_w).item()
# Generate a list of seq_f integers starting from f_o and ending at math.ceil(factor_f * seq_f.item() + f_o.item())
if f_o >= 0:
f_sam = np.linspace(f_o.item(), (t_f + f_o).item() - 1,
seq_f).astype(int).tolist()
else:
f_sam = np.linspace(-f_o.item(),
(-t_f - f_o).item() + 1,
seq_f).astype(int).tolist()
h_sam = np.linspace(h_o.item(), (t_h + h_o).item() - 1,
seq_h).astype(int).tolist()
w_sam = np.linspace(w_o.item(), (t_w + w_o).item() - 1,
seq_w).astype(int).tolist()
assert f_o * f >= 0 and h_o * h >= 0 and w_o * w >= 0
freqs_0 = freqs[0][f_sam] if f_o >= 0 else freqs[0][
f_sam].conj()
freqs_0 = freqs_0.view(seq_f, 1, 1, -1)
freqs_i = torch.cat([
freqs_0.expand(seq_f, seq_h, seq_w, -1),
freqs[1][h_sam].view(1, seq_h, 1, -1).expand(
seq_f, seq_h, seq_w, -1),
freqs[2][w_sam].view(1, 1, seq_w, -1).expand(
seq_f, seq_h, seq_w, -1),
],
dim=-1).reshape(seq_len, 1, -1)
elif t_f < 0:
freqs_i = trainable_freqs.unsqueeze(1)
# apply rotary embedding
output[i, seq_bucket[-1]:seq_bucket[-1] + seq_len] = freqs_i
seq_bucket.append(seq_bucket[-1] + seq_len)
return output
+673
View File
@@ -0,0 +1,673 @@
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import gc
import logging
import math
import os
import random
import sys
import types
from contextlib import contextmanager
from copy import deepcopy
from functools import partial
import numpy as np
import torch
import torch.cuda.amp as amp
import torch.distributed as dist
import torchvision.transforms.functional as TF
from decord import VideoReader
from PIL import Image
from safetensors import safe_open
from torchvision import transforms
from tqdm import tqdm
from .distributed.fsdp import shard_model
from .distributed.sequence_parallel import sp_attn_forward, sp_dit_forward
from .distributed.util import get_world_size
from .modules.s2v.audio_encoder import AudioEncoder
from .modules.s2v.model_s2v import WanModel_S2V, sp_attn_forward_s2v
from .modules.t5 import T5EncoderModel
from .modules.vae2_1 import Wan2_1_VAE
from .utils.fm_solvers import (
FlowDPMSolverMultistepScheduler,
get_sampling_sigmas,
retrieve_timesteps,
)
from .utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
def load_safetensors(path):
tensors = {}
with safe_open(path, framework="pt", device="cpu") as f:
for key in f.keys():
tensors[key] = f.get_tensor(key)
return tensors
class WanS2V:
def __init__(
self,
config,
checkpoint_dir,
device_id=0,
rank=0,
t5_fsdp=False,
dit_fsdp=False,
use_sp=False,
t5_cpu=False,
init_on_cpu=True,
convert_model_dtype=False,
):
r"""
Initializes the image-to-video generation model components.
Args:
config (EasyDict):
Object containing model parameters initialized from config.py
checkpoint_dir (`str`):
Path to directory containing model checkpoints
device_id (`int`, *optional*, defaults to 0):
Id of target GPU device
rank (`int`, *optional*, defaults to 0):
Process rank for distributed training
t5_fsdp (`bool`, *optional*, defaults to False):
Enable FSDP sharding for T5 model
dit_fsdp (`bool`, *optional*, defaults to False):
Enable FSDP sharding for DiT model
use_sp (`bool`, *optional*, defaults to False):
Enable distribution strategy of sequence parallel.
t5_cpu (`bool`, *optional*, defaults to False):
Whether to place T5 model on CPU. Only works without t5_fsdp.
init_on_cpu (`bool`, *optional*, defaults to True):
Enable initializing Transformer Model on CPU. Only works without FSDP or USP.
convert_model_dtype (`bool`, *optional*, defaults to False):
Convert DiT model parameters dtype to 'config.param_dtype'.
Only works without FSDP.
"""
self.device = torch.device(f"cuda:{device_id}")
self.config = config
self.rank = rank
self.t5_cpu = t5_cpu
self.init_on_cpu = init_on_cpu
self.num_train_timesteps = config.num_train_timesteps
self.param_dtype = config.param_dtype
if t5_fsdp or dit_fsdp or use_sp:
self.init_on_cpu = False
shard_fn = partial(shard_model, device_id=device_id)
self.text_encoder = T5EncoderModel(
text_len=config.text_len,
dtype=config.t5_dtype,
device=torch.device('cpu'),
checkpoint_path=os.path.join(checkpoint_dir, config.t5_checkpoint),
tokenizer_path=os.path.join(checkpoint_dir, config.t5_tokenizer),
shard_fn=shard_fn if t5_fsdp else None,
)
self.vae = Wan2_1_VAE(
vae_pth=os.path.join(checkpoint_dir, config.vae_checkpoint),
device=self.device)
logging.info(f"Creating WanModel from {checkpoint_dir}")
if not dit_fsdp:
self.noise_model = WanModel_S2V.from_pretrained(
checkpoint_dir,
torch_dtype=self.param_dtype,
device_map=self.device)
else:
self.noise_model = WanModel_S2V.from_pretrained(
checkpoint_dir, torch_dtype=self.param_dtype)
self.noise_model = self._configure_model(
model=self.noise_model,
use_sp=use_sp,
dit_fsdp=dit_fsdp,
shard_fn=shard_fn,
convert_model_dtype=convert_model_dtype)
self.audio_encoder = AudioEncoder(
model_id=os.path.join(checkpoint_dir,
"wav2vec2-large-xlsr-53-english"))
if use_sp:
self.sp_size = get_world_size()
else:
self.sp_size = 1
self.sample_neg_prompt = config.sample_neg_prompt
self.motion_frames = config.transformer.motion_frames
self.drop_first_motion = config.drop_first_motion
self.fps = config.sample_fps
self.audio_sample_m = 0
def _configure_model(self, model, use_sp, dit_fsdp, shard_fn,
convert_model_dtype):
"""
Configures a model object. This includes setting evaluation modes,
applying distributed parallel strategy, and handling device placement.
Args:
model (torch.nn.Module):
The model instance to configure.
use_sp (`bool`):
Enable distribution strategy of sequence parallel.
dit_fsdp (`bool`):
Enable FSDP sharding for DiT model.
shard_fn (callable):
The function to apply FSDP sharding.
convert_model_dtype (`bool`):
Convert DiT model parameters dtype to 'config.param_dtype'.
Only works without FSDP.
Returns:
torch.nn.Module:
The configured model.
"""
model.eval().requires_grad_(False)
if use_sp:
for block in model.blocks:
block.self_attn.forward = types.MethodType(
sp_attn_forward_s2v, block.self_attn)
model.use_context_parallel = True
if dist.is_initialized():
dist.barrier()
if dit_fsdp:
model = shard_fn(model)
else:
if convert_model_dtype:
model.to(self.param_dtype)
if not self.init_on_cpu:
model.to(self.device)
return model
def get_size_less_than_area(self,
height,
width,
target_area=1024 * 704,
divisor=64):
if height * width <= target_area:
# If the original image area is already less than or equal to the target,
# no resizing is needed—just padding. Still need to ensure that the padded area doesn't exceed the target.
max_upper_area = target_area
min_scale = 0.1
max_scale = 1.0
else:
# Resize to fit within the target area and then pad to multiples of `divisor`
max_upper_area = target_area # Maximum allowed total pixel count after padding
d = divisor - 1
b = d * (height + width)
a = height * width
c = d**2 - max_upper_area
# Calculate scale boundaries using quadratic equation
min_scale = (-b + math.sqrt(b**2 - 2 * a * c)) / (
2 * a) # Scale when maximum padding is applied
max_scale = math.sqrt(max_upper_area /
(height * width)) # Scale without any padding
# We want to choose the largest possible scale such that the final padded area does not exceed max_upper_area
# Use binary search-like iteration to find this scale
find_it = False
for i in range(100):
scale = max_scale - (max_scale - min_scale) * i / 100
new_height, new_width = int(height * scale), int(width * scale)
# Pad to make dimensions divisible by 64
pad_height = (64 - new_height % 64) % 64
pad_width = (64 - new_width % 64) % 64
pad_top = pad_height // 2
pad_bottom = pad_height - pad_top
pad_left = pad_width // 2
pad_right = pad_width - pad_left
padded_height, padded_width = new_height + pad_height, new_width + pad_width
if padded_height * padded_width <= max_upper_area:
find_it = True
break
if find_it:
return padded_height, padded_width
else:
# Fallback: calculate target dimensions based on aspect ratio and divisor alignment
aspect_ratio = width / height
target_width = int(
(target_area * aspect_ratio)**0.5 // divisor * divisor)
target_height = int(
(target_area / aspect_ratio)**0.5 // divisor * divisor)
# Ensure the result is not larger than the original resolution
if target_width >= width or target_height >= height:
target_width = int(width // divisor * divisor)
target_height = int(height // divisor * divisor)
return target_height, target_width
def prepare_default_cond_input(self,
map_shape=[3, 12, 64, 64],
motion_frames=5,
lat_motion_frames=2,
enable_mano=False,
enable_kp=False,
enable_pose=False):
default_value = [1.0, -1.0, -1.0]
cond_enable = [enable_mano, enable_kp, enable_pose]
cond = []
for d, c in zip(default_value, cond_enable):
if c:
map_value = torch.ones(
map_shape, dtype=self.param_dtype, device=self.device) * d
cond_lat = torch.cat([
map_value[:, :, 0:1].repeat(1, 1, motion_frames, 1, 1),
map_value
],
dim=2)
cond_lat = torch.stack(
self.vae.encode(cond_lat.to(
self.param_dtype)))[:, :, lat_motion_frames:].to(
self.param_dtype)
cond.append(cond_lat)
if len(cond) >= 1:
cond = torch.cat(cond, dim=1)
else:
cond = None
return cond
def encode_audio(self, audio_path, infer_frames):
z = self.audio_encoder.extract_audio_feat(
audio_path, return_all_layers=True)
audio_embed_bucket, num_repeat = self.audio_encoder.get_audio_embed_bucket_fps(
z, fps=self.fps, batch_frames=infer_frames, m=self.audio_sample_m)
audio_embed_bucket = audio_embed_bucket.to(self.device,
self.param_dtype)
audio_embed_bucket = audio_embed_bucket.unsqueeze(0)
if len(audio_embed_bucket.shape) == 3:
audio_embed_bucket = audio_embed_bucket.permute(0, 2, 1)
elif len(audio_embed_bucket.shape) == 4:
audio_embed_bucket = audio_embed_bucket.permute(0, 2, 3, 1)
return audio_embed_bucket, num_repeat
def read_last_n_frames(self,
video_path,
n_frames,
target_fps=16,
reverse=False):
"""
Read the last `n_frames` from a video at the specified frame rate.
Parameters:
video_path (str): Path to the video file.
n_frames (int): Number of frames to read.
target_fps (int, optional): Target sampling frame rate. Defaults to 16.
reverse (bool, optional): Whether to read frames in reverse order.
If True, reads the first `n_frames` instead of the last ones.
Returns:
np.ndarray: A NumPy array of shape [n_frames, H, W, 3], representing the sampled video frames.
"""
vr = VideoReader(video_path)
original_fps = vr.get_avg_fps()
total_frames = len(vr)
interval = max(1, round(original_fps / target_fps))
required_span = (n_frames - 1) * interval
start_frame = max(0, total_frames - required_span -
1) if not reverse else 0
sampled_indices = []
for i in range(n_frames):
indice = start_frame + i * interval
if indice >= total_frames:
break
else:
sampled_indices.append(indice)
return vr.get_batch(sampled_indices).asnumpy()
def load_pose_cond(self, pose_video, num_repeat, infer_frames, size):
HEIGHT, WIDTH = size
if not pose_video is None:
pose_seq = self.read_last_n_frames(
pose_video,
n_frames=infer_frames * num_repeat,
target_fps=self.fps,
reverse=True)
resize_opreat = transforms.Resize(min(HEIGHT, WIDTH))
crop_opreat = transforms.CenterCrop((HEIGHT, WIDTH))
tensor_trans = transforms.ToTensor()
cond_tensor = torch.from_numpy(pose_seq)
cond_tensor = cond_tensor.permute(0, 3, 1, 2) / 255.0 * 2 - 1.0
cond_tensor = crop_opreat(resize_opreat(cond_tensor)).permute(
1, 0, 2, 3).unsqueeze(0)
padding_frame_num = num_repeat * infer_frames - cond_tensor.shape[2]
cond_tensor = torch.cat([
cond_tensor,
- torch.ones([1, 3, padding_frame_num, HEIGHT, WIDTH])
],
dim=2)
cond_tensors = torch.chunk(cond_tensor, num_repeat, dim=2)
else:
cond_tensors = [-torch.ones([1, 3, infer_frames, HEIGHT, WIDTH])]
COND = []
for r in range(len(cond_tensors)):
cond = cond_tensors[r]
cond = torch.cat([cond[:, :, 0:1].repeat(1, 1, 1, 1, 1), cond],
dim=2)
cond_lat = torch.stack(
self.vae.encode(
cond.to(dtype=self.param_dtype,
device=self.device)))[:, :,
1:].cpu() # for mem save
COND.append(cond_lat)
return COND
def get_gen_size(self, size, max_area, ref_image_path, pre_video_path):
if not size is None:
HEIGHT, WIDTH = size
else:
if pre_video_path:
ref_image = self.read_last_n_frames(
pre_video_path, n_frames=1)[0]
else:
ref_image = np.array(Image.open(ref_image_path).convert('RGB'))
HEIGHT, WIDTH = ref_image.shape[:2]
HEIGHT, WIDTH = self.get_size_less_than_area(
HEIGHT, WIDTH, target_area=max_area)
return (HEIGHT, WIDTH)
def generate(
self,
input_prompt,
ref_image_path,
audio_path,
num_repeat=1,
pose_video=None,
max_area=720 * 1280,
infer_frames=80,
shift=5.0,
sample_solver='unipc',
sampling_steps=40,
guide_scale=5.0,
n_prompt="",
seed=-1,
offload_model=True,
init_first_frame=False,
):
r"""
Generates video frames from input image and text prompt using diffusion process.
Args:
input_prompt (`str`):
Text prompt for content generation.
ref_image_path ('str'):
Input image path
audio_path ('str'):
Audio for video driven
num_repeat ('int'):
Number of clips to generate; will be automatically adjusted based on the audio length
pose_video ('str'):
If provided, uses a sequence of poses to drive the generated video
max_area (`int`, *optional*, defaults to 720*1280):
Maximum pixel area for latent space calculation. Controls video resolution scaling
infer_frames (`int`, *optional*, defaults to 80):
How many frames to generate per clips. The number should be 4n
shift (`float`, *optional*, defaults to 5.0):
Noise schedule shift parameter. Affects temporal dynamics
[NOTE]: If you want to generate a 480p video, it is recommended to set the shift value to 3.0.
sample_solver (`str`, *optional*, defaults to 'unipc'):
Solver used to sample the video.
sampling_steps (`int`, *optional*, defaults to 40):
Number of diffusion sampling steps. Higher values improve quality but slow generation
guide_scale (`float` or tuple[`float`], *optional*, defaults 5.0):
Classifier-free guidance scale. Controls prompt adherence vs. creativity.
If tuple, the first guide_scale will be used for low noise model and
the second guide_scale will be used for high noise model.
n_prompt (`str`, *optional*, defaults to ""):
Negative prompt for content exclusion. If not given, use `config.sample_neg_prompt`
seed (`int`, *optional*, defaults to -1):
Random seed for noise generation. If -1, use random seed
offload_model (`bool`, *optional*, defaults to True):
If True, offloads models to CPU during generation to save VRAM
init_first_frame (`bool`, *optional*, defaults to False):
Whether to use the reference image as the first frame (i.e., standard image-to-video generation)
Returns:
torch.Tensor:
Generated video frames tensor. Dimensions: (C, N H, W) where:
- C: Color channels (3 for RGB)
- N: Number of frames (81)
- H: Frame height (from max_area)
- W: Frame width from max_area)
"""
# preprocess
size = self.get_gen_size(
size=None,
max_area=max_area,
ref_image_path=ref_image_path,
pre_video_path=None)
HEIGHT, WIDTH = size
channel = 3
resize_opreat = transforms.Resize(min(HEIGHT, WIDTH))
crop_opreat = transforms.CenterCrop((HEIGHT, WIDTH))
tensor_trans = transforms.ToTensor()
ref_image = None
motion_latents = None
if ref_image is None:
ref_image = np.array(Image.open(ref_image_path).convert('RGB'))
if motion_latents is None:
motion_latents = torch.zeros(
[1, channel, self.motion_frames, HEIGHT, WIDTH],
dtype=self.param_dtype,
device=self.device)
# extract audio emb
audio_emb, nr = self.encode_audio(audio_path, infer_frames=infer_frames)
if num_repeat is None or num_repeat > nr:
num_repeat = nr
lat_motion_frames = (self.motion_frames + 3) // 4
model_pic = crop_opreat(resize_opreat(Image.fromarray(ref_image)))
ref_pixel_values = tensor_trans(model_pic)
ref_pixel_values = ref_pixel_values.unsqueeze(1).unsqueeze(
0) * 2 - 1.0 # b c 1 h w
ref_pixel_values = ref_pixel_values.to(
dtype=self.vae.dtype, device=self.vae.device)
ref_latents = torch.stack(self.vae.encode(ref_pixel_values))
# encode the motion latents
videos_last_frames = motion_latents.detach()
drop_first_motion = self.drop_first_motion
if init_first_frame:
drop_first_motion = False
motion_latents[:, :, -6:] = ref_pixel_values
motion_latents = torch.stack(self.vae.encode(motion_latents))
# get pose cond input if need
COND = self.load_pose_cond(
pose_video=pose_video,
num_repeat=num_repeat,
infer_frames=infer_frames,
size=size)
seed = seed if seed >= 0 else random.randint(0, sys.maxsize)
if n_prompt == "":
n_prompt = self.sample_neg_prompt
# preprocess
if not self.t5_cpu:
self.text_encoder.model.to(self.device)
context = self.text_encoder([input_prompt], self.device)
context_null = self.text_encoder([n_prompt], self.device)
if offload_model:
self.text_encoder.model.cpu()
else:
context = self.text_encoder([input_prompt], torch.device('cpu'))
context_null = self.text_encoder([n_prompt], torch.device('cpu'))
context = [t.to(self.device) for t in context]
context_null = [t.to(self.device) for t in context_null]
out = []
# evaluation mode
with (
torch.amp.autocast('cuda', dtype=self.param_dtype),
torch.no_grad(),
):
for r in range(num_repeat):
seed_g = torch.Generator(device=self.device)
seed_g.manual_seed(seed + r)
lat_target_frames = (infer_frames + 3 + self.motion_frames
) // 4 - lat_motion_frames
target_shape = [lat_target_frames, HEIGHT // 8, WIDTH // 8]
noise = [
torch.randn(
16,
target_shape[0],
target_shape[1],
target_shape[2],
dtype=self.param_dtype,
device=self.device,
generator=seed_g)
]
max_seq_len = np.prod(target_shape) // 4
if sample_solver == 'unipc':
sample_scheduler = FlowUniPCMultistepScheduler(
num_train_timesteps=self.num_train_timesteps,
shift=1,
use_dynamic_shifting=False)
sample_scheduler.set_timesteps(
sampling_steps, device=self.device, shift=shift)
timesteps = sample_scheduler.timesteps
elif sample_solver == 'dpm++':
sample_scheduler = FlowDPMSolverMultistepScheduler(
num_train_timesteps=self.num_train_timesteps,
shift=1,
use_dynamic_shifting=False)
sampling_sigmas = get_sampling_sigmas(sampling_steps, shift)
timesteps, _ = retrieve_timesteps(
sample_scheduler,
device=self.device,
sigmas=sampling_sigmas)
else:
raise NotImplementedError("Unsupported solver.")
latents = deepcopy(noise)
with torch.no_grad():
left_idx = r * infer_frames
right_idx = r * infer_frames + infer_frames
cond_latents = COND[r] if pose_video else COND[0] * 0
cond_latents = cond_latents.to(
dtype=self.param_dtype, device=self.device)
audio_input = audio_emb[..., left_idx:right_idx]
input_motion_latents = motion_latents.clone()
arg_c = {
'context': context[0:1],
'seq_len': max_seq_len,
'cond_states': cond_latents,
"motion_latents": input_motion_latents,
'ref_latents': ref_latents,
"audio_input": audio_input,
"motion_frames": [self.motion_frames, lat_motion_frames],
"drop_motion_frames": drop_first_motion and r == 0,
}
if guide_scale > 1:
arg_null = {
'context': context_null[0:1],
'seq_len': max_seq_len,
'cond_states': cond_latents,
"motion_latents": input_motion_latents,
'ref_latents': ref_latents,
"audio_input": 0.0 * audio_input,
"motion_frames": [
self.motion_frames, lat_motion_frames
],
"drop_motion_frames": drop_first_motion and r == 0,
}
if offload_model or self.init_on_cpu:
self.noise_model.to(self.device)
torch.cuda.empty_cache()
for i, t in enumerate(tqdm(timesteps)):
latent_model_input = latents[0:1]
timestep = [t]
timestep = torch.stack(timestep).to(self.device)
noise_pred_cond = self.noise_model(
latent_model_input, t=timestep, **arg_c)
if guide_scale > 1:
noise_pred_uncond = self.noise_model(
latent_model_input, t=timestep, **arg_null)
noise_pred = [
u + guide_scale * (c - u)
for c, u in zip(noise_pred_cond, noise_pred_uncond)
]
else:
noise_pred = noise_pred_cond
temp_x0 = sample_scheduler.step(
noise_pred[0].unsqueeze(0),
t,
latents[0].unsqueeze(0),
return_dict=False,
generator=seed_g)[0]
latents[0] = temp_x0.squeeze(0)
if offload_model:
self.noise_model.cpu()
torch.cuda.synchronize()
torch.cuda.empty_cache()
latents = torch.stack(latents)
if not (drop_first_motion and r == 0):
decode_latents = torch.cat([motion_latents, latents], dim=2)
else:
decode_latents = torch.cat([ref_latents, latents], dim=2)
image = torch.stack(self.vae.decode(decode_latents))
image = image[:, :, -(infer_frames):]
if (drop_first_motion and r == 0):
image = image[:, :, 3:]
overlap_frames_num = min(self.motion_frames, image.shape[2])
videos_last_frames = torch.cat([
videos_last_frames[:, :, overlap_frames_num:],
image[:, :, -overlap_frames_num:]
],
dim=2)
videos_last_frames = videos_last_frames.to(
dtype=motion_latents.dtype, device=motion_latents.device)
motion_latents = torch.stack(
self.vae.encode(videos_last_frames))
out.append(image.cpu())
videos = torch.cat(out, dim=2)
del noise, latents
del sample_scheduler
if offload_model:
gc.collect()
torch.cuda.synchronize()
if dist.is_initialized():
dist.barrier()
return videos[0] if self.rank == 0 else None
+66
View File
@@ -4,6 +4,8 @@ import binascii
import logging
import os
import os.path as osp
import shutil
import subprocess
import imageio
import torch
@@ -21,6 +23,70 @@ def rand_name(length=8, suffix=''):
return name
def merge_video_audio(video_path: str, audio_path: str):
"""
Merge the video and audio into a new video, with the duration set to the shorter of the two,
and overwrite the original video file.
Parameters:
video_path (str): Path to the original video file
audio_path (str): Path to the audio file
"""
# set logging
logging.basicConfig(level=logging.INFO)
# check
if not os.path.exists(video_path):
raise FileNotFoundError(f"video file {video_path} does not exist")
if not os.path.exists(audio_path):
raise FileNotFoundError(f"audio file {audio_path} does not exist")
base, ext = os.path.splitext(video_path)
temp_output = f"{base}_temp{ext}"
try:
# create ffmpeg command
command = [
'ffmpeg',
'-y', # overwrite
'-i',
video_path,
'-i',
audio_path,
'-c:v',
'copy', # copy video stream
'-c:a',
'aac', # use AAC audio encoder
'-b:a',
'192k', # set audio bitrate (optional)
'-map',
'0:v:0', # select the first video stream
'-map',
'1:a:0', # select the first audio stream
'-shortest', # choose the shortest duration
temp_output
]
# execute the command
logging.info("Start merging video and audio...")
result = subprocess.run(
command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
# check result
if result.returncode != 0:
error_msg = f"FFmpeg execute failed: {result.stderr}"
logging.error(error_msg)
raise RuntimeError(error_msg)
shutil.move(temp_output, video_path)
logging.info(f"Merge completed, saved to {video_path}")
except Exception as e:
if os.path.exists(temp_output):
os.remove(temp_output)
logging.error(f"merge_video_audio failed with error: {e}")
def save_video(tensor,
save_file=None,
fps=30,