Wan2.1-Converter / convert_wan.py
1Root1AI's picture
Update convert_wan.py
e3671a4 verified
Raw
History Blame Contribute Delete
8.68 kB
import torch
import torch.nn as nn
import torch.nn.functional as F
import sys
import os
import gdown
import inspect
import textwrap
import math
# --- 0. FORCE CPU PATCH ---
# Patches T5 to avoid "No NVIDIA driver" error
t5_path = "/app/Wan2.1/wan/modules/t5.py"
if os.path.exists(t5_path):
print("--- PATCHING T5.PY ---")
with open(t5_path, 'r') as f:
content = f.read()
new_content = content.replace("device=torch.cuda.current_device(),", "device='cpu',")
with open(t5_path, 'w') as f:
f.write(new_content)
sys.path.append("/app/Wan2.1")
# --- 1. DOWNLOAD WEIGHTS ---
print("--- DOWNLOADING MODEL ---")
file_id = '1pf-fhkWayK3_sv9bujvT2Od04ki2Wh4y'
url = f'https://drive.google.com/uc?id={file_id}'
ckpt_path = "/app/wan13bNSFWFull_v20I2v.safetensors"
if not os.path.exists(ckpt_path):
gdown.download(url, ckpt_path, quiet=False)
# --- 2. CRITICAL PATCHES ---
# A. RECURSION-PROOF LINEAR FIX
_orig_linear = F.linear # <--- SAVES THE ORIGINAL FUNCTION
def safe_linear(input, weight, bias=None):
# Force mixed-precision to match (FP32 input + FP16 weight = FP16 input)
if input.dtype == torch.float32 and weight.dtype == torch.float16:
input = input.half()
elif input.dtype == torch.float16 and weight.dtype == torch.float32:
weight = weight.half()
if bias is not None: bias = bias.half()
return _orig_linear(input, weight, bias) # <--- CALLS SAVED FUNCTION
# Apply the patch
torch.nn.functional.linear = safe_linear
torch.nn.modules.linear.F.linear = safe_linear
# B. ROPE FIX (CPU Compatible)
def manual_rope_apply_real(x, grid_sizes, freqs):
n, l, h, d = x.shape
if freqs.dim() == 2:
l_freqs, d_half = freqs.shape
if l_freqs != l:
freqs_in = freqs.transpose(0, 1).unsqueeze(0).float()
freqs_out = F.interpolate(freqs_in, size=l, mode='linear', align_corners=False)
freqs = freqs_out.squeeze(0).transpose(0, 1)
cos = torch.cos(freqs)
sin = torch.sin(freqs)
elif freqs.dim() == 3:
l_freqs = freqs.shape[0]
if l_freqs != l:
f_flat = freqs.flatten(1)
f_in = f_flat.transpose(0, 1).unsqueeze(0).float()
f_out = F.interpolate(f_in, size=l, mode='linear', align_corners=False)
freqs = f_out.squeeze(0).transpose(0, 1).view(l, -1, 2)
cos = freqs[..., 0]
sin = freqs[..., 1]
else:
cos = freqs[..., 0]
sin = freqs[..., 1]
x_pairs = x.float().view(n, l, h, d // 2, 2)
r = x_pairs[..., 0]
i = x_pairs[..., 1]
cos = cos.unsqueeze(0).unsqueeze(2)
sin = sin.unsqueeze(0).unsqueeze(2)
out_r = r * cos - i * sin
out_i = r * sin + i * cos
return torch.stack([out_r, out_i], dim=-1).flatten(3).type_as(x)
import wan.modules.model
wan.modules.model.rope_apply = manual_rope_apply_real
# C. FLASH ATTENTION FIX
def manual_attention(q, k, v, dropout_p=0.0, **kwargs):
if q.dtype != torch.float16: q = q.half()
if k.dtype != torch.float16: k = k.half()
if v.dtype != torch.float16: v = v.half()
q = q.transpose(1, 2)
k = k.transpose(1, 2)
v = v.transpose(1, 2)
out = F.scaled_dot_product_attention(q, k, v, dropout_p=dropout_p)
return out.transpose(1, 2)
import wan.modules.attention
wan.modules.attention.flash_attention = manual_attention
wan.modules.attention.FLASH_ATTN_2_AVAILABLE = False
wan.modules.model.flash_attention = manual_attention
# D. LAYERNORM FIX
class ManualWanLayerNorm(nn.LayerNorm):
def forward(self, x):
orig_type = x.dtype
return F.layer_norm(x.float(), self.normalized_shape, self.weight.float(), self.bias.float(), self.eps).to(orig_type)
wan.modules.model.WanLayerNorm = ManualWanLayerNorm
# --- 3. DYNAMIC CLASS PATCHER ---
# Removes assert statements that crash the export
from wan.modules.model import WanModel, sinusoidal_embedding_1d
def patch_all_wan_classes():
print("=== APPLYING DYNAMIC PATCHES ===")
target_module = wan.modules.model
classes = [obj for name, obj in inspect.getmembers(target_module) if inspect.isclass(obj) and obj.__module__ == target_module.__name__]
shared_globals = {
'torch': torch, 'math': math, 'F': F,
'amp': torch.cuda.amp if hasattr(torch.cuda, 'amp') else None,
'sinusoidal_embedding_1d': sinusoidal_embedding_1d,
'rope_apply': manual_rope_apply_real,
'nn': nn, 'flash_attention': manual_attention,
}
for cls in classes:
shared_globals[cls.__name__] = cls
for cls in classes:
if not hasattr(cls, 'forward') or cls.__name__ == "ManualWanLayerNorm": continue
try:
src = inspect.getsource(cls.forward)
except OSError: continue
src = textwrap.dedent(src)
lines = src.splitlines()
new_lines = []
modified = False
for line in lines:
indent = len(line) - len(line.lstrip())
# FIX 1: Remove asserts that check for float32
if "assert" in line and ("float32" in line or "dtype" in line):
new_lines.append(" " * indent + "pass")
modified = True
# FIX 2: Force time embedding to Half
elif cls.__name__ == "WanModel" and "e = self.time_embedding(t, shift)" in line:
new_lines.append(line.replace("e = self.time_embedding(t, shift)", "e = self.time_embedding(t, shift).half()"))
modified = True
# FIX 3: Force QKV to Half
elif "q, k, v = qkv_fn(x)" in line:
new_lines.append(" " * indent + "x = x.half()")
new_lines.append(line)
modified = True
else:
new_lines.append(line)
if modified:
new_src = chr(10).join(new_lines)
exec_scope = shared_globals.copy()
exec_scope[cls.__name__] = cls
try:
exec(new_src, exec_scope)
cls.forward = exec_scope['forward']
print(f" [Patcher] SUCCESS: {cls.__name__} patched.")
except Exception as e:
print(f" [Patcher] FAILED {cls.__name__}: {e}")
# --- 4. EXPORT LOGIC (PART 1) ---
class HeadIdentity(nn.Module):
def forward(self, x, e=None): return x
def convert():
print("--- STARTING CONVERSION: PART 1 ---")
patch_all_wan_classes()
cfg = {
"t5_model": "umt5_xxl", "t5_dtype": torch.float16, "text_len": 512,
"param_dtype": torch.float16, "num_train_timesteps": 1000, "sample_fps": 16,
"sample_neg_prompt": "", "t5_checkpoint": "", "t5_tokenizer": "",
"vae_checkpoint": "", "vae_stride": (4, 8, 8), "patch_size": (1, 2, 2),
"dim": 1536, "num_heads": 12, "num_layers": 30, "window_size": (-1, -1),
"qk_norm": True, "cross_attn_norm": True, "eps": 1e-6, "rope_max_len": 4096,
"base_dim": 256, "model_type": "i2v"
}
with torch.no_grad():
model = WanModel.from_config(cfg)
model.eval().to("cpu")
model.half()
from safetensors.torch import load_file
print(f"Loading weights from {ckpt_path}...")
sd = load_file(ckpt_path)
model.load_state_dict(sd, strict=False)
old = model.patch_embedding
model.patch_embedding = torch.nn.Conv3d(72, old.out_channels, old.kernel_size, old.stride, old.padding, bias=(old.bias is not None)).half()
# EXPORT FIRST 15 BLOCKS
all_blocks_ref = list(model.blocks)
model.blocks = nn.ModuleList()
model.blocks.extend(all_blocks_ref[:15])
model.head = HeadIdentity()
model.unpatchify = lambda x, grid_sizes: x
# Dummy inputs
dtype = torch.float16
args = (
[torch.randn(36, 1, 32, 32, dtype=dtype)],
torch.tensor([10.0], dtype=torch.float32),
torch.randn(1, 512, 4096, dtype=dtype),
torch.tensor([4096]),
torch.randn(1, 257, 1280, dtype=dtype),
[torch.randn(36, 1, 32, 32, dtype=dtype)]
)
output_file = "/app/output/wan13b_part1.onnx"
print(f"Exporting to {output_file}...")
torch.onnx.export(
model, args, output_file,
input_names=['latents', 'timestep', 'context', 'seq_len', 'clip_fea', 'y'],
output_names=['mid_block_states'],
opset_version=14,
do_constant_folding=False,
export_params=True,
keep_initializers_as_inputs=True
)
print("--- SUCCESS ---")
if __name__ == "__main__":
convert()