multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
ee69ab7 verified
Raw History Blame Contribute Delete
28.5 kB
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
from __future__ import annotations
from collections.abc import Sequence
from pathlib import Path
from typing import Mapping
import torch
import torch.nn as nn
import torch.nn.functional as F
try:
from model_io import load_checkpoint_file, normalize_key
except ModuleNotFoundError:
from ...model_io import load_checkpoint_file, normalize_key
CACHE_T = 2
WAN_LATENT_MEAN = (
-0.7571,
-0.7089,
-0.9113,
0.1075,
-0.1745,
0.9653,
-0.1517,
1.5508,
0.4134,
-0.0715,
0.5517,
-0.3632,
-0.1922,
-0.9497,
0.2503,
-0.2921,
)
WAN_LATENT_STD = (
2.8184,
1.4541,
2.3275,
2.6558,
1.2196,
1.7708,
2.6052,
2.0743,
3.2687,
2.1526,
2.8652,
1.5579,
1.6382,
1.1253,
2.8251,
1.9160,
)
class WanCausalConv3d(nn.Conv3d):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
pt, ph, pw = self.padding
self._causal_padding = (pw, pw, ph, ph, 2 * pt, 0)
self.padding = (0, 0, 0)
def forward(
self, x: torch.Tensor, cache: torch.Tensor | None = None
) -> torch.Tensor:
padding = list(self._causal_padding)
if cache is not None and padding[4] > 0:
x = torch.cat([cache.to(device=x.device, dtype=x.dtype), x], dim=2)
padding[4] -= cache.shape[2]
return super().forward(F.pad(x, padding))
class WanRMSNorm(nn.Module):
def __init__(self, dim: int, *, images: bool):
super().__init__()
broadcast = (1, 1) if images else (1, 1, 1)
self.scale = dim**0.5
self.gamma = nn.Parameter(torch.ones(dim, *broadcast))
def forward(self, x: torch.Tensor) -> torch.Tensor:
return F.normalize(x, dim=1) * self.scale * self.gamma
class WanResidualBlock(nn.Module):
def __init__(self, in_dim: int, out_dim: int, dropout: float = 0.0):
super().__init__()
self.residual = nn.Sequential(
WanRMSNorm(in_dim, images=False),
nn.SiLU(),
WanCausalConv3d(in_dim, out_dim, 3, padding=1),
WanRMSNorm(out_dim, images=False),
nn.SiLU(),
nn.Dropout(dropout),
WanCausalConv3d(out_dim, out_dim, 3, padding=1),
)
self.shortcut = (
WanCausalConv3d(in_dim, out_dim, 1)
if in_dim != out_dim
else nn.Identity()
)
def forward(
self,
x: torch.Tensor,
feature_cache: list[torch.Tensor | None] | None = None,
feature_index: list[int] | None = None,
) -> torch.Tensor:
residual = self.shortcut(x)
for layer in self.residual:
if isinstance(layer, WanCausalConv3d) and feature_cache is not None:
if feature_index is None:
raise ValueError("feature_index is required with feature_cache")
index = feature_index[0]
cache = x[:, :, -CACHE_T:].clone()
if cache.shape[2] < CACHE_T and feature_cache[index] is not None:
cache = torch.cat(
[feature_cache[index][:, :, -1:].to(cache), cache], dim=2
)
x = layer(x, feature_cache[index])
feature_cache[index] = cache
feature_index[0] += 1
else:
x = layer(x)
return x + residual
class WanAttentionBlock(nn.Module):
def __init__(self, dim: int):
super().__init__()
self.dim = dim
self.norm = WanRMSNorm(dim, images=True)
self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
self.proj = nn.Conv2d(dim, dim, 1)
nn.init.zeros_(self.proj.weight)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x
batch, channels, frames, height, width = x.shape
value = x.permute(0, 2, 1, 3, 4).reshape(
batch * frames, channels, height, width
)
value = self.norm(value)
qkv = self.to_qkv(value).reshape(
batch * frames, 1, channels * 3, height * width
)
q, k, v = qkv.permute(0, 1, 3, 2).contiguous().chunk(3, dim=-1)
value = F.scaled_dot_product_attention(q, k, v)
value = value.squeeze(1).permute(0, 2, 1).reshape(
batch * frames, channels, height, width
)
value = self.proj(value)
value = value.reshape(batch, frames, channels, height, width).permute(
0, 2, 1, 3, 4
)
return value + identity
class WanDownsample(nn.Module):
def __init__(self, dim: int, *, temporal: bool):
super().__init__()
self.mode = "downsample3d" if temporal else "downsample2d"
self.resample = nn.Sequential(
nn.ZeroPad2d((0, 1, 0, 1)),
nn.Conv2d(dim, dim, 3, stride=(2, 2)),
)
self.time_conv = (
WanCausalConv3d(
dim,
dim,
(3, 1, 1),
stride=(2, 1, 1),
padding=(0, 0, 0),
)
if temporal
else None
)
def forward(
self,
x: torch.Tensor,
feature_cache: list[torch.Tensor | None] | None = None,
feature_index: list[int] | None = None,
) -> torch.Tensor:
batch, channels, frames, height, width = x.shape
value = x.permute(0, 2, 1, 3, 4).reshape(
batch * frames, channels, height, width
)
value = self.resample(value)
height, width = value.shape[-2:]
value = value.reshape(batch, frames, channels, height, width).permute(
0, 2, 1, 3, 4
)
if self.time_conv is not None and feature_cache is not None:
if feature_index is None:
raise ValueError("feature_index is required with feature_cache")
index = feature_index[0]
if feature_cache[index] is None:
feature_cache[index] = value.clone()
feature_index[0] += 1
else:
cache = value[:, :, -1:].clone()
value = self.time_conv(
torch.cat([feature_cache[index][:, :, -1:].to(value), value], 2)
)
feature_cache[index] = cache
feature_index[0] += 1
return value
class WanUpsample(nn.Module):
def __init__(self, dim: int, *, temporal: bool):
super().__init__()
self.temporal = bool(temporal)
self.resample = nn.Sequential(
nn.Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
nn.Conv2d(dim, dim // 2, 3, padding=1),
)
self.time_conv = (
WanCausalConv3d(
dim, dim * 2, (3, 1, 1), padding=(1, 0, 0)
)
if temporal
else None
)
def forward(
self,
x: torch.Tensor,
feature_cache: list[torch.Tensor | str | None],
feature_index: list[int],
) -> torch.Tensor:
batch, channels, frames = x.shape[:3]
if self.time_conv is not None:
index = feature_index[0]
cached = feature_cache[index]
if cached is None:
feature_cache[index] = "first"
feature_index[0] += 1
else:
next_cache = x[:, :, -CACHE_T:].clone()
if next_cache.shape[2] < CACHE_T:
if isinstance(cached, torch.Tensor):
next_cache = torch.cat(
[cached[:, :, -1:].to(next_cache), next_cache], dim=2
)
else:
next_cache = torch.cat(
[torch.zeros_like(next_cache), next_cache], dim=2
)
x = self.time_conv(
x, cached if isinstance(cached, torch.Tensor) else None
)
feature_cache[index] = next_cache
feature_index[0] += 1
x = x.reshape(batch, 2, channels, frames, *x.shape[-2:])
x = torch.stack((x[:, 0], x[:, 1]), dim=3).reshape(
batch, channels, frames * 2, *x.shape[-2:]
)
frames = x.shape[2]
value = x.permute(0, 2, 1, 3, 4).reshape(
batch * frames, x.shape[1], x.shape[3], x.shape[4]
)
dtype = value.dtype
value = F.interpolate(
value.float(), scale_factor=2.0, mode="nearest-exact"
).to(dtype=dtype)
value = self.resample[1](value)
return value.reshape(
batch, frames, value.shape[1], value.shape[2], value.shape[3]
).permute(0, 2, 1, 3, 4)
class WanEncoder3d(nn.Module):
def __init__(
self,
dim: int = 96,
z_dim: int = 32,
dim_mult: Sequence[int] = (1, 2, 4, 4),
num_res_blocks: int = 2,
attention_scales: Sequence[float] = (),
temporal_downsample: Sequence[bool] = (False, True, True),
dropout: float = 0.0,
):
super().__init__()
dimensions = [dim * value for value in (1, *dim_mult)]
scale = 1.0
self.conv1 = WanCausalConv3d(3, dimensions[0], 3, padding=1)
downsample_layers: list[nn.Module] = []
for index, (in_dim, out_dim) in enumerate(
zip(dimensions[:-1], dimensions[1:])
):
for _ in range(num_res_blocks):
downsample_layers.append(
WanResidualBlock(in_dim, out_dim, dropout)
)
if scale in attention_scales:
downsample_layers.append(WanAttentionBlock(out_dim))
in_dim = out_dim
if index != len(dim_mult) - 1:
downsample_layers.append(
WanDownsample(
out_dim,
temporal=bool(temporal_downsample[index]),
)
)
scale /= 2
self.downsamples = nn.Sequential(*downsample_layers)
self.middle = nn.Sequential(
WanResidualBlock(out_dim, out_dim, dropout),
WanAttentionBlock(out_dim),
WanResidualBlock(out_dim, out_dim, dropout),
)
self.head = nn.Sequential(
WanRMSNorm(out_dim, images=False),
nn.SiLU(),
WanCausalConv3d(out_dim, z_dim, 3, padding=1),
)
def forward(
self,
x: torch.Tensor,
feature_cache: list[torch.Tensor | None],
feature_index: list[int],
) -> torch.Tensor:
index = feature_index[0]
cache = x[:, :, -CACHE_T:].clone()
if cache.shape[2] < CACHE_T and feature_cache[index] is not None:
cache = torch.cat(
[feature_cache[index][:, :, -1:].to(cache), cache], dim=2
)
x = self.conv1(x, feature_cache[index])
feature_cache[index] = cache
feature_index[0] += 1
for layer in self.downsamples:
if isinstance(layer, (WanResidualBlock, WanDownsample)):
x = layer(x, feature_cache, feature_index)
else:
x = layer(x)
for layer in self.middle:
if isinstance(layer, WanResidualBlock):
x = layer(x, feature_cache, feature_index)
else:
x = layer(x)
for layer in self.head:
if isinstance(layer, WanCausalConv3d):
index = feature_index[0]
cache = x[:, :, -CACHE_T:].clone()
if cache.shape[2] < CACHE_T and feature_cache[index] is not None:
cache = torch.cat(
[feature_cache[index][:, :, -1:].to(cache), cache], dim=2
)
x = layer(x, feature_cache[index])
feature_cache[index] = cache
feature_index[0] += 1
else:
x = layer(x)
return x
class WanDecoder3d(nn.Module):
def __init__(
self,
dim: int = 96,
z_dim: int = 16,
dim_mult: Sequence[int] = (1, 2, 4, 4),
num_res_blocks: int = 2,
attention_scales: Sequence[float] = (),
temporal_upsample: Sequence[bool] = (True, True, False),
dropout: float = 0.0,
):
super().__init__()
dimensions = [dim * value for value in (dim_mult[-1], *dim_mult[::-1])]
scale = 1.0 / 2 ** (len(dim_mult) - 2)
self.conv1 = WanCausalConv3d(z_dim, dimensions[0], 3, padding=1)
self.middle = nn.Sequential(
WanResidualBlock(dimensions[0], dimensions[0], dropout),
WanAttentionBlock(dimensions[0]),
WanResidualBlock(dimensions[0], dimensions[0], dropout),
)
upsample_layers: list[nn.Module] = []
for index, (in_dim, out_dim) in enumerate(
zip(dimensions[:-1], dimensions[1:])
):
if index in {1, 2, 3}:
in_dim //= 2
for _ in range(num_res_blocks + 1):
upsample_layers.append(
WanResidualBlock(in_dim, out_dim, dropout)
)
if scale in attention_scales:
upsample_layers.append(WanAttentionBlock(out_dim))
in_dim = out_dim
if index != len(dim_mult) - 1:
upsample_layers.append(
WanUpsample(
out_dim,
temporal=bool(temporal_upsample[index]),
)
)
scale *= 2
self.upsamples = nn.Sequential(*upsample_layers)
self.head = nn.Sequential(
WanRMSNorm(out_dim, images=False),
nn.SiLU(),
WanCausalConv3d(out_dim, 3, 3, padding=1),
)
@staticmethod
def _run_causal(
layer: WanCausalConv3d,
x: torch.Tensor,
feature_cache: list[torch.Tensor | str | None],
feature_index: list[int],
) -> torch.Tensor:
index = feature_index[0]
cached = feature_cache[index]
next_cache = x[:, :, -CACHE_T:].clone()
if next_cache.shape[2] < CACHE_T and isinstance(cached, torch.Tensor):
next_cache = torch.cat(
[cached[:, :, -1:].to(next_cache), next_cache], dim=2
)
x = layer(x, cached if isinstance(cached, torch.Tensor) else None)
feature_cache[index] = next_cache
feature_index[0] += 1
return x
def forward(
self,
x: torch.Tensor,
feature_cache: list[torch.Tensor | str | None],
feature_index: list[int],
) -> torch.Tensor:
x = self._run_causal(self.conv1, x, feature_cache, feature_index)
for layer in self.middle:
if isinstance(layer, WanResidualBlock):
x = layer(x, feature_cache, feature_index)
else:
x = layer(x)
for layer in self.upsamples:
if isinstance(layer, WanResidualBlock):
x = layer(x, feature_cache, feature_index)
elif isinstance(layer, WanUpsample):
x = layer(x, feature_cache, feature_index)
else:
x = layer(x)
for layer in self.head:
if isinstance(layer, WanCausalConv3d):
x = self._run_causal(
layer, x, feature_cache, feature_index
)
else:
x = layer(x)
return x
def _count_causal_convs(module: nn.Module) -> int:
return sum(isinstance(layer, WanCausalConv3d) for layer in module.modules())
class WanVAEEncoder(nn.Module):
"""Wan 2.1 VAE encoder with the official temporal cache and scaling."""
def __init__(self, dim: int = 96, z_dim: int = 16):
super().__init__()
self.z_dim = int(z_dim)
self.encoder = WanEncoder3d(
dim=dim,
z_dim=z_dim * 2,
dim_mult=(1, 2, 4, 4),
num_res_blocks=2,
attention_scales=(),
temporal_downsample=(False, True, True),
dropout=0.0,
)
self.conv1 = WanCausalConv3d(z_dim * 2, z_dim * 2, 1)
if z_dim != len(WAN_LATENT_MEAN):
raise ValueError("Built-in Wan latent normalization requires z_dim=16")
self.register_buffer(
"latent_mean",
torch.tensor(WAN_LATENT_MEAN),
persistent=False,
)
self.register_buffer(
"latent_inv_std",
torch.tensor(WAN_LATENT_STD).reciprocal(),
persistent=False,
)
def encode_video(self, video: torch.Tensor, **kwargs) -> torch.Tensor:
del kwargs
if video.ndim != 5 or video.shape[1] != 3:
raise ValueError(f"Wan VAE input must be [B,3,F,H,W], got {video.shape}")
frames = video.shape[2]
chunks = 1 + (frames - 1) // 4
feature_cache: list[torch.Tensor | None] = [
None
] * _count_causal_convs(self.encoder)
outputs = []
for index in range(chunks):
feature_index = [0]
if index == 0:
current = video[:, :, :1]
else:
current = video[:, :, 1 + 4 * (index - 1) : 1 + 4 * index]
if current.shape[2] == 0:
raise ValueError("Wan VAE received an empty temporal chunk")
outputs.append(
self.encoder(current, feature_cache, feature_index)
)
value = torch.cat(outputs, dim=2)
mean, _log_variance = self.conv1(value).chunk(2, dim=1)
latent_mean = self.latent_mean.to(device=mean.device, dtype=mean.dtype)
latent_inv_std = self.latent_inv_std.to(
device=mean.device, dtype=mean.dtype
)
return (mean - latent_mean.view(1, -1, 1, 1, 1)) * latent_inv_std.view(
1, -1, 1, 1, 1
)
def encode(self, video: torch.Tensor, **kwargs) -> torch.Tensor:
return self.encode_video(video, **kwargs)
class WanVAE(WanVAEEncoder):
"""Wan encoder/decoder codec with differentiable streaming decode."""
def __init__(self, dim: int = 96, z_dim: int = 16):
super().__init__(dim=dim, z_dim=z_dim)
self.conv2 = WanCausalConv3d(z_dim, z_dim, 1)
self.decoder = WanDecoder3d(
dim=dim,
z_dim=z_dim,
dim_mult=(1, 2, 4, 4),
num_res_blocks=2,
attention_scales=(),
temporal_upsample=(True, True, False),
dropout=0.0,
)
self.clear_cache()
def clear_cache(self) -> None:
self._decoder_cache: list[torch.Tensor | str | None] = [
None
] * _count_causal_convs(self.decoder)
def _stream_decode_untiled(self, latents: torch.Tensor) -> torch.Tensor:
if latents.ndim != 5 or latents.shape[1] != self.z_dim:
raise ValueError(
f"Wan VAE latents must be [B,{self.z_dim},F,H,W], "
f"got {latents.shape}"
)
mean = self.latent_mean.to(device=latents.device, dtype=latents.dtype)
inv_std = self.latent_inv_std.to(
device=latents.device, dtype=latents.dtype
)
value = latents / inv_std.view(1, -1, 1, 1, 1)
value = value + mean.view(1, -1, 1, 1, 1)
value = self.conv2(value)
outputs = []
for index in range(value.shape[2]):
feature_index = [0]
outputs.append(
self.decoder(
value[:, :, index : index + 1],
self._decoder_cache,
feature_index,
)
)
return torch.cat(outputs, dim=2)
@staticmethod
def _tile_starts(length: int, size: int, stride: int) -> list[int]:
size = min(size, length)
starts = list(range(0, max(length - size, 0) + 1, stride))
last = length - size
if not starts or starts[-1] != last:
starts.append(last)
return starts
@staticmethod
def _tile_blend_mask(
height: int,
width: int,
*,
top_bound: bool,
bottom_bound: bool,
left_bound: bool,
right_bound: bool,
border_height: int,
border_width: int,
) -> torch.Tensor:
vertical = torch.ones(height, dtype=torch.float32)
horizontal = torch.ones(width, dtype=torch.float32)
if not top_bound and border_height > 0:
count = min(border_height, height)
vertical[:count] = torch.linspace(1 / count, 1, count)
if not bottom_bound and border_height > 0:
count = min(border_height, height)
vertical[-count:] = torch.linspace(1, 1 / count, count)
if not left_bound and border_width > 0:
count = min(border_width, width)
horizontal[:count] = torch.linspace(1 / count, 1, count)
if not right_bound and border_width > 0:
count = min(border_width, width)
horizontal[-count:] = torch.linspace(1, 1 / count, count)
return torch.minimum(vertical[:, None], horizontal[None, :]).view(
1, 1, 1, height, width
)
def _tiled_stream_decode(
self,
latents: torch.Tensor,
tile_size: tuple[int, int],
tile_stride: tuple[int, int],
) -> torch.Tensor:
if latents.ndim != 5 or latents.shape[1] != self.z_dim:
raise ValueError(
f"Wan VAE latents must be [B,{self.z_dim},F,H,W], got {latents.shape}"
)
tile_height, tile_width = (int(value) for value in tile_size)
stride_height, stride_width = (int(value) for value in tile_stride)
if min(tile_height, tile_width, stride_height, stride_width) <= 0:
raise ValueError("VAE tile sizes and strides must be positive")
if stride_height > tile_height or stride_width > tile_width:
raise ValueError("VAE tile stride cannot exceed tile size")
batch, _channels, latent_frames, latent_height, latent_width = latents.shape
tile_height = min(tile_height, latent_height)
tile_width = min(tile_width, latent_width)
height_starts = self._tile_starts(latent_height, tile_height, stride_height)
width_starts = self._tile_starts(latent_width, tile_width, stride_width)
parameter = next(self.parameters())
computation_device = parameter.device
computation_dtype = parameter.dtype
source = latents.detach().to("cpu")
values = weight = None
for top in height_starts:
for left in width_starts:
bottom = top + tile_height
right = left + tile_width
current = source[:, :, :, top:bottom, left:right].to(
device=computation_device, dtype=computation_dtype
)
self.clear_cache()
decoded_gpu = self._stream_decode_untiled(current).detach()
self.clear_cache()
decoded = decoded_gpu.float().cpu()
del decoded_gpu
del current
if values is None:
scale_height = decoded.shape[-2] // tile_height
scale_width = decoded.shape[-1] // tile_width
if scale_height <= 0 or scale_width <= 0:
raise RuntimeError("VAE tiled decode produced an invalid spatial scale")
output_frames = decoded.shape[2]
values = torch.zeros(
batch,
decoded.shape[1],
output_frames,
latent_height * scale_height,
latent_width * scale_width,
dtype=torch.float32,
)
weight = torch.zeros(
1, 1, 1, values.shape[-2], values.shape[-1], dtype=torch.float32
)
blend_height = max(
0, (tile_height - stride_height) * scale_height
)
blend_width = max(
0, (tile_width - stride_width) * scale_width
)
target_top = top * scale_height
target_left = left * scale_width
mask = self._tile_blend_mask(
decoded.shape[-2],
decoded.shape[-1],
top_bound=top == 0,
bottom_bound=bottom >= latent_height,
left_bound=left == 0,
right_bound=right >= latent_width,
border_height=blend_height,
border_width=blend_width,
)
target_bottom = target_top + decoded.shape[-2]
target_right = target_left + decoded.shape[-1]
values[..., target_top:target_bottom, target_left:target_right] += (
decoded * mask
)
weight[..., target_top:target_bottom, target_left:target_right] += mask
del decoded, mask
self.clear_cache()
if values is None or weight is None:
raise RuntimeError("VAE tiled decode produced no tiles")
return (values / weight.clamp_min(1e-8)).to(dtype=latents.dtype)
def stream_decode(
self,
latents: torch.Tensor,
*,
tiled: bool = False,
tile_size: tuple[int, int] = (30, 52),
tile_stride: tuple[int, int] = (15, 26),
**kwargs,
) -> torch.Tensor:
del kwargs
if tiled:
return self._tiled_stream_decode(latents, tile_size, tile_stride)
return self._stream_decode_untiled(latents)
def decode_video(self, latents: torch.Tensor, **kwargs) -> torch.Tensor:
self.clear_cache()
return self.stream_decode(latents, **kwargs)
def decode(self, latents: torch.Tensor, **kwargs) -> torch.Tensor:
return self.decode_video(latents, **kwargs)
def load_wan_vae_encoder_checkpoint(
model: WanVAEEncoder,
checkpoint: str | Path | Mapping[str, torch.Tensor],
) -> WanVAEEncoder:
source = load_checkpoint_file(checkpoint)
own = model.state_dict()
accepted = {}
for source_key, value in source.items():
key = normalize_key(source_key)
for prefix in ("vae.", "first_stage_model.", "autoencoder."):
if key.startswith(prefix):
key = normalize_key(key[len(prefix) :])
break
if key in own and own[key].shape == value.shape:
accepted[key] = value
result = model.load_state_dict(accepted, strict=False)
missing_parameters = [
key
for key in result.missing_keys
if key not in {"latent_mean", "latent_inv_std"}
]
if missing_parameters:
raise RuntimeError(
"Wan VAE checkpoint is missing encoder parameters: "
f"{missing_parameters[:8]}"
)
return model
def build_wan_vae_encoder(checkpoint_path: str | Path) -> WanVAEEncoder:
return load_wan_vae_encoder_checkpoint(WanVAEEncoder(), checkpoint_path)
def load_wan_vae_checkpoint(
model: WanVAE,
checkpoint: str | Path | Mapping[str, torch.Tensor],
) -> WanVAE:
source = load_checkpoint_file(checkpoint)
own = model.state_dict()
accepted = {}
for source_key, value in source.items():
key = normalize_key(source_key)
for prefix in ("vae.", "model.", "first_stage_model.", "autoencoder."):
if key.startswith(prefix):
key = normalize_key(key[len(prefix) :])
break
if key in own and own[key].shape == value.shape:
accepted[key] = value
result = model.load_state_dict(accepted, strict=False)
missing = [
key
for key in result.missing_keys
if key not in {"latent_mean", "latent_inv_std"}
]
if missing:
raise RuntimeError(
"Wan VAE checkpoint is missing codec parameters: "
f"{missing[:8]}"
)
return model
def build_wan_vae(checkpoint_path: str | Path) -> WanVAE:
return load_wan_vae_checkpoint(WanVAE(), checkpoint_path)