[diffusion] model: LTX-2 Support (2/2) (#17496)
Co-authored-by: Fan Yin <1106310035@qq.com> Co-authored-by: Yuhao Yang <47235274+yhyang201@users.noreply.github.com>
This commit is contained in:
co-authored by
Fan Yin
Yuhao Yang
parent
797a9811a2
commit
d0919be733
@@ -0,0 +1,594 @@
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.attention import FeedForward
|
||||
|
||||
from sglang.multimodal_gen.configs.models.adapter.ltx_2_connector import (
|
||||
LTX2ConnectorConfig,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
|
||||
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
|
||||
|
||||
|
||||
def apply_interleaved_rotary_emb(
|
||||
x: torch.Tensor, freqs: Tuple[torch.Tensor, torch.Tensor]
|
||||
) -> torch.Tensor:
|
||||
cos, sin = freqs
|
||||
x_real, x_imag = x.unflatten(2, (-1, 2)).unbind(-1) # [B, S, C // 2]
|
||||
x_rotated = torch.stack([-x_imag, x_real], dim=-1).flatten(2)
|
||||
out = (x.float() * cos + x_rotated.float() * sin).to(x.dtype)
|
||||
return out
|
||||
|
||||
|
||||
def apply_split_rotary_emb(
|
||||
x: torch.Tensor, freqs: Tuple[torch.Tensor, torch.Tensor]
|
||||
) -> torch.Tensor:
|
||||
cos, sin = freqs
|
||||
|
||||
x_dtype = x.dtype
|
||||
needs_reshape = False
|
||||
if x.ndim != 4 and cos.ndim == 4:
|
||||
# cos is (#b, h, t, r) -> reshape x to (b, h, t, dim_per_head)
|
||||
# The cos/sin batch dim may only be broadcastable, so take batch size from x
|
||||
b = x.shape[0]
|
||||
_, h, t, _ = cos.shape
|
||||
x = x.reshape(b, t, h, -1).swapaxes(1, 2)
|
||||
needs_reshape = True
|
||||
|
||||
# Split last dim (2*r) into (d=2, r)
|
||||
last = x.shape[-1]
|
||||
if last % 2 != 0:
|
||||
raise ValueError(
|
||||
f"Expected x.shape[-1] to be even for split rotary, got {last}."
|
||||
)
|
||||
r = last // 2
|
||||
|
||||
# (..., 2, r)
|
||||
split_x = x.reshape(*x.shape[:-1], 2, r).float() # Explicitly upcast to float
|
||||
first_x = split_x[..., :1, :] # (..., 1, r)
|
||||
second_x = split_x[..., 1:, :] # (..., 1, r)
|
||||
|
||||
cos_u = cos.unsqueeze(-2) # broadcast to (..., 1, r) against (..., 2, r)
|
||||
sin_u = sin.unsqueeze(-2)
|
||||
|
||||
out = split_x * cos_u
|
||||
first_out = out[..., :1, :]
|
||||
second_out = out[..., 1:, :]
|
||||
|
||||
first_out.addcmul_(-sin_u, second_x)
|
||||
second_out.addcmul_(sin_u, first_x)
|
||||
|
||||
out = out.reshape(*out.shape[:-2], last)
|
||||
|
||||
if needs_reshape:
|
||||
out = out.swapaxes(1, 2).reshape(b, t, -1)
|
||||
|
||||
out = out.to(dtype=x_dtype)
|
||||
return out
|
||||
|
||||
|
||||
class LTX2Attention(torch.nn.Module):
|
||||
r"""
|
||||
Attention class for all LTX-2.0 attention layers. Compared to LTX-1.0, this supports specifying the query and key
|
||||
RoPE embeddings separately for audio-to-video (a2v) and video-to-audio (v2a) cross-attention.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
query_dim: int,
|
||||
heads: int = 8,
|
||||
kv_heads: int = 8,
|
||||
dim_head: int = 64,
|
||||
dropout: float = 0.0,
|
||||
bias: bool = True,
|
||||
cross_attention_dim: Optional[int] = None,
|
||||
out_bias: bool = True,
|
||||
qk_norm: str = "rms_norm_across_heads",
|
||||
norm_eps: float = 1e-6,
|
||||
norm_elementwise_affine: bool = True,
|
||||
rope_type: str = "interleaved",
|
||||
processor=None,
|
||||
):
|
||||
super().__init__()
|
||||
if qk_norm != "rms_norm_across_heads":
|
||||
raise NotImplementedError(
|
||||
"Only 'rms_norm_across_heads' is supported as a valid value for `qk_norm`."
|
||||
)
|
||||
|
||||
self.head_dim = dim_head
|
||||
self.inner_dim = dim_head * heads
|
||||
self.inner_kv_dim = self.inner_dim if kv_heads is None else dim_head * kv_heads
|
||||
self.query_dim = query_dim
|
||||
self.cross_attention_dim = (
|
||||
cross_attention_dim if cross_attention_dim is not None else query_dim
|
||||
)
|
||||
self.use_bias = bias
|
||||
self.dropout = dropout
|
||||
self.out_dim = query_dim
|
||||
self.heads = heads
|
||||
self.rope_type = rope_type
|
||||
|
||||
self.norm_q = torch.nn.RMSNorm(
|
||||
dim_head * heads, eps=norm_eps, elementwise_affine=norm_elementwise_affine
|
||||
)
|
||||
self.norm_k = torch.nn.RMSNorm(
|
||||
dim_head * kv_heads,
|
||||
eps=norm_eps,
|
||||
elementwise_affine=norm_elementwise_affine,
|
||||
)
|
||||
self.to_q = torch.nn.Linear(query_dim, self.inner_dim, bias=bias)
|
||||
self.to_k = torch.nn.Linear(
|
||||
self.cross_attention_dim, self.inner_kv_dim, bias=bias
|
||||
)
|
||||
self.to_v = torch.nn.Linear(
|
||||
self.cross_attention_dim, self.inner_kv_dim, bias=bias
|
||||
)
|
||||
self.to_out = torch.nn.ModuleList([])
|
||||
self.to_out.append(torch.nn.Linear(self.inner_dim, self.out_dim, bias=out_bias))
|
||||
self.to_out.append(torch.nn.Dropout(dropout))
|
||||
|
||||
# Scaled dot product attention
|
||||
self.attn = USPAttention(
|
||||
num_heads=heads,
|
||||
head_size=self.head_dim,
|
||||
dropout_rate=0,
|
||||
softmax_scale=None,
|
||||
causal=False,
|
||||
supported_attention_backends={
|
||||
AttentionBackendEnum.FA,
|
||||
AttentionBackendEnum.AITER,
|
||||
AttentionBackendEnum.TORCH_SDPA,
|
||||
AttentionBackendEnum.SAGE_ATTN,
|
||||
AttentionBackendEnum.SAGE_ATTN_3,
|
||||
},
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
encoder_hidden_states: Optional[torch.Tensor] = None,
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
query_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
||||
key_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
||||
) -> torch.Tensor:
|
||||
batch_size, sequence_length, _ = (
|
||||
hidden_states.shape
|
||||
if encoder_hidden_states is None
|
||||
else encoder_hidden_states.shape
|
||||
)
|
||||
|
||||
if encoder_hidden_states is None:
|
||||
encoder_hidden_states = hidden_states
|
||||
|
||||
query = self.to_q(hidden_states)
|
||||
key = self.to_k(encoder_hidden_states)
|
||||
value = self.to_v(encoder_hidden_states)
|
||||
|
||||
query = self.norm_q(query)
|
||||
key = self.norm_k(key)
|
||||
|
||||
if query_rotary_emb is not None:
|
||||
if self.rope_type == "interleaved":
|
||||
query = apply_interleaved_rotary_emb(query, query_rotary_emb)
|
||||
key = apply_interleaved_rotary_emb(
|
||||
key,
|
||||
key_rotary_emb if key_rotary_emb is not None else query_rotary_emb,
|
||||
)
|
||||
elif self.rope_type == "split":
|
||||
query = apply_split_rotary_emb(query, query_rotary_emb)
|
||||
key = apply_split_rotary_emb(
|
||||
key,
|
||||
key_rotary_emb if key_rotary_emb is not None else query_rotary_emb,
|
||||
)
|
||||
|
||||
query = query.unflatten(2, (self.heads, -1))
|
||||
key = key.unflatten(2, (self.heads, -1))
|
||||
value = value.unflatten(2, (self.heads, -1))
|
||||
|
||||
hidden_states = self.attn(
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
)
|
||||
hidden_states = hidden_states.flatten(2, 3)
|
||||
hidden_states = hidden_states.to(query.dtype)
|
||||
|
||||
hidden_states = self.to_out[0](hidden_states)
|
||||
hidden_states = self.to_out[1](hidden_states)
|
||||
return hidden_states
|
||||
|
||||
|
||||
class LTX2RotaryPosEmbed1d(nn.Module):
|
||||
"""
|
||||
1D rotary positional embeddings (RoPE) for the LTX 2.0 text encoder connectors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
base_seq_len: int = 4096,
|
||||
theta: float = 10000.0,
|
||||
double_precision: bool = True,
|
||||
rope_type: str = "interleaved",
|
||||
num_attention_heads: int = 32,
|
||||
):
|
||||
super().__init__()
|
||||
if rope_type not in ["interleaved", "split"]:
|
||||
raise ValueError(
|
||||
f"{rope_type=} not supported. Choose between 'interleaved' and 'split'."
|
||||
)
|
||||
|
||||
self.dim = dim
|
||||
self.base_seq_len = base_seq_len
|
||||
self.theta = theta
|
||||
self.double_precision = double_precision
|
||||
self.rope_type = rope_type
|
||||
self.num_attention_heads = num_attention_heads
|
||||
|
||||
def forward(
|
||||
self,
|
||||
batch_size: int,
|
||||
pos: int,
|
||||
device: Union[str, torch.device],
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
# 1. Get 1D position ids
|
||||
grid_1d = torch.arange(pos, dtype=torch.float32, device=device)
|
||||
# Get fractional indices relative to self.base_seq_len
|
||||
grid_1d = grid_1d / self.base_seq_len
|
||||
grid = grid_1d.unsqueeze(0).repeat(batch_size, 1) # [batch_size, seq_len]
|
||||
|
||||
# 2. Calculate 1D RoPE frequencies
|
||||
num_rope_elems = 2 # 1 (because 1D) * 2 (for cos, sin) = 2
|
||||
freqs_dtype = torch.float64 if self.double_precision else torch.float32
|
||||
pow_indices = torch.pow(
|
||||
self.theta,
|
||||
torch.linspace(
|
||||
start=0.0,
|
||||
end=1.0,
|
||||
steps=self.dim // num_rope_elems,
|
||||
dtype=freqs_dtype,
|
||||
device=device,
|
||||
),
|
||||
)
|
||||
freqs = (pow_indices * torch.pi / 2.0).to(dtype=torch.float32)
|
||||
|
||||
# 3. Matrix-vector outer product between pos ids of shape (batch_size, seq_len) and freqs vector of shape
|
||||
# (self.dim // 2,).
|
||||
freqs = (grid.unsqueeze(-1) * 2 - 1) * freqs # [B, seq_len, self.dim // 2]
|
||||
|
||||
# 4. Get real, interleaved (cos, sin) frequencies, padded to self.dim
|
||||
if self.rope_type == "interleaved":
|
||||
cos_freqs = freqs.cos().repeat_interleave(2, dim=-1)
|
||||
sin_freqs = freqs.sin().repeat_interleave(2, dim=-1)
|
||||
|
||||
if self.dim % num_rope_elems != 0:
|
||||
cos_padding = torch.ones_like(
|
||||
cos_freqs[:, :, : self.dim % num_rope_elems]
|
||||
)
|
||||
sin_padding = torch.zeros_like(
|
||||
sin_freqs[:, :, : self.dim % num_rope_elems]
|
||||
)
|
||||
cos_freqs = torch.cat([cos_padding, cos_freqs], dim=-1)
|
||||
sin_freqs = torch.cat([sin_padding, sin_freqs], dim=-1)
|
||||
|
||||
elif self.rope_type == "split":
|
||||
expected_freqs = self.dim // 2
|
||||
current_freqs = freqs.shape[-1]
|
||||
pad_size = expected_freqs - current_freqs
|
||||
cos_freq = freqs.cos()
|
||||
sin_freq = freqs.sin()
|
||||
|
||||
if pad_size != 0:
|
||||
cos_padding = torch.ones_like(cos_freq[:, :, :pad_size])
|
||||
sin_padding = torch.zeros_like(sin_freq[:, :, :pad_size])
|
||||
|
||||
cos_freq = torch.concatenate([cos_padding, cos_freq], axis=-1)
|
||||
sin_freq = torch.concatenate([sin_padding, sin_freq], axis=-1)
|
||||
|
||||
# Reshape freqs to be compatible with multi-head attention
|
||||
b = cos_freq.shape[0]
|
||||
t = cos_freq.shape[1]
|
||||
|
||||
cos_freq = cos_freq.reshape(b, t, self.num_attention_heads, -1)
|
||||
sin_freq = sin_freq.reshape(b, t, self.num_attention_heads, -1)
|
||||
|
||||
cos_freqs = torch.swapaxes(cos_freq, 1, 2) # (B,H,T,D//2)
|
||||
sin_freqs = torch.swapaxes(sin_freq, 1, 2) # (B,H,T,D//2)
|
||||
|
||||
return cos_freqs, sin_freqs
|
||||
|
||||
|
||||
class LTX2TransformerBlock1d(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
dim: int,
|
||||
num_attention_heads: int,
|
||||
attention_head_dim: int,
|
||||
activation_fn: str = "gelu-approximate",
|
||||
eps: float = 1e-6,
|
||||
rope_type: str = "interleaved",
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.norm1 = torch.nn.RMSNorm(dim, eps=eps, elementwise_affine=False)
|
||||
self.attn1 = LTX2Attention(
|
||||
query_dim=dim,
|
||||
heads=num_attention_heads,
|
||||
kv_heads=num_attention_heads,
|
||||
dim_head=attention_head_dim,
|
||||
rope_type=rope_type,
|
||||
)
|
||||
|
||||
self.norm2 = torch.nn.RMSNorm(dim, eps=eps, elementwise_affine=False)
|
||||
self.ff = FeedForward(dim, activation_fn=activation_fn)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
rotary_emb: Optional[torch.Tensor] = None,
|
||||
) -> torch.Tensor:
|
||||
norm_hidden_states = self.norm1(hidden_states)
|
||||
attn_hidden_states = self.attn1(
|
||||
norm_hidden_states,
|
||||
attention_mask=attention_mask,
|
||||
query_rotary_emb=rotary_emb,
|
||||
)
|
||||
hidden_states = hidden_states + attn_hidden_states
|
||||
|
||||
norm_hidden_states = self.norm2(hidden_states)
|
||||
ff_hidden_states = self.ff(norm_hidden_states)
|
||||
hidden_states = hidden_states + ff_hidden_states
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
class LTX2ConnectorTransformer1d(nn.Module):
|
||||
"""
|
||||
A 1D sequence transformer for modalities such as text.
|
||||
In LTX 2.0, this is used to process the text encoder hidden states for each of the video and audio streams.
|
||||
"""
|
||||
|
||||
_supports_gradient_checkpointing = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
num_attention_heads: int = 30,
|
||||
attention_head_dim: int = 128,
|
||||
num_layers: int = 2,
|
||||
num_learnable_registers: int | None = 128,
|
||||
rope_base_seq_len: int = 4096,
|
||||
rope_theta: float = 10000.0,
|
||||
rope_double_precision: bool = True,
|
||||
eps: float = 1e-6,
|
||||
causal_temporal_positioning: bool = False,
|
||||
rope_type: str = "interleaved",
|
||||
):
|
||||
super().__init__()
|
||||
self.num_attention_heads = num_attention_heads
|
||||
self.inner_dim = num_attention_heads * attention_head_dim
|
||||
self.causal_temporal_positioning = causal_temporal_positioning
|
||||
|
||||
self.num_learnable_registers = num_learnable_registers
|
||||
self.learnable_registers = None
|
||||
if num_learnable_registers is not None:
|
||||
init_registers = (
|
||||
torch.rand(num_learnable_registers, self.inner_dim) * 2.0 - 1.0
|
||||
)
|
||||
self.learnable_registers = torch.nn.Parameter(init_registers)
|
||||
|
||||
self.rope = LTX2RotaryPosEmbed1d(
|
||||
self.inner_dim,
|
||||
base_seq_len=rope_base_seq_len,
|
||||
theta=rope_theta,
|
||||
double_precision=rope_double_precision,
|
||||
rope_type=rope_type,
|
||||
num_attention_heads=num_attention_heads,
|
||||
)
|
||||
|
||||
self.transformer_blocks = torch.nn.ModuleList(
|
||||
[
|
||||
LTX2TransformerBlock1d(
|
||||
dim=self.inner_dim,
|
||||
num_attention_heads=num_attention_heads,
|
||||
attention_head_dim=attention_head_dim,
|
||||
rope_type=rope_type,
|
||||
)
|
||||
for _ in range(num_layers)
|
||||
]
|
||||
)
|
||||
|
||||
self.norm_out = torch.nn.RMSNorm(
|
||||
self.inner_dim, eps=eps, elementwise_affine=False
|
||||
)
|
||||
|
||||
self.gradient_checkpointing = False
|
||||
|
||||
def forward(
|
||||
self,
|
||||
hidden_states: torch.Tensor,
|
||||
attention_mask: Optional[torch.Tensor] = None,
|
||||
attn_mask_binarize_threshold: float = -9000.0,
|
||||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||||
# hidden_states shape: [batch_size, seq_len, hidden_dim]
|
||||
# attention_mask shape: [batch_size, seq_len] or [batch_size, 1, 1, seq_len]
|
||||
batch_size, seq_len, _ = hidden_states.shape
|
||||
|
||||
# 1. Replace padding with learned registers, if using
|
||||
if self.learnable_registers is not None:
|
||||
if seq_len % self.num_learnable_registers != 0:
|
||||
raise ValueError(
|
||||
f"The `hidden_states` sequence length {hidden_states.shape[1]} should be divisible by the number"
|
||||
f" of learnable registers {self.num_learnable_registers}"
|
||||
)
|
||||
|
||||
num_register_repeats = seq_len // self.num_learnable_registers
|
||||
registers = torch.tile(
|
||||
self.learnable_registers, (num_register_repeats, 1)
|
||||
) # [seq_len, inner_dim]
|
||||
|
||||
binary_attn_mask = (attention_mask >= attn_mask_binarize_threshold).int()
|
||||
if binary_attn_mask.ndim == 4:
|
||||
binary_attn_mask = binary_attn_mask.squeeze(1).squeeze(
|
||||
1
|
||||
) # [B, 1, 1, L] --> [B, L]
|
||||
|
||||
hidden_states_non_padded = [
|
||||
hidden_states[i, binary_attn_mask[i].bool(), :]
|
||||
for i in range(batch_size)
|
||||
]
|
||||
valid_seq_lens = [x.shape[0] for x in hidden_states_non_padded]
|
||||
pad_lengths = [seq_len - valid_seq_len for valid_seq_len in valid_seq_lens]
|
||||
padded_hidden_states = [
|
||||
F.pad(x, pad=(0, 0, 0, p), value=0)
|
||||
for x, p in zip(hidden_states_non_padded, pad_lengths)
|
||||
]
|
||||
padded_hidden_states = torch.cat(
|
||||
[x.unsqueeze(0) for x in padded_hidden_states], dim=0
|
||||
) # [B, L, D]
|
||||
|
||||
flipped_mask = torch.flip(binary_attn_mask, dims=[1]).unsqueeze(
|
||||
-1
|
||||
) # [B, L, 1]
|
||||
hidden_states = (
|
||||
flipped_mask * padded_hidden_states + (1 - flipped_mask) * registers
|
||||
)
|
||||
|
||||
# Overwrite attention_mask with an all-zeros mask if using registers.
|
||||
attention_mask = torch.zeros_like(attention_mask)
|
||||
|
||||
# 2. Calculate 1D RoPE positional embeddings
|
||||
rotary_emb = self.rope(batch_size, seq_len, device=hidden_states.device)
|
||||
|
||||
# 3. Run 1D transformer blocks
|
||||
for block in self.transformer_blocks:
|
||||
if torch.is_grad_enabled() and self.gradient_checkpointing:
|
||||
hidden_states = self._gradient_checkpointing_func(
|
||||
block, hidden_states, attention_mask, rotary_emb
|
||||
)
|
||||
else:
|
||||
hidden_states = block(
|
||||
hidden_states, attention_mask=attention_mask, rotary_emb=rotary_emb
|
||||
)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states)
|
||||
|
||||
return hidden_states, attention_mask
|
||||
|
||||
|
||||
class LTX2TextConnectors(nn.Module):
|
||||
"""
|
||||
Text connector stack used by LTX 2.0 to process the packed text encoder hidden states for both the video and audio
|
||||
streams.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: LTX2ConnectorConfig,
|
||||
):
|
||||
super().__init__()
|
||||
caption_channels = config.caption_channels
|
||||
text_proj_in_factor = config.text_proj_in_factor
|
||||
video_connector_num_attention_heads = config.video_connector_num_attention_heads
|
||||
video_connector_attention_head_dim = config.video_connector_attention_head_dim
|
||||
video_connector_num_layers = config.video_connector_num_layers
|
||||
video_connector_num_learnable_registers = (
|
||||
config.video_connector_num_learnable_registers
|
||||
)
|
||||
audio_connector_num_attention_heads = config.audio_connector_num_attention_heads
|
||||
audio_connector_attention_head_dim = config.audio_connector_attention_head_dim
|
||||
audio_connector_num_layers = config.audio_connector_num_layers
|
||||
audio_connector_num_learnable_registers = (
|
||||
config.audio_connector_num_learnable_registers
|
||||
)
|
||||
connector_rope_base_seq_len = config.connector_rope_base_seq_len
|
||||
rope_theta = config.rope_theta
|
||||
rope_double_precision = config.rope_double_precision
|
||||
causal_temporal_positioning = config.causal_temporal_positioning
|
||||
rope_type = config.rope_type
|
||||
|
||||
self.text_proj_in = nn.Linear(
|
||||
caption_channels * text_proj_in_factor, caption_channels, bias=False
|
||||
)
|
||||
self.video_connector = LTX2ConnectorTransformer1d(
|
||||
num_attention_heads=video_connector_num_attention_heads,
|
||||
attention_head_dim=video_connector_attention_head_dim,
|
||||
num_layers=video_connector_num_layers,
|
||||
num_learnable_registers=video_connector_num_learnable_registers,
|
||||
rope_base_seq_len=connector_rope_base_seq_len,
|
||||
rope_theta=rope_theta,
|
||||
rope_double_precision=rope_double_precision,
|
||||
causal_temporal_positioning=causal_temporal_positioning,
|
||||
rope_type=rope_type,
|
||||
)
|
||||
self.audio_connector = LTX2ConnectorTransformer1d(
|
||||
num_attention_heads=audio_connector_num_attention_heads,
|
||||
attention_head_dim=audio_connector_attention_head_dim,
|
||||
num_layers=audio_connector_num_layers,
|
||||
num_learnable_registers=audio_connector_num_learnable_registers,
|
||||
rope_base_seq_len=connector_rope_base_seq_len,
|
||||
rope_theta=rope_theta,
|
||||
rope_double_precision=rope_double_precision,
|
||||
causal_temporal_positioning=causal_temporal_positioning,
|
||||
rope_type=rope_type,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
text_encoder_hidden_states: torch.Tensor,
|
||||
attention_mask: torch.Tensor,
|
||||
additive_mask: bool = False,
|
||||
):
|
||||
# Convert to additive attention mask, if necessary
|
||||
if not additive_mask:
|
||||
text_dtype = text_encoder_hidden_states.dtype
|
||||
attention_mask = (attention_mask - 1).reshape(
|
||||
attention_mask.shape[0], 1, -1, attention_mask.shape[-1]
|
||||
)
|
||||
attention_mask = attention_mask.to(text_dtype) * torch.finfo(text_dtype).max
|
||||
|
||||
# Ensure input dtype matches the layer's weight dtype
|
||||
if text_encoder_hidden_states.dtype != self.text_proj_in.weight.dtype:
|
||||
text_encoder_hidden_states = text_encoder_hidden_states.to(
|
||||
self.text_proj_in.weight.dtype
|
||||
)
|
||||
|
||||
# Ensure sequence length is divisible by num_learnable_registers (128)
|
||||
seq_len = text_encoder_hidden_states.shape[1]
|
||||
num_learnable_registers = self.video_connector.num_learnable_registers
|
||||
if (
|
||||
num_learnable_registers is not None
|
||||
and seq_len % num_learnable_registers != 0
|
||||
):
|
||||
pad_len = num_learnable_registers - (seq_len % num_learnable_registers)
|
||||
text_encoder_hidden_states = F.pad(
|
||||
text_encoder_hidden_states, (0, 0, 0, pad_len), value=0.0
|
||||
)
|
||||
|
||||
if attention_mask.shape[-1] == seq_len:
|
||||
# Pad with a large negative value to mask out the new tokens
|
||||
attention_mask = F.pad(attention_mask, (0, pad_len), value=-1000000.0)
|
||||
|
||||
text_encoder_hidden_states = self.text_proj_in(text_encoder_hidden_states)
|
||||
|
||||
video_text_embedding, new_attn_mask = self.video_connector(
|
||||
text_encoder_hidden_states, attention_mask
|
||||
)
|
||||
|
||||
attn_mask = (new_attn_mask < 1e-6).to(torch.int64)
|
||||
attn_mask = attn_mask.reshape(
|
||||
video_text_embedding.shape[0], video_text_embedding.shape[1], 1
|
||||
)
|
||||
video_text_embedding = video_text_embedding * attn_mask
|
||||
new_attn_mask = attn_mask.squeeze(-1)
|
||||
|
||||
audio_text_embedding, _ = self.audio_connector(
|
||||
text_encoder_hidden_states, attention_mask
|
||||
)
|
||||
|
||||
return video_text_embedding, audio_text_embedding, new_attn_mask
|
||||
|
||||
|
||||
EntryClass = LTX2TextConnectors
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,905 @@
|
||||
from typing import Optional, Tuple, Union
|
||||
|
||||
import torch
|
||||
import torch.nn.functional as F
|
||||
from diffusers.models.autoencoders.vae import (
|
||||
DecoderOutput,
|
||||
DiagonalGaussianDistribution,
|
||||
)
|
||||
from diffusers.models.modeling_outputs import AutoencoderKLOutput
|
||||
from torch import nn
|
||||
|
||||
from sglang.multimodal_gen.configs.models.vaes.ltx_audio import LTXAudioVAEConfig
|
||||
from sglang.multimodal_gen.runtime.models.vaes.common import ParallelTiledVAE
|
||||
|
||||
LATENT_DOWNSAMPLE_FACTOR = 4
|
||||
|
||||
|
||||
class LTX2AudioCausalConv2d(nn.Module):
|
||||
"""
|
||||
A causal 2D convolution that pads asymmetrically along the causal axis.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: int,
|
||||
kernel_size: Union[int, Tuple[int, int]],
|
||||
stride: int = 1,
|
||||
dilation: Union[int, Tuple[int, int]] = 1,
|
||||
groups: int = 1,
|
||||
bias: bool = True,
|
||||
causality_axis: str = "height",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.causality_axis = causality_axis
|
||||
kernel_size = (
|
||||
(kernel_size, kernel_size) if isinstance(kernel_size, int) else kernel_size
|
||||
)
|
||||
dilation = (dilation, dilation) if isinstance(dilation, int) else dilation
|
||||
|
||||
pad_h = (kernel_size[0] - 1) * dilation[0]
|
||||
pad_w = (kernel_size[1] - 1) * dilation[1]
|
||||
|
||||
if self.causality_axis == "none":
|
||||
padding = (pad_w // 2, pad_w - pad_w // 2, pad_h // 2, pad_h - pad_h // 2)
|
||||
elif self.causality_axis in {"width", "width-compatibility"}:
|
||||
padding = (pad_w, 0, pad_h // 2, pad_h - pad_h // 2)
|
||||
elif self.causality_axis == "height":
|
||||
padding = (pad_w // 2, pad_w - pad_w // 2, pad_h, 0)
|
||||
else:
|
||||
raise ValueError(f"Invalid causality_axis: {causality_axis}")
|
||||
|
||||
self.padding = padding
|
||||
self.conv = nn.Conv2d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
padding=0,
|
||||
dilation=dilation,
|
||||
groups=groups,
|
||||
bias=bias,
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = F.pad(x, self.padding)
|
||||
return self.conv(x)
|
||||
|
||||
|
||||
class LTX2AudioPixelNorm(nn.Module):
|
||||
"""
|
||||
Per-pixel (per-location) RMS normalization layer.
|
||||
"""
|
||||
|
||||
def __init__(self, dim: int = 1, eps: float = 1e-8) -> None:
|
||||
super().__init__()
|
||||
self.dim = dim
|
||||
self.eps = eps
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
mean_sq = torch.mean(x**2, dim=self.dim, keepdim=True)
|
||||
rms = torch.sqrt(mean_sq + self.eps)
|
||||
return x / rms
|
||||
|
||||
|
||||
class LTX2AudioAttnBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
norm_type: str = "group",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.in_channels = in_channels
|
||||
|
||||
if norm_type == "group":
|
||||
self.norm = nn.GroupNorm(
|
||||
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
|
||||
)
|
||||
elif norm_type == "pixel":
|
||||
self.norm = LTX2AudioPixelNorm(dim=1, eps=1e-6)
|
||||
else:
|
||||
raise ValueError(f"Invalid normalization type: {norm_type}")
|
||||
self.q = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
||||
self.k = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
||||
self.v = nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
||||
self.proj_out = nn.Conv2d(
|
||||
in_channels, in_channels, kernel_size=1, stride=1, padding=0
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
h_ = self.norm(x)
|
||||
q = self.q(h_)
|
||||
k = self.k(h_)
|
||||
v = self.v(h_)
|
||||
|
||||
batch, channels, height, width = q.shape
|
||||
q = q.reshape(batch, channels, height * width).permute(0, 2, 1).contiguous()
|
||||
k = k.reshape(batch, channels, height * width).contiguous()
|
||||
attn = torch.bmm(q, k) * (int(channels) ** (-0.5))
|
||||
attn = torch.nn.functional.softmax(attn, dim=2)
|
||||
|
||||
v = v.reshape(batch, channels, height * width)
|
||||
attn = attn.permute(0, 2, 1).contiguous()
|
||||
h_ = torch.bmm(v, attn).reshape(batch, channels, height, width)
|
||||
|
||||
h_ = self.proj_out(h_)
|
||||
return x + h_
|
||||
|
||||
|
||||
class LTX2AudioResnetBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
out_channels: Optional[int] = None,
|
||||
conv_shortcut: bool = False,
|
||||
dropout: float = 0.0,
|
||||
temb_channels: int = 512,
|
||||
norm_type: str = "group",
|
||||
causality_axis: str = "height",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.causality_axis = causality_axis
|
||||
|
||||
if (
|
||||
self.causality_axis is not None
|
||||
and self.causality_axis != "none"
|
||||
and norm_type == "group"
|
||||
):
|
||||
raise ValueError("Causal ResnetBlock with GroupNorm is not supported.")
|
||||
self.in_channels = in_channels
|
||||
out_channels = in_channels if out_channels is None else out_channels
|
||||
self.out_channels = out_channels
|
||||
self.use_conv_shortcut = conv_shortcut
|
||||
|
||||
if norm_type == "group":
|
||||
self.norm1 = nn.GroupNorm(
|
||||
num_groups=32, num_channels=in_channels, eps=1e-6, affine=True
|
||||
)
|
||||
elif norm_type == "pixel":
|
||||
self.norm1 = LTX2AudioPixelNorm(dim=1, eps=1e-6)
|
||||
else:
|
||||
raise ValueError(f"Invalid normalization type: {norm_type}")
|
||||
self.non_linearity = nn.SiLU()
|
||||
if causality_axis is not None:
|
||||
self.conv1 = LTX2AudioCausalConv2d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv1 = nn.Conv2d(
|
||||
in_channels, out_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
if temb_channels > 0:
|
||||
self.temb_proj = nn.Linear(temb_channels, out_channels)
|
||||
if norm_type == "group":
|
||||
self.norm2 = nn.GroupNorm(
|
||||
num_groups=32, num_channels=out_channels, eps=1e-6, affine=True
|
||||
)
|
||||
elif norm_type == "pixel":
|
||||
self.norm2 = LTX2AudioPixelNorm(dim=1, eps=1e-6)
|
||||
else:
|
||||
raise ValueError(f"Invalid normalization type: {norm_type}")
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
if causality_axis is not None:
|
||||
self.conv2 = LTX2AudioCausalConv2d(
|
||||
out_channels,
|
||||
out_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv2 = nn.Conv2d(
|
||||
out_channels, out_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
if self.in_channels != self.out_channels:
|
||||
if self.use_conv_shortcut:
|
||||
if causality_axis is not None:
|
||||
self.conv_shortcut = LTX2AudioCausalConv2d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv_shortcut = nn.Conv2d(
|
||||
in_channels, out_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
else:
|
||||
if causality_axis is not None:
|
||||
self.nin_shortcut = LTX2AudioCausalConv2d(
|
||||
in_channels,
|
||||
out_channels,
|
||||
kernel_size=1,
|
||||
stride=1,
|
||||
causality_axis=causality_axis,
|
||||
)
|
||||
else:
|
||||
self.nin_shortcut = nn.Conv2d(
|
||||
in_channels, out_channels, kernel_size=1, stride=1, padding=0
|
||||
)
|
||||
|
||||
def forward(
|
||||
self, x: torch.Tensor, temb: Optional[torch.Tensor] = None
|
||||
) -> torch.Tensor:
|
||||
h = self.norm1(x)
|
||||
h = self.non_linearity(h)
|
||||
h = self.conv1(h)
|
||||
|
||||
if temb is not None:
|
||||
h = h + self.temb_proj(self.non_linearity(temb))[:, :, None, None]
|
||||
|
||||
h = self.norm2(h)
|
||||
h = self.non_linearity(h)
|
||||
h = self.dropout(h)
|
||||
h = self.conv2(h)
|
||||
|
||||
if self.in_channels != self.out_channels:
|
||||
x = (
|
||||
self.conv_shortcut(x)
|
||||
if self.use_conv_shortcut
|
||||
else self.nin_shortcut(x)
|
||||
)
|
||||
|
||||
return x + h
|
||||
|
||||
|
||||
class LTX2AudioDownsample(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
with_conv: bool,
|
||||
causality_axis: Optional[str] = "height",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.with_conv = with_conv
|
||||
self.causality_axis = causality_axis
|
||||
|
||||
if self.with_conv:
|
||||
self.conv = torch.nn.Conv2d(
|
||||
in_channels, in_channels, kernel_size=3, stride=2, padding=0
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
if self.with_conv:
|
||||
# Padding tuple is in the order: (left, right, top, bottom).
|
||||
if self.causality_axis == "none":
|
||||
pad = (0, 1, 0, 1)
|
||||
elif self.causality_axis == "width":
|
||||
pad = (2, 0, 0, 1)
|
||||
elif self.causality_axis == "height":
|
||||
pad = (0, 1, 2, 0)
|
||||
elif self.causality_axis == "width-compatibility":
|
||||
pad = (1, 0, 0, 1)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Invalid `causality_axis` {self.causality_axis}; supported values are `none`, `width`, `height`,"
|
||||
f" and `width-compatibility`."
|
||||
)
|
||||
|
||||
x = F.pad(x, pad, mode="constant", value=0)
|
||||
x = self.conv(x)
|
||||
else:
|
||||
# with_conv=False implies that causality_axis is "none"
|
||||
x = F.avg_pool2d(x, kernel_size=2, stride=2)
|
||||
return x
|
||||
|
||||
|
||||
class LTX2AudioUpsample(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
in_channels: int,
|
||||
with_conv: bool,
|
||||
causality_axis: Optional[str] = "height",
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.with_conv = with_conv
|
||||
self.causality_axis = causality_axis
|
||||
if self.with_conv:
|
||||
if causality_axis is not None:
|
||||
self.conv = LTX2AudioCausalConv2d(
|
||||
in_channels,
|
||||
in_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv = nn.Conv2d(
|
||||
in_channels, in_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
x = torch.nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
|
||||
if self.with_conv:
|
||||
x = self.conv(x)
|
||||
if self.causality_axis is None or self.causality_axis == "none":
|
||||
pass
|
||||
elif self.causality_axis == "height":
|
||||
x = x[:, :, 1:, :]
|
||||
elif self.causality_axis == "width":
|
||||
x = x[:, :, :, 1:]
|
||||
elif self.causality_axis == "width-compatibility":
|
||||
pass
|
||||
else:
|
||||
raise ValueError(f"Invalid causality_axis: {self.causality_axis}")
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class LTX2AudioAudioPatchifier:
|
||||
"""
|
||||
Patchifier for spectrogram/audio latents.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
patch_size: int,
|
||||
sample_rate: int = 16000,
|
||||
hop_length: int = 160,
|
||||
audio_latent_downsample_factor: int = 4,
|
||||
is_causal: bool = True,
|
||||
):
|
||||
self.hop_length = hop_length
|
||||
self.sample_rate = sample_rate
|
||||
self.audio_latent_downsample_factor = audio_latent_downsample_factor
|
||||
self.is_causal = is_causal
|
||||
self._patch_size = (1, patch_size, patch_size)
|
||||
|
||||
def patchify(self, audio_latents: torch.Tensor) -> torch.Tensor:
|
||||
batch, channels, time, freq = audio_latents.shape
|
||||
return audio_latents.permute(0, 2, 1, 3).reshape(batch, time, channels * freq)
|
||||
|
||||
def unpatchify(
|
||||
self, audio_latents: torch.Tensor, channels: int, mel_bins: int
|
||||
) -> torch.Tensor:
|
||||
batch, time, _ = audio_latents.shape
|
||||
return audio_latents.view(batch, time, channels, mel_bins).permute(0, 2, 1, 3)
|
||||
|
||||
@property
|
||||
def patch_size(self) -> Tuple[int, int, int]:
|
||||
return self._patch_size
|
||||
|
||||
|
||||
class LTX2AudioEncoder(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
base_channels: int = 128,
|
||||
output_channels: int = 1,
|
||||
num_res_blocks: int = 2,
|
||||
attn_resolutions: Optional[Tuple[int, ...]] = None,
|
||||
in_channels: int = 2,
|
||||
resolution: int = 256,
|
||||
latent_channels: int = 8,
|
||||
ch_mult: Tuple[int, ...] = (1, 2, 4),
|
||||
norm_type: str = "group",
|
||||
causality_axis: Optional[str] = "width",
|
||||
dropout: float = 0.0,
|
||||
mid_block_add_attention: bool = False,
|
||||
sample_rate: int = 16000,
|
||||
mel_hop_length: int = 160,
|
||||
is_causal: bool = True,
|
||||
mel_bins: Optional[int] = 64,
|
||||
double_z: bool = True,
|
||||
):
|
||||
super().__init__()
|
||||
|
||||
self.sample_rate = sample_rate
|
||||
self.mel_hop_length = mel_hop_length
|
||||
self.is_causal = is_causal
|
||||
self.mel_bins = mel_bins
|
||||
|
||||
self.base_channels = base_channels
|
||||
self.temb_ch = 0
|
||||
self.num_resolutions = len(ch_mult)
|
||||
self.num_res_blocks = num_res_blocks
|
||||
self.resolution = resolution
|
||||
self.in_channels = in_channels
|
||||
self.out_ch = output_channels
|
||||
self.give_pre_end = False
|
||||
self.tanh_out = False
|
||||
self.norm_type = norm_type
|
||||
self.latent_channels = latent_channels
|
||||
self.channel_multipliers = ch_mult
|
||||
self.attn_resolutions = attn_resolutions
|
||||
self.causality_axis = causality_axis
|
||||
|
||||
base_block_channels = base_channels
|
||||
base_resolution = resolution
|
||||
self.z_shape = (1, latent_channels, base_resolution, base_resolution)
|
||||
|
||||
if self.causality_axis is not None:
|
||||
self.conv_in = LTX2AudioCausalConv2d(
|
||||
in_channels,
|
||||
base_block_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv_in = nn.Conv2d(
|
||||
in_channels, base_block_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
|
||||
self.down = nn.ModuleList()
|
||||
block_in = base_block_channels
|
||||
curr_res = self.resolution
|
||||
|
||||
for level in range(self.num_resolutions):
|
||||
stage = nn.Module()
|
||||
stage.block = nn.ModuleList()
|
||||
stage.attn = nn.ModuleList()
|
||||
block_out = self.base_channels * self.channel_multipliers[level]
|
||||
|
||||
for _ in range(self.num_res_blocks):
|
||||
stage.block.append(
|
||||
LTX2AudioResnetBlock(
|
||||
in_channels=block_in,
|
||||
out_channels=block_out,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
)
|
||||
block_in = block_out
|
||||
if self.attn_resolutions:
|
||||
if curr_res in self.attn_resolutions:
|
||||
stage.attn.append(
|
||||
LTX2AudioAttnBlock(block_in, norm_type=self.norm_type)
|
||||
)
|
||||
|
||||
if level != self.num_resolutions - 1:
|
||||
stage.downsample = LTX2AudioDownsample(
|
||||
block_in, True, causality_axis=self.causality_axis
|
||||
)
|
||||
curr_res = curr_res // 2
|
||||
|
||||
self.down.append(stage)
|
||||
|
||||
self.mid = nn.Module()
|
||||
self.mid.block_1 = LTX2AudioResnetBlock(
|
||||
in_channels=block_in,
|
||||
out_channels=block_in,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
if mid_block_add_attention:
|
||||
self.mid.attn_1 = LTX2AudioAttnBlock(block_in, norm_type=self.norm_type)
|
||||
else:
|
||||
self.mid.attn_1 = nn.Identity()
|
||||
self.mid.block_2 = LTX2AudioResnetBlock(
|
||||
in_channels=block_in,
|
||||
out_channels=block_in,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
|
||||
final_block_channels = block_in
|
||||
z_channels = 2 * latent_channels if double_z else latent_channels
|
||||
if self.norm_type == "group":
|
||||
self.norm_out = nn.GroupNorm(
|
||||
num_groups=32, num_channels=final_block_channels, eps=1e-6, affine=True
|
||||
)
|
||||
elif self.norm_type == "pixel":
|
||||
self.norm_out = LTX2AudioPixelNorm(dim=1, eps=1e-6)
|
||||
else:
|
||||
raise ValueError(f"Invalid normalization type: {self.norm_type}")
|
||||
self.non_linearity = nn.SiLU()
|
||||
|
||||
if self.causality_axis is not None:
|
||||
self.conv_out = LTX2AudioCausalConv2d(
|
||||
final_block_channels,
|
||||
z_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv_out = nn.Conv2d(
|
||||
final_block_channels, z_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
|
||||
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
|
||||
# hidden_states expected shape: (batch_size, channels, time, num_mel_bins)
|
||||
hidden_states = self.conv_in(hidden_states)
|
||||
|
||||
for level in range(self.num_resolutions):
|
||||
stage = self.down[level]
|
||||
for block_idx, block in enumerate(stage.block):
|
||||
hidden_states = block(hidden_states, temb=None)
|
||||
if stage.attn:
|
||||
hidden_states = stage.attn[block_idx](hidden_states)
|
||||
|
||||
if level != self.num_resolutions - 1 and hasattr(stage, "downsample"):
|
||||
hidden_states = stage.downsample(hidden_states)
|
||||
|
||||
hidden_states = self.mid.block_1(hidden_states, temb=None)
|
||||
hidden_states = self.mid.attn_1(hidden_states)
|
||||
hidden_states = self.mid.block_2(hidden_states, temb=None)
|
||||
|
||||
hidden_states = self.norm_out(hidden_states)
|
||||
hidden_states = self.non_linearity(hidden_states)
|
||||
hidden_states = self.conv_out(hidden_states)
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
class LTX2AudioDecoder(nn.Module):
|
||||
"""
|
||||
Symmetric decoder that reconstructs audio spectrograms from latent features.
|
||||
|
||||
The decoder mirrors the encoder structure with configurable channel multipliers, attention resolutions, and causal
|
||||
convolutions.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_channels: int = 128,
|
||||
output_channels: int = 1,
|
||||
num_res_blocks: int = 2,
|
||||
attn_resolutions: Optional[Tuple[int, ...]] = None,
|
||||
in_channels: int = 2,
|
||||
resolution: int = 256,
|
||||
latent_channels: int = 8,
|
||||
ch_mult: Tuple[int, ...] = (1, 2, 4),
|
||||
norm_type: str = "group",
|
||||
causality_axis: Optional[str] = "width",
|
||||
dropout: float = 0.0,
|
||||
mid_block_add_attention: bool = False,
|
||||
sample_rate: int = 16000,
|
||||
mel_hop_length: int = 160,
|
||||
is_causal: bool = True,
|
||||
mel_bins: Optional[int] = 64,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.sample_rate = sample_rate
|
||||
self.mel_hop_length = mel_hop_length
|
||||
self.is_causal = is_causal
|
||||
self.mel_bins = mel_bins
|
||||
self.patchifier = LTX2AudioAudioPatchifier(
|
||||
patch_size=1,
|
||||
audio_latent_downsample_factor=LATENT_DOWNSAMPLE_FACTOR,
|
||||
sample_rate=sample_rate,
|
||||
hop_length=mel_hop_length,
|
||||
is_causal=is_causal,
|
||||
)
|
||||
|
||||
self.base_channels = base_channels
|
||||
self.temb_ch = 0
|
||||
self.num_resolutions = len(ch_mult)
|
||||
self.num_res_blocks = num_res_blocks
|
||||
self.resolution = resolution
|
||||
self.in_channels = in_channels
|
||||
self.out_ch = output_channels
|
||||
self.give_pre_end = False
|
||||
self.tanh_out = False
|
||||
self.norm_type = norm_type
|
||||
self.latent_channels = latent_channels
|
||||
self.channel_multipliers = ch_mult
|
||||
self.attn_resolutions = attn_resolutions
|
||||
self.causality_axis = causality_axis
|
||||
|
||||
base_block_channels = base_channels * self.channel_multipliers[-1]
|
||||
base_resolution = resolution // (2 ** (self.num_resolutions - 1))
|
||||
self.z_shape = (1, latent_channels, base_resolution, base_resolution)
|
||||
|
||||
if self.causality_axis is not None:
|
||||
self.conv_in = LTX2AudioCausalConv2d(
|
||||
latent_channels,
|
||||
base_block_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv_in = nn.Conv2d(
|
||||
latent_channels, base_block_channels, kernel_size=3, stride=1, padding=1
|
||||
)
|
||||
self.non_linearity = nn.SiLU()
|
||||
self.mid = nn.Module()
|
||||
self.mid.block_1 = LTX2AudioResnetBlock(
|
||||
in_channels=base_block_channels,
|
||||
out_channels=base_block_channels,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
if mid_block_add_attention:
|
||||
self.mid.attn_1 = LTX2AudioAttnBlock(
|
||||
base_block_channels, norm_type=self.norm_type
|
||||
)
|
||||
else:
|
||||
self.mid.attn_1 = nn.Identity()
|
||||
self.mid.block_2 = LTX2AudioResnetBlock(
|
||||
in_channels=base_block_channels,
|
||||
out_channels=base_block_channels,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
|
||||
self.up = nn.ModuleList()
|
||||
block_in = base_block_channels
|
||||
curr_res = self.resolution // (2 ** (self.num_resolutions - 1))
|
||||
|
||||
for level in reversed(range(self.num_resolutions)):
|
||||
stage = nn.Module()
|
||||
stage.block = nn.ModuleList()
|
||||
stage.attn = nn.ModuleList()
|
||||
block_out = self.base_channels * self.channel_multipliers[level]
|
||||
|
||||
for _ in range(self.num_res_blocks + 1):
|
||||
stage.block.append(
|
||||
LTX2AudioResnetBlock(
|
||||
in_channels=block_in,
|
||||
out_channels=block_out,
|
||||
temb_channels=self.temb_ch,
|
||||
dropout=dropout,
|
||||
norm_type=self.norm_type,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
)
|
||||
block_in = block_out
|
||||
if self.attn_resolutions:
|
||||
if curr_res in self.attn_resolutions:
|
||||
stage.attn.append(
|
||||
LTX2AudioAttnBlock(block_in, norm_type=self.norm_type)
|
||||
)
|
||||
|
||||
if level != 0:
|
||||
stage.upsample = LTX2AudioUpsample(
|
||||
block_in, True, causality_axis=self.causality_axis
|
||||
)
|
||||
curr_res *= 2
|
||||
|
||||
self.up.insert(0, stage)
|
||||
|
||||
final_block_channels = block_in
|
||||
|
||||
if self.norm_type == "group":
|
||||
self.norm_out = nn.GroupNorm(
|
||||
num_groups=32, num_channels=final_block_channels, eps=1e-6, affine=True
|
||||
)
|
||||
elif self.norm_type == "pixel":
|
||||
self.norm_out = LTX2AudioPixelNorm(dim=1, eps=1e-6)
|
||||
else:
|
||||
raise ValueError(f"Invalid normalization type: {self.norm_type}")
|
||||
|
||||
if self.causality_axis is not None:
|
||||
self.conv_out = LTX2AudioCausalConv2d(
|
||||
final_block_channels,
|
||||
output_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
causality_axis=self.causality_axis,
|
||||
)
|
||||
else:
|
||||
self.conv_out = nn.Conv2d(
|
||||
final_block_channels,
|
||||
output_channels,
|
||||
kernel_size=3,
|
||||
stride=1,
|
||||
padding=1,
|
||||
)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
sample: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
_, _, frames, mel_bins = sample.shape
|
||||
|
||||
target_frames = frames * LATENT_DOWNSAMPLE_FACTOR
|
||||
|
||||
if self.causality_axis is not None:
|
||||
target_frames = max(target_frames - (LATENT_DOWNSAMPLE_FACTOR - 1), 1)
|
||||
|
||||
target_channels = self.out_ch
|
||||
target_mel_bins = self.mel_bins if self.mel_bins is not None else mel_bins
|
||||
|
||||
hidden_features = self.conv_in(sample)
|
||||
hidden_features = self.mid.block_1(hidden_features, temb=None)
|
||||
hidden_features = self.mid.attn_1(hidden_features)
|
||||
hidden_features = self.mid.block_2(hidden_features, temb=None)
|
||||
|
||||
for level in reversed(range(self.num_resolutions)):
|
||||
stage = self.up[level]
|
||||
for block_idx, block in enumerate(stage.block):
|
||||
hidden_features = block(hidden_features, temb=None)
|
||||
if stage.attn:
|
||||
hidden_features = stage.attn[block_idx](hidden_features)
|
||||
|
||||
if level != 0 and hasattr(stage, "upsample"):
|
||||
hidden_features = stage.upsample(hidden_features)
|
||||
|
||||
if self.give_pre_end:
|
||||
return hidden_features
|
||||
|
||||
hidden = self.norm_out(hidden_features)
|
||||
hidden = self.non_linearity(hidden)
|
||||
decoded_output = self.conv_out(hidden)
|
||||
decoded_output = torch.tanh(decoded_output) if self.tanh_out else decoded_output
|
||||
|
||||
_, _, current_time, current_freq = decoded_output.shape
|
||||
target_time = target_frames
|
||||
target_freq = target_mel_bins
|
||||
|
||||
decoded_output = decoded_output[
|
||||
:,
|
||||
:target_channels,
|
||||
: min(current_time, target_time),
|
||||
: min(current_freq, target_freq),
|
||||
]
|
||||
|
||||
time_padding_needed = target_time - decoded_output.shape[2]
|
||||
freq_padding_needed = target_freq - decoded_output.shape[3]
|
||||
|
||||
if time_padding_needed > 0 or freq_padding_needed > 0:
|
||||
padding = (
|
||||
0,
|
||||
max(freq_padding_needed, 0),
|
||||
0,
|
||||
max(time_padding_needed, 0),
|
||||
)
|
||||
decoded_output = F.pad(decoded_output, padding)
|
||||
|
||||
decoded_output = decoded_output[:, :target_channels, :target_time, :target_freq]
|
||||
|
||||
return decoded_output
|
||||
|
||||
|
||||
class AutoencoderKLLTX2Audio(ParallelTiledVAE):
|
||||
r"""
|
||||
LTX2 audio VAE for encoding and decoding audio latent representations.
|
||||
"""
|
||||
|
||||
_supports_gradient_checkpointing = False
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: LTXAudioVAEConfig,
|
||||
) -> None:
|
||||
super().__init__(config=config)
|
||||
|
||||
causality_axis = config.arch_config.causality_axis
|
||||
attn_resolutions = config.arch_config.attn_resolutions
|
||||
base_channels = config.arch_config.base_channels
|
||||
output_channels = config.arch_config.output_channels
|
||||
ch_mult = config.arch_config.ch_mult
|
||||
num_res_blocks = config.arch_config.num_res_blocks
|
||||
in_channels = config.arch_config.in_channels
|
||||
resolution = config.arch_config.resolution
|
||||
latent_channels = config.arch_config.latent_channels
|
||||
norm_type = config.arch_config.norm_type
|
||||
dropout = config.arch_config.dropout
|
||||
mid_block_add_attention = config.arch_config.mid_block_add_attention
|
||||
sample_rate = config.arch_config.sample_rate
|
||||
mel_hop_length = config.arch_config.mel_hop_length
|
||||
is_causal = config.arch_config.is_causal
|
||||
mel_bins = config.arch_config.mel_bins
|
||||
double_z = config.arch_config.double_z
|
||||
|
||||
supported_causality_axes = {"none", "width", "height", "width-compatibility"}
|
||||
if causality_axis not in supported_causality_axes:
|
||||
raise ValueError(
|
||||
f"{causality_axis=} is not valid. Supported values: {supported_causality_axes}"
|
||||
)
|
||||
|
||||
attn_resolution_set = (
|
||||
set(attn_resolutions) if attn_resolutions else attn_resolutions
|
||||
)
|
||||
|
||||
self.encoder = LTX2AudioEncoder(
|
||||
base_channels=base_channels,
|
||||
output_channels=output_channels,
|
||||
ch_mult=ch_mult,
|
||||
num_res_blocks=num_res_blocks,
|
||||
attn_resolutions=attn_resolution_set,
|
||||
in_channels=in_channels,
|
||||
resolution=resolution,
|
||||
latent_channels=latent_channels,
|
||||
norm_type=norm_type,
|
||||
causality_axis=causality_axis,
|
||||
dropout=dropout,
|
||||
mid_block_add_attention=mid_block_add_attention,
|
||||
sample_rate=sample_rate,
|
||||
mel_hop_length=mel_hop_length,
|
||||
is_causal=is_causal,
|
||||
mel_bins=mel_bins,
|
||||
double_z=double_z,
|
||||
)
|
||||
|
||||
self.decoder = LTX2AudioDecoder(
|
||||
base_channels=base_channels,
|
||||
output_channels=output_channels,
|
||||
ch_mult=ch_mult,
|
||||
num_res_blocks=num_res_blocks,
|
||||
attn_resolutions=attn_resolution_set,
|
||||
in_channels=in_channels,
|
||||
resolution=resolution,
|
||||
latent_channels=latent_channels,
|
||||
norm_type=norm_type,
|
||||
causality_axis=causality_axis,
|
||||
dropout=dropout,
|
||||
mid_block_add_attention=mid_block_add_attention,
|
||||
sample_rate=sample_rate,
|
||||
mel_hop_length=mel_hop_length,
|
||||
is_causal=is_causal,
|
||||
mel_bins=mel_bins,
|
||||
)
|
||||
|
||||
# Per-channel statistics for normalizing and denormalizing the latent representation. This statistics is computed over
|
||||
# the entire dataset and stored in model's checkpoint under AudioVAE state_dict
|
||||
latents_std = torch.zeros((base_channels,))
|
||||
latents_mean = torch.ones((base_channels,))
|
||||
self.register_buffer("latents_mean", latents_mean, persistent=True)
|
||||
self.register_buffer("latents_std", latents_std, persistent=True)
|
||||
|
||||
# TODO: confirm whether the mel compression ratio below is correct
|
||||
self.mel_compression_ratio = LATENT_DOWNSAMPLE_FACTOR
|
||||
self.use_slicing = False
|
||||
|
||||
def _encode(self, x: torch.Tensor) -> torch.Tensor:
|
||||
return self.encoder(x)
|
||||
|
||||
def encode(self, x: torch.Tensor, return_dict: bool = True):
|
||||
if self.use_slicing and x.shape[0] > 1:
|
||||
encoded_slices = [self._encode(x_slice) for x_slice in x.split(1)]
|
||||
h = torch.cat(encoded_slices)
|
||||
else:
|
||||
h = self._encode(x)
|
||||
posterior = DiagonalGaussianDistribution(h)
|
||||
|
||||
if not return_dict:
|
||||
return (posterior,)
|
||||
return AutoencoderKLOutput(latent_dist=posterior)
|
||||
|
||||
def _decode(self, z: torch.Tensor) -> torch.Tensor:
|
||||
return self.decoder(z)
|
||||
|
||||
def decode(
|
||||
self, z: torch.Tensor, return_dict: bool = True
|
||||
) -> Union[DecoderOutput, torch.Tensor]:
|
||||
if self.use_slicing and z.shape[0] > 1:
|
||||
decoded_slices = [self._decode(z_slice) for z_slice in z.split(1)]
|
||||
decoded = torch.cat(decoded_slices)
|
||||
else:
|
||||
decoded = self._decode(z)
|
||||
|
||||
if not return_dict:
|
||||
return (decoded,)
|
||||
|
||||
return DecoderOutput(sample=decoded)
|
||||
|
||||
def forward(
|
||||
self,
|
||||
sample: torch.Tensor,
|
||||
sample_posterior: bool = False,
|
||||
return_dict: bool = True,
|
||||
generator: Optional[torch.Generator] = None,
|
||||
) -> Union[DecoderOutput, torch.Tensor]:
|
||||
posterior = self.encode(sample).latent_dist
|
||||
if sample_posterior:
|
||||
z = posterior.sample(generator=generator)
|
||||
else:
|
||||
z = posterior.mode()
|
||||
dec = self.decode(z)
|
||||
if not return_dict:
|
||||
return (dec.sample,)
|
||||
return dec
|
||||
|
||||
|
||||
EntryClass = AutoencoderKLLTX2Audio
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,193 @@
|
||||
import math
|
||||
from abc import ABC
|
||||
from typing import Tuple
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import LTXVocoderConfig
|
||||
|
||||
|
||||
class ResBlock(nn.Module):
|
||||
def __init__(
|
||||
self,
|
||||
channels: int,
|
||||
kernel_size: int = 3,
|
||||
stride: int = 1,
|
||||
dilations: Tuple[int, ...] = (1, 3, 5),
|
||||
leaky_relu_negative_slope: float = 0.1,
|
||||
padding_mode: str = "same",
|
||||
):
|
||||
super().__init__()
|
||||
self.dilations = dilations
|
||||
self.negative_slope = leaky_relu_negative_slope
|
||||
|
||||
self.convs1 = nn.ModuleList(
|
||||
[
|
||||
nn.Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
dilation=dilation,
|
||||
padding=padding_mode,
|
||||
)
|
||||
for dilation in dilations
|
||||
]
|
||||
)
|
||||
|
||||
self.convs2 = nn.ModuleList(
|
||||
[
|
||||
nn.Conv1d(
|
||||
channels,
|
||||
channels,
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
dilation=1,
|
||||
padding=padding_mode,
|
||||
)
|
||||
for _ in range(len(dilations))
|
||||
]
|
||||
)
|
||||
|
||||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||||
for conv1, conv2 in zip(self.convs1, self.convs2):
|
||||
xt = F.leaky_relu(x, negative_slope=self.negative_slope)
|
||||
xt = conv1(xt)
|
||||
xt = F.leaky_relu(xt, negative_slope=self.negative_slope)
|
||||
xt = conv2(xt)
|
||||
x = x + xt
|
||||
return x
|
||||
|
||||
|
||||
class LTX2Vocoder(ABC, nn.Module):
|
||||
r"""
|
||||
LTX 2.0 vocoder for converting generated mel spectrograms back to audio waveforms.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: LTXVocoderConfig,
|
||||
):
|
||||
super().__init__()
|
||||
self.config = config
|
||||
self.sample_rate = (
|
||||
getattr(config.arch_config, "sample_rate", None)
|
||||
or getattr(config.arch_config, "sampling_rate", None)
|
||||
or getattr(config.arch_config, "audio_sample_rate", None)
|
||||
)
|
||||
|
||||
in_channels = config.arch_config.in_channels
|
||||
hidden_channels = config.arch_config.hidden_channels
|
||||
out_channels = config.arch_config.out_channels
|
||||
upsample_kernel_sizes = config.arch_config.upsample_kernel_sizes
|
||||
upsample_factors = config.arch_config.upsample_factors
|
||||
resnet_kernel_sizes = config.arch_config.resnet_kernel_sizes
|
||||
resnet_dilations = config.arch_config.resnet_dilations
|
||||
leaky_relu_negative_slope = config.arch_config.leaky_relu_negative_slope
|
||||
|
||||
self.num_upsample_layers = len(upsample_kernel_sizes)
|
||||
self.resnets_per_upsample = len(resnet_kernel_sizes)
|
||||
self.out_channels = out_channels
|
||||
self.total_upsample_factor = math.prod(upsample_factors)
|
||||
self.negative_slope = leaky_relu_negative_slope
|
||||
|
||||
if self.num_upsample_layers != len(upsample_factors):
|
||||
raise ValueError(
|
||||
f"`upsample_kernel_sizes` and `upsample_factors` should be lists of the same length but are length"
|
||||
f" {self.num_upsample_layers} and {len(upsample_factors)}, respectively."
|
||||
)
|
||||
|
||||
if self.resnets_per_upsample != len(resnet_dilations):
|
||||
raise ValueError(
|
||||
f"`resnet_kernel_sizes` and `resnet_dilations` should be lists of the same length but are length"
|
||||
f" {len(self.resnets_per_upsample)} and {len(resnet_dilations)}, respectively."
|
||||
)
|
||||
|
||||
self.conv_in = nn.Conv1d(
|
||||
in_channels, hidden_channels, kernel_size=7, stride=1, padding=3
|
||||
)
|
||||
|
||||
self.upsamplers = nn.ModuleList()
|
||||
self.resnets = nn.ModuleList()
|
||||
input_channels = hidden_channels
|
||||
for i, (stride, kernel_size) in enumerate(
|
||||
zip(upsample_factors, upsample_kernel_sizes)
|
||||
):
|
||||
output_channels = input_channels // 2
|
||||
self.upsamplers.append(
|
||||
nn.ConvTranspose1d(
|
||||
input_channels, # hidden_channels // (2 ** i)
|
||||
output_channels, # hidden_channels // (2 ** (i + 1))
|
||||
kernel_size,
|
||||
stride=stride,
|
||||
padding=(kernel_size - stride) // 2,
|
||||
)
|
||||
)
|
||||
|
||||
for kernel_size, dilations in zip(resnet_kernel_sizes, resnet_dilations):
|
||||
self.resnets.append(
|
||||
ResBlock(
|
||||
output_channels,
|
||||
kernel_size,
|
||||
dilations=dilations,
|
||||
leaky_relu_negative_slope=leaky_relu_negative_slope,
|
||||
)
|
||||
)
|
||||
input_channels = output_channels
|
||||
|
||||
self.conv_out = nn.Conv1d(output_channels, out_channels, 7, stride=1, padding=3)
|
||||
|
||||
def forward(
|
||||
self, hidden_states: torch.Tensor, time_last: bool = False
|
||||
) -> torch.Tensor:
|
||||
r"""
|
||||
Forward pass of the vocoder.
|
||||
|
||||
Args:
|
||||
hidden_states (`torch.Tensor`):
|
||||
Input Mel spectrogram tensor of shape `(batch_size, num_channels, time, num_mel_bins)` if `time_last`
|
||||
is `False` (the default) or shape `(batch_size, num_channels, num_mel_bins, time)` if `time_last` is
|
||||
`True`.
|
||||
time_last (`bool`, *optional*, defaults to `False`):
|
||||
Whether the last dimension of the input is the time/frame dimension or the Mel bins dimension.
|
||||
|
||||
Returns:
|
||||
`torch.Tensor`:
|
||||
Audio waveform tensor of shape (batch_size, out_channels, audio_length)
|
||||
"""
|
||||
|
||||
# Ensure that the time/frame dimension is last
|
||||
if not time_last:
|
||||
hidden_states = hidden_states.transpose(2, 3)
|
||||
# Combine channels and frequency (mel bins) dimensions
|
||||
hidden_states = hidden_states.flatten(1, 2)
|
||||
|
||||
hidden_states = self.conv_in(hidden_states)
|
||||
|
||||
for i in range(self.num_upsample_layers):
|
||||
hidden_states = F.leaky_relu(
|
||||
hidden_states, negative_slope=self.negative_slope
|
||||
)
|
||||
hidden_states = self.upsamplers[i](hidden_states)
|
||||
|
||||
# Run all resnets in parallel on hidden_states
|
||||
start = i * self.resnets_per_upsample
|
||||
end = (i + 1) * self.resnets_per_upsample
|
||||
resnet_outputs = torch.stack(
|
||||
[self.resnets[j](hidden_states) for j in range(start, end)], dim=0
|
||||
)
|
||||
|
||||
hidden_states = torch.mean(resnet_outputs, dim=0)
|
||||
|
||||
# NOTE: unlike the first leaky ReLU, this leaky ReLU is set to use the default F.leaky_relu negative slope of
|
||||
# 0.01 (whereas the others usually use a slope of 0.1). Not sure if this is intended
|
||||
hidden_states = F.leaky_relu(hidden_states, negative_slope=0.01)
|
||||
hidden_states = self.conv_out(hidden_states)
|
||||
hidden_states = torch.tanh(hidden_states)
|
||||
|
||||
return hidden_states
|
||||
|
||||
|
||||
EntryClass = LTX2Vocoder
|
||||
@@ -0,0 +1,191 @@
|
||||
import inspect
|
||||
import json
|
||||
import os
|
||||
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
|
||||
ComposedPipelineBase,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages import (
|
||||
InputValidationStage,
|
||||
LTX2AVDecodingStage,
|
||||
LTX2AVDenoisingStage,
|
||||
LTX2AVLatentPreparationStage,
|
||||
LTX2TextConnectorStage,
|
||||
TextEncodingStage,
|
||||
TimestepPreparationStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
def calculate_shift(
|
||||
image_seq_len,
|
||||
base_seq_len: int = 256,
|
||||
max_seq_len: int = 4096,
|
||||
base_shift: float = 0.5,
|
||||
max_shift: float = 1.15,
|
||||
):
|
||||
m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
|
||||
b = base_shift - m * base_seq_len
|
||||
mu = image_seq_len * m + b
|
||||
return mu
|
||||
|
||||
|
||||
def prepare_mu(batch: Req, server_args: ServerArgs):
|
||||
height = batch.height
|
||||
width = batch.width
|
||||
num_frames = batch.num_frames
|
||||
|
||||
vae_arch = getattr(
|
||||
getattr(server_args.pipeline_config, "vae_config", None), "arch_config", None
|
||||
)
|
||||
vae_scale_factor = (
|
||||
getattr(vae_arch, "spatial_compression_ratio", None)
|
||||
or getattr(vae_arch, "vae_scale_factor", None)
|
||||
or getattr(server_args.pipeline_config, "vae_scale_factor", None)
|
||||
)
|
||||
vae_temporal_compression = getattr(
|
||||
vae_arch, "temporal_compression_ratio", None
|
||||
) or getattr(server_args.pipeline_config, "vae_temporal_compression", None)
|
||||
|
||||
latent_num_frames = (int(num_frames) - 1) // int(vae_temporal_compression) + 1
|
||||
latent_height = int(height) // int(vae_scale_factor)
|
||||
latent_width = int(width) // int(vae_scale_factor)
|
||||
video_sequence_length = latent_num_frames * latent_height * latent_width
|
||||
|
||||
# Values from LTX2Pipeline in diffusers
|
||||
mu = calculate_shift(
|
||||
video_sequence_length,
|
||||
base_seq_len=1024,
|
||||
max_seq_len=4096,
|
||||
base_shift=0.95,
|
||||
max_shift=2.05,
|
||||
)
|
||||
return "mu", mu
|
||||
|
||||
|
||||
def _load_component_config(model_path: str, component_name: str):
|
||||
"""Helper to load component config from model_index.json or config.json"""
|
||||
try:
|
||||
# Try loading model_index.json first
|
||||
index_path = os.path.join(model_path, "model_index.json")
|
||||
if os.path.exists(index_path):
|
||||
with open(index_path, "r") as f:
|
||||
index = json.load(f)
|
||||
|
||||
if component_name in index:
|
||||
# It's a subfolder
|
||||
subfolder = index[component_name][1]
|
||||
config_path = os.path.join(model_path, subfolder, "config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
# Fallback to direct config.json in subfolder if standard structure
|
||||
config_path = os.path.join(model_path, component_name, "config.json")
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, "r") as f:
|
||||
return json.load(f)
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load config for {component_name}: {e}")
|
||||
|
||||
return {}
|
||||
|
||||
|
||||
def _filter_kwargs_for_cls(cls, kwargs):
|
||||
"""Filter kwargs to only include those accepted by cls.__init__"""
|
||||
sig = inspect.signature(cls.__init__)
|
||||
return {k: v for k, v in kwargs.items() if k in sig.parameters}
|
||||
|
||||
|
||||
class LTX2Pipeline(ComposedPipelineBase):
|
||||
# NOTE: must match `model_index.json`'s `_class_name` for native dispatch.
|
||||
pipeline_name = "LTX2Pipeline"
|
||||
|
||||
_required_config_modules = [
|
||||
"transformer",
|
||||
"text_encoder",
|
||||
"tokenizer",
|
||||
"scheduler",
|
||||
"vae",
|
||||
"audio_vae",
|
||||
"vocoder",
|
||||
"connectors",
|
||||
]
|
||||
|
||||
def create_pipeline_stages(self, server_args: ServerArgs):
|
||||
"""Set up pipeline stages with proper dependency injection."""
|
||||
|
||||
# 1. Input Validation
|
||||
self.add_stage(
|
||||
stage_name="input_validation_stage", stage=InputValidationStage()
|
||||
)
|
||||
|
||||
# 2. Text Encoding
|
||||
self.add_stage(
|
||||
stage_name="text_encoding_stage",
|
||||
stage=TextEncodingStage(
|
||||
# LTX-2 needs two contexts (video/audio). We reuse the same
|
||||
# underlying Gemma encoder/tokenizer twice.
|
||||
text_encoders=[
|
||||
self.get_module("text_encoder"),
|
||||
],
|
||||
tokenizers=[
|
||||
self.get_module("tokenizer"),
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
# 3. connector stage
|
||||
self.add_stage(
|
||||
stage_name="text_connector_stage",
|
||||
stage=LTX2TextConnectorStage(connectors=self.get_module("connectors")),
|
||||
)
|
||||
|
||||
# 4. Timestep Preparation
|
||||
self.add_stage(
|
||||
stage_name="timestep_preparation_stage",
|
||||
stage=TimestepPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
prepare_extra_set_timesteps_kwargs=[prepare_mu],
|
||||
),
|
||||
)
|
||||
|
||||
# 4. Latent Preparation
|
||||
self.add_stage(
|
||||
stage_name="latent_preparation_stage",
|
||||
stage=LTX2AVLatentPreparationStage(
|
||||
scheduler=self.get_module("scheduler"),
|
||||
transformer=self.get_module("transformer"),
|
||||
audio_vae=self.get_module("audio_vae"),
|
||||
),
|
||||
)
|
||||
|
||||
# 5. Denoising
|
||||
self.add_stage(
|
||||
stage_name="denoising_stage",
|
||||
stage=LTX2AVDenoisingStage(
|
||||
transformer=self.get_module("transformer"),
|
||||
scheduler=self.get_module("scheduler"),
|
||||
vae=self.get_module("vae"),
|
||||
audio_vae=self.get_module("audio_vae"),
|
||||
),
|
||||
)
|
||||
|
||||
# 6. Decoding
|
||||
self.add_stage(
|
||||
stage_name="decoding_stage",
|
||||
stage=LTX2AVDecodingStage(
|
||||
vae=self.get_module("vae"),
|
||||
audio_vae=self.get_module("audio_vae"),
|
||||
vocoder=self.get_module("vocoder"),
|
||||
pipeline=self,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
EntryClass = LTX2Pipeline
|
||||
@@ -19,7 +19,13 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.conditioning import (
|
||||
ConditioningStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding import DecodingStage
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding_av import (
|
||||
LTX2AVDecodingStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising import DenoisingStage
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising_av import (
|
||||
LTX2AVDenoisingStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising_dmd import (
|
||||
DmdDenoisingStage,
|
||||
)
|
||||
@@ -34,6 +40,12 @@ from sglang.multimodal_gen.runtime.pipelines_core.stages.input_validation import
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.latent_preparation import (
|
||||
LatentPreparationStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.latent_preparation_av import (
|
||||
LTX2AVLatentPreparationStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.text_connector import (
|
||||
LTX2TextConnectorStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.text_encoding import (
|
||||
TextEncodingStage,
|
||||
)
|
||||
@@ -47,13 +59,17 @@ __all__ = [
|
||||
"TimestepPreparationStage",
|
||||
"LatentPreparationStage",
|
||||
"ComfyUILatentPreparationStage",
|
||||
"LTX2AVLatentPreparationStage",
|
||||
"ConditioningStage",
|
||||
"DenoisingStage",
|
||||
"DmdDenoisingStage",
|
||||
"LTX2AVDenoisingStage",
|
||||
"CausalDMDDenoisingStage",
|
||||
"EncodingStage",
|
||||
"DecodingStage",
|
||||
"LTX2AVDecodingStage",
|
||||
"ImageEncodingStage",
|
||||
"ImageVAEEncodingStage",
|
||||
"TextEncodingStage",
|
||||
"LTX2TextConnectorStage",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch, Req
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding import DecodingStage
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class LTX2AVDecodingStage(DecodingStage):
|
||||
"""
|
||||
LTX-2 specific decoding stage that handles both video and audio decoding.
|
||||
"""
|
||||
|
||||
def __init__(self, vae, audio_vae, vocoder, pipeline=None):
|
||||
super().__init__(vae, pipeline)
|
||||
self.audio_vae = audio_vae
|
||||
self.vocoder = vocoder
|
||||
# Add video processor for postprocessing
|
||||
from diffusers.video_processor import VideoProcessor
|
||||
|
||||
self.video_processor = VideoProcessor(vae_scale_factor=32)
|
||||
|
||||
def forward(self, batch: Req, server_args: ServerArgs) -> OutputBatch:
|
||||
self.load_model()
|
||||
|
||||
self.vae = self.vae.to(get_local_torch_device())
|
||||
self.vae.eval()
|
||||
latents = batch.latents.to(get_local_torch_device())
|
||||
|
||||
vae_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||
vae_autocast_enabled = (
|
||||
vae_dtype != torch.float32
|
||||
) and not server_args.disable_autocast
|
||||
|
||||
latents = self.scale_and_shift(latents, server_args)
|
||||
latents = server_args.pipeline_config.preprocess_decoding(
|
||||
latents, server_args, vae=self.vae
|
||||
)
|
||||
|
||||
with torch.autocast(
|
||||
device_type=current_platform.device_type,
|
||||
dtype=vae_dtype,
|
||||
enabled=vae_autocast_enabled,
|
||||
):
|
||||
try:
|
||||
if server_args.pipeline_config.vae_tiling:
|
||||
self.vae.enable_tiling()
|
||||
except Exception:
|
||||
pass
|
||||
if not vae_autocast_enabled:
|
||||
latents = latents.to(vae_dtype)
|
||||
decode_output = self.vae.decode(latents)
|
||||
if isinstance(decode_output, tuple):
|
||||
video = decode_output[0]
|
||||
elif hasattr(decode_output, "sample"):
|
||||
video = decode_output.sample
|
||||
else:
|
||||
video = decode_output
|
||||
|
||||
video = self.video_processor.postprocess_video(video, output_type="np")
|
||||
|
||||
output_batch = OutputBatch(
|
||||
output=video,
|
||||
trajectory_timesteps=batch.trajectory_timesteps,
|
||||
trajectory_latents=batch.trajectory_latents,
|
||||
trajectory_decoded=None,
|
||||
timings=batch.timings,
|
||||
)
|
||||
|
||||
# 2. Decode Audio
|
||||
try:
|
||||
audio_latents = batch.audio_latents
|
||||
except AttributeError:
|
||||
audio_latents = None
|
||||
if audio_latents is not None:
|
||||
# Ensure device/dtype
|
||||
device = get_local_torch_device()
|
||||
self.audio_vae = self.audio_vae.to(device)
|
||||
self.vocoder = self.vocoder.to(device)
|
||||
self.audio_vae.eval()
|
||||
self.vocoder.eval()
|
||||
try:
|
||||
dtype = self.audio_vae.dtype
|
||||
except AttributeError:
|
||||
dtype = None
|
||||
if dtype is None:
|
||||
try:
|
||||
dtype = next(self.audio_vae.parameters()).dtype
|
||||
except StopIteration:
|
||||
dtype = torch.float32
|
||||
audio_latents = audio_latents.to(device, dtype=dtype)
|
||||
try:
|
||||
latents_std = self.audio_vae.latents_std
|
||||
except AttributeError:
|
||||
latents_std = None
|
||||
if isinstance(latents_std, torch.Tensor) and torch.all(latents_std == 0):
|
||||
logger.warning(
|
||||
"audio_vae.latents_std is all zeros; audio denorm may be incorrect."
|
||||
)
|
||||
|
||||
with torch.no_grad():
|
||||
# Decode latents to spectrogram
|
||||
spectrogram = self.audio_vae.decode(audio_latents, return_dict=False)[0]
|
||||
if hasattr(self.vocoder, "conv_in") and hasattr(
|
||||
self.vocoder.conv_in, "in_channels"
|
||||
):
|
||||
expected_in = int(self.vocoder.conv_in.in_channels)
|
||||
actual_in = int(spectrogram.shape[1]) * int(spectrogram.shape[3])
|
||||
if actual_in != expected_in:
|
||||
raise ValueError(
|
||||
f"Vocoder expects channels*mel_bins={expected_in}, got {actual_in} from spectrogram shape {tuple(spectrogram.shape)}"
|
||||
)
|
||||
# Decode spectrogram to waveform
|
||||
waveform = self.vocoder(spectrogram)
|
||||
output_batch.audio = waveform.cpu().float()
|
||||
try:
|
||||
pipeline_audio_cfg = server_args.pipeline_config.audio_vae_config
|
||||
except AttributeError:
|
||||
pipeline_audio_cfg = None
|
||||
try:
|
||||
pipeline_audio_arch = pipeline_audio_cfg.arch_config # type: ignore[union-attr]
|
||||
except AttributeError:
|
||||
pipeline_audio_arch = None
|
||||
try:
|
||||
pipeline_audio_sr = pipeline_audio_arch.sample_rate # type: ignore[union-attr]
|
||||
except AttributeError:
|
||||
pipeline_audio_sr = None
|
||||
|
||||
try:
|
||||
vocoder_sr = self.vocoder.sample_rate
|
||||
except AttributeError:
|
||||
vocoder_sr = None
|
||||
try:
|
||||
audio_vae_sr = self.audio_vae.sample_rate
|
||||
except AttributeError:
|
||||
audio_vae_sr = None
|
||||
output_batch.audio_sample_rate = (
|
||||
vocoder_sr or audio_vae_sr or pipeline_audio_sr
|
||||
)
|
||||
|
||||
self.offload_model()
|
||||
return output_batch
|
||||
@@ -0,0 +1,729 @@
|
||||
import copy
|
||||
import time
|
||||
|
||||
import PIL.Image
|
||||
import torch
|
||||
from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution
|
||||
from diffusers.models.modeling_outputs import AutoencoderKLOutput
|
||||
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
|
||||
from sglang.multimodal_gen.runtime.models.vision_utils import (
|
||||
load_image,
|
||||
normalize,
|
||||
numpy_to_pt,
|
||||
pil_to_numpy,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.denoising import DenoisingStage
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||
StageValidators as V,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||
VerificationResult,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
from sglang.multimodal_gen.runtime.utils.perf_logger import StageProfiler
|
||||
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class LTX2AVDenoisingStage(DenoisingStage):
|
||||
"""
|
||||
LTX-2 specific denoising stage that handles joint video and audio generation.
|
||||
"""
|
||||
|
||||
def __init__(self, transformer, scheduler, vae=None, audio_vae=None, **kwargs):
|
||||
super().__init__(
|
||||
transformer=transformer, scheduler=scheduler, vae=vae, **kwargs
|
||||
)
|
||||
self.audio_vae = audio_vae
|
||||
|
||||
@staticmethod
|
||||
def _get_video_latent_num_frames_for_model(
|
||||
batch: Req, server_args: ServerArgs, latents: torch.Tensor
|
||||
) -> int:
|
||||
"""Return the latent-frame length the DiT model should see.
|
||||
|
||||
- If video latents were time-sharded for SP and are packed as token latents
|
||||
([B, S, D]), the model only sees the local shard and must use the local
|
||||
latent-frame count (stored on the batch during SP sharding).
|
||||
- Otherwise, fall back to the global latent-frame count inferred from the
|
||||
requested output frames and the VAE temporal compression ratio.
|
||||
"""
|
||||
did_sp_shard = bool(getattr(batch, "did_sp_shard_latents", False))
|
||||
is_token_latents = isinstance(latents, torch.Tensor) and latents.ndim == 3
|
||||
|
||||
if did_sp_shard and is_token_latents:
|
||||
if not hasattr(batch, "sp_video_latent_num_frames"):
|
||||
raise ValueError(
|
||||
"SP-sharded LTX2 token latents require `batch.sp_video_latent_num_frames` "
|
||||
"to be set by `LTX2PipelineConfig.shard_latents_for_sp()`."
|
||||
)
|
||||
return int(batch.sp_video_latent_num_frames)
|
||||
|
||||
pc = server_args.pipeline_config
|
||||
return int((batch.num_frames - 1) // int(pc.vae_temporal_compression) + 1)
|
||||
|
||||
@staticmethod
|
||||
def _truncate_sp_padded_token_latents(
|
||||
batch: Req, latents: torch.Tensor
|
||||
) -> torch.Tensor:
|
||||
"""Remove token padding introduced by SP time-sharding (if applicable)."""
|
||||
did_sp_shard = bool(getattr(batch, "did_sp_shard_latents", False))
|
||||
if not did_sp_shard or not (
|
||||
isinstance(latents, torch.Tensor) and latents.ndim == 3
|
||||
):
|
||||
return latents
|
||||
|
||||
raw_shape = getattr(batch, "raw_latent_shape", None)
|
||||
if not (isinstance(raw_shape, tuple) and len(raw_shape) == 3):
|
||||
return latents
|
||||
|
||||
orig_s = int(raw_shape[1])
|
||||
cur_s = int(latents.shape[1])
|
||||
if cur_s == orig_s:
|
||||
return latents
|
||||
if cur_s < orig_s:
|
||||
raise ValueError(
|
||||
f"Unexpected gathered token-latents seq_len {cur_s} < original seq_len {orig_s}."
|
||||
)
|
||||
return latents[:, :orig_s, :].contiguous()
|
||||
|
||||
def _maybe_enable_cache_dit(self, num_inference_steps: int, batch: Req) -> None:
|
||||
"""Disable cache-dit for TI2V-style requests (image-conditioned), to avoid stale activations.
|
||||
|
||||
NOTE: base denoising stage calls this hook with (num_inference_steps, batch).
|
||||
"""
|
||||
if getattr(self, "_disable_cache_dit_for_request", False):
|
||||
return
|
||||
return super()._maybe_enable_cache_dit(num_inference_steps, batch)
|
||||
|
||||
@staticmethod
|
||||
def _resize_center_crop(
|
||||
img: PIL.Image.Image, *, width: int, height: int
|
||||
) -> PIL.Image.Image:
|
||||
return img.resize((width, height), resample=PIL.Image.Resampling.BILINEAR)
|
||||
|
||||
@staticmethod
|
||||
def _pil_to_normed_tensor(img: PIL.Image.Image) -> torch.Tensor:
|
||||
# PIL -> numpy [0,1] -> torch [B,C,H,W], then [-1,1]
|
||||
arr = pil_to_numpy(img)
|
||||
t = numpy_to_pt(arr)
|
||||
return normalize(t)
|
||||
|
||||
@staticmethod
|
||||
def _should_apply_ltx2_ti2v(batch: Req) -> bool:
|
||||
"""True if we have an image-latent token prefix to condition with.
|
||||
|
||||
SP note: when token latents are time-sharded, only the rank that owns the
|
||||
*global* first latent frame should apply TI2V conditioning (rank with start_frame==0).
|
||||
"""
|
||||
if (
|
||||
batch.image_latent is None
|
||||
or int(getattr(batch, "ltx2_num_image_tokens", 0)) <= 0
|
||||
):
|
||||
return False
|
||||
did_sp_shard = bool(getattr(batch, "did_sp_shard_latents", False))
|
||||
if not did_sp_shard:
|
||||
return True
|
||||
return int(getattr(batch, "sp_video_start_frame", 0)) == 0
|
||||
|
||||
def _prepare_ltx2_image_latent(self, batch: Req, server_args: ServerArgs) -> None:
|
||||
"""Encode `batch.image_path` into packed token latents for LTX-2 TI2V."""
|
||||
if (
|
||||
batch.image_latent is not None
|
||||
and int(getattr(batch, "ltx2_num_image_tokens", 0)) > 0
|
||||
):
|
||||
return
|
||||
batch.ltx2_num_image_tokens = 0
|
||||
batch.image_latent = None
|
||||
|
||||
if batch.image_path is None:
|
||||
return
|
||||
if batch.width is None or batch.height is None:
|
||||
raise ValueError("width/height must be provided for LTX-2 TI2V.")
|
||||
if self.vae is None:
|
||||
raise ValueError("VAE must be provided for LTX-2 TI2V.")
|
||||
|
||||
image_path = (
|
||||
batch.image_path[0]
|
||||
if isinstance(batch.image_path, list)
|
||||
else batch.image_path
|
||||
)
|
||||
|
||||
img = load_image(image_path)
|
||||
img = self._resize_center_crop(
|
||||
img, width=int(batch.width), height=int(batch.height)
|
||||
)
|
||||
batch.condition_image = img
|
||||
|
||||
latents_device = (
|
||||
batch.latents.device
|
||||
if isinstance(batch.latents, torch.Tensor)
|
||||
else torch.device("cpu")
|
||||
)
|
||||
image_tensor = self._pil_to_normed_tensor(img).to(
|
||||
latents_device, dtype=torch.float32
|
||||
)
|
||||
# [B, C, H, W] -> [B, C, 1, H, W]
|
||||
video_condition = image_tensor.unsqueeze(2)
|
||||
|
||||
self.vae = self.vae.to(latents_device)
|
||||
vae_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||
vae_autocast_enabled = (
|
||||
vae_dtype != torch.float32
|
||||
) and not server_args.disable_autocast
|
||||
|
||||
with torch.autocast(
|
||||
device_type=current_platform.device_type,
|
||||
dtype=vae_dtype,
|
||||
enabled=vae_autocast_enabled,
|
||||
):
|
||||
try:
|
||||
if server_args.pipeline_config.vae_tiling:
|
||||
self.vae.enable_tiling()
|
||||
except Exception:
|
||||
pass
|
||||
if not vae_autocast_enabled:
|
||||
video_condition = video_condition.to(vae_dtype)
|
||||
|
||||
latent_dist: DiagonalGaussianDistribution = self.vae.encode(video_condition)
|
||||
if isinstance(latent_dist, AutoencoderKLOutput):
|
||||
latent_dist = latent_dist.latent_dist
|
||||
|
||||
mode = server_args.pipeline_config.vae_config.encode_sample_mode()
|
||||
if mode == "argmax":
|
||||
latent = latent_dist.mode()
|
||||
elif mode == "sample":
|
||||
if batch.generator is None:
|
||||
raise ValueError("Generator must be provided for VAE sampling.")
|
||||
latent = latent_dist.sample(batch.generator)
|
||||
else:
|
||||
raise ValueError(f"Unsupported encode_sample_mode: {mode}")
|
||||
|
||||
# Match the normalized latent space used by this pipeline (inverse of DecodingStage.scale_and_shift).
|
||||
scaling_factor, shift_factor = (
|
||||
server_args.pipeline_config.get_decode_scale_and_shift(
|
||||
device=latent.device, dtype=latent.dtype, vae=self.vae
|
||||
)
|
||||
)
|
||||
if isinstance(shift_factor, torch.Tensor):
|
||||
shift_factor = shift_factor.to(latent.device)
|
||||
if isinstance(scaling_factor, torch.Tensor):
|
||||
scaling_factor = scaling_factor.to(latent.device)
|
||||
if shift_factor is not None:
|
||||
latent = latent - shift_factor
|
||||
latent = latent * scaling_factor
|
||||
|
||||
packed = server_args.pipeline_config.maybe_pack_latents(
|
||||
latent, latent.shape[0], batch
|
||||
)
|
||||
if not (isinstance(packed, torch.Tensor) and packed.ndim == 3):
|
||||
raise ValueError("Expected packed image latents [B, S0, D].")
|
||||
|
||||
# Fail-fast token count: must match one latent frame's tokens.
|
||||
vae_sf = int(server_args.pipeline_config.vae_scale_factor)
|
||||
patch = int(server_args.pipeline_config.patch_size)
|
||||
latent_h = int(batch.height) // vae_sf
|
||||
latent_w = int(batch.width) // vae_sf
|
||||
expected_tokens = (latent_h // patch) * (latent_w // patch)
|
||||
if int(packed.shape[1]) != int(expected_tokens):
|
||||
raise ValueError(
|
||||
"LTX-2 conditioning token count mismatch: "
|
||||
f"{int(packed.shape[1])=} {int(expected_tokens)=}."
|
||||
)
|
||||
|
||||
batch.image_latent = packed
|
||||
batch.ltx2_num_image_tokens = int(packed.shape[1])
|
||||
|
||||
if batch.debug:
|
||||
logger.info(
|
||||
"LTX2 TI2V conditioning prepared: %d tokens (shape=%s) for %sx%s",
|
||||
batch.ltx2_num_image_tokens,
|
||||
tuple(batch.image_latent.shape),
|
||||
batch.width,
|
||||
batch.height,
|
||||
)
|
||||
|
||||
if server_args.vae_cpu_offload:
|
||||
self.vae = self.vae.to("cpu")
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||
"""
|
||||
Run the denoising loop.
|
||||
|
||||
Args:
|
||||
batch: The current batch information.
|
||||
server_args: The inference arguments.
|
||||
|
||||
Returns:
|
||||
The batch with denoised latents.
|
||||
"""
|
||||
# Disable cache-dit for image-conditioned requests (TI2V-style) for correctness/debuggability.
|
||||
self._disable_cache_dit_for_request = batch.image_path is not None
|
||||
|
||||
# Prepare variables for the denoising loop
|
||||
|
||||
prepared_vars = self._prepare_denoising_loop(batch, server_args)
|
||||
extra_step_kwargs = prepared_vars["extra_step_kwargs"]
|
||||
target_dtype = prepared_vars["target_dtype"]
|
||||
autocast_enabled = prepared_vars["autocast_enabled"]
|
||||
timesteps = prepared_vars["timesteps"]
|
||||
num_inference_steps = prepared_vars["num_inference_steps"]
|
||||
num_warmup_steps = prepared_vars["num_warmup_steps"]
|
||||
image_kwargs = prepared_vars["image_kwargs"]
|
||||
pos_cond_kwargs = prepared_vars["pos_cond_kwargs"]
|
||||
neg_cond_kwargs = prepared_vars["neg_cond_kwargs"]
|
||||
latents = prepared_vars["latents"]
|
||||
boundary_timestep = prepared_vars["boundary_timestep"]
|
||||
z = prepared_vars["z"]
|
||||
reserved_frames_mask = prepared_vars["reserved_frames_mask"]
|
||||
seq_len = prepared_vars["seq_len"]
|
||||
guidance = prepared_vars["guidance"]
|
||||
|
||||
audio_latents = batch.audio_latents
|
||||
audio_scheduler = copy.deepcopy(self.scheduler)
|
||||
|
||||
# Prepare TI2V conditioning once (encode image -> patchify tokens).
|
||||
self._prepare_ltx2_image_latent(batch, server_args)
|
||||
|
||||
# For LTX-2 packed token latents, SP sharding happens on the time dimension
|
||||
# (frames). The model must see local latent frames (RoPE offset is applied
|
||||
# inside the model using SP rank).
|
||||
latent_num_frames_for_model = self._get_video_latent_num_frames_for_model(
|
||||
batch=batch, server_args=server_args, latents=latents
|
||||
)
|
||||
latent_height = batch.height // server_args.pipeline_config.vae_scale_factor
|
||||
latent_width = batch.width // server_args.pipeline_config.vae_scale_factor
|
||||
|
||||
# Initialize lists for ODE trajectory
|
||||
trajectory_timesteps: list[torch.Tensor] = []
|
||||
trajectory_latents: list[torch.Tensor] = []
|
||||
trajectory_audio_latents: list[torch.Tensor] = []
|
||||
|
||||
# Run denoising loop
|
||||
denoising_start_time = time.time()
|
||||
|
||||
# to avoid device-sync caused by timestep comparison
|
||||
is_warmup = batch.is_warmup
|
||||
self.scheduler.set_begin_index(0)
|
||||
audio_scheduler.set_begin_index(0)
|
||||
timesteps_cpu = timesteps.cpu()
|
||||
num_timesteps = timesteps_cpu.shape[0]
|
||||
|
||||
do_ti2v = self._should_apply_ltx2_ti2v(batch)
|
||||
num_img_tokens = int(getattr(batch, "ltx2_num_image_tokens", 0))
|
||||
denoise_mask = None
|
||||
clean_latent = None
|
||||
if do_ti2v:
|
||||
if not (isinstance(latents, torch.Tensor) and latents.ndim == 3):
|
||||
raise ValueError("LTX-2 TI2V expects packed token latents [B, S, D].")
|
||||
latents[:, :num_img_tokens, :] = batch.image_latent[
|
||||
:, :num_img_tokens, :
|
||||
].to(device=latents.device, dtype=latents.dtype)
|
||||
denoise_mask = torch.ones(
|
||||
(latents.shape[0], latents.shape[1], 1),
|
||||
device=latents.device,
|
||||
dtype=torch.float32,
|
||||
)
|
||||
denoise_mask[:, :num_img_tokens, :] = 0.0
|
||||
clean_latent = latents.detach().clone()
|
||||
clean_latent[:, :num_img_tokens, :] = batch.image_latent[
|
||||
:, :num_img_tokens, :
|
||||
].to(device=latents.device, dtype=latents.dtype)
|
||||
|
||||
with torch.autocast(
|
||||
device_type=current_platform.device_type,
|
||||
dtype=target_dtype,
|
||||
enabled=autocast_enabled,
|
||||
):
|
||||
with self.progress_bar(total=num_inference_steps) as progress_bar:
|
||||
for i, t_host in enumerate(timesteps_cpu):
|
||||
with StageProfiler(
|
||||
f"denoising_step_{i}",
|
||||
logger=logger,
|
||||
timings=batch.timings,
|
||||
perf_dump_path_provided=batch.perf_dump_path is not None,
|
||||
):
|
||||
t_int = int(t_host.item())
|
||||
t_device = timesteps[i]
|
||||
current_model, current_guidance_scale = (
|
||||
self._select_and_manage_model(
|
||||
t_int=t_int,
|
||||
boundary_timestep=boundary_timestep,
|
||||
server_args=server_args,
|
||||
batch=batch,
|
||||
)
|
||||
)
|
||||
|
||||
# Predict noise residual
|
||||
attn_metadata = self._build_attn_metadata(i, batch, server_args)
|
||||
|
||||
# === LTX-2 sigma-space Euler step (flow matching) ===
|
||||
# Use scheduler-generated sigmas (includes terminal sigma=0).
|
||||
sigmas = getattr(self.scheduler, "sigmas", None)
|
||||
if sigmas is None or not isinstance(sigmas, torch.Tensor):
|
||||
raise ValueError(
|
||||
"Expected scheduler.sigmas to be a tensor for LTX-2."
|
||||
)
|
||||
sigma = sigmas[i].to(device=latents.device, dtype=torch.float32)
|
||||
sigma_next = sigmas[i + 1].to(
|
||||
device=latents.device, dtype=torch.float32
|
||||
)
|
||||
dt = sigma_next - sigma
|
||||
|
||||
latent_model_input = latents.to(target_dtype)
|
||||
audio_latent_model_input = audio_latents.to(target_dtype)
|
||||
|
||||
latent_num_frames = latent_num_frames_for_model
|
||||
|
||||
# Audio latent dims
|
||||
if audio_latent_model_input.ndim == 3:
|
||||
audio_num_frames_latent = int(
|
||||
audio_latent_model_input.shape[1]
|
||||
)
|
||||
elif audio_latent_model_input.ndim == 4:
|
||||
audio_num_frames_latent = int(
|
||||
audio_latent_model_input.shape[2]
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unexpected audio latents rank: {audio_latent_model_input.ndim}, shape={tuple(audio_latent_model_input.shape)}"
|
||||
)
|
||||
|
||||
# LTX-2 model can generate coords internally.
|
||||
video_coords = None
|
||||
audio_coords = None
|
||||
|
||||
timestep = t_device.expand(int(latent_model_input.shape[0]))
|
||||
if do_ti2v and denoise_mask is not None:
|
||||
timestep_video = timestep.unsqueeze(
|
||||
-1
|
||||
) * denoise_mask.squeeze(-1)
|
||||
else:
|
||||
timestep_video = timestep
|
||||
timestep_audio = timestep
|
||||
|
||||
# Conditions
|
||||
encoder_hidden_states = batch.prompt_embeds[0]
|
||||
audio_encoder_hidden_states = batch.audio_prompt_embeds[0]
|
||||
encoder_attention_mask = batch.prompt_attention_mask
|
||||
|
||||
# Follow ltx-pipelines structure: separate pos/neg forward passes,
|
||||
# then apply CFG on denoised (x0) predictions.
|
||||
with set_forward_context(
|
||||
current_timestep=i, attn_metadata=attn_metadata
|
||||
):
|
||||
v_pos, a_v_pos = current_model(
|
||||
hidden_states=latent_model_input,
|
||||
audio_hidden_states=audio_latent_model_input,
|
||||
encoder_hidden_states=encoder_hidden_states,
|
||||
audio_encoder_hidden_states=audio_encoder_hidden_states,
|
||||
timestep=timestep_video,
|
||||
audio_timestep=timestep_audio,
|
||||
encoder_attention_mask=encoder_attention_mask,
|
||||
audio_encoder_attention_mask=encoder_attention_mask,
|
||||
num_frames=latent_num_frames,
|
||||
height=latent_height,
|
||||
width=latent_width,
|
||||
fps=batch.fps,
|
||||
audio_num_frames=audio_num_frames_latent,
|
||||
video_coords=video_coords,
|
||||
audio_coords=audio_coords,
|
||||
return_latents=False,
|
||||
return_dict=False,
|
||||
)
|
||||
|
||||
if batch.do_classifier_free_guidance:
|
||||
neg_encoder_hidden_states = (
|
||||
batch.negative_prompt_embeds[0]
|
||||
)
|
||||
neg_audio_encoder_hidden_states = (
|
||||
batch.negative_audio_prompt_embeds[0]
|
||||
)
|
||||
neg_encoder_attention_mask = (
|
||||
batch.negative_attention_mask
|
||||
)
|
||||
|
||||
v_neg, a_v_neg = current_model(
|
||||
hidden_states=latent_model_input,
|
||||
audio_hidden_states=audio_latent_model_input,
|
||||
encoder_hidden_states=neg_encoder_hidden_states,
|
||||
audio_encoder_hidden_states=neg_audio_encoder_hidden_states,
|
||||
timestep=timestep_video,
|
||||
audio_timestep=timestep_audio,
|
||||
encoder_attention_mask=neg_encoder_attention_mask,
|
||||
audio_encoder_attention_mask=neg_encoder_attention_mask,
|
||||
num_frames=latent_num_frames,
|
||||
height=latent_height,
|
||||
width=latent_width,
|
||||
fps=batch.fps,
|
||||
audio_num_frames=audio_num_frames_latent,
|
||||
video_coords=video_coords,
|
||||
audio_coords=audio_coords,
|
||||
return_latents=False,
|
||||
return_dict=False,
|
||||
)
|
||||
else:
|
||||
v_neg = None
|
||||
a_v_neg = None
|
||||
|
||||
v_pos = v_pos.float()
|
||||
a_v_pos = a_v_pos.float()
|
||||
if v_neg is not None:
|
||||
v_neg = v_neg.float()
|
||||
if a_v_neg is not None:
|
||||
a_v_neg = a_v_neg.float()
|
||||
|
||||
# Velocity -> denoised (x0): x0 = x - sigma * v
|
||||
sigma_val = float(sigma.item())
|
||||
denoised_video = latents.float() - sigma_val * v_pos
|
||||
denoised_audio = audio_latents.float() - sigma_val * a_v_pos
|
||||
|
||||
if (
|
||||
batch.do_classifier_free_guidance
|
||||
and v_neg is not None
|
||||
and a_v_neg is not None
|
||||
):
|
||||
denoised_video_neg = latents.float() - sigma_val * v_neg
|
||||
denoised_audio_neg = (
|
||||
audio_latents.float() - sigma_val * a_v_neg
|
||||
)
|
||||
denoised_video = denoised_video + (
|
||||
batch.guidance_scale - 1.0
|
||||
) * (denoised_video - denoised_video_neg)
|
||||
denoised_audio = denoised_audio + (
|
||||
batch.guidance_scale - 1.0
|
||||
) * (denoised_audio - denoised_audio_neg)
|
||||
|
||||
# Apply conditioning mask (keep conditioned tokens clean).
|
||||
if (
|
||||
do_ti2v
|
||||
and denoise_mask is not None
|
||||
and clean_latent is not None
|
||||
):
|
||||
denoised_video = (
|
||||
denoised_video * denoise_mask
|
||||
+ clean_latent.float() * (1.0 - denoise_mask)
|
||||
)
|
||||
|
||||
# Euler step in sigma space: x_next = x + (sigma_next - sigma) * v,
|
||||
# where v = (x - x0) / sigma.
|
||||
if sigma_val == 0.0:
|
||||
v_video = torch.zeros_like(denoised_video)
|
||||
v_audio = torch.zeros_like(denoised_audio)
|
||||
else:
|
||||
v_video = (latents.float() - denoised_video) / sigma_val
|
||||
v_audio = (
|
||||
audio_latents.float() - denoised_audio
|
||||
) / sigma_val
|
||||
|
||||
latents = (latents.float() + v_video * dt).to(
|
||||
dtype=latents.dtype
|
||||
)
|
||||
audio_latents = (audio_latents.float() + v_audio * dt).to(
|
||||
dtype=audio_latents.dtype
|
||||
)
|
||||
|
||||
if do_ti2v:
|
||||
latents[:, :num_img_tokens, :] = batch.image_latent[
|
||||
:, :num_img_tokens, :
|
||||
].to(device=latents.device, dtype=latents.dtype)
|
||||
|
||||
latents = self.post_forward_for_ti2v_task(
|
||||
batch, server_args, reserved_frames_mask, latents, z
|
||||
)
|
||||
|
||||
# save trajectory latents if needed
|
||||
if batch.return_trajectory_latents:
|
||||
trajectory_timesteps.append(t_host)
|
||||
trajectory_latents.append(latents)
|
||||
if audio_latents is not None:
|
||||
trajectory_audio_latents.append(audio_latents)
|
||||
|
||||
# Update progress bar
|
||||
if i == num_timesteps - 1 or (
|
||||
(i + 1) > num_warmup_steps
|
||||
and (i + 1) % self.scheduler.order == 0
|
||||
and progress_bar is not None
|
||||
):
|
||||
progress_bar.update()
|
||||
|
||||
if not is_warmup:
|
||||
self.step_profile()
|
||||
|
||||
denoising_end_time = time.time()
|
||||
|
||||
if num_timesteps > 0 and not is_warmup:
|
||||
self.log_info(
|
||||
"average time per step: %.4f seconds",
|
||||
(denoising_end_time - denoising_start_time) / len(timesteps),
|
||||
)
|
||||
|
||||
batch.audio_latents = audio_latents
|
||||
self._post_denoising_loop(
|
||||
batch=batch,
|
||||
latents=latents,
|
||||
trajectory_latents=trajectory_latents,
|
||||
trajectory_timesteps=trajectory_timesteps,
|
||||
trajectory_audio_latents=trajectory_audio_latents,
|
||||
server_args=server_args,
|
||||
is_warmup=is_warmup,
|
||||
)
|
||||
|
||||
return batch
|
||||
|
||||
def _post_denoising_loop(
|
||||
self,
|
||||
batch: Req,
|
||||
latents: torch.Tensor,
|
||||
trajectory_latents: list,
|
||||
trajectory_timesteps: list,
|
||||
trajectory_audio_latents: list,
|
||||
server_args: ServerArgs,
|
||||
is_warmup: bool = False,
|
||||
):
|
||||
# 1. Handle Trajectory (Video) - Copy from base
|
||||
if trajectory_latents:
|
||||
trajectory_tensor = torch.stack(trajectory_latents, dim=1)
|
||||
trajectory_timesteps_tensor = torch.stack(trajectory_timesteps, dim=0)
|
||||
else:
|
||||
trajectory_tensor = None
|
||||
trajectory_timesteps_tensor = None
|
||||
|
||||
latents, trajectory_tensor = self._postprocess_sp_latents(
|
||||
batch, latents, trajectory_tensor
|
||||
)
|
||||
|
||||
# If SP time-sharding padded whole frames worth of tokens, remove padding
|
||||
# after gather and before unpacking.
|
||||
latents = self._truncate_sp_padded_token_latents(batch, latents)
|
||||
|
||||
if trajectory_tensor is not None and trajectory_timesteps_tensor is not None:
|
||||
batch.trajectory_timesteps = trajectory_timesteps_tensor.cpu()
|
||||
batch.trajectory_latents = trajectory_tensor.cpu()
|
||||
|
||||
# 2. Handle Trajectory (Audio) - LTX-2 specific
|
||||
if trajectory_audio_latents:
|
||||
trajectory_audio_tensor = torch.stack(trajectory_audio_latents, dim=1)
|
||||
# We don't have SP support for audio latents yet (or needed?)
|
||||
batch.trajectory_audio_latents = trajectory_audio_tensor.cpu()
|
||||
|
||||
# 3. Unpack and Denormalize
|
||||
# Call pipeline_config._unpad_and_unpack_latents
|
||||
# latents is video latents.
|
||||
# batch.audio_latents is audio latents.
|
||||
|
||||
audio_latents = batch.audio_latents
|
||||
|
||||
# NOTE: self.vae and self.audio_vae should be populated via __init__ or manual setting
|
||||
if self.vae is None or self.audio_vae is None:
|
||||
logger.warning(
|
||||
"VAE or Audio VAE not found in DenoisingStage. Skipping unpack and denormalize."
|
||||
)
|
||||
batch.latents = latents
|
||||
batch.audio_latents = audio_latents
|
||||
else:
|
||||
latents, audio_latents = (
|
||||
server_args.pipeline_config._unpad_and_unpack_latents(
|
||||
latents, audio_latents, batch, self.vae, self.audio_vae
|
||||
)
|
||||
)
|
||||
|
||||
batch.latents = latents
|
||||
batch.audio_latents = audio_latents
|
||||
|
||||
# 4. Cleanup
|
||||
offload_mgr = getattr(self.transformer, "_layerwise_offload_manager", None)
|
||||
if offload_mgr is not None and getattr(offload_mgr, "enabled", False):
|
||||
offload_mgr.release_all()
|
||||
|
||||
def verify_input(self, batch: Req, server_args: ServerArgs) -> VerificationResult:
|
||||
"""Verify denoising stage inputs.
|
||||
|
||||
Note: LTX-2 connector stage converts `prompt_embeds`/`negative_prompt_embeds`
|
||||
from list-of-tensors to a single tensor (video context) and stores audio
|
||||
context separately.
|
||||
"""
|
||||
|
||||
result = VerificationResult()
|
||||
result.add_check("timesteps", batch.timesteps, [V.is_tensor, V.min_dims(1)])
|
||||
|
||||
# LTX-2 may carry prompt embeddings as either a tensor (preferred) or legacy list.
|
||||
result.add_check(
|
||||
"prompt_embeds",
|
||||
batch.prompt_embeds,
|
||||
lambda x: V.is_tensor(x) or V.list_not_empty(x),
|
||||
)
|
||||
|
||||
# Keep base expectation: image_embeds is always a list (may be empty).
|
||||
result.add_check("image_embeds", batch.image_embeds, V.is_list)
|
||||
|
||||
result.add_check(
|
||||
"num_inference_steps", batch.num_inference_steps, V.positive_int
|
||||
)
|
||||
result.add_check("guidance_scale", batch.guidance_scale, V.non_negative_float)
|
||||
result.add_check("eta", batch.eta, V.non_negative_float)
|
||||
result.add_check("generator", batch.generator, V.generator_or_list_generators)
|
||||
result.add_check(
|
||||
"do_classifier_free_guidance",
|
||||
batch.do_classifier_free_guidance,
|
||||
V.bool_value,
|
||||
)
|
||||
|
||||
# When CFG is enabled, negative prompt embeddings must exist (tensor or legacy list).
|
||||
result.add_check(
|
||||
"negative_prompt_embeds",
|
||||
batch.negative_prompt_embeds,
|
||||
lambda x: (not batch.do_classifier_free_guidance)
|
||||
or V.is_tensor(x)
|
||||
or V.list_not_empty(x),
|
||||
)
|
||||
return result
|
||||
|
||||
def do_classifier_free_guidance(self, batch: Req) -> bool:
|
||||
return batch.guidance_scale > 1.0
|
||||
|
||||
|
||||
class LTX2RefinementStage(LTX2AVDenoisingStage):
|
||||
def __init__(
|
||||
self, transformer, scheduler, distilled_sigmas, vae=None, audio_vae=None
|
||||
):
|
||||
super().__init__(transformer, scheduler, vae, audio_vae)
|
||||
self.distilled_sigmas = torch.tensor(distilled_sigmas)
|
||||
|
||||
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||
# 1. Add noise to latents
|
||||
noise_scale = self.distilled_sigmas[0].to(batch.latents.device)
|
||||
noise = torch.randn_like(batch.latents)
|
||||
batch.latents = batch.latents + noise * noise_scale
|
||||
|
||||
# 2. Run denoising loop with distilled_sigmas
|
||||
# Save original sigmas
|
||||
original_sigmas = self.scheduler.sigmas
|
||||
original_timesteps = self.scheduler.timesteps
|
||||
original_num_inference_steps = self.scheduler.num_inference_steps
|
||||
|
||||
# Set distilled sigmas
|
||||
self.scheduler.sigmas = self.distilled_sigmas.to(self.scheduler.sigmas.device)
|
||||
# Approximation for timesteps
|
||||
self.scheduler.timesteps = self.scheduler.sigmas * 1000
|
||||
self.scheduler.num_inference_steps = len(self.distilled_sigmas) - 1
|
||||
|
||||
# Call parent forward
|
||||
try:
|
||||
batch = super().forward(batch, server_args)
|
||||
finally:
|
||||
# Restore original sigmas
|
||||
self.scheduler.sigmas = original_sigmas
|
||||
self.scheduler.timesteps = original_timesteps
|
||||
self.scheduler.num_inference_steps = original_num_inference_steps
|
||||
|
||||
return batch
|
||||
|
||||
def do_classifier_free_guidance(self, batch: Req) -> bool:
|
||||
return False # Stage 2 uses simple denoising (no CFG)
|
||||
@@ -0,0 +1,104 @@
|
||||
import torch
|
||||
from diffusers.utils.torch_utils import randn_tensor
|
||||
|
||||
from sglang.multimodal_gen.runtime.distributed import get_local_torch_device
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.latent_preparation import (
|
||||
LatentPreparationStage,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||
StageValidators as V,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||
VerificationResult,
|
||||
)
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||
|
||||
logger = init_logger(__name__)
|
||||
|
||||
|
||||
class LTX2AVLatentPreparationStage(LatentPreparationStage):
|
||||
"""
|
||||
LTX-2 specific latent preparation stage that handles both video and audio latents.
|
||||
"""
|
||||
|
||||
def __init__(self, scheduler, transformer=None, audio_vae=None):
|
||||
super().__init__(scheduler, transformer)
|
||||
self.audio_vae = audio_vae
|
||||
|
||||
def verify_input(self, batch: Req, server_args: ServerArgs) -> VerificationResult:
|
||||
"""Verify latent preparation stage inputs."""
|
||||
result = VerificationResult()
|
||||
result.add_check(
|
||||
"prompt_or_embeds",
|
||||
None,
|
||||
lambda _: V.string_or_list_strings(batch.prompt)
|
||||
or V.list_not_empty(batch.prompt_embeds)
|
||||
or V.is_tensor(batch.prompt_embeds),
|
||||
)
|
||||
|
||||
if isinstance(batch.prompt_embeds, list):
|
||||
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_of_tensors)
|
||||
else:
|
||||
result.add_check("prompt_embeds", batch.prompt_embeds, V.is_tensor)
|
||||
|
||||
result.add_check(
|
||||
"num_videos_per_prompt", batch.num_outputs_per_prompt, V.positive_int
|
||||
)
|
||||
result.add_check("generator", batch.generator, V.generator_or_list_generators)
|
||||
result.add_check("num_frames", batch.num_frames, V.positive_int)
|
||||
result.add_check("height", batch.height, V.positive_int)
|
||||
result.add_check("width", batch.width, V.positive_int)
|
||||
result.add_check("latents", batch.latents, V.none_or_tensor)
|
||||
return result
|
||||
|
||||
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||
# 1. Prepare Video Latents using base class logic
|
||||
# This sets batch.latents and batch.raw_latent_shape
|
||||
batch = super().forward(batch, server_args)
|
||||
|
||||
# 2. Prepare Audio Latents (optional)
|
||||
# Default to True if not specified
|
||||
try:
|
||||
generate_audio = batch.generate_audio
|
||||
except AttributeError:
|
||||
generate_audio = True
|
||||
if not generate_audio:
|
||||
batch.audio_latents = None
|
||||
batch.raw_audio_latent_shape = None
|
||||
return batch
|
||||
|
||||
device = get_local_torch_device()
|
||||
if isinstance(batch.prompt_embeds, list) and batch.prompt_embeds:
|
||||
dtype = batch.prompt_embeds[0].dtype
|
||||
elif isinstance(batch.prompt_embeds, torch.Tensor):
|
||||
dtype = batch.prompt_embeds.dtype
|
||||
else:
|
||||
dtype = torch.float16
|
||||
generator = batch.generator
|
||||
|
||||
audio_latents = batch.audio_latents
|
||||
batch_size = batch.batch_size
|
||||
num_frames = batch.num_frames
|
||||
|
||||
if audio_latents is None:
|
||||
shape = server_args.pipeline_config.prepare_audio_latent_shape(
|
||||
batch, batch_size, num_frames
|
||||
)
|
||||
|
||||
audio_latents = randn_tensor(
|
||||
shape, generator=generator, device=device, dtype=dtype
|
||||
)
|
||||
else:
|
||||
audio_latents = audio_latents.to(device)
|
||||
|
||||
audio_latents = server_args.pipeline_config.maybe_pack_audio_latents(
|
||||
audio_latents, batch_size, batch
|
||||
)
|
||||
|
||||
# Store in batch
|
||||
batch.audio_latents = audio_latents
|
||||
batch.raw_audio_latent_shape = audio_latents.shape
|
||||
|
||||
return batch
|
||||
@@ -0,0 +1,92 @@
|
||||
import torch
|
||||
|
||||
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import PipelineStage
|
||||
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||
|
||||
|
||||
class LTX2TextConnectorStage(PipelineStage):
|
||||
"""
|
||||
Stage for applying LTX-2 Text Connectors to split/transform text embeddings
|
||||
into video and audio contexts.
|
||||
"""
|
||||
|
||||
def __init__(self, connectors):
|
||||
super().__init__()
|
||||
self.connectors = connectors
|
||||
|
||||
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||
# Input: batch.prompt_embeds (from Gemma, [B, S, D])
|
||||
# Output: batch.prompt_embeds (Video Context), batch.audio_prompt_embeds (Audio Context)
|
||||
|
||||
prompt_embeds = batch.prompt_embeds
|
||||
prompt_attention_mask = batch.prompt_attention_mask
|
||||
neg_prompt_embeds = batch.negative_prompt_embeds
|
||||
neg_prompt_attention_mask = batch.negative_attention_mask
|
||||
|
||||
if isinstance(prompt_embeds, list):
|
||||
prompt_embeds = prompt_embeds[0] if len(prompt_embeds) > 0 else None
|
||||
|
||||
if isinstance(prompt_attention_mask, list):
|
||||
prompt_attention_mask = (
|
||||
prompt_attention_mask[0] if len(prompt_attention_mask) > 0 else None
|
||||
)
|
||||
|
||||
if isinstance(neg_prompt_embeds, list):
|
||||
neg_prompt_embeds = (
|
||||
neg_prompt_embeds[0] if len(neg_prompt_embeds) > 0 else None
|
||||
)
|
||||
|
||||
if isinstance(neg_prompt_attention_mask, list):
|
||||
neg_prompt_attention_mask = (
|
||||
neg_prompt_attention_mask[0]
|
||||
if len(neg_prompt_attention_mask) > 0
|
||||
else None
|
||||
)
|
||||
|
||||
# Handle CFG: Concatenate negative and positive inputs
|
||||
if batch.do_classifier_free_guidance:
|
||||
|
||||
# Concatenate: [Negative, Positive]
|
||||
prompt_embeds = torch.cat([neg_prompt_embeds, prompt_embeds], dim=0)
|
||||
prompt_attention_mask = torch.cat(
|
||||
[neg_prompt_attention_mask, prompt_attention_mask], dim=0
|
||||
)
|
||||
|
||||
# Prepare additive mask for connectors (as per Diffusers implementation)
|
||||
dtype = prompt_embeds.dtype
|
||||
|
||||
additive_attention_mask = (1 - prompt_attention_mask.to(dtype)) * -1000000.0
|
||||
|
||||
# Call connectors
|
||||
# Expects: prompt_embeds, attention_mask, additive_mask=True
|
||||
with set_forward_context(current_timestep=None, attn_metadata=None):
|
||||
connector_prompt_embeds, connector_audio_prompt_embeds, connector_mask = (
|
||||
self.connectors(
|
||||
prompt_embeds, additive_attention_mask, additive_mask=True
|
||||
)
|
||||
)
|
||||
|
||||
# Split results if CFG was enabled
|
||||
if batch.do_classifier_free_guidance:
|
||||
neg_embeds, pos_embeds = connector_prompt_embeds.chunk(2, dim=0)
|
||||
neg_audio_embeds, pos_audio_embeds = connector_audio_prompt_embeds.chunk(
|
||||
2, dim=0
|
||||
)
|
||||
neg_mask, pos_mask = connector_mask.chunk(2, dim=0)
|
||||
|
||||
batch.prompt_embeds = [pos_embeds]
|
||||
batch.audio_prompt_embeds = [pos_audio_embeds]
|
||||
batch.prompt_attention_mask = pos_mask
|
||||
|
||||
batch.negative_prompt_embeds = [neg_embeds]
|
||||
batch.negative_audio_prompt_embeds = [neg_audio_embeds]
|
||||
batch.negative_attention_mask = neg_mask
|
||||
else:
|
||||
# Update positive fields
|
||||
batch.prompt_embeds = [connector_prompt_embeds]
|
||||
batch.audio_prompt_embeds = [connector_audio_prompt_embeds]
|
||||
batch.prompt_attention_mask = connector_mask
|
||||
|
||||
return batch
|
||||
Reference in New Issue
Block a user