""" Self-contained VAE implementation adapted from the public LTX-2 code. """ import math from enum import Enum from typing import Optional, Tuple, Union, List, Any import torch import torch.nn as nn import torch.nn.functional as F from einops import rearrange __all__ = ['VideoEncoder', 'VideoDecoder'] # ============================================================ # ============================================================ class NormLayerType(Enum): GROUP_NORM = "group_norm" PIXEL_NORM = "pixel_norm" class LogVarianceType(Enum): NONE = "none" UNIFORM = "uniform" PER_CHANNEL = "per_channel" CONSTANT = "constant" class PaddingModeType(Enum): ZEROS = "zeros" REFLECT = "reflect" # ============================================================ # ============================================================ class PixelNorm(nn.Module): """Per-pixel RMS normalization""" def __init__(self, dim=1, eps=1e-8): super().__init__() self.dim, self.eps = dim, eps def forward(self, x): return x / torch.sqrt(torch.mean(x**2, dim=self.dim, keepdim=True) + self.eps) class PerChannelStatistics(nn.Module): """Per-channel statistics for latent normalization""" def __init__(self, latent_channels=128): super().__init__() self.register_buffer("std-of-means", torch.empty(latent_channels)) self.register_buffer("mean-of-means", torch.empty(latent_channels)) def normalize(self, x): return (x - self.get_buffer("mean-of-means").view(1, -1, 1, 1, 1).to(x)) / self.get_buffer("std-of-means").view(1, -1, 1, 1, 1).to(x) def un_normalize(self, x): return x * self.get_buffer("std-of-means").view(1, -1, 1, 1, 1).to(x) + self.get_buffer("mean-of-means").view(1, -1, 1, 1, 1).to(x) def patchify(x, patch_size_hw, patch_size_t=1): """Space-to-depth: (B,C,F,H,W) -> (B,C*p^2,F,H/p,W/p)""" if patch_size_hw == 1 and patch_size_t == 1: return x return rearrange(x, "b c (f p) (h q) (w r) -> b (c p r q) f h w", p=patch_size_t, q=patch_size_hw, r=patch_size_hw) def unpatchify(x, patch_size_hw, patch_size_t=1): """Depth-to-space: inverse of patchify""" if patch_size_hw == 1 and patch_size_t == 1: return x return rearrange(x, "b (c p r q) f h w -> b c (f p) (h q) (w r)", p=patch_size_t, q=patch_size_hw, r=patch_size_hw) # ============================================================ # ============================================================ def get_timestep_embedding(timesteps, dim, flip_sin_to_cos=False, downscale_freq_shift=1, scale=1, max_period=10000): half = dim // 2 exp = -math.log(max_period) * torch.arange(half, dtype=torch.float32, device=timesteps.device) / (half - downscale_freq_shift) emb = timesteps[:, None].float() * torch.exp(exp)[None, :] emb = torch.cat([torch.sin(emb * scale), torch.cos(emb * scale)], -1) if flip_sin_to_cos: emb = torch.cat([emb[:, half:], emb[:, :half]], -1) return emb class TimestepEmbedding(nn.Module): def __init__(self, in_ch, time_dim, out_dim=None): super().__init__() self.linear_1 = nn.Linear(in_ch, time_dim) self.act = nn.SiLU() self.linear_2 = nn.Linear(time_dim, out_dim or time_dim) def forward(self, x): return self.linear_2(self.act(self.linear_1(x))) class PixArtAlphaCombinedTimestepSizeEmbeddings(nn.Module): def __init__(self, embedding_dim, size_emb_dim=0): super().__init__() self.time_proj = lambda t: get_timestep_embedding(t, 256, True, 0) self.timestep_embedder = TimestepEmbedding(256, embedding_dim) def forward(self, timestep, hidden_dtype): return self.timestep_embedder(self.time_proj(timestep).to(hidden_dtype)) # ============================================================ # ============================================================ class CausalConv3d(nn.Module): """Causal 3D convolution: the temporal dimension only sees past frames.""" def __init__(self, in_channels, out_channels, kernel_size=3, stride=1, dilation=1, groups=1, bias=True, spatial_padding_mode=PaddingModeType.ZEROS): super().__init__() self.time_kernel_size = kernel_size height_pad, width_pad = kernel_size // 2, kernel_size // 2 self.conv = nn.Conv3d(in_channels, out_channels, (kernel_size, kernel_size, kernel_size), stride=stride, dilation=(dilation, 1, 1), padding=(0, height_pad, width_pad), padding_mode=spatial_padding_mode.value, groups=groups, bias=bias) def forward(self, x, causal=True): if causal: pad = x[:, :, :1].repeat(1, 1, self.time_kernel_size - 1, 1, 1) x = torch.cat([pad, x], dim=2) else: pad_size = (self.time_kernel_size - 1) // 2 x = torch.cat([x[:, :, :1].repeat(1, 1, pad_size, 1, 1), x, x[:, :, -1:].repeat(1, 1, pad_size, 1, 1)], dim=2) return self.conv(x) def make_conv_nd(dims, in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True, causal=False, spatial_padding_mode=PaddingModeType.ZEROS, **kwargs): if dims == 3 and causal: return CausalConv3d(in_channels, out_channels, kernel_size, stride, dilation, groups, bias, spatial_padding_mode) elif dims == 3: return nn.Conv3d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias, padding_mode=spatial_padding_mode.value) return nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding, dilation, groups, bias, padding_mode=spatial_padding_mode.value) def make_linear_nd(dims, in_channels, out_channels, bias=True): return nn.Conv3d(in_channels, out_channels, 1, bias=bias) if dims == 3 else nn.Conv2d(in_channels, out_channels, 1, bias=bias) # ============================================================ # ============================================================ class SpaceToDepthDownsample(nn.Module): """Downsample with space-to-depth followed by convolution.""" def __init__(self, dims, in_channels, out_channels, stride, spatial_padding_mode=PaddingModeType.ZEROS): super().__init__() self.stride = stride self.group_size = in_channels * math.prod(stride) // out_channels self.conv = make_conv_nd(dims, in_channels, out_channels // math.prod(stride), 3, 1, causal=True, spatial_padding_mode=spatial_padding_mode) def forward(self, x, causal=True): if self.stride[0] == 2: x = torch.cat([x[:, :, :1], x], dim=2) x_in = rearrange(x, "b c (d p1) (h p2) (w p3) -> b (c p1 p2 p3) d h w", p1=self.stride[0], p2=self.stride[1], p3=self.stride[2]) x_in = rearrange(x_in, "b (c g) d h w -> b c g d h w", g=self.group_size).mean(dim=2) x = self.conv(x, causal=causal) x = rearrange(x, "b c (d p1) (h p2) (w p3) -> b (c p1 p2 p3) d h w", p1=self.stride[0], p2=self.stride[1], p3=self.stride[2]) return x + x_in class DepthToSpaceUpsample(nn.Module): """Upsample with convolution followed by depth-to-space.""" def __init__(self, dims, in_channels, stride, residual=False, out_channels_reduction_factor=1, spatial_padding_mode=PaddingModeType.ZEROS): super().__init__() self.stride = stride self.out_channels = math.prod(stride) * in_channels // out_channels_reduction_factor self.conv = make_conv_nd(dims, in_channels, self.out_channels, 3, 1, causal=True, spatial_padding_mode=spatial_padding_mode) self.residual = residual self.out_channels_reduction_factor = out_channels_reduction_factor def forward(self, x, causal=True): if self.residual: x_in = rearrange(x, "b (c p1 p2 p3) d h w -> b c (d p1) (h p2) (w p3)", p1=self.stride[0], p2=self.stride[1], p3=self.stride[2]) x_in = x_in.repeat(1, math.prod(self.stride) // self.out_channels_reduction_factor, 1, 1, 1) if self.stride[0] == 2: x_in = x_in[:, :, 1:] x = self.conv(x, causal=causal) x = rearrange(x, "b (c p1 p2 p3) d h w -> b c (d p1) (h p2) (w p3)", p1=self.stride[0], p2=self.stride[1], p3=self.stride[2]) if self.stride[0] == 2: x = x[:, :, 1:] return x + x_in if self.residual else x # ============================================================ # ============================================================ class ResnetBlock3D(nn.Module): """3D residual block.""" def __init__(self, dims, in_channels, out_channels=None, dropout=0.0, groups=32, eps=1e-6, norm_layer=NormLayerType.PIXEL_NORM, inject_noise=False, timestep_conditioning=False, spatial_padding_mode=PaddingModeType.ZEROS): super().__init__() out_channels = out_channels or in_channels self.inject_noise = inject_noise self.timestep_conditioning = timestep_conditioning self.norm1 = nn.GroupNorm(groups, in_channels, eps) if norm_layer == NormLayerType.GROUP_NORM else PixelNorm() self.conv1 = make_conv_nd(dims, in_channels, out_channels, 3, 1, 1, causal=True, spatial_padding_mode=spatial_padding_mode) self.norm2 = nn.GroupNorm(groups, out_channels, eps) if norm_layer == NormLayerType.GROUP_NORM else PixelNorm() self.conv2 = make_conv_nd(dims, out_channels, out_channels, 3, 1, 1, causal=True, spatial_padding_mode=spatial_padding_mode) self.conv_shortcut = make_linear_nd(dims, in_channels, out_channels) if in_channels != out_channels else nn.Identity() self.norm3 = nn.GroupNorm(1, in_channels, eps) if in_channels != out_channels else nn.Identity() self.act = nn.SiLU() self.dropout = nn.Dropout(dropout) if inject_noise: self.per_channel_scale1 = nn.Parameter(torch.zeros(in_channels, 1, 1)) self.per_channel_scale2 = nn.Parameter(torch.zeros(in_channels, 1, 1)) if timestep_conditioning: self.scale_shift_table = nn.Parameter(torch.randn(4, in_channels) / in_channels**0.5) def forward(self, x, causal=True, timestep=None, generator=None): h = self.norm1(x) if self.timestep_conditioning and timestep is not None: ada = self.scale_shift_table[None, ..., None, None, None].to(h) + timestep.reshape(x.shape[0], 4, -1, *timestep.shape[-3:]) shift1, scale1, shift2, scale2 = ada.unbind(1) h = h * (1 + scale1) + shift1 h = self.act(h) h = self.conv1(h, causal=causal) if self.inject_noise: noise = torch.randn(h.shape[-2:], device=h.device, dtype=h.dtype, generator=generator)[None] h = h + (noise * self.per_channel_scale1.to(h))[None, :, None] h = self.norm2(h) if self.timestep_conditioning and timestep is not None: h = h * (1 + scale2) + shift2 h = self.act(h) h = self.dropout(h) h = self.conv2(h, causal=causal) if self.inject_noise: noise = torch.randn(h.shape[-2:], device=h.device, dtype=h.dtype, generator=generator)[None] h = h + (noise * self.per_channel_scale2.to(h))[None, :, None] return self.conv_shortcut(self.norm3(x)) + h class UNetMidBlock3D(nn.Module): """UNet middle block.""" def __init__(self, dims, in_channels, dropout=0.0, num_layers=1, resnet_eps=1e-6, resnet_groups=32, norm_layer=NormLayerType.GROUP_NORM, inject_noise=False, timestep_conditioning=False, attention_head_dim=None, spatial_padding_mode=PaddingModeType.ZEROS): super().__init__() self.timestep_conditioning = timestep_conditioning if timestep_conditioning: self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(in_channels * 4, 0) self.res_blocks = nn.ModuleList([ ResnetBlock3D(dims, in_channels, in_channels, dropout, resnet_groups, resnet_eps, norm_layer, inject_noise, timestep_conditioning, spatial_padding_mode) for _ in range(num_layers) ]) def forward(self, x, causal=True, timestep=None, generator=None): ts_embed = None if self.timestep_conditioning and timestep is not None: ts_embed = self.time_embedder(timestep.flatten(), x.dtype).view(x.shape[0], -1, 1, 1, 1) for block in self.res_blocks: x = block(x, causal, ts_embed, generator) return x # ============================================================ # ============================================================ def _make_encoder_block(name, config, in_ch, dims, norm, groups, padding_mode): out_ch = in_ch if name == "res_x": return UNetMidBlock3D(dims, in_ch, num_layers=config.get("num_layers", 1), resnet_groups=groups, norm_layer=norm, spatial_padding_mode=padding_mode), out_ch elif name == "res_x_y": out_ch = in_ch * config.get("multiplier", 2) return ResnetBlock3D(dims, in_ch, out_ch, groups=groups, norm_layer=norm, spatial_padding_mode=padding_mode), out_ch elif name == "compress_time": return make_conv_nd(dims, in_ch, out_ch, 3, (2,1,1), causal=True, spatial_padding_mode=padding_mode), out_ch elif name == "compress_space": return make_conv_nd(dims, in_ch, out_ch, 3, (1,2,2), causal=True, spatial_padding_mode=padding_mode), out_ch elif name == "compress_all": return make_conv_nd(dims, in_ch, out_ch, 3, (2,2,2), causal=True, spatial_padding_mode=padding_mode), out_ch elif name == "compress_all_res": out_ch = in_ch * config.get("multiplier", 2) return SpaceToDepthDownsample(dims, in_ch, out_ch, (2,2,2), padding_mode), out_ch elif name == "compress_space_res": out_ch = in_ch * config.get("multiplier", 2) return SpaceToDepthDownsample(dims, in_ch, out_ch, (1,2,2), padding_mode), out_ch elif name == "compress_time_res": out_ch = in_ch * config.get("multiplier", 2) return SpaceToDepthDownsample(dims, in_ch, out_ch, (2,1,1), padding_mode), out_ch raise ValueError(f"Unknown encoder block: {name}") def _make_decoder_block(name, config, in_ch, dims, norm, ts_cond, groups, padding_mode): out_ch = in_ch if name == "res_x": return UNetMidBlock3D(dims, in_ch, num_layers=config.get("num_layers", 1), resnet_groups=groups, norm_layer=norm, inject_noise=config.get("inject_noise", False), timestep_conditioning=ts_cond, spatial_padding_mode=padding_mode), out_ch elif name == "attn_res_x": return UNetMidBlock3D(dims, in_ch, num_layers=config.get("num_layers", 1), resnet_groups=groups, norm_layer=norm, inject_noise=config.get("inject_noise", False), timestep_conditioning=ts_cond, attention_head_dim=config.get("attention_head_dim"), spatial_padding_mode=padding_mode), out_ch elif name == "res_x_y": out_ch = in_ch // config.get("multiplier", 2) return ResnetBlock3D(dims, in_ch, out_ch, groups=groups, norm_layer=norm, inject_noise=config.get("inject_noise", False), spatial_padding_mode=padding_mode), out_ch elif name == "compress_time": mult = config.get("multiplier", 1) out_ch = in_ch // mult return DepthToSpaceUpsample(dims, in_ch, (2,1,1), out_channels_reduction_factor=mult, spatial_padding_mode=padding_mode), out_ch elif name == "compress_space": mult = config.get("multiplier", 1) out_ch = in_ch // mult return DepthToSpaceUpsample(dims, in_ch, (1,2,2), out_channels_reduction_factor=mult, spatial_padding_mode=padding_mode), out_ch elif name == "compress_all": mult = config.get("multiplier", 1) out_ch = in_ch // mult return DepthToSpaceUpsample(dims, in_ch, (2,2,2), config.get("residual", False), mult, padding_mode), out_ch raise ValueError(f"Unknown decoder block: {name}") # ============================================================ # VideoEncoder # ============================================================ class VideoEncoder(nn.Module): """ Video VAE encoder. Input: (B, 3, F, H, W) where F must be 1 + 8*k Output: (B, 128, F', H', W') where F' = 1+(F-1)/8, H' = H/32, W' = W/32 """ def __init__(self, convolution_dimensions=3, in_channels=3, out_channels=128, encoder_blocks=None, patch_size=4, norm_layer=NormLayerType.PIXEL_NORM, latent_log_var=LogVarianceType.UNIFORM, encoder_spatial_padding_mode=PaddingModeType.ZEROS, base_channels=None): super().__init__() encoder_blocks = encoder_blocks or [] self.patch_size = patch_size self.latent_log_var = latent_log_var self.per_channel_statistics = PerChannelStatistics(out_channels) feature_ch = base_channels if base_channels is not None else out_channels self.conv_in = make_conv_nd(convolution_dimensions, in_channels * patch_size**2, feature_ch, 3, 1, 1, causal=True, spatial_padding_mode=encoder_spatial_padding_mode) self.down_blocks = nn.ModuleList() for name, params in encoder_blocks: cfg = {"num_layers": params} if isinstance(params, int) else params block, feature_ch = _make_encoder_block(name, cfg, feature_ch, convolution_dimensions, norm_layer, 32, encoder_spatial_padding_mode) self.down_blocks.append(block) self.conv_norm_out = nn.GroupNorm(32, feature_ch, 1e-6) if norm_layer == NormLayerType.GROUP_NORM else PixelNorm() self.conv_act = nn.SiLU() conv_out_ch = out_channels + (1 if latent_log_var in (LogVarianceType.UNIFORM, LogVarianceType.CONSTANT) else (out_channels if latent_log_var == LogVarianceType.PER_CHANNEL else 0)) self.conv_out = make_conv_nd(convolution_dimensions, feature_ch, conv_out_ch, 3, 1, 1, causal=True, spatial_padding_mode=encoder_spatial_padding_mode) def forward(self, x, return_posterior: bool = False): """ Forward pass. Args: x: input video [B, C, F, H, W] return_posterior: also return the posterior distribution (for VAE training) Returns: when return_posterior=False: the normalized latent mean when return_posterior=True: (latent_mean, posterior_params, raw_mean) - latent_mean: normalized latent [B, C, F', H', W'] - posterior_params: posterior parameters [B, 2*C, F', H', W'] (mean and logvar concatenated) - raw_mean: un-normalized mean [B, C, F', H', W'] """ x = patchify(x, self.patch_size, 1) x = self.conv_in(x) for block in self.down_blocks: x = block(x) x = self.conv_act(self.conv_norm_out(x)) x = self.conv_out(x) if self.latent_log_var == LogVarianceType.UNIFORM: means, logvar = x[:, :-1], x[:, -1:] x = torch.cat([means, logvar.repeat(1, means.shape[1], 1, 1, 1)], 1) elif self.latent_log_var == LogVarianceType.CONSTANT: x = torch.cat([x[:, :-1], torch.full_like(x[:, :-1], -30)], 1) means, logvar = x.chunk(2, 1) normalized_means = self.per_channel_statistics.normalize(means) if return_posterior: return normalized_means, x, means return normalized_means def encode_with_posterior(self, x): """ Encode and return the posterior distribution (for VAE training). Args: x: input video [B, C, F, H, W] Returns: (posterior, z, normalized_z) - posterior: the diagonal Gaussian posterior - z: latent sampled from the posterior (un-normalized) - normalized_z: the normalized latent """ from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution x = patchify(x, self.patch_size, 1) x = self.conv_in(x) for block in self.down_blocks: x = block(x) x = self.conv_act(self.conv_norm_out(x)) x = self.conv_out(x) if self.latent_log_var == LogVarianceType.UNIFORM: means, logvar = x[:, :-1], x[:, -1:] posterior_params = torch.cat([means, logvar.repeat(1, means.shape[1], 1, 1, 1)], 1) elif self.latent_log_var == LogVarianceType.CONSTANT: posterior_params = torch.cat([x[:, :-1], torch.full_like(x[:, :-1], -30)], 1) else: posterior_params = x posterior = DiagonalGaussianDistribution(posterior_params) z = posterior.sample() normalized_z = self.per_channel_statistics.normalize(z) return posterior, z, normalized_z # ============================================================ # VideoDecoder # ============================================================ class VideoDecoder(nn.Module): """ Video VAE decoder. Input: (B, 128, F', H', W') Output: (B, 3, F, H, W) where F = 8*(F'-1)+1, H = 32*H', W = 32*W' """ def __init__(self, convolution_dimensions=3, in_channels=128, out_channels=3, decoder_blocks=None, patch_size=4, norm_layer=NormLayerType.PIXEL_NORM, causal=False, timestep_conditioning=False, decoder_spatial_padding_mode=PaddingModeType.REFLECT, base_channels=None): super().__init__() decoder_blocks = decoder_blocks or [] self.patch_size = patch_size self.causal = causal self.timestep_conditioning = timestep_conditioning self.per_channel_statistics = PerChannelStatistics(in_channels) self.decode_noise_scale = 0.025 self.decode_timestep = 0.05 if base_channels is not None: feature_ch = base_channels * 8 else: feature_ch = in_channels for name, params in reversed(decoder_blocks): cfg = params if isinstance(params, dict) else {} if name == "res_x_y": feature_ch *= cfg.get("multiplier", 2) if name == "compress_all": feature_ch *= cfg.get("multiplier", 1) self.conv_in = make_conv_nd(convolution_dimensions, in_channels, feature_ch, 3, 1, 1, causal=True, spatial_padding_mode=decoder_spatial_padding_mode) self.up_blocks = nn.ModuleList() for name, params in reversed(decoder_blocks): cfg = {"num_layers": params} if isinstance(params, int) else params block, feature_ch = _make_decoder_block(name, cfg, feature_ch, convolution_dimensions, norm_layer, timestep_conditioning, 32, decoder_spatial_padding_mode) self.up_blocks.append(block) self.conv_norm_out = nn.GroupNorm(32, feature_ch, 1e-6) if norm_layer == NormLayerType.GROUP_NORM else PixelNorm() self.conv_act = nn.SiLU() self.conv_out = make_conv_nd(convolution_dimensions, feature_ch, out_channels * patch_size**2, 3, 1, 1, causal=True, spatial_padding_mode=decoder_spatial_padding_mode) if timestep_conditioning: self.timestep_scale_multiplier = nn.Parameter(torch.tensor(1000.0)) self.last_time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(feature_ch * 2, 0) self.last_scale_shift_table = nn.Parameter(torch.empty(2, feature_ch)) def forward(self, x, timestep=None, generator=None): bs = x.shape[0] if self.timestep_conditioning: noise = torch.randn(x.size(), generator=generator, dtype=x.dtype, device=x.device) * self.decode_noise_scale x = noise + (1 - self.decode_noise_scale) * x x = self.per_channel_statistics.un_normalize(x) if timestep is None and self.timestep_conditioning: timestep = torch.full((bs,), self.decode_timestep, device=x.device, dtype=x.dtype) x = self.conv_in(x, causal=self.causal) scaled_ts = timestep * self.timestep_scale_multiplier.to(x) if self.timestep_conditioning else None for block in self.up_blocks: if isinstance(block, UNetMidBlock3D): x = block(x, self.causal, scaled_ts if self.timestep_conditioning else None, generator) elif isinstance(block, ResnetBlock3D): x = block(x, self.causal, generator=generator) else: x = block(x, self.causal) x = self.conv_norm_out(x) if self.timestep_conditioning: ts_embed = self.last_time_embedder(scaled_ts.flatten(), x.dtype).view(bs, -1, 1, 1, 1) ada = self.last_scale_shift_table[None, ..., None, None, None].to(x) + ts_embed.reshape(bs, 2, -1, 1, 1, 1) shift, scale = ada.unbind(1) x = x * (1 + scale) + shift x = self.conv_act(x) x = self.conv_out(x, causal=self.causal) return unpatchify(x, self.patch_size, 1) # ============================================================ # ============================================================ def create_video_encoder(config): """Build a VideoEncoder from a config.""" cfg = config.get("vae", {}) base_channels = cfg.get("encoder_base_channels", None) padding_mode = cfg.get("encoder_spatial_padding_mode", cfg.get("spatial_padding_mode", "zeros")) return VideoEncoder( convolution_dimensions=cfg.get("dims", 3), in_channels=cfg.get("in_channels", 3), out_channels=cfg.get("latent_channels", 128), encoder_blocks=cfg.get("encoder_blocks", []), patch_size=cfg.get("patch_size", 4), norm_layer=NormLayerType(cfg.get("norm_layer", "pixel_norm")), latent_log_var=LogVarianceType(cfg.get("latent_log_var", "uniform")), encoder_spatial_padding_mode=PaddingModeType(padding_mode), base_channels=base_channels, ) def create_video_decoder(config): """Build a VideoDecoder from a config.""" cfg = config.get("vae", {}) base_channels = cfg.get("decoder_base_channels", None) padding_mode = cfg.get("decoder_spatial_padding_mode", cfg.get("spatial_padding_mode", "reflect")) return VideoDecoder( convolution_dimensions=cfg.get("dims", 3), in_channels=cfg.get("latent_channels", 128), out_channels=cfg.get("out_channels", 3), decoder_blocks=cfg.get("decoder_blocks", []), patch_size=cfg.get("patch_size", 4), norm_layer=NormLayerType(cfg.get("norm_layer", "pixel_norm")), causal=cfg.get("causal_decoder", False), timestep_conditioning=cfg.get("timestep_conditioning", False), decoder_spatial_padding_mode=PaddingModeType(padding_mode), base_channels=base_channels, ) # ============================================================ # ============================================================ class LTX2VAE(nn.Module): """High-level LTX-2 VAE wrapper for the inference pipeline.""" SCALE_FACTORS = (8, 32, 32) # temporal, height, width def __init__(self, device=None, dtype=torch.bfloat16): super().__init__() self._device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu") self.dtype = dtype self._encoder = None self._decoder = None self.scale_factors = self.SCALE_FACTORS @property def device(self): return self._device def set_encoder(self, encoder): """Set up the encoder.""" self._encoder = encoder def set_decoder(self, decoder): """Set up the decoder.""" self._decoder = decoder def encode(self, pixel): """Encode pixels into latents.""" if self._encoder is None: raise RuntimeError("Encoder not set") # [B, F, C, H, W] -> [B, C, F, H, W] if pixel.dim() == 5 and pixel.shape[2] == 3: pixel = pixel.permute(0, 2, 1, 3, 4) with torch.no_grad(): latent = self._encoder(pixel.to(self.dtype)) # [B, C, F, H, W] -> [B, F, C, H, W] return latent.permute(0, 2, 1, 3, 4) def decode(self, latent, generator=None): """Decode latents into pixels.""" if self._decoder is None: raise RuntimeError("Decoder not set") # [B, F, C, H, W] -> [B, C, F, H, W] if latent.dim() == 5 and latent.shape[2] != 3: latent = latent.permute(0, 2, 1, 3, 4) with torch.no_grad(): pixel = self._decoder(latent.to(self.dtype)) # [B, C, F, H, W] -> [B, F, C, H, W] return pixel.permute(0, 2, 1, 3, 4)