cddb07a176
docs / deploy (push) Has been cancelled
docs / changes (push) Has been cancelled
docs / check-and-build (push) Has been cancelled
build container image / cpu (push) Has been cancelled
build container image / cuda (push) Has been cancelled
build container image / rocm (push) Has been cancelled
frontend checks / frontend-checks (push) Has been cancelled
frontend tests / frontend-tests (push) Has been cancelled
lfs checks / lfs-check (push) Has been cancelled
python checks / python-checks (push) Has been cancelled
python tests / py3.12: macos-default (push) Has been cancelled
python tests / py3.11: windows-cpu (push) Has been cancelled
python tests / py3.12: windows-cpu (push) Has been cancelled
python tests / py3.11: linux-cpu (push) Has been cancelled
typegen checks / typegen-checks (push) Has been cancelled
uv lock checks / uv-lock-checks (push) Has been cancelled
openapi checks / openapi-checks (push) Has been cancelled
python tests / py3.11: macos-default (push) Has been cancelled
python tests / py3.12: linux-cpu (push) Has been cancelled
1041 lines
39 KiB
Python
1041 lines
39 KiB
Python
"""Anima transformer model: Cosmos Predict2 MiniTrainDIT + LLM Adapter.
|
|
|
|
The Anima architecture combines:
|
|
1. MiniTrainDIT: A Cosmos Predict2 DiT backbone with 28 blocks, 2048-dim hidden state,
|
|
and 3D RoPE positional embeddings.
|
|
2. LLMAdapter: A 6-layer cross-attention transformer that fuses Qwen3 0.6B hidden states
|
|
with learned T5-XXL token embeddings to produce conditioning for the DiT.
|
|
|
|
Original source code:
|
|
- MiniTrainDIT backbone and positional embeddings: https://github.com/nvidia-cosmos/cosmos-predict2
|
|
SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
SPDX-License-Identifier: Apache-2.0
|
|
- LLMAdapter and Anima wrapper: Clean-room implementation based on
|
|
https://github.com/hdae/diffusers-anima (Apache-2.0)
|
|
"""
|
|
|
|
import logging
|
|
import math
|
|
from typing import Optional, Tuple
|
|
|
|
import torch
|
|
import torch.nn.functional as F
|
|
from einops import rearrange, repeat
|
|
from einops.layers.torch import Rearrange
|
|
from torch import nn
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Positional Embeddings
|
|
# Original source: https://github.com/nvidia-cosmos/cosmos-predict2
|
|
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. Apache-2.0
|
|
# ============================================================================
|
|
|
|
|
|
class VideoRopePosition3DEmb(nn.Module):
|
|
"""3D Rotary Position Embedding for video/image transformers.
|
|
|
|
Generates rotary embeddings with separate frequency components for
|
|
height, width, and temporal dimensions.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
head_dim: int,
|
|
len_h: int,
|
|
len_w: int,
|
|
len_t: int,
|
|
base_fps: int = 24,
|
|
h_extrapolation_ratio: float = 1.0,
|
|
w_extrapolation_ratio: float = 1.0,
|
|
t_extrapolation_ratio: float = 1.0,
|
|
enable_fps_modulation: bool = True,
|
|
device: Optional[torch.device] = None,
|
|
**kwargs,
|
|
):
|
|
super().__init__()
|
|
self.base_fps = base_fps
|
|
self.max_h = len_h
|
|
self.max_w = len_w
|
|
self.enable_fps_modulation = enable_fps_modulation
|
|
|
|
dim = head_dim
|
|
dim_h = dim // 6 * 2
|
|
dim_w = dim_h
|
|
dim_t = dim - 2 * dim_h
|
|
assert dim == dim_h + dim_w + dim_t, f"bad dim: {dim} != {dim_h} + {dim_w} + {dim_t}"
|
|
|
|
self.register_buffer(
|
|
"dim_spatial_range",
|
|
torch.arange(0, dim_h, 2, device=device)[: (dim_h // 2)].float() / dim_h,
|
|
persistent=False,
|
|
)
|
|
self.register_buffer(
|
|
"dim_temporal_range",
|
|
torch.arange(0, dim_t, 2, device=device)[: (dim_t // 2)].float() / dim_t,
|
|
persistent=False,
|
|
)
|
|
|
|
self.h_ntk_factor = h_extrapolation_ratio ** (dim_h / (dim_h - 2))
|
|
self.w_ntk_factor = w_extrapolation_ratio ** (dim_w / (dim_w - 2))
|
|
self.t_ntk_factor = t_extrapolation_ratio ** (dim_t / (dim_t - 2))
|
|
|
|
def forward(
|
|
self,
|
|
x_B_T_H_W_C: torch.Tensor,
|
|
fps: Optional[torch.Tensor] = None,
|
|
device: Optional[torch.device] = None,
|
|
) -> torch.Tensor:
|
|
return self.generate_embeddings(x_B_T_H_W_C.shape, fps=fps, device=device)
|
|
|
|
def generate_embeddings(
|
|
self,
|
|
B_T_H_W_C: torch.Size,
|
|
fps: Optional[torch.Tensor] = None,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
) -> torch.Tensor:
|
|
h_theta = 10000.0 * self.h_ntk_factor
|
|
w_theta = 10000.0 * self.w_ntk_factor
|
|
t_theta = 10000.0 * self.t_ntk_factor
|
|
|
|
h_spatial_freqs = 1.0 / (h_theta ** self.dim_spatial_range.to(device=device))
|
|
w_spatial_freqs = 1.0 / (w_theta ** self.dim_spatial_range.to(device=device))
|
|
temporal_freqs = 1.0 / (t_theta ** self.dim_temporal_range.to(device=device))
|
|
|
|
B, T, H, W, _ = B_T_H_W_C
|
|
seq = torch.arange(max(H, W, T), dtype=torch.float, device=device)
|
|
|
|
half_emb_h = torch.outer(seq[:H].to(device=device), h_spatial_freqs)
|
|
half_emb_w = torch.outer(seq[:W].to(device=device), w_spatial_freqs)
|
|
|
|
if fps is None or self.enable_fps_modulation is False:
|
|
half_emb_t = torch.outer(seq[:T].to(device=device), temporal_freqs)
|
|
else:
|
|
half_emb_t = torch.outer(seq[:T].to(device=device) / fps * self.base_fps, temporal_freqs)
|
|
|
|
half_emb_h = torch.stack(
|
|
[torch.cos(half_emb_h), -torch.sin(half_emb_h), torch.sin(half_emb_h), torch.cos(half_emb_h)], dim=-1
|
|
)
|
|
half_emb_w = torch.stack(
|
|
[torch.cos(half_emb_w), -torch.sin(half_emb_w), torch.sin(half_emb_w), torch.cos(half_emb_w)], dim=-1
|
|
)
|
|
half_emb_t = torch.stack(
|
|
[torch.cos(half_emb_t), -torch.sin(half_emb_t), torch.sin(half_emb_t), torch.cos(half_emb_t)], dim=-1
|
|
)
|
|
|
|
em_T_H_W_D = torch.cat(
|
|
[
|
|
repeat(half_emb_t, "t d x -> t h w d x", h=H, w=W),
|
|
repeat(half_emb_h, "h d x -> t h w d x", t=T, w=W),
|
|
repeat(half_emb_w, "w d x -> t h w d x", t=T, h=H),
|
|
],
|
|
dim=-2,
|
|
)
|
|
|
|
return rearrange(em_T_H_W_D, "t h w d (i j) -> (t h w) d i j", i=2, j=2).float()
|
|
|
|
|
|
def _normalize(x: torch.Tensor, dim: Optional[list[int]] = None, eps: float = 0) -> torch.Tensor:
|
|
if dim is None:
|
|
dim = list(range(1, x.ndim))
|
|
norm = torch.linalg.vector_norm(x, dim=dim, keepdim=True, dtype=torch.float32)
|
|
norm = torch.add(eps, norm, alpha=math.sqrt(norm.numel() / x.numel()))
|
|
return x / norm.to(x.dtype)
|
|
|
|
|
|
class LearnablePosEmbAxis(nn.Module):
|
|
"""Learnable per-axis positional embeddings."""
|
|
|
|
def __init__(
|
|
self,
|
|
*,
|
|
model_channels: int,
|
|
len_h: int,
|
|
len_w: int,
|
|
len_t: int,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
**kwargs,
|
|
):
|
|
super().__init__()
|
|
self.pos_emb_h = nn.Parameter(torch.empty(len_h, model_channels, device=device, dtype=dtype))
|
|
self.pos_emb_w = nn.Parameter(torch.empty(len_w, model_channels, device=device, dtype=dtype))
|
|
self.pos_emb_t = nn.Parameter(torch.empty(len_t, model_channels, device=device, dtype=dtype))
|
|
|
|
def forward(
|
|
self,
|
|
x_B_T_H_W_C: torch.Tensor,
|
|
fps: Optional[torch.Tensor] = None,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
) -> torch.Tensor:
|
|
return self.generate_embeddings(x_B_T_H_W_C.shape, device=device, dtype=dtype)
|
|
|
|
def generate_embeddings(
|
|
self,
|
|
B_T_H_W_C: torch.Size,
|
|
fps: Optional[torch.Tensor] = None,
|
|
device: Optional[torch.device] = None,
|
|
dtype: Optional[torch.dtype] = None,
|
|
) -> torch.Tensor:
|
|
B, T, H, W, _ = B_T_H_W_C
|
|
emb_h_H = self.pos_emb_h[:H].to(device=device, dtype=dtype)
|
|
emb_w_W = self.pos_emb_w[:W].to(device=device, dtype=dtype)
|
|
emb_t_T = self.pos_emb_t[:T].to(device=device, dtype=dtype)
|
|
emb = (
|
|
repeat(emb_t_T, "t d -> b t h w d", b=B, h=H, w=W)
|
|
+ repeat(emb_h_H, "h d -> b t h w d", b=B, t=T, w=W)
|
|
+ repeat(emb_w_W, "w d -> b t h w d", b=B, t=T, h=H)
|
|
)
|
|
return _normalize(emb, dim=-1, eps=1e-6)
|
|
|
|
|
|
# ============================================================================
|
|
# Cosmos Predict2 MiniTrainDIT
|
|
# Original source: https://github.com/nvidia-cosmos/cosmos-predict2
|
|
# Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. Apache-2.0
|
|
# ============================================================================
|
|
|
|
|
|
def apply_rotary_pos_emb_cosmos(t: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
|
|
"""Apply rotary position embeddings in Cosmos format (2x2 rotation matrices)."""
|
|
t_ = t.reshape(*t.shape[:-1], 2, -1).movedim(-2, -1).unsqueeze(-2).float()
|
|
t_out = freqs[..., 0] * t_[..., 0] + freqs[..., 1] * t_[..., 1]
|
|
t_out = t_out.movedim(-1, -2).reshape(*t.shape).type_as(t)
|
|
return t_out
|
|
|
|
|
|
class GPT2FeedForward(nn.Module):
|
|
def __init__(self, d_model: int, d_ff: int) -> None:
|
|
super().__init__()
|
|
self.activation = nn.GELU()
|
|
self.layer1 = nn.Linear(d_model, d_ff, bias=False)
|
|
self.layer2 = nn.Linear(d_ff, d_model, bias=False)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
return self.layer2(self.activation(self.layer1(x)))
|
|
|
|
|
|
class CosmosAttention(nn.Module):
|
|
"""Multi-head attention for the Cosmos DiT backbone.
|
|
|
|
Supports both self-attention and cross-attention with QK normalization
|
|
and rotary position embeddings.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
query_dim: int,
|
|
context_dim: Optional[int] = None,
|
|
n_heads: int = 8,
|
|
head_dim: int = 64,
|
|
dropout: float = 0.0,
|
|
) -> None:
|
|
super().__init__()
|
|
self.is_selfattn = context_dim is None
|
|
context_dim = query_dim if context_dim is None else context_dim
|
|
inner_dim = head_dim * n_heads
|
|
|
|
self.n_heads = n_heads
|
|
self.head_dim = head_dim
|
|
|
|
self.q_proj = nn.Linear(query_dim, inner_dim, bias=False)
|
|
self.q_norm = nn.RMSNorm(head_dim, eps=1e-6)
|
|
|
|
self.k_proj = nn.Linear(context_dim, inner_dim, bias=False)
|
|
self.k_norm = nn.RMSNorm(head_dim, eps=1e-6)
|
|
|
|
self.v_proj = nn.Linear(context_dim, inner_dim, bias=False)
|
|
self.v_norm = nn.Identity()
|
|
|
|
self.output_proj = nn.Linear(inner_dim, query_dim, bias=False)
|
|
self.output_dropout = nn.Dropout(dropout) if dropout > 1e-4 else nn.Identity()
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
context: Optional[torch.Tensor] = None,
|
|
rope_emb: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
q = self.q_proj(x)
|
|
context = x if context is None else context
|
|
k = self.k_proj(context)
|
|
v = self.v_proj(context)
|
|
q, k, v = (rearrange(t, "b ... (h d) -> b ... h d", h=self.n_heads, d=self.head_dim) for t in (q, k, v))
|
|
|
|
q = self.q_norm(q)
|
|
k = self.k_norm(k)
|
|
v = self.v_norm(v)
|
|
|
|
if self.is_selfattn and rope_emb is not None:
|
|
q = apply_rotary_pos_emb_cosmos(q, rope_emb)
|
|
k = apply_rotary_pos_emb_cosmos(k, rope_emb)
|
|
|
|
# Reshape for scaled_dot_product_attention: (B, heads, seq, dim)
|
|
in_q_shape = q.shape
|
|
in_k_shape = k.shape
|
|
q = rearrange(q, "b ... h d -> b h ... d").reshape(in_q_shape[0], in_q_shape[-2], -1, in_q_shape[-1])
|
|
k = rearrange(k, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
|
|
v = rearrange(v, "b ... h d -> b h ... d").reshape(in_k_shape[0], in_k_shape[-2], -1, in_k_shape[-1])
|
|
|
|
result = F.scaled_dot_product_attention(q, k, v)
|
|
result = rearrange(result, "b h s d -> b s (h d)")
|
|
return self.output_dropout(self.output_proj(result))
|
|
|
|
|
|
class Timesteps(nn.Module):
|
|
"""Sinusoidal timestep embeddings."""
|
|
|
|
def __init__(self, num_channels: int):
|
|
super().__init__()
|
|
self.num_channels = num_channels
|
|
|
|
def forward(self, timesteps_B_T: torch.Tensor) -> torch.Tensor:
|
|
assert timesteps_B_T.ndim == 2
|
|
timesteps = timesteps_B_T.flatten().float()
|
|
half_dim = self.num_channels // 2
|
|
exponent = -math.log(10000) * torch.arange(half_dim, dtype=torch.float32, device=timesteps.device) / half_dim
|
|
emb = timesteps[:, None].float() * torch.exp(exponent)[None, :]
|
|
emb = torch.cat([torch.cos(emb), torch.sin(emb)], dim=-1)
|
|
return rearrange(emb, "(b t) d -> b t d", b=timesteps_B_T.shape[0], t=timesteps_B_T.shape[1])
|
|
|
|
|
|
class TimestepEmbedding(nn.Module):
|
|
"""Projects sinusoidal timestep embeddings to model dimension."""
|
|
|
|
def __init__(self, in_features: int, out_features: int, use_adaln_lora: bool = False):
|
|
super().__init__()
|
|
self.use_adaln_lora = use_adaln_lora
|
|
self.linear_1 = nn.Linear(in_features, out_features, bias=not use_adaln_lora)
|
|
self.activation = nn.SiLU()
|
|
if use_adaln_lora:
|
|
self.linear_2 = nn.Linear(out_features, 3 * out_features, bias=False)
|
|
else:
|
|
self.linear_2 = nn.Linear(out_features, out_features, bias=False)
|
|
|
|
def forward(self, sample: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
|
|
emb = self.linear_2(self.activation(self.linear_1(sample)))
|
|
if self.use_adaln_lora:
|
|
return sample, emb
|
|
return emb, None
|
|
|
|
|
|
class PatchEmbed(nn.Module):
|
|
"""Patchify input tensor via rearrange + linear projection."""
|
|
|
|
def __init__(
|
|
self,
|
|
spatial_patch_size: int,
|
|
temporal_patch_size: int,
|
|
in_channels: int = 3,
|
|
out_channels: int = 768,
|
|
):
|
|
super().__init__()
|
|
self.spatial_patch_size = spatial_patch_size
|
|
self.temporal_patch_size = temporal_patch_size
|
|
self.proj = nn.Sequential(
|
|
Rearrange(
|
|
"b c (t r) (h m) (w n) -> b t h w (c r m n)",
|
|
r=temporal_patch_size,
|
|
m=spatial_patch_size,
|
|
n=spatial_patch_size,
|
|
),
|
|
nn.Linear(
|
|
in_channels * spatial_patch_size * spatial_patch_size * temporal_patch_size,
|
|
out_channels,
|
|
bias=False,
|
|
),
|
|
)
|
|
|
|
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
assert x.dim() == 5
|
|
return self.proj(x)
|
|
|
|
|
|
class FinalLayer(nn.Module):
|
|
"""Final AdaLN-modulated output projection."""
|
|
|
|
def __init__(
|
|
self,
|
|
hidden_size: int,
|
|
spatial_patch_size: int,
|
|
temporal_patch_size: int,
|
|
out_channels: int,
|
|
use_adaln_lora: bool = False,
|
|
adaln_lora_dim: int = 256,
|
|
):
|
|
super().__init__()
|
|
self.layer_norm = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
|
|
self.linear = nn.Linear(
|
|
hidden_size, spatial_patch_size * spatial_patch_size * temporal_patch_size * out_channels, bias=False
|
|
)
|
|
self.hidden_size = hidden_size
|
|
self.use_adaln_lora = use_adaln_lora
|
|
|
|
if use_adaln_lora:
|
|
self.adaln_modulation = nn.Sequential(
|
|
nn.SiLU(),
|
|
nn.Linear(hidden_size, adaln_lora_dim, bias=False),
|
|
nn.Linear(adaln_lora_dim, 2 * hidden_size, bias=False),
|
|
)
|
|
else:
|
|
self.adaln_modulation = nn.Sequential(
|
|
nn.SiLU(),
|
|
nn.Linear(hidden_size, 2 * hidden_size, bias=False),
|
|
)
|
|
|
|
def forward(
|
|
self,
|
|
x_B_T_H_W_D: torch.Tensor,
|
|
emb_B_T_D: torch.Tensor,
|
|
adaln_lora_B_T_3D: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
if self.use_adaln_lora:
|
|
assert adaln_lora_B_T_3D is not None
|
|
shift, scale = (self.adaln_modulation(emb_B_T_D) + adaln_lora_B_T_3D[:, :, : 2 * self.hidden_size]).chunk(
|
|
2, dim=-1
|
|
)
|
|
else:
|
|
shift, scale = self.adaln_modulation(emb_B_T_D).chunk(2, dim=-1)
|
|
|
|
shift = rearrange(shift, "b t d -> b t 1 1 d")
|
|
scale = rearrange(scale, "b t d -> b t 1 1 d")
|
|
|
|
x_B_T_H_W_D = self.layer_norm(x_B_T_H_W_D) * (1 + scale) + shift
|
|
return self.linear(x_B_T_H_W_D)
|
|
|
|
|
|
class DiTBlock(nn.Module):
|
|
"""Cosmos DiT transformer block with self-attention, cross-attention, and MLP.
|
|
|
|
Each component uses AdaLN (Adaptive Layer Normalization) modulation from
|
|
the timestep embedding.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
x_dim: int,
|
|
context_dim: int,
|
|
num_heads: int,
|
|
mlp_ratio: float = 4.0,
|
|
use_adaln_lora: bool = False,
|
|
adaln_lora_dim: int = 256,
|
|
):
|
|
super().__init__()
|
|
self.x_dim = x_dim
|
|
self.use_adaln_lora = use_adaln_lora
|
|
|
|
self.layer_norm_self_attn = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
|
|
self.self_attn = CosmosAttention(x_dim, None, num_heads, x_dim // num_heads)
|
|
|
|
self.layer_norm_cross_attn = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
|
|
self.cross_attn = CosmosAttention(x_dim, context_dim, num_heads, x_dim // num_heads)
|
|
|
|
self.layer_norm_mlp = nn.LayerNorm(x_dim, elementwise_affine=False, eps=1e-6)
|
|
self.mlp = GPT2FeedForward(x_dim, int(x_dim * mlp_ratio))
|
|
|
|
# AdaLN modulation layers (shift, scale, gate for each of 3 components)
|
|
if use_adaln_lora:
|
|
self.adaln_modulation_self_attn = nn.Sequential(
|
|
nn.SiLU(),
|
|
nn.Linear(x_dim, adaln_lora_dim, bias=False),
|
|
nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
|
|
)
|
|
self.adaln_modulation_cross_attn = nn.Sequential(
|
|
nn.SiLU(),
|
|
nn.Linear(x_dim, adaln_lora_dim, bias=False),
|
|
nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
|
|
)
|
|
self.adaln_modulation_mlp = nn.Sequential(
|
|
nn.SiLU(),
|
|
nn.Linear(x_dim, adaln_lora_dim, bias=False),
|
|
nn.Linear(adaln_lora_dim, 3 * x_dim, bias=False),
|
|
)
|
|
else:
|
|
self.adaln_modulation_self_attn = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
|
|
self.adaln_modulation_cross_attn = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
|
|
self.adaln_modulation_mlp = nn.Sequential(nn.SiLU(), nn.Linear(x_dim, 3 * x_dim, bias=False))
|
|
|
|
def forward(
|
|
self,
|
|
x_B_T_H_W_D: torch.Tensor,
|
|
emb_B_T_D: torch.Tensor,
|
|
crossattn_emb: torch.Tensor,
|
|
rope_emb_L_1_1_D: Optional[torch.Tensor] = None,
|
|
adaln_lora_B_T_3D: Optional[torch.Tensor] = None,
|
|
extra_per_block_pos_emb: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
residual_dtype = x_B_T_H_W_D.dtype
|
|
compute_dtype = emb_B_T_D.dtype
|
|
|
|
if extra_per_block_pos_emb is not None:
|
|
x_B_T_H_W_D = x_B_T_H_W_D + extra_per_block_pos_emb
|
|
|
|
# Compute AdaLN modulations
|
|
if self.use_adaln_lora:
|
|
assert adaln_lora_B_T_3D is not None
|
|
shift_sa, scale_sa, gate_sa = (self.adaln_modulation_self_attn(emb_B_T_D) + adaln_lora_B_T_3D).chunk(
|
|
3, dim=-1
|
|
)
|
|
shift_ca, scale_ca, gate_ca = (self.adaln_modulation_cross_attn(emb_B_T_D) + adaln_lora_B_T_3D).chunk(
|
|
3, dim=-1
|
|
)
|
|
shift_mlp, scale_mlp, gate_mlp = (self.adaln_modulation_mlp(emb_B_T_D) + adaln_lora_B_T_3D).chunk(3, dim=-1)
|
|
else:
|
|
shift_sa, scale_sa, gate_sa = self.adaln_modulation_self_attn(emb_B_T_D).chunk(3, dim=-1)
|
|
shift_ca, scale_ca, gate_ca = self.adaln_modulation_cross_attn(emb_B_T_D).chunk(3, dim=-1)
|
|
shift_mlp, scale_mlp, gate_mlp = self.adaln_modulation_mlp(emb_B_T_D).chunk(3, dim=-1)
|
|
|
|
# Reshape for broadcasting: (B, T, D) -> (B, T, 1, 1, D)
|
|
shift_sa, scale_sa, gate_sa = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_sa, scale_sa, gate_sa))
|
|
shift_ca, scale_ca, gate_ca = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_ca, scale_ca, gate_ca))
|
|
shift_mlp, scale_mlp, gate_mlp = (rearrange(t, "b t d -> b t 1 1 d") for t in (shift_mlp, scale_mlp, gate_mlp))
|
|
|
|
B, T, H, W, D = x_B_T_H_W_D.shape
|
|
|
|
def _adaln(x: torch.Tensor, norm: nn.Module, scale: torch.Tensor, shift: torch.Tensor) -> torch.Tensor:
|
|
return norm(x) * (1 + scale) + shift
|
|
|
|
# Self-attention
|
|
normed = _adaln(x_B_T_H_W_D, self.layer_norm_self_attn, scale_sa, shift_sa)
|
|
result = rearrange(
|
|
self.self_attn(
|
|
rearrange(normed.to(compute_dtype), "b t h w d -> b (t h w) d"), None, rope_emb=rope_emb_L_1_1_D
|
|
),
|
|
"b (t h w) d -> b t h w d",
|
|
t=T,
|
|
h=H,
|
|
w=W,
|
|
)
|
|
x_B_T_H_W_D = x_B_T_H_W_D + gate_sa.to(residual_dtype) * result.to(residual_dtype)
|
|
|
|
# Cross-attention
|
|
normed = _adaln(x_B_T_H_W_D, self.layer_norm_cross_attn, scale_ca, shift_ca)
|
|
result = rearrange(
|
|
self.cross_attn(
|
|
rearrange(normed.to(compute_dtype), "b t h w d -> b (t h w) d"),
|
|
crossattn_emb,
|
|
rope_emb=rope_emb_L_1_1_D,
|
|
),
|
|
"b (t h w) d -> b t h w d",
|
|
t=T,
|
|
h=H,
|
|
w=W,
|
|
)
|
|
x_B_T_H_W_D = result.to(residual_dtype) * gate_ca.to(residual_dtype) + x_B_T_H_W_D
|
|
|
|
# MLP
|
|
normed = _adaln(x_B_T_H_W_D, self.layer_norm_mlp, scale_mlp, shift_mlp)
|
|
result = self.mlp(normed.to(compute_dtype))
|
|
x_B_T_H_W_D = x_B_T_H_W_D + gate_mlp.to(residual_dtype) * result.to(residual_dtype)
|
|
|
|
return x_B_T_H_W_D
|
|
|
|
|
|
class MiniTrainDIT(nn.Module):
|
|
"""Cosmos Predict2 DiT backbone for video/image generation.
|
|
|
|
This is the core transformer architecture that Anima extends. It processes
|
|
3D latent tensors (B, C, T, H, W) with patch embedding, positional encoding,
|
|
and adaptive layer normalization.
|
|
|
|
Args:
|
|
max_img_h: Maximum image height in pixels.
|
|
max_img_w: Maximum image width in pixels.
|
|
max_frames: Maximum number of video frames.
|
|
in_channels: Number of input latent channels.
|
|
out_channels: Number of output channels.
|
|
patch_spatial: Spatial patch size.
|
|
patch_temporal: Temporal patch size.
|
|
concat_padding_mask: Whether to concatenate a padding mask channel.
|
|
model_channels: Hidden dimension of the transformer.
|
|
num_blocks: Number of DiT blocks.
|
|
num_heads: Number of attention heads.
|
|
mlp_ratio: MLP expansion ratio.
|
|
crossattn_emb_channels: Cross-attention context dimension.
|
|
use_adaln_lora: Whether to use AdaLN-LoRA.
|
|
adaln_lora_dim: AdaLN-LoRA bottleneck dimension.
|
|
extra_per_block_abs_pos_emb: Whether to use extra learnable positional embeddings.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
max_img_h: int = 240,
|
|
max_img_w: int = 240,
|
|
max_frames: int = 1,
|
|
in_channels: int = 16,
|
|
out_channels: int = 16,
|
|
patch_spatial: int = 2,
|
|
patch_temporal: int = 1,
|
|
concat_padding_mask: bool = True,
|
|
model_channels: int = 2048,
|
|
num_blocks: int = 28,
|
|
num_heads: int = 16,
|
|
mlp_ratio: float = 4.0,
|
|
crossattn_emb_channels: int = 1024,
|
|
pos_emb_cls: str = "rope3d",
|
|
pos_emb_learnable: bool = False,
|
|
pos_emb_interpolation: str = "crop",
|
|
min_fps: int = 1,
|
|
max_fps: int = 30,
|
|
use_adaln_lora: bool = False,
|
|
adaln_lora_dim: int = 256,
|
|
rope_h_extrapolation_ratio: float = 1.0,
|
|
rope_w_extrapolation_ratio: float = 1.0,
|
|
rope_t_extrapolation_ratio: float = 1.0,
|
|
extra_per_block_abs_pos_emb: bool = False,
|
|
extra_h_extrapolation_ratio: float = 1.0,
|
|
extra_w_extrapolation_ratio: float = 1.0,
|
|
extra_t_extrapolation_ratio: float = 1.0,
|
|
rope_enable_fps_modulation: bool = True,
|
|
image_model: Optional[str] = None,
|
|
) -> None:
|
|
super().__init__()
|
|
self.max_img_h = max_img_h
|
|
self.max_img_w = max_img_w
|
|
self.max_frames = max_frames
|
|
self.in_channels = in_channels
|
|
self.out_channels = out_channels
|
|
self.patch_spatial = patch_spatial
|
|
self.patch_temporal = patch_temporal
|
|
self.num_heads = num_heads
|
|
self.num_blocks = num_blocks
|
|
self.model_channels = model_channels
|
|
self.concat_padding_mask = concat_padding_mask
|
|
self.pos_emb_cls = pos_emb_cls
|
|
self.extra_per_block_abs_pos_emb = extra_per_block_abs_pos_emb
|
|
|
|
# Positional embeddings
|
|
self.pos_embedder = VideoRopePosition3DEmb(
|
|
head_dim=model_channels // num_heads,
|
|
len_h=max_img_h // patch_spatial,
|
|
len_w=max_img_w // patch_spatial,
|
|
len_t=max_frames // patch_temporal,
|
|
max_fps=max_fps,
|
|
min_fps=min_fps,
|
|
h_extrapolation_ratio=rope_h_extrapolation_ratio,
|
|
w_extrapolation_ratio=rope_w_extrapolation_ratio,
|
|
t_extrapolation_ratio=rope_t_extrapolation_ratio,
|
|
enable_fps_modulation=rope_enable_fps_modulation,
|
|
)
|
|
|
|
if extra_per_block_abs_pos_emb:
|
|
self.extra_pos_embedder = LearnablePosEmbAxis(
|
|
model_channels=model_channels,
|
|
len_h=max_img_h // patch_spatial,
|
|
len_w=max_img_w // patch_spatial,
|
|
len_t=max_frames // patch_temporal,
|
|
)
|
|
|
|
self.use_adaln_lora = use_adaln_lora
|
|
self.adaln_lora_dim = adaln_lora_dim
|
|
|
|
# Timestep embedding
|
|
self.t_embedder = nn.Sequential(
|
|
Timesteps(model_channels),
|
|
TimestepEmbedding(model_channels, model_channels, use_adaln_lora=use_adaln_lora),
|
|
)
|
|
self.t_embedding_norm = nn.RMSNorm(model_channels, eps=1e-6)
|
|
|
|
# Patch embedding
|
|
embed_in_channels = in_channels + 1 if concat_padding_mask else in_channels
|
|
self.x_embedder = PatchEmbed(
|
|
spatial_patch_size=patch_spatial,
|
|
temporal_patch_size=patch_temporal,
|
|
in_channels=embed_in_channels,
|
|
out_channels=model_channels,
|
|
)
|
|
|
|
# Transformer blocks
|
|
self.blocks = nn.ModuleList(
|
|
[
|
|
DiTBlock(
|
|
x_dim=model_channels,
|
|
context_dim=crossattn_emb_channels,
|
|
num_heads=num_heads,
|
|
mlp_ratio=mlp_ratio,
|
|
use_adaln_lora=use_adaln_lora,
|
|
adaln_lora_dim=adaln_lora_dim,
|
|
)
|
|
for _ in range(num_blocks)
|
|
]
|
|
)
|
|
|
|
# Final output layer
|
|
self.final_layer = FinalLayer(
|
|
hidden_size=model_channels,
|
|
spatial_patch_size=patch_spatial,
|
|
temporal_patch_size=patch_temporal,
|
|
out_channels=out_channels,
|
|
use_adaln_lora=use_adaln_lora,
|
|
adaln_lora_dim=adaln_lora_dim,
|
|
)
|
|
|
|
def _pad_to_patch_size(self, x: torch.Tensor) -> torch.Tensor:
|
|
"""Pad input tensor so dimensions are divisible by patch sizes."""
|
|
_, _, T, H, W = x.shape
|
|
pad_t = (self.patch_temporal - T % self.patch_temporal) % self.patch_temporal
|
|
pad_h = (self.patch_spatial - H % self.patch_spatial) % self.patch_spatial
|
|
pad_w = (self.patch_spatial - W % self.patch_spatial) % self.patch_spatial
|
|
if pad_t > 0 or pad_h > 0 or pad_w > 0:
|
|
x = F.pad(x, (0, pad_w, 0, pad_h, 0, pad_t))
|
|
return x
|
|
|
|
def prepare_embedded_sequence(
|
|
self,
|
|
x_B_C_T_H_W: torch.Tensor,
|
|
fps: Optional[torch.Tensor] = None,
|
|
padding_mask: Optional[torch.Tensor] = None,
|
|
) -> Tuple[torch.Tensor, Optional[torch.Tensor], Optional[torch.Tensor]]:
|
|
if self.concat_padding_mask:
|
|
if padding_mask is None:
|
|
padding_mask = torch.zeros(
|
|
x_B_C_T_H_W.shape[0],
|
|
1,
|
|
x_B_C_T_H_W.shape[3],
|
|
x_B_C_T_H_W.shape[4],
|
|
dtype=x_B_C_T_H_W.dtype,
|
|
device=x_B_C_T_H_W.device,
|
|
)
|
|
x_B_C_T_H_W = torch.cat(
|
|
[x_B_C_T_H_W, padding_mask.unsqueeze(1).repeat(1, 1, x_B_C_T_H_W.shape[2], 1, 1)], dim=1
|
|
)
|
|
|
|
x_B_T_H_W_D = self.x_embedder(x_B_C_T_H_W)
|
|
|
|
extra_pos_emb = None
|
|
if self.extra_per_block_abs_pos_emb:
|
|
extra_pos_emb = self.extra_pos_embedder(
|
|
x_B_T_H_W_D, fps=fps, device=x_B_C_T_H_W.device, dtype=x_B_C_T_H_W.dtype
|
|
)
|
|
|
|
if "rope" in self.pos_emb_cls.lower():
|
|
return x_B_T_H_W_D, self.pos_embedder(x_B_T_H_W_D, fps=fps, device=x_B_C_T_H_W.device), extra_pos_emb
|
|
|
|
return x_B_T_H_W_D, None, extra_pos_emb
|
|
|
|
def unpatchify(self, x_B_T_H_W_M: torch.Tensor) -> torch.Tensor:
|
|
return rearrange(
|
|
x_B_T_H_W_M,
|
|
"B T H W (p1 p2 t C) -> B C (T t) (H p1) (W p2)",
|
|
p1=self.patch_spatial,
|
|
p2=self.patch_spatial,
|
|
t=self.patch_temporal,
|
|
)
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
timesteps: torch.Tensor,
|
|
context: torch.Tensor,
|
|
fps: Optional[torch.Tensor] = None,
|
|
padding_mask: Optional[torch.Tensor] = None,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
orig_shape = list(x.shape)
|
|
x = self._pad_to_patch_size(x)
|
|
|
|
x_B_T_H_W_D, rope_emb_L_1_1_D, extra_pos_emb = self.prepare_embedded_sequence(
|
|
x, fps=fps, padding_mask=padding_mask
|
|
)
|
|
|
|
if timesteps.ndim == 1:
|
|
timesteps = timesteps.unsqueeze(1)
|
|
t_emb, adaln_lora = self.t_embedder[1](self.t_embedder[0](timesteps).to(x_B_T_H_W_D.dtype))
|
|
t_emb = self.t_embedding_norm(t_emb)
|
|
|
|
block_kwargs = {
|
|
"rope_emb_L_1_1_D": rope_emb_L_1_1_D.unsqueeze(1).unsqueeze(0) if rope_emb_L_1_1_D is not None else None,
|
|
"adaln_lora_B_T_3D": adaln_lora,
|
|
"extra_per_block_pos_emb": extra_pos_emb,
|
|
}
|
|
|
|
# Keep residual stream in fp32 for numerical stability with fp16 compute
|
|
if x_B_T_H_W_D.dtype == torch.float16:
|
|
x_B_T_H_W_D = x_B_T_H_W_D.float()
|
|
|
|
for block in self.blocks:
|
|
x_B_T_H_W_D = block(x_B_T_H_W_D, t_emb, context, **block_kwargs)
|
|
|
|
x_out = self.final_layer(x_B_T_H_W_D.to(context.dtype), t_emb, adaln_lora_B_T_3D=adaln_lora)
|
|
x_out = self.unpatchify(x_out)[:, :, : orig_shape[-3], : orig_shape[-2], : orig_shape[-1]]
|
|
return x_out
|
|
|
|
|
|
# ============================================================================
|
|
# LLM Adapter
|
|
# Reference implementation: https://github.com/hdae/diffusers-anima
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# ============================================================================
|
|
|
|
|
|
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
|
"""Split the last dimension in half and negate-swap: [-x2, x1]."""
|
|
half = x.shape[-1] // 2
|
|
first, second = x[..., :half], x[..., half:]
|
|
return torch.cat((-second, first), dim=-1)
|
|
|
|
|
|
def _apply_rope(x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor) -> torch.Tensor:
|
|
"""Apply rotary position embeddings to tensor x given precomputed cos/sin."""
|
|
return (x * cos.unsqueeze(1)) + (_rotate_half(x) * sin.unsqueeze(1))
|
|
|
|
|
|
class LLMAdapterRotaryEmbedding(nn.Module):
|
|
"""Rotary position embedding for the LLM Adapter's attention layers."""
|
|
|
|
def __init__(self, head_dim: int, theta: float = 10000.0):
|
|
super().__init__()
|
|
half_dim = head_dim // 2
|
|
index = torch.arange(half_dim, dtype=torch.float32)
|
|
exponent = (2.0 / float(head_dim)) * index
|
|
inv_freq = torch.reciprocal(torch.pow(torch.tensor(theta, dtype=torch.float32), exponent))
|
|
self.register_buffer("inv_freq", inv_freq, persistent=False)
|
|
|
|
def forward(self, x: torch.Tensor, position_ids: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
pos = position_ids.to(device=x.device, dtype=torch.float32)
|
|
inv = self.inv_freq.to(device=x.device, dtype=torch.float32)
|
|
freqs = torch.einsum("bl,d->bld", pos, inv)
|
|
emb = freqs.repeat(1, 1, 2)
|
|
return emb.cos().to(dtype=x.dtype), emb.sin().to(dtype=x.dtype)
|
|
|
|
|
|
class LLMAdapterAttention(nn.Module):
|
|
"""Attention for the LLM Adapter with QK normalization and rotary position embeddings."""
|
|
|
|
def __init__(self, query_dim: int, context_dim: int, n_heads: int, head_dim: int):
|
|
super().__init__()
|
|
inner_dim = head_dim * n_heads
|
|
self.n_heads = n_heads
|
|
self.head_dim = head_dim
|
|
|
|
self.q_proj = nn.Linear(query_dim, inner_dim, bias=False)
|
|
self.q_norm = nn.RMSNorm(head_dim, eps=1e-6)
|
|
self.k_proj = nn.Linear(context_dim, inner_dim, bias=False)
|
|
self.k_norm = nn.RMSNorm(head_dim, eps=1e-6)
|
|
self.v_proj = nn.Linear(context_dim, inner_dim, bias=False)
|
|
self.o_proj = nn.Linear(inner_dim, query_dim, bias=False)
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
*,
|
|
context: Optional[torch.Tensor] = None,
|
|
attn_mask: Optional[torch.Tensor] = None,
|
|
pos_q: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
|
pos_k: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
|
) -> torch.Tensor:
|
|
context = x if context is None else context
|
|
|
|
q = self.q_proj(x).view(x.shape[0], x.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
|
|
k = self.k_proj(context).view(context.shape[0], context.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
|
|
v = self.v_proj(context).view(context.shape[0], context.shape[1], self.n_heads, self.head_dim).transpose(1, 2)
|
|
|
|
q = self.q_norm(q)
|
|
k = self.k_norm(k)
|
|
|
|
if pos_q is not None and pos_k is not None:
|
|
q = _apply_rope(q, *pos_q)
|
|
k = _apply_rope(k, *pos_k)
|
|
|
|
y = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
|
|
y = y.transpose(1, 2).reshape(x.shape[0], x.shape[1], -1).contiguous()
|
|
return self.o_proj(y)
|
|
|
|
|
|
class LLMAdapterTransformerBlock(nn.Module):
|
|
"""Single transformer block in the LLM Adapter.
|
|
|
|
Each block contains self-attention, cross-attention, and MLP with
|
|
RMSNorm pre-normalization.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
source_dim: int,
|
|
model_dim: int,
|
|
num_heads: int = 16,
|
|
):
|
|
super().__init__()
|
|
head_dim = model_dim // num_heads
|
|
|
|
self.norm_self_attn = nn.RMSNorm(model_dim, eps=1e-6)
|
|
self.self_attn = LLMAdapterAttention(model_dim, model_dim, num_heads, head_dim)
|
|
|
|
self.norm_cross_attn = nn.RMSNorm(model_dim, eps=1e-6)
|
|
self.cross_attn = LLMAdapterAttention(model_dim, source_dim, num_heads, head_dim)
|
|
|
|
self.norm_mlp = nn.RMSNorm(model_dim, eps=1e-6)
|
|
self.mlp = nn.Sequential(
|
|
nn.Linear(model_dim, model_dim * 4),
|
|
nn.GELU(),
|
|
nn.Linear(model_dim * 4, model_dim),
|
|
)
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
*,
|
|
context: torch.Tensor,
|
|
target_mask: Optional[torch.Tensor] = None,
|
|
source_mask: Optional[torch.Tensor] = None,
|
|
pos_target: Tuple[torch.Tensor, torch.Tensor],
|
|
pos_source: Tuple[torch.Tensor, torch.Tensor],
|
|
) -> torch.Tensor:
|
|
x = x + self.self_attn(
|
|
self.norm_self_attn(x),
|
|
attn_mask=target_mask,
|
|
pos_q=pos_target,
|
|
pos_k=pos_target,
|
|
)
|
|
x = x + self.cross_attn(
|
|
self.norm_cross_attn(x),
|
|
context=context,
|
|
attn_mask=source_mask,
|
|
pos_q=pos_target,
|
|
pos_k=pos_source,
|
|
)
|
|
x = x + self.mlp(self.norm_mlp(x))
|
|
return x
|
|
|
|
|
|
class LLMAdapter(nn.Module):
|
|
"""LLM Adapter: bridges Qwen3 hidden states and T5-XXL token embeddings.
|
|
|
|
Takes Qwen3 hidden states and T5-XXL token IDs, produces conditioning
|
|
embeddings for the Cosmos DiT via cross-attention through 6 transformer layers.
|
|
|
|
Args:
|
|
vocab_size: Size of the T5 token vocabulary.
|
|
dim: Model dimension (used for embeddings, projections, and all layers).
|
|
num_layers: Number of transformer layers.
|
|
num_heads: Number of attention heads.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
vocab_size: int = 32128,
|
|
dim: int = 1024,
|
|
num_layers: int = 6,
|
|
num_heads: int = 16,
|
|
):
|
|
super().__init__()
|
|
self.embed = nn.Embedding(vocab_size, dim)
|
|
self.blocks = nn.ModuleList(
|
|
[LLMAdapterTransformerBlock(source_dim=dim, model_dim=dim, num_heads=num_heads) for _ in range(num_layers)]
|
|
)
|
|
self.out_proj = nn.Linear(dim, dim)
|
|
self.norm = nn.RMSNorm(dim, eps=1e-6)
|
|
self.rotary_emb = LLMAdapterRotaryEmbedding(dim // num_heads)
|
|
|
|
def forward(
|
|
self,
|
|
source_hidden_states: torch.Tensor,
|
|
target_input_ids: torch.Tensor,
|
|
target_attention_mask: Optional[torch.Tensor] = None,
|
|
source_attention_mask: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
# Expand attention masks for multi-head attention
|
|
if target_attention_mask is not None:
|
|
target_attention_mask = target_attention_mask.to(torch.bool)
|
|
if target_attention_mask.ndim == 2:
|
|
target_attention_mask = target_attention_mask[:, None, None, :]
|
|
|
|
if source_attention_mask is not None:
|
|
source_attention_mask = source_attention_mask.to(torch.bool)
|
|
if source_attention_mask.ndim == 2:
|
|
source_attention_mask = source_attention_mask[:, None, None, :]
|
|
|
|
context = source_hidden_states
|
|
x = self.embed(target_input_ids).to(dtype=context.dtype)
|
|
|
|
# Build position IDs and compute rotary embeddings
|
|
target_pos_ids = torch.arange(x.shape[1], device=x.device, dtype=torch.long).unsqueeze(0)
|
|
source_pos_ids = torch.arange(context.shape[1], device=x.device, dtype=torch.long).unsqueeze(0)
|
|
pos_target = self.rotary_emb(x, target_pos_ids)
|
|
pos_source = self.rotary_emb(x, source_pos_ids)
|
|
|
|
for block in self.blocks:
|
|
x = block(
|
|
x,
|
|
context=context,
|
|
target_mask=target_attention_mask,
|
|
source_mask=source_attention_mask,
|
|
pos_target=pos_target,
|
|
pos_source=pos_source,
|
|
)
|
|
return self.norm(self.out_proj(x))
|
|
|
|
|
|
# ============================================================================
|
|
# Anima: MiniTrainDIT + LLMAdapter
|
|
# Reference implementation: https://github.com/hdae/diffusers-anima
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
# ============================================================================
|
|
|
|
|
|
class AnimaTransformer(MiniTrainDIT):
|
|
"""Anima transformer: Cosmos Predict2 DiT with integrated LLM Adapter.
|
|
|
|
Extends MiniTrainDIT by adding the LLMAdapter component that preprocesses
|
|
text embeddings before they are fed to the DiT cross-attention layers.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
super().__init__(*args, **kwargs)
|
|
self.llm_adapter = LLMAdapter()
|
|
|
|
def preprocess_text_embeds(
|
|
self,
|
|
text_embeds: torch.Tensor,
|
|
text_ids: Optional[torch.Tensor],
|
|
t5xxl_weights: Optional[torch.Tensor] = None,
|
|
) -> torch.Tensor:
|
|
"""Run the LLM Adapter to produce conditioning for the DiT.
|
|
|
|
Args:
|
|
text_embeds: Qwen3 hidden states. Shape: (batch, seq_len, 1024).
|
|
text_ids: T5-XXL token IDs. Shape: (batch, seq_len). If None, returns text_embeds directly.
|
|
t5xxl_weights: Optional per-token weights. Shape: (batch, seq_len, 1).
|
|
|
|
Returns:
|
|
Conditioning tensor. Shape: (batch, 512, 1024), zero-padded if needed.
|
|
"""
|
|
if text_ids is None:
|
|
return text_embeds
|
|
out = self.llm_adapter(text_embeds, text_ids)
|
|
if t5xxl_weights is not None:
|
|
out = out * t5xxl_weights
|
|
if out.shape[1] < 512:
|
|
out = F.pad(out, (0, 0, 0, 512 - out.shape[1]))
|
|
return out
|
|
|
|
def forward(
|
|
self,
|
|
x: torch.Tensor,
|
|
timesteps: torch.Tensor,
|
|
context: torch.Tensor,
|
|
t5xxl_ids: Optional[torch.Tensor] = None,
|
|
t5xxl_weights: Optional[torch.Tensor] = None,
|
|
**kwargs,
|
|
) -> torch.Tensor:
|
|
"""Forward pass with LLM Adapter preprocessing.
|
|
|
|
Args:
|
|
x: Input latent tensor. Shape: (B, C, T, H, W).
|
|
timesteps: Timestep values. Shape: (B,) or (B, T).
|
|
context: Qwen3 hidden states. Shape: (B, seq_len, 1024).
|
|
t5xxl_ids: T5-XXL token IDs. Shape: (B, seq_len).
|
|
t5xxl_weights: Per-token weights. Shape: (B, seq_len, 1).
|
|
|
|
Returns:
|
|
Denoised output. Shape: (B, C, T, H, W).
|
|
"""
|
|
if t5xxl_ids is not None:
|
|
context = self.preprocess_text_embeds(context, t5xxl_ids, t5xxl_weights=t5xxl_weights)
|
|
return super().forward(x, timesteps, context, **kwargs)
|