Files
nvlabs--longlive/utils/lightvae_5b_wrapper.py
2026-07-13 12:31:40 +08:00

424 lines
15 KiB
Python

import logging
import os
from typing import Optional
import torch
import torch.nn as nn
from wan_5b.modules.vae2_2 import (
CausalConv3d,
Decoder3d,
Encoder3d,
count_conv3d,
patchify,
unpatchify,
)
def _extract_checkpoint_state_dict(raw):
state = raw
if isinstance(state, dict) and "state_dict" in state:
state = state["state_dict"]
if isinstance(state, dict) and "gen_model" in state:
state = state["gen_model"]
if isinstance(state, dict) and "generator" in state:
state = state["generator"]
if not isinstance(state, dict):
raise ValueError("Unsupported checkpoint format: expected a dict-like state_dict.")
return state
def _map_lightvae_key_to_wanvae(key):
def _map_resnet_tail(tail):
if tail.startswith("norm1."):
return "residual.0." + tail[len("norm1."):]
if tail.startswith("conv1."):
return "residual.2." + tail[len("conv1."):]
if tail.startswith("norm2."):
return "residual.3." + tail[len("norm2."):]
if tail.startswith("conv2."):
return "residual.6." + tail[len("conv2."):]
if tail.startswith("conv_shortcut."):
return "shortcut." + tail[len("conv_shortcut."):]
return tail
if key.startswith("dynamic_feature_projection_heads."):
return None
if key.startswith("quant_conv."):
return key.replace("quant_conv.", "conv1.", 1)
if key.startswith("post_quant_conv."):
return key.replace("post_quant_conv.", "conv2.", 1)
if key.startswith("encoder.conv_in."):
return key.replace("encoder.conv_in.", "encoder.conv1.", 1)
if key.startswith("encoder.mid_block.resnets.0."):
tail = key[len("encoder.mid_block.resnets.0."):]
return "encoder.middle.0." + _map_resnet_tail(tail)
if key.startswith("encoder.mid_block.attentions.0."):
return key.replace("encoder.mid_block.attentions.0.", "encoder.middle.1.", 1)
if key.startswith("encoder.mid_block.resnets.1."):
tail = key[len("encoder.mid_block.resnets.1."):]
return "encoder.middle.2." + _map_resnet_tail(tail)
if key.startswith("encoder.norm_out."):
return key.replace("encoder.norm_out.", "encoder.head.0.", 1)
if key.startswith("encoder.conv_out."):
return key.replace("encoder.conv_out.", "encoder.head.2.", 1)
if key.startswith("encoder.down_blocks."):
parts = key.split(".")
if len(parts) >= 6 and parts[3] == "resnets":
tail = ".".join(parts[5:])
return f"encoder.downsamples.{parts[2]}.downsamples.{parts[4]}." + _map_resnet_tail(tail)
if len(parts) >= 7 and parts[3] == "downsampler" and parts[4] == "resample":
return f"encoder.downsamples.{parts[2]}.downsamples.2.resample.{parts[5]}." + ".".join(parts[6:])
if len(parts) >= 6 and parts[3] == "downsampler" and parts[4] == "time_conv":
return f"encoder.downsamples.{parts[2]}.downsamples.2.time_conv." + ".".join(parts[5:])
if key.startswith("decoder.conv_in."):
return key.replace("decoder.conv_in.", "decoder.conv1.", 1)
if key.startswith("decoder.mid_block.resnets.0."):
tail = key[len("decoder.mid_block.resnets.0."):]
return "decoder.middle.0." + _map_resnet_tail(tail)
if key.startswith("decoder.mid_block.attentions.0."):
return key.replace("decoder.mid_block.attentions.0.", "decoder.middle.1.", 1)
if key.startswith("decoder.mid_block.resnets.1."):
tail = key[len("decoder.mid_block.resnets.1."):]
return "decoder.middle.2." + _map_resnet_tail(tail)
if key.startswith("decoder.norm_out."):
return key.replace("decoder.norm_out.", "decoder.head.0.", 1)
if key.startswith("decoder.conv_out."):
return key.replace("decoder.conv_out.", "decoder.head.2.", 1)
if key.startswith("decoder.up_blocks."):
parts = key.split(".")
if len(parts) >= 6 and parts[3] == "resnets":
tail = ".".join(parts[5:])
return f"decoder.upsamples.{parts[2]}.upsamples.{parts[4]}." + _map_resnet_tail(tail)
if len(parts) >= 7 and parts[3] == "upsampler" and parts[4] == "resample":
return f"decoder.upsamples.{parts[2]}.upsamples.3.resample.{parts[5]}." + ".".join(parts[6:])
if len(parts) >= 6 and parts[3] == "upsampler" and parts[4] == "time_conv":
return f"decoder.upsamples.{parts[2]}.upsamples.3.time_conv." + ".".join(parts[5:])
return key
def _normalize_vae_state_dict(raw_state):
state = _extract_checkpoint_state_dict(raw_state)
normalized = {}
for key, value in state.items():
mapped_key = _map_lightvae_key_to_wanvae(key)
if mapped_key is None:
continue
normalized[mapped_key] = value
return normalized
def infer_lightvae_pruning_rate_from_ckpt(vae_path, full_decoder_conv1_out=1024):
if vae_path is None or not os.path.exists(vae_path):
return None
try:
raw_state = torch.load(vae_path, map_location="cpu")
state = _extract_checkpoint_state_dict(raw_state)
except Exception as exc:
logging.warning("Failed to load checkpoint for pruning-rate inference: %s", exc)
return None
weight = None
if isinstance(state, dict):
if "decoder.conv_in.weight" in state:
weight = state["decoder.conv_in.weight"]
elif "decoder.conv1.weight" in state:
weight = state["decoder.conv1.weight"]
if weight is None:
try:
normalized_state = _normalize_vae_state_dict(state)
weight = normalized_state.get("decoder.conv1.weight", None)
except Exception:
weight = None
if weight is None or not hasattr(weight, "shape") or len(weight.shape) < 1:
return None
student_out = int(weight.shape[0])
if full_decoder_conv1_out <= 0:
return None
pruning_rate = 1.0 - (float(student_out) / float(full_decoder_conv1_out))
pruning_rate = max(0.0, min(0.99, pruning_rate))
return round(pruning_rate, 6)
def convert_to_channels_last_3d(module):
for child in module.children():
if isinstance(child, nn.Conv3d):
child.weight.data = child.weight.data.to(memory_format=torch.channels_last_3d)
else:
convert_to_channels_last_3d(child)
class PrunableWanVAE(nn.Module):
def __init__(
self,
dim=160,
dec_dim=256,
z_dim=48,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_downsample=[False, True, True],
dropout=0.0,
pruning_rate=0.0,
):
super().__init__()
self.dim = dim
self.z_dim = z_dim
self.dim_mult = dim_mult
self.num_res_blocks = num_res_blocks
self.attn_scales = attn_scales
self.temperal_downsample = temperal_downsample
self.temperal_upsample = temperal_downsample[::-1]
dim = max(1, int(round(dim * (1.0 - pruning_rate))))
dec_dim = max(1, int(round(dec_dim * (1.0 - pruning_rate))))
self.encoder = Encoder3d(
dim,
z_dim * 2,
dim_mult,
num_res_blocks,
attn_scales,
self.temperal_downsample,
dropout,
)
self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
self.conv2 = CausalConv3d(z_dim, z_dim, 1)
self.decoder = Decoder3d(
dec_dim,
z_dim,
dim_mult,
num_res_blocks,
attn_scales,
self.temperal_upsample,
dropout,
)
def encode(self, x, scale):
self.clear_cache()
x = patchify(x, patch_size=2)
total_steps = 1 + (x.shape[2] - 1) // 4
for step in range(total_steps):
self._enc_conv_idx = [0]
if step == 0:
out = self.encoder(
x[:, :, :1, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self._enc_conv_idx,
)
else:
out_chunk = self.encoder(
x[:, :, 1 + 4 * (step - 1):1 + 4 * step, :, :],
feat_cache=self._enc_feat_map,
feat_idx=self._enc_conv_idx,
)
out = torch.cat([out, out_chunk], 2)
mu, _ = self.conv1(out).chunk(2, dim=1)
if isinstance(scale[0], torch.Tensor):
mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
1, self.z_dim, 1, 1, 1
)
else:
mu = (mu - scale[0]) * scale[1]
self.clear_cache()
return mu
def decode(self, z, scale):
self.clear_cache()
if isinstance(scale[0], torch.Tensor):
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
1, self.z_dim, 1, 1, 1
)
else:
z = z / scale[1] + scale[0]
total_steps = z.shape[2]
x = self.conv2(z)
for step in range(total_steps):
self._conv_idx = [0]
if step == 0:
out = self.decoder(
x[:, :, step:step + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx,
first_chunk=True,
)
else:
out_chunk = self.decoder(
x[:, :, step:step + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx,
)
out = torch.cat([out, out_chunk], 2)
out = unpatchify(out, patch_size=2)
self.clear_cache()
return out
def cached_decode(self, z, scale):
if isinstance(scale[0], torch.Tensor):
z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
1, self.z_dim, 1, 1, 1
)
else:
z = z / scale[1] + scale[0]
total_steps = z.shape[2]
x = self.conv2(z)
is_first = self._feat_map[0] is None
for step in range(total_steps):
self._conv_idx = [0]
if step == 0 and is_first:
out = self.decoder(
x[:, :, step:step + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx,
first_chunk=True,
)
elif step == 0:
out = self.decoder(
x[:, :, step:step + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx,
)
else:
out_chunk = self.decoder(
x[:, :, step:step + 1, :, :],
feat_cache=self._feat_map,
feat_idx=self._conv_idx,
)
out = torch.cat([out, out_chunk], 2)
return unpatchify(out, patch_size=2)
def clear_cache(self):
self._conv_num = count_conv3d(self.decoder)
self._conv_idx = [0]
self._feat_map = [None] * self._conv_num
self._enc_conv_num = count_conv3d(self.encoder)
self._enc_conv_idx = [0]
self._enc_feat_map = [None] * self._enc_conv_num
def _load_lightvae_model(pretrained_path=None, z_dim=48, dim=160, device="cpu", **kwargs):
cfg = dict(
dim=dim,
z_dim=z_dim,
dim_mult=[1, 2, 4, 4],
num_res_blocks=2,
attn_scales=[],
temperal_downsample=[False, True, True],
dropout=0.0,
)
cfg.update(**kwargs)
with torch.device("meta"):
model = PrunableWanVAE(**cfg)
if pretrained_path is None or not os.path.exists(pretrained_path):
raise FileNotFoundError(f"VAE checkpoint not found at {pretrained_path}")
logging.info("loading %s", pretrained_path)
raw_state = torch.load(pretrained_path, map_location="cpu")
state_dict = _normalize_vae_state_dict(raw_state)
missing, unexpected = model.load_state_dict(state_dict, strict=False, assign=True)
logging.info(
"LightVAE checkpoint loaded with strict=False (missing=%d, unexpected=%d)",
len(missing),
len(unexpected),
)
convert_to_channels_last_3d(model)
return model
class LightVAE5BWrapper(nn.Module):
def __init__(
self,
vae_path: str,
pruning_rate: Optional[float] = None,
dtype: torch.dtype = torch.bfloat16,
device: Optional[torch.device] = None,
):
super().__init__()
if device is None:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
if pruning_rate is None:
pruning_rate = infer_lightvae_pruning_rate_from_ckpt(vae_path)
if pruning_rate is None:
pruning_rate = 0.75
logging.warning(
"Unable to infer LightVAE pruning rate from checkpoint; fallback to 0.75."
)
mean = [
-0.2289, -0.0052, -0.1323, -0.2339, -0.2799, 0.0174, 0.1838, 0.1557,
-0.1382, 0.0542, 0.2813, 0.0891, 0.1570, -0.0098, 0.0375, -0.1825,
-0.2246, -0.1207, -0.0698, 0.5109, 0.2665, -0.2108, -0.2158, 0.2502,
-0.2055, -0.0322, 0.1109, 0.1567, -0.0729, 0.0899, -0.2799, -0.1230,
-0.0313, -0.1649, 0.0117, 0.0723, -0.2839, -0.2083, -0.0520, 0.3748,
0.0152, 0.1957, 0.1433, -0.2944, 0.3573, -0.0548, -0.1681, -0.0667,
]
std = [
0.4765, 1.0364, 0.4514, 1.1677, 0.5313, 0.4990, 0.4818, 0.5013,
0.8158, 1.0344, 0.5894, 1.0901, 0.6885, 0.6165, 0.8454, 0.4978,
0.5759, 0.3523, 0.7135, 0.6804, 0.5833, 1.4146, 0.8986, 0.5659,
0.7069, 0.5338, 0.4889, 0.4917, 0.4069, 0.4999, 0.6866, 0.4093,
0.5709, 0.6065, 0.6415, 0.4944, 0.5726, 1.2042, 0.5458, 1.6887,
0.3971, 1.0600, 0.3943, 0.5537, 0.5444, 0.4089, 0.7468, 0.7744,
]
self.mean = torch.tensor(mean, dtype=torch.float32)
self.std = torch.tensor(std, dtype=torch.float32)
self.vae_path = os.path.abspath(vae_path)
self.pruning_rate = pruning_rate
self.device = torch.device(device)
self.dtype = dtype
self.model = _load_lightvae_model(
pretrained_path=self.vae_path,
pruning_rate=self.pruning_rate,
).eval().requires_grad_(False)
self.to(device=self.device, dtype=self.dtype)
def to(self, device=None, dtype=None):
device = self.device if device is None else torch.device(device)
dtype = self.dtype if dtype is None else dtype
self.model.to(device=device, dtype=dtype)
self.mean = self.mean.to(device=device, dtype=dtype)
self.std = self.std.to(device=device, dtype=dtype)
self.device = device
self.dtype = dtype
return self
def eval(self):
super().eval()
self.model.eval()
return self
@torch.no_grad()
def decode_to_pixel(self, latent: torch.Tensor, use_cache: bool = False) -> torch.Tensor:
zs = latent.permute(0, 2, 1, 3, 4)
if use_cache:
assert latent.shape[0] == 1, "Batch size must be 1 when using cache"
scale = [self.mean, 1.0 / self.std]
decode_fn = self.model.cached_decode if use_cache else self.model.decode
output = []
for item in zs:
output.append(
decode_fn(item.unsqueeze(0).to(device=self.device, dtype=self.dtype), scale)
.float()
.clamp_(-1, 1)
.squeeze(0)
)
output = torch.stack(output, dim=0)
return output.permute(0, 2, 1, 3, 4)