vla-sft-code-motus / models /wan_model.py
poet70's picture
Upload folder using huggingface_hub
3903652 verified
Raw
History Blame Contribute Delete
13.4 kB
# WAN Video Diffusion Model
# Provides VAE encoding/decoding and feature extraction for diffusion-pipe I2V training
import torch
import torch.nn as nn
from typing import List, Optional, Dict, Any
import logging
import sys
import os
import json
from pathlib import Path
from wan.modules.model import WanModel, sinusoidal_embedding_1d
from wan.modules.vae2_2 import Wan2_2_VAE
# Optional safetensors support
try:
from safetensors.torch import load_file as safe_load_file # type: ignore
except Exception: # pragma: no cover
safe_load_file = None
logger = logging.getLogger(__name__)
def _strip_known_prefixes_for_wan(sd: Dict[str, torch.Tensor], target_model: nn.Module) -> Dict[str, torch.Tensor]:
"""Strip only the 'dit.' prefix from checkpoint keys if present."""
if not isinstance(sd, dict):
return sd
if not any(k.startswith('dit.') for k in sd.keys()):
return sd
mapped = { (k[4:] if k.startswith('dit.') else k): v for k, v in sd.items() }
logger.info("Stripped 'dit.' prefix from checkpoint keys")
return mapped
class WanVideoModel(nn.Module):
"""
WAN Video Diffusion Model wrapper for TI2V Teacher Forcing training.
Provides VAE encoding/decoding and feature extraction for joint video-action training.
Uses Teacher Forcing approach for I2V conditioning (DiffSynth-Studio style).
"""
def __init__(
self,
model_config: Dict[str, Any],
vae_path: str,
device: str = "cuda",
precision: str = "bfloat16"
):
super().__init__()
self.device = torch.device(device)
self.precision = {
"float32": torch.float32,
"float16": torch.float16,
"bfloat16": torch.bfloat16,
}[precision]
# Initialize WAN model
self.wan_model = WanModel(**model_config)
self.wan_model.to(device=self.device, dtype=self.precision)
# Initialize VAE
self.vae = Wan2_2_VAE(vae_pth=vae_path, device=self.device)
logger.info(f"WAN Video Model initialized with {sum(p.numel() for p in self.wan_model.parameters()):,} parameters")
def encode_video(self, video_pixels: torch.Tensor) -> torch.Tensor:
"""
Encode video pixels to latent space.
Args:
video_pixels: Video in pixel space [B, C, T, H, W], range [-1, 1]
Returns:
Video latents [B, C', T', H', W']
"""
with torch.no_grad():
return self.vae.encode(video_pixels)
def decode_video(self, video_latents: torch.Tensor) -> torch.Tensor:
"""
Decode video latents to pixel space.
Args:
video_latents: Video latents [B, C, T, H, W]
Returns:
Video pixels [B, C', T', H', W'], range [-1, 1]
"""
with torch.no_grad():
video_pixels = []
for i in range(video_latents.shape[0]):
pixels = self.vae.decode([video_latents[i]])[0]
video_pixels.append(pixels)
result = torch.stack(video_pixels, dim=0)
return result
def get_layer_features(
self,
video_latent: torch.Tensor,
timestep: torch.Tensor,
text_embeddings: List[torch.Tensor],
layer_indices: Optional[List[int]] = None
) -> List[torch.Tensor]:
"""
Extract intermediate layer features for cross-attention injection.
Args:
video_latent: Video latent tensors [B, C, T, H, W]
timestep: Diffusion timesteps [B]
text_embeddings: List of text embeddings
layer_indices: Which layers to extract (None = all layers)
Returns:
List of feature tensors from specified layers
"""
if layer_indices is None:
layer_indices = list(range(len(self.wan_model.blocks)))
# Expect 5D batch input: [B, C, f, h, w] - standard WAN input (48 channels)
if video_latent.ndim != 5:
raise ValueError(f"Expected 5D tensor [B, C, f, h, w], got {video_latent.ndim}D with shape {video_latent.shape}")
# Ensure input has correct channel count for WAN 2.2 (48 channels)
expected_channels = 48
if video_latent.shape[1] != expected_channels:
raise ValueError(f"Expected {expected_channels} channels for WAN 2.2, got {video_latent.shape[1]} channels")
# Convert to WAN format (list of tensors)
video_list = [video_latent[i] for i in range(video_latent.shape[0])]
seq_len = video_latent.shape[2] * video_latent.shape[3] * video_latent.shape[4] // 4
# Prepare inputs similar to WAN forward
device = self.wan_model.patch_embedding.weight.device
if self.wan_model.freqs.device != device:
self.wan_model.freqs = self.wan_model.freqs.to(device)
# Embeddings
x = [self.wan_model.patch_embedding(u.unsqueeze(0)) for u in video_list]
grid_sizes = torch.stack([torch.tensor(u.shape[2:], dtype=torch.long, device=device) 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, device=device)
x = torch.cat([
torch.cat([u, u.new_zeros(1, seq_len - u.size(1), u.size(2))], dim=1)
for u in x
])
# Time embeddings - handle batch of timesteps [B] -> [B, seq_len]
if timestep.dim() == 1:
timestep = timestep.unsqueeze(1).expand(timestep.size(0), seq_len)
with torch.amp.autocast('cuda', dtype=torch.float32):
bt = timestep.size(0)
t_flat = timestep.flatten()
e = self.wan_model.time_embedding(
sinusoidal_embedding_1d(self.wan_model.freq_dim, t_flat).unflatten(0, (bt, seq_len)).float().to(device)
)
e0 = self.wan_model.time_projection(e).unflatten(2, (6, self.wan_model.dim))
assert e.dtype == torch.float32 and e0.dtype == torch.float32
# Context
context = self.wan_model.text_embedding(
torch.stack([
torch.cat([u, u.new_zeros(self.wan_model.text_len - u.size(0), u.size(1))])
for u in text_embeddings
])
)
# Forward through specified layers
layer_features = []
kwargs = dict(
e=e0,
seq_lens=seq_lens,
grid_sizes=grid_sizes,
freqs=self.wan_model.freqs,
context=context,
context_lens=None
)
for i, block in enumerate(self.wan_model.blocks):
x = block(x, **kwargs)
if i in layer_indices:
layer_features.append(x.clone())
# Apply head and unpatchify to get final output (like forward method)
x = self.wan_model.head(x, e)
x = self.wan_model.unpatchify(x, grid_sizes)
final_output = torch.stack([u.float() for u in x], dim=0)
# Add final output as last element
layer_features.append(final_output)
return layer_features
@classmethod
def from_config(
cls,
config_path: str,
vae_path: str,
device: str = "cuda",
precision: str = "bfloat16"
) -> 'WanVideoModel':
"""
Initialize WAN model architecture and VAE only (no WAN weights).
Useful when model weights will be loaded from a higher-level checkpoint.
"""
# Load WAN model config
config_json_path = os.path.join(config_path, 'config.json')
if not os.path.exists(config_json_path):
raise FileNotFoundError(f"WAN config.json not found at {config_json_path}")
with open(config_json_path, 'r') as f:
model_config = json.load(f)
# Create model without loading WAN weights
model = cls(
model_config=model_config,
vae_path=vae_path,
device=device,
precision=precision
)
logger.info("Initialized WAN model from config only (no WAN weights loaded)")
return model
@classmethod
def from_pretrained(
cls,
checkpoint_path: str,
vae_path: str,
config_path: Optional[str] = None,
device: str = "cuda",
precision: str = "bfloat16"
) -> 'WanVideoModel':
"""
Load pretrained WAN model.
Args:
checkpoint_path: Path to WAN checkpoint (.pt file or directory)
vae_path: Path to VAE checkpoint
config_path: Path to config directory (optional, defaults to checkpoint_path)
device: Device to load model on
precision: Model precision
Returns:
WanVideoModel instance
"""
# Load WAN model config
if config_path is None:
config_path = checkpoint_path
config_json_path = os.path.join(config_path, 'config.json')
if os.path.exists(config_json_path):
with open(config_json_path, 'r') as f:
model_config = json.load(f)
# Create model
model = cls(
model_config=model_config,
vae_path=vae_path,
device=device,
precision=precision
)
# Load WAN weights - support directory and file formats
try:
logger.info(f"Loading WAN weights from {checkpoint_path}")
if checkpoint_path.endswith('.pt'):
# Direct .pt file loading (e.g., from cosmos-predict2 continue training)
logger.info(f"Loading weights from .pt file: {checkpoint_path}")
checkpoint_state_dict = torch.load(checkpoint_path, map_location='cpu')
# Handle different .pt file formats
if isinstance(checkpoint_state_dict, dict) and 'model' in checkpoint_state_dict:
# Standard checkpoint format with 'model' key
wan_state_dict = checkpoint_state_dict['model']
else:
# Direct state dict (OrderedDict)
wan_state_dict = checkpoint_state_dict
# Load weights into WAN model
# Strip known prefixes like 'dit.' if present
try:
wan_state_dict = _strip_known_prefixes_for_wan(wan_state_dict, model.wan_model)
except Exception:
pass
incompatible_keys = model.wan_model.load_state_dict(wan_state_dict, strict=False)
if incompatible_keys.missing_keys:
logger.warning(f"Missing keys: {incompatible_keys.missing_keys}")
if incompatible_keys.unexpected_keys:
logger.warning(f"Unexpected keys: {incompatible_keys.unexpected_keys}")
logger.info(f"Successfully loaded WAN weights from .pt file")
elif checkpoint_path.endswith('.bin') or checkpoint_path.endswith('.safetensors'):
# Single-file HF-style weight
logger.info(f"Loading weights from weight file: {checkpoint_path}")
if checkpoint_path.endswith('.safetensors'):
if safe_load_file is None:
raise RuntimeError("safetensors not available. Please 'pip install safetensors'.")
wan_state_dict = safe_load_file(checkpoint_path, device='cpu')
else:
loaded = torch.load(checkpoint_path, map_location='cpu')
# If the loaded object is a wrapper dict, try common keys
if isinstance(loaded, dict) and ('state_dict' in loaded or 'model' in loaded):
wan_state_dict = loaded.get('state_dict', loaded.get('model'))
else:
wan_state_dict = loaded
# Strip known prefixes like 'dit.' if present
try:
wan_state_dict = _strip_known_prefixes_for_wan(wan_state_dict, model.wan_model)
except Exception:
pass
incompatible_keys = model.wan_model.load_state_dict(wan_state_dict, strict=False)
if incompatible_keys.missing_keys:
logger.warning(f"Missing keys: {incompatible_keys.missing_keys}")
if incompatible_keys.unexpected_keys:
logger.warning(f"Unexpected keys: {incompatible_keys.unexpected_keys}")
logger.info("Successfully loaded WAN weights from single file")
else:
# Directory-based loading (original diffusers format)
loaded_model = WanModel.from_pretrained(checkpoint_path)
model.wan_model.load_state_dict(loaded_model.state_dict(), strict=False)
logger.info(f"Successfully loaded WAN weights from directory")
except Exception as e:
logger.warning(f"Failed to load WAN checkpoint from {checkpoint_path}: {e}")
logger.warning("Using random initialization instead")
return model