Files
2026-07-13 13:09:03 +08:00

2296 lines
90 KiB
Python

# Copyright 2025 The Lightricks team and The HuggingFace Team.
# All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""LTX-2 Video VAE with causal encoding/decoding and streaming cache support.
This module implements a 3D variational autoencoder for video compression with support for:
- Causal temporal convolution (future frames depend only on past frames)
- Streaming decode with persistent feature cache for memory-efficient long video processing
- Bidirectional encoding/decoding mode
Key classes:
- AutoencoderKLCausalLTX2Video: Main VAE model with cache management
- DecoderCacheManager: Manages decoder cache state for streaming inference
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Iterator
import torch
import torch.nn as nn
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.loaders import FromOriginalModelMixin
from diffusers.models.activations import get_activation
from diffusers.models.autoencoders.vae import AutoencoderMixin, DecoderOutput, DiagonalGaussianDistribution
from diffusers.models.embeddings import PixArtAlphaCombinedTimestepSizeEmbeddings
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from diffusers.models.modeling_utils import ModelMixin
from diffusers.utils.accelerate_utils import apply_forward_hook
# =============================================================================
# Utility Functions
# =============================================================================
def _shape_of(x) -> tuple | None:
"""Get shape of tensor if it is a torch.Tensor."""
if isinstance(x, torch.Tensor):
return tuple(x.shape)
return None
def _compute_conv_output_size(in_size: int, k: int, s: int, p: int, d: int) -> int:
"""Compute output size of a convolution operation."""
return max((in_size + 2 * p - d * (k - 1) - 1) // s + 1, 0)
# =============================================================================
# Cache Management Classes
# =============================================================================
@dataclass
class DecoderCacheState:
"""State container for decoder streaming cache.
Attributes:
feat_map: Per-layer feature cache for causal convolution
is_first_chunk: Whether this is the first chunk in the sequence
prev_latent_tail: Previous chunk's latent tail for context
cache_mode: Current cache mode (causal or not)
"""
feat_map: list = field(default_factory=list)
is_first_chunk: bool = True
prev_latent_tail: torch.Tensor | None = None
cache_mode: bool | None = None
class DecoderCacheManager:
"""Manages decoder cache state for streaming video decoding.
This class encapsulates all decoder cache logic, providing a clean interface
for cache operations used during streaming decode of long videos.
Example:
>>> manager = DecoderCacheManager()
>>> manager.clear() # Reset cache
>>> # In streaming loop:
>>> z_chunk = manager.prepend_context(z_chunk, prepend_frames=1)
>>> decoded = decoder(z_chunk, feat_cache=manager.feat_map, feat_idx=[0])
>>> decoded = manager.trim_output(decoded, prepend_frames=1, chunk_frames=z.shape[2])
>>> manager.update_tail(z_chunk, prepend_frames=1)
"""
def __init__(self):
self._state = DecoderCacheState()
@property
def feat_map(self) -> list:
"""Get the feature cache map."""
return self._state.feat_map
@property
def is_first_chunk(self) -> bool:
"""Check if this is the first chunk."""
return self._state.is_first_chunk
@property
def prev_latent_tail(self) -> torch.Tensor | None:
"""Get previous latent tail."""
return self._state.prev_latent_tail
@property
def cache_mode(self) -> bool | None:
"""Get current cache mode."""
return self._state.cache_mode
def clear(self) -> None:
"""Clear all cache state."""
self._state = DecoderCacheState()
def validate_mode(self, causal: bool) -> None:
"""Validate and update cache mode.
Args:
causal: Desired causal mode
Raises:
ValueError: If mode conflicts with existing cache
"""
if self._state.cache_mode is None:
self._state.cache_mode = causal
elif self._state.cache_mode != causal:
# Mode mismatch - clear cache to avoid mixing states
self.clear()
self._state.cache_mode = causal
def prepend_context(
self, z: torch.Tensor, prepend_prev_latent_frames: int, temporal_compression_ratio: int
) -> torch.Tensor:
"""Prepend previous chunk's latent tail as left context.
Args:
z: Current latent chunk [B, C, T, H, W]
prepend_prev_latent_frames: Number of frames to prepend
temporal_compression_ratio: Ratio for converting latent to sample frames
Returns:
Latent tensor with context prepended
"""
if prepend_prev_latent_frames <= 0:
return z
prev_tail = self._state.prev_latent_tail
if prev_tail is None:
# First chunk: repeat first frame
left_ctx = z[:, :, :1, :, :].repeat(1, 1, prepend_prev_latent_frames, 1, 1)
else:
# Use previous chunk's tail
left_ctx = prev_tail.to(z.device)
if left_ctx.shape[2] > prepend_prev_latent_frames:
left_ctx = left_ctx[:, :, -prepend_prev_latent_frames:, :, :]
if left_ctx.shape[2] < prepend_prev_latent_frames:
fill = z[:, :, :1, :, :].repeat(1, 1, prepend_prev_latent_frames - left_ctx.shape[2], 1, 1)
left_ctx = torch.cat([fill, left_ctx], dim=2)
return torch.cat([left_ctx, z], dim=2)
def trim_output(
self,
decoded: torch.Tensor,
prepend_prev_latent_frames: int,
chunk_latent_frames: int,
temporal_compression_ratio: int,
) -> torch.Tensor:
"""Trim decoder output to remove context frames.
Args:
decoded: Full decoder output [B, C, T, H, W]
prepend_prev_latent_frames: Number of prepended latent frames
chunk_latent_frames: Number of latent frames in current chunk
temporal_compression_ratio: Ratio for converting latent to sample frames
Returns:
Trimmed output tensor
"""
drop_left_t = prepend_prev_latent_frames * temporal_compression_ratio
if self._state.is_first_chunk:
# First chunk: keep all frames from context start
keep_t = (chunk_latent_frames - 1) * temporal_compression_ratio + 1
self._state.is_first_chunk = False
else:
# Subsequent chunks: keep only new frames
keep_t = chunk_latent_frames * temporal_compression_ratio
return decoded[:, :, drop_left_t : drop_left_t + keep_t, :, :]
def update_tail(self, z: torch.Tensor, prepend_prev_latent_frames: int) -> None:
"""Update the previous latent tail for next chunk.
Args:
z: Current latent chunk before context prepending
prepend_prev_latent_frames: Number of frames to save as tail
"""
if prepend_prev_latent_frames > 0:
self._state.prev_latent_tail = z[:, :, -prepend_prev_latent_frames:, :, :].clone()
class EncoderCacheManager:
"""Manages encoder cache state for streaming video encoding."""
def __init__(self):
self._feat_map: list = []
@property
def feat_map(self) -> list:
"""Get the feature cache map."""
return self._feat_map
def clear(self) -> None:
"""Clear all cache state."""
self._feat_map = []
def check_pending_consumed(self) -> None:
"""Verify that all temporal grouping state has been fully consumed."""
for state in self._feat_map:
if isinstance(state, dict) and state.get("pending") is not None:
pending = state["pending"]
if isinstance(pending, torch.Tensor) and pending.shape[2] > 0:
raise RuntimeError(
"Encoder ended with non-empty temporal pending state. "
"Use chunk sizes aligned with temporal downsampling or process more frames."
)
# =============================================================================
# Normalization Layers
# =============================================================================
class PerChannelRMSNorm(nn.Module):
"""Per-pixel (per-location) RMS normalization layer.
For each element along the chosen dimension, this layer normalizes the tensor by the root-mean-square of its values
across that dimension:
y = x / sqrt(mean(x^2, dim=dim, keepdim=True) + eps)
Args:
channel_dim: Dimension along which to compute the RMS (typically channels).
eps: Small constant added for numerical stability.
"""
def __init__(self, channel_dim: int = 1, eps: float = 1e-8) -> None:
super().__init__()
self.channel_dim = channel_dim
self.eps = eps
def forward(self, x: torch.Tensor, channel_dim: int | None = None) -> torch.Tensor:
"""Apply RMS normalization along the configured dimension."""
channel_dim = channel_dim or self.channel_dim
mean_sq = torch.mean(x**2, dim=self.channel_dim, keepdim=True)
rms = torch.sqrt(mean_sq + self.eps)
return x / rms
# =============================================================================
# Causal Convolution Layer
# =============================================================================
class LTX2VideoCausalConv3d(nn.Module):
"""Causal 3D convolution for video processing.
Like LTXCausalConv3d, but whether causal inference is performed can be
specified at runtime via the `causal` parameter.
Args:
in_channels: Number of input channels
out_channels: Number of output channels
kernel_size: Temporal, height, width kernel size (int or tuple)
stride: Stride for convolution (int or tuple)
dilation: Dilation for convolution (int or tuple)
groups: Number of groups for grouped convolution
spatial_padding_mode: Padding mode for spatial dimensions
"""
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int] = 3,
stride: int | tuple[int, int, int] = 1,
dilation: int | tuple[int, int, int] = 1,
groups: int = 1,
spatial_padding_mode: str = "zeros",
):
super().__init__()
self.in_channels = in_channels
self.out_channels = out_channels
self.kernel_size = kernel_size if isinstance(kernel_size, tuple) else (kernel_size, kernel_size, kernel_size)
dilation = dilation if isinstance(dilation, tuple) else (dilation, 1, 1)
stride = stride if isinstance(stride, tuple) else (stride, stride, stride)
height_pad = self.kernel_size[1] // 2
width_pad = self.kernel_size[2] // 2
padding = (0, height_pad, width_pad)
self.conv = nn.Conv3d(
in_channels,
out_channels,
self.kernel_size,
stride=stride,
dilation=dilation,
groups=groups,
padding=padding,
padding_mode=spatial_padding_mode,
)
def forward(
self,
hidden_states: torch.Tensor,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
"""Forward pass with optional causal convolution and feature caching.
Args:
hidden_states: Input tensor [B, C, T, H, W]
causal: Whether to use causal convolution
feat_cache: Cache for temporal feature reuse
feat_idx: Current index into feat_cache (mutated in-place)
Returns:
Output tensor after convolution
"""
time_kernel_size = self.kernel_size[0]
input_shape = _shape_of(hidden_states)
# Handle empty temporal chunks
if hidden_states.shape[2] == 0:
return self._handle_empty_chunk(hidden_states, causal, feat_cache, feat_idx, time_kernel_size)
if causal:
hidden_states = self._apply_causal_padding(
hidden_states, feat_cache, feat_idx, time_kernel_size, input_shape
)
else:
hidden_states = self._apply_bidirectional_padding(hidden_states, time_kernel_size)
hidden_states = self.conv(hidden_states)
return hidden_states
def _handle_empty_chunk(
self,
hidden_states: torch.Tensor,
causal: bool,
feat_cache: list | None,
feat_idx: list | None,
time_kernel_size: int,
) -> torch.Tensor:
"""Handle empty temporal chunks by reserving cache slots."""
cache_len = max(time_kernel_size - 1, 0)
if causal and feat_cache is not None and feat_idx is not None and cache_len > 0:
idx = feat_idx[0]
while len(feat_cache) <= idx:
feat_cache.append(None)
feat_idx[0] += 1
batch = hidden_states.shape[0]
out_h = _compute_conv_output_size(
hidden_states.shape[3],
self.conv.kernel_size[1],
self.conv.stride[1],
self.conv.padding[1],
self.conv.dilation[1],
)
out_w = _compute_conv_output_size(
hidden_states.shape[4],
self.conv.kernel_size[2],
self.conv.stride[2],
self.conv.padding[2],
self.conv.dilation[2],
)
return hidden_states.new_empty(batch, self.conv.out_channels, 0, out_h, out_w)
def _apply_causal_padding(
self,
hidden_states: torch.Tensor,
feat_cache: list | None,
feat_idx: list | None,
time_kernel_size: int,
input_shape: tuple | None,
) -> torch.Tensor:
"""Apply causal padding using cached history."""
cache_len = max(time_kernel_size - 1, 0)
if feat_cache is not None and feat_idx is not None and cache_len > 0:
idx = feat_idx[0]
while len(feat_cache) <= idx:
feat_cache.append(None)
prefix = feat_cache[idx]
current_cache = hidden_states[:, :, -cache_len:, :, :].clone()
# Handle short chunks by preserving history
if isinstance(prefix, torch.Tensor) and current_cache.shape[2] < cache_len:
needed = cache_len - current_cache.shape[2]
carry = prefix.to(hidden_states.device)
carry = carry[:, :, -needed:, :, :]
if carry.shape[2] < needed:
fill = carry[:, :, :1, :, :].repeat((1, 1, needed - carry.shape[2], 1, 1))
carry = torch.cat([fill, carry], dim=2)
current_cache = torch.cat([carry, current_cache], dim=2)
# Prepare prefix from cache or fallback
if prefix is not None:
prefix = prefix.to(hidden_states.device)
if prefix.shape[2] > cache_len:
prefix = prefix[:, :, -cache_len:, :, :]
if prefix.shape[2] < cache_len:
# Extend by repeating oldest cached frame
fallback = prefix[:, :, :1, :, :].repeat((1, 1, cache_len - prefix.shape[2], 1, 1))
prefix = torch.concatenate([fallback, prefix], dim=2)
else:
# No cache: use first frame as fallback
prefix = hidden_states[:, :, :1, :, :].repeat((1, 1, cache_len, 1, 1))
hidden_states = torch.concatenate([prefix, hidden_states], dim=2)
feat_cache[idx] = current_cache
feat_idx[0] += 1
else:
# No cache: use causal padding
pad_left = hidden_states[:, :, :1, :, :].repeat((1, 1, cache_len, 1, 1))
hidden_states = torch.concatenate([pad_left, hidden_states], dim=2)
return hidden_states
def _apply_bidirectional_padding(self, hidden_states: torch.Tensor, time_kernel_size: int) -> torch.Tensor:
"""Apply symmetric padding for bidirectional mode."""
pad_size = (time_kernel_size - 1) // 2
pad_left = hidden_states[:, :, :1, :, :].repeat((1, 1, pad_size, 1, 1))
pad_right = hidden_states[:, :, -1:, :, :].repeat((1, 1, pad_size, 1, 1))
return torch.concatenate([pad_left, hidden_states, pad_right], dim=2)
# =============================================================================
# ResNet Block
# =============================================================================
class LTX2VideoResnetBlock3d(nn.Module):
"""A 3D ResNet block used in the LTX 2.0 audiovisual model.
Args:
in_channels: Number of input channels.
out_channels: Number of output channels. If None, defaults to `in_channels`.
dropout: Dropout rate.
eps: Epsilon value for normalization layers.
elementwise_affine: Whether to enable elementwise affinity in normalization.
non_linearity: Activation function to use.
conv_shortcut: Whether to use a convolution shortcut.
"""
def __init__(
self,
in_channels: int,
out_channels: int | None = None,
dropout: float = 0.0,
eps: float = 1e-6,
elementwise_affine: bool = False,
non_linearity: str = "swish",
inject_noise: bool = False,
timestep_conditioning: bool = False,
spatial_padding_mode: str = "zeros",
) -> None:
super().__init__()
out_channels = out_channels or in_channels
self.nonlinearity = get_activation(non_linearity)
self.norm1 = PerChannelRMSNorm()
self.conv1 = LTX2VideoCausalConv3d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=3,
spatial_padding_mode=spatial_padding_mode,
)
self.norm2 = PerChannelRMSNorm()
self.dropout = nn.Dropout(dropout)
self.conv2 = LTX2VideoCausalConv3d(
in_channels=out_channels,
out_channels=out_channels,
kernel_size=3,
spatial_padding_mode=spatial_padding_mode,
)
self.norm3 = None
self.conv_shortcut = None
if in_channels != out_channels:
self.norm3 = nn.LayerNorm(in_channels, eps=eps, elementwise_affine=True, bias=True)
# LTX 2.0 uses a normal nn.Conv3d here rather than LTXVideoCausalConv3d
self.conv_shortcut = nn.Conv3d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, stride=1)
self.per_channel_scale1 = None
self.per_channel_scale2 = None
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))
self.scale_shift_table = None
if timestep_conditioning:
self.scale_shift_table = nn.Parameter(torch.randn(4, in_channels) / in_channels**0.5)
def forward(
self,
inputs: torch.Tensor,
temb: torch.Tensor | None = None,
generator: torch.Generator | None = None,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
"""Forward pass through ResNet block."""
hidden_states = inputs
hidden_states = self.norm1(hidden_states)
if self.scale_shift_table is not None:
temb = temb.unflatten(1, (4, -1)) + self.scale_shift_table[None, ..., None, None, None]
shift_1, scale_1, shift_2, scale_2 = temb.unbind(dim=1)
hidden_states = hidden_states * (1 + scale_1) + shift_1
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.conv1(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
if self.per_channel_scale1 is not None:
spatial_shape = hidden_states.shape[-2:]
spatial_noise = torch.randn(
spatial_shape, generator=generator, device=hidden_states.device, dtype=hidden_states.dtype
)[None]
hidden_states = hidden_states + (spatial_noise * self.per_channel_scale1)[None, :, None, ...]
hidden_states = self.norm2(hidden_states)
if self.scale_shift_table is not None:
hidden_states = hidden_states * (1 + scale_2) + shift_2
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.dropout(hidden_states)
hidden_states = self.conv2(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
if self.per_channel_scale2 is not None:
spatial_shape = hidden_states.shape[-2:]
spatial_noise = torch.randn(
spatial_shape, generator=generator, device=hidden_states.device, dtype=hidden_states.dtype
)[None]
hidden_states = hidden_states + (spatial_noise * self.per_channel_scale2)[None, :, None, ...]
if self.norm3 is not None:
inputs = self.norm3(inputs.movedim(1, -1)).movedim(-1, 1)
if self.conv_shortcut is not None:
inputs = self.conv_shortcut(inputs)
hidden_states = hidden_states + inputs
return hidden_states
# =============================================================================
# Downsampler and Upsampler
# =============================================================================
class LTXVideoDownsampler3d(nn.Module):
"""3D downsampling layer for spatiotemporal reduction.
Uses pixel unshuffle pattern for downsampling with causal temporal handling.
"""
def __init__(
self,
in_channels: int,
out_channels: int,
stride: int | tuple[int, int, int] = 1,
spatial_padding_mode: str = "zeros",
) -> None:
super().__init__()
self.stride = stride if isinstance(stride, tuple) else (stride, stride, stride)
self.group_size = (in_channels * stride[0] * stride[1] * stride[2]) // out_channels
out_channels = out_channels // (self.stride[0] * self.stride[1] * self.stride[2])
self.conv = LTX2VideoCausalConv3d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
def forward(
self,
hidden_states: torch.Tensor,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
"""Forward pass with temporal grouping state management."""
_shape_of(hidden_states)
prefix_len = max(self.stride[0] - 1, 0)
if prefix_len > 0 and feat_cache is not None and feat_idx is not None:
hidden_states = self._manage_temporal_grouping(hidden_states, feat_cache, feat_idx, prefix_len)
else:
hidden_states = torch.cat([hidden_states[:, :, : self.stride[0] - 1], hidden_states], dim=2)
# Handle empty temporal dimension after grouping
if hidden_states.shape[2] == 0:
return self._handle_empty_output(hidden_states, feat_cache, feat_idx)
# Apply pixel unshuffle downsampling
residual = self._compute_residual(hidden_states)
hidden_states = self.conv(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
hidden_states = self._unshuffle_output(hidden_states)
hidden_states = hidden_states + residual
return hidden_states
def _manage_temporal_grouping(
self, hidden_states: torch.Tensor, feat_cache: list, feat_idx: list, prefix_len: int
) -> torch.Tensor:
"""Manage temporal grouping state for chunked processing."""
idx = feat_idx[0]
while len(feat_cache) <= idx:
feat_cache.append(None)
state = feat_cache[idx]
if state is None or not isinstance(state, dict):
state = {"prefixed": False, "pending": None}
sequence = hidden_states
if not state["prefixed"]:
prefix = hidden_states[:, :, :1, :, :].repeat((1, 1, prefix_len, 1, 1))
sequence = torch.cat([prefix, sequence], dim=2)
state["prefixed"] = True
if state["pending"] is not None:
pending = state["pending"].to(sequence.device)
sequence = torch.cat([pending, sequence], dim=2)
stride_t = self.stride[0]
usable_t = (sequence.shape[2] // stride_t) * stride_t
state["pending"] = sequence[:, :, usable_t:, :, :].clone() if usable_t < sequence.shape[2] else None
hidden_states = sequence[:, :, :usable_t, :, :]
feat_cache[idx] = state
feat_idx[0] += 1
return hidden_states
def _handle_empty_output(
self, hidden_states: torch.Tensor, feat_cache: list | None, feat_idx: list | None
) -> torch.Tensor:
"""Handle empty output when temporal dimension is 0."""
if feat_cache is not None and feat_idx is not None:
# Reserve cache slot for conv
idx = feat_idx[0]
while len(feat_cache) <= idx:
feat_cache.append(None)
feat_idx[0] += 1
out_channels = self.conv.out_channels * self.stride[0] * self.stride[1] * self.stride[2]
out_h = hidden_states.shape[3] // self.stride[1]
out_w = hidden_states.shape[4] // self.stride[2]
return hidden_states.new_empty(
hidden_states.shape[0],
out_channels,
0,
out_h,
out_w,
)
def _compute_residual(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Compute residual connection for pixel unshuffle."""
residual = (
hidden_states.unflatten(4, (-1, self.stride[2]))
.unflatten(3, (-1, self.stride[1]))
.unflatten(2, (-1, self.stride[0]))
)
residual = residual.permute(0, 1, 3, 5, 7, 2, 4, 6).flatten(1, 4)
residual = residual.unflatten(1, (-1, self.group_size))
return residual.mean(dim=2)
def _unshuffle_output(self, hidden_states: torch.Tensor) -> torch.Tensor:
"""Apply pixel unshuffle to output."""
hidden_states = (
hidden_states.unflatten(4, (-1, self.stride[2]))
.unflatten(3, (-1, self.stride[1]))
.unflatten(2, (-1, self.stride[0]))
)
hidden_states = hidden_states.permute(0, 1, 3, 5, 7, 2, 4, 6).flatten(1, 4)
return hidden_states
class LTXVideoUpsampler3d(nn.Module):
"""3D upsampling layer for spatiotemporal expansion.
Uses pixel shuffle pattern for upsampling with causal temporal handling.
"""
def __init__(
self,
in_channels: int,
stride: int | tuple[int, int, int] = 1,
residual: bool = False,
upscale_factor: int = 1,
spatial_padding_mode: str = "zeros",
) -> None:
super().__init__()
self.stride = stride if isinstance(stride, tuple) else (stride, stride, stride)
self.residual = residual
self.upscale_factor = upscale_factor
out_channels = (in_channels * stride[0] * stride[1] * stride[2]) // upscale_factor
self.conv = LTX2VideoCausalConv3d(
in_channels=in_channels,
out_channels=out_channels,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
def forward(
self,
hidden_states: torch.Tensor,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
"""Forward pass with trim state management."""
batch_size, num_channels, num_frames, height, width = hidden_states.shape
_shape_of(hidden_states)
trim_t = max(self.stride[0] - 1, 0)
# Determine trim amount based on cache state
if trim_t > 0 and feat_cache is not None and feat_idx is not None:
trim_start = self._manage_trim_state(feat_cache, feat_idx, trim_t)
else:
trim_start = trim_t
# Compute residual if enabled
if self.residual:
residual = self._compute_residual(hidden_states, trim_start)
hidden_states = self.conv(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
hidden_states = self._shuffle_output(hidden_states, trim_start)
if self.residual:
hidden_states = hidden_states + residual
return hidden_states
def _manage_trim_state(self, feat_cache: list, feat_idx: list, trim_t: int) -> int:
"""Manage trim state for removing temporal padding."""
idx = feat_idx[0]
while len(feat_cache) <= idx:
feat_cache.append(None)
state = feat_cache[idx]
if state is None or not isinstance(state, dict):
state = {"trim_applied": False}
trim_start = trim_t if not state["trim_applied"] else 0
state["trim_applied"] = True
feat_cache[idx] = state
feat_idx[0] += 1
return trim_start
def _compute_residual(self, hidden_states: torch.Tensor, trim_start: int) -> torch.Tensor:
"""Compute residual connection."""
batch_size, num_channels, num_frames, height, width = hidden_states.shape
residual = hidden_states.reshape(
batch_size, -1, self.stride[0], self.stride[1], self.stride[2], num_frames, height, width
)
residual = residual.permute(0, 1, 5, 2, 6, 3, 7, 4).flatten(6, 7).flatten(4, 5).flatten(2, 3)
repeats = (self.stride[0] * self.stride[1] * self.stride[2]) // self.upscale_factor
residual = residual.repeat(1, repeats, 1, 1, 1)
return residual[:, :, trim_start:]
def _shuffle_output(self, hidden_states: torch.Tensor, trim_start: int) -> torch.Tensor:
"""Apply pixel shuffle and trim to output."""
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.reshape(
batch_size, -1, self.stride[0], self.stride[1], self.stride[2], num_frames, height, width
)
hidden_states = hidden_states.permute(0, 1, 5, 2, 6, 3, 7, 4).flatten(6, 7).flatten(4, 5).flatten(2, 3)
return hidden_states[:, :, trim_start:]
# =============================================================================
# Encoder/Decoder Blocks
# =============================================================================
class LTX2VideoDownBlock3D(nn.Module):
"""Down block used in the LTXVideo model.
Args:
in_channels: Number of input channels.
out_channels: Number of output channels. If None, defaults to `in_channels`.
num_layers: Number of resnet layers.
dropout: Dropout rate.
resnet_eps: Epsilon value for normalization layers.
resnet_act_fn: Activation function to use.
spatio_temporal_scale: Whether to use downsampling layer.
downsample_type: Type of downsampling ("conv", "spatial", "temporal", "spatiotemporal")
"""
_supports_gradient_checkpointing = True
def __init__(
self,
in_channels: int,
out_channels: int | None = None,
num_layers: int = 1,
dropout: float = 0.0,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "swish",
spatio_temporal_scale: bool = True,
downsample_type: str = "conv",
spatial_padding_mode: str = "zeros",
):
super().__init__()
out_channels = out_channels or in_channels
resnets = []
for _ in range(num_layers):
resnets.append(
LTX2VideoResnetBlock3d(
in_channels=in_channels,
out_channels=in_channels,
dropout=dropout,
eps=resnet_eps,
non_linearity=resnet_act_fn,
spatial_padding_mode=spatial_padding_mode,
)
)
self.resnets = nn.ModuleList(resnets)
self.downsamplers = None
if spatio_temporal_scale:
self.downsamplers = nn.ModuleList()
if downsample_type == "conv":
self.downsamplers.append(
LTX2VideoCausalConv3d(
in_channels=in_channels,
out_channels=in_channels,
kernel_size=3,
stride=(2, 2, 2),
spatial_padding_mode=spatial_padding_mode,
)
)
elif downsample_type == "spatial":
self.downsamplers.append(
LTXVideoDownsampler3d(
in_channels=in_channels,
out_channels=out_channels,
stride=(1, 2, 2),
spatial_padding_mode=spatial_padding_mode,
)
)
elif downsample_type == "temporal":
self.downsamplers.append(
LTXVideoDownsampler3d(
in_channels=in_channels,
out_channels=out_channels,
stride=(2, 1, 1),
spatial_padding_mode=spatial_padding_mode,
)
)
elif downsample_type == "spatiotemporal":
self.downsamplers.append(
LTXVideoDownsampler3d(
in_channels=in_channels,
out_channels=out_channels,
stride=(2, 2, 2),
spatial_padding_mode=spatial_padding_mode,
)
)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
temb: torch.Tensor | None = None,
generator: torch.Generator | None = None,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
for i, resnet in enumerate(self.resnets):
if torch.is_grad_enabled() and self.gradient_checkpointing and feat_cache is None:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states, temb, generator, causal)
else:
hidden_states = resnet(
hidden_states, temb, generator, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx
)
if self.downsamplers is not None:
for downsampler in self.downsamplers:
hidden_states = downsampler(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
return hidden_states
class LTX2VideoMidBlock3d(nn.Module):
"""Middle block used in the LTXVideo model.
Args:
in_channels: Number of input channels.
num_layers: Number of resnet layers.
dropout: Dropout rate.
resnet_eps: Epsilon value for normalization layers.
resnet_act_fn: Activation function to use.
"""
_supports_gradient_checkpointing = True
def __init__(
self,
in_channels: int,
num_layers: int = 1,
dropout: float = 0.0,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "swish",
inject_noise: bool = False,
timestep_conditioning: bool = False,
spatial_padding_mode: str = "zeros",
) -> None:
super().__init__()
self.time_embedder = None
if timestep_conditioning:
self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(in_channels * 4, 0)
resnets = []
for _ in range(num_layers):
resnets.append(
LTX2VideoResnetBlock3d(
in_channels=in_channels,
out_channels=in_channels,
dropout=dropout,
eps=resnet_eps,
non_linearity=resnet_act_fn,
inject_noise=inject_noise,
timestep_conditioning=timestep_conditioning,
spatial_padding_mode=spatial_padding_mode,
)
)
self.resnets = nn.ModuleList(resnets)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
temb: torch.Tensor | None = None,
generator: torch.Generator | None = None,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
if self.time_embedder is not None:
temb = self.time_embedder(
timestep=temb.flatten(),
resolution=None,
aspect_ratio=None,
batch_size=hidden_states.size(0),
hidden_dtype=hidden_states.dtype,
)
temb = temb.view(hidden_states.size(0), -1, 1, 1, 1)
for i, resnet in enumerate(self.resnets):
if torch.is_grad_enabled() and self.gradient_checkpointing and feat_cache is None:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states, temb, generator, causal)
else:
hidden_states = resnet(
hidden_states, temb, generator, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx
)
return hidden_states
class LTX2VideoUpBlock3d(nn.Module):
"""Up block used in the LTXVideo model.
Args:
in_channels: Number of input channels.
out_channels: Number of output channels. If None, defaults to `in_channels`.
num_layers: Number of resnet layers.
dropout: Dropout rate.
resnet_eps: Epsilon value for normalization layers.
resnet_act_fn: Activation function to use.
spatio_temporal_scale: Whether to use upsampling layer.
"""
_supports_gradient_checkpointing = True
def __init__(
self,
in_channels: int,
out_channels: int | None = None,
num_layers: int = 1,
dropout: float = 0.0,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "swish",
spatio_temporal_scale: bool = True,
inject_noise: bool = False,
timestep_conditioning: bool = False,
upsample_residual: bool = False,
upscale_factor: int = 1,
spatial_padding_mode: str = "zeros",
upsample_stride: tuple[int, int, int] = (2, 2, 2),
):
super().__init__()
out_channels = out_channels or in_channels
self.time_embedder = None
if timestep_conditioning:
self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(in_channels * 4, 0)
self.conv_in = None
if in_channels != out_channels:
self.conv_in = LTX2VideoResnetBlock3d(
in_channels=in_channels,
out_channels=out_channels,
dropout=dropout,
eps=resnet_eps,
non_linearity=resnet_act_fn,
inject_noise=inject_noise,
timestep_conditioning=timestep_conditioning,
spatial_padding_mode=spatial_padding_mode,
)
self.upsamplers = None
if spatio_temporal_scale:
self.upsamplers = nn.ModuleList(
[
LTXVideoUpsampler3d(
out_channels * upscale_factor,
stride=upsample_stride,
residual=upsample_residual,
upscale_factor=upscale_factor,
spatial_padding_mode=spatial_padding_mode,
)
]
)
resnets = []
for _ in range(num_layers):
resnets.append(
LTX2VideoResnetBlock3d(
in_channels=out_channels,
out_channels=out_channels,
dropout=dropout,
eps=resnet_eps,
non_linearity=resnet_act_fn,
inject_noise=inject_noise,
timestep_conditioning=timestep_conditioning,
spatial_padding_mode=spatial_padding_mode,
)
)
self.resnets = nn.ModuleList(resnets)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
temb: torch.Tensor | None = None,
generator: torch.Generator | None = None,
causal: bool = True,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
if self.conv_in is not None:
hidden_states = self.conv_in(
hidden_states, temb, generator, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx
)
if self.time_embedder is not None:
temb = self.time_embedder(
timestep=temb.flatten(),
resolution=None,
aspect_ratio=None,
batch_size=hidden_states.size(0),
hidden_dtype=hidden_states.dtype,
)
temb = temb.view(hidden_states.size(0), -1, 1, 1, 1)
if self.upsamplers is not None:
for upsampler in self.upsamplers:
hidden_states = upsampler(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
for i, resnet in enumerate(self.resnets):
if torch.is_grad_enabled() and self.gradient_checkpointing and feat_cache is None:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states, temb, generator, causal)
else:
hidden_states = resnet(
hidden_states, temb, generator, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx
)
return hidden_states
# =============================================================================
# Encoder
# =============================================================================
class LTX2VideoEncoder3d(nn.Module):
"""Encoder layer for encoding video samples to latent representation.
Args:
in_channels: Number of input channels.
out_channels: Number of latent channels.
block_out_channels: Number of output channels for each block.
spatio_temporal_scaling: Whether each block should contain downscaling.
layers_per_block: Number of layers per block.
downsample_type: Downsampling pattern per block.
patch_size: Size of spatial patches.
patch_size_t: Size of temporal patches.
resnet_norm_eps: Epsilon for ResNet normalization.
is_causal: Whether to use causal behavior.
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 128,
block_out_channels: tuple[int, ...] = (256, 512, 1024, 2048),
down_block_types: tuple[str, ...] = (
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
),
spatio_temporal_scaling: tuple[bool, ...] = (True, True, True, True),
layers_per_block: tuple[int, ...] = (4, 6, 6, 2, 2),
downsample_type: tuple[str, ...] = ("spatial", "temporal", "spatiotemporal", "spatiotemporal"),
patch_size: int = 4,
patch_size_t: int = 1,
resnet_norm_eps: float = 1e-6,
is_causal: bool = True,
spatial_padding_mode: str = "zeros",
):
super().__init__()
self.patch_size = patch_size
self.patch_size_t = patch_size_t
self.in_channels = in_channels * patch_size**2 * patch_size_t
self.is_causal = is_causal
output_channel = out_channels
self.conv_in = LTX2VideoCausalConv3d(
in_channels=self.in_channels,
out_channels=output_channel,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
# down blocks
num_block_out_channels = len(block_out_channels)
self.down_blocks = nn.ModuleList([])
for i in range(num_block_out_channels):
input_channel = output_channel
output_channel = block_out_channels[i]
if down_block_types[i] == "LTX2VideoDownBlock3D":
down_block = LTX2VideoDownBlock3D(
in_channels=input_channel,
out_channels=output_channel,
num_layers=layers_per_block[i],
resnet_eps=resnet_norm_eps,
spatio_temporal_scale=spatio_temporal_scaling[i],
downsample_type=downsample_type[i],
spatial_padding_mode=spatial_padding_mode,
)
else:
raise ValueError(f"Unknown down block type: {down_block_types[i]}")
self.down_blocks.append(down_block)
# mid block
self.mid_block = LTX2VideoMidBlock3d(
in_channels=output_channel,
num_layers=layers_per_block[-1],
resnet_eps=resnet_norm_eps,
spatial_padding_mode=spatial_padding_mode,
)
# out
self.norm_out = PerChannelRMSNorm()
self.conv_act = nn.SiLU()
self.conv_out = LTX2VideoCausalConv3d(
in_channels=output_channel,
out_channels=out_channels + 1,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
causal: bool | None = None,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
p = self.patch_size
p_t = self.patch_size_t
batch_size, num_channels, num_frames, height, width = hidden_states.shape
post_patch_num_frames = num_frames // p_t
post_patch_height = height // p
post_patch_width = width // p
causal = causal or self.is_causal
if feat_cache is not None and feat_idx is None:
feat_idx = [0]
hidden_states = hidden_states.reshape(
batch_size, num_channels, post_patch_num_frames, p_t, post_patch_height, p, post_patch_width, p
)
hidden_states = hidden_states.permute(0, 1, 3, 7, 5, 2, 4, 6).flatten(1, 4)
hidden_states = self.conv_in(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
if torch.is_grad_enabled() and self.gradient_checkpointing and feat_cache is None:
for down_block in self.down_blocks:
hidden_states = self._gradient_checkpointing_func(down_block, hidden_states, None, None, causal)
hidden_states = self._gradient_checkpointing_func(self.mid_block, hidden_states, None, None, causal)
else:
for down_block in self.down_blocks:
hidden_states = down_block(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
hidden_states = self.mid_block(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
hidden_states = self.norm_out(hidden_states)
hidden_states = self.conv_act(hidden_states)
hidden_states = self.conv_out(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
last_channel = hidden_states[:, -1:]
last_channel = last_channel.repeat(1, hidden_states.size(1) - 2, 1, 1, 1)
hidden_states = torch.cat([hidden_states, last_channel], dim=1)
return hidden_states
# =============================================================================
# Decoder
# =============================================================================
class LTX2VideoDecoder3d(nn.Module):
"""Decoder layer for decoding latent representation to video.
Args:
in_channels: Number of latent channels.
out_channels: Number of output channels.
block_out_channels: Number of output channels for each block.
spatio_temporal_scaling: Whether each block should contain upscaling.
layers_per_block: Number of layers per block.
patch_size: Size of spatial patches.
patch_size_t: Size of temporal patches.
resnet_norm_eps: Epsilon for ResNet normalization.
is_causal: Whether to use causal behavior.
"""
_UPSAMPLE_STRIDE_MAP = {
"spatial": (1, 2, 2),
"temporal": (2, 1, 1),
"spatiotemporal": (2, 2, 2),
}
def __init__(
self,
in_channels: int = 128,
out_channels: int = 3,
block_out_channels: tuple[int, ...] = (256, 512, 1024),
spatio_temporal_scaling: tuple[bool, ...] = (True, True, True),
layers_per_block: tuple[int, ...] = (5, 5, 5, 5),
patch_size: int = 4,
patch_size_t: int = 1,
resnet_norm_eps: float = 1e-6,
is_causal: bool = False,
inject_noise: tuple[bool, ...] = (False, False, False),
timestep_conditioning: bool = False,
upsample_residual: tuple[bool, ...] = (True, True, True),
upsample_factor: tuple[bool, ...] = (2, 2, 2),
upsample_type: tuple[str, ...] | None = None,
spatial_padding_mode: str = "reflect",
) -> None:
super().__init__()
self.patch_size = patch_size
self.patch_size_t = patch_size_t
self.out_channels = out_channels * patch_size**2
self.is_causal = is_causal
block_out_channels = tuple(reversed(block_out_channels))
spatio_temporal_scaling = tuple(reversed(spatio_temporal_scaling))
layers_per_block = tuple(reversed(layers_per_block))
inject_noise = tuple(reversed(inject_noise))
upsample_residual = tuple(reversed(upsample_residual))
upsample_factor = tuple(reversed(upsample_factor))
if upsample_type is not None:
upsample_type = tuple(reversed(upsample_type))
output_channel = block_out_channels[0]
self.conv_in = LTX2VideoCausalConv3d(
in_channels=in_channels,
out_channels=output_channel,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
self.mid_block = LTX2VideoMidBlock3d(
in_channels=output_channel,
num_layers=layers_per_block[0],
resnet_eps=resnet_norm_eps,
inject_noise=inject_noise[0],
timestep_conditioning=timestep_conditioning,
spatial_padding_mode=spatial_padding_mode,
)
# up blocks
num_block_out_channels = len(block_out_channels)
self.up_blocks = nn.ModuleList([])
for i in range(num_block_out_channels):
input_channel = output_channel // upsample_factor[i]
output_channel = block_out_channels[i] // upsample_factor[i]
stride = (2, 2, 2)
if upsample_type is not None:
stride = self._UPSAMPLE_STRIDE_MAP[upsample_type[i]]
up_block = LTX2VideoUpBlock3d(
in_channels=input_channel,
out_channels=output_channel,
num_layers=layers_per_block[i + 1],
resnet_eps=resnet_norm_eps,
spatio_temporal_scale=spatio_temporal_scaling[i],
inject_noise=inject_noise[i + 1],
timestep_conditioning=timestep_conditioning,
upsample_residual=upsample_residual[i],
upscale_factor=upsample_factor[i],
spatial_padding_mode=spatial_padding_mode,
upsample_stride=stride,
)
self.up_blocks.append(up_block)
# out
self.norm_out = PerChannelRMSNorm()
self.conv_act = nn.SiLU()
self.conv_out = LTX2VideoCausalConv3d(
in_channels=output_channel,
out_channels=self.out_channels,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
# timestep embedding
self.time_embedder = None
self.scale_shift_table = None
self.timestep_scale_multiplier = None
if timestep_conditioning:
self.timestep_scale_multiplier = nn.Parameter(torch.tensor(1000.0, dtype=torch.float32))
self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(output_channel * 2, 0)
self.scale_shift_table = nn.Parameter(torch.randn(2, output_channel) / output_channel**0.5)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
temb: torch.Tensor | None = None,
causal: bool | None = None,
feat_cache: list | None = None,
feat_idx: list | None = None,
) -> torch.Tensor:
causal = causal or self.is_causal
_shape_of(hidden_states)
if feat_cache is not None and feat_idx is None:
feat_idx = [0]
hidden_states = self.conv_in(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
if self.timestep_scale_multiplier is not None:
temb = temb * self.timestep_scale_multiplier
if torch.is_grad_enabled() and self.gradient_checkpointing and feat_cache is None:
hidden_states = self._gradient_checkpointing_func(self.mid_block, hidden_states, temb, None, causal)
for up_block in self.up_blocks:
hidden_states = self._gradient_checkpointing_func(up_block, hidden_states, temb, None, causal)
else:
hidden_states = self.mid_block(hidden_states, temb, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
for up_block in self.up_blocks:
hidden_states = up_block(hidden_states, temb, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
hidden_states = self.norm_out(hidden_states)
if self.time_embedder is not None:
temb = self.time_embedder(
timestep=temb.flatten(),
resolution=None,
aspect_ratio=None,
batch_size=hidden_states.size(0),
hidden_dtype=hidden_states.dtype,
)
temb = temb.view(hidden_states.size(0), -1, 1, 1, 1).unflatten(1, (2, -1))
temb = temb + self.scale_shift_table[None, ..., None, None, None]
shift, scale = temb.unbind(dim=1)
hidden_states = hidden_states * (1 + scale) + shift
hidden_states = self.conv_act(hidden_states)
hidden_states = self.conv_out(hidden_states, causal=causal, feat_cache=feat_cache, feat_idx=feat_idx)
p = self.patch_size
p_t = self.patch_size_t
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.reshape(batch_size, -1, p_t, p, p, num_frames, height, width)
hidden_states = hidden_states.permute(0, 1, 5, 2, 6, 4, 7, 3).flatten(6, 7).flatten(4, 5).flatten(2, 3)
return hidden_states
# =============================================================================
# Main VAE Model
# =============================================================================
class AutoencoderKLCausalLTX2Video(ModelMixin, AutoencoderMixin, ConfigMixin, FromOriginalModelMixin):
"""A VAE model with KL loss for encoding images into latents and decoding latent representations into images.
Used in LTX-2. Supports causal encoding/decoding with streaming cache for memory-efficient
processing of long videos.
Args:
in_channels: Number of input channels.
out_channels: Number of output channels.
latent_channels: Number of latent channels.
block_out_channels: Number of output channels for each encoder block.
decoder_block_out_channels: Number of output channels for each decoder block.
layers_per_block: Number of layers per encoder block.
decoder_layers_per_block: Number of layers per decoder block.
spatio_temporal_scaling: Whether each encoder block should downscale.
decoder_spatio_temporal_scaling: Whether each decoder block should upscale.
downsample_type: Downsampling pattern for encoder.
upsample_residual: Whether to use residual in decoder upsampling.
upsample_factor: Upsampling factor for each decoder block.
timestep_conditioning: Whether to condition on timesteps.
patch_size: Size of spatial patches.
patch_size_t: Size of temporal patches.
resnet_norm_eps: Epsilon for ResNet normalization.
scaling_factor: Factor to scale latents.
encoder_causal: Whether encoder should be causal.
decoder_causal: Whether decoder should be causal.
gradient_checkpointing: Whether to use gradient checkpointing.
"""
_supports_gradient_checkpointing = True
@register_to_config
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
latent_channels: int = 128,
block_out_channels: tuple[int, ...] = (256, 512, 1024, 2048),
down_block_types: tuple[str, ...] = (
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
"LTX2VideoDownBlock3D",
),
decoder_block_out_channels: tuple[int, ...] = (256, 512, 1024),
layers_per_block: tuple[int, ...] = (4, 6, 6, 2, 2),
decoder_layers_per_block: tuple[int, ...] = (5, 5, 5, 5),
spatio_temporal_scaling: tuple[bool, ...] = (True, True, True, True),
decoder_spatio_temporal_scaling: tuple[bool, ...] = (True, True, True),
decoder_inject_noise: tuple[bool, ...] = (False, False, False, False),
downsample_type: tuple[str, ...] = ("spatial", "temporal", "spatiotemporal", "spatiotemporal"),
upsample_residual: tuple[bool, ...] = (True, True, True),
upsample_factor: tuple[int, ...] = (2, 2, 2),
decoder_upsample_type: tuple[str, ...] | None = None,
timestep_conditioning: bool = False,
patch_size: int = 4,
patch_size_t: int = 1,
resnet_norm_eps: float = 1e-6,
scaling_factor: float = 1.0,
encoder_causal: bool = True,
decoder_causal: bool = True,
gradient_checkpointing: bool = False,
encoder_spatial_padding_mode: str = "zeros",
decoder_spatial_padding_mode: str = "reflect",
spatial_compression_ratio: int = None,
temporal_compression_ratio: int = None,
) -> None:
super().__init__()
self.encoder = LTX2VideoEncoder3d(
in_channels=in_channels,
out_channels=latent_channels,
block_out_channels=block_out_channels,
down_block_types=down_block_types,
spatio_temporal_scaling=spatio_temporal_scaling,
layers_per_block=layers_per_block,
downsample_type=downsample_type,
patch_size=patch_size,
patch_size_t=patch_size_t,
resnet_norm_eps=resnet_norm_eps,
is_causal=encoder_causal,
spatial_padding_mode=encoder_spatial_padding_mode,
)
self.decoder = LTX2VideoDecoder3d(
in_channels=latent_channels,
out_channels=out_channels,
block_out_channels=decoder_block_out_channels,
spatio_temporal_scaling=decoder_spatio_temporal_scaling,
layers_per_block=decoder_layers_per_block,
patch_size=patch_size,
patch_size_t=patch_size_t,
resnet_norm_eps=resnet_norm_eps,
is_causal=decoder_causal,
timestep_conditioning=timestep_conditioning,
inject_noise=decoder_inject_noise,
upsample_residual=upsample_residual,
upsample_factor=upsample_factor,
upsample_type=decoder_upsample_type,
spatial_padding_mode=decoder_spatial_padding_mode,
)
latents_mean = torch.zeros((latent_channels,), requires_grad=False)
latents_std = torch.ones((latent_channels,), requires_grad=False)
self.register_buffer("latents_mean", latents_mean, persistent=True)
self.register_buffer("latents_std", latents_std, persistent=True)
self.spatial_compression_ratio = (
patch_size * 2 ** sum(spatio_temporal_scaling)
if spatial_compression_ratio is None
else spatial_compression_ratio
)
self.temporal_compression_ratio = (
patch_size_t * 2 ** sum(spatio_temporal_scaling)
if temporal_compression_ratio is None
else temporal_compression_ratio
)
# Memory optimization flags
self.use_slicing = False
self.use_tiling = False
self.use_framewise_encoding = False
self.use_framewise_decoding = False
# Tiling configuration
self.num_sample_frames_batch_size = 16
self.num_latent_frames_batch_size = 2
self.tile_sample_min_height = 512
self.tile_sample_min_width = 512
self.tile_sample_min_num_frames = 16
self.tile_sample_stride_height = 448
self.tile_sample_stride_width = 448
self.tile_sample_stride_num_frames = 8
# Cache managers for streaming inference
self._encoder_cache = EncoderCacheManager()
self._decoder_cache = DecoderCacheManager()
# Expose config-driven gradient checkpointing toggle for training
self.gradient_checkpointing = gradient_checkpointing
if gradient_checkpointing:
self.enable_gradient_checkpointing()
def enable_tiling(
self,
tile_sample_min_height: int | None = None,
tile_sample_min_width: int | None = None,
tile_sample_min_num_frames: int | None = None,
tile_sample_stride_height: float | None = None,
tile_sample_stride_width: float | None = None,
tile_sample_stride_num_frames: float | None = None,
) -> None:
"""Enable tiled VAE decoding for memory-efficient processing of large videos."""
self.use_tiling = True
self.tile_sample_min_height = tile_sample_min_height or self.tile_sample_min_height
self.tile_sample_min_width = tile_sample_min_width or self.tile_sample_min_width
self.tile_sample_min_num_frames = tile_sample_min_num_frames or self.tile_sample_min_num_frames
self.tile_sample_stride_height = tile_sample_stride_height or self.tile_sample_stride_height
self.tile_sample_stride_width = tile_sample_stride_width or self.tile_sample_stride_width
self.tile_sample_stride_num_frames = tile_sample_stride_num_frames or self.tile_sample_stride_num_frames
def _encode(self, x: torch.Tensor, causal: bool | None = None) -> torch.Tensor:
"""Internal encode method."""
batch_size, num_channels, num_frames, height, width = x.shape
if self.use_framewise_decoding and num_frames > self.tile_sample_min_num_frames:
return self._temporal_tiled_encode(x, causal=causal)
if self.use_tiling and (width > self.tile_sample_min_width or height > self.tile_sample_min_height):
return self.tiled_encode(x, causal=causal)
enc = self.encoder(x, causal=causal)
return enc
# -------------------------------------------------------------------------
# Cache Management (Legacy API for backward compatibility)
# -------------------------------------------------------------------------
def clear_encoder_cache(self) -> None:
"""Clear encoder cache (legacy API, delegates to cache manager)."""
self._encoder_cache.clear()
def clear_decoder_cache(self) -> None:
"""Clear decoder cache (legacy API, delegates to cache manager)."""
self._decoder_cache.clear()
@property
def _encoder_feat_map(self) -> list:
"""Legacy property for encoder feature map."""
return self._encoder_cache.feat_map
@_encoder_feat_map.setter
def _encoder_feat_map(self, value: list):
"""Setter for legacy encoder feature map property."""
self._encoder_cache._feat_map = value
# -------------------------------------------------------------------------
# Streaming Encode/Decode Methods
# -------------------------------------------------------------------------
def decode_with_cache(
self,
z: torch.Tensor,
temb: torch.Tensor | None = None,
causal: bool | None = None,
return_dict: bool = True,
reset_cache: bool = False,
prepend_prev_latent_frames: int = 1,
) -> DecoderOutput | tuple[torch.Tensor]:
"""Decode one latent chunk with persistent decoder feature cache.
For decoder_causal=False model: decodes each chunk independently with:
- Left context from previous chunk
- Zero-appended right context placeholder
Args:
z: Input latent tensor [B, C, T, H, W]
temb: Optional timestep embedding
causal: Whether to use causal decoding
return_dict: Whether to return dict or tuple
reset_cache: Whether to reset cache before decoding
prepend_prev_latent_frames: Number of previous frames to prepend as context
Returns:
DecoderOutput containing decoded video
"""
if reset_cache:
self._decoder_cache.clear()
if self.use_slicing and z.shape[0] > 1:
raise ValueError("decode_with_cache does not support batch slicing; pass batch size 1 or disable slicing.")
if z.shape[2] <= 0:
raise ValueError("Input latent chunk must contain at least 1 frame.")
if prepend_prev_latent_frames < 0:
raise ValueError(f"`prepend_prev_latent_frames` must be >= 0, got {prepend_prev_latent_frames}.")
causal = self.decoder.is_causal if causal is None else causal
self._decoder_cache.validate_mode(causal)
ratio = self.temporal_compression_ratio
z_chunk = z
if not causal:
z_chunk = self._decoder_cache.prepend_context(z, prepend_prev_latent_frames, ratio)
decoded_full = self.decoder(
z_chunk,
temb,
causal=causal,
feat_cache=self._decoder_cache.feat_map,
feat_idx=[0],
)
if not causal:
decoded = self._decoder_cache.trim_output(decoded_full, prepend_prev_latent_frames, z.shape[2], ratio)
self._decoder_cache.update_tail(z, prepend_prev_latent_frames)
else:
decoded = decoded_full
if not return_dict:
return (decoded,)
return DecoderOutput(sample=decoded)
@apply_forward_hook
def decode_per_frame_with_cache(
self,
z: torch.Tensor,
temb: torch.Tensor | None = None,
causal: bool | None = None,
reset_cache: bool = False,
) -> Iterator[torch.Tensor]:
"""Stream-decode a latent video one latent frame at a time, yielding pixel chunks.
Each iteration decodes a single latent frame and relies on the per-layer
feature cache for temporal consistency. Yields the decoded pixel tensor
for each latent frame so the caller can process/save/display each chunk
immediately (low peak memory, low latency, no final ``torch.cat``).
Streaming use: pass ``reset_cache=True`` on the first call of a new
stream/session, then ``reset_cache=False`` on subsequent calls so the
per-layer feature cache carries temporal state across calls.
Args:
z: Input latent tensor [B, C, T, H, W].
temb: Optional timestep embedding.
causal: Whether to use causal decoding.
reset_cache: If True, clear the decoder feature cache before decoding.
Set True only on the first chunk of a new stream.
Yields:
Decoded pixel tensor [B, C, T_out, H, W] for each single-latent-frame chunk.
"""
if self.use_slicing and z.shape[0] > 1:
raise ValueError(
"decode_per_frame_with_cache does not support batch slicing; pass batch size 1 or disable slicing."
)
if z.shape[2] <= 0:
raise ValueError("Input latent video must contain at least 1 frame.")
causal = self.decoder.is_causal if causal is None else causal
num_latent_frames = z.shape[2]
if reset_cache:
self._decoder_cache.clear()
self._decoder_cache.validate_mode(causal)
for t in range(num_latent_frames):
z_chunk = z[:, :, t : t + 1, :, :]
decoded_chunk = self.decoder(
z_chunk,
temb,
causal=causal,
feat_cache=self._decoder_cache.feat_map,
feat_idx=[0],
)
yield decoded_chunk
@apply_forward_hook
def streaming_causal_encode(
self,
x: torch.Tensor,
causal: bool | None = True,
return_dict: bool = True,
reset_cache: bool = False,
) -> AutoencoderKLOutput | tuple[DiagonalGaussianDistribution]:
"""Encode one streaming video chunk with persistent causal encoder cache.
Unlike ``encode``, this method does not split the input chunk and does
not clear encoder cache unless ``reset_cache`` is True.
"""
if reset_cache:
self._encoder_cache.clear()
if self.use_slicing and x.shape[0] > 1:
raise ValueError(
"streaming_causal_encode does not support batch slicing; pass batch size 1 or disable slicing."
)
if x.shape[2] <= 0:
raise ValueError("Input video chunk must contain at least 1 frame.")
h = self.encoder(x, causal=causal, feat_cache=self._encoder_cache.feat_map, feat_idx=[0])
posterior = DiagonalGaussianDistribution(h)
if not return_dict:
return (posterior,)
return AutoencoderKLOutput(latent_dist=posterior)
@apply_forward_hook
def encode(
self,
x: torch.Tensor,
causal: bool | None = True,
return_dict: bool = True,
) -> AutoencoderKLOutput | tuple[DiagonalGaussianDistribution]:
"""Encode a full video by iterating over temporal chunks with persistent encoder cache.
Args:
x: Input video tensor [B, C, T, H, W]
causal: Whether to use causal encoding.
return_dict: Whether to return dict or tuple.
Returns:
AutoencoderKLOutput containing latent distribution.
"""
chunk_num_frames = self.temporal_compression_ratio
num_frames = x.shape[2]
first_chunk_num_frames = num_frames % self.temporal_compression_ratio
if self.use_slicing and x.shape[0] > 1:
raise ValueError(
"encode_full_video_with_cache does not support batch slicing; pass batch size 1 or disable slicing."
)
self._encoder_cache.clear()
encoded_chunks = []
start = 0
step = first_chunk_num_frames if first_chunk_num_frames > 0 else chunk_num_frames
while start < num_frames:
end = min(start + step, num_frames)
encoded_chunk = self.encoder(
x[:, :, start:end, :, :],
causal=causal,
feat_cache=self._encoder_cache.feat_map,
feat_idx=[0],
)
encoded_chunks.append(encoded_chunk)
start = end
step = chunk_num_frames
if len(encoded_chunks) == 1:
h = encoded_chunks[0]
else:
h = torch.cat(encoded_chunks, dim=2)
self._encoder_cache.check_pending_consumed()
posterior = DiagonalGaussianDistribution(h)
if not return_dict:
return (posterior,)
return AutoencoderKLOutput(latent_dist=posterior)
def _decode(
self,
z: torch.Tensor,
temb: torch.Tensor | None = None,
causal: bool | None = None,
return_dict: bool = True,
) -> DecoderOutput | torch.Tensor:
"""Internal decode method."""
batch_size, num_channels, num_frames, height, width = z.shape
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
if self.use_framewise_decoding and num_frames > tile_latent_min_num_frames:
return self._temporal_tiled_decode(z, temb, causal=causal, return_dict=return_dict)
if self.use_tiling and (width > tile_latent_min_width or height > tile_latent_min_height):
return self.tiled_decode(z, temb, causal=causal, return_dict=return_dict)
dec = self.decoder(z, temb, causal=causal)
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
@apply_forward_hook
def decode(
self,
z: torch.Tensor,
temb: torch.Tensor | None = None,
causal: bool | None = None,
return_dict: bool = True,
) -> DecoderOutput | torch.Tensor:
"""Decode a batch of latents into images.
Args:
z: Input batch of latent vectors [B, C, T, H, W].
temb: Optional timestep embedding.
causal: Whether to use causal decoding.
return_dict: Whether to return a dict or tuple.
Returns:
DecoderOutput containing decoded video.
"""
if self.use_slicing and z.shape[0] > 1:
if temb is not None:
decoded_slices = [
self._decode(z_slice, t_slice, causal=causal).sample
for z_slice, t_slice in (z.split(1), temb.split(1))
]
else:
decoded_slices = [self._decode(z_slice, causal=causal).sample for z_slice in z.split(1)]
decoded = torch.cat(decoded_slices)
else:
decoded = self._decode(z, temb, causal=causal).sample
if not return_dict:
return (decoded,)
return DecoderOutput(sample=decoded)
# -------------------------------------------------------------------------
# Tiling Methods
# -------------------------------------------------------------------------
def blend_v(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
"""Blend vertically between two tiles."""
blend_extent = min(a.shape[3], b.shape[3], blend_extent)
for y in range(blend_extent):
b[:, :, :, y, :] = a[:, :, :, -blend_extent + y, :] * (1 - y / blend_extent) + b[:, :, :, y, :] * (
y / blend_extent
)
return b
def blend_h(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
"""Blend horizontally between two tiles."""
blend_extent = min(a.shape[4], b.shape[4], blend_extent)
for x in range(blend_extent):
b[:, :, :, :, x] = a[:, :, :, :, -blend_extent + x] * (1 - x / blend_extent) + b[:, :, :, :, x] * (
x / blend_extent
)
return b
def blend_t(self, a: torch.Tensor, b: torch.Tensor, blend_extent: int) -> torch.Tensor:
"""Blend temporally between two tiles."""
blend_extent = min(a.shape[-3], b.shape[-3], blend_extent)
for x in range(blend_extent):
b[:, :, x, :, :] = a[:, :, -blend_extent + x, :, :] * (1 - x / blend_extent) + b[:, :, x, :, :] * (
x / blend_extent
)
return b
@staticmethod
def _get_tile_positions(total_size: int, tile_size: int, overlap: int) -> list:
"""Compute tile start positions ensuring full coverage with given overlap.
Returns a list of start positions such that every pixel in [0, total_size)
is covered by at least one tile of size ``tile_size``.
"""
if total_size <= tile_size:
return [0]
stride = tile_size - overlap
positions = list(range(0, total_size - tile_size, stride))
last = total_size - tile_size
if not positions or positions[-1] < last:
positions.append(last)
return positions
@staticmethod
def _make_blend_mask(
h: int,
w: int,
overlap_h: int,
overlap_w: int,
is_top: bool,
is_bottom: bool,
is_left: bool,
is_right: bool,
device: torch.device,
dtype: torch.dtype,
) -> torch.Tensor:
"""Create a 2D spatial blend mask with linear ramps in overlap regions.
Interior pixels get weight 1. Pixels in the overlap band of a non-edge
side get linearly increasing/decreasing weights so that the weighted
average of overlapping tiles transitions smoothly.
Returns:
Tensor of shape ``[h, w]`` with values in (0, 1].
"""
mask_h = torch.ones(h, device=device, dtype=dtype)
mask_w = torch.ones(w, device=device, dtype=dtype)
if not is_top and overlap_h > 0:
mask_h[:overlap_h] = torch.linspace(0, 1, overlap_h + 2, device=device, dtype=dtype)[1:-1]
if not is_bottom and overlap_h > 0:
mask_h[-overlap_h:] = torch.linspace(1, 0, overlap_h + 2, device=device, dtype=dtype)[1:-1]
if not is_left and overlap_w > 0:
mask_w[:overlap_w] = torch.linspace(0, 1, overlap_w + 2, device=device, dtype=dtype)[1:-1]
if not is_right and overlap_w > 0:
mask_w[-overlap_w:] = torch.linspace(1, 0, overlap_w + 2, device=device, dtype=dtype)[1:-1]
return mask_h[:, None] * mask_w[None, :]
def tiled_encode(
self,
x: torch.Tensor,
causal: bool | None = None,
tile_caches: dict | None = None,
) -> torch.Tensor:
r"""Encode a batch of images using a tiled encoder.
Args:
x: Input batch of videos [B, C, T, H, W].
causal: Whether to use causal encoding.
tile_caches: Optional dict mapping ``(row_idx, col_idx)`` to a
per-tile ``feat_cache`` list. When provided each tile's
encoder call receives its own persistent cache so temporal
context is maintained across successive calls.
Returns:
The latent representation of the encoded videos.
"""
batch_size, num_channels, num_frames, height, width = x.shape
latent_height = height // self.spatial_compression_ratio
latent_width = width // self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = tile_latent_min_height - tile_latent_stride_height
blend_width = tile_latent_min_width - tile_latent_stride_width
# Split x into overlapping tiles and encode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i_idx, i in enumerate(range(0, height, self.tile_sample_stride_height)):
row = []
for j_idx, j in enumerate(range(0, width, self.tile_sample_stride_width)):
tile = x[:, :, :, i : i + self.tile_sample_min_height, j : j + self.tile_sample_min_width]
if tile_caches is not None:
enc_tile = self.encoder(
tile,
causal=causal,
feat_cache=tile_caches[(i_idx, j_idx)],
feat_idx=[0],
)
else:
enc_tile = self.encoder(tile, causal=causal)
row.append(enc_tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_height)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_width)
result_row.append(tile[:, :, :, :tile_latent_stride_height, :tile_latent_stride_width])
result_rows.append(torch.cat(result_row, dim=4))
enc = torch.cat(result_rows, dim=3)[:, :, :, :latent_height, :latent_width]
return enc
def tiled_decode(
self,
z: torch.Tensor,
temb: torch.Tensor | None,
causal: bool | None = None,
return_dict: bool = True,
tile_caches: dict | None = None,
) -> DecoderOutput | torch.Tensor:
r"""Decode a batch of images using a tiled decoder.
Args:
z: Input batch of latent vectors [B, C, T, H, W].
temb: Optional timestep embedding.
causal: Whether to use causal decoding.
return_dict: Whether to return a DecoderOutput instead of a plain tuple.
tile_caches: Optional dict mapping ``(row_idx, col_idx)`` to a
per-tile ``feat_cache`` list. When provided each tile's
decoder call receives its own persistent cache so temporal
context is maintained across successive calls.
Returns:
DecoderOutput or tuple containing decoded video.
"""
batch_size, num_channels, num_frames, height, width = z.shape
sample_height = height * self.spatial_compression_ratio
sample_width = width * self.spatial_compression_ratio
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_stride_height = self.tile_sample_stride_height // self.spatial_compression_ratio
tile_latent_stride_width = self.tile_sample_stride_width // self.spatial_compression_ratio
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
# Split z into overlapping tiles and decode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i_idx, i in enumerate(range(0, height, tile_latent_stride_height)):
row = []
for j_idx, j in enumerate(range(0, width, tile_latent_stride_width)):
tile = z[:, :, :, i : i + tile_latent_min_height, j : j + tile_latent_min_width]
if tile_caches is not None:
dec_tile = self.decoder(
tile,
temb,
causal=causal,
feat_cache=tile_caches[(i_idx, j_idx)],
feat_idx=[0],
)
else:
dec_tile = self.decoder(tile, temb, causal=causal)
row.append(dec_tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_height)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_width)
result_row.append(tile[:, :, :, : self.tile_sample_stride_height, : self.tile_sample_stride_width])
result_rows.append(torch.cat(result_row, dim=4))
dec = torch.cat(result_rows, dim=3)[:, :, :, :sample_height, :sample_width]
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def decode_chunk_tile(
self, z: torch.Tensor, temb: torch.Tensor | None, causal: bool | None = None, return_dict: bool = True
) -> DecoderOutput | torch.Tensor:
"""Decode latent videos with temporal-only tiling.
This is used by SANA-Streaming inference with the public LTX-2 VAE
weights. It avoids spatial tiling and only chunks along latent time,
matching the release recipe used for long 720p V2V editing.
"""
batch_size, num_channels, num_frames, height, width = z.shape
del batch_size, num_channels, height, width
num_sample_frames = (num_frames - 1) * self.temporal_compression_ratio + 1
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_num_frames = self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
row = []
for i in range(0, num_frames, tile_latent_stride_num_frames):
tile = z[:, :, i : i + tile_latent_min_num_frames + 1, :, :]
decoded = self.decoder(tile, temb, causal=causal)
if i > 0:
decoded = decoded[:, :, 1:, :, :]
row.append(decoded)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, blend_num_frames)
tile = tile[:, :, : self.tile_sample_stride_num_frames, :, :]
result_row.append(tile)
else:
result_row.append(tile[:, :, : self.tile_sample_stride_num_frames + 1, :, :])
dec = torch.cat(result_row, dim=2)[:, :, :num_sample_frames]
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def _temporal_tiled_encode(self, x: torch.Tensor, causal: bool | None = None) -> torch.Tensor:
"""Encode with temporal tiling for long videos."""
batch_size, num_channels, num_frames, height, width = x.shape
latent_num_frames = (num_frames - 1) // self.temporal_compression_ratio + 1
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_num_frames = tile_latent_min_num_frames - tile_latent_stride_num_frames
row = []
for i in range(0, num_frames, self.tile_sample_stride_num_frames):
tile = x[:, :, i : i + self.tile_sample_min_num_frames + 1, :, :]
if self.use_tiling and (height > self.tile_sample_min_height or width > self.tile_sample_min_width):
tile = self.tiled_encode(tile, causal=causal)
else:
tile = self.encoder(tile, causal=causal)
if i > 0:
tile = tile[:, :, 1:, :, :]
row.append(tile)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, blend_num_frames)
result_row.append(tile[:, :, :tile_latent_stride_num_frames, :, :])
else:
result_row.append(tile[:, :, : tile_latent_stride_num_frames + 1, :, :])
enc = torch.cat(result_row, dim=2)[:, :, :latent_num_frames]
return enc
def _temporal_tiled_decode(
self, z: torch.Tensor, temb: torch.Tensor | None, causal: bool | None = None, return_dict: bool = True
) -> DecoderOutput | torch.Tensor:
"""Decode with temporal tiling for long videos."""
batch_size, num_channels, num_frames, height, width = z.shape
num_sample_frames = (num_frames - 1) * self.temporal_compression_ratio + 1
tile_latent_min_height = self.tile_sample_min_height // self.spatial_compression_ratio
tile_latent_min_width = self.tile_sample_min_width // self.spatial_compression_ratio
tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = self.tile_sample_stride_num_frames // self.temporal_compression_ratio
blend_num_frames = self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
row = []
for i in range(0, num_frames, tile_latent_stride_num_frames):
tile = z[:, :, i : i + tile_latent_min_num_frames + 1, :, :]
if self.use_tiling and (tile.shape[-1] > tile_latent_min_width or tile.shape[-2] > tile_latent_min_height):
decoded = self.tiled_decode(tile, temb, causal=causal, return_dict=True).sample
else:
decoded = self.decoder(tile, temb, causal=causal)
if i > 0:
decoded = decoded[:, :, 1:, :, :]
row.append(decoded)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, blend_num_frames)
tile = tile[:, :, : self.tile_sample_stride_num_frames, :, :]
result_row.append(tile)
else:
result_row.append(tile[:, :, : self.tile_sample_stride_num_frames + 1, :, :])
dec = torch.cat(result_row, dim=2)[:, :, :num_sample_frames]
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def forward(
self,
sample: torch.Tensor,
temb: torch.Tensor | None = None,
sample_posterior: bool = False,
encoder_causal: bool | None = None,
decoder_causal: bool | None = None,
return_dict: bool = True,
generator: torch.Generator | None = None,
) -> torch.Tensor | torch.Tensor:
"""Full forward pass: encode then decode.
Args:
sample: Input video tensor [B, C, T, H, W]
temb: Optional timestep embedding
sample_posterior: Whether to sample from posterior or use mode
encoder_causal: Whether to use causal encoding
decoder_causal: Whether to use causal decoding
return_dict: Whether to return dict
generator: Optional random generator
Returns:
Decoded video
"""
x = sample
posterior = self.encode(x, causal=encoder_causal).latent_dist
if sample_posterior:
z = posterior.sample(generator=generator)
else:
z = posterior.mode()
dec = self.decode(z, temb, causal=decoder_causal)
if not return_dict:
return (dec.sample,)
return dec