chore: import upstream snapshot with attribution
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 12:38:16 +08:00
commit 94057c3d3e
7152 changed files with 2120455 additions and 0 deletions
@@ -0,0 +1 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
@@ -0,0 +1,708 @@
import functools
import math
from typing import Optional, Tuple, Union
import numpy as np
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)
return x * cos + x_rotated * sin
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).transpose(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)
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.transpose(1, 2).reshape(b, t, -1)
out = out.to(dtype=x_dtype)
return out
@functools.lru_cache(maxsize=5)
def _ltx2_connector_rope_freq_grid_np(
theta: float, num_pos_dims: int, dim: int
) -> torch.Tensor:
# Official LTX uses NumPy float64 for double-precision RoPE frequencies.
n_elem = 2 * num_pos_dims
pow_indices = np.power(
theta,
np.linspace(0.0, 1.0, dim // n_elem, dtype=np.float64),
)
return torch.tensor(pow_indices * math.pi / 2.0, dtype=torch.float32)
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",
apply_gated_attention: bool = False,
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_gate_logits = None
if apply_gated_attention:
self.to_gate_logits = torch.nn.Linear(query_dim, heads, bias=True)
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:
gate_input = hidden_states
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)).transpose(1, 2)
key = key.unflatten(2, (self.heads, -1)).transpose(1, 2)
value = value.unflatten(2, (self.heads, -1)).transpose(1, 2)
if attention_mask is not None:
if attention_mask.ndim == 2:
attention_mask = attention_mask[:, None, None, :]
elif attention_mask.ndim == 3:
attention_mask = attention_mask[:, None, :, :]
attention_mask = attention_mask.to(dtype=query.dtype)
hidden_states = F.scaled_dot_product_attention(
query,
key,
value,
attn_mask=attention_mask,
dropout_p=0.0,
is_causal=False,
)
hidden_states = hidden_states.transpose(1, 2).flatten(2, 3)
hidden_states = hidden_states.to(query.dtype)
if self.to_gate_logits is not None:
gate_logits = self.to_gate_logits(gate_input)
b, t, _ = hidden_states.shape
hidden_states = hidden_states.view(b, t, self.heads, self.head_dim)
hidden_states = hidden_states * (
2.0 * torch.sigmoid(gate_logits).unsqueeze(-1)
)
hidden_states = hidden_states.view(b, t, self.heads * self.head_dim)
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],
dtype: Optional[torch.dtype] = None,
) -> 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
if self.double_precision:
freqs = _ltx2_connector_rope_freq_grid_np(self.theta, 1, self.dim).to(
device=device
)
else:
pow_indices = torch.pow(
self.theta,
torch.linspace(
start=0.0,
end=1.0,
steps=self.dim // num_rope_elems,
dtype=torch.float32,
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)
if dtype is not None:
cos_freqs = cos_freqs.to(dtype)
sin_freqs = sin_freqs.to(dtype)
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",
apply_gated_attention: bool = False,
):
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,
apply_gated_attention=apply_gated_attention,
)
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",
apply_gated_attention: bool = False,
):
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,
apply_gated_attention=apply_gated_attention,
)
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,
dtype=hidden_states.dtype,
)
# 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
self.caption_channels = 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
connector_apply_gated_attention = config.connector_apply_gated_attention
feature_extractor_in_features = config.feature_extractor_in_features
video_feature_extractor_out_features = (
config.video_feature_extractor_out_features
)
audio_feature_extractor_out_features = (
config.audio_feature_extractor_out_features
)
self.text_proj_in: nn.Linear | None = None
self.video_aggregate_embed: nn.Linear | None = None
self.audio_aggregate_embed: nn.Linear | None = None
if (
feature_extractor_in_features > 0
and video_feature_extractor_out_features > 0
and audio_feature_extractor_out_features > 0
):
self.video_aggregate_embed = nn.Linear(
feature_extractor_in_features,
video_feature_extractor_out_features,
bias=True,
)
self.audio_aggregate_embed = nn.Linear(
feature_extractor_in_features,
audio_feature_extractor_out_features,
bias=True,
)
else:
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,
apply_gated_attention=connector_apply_gated_attention,
)
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,
apply_gated_attention=connector_apply_gated_attention,
)
@staticmethod
def _rescale_v2_features(
x: torch.Tensor, target_dim: int, source_dim: int
) -> torch.Tensor:
return x * math.sqrt(target_dim / source_dim)
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 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)
if (
self.video_aggregate_embed is not None
and self.audio_aggregate_embed is not None
):
video_hidden_states = text_encoder_hidden_states
audio_hidden_states = text_encoder_hidden_states
if video_hidden_states.dtype != self.video_aggregate_embed.weight.dtype:
video_hidden_states = video_hidden_states.to(
self.video_aggregate_embed.weight.dtype
)
if audio_hidden_states.dtype != self.audio_aggregate_embed.weight.dtype:
audio_hidden_states = audio_hidden_states.to(
self.audio_aggregate_embed.weight.dtype
)
source_dim = self.caption_channels
video_hidden_states = self._rescale_v2_features(
video_hidden_states,
self.video_aggregate_embed.out_features,
source_dim,
)
audio_hidden_states = self._rescale_v2_features(
audio_hidden_states,
self.audio_aggregate_embed.out_features,
source_dim,
)
video_hidden_states = self.video_aggregate_embed(video_hidden_states)
audio_hidden_states = self.audio_aggregate_embed(audio_hidden_states)
else:
assert self.text_proj_in is not None
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
)
video_hidden_states = self.text_proj_in(text_encoder_hidden_states)
audio_hidden_states = video_hidden_states
video_text_embedding, new_attn_mask = self.video_connector(
video_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(
audio_hidden_states, attention_mask
)
return video_text_embedding, audio_text_embedding, new_attn_mask
EntryClass = LTX2TextConnectors
@@ -0,0 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
from sglang.multimodal_gen.runtime.models.bridges.mova_dual_tower import (
DualTowerConditionalBridge,
)
__all__ = ["DualTowerConditionalBridge"]
@@ -0,0 +1,675 @@
# SPDX-License-Identifier: Apache-2.0
# Copied and adapted from: mossVG/mova/diffusion/models/interactionv2.py
from typing import Any, Dict, List, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
MOVADualTowerConfig,
)
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import (
RMSNorm,
tensor_parallel_rms_norm,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
apply_flashinfer_rope_qk_inplace,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@torch.no_grad()
def compute_rope_cos_sin(
position_ids: torch.Tensor,
head_dim: int,
base: float = 10000.0,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""
Compute RoPE cos/sin embeddings for given position IDs.
This is a functional implementation that doesn't require storing buffers,
making it compatible with FSDP meta device initialization.
Args:
position_ids: Position IDs tensor [B, L] or [1, L]
head_dim: Dimension of each attention head
base: RoPE base frequency (default: 10000.0)
device: Target device
dtype: Output dtype
Returns:
(cos, sin): Each with shape [B, L, head_dim]
"""
device = device or position_ids.device
dtype = dtype or torch.float32
# Compute inverse frequencies
inv_freq = 1.0 / (
base
** (torch.arange(0, head_dim, 2, dtype=torch.float32, device=device) / head_dim)
)
# Expand for batch computation: [B, L] -> [B, 1, L] @ [1, head_dim/2, 1] -> [B, head_dim/2, L]
inv_freq_expanded = inv_freq[None, :, None].expand(position_ids.shape[0], -1, 1)
position_ids_expanded = position_ids[:, None, :].float()
# Compute frequencies: [B, head_dim/2, L] -> [B, L, head_dim/2]
freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
# Double the frequencies for full head_dim: [B, L, head_dim]
emb = torch.cat((freqs, freqs), dim=-1)
cos = emb.cos().to(dtype=dtype)
sin = emb.sin().to(dtype=dtype)
return cos, sin
class PerFrameAttentionPooling(nn.Module):
"""Per-frame multi-head attention pooling.
Flattens the input sequence [B, L, D] and grid size (T, H, W).
Performs single-query attention pooling on the H*W tokens for each time frame.
Output shape: [B, T, D].
"""
def __init__(self, dim: int, num_heads: int, eps: float = 1e-6):
super().__init__()
assert dim % num_heads == 0, "dim must be divisible by num_heads"
self.dim = dim
self.num_heads = num_heads
self.probe = nn.Parameter(torch.randn(1, 1, dim))
nn.init.normal_(self.probe, std=0.02)
self.attention = nn.MultiheadAttention(
embed_dim=dim, num_heads=num_heads, batch_first=True
)
self.layernorm = nn.LayerNorm(dim, eps=eps)
def forward(self, x: torch.Tensor, grid_size: Tuple[int, int, int]) -> torch.Tensor:
"""Forward pass.
Args:
x: Input tensor of shape [B, L, D], where L = T * H * W.
grid_size: Tuple of (T, H, W).
Returns:
Pooled tensor of shape [B, T, D].
"""
B, L, D = x.shape
T, H, W = grid_size
assert (
D == self.dim
), f"Input dimension D={D} does not match module dim={self.dim}"
assert L == T * H * W, f"Flattened length L={L} does not match T*H*W={T*H*W}"
S = H * W
x_bt_s_d = x.view(B, T, S, D).contiguous().view(B * T, S, D) # [B*T, S, D]
probe = self.probe.expand(B * T, -1, -1) # [B*T, 1, D]
pooled_bt_1_d = self.attention(probe, x_bt_s_d, x_bt_s_d, need_weights=False)[0]
pooled_bt_d = pooled_bt_1_d.squeeze(1) # [B*T, D]
pooled = pooled_bt_d.view(B, T, D)
pooled = self.layernorm(pooled)
return pooled
class CrossModalInteractionController:
"""Strategy class to control dual-tower interaction.
Manages the interaction mapping between Visual DiT (e.g., 30 layers)
and Audio DiT (e.g., 30 layers).
"""
def __init__(self, visual_layers: int = 30, audio_layers: int = 30):
self.visual_layers = visual_layers
self.audio_layers = audio_layers
self.min_layers = min(visual_layers, audio_layers)
def get_interaction_layers(
self, strategy: str = "shallow_focus"
) -> Dict[str, List[Tuple[int, int]]]:
"""Gets the mapping relationship of interaction layers."""
if strategy == "shallow_focus":
num_interact = min(10, self.min_layers // 3)
interact_layers = list(range(0, num_interact))
elif strategy == "distributed":
step = 3
interact_layers = list(range(0, self.min_layers, step))
elif strategy == "progressive":
shallow = list(range(0, min(8, self.min_layers)))
if self.min_layers > 8:
deep = list(range(8, self.min_layers, 3))
interact_layers = shallow + deep
else:
interact_layers = shallow
elif strategy == "custom":
interact_layers = [0, 2, 4, 6, 8, 12, 16, 20]
interact_layers = [i for i in interact_layers if i < self.min_layers]
elif strategy == "full":
interact_layers = list(range(0, self.min_layers))
else:
raise ValueError(f"Unknown interaction strategy: {strategy}")
mapping = {
"v2a": [(i, i) for i in interact_layers],
"a2v": [(i, i) for i in interact_layers],
}
return mapping
def should_interact(
self, layer_idx: int, direction: str, interaction_mapping: Dict
) -> bool:
"""Determines if the specified layer needs to interact."""
if direction not in interaction_mapping:
return False
return any(src == layer_idx for src, _ in interaction_mapping[direction])
class ConditionalCrossAttention(nn.Module):
"""
Cross-modal attention for dual-tower bridge with Tensor Parallel support.
This module handles attention between video and audio hidden states,
which have different sequence lengths.
"""
def __init__(self, dim: int, kv_dim: int, num_heads: int, eps: float = 1e-6):
super().__init__()
self.q_dim = dim
self.kv_dim = kv_dim
self.num_heads = num_heads
self.head_dim = self.q_dim // num_heads
self.tp_size = get_tp_world_size()
if self.num_heads % self.tp_size != 0:
raise ValueError(
f"num_heads ({self.num_heads}) must be divisible by tp_size ({self.tp_size})."
)
self.num_heads_per_rank = self.num_heads // self.tp_size
# TP strategy: shard Q/K/V over heads (column-parallel), then row-parallel output.
self.q = ColumnParallelLinear(dim, dim, bias=True, gather_output=False)
self.k = ColumnParallelLinear(kv_dim, dim, bias=True, gather_output=False)
self.v = ColumnParallelLinear(kv_dim, dim, bias=True, gather_output=False)
self.o = RowParallelLinear(dim, dim, bias=True, input_is_parallel=True)
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
self.attn = USPAttention(
num_heads=self.num_heads_per_rank,
head_size=self.head_dim,
causal=False,
softmax_scale=None,
# is_cross_attention=True,
)
def forward(
self,
x: torch.Tensor,
y: torch.Tensor,
x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
):
ctx = y
q, _ = self.q(x)
k, _ = self.k(ctx)
v, _ = self.v(ctx)
# RMSNorm over sharded hidden dimension
if self.tp_size > 1:
q = tensor_parallel_rms_norm(q, self.norm_q)
k = tensor_parallel_rms_norm(k, self.norm_k)
else:
q = self.norm_q(q)
k = self.norm_k(k)
if x_freqs is not None:
x_cos, x_sin = x_freqs
q_view = rearrange(q, "b l (h d) -> b l h d", d=self.head_dim)
x_cos = x_cos.to(q_view.dtype).to(q_view.device).squeeze(0)
x_sin = x_sin.to(q_view.dtype).to(q_view.device).squeeze(0)
# FlashInfer expects cos_sin_cache with shape [seqlen, head_dim],
# where the first half is cos and the second half is sin, each with
# head_dim//2 elements. Since compute_rope_cos_sin duplicates the
# frequencies (cat((freqs, freqs))), we only take the first half.
half_dim = self.head_dim // 2
cos_sin_cache = torch.cat(
[
x_cos[:, :half_dim].to(dtype=torch.float32).contiguous(),
x_sin[:, :half_dim].to(dtype=torch.float32).contiguous(),
],
dim=-1,
)
q_view, _ = apply_flashinfer_rope_qk_inplace(
q_view, q_view.clone(), cos_sin_cache, is_neox=True
)
q = rearrange(q_view, "b l h d -> b l (h d)")
if y_freqs is not None:
y_cos, y_sin = y_freqs
k_view = rearrange(k, "b l (h d) -> b l h d", d=self.head_dim)
y_cos = y_cos.to(k_view.dtype).to(k_view.device).squeeze(0)
y_sin = y_sin.to(k_view.dtype).to(k_view.device).squeeze(0)
# FlashInfer expects cos_sin_cache with shape [seqlen, head_dim],
# where the first half is cos and the second half is sin, each with
# head_dim//2 elements. Since compute_rope_cos_sin duplicates the
# frequencies (cat((freqs, freqs))), we only take the first half.
half_dim = self.head_dim // 2
cos_sin_cache = torch.cat(
[
y_cos[:, :half_dim].to(dtype=torch.float32).contiguous(),
y_sin[:, :half_dim].to(dtype=torch.float32).contiguous(),
],
dim=-1,
)
k_view, _ = apply_flashinfer_rope_qk_inplace(
k_view, k_view.clone(), cos_sin_cache, is_neox=True
)
k = rearrange(k_view, "b l h d -> b l (h d)")
q = rearrange(q, "b l (h d) -> b l h d", h=self.num_heads_per_rank)
k = rearrange(k, "b l (h d) -> b l h d", h=self.num_heads_per_rank)
v = rearrange(v, "b l (h d) -> b l h d", h=self.num_heads_per_rank)
x = self.attn(q, k, v)
x = rearrange(x, "b l h d -> b l (h d)")
x, _ = self.o(x)
return x
class AdaLayerNorm(nn.Module):
"""
Norm layer modified to incorporate timestep embeddings.
"""
def __init__(
self,
embedding_dim: int,
num_embeddings: Optional[int] = None,
output_dim: Optional[int] = None,
norm_elementwise_affine: bool = False,
norm_eps: float = 1e-5,
chunk_dim: int = 0,
):
super().__init__()
self.chunk_dim = chunk_dim
output_dim = output_dim or embedding_dim * 2
if num_embeddings is not None:
self.emb = nn.Embedding(num_embeddings, embedding_dim)
else:
self.emb = None
self.silu = nn.SiLU()
self.linear = ReplicatedLinear(embedding_dim, output_dim)
self.norm = nn.LayerNorm(output_dim // 2, norm_eps, norm_elementwise_affine)
def forward(
self,
x: torch.Tensor,
timestep: Optional[torch.Tensor] = None,
temb: Optional[torch.Tensor] = None,
) -> torch.Tensor:
if self.emb is not None:
temb = self.emb(timestep)
temb, _ = self.linear(self.silu(temb))
if self.chunk_dim == 2:
scale, shift = temb.chunk(2, dim=2)
elif self.chunk_dim == 1:
shift, scale = temb.chunk(2, dim=1)
shift = shift[:, None, :]
scale = scale[:, None, :]
else:
scale, shift = temb.chunk(2, dim=0)
x = self.norm(x) * (1 + scale) + shift
return x
class ConditionalCrossAttentionBlock(nn.Module):
"""A wrapper block for ConditionalCrossAttention that applies LayerNorm to the condition input y."""
def __init__(
self,
dim: int,
kv_dim: int,
num_heads: int,
eps: float = 1e-6,
pooled_adaln: bool = False,
):
super().__init__()
self.y_norm = nn.LayerNorm(kv_dim, eps=eps)
self.inner = ConditionalCrossAttention(
dim=dim, kv_dim=kv_dim, num_heads=num_heads, eps=eps
)
self.pooled_adaln = pooled_adaln
if pooled_adaln:
self.per_frame_pooling = PerFrameAttentionPooling(
kv_dim, num_heads=num_heads, eps=eps
)
self.adaln = AdaLayerNorm(kv_dim, output_dim=dim * 2, chunk_dim=2)
def forward(
self,
x: torch.Tensor,
y: torch.Tensor,
x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
video_grid_size: Optional[Tuple[int, int, int]] = None,
) -> torch.Tensor:
if self.pooled_adaln:
assert video_grid_size is not None, "video_grid_size cannot be None"
pooled_y = self.per_frame_pooling(y, video_grid_size)
if pooled_y.shape[1] != x.shape[1]:
pooled_y = F.interpolate(
pooled_y.permute(0, 2, 1),
size=x.shape[1],
mode="linear",
align_corners=False,
).permute(0, 2, 1)
x = self.adaln(x, temb=pooled_y)
y = self.y_norm(y)
return self.inner(x=x, y=y, x_freqs=x_freqs, y_freqs=y_freqs)
class DualTowerConditionalBridge(
CachableDiT,
LayerwiseOffloadableModuleMixin,
):
"""Dual-tower conditional bridge module v2 (SGLang optimized version).
Implements the correct architecture:
1. Audio latents -> Audio DiT -> Audio hidden states [B, L, 1536].
2. Visual latents -> Visual DiT -> Visual hidden states [B, L, 5120].
3. Cross-attention interaction between the hidden states of the two DiTs.
"""
layerwise_offload_dit_group_enabled = False
_fsdp_shard_conditions = MOVADualTowerConfig()._fsdp_shard_conditions
_compile_conditions = MOVADualTowerConfig()._compile_conditions
_supported_attention_backends = MOVADualTowerConfig()._supported_attention_backends
param_names_mapping = MOVADualTowerConfig().param_names_mapping
reverse_param_names_mapping = MOVADualTowerConfig().reverse_param_names_mapping
lora_param_names_mapping = MOVADualTowerConfig().lora_param_names_mapping
def __init__(
self,
config: MOVADualTowerConfig | None = None,
hf_config: dict[str, Any] | None = None,
# Fallback parameters for from_pretrained compatibility
visual_layers: int = 40,
audio_layers: int = 30,
visual_hidden_dim: int = 5120,
audio_hidden_dim: int = 1536,
audio_fps: float = 50.0,
head_dim: int = 128,
interaction_strategy: str = "full",
apply_cross_rope: bool = True,
apply_first_frame_bias_in_rope: bool = False,
trainable_condition_scale: bool = False,
pooled_adaln: bool = False,
):
super().__init__(config=config, hf_config=hf_config)
# Use config if provided, otherwise use individual parameters
if config is not None:
visual_layers = config.visual_layers
audio_layers = config.audio_layers
visual_hidden_dim = config.visual_hidden_dim
audio_hidden_dim = config.audio_hidden_dim
audio_fps = config.audio_fps
head_dim = config.head_dim
interaction_strategy = config.interaction_strategy
apply_cross_rope = config.apply_cross_rope
apply_first_frame_bias_in_rope = config.apply_first_frame_bias_in_rope
trainable_condition_scale = config.trainable_condition_scale
pooled_adaln = config.pooled_adaln
self.visual_hidden_dim = visual_hidden_dim
self.audio_hidden_dim = audio_hidden_dim
self.audio_fps = audio_fps
self.head_dim = head_dim
self.apply_cross_rope = apply_cross_rope
self.apply_first_frame_bias_in_rope = apply_first_frame_bias_in_rope
self.trainable_condition_scale = trainable_condition_scale
self.pooled_adaln = pooled_adaln
if self.trainable_condition_scale:
self.condition_scale = nn.Parameter(
torch.tensor([1.0], dtype=torch.float32)
)
else:
self.condition_scale = 1.0
self.controller = CrossModalInteractionController(visual_layers, audio_layers)
self.interaction_mapping = self.controller.get_interaction_layers(
interaction_strategy
)
# Cross-modal attention modules - interaction at DiT hidden states level
self.audio_to_video_conditioners = nn.ModuleDict()
self.video_to_audio_conditioners = nn.ModuleDict()
self.rope_base = 10000.0 # RoPE base frequency hardcode. adapted from original mova implementation.
# Audio DiT hidden states conditioning Video DiT
for v_layer, _ in self.interaction_mapping["a2v"]:
self.audio_to_video_conditioners[str(v_layer)] = (
ConditionalCrossAttentionBlock(
dim=visual_hidden_dim,
kv_dim=audio_hidden_dim,
num_heads=visual_hidden_dim // head_dim,
pooled_adaln=False,
)
)
# Visual DiT hidden states conditioning Audio DiT
for a_layer, _ in self.interaction_mapping["v2a"]:
self.video_to_audio_conditioners[str(a_layer)] = (
ConditionalCrossAttentionBlock(
dim=audio_hidden_dim,
kv_dim=visual_hidden_dim,
num_heads=audio_hidden_dim // head_dim,
pooled_adaln=self.pooled_adaln,
)
)
# Required attributes for CachableDiT/BaseDiT
self.hidden_size = visual_hidden_dim
self.num_attention_heads = visual_hidden_dim // head_dim
self.num_channels_latents = (
visual_hidden_dim # Bridge doesn't output latents, but required by BaseDiT
)
self.layer_names = [
"audio_to_video_conditioners",
"video_to_audio_conditioners",
]
self.__post_init__()
@torch.no_grad()
def build_aligned_freqs(
self,
video_fps: float,
grid_size: Tuple[int, int, int],
audio_steps: int,
device: Optional[torch.device] = None,
dtype: Optional[torch.dtype] = None,
) -> Tuple[Tuple[torch.Tensor, torch.Tensor], Tuple[torch.Tensor, torch.Tensor]]:
"""Generates aligned RoPE (cos, sin) based on video FPS, grid size, and audio length.
Uses functional RoPE computation to avoid FSDP meta device issues.
Args:
video_fps: FPS of the video.
grid_size: Tuple of (f_v, h, w).
audio_steps: Length of the audio sequence.
device: Target device.
dtype: Output dtype.
Returns:
A tuple of ((cos_v, sin_v), (cos_a, sin_a)).
"""
f_v, h, w = grid_size
L_a = int(audio_steps)
device = device or next(self.parameters()).device
dtype = dtype or torch.float32
# Audio positions: 0, 1, 2, ..., L_a-1
audio_pos = torch.arange(L_a, device=device, dtype=torch.float32).unsqueeze(0)
# Video positions: Align video frames to audio step units
if self.apply_first_frame_bias_in_rope:
video_effective_fps = float(video_fps) / 4.0
if f_v > 0:
t_starts = torch.zeros((f_v,), device=device, dtype=torch.float32)
if f_v > 1:
t_starts[1:] = (1.0 / float(video_fps)) + torch.arange(
f_v - 1, device=device, dtype=torch.float32
) * (1.0 / video_effective_fps)
else:
t_starts = torch.zeros((0,), device=device, dtype=torch.float32)
video_pos_per_frame = t_starts * float(self.audio_fps)
else:
scale = float(self.audio_fps) / float(video_fps / 4.0)
video_pos_per_frame = (
torch.arange(f_v, device=device, dtype=torch.float32) * scale
)
video_pos = video_pos_per_frame.repeat_interleave(h * w).unsqueeze(0)
# Use functional RoPE to compute cos/sin
cos_v, sin_v = compute_rope_cos_sin(
video_pos, self.head_dim, base=self.rope_base, device=device, dtype=dtype
)
cos_a, sin_a = compute_rope_cos_sin(
audio_pos, self.head_dim, base=self.rope_base, device=device, dtype=dtype
)
return (cos_v, sin_v), (cos_a, sin_a)
def should_interact(self, layer_idx: int, direction: str) -> bool:
return self.controller.should_interact(
layer_idx, direction, self.interaction_mapping
)
def apply_conditional_control(
self,
layer_idx: int,
direction: str,
primary_hidden_states: torch.Tensor,
condition_hidden_states: torch.Tensor,
x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
condition_scale: Optional[float] = None,
video_grid_size: Optional[Tuple[int, int, int]] = None,
) -> torch.Tensor:
"""Applies conditional control at the DiT hidden states level."""
if not self.controller.should_interact(
layer_idx, direction, self.interaction_mapping
):
return primary_hidden_states
if direction == "a2v":
conditioner = self.audio_to_video_conditioners[str(layer_idx)]
elif direction == "v2a":
conditioner = self.video_to_audio_conditioners[str(layer_idx)]
else:
raise ValueError(f"Invalid direction: {direction}")
conditioned_features = conditioner(
x=primary_hidden_states,
y=condition_hidden_states,
x_freqs=x_freqs,
y_freqs=y_freqs,
video_grid_size=video_grid_size,
)
if self.trainable_condition_scale and condition_scale is not None:
logger.warning(
"The current model has a trainable condition_scale, but condition_scale "
"was passed externally. Ignoring the trainable condition_scale and "
"using the external condition_scale=%s.",
condition_scale,
)
scale = condition_scale if condition_scale is not None else self.condition_scale
primary_hidden_states = primary_hidden_states + conditioned_features * scale
return primary_hidden_states
def forward(
self,
layer_idx: int,
visual_hidden_states: torch.Tensor,
audio_hidden_states: torch.Tensor,
*,
x_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
y_freqs: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
a2v_condition_scale: Optional[float] = None,
v2a_condition_scale: Optional[float] = None,
condition_scale: Optional[float] = None,
video_grid_size: Optional[Tuple[int, int, int]] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Performs bidirectional conditional control for both visual and audio towers."""
visual_conditioned = self.apply_conditional_control(
layer_idx=layer_idx,
direction="a2v",
primary_hidden_states=visual_hidden_states,
condition_hidden_states=audio_hidden_states,
x_freqs=x_freqs,
y_freqs=y_freqs,
condition_scale=(
a2v_condition_scale
if a2v_condition_scale is not None
else condition_scale
),
video_grid_size=video_grid_size,
)
audio_conditioned = self.apply_conditional_control(
layer_idx=layer_idx,
direction="v2a",
primary_hidden_states=audio_hidden_states,
condition_hidden_states=visual_hidden_states,
x_freqs=y_freqs,
y_freqs=x_freqs,
condition_scale=(
v2a_condition_scale
if v2a_condition_scale is not None
else condition_scale
),
video_grid_size=video_grid_size,
)
return visual_conditioned, audio_conditioned
EntryClass = DualTowerConditionalBridge
@@ -0,0 +1,127 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from abc import ABC, abstractmethod
from typing import Any
import torch
from torch import nn
from sglang.multimodal_gen.configs.models import DiTConfig
# NOTE: TeaCacheContext and TeaCacheMixin have been moved to
# sglang.multimodal_gen.runtime.cache.teacache
# For backwards compatibility, re-export from the new location
from sglang.multimodal_gen.runtime.cache.teacache import TeaCacheContext # noqa: F401
from sglang.multimodal_gen.runtime.cache.teacache import TeaCacheMixin
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
# TODO
class BaseDiT(nn.Module, ABC):
_fsdp_shard_conditions: list = []
_compile_conditions: list = []
param_names_mapping: dict
reverse_param_names_mapping: dict
hidden_size: int
num_attention_heads: int
num_channels_latents: int
# always supports torch_sdpa
_supported_attention_backends: set[AttentionBackendEnum] = (
DiTConfig()._supported_attention_backends
)
def __init_subclass__(cls) -> None:
required_class_attrs = [
"_fsdp_shard_conditions",
"param_names_mapping",
"_compile_conditions",
]
super().__init_subclass__()
for attr in required_class_attrs:
if not hasattr(cls, attr):
raise AttributeError(
f"Subclasses of BaseDiT must define '{attr}' class variable"
)
def __init__(self, config: DiTConfig, hf_config: dict[str, Any], **kwargs) -> None:
super().__init__()
self.config = config
self.hf_config = hf_config
if not self.supported_attention_backends:
raise ValueError(
f"Subclass {self.__class__.__name__} must define _supported_attention_backends"
)
@abstractmethod
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
guidance=None,
**kwargs,
) -> torch.Tensor:
pass
def __post_init__(self) -> None:
required_attrs = ["hidden_size", "num_attention_heads", "num_channels_latents"]
for attr in required_attrs:
if not hasattr(self, attr):
raise AttributeError(
f"Subclasses of BaseDiT must define '{attr}' instance variable"
)
def post_load_weights(self) -> None:
"""Run model-specific post-load weight fixups after all parameters are materialized."""
return None
@property
def supported_attention_backends(self) -> set[AttentionBackendEnum]:
return self._supported_attention_backends
@property
def device(self) -> torch.device:
"""Get the device of the model."""
return next(self.parameters()).device
class CachableDiT(TeaCacheMixin, BaseDiT):
"""
An intermediate base class that adds TeaCache optimization functionality to DiT models.
Inherits TeaCacheMixin for cache logic and BaseDiT for core DiT functionality.
"""
# These are required class attributes that should be overridden by concrete implementations
_fsdp_shard_conditions = []
param_names_mapping = {}
reverse_param_names_mapping = {}
lora_param_names_mapping: dict = {}
# Ensure these instance attributes are properly defined in subclasses
hidden_size: int
num_attention_heads: int
num_channels_latents: int
# always supports torch_sdpa
_supported_attention_backends: set[AttentionBackendEnum] = (
DiTConfig()._supported_attention_backends
)
def __init__(self, config: DiTConfig, **kwargs) -> None:
super().__init__(config, **kwargs)
self._init_teacache_state()
@classmethod
def get_nunchaku_quant_rules(cls) -> dict[str, dict[str, Any]]:
"""
Get quantization rules for Nunchaku quantization.
Returns a dict mapping layer name patterns to quantization configs:
{
"skip": [list of patterns to skip quantization],
"svdq_w4a4": [list of patterns for SVDQ W4A4],
"awq_w4a16": [list of patterns for AWQ W4A16],
}
"""
return {}
@@ -0,0 +1,751 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import math
from typing import Any
import torch
import torch.nn as nn
from torch.nn.attention.flex_attention import (
BlockMask,
create_block_mask,
flex_attention,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
# wan 1.3B model has a weird channel / head configurations and require max-autotune to work with flexattention
# see https://github.com/pytorch/pytorch/issues/133254
# change to default for other models
flex_attention = torch.compile(
flex_attention, dynamic=False, mode="max-autotune-no-cudagraphs"
)
import torch.distributed as dist
from sglang.multimodal_gen.configs.models.dits import WanVideoConfig
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_sp_world_size,
get_tp_rank,
get_tp_world_size,
)
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.elementwise import MulAdd
from sglang.multimodal_gen.runtime.layers.kvcache.causal_attention_cache import (
CausalSelfAttentionKVCache,
CrossAttentionKVCache,
)
from sglang.multimodal_gen.runtime.layers.layernorm import (
FP32LayerNorm,
LayerNormScaleShift,
RMSNorm,
ScaleResidualLayerNormScaleShift,
tensor_parallel_rms_norm,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
_apply_rotary_emb,
get_rotary_pos_embed,
)
from sglang.multimodal_gen.runtime.layers.visual_embedding import PatchEmbed
from sglang.multimodal_gen.runtime.models.dits.base import BaseDiT
from sglang.multimodal_gen.runtime.models.dits.wanvideo import (
WanT2VCrossAttention,
WanTimeTextImageEmbedding,
)
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.srt.utils import add_prefix
logger = init_logger(__name__)
class CausalWanSelfAttention(nn.Module):
def __init__(
self,
dim: int,
num_heads: int,
local_attn_size: int = -1,
sink_size: int = 0,
qk_norm=True,
eps=1e-6,
parallel_attention=False,
head_dim: int | None = None,
head_start: int = 0,
) -> None:
if head_dim is None:
assert dim % num_heads == 0
head_dim = dim // num_heads
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = head_dim
self.head_start = head_start
self.local_attn_size = local_attn_size
self.sink_size = sink_size
self.qk_norm = qk_norm
self.eps = eps
self.parallel_attention = parallel_attention
# Scaled dot product attention
self.attn = LocalAttention(
num_heads=num_heads,
head_size=self.head_dim,
dropout_rate=0,
softmax_scale=None,
causal=False,
supported_attention_backends=(
AttentionBackendEnum.FA,
AttentionBackendEnum.AITER,
AttentionBackendEnum.TORCH_SDPA,
),
)
def forward(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
freqs_cis: tuple[torch.Tensor, torch.Tensor],
block_mask: BlockMask,
kv_cache: CausalSelfAttentionKVCache | None = None,
current_start: int = 0,
cache_start: int | None = None,
):
r"""
Args:
x(Tensor): Shape [B, L, num_heads, C / num_heads]
seq_lens(Tensor): Shape [B]
grid_sizes(Tensor): Shape [B, 3], the second dimension contains (F, H, W)
freqs(Tensor): Rope freqs, shape [1024, C / num_heads / 2]
"""
cos, sin = freqs_cis
roped_query = _apply_rotary_emb(q, cos, sin, is_neox_style=False).type_as(v)
roped_key = _apply_rotary_emb(k, cos, sin, is_neox_style=False).type_as(v)
if kv_cache is None:
# Padding for flex attention
padded_length = math.ceil(q.shape[1] / 128) * 128 - q.shape[1]
padded_roped_query = torch.cat(
[
roped_query,
torch.zeros(
[q.shape[0], padded_length, q.shape[2], q.shape[3]],
device=q.device,
dtype=v.dtype,
),
],
dim=1,
)
padded_roped_key = torch.cat(
[
roped_key,
torch.zeros(
[k.shape[0], padded_length, k.shape[2], k.shape[3]],
device=k.device,
dtype=v.dtype,
),
],
dim=1,
)
padded_v = torch.cat(
[
v,
torch.zeros(
[v.shape[0], padded_length, v.shape[2], v.shape[3]],
device=v.device,
dtype=v.dtype,
),
],
dim=1,
)
x = flex_attention(
query=padded_roped_query.transpose(2, 1),
key=padded_roped_key.transpose(2, 1),
value=padded_v.transpose(2, 1),
block_mask=block_mask,
)[:, :, :-padded_length].transpose(2, 1)
else:
if kv_cache.can_direct_current_attention(roped_key.shape[1]):
return self.attn(roped_query, roped_key, v)
cache_view = kv_cache.update_and_get_attention_kv(
key=roped_key,
value=v,
current_chunk_start=current_start,
cache_head_start=self.head_start,
debug_name="CausalWan KV cache",
)
x = self.attn(
roped_query,
cache_view.k,
cache_view.v,
)
return x
class CausalWanTransformerBlock(nn.Module):
def __init__(
self,
dim: int,
ffn_dim: int,
num_heads: int,
local_attn_size: int = -1,
sink_size: int = 0,
qk_norm: str = "rms_norm_across_heads",
cross_attn_norm: bool = False,
eps: float = 1e-6,
added_kv_proj_dim: int | None = None,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "",
quant_config: QuantizationConfig | None = None,
):
super().__init__()
# 1. Self-attention
self.norm1 = FP32LayerNorm(dim, eps, elementwise_affine=False)
use_megatron_tp = getattr(
self, "_use_megatron_tp", type(self) is CausalWanTransformerBlock
)
if use_megatron_tp:
self.to_q = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=add_prefix("to_q", prefix),
)
self.to_k = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=add_prefix("to_k", prefix),
)
self.to_v = ColumnParallelLinear(
dim,
dim,
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=add_prefix("to_v", prefix),
)
self.to_out = RowParallelLinear(
dim,
dim,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=add_prefix("to_out", prefix),
)
# megatron-style tp shards the weight (qkv) column-wise, effectively splitting the attention heads
tp_size = get_tp_world_size()
self.local_num_heads = divide(num_heads, tp_size)
head_start = get_tp_rank() * self.local_num_heads
else:
self.to_q = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config)
self.to_k = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config)
self.to_v = ReplicatedLinear(dim, dim, bias=True, quant_config=quant_config)
self.to_out = ReplicatedLinear(
dim, dim, bias=True, quant_config=quant_config
)
tp_size = 1
self.local_num_heads = num_heads
head_start = 0
dim_head = dim // num_heads
self.attn1 = CausalWanSelfAttention(
dim,
self.local_num_heads,
local_attn_size=local_attn_size,
sink_size=sink_size,
qk_norm=qk_norm,
eps=eps,
head_dim=dim_head,
head_start=head_start,
)
self.hidden_dim = dim
self.num_attention_heads = num_heads
self.local_attn_size = local_attn_size
self.dim_head = dim_head
if qk_norm == "rms_norm":
self.norm_q = RMSNorm(dim_head, eps=eps)
self.norm_k = RMSNorm(dim_head, eps=eps)
elif qk_norm == "rms_norm_across_heads":
# LTX applies qk norm across all heads
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
else:
print("QK Norm type not supported")
raise Exception
self.tp_rmsnorm = (
use_megatron_tp and qk_norm == "rms_norm_across_heads" and tp_size > 1
)
assert cross_attn_norm is True
self.self_attn_residual_norm = ScaleResidualLayerNormScaleShift(
dim, eps=eps, elementwise_affine=True, dtype=torch.float32
)
# 2. Cross-attention
# Only T2V for now
cross_attn_backends = {
b for b in supported_attention_backends if not b.is_sparse
}
self.attn2 = WanT2VCrossAttention(
dim,
num_heads,
qk_norm=qk_norm,
eps=eps,
supported_attention_backends=cross_attn_backends,
quant_config=quant_config,
)
self.cross_attn_residual_norm = ScaleResidualLayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
# 3. Feed-forward
self.ffn = MLP(
dim, ffn_dim, act_type="gelu_pytorch_tanh", quant_config=quant_config
)
self.mlp_residual = MulAdd()
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
temb: torch.Tensor,
freqs_cis: tuple[torch.Tensor, torch.Tensor],
block_mask: BlockMask,
kv_cache: CausalSelfAttentionKVCache | None = None,
crossattn_cache: CrossAttentionKVCache | None = None,
current_start: int = 0,
cache_start: int | None = None,
) -> torch.Tensor:
# hidden_states.shape: [batch_size, seq_length, inner_dim]
# temb.shape: [batch_size, num_frames, 6, inner_dim]
if hidden_states.dim() == 4:
hidden_states = hidden_states.squeeze(1)
num_frames = temb.shape[1]
frame_seqlen = hidden_states.shape[1] // num_frames
bs, seq_length, _ = hidden_states.shape
orig_dtype = hidden_states.dtype
# assert orig_dtype != torch.float32
e = self.scale_shift_table + temb.float()
# e.shape: [batch_size, num_frames, 6, inner_dim]
assert e.shape == (bs, num_frames, 6, self.hidden_dim)
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = e.chunk(
6, dim=2
)
# *_msa.shape: [batch_size, num_frames, 1, inner_dim]
assert shift_msa.dtype == torch.float32
# 1. Self-attention
norm_hidden_states = (
(
self.norm1(hidden_states.float()).unflatten(
dim=1, sizes=(num_frames, frame_seqlen)
)
* (1 + scale_msa)
+ shift_msa
)
.flatten(1, 2)
.to(orig_dtype)
)
query, _ = self.to_q(norm_hidden_states)
key, _ = self.to_k(norm_hidden_states)
value, _ = self.to_v(norm_hidden_states)
if self.norm_q is not None:
if self.tp_rmsnorm:
query = tensor_parallel_rms_norm(query, self.norm_q)
else:
query = self.norm_q(query)
if self.norm_k is not None:
if self.tp_rmsnorm:
key = tensor_parallel_rms_norm(key, self.norm_k)
else:
key = self.norm_k(key)
query = query.squeeze(1).unflatten(2, (self.local_num_heads, self.dim_head))
key = key.squeeze(1).unflatten(2, (self.local_num_heads, self.dim_head))
value = value.squeeze(1).unflatten(2, (self.local_num_heads, self.dim_head))
attn_output = self.attn1(
query,
key,
value,
freqs_cis,
block_mask,
kv_cache,
current_start,
cache_start,
)
attn_output = attn_output.flatten(2)
attn_output, _ = self.to_out(attn_output)
attn_output = attn_output.squeeze(1)
null_shift = null_scale = torch.zeros(
(1,), device=hidden_states.device, dtype=hidden_states.dtype
)
norm_hidden_states, hidden_states = self.self_attn_residual_norm(
hidden_states, attn_output, gate_msa, null_shift, null_scale
)
norm_hidden_states, hidden_states = norm_hidden_states.to(
orig_dtype
), hidden_states.to(orig_dtype)
# 2. Cross-attention
attn_output = self.attn2(
norm_hidden_states,
context=encoder_hidden_states,
context_lens=None,
crossattn_cache=crossattn_cache,
)
norm_hidden_states, hidden_states = self.cross_attn_residual_norm(
hidden_states, attn_output, 1, c_shift_msa, c_scale_msa
)
norm_hidden_states, hidden_states = norm_hidden_states.to(
orig_dtype
), hidden_states.to(orig_dtype)
# 3. Feed-forward
ff_output = self.ffn(norm_hidden_states)
hidden_states = self.mlp_residual(ff_output, c_gate_msa, hidden_states)
hidden_states = hidden_states.to(orig_dtype)
return hidden_states
class CausalWanTransformer3DModel(BaseDiT, LayerwiseOffloadableModuleMixin):
_fsdp_shard_conditions = WanVideoConfig()._fsdp_shard_conditions
_compile_conditions = WanVideoConfig()._compile_conditions
_supported_attention_backends = WanVideoConfig()._supported_attention_backends
param_names_mapping = WanVideoConfig().param_names_mapping
reverse_param_names_mapping = WanVideoConfig().reverse_param_names_mapping
lora_param_names_mapping = WanVideoConfig().lora_param_names_mapping
def __init__(
self,
config: WanVideoConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config=config, hf_config=hf_config)
inner_dim = config.num_attention_heads * config.attention_head_dim
self.hidden_size = config.hidden_size
self.num_attention_heads = config.num_attention_heads
self.attention_head_dim = config.attention_head_dim
self.in_channels = config.in_channels
self.out_channels = config.out_channels
self.num_channels_latents = config.num_channels_latents
self.patch_size = config.patch_size
self.text_len = config.text_len
self.local_attn_size = config.local_attn_size
# 1. Patch & position embedding
self.patch_embedding = PatchEmbed(
in_chans=config.in_channels,
embed_dim=inner_dim,
patch_size=config.patch_size,
flatten=False,
)
# 2. Condition embeddings
self.condition_embedder = WanTimeTextImageEmbedding(
dim=inner_dim,
time_freq_dim=config.freq_dim,
text_embed_dim=config.text_dim,
image_embed_dim=config.image_dim,
)
# 3. Transformer blocks
self.blocks = nn.ModuleList(
[
CausalWanTransformerBlock(
inner_dim,
config.ffn_dim,
config.num_attention_heads,
config.local_attn_size,
config.sink_size,
config.qk_norm,
config.cross_attn_norm,
config.eps,
config.added_kv_proj_dim,
self._supported_attention_backends,
prefix=f"{config.prefix}.blocks.{i}",
quant_config=quant_config,
)
for i in range(config.num_layers)
]
)
# 4. Output norm & projection
self.norm_out = LayerNormScaleShift(
inner_dim,
eps=config.eps,
elementwise_affine=False,
dtype=torch.float32,
)
self.proj_out = nn.Linear(
inner_dim, config.out_channels * math.prod(config.patch_size)
)
self.scale_shift_table = nn.Parameter(
torch.randn(1, 2, inner_dim) / inner_dim**0.5
)
self.gradient_checkpointing = False
# Causal-specific
self.block_mask = None
self.num_frame_per_block = config.arch_config.num_frames_per_block
assert self.num_frame_per_block <= 3
self.independent_first_frame = False
self.__post_init__()
self.layer_names = [
"blocks",
]
@staticmethod
def _prepare_blockwise_causal_attn_mask(
device: torch.device | str,
num_frames: int = 21,
frame_seqlen: int = 1560,
num_frame_per_block=1,
local_attn_size=-1,
) -> BlockMask:
"""
we will divide the token sequence into the following format
[1 latent frame] [1 latent frame] ... [1 latent frame]
We use flexattention to construct the attention mask
"""
total_length = num_frames * frame_seqlen
# we do right padding to get to a multiple of 128
padded_length = math.ceil(total_length / 128) * 128 - total_length
ends = torch.zeros(
total_length + padded_length, device=device, dtype=torch.long
)
# Block-wise causal mask will attend to all elements that are before the end of the current chunk
frame_indices = torch.arange(
start=0,
end=total_length,
step=frame_seqlen * num_frame_per_block,
device=device,
)
for tmp in frame_indices:
ends[tmp : tmp + frame_seqlen * num_frame_per_block] = (
tmp + frame_seqlen * num_frame_per_block
)
def attention_mask(b, h, q_idx, kv_idx):
if local_attn_size == -1:
return (kv_idx < ends[q_idx]) | (q_idx == kv_idx)
else:
return (
(kv_idx < ends[q_idx])
& (kv_idx >= (ends[q_idx] - local_attn_size * frame_seqlen))
) | (q_idx == kv_idx)
# return ((kv_idx < total_length) & (q_idx < total_length)) | (q_idx == kv_idx) # bidirectional mask
block_mask = create_block_mask(
attention_mask,
B=None,
H=None,
Q_LEN=total_length + padded_length,
KV_LEN=total_length + padded_length,
_compile=False,
device=device,
)
if not dist.is_initialized() or dist.get_rank() == 0:
print(
f" cache a block wise causal mask with block size of {num_frame_per_block} frames"
)
print(block_mask)
# import imageio
# import numpy as np
# from torch.nn.attention.flex_attention import create_mask
# mask = create_mask(attention_mask, B=None, H=None, Q_LEN=total_length +
# padded_length, KV_LEN=total_length + padded_length, device=device)
# import cv2
# mask = cv2.resize(mask[0, 0].cpu().float().numpy(), (1024, 1024))
# imageio.imwrite("mask_%d.jpg" % (0), np.uint8(255. * mask))
return block_mask
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
kv_cache: list[CausalSelfAttentionKVCache] | None = None,
crossattn_cache: list[CrossAttentionKVCache] | None = None,
current_start: int = 0,
cache_start: int = 0,
start_frame: int = 0,
) -> torch.Tensor:
r"""
Run the diffusion model with kv caching.
See Algorithm 2 of CausVid paper https://arxiv.org/abs/2412.07772 for details.
This function will be run for num_frame times.
Process the latent frames one by one (1560 tokens each)
"""
orig_dtype = hidden_states.dtype
if not isinstance(encoder_hidden_states, torch.Tensor):
encoder_hidden_states = encoder_hidden_states[0]
if (
isinstance(encoder_hidden_states_image, list)
and len(encoder_hidden_states_image) > 0
):
encoder_hidden_states_image = encoder_hidden_states_image[0]
else:
encoder_hidden_states_image = None
batch_size, num_channels, num_frames, height, width = hidden_states.shape
p_t, p_h, p_w = self.patch_size
post_patch_num_frames = num_frames // p_t
post_patch_height = height // p_h
post_patch_width = width // p_w
# Get rotary embeddings
d = self.hidden_size // self.num_attention_heads
rope_dim_list = [d - 4 * (d // 6), 2 * (d // 6), 2 * (d // 6)]
freqs_cos, freqs_sin = get_rotary_pos_embed(
(
post_patch_num_frames * get_sp_world_size(),
post_patch_height,
post_patch_width,
),
self.hidden_size,
self.num_attention_heads,
rope_dim_list,
dtype=(
torch.float64
if current_platform.is_float64_supported()
else torch.float32
),
rope_theta=10000,
start_frame=start_frame, # Assume that start_frame is 0 when kv_cache is None
)
freqs_cos = freqs_cos.to(hidden_states.device)
freqs_sin = freqs_sin.to(hidden_states.device)
freqs_cis = (
(freqs_cos.float(), freqs_sin.float()) if freqs_cos is not None else None
)
hidden_states = self.patch_embedding(hidden_states)
hidden_states = hidden_states.flatten(2).transpose(1, 2)
(
temb,
timestep_proj,
encoder_hidden_states,
encoder_hidden_states_image,
) = self.condition_embedder(
timestep.flatten(), encoder_hidden_states, encoder_hidden_states_image
)
timestep_proj = timestep_proj.unflatten(1, (6, self.hidden_size)).unflatten(
dim=0, sizes=timestep.shape
)
if encoder_hidden_states_image is not None:
encoder_hidden_states = torch.concat(
[encoder_hidden_states_image, encoder_hidden_states], dim=1
)
encoder_hidden_states = (
encoder_hidden_states.to(orig_dtype)
if current_platform.is_mps()
else encoder_hidden_states
) # cast to orig_dtype for MPS
assert encoder_hidden_states.dtype == orig_dtype
# 4. Transformer blocks
for block_index, block in enumerate(self.blocks):
if torch.is_grad_enabled() and self.gradient_checkpointing:
causal_kwargs = {
"kv_cache": kv_cache[block_index],
"current_start": current_start,
"cache_start": cache_start,
"block_mask": self.block_mask,
}
hidden_states = self._gradient_checkpointing_func(
block,
hidden_states,
encoder_hidden_states,
timestep_proj,
freqs_cis,
**causal_kwargs,
)
else:
causal_kwargs = {
"kv_cache": kv_cache[block_index],
"crossattn_cache": crossattn_cache[block_index],
"current_start": current_start,
"cache_start": cache_start,
"block_mask": self.block_mask,
}
hidden_states = block(
hidden_states,
encoder_hidden_states,
timestep_proj,
freqs_cis,
**causal_kwargs,
)
# 5. Output norm, projection & unpatchify
temb = temb.unflatten(dim=0, sizes=timestep.shape).unsqueeze(2)
shift, scale = (self.scale_shift_table.unsqueeze(1) + temb).chunk(2, dim=2)
hidden_states = self.norm_out(hidden_states, shift, scale)
hidden_states = self.proj_out(hidden_states)
hidden_states = hidden_states.reshape(
batch_size,
post_patch_num_frames,
post_patch_height,
post_patch_width,
p_t,
p_h,
p_w,
-1,
)
hidden_states = hidden_states.permute(0, 7, 1, 4, 2, 5, 3, 6)
output = hidden_states.flatten(6, 7).flatten(4, 5).flatten(2, 3)
return output
EntryClass = CausalWanTransformer3DModel
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
import torch
def modulate(
x: torch.Tensor,
shift: torch.Tensor | None = None,
scale: torch.Tensor | None = None,
) -> torch.Tensor:
"""Modulate by shift and scale."""
if scale is None and shift is None:
return x
if shift is None:
return x * (1 + scale.unsqueeze(1)) # type: ignore[union-attr]
if scale is None:
return x + shift.unsqueeze(1) # type: ignore[union-attr]
return x * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,506 @@
# Copyright 2026 Baidu ERNIE-Image Team and The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Optional, Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.embeddings import TimestepEmbedding, Timesteps
from sglang.multimodal_gen.configs.models.dits.ernie_image import (
ErnieImageDitConfig,
)
from sglang.multimodal_gen.runtime.distributed import (
get_tp_world_size,
)
from sglang.multimodal_gen.runtime.layers.attention.layer import (
USPAttention,
build_varlen_mask_meta,
)
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm, apply_qk_norm
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
def _rope(pos: torch.Tensor, dim: int, theta: int) -> torch.Tensor:
assert dim % 2 == 0
scale = torch.arange(0, dim, 2, dtype=torch.float64, device=pos.device) / dim
omega = 1.0 / (theta**scale)
out = torch.einsum("...n,d->...nd", pos, omega) # codespell:ignore nd
return out.float()
class EmbedND3(nn.Module):
"""3D rotary positional embedding for (temporal/batch_idx, height, width)."""
def __init__(self, dim: int, theta: int, axes_dim: Tuple[int, int, int]):
super().__init__()
self.dim = dim
self.theta = theta
self.axes_dim = list(axes_dim)
def forward(self, ids: torch.Tensor) -> torch.Tensor:
emb = torch.cat(
[_rope(ids[..., i], self.axes_dim[i], self.theta) for i in range(3)],
dim=-1,
)
emb = emb.unsqueeze(1).permute(2, 0, 1, 3)
return torch.stack([emb, emb], dim=-1).reshape(*emb.shape[:-1], -1)
class ErnieImageSelfAttention(nn.Module):
"""Self-attention with separate Q/K/V projections and QK LayerNorm.
Module name hierarchy matches diffusers Attention naming convention:
self_attention.to_q, self_attention.to_k, self_attention.to_v,
self_attention.to_out.0, self_attention.norm_q, self_attention.norm_k.
Supports tensor parallelism: Q/K/V projections use ColumnParallelLinear
(output dim sharded by heads), output projection uses RowParallelLinear
(input dim sharded, all-reduce after matmul).
"""
def __init__(
self,
hidden_size: int,
num_heads: int,
head_dim: int,
eps: float = 1e-6,
qk_layernorm: bool = True,
prefix: str = "",
):
super().__init__()
self.num_heads = num_heads
self.head_dim = head_dim
tp_size = get_tp_world_size()
self.num_local_heads = num_heads // tp_size
assert (
num_heads % tp_size == 0
), f"num_heads ({num_heads}) must be divisible by tp_size ({tp_size})"
self.to_q = ColumnParallelLinear(
hidden_size,
hidden_size,
bias=False,
gather_output=False,
prefix=f"{prefix}.to_q",
)
self.to_k = ColumnParallelLinear(
hidden_size,
hidden_size,
bias=False,
gather_output=False,
prefix=f"{prefix}.to_k",
)
self.to_v = ColumnParallelLinear(
hidden_size,
hidden_size,
bias=False,
gather_output=False,
prefix=f"{prefix}.to_v",
)
self.to_out = nn.ModuleList(
[
RowParallelLinear(
hidden_size,
hidden_size,
bias=False,
input_is_parallel=True,
prefix=f"{prefix}.to_out.0",
),
]
)
self.qk_layernorm = qk_layernorm
if qk_layernorm:
self.norm_q = RMSNorm(head_dim, eps=eps)
self.norm_k = RMSNorm(head_dim, eps=eps)
# The joint [image, text] stream is fully replicated, so the ulysses
# all-to-all would wrongly treat it as sharded and duplicate it. Skip
# SP until the stream is sharded (sp_shard + num_replicated_suffix).
self.attn = USPAttention(
num_heads=self.num_local_heads,
head_size=head_dim,
prefix=f"{prefix}.attn",
skip_sequence_parallel=True,
)
def forward(
self,
x: torch.Tensor,
rotary_pos_emb: torch.Tensor,
attn_mask: torch.Tensor | None = None,
attn_mask_meta: dict | None = None,
) -> torch.Tensor:
B, S, H = x.shape
q, _ = self.to_q(x)
k, _ = self.to_k(x)
v, _ = self.to_v(x)
q = q.view(B, S, self.num_local_heads, self.head_dim)
k = k.view(B, S, self.num_local_heads, self.head_dim)
v = v.view(B, S, self.num_local_heads, self.head_dim)
if self.qk_layernorm:
q, k = apply_qk_norm(
q,
k,
self.norm_q,
self.norm_k,
self.head_dim,
)
q = _apply_rotary_bshd(q, rotary_pos_emb)
k = _apply_rotary_bshd(k, rotary_pos_emb)
attn_out = self.attn(
q, k, v, attn_mask=attn_mask, attn_mask_meta=attn_mask_meta
)
attn_out = attn_out.reshape(B, S, self.num_local_heads * self.head_dim)
out, _ = self.to_out[0](attn_out)
return out
class ErnieImageMLP(nn.Module):
def __init__(
self,
hidden_size: int,
ffn_hidden_size: int,
prefix: str = "",
):
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[ffn_hidden_size, ffn_hidden_size],
bias=False,
gather_output=False,
prefix=f"{prefix}.gate_up_proj",
)
self.linear_fc2 = RowParallelLinear(
ffn_hidden_size,
hidden_size,
bias=False,
input_is_parallel=True,
prefix=f"{prefix}.linear_fc2",
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
gate_up, _ = self.gate_up_proj(x)
gate, up = gate_up.chunk(2, dim=-1)
x = up * F.gelu(gate)
x, _ = self.linear_fc2(x)
return x
class ErnieImageSharedAdaLNBlock(nn.Module):
"""Single-stream transformer block with externally-computed Shared AdaLN."""
def __init__(
self,
hidden_size: int,
num_heads: int,
head_dim: int,
ffn_hidden_size: int,
eps: float = 1e-6,
qk_layernorm: bool = True,
prefix: str = "",
):
super().__init__()
self.adaLN_sa_ln = RMSNorm(hidden_size, eps=eps)
self.self_attention = ErnieImageSelfAttention(
hidden_size,
num_heads,
head_dim,
eps,
qk_layernorm,
prefix=f"{prefix}.self_attention",
)
self.adaLN_mlp_ln = RMSNorm(hidden_size, eps=eps)
self.mlp = ErnieImageMLP(hidden_size, ffn_hidden_size, prefix=f"{prefix}.mlp")
def forward(
self,
x: torch.Tensor,
rotary_pos_emb: torch.Tensor,
shift_msa: torch.Tensor,
scale_msa: torch.Tensor,
gate_msa: torch.Tensor,
shift_mlp: torch.Tensor,
scale_mlp: torch.Tensor,
gate_mlp: torch.Tensor,
attn_mask: torch.Tensor | None = None,
attn_mask_meta: dict | None = None,
) -> torch.Tensor:
residual = x
x = self.adaLN_sa_ln(x) * (1 + scale_msa) + shift_msa
x = residual + gate_msa * self.self_attention(
x, rotary_pos_emb, attn_mask=attn_mask, attn_mask_meta=attn_mask_meta
)
residual = x
x = self.adaLN_mlp_ln(x) * (1 + scale_mlp) + shift_mlp
x = residual + gate_mlp * self.mlp(x)
return x
def _apply_rotary_bshd(x: torch.Tensor, freqs: torch.Tensor) -> torch.Tensor:
freqs = freqs.permute(1, 0, 2, 3)
rot_dim = freqs.shape[-1]
x_rot, x_pass = x[..., :rot_dim], x[..., rot_dim:]
cos_ = torch.cos(freqs).to(x.dtype)
sin_ = torch.sin(freqs).to(x.dtype)
x1, x2 = x_rot.chunk(2, dim=-1)
x_rotated = torch.cat((-x2, x1), dim=-1)
x_rot = x_rot * cos_ + x_rotated * sin_
return torch.cat((x_rot, x_pass), dim=-1)
class ErnieImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
"""ErnieImage DiT: Single-stream transformer with Shared AdaLN."""
_supports_gradient_checkpointing = True
_no_split_modules = ["ErnieImageSharedAdaLNBlock"]
_skip_layerwise_casting_patterns = ["pos_embed", "norm"]
_fsdp_shard_conditions = ErnieImageDitConfig().arch_config._fsdp_shard_conditions
_compile_conditions = []
param_names_mapping = ErnieImageDitConfig().arch_config.param_names_mapping
reverse_param_names_mapping = {}
def __init__(
self,
config: ErnieImageDitConfig,
hf_config: dict[str, Any],
quant_config: Optional[QuantizationConfig] = None,
):
super().__init__(config=config, hf_config=hf_config)
arch = config.arch_config
self.hidden_size = arch.hidden_size
self.num_attention_heads = arch.num_attention_heads
self.num_channels_latents = arch.out_channels
self.head_dim = arch.attention_head_dim
self.num_layers = arch.num_layers
self.patch_size = arch.patch_size
self.out_channels = arch.out_channels
self.inner_dim = self.hidden_size
self.x_embedder = nn.ModuleDict(
{
"proj": nn.Conv2d(
arch.in_channels,
self.inner_dim,
kernel_size=arch.patch_size,
stride=arch.patch_size,
bias=True,
),
}
)
if arch.text_in_dim != self.inner_dim:
self.text_proj = nn.Linear(arch.text_in_dim, self.inner_dim, bias=False)
else:
self.text_proj = None
self.time_proj = Timesteps(
self.inner_dim,
flip_sin_to_cos=False,
downscale_freq_shift=0,
)
self.time_embedding = TimestepEmbedding(
in_channels=self.inner_dim,
time_embed_dim=self.inner_dim,
)
self.pos_embed = EmbedND3(
dim=self.head_dim,
theta=arch.rope_theta,
axes_dim=arch.rope_axes_dim,
)
self.adaLN_modulation = nn.Sequential(
nn.SiLU(),
nn.Linear(self.inner_dim, 6 * self.inner_dim),
)
self.layers = nn.ModuleList(
[
ErnieImageSharedAdaLNBlock(
hidden_size=self.inner_dim,
num_heads=self.num_attention_heads,
head_dim=self.head_dim,
ffn_hidden_size=arch.ffn_hidden_size,
eps=arch.eps,
qk_layernorm=arch.qk_layernorm,
prefix=f"layers.{i}",
)
for i in range(self.num_layers)
]
)
self.final_norm = nn.ModuleDict(
{
"norm": nn.LayerNorm(
self.inner_dim, elementwise_affine=False, eps=arch.eps
),
"linear": nn.Linear(self.inner_dim, self.inner_dim * 2),
}
)
self.final_linear = ColumnParallelLinear(
self.inner_dim,
arch.patch_size * arch.patch_size * self.out_channels,
bias=True,
gather_output=True,
prefix="final_linear",
)
self.layer_names = ["layers"]
self.__post_init__()
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
guidance=None,
encoder_hidden_states_mask: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor:
"""
Args:
hidden_states: [B, C, H, W] latent images (patchified, 128 channels)
encoder_hidden_states: [B, T, text_dim] or list of text embeddings
timestep: [B] timestep values
Returns:
output: [B, C, H, W] predicted noise / denoised output
"""
device, dtype = hidden_states.device, hidden_states.dtype
B, C, H, W = hidden_states.shape
p = self.patch_size
Hp, Wp = H // p, W // p
N_img = Hp * Wp
img_tokens = self.x_embedder["proj"](hidden_states) # [B, D, Hp, Wp]
img_tokens = img_tokens.reshape(B, self.inner_dim, N_img).transpose(
1, 2
) # [B, N_img, D]
if isinstance(encoder_hidden_states, (list, tuple)):
encoder_hidden_states = encoder_hidden_states[0]
text_tokens = encoder_hidden_states # [B, T, text_dim]
if self.text_proj is not None and text_tokens.numel() > 0:
text_tokens = self.text_proj(text_tokens)
Tmax = text_tokens.shape[1]
x = torch.cat([img_tokens, text_tokens], dim=1) # [B, S, D]
grid_yx = torch.stack(
torch.meshgrid(
torch.arange(Hp, device=device, dtype=torch.float32),
torch.arange(Wp, device=device, dtype=torch.float32),
indexing="ij",
),
dim=-1,
).reshape(-1, 2)
image_ids = torch.cat(
[
torch.full((B, N_img, 1), Tmax, device=device, dtype=torch.float32),
grid_yx.view(1, N_img, 2).expand(B, -1, -1),
],
dim=-1,
)
if Tmax > 0:
text_ids = torch.cat(
[
torch.arange(Tmax, device=device, dtype=torch.float32)
.view(1, Tmax, 1)
.expand(B, -1, -1),
torch.zeros((B, Tmax, 2), device=device),
],
dim=-1,
)
else:
text_ids = torch.zeros((B, 0, 3), device=device)
all_ids = torch.cat([image_ids, text_ids], dim=1)
rotary_pos_emb = self.pos_embed(all_ids)
attn_mask = attn_mask_meta = None
if encoder_hidden_states_mask is not None:
image_mask = torch.ones((B, N_img), dtype=torch.bool, device=device)
attn_mask = torch.cat(
[
image_mask,
encoder_hidden_states_mask.to(device=device, dtype=torch.bool),
],
dim=1,
)
attn_mask_meta = build_varlen_mask_meta(attn_mask)
t_emb = self.time_proj(timestep.to(dtype))
c = self.time_embedding(t_emb.to(dtype=dtype))
mod_params = self.adaLN_modulation(c)
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
t.unsqueeze(1) for t in mod_params.chunk(6, dim=-1)
)
for layer in self.layers:
x = layer(
x,
rotary_pos_emb,
shift_msa,
scale_msa,
gate_msa,
shift_mlp,
scale_mlp,
gate_mlp,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
)
scale, shift = self.final_norm["linear"](c).chunk(2, dim=-1)
x = self.final_norm["norm"](x) * (1 + scale.unsqueeze(1)) + shift.unsqueeze(1)
patches, _ = self.final_linear(x[:, :N_img, :])
output = patches.view(B, Hp, Wp, p, p, self.out_channels)
output = output.permute(0, 5, 1, 3, 2, 4).contiguous()
output = output.view(B, self.out_channels, H, W)
return output
EntryClass = ErnieImageTransformer2DModel
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,976 @@
# Copyright 2025 The CogView team, Tsinghua University & ZhipuAI and The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from typing import Any, Dict, List, Optional, Tuple, Union
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.configs.models.dits.glmimage import GlmImageDitConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_sp_parallel_rank,
get_sp_world_size,
get_tp_world_size,
)
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import (
ScaleResidualLayerNormScaleShift,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.mlp import FeedForward
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
_apply_rotary_emb,
apply_flashinfer_rope_qk_inplace,
)
from sglang.multimodal_gen.runtime.layers.visual_embedding import Timesteps
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_is_cuda = current_platform.is_cuda()
class GlmImageLayerKVCache:
"""KV cache for GlmImage model."""
def __init__(self):
self.k_cache = None
self.v_cache = None
self.mode: Optional[str] = None # "write", "read", "skip"
def store(self, k: torch.Tensor, v: torch.Tensor):
if self.k_cache is None:
self.k_cache = k
self.v_cache = v
else:
self.k_cache = torch.cat([self.k_cache, k], dim=1)
self.v_cache = torch.cat([self.v_cache, v], dim=1)
def get(self):
return self.k_cache, self.v_cache
def clear(self):
self.k_cache = None
self.v_cache = None
self.mode = None
class GlmImageKVCache:
"""Container for all layers' KV caches."""
def __init__(self, num_layers: int):
self.num_layers = num_layers
self.caches = [GlmImageLayerKVCache() for _ in range(num_layers)]
def __getitem__(self, layer_idx: int) -> GlmImageLayerKVCache:
return self.caches[layer_idx]
def set_mode(self, mode: Optional[str]):
if mode is not None and mode not in ["write", "read", "skip"]:
raise ValueError(
f"Invalid mode: {mode}, must be one of 'write', 'read', 'skip'"
)
for cache in self.caches:
cache.mode = mode
def clear(self):
for cache in self.caches:
cache.clear()
class GlmImageTimestepEmbedding(nn.Module):
"""
Replacement for diffusers TimestepEmbedding using ReplicatedLinear.
Structure: linear_1 -> act(silu) -> linear_2
"""
def __init__(
self,
in_channels: int,
time_embed_dim: int,
act_fn: str = "silu",
out_dim: int = None,
):
super().__init__()
if out_dim is None:
out_dim = time_embed_dim
self.linear_1 = ReplicatedLinear(in_channels, time_embed_dim, bias=True)
if act_fn == "silu":
self.act = nn.SiLU()
elif act_fn == "gelu":
self.act = nn.GELU(approximate="tanh")
else:
self.act = nn.SiLU()
self.linear_2 = ReplicatedLinear(time_embed_dim, out_dim, bias=True)
def forward(self, sample: torch.Tensor) -> torch.Tensor:
sample, _ = self.linear_1(sample)
sample = self.act(sample)
sample, _ = self.linear_2(sample)
return sample
class GlmImageTextProjection(nn.Module):
"""
Replacement for diffusers PixArtAlphaTextProjection using ReplicatedLinear.
Structure: linear_1 -> act_1 -> linear_2
"""
def __init__(
self,
in_features: int,
hidden_size: int,
out_features: int = None,
act_fn: str = "silu",
):
super().__init__()
if out_features is None:
out_features = hidden_size
self.linear_1 = ReplicatedLinear(in_features, hidden_size, bias=True)
if act_fn == "silu":
self.act_1 = nn.SiLU()
elif act_fn == "gelu_tanh":
self.act_1 = nn.GELU(approximate="tanh")
else:
self.act_1 = nn.SiLU()
self.linear_2 = ReplicatedLinear(hidden_size, out_features, bias=True)
def forward(self, caption: torch.Tensor) -> torch.Tensor:
hidden_states, _ = self.linear_1(caption)
hidden_states = self.act_1(hidden_states)
hidden_states, _ = self.linear_2(hidden_states)
return hidden_states
class GlmImageCombinedTimestepSizeEmbeddings(nn.Module):
def __init__(
self,
embedding_dim: int,
condition_dim: int,
pooled_projection_dim: int,
timesteps_dim: int = 256,
):
super().__init__()
self.time_proj = Timesteps(
num_channels=timesteps_dim, flip_sin_to_cos=True, downscale_freq_shift=0
)
self.condition_proj = Timesteps(
num_channels=condition_dim, flip_sin_to_cos=True, downscale_freq_shift=0
)
self.timestep_embedder = GlmImageTimestepEmbedding(
in_channels=timesteps_dim, time_embed_dim=embedding_dim
)
self.condition_embedder = GlmImageTextProjection(
pooled_projection_dim, embedding_dim, act_fn="silu"
)
def forward(
self,
timestep: torch.Tensor,
target_size: torch.Tensor,
crop_coords: torch.Tensor,
hidden_dtype: torch.dtype,
) -> torch.Tensor:
timesteps_proj = self.time_proj(timestep)
crop_coords_proj = self.condition_proj(crop_coords.flatten()).view(
crop_coords.size(0), -1
)
target_size_proj = self.condition_proj(target_size.flatten()).view(
target_size.size(0), -1
)
# (B, 2 * condition_dim)
condition_proj = torch.cat([crop_coords_proj, target_size_proj], dim=1)
timesteps_emb = self.timestep_embedder(
timesteps_proj.to(dtype=hidden_dtype)
) # (B, embedding_dim)
condition_emb = self.condition_embedder(
condition_proj.to(dtype=hidden_dtype)
) # (B, embedding_dim)
conditioning = timesteps_emb + condition_emb
return conditioning
class GlmImageImageProjector(nn.Module):
def __init__(
self,
in_channels: int = 16,
hidden_size: int = 2560,
patch_size: int = 2,
):
super().__init__()
self.patch_size = patch_size
self.proj = nn.Linear(in_channels * patch_size**2, hidden_size)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
batch_size, channel, height, width = hidden_states.shape
post_patch_height = height // self.patch_size
post_patch_width = width // self.patch_size
hidden_states = hidden_states.reshape(
batch_size,
channel,
post_patch_height,
self.patch_size,
post_patch_width,
self.patch_size,
)
hidden_states = (
hidden_states.permute(0, 2, 4, 1, 3, 5).flatten(3, 5).flatten(1, 2)
)
hidden_states = self.proj(hidden_states)
return hidden_states
class GlmImageAdaLayerNormZero(nn.Module):
def __init__(self, embedding_dim: int, dim: int) -> None:
super().__init__()
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.norm_context = nn.LayerNorm(dim, elementwise_affine=False, eps=1e-5)
self.linear = ReplicatedLinear(embedding_dim, 12 * dim, bias=True)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
temb: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
dtype = hidden_states.dtype
norm_hidden_states = self.norm(hidden_states).to(dtype=dtype)
norm_encoder_hidden_states = self.norm_context(encoder_hidden_states).to(
dtype=dtype
)
emb, _ = self.linear(temb)
(
shift_msa,
c_shift_msa,
scale_msa,
c_scale_msa,
gate_msa,
c_gate_msa,
shift_mlp,
c_shift_mlp,
scale_mlp,
c_scale_mlp,
gate_mlp,
c_gate_mlp,
) = emb.chunk(12, dim=1)
hidden_states = norm_hidden_states * (
1 + scale_msa.unsqueeze(1)
) + shift_msa.unsqueeze(1)
encoder_hidden_states = norm_encoder_hidden_states * (
1 + c_scale_msa.unsqueeze(1)
) + c_shift_msa.unsqueeze(1)
return (
hidden_states,
gate_msa,
shift_mlp,
scale_mlp,
gate_mlp,
encoder_hidden_states,
c_gate_msa,
c_shift_mlp,
c_scale_mlp,
c_gate_mlp,
)
class GlmImageGELU(nn.Module):
def __init__(
self,
dim: int,
inner_dim: int,
bias: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.proj = ColumnParallelLinear(
dim,
inner_dim,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.proj" if prefix else "proj",
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states, _ = self.proj(hidden_states)
return F.gelu(hidden_states, approximate="tanh")
class GlmImageFeedForward(nn.Module):
def __init__(
self,
dim: int,
dim_out: Optional[int] = None,
mult: int = 4,
inner_dim: Optional[int] = None,
bias: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
if inner_dim is None:
inner_dim = int(dim * mult)
dim_out = dim_out if dim_out is not None else dim
self.net = nn.ModuleList(
[
GlmImageGELU(
dim,
inner_dim,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.net.0" if prefix else "net.0",
),
nn.Dropout(0.0),
RowParallelLinear(
inner_dim,
dim_out,
bias=bias,
input_is_parallel=True,
quant_config=quant_config,
prefix=f"{prefix}.net.2" if prefix else "net.2",
),
]
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.net[0](hidden_states)
hidden_states = self.net[1](hidden_states)
hidden_states, _ = self.net[2](hidden_states)
return hidden_states
class GlmImageAttention(torch.nn.Module):
def __init__(
self,
query_dim,
heads,
dim_head,
out_dim,
bias,
qk_norm,
elementwise_affine,
eps,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "",
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.k_cache = None
self.v_cache = None
self.heads = out_dim // dim_head if out_dim is not None else heads
self.dim_head = dim_head
self.inner_dim = out_dim if out_dim is not None else dim_head * heads
self.inner_kv_dim = self.inner_dim
self.out_dim = out_dim if out_dim is not None else query_dim
tp_size = get_tp_world_size()
assert (
self.heads % tp_size == 0
), f"heads ({self.heads}) must be divisible by tp_size ({tp_size})"
self.num_local_heads = self.heads // tp_size
self.num_local_kv_heads = self.num_local_heads
self.to_q = ColumnParallelLinear(
query_dim,
self.inner_dim,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.to_q" if prefix else "to_q",
)
self.to_k = ColumnParallelLinear(
query_dim,
self.inner_kv_dim,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.to_k" if prefix else "to_k",
)
self.to_v = ColumnParallelLinear(
query_dim,
self.inner_kv_dim,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.to_v" if prefix else "to_v",
)
# (dropout omitted)
self.to_out = nn.ModuleList(
[
RowParallelLinear(
self.inner_dim,
self.out_dim,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=f"{prefix}.to_out.0" if prefix else "to_out.0",
)
]
)
if qk_norm is None:
self.norm_q = None
self.norm_k = None
elif qk_norm == "layer_norm":
self.norm_q = nn.LayerNorm(
dim_head, eps=eps, elementwise_affine=elementwise_affine
)
self.norm_k = nn.LayerNorm(
dim_head, eps=eps, elementwise_affine=elementwise_affine
)
else:
raise ValueError(
f"unknown qk_norm: {qk_norm}. Should be one of None, 'layer_norm', 'fp32_layer_norm', 'layer_norm_across_heads', 'rms_norm', 'rms_norm_across_heads', 'l2'."
)
self.attn = USPAttention(
num_heads=self.num_local_heads,
head_size=dim_head,
num_kv_heads=self.num_local_kv_heads,
dropout_rate=0,
softmax_scale=None,
causal=False,
)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
image_rotary_emb: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
kv_cache: Optional[GlmImageLayerKVCache] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
dtype = encoder_hidden_states.dtype
batch_size, text_seq_length, embed_dim = encoder_hidden_states.shape
batch_size, image_seq_length, embed_dim = hidden_states.shape
hidden_states = torch.cat([encoder_hidden_states, hidden_states], dim=1)
# 1. QKV projections
query, _ = self.to_q(hidden_states)
key, _ = self.to_k(hidden_states)
value, _ = self.to_v(hidden_states)
query = query.unflatten(2, (self.num_local_heads, -1))
key = key.unflatten(2, (self.num_local_kv_heads, -1))
value = value.unflatten(2, (self.num_local_kv_heads, -1))
# 2. QK normalization
if self.norm_q is not None:
query = self.norm_q(query).to(dtype=dtype)
if self.norm_k is not None:
key = self.norm_k(key).to(dtype=dtype)
# 3. Rotational positional embeddings applied to latent stream
if image_rotary_emb is not None:
cos, sin = image_rotary_emb
if _is_cuda and cos.dim() == 2:
q_img = query[:, text_seq_length:, :, :]
k_img = key[:, text_seq_length:, :, :]
cos_sin_cache = torch.cat(
[
cos.to(dtype=torch.float32).contiguous(),
sin.to(dtype=torch.float32).contiguous(),
],
dim=-1,
)
# apply_flashinfer_rope_qk_inplace is inplace kernel and q_img/k_img are views of query/key, so we need not copy back
q_out, k_out = apply_flashinfer_rope_qk_inplace(
q_img, k_img, cos_sin_cache, is_neox=True
)
else:
query[:, text_seq_length:, :, :] = _apply_rotary_emb(
query[:, text_seq_length:, :, :], cos, sin, is_neox_style=True
)
key[:, text_seq_length:, :, :] = _apply_rotary_emb(
key[:, text_seq_length:, :, :], cos, sin, is_neox_style=True
)
if kv_cache is not None:
if kv_cache.mode == "write":
kv_cache.store(key, value)
elif kv_cache.mode == "read":
k_cache, v_cache = kv_cache.get()
key = torch.cat([k_cache, key], dim=1) if k_cache is not None else key
value = (
torch.cat([v_cache, value], dim=1) if v_cache is not None else value
)
elif kv_cache.mode == "skip":
pass
# 4. Attention
if attention_mask is not None:
text_attn_mask = attention_mask
assert (
text_attn_mask.dim() == 2
), "the shape of text_attn_mask should be (batch_size, text_seq_length)"
hidden_states = self.attn(
query, key, value, num_replicated_prefix=text_seq_length
)
hidden_states = hidden_states.flatten(2, 3)
hidden_states = hidden_states.to(query.dtype)
# 5. Output projection
hidden_states, _ = self.to_out[0](hidden_states)
# hidden_states = self.to_out[1](hidden_states) # (dropout omitted)
encoder_hidden_states, hidden_states = hidden_states.split(
[text_seq_length, hidden_states.size(1) - text_seq_length], dim=1
)
return hidden_states, encoder_hidden_states
class GlmImageTransformerBlock(nn.Module):
def __init__(
self,
dim: int = 2560,
num_attention_heads: int = 64,
attention_head_dim: int = 40,
time_embed_dim: int = 512,
supported_attention_backends: set[AttentionBackendEnum] | None = None,
prefix: str = "",
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__()
# 1. Attention
self.norm1 = GlmImageAdaLayerNormZero(time_embed_dim, dim)
self.attn1 = GlmImageAttention(
query_dim=dim,
heads=num_attention_heads,
dim_head=attention_head_dim,
out_dim=dim,
bias=True,
qk_norm="layer_norm",
elementwise_affine=False,
eps=1e-5,
supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.attn1",
quant_config=quant_config,
)
# 2. Feedforward
self.norm2 = ScaleResidualLayerNormScaleShift(
dim, eps=1e-5, elementwise_affine=False
)
self.norm2_context = ScaleResidualLayerNormScaleShift(
dim, eps=1e-5, elementwise_affine=False
)
self.ff = GlmImageFeedForward(
dim=dim,
dim_out=dim,
quant_config=quant_config,
prefix=f"{prefix}.ff" if prefix else "ff",
)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
temb: Optional[torch.Tensor] = None,
image_rotary_emb: Optional[
Union[
Tuple[torch.Tensor, torch.Tensor],
List[Tuple[torch.Tensor, torch.Tensor]],
]
] = None,
attention_mask: Optional[Dict[str, torch.Tensor]] = None,
attention_kwargs: Optional[Dict[str, Any]] = None,
kv_cache: Optional[GlmImageLayerKVCache] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
# 1. Timestep conditioning
(
norm_hidden_states,
gate_msa,
shift_mlp,
scale_mlp,
gate_mlp,
norm_encoder_hidden_states,
c_gate_msa,
c_shift_mlp,
c_scale_mlp,
c_gate_mlp,
) = self.norm1(hidden_states, encoder_hidden_states, temb)
# 2. Attention
if attention_kwargs is None:
attention_kwargs = {}
attn_hidden_states, attn_encoder_hidden_states = self.attn1(
hidden_states=norm_hidden_states,
encoder_hidden_states=norm_encoder_hidden_states,
image_rotary_emb=image_rotary_emb,
attention_mask=attention_mask,
kv_cache=kv_cache,
**attention_kwargs,
)
# 3. Feedforward (fused residual + norm + scale/shift)
norm_hidden_states, hidden_states = self.norm2(
hidden_states,
attn_hidden_states,
gate_msa.unsqueeze(1),
shift_mlp.unsqueeze(1),
scale_mlp.unsqueeze(1),
)
norm_encoder_hidden_states, encoder_hidden_states = self.norm2_context(
encoder_hidden_states,
attn_encoder_hidden_states,
c_gate_msa.unsqueeze(1),
c_shift_mlp.unsqueeze(1),
c_scale_mlp.unsqueeze(1),
)
ff_output = self.ff(norm_hidden_states)
ff_output_context = self.ff(norm_encoder_hidden_states)
hidden_states = hidden_states + ff_output * gate_mlp.unsqueeze(1)
encoder_hidden_states = (
encoder_hidden_states + ff_output_context * c_gate_mlp.unsqueeze(1)
)
return hidden_states, encoder_hidden_states
class GlmImageRotaryPosEmbed(nn.Module):
def __init__(self, dim: int, patch_size: int, theta: float = 10000.0) -> None:
super().__init__()
self.dim = dim
self.patch_size = patch_size
self.theta = theta
def forward(self, hidden_states: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
batch_size, num_channels, height, width = hidden_states.shape
height, width = height // self.patch_size, width // self.patch_size
device = hidden_states.device
dim_h, dim_w = self.dim // 2, self.dim // 2
h_inv_freq = 1.0 / (
self.theta
** (
torch.arange(0, dim_h, 2, dtype=torch.float32, device=device)[
: (dim_h // 2)
].float()
/ dim_h
)
)
w_inv_freq = 1.0 / (
self.theta
** (
torch.arange(0, dim_w, 2, dtype=torch.float32, device=device)[
: (dim_w // 2)
].float()
/ dim_w
)
)
h_seq = torch.arange(height, device=device)
w_seq = torch.arange(width, device=device)
freqs_h = torch.outer(h_seq, h_inv_freq)
freqs_w = torch.outer(w_seq, w_inv_freq)
# Create position matrices for height and width
# [height, 1, dim//4] and [1, width, dim//4]
freqs_h = freqs_h.unsqueeze(1)
freqs_w = freqs_w.unsqueeze(0)
# Broadcast freqs_h and freqs_w to [height, width, dim//4]
freqs_h = freqs_h.expand(height, width, -1)
freqs_w = freqs_w.expand(height, width, -1)
# Concatenate along last dimension to get [height, width, dim//2]
freqs = torch.cat([freqs_h, freqs_w], dim=-1)
freqs = freqs.reshape(height * width, -1) # [height * width, dim//2]
return (freqs.cos(), freqs.sin())
class GlmImageAdaLayerNormContinuous(nn.Module):
"""
GlmImage-only final AdaLN: LN(x) -> Linear(cond) -> chunk -> affine. Matches Megatron: **no activation** before the
Linear on conditioning embedding.
"""
def __init__(
self,
embedding_dim: int,
conditioning_embedding_dim: int,
elementwise_affine: bool = True,
eps: float = 1e-5,
bias: bool = True,
norm_type: str = "layer_norm",
):
super().__init__()
self.linear = nn.Linear(
conditioning_embedding_dim, embedding_dim * 2, bias=bias
)
if norm_type == "layer_norm":
self.norm = nn.LayerNorm(embedding_dim, eps, elementwise_affine, bias)
# For now, dont replace this with sglangs LayerNorm
# because the model doesnt have this parameter and it will break model loading
elif norm_type == "rms_norm":
self.norm = nn.RMSNorm(embedding_dim, eps, elementwise_affine)
else:
raise ValueError(f"unknown norm_type {norm_type}")
def forward(
self, x: torch.Tensor, conditioning_embedding: torch.Tensor
) -> torch.Tensor:
# *** NO SiLU here ***
emb = self.linear(conditioning_embedding.to(x.dtype))
scale, shift = torch.chunk(emb, 2, dim=1)
x = self.norm(x) * (1 + scale)[:, None, :] + shift[:, None, :]
return x
class GlmImageTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
r"""
Args:
patch_size (`int`, defaults to `2`):
The size of the patches to use in the patch embedding layer.
in_channels (`int`, defaults to `16`):
The number of channels in the input.
num_layers (`int`, defaults to `30`):
The number of layers of Transformer blocks to use.
attention_head_dim (`int`, defaults to `40`):
The number of channels in each head.
num_attention_heads (`int`, defaults to `64`):
The number of heads to use for multi-head attention.
out_channels (`int`, defaults to `16`):
The number of channels in the output.
text_embed_dim (`int`, defaults to `1472`):
Input dimension of text embeddings from the text encoder.
time_embed_dim (`int`, defaults to `512`):
Output dimension of timestep embeddings.
condition_dim (`int`, defaults to `256`):
The embedding dimension of the input SDXL-style resolution conditions (original_size, target_size,
crop_coords).
pos_embed_max_size (`int`, defaults to `128`):
The maximum resolution of the positional embeddings, from which slices of shape `H x W` are taken and added
to input patched latents, where `H` and `W` are the latent height and width respectively. A value of 128
means that the maximum supported height and width for image generation is `128 * vae_scale_factor *
patch_size => 128 * 8 * 2 => 2048`.
sample_size (`int`, defaults to `128`):
The base resolution of input latents. If height/width is not provided during generation, this value is used
to determine the resolution as `sample_size * vae_scale_factor => 128 * 8 => 1024`
"""
def __init__(
self,
config: GlmImageDitConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
):
super().__init__(config=config, hf_config=hf_config)
self.config_data = config # Store config
arch_config = config.arch_config
self.in_channels = arch_config.in_channels
self.out_channels = arch_config.out_channels
self.patch_size = arch_config.patch_size
self.num_layers = arch_config.num_layers
self.attention_head_dim = arch_config.attention_head_dim
self.num_attention_heads = arch_config.num_attention_heads
self.text_embed_dim = arch_config.text_embed_dim
self.time_embed_dim = arch_config.time_embed_dim
# GlmImage uses 2 additional SDXL-like conditions - target_size, crop_coords
# Each of these are sincos embeddings of shape 2 * condition_dim
pooled_projection_dim = 2 * 2 * arch_config.condition_dim
inner_dim = arch_config.num_attention_heads * arch_config.attention_head_dim
# 1. RoPE
self.rotary_emb = GlmImageRotaryPosEmbed(
arch_config.attention_head_dim, arch_config.patch_size, theta=10000.0
)
# 2. Patch & Text-timestep embedding
self.image_projector = GlmImageImageProjector(
arch_config.in_channels, inner_dim, arch_config.patch_size
)
self.glyph_projector = FeedForward(
arch_config.text_embed_dim,
inner_dim,
inner_dim=inner_dim,
activation_fn="gelu",
)
self.prior_token_embedding = nn.Embedding(
arch_config.prior_vq_quantizer_codebook_size, inner_dim
)
self.prior_projector = FeedForward(
inner_dim, inner_dim, inner_dim=inner_dim, activation_fn="linear-silu"
)
self.time_condition_embed = GlmImageCombinedTimestepSizeEmbeddings(
embedding_dim=arch_config.time_embed_dim,
condition_dim=arch_config.condition_dim,
pooled_projection_dim=pooled_projection_dim,
timesteps_dim=arch_config.time_embed_dim,
)
# 3. Transformer blocks
self._supported_attention_backends = arch_config._supported_attention_backends
self.transformer_blocks = nn.ModuleList(
[
GlmImageTransformerBlock(
inner_dim,
arch_config.num_attention_heads,
arch_config.attention_head_dim,
arch_config.time_embed_dim,
supported_attention_backends=self._supported_attention_backends,
prefix=f"transformer_blocks.{i}",
quant_config=quant_config,
)
for i in range(arch_config.num_layers)
]
)
# 4. Output projection
self.norm_out = GlmImageAdaLayerNormContinuous(
inner_dim, arch_config.time_embed_dim, elementwise_affine=False
)
self.proj_out = nn.Linear(
inner_dim,
arch_config.patch_size * arch_config.patch_size * arch_config.out_channels,
bias=True,
)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
prior_token_id: torch.Tensor,
prior_token_drop: torch.Tensor,
timestep: torch.LongTensor,
target_size: torch.Tensor,
crop_coords: torch.Tensor,
attention_kwargs: Optional[Dict[str, Any]] = None,
attention_mask: Optional[torch.Tensor] = None,
kv_caches: Optional[GlmImageKVCache] = None,
kv_caches_mode: Optional[str] = None,
freqs_cis: Optional[
Union[
Tuple[torch.Tensor, torch.Tensor],
List[Tuple[torch.Tensor, torch.Tensor]],
]
] = None,
###
guidance: torch.Tensor = None,
) -> Tuple[torch.Tensor]:
if kv_caches is not None:
kv_caches.set_mode(kv_caches_mode)
batch_size, num_channels, height, width = hidden_states.shape
timestep = timestep - 1.0
if isinstance(encoder_hidden_states, list):
encoder_hidden_states = encoder_hidden_states[0]
# 1. RoPE
image_rotary_emb = freqs_cis
if image_rotary_emb is None:
image_rotary_emb = self.rotary_emb(hidden_states)
# 2. Patch & Timestep embeddings
p = self.config.patch_size
post_patch_height = height // p
post_patch_width = width // p
hidden_states = self.image_projector(hidden_states)
encoder_hidden_states = self.glyph_projector(encoder_hidden_states)
prior_embedding = self.prior_token_embedding(prior_token_id)
prior_embedding = prior_embedding.masked_fill(prior_token_drop.unsqueeze(-1), 0)
prior_hidden_states = self.prior_projector(prior_embedding)
# SP: when latents are H-sharded, hidden_states has fewer patches than prior_hidden_states.
# Shard prior_hidden_states along seq dim to match (prior is row-major, same as latent patches).
if (
get_sp_world_size() > 1
and prior_hidden_states.shape[1] != hidden_states.shape[1]
):
rank = get_sp_parallel_rank()
sp_world_size = get_sp_world_size()
chunk = prior_hidden_states.shape[1] // sp_world_size
prior_hidden_states = prior_hidden_states[
:, rank * chunk : (rank + 1) * chunk, :
]
hidden_states = hidden_states + prior_hidden_states
temb = self.time_condition_embed(
timestep, target_size, crop_coords, hidden_states.dtype
)
temb = F.silu(temb)
# 3. Transformer blocks
for idx, block in enumerate(self.transformer_blocks):
hidden_states, encoder_hidden_states = block(
hidden_states,
encoder_hidden_states,
temb,
image_rotary_emb,
attention_mask,
attention_kwargs,
kv_cache=kv_caches[idx] if kv_caches is not None else None,
)
# 4. Output norm & projection
hidden_states = self.norm_out(hidden_states, temb)
hidden_states = self.proj_out(hidden_states)
# 5. Unpatchify
hidden_states = hidden_states.reshape(
batch_size, post_patch_height, post_patch_width, -1, p, p
)
output = hidden_states.permute(0, 3, 1, 4, 2, 5).flatten(4, 5).flatten(2, 3)
return output.float()
# float()
# reference: https://github.com/zRzRzRzRzRzRzR/diffusers/blob/6cfc83b4abc5b083fef56a18ec4700f48ba3aaba/src/diffusers/pipelines/glm_image/pipeline_glm_image.py#L737
EntryClass = GlmImageTransformer2DModel
@@ -0,0 +1,974 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from Helios diffusers transformer:
# https://github.com/BestWishYsh/Helios
"""
Helios Transformer 3D model for video generation.
Implements the HeliosTransformer3DModel with multi-term memory patches,
3D rotary position embeddings, and per-block scale-shift modulation.
"""
import math
from functools import lru_cache
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.configs.models.dits.helios import HeliosConfig
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_sp_world_size,
get_tp_world_size,
)
from sglang.multimodal_gen.runtime.distributed.communication_op import (
sequence_model_parallel_all_gather,
)
from sglang.multimodal_gen.runtime.distributed.parallel_state import get_sp_group
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNorm,
LayerNormScaleShift,
RMSNorm,
tensor_parallel_rms_norm,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.visual_embedding import (
ModulateProjection,
PatchEmbed,
TimestepEmbedder,
)
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# ---------------------------------------------------------------------------
# Utility functions
# ---------------------------------------------------------------------------
def pad_for_3d_conv(x, kernel_size):
"""Pad input to make it divisible by kernel_size using replicate mode."""
b, c, t, h, w = x.shape
pt, ph, pw = kernel_size
pad_t = (pt - (t % pt)) % pt
pad_h = (ph - (h % ph)) % ph
pad_w = (pw - (w % pw)) % pw
return F.pad(x, (0, pad_w, 0, pad_h, 0, pad_t), mode="replicate")
def center_down_sample_3d(x, kernel_size):
"""Average pooling for 3D downsampling."""
return F.avg_pool3d(x, kernel_size, stride=kernel_size)
def apply_rotary_emb_transposed(hidden_states, freqs_cis):
"""Apply rotary positional embeddings with transposed cos/sin format."""
x_1, x_2 = hidden_states.unflatten(-1, (-1, 2)).unbind(-1)
cos, sin = freqs_cis.unsqueeze(-2).chunk(2, dim=-1)
out = torch.empty_like(hidden_states)
out[..., 0::2] = x_1 * cos[..., 0::2] - x_2 * sin[..., 1::2]
out[..., 1::2] = x_1 * sin[..., 1::2] + x_2 * cos[..., 0::2]
return out.type_as(hidden_states)
# ---------------------------------------------------------------------------
# Output norm
# ---------------------------------------------------------------------------
class HeliosOutputNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.scale_shift_table = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
self.norm = LayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
def forward(self, hidden_states, temb, original_context_length):
temb = temb[:, -original_context_length:, :]
shift, scale = (
self.scale_shift_table.unsqueeze(0).to(temb.device) + temb.unsqueeze(2)
).chunk(2, dim=2)
shift = shift.squeeze(2).to(hidden_states.device)
scale = scale.squeeze(2).to(hidden_states.device)
hidden_states = hidden_states[:, -original_context_length:, :]
hidden_states = self.norm(hidden_states, shift, scale)
return hidden_states
# ---------------------------------------------------------------------------
# Rotary Positional Embedding (3D)
# ---------------------------------------------------------------------------
class HeliosRotaryPosEmbed(nn.Module):
"""3D rotary position embeddings for (time, height, width)."""
def __init__(self, rope_dim, theta):
super().__init__()
self.DT, self.DY, self.DX = rope_dim
self.theta = theta
# Store as plain attributes (not buffers) to avoid meta-device issues
# during FSDP loading. They'll be re-created on the correct device in forward.
self._freqs_base_t = None
self._freqs_base_y = None
self._freqs_base_x = None
def _get_freqs_base(self, dim):
return 1.0 / (
self.theta
** (torch.arange(0, dim, 2, dtype=torch.float32)[: (dim // 2)] / dim)
)
def _ensure_freqs_base(self, device):
"""Lazily create frequency bases on the correct device."""
if self._freqs_base_t is None or self._freqs_base_t.device != device:
self._freqs_base_t = self._get_freqs_base(self.DT).to(device)
self._freqs_base_y = self._get_freqs_base(self.DY).to(device)
self._freqs_base_x = self._get_freqs_base(self.DX).to(device)
@torch.no_grad()
def get_frequency_batched(self, freqs_base, pos):
freqs = torch.einsum("d,bthw->dbthw", freqs_base, pos)
freqs = freqs.repeat_interleave(2, dim=0)
return freqs.cos(), freqs.sin()
@torch.no_grad()
@lru_cache(maxsize=32)
def _get_spatial_meshgrid(self, height, width, device_str):
device = torch.device(device_str)
grid_y_coords = torch.arange(height, device=device, dtype=torch.float32)
grid_x_coords = torch.arange(width, device=device, dtype=torch.float32)
grid_y, grid_x = torch.meshgrid(grid_y_coords, grid_x_coords, indexing="ij")
return grid_y, grid_x
@torch.no_grad()
def forward(self, frame_indices, height, width, device):
self._ensure_freqs_base(device)
batch_size = frame_indices.shape[0]
num_frames = frame_indices.shape[1]
frame_indices = frame_indices.to(device=device, dtype=torch.float32)
grid_y, grid_x = self._get_spatial_meshgrid(height, width, str(device))
grid_t = frame_indices[:, :, None, None].expand(
batch_size, num_frames, height, width
)
grid_y_batch = grid_y[None, None, :, :].expand(batch_size, num_frames, -1, -1)
grid_x_batch = grid_x[None, None, :, :].expand(batch_size, num_frames, -1, -1)
freqs_cos_t, freqs_sin_t = self.get_frequency_batched(
self._freqs_base_t, grid_t
)
freqs_cos_y, freqs_sin_y = self.get_frequency_batched(
self._freqs_base_y, grid_y_batch
)
freqs_cos_x, freqs_sin_x = self.get_frequency_batched(
self._freqs_base_x, grid_x_batch
)
result = torch.cat(
[
freqs_cos_t,
freqs_cos_y,
freqs_cos_x,
freqs_sin_t,
freqs_sin_y,
freqs_sin_x,
],
dim=0,
)
return result.permute(1, 0, 2, 3, 4)
# ---------------------------------------------------------------------------
# Condition Embedder
# ---------------------------------------------------------------------------
class HeliosTimeTextEmbedding(nn.Module):
"""Condition embedder combining timestep and text embeddings."""
def __init__(self, dim, time_freq_dim, time_proj_dim, text_embed_dim):
super().__init__()
self.time_embedder = TimestepEmbedder(
dim, frequency_embedding_size=time_freq_dim, act_layer="silu"
)
self.time_modulation = ModulateProjection(dim, factor=6, act_layer="silu")
self.text_embedder = MLP(
text_embed_dim, dim, dim, bias=True, act_type="gelu_pytorch_tanh"
)
def forward(
self, timestep, encoder_hidden_states, is_return_encoder_hidden_states=True
):
temb = self.time_embedder(timestep)
timestep_proj = self.time_modulation(temb)
if encoder_hidden_states is not None and is_return_encoder_hidden_states:
encoder_hidden_states = self.text_embedder(encoder_hidden_states)
return temb, timestep_proj, encoder_hidden_states
# ---------------------------------------------------------------------------
# Self-Attention for Helios
# ---------------------------------------------------------------------------
class HeliosSelfAttention(nn.Module):
"""Self-attention with RMSNorm Q/K, optional history key amplification."""
def __init__(
self,
dim: int,
num_heads: int,
eps: float = 1e-6,
is_amplify_history: bool = False,
history_scale_mode: str = "per_head",
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
tp_size = get_tp_world_size()
self.local_num_heads = divide(num_heads, tp_size)
self.to_q = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_k = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_v = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_out = RowParallelLinear(
dim, dim, bias=True, reduce_results=True, quant_config=quant_config
)
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
self.tp_rmsnorm = tp_size > 1
self.attn = USPAttention(
num_heads=self.local_num_heads,
head_size=self.head_dim,
causal=False,
is_cross_attention=False,
)
self.is_amplify_history = is_amplify_history
if is_amplify_history:
if history_scale_mode == "scalar":
self.history_key_scale = nn.Parameter(torch.ones(1))
elif history_scale_mode == "per_head":
self.history_key_scale = nn.Parameter(torch.ones(num_heads))
else:
raise ValueError(f"Unknown history_scale_mode: {history_scale_mode}")
self.history_scale_mode = history_scale_mode
self.max_scale = 10.0
def forward(self, hidden_states, rotary_emb=None, original_context_length=None):
q, _ = self.to_q(hidden_states)
k, _ = self.to_k(hidden_states)
v, _ = self.to_v(hidden_states)
if self.tp_rmsnorm:
q = tensor_parallel_rms_norm(q, self.norm_q)
k = tensor_parallel_rms_norm(k, self.norm_k)
else:
q = self.norm_q(q)
k = self.norm_k(k)
q = q.unflatten(2, (self.local_num_heads, self.head_dim))
k = k.unflatten(2, (self.local_num_heads, self.head_dim))
v = v.unflatten(2, (self.local_num_heads, self.head_dim))
if rotary_emb is not None:
q = apply_rotary_emb_transposed(q, rotary_emb)
k = apply_rotary_emb_transposed(k, rotary_emb)
history_seq_len = (
hidden_states.shape[1] - original_context_length
if original_context_length is not None
else 0
)
if self.is_amplify_history and original_context_length is not None:
if history_seq_len > 0:
scale_key = 1.0 + torch.sigmoid(self.history_key_scale) * (
self.max_scale - 1.0
)
if self.history_scale_mode == "per_head":
scale_key = scale_key.view(1, 1, -1, 1)
k = torch.cat(
[k[:, :history_seq_len] * scale_key, k[:, history_seq_len:]],
dim=1,
)
x = self.attn(q, k, v, num_replicated_prefix=history_seq_len)
x = x.flatten(2)
x, _ = self.to_out(x)
return x
# ---------------------------------------------------------------------------
# Cross-Attention for Helios
# ---------------------------------------------------------------------------
class HeliosCrossAttention(nn.Module):
"""Cross-attention with RMSNorm Q/K normalization."""
def __init__(
self,
dim: int,
num_heads: int,
eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
tp_size = get_tp_world_size()
self.local_num_heads = divide(num_heads, tp_size)
self.to_q = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_k = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_v = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.to_out = RowParallelLinear(
dim, dim, bias=True, reduce_results=True, quant_config=quant_config
)
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
self.tp_rmsnorm = tp_size > 1
self.attn = USPAttention(
num_heads=self.local_num_heads,
head_size=self.head_dim,
causal=False,
skip_sequence_parallel=True,
)
def project_kv(self, encoder_hidden_states):
"""Project encoder states to this block's cross-attn (key, value)."""
k, _ = self.to_k(encoder_hidden_states)
v, _ = self.to_v(encoder_hidden_states)
if self.tp_rmsnorm:
k = tensor_parallel_rms_norm(k, self.norm_k)
else:
k = self.norm_k(k)
k = k.unflatten(2, (self.local_num_heads, self.head_dim))
v = v.unflatten(2, (self.local_num_heads, self.head_dim))
return k, v
def forward(
self, hidden_states, encoder_hidden_states=None, encoder_key_value=None
):
q, _ = self.to_q(hidden_states)
if self.tp_rmsnorm:
q = tensor_parallel_rms_norm(q, self.norm_q)
else:
q = self.norm_q(q)
q = q.unflatten(2, (self.local_num_heads, self.head_dim))
if encoder_key_value is None:
if encoder_hidden_states is None:
raise ValueError(
"encoder_hidden_states is required when encoder_key_value"
" is not provided."
)
encoder_key_value = self.project_kv(encoder_hidden_states)
k, v = encoder_key_value
x = self.attn(q, k, v)
x = x.flatten(2)
x, _ = self.to_out(x)
return x
# ---------------------------------------------------------------------------
# Transformer Block
# ---------------------------------------------------------------------------
class HeliosTransformerBlock(nn.Module):
"""
Single transformer block with self-attention, cross-attention, FFN,
and scale-shift modulation from timestep embeddings.
"""
def __init__(
self,
dim: int,
ffn_dim: int,
num_heads: int,
cross_attn_norm: bool = True,
eps: float = 1e-6,
guidance_cross_attn: bool = True,
is_amplify_history: bool = False,
history_scale_mode: str = "per_head",
quant_config: QuantizationConfig | None = None,
):
super().__init__()
# 1. Self-attention
self.norm1 = LayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
self.attn1 = HeliosSelfAttention(
dim=dim,
num_heads=num_heads,
eps=eps,
is_amplify_history=is_amplify_history,
history_scale_mode=history_scale_mode,
quant_config=quant_config,
)
# 2. Cross-attention
self.attn2 = HeliosCrossAttention(
dim=dim,
num_heads=num_heads,
eps=eps,
quant_config=quant_config,
)
self.self_attn_residual_norm = (
LayerNorm(dim, eps=eps, elementwise_affine=True, dtype=torch.float32)
if cross_attn_norm
else nn.Identity()
)
# 3. Feed-forward
self.ffn = MLP(
dim, ffn_dim, act_type="gelu_pytorch_tanh", quant_config=quant_config
)
self.norm3 = LayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
self.scale_shift_table = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
# 4. Guidance cross-attention flag
self.guidance_cross_attn = guidance_cross_attn
def forward(
self,
hidden_states,
encoder_hidden_states,
temb,
rotary_emb,
original_context_length=None,
cross_attn_key_value=None,
):
if temb.ndim == 4:
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = (
self.scale_shift_table.unsqueeze(0) + temb.float()
).chunk(6, dim=2)
shift_msa = shift_msa.squeeze(2)
scale_msa = scale_msa.squeeze(2)
gate_msa = gate_msa.squeeze(2)
c_shift_msa = c_shift_msa.squeeze(2)
c_scale_msa = c_scale_msa.squeeze(2)
c_gate_msa = c_gate_msa.squeeze(2)
else:
shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = (
self.scale_shift_table + temb.float()
).chunk(6, dim=1)
# 1. Self-attention
norm_hidden_states = self.norm1(hidden_states, shift_msa, scale_msa)
attn_output = self.attn1(
norm_hidden_states, rotary_emb, original_context_length
)
hidden_states = (hidden_states.float() + attn_output * gate_msa).type_as(
hidden_states
)
# 2. Cross-attention
if self.guidance_cross_attn:
history_seq_len = hidden_states.shape[1] - original_context_length
history_hidden_states, current_hidden_states = torch.split(
hidden_states, [history_seq_len, original_context_length], dim=1
)
norm_hidden_states = self.self_attn_residual_norm(
current_hidden_states.float()
).type_as(current_hidden_states)
attn_output = self.attn2(
norm_hidden_states,
encoder_hidden_states,
encoder_key_value=cross_attn_key_value,
)
current_hidden_states = current_hidden_states + attn_output
hidden_states = torch.cat(
[history_hidden_states, current_hidden_states], dim=1
)
else:
norm_hidden_states = self.self_attn_residual_norm(
hidden_states.float()
).type_as(hidden_states)
attn_output = self.attn2(
norm_hidden_states,
encoder_hidden_states,
encoder_key_value=cross_attn_key_value,
)
hidden_states = hidden_states + attn_output
# 3. Feed-forward
norm_hidden_states = self.norm3(hidden_states, c_shift_msa, c_scale_msa)
ff_output = self.ffn(norm_hidden_states)
hidden_states = (
hidden_states.float() + ff_output.float() * c_gate_msa
).type_as(hidden_states)
return hidden_states
# ---------------------------------------------------------------------------
# Main model
# ---------------------------------------------------------------------------
class HeliosTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
"""
Helios Transformer 3D model for video generation.
Implements multi-scale history patches, 3D RoPE, and chunked denoising
with zero_history_timestep and guidance_cross_attn.
"""
_fsdp_shard_conditions = HeliosConfig()._fsdp_shard_conditions
_compile_conditions = HeliosConfig()._compile_conditions
_supported_attention_backends = HeliosConfig()._supported_attention_backends
param_names_mapping = HeliosConfig().param_names_mapping
reverse_param_names_mapping = HeliosConfig().reverse_param_names_mapping
lora_param_names_mapping = HeliosConfig().lora_param_names_mapping
def __init__(
self,
config: HeliosConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config=config, hf_config=hf_config)
inner_dim = config.num_attention_heads * config.attention_head_dim
self.hidden_size = config.hidden_size
self.num_attention_heads = config.num_attention_heads
self.in_channels = config.in_channels
self.out_channels = config.out_channels
self.num_channels_latents = config.num_channels_latents
self.patch_size = config.patch_size
self.text_len = config.text_len
self.inner_dim = inner_dim
# Helios-specific config
self.zero_history_timestep = config.zero_history_timestep
self.has_multi_term_memory_patch = config.has_multi_term_memory_patch
self.guidance_cross_attn = config.guidance_cross_attn
# 1. Patch & position embedding
self.patch_embedding = PatchEmbed(
in_chans=config.in_channels,
embed_dim=inner_dim,
patch_size=config.patch_size,
flatten=False,
)
# 2. Rotary position embeddings
self.rope = HeliosRotaryPosEmbed(
rope_dim=config.rope_dim, theta=config.rope_theta
)
# 3. Multi-term memory patches
if self.has_multi_term_memory_patch:
self.patch_short = nn.Conv3d(
config.in_channels,
inner_dim,
kernel_size=config.patch_size,
stride=config.patch_size,
)
self.patch_mid = nn.Conv3d(
config.in_channels,
inner_dim,
kernel_size=tuple(2 * p for p in config.patch_size),
stride=tuple(2 * p for p in config.patch_size),
)
self.patch_long = nn.Conv3d(
config.in_channels,
inner_dim,
kernel_size=tuple(4 * p for p in config.patch_size),
stride=tuple(4 * p for p in config.patch_size),
)
# 4. Condition embeddings
self.condition_embedder = HeliosTimeTextEmbedding(
dim=inner_dim,
time_freq_dim=config.freq_dim,
time_proj_dim=inner_dim * 6,
text_embed_dim=config.text_dim,
)
# 5. Transformer blocks
self.blocks = nn.ModuleList(
[
HeliosTransformerBlock(
dim=inner_dim,
ffn_dim=config.ffn_dim,
num_heads=config.num_attention_heads,
cross_attn_norm=config.cross_attn_norm,
eps=config.eps,
guidance_cross_attn=config.guidance_cross_attn,
is_amplify_history=config.is_amplify_history,
history_scale_mode=config.history_scale_mode,
quant_config=quant_config,
)
for _ in range(config.num_layers)
]
)
# 6. Output norm & projection
self.norm_out = HeliosOutputNorm(inner_dim, config.eps)
self.proj_out = ColumnParallelLinear(
inner_dim,
config.out_channels * math.prod(config.patch_size),
bias=True,
gather_output=True,
quant_config=quant_config,
)
self.cnt = 0
self.__post_init__()
self.layer_names = ["blocks"]
self.sp_size = get_sp_world_size()
# Cross-attention K/V cache.
#
# Text conditioning is constant across the denoise loop, so the text
# projection and every block's cross-attn K/V are computed once per request
# (keyed by encoder-tensor identity) and reused across steps.
@staticmethod
def _request_cache(forward_batch, name):
"""Per-request cache dict on ``forward_batch.extra``.
Returns None (-> caller recomputes, caching disabled) when there is no
forward batch or gradients are enabled."""
if forward_batch is None or torch.is_grad_enabled():
return None
extra = getattr(forward_batch, "extra", None)
return None if extra is None else extra.setdefault(name, {})
@staticmethod
def _tensor_key(t):
"""Identity key for ``t``; equal only for the same underlying tensor."""
return (
t.data_ptr(),
tuple(t.shape),
tuple(t.stride()),
t.dtype,
t.device.type,
t.device.index,
)
def _get_cross_attn_key_values(self, encoder_hidden_states, forward_batch):
"""Per-block cross-attn (key, value) for ``encoder_hidden_states``.
Cached per request, keyed on the encoder tensor's identity
(``_tensor_key``). The same object — ``batch.prompt_embeds`` — is passed
every denoise step, so the key is stable and steps after the first hit
the cache.
"""
cache = self._request_cache(forward_batch, "helios_cross_attn_kv")
key = self._tensor_key(encoder_hidden_states) if cache is not None else None
kvs = cache.get(key) if key is not None else None
if kvs is None:
projected = self.condition_embedder.text_embedder(encoder_hidden_states)
kvs = [block.attn2.project_kv(projected) for block in self.blocks]
if key is not None:
cache[key] = kvs
return kvs
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
# Stage 1 history inputs
indices_hidden_states=None,
indices_latents_history_short=None,
indices_latents_history_mid=None,
indices_latents_history_long=None,
latents_history_short=None,
latents_history_mid=None,
latents_history_long=None,
**kwargs,
) -> torch.Tensor:
if not isinstance(encoder_hidden_states, torch.Tensor):
encoder_hidden_states = encoder_hidden_states[0]
# Check if sequence parallelism is enabled
forward_batch = get_forward_context().forward_batch
if forward_batch is not None:
sequence_shard_enabled = (
forward_batch.enable_sequence_shard and self.sp_size > 1
)
else:
sequence_shard_enabled = False
batch_size = hidden_states.shape[0]
p_t, p_h, p_w = self.patch_size
# 1. Patch embed the noisy latents
hidden_states = self.patch_embedding(hidden_states)
(
_,
_,
post_patch_num_frames,
post_patch_height,
post_patch_width,
) = hidden_states.shape
if indices_hidden_states is None:
indices_hidden_states = (
torch.arange(0, post_patch_num_frames)
.unsqueeze(0)
.expand(batch_size, -1)
)
hidden_states = hidden_states.flatten(2).transpose(1, 2)
# 2. Compute rotary embeddings
rotary_emb = self.rope(
frame_indices=indices_hidden_states,
height=post_patch_height,
width=post_patch_width,
device=hidden_states.device,
)
rotary_emb = rotary_emb.flatten(2).transpose(1, 2)
original_context_length = hidden_states.shape[1]
# Sequence parallelism: shard current tokens and RoPE across SP ranks
seq_shard_pad = 0
if sequence_shard_enabled:
sp_rank = get_sp_group().rank_in_group
seq_len = hidden_states.shape[1]
if seq_len % self.sp_size != 0:
seq_shard_pad = self.sp_size - (seq_len % self.sp_size)
hs_pad = torch.zeros(
batch_size,
seq_shard_pad,
hidden_states.shape[2],
dtype=hidden_states.dtype,
device=hidden_states.device,
)
re_pad = torch.zeros(
batch_size,
seq_shard_pad,
rotary_emb.shape[2],
dtype=rotary_emb.dtype,
device=rotary_emb.device,
)
hidden_states = torch.cat([hidden_states, hs_pad], dim=1)
rotary_emb = torch.cat([rotary_emb, re_pad], dim=1)
local_seq_len = hidden_states.shape[1] // self.sp_size
hidden_states = hidden_states.view(
batch_size, self.sp_size, local_seq_len, -1
)[:, sp_rank, :, :].contiguous()
rotary_emb = rotary_emb.view(batch_size, self.sp_size, local_seq_len, -1)[
:, sp_rank, :, :
].contiguous()
effective_context_length = local_seq_len
else:
effective_context_length = original_context_length
# 3. Process short history
if (
latents_history_short is not None
and indices_latents_history_short is not None
):
latents_history_short = latents_history_short.to(hidden_states)
latents_history_short = self.patch_short(latents_history_short)
_, _, _, H1, W1 = latents_history_short.shape
latents_history_short = latents_history_short.flatten(2).transpose(1, 2)
rotary_emb_history_short = self.rope(
frame_indices=indices_latents_history_short,
height=H1,
width=W1,
device=latents_history_short.device,
)
rotary_emb_history_short = rotary_emb_history_short.flatten(2).transpose(
1, 2
)
hidden_states = torch.cat([latents_history_short, hidden_states], dim=1)
rotary_emb = torch.cat([rotary_emb_history_short, rotary_emb], dim=1)
# 4. Process mid history
if latents_history_mid is not None and indices_latents_history_mid is not None:
latents_history_mid = latents_history_mid.to(hidden_states)
latents_history_mid = pad_for_3d_conv(latents_history_mid, (2, 4, 4))
latents_history_mid = self.patch_mid(latents_history_mid)
latents_history_mid = latents_history_mid.flatten(2).transpose(1, 2)
rotary_emb_history_mid = self.rope(
frame_indices=indices_latents_history_mid,
height=H1,
width=W1,
device=latents_history_mid.device,
)
rotary_emb_history_mid = pad_for_3d_conv(rotary_emb_history_mid, (2, 2, 2))
rotary_emb_history_mid = center_down_sample_3d(
rotary_emb_history_mid, (2, 2, 2)
)
rotary_emb_history_mid = rotary_emb_history_mid.flatten(2).transpose(1, 2)
hidden_states = torch.cat([latents_history_mid, hidden_states], dim=1)
rotary_emb = torch.cat([rotary_emb_history_mid, rotary_emb], dim=1)
# 5. Process long history
if (
latents_history_long is not None
and indices_latents_history_long is not None
):
latents_history_long = latents_history_long.to(hidden_states)
latents_history_long = pad_for_3d_conv(latents_history_long, (4, 8, 8))
latents_history_long = self.patch_long(latents_history_long)
latents_history_long = latents_history_long.flatten(2).transpose(1, 2)
rotary_emb_history_long = self.rope(
frame_indices=indices_latents_history_long,
height=H1,
width=W1,
device=latents_history_long.device,
)
rotary_emb_history_long = pad_for_3d_conv(
rotary_emb_history_long, (4, 4, 4)
)
rotary_emb_history_long = center_down_sample_3d(
rotary_emb_history_long, (4, 4, 4)
)
rotary_emb_history_long = rotary_emb_history_long.flatten(2).transpose(1, 2)
hidden_states = torch.cat([latents_history_long, hidden_states], dim=1)
rotary_emb = torch.cat([rotary_emb_history_long, rotary_emb], dim=1)
history_context_length = hidden_states.shape[1] - effective_context_length
# 6. Compute condition embeddings
if indices_hidden_states is not None and self.zero_history_timestep:
timestep_t0 = torch.zeros(
(1,), dtype=timestep.dtype, device=timestep.device
)
temb_t0, timestep_proj_t0, _ = self.condition_embedder(
timestep_t0,
encoder_hidden_states,
is_return_encoder_hidden_states=False,
)
temb_t0 = temb_t0.unsqueeze(1).expand(
batch_size, history_context_length, -1
)
timestep_proj_t0 = (
timestep_proj_t0.unflatten(-1, (6, -1))
.view(1, 6, 1, -1)
.expand(batch_size, -1, history_context_length, -1)
)
# Take only the time embeddings (temb, timestep_proj); skip the text
# projection (is_return_encoder_hidden_states=False) since it is computed
# once per request and cached by _get_cross_attn_key_values below.
temb, timestep_proj, _ = self.condition_embedder(
timestep, encoder_hidden_states, is_return_encoder_hidden_states=False
)
cross_attn_key_values = self._get_cross_attn_key_values(
encoder_hidden_states, forward_batch
)
timestep_proj = timestep_proj.unflatten(-1, (6, -1))
if indices_hidden_states is not None and not self.zero_history_timestep:
main_repeat_size = hidden_states.shape[1]
else:
main_repeat_size = effective_context_length
temb = temb.view(batch_size, 1, -1).expand(batch_size, main_repeat_size, -1)
timestep_proj = timestep_proj.view(batch_size, 6, 1, -1).expand(
batch_size, 6, main_repeat_size, -1
)
if indices_hidden_states is not None and self.zero_history_timestep:
temb = torch.cat([temb_t0, temb], dim=1)
timestep_proj = torch.cat([timestep_proj_t0, timestep_proj], dim=2)
if timestep_proj.ndim == 4:
timestep_proj = timestep_proj.permute(0, 2, 1, 3)
# 7. Transformer blocks
hidden_states = hidden_states.contiguous()
encoder_hidden_states = encoder_hidden_states.contiguous()
rotary_emb = rotary_emb.contiguous()
for block, key_value in zip(self.blocks, cross_attn_key_values):
hidden_states = block(
hidden_states,
encoder_hidden_states,
timestep_proj,
rotary_emb,
effective_context_length,
cross_attn_key_value=key_value,
)
self.cnt += 1
# SP: all-gather current tokens before output
if sequence_shard_enabled:
current_tokens = hidden_states[:, -local_seq_len:, :].contiguous()
current_tokens = sequence_model_parallel_all_gather(current_tokens, dim=1)
if seq_shard_pad > 0:
current_tokens = current_tokens[:, :original_context_length, :]
hidden_states = current_tokens
# Re-create temb for norm_out (all current tokens share same timestep)
temb = temb[:, :1, :].expand(batch_size, original_context_length, -1)
# 8. Output norm & projection
hidden_states = self.norm_out(hidden_states, temb, original_context_length)
hidden_states, _ = self.proj_out(hidden_states)
# 9. Unpatchify
hidden_states = hidden_states.reshape(
batch_size,
post_patch_num_frames,
post_patch_height,
post_patch_width,
p_t,
p_h,
p_w,
-1,
)
hidden_states = hidden_states.permute(0, 7, 1, 4, 2, 5, 3, 6)
output = hidden_states.flatten(6, 7).flatten(4, 5).flatten(2, 3)
return output
EntryClass = HeliosTransformer3DModel
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,575 @@
# SPDX-License-Identifier: Apache-2.0
import math
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.configs.models.dits.ideogram import Ideogram4DiTConfig
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_tp_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.layers.attention import (
USPAttention,
build_varlen_mask_meta,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
MergedColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.weight_only_fp8 import (
WeightOnlyFP8ColumnParallelLinear,
WeightOnlyFP8Linear,
WeightOnlyFP8MergedColumnParallelLinear,
WeightOnlyFP8RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import (
Qwen3VLTextRotaryEmbedding,
qwen3_apply_rotary_pos_emb,
)
from sglang.multimodal_gen.runtime.models.dits.base import BaseDiT
OUTPUT_IMAGE_INDICATOR = 2
LLM_TOKEN_INDICATOR = 3
class Ideogram4RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6) -> None:
super().__init__()
self.weight = nn.Parameter(torch.ones(dim))
self.eps = eps
def forward(self, x: torch.Tensor) -> torch.Tensor:
return F.rms_norm(x, self.weight.shape, self.weight, self.eps)
class Ideogram4QuantizedLinear(ReplicatedLinear):
def forward(self, x: torch.Tensor) -> torch.Tensor:
return super().forward(x)[0]
class Ideogram4ColumnParallelLinear(ColumnParallelLinear):
def forward(self, x: torch.Tensor) -> torch.Tensor:
return super().forward(x)[0]
class Ideogram4MergedColumnParallelLinear(MergedColumnParallelLinear):
def forward(self, x: torch.Tensor) -> torch.Tensor:
return super().forward(x)[0]
class Ideogram4RowParallelLinear(RowParallelLinear):
def forward(self, x: torch.Tensor) -> torch.Tensor:
return super().forward(x)[0]
def _tp_size() -> int:
return get_tp_world_size() if model_parallel_is_initialized() else 1
def _linear(
in_features: int,
out_features: int,
bias: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
gather_output: bool = True,
):
tp_size = _tp_size()
use_column_parallel = tp_size > 1 and out_features % tp_size == 0
if quant_config is None:
if use_column_parallel:
return WeightOnlyFP8ColumnParallelLinear(
in_features,
out_features,
bias=bias,
gather_output=gather_output,
)
return WeightOnlyFP8Linear(in_features, out_features, bias=bias)
if use_column_parallel:
return Ideogram4ColumnParallelLinear(
in_features,
out_features,
bias=bias,
gather_output=gather_output,
quant_config=quant_config,
prefix=prefix,
)
return Ideogram4QuantizedLinear(
in_features,
out_features,
bias=bias,
quant_config=quant_config,
prefix=prefix,
)
def _merged_column_linear(
in_features: int,
output_sizes: list[int],
bias: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
tp_size = _tp_size()
use_column_parallel = tp_size > 1 and all(
output_size % tp_size == 0 for output_size in output_sizes
)
out_features = sum(output_sizes)
if quant_config is None:
if use_column_parallel:
return WeightOnlyFP8MergedColumnParallelLinear(
in_features,
output_sizes,
bias=bias,
gather_output=False,
)
return WeightOnlyFP8Linear(in_features, out_features, bias=bias)
if use_column_parallel:
return Ideogram4MergedColumnParallelLinear(
in_features,
output_sizes,
bias=bias,
gather_output=False,
quant_config=quant_config,
prefix=prefix,
)
return Ideogram4QuantizedLinear(
in_features,
out_features,
bias=bias,
quant_config=quant_config,
prefix=prefix,
)
def _row_linear(
in_features: int,
out_features: int,
bias: bool = True,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
tp_size = _tp_size()
use_row_parallel = tp_size > 1 and in_features % tp_size == 0
if quant_config is None:
if use_row_parallel:
return WeightOnlyFP8RowParallelLinear(
in_features,
out_features,
bias=bias,
input_is_parallel=True,
)
return WeightOnlyFP8Linear(in_features, out_features, bias=bias)
if use_row_parallel:
return Ideogram4RowParallelLinear(
in_features,
out_features,
bias=bias,
input_is_parallel=True,
quant_config=quant_config,
prefix=prefix,
)
return Ideogram4QuantizedLinear(
in_features,
out_features,
bias=bias,
quant_config=quant_config,
prefix=prefix,
)
class Ideogram4Attention(nn.Module):
def __init__(
self,
hidden_size: int,
num_heads: int,
eps: float,
supported_attention_backends,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = hidden_size
self.num_heads = num_heads
self.head_dim = hidden_size // num_heads
tp_size = _tp_size()
assert num_heads % tp_size == 0
self.local_num_heads = divide(num_heads, tp_size)
self.qkv = _merged_column_linear(
hidden_size,
[hidden_size, hidden_size, hidden_size],
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.qkv",
)
self.norm_q = Ideogram4RMSNorm(self.head_dim, eps=eps)
self.norm_k = Ideogram4RMSNorm(self.head_dim, eps=eps)
self.attn = USPAttention(
num_heads=self.local_num_heads,
head_size=self.head_dim,
dropout_rate=0,
softmax_scale=None,
causal=False,
supported_attention_backends=supported_attention_backends,
)
self.o = _row_linear(
hidden_size,
hidden_size,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.o",
)
def forward(self, x, cos, sin, attn_mask, attn_mask_meta):
batch_size, seq_len, _ = x.shape
qkv = self.qkv(x).view(
batch_size, seq_len, 3, self.local_num_heads, self.head_dim
)
q, k, v = qkv.unbind(dim=2)
q = self.norm_q(q)
k = self.norm_k(k)
q, k = qwen3_apply_rotary_pos_emb(q, k, cos, sin)
out = self.attn(q, k, v, attn_mask=attn_mask, attn_mask_meta=attn_mask_meta)
out = out.reshape(batch_size, seq_len, self.local_num_heads * self.head_dim)
return self.o(out)
class Ideogram4MLP(nn.Module):
def __init__(
self,
dim: int,
hidden_dim: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.w1 = _linear(
dim,
hidden_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.w1",
gather_output=False,
)
self.w2 = _row_linear(
hidden_dim,
dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.w2",
)
self.w3 = _linear(
dim,
hidden_dim,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.w3",
gather_output=False,
)
def forward(self, x):
return self.w2(F.silu(self.w1(x)) * self.w3(x))
class Ideogram4TransformerBlock(nn.Module):
def __init__(
self,
hidden_size,
intermediate_size,
num_heads,
norm_eps,
adaln_dim,
supported_attention_backends,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.attention = Ideogram4Attention(
hidden_size,
num_heads,
eps=1e-5,
supported_attention_backends=supported_attention_backends,
quant_config=quant_config,
prefix=f"{prefix}.attention",
)
self.feed_forward = Ideogram4MLP(
hidden_size,
intermediate_size,
quant_config=quant_config,
prefix=f"{prefix}.feed_forward",
)
self.attention_norm1 = Ideogram4RMSNorm(hidden_size, eps=norm_eps)
self.ffn_norm1 = Ideogram4RMSNorm(hidden_size, eps=norm_eps)
self.attention_norm2 = Ideogram4RMSNorm(hidden_size, eps=norm_eps)
self.ffn_norm2 = Ideogram4RMSNorm(hidden_size, eps=norm_eps)
self.adaln_modulation = _linear(
adaln_dim,
4 * hidden_size,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.adaln_modulation",
)
def forward(self, x, cos, sin, adaln_input, attn_mask, attn_mask_meta):
scale_msa, gate_msa, scale_mlp, gate_mlp = self.adaln_modulation(
adaln_input
).chunk(4, dim=-1)
gate_msa = torch.tanh(gate_msa)
gate_mlp = torch.tanh(gate_mlp)
attn_out = self.attention(
self.attention_norm1(x) * (1.0 + scale_msa),
cos=cos,
sin=sin,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
)
x = x + gate_msa * self.attention_norm2(attn_out)
x = x + gate_mlp * self.ffn_norm2(
self.feed_forward(self.ffn_norm1(x) * (1.0 + scale_mlp))
)
return x
def _sinusoidal_embedding(t: torch.Tensor, dim: int, scale: float = 1e4):
t = t.to(torch.float32)
half = dim // 2
freq = math.log(scale) / (half - 1)
freq = torch.exp(torch.arange(half, dtype=torch.float32, device=t.device) * -freq)
emb = t.unsqueeze(-1) * freq
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
if dim % 2 == 1:
emb = F.pad(emb, (0, 1))
return emb
class Ideogram4EmbedScalar(nn.Module):
def __init__(
self,
dim: int,
input_range: tuple[float, float],
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.dim = dim
self.range_min, self.range_max = input_range
self.mlp_in = _linear(
dim,
dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.mlp_in",
)
self.mlp_out = _linear(
dim,
dim,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.mlp_out",
)
def forward(self, x):
compute_dtype = x.dtype
x = x.to(torch.float32)
scaled = 1e4 * (x - self.range_min) / (self.range_max - self.range_min)
emb = _sinusoidal_embedding(scaled, self.dim).to(compute_dtype)
return self.mlp_out(F.silu(self.mlp_in(emb)))
class Ideogram4FinalLayer(nn.Module):
def __init__(
self,
hidden_size: int,
out_channels: int,
adaln_dim: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.norm_final = nn.LayerNorm(hidden_size, eps=1e-6, elementwise_affine=False)
self.linear = _linear(
hidden_size,
out_channels,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.linear",
)
self.adaln_modulation = _linear(
adaln_dim,
hidden_size,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.adaln_modulation",
)
def forward(self, x, c):
scale = 1.0 + self.adaln_modulation(F.silu(c))
return self.linear(self.norm_final(x) * scale)
class Ideogram4Transformer2DModel(BaseDiT):
_repeated_blocks = ["Ideogram4TransformerBlock"]
_fsdp_shard_conditions = Ideogram4DiTConfig().arch_config._fsdp_shard_conditions
_compile_conditions = Ideogram4DiTConfig().arch_config._compile_conditions
_supported_attention_backends = (
Ideogram4DiTConfig().arch_config._supported_attention_backends
)
param_names_mapping = {}
reverse_param_names_mapping = {}
def __init__(
self,
config: Ideogram4DiTConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
**kwargs,
) -> None:
super().__init__(config, hf_config, **kwargs)
cfg = config.arch_config
self._supported_attention_backends = cfg._supported_attention_backends
hidden_size = cfg.num_attention_heads * cfg.attention_head_dim
self.hidden_size = hidden_size
self.num_attention_heads = cfg.num_attention_heads
self.num_channels_latents = cfg.in_channels
self.input_proj = _linear(
cfg.in_channels,
hidden_size,
bias=True,
quant_config=quant_config,
prefix="input_proj",
)
self.llm_cond_norm = Ideogram4RMSNorm(cfg.llm_features_dim, eps=1e-6)
self.llm_cond_proj = _linear(
cfg.llm_features_dim,
hidden_size,
bias=True,
quant_config=quant_config,
prefix="llm_cond_proj",
)
self.t_embedding = Ideogram4EmbedScalar(
hidden_size,
input_range=(0.0, 1.0),
quant_config=quant_config,
prefix="t_embedding",
)
self.adaln_proj = _linear(
hidden_size,
cfg.adaln_dim,
bias=True,
quant_config=quant_config,
prefix="adaln_proj",
)
self.embed_image_indicator = nn.Embedding(2, hidden_size)
self.rotary_emb = Qwen3VLTextRotaryEmbedding(
head_dim=cfg.attention_head_dim,
rope_theta=cfg.rope_theta,
mrope_section=cfg.mrope_section,
)
self.layers = nn.ModuleList(
[
Ideogram4TransformerBlock(
hidden_size=hidden_size,
intermediate_size=cfg.intermediate_size,
num_heads=cfg.num_attention_heads,
norm_eps=cfg.norm_eps,
adaln_dim=cfg.adaln_dim,
supported_attention_backends=self._supported_attention_backends,
quant_config=quant_config,
prefix=f"layers.{i}",
)
for i in range(cfg.num_layers)
]
)
self.final_layer = Ideogram4FinalLayer(
hidden_size=hidden_size,
out_channels=cfg.in_channels,
adaln_dim=cfg.adaln_dim,
quant_config=quant_config,
prefix="final_layer",
)
def post_load_weights(self) -> None:
if not self.rotary_emb.inv_freq.is_meta:
return
cfg = self.config.arch_config
inv_freq = 1.0 / (
cfg.rope_theta
** (
torch.arange(
0,
cfg.attention_head_dim,
2,
dtype=torch.float32,
device=self.input_proj.weight.device,
)
/ cfg.attention_head_dim
)
)
self.rotary_emb.register_buffer("inv_freq", inv_freq, persistent=False)
def forward(
self,
*,
llm_features: torch.Tensor,
x: torch.Tensor,
t: torch.Tensor,
position_ids: torch.Tensor,
segment_ids: torch.Tensor,
indicator: torch.Tensor,
attn_mask: torch.Tensor | None = None,
attn_mask_meta: dict | None = None,
**kwargs,
) -> torch.Tensor:
param_dtype = self.embed_image_indicator.weight.dtype
x = x.to(param_dtype)
t = t.to(param_dtype)
llm_features = llm_features.to(param_dtype)
indicator = indicator.to(torch.long)
llm_token_mask = (indicator == LLM_TOKEN_INDICATOR).to(x.dtype).unsqueeze(-1)
output_image_mask = (
(indicator == OUTPUT_IMAGE_INDICATOR).to(x.dtype).unsqueeze(-1)
)
llm_features = llm_features * llm_token_mask
x = x * output_image_mask
x = self.input_proj(x) * output_image_mask
t_cond = self.t_embedding(t)
if t.dim() == 1:
t_cond = t_cond.unsqueeze(1)
adaln_input = F.silu(self.adaln_proj(t_cond))
llm_features = self.llm_cond_proj(self.llm_cond_norm(llm_features))
llm_features = llm_features * llm_token_mask
h = x + llm_features
h = h + self.embed_image_indicator(
(indicator == OUTPUT_IMAGE_INDICATOR).to(torch.long)
)
cos, sin = self.rotary_emb(h, position_ids)
cos = cos.unsqueeze(2)
sin = sin.unsqueeze(2)
# ideogram uses -1 padding; varlen meta enables fa packed attention
if attn_mask is None:
attn_mask = segment_ids > 0
if attn_mask_meta is None:
attn_mask_meta = build_varlen_mask_meta(attn_mask)
for layer in self.layers:
h = layer(
h,
cos=cos,
sin=sin,
adaln_input=adaln_input,
attn_mask=attn_mask,
attn_mask_meta=attn_mask_meta,
)
return self.final_layer(h, c=adaln_input).to(torch.float32)
EntryClass = Ideogram4Transformer2DModel
@@ -0,0 +1,596 @@
# SPDX-License-Identifier: Apache-2.0
import math
from functools import lru_cache
from typing import Any, Optional, Tuple
import torch
import torch.nn as nn
from einops import rearrange
from sglang.multimodal_gen.configs.models.dits.joy_image import JoyImageDiTConfig
from sglang.multimodal_gen.runtime.distributed import (
divide,
get_sp_group,
get_sp_world_size,
get_tp_world_size,
sequence_model_parallel_all_gather,
)
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNormScaleShift,
RMSNorm,
apply_qk_norm_with_optional_rope,
)
from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.layers.rotary_embedding import NDRotaryEmbedding
from sglang.multimodal_gen.runtime.managers.forward_context import get_forward_context
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.models.dits.wanvideo import WanTimeTextImageEmbedding
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.weight_attrs import set_weight_attrs
logger = init_logger(__name__)
_MODULATION_FACTOR = 6
def fused_add_gate(
residual: torch.Tensor, x: torch.Tensor, gate: torch.Tensor
) -> torch.Tensor:
"""Fused residual addition with gate.
Computes: residual + x * gate.unsqueeze(1)
This fuses the gate multiplication and residual addition to reduce
intermediate tensor allocations and memory bandwidth.
Args:
residual (torch.Tensor): The residual tensor to add to. Shape: (B, L, D)
x (torch.Tensor): The input tensor to be gated. Shape: (B, L, D)
gate (torch.Tensor): The gate tensor. Shape: (B, D)
Returns:
torch.Tensor: residual + x * gate.unsqueeze(1)
"""
return torch.addcmul(residual, x, gate.unsqueeze(1))
class ModulateWan(nn.Module):
"""Modulation layer for WanX."""
def __init__(self, hidden_size: int, factor: int, dtype=None, device=None):
super().__init__()
self.factor = factor
self.modulate_table = nn.Parameter(
torch.zeros(1, factor, hidden_size, dtype=dtype, device=device)
/ hidden_size**0.5,
requires_grad=False,
)
set_weight_attrs(
self.modulate_table,
{
"input_dim": 1,
"output_dim": 2,
},
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if len(x.shape) != 3:
x = x.unsqueeze(1)
return [
o.squeeze(1) for o in (self.modulate_table + x).chunk(self.factor, dim=1)
]
class MMDoubleStreamBlock(nn.Module):
def __init__(
self,
hidden_size: int,
heads_num: int,
mlp_width_ratio: float,
mlp_act_type: str = "gelu_pytorch_tanh",
supported_attention_backends: set[AttentionBackendEnum] | None = None,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
):
super().__init__()
self.heads_num = heads_num
self.hidden_size = hidden_size
self.tp_size = get_tp_world_size()
self.local_heads_num = divide(self.heads_num, self.tp_size)
self.head_dim = self.hidden_size // self.heads_num
self.mlp_hidden_dim = int(self.hidden_size * mlp_width_ratio)
self.img_mod = ModulateWan(self.hidden_size, factor=_MODULATION_FACTOR)
self.fused_modulate_img_norm1 = LayerNormScaleShift(
self.hidden_size,
eps=1e-6,
elementwise_affine=False,
)
self.img_attn_qkv = MergedColumnParallelLinear(
self.hidden_size,
[hidden_size, hidden_size, hidden_size],
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.img_attn_qkv",
)
self.img_attn_q_norm = RMSNorm(
self.head_dim,
eps=1e-6,
)
self.img_attn_k_norm = RMSNorm(
self.head_dim,
eps=1e-6,
)
self.img_attn_proj = RowParallelLinear(
self.hidden_size,
hidden_size,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=f"{prefix}.img_attn_proj",
)
self.fused_modulate_img_norm2 = LayerNormScaleShift(
self.hidden_size,
eps=1e-6,
elementwise_affine=False,
)
self.img_mlp = MLP(
input_dim=self.hidden_size,
mlp_hidden_dim=self.mlp_hidden_dim,
act_type=mlp_act_type,
quant_config=quant_config,
prefix=f"{prefix}.img_mlp",
)
# Text modulation and attention
self.txt_mod = ModulateWan(self.hidden_size, factor=_MODULATION_FACTOR)
self.fused_modulate_txt_norm1 = LayerNormScaleShift(
self.hidden_size,
eps=1e-6,
elementwise_affine=False,
)
self.txt_attn_qkv = MergedColumnParallelLinear(
self.hidden_size,
[self.hidden_size, self.hidden_size, self.hidden_size],
bias=True,
gather_output=False,
quant_config=quant_config,
prefix=f"{prefix}.txt_attn_qkv",
)
self.txt_attn_q_norm = RMSNorm(
self.head_dim,
eps=1e-6,
)
self.txt_attn_k_norm = RMSNorm(
self.head_dim,
eps=1e-6,
)
self.txt_attn_proj = RowParallelLinear(
self.hidden_size,
self.hidden_size,
bias=True,
input_is_parallel=True,
quant_config=quant_config,
prefix=f"{prefix}.txt_attn_proj",
)
self.fused_modulate_txt_norm2 = LayerNormScaleShift(
self.hidden_size,
eps=1e-6,
elementwise_affine=False,
)
self.txt_mlp = MLP(
input_dim=self.hidden_size,
mlp_hidden_dim=self.mlp_hidden_dim,
act_type=mlp_act_type,
quant_config=quant_config,
prefix=f"{prefix}.txt_mlp",
)
self.attn = USPAttention(
num_heads=self.local_heads_num,
head_size=self.head_dim,
causal=False,
supported_attention_backends=supported_attention_backends,
softmax_scale=None,
)
def forward(
self,
img: torch.Tensor,
txt: torch.Tensor,
vec: torch.Tensor,
vis_freqs_cis: Optional[torch.Tensor] = None,
txt_freqs_cis: Optional[torch.Tensor] = None,
num_replicated_suffix: int = 0,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Forward pass through multimodal double stream block."""
(
img_mod1_shift,
img_mod1_scale,
img_mod1_gate,
img_mod2_shift,
img_mod2_scale,
img_mod2_gate,
) = self.img_mod(vec)
(
txt_mod1_shift,
txt_mod1_scale,
txt_mod1_gate,
txt_mod2_shift,
txt_mod2_scale,
txt_mod2_gate,
) = self.txt_mod(vec)
# Image attention
img_modulated = self.fused_modulate_img_norm1(
img, shift=img_mod1_shift, scale=img_mod1_scale
)
img_qkv, _ = self.img_attn_qkv(img_modulated)
img_q, img_k, img_v = rearrange(
img_qkv, "B L (K H D) -> K B L H D", K=3, H=self.local_heads_num
)
if vis_freqs_cis is None:
raise ValueError(
"vis_freqs_cis is required for fused QK-Norm + RoPE kernel"
)
if not (isinstance(vis_freqs_cis, torch.Tensor) and vis_freqs_cis.dim() == 2):
raise ValueError("vis_freqs_cis must be a 2D cos_sin_cache tensor")
if img_q.dtype not in (torch.float16, torch.bfloat16):
raise ValueError(
f"Fused QK-Norm + RoPE kernel only supports float16/bfloat16, but got {img_q.dtype}"
)
img_q = img_q.contiguous()
img_k = img_k.contiguous()
img_q, img_k = apply_qk_norm_with_optional_rope(
q=img_q,
k=img_k,
q_norm=self.img_attn_q_norm,
k_norm=self.img_attn_k_norm,
head_dim=img_q.shape[-1],
cos_sin_cache=vis_freqs_cis,
is_neox=False,
allow_inplace=True,
)
img_q, img_k = img_q.to(img_v), img_k.to(img_v)
# Text attention
txt_modulated = self.fused_modulate_txt_norm1(
txt, shift=txt_mod1_shift, scale=txt_mod1_scale
)
txt_qkv, _ = self.txt_attn_qkv(txt_modulated)
txt_q, txt_k, txt_v = rearrange(
txt_qkv, "B L (K H D) -> K B L H D", K=3, H=self.local_heads_num
)
if txt_freqs_cis is not None and not (
isinstance(txt_freqs_cis, torch.Tensor) and txt_freqs_cis.dim() == 2
):
raise ValueError("txt_freqs_cis must be a 2D cos_sin_cache tensor")
txt_q = txt_q.contiguous()
txt_k = txt_k.contiguous()
txt_q, txt_k = apply_qk_norm_with_optional_rope(
q=txt_q,
k=txt_k,
q_norm=self.txt_attn_q_norm,
k_norm=self.txt_attn_k_norm,
head_dim=txt_q.shape[-1],
cos_sin_cache=txt_freqs_cis,
is_neox=False,
allow_inplace=True,
)
txt_q, txt_k = txt_q.to(txt_v), txt_k.to(txt_v)
# Attention
joint_query = torch.cat([img_q, txt_q], dim=1)
joint_key = torch.cat([img_k, txt_k], dim=1)
joint_value = torch.cat([img_v, txt_v], dim=1)
attn = self.attn(
joint_query,
joint_key,
joint_value,
num_replicated_suffix=num_replicated_suffix,
)
attn = attn.flatten(2, 3)
img_attn, txt_attn = (
attn[:, : img.shape[1]],
attn[:, img.shape[1] :],
)
img = fused_add_gate(img, self.img_attn_proj(img_attn)[0], img_mod1_gate)
img = fused_add_gate(
img,
self.img_mlp(
self.fused_modulate_img_norm2(
img, shift=img_mod2_shift, scale=img_mod2_scale
)
),
img_mod2_gate,
)
# Text blocks
txt = fused_add_gate(txt, self.txt_attn_proj(txt_attn)[0], txt_mod1_gate)
txt = fused_add_gate(
txt,
self.txt_mlp(
self.fused_modulate_txt_norm2(
txt, shift=txt_mod2_shift, scale=txt_mod2_scale
)
),
txt_mod2_gate,
)
return img, txt
class JoyTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
"""
JoyImage Transformer 3D Model for image generation.
"""
_supports_gradient_checkpointing = True
_fsdp_shard_conditions = JoyImageDiTConfig()._fsdp_shard_conditions
_compile_conditions = JoyImageDiTConfig()._compile_conditions
_supported_attention_backends = JoyImageDiTConfig()._supported_attention_backends
param_names_mapping = JoyImageDiTConfig().param_names_mapping
reverse_param_names_mapping = JoyImageDiTConfig().reverse_param_names_mapping
lora_param_names_mapping = JoyImageDiTConfig().lora_param_names_mapping
def __init__(
self,
config: JoyImageDiTConfig,
hf_config: dict[str, Any],
quant_config: Optional[QuantizationConfig] = None,
) -> None:
super().__init__(
config=config,
hf_config=hf_config,
)
self.in_channels = config.in_channels
self.out_channels = config.out_channels or config.in_channels
self.patch_size = config.patch_size
self.hidden_size = config.hidden_size
self.num_attention_heads = config.num_attention_heads
self.rope_dim_list = config.rope_dim_list
self.mm_double_blocks_depth = config.mm_double_blocks_depth
self.rope_theta = config.rope_theta
self.quant_config = quant_config
self.num_channels_latents = self.out_channels
if self.hidden_size % self.num_attention_heads != 0:
raise ValueError(
f"Hidden size {self.hidden_size} must be divisible by num_attention_heads {self.num_attention_heads}"
)
# Image projection (patch embedding)
self.img_in = nn.Conv3d(
self.in_channels,
self.hidden_size,
kernel_size=self.patch_size,
stride=self.patch_size,
)
# Condition embedding
self.condition_embedder = WanTimeTextImageEmbedding(
dim=self.hidden_size,
time_freq_dim=config.freq_dim,
text_embed_dim=config.text_states_dim,
)
# Double blocks (DiT layers)
self.double_blocks = nn.ModuleList(
[
MMDoubleStreamBlock(
self.hidden_size,
self.num_attention_heads,
mlp_width_ratio=config.mlp_width_ratio,
supported_attention_backends=self._supported_attention_backends,
quant_config=quant_config,
prefix=f"{config.prefix}.double_blocks.{i}",
)
for i in range(self.mm_double_blocks_depth)
]
)
# Layerwise offload expects ModuleList names here.
self.layer_names = ["double_blocks"]
# Output norm & projection
self.norm_out = nn.LayerNorm(
self.hidden_size, elementwise_affine=False, eps=1e-6
)
self.proj_out = ReplicatedLinear(
self.hidden_size,
self.out_channels * math.prod(self.patch_size),
quant_config=quant_config,
prefix="proj_out",
)
self.__post_init__()
self.sp_size = get_sp_world_size()
self.rotary_emb = NDRotaryEmbedding(
rope_dim_list=config.rope_dim_list,
rope_theta=config.rope_theta,
dtype=torch.float32,
)
@lru_cache(maxsize=1)
def _compute_rope_for_local_shard(
self,
local_len: int,
rank: int,
vae_image_sizes: tuple[tuple[int, int, int], ...],
device: torch.device,
) -> tuple[torch.Tensor, torch.Tensor]:
token_start = rank * local_len
token_indices = torch.arange(
token_start,
token_start + local_len,
device=device,
dtype=torch.long,
)
positions = torch.zeros(local_len, 3, device=device, dtype=torch.long)
cumsum = 0
current_t_offset = 0
for t, h, w in vae_image_sizes:
item_size = t * h * w
mask = (token_indices >= cumsum) & (token_indices < cumsum + item_size)
if mask.any():
local_idx = token_indices[mask] - cumsum
frame_stride = h * w
positions[mask, 0] = local_idx // frame_stride + current_t_offset
positions[mask, 1] = (local_idx % frame_stride) // w
positions[mask, 2] = local_idx % w
cumsum += item_size
current_t_offset += t
return self.rotary_emb.forward_uncached(positions)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
encoder_hidden_states_mask: torch.Tensor | list[torch.Tensor] | None = None,
vis_freqs_cis: torch.Tensor | None = None,
txt_freqs_cis: torch.Tensor | None = None,
**kwargs,
) -> torch.Tensor:
"""Forward pass through JoyImage Transformer."""
forward_batch = get_forward_context().forward_batch
sequence_shard_enabled = (
forward_batch is not None
and getattr(forward_batch, "enable_sequence_shard", False)
and self.sp_size > 1
)
batch_size = hidden_states.shape[0]
if not isinstance(encoder_hidden_states, torch.Tensor):
encoder_hidden_states = encoder_hidden_states[0]
if isinstance(encoder_hidden_states_mask, list):
encoder_hidden_states_mask = encoder_hidden_states_mask[0]
cond_batch = int(encoder_hidden_states.shape[0])
if cond_batch != int(batch_size):
if cond_batch <= 0 or int(batch_size) % cond_batch != 0:
raise ValueError(
"JoyImage conditioning batch mismatch: "
f"hidden_states batch={batch_size}, "
f"encoder_hidden_states batch={cond_batch}."
)
repeat_factor = int(batch_size) // cond_batch
encoder_hidden_states = encoder_hidden_states.repeat_interleave(
repeat_factor, dim=0
)
if encoder_hidden_states_mask is not None:
encoder_hidden_states_mask = (
encoder_hidden_states_mask.repeat_interleave(repeat_factor, dim=0)
)
# Prepare img
x = rearrange(hidden_states, "b n c p1 p2 p3 -> (b n) c p1 p2 p3")
x = self.img_in(x)
img = rearrange(x, "(b n) d 1 1 1 -> b n d", b=batch_size)
seq_len_orig = img.shape[1]
seq_shard_pad = 0
if sequence_shard_enabled:
if seq_len_orig % self.sp_size != 0:
seq_shard_pad = self.sp_size - (seq_len_orig % self.sp_size)
pad = torch.zeros(
(batch_size, seq_shard_pad, img.shape[2]),
dtype=img.dtype,
device=img.device,
)
img = torch.cat([img, pad], dim=1)
sp_rank = get_sp_group().rank_in_group
local_seq_len = img.shape[1] // self.sp_size
img = img.view(batch_size, self.sp_size, local_seq_len, img.shape[2])[
:, sp_rank, :, :
].contiguous()
# Compute rope in model for all SP modes
if forward_batch is not None and forward_batch.vae_image_sizes is not None:
vae_image_sizes = tuple(tuple(s) for s in forward_batch.vae_image_sizes)
local_len = img.shape[1]
rank = get_sp_group().rank_in_group if self.sp_size > 1 else 0
freqs_cos, freqs_sin = self._compute_rope_for_local_shard(
local_len,
rank,
vae_image_sizes,
img.device,
)
vis_freqs_cis = torch.cat(
[
freqs_cos.to(dtype=torch.float32).contiguous(),
freqs_sin.to(dtype=torch.float32).contiguous(),
],
dim=-1,
)
_, vec, txt, _ = self.condition_embedder(timestep, encoder_hidden_states)
if vec.shape[-1] > self.hidden_size:
vec = vec.unflatten(1, (_MODULATION_FACTOR, -1))
txt_suffix_len = txt.shape[1] if sequence_shard_enabled else 0
# Pass through DiT blocks
for block in self.double_blocks:
img, txt = block(
img,
txt,
vec,
vis_freqs_cis,
txt_freqs_cis,
num_replicated_suffix=txt_suffix_len,
)
if sequence_shard_enabled:
img = img.contiguous()
img = sequence_model_parallel_all_gather(img, dim=1)
if seq_shard_pad > 0:
img = img[:, :seq_len_orig, :]
img, _ = self.proj_out(self.norm_out(img))
# Restore patch layout expected by downstream latent decoding.
img = rearrange(
img,
"b n (pt ph pw c) -> b n c pt ph pw",
pt=self.patch_size[0],
ph=self.patch_size[1],
pw=self.patch_size[2],
c=self.out_channels,
)
return img
class JoyImageEditTransformer3DModel(JoyTransformer3DModel):
"""Backward-compatible alias for JoyImageEdit model configs."""
pass
EntryClass = [JoyTransformer3DModel, JoyImageEditTransformer3DModel]
@@ -0,0 +1,634 @@
"""Krea-2 (K2) single-stream MMDiT.
Text and image tokens are concatenated into a single joint-attention stream. The
model uses GQA attention with a sigmoid output gate, 6-way shared adaLN
modulation, a text-fusion transformer that fuses the selected text-encoder
hidden-state layers into one, and interleaved 3-axis RoPE. Module and parameter
names follow the released K2 checkpoint, so weights load without remapping.
"""
import math
import os
from typing import Any, Optional
import torch
import torch.nn as nn
import torch.nn.functional as F
from einops import rearrange
from torch import Tensor
from sglang.multimodal_gen.configs.models.dits.krea2 import Krea2DitConfig
from sglang.multimodal_gen.runtime.distributed import (
get_sp_world_size,
get_tp_world_size,
)
from sglang.multimodal_gen.runtime.layers.attention import USPAttention
from sglang.multimodal_gen.runtime.layers.attention.layer import build_varlen_mask_meta
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# --------------------------------------------------------------------------- #
# Functional helpers
# --------------------------------------------------------------------------- #
def rope(pos: Tensor, dim: int, theta: float = 1e4, ntk: float = 1.0) -> Tensor:
scale = torch.arange(0, dim, 2, dtype=torch.float64, device=pos.device) / dim
omega = 1.0 / ((theta * ntk) ** scale)
out = torch.einsum("...n,d->...nd", pos, omega)
out = torch.stack(
[torch.cos(out), -torch.sin(out), torch.sin(out), torch.cos(out)], dim=-1
)
out = rearrange(out, "b n d (i j) -> b n d i j", i=2, j=2)
return out.float()
def ropeapply(xq: Tensor, xk: Tensor, freqs: Tensor) -> tuple[Tensor, Tensor]:
xq_ = xq.float().reshape(*xq.shape[:-1], -1, 1, 2)
xk_ = xk.float().reshape(*xk.shape[:-1], -1, 1, 2)
freqs = freqs[:, None, :, :, :]
xq_ = freqs[..., 0] * xq_[..., 0] + freqs[..., 1] * xq_[..., 1]
xk_ = freqs[..., 0] * xk_[..., 0] + freqs[..., 1] * xk_[..., 1]
return xq_.reshape(*xq.shape).to(xq.dtype), xk_.reshape(*xk.shape).to(xk.dtype)
def _fused_qknorm_rope_enabled() -> bool:
return os.getenv("SGLANG_ENABLE_FUSED_QKNORM_ROPE", "1").lower() not in (
"0",
"false",
"off",
"no",
)
def _can_use_fused_qknorm_rope(head_dim: int, dtype: torch.dtype) -> bool:
from sglang.jit_kernel.diffusion.qknorm_rope import (
can_use_fused_inplace_qknorm_rope,
)
return can_use_fused_inplace_qknorm_rope(head_dim, head_dim, False, dtype)
def _qknorm_rope_cos_sin_cache(freqs: Tensor) -> Tensor:
"""``[num_tokens, head_dim]`` cos|sin cache for the fused QKNorm+RoPE kernel.
K2's ``rope`` packs each token's rotation as ``[[cos, -sin], [sin, cos]]`` in a
``[B, N, head_dim//2, 2, 2]`` tensor; the kernel wants the per-token cosines then
sines concatenated. Positions come from the image grid (batch-invariant), so the
first batch row is representative.
"""
return torch.cat([freqs[0, :, :, 0, 0], freqs[0, :, :, 1, 0]], dim=-1).float()
def temb(
t: Tensor,
dim: int,
period: float = 1e4,
tfactor: float = 1e3,
device: torch.device = None,
dtype: torch.dtype = None,
) -> Tensor:
half = dim // 2
freqs = torch.exp(
-math.log(period)
* torch.arange(half, dtype=torch.float32, device=device)
/ half
)
args = (t.float() * tfactor)[:, None, None] * freqs
sin, cos = torch.sin(args), torch.cos(args)
return torch.cat((cos, sin), dim=-1).to(dtype=dtype)
def norm_scale_shift(
x: Tensor, weight: Tensor, scale: Tensor, shift: Tensor, eps: float
) -> Tensor:
"""Fused RMSNorm + modulation: ``rms_norm(x) * weight * (1 + scale) + shift``.
``weight`` is the effective RMSNorm weight (K2 stores ``scale``, so callers
pass ``scale + 1``), kept off the checkpoint so the identity load is unaffected.
"""
if x.is_cuda and x.shape[-1] % 256 == 0:
from sglang.jit_kernel.diffusion.cutedsl.scale_residual_norm_scale_shift import (
fused_norm_scale_shift,
)
return fused_norm_scale_shift(
x.contiguous(),
weight.contiguous(),
None,
scale.contiguous(),
shift.contiguous(),
"rms",
eps,
)
normed = F.rms_norm(x.float(), (x.shape[-1],), weight=weight.float(), eps=eps)
return (normed.to(x.dtype) * (1 + scale) + shift).to(x.dtype)
# --------------------------------------------------------------------------- #
# Submodules
# --------------------------------------------------------------------------- #
class TimeEmbed(nn.Module):
"""Timestep embedding MLP: linear_1 -> gelu(tanh) -> linear_2."""
def __init__(self, in_dim: int, dim: int):
super().__init__()
self.linear_1 = nn.Linear(in_dim, dim)
self.linear_2 = nn.Linear(dim, dim)
def forward(self, x: Tensor) -> Tensor:
return self.linear_2(F.gelu(self.linear_1(x), approximate="tanh"))
class TxtIn(nn.Module):
"""Text-context projection: rms-norm -> linear_1 -> gelu(tanh) -> linear_2."""
def __init__(self, txt_dim: int, dim: int):
super().__init__()
self.norm = RMSNorm(txt_dim)
self.linear_1 = nn.Linear(txt_dim, dim)
self.linear_2 = nn.Linear(dim, dim)
def forward(self, x: Tensor) -> Tensor:
return self.linear_2(F.gelu(self.linear_1(self.norm(x)), approximate="tanh"))
class PositionalEncoding(nn.Module):
def __init__(self, dim, axdims: list[int], theta: float = 1e2, ntk: float = 1.0):
super().__init__()
self.axdims = axdims
self.theta = theta
self.ntk = ntk
def forward(self, pos: Tensor) -> Tensor:
return torch.cat(
[
rope(pos[..., i], d, self.theta, self.ntk)
for i, d in enumerate(self.axdims)
],
dim=-3,
)
class RMSNorm(nn.Module):
"""RMSNorm with effective scale ``weight + 1`` (``weight`` initialized to 0),
computed in fp32. The parameter is named ``weight`` to match the released
checkpoint; the ``+ 1`` is applied in the forward."""
def __init__(self, features: int, eps: float = 1e-05, device: torch.device = None):
super().__init__()
self.features = features
self.eps = eps
self.weight = nn.Parameter(
torch.zeros(features, device=device, dtype=torch.float32)
)
def forward(self, x: Tensor) -> Tensor:
t, dtype = x.float(), x.dtype
t = F.rms_norm(
t, (self.features,), eps=self.eps, weight=(self.weight.float() + 1.0)
)
return t.to(dtype)
class SwiGLU(nn.Module):
def __init__(
self, features: int, multiplier: int, bias: bool = False, multiple: int = 128
):
super().__init__()
mlpdim = int(2 * features / 3) * multiplier
mlpdim = multiple * ((mlpdim + multiple - 1) // multiple)
# Tensor-parallel: gate/up shard the hidden dim by column, down all-reduces.
self.gate = ColumnParallelLinear(
features, mlpdim, bias=bias, gather_output=False
)
self.up = ColumnParallelLinear(features, mlpdim, bias=bias, gather_output=False)
self.down = RowParallelLinear(
mlpdim, features, bias=bias, input_is_parallel=True
)
def forward(self, x: Tensor) -> Tensor:
gate, _ = self.gate(x)
up, _ = self.up(x)
out, _ = self.down(F.silu(gate) * up)
return out
class Attention(nn.Module):
def __init__(self, dim: int, heads: int, kvheads: int = None, bias: bool = False):
super().__init__()
self.heads = heads
self.kvheads = kvheads if kvheads is not None else heads
self.headdim = dim // self.heads
# Tensor-parallel: q/k/v/gate shard heads by column, to_out all-reduces.
# Parameter names match the released checkpoint (to_q/to_k/to_v/to_gate,
# norm_q/norm_k, to_out.0) so the checkpoint loads with an identity mapping.
tp = get_tp_world_size()
assert (
self.heads % tp == 0 and self.kvheads % tp == 0
), f"heads={self.heads}, kvheads={self.kvheads} must be divisible by tp={tp}"
self.local_heads = self.heads // tp
self.local_kvheads = self.kvheads // tp
self.to_q = ColumnParallelLinear(
dim, self.headdim * self.heads, bias=bias, gather_output=False
)
self.to_k = ColumnParallelLinear(
dim, self.headdim * self.kvheads, bias=bias, gather_output=False
)
self.to_v = ColumnParallelLinear(
dim, self.headdim * self.kvheads, bias=bias, gather_output=False
)
self.to_gate = ColumnParallelLinear(dim, dim, bias=bias, gather_output=False)
self.norm_q = RMSNorm(self.headdim)
self.norm_k = RMSNorm(self.headdim)
# to_out is a ModuleList ([linear]) so the param is to_out.0.weight, matching
# the diffusers Attention layout in the released checkpoint.
self.to_out = nn.ModuleList(
[RowParallelLinear(dim, dim, bias=bias, input_is_parallel=True)]
)
# Native GQA flash via the platform backend; parameterless.
self.attn = USPAttention(
num_heads=self.local_heads,
head_size=self.headdim,
num_kv_heads=self.local_kvheads,
dropout_rate=0,
softmax_scale=None,
causal=False,
)
def forward(
self,
qkv: Tensor,
freqs: Tensor | None = None,
key_mask: Tensor | None = None,
mask_meta: dict | None = None,
num_replicated_prefix: int = 0,
skip_sequence_parallel: bool = False,
) -> Tensor:
q, _ = self.to_q(qkv)
k, _ = self.to_k(qkv)
v, _ = self.to_v(qkv)
gate, _ = self.to_gate(qkv)
hd = self.headdim
# Fast path: fuse RMSNorm(q), RMSNorm(k) and RoPE into one in-place kernel on
# the [B, S, H, D] layout USPAttention consumes (also skips the [B, H, L, D]
# transpose round-trip the eager path needs). Eager fallback below preserves
# parity off CUDA / for unsupported dtypes.
if (
freqs is not None
and q.is_cuda
and q.dtype in (torch.float16, torch.bfloat16)
and _fused_qknorm_rope_enabled()
and _can_use_fused_qknorm_rope(hd, q.dtype)
):
from sglang.jit_kernel.diffusion.qknorm_rope import (
fused_inplace_qknorm_rope,
)
b, s = qkv.shape[0], qkv.shape[1]
q = q.view(b, s, self.local_heads, hd)
k = k.view(b, s, self.local_kvheads, hd)
v = v.view(b, s, self.local_kvheads, hd)
positions = torch.arange(s, device=q.device, dtype=torch.long)
if b > 1:
positions = positions.repeat(b)
fused_inplace_qknorm_rope(
q.reshape(-1, self.local_heads, hd),
k.reshape(-1, self.local_kvheads, hd),
(self.norm_q.weight.float() + 1.0).to(q.dtype),
(self.norm_k.weight.float() + 1.0).to(k.dtype),
_qknorm_rope_cos_sin_cache(freqs),
positions,
is_neox=False,
eps=self.norm_q.eps,
head_dim=hd,
rope_dim=hd,
)
out = self.attn(
q,
k,
v,
attn_mask=key_mask,
attn_mask_meta=mask_meta,
num_replicated_prefix=num_replicated_prefix,
skip_sequence_parallel_override=skip_sequence_parallel,
).flatten(2)
else:
q, k, v = (
rearrange(q, "B L (H D) -> B H L D", H=self.local_heads),
rearrange(k, "B L (H D) -> B H L D", H=self.local_kvheads),
rearrange(v, "B L (H D) -> B H L D", H=self.local_kvheads),
)
q, k = self.norm_q(q), self.norm_k(k)
if freqs is not None:
q, k = ropeapply(q, k, freqs)
# USPAttention expects [B, S, H, D]; a [B, S] key mask + varlen metadata
# routes a ragged batch through the FA varlen fast path, else maskless.
out = self.attn(
q.transpose(1, 2).contiguous(),
k.transpose(1, 2).contiguous(),
v.transpose(1, 2).contiguous(),
attn_mask=key_mask,
attn_mask_meta=mask_meta,
num_replicated_prefix=num_replicated_prefix,
skip_sequence_parallel_override=skip_sequence_parallel,
).flatten(2)
out, _ = self.to_out[0](out * F.sigmoid(gate))
return out
class LastLayer(nn.Module):
def __init__(self, features: int, patch: int, channels: int):
super().__init__()
self.norm = RMSNorm(features)
self.linear = nn.Linear(features, patch * patch * channels, bias=True)
self.scale_shift_table = nn.Parameter(torch.zeros(2, features))
def forward(self, x: Tensor, tvec: Tensor) -> Tensor:
mod = tvec + rearrange(self.scale_shift_table, "two d -> 1 two d")
scale, shift = mod.chunk(2, dim=1)
x = norm_scale_shift(x, self.norm.weight + 1, scale, shift, self.norm.eps)
x = self.linear(x)
return x
class TextFusionBlock(nn.Module):
def __init__(
self,
features: int,
heads: int,
multiplier: int,
bias: bool = False,
kvheads: int = None,
):
super().__init__()
self.norm1 = RMSNorm(features)
self.norm2 = RMSNorm(features)
self.attn = Attention(dim=features, heads=heads, bias=bias, kvheads=kvheads)
self.ff = SwiGLU(features, multiplier, bias)
def forward(
self,
x: Tensor,
key_mask: Tensor | None = None,
mask_meta: dict | None = None,
) -> Tensor:
# Text-fusion runs on the full replicated text, so skip the SP all-to-all.
x = x + self.attn(
self.norm1(x),
key_mask=key_mask,
mask_meta=mask_meta,
skip_sequence_parallel=True,
)
x = x + self.ff(self.norm2(x))
return x
class TextFusionTransformer(nn.Module):
"""Fuses `num_txt_layers` selected encoder hidden-state layers into one.
Depth is fixed at 2 layerwise + 2 refiner blocks; `num_txt_layers` is the
projector input width (the layer axis), NOT the transformer depth.
"""
def __init__(
self,
num_txt_layers: int,
txt_dim: int,
heads: int,
multiplier: int,
bias: bool = False,
kvheads: int = None,
):
super().__init__()
self.layerwise_blocks = nn.ModuleList(
[
TextFusionBlock(txt_dim, heads, multiplier, bias, kvheads)
for _ in range(2)
]
)
self.projector = nn.Linear(num_txt_layers, 1, bias=False)
self.refiner_blocks = nn.ModuleList(
[
TextFusionBlock(txt_dim, heads, multiplier, bias, kvheads)
for _ in range(2)
]
)
def forward(
self,
x: Tensor,
key_mask: Tensor | None = None,
mask_meta: dict | None = None,
) -> Tensor:
b, l, n, d = x.shape
x = x.reshape(b * l, n, d)
for block in self.layerwise_blocks:
x = block(x.contiguous())
x = rearrange(x, "(b l) n d -> b l d n", b=b, l=l)
x = self.projector(x)
x = x.squeeze(-1)
for block in self.refiner_blocks:
x = block(x, key_mask=key_mask, mask_meta=mask_meta)
return x
class SingleStreamBlock(nn.Module):
def __init__(
self,
features: int,
heads: int,
multiplier: int,
bias: bool = False,
kvheads: int = None,
):
super().__init__()
# (6, features) modulation table added to the timestep projection (AdaLN-single),
# stored directly on the block to match the released checkpoint.
self.scale_shift_table = nn.Parameter(torch.zeros(6, features))
self.norm1 = RMSNorm(features)
self.norm2 = RMSNorm(features)
self.attn = Attention(dim=features, heads=heads, bias=bias, kvheads=kvheads)
self.ff = SwiGLU(features, multiplier, bias)
def forward(
self,
hidden_states: Tensor,
vec: Tensor,
freqs: Tensor,
key_mask: Tensor | None = None,
mask_meta: dict | None = None,
num_replicated_prefix: int = 0,
) -> Tensor:
mod = vec + self.scale_shift_table.reshape(-1)
prescale, preshift, pregate, postscale, postshift, postgate = mod.chunk(
6, dim=-1
)
hidden_states = hidden_states + pregate * self.attn(
norm_scale_shift(
hidden_states,
self.norm1.weight + 1,
prescale,
preshift,
self.norm1.eps,
),
freqs,
key_mask,
mask_meta,
num_replicated_prefix=num_replicated_prefix,
)
hidden_states = hidden_states + postgate * self.ff(
norm_scale_shift(
hidden_states,
self.norm2.weight + 1,
postscale,
postshift,
self.norm2.eps,
)
)
return hidden_states
# --------------------------------------------------------------------------- #
# Top-level model
# --------------------------------------------------------------------------- #
class Krea2Transformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
"""K2 single-stream MMDiT for the SGLang diffusion runtime.
Attribute names follow the released K2 checkpoint, so weights load with an
identity ``param_names_mapping``.
"""
_fsdp_shard_conditions = []
_compile_conditions = []
param_names_mapping = Krea2DitConfig().arch_config.param_names_mapping
reverse_param_names_mapping = {}
def __init__(
self,
config: Krea2DitConfig,
hf_config: dict[str, Any],
quant_config: Optional[Any] = None,
) -> None:
super().__init__(config=config, hf_config=hf_config)
ac = config.arch_config
self.arch_config = ac
self.hidden_size = ac.features
self.num_attention_heads = ac.heads
self.num_channels_latents = ac.channels
self.patch = ac.patch
self.channels = ac.channels
self.tdim = ac.tdim
head_dim = ac.features // ac.heads
axes = list(ac.axes_dims)
assert sum(axes) == head_dim, f"sum(axes)={sum(axes)}, head_dim={head_dim}"
assert all(a % 2 == 0 for a in axes), f"axes={axes}"
self.posemb = PositionalEncoding(ac.features, axes, theta=ac.theta, ntk=1.0)
self.img_in = nn.Linear(ac.channels * ac.patch**2, ac.features, bias=True)
self.transformer_blocks = nn.ModuleList(
[
SingleStreamBlock(
ac.features, ac.heads, ac.multiplier, ac.bias, ac.kvheads
)
for _ in range(ac.layers)
]
)
self.time_embed = TimeEmbed(ac.tdim, ac.features)
self.text_fusion = TextFusionTransformer(
ac.txtlayers,
ac.txtdim,
ac.txtheads,
ac.multiplier,
ac.bias,
ac.txtkvheads,
)
self.txt_in = TxtIn(ac.txtdim, ac.features)
self.final_layer = LastLayer(ac.features, ac.patch, ac.channels)
# GELU(tanh) is applied in the forward; the linear matches time_mod_proj.weight.
self.time_mod_proj = nn.Linear(ac.features, ac.features * 6)
self.seq_multiple_of = ac.seq_multiple_of
# The 28 single-stream blocks (the ~24GB bulk) are streamed layer-by-layer
# under --dit-layerwise-offload, keeping only a small working set resident.
self.layer_names = ["transformer_blocks"]
def _forward_impl(
self,
img: Tensor,
context: Tensor,
t: Tensor,
pos: Tensor,
mask: Tensor | None = None,
) -> Tensor:
img = self.img_in(img)
t = self.time_embed(temb(t, self.tdim, device=img.device, dtype=img.dtype))
tvec = self.time_mod_proj(F.gelu(t, approximate="tanh"))
# A single or same-prompt batch has no padding, so attention runs maskless
# (native-GQA flash). A ragged batch builds varlen metadata from the
# key mask and takes the FA varlen path instead.
txt_key = txt_meta = joint_key = joint_meta = None
if mask is not None and not bool(mask.all()):
txt_key = mask[:, : context.shape[1]]
txt_meta = build_varlen_mask_meta(txt_key)
joint_key = mask
joint_meta = build_varlen_mask_meta(mask)
context = self.text_fusion(context, key_mask=txt_key, mask_meta=txt_meta)
context = self.txt_in(context)
txtlen, imglen = context.shape[1], img.shape[1]
combined = torch.cat((context, img), dim=1)
freqs = self.posemb(pos)
# Under SP the image tokens are sharded across ranks while the text prefix
# stays replicated; keep the leading txtlen tokens out of the all-to-all.
num_replicated_prefix = txtlen if get_sp_world_size() > 1 else 0
for block in self.transformer_blocks:
combined = block(
combined,
tvec,
freqs,
joint_key,
joint_meta,
num_replicated_prefix=num_replicated_prefix,
)
final = self.final_layer(combined, t)
output = final[:, txtlen : txtlen + imglen, :]
return output
def forward(
self,
hidden_states: Tensor,
encoder_hidden_states: Tensor,
timestep: Tensor,
encoder_hidden_states_image=None,
guidance=None,
pos: Tensor = None,
mask: Tensor = None,
**kwargs,
) -> Tensor:
return self._forward_impl(
img=hidden_states,
context=encoder_hidden_states,
t=timestep,
pos=pos,
mask=mask,
)
EntryClass = [Krea2Transformer2DModel]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,269 @@
# Copied and adapted from: mossVG/mova/diffusion/models/wan_audio_dit.py
# SPDX-License-Identifier: Apache-2.0
#
# NOTE: This module reuses common functions from mova_video_dit.py to reduce code duplication.
# Audio-specific functions (precompute_freqs_cis_1d, legacy_precompute_freqs_cis_1d) are kept here.
import math
from typing import Any, Optional, Tuple
import torch
import torch.nn as nn
from einops import rearrange
from torch.distributed.tensor import DTensor
from sglang.multimodal_gen.configs.models.dits.mova_audio import MOVAAudioConfig
from sglang.multimodal_gen.runtime.layers.linear import ReplicatedLinear
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
# Reuse common functions and classes from mova_video_dit
from .mova_video_dit import DiTBlock, precompute_freqs_cis, sinusoidal_embedding_1d
# Audio-specific positional encoding functions
def legacy_precompute_freqs_cis_1d(
dim: int,
end: int = 16384,
theta: float = 10000.0,
base_tps=4.0,
target_tps=44100 / 2048,
):
s = float(base_tps) / float(target_tps)
# 1d rope precompute
f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta, s)
# No positional encoding is applied to the remaining dimensions
no_freqs_cis = precompute_freqs_cis(dim // 3, end, theta, s)
no_freqs_cis = torch.ones_like(no_freqs_cis)
return f_freqs_cis, no_freqs_cis, no_freqs_cis
def precompute_freqs_cis_1d(dim: int, end: int = 16384, theta: float = 10000.0):
f_freqs_cis = precompute_freqs_cis(dim, end, theta)
return f_freqs_cis.chunk(3, dim=-1)
class Head(nn.Module):
def __init__(
self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float
):
super().__init__()
self.dim = dim
self.patch_size = patch_size
self.norm = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
self.head = ReplicatedLinear(dim, out_dim * math.prod(patch_size))
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
def forward(self, x, t_mod):
if len(t_mod.shape) == 3:
shift, scale = (
self.modulation.unsqueeze(0).to(dtype=t_mod.dtype, device=t_mod.device)
+ t_mod.unsqueeze(2)
).chunk(2, dim=2)
x, _ = self.head(self.norm(x) * (1 + scale.squeeze(2)) + shift.squeeze(2))
else:
# NOTE: t_mod was originally [B, C]. This works correctly with broadcasting when B=1, but it won't match [1, 2, C] when B > 1.
shift, scale = (
self.modulation.to(dtype=t_mod.dtype, device=t_mod.device)
+ t_mod.unsqueeze(1)
).chunk(2, dim=1)
x, _ = self.head(self.norm(x) * (1 + scale) + shift)
return x
class Conv1dLocalIsland(nn.Conv1d):
"""Inherits from Conv1d and overrides forward.
- Parameters remain as DTensors (optimizer consistency is maintained).
- In the forward pass, x, weight, and bias are aggregated as Replicate,
and then local convolution is performed via to_local.
- The output is then redistributed as a DTensor (default is Replicate,
placements can be customized).
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def forward(self, input):
if isinstance(input, DTensor):
x_local = input.to_local() # type: ignore[attr-defined]
w_local = self.weight.to_local() # type: ignore[attr-defined]
b_local = (
self.bias.to_local() if self.bias is not None else None # type: ignore[attr-defined]
)
return self._conv_forward(x_local, w_local, b_local)
else:
return super().forward(input)
class WanAudioModel(CachableDiT, LayerwiseOffloadableModuleMixin):
_fsdp_shard_conditions = MOVAAudioConfig()._fsdp_shard_conditions
_compile_conditions = MOVAAudioConfig()._compile_conditions
_supported_attention_backends = MOVAAudioConfig()._supported_attention_backends
param_names_mapping = MOVAAudioConfig().param_names_mapping
reverse_param_names_mapping = MOVAAudioConfig().reverse_param_names_mapping
lora_param_names_mapping = MOVAAudioConfig().lora_param_names_mapping
def __init__(
self,
config: MOVAAudioConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config=config, hf_config=hf_config)
# Extract parameters from config
dim = config.dim
in_dim = config.in_dim
ffn_dim = config.ffn_dim
out_dim = config.out_dim
text_dim = config.text_dim
freq_dim = config.freq_dim
eps = config.eps
patch_size = config.patch_size
num_heads = config.num_heads
num_layers = config.num_layers
has_image_pos_emb = config.has_image_pos_emb
has_ref_conv = config.has_ref_conv
separated_timestep = config.separated_timestep
require_vae_embedding = config.require_vae_embedding
require_clip_embedding = config.require_clip_embedding
fuse_vae_embedding_in_latents = config.fuse_vae_embedding_in_latents
vae_type = config.vae_type
self.dim = dim
self.freq_dim = freq_dim
self.patch_size = patch_size
self.separated_timestep = separated_timestep
self.require_vae_embedding = require_vae_embedding
self.require_clip_embedding = require_clip_embedding
self.fuse_vae_embedding_in_latents = fuse_vae_embedding_in_latents
self.vae_type = vae_type
# self.patch_embedding = nn.Conv3d(
# in_dim, dim, kernel_size=patch_size, stride=patch_size)
self.patch_embedding = Conv1dLocalIsland(
in_dim, dim, kernel_size=patch_size, stride=patch_size
)
self.text_embedding = MLP(
text_dim,
dim,
output_dim=dim,
act_type="gelu_pytorch_tanh",
quant_config=quant_config,
)
self.time_embedding = MLP(
freq_dim, dim, output_dim=dim, act_type="silu", quant_config=quant_config
)
# Preserve state_dict keys (time_projection.1.weight/bias).
self.time_projection = nn.Sequential(
nn.SiLU(), ReplicatedLinear(dim, dim * 6, quant_config=quant_config)
)
self.blocks = nn.ModuleList(
[
DiTBlock(dim, num_heads, ffn_dim, eps, quant_config=quant_config)
for _ in range(num_layers)
]
)
self.head = Head(dim, out_dim, patch_size, eps)
self.num_heads = num_heads
self.freqs = None
self.img_pos_emb = None
if has_ref_conv:
self.ref_conv = nn.Conv2d(16, dim, kernel_size=(2, 2), stride=(2, 2))
self.has_image_pos_emb = has_image_pos_emb
self.has_ref_conv = has_ref_conv
self.hidden_size = dim
self.num_attention_heads = num_heads
self.num_channels_latents = out_dim
self.layer_names = ["blocks"]
self.cnt = 0
self.teacache_thresh = 0
self.coefficients = []
self.accumulated_rel_l1_distance = 0
self.previous_modulated_input = None
self.previous_resiual = None
self.previous_e0_even = None
self.previous_e0_odd = None
self.previous_residual_even = None
self.previous_residual_odd = None
self.is_even = False
self.should_calc_even = True
self.should_calc_odd = True
self.accumulated_rel_l1_distance_even = 0
self.accumulated_rel_l1_distance_odd = 0
self.__post_init__()
def _init_freqs(self):
if self.freqs is not None:
return
head_dim = self.dim // self.num_heads
if self.vae_type == "dac":
self.freqs = precompute_freqs_cis_1d(head_dim)
else:
raise ValueError(f"Invalid VAE type: {self.vae_type}")
def patchify(
self,
x: torch.Tensor,
control_camera_latents_input: Optional[torch.Tensor] = None,
):
x = self.patch_embedding(x)
grid_size = x.shape[2:]
x = rearrange(x, "b c f -> b f c").contiguous()
return x, grid_size # x, grid_size: (f)
def unpatchify(self, x: torch.Tensor, grid_size: tuple[int]):
return rearrange(
x, "b f (p c) -> b c (f p)", f=grid_size[0], p=self.patch_size[0]
)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
) -> torch.Tensor:
# MOVA audio uses x/context naming historically.
x = hidden_states
context = (
encoder_hidden_states[0]
if isinstance(encoder_hidden_states, list)
else encoder_hidden_states
)
t = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, timestep))
t_proj, _ = self.time_projection(t)
t_mod = t_proj.unflatten(1, (6, self.dim))
context = self.text_embedding(context)
x, (f,) = self.patchify(x)
freqs = (
torch.cat(
[
self.freqs[0][:f].view(f, -1).expand(f, -1),
self.freqs[1][:f].view(f, -1).expand(f, -1),
self.freqs[2][:f].view(f, -1).expand(f, -1),
],
dim=-1,
)
.reshape(f, 1, -1)
.to(x.device)
)
for block in self.blocks:
x = block(x, context, t_mod, freqs)
x = self.head(x, t)
x = self.unpatchify(x, (f,))
return x
EntryClass = WanAudioModel
@@ -0,0 +1,595 @@
# Copied and adapted from: mossVG/mova/diffusion/models/wan_video_dit.py
# SPDX-License-Identifier: Apache-2.0
#
# NOTE: This module shares common functions (sinusoidal_embedding_1d, precompute_freqs_cis, etc.)
# with wanvideo.py. These functions are kept here for MOVA-specific model architecture,
# but could be refactored to a common module in the future.
import math
from typing import Any, Tuple
import torch
import torch.nn as nn
from einops import rearrange
from torch.distributed.tensor import DTensor
from sglang.multimodal_gen.configs.models.dits.mova_video import MOVAVideoConfig
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention, USPAttention
# Reuse SGLang's optimized RMSNorm instead of torch.nn.RMSNorm or custom SlowRMSNorm
from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNormScaleShift,
RMSNorm,
ScaleResidualLayerNormScaleShift,
tensor_parallel_rms_norm,
)
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.mlp import MLP
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.platforms import current_platform
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# @torch.compile(fullgraph=True)
def modulate(x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor):
return x * (1 + scale) + shift
def sinusoidal_embedding_1d(dim, position):
sinusoid = torch.outer(
position.type(torch.float64),
torch.pow(
10000,
-torch.arange(dim // 2, dtype=torch.float64, device=position.device).div(
dim // 2
),
),
)
x = torch.cat([torch.cos(sinusoid), torch.sin(sinusoid)], dim=1)
return x.to(position.dtype)
def precompute_freqs_cis_3d(dim: int, end: int = 1024, theta: float = 10000.0):
# 3d rope precompute
f_freqs_cis = precompute_freqs_cis(dim - 2 * (dim // 3), end, theta)
h_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
w_freqs_cis = precompute_freqs_cis(dim // 3, end, theta)
return f_freqs_cis, h_freqs_cis, w_freqs_cis
def precompute_freqs_cis(
dim: int, end: int = 1024, theta: float = 10000.0, s: float = 1.0
):
# 1d rope precompute
# Note: s parameter is used for audio-specific scaling (e.g., tps adjustment)
freqs = 1.0 / (theta ** (torch.arange(0, dim, 2)[: (dim // 2)].double() / dim))
pos = torch.arange(end, dtype=torch.float64, device=freqs.device) * s
freqs = torch.outer(pos, freqs)
freqs_cis = torch.polar(torch.ones_like(freqs), freqs) # complex64
return freqs_cis
def rope_apply(x, freqs, num_heads):
x = rearrange(x, "b s (n d) -> b s n d", n=num_heads)
x_out = torch.view_as_complex(
x.to(torch.float64).reshape(x.shape[0], x.shape[1], x.shape[2], -1, 2)
)
x_out = torch.view_as_real(x_out * freqs).flatten(2)
return x_out.to(x.dtype)
def rope_apply_head_dim(x, freqs, head_dim):
x = rearrange(x, "b s (n d) -> b s n d", d=head_dim)
x_out = torch.view_as_complex(
x.to(torch.float64).reshape(x.shape[0], x.shape[1], x.shape[2], -1, 2)
)
# print(f"{x_out.shape = }, {freqs.shape = }")
x_out = torch.view_as_real(x_out * freqs).flatten(2)
return x_out.to(x.dtype)
class SelfAttention(nn.Module):
"""
Self-Attention module for MOVA DiT with Sequence Parallelism support.
SP is handled at the pipeline level (latents are pre-sharded before DiT forward).
USPAttention internally handles the all-to-all communication for distributed attention.
Input x should already be the local shard [B, S_local, D] when SP is enabled.
"""
def __init__(
self,
dim: int,
num_heads: int,
eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.tp_size = get_tp_world_size()
if self.num_heads % self.tp_size != 0:
raise ValueError(
f"num_heads ({self.num_heads}) must be divisible by tp_size ({self.tp_size})."
)
self.num_heads_per_rank = self.num_heads // self.tp_size
# TP strategy: shard Q/K/V over heads (column-parallel), then row-parallel output.
self.q = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.k = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.v = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.o = RowParallelLinear(
dim, dim, bias=True, input_is_parallel=True, quant_config=quant_config
)
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
self.attn = USPAttention(
# Local heads per TP rank.
num_heads=self.num_heads_per_rank,
head_size=self.head_dim,
causal=False,
softmax_scale=None,
)
def forward(self, x, freqs, attn_mask_meta=None):
"""
Forward pass for self-attention.
Args:
x: Input tensor [B, S_local, D] - already sharded by SP when SP > 1
freqs: RoPE frequencies [S_local, 1, head_dim] - should match x's sequence length
attn_mask_meta: sp_shard tail-pad meta; excludes SP padding from attention
Returns:
Output tensor [B, S_local, D]
"""
if isinstance(freqs, DTensor):
freqs = freqs.to_local()
# Compute Q, K, V on local sequence
q, _ = self.q(x)
k, _ = self.k(x)
v, _ = self.v(x)
# RMSNorm over sharded hidden dimension.
if self.tp_size > 1:
q = tensor_parallel_rms_norm(q, self.norm_q)
k = tensor_parallel_rms_norm(k, self.norm_k)
else:
q = self.norm_q(q)
k = self.norm_k(k)
# Apply RoPE
q = rope_apply_head_dim(q, freqs, self.head_dim)
k = rope_apply_head_dim(k, freqs, self.head_dim)
# USPAttention expects [B, S_local, H, D] format
q = rearrange(q, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
k = rearrange(k, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
v = rearrange(v, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
# USPAttention handles SP communication internally; the tail meta keeps
# SP padding out of the softmax.
out = self.attn(q, k, v, attn_mask_meta=attn_mask_meta)
out = rearrange(out, "b s n d -> b s (n d)")
out, _ = self.o(out)
return out
class CrossAttention(nn.Module):
"""
Cross-Attention module for MOVA DiT.
Cross-attention does NOT require SP communication because:
- Query comes from the main sequence (already sharded by SP)
- Key/Value come from context (text embeddings, which are replicated across all ranks)
Uses LocalAttention instead of USPAttention for efficiency.
"""
def __init__(
self,
dim: int,
num_heads: int,
eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.head_dim = dim // num_heads
self.tp_size = get_tp_world_size()
if self.num_heads % self.tp_size != 0:
raise ValueError(
f"num_heads ({self.num_heads}) must be divisible by tp_size ({self.tp_size})."
)
self.num_heads_per_rank = self.num_heads // self.tp_size
self.q = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.k = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.v = ColumnParallelLinear(
dim, dim, bias=True, gather_output=False, quant_config=quant_config
)
self.o = RowParallelLinear(
dim, dim, bias=True, input_is_parallel=True, quant_config=quant_config
)
self.norm_q = RMSNorm(dim, eps=eps)
self.norm_k = RMSNorm(dim, eps=eps)
# Use LocalAttention for cross-attention (no SP communication needed)
self.attn = LocalAttention(
num_heads=self.num_heads_per_rank,
head_size=self.head_dim,
causal=False,
softmax_scale=None,
)
def forward(self, x: torch.Tensor, y: torch.Tensor):
"""
Forward pass for cross-attention.
Args:
x: Query tensor [B, S_local, D] - the main sequence (sharded by SP)
y: Context tensor [B, S_ctx, D] - text/image embeddings (replicated)
Returns:
Output tensor [B, S_local, D]
"""
ctx = y
q, _ = self.q(x)
k, _ = self.k(ctx)
v, _ = self.v(ctx)
if self.tp_size > 1:
q = tensor_parallel_rms_norm(q, self.norm_q)
k = tensor_parallel_rms_norm(k, self.norm_k)
else:
q = self.norm_q(q)
k = self.norm_k(k)
q = rearrange(q, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
k = rearrange(k, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
v = rearrange(v, "b s (n d) -> b s n d", n=self.num_heads_per_rank)
x = self.attn(q, k, v)
x = rearrange(x, "b s n d -> b s (n d)")
x, _ = self.o(x)
return x
class MulAdd(nn.Module):
def __init__(self):
super().__init__()
def forward(self, x, gate, residual):
return residual + gate * x
class DiTBlock(nn.Module):
def __init__(
self,
dim: int,
num_heads: int,
ffn_dim: int,
eps: float = 1e-6,
quant_config: QuantizationConfig | None = None,
):
super().__init__()
self.dim = dim
self.num_heads = num_heads
self.ffn_dim = ffn_dim
self.self_attn = SelfAttention(dim, num_heads, eps, quant_config=quant_config)
self.cross_attn = CrossAttention(dim, num_heads, eps, quant_config=quant_config)
self.norm1 = LayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
self.self_attn_norm = nn.LayerNorm(dim, eps=eps)
# Fused: residual + 1 * cross_attn_out → layernorm + scale/shift
# Replaces the old norm2 (LayerNormScaleShift) + residual add for cross-attention
self.cross_attn_residual_norm = ScaleResidualLayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
self.ffn = MLP(
dim,
ffn_dim,
output_dim=dim,
act_type="gelu_pytorch_tanh",
quant_config=quant_config,
)
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
self.mlp_residual = MulAdd()
def forward(self, x, context, t_mod, freqs, attn_mask_meta=None):
has_seq = len(t_mod.shape) == 4
chunk_dim = 2 if has_seq else 1
# msa: multi-head self-attention mlp: multi-layer perceptron
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod
).chunk(6, dim=chunk_dim)
if has_seq:
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
shift_msa.squeeze(2),
scale_msa.squeeze(2),
gate_msa.squeeze(2),
shift_mlp.squeeze(2),
scale_mlp.squeeze(2),
gate_mlp.squeeze(2),
)
orig_dtype = x.dtype
# 1. Self-attention, fuse:
# - layernorm(x) * (1 + scale_msa) + shift_msa
input_x = self.norm1(x, shift_msa, scale_msa)
# 2. torch.compile may fuse mlp_residual and self_attn_norm
x = self.mlp_residual(
self.self_attn(input_x, freqs, attn_mask_meta=attn_mask_meta), gate_msa, x
)
norm_x = self.self_attn_norm(x)
# 3. Cross-attention, fuse:
# - x = x + 1 * cross_output
# - input_x = layernorm(x) * (1 + scale_mlp) + shift_mlp
cross_output = self.cross_attn(norm_x, context)
input_x, x = self.cross_attn_residual_norm(
x, cross_output, 1, shift_mlp, scale_mlp
)
# 4. Feed-forward
x = self.mlp_residual(self.ffn(input_x), gate_mlp, x)
x = x.to(orig_dtype)
return x
class Head(nn.Module):
def __init__(
self, dim: int, out_dim: int, patch_size: Tuple[int, int, int], eps: float
):
super().__init__()
self.dim = dim
self.patch_size = patch_size
self.norm = LayerNormScaleShift(
dim, eps=eps, elementwise_affine=False, dtype=torch.float32
)
# Output dim is small for MOVA; replicate to avoid TP shape coupling.
self.head = ReplicatedLinear(dim, out_dim * math.prod(patch_size))
self.modulation = nn.Parameter(torch.randn(1, 2, dim) / dim**0.5)
def forward(self, x, t_mod):
if len(t_mod.shape) == 3:
shift, scale = (
self.modulation.unsqueeze(0).to(dtype=t_mod.dtype, device=t_mod.device)
+ t_mod.unsqueeze(2)
).chunk(2, dim=2)
x, _ = self.head(self.norm(x, shift.squeeze(2), scale.squeeze(2)))
else:
shift, scale = (
self.modulation.to(dtype=t_mod.dtype, device=t_mod.device) + t_mod
).chunk(2, dim=1)
x, _ = self.head(self.norm(x, shift, scale))
return x
class Conv3dLocalIsland(nn.Conv3d):
"""
Inherits from Conv3d and overrides the forward method.
Key behaviors:
- Parameters are kept as DTensor to maintain optimizer consistency.
- The forward pass aggregates input, weight, and bias into a Replicate state,
then performs the convolution locally using to_local().
- The output is then redistributed as a DTensor (defaults to Replicate,
but placements can be customized).
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def forward(self, input):
if isinstance(input, DTensor):
# NOTE: DTensor typing stubs are incomplete; at runtime DTensor has
# to_local() and parameters may also be DTensor.
x_local = input.to_local() # type: ignore[attr-defined]
w_local = self.weight.to_local() # type: ignore[attr-defined]
b_local = (
self.bias.to_local() if self.bias is not None else None # type: ignore[attr-defined]
)
return self._conv_forward(x_local, w_local, b_local)
else:
return super().forward(input)
class WanModel(CachableDiT, LayerwiseOffloadableModuleMixin):
_fsdp_shard_conditions = MOVAVideoConfig()._fsdp_shard_conditions
_compile_conditions = MOVAVideoConfig()._compile_conditions
_supported_attention_backends = MOVAVideoConfig()._supported_attention_backends
param_names_mapping = MOVAVideoConfig().param_names_mapping
reverse_param_names_mapping = MOVAVideoConfig().reverse_param_names_mapping
lora_param_names_mapping = MOVAVideoConfig().lora_param_names_mapping
def __init__(
self,
config: MOVAVideoConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config=config, hf_config=hf_config)
# Extract parameters from config
dim = config.dim
in_dim = config.in_dim
ffn_dim = config.ffn_dim
out_dim = config.out_dim
text_dim = config.text_dim
freq_dim = config.freq_dim
eps = config.eps
patch_size = config.patch_size
num_heads = config.num_heads
num_layers = config.num_layers
has_image_pos_emb = config.has_image_pos_emb
has_ref_conv = config.has_ref_conv
separated_timestep = config.separated_timestep
require_vae_embedding = config.require_vae_embedding
require_clip_embedding = config.require_clip_embedding
fuse_vae_embedding_in_latents = config.fuse_vae_embedding_in_latents
self.dim = dim
self.freq_dim = freq_dim
self.patch_size = patch_size
self.separated_timestep = separated_timestep
self.require_vae_embedding = require_vae_embedding
self.require_clip_embedding = require_clip_embedding
self.fuse_vae_embedding_in_latents = fuse_vae_embedding_in_latents
self.patch_embedding = Conv3dLocalIsland(
in_dim, dim, kernel_size=patch_size, stride=patch_size
)
self.text_embedding = MLP(
text_dim,
dim,
output_dim=dim,
act_type="gelu_pytorch_tanh",
quant_config=quant_config,
)
self.time_embedding = MLP(
freq_dim, dim, output_dim=dim, act_type="silu", quant_config=quant_config
)
# Preserve state_dict keys (time_projection.1.weight/bias).
self.time_projection = nn.Sequential(
nn.SiLU(), ReplicatedLinear(dim, dim * 6, quant_config=quant_config)
)
self.blocks = nn.ModuleList(
[
DiTBlock(dim, num_heads, ffn_dim, eps, quant_config=quant_config)
for _ in range(num_layers)
]
)
self.head = Head(dim, out_dim, patch_size, eps)
self.num_heads = num_heads
self.freqs = None
if has_ref_conv:
self.ref_conv = nn.Conv2d(16, dim, kernel_size=(2, 2), stride=(2, 2))
self.has_image_pos_emb = has_image_pos_emb
self.has_ref_conv = has_ref_conv
self.hidden_size = dim
self.num_attention_heads = num_heads
self.num_channels_latents = out_dim
self.layer_names = ["blocks"]
self.cnt = 0
self.teacache_thresh = 0
self.coefficients = []
self.accumulated_rel_l1_distance = 0
self.previous_modulated_input = None
self.previous_resiual = None
self.previous_e0_even = None
self.previous_e0_odd = None
self.previous_residual_even = None
self.previous_residual_odd = None
self.is_even = False
self.should_calc_even = True
self.should_calc_odd = True
self.accumulated_rel_l1_distance_even = 0
self.accumulated_rel_l1_distance_odd = 0
self.__post_init__()
def _init_freqs(self):
if self.freqs is not None:
return
head_dim = self.dim // self.num_heads
self.freqs = precompute_freqs_cis_3d(head_dim)
def patchify(
self, x: torch.Tensor, control_camera_latents_input: torch.Tensor | None = None
):
if current_platform.is_npu:
# torch.channels_last_3d is not supported on NPU
x = x.contiguous()
else:
# NOTE(dhyu): avoid slow_conv
x = x.contiguous(memory_format=torch.channels_last_3d)
x = self.patch_embedding(x)
grid_size = x.shape[2:]
x = rearrange(x, "b c f h w -> b (f h w) c").contiguous()
return x, grid_size # x, grid_size: (f, h, w)
def unpatchify(self, x: torch.Tensor, grid_size: tuple[int, int, int]):
return rearrange(
x,
"b (f h w) (x y z c) -> b c (f x) (h y) (w z)",
f=grid_size[0],
h=grid_size[1],
w=grid_size[2],
x=self.patch_size[0],
y=self.patch_size[1],
z=self.patch_size[2],
)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | list[torch.Tensor],
timestep: torch.LongTensor,
) -> torch.Tensor:
# MOVA code historically uses x/context/y/clip_feature naming.
x = hidden_states
context = (
encoder_hidden_states[0]
if isinstance(encoder_hidden_states, list)
else encoder_hidden_states
)
t = self.time_embedding(sinusoidal_embedding_1d(self.freq_dim, timestep))
t_proj, _ = self.time_projection(t)
t_mod = t_proj.unflatten(1, (6, self.dim))
context = self.text_embedding(context)
x, (f, h, w) = self.patchify(x)
freqs = (
torch.cat(
[
self.freqs[0][:f].view(f, 1, 1, -1).expand(f, h, w, -1),
self.freqs[1][:h].view(1, h, 1, -1).expand(f, h, w, -1),
self.freqs[2][:w].view(1, 1, w, -1).expand(f, h, w, -1),
],
dim=-1,
)
.reshape(f * h * w, 1, -1)
.to(x.device)
)
for block in self.blocks:
x = block(x, context, t_mod, freqs)
x = self.head(x, t)
x = self.unpatchify(x, (f, h, w))
return x
EntryClass = WanModel
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,394 @@
# SPDX-License-Identifier: Apache-2.0
import torch
import torch.nn as nn
import torch.nn.functional as F
from diffusers.models.embeddings import PixArtAlphaTextProjection, TimestepEmbedding
from sglang.multimodal_gen.configs.models.dits.sana import SanaConfig
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import MergedColumnParallelLinear
from sglang.multimodal_gen.runtime.layers.visual_embedding import Timesteps
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SanaCombinedTimestepSizeEmbeddings(nn.Module):
def __init__(self, embedding_dim):
super().__init__()
self.time_proj = Timesteps(
num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0
)
self.timestep_embedder = TimestepEmbedding(
in_channels=256, time_embed_dim=embedding_dim
)
def forward(self, timestep, hidden_dtype=None):
timesteps_proj = self.time_proj(timestep)
if hidden_dtype is not None:
timesteps_proj = timesteps_proj.to(dtype=hidden_dtype)
timesteps_emb = self.timestep_embedder(timesteps_proj)
return timesteps_emb
class SanaAdaLayerNormSingle(nn.Module):
def __init__(self, embedding_dim):
super().__init__()
self.emb = SanaCombinedTimestepSizeEmbeddings(embedding_dim)
self.silu = nn.SiLU()
self.linear = nn.Linear(embedding_dim, 6 * embedding_dim, bias=True)
def forward(self, timestep, hidden_dtype=None):
embedded_timestep = self.emb(timestep, hidden_dtype=hidden_dtype)
out = self.linear(self.silu(embedded_timestep))
return out, embedded_timestep
class SanaModulatedNorm(nn.Module):
def __init__(self, dim, eps=1e-6):
super().__init__()
self.norm = nn.LayerNorm(dim, elementwise_affine=False, eps=eps)
def forward(self, x, temb, scale_shift_table):
x = self.norm(x)
shift, scale = (scale_shift_table[None] + temb[:, None]).chunk(2, dim=1)
x = x * (1 + scale) + shift
return x
class GLUMBConv(nn.Module):
"""Gated Linear Unit with Multi-Branch Convolution."""
def __init__(self, in_channels, out_channels, expand_ratio=2.5):
super().__init__()
hidden_channels = int(expand_ratio * in_channels)
self.nonlinearity = nn.SiLU()
self.conv_inverted = nn.Conv2d(in_channels, hidden_channels * 2, 1, 1, 0)
self.conv_depth = nn.Conv2d(
hidden_channels * 2,
hidden_channels * 2,
3,
1,
1,
groups=hidden_channels * 2,
)
self.conv_point = nn.Conv2d(hidden_channels, out_channels, 1, 1, 0, bias=False)
def forward(self, hidden_states):
hidden_states = self.conv_inverted(hidden_states)
hidden_states = self.nonlinearity(hidden_states)
hidden_states = self.conv_depth(hidden_states)
hidden_states, gate = torch.chunk(hidden_states, 2, dim=1)
hidden_states = hidden_states * self.nonlinearity(gate)
hidden_states = self.conv_point(hidden_states)
return hidden_states
class SanaLinearAttention(nn.Module):
"""Linear attention with O(N*D^2) complexity instead of O(N^2*D)."""
def __init__(self, query_dim, num_heads, head_dim, bias=False):
super().__init__()
inner_dim = num_heads * head_dim
self.num_heads = num_heads
self.head_dim = head_dim
self.inner_dim = inner_dim
# Self-attention q/k/v share the same input -> one packed GEMM.
self.to_qkv = MergedColumnParallelLinear(
query_dim, [inner_dim, inner_dim, inner_dim], bias=bias, gather_output=True
)
self.to_out = nn.ModuleList(
[nn.Linear(inner_dim, query_dim, bias=True), nn.Identity()]
)
def forward(self, hidden_states):
B, S, _ = hidden_states.shape
qkv, _ = self.to_qkv(hidden_states)
query, key, value = qkv.split(
[self.inner_dim, self.inner_dim, self.inner_dim], dim=-1
)
query = query.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
key = key.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
value = value.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
query = F.relu(query)
key = F.relu(key)
kv = torch.matmul(key.transpose(-2, -1), value) # (B, H, D, D)
qkv = torch.matmul(query, kv) # (B, H, S, D)
key_sum = key.sum(dim=-2, keepdim=True) # (B, H, 1, D)
normalizer = torch.matmul(query, key_sum.transpose(-2, -1)).clamp(min=1e-6)
hidden_states = qkv / normalizer
hidden_states = hidden_states.transpose(1, 2).reshape(B, S, -1)
hidden_states = self.to_out[0](hidden_states)
return hidden_states
class SanaCrossAttention(nn.Module):
def __init__(self, query_dim, cross_attention_dim, num_heads, head_dim, bias=False):
super().__init__()
inner_dim = num_heads * head_dim
self.num_heads = num_heads
self.head_dim = head_dim
self.inner_dim = inner_dim
self.to_q = nn.Linear(query_dim, inner_dim, bias=bias)
# k/v share the (step-invariant) encoder input -> one packed GEMM.
self.to_kv = MergedColumnParallelLinear(
cross_attention_dim, [inner_dim, inner_dim], bias=bias, gather_output=True
)
self.to_out = nn.ModuleList(
[nn.Linear(inner_dim, query_dim, bias=True), nn.Identity()]
)
def forward(
self, hidden_states, encoder_hidden_states, encoder_attention_mask=None
):
B, S, _ = hidden_states.shape
T = encoder_hidden_states.shape[1]
query = self.to_q(hidden_states)
kv, _ = self.to_kv(encoder_hidden_states)
key, value = kv.split([self.inner_dim, self.inner_dim], dim=-1)
query = query.view(B, S, self.num_heads, self.head_dim).transpose(1, 2)
key = key.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
value = value.view(B, T, self.num_heads, self.head_dim).transpose(1, 2)
attn_mask = None
if encoder_attention_mask is not None:
attn_mask = encoder_attention_mask.bool()
attn_mask = attn_mask[:, None, None, :].expand(B, self.num_heads, S, T)
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=attn_mask
)
hidden_states = hidden_states.transpose(1, 2).reshape(B, S, -1)
hidden_states = self.to_out[0](hidden_states)
return hidden_states
class SanaTransformerBlock(nn.Module):
def __init__(
self,
dim,
num_attention_heads,
attention_head_dim,
num_cross_attention_heads,
cross_attention_head_dim,
cross_attention_dim,
mlp_ratio,
norm_eps,
attention_bias=False,
):
super().__init__()
self.scale_shift_table = nn.Parameter(torch.randn(6, dim) / dim**0.5)
self.norm1 = nn.LayerNorm(dim, elementwise_affine=False, eps=norm_eps)
self.attn1 = SanaLinearAttention(
query_dim=dim,
num_heads=num_attention_heads,
head_dim=attention_head_dim,
bias=attention_bias,
)
self.norm2 = nn.LayerNorm(dim, elementwise_affine=False, eps=norm_eps)
self.attn2 = SanaCrossAttention(
query_dim=dim,
cross_attention_dim=cross_attention_dim,
num_heads=num_cross_attention_heads,
head_dim=cross_attention_head_dim,
bias=True,
)
self.ff = GLUMBConv(in_channels=dim, out_channels=dim, expand_ratio=mlp_ratio)
def forward(
self,
hidden_states,
encoder_hidden_states,
timestep,
height,
width,
encoder_attention_mask=None,
):
batch_size = hidden_states.shape[0]
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.scale_shift_table[None] + timestep.reshape(batch_size, 6, -1)
).chunk(6, dim=1)
norm_hidden = self.norm1(hidden_states)
norm_hidden = norm_hidden * (1 + scale_msa) + shift_msa
attn_output = self.attn1(norm_hidden)
hidden_states = hidden_states + gate_msa * attn_output
attn_output = self.attn2(
hidden_states, encoder_hidden_states, encoder_attention_mask
)
hidden_states = hidden_states + attn_output
norm_hidden = self.norm2(hidden_states)
norm_hidden = norm_hidden * (1 + scale_mlp) + shift_mlp
norm_hidden = norm_hidden.unflatten(1, (height, width)).permute(0, 3, 1, 2)
ff_output = self.ff(norm_hidden)
ff_output = ff_output.flatten(2, 3).permute(0, 2, 1)
hidden_states = hidden_states + gate_mlp * ff_output
return hidden_states
class SanaTransformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
_fsdp_shard_conditions = [
lambda n, m: isinstance(m, SanaTransformerBlock),
]
_compile_conditions = [
lambda n, m: isinstance(m, SanaTransformerBlock),
]
param_names_mapping = SanaConfig().arch_config.param_names_mapping
reverse_param_names_mapping = {}
def __init__(self, config: SanaConfig, hf_config=None, **kwargs):
super().__init__(config, hf_config=hf_config or {}, **kwargs)
arch = config.arch_config
self.out_channels = arch.out_channels
self.patch_size = arch.patch_size
self.inner_dim = arch.num_attention_heads * arch.attention_head_dim
self.hidden_size = self.inner_dim
self.num_attention_heads = arch.num_attention_heads
self.num_channels_latents = arch.num_channels_latents
self.patch_embed = nn.ModuleDict(
{
"proj": nn.Conv2d(
arch.in_channels,
self.inner_dim,
kernel_size=arch.patch_size,
stride=arch.patch_size,
bias=True,
),
}
)
self.time_embed = SanaAdaLayerNormSingle(self.inner_dim)
self.caption_projection = PixArtAlphaTextProjection(
in_features=arch.caption_channels,
hidden_size=self.inner_dim,
)
self.caption_norm = RMSNorm(self.inner_dim)
self.transformer_blocks = nn.ModuleList(
[
SanaTransformerBlock(
dim=self.inner_dim,
num_attention_heads=arch.num_attention_heads,
attention_head_dim=arch.attention_head_dim,
num_cross_attention_heads=arch.num_cross_attention_heads,
cross_attention_head_dim=arch.cross_attention_head_dim,
cross_attention_dim=arch.cross_attention_dim,
mlp_ratio=arch.mlp_ratio,
norm_eps=arch.norm_eps,
attention_bias=False,
)
for _ in range(arch.num_layers)
]
)
self.scale_shift_table = nn.Parameter(
torch.randn(2, self.inner_dim) / self.inner_dim**0.5
)
self.norm_out = SanaModulatedNorm(self.inner_dim, eps=arch.norm_eps)
self.proj_out = nn.Linear(
self.inner_dim,
arch.patch_size * arch.patch_size * self.out_channels,
bias=True,
)
self.layer_names = ["transformer_blocks"]
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor = None,
timestep: torch.LongTensor = None,
guidance: torch.Tensor = None,
encoder_attention_mask: torch.Tensor = None,
**kwargs,
) -> torch.Tensor:
# Input validation - fail fast
if encoder_hidden_states is None:
raise ValueError("SANA forward pass requires encoder_hidden_states")
batch_size, channels, height, width = hidden_states.shape
p = self.patch_size
post_patch_height = height // p
post_patch_width = width // p
hidden_states = self.patch_embed["proj"](hidden_states)
hidden_states = hidden_states.flatten(2).transpose(1, 2)
timestep_emb, embedded_timestep = self.time_embed(
timestep, hidden_dtype=hidden_states.dtype
)
if isinstance(encoder_attention_mask, (list, tuple)):
encoder_attention_mask = encoder_attention_mask[0]
encoder_hidden_states = self.caption_projection(encoder_hidden_states)
if encoder_hidden_states.shape[0] != batch_size:
encoder_hidden_states = encoder_hidden_states.expand(
batch_size, -1, -1
).contiguous()
encoder_hidden_states = encoder_hidden_states.view(
batch_size, -1, hidden_states.shape[-1]
)
encoder_hidden_states = self.caption_norm(encoder_hidden_states)
if (
encoder_attention_mask is not None
and encoder_attention_mask.shape[0] != batch_size
):
encoder_attention_mask = encoder_attention_mask.expand(
batch_size, -1
).contiguous()
for block in self.transformer_blocks:
hidden_states = block(
hidden_states,
encoder_hidden_states,
timestep_emb,
post_patch_height,
post_patch_width,
encoder_attention_mask=encoder_attention_mask,
)
hidden_states = self.norm_out(
hidden_states, embedded_timestep, self.scale_shift_table
)
hidden_states = self.proj_out(hidden_states)
hidden_states = hidden_states.reshape(
batch_size, post_patch_height, post_patch_width, p, p, self.out_channels
)
hidden_states = hidden_states.permute(0, 5, 1, 3, 2, 4)
hidden_states = hidden_states.reshape(
batch_size, self.out_channels, height, width
)
return hidden_states
EntryClass = SanaTransformer2DModel
@@ -0,0 +1,906 @@
# SPDX-License-Identifier: Apache-2.0
import os
from typing import Callable, List, Optional, Tuple
import torch
import torch.nn as nn
from sglang.multimodal_gen.configs.models.dits.sana_wm import SanaWMConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
# Re-exported for back-compat: callers import these names from this module path.
from sglang.multimodal_gen.runtime.models.dits.sana_wm_components import ( # noqa: F401
_CACHE_TYPE_CONCAT,
_CACHE_TYPE_STATE,
_INT32_SAFE_CONV_ELEMENTS,
_NUM_STREAM_CACHE_SLOTS,
_SLOT_CAM_K,
_SLOT_CAM_V,
_SLOT_FFN_TCONV,
_SLOT_K,
_SLOT_SHORTCONV,
_SLOT_TYPE_FLAG,
_SLOT_V,
BidirectionalGDNUCPESinglePathLiteLA,
CaptionEmbedder,
GLUMBConvTemp,
MultiHeadCrossAttention,
PatchEmbedMS3D,
T2IFinalLayer,
TimestepEmbedder,
WanRotaryPosEmbed,
_apply_block_diagonal,
_apply_complex_rope,
_apply_ray_projmat,
_apply_rotary_emb_bhnd,
_apply_rotary_emb_dn,
_bidirectional_short_conv,
_build_ucpe_apply_fns,
_compute_fov_from_focal,
_compute_frame_gates,
_ConvLayer,
_downscale_to_reference_rms,
_flip_and_shift,
_gdn_chunk_scan_forward,
_gdn_scan_bidirectional,
_gdn_scan_cached,
_gdn_scan_forward,
_gdn_scan_forward_stateful,
_invert_SE3,
_log_sana_wm_triton_cam_gdn_fallback,
_log_sana_wm_triton_gdn_fallback,
_RMSNorm,
_sana_wm_chunk_boundaries_for_attention,
_sana_wm_chunk_index_from_chunk_size,
_sana_wm_chunked_attention,
_sana_wm_normalize_chunk_index,
_sana_wm_padded_scale,
_sana_wm_sdpa,
_ShortConvolution,
_single_path_delta_chunk_scan_forward,
_single_path_delta_scan_bidirectional,
_single_path_delta_scan_cached,
_single_path_delta_scan_forward,
_single_path_delta_scan_forward_stateful,
_sinusoidal_timestep_embedding,
_slice_rope_for_cam,
_slice_rope_to_current_chunk,
_temporal_short_conv_cached,
_tensor_cache_key,
_UpstreamMlp,
compute_chunk_plucker,
process_camera_conditions_ucpe,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.sana_wm import (
parity_probe,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SanaWMBlock(nn.Module):
"""One transformer block of SANA-WM."""
def __init__(
self,
hidden_size: int,
num_heads: int,
head_dim: int,
mlp_ratio: float,
t_kernel_size: int,
qk_norm: bool,
cross_norm: bool,
conv_kernel_size: int,
k_conv_only: bool,
softmax_main: bool,
use_chunk_plucker_post_attn: bool,
chunk_size: Optional[int] = None,
chunk_split_strategy: str = "uniform",
update_rule: str = "torch_chunk",
cam_update_rule: str = "torch_chunk",
chunk_gdn_chunk_size: int = 21,
use_chunked_softmax_attention: bool = False,
gdn_backend: str = "auto",
) -> None:
super().__init__()
self.softmax_main = softmax_main
self.chunk_size = chunk_size
self.chunk_split_strategy = chunk_split_strategy
self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
self.attn = BidirectionalGDNUCPESinglePathLiteLA(
in_dim=hidden_size,
heads=num_heads,
head_dim=head_dim,
qk_norm=qk_norm,
conv_kernel_size=conv_kernel_size,
k_conv_only=k_conv_only,
softmax_main=softmax_main,
update_rule=update_rule,
cam_update_rule=cam_update_rule,
chunk_gdn_chunk_size=chunk_gdn_chunk_size,
use_chunked_softmax_attention=use_chunked_softmax_attention,
gdn_backend=gdn_backend,
)
self.cross_attn = MultiHeadCrossAttention(
d_model=hidden_size,
num_heads=num_heads,
qk_norm=cross_norm,
)
self.mlp = GLUMBConvTemp(
in_features=hidden_size,
hidden_features=int(hidden_size * mlp_ratio),
t_kernel_size=t_kernel_size,
)
self.scale_shift_table = nn.Parameter(
torch.randn(6, hidden_size) / hidden_size**0.5
)
if use_chunk_plucker_post_attn:
self.plucker_proj = nn.Linear(hidden_size, hidden_size, bias=True)
nn.init.zeros_(self.plucker_proj.weight)
nn.init.zeros_(self.plucker_proj.bias)
else:
self.plucker_proj = None
@staticmethod
def _modulate(
x: torch.Tensor, shift: torch.Tensor, scale: torch.Tensor
) -> torch.Tensor:
return x * (1 + scale) + shift
@staticmethod
def _reshape_framewise_modulation(
x: torch.Tensor,
num_frames: int,
) -> tuple[torch.Tensor, int]:
B, N, C = x.shape
tokens_per_frame = N // num_frames
return x.reshape(B, num_frames, tokens_per_frame, C), tokens_per_frame
def forward(
self,
x: torch.Tensor, # (B, N, D)
y: torch.Tensor, # (B, L, D) text embeds
t: torch.Tensor, # (B, 6*D) AdaLN-single
HW: Tuple[int, int, int],
rotary_emb: Optional[torch.Tensor],
prope_fns: Optional[Tuple[Callable, Callable, Callable]],
plucker_emb: Optional[torch.Tensor],
mask: Optional[torch.Tensor],
chunk_size: Optional[int] = None,
chunk_split_strategy: Optional[str] = None,
chunk_index: Optional[List[int]] = None,
) -> torch.Tensor:
B = x.shape[0]
if t.dim() == 2:
num_frames = None
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.scale_shift_table[None] + t.reshape(B, 6, -1)
).chunk(6, dim=1)
else:
num_frames = t.reshape(B, -1, 6, t.shape[-1] // 6).shape[1]
t = t.reshape(B, num_frames, 6, -1)
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.scale_shift_table[None, None, :, :] + t
).chunk(6, dim=2)
# Self-attention with UCPE camera branch
if num_frames is None:
x_in = self._modulate(self.norm1(x), shift_msa, scale_msa)
else:
x_norm, tokens_per_frame = self._reshape_framewise_modulation(
self.norm1(x), num_frames
)
x_in = self._modulate(x_norm, shift_msa, scale_msa).reshape_as(x)
attn_out = self.attn(
x_in,
HW=HW,
rotary_emb=rotary_emb,
prope_fns=prope_fns,
chunk_size=self.chunk_size if chunk_size is None else chunk_size,
chunk_split_strategy=(
self.chunk_split_strategy
if chunk_split_strategy is None
else chunk_split_strategy
),
chunk_index=chunk_index,
)
if num_frames is None:
x = x + gate_msa * attn_out
else:
attn_out = attn_out.reshape(B, num_frames, tokens_per_frame, -1)
x = x + (gate_msa * attn_out).reshape_as(x)
# Plücker post-attn injection (zero-init linear)
if self.plucker_proj is not None and plucker_emb is not None:
x = x + self.plucker_proj(plucker_emb)
# Cross-attention
x = x + self.cross_attn(x, y, mask=mask)
# FFN
if num_frames is None:
x_in = self._modulate(self.norm2(x), shift_mlp, scale_mlp)
x = x + gate_mlp * self.mlp(x_in, HW=HW)
else:
x_norm, tokens_per_frame = self._reshape_framewise_modulation(
self.norm2(x), num_frames
)
x_in = self._modulate(x_norm, shift_mlp, scale_mlp).reshape_as(x)
mlp_out = self.mlp(x_in, HW=HW).reshape(B, num_frames, tokens_per_frame, -1)
x = x + (gate_mlp * mlp_out).reshape_as(x)
return x
def forward_long(
self,
x: torch.Tensor,
y: torch.Tensor,
t: torch.Tensor,
HW: Tuple[int, int, int],
rotary_emb: Optional[torch.Tensor],
prope_fns: Optional[Tuple[Callable, Callable, Callable]],
plucker_emb: Optional[torch.Tensor],
mask: Optional[torch.Tensor],
*,
kv_cache: list,
save_kv_cache: bool,
) -> Tuple[torch.Tensor, list]:
"""Streaming counterpart of ``forward``: threads the per-block 10-slot ``kv_cache`` through cached attention + FFN."""
B = x.shape[0]
if t.dim() == 2:
num_frames = None
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.scale_shift_table[None] + t.reshape(B, 6, -1)
).chunk(6, dim=1)
else:
num_frames = t.reshape(B, -1, 6, t.shape[-1] // 6).shape[1]
t = t.reshape(B, num_frames, 6, -1)
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = (
self.scale_shift_table[None, None, :, :] + t
).chunk(6, dim=2)
if num_frames is None:
x_in = self._modulate(self.norm1(x), shift_msa, scale_msa)
else:
x_norm, tokens_per_frame = self._reshape_framewise_modulation(
self.norm1(x), num_frames
)
x_in = self._modulate(x_norm, shift_msa, scale_msa).reshape_as(x)
attn_out, kv_cache = self.attn.forward_long(
x_in,
HW=HW,
rotary_emb=rotary_emb,
prope_fns=prope_fns,
kv_cache=kv_cache,
save_kv_cache=save_kv_cache,
)
if num_frames is None:
x = x + gate_msa * attn_out
else:
attn_out = attn_out.reshape(B, num_frames, tokens_per_frame, -1)
x = x + (gate_msa * attn_out).reshape_as(x)
if self.plucker_proj is not None and plucker_emb is not None:
x = x + self.plucker_proj(plucker_emb)
x = x + self.cross_attn(x, y, mask=mask)
if num_frames is None:
x_in = self._modulate(self.norm2(x), shift_mlp, scale_mlp)
else:
x_norm, tokens_per_frame = self._reshape_framewise_modulation(
self.norm2(x), num_frames
)
x_in = self._modulate(x_norm, shift_mlp, scale_mlp).reshape_as(x)
# GLUMBConvTemp returns a tuple whenever the streaming path is active
# (ffn_tail set OR save requested); branch on tuple-ness, never on
# save_kv_cache alone (a read-only pass with a populated slot 9 still
# returns a tuple).
mlp_out = self.mlp(
x_in,
HW=HW,
ffn_tail=kv_cache[_SLOT_FFN_TCONV],
save_ffn_tail=save_kv_cache,
)
if isinstance(mlp_out, tuple):
mlp_out, ffn_tail = mlp_out
if save_kv_cache:
kv_cache[_SLOT_FFN_TCONV] = ffn_tail
if num_frames is None:
x = x + gate_mlp * mlp_out
else:
mlp_out = mlp_out.reshape(B, num_frames, tokens_per_frame, -1)
x = x + (gate_mlp * mlp_out).reshape_as(x)
return x, kv_cache
class SanaWMTransformer3DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
"""SANA-WM 2.6B TI2V world model.
Forward inputs:
hidden_states: (B, C, T, H, W) 128-ch LTX-2 latent
encoder_hidden_states: (B, L, 2304) Gemma-2 embeddings
timestep: (B,)
encoder_attention_mask: (B, L) optional bool
camera_conditions: (B, T, 20) latent-frame raymap:
16 c2w + (fx,fy,cx,cy)
chunk_plucker: (B, 48, T, H, W) optional, computed
from camera_conditions
if absent.
Returns: ``(B, C, T, H, W)`` predicted velocity / noise.
"""
_fsdp_shard_conditions = SanaWMConfig()._fsdp_shard_conditions
_compile_conditions = SanaWMConfig()._compile_conditions
_supported_attention_backends = SanaWMConfig()._supported_attention_backends
param_names_mapping = SanaWMConfig().param_names_mapping
reverse_param_names_mapping = SanaWMConfig().reverse_param_names_mapping
lora_param_names_mapping: dict = {}
def __init__(self, config: SanaWMConfig, hf_config=None, **kwargs) -> None:
super().__init__(config, hf_config=hf_config or {}, **kwargs)
arch = config.arch_config
self.patch_size = (arch.patch_size_t, arch.patch_size, arch.patch_size)
self.inner_dim = arch.num_attention_heads * arch.attention_head_dim
self.hidden_size = self.inner_dim
self.num_attention_heads = arch.num_attention_heads
self.attention_head_dim = arch.attention_head_dim
self.out_channels = arch.out_channels
self.num_channels_latents = arch.num_channels_latents
self.vae_temporal_stride = arch.vae_temporal_stride
self.timestep_norm_scale_factor = getattr(
arch, "timestep_norm_scale_factor", 1.0
)
# --- Embedders ---
self.x_embedder = PatchEmbedMS3D(
self.patch_size,
arch.in_channels,
self.inner_dim,
bias=True,
)
self.t_embedder = TimestepEmbedder(self.inner_dim, frequency_embedding_size=256)
self.t_block = nn.Sequential(
nn.SiLU(),
nn.Linear(self.inner_dim, 6 * self.inner_dim, bias=True),
)
self.y_embedder = CaptionEmbedder(
in_channels=arch.caption_channels,
hidden_size=self.inner_dim,
token_num=arch.model_max_length,
)
self.y_norm = bool(getattr(arch, "y_norm", True))
self.attention_y_norm = _RMSNorm(
self.inner_dim,
scale_factor=getattr(arch, "y_norm_scale_factor", 1.0),
eps=getattr(arch, "y_norm_eps", 1e-5),
)
# 3-channel raymap embedder -- kept for state_dict compatibility but
# only invoked when ``use_chunk_plucker_post_attn`` is False.
# When ``True`` (the case for the released checkpoint) the absmap
# path is skipped entirely.
self.raymap_embedder = PatchEmbedMS3D(
self.patch_size,
3,
self.inner_dim,
bias=True,
)
# 48-channel plucker embedder (chunk-packed)
if arch.use_chunk_plucker_post_attn or arch.use_chunk_plucker_input:
self.plucker_embedder = PatchEmbedMS3D(
self.patch_size,
arch.chunk_plucker_channels,
self.inner_dim,
bias=True,
)
nn.init.zeros_(self.plucker_embedder.proj.weight)
nn.init.zeros_(self.plucker_embedder.proj.bias)
else:
self.plucker_embedder = None
self.use_chunk_plucker_post_attn = arch.use_chunk_plucker_post_attn
self.use_chunk_plucker_input = arch.use_chunk_plucker_input
self.chunk_size = getattr(arch, "chunk_size", None)
self.chunk_split_strategy = getattr(arch, "chunk_split_strategy", "uniform")
# --- RoPE ---
self.rope = WanRotaryPosEmbed(
attention_head_dim=arch.linear_head_dim,
patch_size=self.patch_size,
max_seq_len=1024,
)
# --- Transformer blocks ---
depth = arch.num_layers
self.softmax_every_n = arch.softmax_every_n
softmax_idx = set(
i
for i in range(depth)
if arch.softmax_every_n > 0 and (i + 1) % arch.softmax_every_n == 0
)
self.softmax_block_indices = tuple(sorted(softmax_idx))
self.blocks = nn.ModuleList(
[
SanaWMBlock(
hidden_size=self.inner_dim,
num_heads=arch.num_attention_heads,
head_dim=arch.linear_head_dim,
mlp_ratio=arch.mlp_ratio,
t_kernel_size=arch.t_kernel_size,
qk_norm=arch.qk_norm,
cross_norm=arch.cross_norm,
conv_kernel_size=arch.conv_kernel_size,
k_conv_only=arch.k_conv_only,
softmax_main=(i in softmax_idx),
use_chunk_plucker_post_attn=(
arch.use_chunk_plucker_post_attn
and (
arch.chunk_plucker_post_attn_blocks < 0
or i < arch.chunk_plucker_post_attn_blocks
)
),
chunk_size=self.chunk_size,
chunk_split_strategy=self.chunk_split_strategy,
update_rule=getattr(arch, "update_rule", "torch_chunk"),
cam_update_rule=getattr(arch, "cam_update_rule", "torch_chunk"),
chunk_gdn_chunk_size=getattr(arch, "chunk_gdn_chunk_size", 21),
use_chunked_softmax_attention=getattr(
arch, "use_chunked_softmax_attention", False
),
gdn_backend=getattr(arch, "gdn_backend", "auto"),
)
for i in range(depth)
]
)
self.final_layer = T2IFinalLayer(
self.inner_dim, self.patch_size, self.out_channels
)
# Cache RoPE freqs per shape -- avoids recomputation across denoising
# steps with constant latent shapes.
self._freqs_cache: dict = {}
self._ucpe_apply_fns_cache: Optional[
Tuple[Tuple, torch.Tensor, Tuple[Callable, Callable, Callable]]
] = None
self._plucker_emb_cache: Optional[Tuple[Tuple, torch.Tensor, torch.Tensor]] = (
None
)
# FSDP shard targets
self.layer_names = ["blocks"]
def post_load_weights(self) -> None:
# FSDP loader initializes the model on meta and only materializes
# tensors that appear in the checkpoint. WanRotaryPosEmbed._freqs is a
# derived, non-persistent constant, so recompute it deterministically.
for module in self.modules():
if isinstance(module, WanRotaryPosEmbed):
if module._freqs.is_meta:
module._init_freqs_buffer()
# ------------------------------------------------------------------ #
# Forward
# ------------------------------------------------------------------ #
def _get_freqs(self, T: int, H: int, W: int, device: torch.device) -> torch.Tensor:
key = (T, H, W, str(device))
if key not in self._freqs_cache:
self._freqs_cache[key] = self.rope((T, H, W), device)
return self._freqs_cache[key]
def _get_freqs_window(
self,
start: int,
end: int,
H: int,
W: int,
device: torch.device,
frame_index: Optional[torch.Tensor] = None,
) -> torch.Tensor:
"""RoPE freqs for a streaming chunk at GLOBAL frame positions.
``frame_index`` (per-token global positions) overrides ``(start, end)``;
the count branch is cached, the tensor branch is computed fresh.
"""
if frame_index is not None:
return self.rope((end - start, H, W), device, frame_index=frame_index)
key = ("win", int(start), int(end), H, W, str(device))
if key not in self._freqs_cache:
self._freqs_cache[key] = self.rope(((int(start), int(end)), H, W), device)
return self._freqs_cache[key]
def _get_ucpe_apply_fns(
self,
camera_conditions: torch.Tensor,
*,
HW: Tuple[int, int, int],
freqs: torch.Tensor,
) -> Tuple[Callable, Callable, Callable]:
head_dim = self.attention_head_dim
if torch.is_grad_enabled():
raymats = process_camera_conditions_ucpe(
camera_conditions,
HW=HW,
patch_size=self.patch_size,
)
raymats_flat = raymats.reshape(camera_conditions.shape[0], -1, 4, 4)
return _build_ucpe_apply_fns(head_dim, raymats_flat, freqs)
key = (
"ucpe",
HW,
self.patch_size,
head_dim,
_tensor_cache_key(camera_conditions),
_tensor_cache_key(freqs),
)
cached = self._ucpe_apply_fns_cache
if cached is not None and cached[0] == key:
return cached[2]
raymats = process_camera_conditions_ucpe(
camera_conditions,
HW=HW,
patch_size=self.patch_size,
)
raymats_flat = raymats.reshape(camera_conditions.shape[0], -1, 4, 4)
prope_fns = _build_ucpe_apply_fns(head_dim, raymats_flat, freqs)
self._ucpe_apply_fns_cache = (key, camera_conditions, prope_fns)
return prope_fns
def _get_plucker_emb(
self,
chunk_plucker: torch.Tensor,
*,
latent_token_count: int,
) -> torch.Tensor:
if self.plucker_embedder is None:
raise ValueError("SANA-WM plucker_embedder is not initialized.")
weight = self.plucker_embedder.proj.weight
bias = self.plucker_embedder.proj.bias
key = (
"plucker_emb",
latent_token_count,
self.patch_size,
_tensor_cache_key(chunk_plucker),
_tensor_cache_key(weight),
None if bias is None else _tensor_cache_key(bias),
)
if not torch.is_grad_enabled():
cached = self._plucker_emb_cache
if cached is not None and cached[0] == key:
return cached[2]
plucker_emb = self.plucker_embedder(chunk_plucker.to(weight.dtype))
if plucker_emb.shape[1] != latent_token_count:
raise ValueError(
f"plucker_emb token count {plucker_emb.shape[1]} != "
f"latent token count {latent_token_count}; "
"expected chunk_plucker shape (B, 48, T, H, W)."
)
if not torch.is_grad_enabled():
self._plucker_emb_cache = (key, chunk_plucker, plucker_emb)
return plucker_emb
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
timestep: Optional[torch.Tensor] = None,
encoder_attention_mask: Optional[torch.Tensor] = None,
camera_conditions: Optional[torch.Tensor] = None,
chunk_plucker: Optional[torch.Tensor] = None,
guidance: Optional[torch.Tensor] = None, # kept for compat
**kwargs,
) -> torch.Tensor:
if encoder_hidden_states is None:
raise ValueError("SANA-WM forward requires encoder_hidden_states.")
if timestep is None:
raise ValueError("SANA-WM forward requires timestep.")
B, C, T_raw, H_raw, W_raw = hidden_states.shape
p_t, p_h, p_w = self.patch_size
T = T_raw // p_t
H = H_raw // p_h
W = W_raw // p_w
chunk_size = kwargs.get("chunk_size", self.chunk_size)
chunk_split_strategy = kwargs.get(
"chunk_split_strategy", self.chunk_split_strategy
)
chunk_index = kwargs.get("chunk_index", None)
# Patch embed: (B, C, T, H, W) -> (B, T*H*W, D)
x = self.x_embedder(hidden_states.to(dtype=self.x_embedder.proj.weight.dtype))
# Timestep AdaLN-single. SANA-WM's LTX sampler passes per-frame
# timesteps shaped (B, 1, T) so the clean first-frame condition can stay
# at timestep 0 while remaining latent frames denoise. Keep the scalar
# path for generic scheduler compatibility.
if self.timestep_norm_scale_factor != 1.0:
timestep_for_embed = (
timestep.float() / self.timestep_norm_scale_factor
).to(torch.float32)
else:
timestep_for_embed = timestep.long().to(torch.float32)
if timestep_for_embed.dim() == 1:
t_emb = self.t_embedder(timestep_for_embed) # (B, D)
t6 = self.t_block(t_emb) # (B, 6D)
else:
timestep_shape = tuple(timestep_for_embed.shape)
t_flat = self.t_embedder(timestep_for_embed.flatten())
t6_flat = self.t_block(t_flat)
t_emb = t_flat.unflatten(0, timestep_shape)
t6 = t6_flat.unflatten(0, timestep_shape)
if isinstance(encoder_attention_mask, (list, tuple)):
encoder_attention_mask = encoder_attention_mask[0]
y = encoder_hidden_states
if y.dim() == 3:
y = y.unsqueeze(1)
y = self.y_embedder(y).squeeze(1) # (B, L, D)
if y.shape[0] != B:
y = y.expand(B, -1, -1).contiguous()
if self.y_norm:
y = self.attention_y_norm(y)
if encoder_attention_mask is not None and encoder_attention_mask.shape[0] != B:
encoder_attention_mask = encoder_attention_mask.expand(B, -1).contiguous()
freqs = self._get_freqs(T, H, W, x.device)
# Camera conditioning: UCPE prope_fns + Plücker
prope_fns = None
if camera_conditions is not None:
if camera_conditions.shape[1] != T:
raise ValueError(
"SANA-WM camera_conditions must be sampled at latent "
f"frames: got {camera_conditions.shape[1]} frames, "
f"expected T={T}."
)
prope_fns = self._get_ucpe_apply_fns(
camera_conditions,
HW=(T, H, W),
freqs=freqs,
)
# Plücker post-attn embedding (shared across all blocks)
plucker_emb = None
needs_plucker_emb = (
chunk_plucker is not None
and self.plucker_embedder is not None
and (self.use_chunk_plucker_post_attn or self.use_chunk_plucker_input)
)
if needs_plucker_emb:
plucker_emb = self._get_plucker_emb(
chunk_plucker,
latent_token_count=x.shape[1],
) # (B, T*H*W, D)
if self.use_chunk_plucker_input and plucker_emb is not None:
x = x + plucker_emb
if not self.use_chunk_plucker_post_attn:
plucker_emb = None
# --- 6. Transformer blocks ---
HW = (T, H, W)
for block in self.blocks:
x = block(
x,
y=y,
t=t6,
HW=HW,
rotary_emb=freqs,
prope_fns=prope_fns,
plucker_emb=plucker_emb,
mask=encoder_attention_mask,
chunk_size=chunk_size,
chunk_split_strategy=chunk_split_strategy,
chunk_index=chunk_index,
)
x = self.final_layer(x, t_emb) # (B, N, p_t*p_h*p_w*C_out)
# Un-patch
x = x.reshape(B, T, H, W, p_t, p_h, p_w, self.out_channels)
x = x.permute(0, 7, 1, 4, 2, 5, 3, 6).contiguous()
x = x.reshape(B, self.out_channels, T * p_t, H * p_h, W * p_w)
return x
def forward_long(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: Optional[torch.Tensor] = None,
timestep: Optional[torch.Tensor] = None,
encoder_attention_mask: Optional[torch.Tensor] = None,
camera_conditions: Optional[torch.Tensor] = None,
chunk_plucker: Optional[torch.Tensor] = None,
*,
kv_cache: Optional[list] = None,
save_kv_cache: bool = True,
start_f: Optional[int] = None,
end_f: Optional[int] = None,
frame_index: Optional[torch.Tensor] = None,
**kwargs,
) -> Tuple[torch.Tensor, list]:
"""Streaming autoregressive forward over a chunk of latent frames.
RoPE / camera / plücker are windowed to the chunk's GLOBAL frame range
``[start_f, end_f)``; a per-block 10-slot ``kv_cache`` carries recurrent
state / concat-windows across chunks. Returns ``(out, new_cache)``.
"""
if encoder_hidden_states is None:
raise ValueError("SANA-WM forward_long requires encoder_hidden_states.")
if timestep is None:
raise ValueError("SANA-WM forward_long requires timestep.")
if kv_cache is None:
kv_cache = [[None] * _NUM_STREAM_CACHE_SLOTS for _ in self.blocks]
B, C, T_raw, H_raw, W_raw = hidden_states.shape
p_t, p_h, p_w = self.patch_size
T = T_raw // p_t
H = H_raw // p_h
W = W_raw // p_w
start = 0 if start_f is None else int(start_f)
end = start + T if end_f is None else int(end_f)
x = self.x_embedder(hidden_states.to(dtype=self.x_embedder.proj.weight.dtype))
# Timestep AdaLN-single: force the framewise (B, 1, T) path so blocks
# always apply per-frame modulation.
if timestep.dim() == 1:
timestep = timestep[:, None, None].expand(-1, 1, T)
elif timestep.dim() == 2:
timestep = timestep[:, None, :]
if self.timestep_norm_scale_factor != 1.0:
timestep_for_embed = (
timestep.float() / self.timestep_norm_scale_factor
).to(torch.float32)
else:
timestep_for_embed = timestep.long().to(torch.float32)
timestep_shape = tuple(timestep_for_embed.shape)
t_flat = self.t_embedder(timestep_for_embed.flatten())
t6_flat = self.t_block(t_flat)
t_emb = t_flat.unflatten(0, timestep_shape)
t6 = t6_flat.unflatten(0, timestep_shape)
if isinstance(encoder_attention_mask, (list, tuple)):
encoder_attention_mask = encoder_attention_mask[0]
y = encoder_hidden_states
if y.dim() == 3:
y = y.unsqueeze(1)
y = self.y_embedder(y).squeeze(1)
if y.shape[0] != B:
y = y.expand(B, -1, -1).contiguous()
if self.y_norm:
y = self.attention_y_norm(y)
if encoder_attention_mask is not None and encoder_attention_mask.shape[0] != B:
encoder_attention_mask = encoder_attention_mask.expand(B, -1).contiguous()
# RoPE windowed to global frame positions [start, end)
freqs = self._get_freqs_window(
start, end, H, W, x.device, frame_index=frame_index
)
# Camera conditioning: slice to the chunk, co-windowed w/ freqs
prope_fns = None
if camera_conditions is not None:
if camera_conditions.shape[1] != T:
# .contiguous(): canonical layout regardless of how the caller
# built the full-length tensor, so batch and realtime windows are
# kernel-level identical (slice offset/stride changes the reduction
# order otherwise — measured 1e-7 seeds amplifying to %-level drift
# through the bf16 block stack).
camera_conditions = camera_conditions[:, start:end].contiguous()
if camera_conditions.shape[0] != B:
camera_conditions = camera_conditions.repeat(
B // camera_conditions.shape[0], 1, 1
)
prope_fns = self._get_ucpe_apply_fns(
camera_conditions, HW=(T, H, W), freqs=freqs
)
# Plücker post-attn / input embedding, sliced to the chunk.
if chunk_plucker is not None and chunk_plucker.shape[2] != T:
chunk_plucker = chunk_plucker[
:, :, start:end
].contiguous() # see camera note
if chunk_plucker is not None and chunk_plucker.shape[0] != B:
chunk_plucker = chunk_plucker.repeat(
B // chunk_plucker.shape[0], 1, 1, 1, 1
)
plucker_emb = None
needs_plucker_emb = (
chunk_plucker is not None
and self.plucker_embedder is not None
and (self.use_chunk_plucker_post_attn or self.use_chunk_plucker_input)
)
if needs_plucker_emb:
plucker_emb = self._get_plucker_emb(
chunk_plucker, latent_token_count=x.shape[1]
)
if self.use_chunk_plucker_input and plucker_emb is not None:
x = x + plucker_emb
if not self.use_chunk_plucker_post_attn:
plucker_emb = None
# parity harness (env-gated, no-op in prod): on the FIRST sink-path call
# (frame_index not None), checksum the pre-block tensors and x after every
# block to localize where the two execution paths first diverge.
_probe_path = os.environ.get(parity_probe.ENV_BLOCK_PROBE)
_probe = None
if (
_probe_path
and frame_index is not None
and not getattr(self, "_block_probe_done", False)
):
_ck = parity_probe.checksum
_probe = {
"x_embed": _ck(x),
"t6": _ck(t6),
"y": _ck(y),
"freqs": (
(
tuple(freqs.shape),
float(freqs.real.detach().double().sum().item()),
float(freqs.imag.detach().double().sum().item()),
)
if freqs is not None
else None
),
"plucker_emb": _ck(plucker_emb),
"frame_index": frame_index.tolist(),
}
HW = (T, H, W)
new_cache = []
for i, block in enumerate(self.blocks):
x, block_cache = block.forward_long(
x,
y,
t6,
HW,
freqs,
prope_fns,
plucker_emb,
encoder_attention_mask,
kv_cache=kv_cache[i],
save_kv_cache=save_kv_cache,
)
new_cache.append(block_cache)
if _probe is not None:
_probe[f"x_after_block_{i:02d}"] = parity_probe.checksum(x)
if _probe is not None:
torch.save(_probe, _probe_path)
self._block_probe_done = True
x = self.final_layer(x, t_emb)
x = x.reshape(B, T, H, W, p_t, p_h, p_w, self.out_channels)
x = x.permute(0, 7, 1, 4, 2, 5, 3, 6).contiguous()
x = x.reshape(B, self.out_channels, T * p_t, H * p_h, W * p_w)
return x, new_cache
EntryClass = SanaWMTransformer3DModel
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,426 @@
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
from typing import Any, Optional
import torch
import torch.nn as nn
from sglang.multimodal_gen.configs.models.dits.sana_wm_refiner import (
SanaWMRefinerArchConfig,
SanaWMRefinerConfig,
)
from sglang.multimodal_gen.runtime.layers.linear import ColumnParallelLinear
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.models.dits.ltx_2 import (
LTX2AdaLayerNormSingle,
LTX2Attention,
LTX2AudioVideoRotaryPosEmbed,
LTX2FeedForward,
LTX2TextProjection,
)
def pack_latents(
latents: torch.Tensor, patch_size: int = 1, patch_size_t: int = 1
) -> torch.Tensor:
"""Pack a 5D latent (B, C, T, H, W) into a 3D token sequence (B, L, in_dim)."""
B, _, T, H, W = latents.shape
pT = T // patch_size_t
pH = H // patch_size
pW = W // patch_size
latents = latents.reshape(B, -1, pT, patch_size_t, pH, patch_size, pW, patch_size)
return latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
def unpack_latents(
tokens: torch.Tensor,
num_frames: int,
height: int,
width: int,
patch_size: int = 1,
patch_size_t: int = 1,
) -> torch.Tensor:
"""Inverse of `pack_latents`: (B, L, out_dim) -> (B, C, T, H, W)."""
B = tokens.size(0)
tokens = tokens.reshape(
B,
num_frames // patch_size_t,
height // patch_size,
width // patch_size,
-1,
patch_size_t,
patch_size,
patch_size,
)
return (
tokens.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
)
def _slice_rope(
rope: tuple[torch.Tensor, torch.Tensor], start: int, end: Optional[int] = None
) -> tuple[torch.Tensor, torch.Tensor]:
"""Slice along token axis for either interleaved (rank-3) or split (rank-4)."""
cos, sin = rope
end_ = end if end is not None else cos.shape[-2 if cos.ndim == 4 else 1]
if cos.ndim == 3:
return cos[:, start:end_], sin[:, start:end_]
if cos.ndim == 4:
return cos[:, :, start:end_, :], sin[:, :, start:end_, :]
raise ValueError(f"Unexpected RoPE rank: {cos.ndim}")
def _streaming_self_attention(
attn: LTX2Attention,
hidden_states: torch.Tensor,
video_rotary_emb: tuple[torch.Tensor, torch.Tensor],
n_context_tokens: int,
) -> torch.Tensor:
"""Streaming SLA: context attends to context only, current attends to context+current.
Mirrors NVlabs `inference_sana_wm.py::_streaming_self_attention`.
"""
seq_len = hidden_states.shape[1]
if n_context_tokens <= 0 or n_context_tokens >= seq_len:
return attn(hidden_states, context=None, pe=video_rotary_emb)
ctx_rope = _slice_rope(video_rotary_emb, 0, n_context_tokens)
out_ctx = attn(
hidden_states[:, :n_context_tokens],
context=None,
pe=ctx_rope,
)
cur_rope = _slice_rope(video_rotary_emb, n_context_tokens, seq_len)
out_cur = attn(
hidden_states[:, n_context_tokens:],
context=hidden_states,
pe=cur_rope,
k_pe=video_rotary_emb,
)
return torch.cat([out_ctx, out_cur], dim=1)
class SanaWMRefinerBlock(nn.Module):
"""Video-only LTX-2 transformer block.
Diffusers-compatible layout: `norm1 -> attn1 (self) -> norm2 -> attn2 (cross) -> norm3 -> ff`,
each modulated via per-block `scale_shift_table` + token-wise `temb`.
"""
def __init__(
self,
dim: int,
num_attention_heads: int,
attention_head_dim: int,
cross_attention_dim: int,
qk_norm: bool = True,
norm_eps: float = 1e-6,
apply_gated_attention: bool = False,
prefix: str = "",
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__()
self.dim = int(dim)
self.norm1 = nn.RMSNorm(self.dim, eps=norm_eps, elementwise_affine=False)
self.attn1 = LTX2Attention(
query_dim=self.dim,
heads=num_attention_heads,
dim_head=attention_head_dim,
norm_eps=norm_eps,
qk_norm=qk_norm,
apply_gated_attention=apply_gated_attention,
prefix=f"{prefix}.attn1",
quant_config=quant_config,
)
self.norm2 = nn.RMSNorm(self.dim, eps=norm_eps, elementwise_affine=False)
self.attn2 = LTX2Attention(
query_dim=self.dim,
context_dim=cross_attention_dim,
heads=num_attention_heads,
dim_head=attention_head_dim,
norm_eps=norm_eps,
qk_norm=qk_norm,
use_local_attention=True,
apply_gated_attention=apply_gated_attention,
prefix=f"{prefix}.attn2",
quant_config=quant_config,
)
self.norm3 = nn.RMSNorm(self.dim, eps=norm_eps, elementwise_affine=False)
self.ff = LTX2FeedForward(self.dim, dim_out=self.dim, quant_config=quant_config)
self.scale_shift_table = nn.Parameter(torch.randn(6, self.dim) / self.dim**0.5)
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
temb: torch.Tensor,
video_rotary_emb: tuple[torch.Tensor, torch.Tensor],
encoder_attention_mask: Optional[torch.Tensor] = None,
n_context_tokens: int = 0,
) -> torch.Tensor:
B = hidden_states.size(0)
T = temb.size(1)
D = self.dim
ada = self.scale_shift_table[None, None].to(
device=temb.device, dtype=temb.dtype
) + temb.reshape(B, T, 6, D)
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = ada.unbind(
dim=2
)
normed = self.norm1(hidden_states) * (1 + scale_msa) + shift_msa
attn_out = _streaming_self_attention(
self.attn1,
normed,
video_rotary_emb,
n_context_tokens=n_context_tokens,
)
hidden_states = hidden_states + attn_out * gate_msa
normed = self.norm2(hidden_states)
ca_out = self.attn2(
normed,
context=encoder_hidden_states,
mask=encoder_attention_mask,
pe=None,
)
hidden_states = hidden_states + ca_out
normed = self.norm3(hidden_states) * (1 + scale_mlp) + shift_mlp
hidden_states = hidden_states + self.ff(normed) * gate_mlp
return hidden_states
class SanaWMLTX2VideoRefiner(CachableDiT, LayerwiseOffloadableModuleMixin):
"""SANA-WM stage-2 LTX-2 video-only refiner.
Loads Diffusers-format refiner weights from `<model_path>/refiner/transformer/`.
Audio params present in the checkpoint are silently dropped by the loader's
`strict=False` state_dict load.
"""
_fsdp_shard_conditions = SanaWMRefinerArchConfig()._fsdp_shard_conditions
_compile_conditions = SanaWMRefinerArchConfig()._compile_conditions
_supported_attention_backends = (
SanaWMRefinerArchConfig()._supported_attention_backends
)
param_names_mapping = SanaWMRefinerArchConfig().param_names_mapping
reverse_param_names_mapping: dict = {}
lora_param_names_mapping: dict = {}
def __init__(
self,
config: SanaWMRefinerConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config, hf_config=hf_config)
arch = config.arch_config
self.in_channels = int(arch.in_channels)
self.out_channels = int(arch.out_channels)
self.patch_size = int(arch.patch_size)
self.patch_size_t = int(arch.patch_size_t)
self.hidden_size = int(arch.hidden_size)
self.num_attention_heads = int(arch.num_attention_heads)
self.num_channels_latents = int(arch.num_channels_latents)
self.attention_head_dim = int(arch.attention_head_dim)
self.timestep_scale_multiplier = float(arch.timestep_scale_multiplier)
self.rope_type = str(arch.rope_type)
in_dim = (
self.in_channels * self.patch_size_t * self.patch_size * self.patch_size
)
out_dim = (
self.out_channels * self.patch_size_t * self.patch_size * self.patch_size
)
self.proj_in = ColumnParallelLinear(
in_dim,
self.hidden_size,
bias=True,
gather_output=True,
quant_config=quant_config,
)
self.time_embed = LTX2AdaLayerNormSingle(
self.hidden_size, embedding_coefficient=6
)
self.caption_projection = LTX2TextProjection(
in_features=int(arch.caption_channels),
hidden_size=self.hidden_size,
out_features=self.hidden_size,
act_fn="gelu_tanh",
)
self.transformer_blocks = nn.ModuleList(
[
SanaWMRefinerBlock(
dim=self.hidden_size,
num_attention_heads=self.num_attention_heads,
attention_head_dim=self.attention_head_dim,
cross_attention_dim=int(arch.cross_attention_dim),
qk_norm=bool(arch.qk_norm),
norm_eps=float(arch.norm_eps),
apply_gated_attention=bool(arch.apply_gated_attention),
prefix=f"transformer_blocks.{i}",
quant_config=quant_config,
)
for i in range(int(arch.num_layers))
]
)
self.scale_shift_table = nn.Parameter(
torch.randn(2, self.hidden_size) / self.hidden_size**0.5
)
self.norm_out = nn.LayerNorm(
self.hidden_size, eps=float(arch.norm_eps), elementwise_affine=False
)
self.proj_out = ColumnParallelLinear(
self.hidden_size,
out_dim,
bias=True,
gather_output=True,
quant_config=quant_config,
)
# LTX2AudioVideoRotaryPosEmbed expects `dim` to be the *total* hidden
# size (num_heads * head_dim), not the per-head dim. It internally
# reshapes cos/sin to (B, T, num_heads, head_dim/2). Passing
# `attention_head_dim` here would size the RoPE to head_dim/num_heads
# and produce a (1, num_heads, L, 2) cos/sin that won't match
# LTX2Attention's q/k. See LTX2Transformer3DAVModel.__init__ in
# ltx_2.py for the canonical convention (`dim=self.hidden_size`).
self.rope = LTX2AudioVideoRotaryPosEmbed(
dim=self.hidden_size,
patch_size=self.patch_size,
patch_size_t=self.patch_size_t,
base_num_frames=int(arch.base_num_frames),
base_height=int(arch.base_height),
base_width=int(arch.base_width),
sampling_rate=int(arch.sampling_rate),
hop_length=int(arch.hop_length),
scale_factors=tuple(arch.scale_factors),
causal_offset=int(arch.causal_offset),
modality="video",
rope_type=self.rope_type,
num_attention_heads=self.num_attention_heads,
)
self.layer_names = ["transformer_blocks"]
def _scale_timestep_for_adaln(self, timestep: torch.Tensor) -> torch.Tensor:
return timestep * self.timestep_scale_multiplier
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
timestep: torch.Tensor,
encoder_hidden_states_image=None,
encoder_attention_mask: Optional[torch.Tensor] = None,
num_frames: Optional[int] = None,
height: Optional[int] = None,
width: Optional[int] = None,
fps: float = 24.0,
n_context_tokens: int = 0,
guidance=None,
**kwargs,
) -> torch.Tensor:
# Accept either packed (B, L, in_dim) or raw 5D (B, C, T, H, W).
if hidden_states.dim() == 5:
B_, _, T_, H_, W_ = hidden_states.shape
if num_frames is None:
num_frames = T_
if height is None:
height = H_
if width is None:
width = W_
hidden_states = pack_latents(
hidden_states,
patch_size=self.patch_size,
patch_size_t=self.patch_size_t,
)
packed_input = True
else:
if num_frames is None or height is None or width is None:
raise ValueError(
"num_frames/height/width are required when hidden_states is pre-packed."
)
packed_input = False
B = hidden_states.size(0)
video_coords = self.rope.prepare_video_coords(
batch_size=B,
num_frames=num_frames,
height=height,
width=width,
device=hidden_states.device,
fps=fps,
)
video_rotary_emb = self.rope(
video_coords,
device=hidden_states.device,
out_dtype=hidden_states.dtype,
)
hidden_states, _ = self.proj_in(hidden_states)
scaled_t = self._scale_timestep_for_adaln(timestep)
temb, embedded_timestep = self.time_embed(
scaled_t.flatten(), hidden_dtype=hidden_states.dtype
)
if timestep.dim() >= 2:
temb = temb.view(B, -1, temb.size(-1))
embedded_timestep = embedded_timestep.view(
B, -1, embedded_timestep.size(-1)
)
else:
temb = temb.view(B, 1, temb.size(-1))
embedded_timestep = embedded_timestep.view(B, 1, embedded_timestep.size(-1))
encoder_hidden_states = self.caption_projection(encoder_hidden_states)
encoder_hidden_states = encoder_hidden_states.view(B, -1, self.hidden_size)
for block in self.transformer_blocks:
hidden_states = block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_hidden_states,
temb=temb,
video_rotary_emb=video_rotary_emb,
encoder_attention_mask=encoder_attention_mask,
n_context_tokens=n_context_tokens,
)
scale_shift_values = self.scale_shift_table[None, None].to(
device=hidden_states.device, dtype=hidden_states.dtype
) + embedded_timestep[:, :, None].to(hidden_states.dtype)
shift, scale = scale_shift_values[:, :, 0], scale_shift_values[:, :, 1]
hidden_states = self.norm_out(hidden_states) * (1 + scale) + shift
hidden_states, _ = self.proj_out(hidden_states)
if packed_input:
return hidden_states
return unpack_latents(
hidden_states,
num_frames=num_frames,
height=height,
width=width,
patch_size=self.patch_size,
patch_size_t=self.patch_size_t,
)
EntryClass = SanaWMLTX2VideoRefiner
@@ -0,0 +1,184 @@
# SPDX-License-Identifier: Apache-2.0
"""StableDiffusion3 Transformer model implementation.
NOTE: This initial implementation uses diffusers' JointTransformerBlock directly.
A native SGLang attention implementation is needed for FlashAttention, TP/SP,
quantization, and LoRA support.
"""
from typing import Any
import torch
import torch.nn as nn
from diffusers.models.attention import JointTransformerBlock
from diffusers.models.embeddings import CombinedTimestepTextProjEmbeddings, PatchEmbed
from diffusers.models.normalization import AdaLayerNormContinuous
from sglang.multimodal_gen.configs.models.dits.stablediffusion3 import (
StableDiffusion3TransformerConfig,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.dits.base import CachableDiT
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SD3Transformer2DModel(CachableDiT, LayerwiseOffloadableModuleMixin):
_supports_gradient_checkpointing = True
_no_split_modules = ["JointTransformerBlock"]
_skip_layerwise_casting_patterns = ["pos_embed", "norm"]
layer_names = ["transformer_blocks"]
def __init__(
self,
config: StableDiffusion3TransformerConfig,
hf_config: dict[str, Any] | None = None,
quant_config=None,
):
super().__init__(config=config, hf_config=hf_config)
self.config = config
arch_config = config.arch_config
sample_size = arch_config.sample_size
patch_size = arch_config.patch_size
in_channels = arch_config.in_channels
num_layers = arch_config.num_layers
attention_head_dim = arch_config.attention_head_dim
num_attention_heads = arch_config.num_attention_heads
joint_attention_dim = arch_config.joint_attention_dim
caption_projection_dim = arch_config.caption_projection_dim
pooled_projection_dim = arch_config.pooled_projection_dim
out_channels = arch_config.out_channels
pos_embed_max_size = arch_config.pos_embed_max_size
dual_attention_layers = arch_config.dual_attention_layers
qk_norm = arch_config.qk_norm
self.out_channels = out_channels if out_channels is not None else in_channels
self.inner_dim = num_attention_heads * attention_head_dim
self.patch_size = patch_size
self.pos_embed = PatchEmbed(
height=sample_size,
width=sample_size,
patch_size=patch_size,
in_channels=in_channels,
embed_dim=self.inner_dim,
pos_embed_max_size=pos_embed_max_size,
)
self.time_text_embed = CombinedTimestepTextProjEmbeddings(
embedding_dim=self.inner_dim, pooled_projection_dim=pooled_projection_dim
)
self.context_embedder = nn.Linear(joint_attention_dim, caption_projection_dim)
self.transformer_blocks = nn.ModuleList(
[
JointTransformerBlock(
dim=self.inner_dim,
num_attention_heads=num_attention_heads,
attention_head_dim=attention_head_dim,
context_pre_only=i == num_layers - 1,
qk_norm=qk_norm,
use_dual_attention=i in dual_attention_layers,
)
for i in range(num_layers)
]
)
self.norm_out = AdaLayerNormContinuous(
self.inner_dim, self.inner_dim, elementwise_affine=False, eps=1e-6
)
self.proj_out = nn.Linear(
self.inner_dim, patch_size * patch_size * self.out_channels, bias=True
)
self.gradient_checkpointing = False
def forward(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor | None = None,
pooled_projections: torch.Tensor | None = None,
timestep: torch.LongTensor | None = None,
block_controlnet_hidden_states: list | None = None,
guidance: torch.Tensor | None = None,
joint_attention_kwargs: dict[str, Any] | None = None,
skip_layers: list[int] | None = None,
) -> torch.Tensor:
if encoder_hidden_states is None:
raise ValueError("encoder_hidden_states must be provided.")
if pooled_projections is None:
raise ValueError("pooled_projections must be provided.")
encoder_embeddings = encoder_hidden_states
height, width = hidden_states.shape[-2:]
hidden_states = self.pos_embed(hidden_states)
temb = self.time_text_embed(timestep, pooled_projections)
encoder_embeddings = self.context_embedder(encoder_embeddings)
skip_layer_set = set(skip_layers) if skip_layers else set()
if block_controlnet_hidden_states is not None:
interval_control = len(self.transformer_blocks) / len(
block_controlnet_hidden_states
)
else:
interval_control = 0
for index_block, block in enumerate(self.transformer_blocks):
if index_block not in skip_layer_set:
encoder_embeddings, hidden_states = block(
hidden_states=hidden_states,
encoder_hidden_states=encoder_embeddings,
temb=temb,
joint_attention_kwargs=joint_attention_kwargs,
)
# controlnet residual
if (
block_controlnet_hidden_states is not None
and block.context_pre_only is False
):
hidden_states = (
hidden_states
+ block_controlnet_hidden_states[
int(index_block / interval_control)
]
)
hidden_states = self.norm_out(hidden_states, temb)
hidden_states = self.proj_out(hidden_states)
# unpatchify
patch_size = self.patch_size
height = height // patch_size
width = width // patch_size
hidden_states = hidden_states.reshape(
shape=(
hidden_states.shape[0],
height,
width,
patch_size,
patch_size,
self.out_channels,
)
)
hidden_states = hidden_states.permute(0, 5, 1, 3, 2, 4)
output = hidden_states.reshape(
shape=(
hidden_states.shape[0],
self.out_channels,
height * patch_size,
width * patch_size,
)
)
return output
# Entry class for registry
EntryClass = SD3Transformer2DModel
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,170 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from abc import ABC, abstractmethod
from dataclasses import field
import torch
from torch import nn
from sglang.multimodal_gen.configs.models.encoders import (
BaseEncoderOutput,
EncoderConfig,
ImageEncoderConfig,
TextEncoderConfig,
)
from sglang.multimodal_gen.runtime.distributed import (
get_sp_group,
get_tp_group,
get_world_group,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
def get_folding_tp_group(config: EncoderConfig):
"""Group an encoder should tensor-parallel over.
``config.parallel_folding_mode`` is set by ServerArgs.adjust_pipeline_config
when the encoder is folded over a larger group than its own TP (the idle DiT
replica during the encoding stage); when it is None the encoder uses the
default TP group. Shared by every text/image encoder so the choice lives in
one place.
"""
mode = config.parallel_folding_mode
if mode == "sp":
return get_sp_group()
elif mode == "ulysses":
return get_sp_group().ulysses_group
elif mode == "ring":
return get_sp_group().ring_group
elif mode == "world":
# the whole single-replica DiT (all GPUs), regardless of tp/sp/cfg.
return get_world_group()
return get_tp_group()
# Folding pays off only for wide encoders: measured ~-22% encode latency for
# T5-XXL (hidden 4096) and larger for Mistral-24B (hidden 5120), but a net loss
# for narrower ones (Qwen3 hidden 2560, CLIP 512) whose per-layer all_reduce
# dominates the sharded compute. Decided on the real (post-load) hidden size.
FOLD_MIN_HIDDEN_SIZE = 4096
def _encoder_dims(config: EncoderConfig):
"""Best-effort (hidden, attention_heads, mlp_intermediate) from a config,
spelled differently across families (hidden_size/d_model, num_heads, d_ff)."""
def first(names):
for name in names:
value = getattr(config, name, None)
if isinstance(value, int) and value > 0:
return value
return None
return (
first(("hidden_size", "d_model")),
first(("num_attention_heads", "num_heads", "n_heads")),
first(("intermediate_size", "d_ff", "ffn_dim")),
)
def encoder_folding_worthwhile(config: EncoderConfig, group_size: int) -> bool:
"""Fold only encoders wide enough to benefit whose heads and MLP divide the
fold group. Size-based (not per-architecture), so the same encoder family at
different parameter counts is handled correctly."""
hidden, heads, inter = _encoder_dims(config)
return (
group_size > 1
and hidden is not None
and hidden >= FOLD_MIN_HIDDEN_SIZE
and heads is not None
and heads % group_size == 0
and inter is not None
and inter % group_size == 0
)
def finalize_encoder_folding(config: EncoderConfig) -> None:
"""Loader hook: call after the encoder's real dims are populated
(update_model_arch) and before construction. adjust_pipeline_config proposes
a fold group from the parallelism alone; here we keep it only if the encoder
is actually worth folding at its real size, otherwise fall back to
replicated by clearing the mode.
"""
if config.parallel_folding_mode is None:
return
group_size = getattr(get_folding_tp_group(config), "world_size", 1)
if not encoder_folding_worthwhile(config, group_size):
config.parallel_folding_mode = None
class TextEncoder(nn.Module, ABC, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"layers",
"encoder.block",
"text_model.encoder.layers",
"model.language_model.layers",
]
_fsdp_shard_conditions: list = field(default_factory=lambda: [])
_stacked_params_mapping: list[tuple[str, str, str]] = field(default_factory=list)
_supported_attention_backends: set[AttentionBackendEnum] = (
TextEncoderConfig()._supported_attention_backends
)
def __init__(self, config: TextEncoderConfig) -> None:
super().__init__()
self.config = config
self._fsdp_shard_conditions = config.arch_config._fsdp_shard_conditions
self._stacked_params_mapping = config.arch_config.stacked_params_mapping
if not self.supported_attention_backends:
raise ValueError(
f"Subclass {self.__class__.__name__} must define _supported_attention_backends"
)
@abstractmethod
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
pass
@property
def supported_attention_backends(self) -> set[AttentionBackendEnum]:
return self._supported_attention_backends
class ImageEncoder(nn.Module, ABC, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"layers",
"vision_model.encoder.layers",
"model.visual.blocks",
]
_supported_attention_backends: set[AttentionBackendEnum] = (
ImageEncoderConfig()._supported_attention_backends
)
def __init__(self, config: ImageEncoderConfig) -> None:
super().__init__()
self.config = config
if not self.supported_attention_backends:
raise ValueError(
f"Subclass {self.__class__.__name__} must define _supported_attention_backends"
)
@abstractmethod
def forward(self, pixel_values: torch.Tensor, **kwargs) -> BaseEncoderOutput:
pass
@property
def supported_attention_backends(self) -> set[AttentionBackendEnum]:
return self._supported_attention_backends
@@ -0,0 +1,46 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# type: ignore
import os
import torch
import torch.nn as nn
from transformers import BertModel, BertTokenizer
class HunyuanClip(nn.Module):
"""
Hunyuan clip code copied from https://github.com/huggingface/diffusers/blob/main/src/diffusers/pipelines/hunyuandit/pipeline_hunyuandit.py
hunyuan's clip used BertModel and BertTokenizer, so we copy it.
"""
def __init__(self, model_dir, max_length=77):
super().__init__()
self.max_length = max_length
self.tokenizer = BertTokenizer.from_pretrained(
os.path.join(model_dir, "tokenizer")
)
self.text_encoder = BertModel.from_pretrained(
os.path.join(model_dir, "clip_text_encoder")
)
@torch.no_grad
def forward(self, prompts, with_mask=True):
self.device = next(self.text_encoder.parameters()).device
text_inputs = self.tokenizer(
prompts,
padding="max_length",
max_length=self.max_length,
truncation=True,
return_attention_mask=True,
return_tensors="pt",
)
prompt_embeds = self.text_encoder(
text_inputs.input_ids.to(self.device),
attention_mask=(
text_inputs.attention_mask.to(self.device) if with_mask else None
),
)
return prompt_embeds.last_hidden_state, prompt_embeds.pooler_output
@@ -0,0 +1,804 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/models/clip.py
# Adapted from transformers: https://github.com/huggingface/transformers/blob/v4.39.0/src/transformers/models/clip/modeling_clip.py
"""Minimal implementation of CLIPVisionModel intended to be only used
within a vision language model."""
from collections.abc import Iterable
from typing import Optional
import torch
import torch.nn as nn
from sglang.multimodal_gen.configs.models.encoders import (
BaseEncoderOutput,
CLIPTextConfig,
CLIPVisionConfig,
)
from sglang.multimodal_gen.runtime.distributed import divide, get_tp_world_size
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
# TODO: support quantization
# from vllm.model_executor.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.models.encoders.base import ImageEncoder, TextEncoder
from sglang.multimodal_gen.runtime.models.encoders.vision import (
resolve_visual_encoder_outputs,
)
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
# Adapted from https://github.com/huggingface/transformers/blob/v4.39.0/src/transformers/models/clip/modeling_clip.py#L164 # noqa
class CLIPVisionEmbeddings(nn.Module):
def __init__(self, config: CLIPVisionConfig):
super().__init__()
self.config = config
self.embed_dim = config.hidden_size
self.image_size = config.image_size
self.patch_size = config.patch_size
assert self.image_size % self.patch_size == 0
self.class_embedding = nn.Parameter(torch.randn(self.embed_dim))
self.patch_embedding = nn.Conv2d(
in_channels=config.num_channels,
out_channels=self.embed_dim,
kernel_size=self.patch_size,
stride=self.patch_size,
bias=False,
)
self.num_patches = (self.image_size // self.patch_size) ** 2
self.num_positions = self.num_patches + 1
self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
self.register_buffer(
"position_ids",
torch.arange(self.num_positions).expand((1, -1)),
persistent=False,
)
def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
batch_size = pixel_values.shape[0]
target_dtype = self.patch_embedding.weight.dtype
patch_embeds = self.patch_embedding(
pixel_values.to(dtype=target_dtype)
) # shape = [*, width, grid, grid]
patch_embeds = patch_embeds.flatten(2).transpose(1, 2)
class_embeds = self.class_embedding.expand(batch_size, 1, -1)
embeddings = torch.cat([class_embeds, patch_embeds], dim=1)
embeddings = embeddings + self.position_embedding(self.position_ids)
return embeddings
class CLIPTextEmbeddings(nn.Module):
def __init__(self, config: CLIPTextConfig):
super().__init__()
self.config = config
embed_dim = config.hidden_size
self.token_embedding = nn.Embedding(config.vocab_size, embed_dim)
self.position_embedding = nn.Embedding(
config.max_position_embeddings, embed_dim
)
# position_ids (1, len position emb) is contiguous in memory and exported when serialized
self.register_buffer(
"position_ids",
torch.arange(config.max_position_embeddings).expand((1, -1)),
persistent=False,
)
def forward(
self,
input_ids: torch.LongTensor | None = None,
position_ids: torch.LongTensor | None = None,
inputs_embeds: torch.FloatTensor | None = None,
) -> torch.Tensor:
if input_ids is not None:
seq_length = input_ids.shape[-1]
elif inputs_embeds is not None:
seq_length = inputs_embeds.shape[-2]
else:
raise ValueError("Either input_ids or inputs_embeds must be provided.")
max_position_embedding = self.position_embedding.weight.shape[0]
if seq_length > max_position_embedding:
raise ValueError(
f"Sequence length must be less than max_position_embeddings (got `sequence length`: "
f"{seq_length} and max_position_embeddings: {max_position_embedding}"
)
if position_ids is None:
position_ids = self.position_ids[:, :seq_length]
if inputs_embeds is None:
inputs_embeds = self.token_embedding(input_ids)
position_embeddings = self.position_embedding(position_ids)
embeddings = inputs_embeds + position_embeddings
return embeddings
class CLIPAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
def __init__(
self,
config: CLIPVisionConfig | CLIPTextConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.config = config
self.embed_dim = config.hidden_size
self.num_heads = config.num_attention_heads
self.head_dim = self.embed_dim // self.num_heads
if self.head_dim * self.num_heads != self.embed_dim:
raise ValueError(
"embed_dim must be divisible by num_heads "
f"(got `embed_dim`: {self.embed_dim} and `num_heads`:"
f" {self.num_heads})."
)
self.scale = self.head_dim**-0.5
self.dropout = config.attention_dropout
self.qkv_proj = QKVParallelLinear(
hidden_size=self.embed_dim,
head_size=self.head_dim,
total_num_heads=self.num_heads,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.out_proj = RowParallelLinear(
input_size=self.embed_dim,
output_size=self.embed_dim,
quant_config=quant_config,
prefix=f"{prefix}.out_proj",
)
self.tp_size = get_tp_world_size()
self.num_heads_per_partition = divide(self.num_heads, self.tp_size)
self.attn = LocalAttention(
self.num_heads_per_partition,
self.head_dim,
self.num_heads_per_partition,
softmax_scale=self.scale,
causal=True,
supported_attention_backends=config._supported_attention_backends,
)
def _shape(self, tensor: torch.Tensor, seq_len: int, bsz: int):
return (
tensor.view(bsz, seq_len, self.num_heads, self.head_dim)
.transpose(1, 2)
.contiguous()
)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
):
"""Input shape: Batch x Time x Channel"""
qkv_states, _ = self.qkv_proj(hidden_states)
query_states, key_states, value_states = qkv_states.chunk(3, dim=-1)
# use flash_attn_func
query_states = query_states.reshape(
query_states.shape[0],
query_states.shape[1],
self.num_heads_per_partition,
self.head_dim,
)
key_states = key_states.reshape(
key_states.shape[0],
key_states.shape[1],
self.num_heads_per_partition,
self.head_dim,
)
value_states = value_states.reshape(
value_states.shape[0],
value_states.shape[1],
self.num_heads_per_partition,
self.head_dim,
)
if self.attn.backend == AttentionBackendEnum.TORCH_SDPA:
query_states = query_states.transpose(1, 2) # [B, H, S, D]
key_states = key_states.transpose(1, 2)
value_states = value_states.transpose(1, 2)
if (
current_platform.is_rocm()
or current_platform.is_musa()
or current_platform.is_xpu()
):
# ROCm: Using both is_causal=True and attn_mask causes NaN.
# Use is_causal=True alone (padding mask not needed for CLIP
# since pooler_output comes from EOS token before padding).
# XXX (MUSA): Torch SDPA on MUSA currently does not support
# using both `attn_mask` and `is_causal=True` simultaneously.
attn_output = torch.nn.functional.scaled_dot_product_attention(
query_states,
key_states,
value_states,
attn_mask=None,
is_causal=True,
scale=self.scale,
)
else:
if attention_mask is not None:
# SDPA requires [B, 1, 1, S] or [B, S, S] format mask
if attention_mask.dim() == 2:
attn_mask = attention_mask[:, None, None, :].to(
dtype=query_states.dtype
)
attn_mask = (1.0 - attn_mask) * torch.finfo(
query_states.dtype
).min
else:
attn_mask = attention_mask
else:
attn_mask = None
attn_output = torch.nn.functional.scaled_dot_product_attention(
query_states,
key_states,
value_states,
attn_mask=attn_mask,
is_causal=attention_mask is None,
scale=self.scale,
)
attn_output = attn_output.transpose(1, 2)
else:
# Use LocalAttention (doesn't support attention_mask, but maintains compatibility)
attn_output = self.attn(query_states, key_states, value_states)
attn_output = attn_output.reshape(
attn_output.shape[0],
attn_output.shape[1],
self.num_heads_per_partition * self.head_dim,
)
attn_output, _ = self.out_proj(attn_output)
return attn_output, None
class CLIPMLP(nn.Module):
def __init__(
self,
config: CLIPVisionConfig | CLIPTextConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.activation_fn = get_act_fn(config.hidden_act)
self.fc1 = ColumnParallelLinear(
config.hidden_size,
config.intermediate_size,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.fc1",
)
self.fc2 = RowParallelLinear(
config.intermediate_size,
config.hidden_size,
bias=True,
quant_config=quant_config,
prefix=f"{prefix}.fc2",
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states, _ = self.fc1(hidden_states)
hidden_states = self.activation_fn(hidden_states)
hidden_states, _ = self.fc2(hidden_states)
return hidden_states
class CLIPEncoderLayer(nn.Module):
def __init__(
self,
config: CLIPTextConfig | CLIPVisionConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.self_attn = CLIPAttention(
config,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
)
self.layer_norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
self.mlp = CLIPMLP(config, quant_config=quant_config, prefix=f"{prefix}.mlp")
self.layer_norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.layer_norm1(hidden_states)
hidden_states, _ = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.layer_norm2(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class CLIPEncoder(nn.Module):
"""
Transformer encoder consisting of `config.num_hidden_layers` self
attention layers. Each layer is a [`CLIPEncoderLayer`].
Args:
config: CLIPConfig
"""
def __init__(
self,
config: CLIPVisionConfig | CLIPTextConfig,
quant_config: QuantizationConfig | None = None,
num_hidden_layers_override: int | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
if num_hidden_layers_override is None:
num_hidden_layers = config.num_hidden_layers
else:
num_hidden_layers = num_hidden_layers_override
self.layers = nn.ModuleList(
[
CLIPEncoderLayer(
config=config,
quant_config=quant_config,
prefix=f"{prefix}.layers.{layer_idx}",
)
for layer_idx in range(num_hidden_layers)
]
)
def forward(
self,
inputs_embeds: torch.Tensor,
return_all_hidden_states: bool,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor | list[torch.Tensor]:
hidden_states_pool = [inputs_embeds]
hidden_states = inputs_embeds
for idx, encoder_layer in enumerate(self.layers):
hidden_states = encoder_layer(
hidden_states,
attention_mask=attention_mask,
)
if return_all_hidden_states:
hidden_states_pool.append(hidden_states)
# If we have multiple feature sample layers, we return all hidden
# states in order and grab the ones we need by index.
if return_all_hidden_states:
return hidden_states_pool
return [hidden_states]
class CLIPTextTransformer(nn.Module):
def __init__(
self,
config: CLIPTextConfig,
quant_config: QuantizationConfig | None = None,
num_hidden_layers_override: int | None = None,
prefix: str = "",
):
super().__init__()
self.config = config
embed_dim = config.hidden_size
self.embeddings = CLIPTextEmbeddings(config)
self.encoder = CLIPEncoder(
config,
quant_config=quant_config,
num_hidden_layers_override=num_hidden_layers_override,
prefix=prefix,
)
self.final_layer_norm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
# For `pooled_output` computation
self.eos_token_id = config.eos_token_id
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
) -> BaseEncoderOutput:
output_hidden_states = (
output_hidden_states
if output_hidden_states is not None
else self.config.output_hidden_states
)
if input_ids is None:
raise ValueError("You have to specify input_ids")
input_shape = input_ids.size()
input_ids = input_ids.view(-1, input_shape[-1])
hidden_states = self.embeddings(input_ids=input_ids, position_ids=position_ids)
# CLIP's text model uses causal mask, prepare it here.
# https://github.com/openai/CLIP/blob/cfcffb90e69f37bf2ff1e988237a0fbe41f33c04/clip/model.py#L324
# causal_attention_mask = _create_4d_causal_attention_mask(
# input_shape, hidden_states.dtype, device=hidden_states.device
# )
# # expand attention_mask
# if attention_mask is not None and not self._use_flash_attention_2:
# raise NotImplementedError("attention_mask is not supported for CLIPTextTransformer")
# # [bsz, seq_len] -> [bsz, 1, tgt_seq_len, src_seq_len]
# attention_mask = _prepare_4d_attention_mask(attention_mask, hidden_states.dtype)
encoder_outputs = self.encoder(
inputs_embeds=hidden_states,
return_all_hidden_states=output_hidden_states,
attention_mask=attention_mask,
)
last_hidden_state = encoder_outputs[-1]
last_hidden_state = self.final_layer_norm(last_hidden_state)
if self.eos_token_id == 2:
# The `eos_token_id` was incorrect before PR #24773: Let's keep what have been done here.
# A CLIP model with such `eos_token_id` in the config can't work correctly with extra new tokens added
# ------------------------------------------------------------
# text_embeds.shape = [batch_size, sequence_length, transformer.width]
# take features from the eot embedding (eot_token is the highest number in each sequence)
# casting to torch.int for onnx compatibility: argmax doesn't support int64 inputs with opset 14
pooled_output = last_hidden_state[
torch.arange(
last_hidden_state.shape[0], device=last_hidden_state.device
),
input_ids.to(dtype=torch.int, device=last_hidden_state.device).argmax(
dim=-1
),
]
else:
# The config gets updated `eos_token_id` from PR #24773 (so the use of exta new tokens is possible)
pooled_output = last_hidden_state[
torch.arange(
last_hidden_state.shape[0], device=last_hidden_state.device
),
# We need to get the first position of `eos_token_id` value (`pad_token_ids` might equal to `eos_token_id`)
# Note: we assume each sequence (along batch dim.) contains an `eos_token_id` (e.g. prepared by the tokenizer)
(
input_ids.to(dtype=torch.int, device=last_hidden_state.device)
== self.eos_token_id
)
.int()
.argmax(dim=-1),
]
return BaseEncoderOutput(
last_hidden_state=last_hidden_state,
pooler_output=pooled_output,
hidden_states=encoder_outputs,
# attentions=encoder_outputs.attentions,
)
class CLIPTextModel(TextEncoder):
def __init__(
self,
config: CLIPTextConfig,
) -> None:
super().__init__(config)
self.text_model = CLIPTextTransformer(
config=config, quant_config=config.quant_config, prefix=config.prefix
)
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
outputs: BaseEncoderOutput = self.text_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
output_hidden_states=output_hidden_states,
)
return outputs
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
# Define mapping for stacked parameters
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("qkv_proj", "q_proj", "q"),
("qkv_proj", "k_proj", "k"),
("qkv_proj", "v_proj", "v"),
]
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
# Handle q_proj, k_proj, v_proj -> qkv_proj mapping
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name in name:
# Replace the weight name with the parameter name
model_param_name = name.replace(weight_name, param_name)
if model_param_name in params_dict:
param = params_dict[model_param_name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded_params.add(model_param_name)
break
else:
# Use default weight loader for all other parameters
if name in params_dict:
param = params_dict[name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class CLIPTextModelWithProjection(CLIPTextModel):
"""
CLIP text encoder with projection head for pooled_output.
"""
def __init__(
self,
config: CLIPTextConfig,
) -> None:
super().__init__(config)
self.text_projection = nn.Linear(
config.hidden_size, config.projection_dim, bias=False
)
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
outputs: BaseEncoderOutput = self.text_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
output_hidden_states=output_hidden_states,
)
pooled_output = outputs.pooler_output
if pooled_output is not None:
pooled_output = self.text_projection(pooled_output)
return BaseEncoderOutput(
last_hidden_state=outputs.last_hidden_state,
pooler_output=pooled_output,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
class CLIPVisionTransformer(nn.Module):
def __init__(
self,
config: CLIPVisionConfig,
quant_config: QuantizationConfig | None = None,
num_hidden_layers_override: int | None = None,
require_post_norm: bool | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
embed_dim = config.hidden_size
self.embeddings = CLIPVisionEmbeddings(config)
# NOTE: This typo of "layrnorm" is not fixed on purpose to match
# the original transformers code and name of the model weights.
self.pre_layrnorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
self.encoder = CLIPEncoder(
config=config,
quant_config=quant_config,
num_hidden_layers_override=num_hidden_layers_override,
prefix=f"{prefix}.encoder",
)
num_hidden_layers = config.num_hidden_layers
if len(self.encoder.layers) > config.num_hidden_layers:
raise ValueError(
f"The original encoder only has {num_hidden_layers} "
f"layers, but you requested {len(self.encoder.layers)} layers."
)
# If possible, skip post_layernorm to conserve memory
if require_post_norm is None:
require_post_norm = len(self.encoder.layers) == num_hidden_layers
if require_post_norm:
self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
else:
self.post_layernorm = None
def forward(
self,
pixel_values: torch.Tensor,
output_hidden_states: Optional[bool] = None,
feature_sample_layers: list[int] | None = None,
) -> BaseEncoderOutput:
hidden_states = self.embeddings(pixel_values)
hidden_states = self.pre_layrnorm(hidden_states)
return_all_hidden_states = output_hidden_states or (
feature_sample_layers is not None
)
# Produces either the last layer output or all of the hidden states,
# depending on if we have feature_sample_layers or not
encoder_outputs = self.encoder(
inputs_embeds=hidden_states,
return_all_hidden_states=return_all_hidden_states,
)
if not return_all_hidden_states:
encoder_outputs = encoder_outputs[0]
# Handle post-norm (if applicable) and stacks feature layers if needed
encoder_outputs = resolve_visual_encoder_outputs(
encoder_outputs,
feature_sample_layers,
self.post_layernorm,
self.config.num_hidden_layers,
)
if return_all_hidden_states:
return BaseEncoderOutput(hidden_states=encoder_outputs)
return BaseEncoderOutput(last_hidden_state=encoder_outputs)
class CLIPVisionModel(ImageEncoder):
config_class = CLIPVisionConfig
main_input_name = "pixel_values"
packed_modules_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
def __init__(self, config: CLIPVisionConfig) -> None:
super().__init__(config)
self.vision_model = CLIPVisionTransformer(
config=config,
quant_config=config.quant_config,
num_hidden_layers_override=config.num_hidden_layers_override,
require_post_norm=config.require_post_norm,
prefix=f"{config.prefix}.vision_model",
)
def forward(
self,
pixel_values: torch.Tensor,
feature_sample_layers: list[int] | None = None,
output_hidden_states: Optional[bool] = None,
**kwargs,
) -> BaseEncoderOutput:
base_encoder_output = self.vision_model(
pixel_values,
output_hidden_states=output_hidden_states,
feature_sample_layers=feature_sample_layers,
)
return base_encoder_output
@property
def device(self):
return next(self.parameters()).device
# (TODO) Add prefix argument for filtering out weights to be loaded
# ref: https://github.com/vllm-project/vllm/pull/7186#discussion_r1734163986
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
layer_count = len(self.vision_model.encoder.layers)
for name, loaded_weight in weights:
if name.startswith("visual_projection"):
continue
# post_layernorm is not needed in CLIPVisionModel
if (
name.startswith("vision_model.post_layernorm")
and self.vision_model.post_layernorm is None
):
continue
# omit layers when num_hidden_layers_override is set
if name.startswith("vision_model.encoder.layers"):
layer_idx = int(name.split(".")[3])
if layer_idx >= layer_count:
continue
for (
param_name,
weight_name,
shard_id,
) in self.config.arch_config.stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class BertModel(CLIPTextModel):
pass
EntryClass = [CLIPTextModel, CLIPTextModelWithProjection, CLIPVisionModel]
@@ -0,0 +1,447 @@
# SPDX-License-Identifier: Apache-2.0
#
# Gemma2 2B text encoder for SANA.
#
# This is a decoder-only language model used as a text encoder: we feed
# in tokenized text and extract the final hidden states (not logits) as
# the conditioning signal for SANA's cross-attention layers.
#
# Architecture follows google/gemma-2-2b-it:
# - 26 layers, alternating global / sliding-window attention
# - GQA with 8 query heads, 4 KV heads, head_dim=256
# - Pre/post attention + pre/post feedforward LayerNorm (Gemma2-style)
# - GeGLU activation (gelu_pytorch_tanh)
#
# Adapted from the Gemma3 text model implementation in this codebase.
import logging
from typing import Any, Iterable
import torch
from torch import nn
from sglang.multimodal_gen.configs.models.encoders.base import BaseEncoderOutput
from sglang.multimodal_gen.configs.models.encoders.gemma2 import Gemma2Config
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.activation import GeluAndMul
from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.layers.rotary_embedding import get_rope
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
logger = logging.getLogger(__name__)
class Gemma2RMSNorm(nn.Module):
def __init__(self, dim: int, eps: float = 1e-6):
super().__init__()
self.eps = eps
self.weight = nn.Parameter(torch.zeros(dim))
def _norm(self, x):
return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
def forward(self, x):
output = self._norm(x.float())
output = output * (1.0 + self.weight.float())
return output.type_as(x)
class Gemma2MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "gelu_pytorch_tanh":
raise ValueError(
"Gemma2 uses `gelu_pytorch_tanh` as the hidden activation. "
f"Got: {hidden_act}"
)
self.act_fn = GeluAndMul(approximate="tanh")
def forward(self, x):
x, _ = self.gate_up_proj(x)
x = self.act_fn(x)
x, _ = self.down_proj(x)
return x
class Gemma2Attention(nn.Module):
def __init__(
self,
layer_id: int,
config: Gemma2Config,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.layer_id = layer_id
self.hidden_size = hidden_size
tp_size = get_tp_world_size()
self.total_num_heads = num_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= tp_size:
assert self.total_num_kv_heads % tp_size == 0
else:
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
arch = config.arch_config
self.head_dim = arch.head_dim
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = arch.query_pre_attn_scalar**-0.5
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=self.total_num_heads,
total_num_kv_heads=self.total_num_kv_heads,
bias=arch.attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = RowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=hidden_size,
bias=arch.attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
# Gemma2 interleaves global (even layers) and sliding-window (odd layers)
# attention. This pattern reduces memory for long sequences while
# maintaining global context every other layer.
self.is_sliding = (layer_id % 2) == 1
if self.is_sliding:
self.sliding_window = arch.sliding_window
else:
self.sliding_window = None
self.rotary_emb = get_rope(
self.head_dim,
rotary_dim=self.head_dim,
max_position=arch.max_position_embeddings,
base=arch.rope_theta,
is_neox_style=True,
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
batch_size, seq_len, _ = q.shape
q = q.view(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.view(batch_size, seq_len, self.num_kv_heads, self.head_dim)
v = v.view(batch_size, seq_len, self.num_kv_heads, self.head_dim)
q, k = self.rotary_emb(positions, q, k)
query = q.transpose(1, 2)
key = k.transpose(1, 2)
value = v.transpose(1, 2)
attn_mask = torch.tril(
torch.ones(
(seq_len, seq_len), device=hidden_states.device, dtype=torch.bool
)
)
if self.is_sliding and self.sliding_window is not None:
idx = torch.arange(seq_len, device=hidden_states.device)
dist = idx[None, :] - idx[:, None]
too_far = dist > self.sliding_window
attn_mask = attn_mask.masked_fill(too_far, False)
if attention_mask is not None:
attn_mask = attn_mask[None, None, :, :].expand(
batch_size, 1, seq_len, seq_len
)
attn_mask = attn_mask & attention_mask.to(torch.bool)[:, None, None, :]
attn_kwargs = {
"attn_mask": attn_mask,
"dropout_p": 0.0,
"is_causal": False,
"scale": self.scaling,
}
if query.shape[1] != key.shape[1]:
attn_kwargs["enable_gqa"] = True
attn_output = torch.nn.functional.scaled_dot_product_attention(
query, key, value, **attn_kwargs
)
# NOTE: Gemma2 specifies attn_logit_softcapping (tanh(logits/cap)*cap) but
# PyTorch's scaled_dot_product_attention does not support it natively.
# For short text-encoder sequences (~300 tokens), the quality impact is
# negligible. A custom attention kernel would be needed for full fidelity.
attn_output = attn_output.transpose(1, 2)
attn_output = attn_output.reshape(
batch_size, seq_len, self.num_heads * self.head_dim
)
output, _ = self.o_proj(attn_output)
return output
class Gemma2DecoderLayer(nn.Module):
def __init__(
self,
layer_id: int,
config: Gemma2Config,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
arch = config.arch_config
self.hidden_size = arch.hidden_size
self.self_attn = Gemma2Attention(
layer_id=layer_id,
config=config,
hidden_size=self.hidden_size,
num_heads=arch.num_attention_heads,
num_kv_heads=arch.num_key_value_heads,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
)
self.mlp = Gemma2MLP(
hidden_size=self.hidden_size,
intermediate_size=arch.intermediate_size,
hidden_act=arch.hidden_activation,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
self.input_layernorm = Gemma2RMSNorm(self.hidden_size, eps=arch.rms_norm_eps)
self.post_attention_layernorm = Gemma2RMSNorm(
self.hidden_size, eps=arch.rms_norm_eps
)
self.pre_feedforward_layernorm = Gemma2RMSNorm(
self.hidden_size, eps=arch.rms_norm_eps
)
self.post_feedforward_layernorm = Gemma2RMSNorm(
self.hidden_size, eps=arch.rms_norm_eps
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
hidden_states = self.self_attn(positions, hidden_states, attention_mask)
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = residual + hidden_states
residual = hidden_states
hidden_states = self.pre_feedforward_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = self.post_feedforward_layernorm(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class Gemma2Model(nn.Module, LayerwiseOffloadableModuleMixin):
"""Gemma2 text encoder model for SANA pipeline."""
_fsdp_shard_conditions = []
layerwise_offload_dit_group_enabled = False
layer_names = ["layers"]
def __init__(self, config: Gemma2Config, **kwargs):
super().__init__()
self.config = config
arch = config.arch_config
self.quant_config = None
self.vocab_size = arch.vocab_size
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
arch.hidden_size,
org_num_embeddings=arch.vocab_size,
quant_config=self.quant_config,
)
self.embed_scale = arch.hidden_size**0.5
self.layers = nn.ModuleList(
[
Gemma2DecoderLayer(
layer_id=i,
config=config,
quant_config=self.quant_config,
prefix=f"model.layers.{i}",
)
for i in range(arch.num_hidden_layers)
]
)
self.norm = Gemma2RMSNorm(arch.hidden_size, eps=arch.rms_norm_eps)
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids) * self.embed_scale
def forward(
self,
input_ids: torch.Tensor | None = None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError(
"You must specify exactly one of input_ids or inputs_embeds"
)
output_hidden_states = (
output_hidden_states
if output_hidden_states is not None
else getattr(self.config.arch_config, "output_hidden_states", False)
)
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
if position_ids is None:
position_ids = torch.arange(
0, hidden_states.shape[1], device=hidden_states.device
).unsqueeze(0)
all_hidden_states: tuple[Any, ...] | None = () if output_hidden_states else None
for layer in self.layers:
if all_hidden_states is not None:
all_hidden_states += (hidden_states,)
hidden_states = layer(position_ids, hidden_states, attention_mask)
hidden_states = self.norm(hidden_states)
if all_hidden_states is not None:
all_hidden_states += (hidden_states,)
return BaseEncoderOutput(
last_hidden_state=hidden_states,
hidden_states=all_hidden_states,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
stacked_params_mapping = getattr(
self.config.arch_config, "stacked_params_mapping", None
)
if stacked_params_mapping is None:
stacked_params_mapping = [
(".qkv_proj", ".q_proj", "q"),
(".qkv_proj", ".k_proj", "k"),
(".qkv_proj", ".v_proj", "v"),
(".gate_up_proj", ".gate_proj", "0"),
(".gate_up_proj", ".up_proj", "1"),
]
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
# HF Gemma2Model stores weights as model.layers.X... / model.embed_tokens...
# Strip "model." prefix if present to match our naming
if name.startswith("model."):
name = name[len("model.") :]
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
self._load_with_shard_id(weight_loader, param, loaded_weight, shard_id)
break
else:
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
@staticmethod
def _load_with_shard_id(weight_loader, param, loaded_weight, shard_id):
try:
weight_loader(param, loaded_weight, shard_id)
return
except (AssertionError, TypeError):
pass
if isinstance(shard_id, str):
mapping = {"q": 0, "k": 1, "v": 2}
if shard_id in mapping:
weight_loader(param, loaded_weight, mapping[shard_id])
return
if shard_id.isdigit():
weight_loader(param, loaded_weight, int(shard_id))
return
elif isinstance(shard_id, int):
mapping = {0: "q", 1: "k", 2: "v"}
if shard_id in mapping:
weight_loader(param, loaded_weight, mapping[shard_id])
return
raise TypeError(
f"Unsupported shard_id={shard_id!r} for weight_loader={weight_loader}"
)
EntryClass = Gemma2Model
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,287 @@
# Copied and adapted from: https://github.com/Tencent-Hunyuan/Hunyuan3D-2
import numpy as np
import torch
import torch.nn as nn
from torchvision import transforms
from transformers import (
CLIPVisionConfig,
CLIPVisionModelWithProjection,
Dinov2Config,
Dinov2Model,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
assert embed_dim % 2 == 0
omega = np.arange(embed_dim // 2, dtype=np.float64)
omega /= embed_dim / 2.0
omega = 1.0 / 10000**omega # (D/2,)
pos = pos.reshape(-1) # (M,)
out = np.einsum("m,d->md", pos, omega) # (M, D/2), outer product
emb_sin = np.sin(out) # (M, D/2)
emb_cos = np.cos(out) # (M, D/2)
return np.concatenate([emb_sin, emb_cos], axis=1)
class ImageEncoder(nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"model.encoder.layer",
"model.vision_model.encoder.layers",
]
MODEL_CLASS = None
MODEL_CONFIG_CLASS = None
mean = []
std = []
def __init__(
self,
version=None,
config=None,
use_cls_token=True,
image_size=224,
**kwargs,
):
super().__init__()
if config is None:
self.model = self.MODEL_CLASS.from_pretrained(version)
else:
self.model = self.MODEL_CLASS(self.MODEL_CONFIG_CLASS.from_dict(config))
self.model.eval()
self.model.requires_grad_(False)
self.use_cls_token = use_cls_token
self.size = image_size // 14
self.num_patches = (image_size // 14) ** 2
if self.use_cls_token:
self.num_patches += 1
self.transform = transforms.Compose(
[
transforms.Resize(
image_size, transforms.InterpolationMode.BILINEAR, antialias=True
),
transforms.CenterCrop(image_size),
transforms.Normalize(
mean=self.mean,
std=self.std,
),
]
)
def forward(self, image, mask=None, value_range=(-1, 1), **kwargs):
if value_range is not None:
low, high = value_range
image = (image - low) / (high - low)
image = image.to(self.model.device, dtype=self.model.dtype)
inputs = self.transform(image)
outputs = self.model(inputs)
last_hidden_state = outputs.last_hidden_state
if not self.use_cls_token:
last_hidden_state = last_hidden_state[:, 1:, :]
return last_hidden_state
def unconditional_embedding(self, batch_size, **kwargs):
device = next(self.model.parameters()).device
dtype = next(self.model.parameters()).dtype
zero = torch.zeros(
batch_size,
self.num_patches,
self.model.config.hidden_size,
device=device,
dtype=dtype,
)
return zero
class CLIPImageEncoder(ImageEncoder):
MODEL_CLASS = CLIPVisionModelWithProjection
MODEL_CONFIG_CLASS = CLIPVisionConfig
mean = [0.48145466, 0.4578275, 0.40821073]
std = [0.26862954, 0.26130258, 0.27577711]
class DinoImageEncoder(ImageEncoder):
MODEL_CLASS = Dinov2Model
MODEL_CONFIG_CLASS = Dinov2Config
mean = [0.485, 0.456, 0.406]
std = [0.229, 0.224, 0.225]
class DinoImageEncoderMV(DinoImageEncoder):
_aliases = [
"hy3dshape.models.conditioner.DinoImageEncoderMV",
]
def __init__(
self,
version=None,
config=None,
use_cls_token=True,
image_size=224,
view_num=4,
**kwargs,
):
super().__init__(version, config, use_cls_token, image_size, **kwargs)
self.view_num = view_num
self.num_patches = self.num_patches
pos = np.arange(self.view_num, dtype=np.float32)
view_embedding = torch.from_numpy(
get_1d_sincos_pos_embed_from_grid(self.model.config.hidden_size, pos)
).float()
view_embedding = view_embedding.unsqueeze(1).repeat(1, self.num_patches, 1)
self.view_embed = view_embedding.unsqueeze(0)
def forward(self, image, mask=None, value_range=(-1, 1), view_idxs=None, **kwargs):
if value_range is not None:
low, high = value_range
image = (image - low) / (high - low)
image = image.to(self.model.device, dtype=self.model.dtype)
bs, num_views, c, h, w = image.shape
image = image.view(bs * num_views, c, h, w)
inputs = self.transform(image)
outputs = self.model(inputs)
last_hidden_state = outputs.last_hidden_state
last_hidden_state = last_hidden_state.view(
bs, num_views, last_hidden_state.shape[-2], last_hidden_state.shape[-1]
)
view_embedding = self.view_embed.to(last_hidden_state.dtype).to(
last_hidden_state.device
)
if view_idxs is not None:
assert len(view_idxs) == bs
view_embeddings = []
for i in range(bs):
view_idx = view_idxs[i]
assert num_views == len(view_idx)
view_embeddings.append(self.view_embed[:, view_idx, ...])
view_embedding = (
torch.cat(view_embeddings, 0)
.to(last_hidden_state.dtype)
.to(last_hidden_state.device)
)
if num_views != self.view_num:
view_embedding = view_embedding[:, :num_views, ...]
last_hidden_state = last_hidden_state + view_embedding
last_hidden_state = last_hidden_state.view(
bs, num_views * last_hidden_state.shape[-2], last_hidden_state.shape[-1]
)
return last_hidden_state
def unconditional_embedding(self, batch_size, view_idxs, **kwargs):
device = next(self.model.parameters()).device
dtype = next(self.model.parameters()).dtype
zero = torch.zeros(
batch_size,
self.num_patches * len(view_idxs[0]),
self.model.config.hidden_size,
device=device,
dtype=dtype,
)
return zero
def build_image_encoder(config):
if config["type"] == "CLIPImageEncoder":
return CLIPImageEncoder(**config["kwargs"])
elif config["type"] == "DinoImageEncoder":
return DinoImageEncoder(**config["kwargs"])
elif config["type"] == "DinoImageEncoderMV":
return DinoImageEncoderMV(**config["kwargs"])
else:
raise ValueError(f'Unknown image encoder type: {config["type"]}')
class DualImageEncoder(nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"main_image_encoder.model.encoder.layer",
"main_image_encoder.model.vision_model.encoder.layers",
"additional_image_encoder.model.encoder.layer",
"additional_image_encoder.model.vision_model.encoder.layers",
]
def __init__(
self,
main_image_encoder,
additional_image_encoder,
):
super().__init__()
self.main_image_encoder = build_image_encoder(main_image_encoder)
self.additional_image_encoder = build_image_encoder(additional_image_encoder)
def forward(self, image, mask=None, **kwargs):
outputs = {
"main": self.main_image_encoder(image, mask=mask, **kwargs),
"additional": self.additional_image_encoder(image, mask=mask, **kwargs),
}
return outputs
def unconditional_embedding(self, batch_size, **kwargs):
outputs = {
"main": self.main_image_encoder.unconditional_embedding(
batch_size, **kwargs
),
"additional": self.additional_image_encoder.unconditional_embedding(
batch_size, **kwargs
),
}
return outputs
class SingleImageEncoder(nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"main_image_encoder.model.encoder.layer",
"main_image_encoder.model.vision_model.encoder.layers",
]
def __init__(
self,
main_image_encoder,
):
super().__init__()
self.main_image_encoder = build_image_encoder(main_image_encoder)
def forward(self, image, mask=None, **kwargs):
outputs = {
"main": self.main_image_encoder(image, mask=mask, **kwargs),
}
return outputs
def unconditional_embedding(self, batch_size, **kwargs):
outputs = {
"main": self.main_image_encoder.unconditional_embedding(
batch_size, **kwargs
),
}
return outputs
# Entry class for model registry
EntryClass = [
SingleImageEncoder,
DualImageEncoder,
DinoImageEncoder,
DinoImageEncoderMV,
]
@@ -0,0 +1,190 @@
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Iterable
from typing import Tuple
import torch
from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLTextConfig
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput
from sglang.multimodal_gen.configs.models.encoders.ideogram import (
Ideogram4TextEncoderConfig,
)
from sglang.multimodal_gen.runtime.layers.quantization.bitsandbytes import (
BitsAndBytesConfig,
attach_bitsandbytes_4bit_quant_states,
build_bitsandbytes_4bit_quant_states,
is_bitsandbytes_4bit_state_name,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
from sglang.multimodal_gen.runtime.models.encoders.qwen3vl import Qwen3VLTextModel
class IdeogramQwen3VLTextEncoder(TextEncoder):
"""Language-only Qwen3-VL text encoder stored inside Ideogram checkpoints."""
_activation_layers = (0, 3, 6, 9, 12, 15, 18, 21, 24, 27, 30, 33, 35)
def __init__(self, config: Ideogram4TextEncoderConfig) -> None:
super().__init__(config)
arch_config = config.arch_config
text_config = getattr(arch_config, "text_config")
if isinstance(text_config, dict):
text_config = Qwen3VLTextConfig(**text_config)
self._uses_bitsandbytes_4bit = getattr(
arch_config, "ideogram_bnb_4bit_weight_only", False
)
self._uses_weight_only_fp8 = getattr(
arch_config, "ideogram_fp8_weight_only", False
)
quant_config = None
if self._uses_bitsandbytes_4bit:
source_quant_config = getattr(arch_config, "quantization_config")
if isinstance(source_quant_config, dict):
quant_config_dict = source_quant_config
else:
quant_config_dict = source_quant_config.to_dict()
quant_config = BitsAndBytesConfig.from_config(quant_config_dict)
self.language_model = Qwen3VLTextModel(
text_config,
quant_config=quant_config,
use_weight_only_fp8=self._uses_weight_only_fp8,
use_tensor_parallel=True,
)
@torch.no_grad()
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
outputs = self.language_model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
inputs_embeds=inputs_embeds,
use_cache=False,
output_hidden_states=output_hidden_states,
return_dict=True,
)
return BaseEncoderOutput(
last_hidden_state=outputs.last_hidden_state,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
def encode_ideogram_features(
self,
token_ids: torch.Tensor,
text_position_ids: torch.Tensor,
indicator: torch.Tensor,
llm_token_indicator: int,
) -> torch.Tensor:
batch_size, seq_len = token_ids.shape
hidden_size = self.language_model.config.hidden_size
out_dim = hidden_size * len(self._activation_layers)
features = torch.zeros(
batch_size,
seq_len,
out_dim,
dtype=torch.float32,
device=token_ids.device,
)
for batch_idx in range(batch_size):
text_mask = indicator[batch_idx] == llm_token_indicator
cur_token_ids = token_ids[batch_idx, text_mask].unsqueeze(0)
if cur_token_ids.numel() == 0:
continue
pos_2d = text_position_ids[batch_idx, text_mask, 0].unsqueeze(0)
position_ids = pos_2d[None, ...].expand(4, 1, -1)
attention_mask = torch.ones_like(cur_token_ids)
with set_forward_context(current_timestep=0, attn_metadata=None):
outputs = self.forward(
input_ids=cur_token_ids,
position_ids=position_ids,
attention_mask=attention_mask,
output_hidden_states=True,
)
assert outputs.hidden_states is not None
selected = [outputs.hidden_states[i] for i in self._activation_layers]
stacked = torch.stack(selected, dim=0).permute(1, 2, 3, 0)
features[batch_idx, text_mask] = stacked.reshape(
1, cur_token_ids.shape[1], -1
)[0].to(torch.float32)
return features
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
if self._uses_bitsandbytes_4bit:
return self._load_bitsandbytes_4bit_weights(weights)
loaded_params: set[str] = set()
params_dict = dict(self.named_parameters(remove_duplicate=False))
for name, loaded_weight in weights:
if name.startswith("visual."):
continue
if "rotary_emb.inv_freq" in name:
continue
param = params_dict.get(name)
if param is None:
raise KeyError(
f"Unexpected weight name while loading Ideogram text encoder: {name}"
)
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight.to(param.dtype))
loaded_params.add(name)
return loaded_params
def _load_bitsandbytes_4bit_weights(
self, weights: Iterable[Tuple[str, torch.Tensor]]
):
params_dict = dict(self.named_parameters(remove_duplicate=False))
raw_quant_state: dict[str, torch.Tensor] = {}
normal_weight_names: list[str] = []
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if is_bitsandbytes_4bit_state_name(name):
if "quant_state.bitsandbytes" in name:
loaded_weight = loaded_weight.cpu().data
raw_quant_state[name] = loaded_weight
continue
if name.startswith("visual."):
continue
if "rotary_emb.inv_freq" in name:
continue
param = params_dict.get(name)
if param is None:
raise KeyError(
f"Unexpected weight name while loading Ideogram text encoder: {name}"
)
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight.to(param.dtype))
normal_weight_names.append(name)
loaded_params.add(name)
quant_states = build_bitsandbytes_4bit_quant_states(
normal_weight_names,
raw_quant_state,
next(self.parameters()).device,
)
attach_bitsandbytes_4bit_quant_states(params_dict, quant_states)
quantized_params_missing_state = [
name
for name, param in params_dict.items()
if getattr(param, "use_bitsandbytes_4bit", False)
and name not in quant_states
]
if quantized_params_missing_state:
raise ValueError(
"Missing bitsandbytes quant_state for Ideogram text encoder weights: "
f"{quantized_params_missing_state[:8]}"
)
return loaded_params
EntryClass = IdeogramQwen3VLTextEncoder
@@ -0,0 +1,460 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/models/llama.py
# Adapted from
# https://github.com/huggingface/transformers/blob/v4.28.0/src/transformers/models/llama/modeling_llama.py
# Copyright 2023 The vLLM team.
# Copyright 2022 EleutherAI and the HuggingFace Inc. team. All rights reserved.
#
# This code is based on EleutherAI's GPT-NeoX library and the GPT-NeoX
# and OPT implementations in this library. It has been modified from its
# original forms to accommodate minor architectural differences compared
# to GPT-NeoX and OPT used by the Meta AI team that trained the model.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Inference-only LLaMA model compatible with HuggingFace weights."""
from collections.abc import Iterable
from typing import Any
import torch
from torch import nn
# from ..utils import (extract_layer_index)
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput, LlamaConfig
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
# from vllm.model_executor.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.layers.rotary_embedding import get_rope
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import (
default_weight_loader,
maybe_remap_kv_scale_name,
)
from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
class LlamaMLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: QuantizationConfig | None = None,
bias: bool = False,
prefix: str = "",
) -> None:
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size] * 2,
# output_size=intermediate_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(
f"Unsupported activation: {hidden_act}. "
"Only silu is supported for now."
)
self.act_fn = SiluAndMul()
def forward(self, x):
x, _ = self.gate_up_proj(x)
x = self.act_fn(x)
x, _ = self.down_proj(x)
return x
class LlamaAttention(nn.Module):
def __init__(
self,
config: LlamaConfig,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
rope_theta: float = 10000,
rope_scaling: dict[str, Any] | None = None,
max_position_embeddings: int = 8192,
quant_config: QuantizationConfig | None = None,
bias: bool = False,
bias_o_proj: bool = False,
prefix: str = "",
) -> None:
super().__init__()
# layer_idx = extract_layer_index(prefix)
self.hidden_size = hidden_size
tp_size = get_tp_world_size()
self.total_num_heads = num_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= tp_size:
# Number of KV heads is greater than TP size, so we partition
# the KV heads across multiple tensor parallel GPUs.
assert self.total_num_kv_heads % tp_size == 0
else:
# Number of KV heads is less than TP size, so we replicate
# the KV heads across multiple tensor parallel GPUs.
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
# MistralConfig has an optional head_dim introduced by Mistral-Nemo
self.head_dim = getattr(
config, "head_dim", self.hidden_size // self.total_num_heads
)
# Phi models introduced a partial_rotary_factor parameter in the config
partial_rotary_factor = getattr(config, "partial_rotary_factor", 1)
self.rotary_dim = int(partial_rotary_factor * self.head_dim)
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.rope_theta = rope_theta
self.max_position_embeddings = max_position_embeddings
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=self.total_num_heads,
total_num_kv_heads=self.total_num_kv_heads,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = RowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=hidden_size,
bias=bias_o_proj,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
is_neox_style = True
is_gguf = (
quant_config
and hasattr(quant_config, "get_name")
and quant_config.get_name() == "gguf"
)
if is_gguf and config.model_type == "llama":
is_neox_style = False
self.rotary_emb = get_rope(
self.head_dim,
rotary_dim=self.rotary_dim,
max_position=max_position_embeddings,
base=int(rope_theta),
rope_scaling=rope_scaling,
is_neox_style=is_neox_style,
)
self.attn = LocalAttention(
self.num_heads,
self.head_dim,
self.num_kv_heads,
softmax_scale=self.scaling,
causal=True,
supported_attention_backends=config._supported_attention_backends,
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q, k = self.rotary_emb(positions, q, k)
# attn_output = self.attn(q, k, v)
# use flash_attn_func
# TODO (Attn abstraction and backend)
# reshape q, k, v to (batch_size, seq_len, num_heads, head_dim)
batch_size = q.shape[0]
seq_len = q.shape[1]
q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
v = v.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
# import pdb; pdb.set_trace()
# attn_output = flash_attn_varlen_func(q, k, v, softmax_scale=self.scaling, causal=True)
attn_output = self.attn(q, k, v)
attn_output = attn_output.reshape(
batch_size, seq_len, self.num_heads * self.head_dim
)
output, _ = self.o_proj(attn_output)
return output
class LlamaDecoderLayer(nn.Module):
def __init__(
self,
config: LlamaConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = config.hidden_size
rope_theta = config.rope_parameters["rope_theta"]
rope_scaling = config.rope_parameters
if rope_scaling is not None and getattr(
config, "original_max_position_embeddings", None
):
rope_scaling["original_max_position_embeddings"] = (
config.original_max_position_embeddings
)
max_position_embeddings = getattr(config, "max_position_embeddings", 8192)
# Support abacusai/Smaug-72B-v0.1 with attention_bias
# Support internlm/internlm-7b with bias
attention_bias = getattr(config, "attention_bias", False) or getattr(
config, "bias", False
)
bias_o_proj = attention_bias
# support internlm/internlm3-8b with qkv_bias
if hasattr(config, "qkv_bias"):
attention_bias = config.qkv_bias
self.self_attn = LlamaAttention(
config=config,
hidden_size=self.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=getattr(
config, "num_key_value_heads", config.num_attention_heads
),
rope_theta=rope_theta,
rope_scaling=rope_scaling,
max_position_embeddings=max_position_embeddings,
quant_config=quant_config,
bias=attention_bias,
bias_o_proj=bias_o_proj,
prefix=f"{prefix}.self_attn",
)
self.mlp = LlamaMLP(
hidden_size=self.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
bias=getattr(config, "mlp_bias", False),
prefix=f"{prefix}.mlp",
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
# Self Attention
if residual is None:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else:
hidden_states, residual = self.input_layernorm(hidden_states, residual)
hidden_states = self.self_attn(positions=positions, hidden_states=hidden_states)
# Fully Connected
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
hidden_states = self.mlp(hidden_states)
return hidden_states, residual
class LlamaModel(TextEncoder):
def __init__(
self,
config: LlamaConfig,
):
super().__init__(config)
self.config = config
self.quant_config = self.config.quant_config
if config.lora_config is not None:
max_loras = 1
lora_vocab_size = 1
if hasattr(config.lora_config, "max_loras"):
max_loras = config.lora_config.max_loras
if hasattr(config.lora_config, "lora_extra_vocab_size"):
lora_vocab_size = config.lora_config.lora_extra_vocab_size
lora_vocab = lora_vocab_size * max_loras
else:
lora_vocab = 0
self.vocab_size = config.vocab_size + lora_vocab
self.org_vocab_size = config.vocab_size
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
quant_config=config.quant_config,
)
self.layers = nn.ModuleList(
[
LlamaDecoderLayer(
config=config,
quant_config=config.quant_config,
prefix=f"{config.prefix}.layers.{i}",
)
for i in range(config.num_hidden_layers)
]
)
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
output_hidden_states = (
output_hidden_states
if output_hidden_states is not None
else self.config.output_hidden_states
)
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
residual = None
if position_ids is None:
position_ids = torch.arange(
0, hidden_states.shape[1], device=hidden_states.device
).unsqueeze(0)
all_hidden_states: tuple[Any, ...] | None = () if output_hidden_states else None
for layer in self.layers:
if all_hidden_states is not None:
# TODO
all_hidden_states += (
(hidden_states,)
if residual is None
else (hidden_states + residual,)
)
hidden_states, residual = layer(position_ids, hidden_states, residual)
hidden_states, _ = self.norm(hidden_states, residual)
# add hidden states from the last decoder layer
if all_hidden_states is not None:
all_hidden_states += (hidden_states,)
# TODO(will): maybe unify the output format with other models and use
# our own class
output = BaseEncoderOutput(
last_hidden_state=hidden_states,
# past_key_values=past_key_values if use_cache else None,
hidden_states=all_hidden_states,
# attentions=all_self_attns,
)
return output
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
if "rotary_emb.inv_freq" in name:
continue
if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
# Models trained using ColossalAI may include these tensors in
# the checkpoint. Skip them.
continue
# if (self.quant_config is not None and
# (scale_name := self.quant_config.get_cache_scale(name))):
# # Loading kv cache quantization scales
# param = params_dict[scale_name]
# weight_loader = getattr(param, "weight_loader",
# default_weight_loader)
# loaded_weight = (loaded_weight if loaded_weight.dim() == 0 else
# loaded_weight[0])
# weight_loader(param, loaded_weight)
# loaded_params.add(scale_name)
# continue
if "scale" in name:
# Remapping the name of FP8 kv-scale.
kv_scale_name: str | None = maybe_remap_kv_scale_name(name, params_dict)
if kv_scale_name is None:
continue
else:
name = kv_scale_name
for (
param_name,
weight_name,
shard_id,
) in self.config.arch_config.stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
EntryClass = LlamaModel
@@ -0,0 +1,630 @@
# coding=utf-8
# Copyright 2025 HuggingFace Inc. team. All rights reserved.
#
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
from contextlib import nullcontext
from typing import Iterable, Optional, Union
import torch
from torch import nn
from torch.nn.attention import SDPBackend, sdpa_kernel
from transformers import Cache, DynamicCache, LlavaConfig, Mistral3Config, MistralConfig
from transformers.activations import ACT2FN
from transformers.masking_utils import (
create_causal_mask,
create_sliding_window_causal_mask,
)
from transformers.modeling_outputs import BaseModelOutputWithPast
from transformers.models.mistral3.modeling_mistral3 import (
Mistral3CausalLMOutputWithPast,
Mistral3ModelOutputWithPast,
)
from transformers.models.mistral.modeling_mistral import (
MistralPreTrainedModel,
MistralRMSNorm,
MistralRotaryEmbedding,
apply_rotary_pos_emb,
)
from sglang.multimodal_gen.runtime.distributed import (
get_tp_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.linear import (
ColumnParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.platforms import (
AttentionBackendEnum,
current_platform,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_CREATE_CAUSAL_MASK_ARG = (
"inputs_embeds"
if "inputs_embeds" in inspect.signature(create_causal_mask).parameters
else "input_embeds"
)
def _tp_world_size() -> int:
if not model_parallel_is_initialized():
return 1
return get_tp_world_size()
def _linear_output(linear: nn.Module, x: torch.Tensor) -> torch.Tensor:
output = linear(x)
return output[0] if isinstance(output, tuple) else output
def _make_column_linear(
in_features: int,
out_features: int,
*,
bias: bool,
use_tensor_parallel: bool,
):
if use_tensor_parallel:
return ColumnParallelLinear(
in_features,
out_features,
bias=bias,
gather_output=False,
)
return nn.Linear(in_features, out_features, bias=bias)
def _make_row_linear(
in_features: int,
out_features: int,
*,
bias: bool,
use_tensor_parallel: bool,
):
if use_tensor_parallel:
return RowParallelLinear(
in_features,
out_features,
bias=bias,
)
return nn.Linear(in_features, out_features, bias=bias)
def _can_use_unmasked_causal_attention(
attention_mask: Optional[torch.Tensor],
config: MistralConfig,
past_key_values: Optional[Cache],
) -> bool:
if (
getattr(config, "sliding_window", None) is not None
or past_key_values is not None
):
return False
if attention_mask is None:
return True
if attention_mask.dim() != 2:
return False
return bool(torch.all(attention_mask > 0).item())
def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
"""
This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep).
The hidden states go from (batch, num_key_value_heads, seqlen, head_dim) to
(batch, num_attention_heads, seqlen, head_dim)
"""
batch, num_key_value_heads, slen, head_dim = hidden_states.shape
if n_rep == 1:
return hidden_states
hidden_states = hidden_states[:, :, None, :, :].expand(
batch, num_key_value_heads, n_rep, slen, head_dim
)
return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
class MistralAttention(nn.Module):
"""Multi-headed attention from 'Attention Is All You Need' paper"""
def __init__(self, config: MistralConfig, layer_idx: int):
super().__init__()
self.config = config
self.layer_idx = layer_idx
self.head_dim = (
getattr(config, "head_dim", None)
or config.hidden_size // config.num_attention_heads
)
self.scaling = self.head_dim**-0.5
self.attention_dropout = config.attention_dropout
self.total_num_heads = config.num_attention_heads
self.total_num_key_value_heads = config.num_key_value_heads
tp_size = _tp_world_size()
q_size = self.total_num_heads * self.head_dim
kv_size = self.total_num_key_value_heads * self.head_dim
self.use_tensor_parallel = (
tp_size > 1
and self.total_num_heads % tp_size == 0
and self.total_num_key_value_heads % tp_size == 0
and q_size % tp_size == 0
and kv_size % tp_size == 0
)
self.num_heads = (
self.total_num_heads // tp_size
if self.use_tensor_parallel
else self.total_num_heads
)
self.num_key_value_heads = (
self.total_num_key_value_heads // tp_size
if self.use_tensor_parallel
else self.total_num_key_value_heads
)
self.num_key_value_groups = self.num_heads // self.num_key_value_heads
self.q_proj = _make_column_linear(
config.hidden_size,
q_size,
bias=False,
use_tensor_parallel=self.use_tensor_parallel,
)
self.k_proj = _make_column_linear(
config.hidden_size,
kv_size,
bias=False,
use_tensor_parallel=self.use_tensor_parallel,
)
self.v_proj = _make_column_linear(
config.hidden_size,
kv_size,
bias=False,
use_tensor_parallel=self.use_tensor_parallel,
)
self.o_proj = _make_row_linear(
q_size,
config.hidden_size,
bias=False,
use_tensor_parallel=self.use_tensor_parallel,
)
self.is_causal = True
self.attn = LocalAttention(
self.num_heads,
self.head_dim,
self.num_key_value_heads,
softmax_scale=self.scaling,
causal=True,
supported_attention_backends={
AttentionBackendEnum.FA,
AttentionBackendEnum.TORCH_SDPA,
},
allow_cudnn_sdp=True,
)
def forward(
self,
hidden_states: torch.Tensor,
position_embeddings: tuple[torch.Tensor, torch.Tensor],
attention_mask: Optional[torch.Tensor],
past_key_values: Optional[Cache] = None,
cache_position: Optional[torch.LongTensor] = None,
**kwargs,
) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
input_shape = hidden_states.shape[:-1]
hidden_shape = (*input_shape, -1, self.head_dim)
query_states = (
_linear_output(self.q_proj, hidden_states)
.view(hidden_shape)
.transpose(1, 2)
)
key_states = (
_linear_output(self.k_proj, hidden_states)
.view(hidden_shape)
.transpose(1, 2)
)
value_states = (
_linear_output(self.v_proj, hidden_states)
.view(hidden_shape)
.transpose(1, 2)
)
cos, sin = position_embeddings
query_states, key_states = apply_rotary_pos_emb(
query_states, key_states, cos, sin
)
if past_key_values is not None:
# sin and cos are specific to RoPE models; cache_position needed for the static cache
cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
key_states, value_states = past_key_values.update(
key_states, value_states, self.layer_idx, cache_kwargs
)
attn_output = self.attn(
query_states.transpose(1, 2),
key_states.transpose(1, 2),
value_states.transpose(1, 2),
attn_mask=attention_mask,
)
attn_output = attn_output.reshape(*input_shape, -1).contiguous()
attn_output = _linear_output(self.o_proj, attn_output)
return attn_output, None
class MistralTPMLP(nn.Module):
def __init__(self, config: MistralConfig):
super().__init__()
tp_size = _tp_world_size()
use_tensor_parallel = tp_size > 1 and config.intermediate_size % tp_size == 0
self.gate_proj = _make_column_linear(
config.hidden_size,
config.intermediate_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.up_proj = _make_column_linear(
config.hidden_size,
config.intermediate_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.down_proj = _make_row_linear(
config.intermediate_size,
config.hidden_size,
bias=False,
use_tensor_parallel=use_tensor_parallel,
)
self.act_fn = ACT2FN[config.hidden_act]
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.act_fn(_linear_output(self.gate_proj, x)) * _linear_output(
self.up_proj, x
)
return _linear_output(self.down_proj, x)
class MistralDecoderLayer(nn.Module):
def __init__(self, config: MistralConfig, layer_idx: int):
super().__init__()
self.hidden_size = config.hidden_size
self.self_attn = MistralAttention(config=config, layer_idx=layer_idx)
self.mlp = MistralTPMLP(config)
self.input_layernorm = MistralRMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.post_attention_layernorm = MistralRMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
use_cache: Optional[bool] = False,
cache_position: Optional[torch.LongTensor] = None,
position_embeddings: Optional[
tuple[torch.Tensor, torch.Tensor]
] = None, # necessary, but kept here for BC
**kwargs,
) -> torch.Tensor:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
# Self Attention
hidden_states, _ = self.self_attn(
hidden_states=hidden_states,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = residual + hidden_states
# Fully Connected
residual = hidden_states
hidden_states = self.post_attention_layernorm(hidden_states)
hidden_states = self.mlp(hidden_states)
hidden_states = residual + hidden_states
return hidden_states
class MistralModel(MistralPreTrainedModel):
def __init__(self, config: MistralConfig):
super().__init__(config)
self.padding_idx = config.pad_token_id
self.vocab_size = config.vocab_size
self.embed_tokens = nn.Embedding(
config.vocab_size, config.hidden_size, self.padding_idx
)
self.layers = nn.ModuleList(
[
MistralDecoderLayer(config, layer_idx)
for layer_idx in range(config.num_hidden_layers)
]
)
self.norm = MistralRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.rotary_emb = MistralRotaryEmbedding(config=config)
self.gradient_checkpointing = False
self.post_init()
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
use_cache: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
output_hidden_states: Optional[bool] = None,
**kwargs,
) -> BaseModelOutputWithPast:
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError(
"You must specify exactly one of input_ids or inputs_embeds"
)
if inputs_embeds is None:
inputs_embeds = self.embed_tokens(input_ids)
if use_cache and past_key_values is None:
past_key_values = DynamicCache(config=self.config)
if cache_position is None:
past_seen_tokens = (
past_key_values.get_seq_length() if past_key_values is not None else 0
)
cache_position = torch.arange(
past_seen_tokens,
past_seen_tokens + inputs_embeds.shape[1],
device=inputs_embeds.device,
)
if position_ids is None:
position_ids = cache_position.unsqueeze(0)
if _can_use_unmasked_causal_attention(
attention_mask, self.config, past_key_values
):
causal_mask = None
else:
mask_function = (
create_causal_mask
if getattr(self.config, "sliding_window", None) is None
else create_sliding_window_causal_mask
)
mask_kwargs = {
"config": self.config,
_CREATE_CAUSAL_MASK_ARG: inputs_embeds,
"attention_mask": attention_mask,
"past_key_values": past_key_values,
"position_ids": position_ids,
}
causal_mask = mask_function(**mask_kwargs)
hidden_states = inputs_embeds
position_embeddings = self.rotary_emb(hidden_states, position_ids)
hidden_states_pool = [] if output_hidden_states else None
for decoder_layer in self.layers[: self.config.num_hidden_layers]:
if output_hidden_states:
hidden_states_pool.append(hidden_states)
hidden_states = decoder_layer(
hidden_states,
attention_mask=causal_mask,
position_ids=position_ids,
past_key_values=past_key_values,
use_cache=use_cache,
cache_position=cache_position,
position_embeddings=position_embeddings,
**kwargs,
)
hidden_states = self.norm(hidden_states)
if output_hidden_states:
hidden_states_pool.append(hidden_states)
return BaseModelOutputWithPast(
hidden_states=hidden_states_pool,
last_hidden_state=hidden_states,
past_key_values=past_key_values if use_cache else None,
)
class Mistral3Model(nn.Module):
_checkpoint_conversion_mapping = {"language_model.model": "language_model"}
def __init__(self, config: Mistral3Config):
super().__init__()
self.language_model = MistralModel(config.text_config)
self.config = config
def get_input_embeddings(self):
return self.language_model.embed_tokens
def set_decoder(self, decoder):
self.language_model = decoder
def get_decoder(self):
return self.language_model
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
pixel_values: Optional[torch.FloatTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
vision_feature_layer: Optional[Union[int, list[int]]] = None,
use_cache: Optional[bool] = None,
output_attentions: Optional[bool] = None,
output_hidden_states: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
image_sizes: Optional[torch.Tensor] = None,
**kwargs,
) -> Union[tuple, Mistral3ModelOutputWithPast]:
del pixel_values, vision_feature_layer, return_dict
output_attentions = False
output_hidden_states = True
if (input_ids is None) ^ (inputs_embeds is not None):
raise ValueError(
"You must specify exactly one of input_ids or inputs_embeds"
)
if inputs_embeds is None:
inputs_embeds = self.get_input_embeddings()(input_ids)
outputs: BaseModelOutputWithPast = self.language_model(
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_attentions=output_attentions,
output_hidden_states=output_hidden_states,
return_dict=True,
cache_position=cache_position,
**kwargs,
)
return Mistral3ModelOutputWithPast(
last_hidden_state=outputs.last_hidden_state,
past_key_values=outputs.past_key_values,
hidden_states=outputs.hidden_states,
attentions=outputs.attentions,
)
class Mistral3ForConditionalGeneration(nn.Module, LayerwiseOffloadableModuleMixin):
_checkpoint_conversion_mapping = {
"^language_model.model": "model.language_model",
"^multi_modal_projector": "model.multi_modal_projector",
"^language_model.lm_head": "lm_head",
}
_tied_weights_keys = ["lm_head.weight"]
uses_sglang_forward_context = True
layerwise_offload_dit_group_enabled = False
layer_names = ["model.language_model.layers"]
def __init__(self, config: LlavaConfig):
super().__init__()
self.model = Mistral3Model(config.arch_config)
def get_input_embeddings(self):
return self.model.get_input_embeddings()
def set_decoder(self, decoder):
self.model.set_decoder(decoder)
def get_decoder(self):
return self.model.get_decoder()
# Make modules available through conditional class for BC
@property
def language_model(self):
return self.model.language_model
def forward(
self,
input_ids: Optional[torch.LongTensor] = None,
attention_mask: Optional[torch.Tensor] = None,
position_ids: Optional[torch.LongTensor] = None,
past_key_values: Optional[Cache] = None,
output_hidden_states: Optional[bool] = None,
inputs_embeds: Optional[torch.FloatTensor] = None,
labels: Optional[torch.LongTensor] = None,
use_cache: Optional[bool] = None,
return_dict: Optional[bool] = None,
cache_position: Optional[torch.LongTensor] = None,
logits_to_keep: Union[int, torch.Tensor] = 0,
image_sizes: Optional[torch.Tensor] = None,
**kwargs,
) -> Union[tuple, Mistral3CausalLMOutputWithPast]:
r"""
labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
(masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
Example:
"""
output_hidden_states = True
execution_tensor = input_ids if input_ids is not None else inputs_embeds
sdpa_context = (
sdpa_kernel(SDPBackend.CUDNN_ATTENTION)
if execution_tensor is not None
and execution_tensor.device.type == "cuda"
and current_platform.is_cuda()
else nullcontext()
)
with sdpa_context:
# FLUX.2 uses the text-only Mistral3 path but still expects the
# same local SDPA kernel choice as the official HF implementation.
outputs = self.model(
input_ids=input_ids,
attention_mask=attention_mask,
position_ids=position_ids,
past_key_values=past_key_values,
inputs_embeds=inputs_embeds,
use_cache=use_cache,
output_hidden_states=output_hidden_states,
return_dict=True,
cache_position=cache_position,
image_sizes=image_sizes,
**kwargs,
)
return Mistral3CausalLMOutputWithPast(
hidden_states=outputs.hidden_states,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
# Define mapping for stacked parameters
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
name_lower = name.lower()
if (
"vision" in name_lower
or "multi" in name_lower
or "lm_head" in name_lower
):
continue
final_name = name.replace("language_model.model.", "model.language_model.")
if final_name in params_dict:
param = params_dict[final_name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(final_name)
else:
logger.warning(f"Param {name=} {final_name=} from weight is not loaded")
return loaded_params
EntryClass = Mistral3ForConditionalGeneration
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,487 @@
from collections.abc import Iterable
from typing import Any
import torch
from torch import nn
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput
from sglang.multimodal_gen.configs.models.encoders.qwen3 import Qwen3TextConfig
from sglang.multimodal_gen.runtime.distributed import get_tp_world_size
from sglang.multimodal_gen.runtime.layers.activation import SiluAndMul
from sglang.multimodal_gen.runtime.layers.attention import LocalAttention
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.layers.rotary_embedding import get_rope
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import (
default_weight_loader,
maybe_remap_kv_scale_name,
)
from sglang.multimodal_gen.runtime.models.encoders.base import TextEncoder
class Qwen3MLP(nn.Module):
"""Qwen3 MLP with SwiGLU activation and tensor parallelism."""
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
quant_config: QuantizationConfig | None = None,
bias: bool = False,
prefix: str = "",
) -> None:
super().__init__()
self.gate_up_proj = MergedColumnParallelLinear(
input_size=hidden_size,
output_sizes=[intermediate_size] * 2,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
input_size=intermediate_size,
output_size=hidden_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(
f"Unsupported activation: {hidden_act}. Only silu is supported."
)
self.act_fn = SiluAndMul()
def forward(self, x: torch.Tensor) -> torch.Tensor:
x, _ = self.gate_up_proj(x)
x = self.act_fn(x)
x, _ = self.down_proj(x)
return x
class Qwen3Attention(nn.Module):
"""Qwen3 attention with QK-Norm and tensor parallelism.
Key difference from LLaMA: RMSNorm is applied to Q and K before attention.
"""
def __init__(
self,
config: Qwen3TextConfig,
hidden_size: int,
num_heads: int,
num_kv_heads: int,
rope_theta: float = 1000000.0,
rope_scaling: dict[str, Any] | None = None,
max_position_embeddings: int = 40960,
quant_config: QuantizationConfig | None = None,
bias: bool = False,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = hidden_size
tp_size = get_tp_world_size()
self.total_num_heads = num_heads
assert self.total_num_heads % tp_size == 0
self.num_heads = self.total_num_heads // tp_size
self.total_num_kv_heads = num_kv_heads
if self.total_num_kv_heads >= tp_size:
assert self.total_num_kv_heads % tp_size == 0
else:
assert tp_size % self.total_num_kv_heads == 0
self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
self.head_dim = getattr(
config, "head_dim", self.hidden_size // self.total_num_heads
)
self.rotary_dim = self.head_dim
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.scaling = self.head_dim**-0.5
self.rope_theta = rope_theta
self.max_position_embeddings = max_position_embeddings
# QKV projection with tensor parallelism
self.qkv_proj = QKVParallelLinear(
hidden_size=hidden_size,
head_size=self.head_dim,
total_num_heads=self.total_num_heads,
total_num_kv_heads=self.total_num_kv_heads,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
# Output projection
self.o_proj = RowParallelLinear(
input_size=self.total_num_heads * self.head_dim,
output_size=hidden_size,
bias=bias,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
# QK-Norm: Key difference from LLaMA
rms_norm_eps = getattr(config, "rms_norm_eps", 1e-6)
self.q_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=rms_norm_eps)
# Rotary embeddings
self.rotary_emb = get_rope(
self.head_dim,
rotary_dim=self.rotary_dim,
max_position=max_position_embeddings,
base=int(rope_theta),
rope_scaling=rope_scaling,
is_neox_style=True,
)
# Attention with FlashAttention/SageAttn support
self.attn = LocalAttention(
self.num_heads,
self.head_dim,
self.num_kv_heads,
softmax_scale=self.scaling,
causal=True,
supported_attention_backends=config._supported_attention_backends,
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
attention_lengths: tuple[int, ...] | None = None,
) -> torch.Tensor:
# QKV projection
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
# Reshape for QK-norm
batch_size, seq_len = q.shape[0], q.shape[1]
q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
v = v.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
# Apply QK-Norm (key difference from LLaMA)
q = self.q_norm(q)
k = self.k_norm(k)
# Reshape back for rotary embeddings
q = q.reshape(batch_size, seq_len, -1)
k = k.reshape(batch_size, seq_len, -1)
# Apply rotary embeddings
q, k = self.rotary_emb(positions, q, k)
# Reshape for attention
q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
# Attention
attn_output = self._masked_causal_attention(q, k, v, attention_lengths)
attn_output = attn_output.reshape(batch_size, seq_len, -1)
# Output projection
output, _ = self.o_proj(attn_output)
return output
def _masked_causal_attention(
self,
q: torch.Tensor,
k: torch.Tensor,
v: torch.Tensor,
attention_lengths: tuple[int, ...] | None,
) -> torch.Tensor:
if attention_lengths is None:
return self.attn(q, k, v)
seq_len = q.shape[1]
if all(valid_len == seq_len for valid_len in attention_lengths):
return self.attn(q, k, v)
outputs: list[torch.Tensor] = []
for batch_index, valid_len in enumerate(attention_lengths):
q_item = q[batch_index : batch_index + 1]
k_item = k[batch_index : batch_index + 1]
v_item = v[batch_index : batch_index + 1]
real_output = self.attn(
q_item[:, :valid_len],
k_item[:, :valid_len],
v_item[:, :valid_len],
)
if valid_len == seq_len:
outputs.append(real_output)
continue
pad_q = q_item[:, valid_len:].transpose(1, 2)
real_k = k_item[:, :valid_len].transpose(1, 2)
real_v = v_item[:, :valid_len].transpose(1, 2)
if self.num_heads != self.num_kv_heads:
repeat_factor = self.num_heads // self.num_kv_heads
real_k = real_k.repeat_interleave(repeat_factor, dim=1)
real_v = real_v.repeat_interleave(repeat_factor, dim=1)
pad_output = torch.nn.functional.scaled_dot_product_attention(
pad_q,
real_k,
real_v,
dropout_p=0.0,
is_causal=False,
scale=self.scaling,
).transpose(1, 2)
outputs.append(torch.cat([real_output, pad_output], dim=1))
return torch.cat(outputs, dim=0)
class Qwen3DecoderLayer(nn.Module):
"""Qwen3 transformer decoder layer."""
def __init__(
self,
config: Qwen3TextConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
self.hidden_size = config.hidden_size
rope_theta = config.rope_parameters["rope_theta"]
rope_scaling = config.rope_parameters
max_position_embeddings = getattr(config, "max_position_embeddings", 40960)
attention_bias = getattr(config, "attention_bias", False)
self.self_attn = Qwen3Attention(
config=config,
hidden_size=self.hidden_size,
num_heads=config.num_attention_heads,
num_kv_heads=getattr(
config, "num_key_value_heads", config.num_attention_heads
),
rope_theta=rope_theta,
rope_scaling=rope_scaling,
max_position_embeddings=max_position_embeddings,
quant_config=quant_config,
bias=attention_bias,
prefix=f"{prefix}.self_attn",
)
self.mlp = Qwen3MLP(
hidden_size=self.hidden_size,
intermediate_size=config.intermediate_size,
hidden_act=config.hidden_act,
quant_config=quant_config,
bias=getattr(config, "mlp_bias", False),
prefix=f"{prefix}.mlp",
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
residual: torch.Tensor | None,
attention_lengths: tuple[int, ...] | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
# Self Attention
if residual is None:
residual = hidden_states
hidden_states = self.input_layernorm(hidden_states)
else:
hidden_states, residual = self.input_layernorm(hidden_states, residual)
hidden_states = self.self_attn(
positions=positions,
hidden_states=hidden_states,
attention_lengths=attention_lengths,
)
# MLP
hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
hidden_states = self.mlp(hidden_states)
return hidden_states, residual
class Qwen3ForCausalLM(TextEncoder):
"""Qwen3 causal language model for text encoding in diffusion models.
Features:
- Tensor parallelism support
- FlashAttention/SageAttn/SDPA support via LocalAttention
- QK-Norm for better training stability
- FSDP sharding for CPU offload
"""
def __init__(self, config: Qwen3TextConfig) -> None:
super().__init__(config)
self.config = config
self.quant_config = config.quant_config
# Embedding layer with tensor parallelism
if config.lora_config is not None:
max_loras = getattr(config.lora_config, "max_loras", 1)
lora_vocab_size = getattr(config.lora_config, "lora_extra_vocab_size", 1)
lora_vocab = lora_vocab_size * max_loras
else:
lora_vocab = 0
self.vocab_size = config.vocab_size + lora_vocab
self.org_vocab_size = config.vocab_size
self.embed_tokens = VocabParallelEmbedding(
self.vocab_size,
config.hidden_size,
org_num_embeddings=config.vocab_size,
quant_config=config.quant_config,
)
# Transformer layers
self.layers = nn.ModuleList(
[
Qwen3DecoderLayer(
config=config,
quant_config=config.quant_config,
prefix=f"{config.prefix}.layers.{i}",
)
for i in range(config.num_hidden_layers)
]
)
# Final layer norm
self.norm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
def get_input_embeddings(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def forward(
self,
input_ids: torch.Tensor | None = None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
output_hidden_states = (
output_hidden_states
if output_hidden_states is not None
else self.config.output_hidden_states
)
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.get_input_embeddings(input_ids)
residual = None
if position_ids is None:
position_ids = (
torch.arange(0, hidden_states.shape[1], device=hidden_states.device)
.unsqueeze(0)
.expand(hidden_states.shape[0], -1)
)
attention_lengths = None
if attention_mask is not None:
attention_lengths = tuple(
int(valid_len)
for valid_len in attention_mask.sum(dim=-1).detach().cpu().tolist()
)
all_hidden_states: tuple[Any, ...] | None = () if output_hidden_states else None
for layer in self.layers:
if all_hidden_states is not None:
all_hidden_states += (
(hidden_states,)
if residual is None
else (hidden_states + residual,)
)
hidden_states, residual = layer(
position_ids, hidden_states, residual, attention_lengths
)
hidden_states, _ = self.norm(hidden_states, residual)
# Add hidden states from the last decoder layer
if all_hidden_states is not None:
all_hidden_states += (hidden_states,)
return BaseEncoderOutput(
last_hidden_state=hidden_states,
hidden_states=all_hidden_states,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Load weights with support for tensor parallelism and weight remapping."""
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
# Strip 'model.' prefix from HuggingFace Qwen3 weights
if name.startswith("model."):
name = name[6:] # len("model.") == 6
# Skip rotary embedding weights
if "rotary_emb.inv_freq" in name:
continue
if "rotary_emb.cos_cached" in name or "rotary_emb.sin_cached" in name:
continue
# Handle KV scale remapping
if "scale" in name:
kv_scale_name: str | None = maybe_remap_kv_scale_name(name, params_dict)
if kv_scale_name is None:
continue
else:
name = kv_scale_name
# Handle stacked params mapping (qkv_proj, gate_up_proj)
for (
param_name,
weight_name,
shard_id,
) in self.config.arch_config.stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
break
else:
# Skip loading extra bias for GPTQ models
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
EntryClass = Qwen3ForCausalLM
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,749 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from transformers: https://github.com/huggingface/transformers/blob/v4.39.0/src/transformers/models/t5/modeling_t5.py
# Derived from T5 implementation posted on HuggingFace; license below:
#
# coding=utf-8
# Copyright 2018 Mesh TensorFlow authors, T5 Authors and HuggingFace Inc. team.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""PyTorch T5 & UMT5 model."""
import math
from collections.abc import Iterable
from dataclasses import dataclass
import torch
import torch.nn.functional as F
from torch import nn
from sglang.multimodal_gen.configs.models.encoders import BaseEncoderOutput, T5Config
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.layernorm import RMSNorm
from sglang.multimodal_gen.runtime.layers.linear import (
MergedColumnParallelLinear,
QKVParallelLinear,
RowParallelLinear,
)
from sglang.multimodal_gen.runtime.layers.quantization import QuantizationConfig
from sglang.multimodal_gen.runtime.layers.utils import get_group_rank, get_group_size
from sglang.multimodal_gen.runtime.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from sglang.multimodal_gen.runtime.loader.weight_utils import default_weight_loader
from sglang.multimodal_gen.runtime.models.encoders.base import (
TextEncoder,
get_folding_tp_group,
)
from sglang.multimodal_gen.runtime.platforms import current_platform
class AttentionType:
"""
Attention type.
Use string to be compatible with `torch.compile`.
"""
# Decoder attention between previous layer Q/K/V
DECODER = "decoder"
# Encoder attention between previous layer Q/K/V for encoder-decoder
ENCODER = "encoder"
# Encoder attention between previous layer Q/K/V
ENCODER_ONLY = "encoder_only"
# Attention between dec. Q and enc. K/V for encoder-decoder
ENCODER_DECODER = "encoder_decoder"
@dataclass
class AttentionMetadata:
attn_bias: torch.Tensor
class T5DenseActDense(nn.Module):
def __init__(
self, config: T5Config, quant_config: QuantizationConfig | None = None
):
super().__init__()
tp_group = get_folding_tp_group(config)
self.wi = MergedColumnParallelLinear(
config.d_model, [config.d_ff], bias=False, tp_group=tp_group
)
self.wo = RowParallelLinear(
config.d_ff,
config.d_model,
bias=False,
quant_config=quant_config,
tp_group=tp_group,
)
self.act = get_act_fn(config.dense_act_fn)
def forward(self, hidden_states) -> torch.Tensor:
hidden_states, _ = self.wi(hidden_states)
hidden_states = self.act(hidden_states)
hidden_states, _ = self.wo(hidden_states)
return hidden_states
class T5DenseGatedActDense(nn.Module):
def __init__(
self, config: T5Config, quant_config: QuantizationConfig | None = None
):
super().__init__()
tp_group = get_folding_tp_group(config)
self.wi_0 = MergedColumnParallelLinear(
config.d_model,
[config.d_ff],
bias=False,
quant_config=quant_config,
tp_group=tp_group,
)
self.wi_1 = MergedColumnParallelLinear(
config.d_model,
[config.d_ff],
bias=False,
quant_config=quant_config,
tp_group=tp_group,
)
# Should not run in fp16 unless mixed-precision is used,
# see https://github.com/huggingface/transformers/issues/20287.
self.wo = RowParallelLinear(
config.d_ff,
config.d_model,
bias=False,
quant_config=quant_config,
tp_group=tp_group,
)
self.act = get_act_fn(config.dense_act_fn)
def forward(self, hidden_states) -> torch.Tensor:
hidden_gelu = self.act(self.wi_0(hidden_states)[0])
hidden_linear, _ = self.wi_1(hidden_states)
hidden_states = hidden_gelu * hidden_linear
hidden_states, _ = self.wo(hidden_states)
return hidden_states
class T5LayerFF(nn.Module):
def __init__(
self, config: T5Config, quant_config: QuantizationConfig | None = None
):
super().__init__()
if config.is_gated_act:
self.DenseReluDense = T5DenseGatedActDense(
config, quant_config=quant_config
)
else:
self.DenseReluDense = T5DenseActDense(config, quant_config=quant_config)
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
def forward(self, hidden_states) -> torch.Tensor:
forwarded_states = self.layer_norm(hidden_states)
forwarded_states = self.DenseReluDense(forwarded_states)
hidden_states = hidden_states + forwarded_states
return hidden_states
# T5 has attn_bias and does not use softmax scaling
class T5MultiHeadAttention(nn.Module):
def __init__(self) -> None:
super().__init__()
def forward(self, q, k, v, attn_bias=None):
b, _, n, c = q.shape
attn = torch.einsum("binc,bjnc->bnij", q, k)
if attn_bias is not None:
attn += attn_bias
attn = F.softmax(attn.float(), dim=-1).type_as(attn)
x = torch.einsum("bnij,bjnc->binc", attn, v)
x = x.reshape(b, -1, n * c)
return x
class T5Attention(nn.Module):
def __init__(
self,
config: T5Config,
attn_type: str,
has_relative_attention_bias=False,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.attn_type = attn_type
# Cross-attention has no relative pos encoding anyway
self.is_decoder = attn_type == AttentionType.DECODER
self.has_relative_attention_bias = has_relative_attention_bias
self.relative_attention_num_buckets = config.relative_attention_num_buckets
self.relative_attention_max_distance = config.relative_attention_max_distance
self.d_model = config.d_model
self.key_value_proj_dim = config.d_kv
self.total_num_heads = self.total_num_kv_heads = config.num_heads
# Partition heads across multiple tensor parallel GPUs.
self.tp_group = get_folding_tp_group(config)
self.tp_world_size = get_group_size(self.tp_group)
assert config.num_heads % self.tp_world_size == 0
self.n_heads = config.num_heads // self.tp_world_size
self.inner_dim = self.n_heads * self.key_value_proj_dim
# No GQA in t5.
# self.n_kv_heads = self.n_heads
self.qkv_proj = QKVParallelLinear(
self.d_model,
self.key_value_proj_dim,
self.total_num_heads,
self.total_num_kv_heads,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
tp_group=self.tp_group,
)
self.attn = T5MultiHeadAttention()
if self.has_relative_attention_bias:
self.relative_attention_bias = VocabParallelEmbedding(
self.relative_attention_num_buckets,
self.total_num_heads,
org_num_embeddings=self.relative_attention_num_buckets,
padding_size=self.relative_attention_num_buckets,
quant_config=quant_config,
tp_group=self.tp_group,
)
self.o = RowParallelLinear(
self.total_num_heads * self.key_value_proj_dim,
self.d_model,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
tp_group=self.tp_group,
)
@staticmethod
def _relative_position_bucket(
relative_position, bidirectional=True, num_buckets=32, max_distance=128
) -> torch.Tensor:
"""
Adapted from Mesh Tensorflow:
https://github.com/tensorflow/mesh/blob/0cb87fe07da627bf0b7e60475d59f95ed6b5be3d/mesh_tensorflow/transformer/transformer_layers.py#L593
Translate relative position to a bucket number for relative attention.
The relative position is defined as memory_position - query_position,
i.e. the distance in tokens from the attending position to the
attended-to position. If bidirectional=False, then positive relative
positions are invalid. We use smaller buckets for small absolute
relative_position and larger buckets for larger absolute
relative_positions. All relative positions >=max_distance map to the
same bucket. All relative positions <=-max_distance map to the same
bucket. This should allow for more graceful generalization to longer
sequences than the model has been trained on
Args:
relative_position: an int32 Tensor
bidirectional: a boolean - whether the attention is bidirectional
num_buckets: an integer
max_distance: an integer
Returns:
a Tensor with the same shape as relative_position, containing int32
values in the range [0, num_buckets)
""" # noqa: E501
relative_buckets = 0
if bidirectional:
num_buckets //= 2
relative_buckets += (relative_position > 0).to(torch.long) * num_buckets
relative_position = torch.abs(relative_position)
else:
relative_position = -torch.min(
relative_position, torch.zeros_like(relative_position)
)
# now relative_position is in the range [0, inf)
# half of the buckets are for exact increments in positions
max_exact = num_buckets // 2
is_small = relative_position < max_exact
# The other half of the buckets are for logarithmically bigger bins
# in positions up to max_distance
relative_position_if_large = max_exact + (
torch.log(relative_position.float() / max_exact)
/ math.log(max_distance / max_exact)
* (num_buckets - max_exact)
).to(torch.long)
relative_position_if_large = torch.min(
relative_position_if_large,
torch.full_like(relative_position_if_large, num_buckets - 1),
)
relative_buckets += torch.where(
is_small, relative_position, relative_position_if_large
)
return relative_buckets
def compute_bias(self, query_length, key_length, device=None) -> torch.Tensor:
"""Compute binned relative position bias"""
if device is None:
device = self.relative_attention_bias.weight.device
context_position = torch.arange(query_length, dtype=torch.long, device=device)[
:, None
]
memory_position = torch.arange(key_length, dtype=torch.long, device=device)[
None, :
]
# max_seq_len, nh
relative_position = memory_position - context_position
relative_position_bucket = self._relative_position_bucket(
relative_position, # shape (query_length, key_length)
bidirectional=(not self.is_decoder),
num_buckets=self.relative_attention_num_buckets,
max_distance=self.relative_attention_max_distance,
)
values = self.relative_attention_bias(
relative_position_bucket
) # shape (query_length, key_length, num_heads)
x = values.permute([2, 0, 1]).unsqueeze(
0
) # shape (1, num_heads, query_length, key_length)
return x
def forward(
self,
hidden_states: torch.Tensor, # (num_tokens, d_model)
attention_mask: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
bs, seq_len, _ = hidden_states.shape
num_seqs = bs
n, c = (
self.n_heads,
self.key_value_proj_dim,
)
qkv, _ = self.qkv_proj(hidden_states)
# Projection of 'own' hidden state (self-attention). No GQA here.
q, k, v = qkv.split(self.inner_dim, dim=-1)
q = q.reshape(bs, seq_len, n, c)
k = k.reshape(bs, seq_len, n, c)
v = v.reshape(bs, seq_len, n, c)
assert attn_metadata is not None
attn_bias = attn_metadata.attn_bias
# Not compatible with CP here (as all encoder-decoder models),
# as it assumes homogeneous batch (prefills or decodes).
if self.has_relative_attention_bias:
# Self-attention. Compute T5 relative positional encoding.
# The bias term is computed on longest sequence in batch. Biases
# for shorter sequences are slices of the longest.
assert self.attn_type == AttentionType.ENCODER
attn_bias = self.compute_bias(seq_len, seq_len).repeat(num_seqs, 1, 1, 1)
attn_metadata.attn_bias = attn_bias
else:
# Encoder/Decoder Self-Attention Layer, attn bias already cached.
assert attn_bias is not None
if attention_mask is not None:
attention_mask = (
attention_mask.view(bs, 1, 1, -1)
if attention_mask.ndim == 2
else attention_mask.unsqueeze(1)
)
mask_val = -1e4 if current_platform.is_mps() else torch.finfo(q.dtype).min
attn_bias.masked_fill_(attention_mask == 0, mask_val)
if self.tp_world_size > 1:
rank = get_group_rank(self.tp_group)
attn_bias = attn_bias[
:, rank * self.n_heads : (rank + 1) * self.n_heads, :, :
]
attn_output = self.attn(q, k, v, attn_bias)
output, _ = self.o(attn_output)
return output
class T5LayerSelfAttention(nn.Module):
def __init__(
self,
config,
has_relative_attention_bias=False,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.SelfAttention = T5Attention(
config,
AttentionType.DECODER if "decoder" in prefix else AttentionType.ENCODER,
has_relative_attention_bias=has_relative_attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.SelfAttention",
)
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
normed_hidden_states = self.layer_norm(hidden_states)
attention_output = self.SelfAttention(
hidden_states=normed_hidden_states,
attention_mask=attention_mask,
attn_metadata=attn_metadata,
)
hidden_states = hidden_states + attention_output
return hidden_states
class T5LayerCrossAttention(nn.Module):
def __init__(
self, config, quant_config: QuantizationConfig | None = None, prefix: str = ""
):
super().__init__()
self.EncDecAttention = T5Attention(
config,
AttentionType.ENCODER_DECODER,
has_relative_attention_bias=False,
quant_config=quant_config,
prefix=f"{prefix}.EncDecAttention",
)
self.layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
def forward(
self,
hidden_states: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
normed_hidden_states = self.layer_norm(hidden_states)
attention_output = self.EncDecAttention(
hidden_states=normed_hidden_states,
attn_metadata=attn_metadata,
)
hidden_states = hidden_states + attention_output
return hidden_states
class T5Block(nn.Module):
def __init__(
self,
config: T5Config,
is_decoder: bool,
has_relative_attention_bias=False,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.is_decoder = is_decoder
self.layer = nn.ModuleList()
self.layer.append(
T5LayerSelfAttention(
config,
has_relative_attention_bias=has_relative_attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.self_attn",
)
)
if self.is_decoder:
self.layer.append(
T5LayerCrossAttention(
config, quant_config=quant_config, prefix=f"{prefix}.cross_attn"
)
)
self.layer.append(T5LayerFF(config, quant_config=quant_config))
def forward(
self,
hidden_states: torch.Tensor,
attention_mask: torch.Tensor,
attn_metadata: AttentionMetadata | None = None,
) -> torch.Tensor:
if attention_mask is None:
attention_mask = torch.ones(
hidden_states.shape[:2], device=hidden_states.device
)
hidden_states = self.layer[0](
hidden_states=hidden_states,
attention_mask=attention_mask,
attn_metadata=attn_metadata,
)
if self.is_decoder:
hidden_states = self.layer[1](
hidden_states=hidden_states, attn_metadata=attn_metadata
)
# Apply Feed Forward layer
hidden_states = self.layer[-1](hidden_states)
return hidden_states
class T5Stack(nn.Module):
def __init__(
self,
config: T5Config,
is_decoder: bool,
n_layers: int,
embed_tokens=None,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
is_umt5: bool = False,
):
super().__init__()
self.embed_tokens = embed_tokens
self.is_umt5 = is_umt5
if is_umt5:
self.block = nn.ModuleList(
[
T5Block(
config,
is_decoder=is_decoder,
has_relative_attention_bias=True,
quant_config=quant_config,
prefix=f"{prefix}.blocks.{i}",
)
for i in range(n_layers)
]
)
else:
# Only the first block has relative positional encoding.
self.block = nn.ModuleList(
[
T5Block(
config,
is_decoder=is_decoder,
has_relative_attention_bias=i == 0,
quant_config=quant_config,
prefix=f"{prefix}.blocks.{i}",
)
for i in range(n_layers)
]
)
self.final_layer_norm = RMSNorm(config.d_model, eps=config.layer_norm_epsilon)
def forward(
self,
input_ids: torch.Tensor,
attention_mask: torch.Tensor,
attn_metadata: AttentionMetadata,
) -> torch.Tensor:
hidden_states = self.embed_tokens(input_ids)
for idx, block in enumerate(self.block):
hidden_states = block(
hidden_states=hidden_states,
attention_mask=attention_mask,
attn_metadata=attn_metadata,
)
hidden_states = self.final_layer_norm(hidden_states)
return hidden_states
class T5EncoderModel(TextEncoder):
def __init__(self, config: T5Config, prefix: str = ""):
super().__init__(config)
quant_config = None
tp_group = get_folding_tp_group(config)
self.shared = VocabParallelEmbedding(
config.vocab_size,
config.d_model,
org_num_embeddings=config.vocab_size,
tp_group=tp_group,
)
self.encoder = T5Stack(
config,
False,
config.num_layers,
self.shared,
quant_config=quant_config,
prefix=f"{prefix}.encoder",
is_umt5=False,
)
def get_input_embeddings(self):
return self.shared
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
attn_metadata = AttentionMetadata(None)
hidden_states = self.encoder(
input_ids=input_ids,
attention_mask=attention_mask,
attn_metadata=attn_metadata,
)
return BaseEncoderOutput(last_hidden_state=hidden_states)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
(".qkv_proj", ".q", "q"),
(".qkv_proj", ".k", "k"),
(".qkv_proj", ".v", "v"),
]
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
loaded = False
if "decoder" in name or "lm_head" in name:
continue
for param_name, weight_name, shard_id in stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded = True
break
if not loaded:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
class UMT5EncoderModel(TextEncoder):
def __init__(self, config: T5Config, prefix: str = ""):
super().__init__(config)
quant_config = None
tp_group = get_folding_tp_group(config)
self.shared = VocabParallelEmbedding(
config.vocab_size,
config.d_model,
org_num_embeddings=config.vocab_size,
tp_group=tp_group,
)
self.encoder = T5Stack(
config,
False,
config.num_layers,
self.shared,
quant_config=quant_config,
prefix=f"{prefix}.encoder",
is_umt5=True,
)
def get_input_embeddings(self):
return self.shared
def forward(
self,
input_ids: torch.Tensor | None,
position_ids: torch.Tensor | None = None,
attention_mask: torch.Tensor | None = None,
inputs_embeds: torch.Tensor | None = None,
output_hidden_states: bool | None = None,
**kwargs,
) -> BaseEncoderOutput:
attn_metadata = AttentionMetadata(None)
hidden_states = self.encoder(
input_ids=input_ids,
attention_mask=attention_mask,
attn_metadata=attn_metadata,
)
return BaseEncoderOutput(
last_hidden_state=hidden_states,
attention_mask=attention_mask,
)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
for name, loaded_weight in weights:
loaded = False
if "decoder" in name or "lm_head" in name:
continue
for (
param_name,
weight_name,
shard_id,
) in self.config.arch_config.stacked_params_mapping:
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded = True
break
if not loaded:
# Skip loading extra bias for GPTQ models.
if name.endswith(".bias") and name not in params_dict:
continue
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
loaded_params.add(name)
return loaded_params
EntryClass = [T5EncoderModel, UMT5EncoderModel]
@@ -0,0 +1,96 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/models/vision.py
from abc import ABC, abstractmethod
from typing import Generic, TypeVar
import torch
from transformers import PretrainedConfig
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
_C = TypeVar("_C", bound=PretrainedConfig)
class VisionEncoderInfo(ABC, Generic[_C]):
def __init__(self, vision_config: _C) -> None:
super().__init__()
self.vision_config = vision_config
@abstractmethod
def get_num_image_tokens(
self,
*,
image_width: int,
image_height: int,
) -> int:
raise NotImplementedError
@abstractmethod
def get_max_image_tokens(self) -> int:
raise NotImplementedError
@abstractmethod
def get_image_size(self) -> int:
raise NotImplementedError
@abstractmethod
def get_patch_size(self) -> int:
raise NotImplementedError
@abstractmethod
def get_patch_grid_length(self) -> int:
raise NotImplementedError
def resolve_visual_encoder_outputs(
encoder_outputs: torch.Tensor | list[torch.Tensor],
feature_sample_layers: list[int] | None,
post_layer_norm: torch.nn.LayerNorm | None,
max_possible_layers: int,
) -> torch.Tensor:
"""Given the outputs a visual encoder module that may correspond to the
output of the last layer, or a list of hidden states to be stacked,
handle post normalization and resolve it into a single output tensor.
Args:
encoder_outputs: Output of encoder's last layer or all hidden states.
feature_sample_layers: Optional layer indices to grab from the encoder
outputs; if provided, encoder outputs must be a list.
post_layer_norm: Post norm to apply to the output of the encoder.
max_possible_layers: Total layers in the fully loaded visual encoder.
"""
if feature_sample_layers is None:
if post_layer_norm is not None:
return post_layer_norm(encoder_outputs)
return encoder_outputs
# Get the hidden states corresponding to the layer indices.
# Negative values are relative to the full visual encoder,
# so offset them depending on how many layers were loaded.
# NOTE: this assumes that encoder_outputs is a list containing
# the inputs to the visual encoder, followed by the hidden states
# of each layer.
num_loaded_layers = len(encoder_outputs) - 1
offset = max_possible_layers - num_loaded_layers
hs_pool = [
(
encoder_outputs[layer_idx]
if layer_idx >= 0
else encoder_outputs[layer_idx + offset]
)
for layer_idx in feature_sample_layers
]
# Apply post-norm on the final hidden state if we are using it
uses_last_layer = feature_sample_layers[-1] in (len(hs_pool) - 1, -1)
if post_layer_norm is not None and uses_last_layer:
hs_pool[-1] = post_layer_norm(encoder_outputs)
return torch.cat(hs_pool, dim=-1)
@@ -0,0 +1,424 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
# Adapted from: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/parameter.py
from collections.abc import Callable
from fractions import Fraction
from typing import Any
import torch
from torch.nn import Parameter
from sglang.multimodal_gen.runtime.distributed import get_tp_rank
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.weight_attrs import make_synced_weight_loader
logger = init_logger(__name__)
class BasevLLMParameter(Parameter):
"""
Base parameter for vLLM linear layers. Extends the torch.nn.parameter
by taking in a linear weight loader. Will copy the loaded weight
into the parameter when the provided weight loader is called.
"""
def __new__(cls, data: torch.Tensor, **kwargs):
return super().__new__(cls, data=data, requires_grad=False)
def __init__(self, data: torch.Tensor, weight_loader: Callable):
"""
Initialize the BasevLLMParameter
:param data: torch tensor with the parameter data
:param weight_loader: weight loader callable
:returns: a torch.nn.parameter
"""
# During weight loading, we often do something like:
# narrowed_tensor = param.data.narrow(0, offset, len)
# narrowed_tensor.copy_(real_weight)
# expecting narrowed_tensor and param.data to share the same storage.
# However, on TPUs, narrowed_tensor will lazily propagate to the base
# tensor, which is param.data, leading to the redundant memory usage.
# This sometimes causes OOM errors during model loading. To avoid this,
# we sync the param tensor after its weight loader is called.
from sglang.multimodal_gen.runtime.platforms import current_platform
if current_platform.is_tpu():
weight_loader = make_synced_weight_loader(weight_loader)
self._weight_loader = weight_loader
@property
def weight_loader(self):
return self._weight_loader
def _is_1d_and_scalar(self, loaded_weight: torch.Tensor):
cond1 = self.data.ndim == 1 and self.data.numel() == 1
cond2 = loaded_weight.ndim == 0 and loaded_weight.numel() == 1
return cond1 and cond2
def _assert_and_load(self, loaded_weight: torch.Tensor) -> None:
assert self.data.shape == loaded_weight.shape or self._is_1d_and_scalar(
loaded_weight
)
self.data.copy_(loaded_weight)
def load_column_parallel_weight(self, loaded_weight: torch.Tensor) -> None:
self._assert_and_load(loaded_weight)
def load_row_parallel_weight(self, loaded_weight: torch.Tensor) -> None:
self._assert_and_load(loaded_weight)
def load_merged_column_weight(self, loaded_weight: torch.Tensor, **kwargs) -> None:
self._assert_and_load(loaded_weight)
def load_qkv_weight(self, loaded_weight: torch.Tensor, **kwargs) -> None:
self._assert_and_load(loaded_weight)
class _ColumnvLLMParameter(BasevLLMParameter):
"""
Private class defining weight loading functionality
(load_merged_column_weight, load_qkv_weight)
for parameters being loaded into linear layers with column
parallelism. This includes QKV and MLP layers which are
not already fused on disk. Requires an output dimension
to be defined. Called within the weight loader of
each of the column parallel linear layers.
"""
def __init__(self, output_dim: int, **kwargs):
self._output_dim = output_dim
super().__init__(**kwargs)
@property
def output_dim(self):
return self._output_dim
def load_column_parallel_weight(self, loaded_weight: torch.Tensor) -> None:
tp_rank = get_tp_rank()
shard_size = self.data.shape[self.output_dim]
loaded_weight = loaded_weight.narrow(
self.output_dim, tp_rank * shard_size, shard_size
)
assert self.data.shape == loaded_weight.shape
self.data.copy_(loaded_weight)
def load_merged_column_weight(self, loaded_weight: torch.Tensor, **kwargs) -> None:
shard_offset = kwargs.get("shard_offset")
shard_size = kwargs.get("shard_size")
if shard_offset is None or shard_size is None:
raise ValueError("shard_offset and shard_size must be provided")
if (
isinstance(self, PackedColumnParameter | PackedvLLMParameter)
and self.packed_dim == self.output_dim
):
shard_size, shard_offset = self.adjust_shard_indexes_for_packing(
shard_offset=shard_offset, shard_size=shard_size
)
param_data = self.data
tp_rank = get_tp_rank()
param_data = param_data.narrow(self.output_dim, shard_offset, shard_size)
loaded_weight = loaded_weight.narrow(
self.output_dim, tp_rank * shard_size, shard_size
)
assert param_data.shape == loaded_weight.shape
param_data.copy_(loaded_weight)
def load_qkv_weight(self, loaded_weight: torch.Tensor, **kwargs) -> None:
shard_offset = kwargs.get("shard_offset")
shard_size = kwargs.get("shard_size")
shard_id = kwargs.get("shard_id")
num_heads = kwargs.get("num_heads")
assert shard_offset is not None
assert shard_size is not None
assert shard_id is not None
assert num_heads is not None
if (
isinstance(self, PackedColumnParameter | PackedvLLMParameter)
and self.output_dim == self.packed_dim
):
shard_size, shard_offset = self.adjust_shard_indexes_for_packing(
shard_offset=shard_offset, shard_size=shard_size
)
param_data = self.data
tp_rank = get_tp_rank()
shard_id = tp_rank if shard_id == "q" else tp_rank // num_heads
param_data = param_data.narrow(self.output_dim, shard_offset, shard_size)
loaded_weight = loaded_weight.narrow(
self.output_dim, shard_id * shard_size, shard_size
)
assert param_data.shape == loaded_weight.shape
param_data.copy_(loaded_weight)
class RowvLLMParameter(BasevLLMParameter):
"""
Parameter class defining weight_loading functionality
(load_row_parallel_weight) for parameters being loaded
into linear layers with row parallel functionality.
Requires an input_dim to be defined.
"""
def __init__(self, input_dim: int, **kwargs):
self._input_dim = input_dim
super().__init__(**kwargs)
@property
def input_dim(self):
return self._input_dim
def load_row_parallel_weight(self, loaded_weight: torch.Tensor) -> None:
tp_rank = get_tp_rank()
shard_size = self.data.shape[self.input_dim]
loaded_weight = loaded_weight.narrow(
self.input_dim, tp_rank * shard_size, shard_size
)
if len(loaded_weight.shape) == 0:
loaded_weight = loaded_weight.reshape(1)
assert self.data.shape == loaded_weight.shape
self.data.copy_(loaded_weight)
class ModelWeightParameter(_ColumnvLLMParameter, RowvLLMParameter):
"""
Parameter class for linear layer weights. Uses both column and
row parallelism.
"""
pass
class GroupQuantScaleParameter(_ColumnvLLMParameter, RowvLLMParameter):
"""
Parameter class for weight scales loaded for weights with
grouped quantization. Uses both column and row parallelism.
"""
pass
class ChannelQuantScaleParameter(_ColumnvLLMParameter):
"""
Parameter class for weight scales loaded for weights with
channel-wise quantization. Equivalent to _ColumnvLLMParameter.
"""
pass
class PerTensorScaleParameter(BasevLLMParameter):
"""
Parameter class for scales where the number of scales is
equivalent to the number of logical matrices in fused linear
layers (e.g. for QKV, there are 3 scales loaded from disk).
This is relevant to weights with per-tensor quantization.
Adds functionality to map the scalers to a shard during
weight loading.
Note: additional parameter manipulation may be handled
for each quantization config specifically, within
process_weights_after_loading
"""
def __init__(self, **kwargs):
self.qkv_idxs = {"q": 0, "k": 1, "v": 2}
super().__init__(**kwargs)
def _shard_id_as_int(self, shard_id: str | int) -> int:
if isinstance(shard_id, int):
return shard_id
# if not int, assume shard_id for qkv
# map to int and return
assert isinstance(shard_id, str)
assert shard_id in self.qkv_idxs
return self.qkv_idxs[shard_id]
# For row parallel layers, no sharding needed
# load weight into parameter as is
def load_row_parallel_weight(self, *args, **kwargs) -> None:
super().load_row_parallel_weight(*args, **kwargs)
def load_merged_column_weight(self, *args, **kwargs) -> None:
self._load_into_shard_id(*args, **kwargs)
def load_qkv_weight(self, *args, **kwargs) -> None:
self._load_into_shard_id(*args, **kwargs)
def load_column_parallel_weight(self, *args, **kwargs) -> None:
super().load_row_parallel_weight(*args, **kwargs)
def _load_into_shard_id(
self, loaded_weight: torch.Tensor, shard_id: str | int, **kwargs
):
"""
Slice the parameter data based on the shard id for
loading.
"""
param_data = self.data
shard_id = self._shard_id_as_int(shard_id)
# AutoFP8 scales do not have a shape
# compressed-tensors scales do have a shape
if len(loaded_weight.shape) != 0:
assert loaded_weight.shape[0] == 1
loaded_weight = loaded_weight[0]
param_data = param_data[shard_id]
assert param_data.shape == loaded_weight.shape
param_data.copy_(loaded_weight)
class PackedColumnParameter(_ColumnvLLMParameter):
"""
Parameter for model parameters which are packed on disk
and support column parallelism only. See PackedvLLMParameter
for more details on the packed properties.
"""
def __init__(self, packed_factor: int | Fraction, packed_dim: int, **kwargs):
self._packed_factor = packed_factor
self._packed_dim = packed_dim
super().__init__(**kwargs)
@property
def packed_dim(self):
return self._packed_dim
@property
def packed_factor(self):
return self._packed_factor
def adjust_shard_indexes_for_packing(
self, shard_size, shard_offset
) -> tuple[Any, Any]:
return _adjust_shard_indexes_for_packing(
shard_size=shard_size,
shard_offset=shard_offset,
packed_factor=self.packed_factor,
)
class PackedvLLMParameter(ModelWeightParameter):
"""
Parameter for model weights which are packed on disk.
Example: GPTQ Marlin weights are int4 or int8, packed into int32.
Extends the ModelWeightParameter to take in the
packed factor, the packed dimension, and optionally, marlin
tile size for marlin kernels. Adjusts the shard_size and
shard_offset for fused linear layers model weight loading
by accounting for packing and optionally, marlin tile size.
"""
def __init__(self, packed_factor: int | Fraction, packed_dim: int, **kwargs):
self._packed_factor = packed_factor
self._packed_dim = packed_dim
super().__init__(**kwargs)
@property
def packed_dim(self):
return self._packed_dim
@property
def packed_factor(self):
return self._packed_factor
def adjust_shard_indexes_for_packing(self, shard_size, shard_offset):
return _adjust_shard_indexes_for_packing(
shard_size=shard_size,
shard_offset=shard_offset,
packed_factor=self.packed_factor,
)
class BlockQuantScaleParameter(_ColumnvLLMParameter, RowvLLMParameter):
"""
Parameter class for weight scales loaded for weights with
block-wise quantization. Uses both column and row parallelism.
"""
pass
def permute_param_layout_(
param: BasevLLMParameter, input_dim: int, output_dim: int, **kwargs
) -> BasevLLMParameter:
"""
Permute a parameter's layout to the specified input and output dimensions,
useful for forcing the parameter into a known layout, for example, if I need
a packed (quantized) weight matrix to be in the layout
{input_dim = 0, output_dim = 1, packed_dim = 0}
then I can call:
permute_param_layout_(x, input_dim=0, output_dim=1, packed_dim=0)
to ensure x is in the correct layout (permuting it to the correct layout if
required, asserting if it cannot get it to the correct layout)
"""
curr_input_dim = getattr(param, "input_dim", None)
curr_output_dim = getattr(param, "output_dim", None)
if curr_input_dim is None or curr_output_dim is None:
assert param.data.dim() == 2, (
"permute_param_layout_ only supports 2D parameters when either "
"input_dim or output_dim is not set"
)
# if one of the dimensions is not set, set it to the opposite of the other
# we can only do this since we asserted the parameter is 2D above
if curr_input_dim is None:
assert curr_output_dim is not None, "either input or output dim must be set"
curr_input_dim = (curr_output_dim + 1) % 2
if curr_output_dim is None:
assert curr_input_dim is not None, "either input or output dim must be set"
curr_output_dim = (curr_input_dim + 1) % 2
# create permutation from the current layout to the layout with
# self.input_dim at input_dim and self.output_dim at output_dim preserving
# other dimensions
perm = [
i for i in range(param.data.dim()) if i not in [curr_input_dim, curr_output_dim]
]
perm.insert(input_dim, curr_input_dim)
perm.insert(output_dim, curr_output_dim)
if "packed_dim" in kwargs:
assert (
hasattr(param, "packed_dim")
and param.packed_dim == perm[kwargs["packed_dim"]]
), "permute_param_layout_ currently doesn't support repacking"
param.data = param.data.permute(*perm)
if hasattr(param, "_input_dim"):
param._input_dim = input_dim
if hasattr(param, "_output_dim"):
param._output_dim = output_dim
if "packed_dim" in kwargs and hasattr(param, "_packed_dim"):
param._packed_dim = kwargs["packed_dim"]
return param
def _adjust_shard_indexes_for_packing(
shard_size, shard_offset, packed_factor
) -> tuple[Any, Any]:
shard_size = shard_size // packed_factor
shard_offset = shard_offset // packed_factor
return shard_size, shard_offset
@@ -0,0 +1,422 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from vllm: https://github.com/vllm-project/vllm/blob/v0.7.3/vllm/model_executor/models/registry.py
import ast
import importlib
import os
import pickle
import subprocess
import sys
import tempfile
from abc import ABC, abstractmethod
from collections.abc import Callable, Set
from dataclasses import dataclass, field
from functools import lru_cache
from typing import NoReturn, TypeVar, cast
import cloudpickle
from torch import nn
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
MODELS_PATH = os.path.dirname(__file__)
COMPONENT_DIRS = [
d
for d in os.listdir(MODELS_PATH)
if os.path.isdir(os.path.join(MODELS_PATH, d))
and not d.startswith("__")
and not d.startswith(".")
]
_IMAGE_ENCODER_MODELS: dict[str, tuple] = {
# "HunyuanVideoTransformer3DModel": ("image_encoder", "hunyuanvideo", "HunyuanVideoImageEncoder"),
"CLIPVisionModelWithProjection": ("encoders", "clip", "CLIPVisionModel"),
}
# Global alias mapping: external_path -> canonical_class_name
_ALIAS_TO_MODEL: dict[str, str] = {}
def _parse_aliases_from_ast(value_node: ast.expr) -> list[str]:
"""Parse _aliases list from AST node."""
aliases = []
if isinstance(value_node, (ast.List, ast.Tuple)):
for elt in value_node.elts:
if isinstance(elt, ast.Constant) and isinstance(elt.value, str):
aliases.append(elt.value)
return aliases
@lru_cache(maxsize=None)
def _discover_and_register_models() -> dict[str, tuple[str, str, str]]:
discovered_models = dict(_IMAGE_ENCODER_MODELS)
# Collect class definitions with their _aliases
class_aliases: dict[str, list[str]] = {}
for component in COMPONENT_DIRS:
component_path = os.path.join(MODELS_PATH, component)
for filename in os.listdir(component_path):
if not filename.endswith(".py"):
continue
mod_relname = filename[:-3]
filepath = os.path.join(component_path, filename)
try:
with open(filepath, "r", encoding="utf-8") as f:
source = f.read()
tree = ast.parse(source, filename=filename)
entry_class_node = None
first_class_def = None
# Collect all class definitions and their _aliases
file_class_aliases: dict[str, list[str]] = {}
for node in ast.walk(tree):
if isinstance(node, ast.ClassDef):
if first_class_def is None:
first_class_def = node
# Look for _aliases in the class body
for class_body_node in node.body:
if isinstance(class_body_node, ast.Assign):
for target in class_body_node.targets:
if (
isinstance(target, ast.Name)
and target.id == "_aliases"
):
aliases = _parse_aliases_from_ast(
class_body_node.value
)
if aliases:
file_class_aliases[node.name] = aliases
if isinstance(node, ast.Assign):
for target in node.targets:
if (
isinstance(target, ast.Name)
and target.id == "EntryClass"
):
entry_class_node = node
break
if entry_class_node and first_class_def:
model_cls_name_list = []
value_node = entry_class_node.value
# EntryClass = ClassName
if isinstance(value_node, ast.Name):
model_cls_name_list.append(value_node.id)
# EntryClass = ["...", ClassName, ...]
elif isinstance(value_node, (ast.List, ast.Tuple)):
for elt in value_node.elts:
if isinstance(elt, ast.Constant):
model_cls_name_list.append(elt.value)
elif isinstance(elt, ast.Name):
model_cls_name_list.append(elt.id)
if model_cls_name_list:
for model_cls_str in model_cls_name_list:
if model_cls_str in discovered_models:
logger.warning(
f"Duplicate architecture found: {model_cls_str}. It will be overwritten."
)
model_arch = model_cls_str
discovered_models[model_arch] = (
component,
mod_relname,
model_cls_str,
)
# Collect aliases for this class
if model_cls_str in file_class_aliases:
class_aliases[model_cls_str] = file_class_aliases[
model_cls_str
]
except Exception as e:
logger.warning(f"Could not parse {filepath} to find models: {e}")
# Build alias -> canonical class name mapping
for class_name, aliases in class_aliases.items():
for alias in aliases:
if alias in _ALIAS_TO_MODEL:
logger.warning(
f"Alias '{alias}' already registered for '{_ALIAS_TO_MODEL[alias]}', "
f"will be overwritten by '{class_name}'"
)
_ALIAS_TO_MODEL[alias] = class_name
return discovered_models
_SGLANG_DIFFUSION_MODELS = _discover_and_register_models()
_SUBPROCESS_COMMAND = [
sys.executable,
"-m",
"sglang.multimodal_gen.runtime.models.dits.registry",
]
_T = TypeVar("_T")
@dataclass(frozen=True)
class _ModelInfo:
architecture: str
@staticmethod
def from_model_cls(model: type[nn.Module]) -> "_ModelInfo":
return _ModelInfo(
architecture=model.__name__,
)
class _BaseRegisteredModel(ABC):
@abstractmethod
def inspect_model_cls(self) -> _ModelInfo:
raise NotImplementedError
@abstractmethod
def load_model_cls(self) -> type[nn.Module]:
raise NotImplementedError
@dataclass(frozen=True)
class _RegisteredModel(_BaseRegisteredModel):
"""
Represents a model that has already been imported in the main process.
"""
interfaces: _ModelInfo
model_cls: type[nn.Module]
@staticmethod
def from_model_cls(model_cls: type[nn.Module]):
return _RegisteredModel(
interfaces=_ModelInfo.from_model_cls(model_cls),
model_cls=model_cls,
)
def inspect_model_cls(self) -> _ModelInfo:
return self.interfaces
def load_model_cls(self) -> type[nn.Module]:
return self.model_cls
def _run_in_subprocess(fn: Callable[[], _T]) -> _T:
# NOTE: We use a temporary directory instead of a temporary file to avoid
# issues like https://stackoverflow.com/questions/23212435/permission-denied-to-write-to-my-temporary-file
with tempfile.TemporaryDirectory() as tempdir:
output_filepath = os.path.join(tempdir, "registry_output.tmp")
# `cloudpickle` allows pickling lambda functions directly
input_bytes = cloudpickle.dumps((fn, output_filepath))
# cannot use `sys.executable __file__` here because the script
# contains relative imports
returned = subprocess.run(
_SUBPROCESS_COMMAND, input=input_bytes, capture_output=True
)
# check if the subprocess is successful
try:
returned.check_returncode()
except Exception as e:
# wrap raised exception to provide more information
raise RuntimeError(
f"Error raised in subprocess:\n" f"{returned.stderr.decode()}"
) from e
with open(output_filepath, "rb") as f:
return cast(_T, pickle.load(f))
@dataclass(frozen=True)
class _LazyRegisteredModel(_BaseRegisteredModel):
"""
Represents a model that has not been imported in the main process.
"""
module_name: str
component_name: str
class_name: str
# Performed in another process to avoid initializing CUDA
def inspect_model_cls(self) -> _ModelInfo:
return _run_in_subprocess(
lambda: _ModelInfo.from_model_cls(self.load_model_cls())
)
def load_model_cls(self) -> type[nn.Module]:
mod = importlib.import_module(self.module_name)
return cast(type[nn.Module], getattr(mod, self.class_name))
@lru_cache(maxsize=128)
def _try_load_model_cls(
model_arch: str,
model: _BaseRegisteredModel,
) -> type[nn.Module] | None:
from sglang.multimodal_gen.runtime.platforms import current_platform
current_platform.verify_model_arch(model_arch)
try:
return model.load_model_cls()
except Exception:
logger.exception("Ignore import error when loading '%s'", model_arch)
return None
@lru_cache(maxsize=128)
def _try_inspect_model_cls(
model_arch: str,
model: _BaseRegisteredModel,
) -> _ModelInfo | None:
try:
return model.inspect_model_cls()
except Exception:
logger.exception("Error in inspecting model architecture '%s'", model_arch)
return None
@dataclass
class _ModelRegistry:
# Keyed by model_arch
registered_models: dict[str, _BaseRegisteredModel] = field(default_factory=dict)
def get_supported_archs(self) -> Set[str]:
return self.registered_models.keys()
def resolve_by_alias(self, alias: str) -> type[nn.Module] | None:
"""Resolve a model class by its alias (external module path)."""
if alias in _ALIAS_TO_MODEL:
canonical_name = _ALIAS_TO_MODEL[alias]
return self._try_load_model_cls(canonical_name)
return None
def register_model(
self,
model_arch: str,
model_cls: type[nn.Module] | str,
) -> None:
"""
Register an external model to be used in vLLM.
:code:`model_cls` can be either:
- A :class:`torch.nn.Module` class directly referencing the model.
- A string in the format :code:`<module>:<class>` which can be used to
lazily import the model. This is useful to avoid initializing CUDA
when importing the model and thus the related error
:code:`RuntimeError: Cannot re-initialize CUDA in forked subprocess`.
"""
if model_arch in self.registered_models:
logger.warning(
"Model architecture %s is already registered, and will be "
"overwritten by the new model class %s.",
model_arch,
model_cls,
)
if isinstance(model_cls, str):
split_str = model_cls.split(":")
if len(split_str) != 2:
msg = "Expected a string in the format `<module>:<class>`"
raise ValueError(msg)
model = _LazyRegisteredModel(*split_str)
else:
model = _RegisteredModel.from_model_cls(model_cls)
self.registered_models[model_arch] = model
def _raise_for_unsupported(self, architectures: list[str]) -> NoReturn:
all_supported_archs = self.get_supported_archs()
if any(arch in all_supported_archs for arch in architectures):
raise ValueError(
f"Model architectures {architectures} failed "
"to be inspected. Please check the logs for more details."
)
raise ValueError(
f"Model architectures {architectures} are not supported for now. "
f"Supported architectures: {all_supported_archs}"
)
def _try_load_model_cls(self, model_arch: str) -> type[nn.Module] | None:
if model_arch not in self.registered_models:
return None
return _try_load_model_cls(model_arch, self.registered_models[model_arch])
def _try_inspect_model_cls(self, model_arch: str) -> _ModelInfo | None:
if model_arch not in self.registered_models:
return None
return _try_inspect_model_cls(model_arch, self.registered_models[model_arch])
def _normalize_archs(
self,
architectures: str | list[str],
) -> list[str]:
if isinstance(architectures, str):
architectures = [architectures]
if not architectures:
logger.warning("No model architectures are specified")
normalized_arch = []
for arch in architectures:
if arch not in self.registered_models:
registered_models = list(self.registered_models.keys())
raise Exception(
f"Unsupported model architecture: {arch}. Registered architectures: {registered_models}"
)
normalized_arch.append(arch)
return normalized_arch
def inspect_model_cls(
self,
architectures: str | list[str],
) -> tuple[_ModelInfo, str]:
architectures = self._normalize_archs(architectures)
for arch in architectures:
model_info = self._try_inspect_model_cls(arch)
if model_info is not None:
return (model_info, arch)
return self._raise_for_unsupported(architectures)
def resolve_model_cls(
self,
architectures: str | list[str],
) -> tuple[type[nn.Module], str]:
architectures = self._normalize_archs(architectures)
for arch in architectures:
model_cls = self._try_load_model_cls(arch)
if model_cls is not None:
return (model_cls, arch)
return self._raise_for_unsupported(architectures)
ModelRegistry = _ModelRegistry(
{
model_arch: _LazyRegisteredModel(
module_name=f"sglang.multimodal_gen.runtime.models.{component_name}.{mod_relname}",
component_name=component_name,
class_name=cls_name,
)
for model_arch, (
component_name,
mod_relname,
cls_name,
) in _SGLANG_DIFFUSION_MODELS.items()
}
)
@@ -0,0 +1,37 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from abc import ABC, abstractmethod
import torch
class BaseScheduler(ABC):
timesteps: torch.Tensor
order: int
num_train_timesteps: int
def __init__(self, *args, **kwargs) -> None:
# Check if subclass has defined all required properties
required_attributes = ["timesteps", "order", "num_train_timesteps"]
for attr in required_attributes:
if not hasattr(self, attr):
raise AttributeError(
f"Subclasses of BaseScheduler must define '{attr}' property"
)
@abstractmethod
def set_shift(self, shift: float) -> None:
pass
@abstractmethod
def set_timesteps(self, *args, **kwargs) -> None:
pass
@abstractmethod
def scale_model_input(
self, sample: torch.Tensor, timestep: int | None = None
) -> torch.Tensor:
pass
@@ -0,0 +1,531 @@
# Copied and adapted from: https://github.com/OpenMOSS/MOVA/tree/main/mova/diffusion/schedulers/flow_match.py and flow_match_pair.py
# SPDX-License-Identifier: Apache-2.0
from __future__ import annotations
import math
import torch
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
class FlowMatchScheduler(BaseScheduler):
def __init__(
self,
num_inference_steps=100,
num_train_timesteps=1000,
shift=3.0,
sigma_max=1.0,
sigma_min=0.003 / 1.002,
inverse_timesteps=False,
extra_one_step=False,
reverse_sigmas=False,
exponential_shift=False,
exponential_shift_mu=None,
shift_terminal=None,
):
self.order = 1
self.num_train_timesteps = num_train_timesteps
self.shift = shift
self.sigma_max = sigma_max
self.sigma_min = sigma_min
self.inverse_timesteps = inverse_timesteps
self.extra_one_step = extra_one_step
self.reverse_sigmas = reverse_sigmas
self.exponential_shift = exponential_shift
self.exponential_shift_mu = exponential_shift_mu
self.shift_terminal = shift_terminal
self.train_timesteps = None
self.train_sigmas = None
self.set_timesteps(num_train_timesteps)
self.set_timesteps(num_inference_steps)
BaseScheduler.__init__(self)
def set_shift(self, shift: float) -> None:
self.shift = shift
def set_timesteps(
self,
num_inference_steps=100,
denoising_strength=1.0,
training=False,
shift=None,
dynamic_shift_len=None,
):
if shift is not None:
self.shift = shift
sigma_start = (
self.sigma_min + (self.sigma_max - self.sigma_min) * denoising_strength
)
if self.extra_one_step:
self.sigmas = torch.linspace(
sigma_start, self.sigma_min, num_inference_steps + 1
)[:-1]
else:
self.sigmas = torch.linspace(
sigma_start, self.sigma_min, num_inference_steps
)
if self.inverse_timesteps:
self.sigmas = torch.flip(self.sigmas, dims=[0])
if self.exponential_shift:
mu = (
self.calculate_shift(dynamic_shift_len)
if dynamic_shift_len is not None
else self.exponential_shift_mu
)
self.sigmas = math.exp(mu) / (math.exp(mu) + (1 / self.sigmas - 1))
else:
self.sigmas = (
self.shift * self.sigmas / (1 + (self.shift - 1) * self.sigmas)
)
if self.shift_terminal is not None:
one_minus_z = 1 - self.sigmas
scale_factor = one_minus_z[-1] / (1 - self.shift_terminal)
if scale_factor != 0:
self.sigmas = 1 - (one_minus_z / scale_factor)
if self.reverse_sigmas:
self.sigmas = 1 - self.sigmas
self.timesteps = self.sigmas * self.num_train_timesteps
# Initialize train_timesteps on first set.
if self.train_timesteps is None:
self.train_timesteps = self.timesteps
self.train_sigmas = self.sigmas
if training:
x = self.timesteps
y = torch.exp(
-2 * ((x - num_inference_steps / 2) / num_inference_steps) ** 2
)
y_shifted = y - y.min()
bsmntw_weighing = y_shifted * (num_inference_steps / y_shifted.sum())
self.linear_timesteps_weights = bsmntw_weighing
self.training = True
else:
self.training = False
def scale_model_input(self, sample: torch.Tensor, timestep: int | None = None):
return sample
def step(self, model_output, timestep, sample, to_final=False, **kwargs):
if isinstance(timestep, torch.Tensor):
timestep = timestep.cpu()
timestep_id = torch.argmin((self.timesteps - timestep).abs())
sigma = self.sigmas[timestep_id]
if to_final or timestep_id + 1 >= len(self.timesteps):
sigma_ = 1 if (self.inverse_timesteps or self.reverse_sigmas) else 0
else:
sigma_ = self.sigmas[timestep_id + 1]
prev_sample = sample + model_output * (sigma_ - sigma)
return prev_sample
def return_to_timestep(self, timestep, sample, sample_stablized):
if isinstance(timestep, torch.Tensor):
timestep = timestep.cpu()
timestep_id = torch.argmin((self.timesteps - timestep).abs())
sigma = self.sigmas[timestep_id]
model_output = (sample - sample_stablized) / sigma
return model_output
def add_noise(self, original_samples, noise, timestep):
if isinstance(timestep, torch.Tensor):
timestep = timestep.cpu()
timestep_id = torch.argmin((self.timesteps - timestep).abs())
sigma = self.sigmas[timestep_id]
sample = (1 - sigma) * original_samples + sigma * noise
return sample
def training_target(self, sample, noise, timestep):
target = noise - sample
return target
def training_weight(self, timestep):
timestep_id = torch.argmin(
(self.timesteps - timestep.to(self.timesteps.device)).abs()
)
weights = self.linear_timesteps_weights[timestep_id]
return weights
def calculate_shift(
self,
image_seq_len,
base_seq_len: int = 256,
max_seq_len: int = 8192,
base_shift: float = 0.5,
max_shift: float = 0.9,
):
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
class FlowMatchPairScheduler(FlowMatchScheduler):
"""Pairing scheduler built on FlowMatchScheduler.
Provides a convenient pairing interface for timesteps or sigmas.
Attributes:
pair_timesteps: Cached timestep pairs of shape [num_timesteps, 2].
pair_sigmas: Cached sigma pairs of shape [num_timesteps, 2].
"""
def __init__(
self,
num_inference_steps=100,
num_train_timesteps=1000,
shift=3.0,
sigma_max=1.0,
sigma_min=0.003 / 1.002,
inverse_timesteps=False,
extra_one_step=False,
reverse_sigmas=False,
exponential_shift=False,
exponential_shift_mu=None,
shift_terminal=None,
):
self._pair_postprocess_fn = None
self._pair_postprocess_requires_source = False
self.pair_timesteps: torch.Tensor | None = None
self.pair_sigmas: torch.Tensor | None = None
self.timesteps: torch.Tensor | None = None
self.sigmas: torch.Tensor | None = None
super().__init__(
num_inference_steps=num_inference_steps,
num_train_timesteps=num_train_timesteps,
shift=shift,
sigma_max=sigma_max,
sigma_min=sigma_min,
inverse_timesteps=inverse_timesteps,
extra_one_step=extra_one_step,
reverse_sigmas=reverse_sigmas,
exponential_shift=exponential_shift,
exponential_shift_mu=exponential_shift_mu,
shift_terminal=shift_terminal,
)
def set_pair_postprocess(self, fn):
"""Set a postprocess function to customize pairs after construction.
Args:
fn: Callable with signature fn(pairs: torch.Tensor) -> torch.Tensor.
The returned tensor must have the same shape as input pairs.
Raises:
TypeError: If fn is not callable or None.
RuntimeError: If scheduler is not initialized.
"""
if fn is not None and not callable(fn):
raise TypeError("pair_postprocess must be callable or None")
self._pair_postprocess_fn = fn
self._pair_postprocess_requires_source = (
False if fn is None else bool(getattr(fn, "_requires_source", False))
)
if self.timesteps is None or self.sigmas is None:
raise RuntimeError("Scheduler not initialized; call set_timesteps() first")
self._refresh_pair_cache()
def set_pair_postprocess_by_name(self, name: str | None, **kwargs):
"""Configure a postprocess function by name.
Supported names:
- None/"none"/"off"/"false"/"no": disable
- "quadratic_perp_bulge_swap": x2=x+d, y2=x-d, where d=4*amp*s*(1-s), s=t/T
- "v2a_sequential": assume pairs are (t,t); sample half sequence from column 0
with stride 2, then let column 0 follow this sequence first, followed by column 1
- "a2v_sequential": same as above, but column 1 first then column 0
- "dual_sigma_shift": use only timestep count; rebuild two columns independently using
FlowMatchScheduler sigma transform logic; configurable visual_shift/audio_shift
Args:
name: Postprocess name or None to disable.
**kwargs: Extra parameters for the named postprocess. For example:
- amp: Float amplitude, default 150.0.
Raises:
ValueError: If name is unknown.
"""
if name is None or str(name).lower() in ("none", "off", "false", "no"):
self.set_pair_postprocess(None)
return
if name == "quadratic_perp_bulge_swap":
amp = float(kwargs.get("amp", 150.0))
def _quadratic_perp_bulge_swap(pairs: torch.Tensor):
if (
not isinstance(pairs, torch.Tensor)
or pairs.ndim != 2
or pairs.shape[1] != 2
):
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
x = pairs[:, 0]
T = float(self.num_train_timesteps)
s = x / T
d = 4.0 * amp * s * (1.0 - s)
x2 = x + d
y2 = x - d
return torch.stack([x2, y2], dim=1)
self.set_pair_postprocess(_quadratic_perp_bulge_swap)
return
if name == "v2a_sequential":
def _v2a(pairs: torch.Tensor):
if (
not isinstance(pairs, torch.Tensor)
or pairs.ndim != 2
or pairs.shape[1] != 2
):
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
N = pairs.shape[0]
base = pairs[:, 0]
seq_half = base[::2]
m = int(seq_half.shape[0])
col0 = torch.cat([seq_half, seq_half[-1:].repeat(m)], dim=0)[:N]
col1 = torch.cat([seq_half[0:1].repeat(m), seq_half], dim=0)[:N]
return torch.stack(
[
col0.to(dtype=pairs.dtype, device=pairs.device),
col1.to(dtype=pairs.dtype, device=pairs.device),
],
dim=1,
)
self.set_pair_postprocess(_v2a)
return
if name == "a2v_sequential":
def _a2v(pairs: torch.Tensor):
if (
not isinstance(pairs, torch.Tensor)
or pairs.ndim != 2
or pairs.shape[1] != 2
):
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
N = pairs.shape[0]
base = pairs[:, 0]
seq_half = base[::2]
m = int(seq_half.shape[0])
col0 = torch.cat([seq_half[0:1].repeat(m), seq_half], dim=0)[:N]
col1 = torch.cat([seq_half, seq_half[-1:].repeat(m)], dim=0)[:N]
return torch.stack(
[
col0.to(dtype=pairs.dtype, device=pairs.device),
col1.to(dtype=pairs.dtype, device=pairs.device),
],
dim=1,
)
self.set_pair_postprocess(_a2v)
return
if name == "v2a":
def _v2a_classic(pairs: torch.Tensor):
if (
not isinstance(pairs, torch.Tensor)
or pairs.ndim != 2
or pairs.shape[1] != 2
):
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
zeros = torch.zeros_like(pairs[:, 0])
return torch.stack([zeros, pairs[:, 1]], dim=1)
self.set_pair_postprocess(_v2a_classic)
return
if name == "a2v":
def _a2v_classic(pairs: torch.Tensor):
if (
not isinstance(pairs, torch.Tensor)
or pairs.ndim != 2
or pairs.shape[1] != 2
):
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
zeros = torch.zeros_like(pairs[:, 1])
return torch.stack([pairs[:, 0], zeros], dim=1)
self.set_pair_postprocess(_a2v_classic)
return
if name == "dual_sigma_shift":
visual_shift = float(kwargs.get("visual_shift", self.shift))
audio_shift = float(kwargs.get("audio_shift", self.shift))
visual_denoising_strength = float(
kwargs.get("visual_denoising_strength", 1.0)
)
audio_denoising_strength = float(
kwargs.get("audio_denoising_strength", 1.0)
)
visual_mu = kwargs.get(
"visual_exponential_shift_mu", self.exponential_shift_mu
)
audio_mu = kwargs.get(
"audio_exponential_shift_mu", self.exponential_shift_mu
)
def _dual_sigma_shift(pairs: torch.Tensor, *, source: str):
if not isinstance(pairs, torch.Tensor):
raise TypeError("pairs must be a torch.Tensor")
if pairs.ndim != 2 or pairs.shape[1] != 2:
raise ValueError("pairs must be a torch.Tensor of shape [N, 2]")
if pairs.shape[0] == 0:
raise ValueError("pairs length must be greater than 0")
if source not in ("timesteps", "sigmas"):
raise ValueError("source must be 'timesteps' or 'sigmas'")
num_steps = pairs.shape[0]
device = pairs.device
dtype = pairs.dtype
def _build_column(
shift_value: float, denoising_strength: float, mu_override
):
if shift_value <= 0:
raise ValueError("shift must be positive")
if denoising_strength <= 0:
raise ValueError("denoising_strength must be positive")
sigma_start = (
self.sigma_min
+ (self.sigma_max - self.sigma_min) * denoising_strength
)
if self.extra_one_step:
base = torch.linspace(
sigma_start,
self.sigma_min,
num_steps + 1,
device=device,
dtype=dtype,
)[:-1]
else:
base = torch.linspace(
sigma_start,
self.sigma_min,
num_steps,
device=device,
dtype=dtype,
)
if self.inverse_timesteps:
base = torch.flip(base, dims=[0])
if self.exponential_shift:
mu_value = mu_override
if mu_value is None:
raise RuntimeError(
"exponential_shift enabled but exponential_shift_mu is missing"
)
exp_mu = math.exp(float(mu_value))
base = exp_mu / (exp_mu + (1 / base - 1))
else:
base = shift_value * base / (1 + (shift_value - 1) * base)
if self.shift_terminal is not None:
one_minus_z = 1 - base
scale_factor = one_minus_z[-1] / (1 - self.shift_terminal)
if scale_factor != 0:
base = 1 - (one_minus_z / scale_factor)
if self.reverse_sigmas:
base = 1 - base
if source == "timesteps":
return base * self.num_train_timesteps
return base
col0 = _build_column(visual_shift, visual_denoising_strength, visual_mu)
col1 = _build_column(audio_shift, audio_denoising_strength, audio_mu)
return torch.stack([col0, col1], dim=1)
_dual_sigma_shift._requires_source = True
self.set_pair_postprocess(_dual_sigma_shift)
return
raise ValueError(f"Unknown pair_postprocess name: {name}")
def _make_pairs_from_vector(self, vec: torch.Tensor) -> torch.Tensor:
if vec.ndim != 1:
raise ValueError("vec must be 1D")
return torch.stack([vec, vec], dim=1)
def get_pairs(self, source: str = "timesteps") -> torch.Tensor:
if source == "timesteps":
if self.pair_timesteps is None:
self._refresh_pair_cache()
return self.pair_timesteps
if source == "sigmas":
if self.pair_sigmas is None:
self._refresh_pair_cache()
return self.pair_sigmas
raise ValueError("source must be 'timesteps' or 'sigmas'")
def timestep_to_sigma(self, timestep: torch.Tensor | float) -> torch.Tensor:
"""Return sigma for a scalar timestep via nearest neighbor lookup.
Args:
timestep: Scalar timestep value.
Returns:
Sigma corresponding to the nearest timestep.
"""
t_value = float(timestep)
t_cpu = torch.tensor(t_value)
idx = torch.argmin((self.train_timesteps - t_cpu).abs())
return self.train_sigmas[idx]
def step_from_to(
self,
model_output: torch.Tensor,
timestep_from: torch.Tensor,
timestep_to: torch.Tensor | None,
sample: torch.Tensor,
) -> torch.Tensor:
"""Advance one step using an explicit (from, to) timestep pair.
The update rule is:
x_{to} = x_{from} + model_output * (sigma(to) - sigma(from))
Args:
model_output: Predicted model output.
timestep_from: Source timestep.
timestep_to: Target timestep or None for terminal.
sample: Current sample at timestep_from.
Returns:
Updated sample at timestep_to.
"""
sigma_from = self.timestep_to_sigma(timestep_from)
if timestep_to is None:
sigma_to = torch.tensor(
1.0 if (self.inverse_timesteps or self.reverse_sigmas) else 0.0,
device=sigma_from.device,
dtype=sigma_from.dtype,
)
else:
sigma_to = self.timestep_to_sigma(timestep_to)
prev_sample = sample + model_output * (sigma_to - sigma_from)
return prev_sample
def _refresh_pair_cache(self) -> None:
if self.timesteps is None or self.sigmas is None:
raise RuntimeError("Scheduler not initialized; call set_timesteps() first")
def _apply_postprocess(pairs: torch.Tensor, source: str) -> torch.Tensor:
if self._pair_postprocess_fn is None:
return pairs
if self._pair_postprocess_requires_source:
modified = self._pair_postprocess_fn(pairs, source=source)
else:
modified = self._pair_postprocess_fn(pairs)
if not isinstance(modified, torch.Tensor):
raise TypeError("pair_postprocess must return a torch.Tensor")
if modified.shape != pairs.shape:
raise ValueError("pair_postprocess must return the same shape as input")
return modified
base_pairs_timesteps = self._make_pairs_from_vector(self.timesteps)
base_pairs_sigmas = self._make_pairs_from_vector(self.sigmas)
self.pair_timesteps = _apply_postprocess(base_pairs_timesteps, "timesteps")
self.pair_sigmas = _apply_postprocess(base_pairs_sigmas, "sigmas")
EntryClass = FlowMatchPairScheduler
@@ -0,0 +1,371 @@
# Copied and adapted from: https://github.com/Tencent-Hunyuan/Hunyuan3D-2
from __future__ import annotations
import math
from dataclasses import dataclass
from typing import List, Optional, Tuple, Union
import numpy as np
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput
@dataclass
class Hunyuan3DFlowMatchSchedulerOutput(BaseOutput):
"""Output class for the scheduler's step function."""
prev_sample: torch.FloatTensor
class Hunyuan3DFlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin):
"""Euler discrete scheduler for flow matching."""
# External module path aliases for compatibility with Hunyuan3D configs
_aliases = [
"hy3dgen.shapegen.schedulers.FlowMatchEulerDiscreteScheduler",
"hy3dshape.schedulers.FlowMatchEulerDiscreteScheduler",
]
_compatibles = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
shift: float = 1.0,
use_dynamic_shifting: bool = False,
):
timesteps = np.linspace(
1, num_train_timesteps, num_train_timesteps, dtype=np.float32
).copy()
timesteps = torch.from_numpy(timesteps).to(dtype=torch.float32)
sigmas = timesteps / num_train_timesteps
if not use_dynamic_shifting:
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
self.timesteps = sigmas * num_train_timesteps
self._step_index = None
self._begin_index = None
self.sigmas = sigmas.to("cpu")
self.sigma_min = self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
@property
def step_index(self) -> Optional[int]:
"""The index counter for current timestep."""
return self._step_index
@property
def begin_index(self) -> Optional[int]:
"""The index for the first timestep."""
return self._begin_index
def set_begin_index(self, begin_index: int = 0):
"""Set the begin index for the scheduler.
Args:
begin_index: The begin index for the scheduler.
"""
self._begin_index = begin_index
def scale_model_input(
self,
sample: torch.FloatTensor,
timestep: Optional[Union[float, torch.FloatTensor]] = None,
) -> torch.FloatTensor:
"""Identity operation for flow matching (no input scaling needed)."""
return sample
def scale_noise(
self,
sample: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
noise: Optional[torch.FloatTensor] = None,
) -> torch.FloatTensor:
"""Forward process in flow-matching (add noise to sample)."""
sigmas = self.sigmas.to(device=sample.device, dtype=sample.dtype)
if sample.device.type == "mps" and torch.is_floating_point(timestep):
schedule_timesteps = self.timesteps.to(sample.device, dtype=torch.float32)
timestep = timestep.to(sample.device, dtype=torch.float32)
else:
schedule_timesteps = self.timesteps.to(sample.device)
timestep = timestep.to(sample.device)
if self.begin_index is None:
step_indices = [
self.index_for_timestep(t, schedule_timesteps) for t in timestep
]
elif self.step_index is not None:
step_indices = [self.step_index] * timestep.shape[0]
else:
step_indices = [self.begin_index] * timestep.shape[0]
sigma = sigmas[step_indices].flatten()
while len(sigma.shape) < len(sample.shape):
sigma = sigma.unsqueeze(-1)
sample = sigma * noise + (1.0 - sigma) * sample
return sample
def _sigma_to_t(self, sigma: float) -> float:
"""Convert sigma to timestep."""
return sigma * self.config.num_train_timesteps
def time_shift(self, mu: float, sigma: float, t: torch.Tensor) -> torch.Tensor:
"""Apply time shift transformation."""
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
def set_timesteps(
self,
num_inference_steps: int = None,
device: Union[str, torch.device] = None,
sigmas: Optional[List[float]] = None,
mu: Optional[float] = None,
):
"""Set the discrete timesteps for the diffusion chain."""
if self.config.use_dynamic_shifting and mu is None:
raise ValueError(
"Must pass a value for `mu` when `use_dynamic_shifting` is True"
)
if sigmas is None:
self.num_inference_steps = num_inference_steps
timesteps = np.linspace(
self._sigma_to_t(self.sigma_max),
self._sigma_to_t(self.sigma_min),
num_inference_steps,
)
sigmas = timesteps / self.config.num_train_timesteps
if self.config.use_dynamic_shifting:
sigmas = self.time_shift(mu, 1.0, sigmas)
else:
sigmas = self.config.shift * sigmas / (1 + (self.config.shift - 1) * sigmas)
sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32, device=device)
timesteps = sigmas * self.config.num_train_timesteps
self.timesteps = timesteps.to(device=device)
self.sigmas = torch.cat([sigmas, torch.ones(1, device=sigmas.device)])
self._step_index = None
self._begin_index = None
def index_for_timestep(
self, timestep: float, schedule_timesteps: Optional[torch.Tensor] = None
) -> int:
"""Find the index for a given timestep."""
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep: Union[float, torch.Tensor]):
"""Initialize step index from timestep."""
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
sample: torch.FloatTensor,
s_churn: float = 0.0,
s_tmin: float = 0.0,
s_tmax: float = float("inf"),
s_noise: float = 1.0,
generator: Optional[torch.Generator] = None,
return_dict: bool = True,
) -> Union[Hunyuan3DFlowMatchSchedulerOutput, Tuple]:
"""Predict the sample from the previous timestep."""
if isinstance(timestep, (int, torch.IntTensor, torch.LongTensor)):
raise ValueError(
"Passing integer indices as timesteps is not supported. "
"Pass one of `scheduler.timesteps` as a timestep."
)
if self.step_index is None:
self._init_step_index(timestep)
# Upcast to avoid precision issues
sample = sample.to(torch.float32)
sigma = self.sigmas[self.step_index]
sigma_next = self.sigmas[self.step_index + 1]
prev_sample = sample + (sigma_next - sigma) * model_output
prev_sample = prev_sample.to(model_output.dtype)
self._step_index += 1
if not return_dict:
return (prev_sample,)
return Hunyuan3DFlowMatchSchedulerOutput(prev_sample=prev_sample)
def __len__(self) -> int:
return self.config.num_train_timesteps
@dataclass
class Hunyuan3DConsistencyFlowMatchSchedulerOutput(BaseOutput):
"""Output for consistency flow matching scheduler."""
prev_sample: torch.FloatTensor
pred_original_sample: torch.FloatTensor
class Hunyuan3DConsistencyFlowMatchEulerDiscreteScheduler(SchedulerMixin, ConfigMixin):
"""Consistency Flow Matching Euler Discrete Scheduler."""
# External module path aliases for compatibility with Hunyuan3D configs
_aliases = [
"hy3dshape.schedulers.Hunyuan3DConsistencyFlowMatchEulerDiscreteScheduler",
]
_compatibles = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
pcm_timesteps: int = 50,
):
sigmas = np.linspace(0, 1, num_train_timesteps)
step_ratio = num_train_timesteps // pcm_timesteps
euler_timesteps = (np.arange(1, pcm_timesteps) * step_ratio).round().astype(
np.int64
) - 1
euler_timesteps = np.asarray([0] + euler_timesteps.tolist())
self.euler_timesteps = euler_timesteps
self.sigmas = sigmas[self.euler_timesteps]
self.sigmas = torch.from_numpy(self.sigmas.copy()).to(dtype=torch.float32)
self.timesteps = self.sigmas * num_train_timesteps
self._step_index = None
self._begin_index = None
self.sigmas = self.sigmas.to("cpu")
@property
def step_index(self) -> Optional[int]:
return self._step_index
@property
def begin_index(self) -> Optional[int]:
return self._begin_index
def set_begin_index(self, begin_index: int = 0):
self._begin_index = begin_index
def scale_model_input(
self,
sample: torch.FloatTensor,
timestep: Optional[Union[float, torch.FloatTensor]] = None,
) -> torch.FloatTensor:
"""Identity operation for flow matching (no input scaling needed)."""
return sample
def _sigma_to_t(self, sigma: float) -> float:
return sigma * self.config.num_train_timesteps
def set_timesteps(
self,
num_inference_steps: int = None,
device: Union[str, torch.device] = None,
sigmas: Optional[List[float]] = None,
):
"""Set timesteps for inference."""
self.num_inference_steps = (
num_inference_steps if num_inference_steps is not None else len(sigmas)
)
inference_indices = np.linspace(
0, self.config.pcm_timesteps, num=self.num_inference_steps, endpoint=False
)
inference_indices = np.floor(inference_indices).astype(np.int64)
inference_indices = torch.from_numpy(inference_indices).long()
self.sigmas_ = self.sigmas[inference_indices]
timesteps = self.sigmas_ * self.config.num_train_timesteps
self.timesteps = timesteps.to(device=device)
self.sigmas_ = torch.cat(
[self.sigmas_, torch.ones(1, device=self.sigmas_.device)]
)
self._step_index = None
self._begin_index = None
def index_for_timestep(
self, timestep: float, schedule_timesteps: Optional[torch.Tensor] = None
) -> int:
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep: Union[float, torch.Tensor]):
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.FloatTensor,
timestep: Union[float, torch.FloatTensor],
sample: torch.FloatTensor,
generator: Optional[torch.Generator] = None,
return_dict: bool = True,
) -> Union[Hunyuan3DConsistencyFlowMatchSchedulerOutput, Tuple]:
"""Perform one step of the consistency flow matching scheduler."""
if isinstance(timestep, (int, torch.IntTensor, torch.LongTensor)):
raise ValueError("Passing integer indices as timesteps is not supported.")
if self.step_index is None:
self._init_step_index(timestep)
sample = sample.to(torch.float32)
sigma = self.sigmas_[self.step_index]
sigma_next = self.sigmas_[self.step_index + 1]
prev_sample = sample + (sigma_next - sigma) * model_output
prev_sample = prev_sample.to(model_output.dtype)
pred_original_sample = sample + (1.0 - sigma) * model_output
pred_original_sample = pred_original_sample.to(model_output.dtype)
self._step_index += 1
if not return_dict:
return (prev_sample,)
return Hunyuan3DConsistencyFlowMatchSchedulerOutput(
prev_sample=prev_sample, pred_original_sample=pred_original_sample
)
def __len__(self) -> int:
return self.config.num_train_timesteps
# Entry class for model registry
EntryClass = [
Hunyuan3DFlowMatchEulerDiscreteScheduler,
Hunyuan3DConsistencyFlowMatchEulerDiscreteScheduler,
]
@@ -0,0 +1,193 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
"""
Pass-through scheduler for ComfyUI integration.
This scheduler does not modify latents - it simply returns the input sample unchanged.
The actual denoising logic is handled by ComfyUI.
"""
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class ComfyUIPassThroughSchedulerOutput(BaseOutput):
"""
Output class for the scheduler's `step` function output.
Args:
prev_sample (`torch.FloatTensor`): The input sample unchanged (pass-through).
"""
prev_sample: torch.FloatTensor
class ComfyUIPassThroughScheduler(BaseScheduler, ConfigMixin, SchedulerMixin):
"""
Pass-through scheduler for ComfyUI integration.
This scheduler does not modify latents. It is used when the denoising logic
is handled externally by ComfyUI. The scheduler simply returns the input
sample unchanged, allowing ComfyUI to manage the denoising process.
Usage:
- num_inference_steps is always 1 (each step is handled separately)
- timesteps are provided externally by ComfyUI
- step() returns the input sample unchanged
"""
config_name = "scheduler_config.json"
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps=1000,
*args,
**kwargs,
):
self.num_train_timesteps = num_train_timesteps
# Initialize timesteps as empty - will be set externally
self.timesteps = torch.tensor([], dtype=torch.long)
self.shift = 0.0
self._step_index = 0 # Track current step index
self._begin_index: int | None = None # For compatibility with DenoisingStage
def set_timesteps(
self,
num_inference_steps=1, # Always 1 for ComfyUI
timesteps=None, # Can be provided externally
device=None,
**kwargs,
):
"""
Set timesteps. For ComfyUI, timesteps are provided externally.
Args:
num_inference_steps: Ignored (always 1 for ComfyUI)
timesteps: External timesteps provided by ComfyUI
device: Device to place timesteps on
"""
if timesteps is not None:
# Use externally provided timesteps
if isinstance(timesteps, torch.Tensor):
self.timesteps = timesteps
else:
self.timesteps = torch.tensor(timesteps, dtype=torch.long)
if device is not None:
self.timesteps = self.timesteps.to(device)
else:
# Create a single timestep if none provided
if device is None:
device = torch.device("cpu")
self.timesteps = torch.tensor([0], dtype=torch.long, device=device)
def step(
self,
model_output: torch.FloatTensor,
timestep: torch.FloatTensor | int,
sample: torch.FloatTensor,
return_dict: bool = False,
**kwargs,
) -> tuple | ComfyUIPassThroughSchedulerOutput:
"""
Pass-through step: returns the input sample unchanged.
This scheduler does not modify latents. The actual denoising is handled
by ComfyUI, so we simply return the input sample as-is.
Args:
model_output: Predicted noise (ignored, but kept for API compatibility)
timestep: Current timestep (ignored, but kept for API compatibility)
sample: Input latents (returned unchanged)
return_dict: Whether to return a dict or tuple
Returns:
The input sample unchanged (prev_sample = sample)
"""
# Increment step index for tracking
self._step_index += 1
# Simply return the input sample unchanged
prev_sample = sample
if not return_dict:
return (prev_sample,)
return ComfyUIPassThroughSchedulerOutput(prev_sample=prev_sample)
def scale_model_input(
self, sample: torch.Tensor, timestep: int | None = None
) -> torch.Tensor:
"""
Scale model input. For pass-through scheduler, returns input unchanged.
Args:
sample: Input sample
timestep: Timestep (ignored)
Returns:
Input sample unchanged
"""
return sample
def set_shift(self, shift: float) -> None:
"""
Set shift parameter (no-op for pass-through scheduler).
Args:
shift: Shift value (ignored)
"""
self.shift = shift
def set_begin_index(self, begin_index: int = 0) -> None:
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index: The begin index for the scheduler.
"""
self._begin_index = begin_index
@property
def begin_index(self) -> int | None:
"""
The index for the first timestep.
"""
return self._begin_index
@property
def step_index(self) -> int:
"""
The index counter for current timestep.
"""
return self._step_index
def add_noise(
self,
original_samples: torch.Tensor,
noise: torch.Tensor,
timestep: torch.Tensor,
) -> torch.Tensor:
"""
Add noise to samples. For pass-through scheduler, returns original samples.
Args:
original_samples: Original clean samples
noise: Noise to add (ignored)
timestep: Timestep (ignored)
Returns:
Original samples unchanged
"""
return original_samples
EntryClass = ComfyUIPassThroughScheduler
@@ -0,0 +1,148 @@
# SPDX-License-Identifier: Apache-2.0
#
# DPM-Solver++ multistep scheduler wrapper for SANA.
#
# SANA uses DPM-Solver++ (Lu et al., 2022) as its noise scheduler, which
# is a high-order ODE solver that converges in fewer steps than DDIM.
# With solver_order=2 and 20 steps, SANA achieves high-quality results.
#
# This wrapper delegates all numerical work to diffusers' implementation
# and only adapts the interface for sglang's denoising stage.
import torch
from diffusers import (
DPMSolverMultistepScheduler as DiffusersDPMSolverMultistepScheduler,
)
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
class DPMSolverMultistepScheduler(SchedulerMixin, ConfigMixin, BaseScheduler):
"""DPM-Solver++ multistep scheduler wrapper for sglang's BaseScheduler interface."""
order = 1
num_train_timesteps = 1000
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
beta_start: float = 0.0001,
beta_end: float = 0.02,
beta_schedule: str = "scaled_linear",
trained_betas=None,
solver_order: int = 2,
prediction_type: str = "epsilon",
thresholding: bool = False,
dynamic_thresholding_ratio: float = 0.995,
sample_max_value: float = 1.0,
algorithm_type: str = "dpmsolver++",
solver_type: str = "midpoint",
lower_order_final: bool = True,
euler_at_final: bool = False,
use_karras_sigmas: bool = False,
use_lu_lambdas: bool = False,
use_exponential_sigmas: bool = False,
use_beta_sigmas: bool = False,
use_flow_sigmas: bool = False,
final_sigmas_type: str = "zero",
lambda_min_clipped: float = -float("inf"),
variance_type: str | None = None,
timestep_spacing: str = "linspace",
steps_offset: int = 0,
rescale_betas_zero_snr: bool = False,
flow_shift: float | None = None,
**kwargs,
):
self.num_train_timesteps = num_train_timesteps
self._inner = DiffusersDPMSolverMultistepScheduler(
num_train_timesteps=num_train_timesteps,
beta_start=beta_start,
beta_end=beta_end,
beta_schedule=beta_schedule,
trained_betas=trained_betas,
solver_order=solver_order,
prediction_type=prediction_type,
thresholding=thresholding,
dynamic_thresholding_ratio=dynamic_thresholding_ratio,
sample_max_value=sample_max_value,
algorithm_type=algorithm_type,
solver_type=solver_type,
lower_order_final=lower_order_final,
euler_at_final=euler_at_final,
use_karras_sigmas=use_karras_sigmas,
use_lu_lambdas=use_lu_lambdas,
use_exponential_sigmas=use_exponential_sigmas,
use_beta_sigmas=use_beta_sigmas,
use_flow_sigmas=use_flow_sigmas,
flow_shift=flow_shift,
final_sigmas_type=final_sigmas_type,
lambda_min_clipped=lambda_min_clipped,
variance_type=variance_type,
timestep_spacing=timestep_spacing,
steps_offset=steps_offset,
rescale_betas_zero_snr=rescale_betas_zero_snr,
)
self.timesteps = self._inner.timesteps
self.order = solver_order
self._flow_shift = flow_shift
self._begin_index: int | None = None
BaseScheduler.__init__(self)
def set_shift(self, shift: float) -> None:
self._flow_shift = shift
def set_begin_index(self, begin_index: int = 0) -> None:
self._begin_index = begin_index
@property
def begin_index(self) -> int | None:
return self._begin_index
def set_timesteps(self, num_inference_steps: int, device=None, **kwargs):
self._inner.set_timesteps(num_inference_steps, device=device, **kwargs)
self.timesteps = self._inner.timesteps
def scale_model_input(
self, sample: torch.Tensor, timestep: int | None = None
) -> torch.Tensor:
return self._inner.scale_model_input(sample, timestep)
def step(
self,
model_output: torch.Tensor,
timestep: int,
sample: torch.Tensor,
generator: torch.Generator | None = None,
variance_noise: torch.Tensor | None = None,
return_dict: bool = True,
):
return self._inner.step(
model_output,
timestep,
sample,
generator=generator,
variance_noise=variance_noise,
return_dict=return_dict,
)
@property
def sigmas(self):
return getattr(self._inner, "sigmas", None)
@property
def init_noise_sigma(self):
return self._inner.init_noise_sigma
def add_noise(
self,
original_samples: torch.Tensor,
noise: torch.Tensor,
timesteps: torch.Tensor,
) -> torch.Tensor:
return self._inner.add_noise(original_samples, noise, timesteps)
EntryClass = DPMSolverMultistepScheduler
@@ -0,0 +1,703 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Copyright 2024 Stability AI, Katherine Crowson and The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ==============================================================================
#
# Modified from diffusers==0.29.2
#
# ==============================================================================
import math
from dataclasses import dataclass
from typing import Any
import numpy as np
import scipy.stats
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
from sglang.multimodal_gen.runtime.post_training.scheduler_rl_mixin import (
SchedulerRLMixin,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@dataclass
class FlowMatchEulerDiscreteSchedulerOutput(BaseOutput):
"""
Output class for the scheduler's `step` function output.
Args:
prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images):
Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the
denoising loop.
"""
prev_sample: torch.FloatTensor
class FlowMatchEulerDiscreteScheduler(
SchedulerMixin, ConfigMixin, BaseScheduler, SchedulerRLMixin
):
"""
Euler scheduler.
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
methods the library implements for all schedulers such as loading and saving.
Args:
num_train_timesteps (`int`, defaults to 1000):
The number of diffusion steps to train the model.
shift (`float`, defaults to 1.0):
The shift value for the timestep schedule.
use_dynamic_shifting (`bool`, defaults to False):
Whether to apply timestep shifting on-the-fly based on the image resolution.
base_shift (`float`, defaults to 0.5):
Value to stabilize image generation. Increasing `base_shift` reduces variation and image is more consistent
with desired output.
max_shift (`float`, defaults to 1.15):
Value change allowed to latent vectors. Increasing `max_shift` encourages more variation and image may be
more exaggerated or stylized.
base_image_seq_len (`int`, defaults to 256):
The base image sequence length.
max_image_seq_len (`int`, defaults to 4096):
The maximum image sequence length.
invert_sigmas (`bool`, defaults to False):
Whether to invert the sigmas.
shift_terminal (`float`, defaults to None):
The end value of the shifted timestep schedule.
use_karras_sigmas (`bool`, defaults to False):
Whether to use Karras sigmas for step sizes in the noise schedule during sampling.
use_exponential_sigmas (`bool`, defaults to False):
Whether to use exponential sigmas for step sizes in the noise schedule during sampling.
use_beta_sigmas (`bool`, defaults to False):
Whether to use beta sigmas for step sizes in the noise schedule during sampling.
time_shift_type (`str`, defaults to "exponential"):
The type of dynamic resolution-dependent timestep shifting to apply. Either "exponential" or "linear".
stochastic_sampling (`bool`, defaults to False):
Whether to use stochastic sampling.
"""
_compatibles: list[Any] = []
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
shift: float = 1.0,
use_dynamic_shifting: bool = False,
base_shift: float | None = 0.5,
max_shift: float | None = 1.15,
base_image_seq_len: int | None = 256,
max_image_seq_len: int | None = 4096,
invert_sigmas: bool = False,
shift_terminal: float | None = None,
use_karras_sigmas: bool | None = False,
use_exponential_sigmas: bool | None = False,
use_beta_sigmas: bool | None = False,
time_shift_type: str = "exponential",
stochastic_sampling: bool = False,
):
if (
sum(
[
self.config.use_beta_sigmas,
self.config.use_exponential_sigmas,
self.config.use_karras_sigmas,
]
)
> 1
):
raise ValueError(
"Only one of `config.use_beta_sigmas`, `config.use_exponential_sigmas`, `config.use_karras_sigmas` can be used."
)
if time_shift_type not in {"exponential", "linear"}:
raise ValueError(
"`time_shift_type` must either be 'exponential' or 'linear'."
)
timesteps = np.linspace(
1, num_train_timesteps, num_train_timesteps, dtype=np.float32
)[::-1].copy()
timesteps = torch.from_numpy(timesteps).to(dtype=torch.float32)
sigmas = timesteps / num_train_timesteps
if not use_dynamic_shifting:
# when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas)
self.timesteps = sigmas * num_train_timesteps
self.num_train_timesteps = num_train_timesteps
self._step_index: int | None = None
self._begin_index: int | None = None
self._shift = shift
self.sigmas = sigmas.to("cpu") # to avoid too much CPU/GPU communication
self.sigma_min = self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
BaseScheduler.__init__(self)
@property
def shift(self) -> float:
"""
The value used for shifting.
"""
return self._shift
@property
def step_index(self) -> int | None:
"""
The index counter for current timestep. It will increase 1 after each scheduler step.
"""
return self._step_index
@property
def begin_index(self) -> int | None:
"""
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
"""
return self._begin_index
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
def set_begin_index(self, begin_index: int = 0) -> None:
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index (`int`):
The begin index for the scheduler.
"""
self._begin_index = begin_index
def set_shift(self, shift: float) -> None:
self._shift = shift
def scale_noise(
self,
sample: torch.FloatTensor,
timestep: float | torch.FloatTensor,
noise: torch.FloatTensor | None = None,
) -> torch.FloatTensor:
"""
Forward process in flow-matching
"""
# Make sure sigmas and timesteps have the same device and dtype as original_samples
sigmas = self.sigmas.to(device=sample.device, dtype=sample.dtype)
if sample.device.type == "mps" and torch.is_floating_point(timestep):
# mps does not support float64
schedule_timesteps = self.timesteps.to(sample.device, dtype=torch.float32)
assert isinstance(timestep, torch.Tensor)
timestep = timestep.to(sample.device, dtype=torch.float32)
else:
schedule_timesteps = self.timesteps.to(sample.device)
assert isinstance(timestep, torch.Tensor)
timestep = timestep.to(sample.device)
# self.begin_index is None when scheduler is used for training, or pipeline does not implement set_begin_index
if self.begin_index is None:
step_indices = [
self.index_for_timestep(t, schedule_timesteps) for t in timestep
]
elif self.step_index is not None:
# add_noise is called after first denoising step (for inpainting)
step_indices = [self.step_index] * timestep.shape[0]
else:
# add noise is called before first denoising step to create initial latent(img2img)
step_indices = [self.begin_index] * timestep.shape[0]
sigma = sigmas[step_indices].flatten()
while len(sigma.shape) < len(sample.shape):
sigma = sigma.unsqueeze(-1)
sample = sigma * noise + (1.0 - sigma) * sample
return sample
def _sigma_to_t(self, sigma: float) -> float:
return sigma * self.config.num_train_timesteps
def time_shift(
self, mu: float, sigma: float, t: torch.Tensor | np.ndarray
) -> torch.Tensor | np.ndarray:
if self.config.time_shift_type == "exponential":
return self._time_shift_exponential(mu, sigma, t)
elif self.config.time_shift_type == "linear":
return self._time_shift_linear(mu, sigma, t)
else:
raise ValueError(f"Unknown time_shift_type: {self.config.time_shift_type}")
def stretch_shift_to_terminal(self, t: torch.Tensor) -> torch.Tensor:
r"""
Stretches and shifts the timestep schedule to ensure it terminates at the configured `shift_terminal` config
value.
Reference:
https://github.com/Lightricks/LTX-Video/blob/a01a171f8fe3d99dce2728d60a73fecf4d4238ae/ltx_video/schedulers/rf.py#L51
Args:
t (`torch.Tensor`):
A tensor of timesteps to be stretched and shifted.
Returns:
`torch.Tensor`:
A tensor of adjusted timesteps such that the final value equals `self.config.shift_terminal`.
"""
one_minus_z = 1 - t
scale_factor = one_minus_z[-1] / (1 - self.config.shift_terminal)
if scale_factor == 0:
return t
stretched_t = 1 - (one_minus_z / scale_factor)
return stretched_t
def set_timesteps(
self,
num_inference_steps: int | None = None,
device: str | torch.device = None,
sigmas: list[float] | None = None,
mu: float | None = None,
timesteps: list[float] | None = None,
) -> None:
"""
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
Args:
num_inference_steps (`int`, *optional*):
The number of diffusion steps used when generating samples with a pre-trained model.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
sigmas (`List[float]`, *optional*):
Custom values for sigmas to be used for each diffusion step. If `None`, the sigmas are computed
automatically.
mu (`float`, *optional*):
Determines the amount of shifting applied to sigmas when performing resolution-dependent timestep
shifting.
timesteps (`List[float]`, *optional*):
Custom values for timesteps to be used for each diffusion step. If `None`, the timesteps are computed
automatically.
"""
if self.config.use_dynamic_shifting and mu is None:
raise ValueError(
"`mu` must be passed when `use_dynamic_shifting` is set to be `True`"
)
if (
sigmas is not None
and timesteps is not None
and len(sigmas) != len(timesteps)
):
raise ValueError("`sigmas` and `timesteps` should have the same length")
if num_inference_steps is not None:
if (sigmas is not None and len(sigmas) != num_inference_steps) or (
timesteps is not None and len(timesteps) != num_inference_steps
):
raise ValueError(
"`sigmas` and `timesteps` should have the same length as num_inference_steps, if `num_inference_steps` is provided"
)
else:
if sigmas is not None:
num_inference_steps = len(sigmas)
elif timesteps is not None:
num_inference_steps = len(timesteps)
else:
raise ValueError(
"Either num_inference_steps, sigmas, or timesteps must be provided"
)
self.num_inference_steps = num_inference_steps
# 1. Prepare default sigmas
is_timesteps_provided = timesteps is not None
timesteps_array: np.ndarray | None = None
if is_timesteps_provided:
assert timesteps is not None
timesteps_array = np.array(timesteps).astype(np.float32)
sigmas_array: np.ndarray
if sigmas is None:
if timesteps_array is None:
timesteps_array = np.linspace(
self._sigma_to_t(self.sigma_max),
self._sigma_to_t(self.sigma_min),
num_inference_steps,
)
sigmas_array = timesteps_array / self.config.num_train_timesteps
else:
sigmas_array = np.array(sigmas).astype(np.float32)
num_inference_steps = len(sigmas_array)
# 2. Perform timestep shifting. Either no shifting is applied, or resolution-dependent shifting of
# "exponential" or "linear" type is applied
if self.config.use_dynamic_shifting:
assert mu is not None, "mu cannot be None when use_dynamic_shifting is True"
sigmas_array = self.time_shift(mu, 1.0, sigmas_array)
else:
sigmas_array = (
self.shift * sigmas_array / (1 + (self.shift - 1) * sigmas_array)
)
# 3. If required, stretch the sigmas schedule to terminate at the configured `shift_terminal` value
if self.config.shift_terminal:
sigmas_tensor = torch.from_numpy(sigmas_array).to(dtype=torch.float32)
sigmas_tensor = self.stretch_shift_to_terminal(sigmas_tensor)
sigmas_array = sigmas_tensor.numpy()
# 4. If required, convert sigmas to one of karras, exponential, or beta sigma schedules
if self.config.use_karras_sigmas:
sigmas_tensor = torch.from_numpy(sigmas_array).to(dtype=torch.float32)
sigmas_tensor = self._convert_to_karras(
in_sigmas=sigmas_tensor, num_inference_steps=num_inference_steps
)
sigmas_array = sigmas_tensor.numpy()
elif self.config.use_exponential_sigmas:
sigmas_tensor = torch.from_numpy(sigmas_array).to(dtype=torch.float32)
sigmas_tensor = self._convert_to_exponential(
in_sigmas=sigmas_tensor, num_inference_steps=num_inference_steps
)
sigmas_array = sigmas_tensor.numpy()
elif self.config.use_beta_sigmas:
sigmas_tensor = torch.from_numpy(sigmas_array).to(dtype=torch.float32)
sigmas_tensor = self._convert_to_beta(
in_sigmas=sigmas_tensor, num_inference_steps=num_inference_steps
)
sigmas_array = sigmas_tensor.numpy()
# 5. Convert sigmas and timesteps to tensors and move to specified device
sigmas_tensor = torch.from_numpy(sigmas_array).to(
dtype=torch.float32, device=device
)
if not is_timesteps_provided:
timesteps_tensor = sigmas_tensor * self.config.num_train_timesteps
else:
assert timesteps_array is not None
timesteps_tensor = torch.from_numpy(timesteps_array).to(
dtype=torch.float32, device=device
)
# 6. Append the terminal sigma value.
# If a model requires inverted sigma schedule for denoising but timesteps without inversion, the
# `invert_sigmas` flag can be set to `True`. This case is only required in Mochi
if self.config.invert_sigmas:
sigmas_tensor = 1.0 - sigmas_tensor
timesteps_tensor = sigmas_tensor * self.config.num_train_timesteps
sigmas_tensor = torch.cat(
[sigmas_tensor, torch.ones(1, device=sigmas_tensor.device)]
)
else:
sigmas_tensor = torch.cat(
[sigmas_tensor, torch.zeros(1, device=sigmas_tensor.device)]
)
self.timesteps = timesteps_tensor
self.sigmas = sigmas_tensor
self._step_index = None
self._begin_index = None
def index_for_timestep(
self,
timestep: float | torch.FloatTensor,
schedule_timesteps: torch.Tensor | None = None,
) -> int:
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
# The sigma index that is taken for the **very** first `step`
# is always the second index (or the last index if there is only 1)
# This way we can ensure we don't accidentally skip a sigma in
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep: float | torch.FloatTensor) -> None:
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.FloatTensor,
timestep: int | torch.Tensor,
sample: torch.FloatTensor,
s_churn: float = 0.0,
s_tmin: float = 0.0,
s_tmax: float = float("inf"),
s_noise: float = 1.0,
generator: torch.Generator | None = None,
per_token_timesteps: torch.Tensor | None = None,
batch=None,
return_dict: bool = True,
) -> FlowMatchEulerDiscreteSchedulerOutput | tuple[torch.FloatTensor, ...]:
"""
Predict the sample from the previous timestep by reversing the SDE. This function propagates the diffusion
process from the learned model outputs (most often the predicted noise).
Args:
model_output (`torch.FloatTensor`):
The direct output from learned diffusion model.
timestep (`int` or `torch.Tensor`):
The current discrete timestep in the diffusion chain.
sample (`torch.FloatTensor`):
A current instance of a sample created by the diffusion process.
s_churn (`float`):
s_tmin (`float`):
s_tmax (`float`):
s_noise (`float`, defaults to 1.0):
Scaling factor for noise added to the sample.
generator (`torch.Generator`, *optional*):
A random number generator.
per_token_timesteps (`torch.Tensor`, *optional*):
The timesteps for each token in the sample.
return_dict (`bool`):
Whether or not to return a
[`~schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteSchedulerOutput`] or tuple.
Returns:
[`~schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteSchedulerOutput`] or `tuple`:
If return_dict is `True`,
[`~schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteSchedulerOutput`] is returned,
otherwise a tuple is returned where the first element is the sample tensor.
"""
if isinstance(timestep, int | torch.IntTensor | torch.LongTensor):
raise ValueError(
(
"Passing integer indices (e.g. from `enumerate(timesteps)`) as timesteps to"
" `FlowMatchEulerDiscreteScheduler.step()` is not supported. Make sure to pass"
" one of the `scheduler.timesteps` as a timestep."
),
)
if self.step_index is None:
self._init_step_index(timestep)
# Upcast to avoid precision issues when computing prev_sample
sample = sample.to(torch.float32)
if per_token_timesteps is not None:
per_token_sigmas = per_token_timesteps / self.config.num_train_timesteps
sigmas = self.sigmas[:, None, None]
lower_mask = sigmas < per_token_sigmas[None] - 1e-6
lower_sigmas = lower_mask * sigmas
lower_sigmas, _ = lower_sigmas.max(dim=0)
current_sigma = per_token_sigmas[..., None]
next_sigma = lower_sigmas[..., None]
dt = current_sigma - next_sigma
else:
assert self.step_index is not None, "step_index should not be None"
sigma_idx = self.step_index
sigma = self.sigmas[sigma_idx]
sigma_next = self.sigmas[sigma_idx + 1]
current_sigma = sigma
next_sigma = sigma_next
dt = sigma_next - sigma
if batch is not None and batch.rollout:
if not self.already_prepared_rollout(batch):
raise RuntimeError("Rollout not prepared before step")
prev_sample = self.flow_sde_sampling(
batch, model_output, sample, current_sigma, next_sigma, generator
)
else:
if self.config.stochastic_sampling:
x0 = sample - current_sigma * model_output
noise = torch.randn_like(sample)
prev_sample = (1.0 - next_sigma) * x0 + next_sigma * noise
else:
prev_sample = sample + dt * model_output
# upon completion increase step index by one
assert self._step_index is not None, "_step_index should not be None"
self._step_index += 1
if per_token_timesteps is None:
# Cast sample back to model compatible dtype
prev_sample = prev_sample.to(model_output.dtype)
if isinstance(prev_sample, torch.Tensor | float) and not return_dict:
return (prev_sample,)
return FlowMatchEulerDiscreteSchedulerOutput(prev_sample=prev_sample)
# Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_karras
def _convert_to_karras(
self, in_sigmas: torch.Tensor, num_inference_steps: int
) -> torch.Tensor:
"""Constructs the noise schedule of Karras et al. (2022)."""
# Hack to make sure that other schedulers which copy this function don't break
# TODO: Add this logic to the other schedulers
if hasattr(self.config, "sigma_min"):
sigma_min = self.config.sigma_min
else:
sigma_min = None
if hasattr(self.config, "sigma_max"):
sigma_max = self.config.sigma_max
else:
sigma_max = None
sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item()
sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item()
rho = 7.0 # 7.0 is the value used in the paper
ramp = np.linspace(0, 1, num_inference_steps)
min_inv_rho = sigma_min ** (1 / rho)
max_inv_rho = sigma_max ** (1 / rho)
sigmas = (max_inv_rho + ramp * (min_inv_rho - max_inv_rho)) ** rho
return sigmas
# Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_exponential
def _convert_to_exponential(
self, in_sigmas: torch.Tensor, num_inference_steps: int
) -> torch.Tensor:
"""Constructs an exponential noise schedule."""
# Hack to make sure that other schedulers which copy this function don't break
# TODO: Add this logic to the other schedulers
if hasattr(self.config, "sigma_min"):
sigma_min = self.config.sigma_min
else:
sigma_min = None
if hasattr(self.config, "sigma_max"):
sigma_max = self.config.sigma_max
else:
sigma_max = None
sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item()
sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item()
sigmas = np.exp(
np.linspace(math.log(sigma_max), math.log(sigma_min), num_inference_steps)
)
return sigmas
# Copied from diffusers.schedulers.scheduling_euler_discrete.EulerDiscreteScheduler._convert_to_beta
def _convert_to_beta(
self,
in_sigmas: torch.Tensor,
num_inference_steps: int,
alpha: float = 0.6,
beta: float = 0.6,
) -> torch.Tensor:
"""From "Beta Sampling is All You Need" [arXiv:2407.12173] (Lee et. al, 2024)"""
# Hack to make sure that other schedulers which copy this function don't break
# TODO: Add this logic to the other schedulers
if hasattr(self.config, "sigma_min"):
sigma_min = self.config.sigma_min
else:
sigma_min = None
if hasattr(self.config, "sigma_max"):
sigma_max = self.config.sigma_max
else:
sigma_max = None
sigma_min = sigma_min if sigma_min is not None else in_sigmas[-1].item()
sigma_max = sigma_max if sigma_max is not None else in_sigmas[0].item()
sigmas = np.array(
[
sigma_min + (ppf * (sigma_max - sigma_min))
for ppf in [
scipy.stats.beta.ppf(timestep, alpha, beta)
for timestep in 1 - np.linspace(0, 1, num_inference_steps)
]
]
)
return sigmas
def _time_shift_exponential(
self, mu: float, sigma: float, t: torch.Tensor | np.ndarray
) -> torch.Tensor | np.ndarray:
if isinstance(t, np.ndarray):
return np.exp(mu) / (np.exp(mu) + (1 / t - 1) ** sigma)
else:
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
def _time_shift_linear(
self, mu: float, sigma: float, t: torch.Tensor | np.ndarray
) -> torch.Tensor | np.ndarray:
return mu / (mu + (1 / t - 1) ** sigma)
def add_noise(
self,
clean_latent: torch.Tensor,
noise: torch.Tensor,
timestep: torch.IntTensor,
) -> torch.Tensor:
"""
Args:
clean_latent: the clean latent with shape [B, C, H, W],
where B is batch_size or batch_size * num_frames
noise: the noise with shape [B, C, H, W]
timestep: the timestep with shape [1] or [bs * num_frames] or [bs, num_frames]
Returns:
the corrupted latent with shape [B, C, H, W]
"""
# If timestep is [bs, num_frames]
if timestep.ndim == 2:
timestep = timestep.flatten(0, 1)
assert timestep.numel() == clean_latent.shape[0]
elif timestep.ndim == 1:
# If timestep is [1]
if timestep.shape[0] == 1:
timestep = timestep.expand(clean_latent.shape[0])
else:
assert timestep.numel() == clean_latent.shape[0]
else:
raise ValueError(f"[add_noise] Invalid timestep shape: {timestep.shape}")
# timestep shape should be [B]
self.sigmas = self.sigmas.to(noise.device)
self.timesteps = self.timesteps.to(noise.device)
timestep_id = torch.argmin(
(self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
sample = (1 - sigma) * clean_latent + sigma * noise
return sample.type_as(noise)
def scale_model_input(
self, sample: torch.Tensor, timestep: int | None = None
) -> torch.Tensor:
return sample
def __len__(self) -> int:
return 0
EntryClass = FlowMatchEulerDiscreteScheduler
@@ -0,0 +1,843 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Copied from https://github.com/huggingface/diffusers/blob/v0.31.0/src/diffusers/schedulers/scheduling_unipc_multistep.py
# Convert unipc for flow matching
# Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
import math
from typing import Any
import numpy as np
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import (
KarrasDiffusionSchedulers,
SchedulerMixin,
SchedulerOutput,
)
from diffusers.utils import deprecate
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
class FlowUniPCMultistepScheduler(SchedulerMixin, ConfigMixin, BaseScheduler):
"""
`UniPCMultistepScheduler` is a training-free framework designed for the fast sampling of diffusion models.
This model inherits from [`SchedulerMixin`] and [`ConfigMixin`]. Check the superclass documentation for the generic
methods the library implements for all schedulers such as loading and saving.
Args:
num_train_timesteps (`int`, defaults to 1000):
The number of diffusion steps to train the model.
solver_order (`int`, default `2`):
The UniPC order which can be any positive integer. The effective order of accuracy is `solver_order + 1`
due to the UniC. It is recommended to use `solver_order=2` for guided sampling, and `solver_order=3` for
unconditional sampling.
prediction_type (`str`, defaults to "flow_prediction"):
Prediction type of the scheduler function; must be `flow_prediction` for this scheduler, which predicts
the flow of the diffusion process.
thresholding (`bool`, defaults to `False`):
Whether to use the "dynamic thresholding" method. This is unsuitable for latent-space diffusion models such
as Stable Diffusion.
dynamic_thresholding_ratio (`float`, defaults to 0.995):
The ratio for the dynamic thresholding method. Valid only when `thresholding=True`.
sample_max_value (`float`, defaults to 1.0):
The threshold value for dynamic thresholding. Valid only when `thresholding=True` and `predict_x0=True`.
predict_x0 (`bool`, defaults to `True`):
Whether to use the updating algorithm on the predicted x0.
solver_type (`str`, default `bh2`):
Solver type for UniPC. It is recommended to use `bh1` for unconditional sampling when steps < 10, and `bh2`
otherwise.
lower_order_final (`bool`, default `True`):
Whether to use lower-order solvers in the final steps. Only valid for < 15 inference steps. This can
stabilize the sampling of DPMSolver for steps < 15, especially for steps <= 10.
disable_corrector (`list`, default `[]`):
Decides which step to disable the corrector to mitigate the misalignment between `epsilon_theta(x_t, c)`
and `epsilon_theta(x_t^c, c)` which can influence convergence for a large guidance scale. Corrector is
usually disabled during the first few steps.
solver_p (`SchedulerMixin`, default `None`):
Any other scheduler that if specified, the algorithm becomes `solver_p + UniC`.
use_karras_sigmas (`bool`, *optional*, defaults to `False`):
Whether to use Karras sigmas for step sizes in the noise schedule during the sampling process. If `True`,
the sigmas are determined according to a sequence of noise levels {σi}.
use_exponential_sigmas (`bool`, *optional*, defaults to `False`):
Whether to use exponential sigmas for step sizes in the noise schedule during the sampling process.
timestep_spacing (`str`, defaults to `"linspace"`):
The way the timesteps should be scaled. Refer to Table 2 of the [Common Diffusion Noise Schedules and
Sample Steps are Flawed](https://huggingface.co/papers/2305.08891) for more information.
steps_offset (`int`, defaults to 0):
An offset added to the inference steps, as required by some model families.
final_sigmas_type (`str`, defaults to `"zero"`):
The final `sigma` value for the noise schedule during the sampling process. If `"sigma_min"`, the final
sigma is the same as the last sigma in the training schedule. If `zero`, the final sigma is set to 0.
"""
_compatibles = [e.name for e in KarrasDiffusionSchedulers]
order = 1
@register_to_config
def __init__(
self,
num_train_timesteps: int = 1000,
solver_order: int = 2,
prediction_type: str = "flow_prediction",
shift: float | None = 1.0,
use_dynamic_shifting=False,
thresholding: bool = False,
dynamic_thresholding_ratio: float = 0.995,
sample_max_value: float = 1.0,
predict_x0: bool = True,
solver_type: str = "bh2",
lower_order_final: bool = True,
disable_corrector: tuple = (),
solver_p: SchedulerMixin = None,
timestep_spacing: str = "linspace",
steps_offset: int = 0,
final_sigmas_type: str | None = "zero", # "zero", "sigma_min"
**kwargs,
):
if solver_type not in ["bh1", "bh2"]:
if solver_type in ["midpoint", "heun", "logrho"]:
self.register_to_config(solver_type="bh2")
else:
raise NotImplementedError(
f"{solver_type} is not implemented for {self.__class__}"
)
self.predict_x0 = predict_x0
# setable values
self.num_inference_steps: int | None = None
alphas = np.linspace(1, 1 / num_train_timesteps, num_train_timesteps)[
::-1
].copy()
sigmas = 1.0 - alphas
sigmas = torch.from_numpy(sigmas).to(dtype=torch.float32)
if not use_dynamic_shifting:
# when use_dynamic_shifting is True, we apply the timestep shifting on the fly based on the image resolution
assert shift is not None
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # pyright: ignore
self.sigmas = sigmas
self.sigma_min = self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
self.timesteps = sigmas * num_train_timesteps
self.num_train_timesteps = num_train_timesteps
self.model_outputs = [None] * solver_order
self.timestep_list: list[Any | None] = [None] * solver_order
self.lower_order_nums = 0
self.disable_corrector = list(disable_corrector)
self.solver_p = solver_p
self.last_sample = None
self._step_index: int | None = None
self._begin_index: int | None = None
BaseScheduler.__init__(self)
@property
def step_index(self):
"""
The index counter for current timestep. It will increase 1 after each scheduler step.
"""
return self._step_index
@property
def begin_index(self):
"""
The index for the first timestep. It should be set from pipeline with `set_begin_index` method.
"""
return self._begin_index
def set_shift(self, shift: float) -> None:
self.config.shift = shift
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.set_begin_index
def set_begin_index(self, begin_index: int = 0):
"""
Sets the begin index for the scheduler. This function should be run from pipeline before the inference.
Args:
begin_index (`int`):
The begin index for the scheduler.
"""
self._begin_index = begin_index
# Modified from diffusers.schedulers.scheduling_flow_match_euler_discrete.FlowMatchEulerDiscreteScheduler.set_timesteps
def set_timesteps(
self,
num_inference_steps: int | None = None,
device: str | torch.device = None,
sigmas: list[float] | None = None,
mu: float | None | None = None,
shift: float | None | None = None,
):
"""
Sets the discrete timesteps used for the diffusion chain (to be run before inference).
Args:
num_inference_steps (`int`):
Total number of the spacing of the time steps.
device (`str` or `torch.device`, *optional*):
The device to which the timesteps should be moved to. If `None`, the timesteps are not moved.
"""
if self.config.use_dynamic_shifting and mu is None:
raise ValueError(
" you have to pass a value for `mu` when `use_dynamic_shifting` is set to be `True`"
)
if sigmas is None:
assert num_inference_steps is not None
sigmas = np.linspace(
self.sigma_max, self.sigma_min, num_inference_steps + 1
).copy()[
:-1
] # pyright: ignore
if self.config.use_dynamic_shifting:
assert mu is not None
sigmas = self.time_shift(mu, 1.0, sigmas) # pyright: ignore
else:
if shift is None:
shift = self.config.shift
assert isinstance(sigmas, np.ndarray)
sigmas = shift * sigmas / (1 + (shift - 1) * sigmas) # pyright: ignore
if self.config.final_sigmas_type == "sigma_min":
sigma_last = sigmas[-1]
elif self.config.final_sigmas_type == "zero":
sigma_last = 0
else:
raise ValueError(
f"`final_sigmas_type` must be one of 'zero', or 'sigma_min', but got {self.config.final_sigmas_type}"
)
timesteps = sigmas * self.config.num_train_timesteps
sigmas = np.concatenate([sigmas, [sigma_last]]).astype(
np.float32
) # pyright: ignore
self.sigmas = torch.from_numpy(sigmas).to(device=device)
self.timesteps = torch.from_numpy(timesteps).to(
device=device, dtype=torch.int64
)
self.num_inference_steps = len(timesteps)
self.model_outputs = [
None,
] * self.config.solver_order
self.lower_order_nums = 0
self.last_sample = None
if self.solver_p:
self.solver_p.set_timesteps(self.num_inference_steps, device=device)
# add an index counter for schedulers that allow duplicated timesteps
self._step_index = None
self._begin_index = None
# Copied from diffusers.schedulers.scheduling_ddpm.DDPMScheduler._threshold_sample
def _threshold_sample(self, sample: torch.Tensor) -> torch.Tensor:
"""
"Dynamic thresholding: At each sampling step we set s to a certain percentile absolute pixel value in xt0 (the
prediction of x_0 at timestep t), and if s > 1, then we threshold xt0 to the range [-s, s] and then divide by
s. Dynamic thresholding pushes saturated pixels (those near -1 and 1) inwards, thereby actively preventing
pixels from saturation at each step. We find that dynamic thresholding results in significantly better
photorealism as well as better image-text alignment, especially when using very large guidance weights."
https://arxiv.org/abs/2205.11487
"""
dtype = sample.dtype
batch_size, channels, *remaining_dims = sample.shape
if dtype not in (torch.float32, torch.float64):
sample = (
sample.float()
) # upcast for quantile calculation, and clamp not implemented for cpu half
# Flatten sample for doing quantile calculation along each image
sample = sample.reshape(batch_size, channels * np.prod(remaining_dims))
abs_sample = sample.abs() # "a certain percentile absolute pixel value"
s = torch.quantile(abs_sample, self.config.dynamic_thresholding_ratio, dim=1)
s = torch.clamp(
s, min=1, max=self.config.sample_max_value
) # When clamped to min=1, equivalent to standard clipping to [-1, 1]
s = s.unsqueeze(1) # (batch_size, 1) because clamp will broadcast along dim=0
sample = (
torch.clamp(sample, -s, s) / s
) # "we threshold xt0 to the range [-s, s] and then divide by s"
sample = sample.reshape(batch_size, channels, *remaining_dims)
sample = sample.to(dtype)
return sample
def _sigma_to_alpha_sigma_t(self, sigma) -> tuple[Any, Any]:
return 1 - sigma, sigma
# Copied from diffusers.schedulers.scheduling_flow_match_euler_discrete.set_timesteps
def time_shift(self, mu: float, sigma: float, t: torch.Tensor):
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
def convert_model_output(
self,
model_output: torch.Tensor,
*args,
sample: torch.Tensor = None,
**kwargs,
) -> torch.Tensor:
r"""
Convert the model output to the corresponding type the UniPC algorithm needs.
Args:
model_output (`torch.Tensor`):
The direct output from the learned diffusion model.
timestep (`int`):
The current discrete timestep in the diffusion chain.
sample (`torch.Tensor`):
A current instance of a sample created by the diffusion process.
Returns:
`torch.Tensor`:
The converted model output.
"""
timestep = args[0] if len(args) > 0 else kwargs.pop("timestep", None)
if sample is None:
if len(args) > 1:
sample = args[1]
else:
raise ValueError("missing `sample` as a required keyword argument")
if timestep is not None:
deprecate(
"timesteps",
"1.0.0",
"Passing `timesteps` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
)
if self.predict_x0:
if self.config.prediction_type == "flow_prediction":
sigma_t = self.sigmas[self.step_index]
x0_pred = sample - sigma_t * model_output
else:
raise ValueError(
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
)
if self.config.thresholding:
x0_pred = self._threshold_sample(x0_pred)
return x0_pred
else:
if self.config.prediction_type == "flow_prediction":
sigma_t = self.sigmas[self.step_index]
epsilon = sample - (1 - sigma_t) * model_output
else:
raise ValueError(
f"prediction_type given as {self.config.prediction_type} must be one of `epsilon`, `sample`,"
" `v_prediction` or `flow_prediction` for the UniPCMultistepScheduler."
)
if self.config.thresholding:
sigma_t = self.sigmas[self.step_index]
x0_pred = sample - sigma_t * model_output
x0_pred = self._threshold_sample(x0_pred)
epsilon = model_output + x0_pred
return epsilon
def multistep_uni_p_bh_update(
self,
model_output: torch.Tensor,
*args,
sample: torch.Tensor = None,
order: int | None = None, # pyright: ignore
**kwargs,
) -> torch.Tensor:
"""
One step for the UniP (B(h) version). Alternatively, `self.solver_p` is used if is specified.
Args:
model_output (`torch.Tensor`):
The direct output from the learned diffusion model at the current timestep.
prev_timestep (`int`):
The previous discrete timestep in the diffusion chain.
sample (`torch.Tensor`):
A current instance of a sample created by the diffusion process.
order (`int`):
The order of UniP at this timestep (corresponds to the *p* in UniPC-p).
Returns:
`torch.Tensor`:
The sample tensor at the previous timestep.
"""
prev_timestep = args[0] if len(args) > 0 else kwargs.pop("prev_timestep", None)
if sample is None:
if len(args) > 1:
sample = args[1]
else:
raise ValueError(" missing `sample` as a required keyword argument")
if order is None:
if len(args) > 2:
order = args[2]
else:
raise ValueError(" missing `order` as a required keyword argument")
if prev_timestep is not None:
deprecate(
"prev_timestep",
"1.0.0",
"Passing `prev_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
)
model_output_list = self.model_outputs
s0 = self.timestep_list[-1]
m0 = model_output_list[-1]
x = sample
if self.solver_p:
x_t = self.solver_p.step(model_output, s0, x).prev_sample
return x_t
sigma_t, sigma_s0 = (
self.sigmas[self.step_index + 1],
self.sigmas[self.step_index],
) # pyright: ignore
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
h = lambda_t - lambda_s0
device = sample.device
rks = []
D1s: list[Any] | None = []
sigmas = self.sigmas.to(device=device)
for i in range(1, order):
si = self.step_index - i # pyright: ignore
mi = model_output_list[-(i + 1)]
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(sigmas[si])
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
rk = (lambda_si - lambda_s0) / h
rks.append(rk)
assert mi is not None
D1s.append((mi - m0) / rk) # pyright: ignore
if len(rks) > 0:
rks = torch.stack(rks)
one = torch.ones(1, device=device, dtype=rks.dtype)
rks = torch.cat([rks, one])
else:
rks = torch.ones(1, device=device, dtype=h.dtype)
R = []
b = []
hh = -h if self.predict_x0 else h
h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
h_phi_k = h_phi_1 / hh - 1
factorial_i = 1
if self.config.solver_type == "bh1":
B_h = hh
elif self.config.solver_type == "bh2":
B_h = torch.expm1(hh)
else:
raise NotImplementedError()
for i in range(1, order + 1):
R.append(torch.pow(rks, i - 1))
b.append(h_phi_k * factorial_i / B_h)
factorial_i *= i + 1
h_phi_k = h_phi_k / hh - 1 / factorial_i
R = torch.stack(R)
b = torch.stack(b)
if D1s is not None and len(D1s) > 0:
D1s = torch.stack(D1s, dim=1) # (B, K)
# for order 2, we use a simplified version
if order == 2:
rhos_p = 0.5 * torch.ones(1, dtype=x.dtype, device=device)
else:
assert isinstance(R, torch.Tensor)
rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1]).to(device).to(x.dtype)
else:
D1s = None
if self.predict_x0:
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
if D1s is not None:
pred_res = torch.einsum(
"k,bkc...->bc...", rhos_p, D1s
) # pyright: ignore
else:
pred_res = 0
x_t = x_t_ - alpha_t * B_h * pred_res
else:
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
if D1s is not None:
pred_res = torch.einsum(
"k,bkc...->bc...", rhos_p, D1s
) # pyright: ignore
else:
pred_res = 0
x_t = x_t_ - sigma_t * B_h * pred_res
x_t = x_t.to(x.dtype)
return x_t
def multistep_uni_c_bh_update(
self,
this_model_output: torch.Tensor,
*args,
last_sample: torch.Tensor = None,
this_sample: torch.Tensor = None,
order: int | None = None, # pyright: ignore
**kwargs,
) -> torch.Tensor:
"""
One step for the UniC (B(h) version).
Args:
this_model_output (`torch.Tensor`):
The model outputs at `x_t`.
this_timestep (`int`):
The current timestep `t`.
last_sample (`torch.Tensor`):
The generated sample before the last predictor `x_{t-1}`.
this_sample (`torch.Tensor`):
The generated sample after the last predictor `x_{t}`.
order (`int`):
The `p` of UniC-p at this step. The effective order of accuracy should be `order + 1`.
Returns:
`torch.Tensor`:
The corrected sample tensor at the current timestep.
"""
this_timestep = args[0] if len(args) > 0 else kwargs.pop("this_timestep", None)
if last_sample is None:
if len(args) > 1:
last_sample = args[1]
else:
raise ValueError(" missing`last_sample` as a required keyword argument")
if this_sample is None:
if len(args) > 2:
this_sample = args[2]
else:
raise ValueError(" missing`this_sample` as a required keyword argument")
if order is None:
if len(args) > 3:
order = args[3]
else:
raise ValueError(" missing`order` as a required keyword argument")
if this_timestep is not None:
deprecate(
"this_timestep",
"1.0.0",
"Passing `this_timestep` is deprecated and has no effect as model output conversion is now handled via an internal counter `self.step_index`",
)
model_output_list = self.model_outputs
m0 = model_output_list[-1]
x = last_sample
x_t = this_sample
model_t = this_model_output
sigma_t, sigma_s0 = (
self.sigmas[self.step_index],
self.sigmas[self.step_index - 1],
) # pyright: ignore
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
h = lambda_t - lambda_s0
device = this_sample.device
# Build rks and D1s fully on device to avoid any host-device sync
# Fast paths for small orders (common cases: 1 or 2)
if order == 1:
rks = torch.ones(1, device=device, dtype=h.dtype)
D1s = None
elif order == 2:
# order == 2 -> only one historical point is used
si = self.step_index - 2 # i = 1
mi = model_output_list[-2]
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
rk = (lambda_si - lambda_s0) / h # 0-dim tensor on device
# rks = [rk, 1.0] but keep it on device without list->tensor sync
rks = torch.stack((rk, torch.ones_like(rk)))
assert mi is not None
# D1s shape: (B, K=1, C, ...) to match later einsum over K
D1s = ((mi - m0) / rk).unsqueeze(1) # pyright: ignore
else:
rks_list = []
D1s_list = []
for i in range(1, order):
si = self.step_index - (i + 1)
mi = model_output_list[-(i + 1)]
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
rk = (lambda_si - lambda_s0) / h
rks_list.append(rk)
assert mi is not None
D1s_list.append((mi - m0) / rk) # pyright: ignore
# Append 1.0 as a device tensor to rks
rks = torch.stack(rks_list + [torch.ones_like(rks_list[0])])
D1s = torch.stack(D1s_list, dim=1) if len(D1s_list) > 0 else None
R = []
b = []
hh = -h if self.predict_x0 else h
h_phi_1 = torch.expm1(hh) # h\phi_1(h) = e^h - 1
h_phi_k = h_phi_1 / hh - 1
factorial_i = 1
if self.config.solver_type == "bh1":
B_h = hh
elif self.config.solver_type == "bh2":
B_h = torch.expm1(hh)
else:
raise NotImplementedError()
for i in range(1, order + 1):
R.append(torch.pow(rks, i - 1))
b.append(h_phi_k * factorial_i / B_h)
factorial_i *= i + 1
h_phi_k = h_phi_k / hh - 1 / factorial_i
R = torch.stack(R)
# Avoid torch.tensor(list_of_gpu_scalars) which syncs to host
b = torch.stack(b)
# D1s is already prepared above for order==2; remains None for order==1
# for order 1, we use a simplified version
if order == 1:
rhos_c = 0.5 * torch.ones(1, dtype=x.dtype, device=device)
elif order == 2:
# Manually solve the 2x2 linear system to avoid device synchronization from torch.linalg.solve
# R = [[1, 1], [rk, 1]], where rk = rks[0]
rk = rks[0]
det = 1 - rk
# Using Cramer's rule to solve for rhos_c = [x0, x1]
# x0 = (b0 - b1) / det
# x1 = (b1 - rk * b0) / det
rhos_c_0 = (b[0] - b[1]) / det
rhos_c_1 = (b[1] - rk * b[0]) / det
rhos_c = torch.stack([rhos_c_0, rhos_c_1])
else:
rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype)
if self.predict_x0:
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
if D1s is not None:
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
else:
corr_res = 0
D1_t = model_t - m0
x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t)
else:
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
if D1s is not None:
corr_res = torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
else:
corr_res = 0
D1_t = model_t - m0
x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t)
x_t = x_t.to(x.dtype)
return x_t
def index_for_timestep(self, timestep, schedule_timesteps=None) -> int:
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
# The sigma index that is taken for the **very** first `step`
# is always the second index (or the last index if there is only 1)
# This way we can ensure we don't accidentally skip a sigma in
# case we start in the middle of the denoising schedule (e.g. for image-to-image)
pos = 1 if len(indices) > 1 else 0
step_index: int = indices[pos].item()
return step_index
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler._init_step_index
def _init_step_index(self, timestep) -> None:
"""
Initialize the step_index counter for the scheduler.
"""
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step(
self,
model_output: torch.Tensor,
timestep: int | torch.Tensor,
sample: torch.Tensor,
return_dict: bool = True,
generator=None,
) -> SchedulerOutput | tuple:
"""
Predict the sample from the previous timestep by reversing the SDE. This function propagates the sample with
the multistep UniPC.
Args:
model_output (`torch.Tensor`):
The direct output from learned diffusion model.
timestep (`int`):
The current discrete timestep in the diffusion chain.
sample (`torch.Tensor`):
A current instance of a sample created by the diffusion process.
return_dict (`bool`):
Whether or not to return a [`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`.
Returns:
[`~schedulers.scheduling_utils.SchedulerOutput`] or `tuple`:
If return_dict is `True`, [`~schedulers.scheduling_utils.SchedulerOutput`] is returned, otherwise a
tuple is returned where the first element is the sample tensor.
"""
if self.num_inference_steps is None:
raise ValueError(
"Number of inference steps is 'None', you need to call 'set_timesteps' after creating the scheduler"
)
if self.step_index is None:
self._init_step_index(timestep)
use_corrector = (
self.step_index > 0
and self.step_index - 1 not in self.disable_corrector
and self.last_sample is not None # pyright: ignore
)
sample = sample.to(model_output.device)
model_output_convert = self.convert_model_output(model_output, sample=sample)
if use_corrector:
sample = self.multistep_uni_c_bh_update(
this_model_output=model_output_convert,
last_sample=self.last_sample,
this_sample=sample,
order=self.this_order,
)
for i in range(self.config.solver_order - 1):
self.model_outputs[i] = self.model_outputs[i + 1]
self.timestep_list[i] = self.timestep_list[i + 1]
self.model_outputs[-1] = model_output_convert
self.timestep_list[-1] = timestep # pyright: ignore
if self.config.lower_order_final:
this_order = min(
self.config.solver_order, len(self.timesteps) - self.step_index
) # pyright: ignore
else:
this_order = self.config.solver_order
self.this_order: int = min(
this_order, self.lower_order_nums + 1
) # warmup for multistep
assert self.this_order > 0
self.last_sample = sample
prev_sample = self.multistep_uni_p_bh_update(
model_output=model_output, # pass the original non-converted model output, in case solver-p is used
sample=sample,
order=self.this_order,
)
if self.lower_order_nums < self.config.solver_order:
self.lower_order_nums += 1
# upon completion increase step index by one
assert self._step_index is not None
self._step_index += 1 # pyright: ignore
if not return_dict:
return (prev_sample,)
return SchedulerOutput(prev_sample=prev_sample)
def scale_model_input(self, sample: torch.Tensor, *args, **kwargs) -> torch.Tensor:
"""
Ensures interchangeability with schedulers that need to scale the denoising model input depending on the
current timestep.
Args:
sample (`torch.Tensor`):
The input sample.
Returns:
`torch.Tensor`:
A scaled input sample.
"""
return sample
# Copied from diffusers.schedulers.scheduling_dpmsolver_multistep.DPMSolverMultistepScheduler.add_noise
def add_noise(
self,
original_samples: torch.Tensor,
noise: torch.Tensor,
timesteps: torch.IntTensor,
) -> torch.Tensor:
# Make sure sigmas and timesteps have the same device and dtype as original_samples
sigmas = self.sigmas.to(
device=original_samples.device, dtype=original_samples.dtype
)
if original_samples.device.type == "mps" and torch.is_floating_point(timesteps):
# mps does not support float64
schedule_timesteps = self.timesteps.to(
original_samples.device, dtype=torch.float32
)
timesteps = timesteps.to(original_samples.device, dtype=torch.float32)
else:
schedule_timesteps = self.timesteps.to(original_samples.device)
timesteps = timesteps.to(original_samples.device)
# begin_index is None when the scheduler is used for training or pipeline does not implement set_begin_index
if self.begin_index is None:
step_indices = [
self.index_for_timestep(t, schedule_timesteps) for t in timesteps
]
elif self.step_index is not None:
# add_noise is called after first denoising step (for inpainting)
step_indices = [self.step_index] * timesteps.shape[0]
else:
# add noise is called before first denoising step to create initial latent(img2img)
step_indices = [self.begin_index] * timesteps.shape[0]
sigma = sigmas[step_indices].flatten()
while len(sigma.shape) < len(original_samples.shape):
sigma = sigma.unsqueeze(-1)
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
noisy_samples = alpha_t * original_samples + sigma_t * noise
return noisy_samples
EntryClass = FlowUniPCMultistepScheduler
@@ -0,0 +1,744 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from Helios diffusers scheduler:
# https://github.com/BestWishYsh/Helios
"""
Helios scheduler implementing flow-matching with UniPC/Euler solvers.
For Phase 1 T2V (stages=1), this simplifies to standard flow-matching
with dynamic shifting and UniPC multistep solver.
"""
import math
from dataclasses import dataclass
import numpy as np
import torch
from sglang.multimodal_gen.runtime.platforms import current_platform
@dataclass
class HeliosSchedulerOutput:
prev_sample: torch.FloatTensor
model_outputs: torch.FloatTensor | None = None
last_sample: torch.FloatTensor | None = None
this_order: int | None = None
class HeliosSchedulerConfig:
"""Mimics diffusers config interface for scheduler parameters."""
def __init__(self, **kwargs):
for k, v in kwargs.items():
setattr(self, k, v)
def get(self, key, default=None):
return getattr(self, key, default)
class HeliosScheduler:
"""
Helios multi-stage scheduler supporting Euler, UniPC, and DMD solvers.
For Phase 1 T2V with stages=1, this is a standard flow-matching scheduler
with optional time shifting and UniPC multistep updates.
"""
order = 1
def __init__(
self,
num_train_timesteps: int = 1000,
shift: float = 1.0,
stages: int = 1,
stage_range: list | None = None,
gamma: float = 1 / 3,
thresholding: bool = False,
prediction_type: str = "flow_prediction",
solver_order: int = 2,
predict_x0: bool = True,
solver_type: str = "bh2",
lower_order_final: bool = True,
disable_corrector: list[int] | None = None,
use_flow_sigmas: bool = True,
scheduler_type: str = "unipc",
use_dynamic_shifting: bool = False,
time_shift_type: str = "linear",
**kwargs,
):
if stage_range is None:
# Evenly divide [0, 1] into 3 stages for pyramid SR
stage_range = [0, 1 / 3, 2 / 3, 1]
if disable_corrector is None:
disable_corrector = []
self.config = HeliosSchedulerConfig(
num_train_timesteps=num_train_timesteps,
shift=shift,
stages=stages,
stage_range=stage_range,
gamma=gamma,
thresholding=thresholding,
prediction_type=prediction_type,
solver_order=solver_order,
predict_x0=predict_x0,
solver_type=solver_type,
lower_order_final=lower_order_final,
disable_corrector=disable_corrector,
use_flow_sigmas=use_flow_sigmas,
scheduler_type=scheduler_type,
use_dynamic_shifting=use_dynamic_shifting,
time_shift_type=time_shift_type,
)
self.timestep_ratios = {}
self.timesteps_per_stage = {}
self.sigmas_per_stage = {}
self.start_sigmas = {}
self.end_sigmas = {}
self.ori_start_sigmas = {}
self.init_sigmas_for_each_stage()
self.sigma_min = self.sigmas[-1].item()
self.sigma_max = self.sigmas[0].item()
self.gamma = gamma
if solver_type not in ["bh1", "bh2"]:
raise NotImplementedError(f"{solver_type} is not implemented")
self.predict_x0 = predict_x0
self.model_outputs = [None] * solver_order
self.timestep_list = [None] * solver_order
self.lower_order_nums = 0
self.disable_corrector = disable_corrector
self.solver_p = None
self.last_sample = None
self._step_index = None
self._begin_index = None
self.num_inference_steps = None
def init_sigmas(self):
num_train_timesteps = self.config.num_train_timesteps
shift = self.config.shift
alphas = np.linspace(1, 1 / num_train_timesteps, num_train_timesteps + 1)
sigmas = 1.0 - alphas
sigmas = np.flip(shift * sigmas / (1 + (shift - 1) * sigmas))[:-1].copy()
sigmas = torch.from_numpy(sigmas)
timesteps = (sigmas * num_train_timesteps).clone()
self._step_index = None
self._begin_index = None
self.timesteps = timesteps
self.sigmas = sigmas.to("cpu")
def init_sigmas_for_each_stage(self):
self.init_sigmas()
stage_distance = []
stages = self.config.stages
training_steps = self.config.num_train_timesteps
stage_range = self.config.stage_range
for i_s in range(stages):
start_indice = int(stage_range[i_s] * training_steps)
start_indice = max(start_indice, 0)
end_indice = int(stage_range[i_s + 1] * training_steps)
end_indice = min(end_indice, training_steps)
start_sigma = self.sigmas[start_indice].item()
end_sigma = (
self.sigmas[end_indice].item() if end_indice < training_steps else 0.0
)
self.ori_start_sigmas[i_s] = start_sigma
if i_s != 0:
ori_sigma = 1 - start_sigma
gamma = self.config.gamma
corrected_sigma = (
1 / (math.sqrt(1 + (1 / gamma)) * (1 - ori_sigma) + ori_sigma)
) * ori_sigma
start_sigma = 1 - corrected_sigma
stage_distance.append(start_sigma - end_sigma)
self.start_sigmas[i_s] = start_sigma
self.end_sigmas[i_s] = end_sigma
tot_distance = sum(stage_distance)
for i_s in range(stages):
if i_s == 0:
start_ratio = 0.0
else:
start_ratio = sum(stage_distance[:i_s]) / tot_distance
if i_s == stages - 1:
# Use value just below 1.0 to avoid out-of-bounds indexing
end_ratio = 1.0 - 1e-16
else:
end_ratio = sum(stage_distance[: i_s + 1]) / tot_distance
self.timestep_ratios[i_s] = (start_ratio, end_ratio)
for i_s in range(stages):
timestep_ratio = self.timestep_ratios[i_s]
# Clamp to max valid timestep (num_train_timesteps - 1)
timestep_max = min(
self.timesteps[int(timestep_ratio[0] * training_steps)], 999
)
timestep_min = self.timesteps[
min(int(timestep_ratio[1] * training_steps), training_steps - 1)
]
timesteps = np.linspace(timestep_max, timestep_min, training_steps + 1)
self.timesteps_per_stage[i_s] = (
timesteps[:-1]
if isinstance(timesteps, torch.Tensor)
else torch.from_numpy(timesteps[:-1])
)
# Sigma range [0.999, 0]: start just below 1.0 to avoid singularity
stage_sigmas = np.linspace(0.999, 0, training_steps + 1)
self.sigmas_per_stage[i_s] = torch.from_numpy(stage_sigmas[:-1])
@property
def step_index(self):
return self._step_index
@property
def begin_index(self):
return self._begin_index
def set_begin_index(self, begin_index: int = 0):
self._begin_index = begin_index
def time_shift(self, mu, sigma, t):
if self.config.time_shift_type == "exponential":
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
elif self.config.time_shift_type == "linear":
return mu / (mu + (1 / t - 1) ** sigma)
def set_timesteps(
self,
num_inference_steps: int,
stage_index: int | None = None,
device: str | torch.device = None,
sigmas=None,
mu=None,
is_amplify_first_chunk: bool = False,
):
if self.config.scheduler_type == "dmd":
if is_amplify_first_chunk:
num_inference_steps = num_inference_steps * 2 + 1
else:
num_inference_steps = num_inference_steps + 1
self.num_inference_steps = num_inference_steps
self.init_sigmas()
if self.config.stages == 1:
if sigmas is None:
sigmas = np.linspace(
1,
1 / self.config.num_train_timesteps,
num_inference_steps + 1,
)[:-1].astype(np.float32)
if self.config.shift != 1.0:
assert not self.config.use_dynamic_shifting
sigmas = self.time_shift(self.config.shift, 1.0, sigmas)
timesteps = (sigmas * self.config.num_train_timesteps).copy()
sigmas = torch.from_numpy(sigmas)
else:
stage_timesteps = self.timesteps_per_stage[stage_index]
timesteps = np.linspace(
stage_timesteps[0].item(),
stage_timesteps[-1].item(),
num_inference_steps,
)
stage_sigmas = self.sigmas_per_stage[stage_index]
ratios = np.linspace(
stage_sigmas[0].item(), stage_sigmas[-1].item(), num_inference_steps
)
sigmas = torch.from_numpy(ratios)
self.timesteps = torch.from_numpy(timesteps).to(device=device)
self.sigmas = torch.cat([sigmas, torch.zeros(1)]).to(device=device)
if current_platform.is_npu():
# self.sigmas is float64 (np.linspace default); Ascend aclnnExpm1 does
# not support float64 (DT_DOUBLE) and crashes the UniPC step's expm1.
# Pin fp32 on NPU; remove once aclnnExpm1 supports float64.
self.sigmas = self.sigmas.to(torch.float32)
self._step_index = None
self.reset_scheduler_history()
if self.config.scheduler_type == "dmd":
self.timesteps = self.timesteps[:-1]
self.sigmas = torch.cat([self.sigmas[:-2], self.sigmas[-1:]])
if self.config.use_dynamic_shifting:
assert self.config.shift == 1.0
self.sigmas = self.time_shift(mu, 1.0, self.sigmas)
if self.config.stages == 1:
self.timesteps = self.sigmas[:-1] * self.config.num_train_timesteps
else:
self.timesteps = self.timesteps_per_stage[
stage_index
].min() + self.sigmas[:-1] * (
self.timesteps_per_stage[stage_index].max()
- self.timesteps_per_stage[stage_index].min()
)
# ---------------------------------- Euler ----------------------------------
def index_for_timestep(self, timestep, schedule_timesteps=None):
if schedule_timesteps is None:
schedule_timesteps = self.timesteps
indices = (schedule_timesteps == timestep).nonzero()
pos = 1 if len(indices) > 1 else 0
return indices[pos].item()
def _init_step_index(self, timestep):
if self.begin_index is None:
if isinstance(timestep, torch.Tensor):
timestep = timestep.to(self.timesteps.device)
self._step_index = self.index_for_timestep(timestep)
else:
self._step_index = self._begin_index
def step_euler(
self,
model_output: torch.FloatTensor,
timestep=None,
sample: torch.FloatTensor = None,
return_dict: bool = True,
**kwargs,
) -> HeliosSchedulerOutput | tuple:
if self.step_index is None:
self._step_index = 0
sample = sample.to(torch.float32)
sigma = self.sigmas[self.step_index]
sigma_next = self.sigmas[self.step_index + 1]
prev_sample = sample + (sigma_next - sigma) * model_output
prev_sample = prev_sample.to(model_output.dtype)
self._step_index += 1
if not return_dict:
return (prev_sample,)
return HeliosSchedulerOutput(prev_sample=prev_sample)
# ---------------------------------- UniPC ----------------------------------
def _sigma_to_alpha_sigma_t(self, sigma):
if self.config.use_flow_sigmas:
alpha_t = 1 - sigma
sigma_t = torch.clamp(sigma, min=1e-8)
else:
alpha_t = 1 / ((sigma**2 + 1) ** 0.5)
sigma_t = sigma * alpha_t
return alpha_t, sigma_t
def convert_model_output(self, model_output, sample=None, sigma=None, **kwargs):
flag = False
if sigma is None:
flag = True
sigma = self.sigmas[self.step_index]
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma)
if self.predict_x0:
if self.config.prediction_type == "flow_prediction":
if flag:
sigma_t = self.sigmas[self.step_index]
else:
sigma_t = sigma
x0_pred = sample - sigma_t * model_output
elif self.config.prediction_type == "epsilon":
x0_pred = (sample - sigma_t * model_output) / alpha_t
elif self.config.prediction_type == "sample":
x0_pred = model_output
elif self.config.prediction_type == "v_prediction":
x0_pred = alpha_t * sample - sigma_t * model_output
else:
raise ValueError(
f"prediction_type {self.config.prediction_type} not supported"
)
return x0_pred
else:
if self.config.prediction_type == "epsilon":
return model_output
elif self.config.prediction_type == "sample":
return (sample - alpha_t * model_output) / sigma_t
elif self.config.prediction_type == "v_prediction":
return alpha_t * model_output + sigma_t * sample
else:
raise ValueError(
f"prediction_type {self.config.prediction_type} not supported"
)
def multistep_uni_p_bh_update(
self, model_output, sample=None, order=None, sigma=None, sigma_next=None
):
model_output_list = self.model_outputs
m0 = model_output_list[-1]
x = sample
if sigma_next is None and sigma is None:
sigma_t, sigma_s0 = (
self.sigmas[self.step_index + 1],
self.sigmas[self.step_index],
)
else:
sigma_t, sigma_s0 = sigma_next, sigma
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
h = lambda_t - lambda_s0
device = sample.device
rks = []
D1s = []
for i in range(1, order):
si = self.step_index - i
mi = model_output_list[-(i + 1)]
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
rk = (lambda_si - lambda_s0) / h
rks.append(rk)
D1s.append((mi - m0) / rk)
rks.append(1.0)
rks = torch.tensor(rks, device=device)
R = []
b = []
hh = -h if self.predict_x0 else h
h_phi_1 = torch.expm1(hh)
h_phi_k = h_phi_1 / hh - 1
factorial_i = 1
if self.config.solver_type == "bh1":
B_h = hh
elif self.config.solver_type == "bh2":
B_h = torch.expm1(hh)
else:
raise NotImplementedError()
for i in range(1, order + 1):
R.append(torch.pow(rks, i - 1))
b.append(h_phi_k * factorial_i / B_h)
factorial_i *= i + 1
h_phi_k = h_phi_k / hh - 1 / factorial_i
R = torch.stack(R)
b = torch.tensor(b, device=device)
if len(D1s) > 0:
D1s = torch.stack(D1s, dim=1)
if order == 2:
rhos_p = torch.tensor([0.5], dtype=x.dtype, device=device)
else:
rhos_p = torch.linalg.solve(R[:-1, :-1], b[:-1]).to(device).to(x.dtype)
else:
D1s = None
if self.predict_x0:
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
pred_res = (
torch.einsum("k,bkc...->bc...", rhos_p, D1s) if D1s is not None else 0
)
x_t = x_t_ - alpha_t * B_h * pred_res
else:
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
pred_res = (
torch.einsum("k,bkc...->bc...", rhos_p, D1s) if D1s is not None else 0
)
x_t = x_t_ - sigma_t * B_h * pred_res
return x_t.to(x.dtype)
def multistep_uni_c_bh_update(
self,
this_model_output,
last_sample=None,
this_sample=None,
order=None,
sigma_before=None,
sigma=None,
):
model_output_list = self.model_outputs
m0 = model_output_list[-1]
x = last_sample
model_t = this_model_output
if sigma_before is None and sigma is None:
sigma_t, sigma_s0 = (
self.sigmas[self.step_index],
self.sigmas[self.step_index - 1],
)
else:
sigma_t, sigma_s0 = sigma, sigma_before
alpha_t, sigma_t = self._sigma_to_alpha_sigma_t(sigma_t)
alpha_s0, sigma_s0 = self._sigma_to_alpha_sigma_t(sigma_s0)
lambda_t = torch.log(alpha_t) - torch.log(sigma_t)
lambda_s0 = torch.log(alpha_s0) - torch.log(sigma_s0)
h = lambda_t - lambda_s0
device = this_sample.device
rks = []
D1s = []
for i in range(1, order):
si = self.step_index - (i + 1)
mi = model_output_list[-(i + 1)]
alpha_si, sigma_si = self._sigma_to_alpha_sigma_t(self.sigmas[si])
lambda_si = torch.log(alpha_si) - torch.log(sigma_si)
rk = (lambda_si - lambda_s0) / h
rks.append(rk)
D1s.append((mi - m0) / rk)
rks.append(1.0)
rks = torch.tensor(rks, device=device)
R = []
b = []
hh = -h if self.predict_x0 else h
h_phi_1 = torch.expm1(hh)
h_phi_k = h_phi_1 / hh - 1
factorial_i = 1
if self.config.solver_type == "bh1":
B_h = hh
elif self.config.solver_type == "bh2":
B_h = torch.expm1(hh)
else:
raise NotImplementedError()
for i in range(1, order + 1):
R.append(torch.pow(rks, i - 1))
b.append(h_phi_k * factorial_i / B_h)
factorial_i *= i + 1
h_phi_k = h_phi_k / hh - 1 / factorial_i
R = torch.stack(R)
b = torch.tensor(b, device=device)
if len(D1s) > 0:
D1s = torch.stack(D1s, dim=1)
else:
D1s = None
if order == 1:
rhos_c = torch.tensor([0.5], dtype=x.dtype, device=device)
else:
rhos_c = torch.linalg.solve(R, b).to(device).to(x.dtype)
if self.predict_x0:
x_t_ = sigma_t / sigma_s0 * x - alpha_t * h_phi_1 * m0
corr_res = (
torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
if D1s is not None
else 0
)
D1_t = model_t - m0
x_t = x_t_ - alpha_t * B_h * (corr_res + rhos_c[-1] * D1_t)
else:
x_t_ = alpha_t / alpha_s0 * x - sigma_t * h_phi_1 * m0
corr_res = (
torch.einsum("k,bkc...->bc...", rhos_c[:-1], D1s)
if D1s is not None
else 0
)
D1_t = model_t - m0
x_t = x_t_ - sigma_t * B_h * (corr_res + rhos_c[-1] * D1_t)
return x_t.to(x.dtype)
def step_unipc(
self,
model_output,
timestep=None,
sample=None,
return_dict: bool = True,
**kwargs,
) -> HeliosSchedulerOutput | tuple:
if self.num_inference_steps is None:
raise ValueError(
"Number of inference steps is 'None', run 'set_timesteps' first"
)
if self.step_index is None:
self._step_index = 0
use_corrector = (
self.step_index > 0
and self.step_index - 1 not in self.disable_corrector
and self.last_sample is not None
)
model_output_convert = self.convert_model_output(model_output, sample=sample)
if use_corrector:
sample = self.multistep_uni_c_bh_update(
this_model_output=model_output_convert,
last_sample=self.last_sample,
this_sample=sample,
order=self.this_order,
)
for i in range(self.config.solver_order - 1):
self.model_outputs[i] = self.model_outputs[i + 1]
self.timestep_list[i] = self.timestep_list[i + 1]
self.model_outputs[-1] = model_output_convert
self.timestep_list[-1] = timestep
if self.config.lower_order_final:
this_order = min(
self.config.solver_order, len(self.timesteps) - self.step_index
)
else:
this_order = self.config.solver_order
self.this_order = min(this_order, self.lower_order_nums + 1)
assert self.this_order > 0
self.last_sample = sample
prev_sample = self.multistep_uni_p_bh_update(
model_output=model_output,
sample=sample,
order=self.this_order,
)
if self.lower_order_nums < self.config.solver_order:
self.lower_order_nums += 1
self._step_index += 1
if not return_dict:
return (prev_sample,)
return HeliosSchedulerOutput(prev_sample=prev_sample)
# ---------------------------------- DMD ----------------------------------
def add_noise(self, original_samples, noise, timestep, sigmas, timesteps):
sigmas = sigmas.to(noise.device)
timesteps = timesteps.to(noise.device)
timestep_id = torch.argmin(
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma = sigmas[timestep_id].reshape(-1, 1, 1, 1, 1)
sample = (1 - sigma) * original_samples + sigma * noise
return sample.type_as(noise)
def convert_flow_pred_to_x0(self, flow_pred, xt, timestep, sigmas, timesteps):
original_dtype = flow_pred.dtype
device = flow_pred.device
flow_pred, xt, sigmas, timesteps = (
x.double().to(device) for x in (flow_pred, xt, sigmas, timesteps)
)
timestep_id = torch.argmin(
(timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma_t = sigmas[timestep_id].reshape(-1, 1, 1, 1, 1)
x0_pred = xt - sigma_t * flow_pred
return x0_pred.to(original_dtype)
def step_dmd(
self,
model_output: torch.FloatTensor,
timestep=None,
sample: torch.FloatTensor = None,
return_dict: bool = True,
cur_sampling_step: int = 0,
dmd_noisy_tensor: torch.FloatTensor | None = None,
dmd_sigmas: torch.FloatTensor | None = None,
dmd_timesteps: torch.FloatTensor | None = None,
all_timesteps: torch.FloatTensor | None = None,
**kwargs,
) -> HeliosSchedulerOutput | tuple:
pred_image_or_video = self.convert_flow_pred_to_x0(
flow_pred=model_output,
xt=sample,
timestep=torch.full(
(model_output.shape[0],),
timestep,
dtype=torch.long,
device=model_output.device,
),
sigmas=dmd_sigmas,
timesteps=dmd_timesteps,
)
if cur_sampling_step < len(all_timesteps) - 1:
prev_sample = self.add_noise(
pred_image_or_video,
dmd_noisy_tensor,
torch.full(
(model_output.shape[0],),
all_timesteps[cur_sampling_step + 1],
dtype=torch.long,
device=model_output.device,
),
sigmas=dmd_sigmas,
timesteps=dmd_timesteps,
)
else:
prev_sample = pred_image_or_video
if not return_dict:
return (prev_sample,)
return HeliosSchedulerOutput(prev_sample=prev_sample)
# ---------------------------------- Main step ----------------------------------
def step(
self,
model_output,
timestep=None,
sample=None,
return_dict: bool = True,
**kwargs,
) -> HeliosSchedulerOutput | tuple:
if self.config.scheduler_type == "euler":
return self.step_euler(
model_output=model_output,
timestep=timestep,
sample=sample,
return_dict=return_dict,
)
elif self.config.scheduler_type == "unipc":
return self.step_unipc(
model_output=model_output,
timestep=timestep,
sample=sample,
return_dict=return_dict,
)
elif self.config.scheduler_type == "dmd":
return self.step_dmd(
model_output=model_output,
timestep=timestep,
sample=sample,
return_dict=return_dict,
**kwargs,
)
else:
raise NotImplementedError(
f"Scheduler type '{self.config.scheduler_type}' not implemented"
)
def reset_scheduler_history(self):
self.model_outputs = [None] * self.config.solver_order
self.timestep_list = [None] * self.config.solver_order
self.lower_order_nums = 0
self.disable_corrector = self.config.disable_corrector
self.solver_p = None
self.last_sample = None
self._step_index = None
self._begin_index = None
def set_shift(self, shift: float):
"""Update the shift parameter (called by SchedulerLoader after loading)."""
self.config.shift = shift
self.shift = shift
def __len__(self):
return self.config.num_train_timesteps
# Alias for Helios-Distilled which uses "HeliosDMDScheduler" in scheduler_config.json
HeliosDMDScheduler = HeliosScheduler
EntryClass = [HeliosScheduler, "HeliosDMDScheduler"]
@@ -0,0 +1,142 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
import torch
from diffusers.configuration_utils import ConfigMixin, register_to_config
from diffusers.schedulers.scheduling_utils import SchedulerMixin
from diffusers.utils import BaseOutput
from sglang.multimodal_gen.runtime.models.schedulers.base import BaseScheduler
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class SelfForcingFlowMatchSchedulerOutput(BaseOutput):
"""
Output class for the scheduler's `step` function output.
Args:
prev_sample (`torch.FloatTensor` of shape `(batch_size, num_channels, height, width)` for images):
Computed sample `(x_{t-1})` of previous timestep. `prev_sample` should be used as next model input in the
denoising loop.
"""
prev_sample: torch.FloatTensor
class SelfForcingFlowMatchScheduler(BaseScheduler, ConfigMixin, SchedulerMixin):
config_name = "scheduler_config.json"
order = 1
@register_to_config
def __init__(
self,
num_inference_steps=100,
num_train_timesteps=1000,
shift=3.0,
sigma_max=1.0,
sigma_min=0.003 / 1.002,
inverse_timesteps=False,
extra_one_step=False,
reverse_sigmas=False,
*args,
**kwargs,
):
self.num_train_timesteps = num_train_timesteps
self.shift = shift
self.sigma_max = sigma_max
self.sigma_min = sigma_min
self.inverse_timesteps = inverse_timesteps
self.extra_one_step = extra_one_step
self.reverse_sigmas = reverse_sigmas
self.set_timesteps(num_inference_steps)
def set_timesteps(
self,
num_inference_steps=100,
denoising_strength=1.0,
return_dict=False,
**kwargs,
):
sigma_start = (
self.sigma_min + (self.sigma_max - self.sigma_min) * denoising_strength
)
if self.extra_one_step:
self.sigmas = torch.linspace(
sigma_start, self.sigma_min, num_inference_steps + 1
)[:-1]
else:
self.sigmas = torch.linspace(
sigma_start, self.sigma_min, num_inference_steps
)
if self.inverse_timesteps:
self.sigmas = torch.flip(self.sigmas, dims=[0])
self.sigmas = self.shift * self.sigmas / (1 + (self.shift - 1) * self.sigmas)
if self.reverse_sigmas:
self.sigmas = 1 - self.sigmas
self.timesteps = self.sigmas * self.num_train_timesteps
def step(
self,
model_output: torch.FloatTensor,
timestep: torch.FloatTensor,
sample: torch.FloatTensor,
to_final=False,
return_dict=False,
**kwargs,
):
if timestep.ndim == 2:
timestep = timestep.flatten(0, 1)
elif timestep.ndim == 0:
# handles the case where timestep is a scalar, this occurs when we
# use this scheduler for ODE trajectory
timestep = timestep.unsqueeze(0)
self.sigmas = self.sigmas.to(model_output.device)
self.timesteps = self.timesteps.to(model_output.device)
timestep = timestep.to(model_output.device)
timestep_id = torch.argmin(
(self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
if to_final or (timestep_id + 1 >= len(self.timesteps)).any():
sigma_ = 1 if (self.inverse_timesteps or self.reverse_sigmas) else 0
else:
sigma_ = self.sigmas[timestep_id + 1].reshape(-1, 1, 1, 1)
prev_sample = sample + model_output * (sigma_ - sigma)
if isinstance(prev_sample, torch.Tensor | float) and not return_dict:
return (prev_sample,)
return SelfForcingFlowMatchSchedulerOutput(prev_sample=prev_sample)
def add_noise(self, original_samples, noise, timestep):
"""
Diffusion forward corruption process.
Input:
- clean_latent: the clean latent with shape [B*T, C, H, W]
- noise: the noise with shape [B*T, C, H, W]
- timestep: the timestep with shape [B*T]
Output: the corrupted latent with shape [B*T, C, H, W]
"""
if timestep.ndim == 2:
timestep = timestep.flatten(0, 1)
self.sigmas = self.sigmas.to(noise.device)
self.timesteps = self.timesteps.to(noise.device)
timestep_id = torch.argmin(
(self.timesteps.unsqueeze(0) - timestep.unsqueeze(1)).abs(), dim=1
)
sigma = self.sigmas[timestep_id].reshape(-1, 1, 1, 1)
sample = (1 - sigma) * original_samples + sigma * noise
return sample.type_as(noise)
def scale_model_input(
self, sample: torch.Tensor, timestep: int | None = None
) -> torch.Tensor:
return sample
def set_shift(self, shift: float) -> None:
self.shift = shift
EntryClass = SelfForcingFlowMatchScheduler
@@ -0,0 +1,5 @@
from sglang.multimodal_gen.runtime.models.upsampler.latent_upsampler import (
LatentUpsampler,
)
__all__ = ["LatentUpsampler"]
@@ -0,0 +1,277 @@
# Ported from https://github.com/Lightricks/LTX-2
# SPDX-License-Identifier: Apache-2.0
import math
from typing import Optional, Tuple
import torch
import torch.nn.functional as F
from einops import rearrange
from sglang.jit_kernel.diffusion.group_norm_silu import apply_group_norm_silu
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
class BlurDownsample(torch.nn.Module):
"""Anti-aliased spatial downsampling by integer stride using a fixed separable binomial kernel."""
def __init__(self, dims: int, stride: int, kernel_size: int = 5) -> None:
super().__init__()
assert dims in (2, 3)
assert isinstance(stride, int) and stride >= 1
assert kernel_size >= 3 and kernel_size % 2 == 1
self.dims = dims
self.stride = stride
self.kernel_size = kernel_size
k = torch.tensor([math.comb(kernel_size - 1, i) for i in range(kernel_size)])
k2d = k[:, None] @ k[None, :]
k2d = (k2d / k2d.sum()).float()
self.register_buffer("kernel", k2d[None, None, :, :])
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.stride == 1:
return x
if self.dims == 2:
return self._apply_2d(x)
b, _, f, _, _ = x.shape
x = rearrange(x, "b c f h w -> (b f) c h w")
x = self._apply_2d(x)
h2, w2 = x.shape[-2:]
x = rearrange(x, "(b f) c h w -> b c f h w", b=b, f=f, h=h2, w=w2)
return x
def _apply_2d(self, x2d: torch.Tensor) -> torch.Tensor:
c = x2d.shape[1]
weight = self.kernel.expand(c, 1, self.kernel_size, self.kernel_size)
x2d = F.conv2d(
x2d,
weight=weight,
bias=None,
stride=self.stride,
padding=self.kernel_size // 2,
groups=c,
)
return x2d
class PixelShuffleND(torch.nn.Module):
"""N-dimensional pixel shuffle for upsampling tensors."""
def __init__(self, dims: int, upscale_factors: Tuple[int, int, int] = (2, 2, 2)):
super().__init__()
assert dims in [1, 2, 3]
self.dims = dims
self.upscale_factors = upscale_factors
def forward(self, x: torch.Tensor) -> torch.Tensor:
if self.dims == 3:
return rearrange(
x,
"b (c p1 p2 p3) d h w -> b c (d p1) (h p2) (w p3)",
p1=self.upscale_factors[0],
p2=self.upscale_factors[1],
p3=self.upscale_factors[2],
)
elif self.dims == 2:
return rearrange(
x,
"b (c p1 p2) h w -> b c (h p1) (w p2)",
p1=self.upscale_factors[0],
p2=self.upscale_factors[1],
)
elif self.dims == 1:
return rearrange(
x,
"b (c p1) f h w -> b c (f p1) h w",
p1=self.upscale_factors[0],
)
else:
raise ValueError(f"Unsupported dims: {self.dims}")
class ResBlock(torch.nn.Module):
"""Residual block with two conv layers, group norm, and SiLU activation."""
def __init__(
self, channels: int, mid_channels: Optional[int] = None, dims: int = 3
):
super().__init__()
if mid_channels is None:
mid_channels = channels
conv = torch.nn.Conv2d if dims == 2 else torch.nn.Conv3d
self.conv1 = conv(channels, mid_channels, kernel_size=3, padding=1)
self.norm1 = torch.nn.GroupNorm(32, mid_channels)
self.conv2 = conv(mid_channels, channels, kernel_size=3, padding=1)
self.norm2 = torch.nn.GroupNorm(32, channels)
self.activation = torch.nn.SiLU()
def forward(self, x: torch.Tensor) -> torch.Tensor:
residual = x
x = self.conv1(x)
# Fused GroupNorm + SiLU on the first norm of the block. The second
# norm (line below) is followed by `silu(norm + residual)`, which the
# current `apply_group_norm_silu` helper does not cover -- left on
# the eager path until a `group_norm_add_silu` helper exists.
x = apply_group_norm_silu(x, self.norm1, self.activation)
x = self.conv2(x)
x = self.norm2(x)
x = self.activation(x + residual)
return x
def _rational_for_scale(scale: float) -> Tuple[int, int]:
mapping = {0.75: (3, 4), 1.5: (3, 2), 2.0: (2, 1), 4.0: (4, 1)}
if float(scale) not in mapping:
raise ValueError(
f"Unsupported scale {scale}. Choose from {list(mapping.keys())}"
)
return mapping[float(scale)]
class SpatialRationalResampler(torch.nn.Module):
"""Fully-learned rational spatial scaling via PixelShuffle + anti-aliased downsample."""
def __init__(self, mid_channels: int, scale: float):
super().__init__()
self.scale = float(scale)
self.num, self.den = _rational_for_scale(self.scale)
self.conv = torch.nn.Conv2d(
mid_channels, (self.num**2) * mid_channels, kernel_size=3, padding=1
)
self.pixel_shuffle = PixelShuffleND(2, upscale_factors=(self.num, self.num))
self.blur_down = BlurDownsample(dims=2, stride=self.den)
def forward(self, x: torch.Tensor) -> torch.Tensor:
b, _, f, _, _ = x.shape
x = rearrange(x, "b c f h w -> (b f) c h w")
x = self.conv(x)
x = self.pixel_shuffle(x)
x = self.blur_down(x)
x = rearrange(x, "(b f) c h w -> b c f h w", b=b, f=f)
return x
class LatentUpsampler(torch.nn.Module, LayerwiseOffloadableModuleMixin):
"""
Upsample VAE latents spatially and/or temporally.
Args:
in_channels: Number of channels in the input latent.
mid_channels: Number of channels in the middle layers.
num_blocks_per_stage: Number of ResBlocks per stage (pre/post upsampling).
dims: Dimensionality of convolutions (2 or 3).
spatial_upsample: Whether to spatially upsample.
temporal_upsample: Whether to temporally upsample.
spatial_scale: Scale factor for spatial upsampling.
rational_resampler: Whether to use rational resampler for spatial upsampling.
"""
layerwise_offload_dit_group_enabled = False
layer_names = ["res_blocks", "post_upsample_res_blocks"]
def __init__(
self,
in_channels: int = 128,
mid_channels: int = 512,
num_blocks_per_stage: int = 4,
dims: int = 3,
spatial_upsample: bool = True,
temporal_upsample: bool = False,
spatial_scale: float = 2.0,
rational_resampler: bool = False,
):
super().__init__()
self.in_channels = in_channels
self.mid_channels = mid_channels
self.num_blocks_per_stage = num_blocks_per_stage
self.dims = dims
self.spatial_upsample = spatial_upsample
self.temporal_upsample = temporal_upsample
self.spatial_scale = float(spatial_scale)
self.rational_resampler = rational_resampler
conv = torch.nn.Conv2d if dims == 2 else torch.nn.Conv3d
self.initial_conv = conv(in_channels, mid_channels, kernel_size=3, padding=1)
self.initial_norm = torch.nn.GroupNorm(32, mid_channels)
self.initial_activation = torch.nn.SiLU()
self.res_blocks = torch.nn.ModuleList(
[ResBlock(mid_channels, dims=dims) for _ in range(num_blocks_per_stage)]
)
if spatial_upsample and temporal_upsample:
self.upsampler = torch.nn.Sequential(
torch.nn.Conv3d(
mid_channels, 8 * mid_channels, kernel_size=3, padding=1
),
PixelShuffleND(3),
)
elif spatial_upsample:
if rational_resampler:
self.upsampler = SpatialRationalResampler(
mid_channels=mid_channels, scale=self.spatial_scale
)
else:
self.upsampler = torch.nn.Sequential(
torch.nn.Conv2d(
mid_channels, 4 * mid_channels, kernel_size=3, padding=1
),
PixelShuffleND(2),
)
elif temporal_upsample:
self.upsampler = torch.nn.Sequential(
torch.nn.Conv3d(
mid_channels, 2 * mid_channels, kernel_size=3, padding=1
),
PixelShuffleND(1),
)
else:
raise ValueError(
"Either spatial_upsample or temporal_upsample must be True"
)
self.post_upsample_res_blocks = torch.nn.ModuleList(
[ResBlock(mid_channels, dims=dims) for _ in range(num_blocks_per_stage)]
)
self.final_conv = conv(mid_channels, in_channels, kernel_size=3, padding=1)
def forward(self, latent: torch.Tensor) -> torch.Tensor:
b, _, f, _, _ = latent.shape
if self.dims == 2:
x = rearrange(latent, "b c f h w -> (b f) c h w")
x = self.initial_conv(x)
x = apply_group_norm_silu(x, self.initial_norm, self.initial_activation)
for block in self.res_blocks:
x = block(x)
x = self.upsampler(x)
for block in self.post_upsample_res_blocks:
x = block(x)
x = self.final_conv(x)
x = rearrange(x, "(b f) c h w -> b c f h w", b=b, f=f)
else:
x = self.initial_conv(latent)
x = apply_group_norm_silu(x, self.initial_norm, self.initial_activation)
for block in self.res_blocks:
x = block(x)
if self.temporal_upsample:
x = self.upsampler(x)
x = x[:, :, 1:, :, :]
elif isinstance(self.upsampler, SpatialRationalResampler):
x = self.upsampler(x)
else:
x = rearrange(x, "b c f h w -> (b f) c h w")
x = self.upsampler(x)
x = rearrange(x, "(b f) c h w -> b c f h w", b=b, f=f)
for block in self.post_upsample_res_blocks:
x = block(x)
x = self.final_conv(x)
return x
@@ -0,0 +1,602 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
from typing import Dict, Optional, Tuple, Union
import torch
from diffusers.models.attention_processor import (
ADDED_KV_ATTENTION_PROCESSORS,
CROSS_ATTENTION_PROCESSORS,
Attention,
AttentionProcessor,
AttnAddedKVProcessor,
AttnProcessor,
FusedAttnProcessor2_0,
)
from diffusers.models.autoencoders.vae import (
Decoder,
DecoderOutput,
DiagonalGaussianDistribution,
Encoder,
)
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from torch import nn
from sglang.multimodal_gen.configs.models.vaes.flux import FluxVAEConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
spatial_parallel_diffusers_decode,
)
class AutoencoderKL(nn.Module, LayerwiseOffloadableModuleMixin):
r"""
A VAE model with KL loss for encoding images into latents and decoding latent representations into images.
This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented
for all models (such as downloading or saving).
Parameters:
in_channels (int, *optional*, defaults to 3): Number of channels in the input image.
out_channels (int, *optional*, defaults to 3): Number of channels in the output.
down_block_types (`Tuple[str]`, *optional*, defaults to `("DownEncoderBlock2D",)`):
Tuple of downsample block types.
up_block_types (`Tuple[str]`, *optional*, defaults to `("UpDecoderBlock2D",)`):
Tuple of upsample block types.
block_out_channels (`Tuple[int]`, *optional*, defaults to `(64,)`):
Tuple of block output channels.
act_fn (`str`, *optional*, defaults to `"silu"`): The activation function to use.
latent_channels (`int`, *optional*, defaults to 4): Number of channels in the latent space.
sample_size (`int`, *optional*, defaults to `32`): Sample input size.
scaling_factor (`float`, *optional*, defaults to 0.18215):
The component-wise standard deviation of the trained latent space computed using the first batch of the
training set. This is used to scale the latent space to have unit variance when training the diffusion
model. The latents are scaled with the formula `z = z * scaling_factor` before being passed to the
diffusion model. When decoding, the latents are scaled back to the original scale with the formula: `z = 1
/ scaling_factor * z`. For more details, refer to sections 4.3.2 and D.1 of the [High-Resolution Image
Synthesis with Latent Diffusion Models](https://huggingface.co/papers/2112.10752) paper.
force_upcast (`bool`, *optional*, default to `True`):
If enabled it will force the VAE to run in float32 for high image resolution pipelines, such as SD-XL. VAE
can be fine-tuned / trained to a lower range without losing too much precision in which case `force_upcast`
can be set to `False` - see: https://huggingface.co/madebyollin/sdxl-vae-fp16-fix
mid_block_add_attention (`bool`, *optional*, default to `True`):
If enabled, the mid_block of the Encoder and Decoder will have attention blocks. If set to false, the
mid_block will only have resnet blocks
"""
layerwise_offload_dit_group_enabled = False
_supports_gradient_checkpointing = True
_no_split_modules = ["BasicTransformerBlock", "ResnetBlock2D"]
layer_names = ["encoder.down_blocks", "decoder.up_blocks"]
def __init__(
self,
config: FluxVAEConfig,
):
super().__init__()
self.config = config
arch_config = config.arch_config
in_channels = arch_config.in_channels
out_channels = arch_config.out_channels
down_block_types = arch_config.down_block_types
up_block_types = arch_config.up_block_types
block_out_channels = arch_config.block_out_channels
layers_per_block = arch_config.layers_per_block
act_fn = arch_config.act_fn
latent_channels = arch_config.latent_channels
norm_num_groups = arch_config.norm_num_groups
sample_size = arch_config.sample_size
use_quant_conv = arch_config.use_quant_conv
use_post_quant_conv = arch_config.use_post_quant_conv
mid_block_add_attention = arch_config.mid_block_add_attention
# pass init params to Encoder
self.encoder = Encoder(
in_channels=in_channels,
out_channels=latent_channels,
down_block_types=down_block_types,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
act_fn=act_fn,
norm_num_groups=norm_num_groups,
double_z=True,
mid_block_add_attention=mid_block_add_attention,
)
# pass init params to Decoder
self.decoder = Decoder(
in_channels=latent_channels,
out_channels=out_channels,
up_block_types=up_block_types,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
norm_num_groups=norm_num_groups,
act_fn=act_fn,
mid_block_add_attention=mid_block_add_attention,
)
self.quant_conv = (
nn.Conv2d(2 * latent_channels, 2 * latent_channels, 1)
if use_quant_conv
else None
)
self.post_quant_conv = (
nn.Conv2d(latent_channels, latent_channels, 1)
if use_post_quant_conv
else None
)
self.use_slicing = False
self.use_tiling = False
self.use_parallel_decode = config.use_parallel_decode
self.parallel_decode_mode = config.parallel_decode_mode
self._spatial_parallel_decode_enabled = False
self._spatial_parallel_upsample_count = 0
if can_install_spatial_shard_parallel_decode(self.config):
self._spatial_parallel_upsample_count = (
enable_diffusers_decoder_spatial_parallel(self.decoder)
)
self._spatial_parallel_decode_enabled = True
# only relevant if vae tiling is enabled
self.tile_sample_min_size = sample_size
sample_size = (
self.config.sample_size[0]
if isinstance(self.config.sample_size, (list, tuple))
else self.config.sample_size
)
self.tile_latent_min_size = int(
sample_size / (2 ** (len(self.config.block_out_channels) - 1))
)
self.tile_overlap_factor = 0.25
def enable_tiling(self, use_tiling: bool = True):
r"""
Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to
compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow
processing larger images.
"""
self.use_tiling = use_tiling
def disable_tiling(self):
r"""
Disable tiled VAE decoding. If `enable_tiling` was previously enabled, this method will go back to computing
decoding in one step.
"""
self.enable_tiling(False)
def enable_slicing(self):
r"""
Enable sliced VAE decoding. When this option is enabled, the VAE will split the input tensor in slices to
compute decoding in several steps. This is useful to save some memory and allow larger batch sizes.
"""
self.use_slicing = True
def disable_slicing(self):
r"""
Disable sliced VAE decoding. If `enable_slicing` was previously enabled, this method will go back to computing
decoding in one step.
"""
self.use_slicing = False
@property
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.attn_processors
def attn_processors(self) -> Dict[str, AttentionProcessor]:
r"""
Returns:
`dict` of attention processors: A dictionary containing all attention processors used in the model with
indexed by its weight name.
"""
# set recursively
processors = {}
def fn_recursive_add_processors(
name: str,
module: torch.nn.Module,
processors: Dict[str, AttentionProcessor],
):
if hasattr(module, "get_processor"):
processors[f"{name}.processor"] = module.get_processor()
for sub_name, child in module.named_children():
fn_recursive_add_processors(f"{name}.{sub_name}", child, processors)
return processors
for name, module in self.named_children():
fn_recursive_add_processors(name, module, processors)
return processors
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attn_processor
def set_attn_processor(
self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]
):
r"""
Sets the attention processor to use to compute attention.
Parameters:
processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`):
The instantiated processor class or a dictionary of processor classes that will be set as the processor
for **all** `Attention` layers.
If `processor` is a dict, the key needs to define the path to the corresponding cross attention
processor. This is strongly recommended when setting trainable attention processors.
"""
count = len(self.attn_processors.keys())
if isinstance(processor, dict) and len(processor) != count:
raise ValueError(
f"A dict of processors was passed, but the number of processors {len(processor)} does not match the"
f" number of attention layers: {count}. Please make sure to pass {count} processor classes."
)
def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor):
if hasattr(module, "set_processor"):
if not isinstance(processor, dict):
module.set_processor(processor)
else:
module.set_processor(processor.pop(f"{name}.processor"))
for sub_name, child in module.named_children():
fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor)
for name, module in self.named_children():
fn_recursive_attn_processor(name, module, processor)
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_default_attn_processor
def set_default_attn_processor(self):
"""
Disables custom attention processors and sets the default attention implementation.
"""
if all(
proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS
for proc in self.attn_processors.values()
):
processor = AttnAddedKVProcessor()
elif all(
proc.__class__ in CROSS_ATTENTION_PROCESSORS
for proc in self.attn_processors.values()
):
processor = AttnProcessor()
else:
raise ValueError(
f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}"
)
self.set_attn_processor(processor)
def _encode(self, x: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, height, width = x.shape
if self.use_tiling and (
width > self.tile_sample_min_size or height > self.tile_sample_min_size
):
return self._tiled_encode(x)
enc = self.encoder(x)
if self.quant_conv is not None:
enc = self.quant_conv(enc)
return enc
def encode(
self, x: torch.Tensor, return_dict: bool = True
) -> Union[AutoencoderKLOutput, Tuple[DiagonalGaussianDistribution]]:
"""
Encode a batch of images into latents.
Args:
x (`torch.Tensor`): Input batch of images.
return_dict (`bool`, *optional*, defaults to `True`):
Whether to return a [`~models.autoencoder_kl.AutoencoderKLOutput`] instead of a plain tuple.
Returns:
The latent representations of the encoded images. If `return_dict` is True, a
[`~models.autoencoder_kl.AutoencoderKLOutput`] is returned, otherwise a plain `tuple` is returned.
"""
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, return_dict: bool = True
) -> Union[DecoderOutput, torch.Tensor]:
if self.use_tiling and (
z.shape[-1] > self.tile_latent_min_size
or z.shape[-2] > self.tile_latent_min_size
):
return self.tiled_decode(z, return_dict=return_dict)
if self.post_quant_conv is not None:
z = self.post_quant_conv(z)
if self._spatial_parallel_decode_enabled:
dec = spatial_parallel_diffusers_decode(
self.decoder, z, self._spatial_parallel_upsample_count
)
else:
dec = self.decoder(z)
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def decode(self, z: torch.FloatTensor) -> Union[DecoderOutput, torch.FloatTensor]:
"""
Decode a batch of images.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
Returns:
[`~models.vae.DecoderOutput`] or `tuple`:
If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is
returned.
"""
if self.use_slicing and z.shape[0] > 1:
decoded_slices = [self._decode(z_slice).sample for z_slice in z.split(1)]
decoded = torch.cat(decoded_slices)
else:
decoded = self._decode(z).sample
return decoded
def blend_v(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[2], b.shape[2], blend_extent)
for y in range(blend_extent):
b[:, :, y, :] = a[:, :, -blend_extent + y, :] * (1 - y / blend_extent) + b[
:, :, y, :
] * (y / blend_extent)
return b
def blend_h(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[3], b.shape[3], blend_extent)
for x in range(blend_extent):
b[:, :, :, x] = a[:, :, :, -blend_extent + x] * (1 - x / blend_extent) + b[
:, :, :, x
] * (x / blend_extent)
return b
def _tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
r"""Encode a batch of images using a tiled encoder.
When this option is enabled, the VAE will split the input tensor into tiles to compute encoding in several
steps. This is useful to keep memory use constant regardless of image size. The end result of tiled encoding is
different from non-tiled encoding because each tile uses a different encoder. To avoid tiling artifacts, the
tiles overlap and are blended together to form a smooth output. You may still see tile-sized changes in the
output, but they should be much less noticeable.
Args:
x (`torch.Tensor`): Input batch of images.
Returns:
`torch.Tensor`:
The latent representation of the encoded videos.
"""
overlap_size = int(self.tile_sample_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_latent_min_size * self.tile_overlap_factor)
row_limit = self.tile_latent_min_size - blend_extent
# Split the image into 512x512 tiles and encode them separately.
rows = []
for i in range(0, x.shape[2], overlap_size):
row = []
for j in range(0, x.shape[3], overlap_size):
tile = x[
:,
:,
i : i + self.tile_sample_min_size,
j : j + self.tile_sample_min_size,
]
tile = self.encoder(tile)
if self.config.use_quant_conv:
tile = self.quant_conv(tile)
row.append(tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
enc = torch.cat(result_rows, dim=2)
return enc
def tiled_encode(
self, x: torch.Tensor, return_dict: bool = True
) -> AutoencoderKLOutput:
r"""Encode a batch of images using a tiled encoder.
When this option is enabled, the VAE will split the input tensor into tiles to compute encoding in several
steps. This is useful to keep memory use constant regardless of image size. The end result of tiled encoding is
different from non-tiled encoding because each tile uses a different encoder. To avoid tiling artifacts, the
tiles overlap and are blended together to form a smooth output. You may still see tile-sized changes in the
output, but they should be much less noticeable.
Args:
x (`torch.Tensor`): Input batch of images.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~models.autoencoder_kl.AutoencoderKLOutput`] instead of a plain tuple.
Returns:
[`~models.autoencoder_kl.AutoencoderKLOutput`] or `tuple`:
If return_dict is True, a [`~models.autoencoder_kl.AutoencoderKLOutput`] is returned, otherwise a plain
`tuple` is returned.
"""
overlap_size = int(self.tile_sample_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_latent_min_size * self.tile_overlap_factor)
row_limit = self.tile_latent_min_size - blend_extent
# Split the image into 512x512 tiles and encode them separately.
rows = []
for i in range(0, x.shape[2], overlap_size):
row = []
for j in range(0, x.shape[3], overlap_size):
tile = x[
:,
:,
i : i + self.tile_sample_min_size,
j : j + self.tile_sample_min_size,
]
tile = self.encoder(tile)
if self.config.use_quant_conv:
tile = self.quant_conv(tile)
row.append(tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
moments = torch.cat(result_rows, dim=2)
posterior = DiagonalGaussianDistribution(moments)
if not return_dict:
return (posterior,)
return AutoencoderKLOutput(latent_dist=posterior)
def tiled_decode(
self, z: torch.Tensor, return_dict: bool = True
) -> Union[DecoderOutput, torch.Tensor]:
r"""
Decode a batch of images using a tiled decoder.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~models.vae.DecoderOutput`] instead of a plain tuple.
Returns:
[`~models.vae.DecoderOutput`] or `tuple`:
If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is
returned.
"""
overlap_size = int(self.tile_latent_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_sample_min_size * self.tile_overlap_factor)
row_limit = self.tile_sample_min_size - blend_extent
# Split z into overlapping 64x64 tiles and decode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, z.shape[2], overlap_size):
row = []
for j in range(0, z.shape[3], overlap_size):
tile = z[
:,
:,
i : i + self.tile_latent_min_size,
j : j + self.tile_latent_min_size,
]
if self.config.use_post_quant_conv:
tile = self.post_quant_conv(tile)
decoded = self.decoder(tile)
row.append(decoded)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
dec = torch.cat(result_rows, dim=2)
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def forward(
self,
sample: torch.Tensor,
sample_posterior: bool = False,
generator: Optional[torch.Generator] = None,
) -> Union[DecoderOutput, torch.Tensor]:
r"""
Args:
sample (`torch.Tensor`): Input sample.
sample_posterior (`bool`, *optional*, defaults to `False`):
Whether to sample from the posterior.
"""
x = sample
posterior = self.encode(x).latent_dist
if sample_posterior:
z = posterior.sample(generator=generator)
else:
z = posterior.mode()
dec = self.decode(z).sample
return dec
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.fuse_qkv_projections
def fuse_qkv_projections(self):
"""
Enables fused QKV projections. For self-attention modules, all projection matrices (i.e., query, key, value)
are fused. For cross-attention modules, key and value projection matrices are fused.
> [!WARNING] > This API is 🧪 experimental.
"""
self.original_attn_processors = None
for _, attn_processor in self.attn_processors.items():
if "Added" in str(attn_processor.__class__.__name__):
raise ValueError(
"`fuse_qkv_projections()` is not supported for models having added KV projections."
)
self.original_attn_processors = self.attn_processors
for module in self.modules():
if isinstance(module, Attention):
module.fuse_projections(fuse=True)
self.set_attn_processor(FusedAttnProcessor2_0())
EntryClass = AutoencoderKL
@@ -0,0 +1,169 @@
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Iterable
import torch
from torch import nn
from sglang.multimodal_gen.configs.models.vaes.sana import SanaVAEConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
gather_and_trim_height,
split_height_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
)
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
class AutoencoderDC(nn.Module, LayerwiseOffloadableModuleMixin):
"""Deep Compression Autoencoder wrapper with 32x spatial compression."""
layerwise_offload_dit_group_enabled = False
layer_names = ["_inner_model.encoder.down_blocks", "_inner_model.decoder.up_blocks"]
def __init__(self, config: SanaVAEConfig = None, **kwargs):
super().__init__()
self._config = config
self._inner_model = None
self._loaded_state_dict: dict[str, torch.Tensor] = {}
self._spatial_parallel_decode_enabled = False
def _ensure_inner_model(self, state_dict: dict[str, torch.Tensor] | None = None):
if self._inner_model is not None:
return
from diffusers import AutoencoderDC as DiffusersAutoencoderDC
device = "cpu"
state_to_load = (
state_dict if state_dict is not None else self._loaded_state_dict
)
if state_to_load:
first_tensor = next(iter(state_to_load.values()))
device = first_tensor.device
hf_config = {}
if self._config is not None:
arch = self._config.arch_config
for key, value in vars(arch).items():
if key == "extra_attrs" and isinstance(value, dict):
for ek, ev in value.items():
hf_config[ek] = ev
elif not key.startswith("_") and not callable(value):
hf_config[key] = value
self._inner_model = DiffusersAutoencoderDC.from_config(hf_config)
if state_to_load:
missing, unexpected = self._inner_model.load_state_dict(
state_to_load, strict=False
)
if missing:
logger.warning(
"AutoencoderDC missing keys when loading: %d keys", len(missing)
)
if len(missing) > 10:
logger.debug("First 10 missing keys: %s", list(missing)[:10])
else:
logger.debug("Missing keys: %s", list(missing))
if unexpected:
logger.debug(
"AutoencoderDC unexpected keys when loading: %d keys",
len(unexpected),
)
if state_dict is None:
self._loaded_state_dict.clear()
self._inner_model = self._inner_model.to(device)
if can_install_spatial_shard_parallel_decode(self._config):
enable_diffusers_decoder_spatial_parallel(self._inner_model.decoder)
self._spatial_parallel_decode_enabled = True
@property
def config(self):
if self._inner_model is not None:
return self._inner_model.config
return self._config
@property
def dtype(self):
if self._inner_model is not None:
return next(self._inner_model.parameters()).dtype
return torch.float32
@property
def device(self):
if self._inner_model is not None:
return next(self._inner_model.parameters()).device
return torch.device("cpu")
def encode(self, x: torch.Tensor, **kwargs):
self._ensure_inner_model()
return self._inner_model.encode(x, **kwargs)
def decode(self, z: torch.Tensor, **kwargs):
self._ensure_inner_model()
z = z.to(dtype=self.dtype)
if not self._spatial_parallel_decode_enabled:
return self._inner_model.decode(z, **kwargs)
expected_height = (
z.shape[-2] * self._config.arch_config.spatial_compression_ratio
)
z, expected_height = split_height_for_parallel_decode(
z,
expected_height=expected_height,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
decoded = self._inner_model.decode(z, **kwargs)
if isinstance(decoded, tuple):
sample = gather_and_trim_height(decoded[0], expected_height)
return (sample, *decoded[1:])
sample = gather_and_trim_height(decoded.sample, expected_height)
return decoded.__class__(sample=sample)
def forward(self, x: torch.Tensor, **kwargs):
self._ensure_inner_model()
return self._inner_model(x, **kwargs)
def load_state_dict(
self,
state_dict: dict[str, torch.Tensor],
strict: bool = True,
assign: bool = False,
):
"""Intercept load_state_dict to route weights into the inner diffusers model."""
self._ensure_inner_model(state_dict=state_dict)
def state_dict(self, *args, **kwargs) -> dict[str, torch.Tensor]:
self._ensure_inner_model()
return self._inner_model.state_dict(*args, **kwargs)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
"""Buffer weights for deferred loading. The inner model is built lazily."""
loaded_params: set[str] = set()
for name, weight in weights:
self._loaded_state_dict[name] = weight
loaded_params.add(name)
return loaded_params
def to(self, *args, **kwargs):
if self._inner_model is not None:
self._inner_model = self._inner_model.to(*args, **kwargs)
return super().to(*args, **kwargs)
EntryClass = AutoencoderDC
@@ -0,0 +1,539 @@
import math
from typing import Dict, Optional, Tuple, Union
import torch
import torch.nn as nn
from diffusers.models.attention_processor import (
ADDED_KV_ATTENTION_PROCESSORS,
CROSS_ATTENTION_PROCESSORS,
AttentionProcessor,
AttnAddedKVProcessor,
AttnProcessor,
)
from diffusers.models.autoencoders.vae import (
Decoder,
DecoderOutput,
DiagonalGaussianDistribution,
Encoder,
)
from diffusers.models.modeling_outputs import AutoencoderKLOutput
from sglang.multimodal_gen.configs.models.vaes.flux import Flux2VAEConfig
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.parallel.diffusers_spatial import (
enable_diffusers_decoder_spatial_parallel,
spatial_parallel_diffusers_decode,
)
class AutoencoderKLFlux2(ParallelTiledVAE):
r"""
A VAE model with KL loss for encoding images into latents and decoding latent representations into images.
This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented
for all models (such as downloading or saving).
Parameters:
"""
_supports_gradient_checkpointing = True
_no_split_modules = ["BasicTransformerBlock", "ResnetBlock2D"]
def __init__(
self,
config: Flux2VAEConfig,
):
super().__init__(config=config)
self.config = config
arch_config = config.arch_config
in_channels: int = arch_config.in_channels
out_channels: int = arch_config.out_channels
down_block_types: Tuple[str, ...] = arch_config.down_block_types
up_block_types: Tuple[str, ...] = arch_config.up_block_types
block_out_channels: Tuple[int, ...] = arch_config.block_out_channels
decoder_block_out_channels: Optional[Tuple[int, ...]] = getattr(
arch_config, "decoder_block_out_channels", None
)
layers_per_block: int = arch_config.layers_per_block
act_fn: str = arch_config.act_fn
latent_channels: int = arch_config.latent_channels
norm_num_groups: int = arch_config.norm_num_groups
sample_size: int = arch_config.sample_size
use_quant_conv: bool = arch_config.use_quant_conv
use_post_quant_conv: bool = arch_config.use_post_quant_conv
mid_block_add_attention: bool = arch_config.mid_block_add_attention
batch_norm_eps: float = arch_config.batch_norm_eps
batch_norm_momentum: float = arch_config.batch_norm_momentum
patch_size: Tuple[int, int] = arch_config.patch_size
# pass init params to Encoder
self.encoder = Encoder(
in_channels=in_channels,
out_channels=latent_channels,
down_block_types=down_block_types,
block_out_channels=block_out_channels,
layers_per_block=layers_per_block,
act_fn=act_fn,
norm_num_groups=norm_num_groups,
double_z=True,
mid_block_add_attention=mid_block_add_attention,
)
# pass init params to Decoder
self.decoder = Decoder(
in_channels=latent_channels,
out_channels=out_channels,
up_block_types=up_block_types,
block_out_channels=decoder_block_out_channels or block_out_channels,
layers_per_block=layers_per_block,
norm_num_groups=norm_num_groups,
act_fn=act_fn,
mid_block_add_attention=mid_block_add_attention,
)
self.quant_conv = (
nn.Conv2d(2 * latent_channels, 2 * latent_channels, 1)
if use_quant_conv
else None
)
self.post_quant_conv = (
nn.Conv2d(latent_channels, latent_channels, 1)
if use_post_quant_conv
else None
)
self.bn = nn.BatchNorm2d(
math.prod(patch_size) * latent_channels,
eps=batch_norm_eps,
momentum=batch_norm_momentum,
affine=False,
track_running_stats=True,
)
self.use_slicing = False
self.use_tiling = False
self._spatial_parallel_decode_enabled = False
self._spatial_parallel_upsample_count = 0
if can_install_spatial_shard_parallel_decode(self.config):
self._spatial_parallel_upsample_count = (
enable_diffusers_decoder_spatial_parallel(self.decoder)
)
self._spatial_parallel_decode_enabled = True
# only relevant if vae tiling is enabled
self.tile_sample_min_size = self.config.sample_size
sample_size = (
self.config.sample_size[0]
if isinstance(self.config.sample_size, (list, tuple))
else self.config.sample_size
)
self.tile_latent_min_size = int(
sample_size / (2 ** (len(self.config.block_out_channels) - 1))
)
self.tile_overlap_factor = 0.25
@property
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.attn_processors
def attn_processors(self) -> Dict[str, AttentionProcessor]:
r"""
Returns:
`dict` of attention processors: A dictionary containing all attention processors used in the model with
indexed by its weight name.
"""
# set recursively
processors = {}
def fn_recursive_add_processors(
name: str,
module: torch.nn.Module,
processors: Dict[str, AttentionProcessor],
):
if hasattr(module, "get_processor"):
processors[f"{name}.processor"] = module.get_processor()
for sub_name, child in module.named_children():
fn_recursive_add_processors(f"{name}.{sub_name}", child, processors)
return processors
for name, module in self.named_children():
fn_recursive_add_processors(name, module, processors)
return processors
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_attn_processor
def set_attn_processor(
self, processor: Union[AttentionProcessor, Dict[str, AttentionProcessor]]
):
r"""
Sets the attention processor to use to compute attention.
Parameters:
processor (`dict` of `AttentionProcessor` or only `AttentionProcessor`):
The instantiated processor class or a dictionary of processor classes that will be set as the processor
for **all** `Attention` layers.
If `processor` is a dict, the key needs to define the path to the corresponding cross attention
processor. This is strongly recommended when setting trainable attention processors.
"""
count = len(self.attn_processors.keys())
if isinstance(processor, dict) and len(processor) != count:
raise ValueError(
f"A dict of processors was passed, but the number of processors {len(processor)} does not match the"
f" number of attention layers: {count}. Please make sure to pass {count} processor classes."
)
def fn_recursive_attn_processor(name: str, module: torch.nn.Module, processor):
if hasattr(module, "set_processor"):
if not isinstance(processor, dict):
module.set_processor(processor)
else:
module.set_processor(processor.pop(f"{name}.processor"))
for sub_name, child in module.named_children():
fn_recursive_attn_processor(f"{name}.{sub_name}", child, processor)
for name, module in self.named_children():
fn_recursive_attn_processor(name, module, processor)
# Copied from diffusers.models.unets.unet_2d_condition.UNet2DConditionModel.set_default_attn_processor
def set_default_attn_processor(self):
"""
Disables custom attention processors and sets the default attention implementation.
"""
if all(
proc.__class__ in ADDED_KV_ATTENTION_PROCESSORS
for proc in self.attn_processors.values()
):
processor = AttnAddedKVProcessor()
elif all(
proc.__class__ in CROSS_ATTENTION_PROCESSORS
for proc in self.attn_processors.values()
):
processor = AttnProcessor()
else:
raise ValueError(
f"Cannot call `set_default_attn_processor` when attention processors are of type {next(iter(self.attn_processors.values()))}"
)
self.set_attn_processor(processor)
def _encode(self, x: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, height, width = x.shape
if self.use_tiling and (
width > self.tile_sample_min_size or height > self.tile_sample_min_size
):
return self._tiled_encode(x)
enc = self.encoder(x)
if self.quant_conv is not None:
enc = self.quant_conv(enc)
return enc
def encode(
self, x: torch.Tensor, return_dict: bool = True
) -> Union[DiagonalGaussianDistribution]:
"""
Encode a batch of images into latents.
Args:
x (`torch.Tensor`): Input batch of images.
return_dict (`bool`, *optional*, defaults to `True`):
Whether to return a [`~models.autoencoder_kl.AutoencoderKLOutput`] instead of a plain tuple.
Returns:
The latent representations of the encoded images. If `return_dict` is True, a
[`~models.autoencoder_kl.AutoencoderKLOutput`] is returned, otherwise a plain `tuple` is returned.
"""
if x.ndim == 5:
assert x.shape[2] == 1
x = x.squeeze(2)
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)
return posterior
def _decode(
self, z: torch.Tensor, return_dict: bool = True
) -> Union[DecoderOutput, torch.Tensor]:
if self.use_tiling and (
z.shape[-1] > self.tile_latent_min_size
or z.shape[-2] > self.tile_latent_min_size
):
return self.tiled_decode(z, return_dict=return_dict)
if self.post_quant_conv is not None:
z = self.post_quant_conv(z)
if self._spatial_parallel_decode_enabled:
dec = spatial_parallel_diffusers_decode(
self.decoder, z, self._spatial_parallel_upsample_count
)
else:
dec = self.decoder(z)
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def decode(
self, z: torch.FloatTensor, return_dict: bool = True, generator=None
) -> Union[DecoderOutput, torch.FloatTensor]:
"""
Decode a batch of images.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
return_dict (`bool`, *optional*, defaults to `True`):
Whether to return a [`~models.vae.DecoderOutput`] instead of a plain tuple.
Returns:
[`~models.vae.DecoderOutput`] or `tuple`:
If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is
returned.
"""
if self.use_slicing and z.shape[0] > 1:
decoded_slices = [self._decode(z_slice).sample for z_slice in z.split(1)]
decoded = torch.cat(decoded_slices)
else:
decoded = self._decode(z).sample
return decoded
def blend_v(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[2], b.shape[2], blend_extent)
for y in range(blend_extent):
b[:, :, y, :] = a[:, :, -blend_extent + y, :] * (1 - y / blend_extent) + b[
:, :, y, :
] * (y / blend_extent)
return b
def blend_h(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[3], b.shape[3], blend_extent)
for x in range(blend_extent):
b[:, :, :, x] = a[:, :, :, -blend_extent + x] * (1 - x / blend_extent) + b[
:, :, :, x
] * (x / blend_extent)
return b
def _tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
r"""Encode a batch of images using a tiled encoder.
When this option is enabled, the VAE will split the input tensor into tiles to compute encoding in several
steps. This is useful to keep memory use constant regardless of image size. The end result of tiled encoding is
different from non-tiled encoding because each tile uses a different encoder. To avoid tiling artifacts, the
tiles overlap and are blended together to form a smooth output. You may still see tile-sized changes in the
output, but they should be much less noticeable.
Args:
x (`torch.Tensor`): Input batch of images.
Returns:
`torch.Tensor`:
The latent representation of the encoded videos.
"""
overlap_size = int(self.tile_sample_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_latent_min_size * self.tile_overlap_factor)
row_limit = self.tile_latent_min_size - blend_extent
# Split the image into 512x512 tiles and encode them separately.
rows = []
for i in range(0, x.shape[2], overlap_size):
row = []
for j in range(0, x.shape[3], overlap_size):
tile = x[
:,
:,
i : i + self.tile_sample_min_size,
j : j + self.tile_sample_min_size,
]
tile = self.encoder(tile)
if self.config.use_quant_conv:
tile = self.quant_conv(tile)
row.append(tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
enc = torch.cat(result_rows, dim=2)
return enc
def tiled_encode(
self, x: torch.Tensor, return_dict: bool = True
) -> AutoencoderKLOutput:
r"""Encode a batch of images using a tiled encoder.
When this option is enabled, the VAE will split the input tensor into tiles to compute encoding in several
steps. This is useful to keep memory use constant regardless of image size. The end result of tiled encoding is
different from non-tiled encoding because each tile uses a different encoder. To avoid tiling artifacts, the
tiles overlap and are blended together to form a smooth output. You may still see tile-sized changes in the
output, but they should be much less noticeable.
Args:
x (`torch.Tensor`): Input batch of images.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~models.autoencoder_kl.AutoencoderKLOutput`] instead of a plain tuple.
Returns:
[`~models.autoencoder_kl.AutoencoderKLOutput`] or `tuple`:
If return_dict is True, a [`~models.autoencoder_kl.AutoencoderKLOutput`] is returned, otherwise a plain
`tuple` is returned.
"""
overlap_size = int(self.tile_sample_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_latent_min_size * self.tile_overlap_factor)
row_limit = self.tile_latent_min_size - blend_extent
# Split the image into 512x512 tiles and encode them separately.
rows = []
for i in range(0, x.shape[2], overlap_size):
row = []
for j in range(0, x.shape[3], overlap_size):
tile = x[
:,
:,
i : i + self.tile_sample_min_size,
j : j + self.tile_sample_min_size,
]
tile = self.encoder(tile)
if self.config.use_quant_conv:
tile = self.quant_conv(tile)
row.append(tile)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
moments = torch.cat(result_rows, dim=2)
posterior = DiagonalGaussianDistribution(moments)
if not return_dict:
return (posterior,)
return AutoencoderKLOutput(latent_dist=posterior)
def tiled_decode(
self, z: torch.Tensor, return_dict: bool = True
) -> Union[DecoderOutput, torch.Tensor]:
r"""
Decode a batch of images using a tiled decoder.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`~models.vae.DecoderOutput`] instead of a plain tuple.
Returns:
[`~models.vae.DecoderOutput`] or `tuple`:
If return_dict is True, a [`~models.vae.DecoderOutput`] is returned, otherwise a plain `tuple` is
returned.
"""
overlap_size = int(self.tile_latent_min_size * (1 - self.tile_overlap_factor))
blend_extent = int(self.tile_sample_min_size * self.tile_overlap_factor)
row_limit = self.tile_sample_min_size - blend_extent
# Split z into overlapping 64x64 tiles and decode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, z.shape[2], overlap_size):
row = []
for j in range(0, z.shape[3], overlap_size):
tile = z[
:,
:,
i : i + self.tile_latent_min_size,
j : j + self.tile_latent_min_size,
]
if self.config.use_post_quant_conv:
tile = self.post_quant_conv(tile)
decoded = self.decoder(tile)
row.append(decoded)
rows.append(row)
result_rows = []
for i, row in enumerate(rows):
result_row = []
for j, tile in enumerate(row):
# blend the above tile and the left tile
# to the current tile and add the current tile to the result row
if i > 0:
tile = self.blend_v(rows[i - 1][j], tile, blend_extent)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_extent)
result_row.append(tile[:, :, :row_limit, :row_limit])
result_rows.append(torch.cat(result_row, dim=3))
dec = torch.cat(result_rows, dim=2)
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
def forward(
self,
sample: torch.Tensor,
sample_posterior: bool = False,
return_dict: bool = True,
generator: Optional[torch.Generator] = None,
) -> Union[DecoderOutput, torch.Tensor]:
r"""
Args:
sample (`torch.Tensor`): Input sample.
sample_posterior (`bool`, *optional*, defaults to `False`):
Whether to sample from the posterior.
return_dict (`bool`, *optional*, defaults to `True`):
Whether or not to return a [`DecoderOutput`] instead of a plain tuple.
"""
x = sample
posterior = self.encode(x).latent_dist
if sample_posterior:
z = posterior.sample(generator=generator)
else:
z = posterior.mode()
dec = self.decode(z).sample
if not return_dict:
return (dec,)
return DecoderOutput(sample=dec)
EntryClass = AutoencoderKLFlux2
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,810 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
from abc import ABC, abstractmethod
from functools import lru_cache
from math import isqrt, prod
from typing import Optional, cast
import numpy as np
import torch
import torch.distributed as dist
from diffusers.models.autoencoders.vae import DiagonalGaussianDistribution
from diffusers.utils.torch_utils import randn_tensor
from torch import nn
from sglang.multimodal_gen.configs.models import VAEConfig
from sglang.multimodal_gen.configs.models.vaes.base import (
should_use_spatial_shard_parallel_decode,
)
from sglang.multimodal_gen.runtime.distributed import (
get_decode_parallel_group_coordinator,
get_decode_parallel_world_size,
get_sp_parallel_rank,
get_sp_world_size,
model_parallel_is_initialized,
)
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
@lru_cache(maxsize=1)
def _cached_decode_parallel_world_size(
is_dist_initialized: bool, is_model_parallel_initialized: bool, group_id: int
) -> int:
if not is_dist_initialized or not is_model_parallel_initialized:
return 1
return get_decode_parallel_world_size()
def _decode_parallel_world_size() -> int:
is_dist_initialized = dist.is_initialized()
is_model_parallel_initialized = model_parallel_is_initialized()
if not is_dist_initialized or not is_model_parallel_initialized:
return _cached_decode_parallel_world_size(
is_dist_initialized, is_model_parallel_initialized, 0
)
return _cached_decode_parallel_world_size(
is_dist_initialized,
is_model_parallel_initialized,
id(get_decode_parallel_group_coordinator()),
)
def has_decode_parallel_world() -> bool:
return _decode_parallel_world_size() > 1
def can_install_spatial_shard_parallel_decode(config: VAEConfig | None) -> bool:
world_size = _decode_parallel_world_size()
return (
config is not None
and world_size > 1
and should_use_spatial_shard_parallel_decode(config, world_size=world_size)
)
def should_run_spatial_shard_parallel_decode(
config: VAEConfig, z: torch.Tensor
) -> bool:
world_size = _decode_parallel_world_size()
return world_size > 1 and should_use_spatial_shard_parallel_decode(
config, z, world_size
)
class ParallelTiledVAE(ABC, nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = [
"encoder.down_blocks",
"decoder.up_blocks",
"encoder.down",
"decoder.up",
]
tile_sample_min_height: int
tile_sample_min_width: int
tile_sample_min_num_frames: int
tile_sample_stride_height: int
tile_sample_stride_width: int
tile_sample_stride_num_frames: int
blend_num_frames: int
use_tiling: bool
use_temporal_tiling: bool
use_parallel_tiling: bool
use_parallel_decode: bool
parallel_decode_mode: str
def __init__(self, config: VAEConfig, **kwargs) -> None:
super().__init__()
self.config = config
self.tile_sample_min_height = config.tile_sample_min_height
self.tile_sample_min_width = config.tile_sample_min_width
self.tile_sample_min_num_frames = config.tile_sample_min_num_frames
self.tile_sample_stride_height = config.tile_sample_stride_height
self.tile_sample_stride_width = config.tile_sample_stride_width
self.tile_sample_stride_num_frames = config.tile_sample_stride_num_frames
self.blend_num_frames = config.blend_num_frames
self.use_tiling = config.use_tiling
self.use_temporal_tiling = config.use_temporal_tiling
self.use_parallel_tiling = config.use_parallel_tiling
self.use_parallel_decode = config.use_parallel_decode
self.parallel_decode_mode = config.parallel_decode_mode
@property
def device(self):
return next(self.parameters()).device
@property
def temporal_compression_ratio(self) -> int:
return cast(int, self.config.temporal_compression_ratio)
@property
def spatial_compression_ratio(self) -> int:
return cast(int, self.config.spatial_compression_ratio)
@property
def scaling_factor(self) -> float | torch.Tensor:
return cast(float | torch.Tensor, self.config.scaling_factor)
@abstractmethod
def _encode(self, *args, **kwargs) -> torch.Tensor:
pass
@abstractmethod
def _decode(self, *args, **kwargs) -> torch.Tensor:
pass
def encode(self, x: torch.Tensor) -> DiagonalGaussianDistribution:
batch_size, num_channels, num_frames, height, width = x.shape
latent_num_frames = (num_frames - 1) // self.temporal_compression_ratio + 1
if (
self.use_tiling
and self.use_temporal_tiling
and num_frames > self.tile_sample_min_num_frames
):
latents = self.tiled_encode(x)[:, :, :latent_num_frames]
elif self.use_tiling and (
width > self.tile_sample_min_width or height > self.tile_sample_min_height
):
latents = self.spatial_tiled_encode(x)[:, :, :latent_num_frames]
else:
latents = self._encode(x)[:, :, :latent_num_frames]
return DiagonalGaussianDistribution(latents)
def decode(self, z: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = z.shape
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_stride_width // self.spatial_compression_ratio
)
tile_latent_min_num_frames = (
self.tile_sample_min_num_frames // self.temporal_compression_ratio
)
num_sample_frames = (num_frames - 1) * self.temporal_compression_ratio + 1
if should_run_spatial_shard_parallel_decode(self.config, z):
return self._decode(z)[:, :, :num_sample_frames]
if (
self.parallel_decode_mode == "tiled"
and self.use_tiling
and self.use_parallel_tiling
and get_sp_world_size() > 1
):
return self.parallel_tiled_decode(z)[:, :, :num_sample_frames]
if (
self.use_tiling
and self.use_temporal_tiling
and num_frames > tile_latent_min_num_frames
):
return self.tiled_decode(z)[:, :, :num_sample_frames]
if self.use_tiling and (
width > tile_latent_min_width or height > tile_latent_min_height
):
return self.spatial_tiled_decode(z)[:, :, :num_sample_frames]
return self._decode(z)[:, :, :num_sample_frames]
def blend_v(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[-2], b.shape[-2], blend_extent)
for y in range(blend_extent):
b[:, :, :, y, :] = a[:, :, :, -blend_extent + y, :] * (
1 - y / blend_extent
) + b[:, :, :, y, :] * (y / blend_extent)
return b
def blend_h(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[-1], b.shape[-1], blend_extent)
for x in range(blend_extent):
b[:, :, :, :, x] = a[:, :, :, :, -blend_extent + x] * (
1 - x / blend_extent
) + b[:, :, :, :, x] * (x / blend_extent)
return b
def blend_t(
self, a: torch.Tensor, b: torch.Tensor, blend_extent: int
) -> torch.Tensor:
blend_extent = min(a.shape[-3], b.shape[-3], blend_extent)
for x in range(blend_extent):
b[:, :, x, :, :] = a[:, :, -blend_extent + x, :, :] * (
1 - x / blend_extent
) + b[:, :, x, :, :] * (x / blend_extent)
return b
def spatial_tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
r"""Encode a batch of images using a tiled encoder.
Args:
x (`torch.Tensor`): Input batch of videos.
Returns:
`torch.Tensor`:
The latent representation of the encoded videos.
"""
_, _, _, height, width = x.shape
# latent_height = height // self.spatial_compression_ratio
# latent_width = width // self.spatial_compression_ratio
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
tile_latent_stride_height = (
self.tile_sample_stride_height // self.spatial_compression_ratio
)
tile_latent_stride_width = (
self.tile_sample_stride_width // self.spatial_compression_ratio
)
blend_height = tile_latent_min_height - tile_latent_stride_height
blend_width = tile_latent_min_width - tile_latent_stride_width
# Split x into overlapping tiles and encode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, height, self.tile_sample_stride_height):
row = []
for j in range(0, width, self.tile_sample_stride_width):
tile = x[
:,
:,
:,
i : i + self.tile_sample_min_height,
j : j + self.tile_sample_min_width,
]
tile = self._encode(tile)
row.append(tile)
rows.append(row)
return self._merge_spatial_tiles(
rows,
blend_height,
blend_width,
tile_latent_stride_height,
tile_latent_stride_width,
)
def parallel_tiled_decode(self, z: torch.FloatTensor) -> torch.FloatTensor:
"""
Parallel version of tiled_decode that distributes both temporal and spatial computation across GPUs
"""
world_size, rank = get_sp_world_size(), get_sp_parallel_rank()
_, _, T, H, W = z.shape
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
tile_latent_min_num_frames = (
self.tile_sample_min_num_frames // self.temporal_compression_ratio
)
tile_latent_stride_height = (
self.tile_sample_stride_height // self.spatial_compression_ratio
)
tile_latent_stride_width = (
self.tile_sample_stride_width // self.spatial_compression_ratio
)
tile_latent_stride_num_frames = (
self.tile_sample_stride_num_frames // self.temporal_compression_ratio
)
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
# Calculate tile dimensions
num_t_tiles = (
T + tile_latent_stride_num_frames - 1
) // tile_latent_stride_num_frames
num_h_tiles = (H + tile_latent_stride_height - 1) // tile_latent_stride_height
num_w_tiles = (W + tile_latent_stride_width - 1) // tile_latent_stride_width
total_spatial_tiles = num_h_tiles * num_w_tiles
total_tiles = num_t_tiles * total_spatial_tiles
tiles_per_rank = (total_tiles + world_size - 1) // world_size
start_tile_idx = rank * tiles_per_rank
end_tile_idx = min((rank + 1) * tiles_per_rank, total_tiles)
local_results = []
local_dim_metadata = []
for global_idx in range(start_tile_idx, end_tile_idx):
t_idx = global_idx // total_spatial_tiles
spatial_idx = global_idx % total_spatial_tiles
h_idx = spatial_idx // num_w_tiles
w_idx = spatial_idx % num_w_tiles
t_start = t_idx * tile_latent_stride_num_frames
h_start = h_idx * tile_latent_stride_height
w_start = w_idx * tile_latent_stride_width
tile = z[
:,
:,
t_start : t_start + tile_latent_min_num_frames + 1,
h_start : h_start + tile_latent_min_height,
w_start : w_start + tile_latent_min_width,
]
decoded_tile = self._decode(tile)
if t_start > 0:
decoded_tile = decoded_tile[:, :, 1:, :, :]
local_results.append(decoded_tile.reshape(-1))
local_dim_metadata.append(decoded_tile.shape)
if local_results:
results = torch.cat(local_results, dim=0).contiguous()
else:
results = z.new_empty((0,), dtype=z.dtype)
del local_results
local_size = torch.tensor(
[results.size(0)], device=results.device, dtype=torch.int64
)
all_sizes = [
torch.zeros(1, device=results.device, dtype=torch.int64)
for _ in range(world_size)
]
dist.all_gather(all_sizes, local_size)
max_size = max(size.item() for size in all_sizes)
padded_results = torch.zeros(
max_size, device=results.device, dtype=results.dtype
)
padded_results[: results.size(0)] = results
gathered_dim_metadata = [None] * world_size
gathered_results = (
torch.zeros_like(padded_results)
.repeat(world_size, *[1] * len(padded_results.shape))
.contiguous()
)
dist.all_gather_into_tensor(gathered_results, padded_results)
dist.all_gather_object(gathered_dim_metadata, local_dim_metadata)
gathered_dim_metadata = cast(list[list[torch.Size]], gathered_dim_metadata)
data: list = [
[[[] for _ in range(num_w_tiles)] for _ in range(num_h_tiles)]
for _ in range(num_t_tiles)
]
global_idx = 0
for i, per_rank_metadata in enumerate(gathered_dim_metadata):
start_shape = 0
for shape in per_rank_metadata:
mul_shape = prod(shape)
current_data = gathered_results[
i, start_shape : start_shape + mul_shape
].reshape(shape)
t_idx = global_idx // total_spatial_tiles
spatial_idx = global_idx % total_spatial_tiles
h_idx = spatial_idx // num_w_tiles
w_idx = spatial_idx % num_w_tiles
data[t_idx][h_idx][w_idx] = current_data
start_shape += mul_shape
global_idx += 1
result_slices = []
last_slice_data = None
for i, tem_data in enumerate(data):
slice_data = self._merge_spatial_tiles(
tem_data,
blend_height,
blend_width,
self.tile_sample_stride_height,
self.tile_sample_stride_width,
)
if i > 0:
slice_data = self.blend_t(
last_slice_data, slice_data, self.blend_num_frames
)
result_slices.append(
slice_data[:, :, : self.tile_sample_stride_num_frames, :, :]
)
else:
result_slices.append(
slice_data[:, :, : self.tile_sample_stride_num_frames + 1, :, :]
)
last_slice_data = slice_data
return torch.cat(result_slices, dim=2)
def parallel_patch_decode(self, z: torch.FloatTensor) -> torch.FloatTensor:
world_size, rank = get_sp_world_size(), get_sp_parallel_rank()
if world_size <= 1:
return self._decode(z)
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
tile_latent_stride_height = (
self.tile_sample_stride_height // self.spatial_compression_ratio
)
tile_latent_stride_width = (
self.tile_sample_stride_width // self.spatial_compression_ratio
)
overlap_h = max(0, tile_latent_min_height - tile_latent_stride_height)
overlap_w = max(0, tile_latent_min_width - tile_latent_stride_width)
halo_h = overlap_h // 2
halo_w = overlap_w // 2
_, _, _, latent_h, latent_w = z.shape
scale = self.spatial_compression_ratio
out_h = latent_h * scale
out_w = latent_w * scale
root = isqrt(world_size)
grid_rows, grid_cols = 1, world_size
for rows in range(root, 0, -1):
if world_size % rows == 0:
grid_rows, grid_cols = rows, world_size // rows
break
patch_id = rank
patch_row = patch_id // grid_cols
patch_col = patch_id % grid_cols
h0 = (patch_row * latent_h) // grid_rows
h1 = ((patch_row + 1) * latent_h) // grid_rows
w0 = (patch_col * latent_w) // grid_cols
w1 = ((patch_col + 1) * latent_w) // grid_cols
ext_h0 = max(0, h0 - halo_h)
ext_h1 = min(latent_h, h1 + halo_h)
ext_w0 = max(0, w0 - halo_w)
ext_w1 = min(latent_w, w1 + halo_w)
local_patch = z[:, :, :, ext_h0:ext_h1, ext_w0:ext_w1]
decoded_patch = self._decode(local_patch)
crop_top = (h0 - ext_h0) * scale
crop_bottom = crop_top + (h1 - h0) * scale
crop_left = (w0 - ext_w0) * scale
crop_right = crop_left + (w1 - w0) * scale
decoded_core = decoded_patch[
:, :, :, crop_top:crop_bottom, crop_left:crop_right
].contiguous()
local_result = decoded_core.reshape(-1)
local_dim_metadata = torch.tensor(
decoded_core.shape, device=z.device, dtype=torch.int64
)
local_position = torch.tensor(
[h0 * scale, h1 * scale, w0 * scale, w1 * scale],
device=z.device,
dtype=torch.int64,
)
gathered_positions = [
torch.empty_like(local_position) for _ in range(world_size)
]
dist.all_gather(gathered_positions, local_position)
local_size = torch.tensor(
[local_result.size(0)], device=z.device, dtype=torch.int64
)
gathered_dim_metadata = [
torch.empty_like(local_dim_metadata) for _ in range(world_size)
]
dist.all_gather(gathered_dim_metadata, local_dim_metadata)
all_sizes = [
torch.zeros(1, device=z.device, dtype=torch.int64)
for _ in range(world_size)
]
dist.all_gather(all_sizes, local_size)
max_size = max(size.item() for size in all_sizes)
padded_results = torch.zeros(max_size, device=z.device, dtype=z.dtype)
padded_results[: local_result.size(0)] = local_result
gathered_results = torch.empty(
(world_size, *padded_results.shape),
device=padded_results.device,
dtype=padded_results.dtype,
)
dist.all_gather_into_tensor(gathered_results, padded_results)
dec = z.new_empty(
(
decoded_core.shape[0],
decoded_core.shape[1],
decoded_core.shape[2],
out_h,
out_w,
)
)
for src_rank, positions in enumerate(gathered_positions):
h_start, h_end, w_start, w_end = [int(x.item()) for x in positions]
shape = tuple(int(x.item()) for x in gathered_dim_metadata[src_rank])
patch = gathered_results[src_rank][: prod(shape)].reshape(shape)
dec[:, :, :, h_start:h_end, w_start:w_end] = patch
return dec
def _merge_spatial_tiles(
self, tiles, blend_height, blend_width, stride_height, stride_width
) -> torch.Tensor:
"""Helper function to merge spatial tiles with blending"""
result_rows = []
for i, row in enumerate(tiles):
result_row = []
for j, tile in enumerate(row):
if i > 0:
tile = self.blend_v(tiles[i - 1][j], tile, blend_height)
if j > 0:
tile = self.blend_h(row[j - 1], tile, blend_width)
result_row.append(tile[:, :, :, :stride_height, :stride_width])
result_rows.append(torch.cat(result_row, dim=-1))
return torch.cat(result_rows, dim=-2)
def spatial_tiled_decode(self, z: torch.Tensor) -> torch.Tensor:
r"""
Decode a batch of images using a tiled decoder.
Args:
z (`torch.Tensor`): Input batch of latent vectors.
Returns:
`torch.Tensor`:
The decoded images.
"""
_, _, _, height, width = z.shape
# sample_height = height * self.spatial_compression_ratio
# sample_width = width * self.spatial_compression_ratio
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
tile_latent_stride_height = (
self.tile_sample_stride_height // self.spatial_compression_ratio
)
tile_latent_stride_width = (
self.tile_sample_stride_width // self.spatial_compression_ratio
)
blend_height = self.tile_sample_min_height - self.tile_sample_stride_height
blend_width = self.tile_sample_min_width - self.tile_sample_stride_width
# Split z into overlapping tiles and decode them separately.
# The tiles have an overlap to avoid seams between tiles.
rows = []
for i in range(0, height, tile_latent_stride_height):
row = []
for j in range(0, width, tile_latent_stride_width):
tile = z[
:,
:,
:,
i : i + tile_latent_min_height,
j : j + tile_latent_min_width,
]
decoded = self._decode(tile)
row.append(decoded)
rows.append(row)
return self._merge_spatial_tiles(
rows,
blend_height,
blend_width,
self.tile_sample_stride_height,
self.tile_sample_stride_width,
)
def tiled_encode(self, x: torch.Tensor) -> torch.Tensor:
_, _, num_frames, height, width = x.shape
# tile_latent_min_num_frames = self.tile_sample_min_num_frames // self.temporal_compression_ratio
tile_latent_stride_num_frames = (
self.tile_sample_stride_num_frames // self.temporal_compression_ratio
)
row = []
for i in range(0, num_frames, self.tile_sample_stride_num_frames):
tile = x[:, :, i : i + self.tile_sample_min_num_frames + 1, :, :]
if self.use_tiling and (
height > self.tile_sample_min_height
or width > self.tile_sample_min_width
):
tile = self.spatial_tiled_encode(tile)
else:
tile = self._encode(tile)
if i > 0:
tile = tile[:, :, 1:, :, :]
row.append(tile)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, self.blend_num_frames)
result_row.append(tile[:, :, :tile_latent_stride_num_frames, :, :])
else:
result_row.append(tile[:, :, : tile_latent_stride_num_frames + 1, :, :])
enc = torch.cat(result_row, dim=2)
return enc
def tiled_decode(self, z: torch.Tensor) -> torch.Tensor:
batch_size, num_channels, num_frames, height, width = z.shape
tile_latent_min_height = (
self.tile_sample_min_height // self.spatial_compression_ratio
)
tile_latent_min_width = (
self.tile_sample_min_width // self.spatial_compression_ratio
)
tile_latent_min_num_frames = (
self.tile_sample_min_num_frames // self.temporal_compression_ratio
)
tile_latent_stride_num_frames = (
self.tile_sample_stride_num_frames // self.temporal_compression_ratio
)
row = []
for i in range(0, num_frames, tile_latent_stride_num_frames):
tile = z[:, :, i : i + tile_latent_min_num_frames + 1, :, :]
if self.use_tiling and (
tile.shape[-1] > tile_latent_min_width
or tile.shape[-2] > tile_latent_min_height
):
decoded = self.spatial_tiled_decode(tile)
else:
decoded = self._decode(tile)
if i > 0:
decoded = decoded[:, :, 1:, :, :]
row.append(decoded)
result_row = []
for i, tile in enumerate(row):
if i > 0:
tile = self.blend_t(row[i - 1], tile, self.blend_num_frames)
result_row.append(
tile[:, :, : self.tile_sample_stride_num_frames, :, :]
)
else:
result_row.append(
tile[:, :, : self.tile_sample_stride_num_frames + 1, :, :]
)
dec = torch.cat(result_row, dim=2)
return dec
def enable_tiling(
self,
tile_sample_min_height: int | None = None,
tile_sample_min_width: int | None = None,
tile_sample_min_num_frames: int | None = None,
tile_sample_stride_height: int | None = None,
tile_sample_stride_width: int | None = None,
tile_sample_stride_num_frames: int | None = None,
blend_num_frames: int | None = None,
use_tiling: bool | None = None,
use_temporal_tiling: bool | None = None,
use_parallel_tiling: bool | None = None,
) -> None:
r"""
Enable tiled VAE decoding. When this option is enabled, the VAE will split the input tensor into tiles to
compute decoding and encoding in several steps. This is useful for saving a large amount of memory and to allow
processing larger images.
Args:
tile_sample_min_height (`int`, *optional*):
The minimum height required for a sample to be separated into tiles across the height dimension.
tile_sample_min_width (`int`, *optional*):
The minimum width required for a sample to be separated into tiles across the width dimension.
tile_sample_min_num_frames (`int`, *optional*):
The minimum number of frames required for a sample to be separated into tiles across the frame
dimension.
tile_sample_stride_height (`int`, *optional*):
The minimum amount of overlap between two consecutive vertical tiles. This is to ensure that there are
no tiling artifacts produced across the height dimension.
tile_sample_stride_width (`int`, *optional*):
The stride between two consecutive horizontal tiles. This is to ensure that there are no tiling
artifacts produced across the width dimension.
tile_sample_stride_num_frames (`int`, *optional*):
The stride between two consecutive frame tiles. This is to ensure that there are no tiling artifacts
produced across the frame dimension.
"""
self.use_tiling = True
self.tile_sample_min_height = (
tile_sample_min_height or self.tile_sample_min_height
)
self.tile_sample_min_width = tile_sample_min_width or self.tile_sample_min_width
self.tile_sample_min_num_frames = (
tile_sample_min_num_frames or self.tile_sample_min_num_frames
)
self.tile_sample_stride_height = (
tile_sample_stride_height or self.tile_sample_stride_height
)
self.tile_sample_stride_width = (
tile_sample_stride_width or self.tile_sample_stride_width
)
self.tile_sample_stride_num_frames = (
tile_sample_stride_num_frames or self.tile_sample_stride_num_frames
)
if blend_num_frames is not None:
self.blend_num_frames = blend_num_frames
else:
self.blend_num_frames = (
self.tile_sample_min_num_frames - self.tile_sample_stride_num_frames
)
self.use_tiling = use_tiling or self.use_tiling
self.use_temporal_tiling = use_temporal_tiling or self.use_temporal_tiling
self.use_parallel_tiling = use_parallel_tiling or self.use_parallel_tiling
def disable_tiling(self) -> None:
r"""
Disable tiled VAE decoding. If `enable_tiling` was previously enabled, this method will go back to computing
decoding in one step.
"""
self.use_tiling = False
# adapted from https://github.com/huggingface/diffusers/blob/e7ffeae0a191f710881d1fbde00cd6ff025e81f2/src/diffusers/models/autoencoders/vae.py#L691
class DiagonalGaussianDistribution:
def __init__(self, parameters: torch.Tensor, deterministic: bool = False):
self.parameters = parameters
self.mean, self.logvar = torch.chunk(parameters, 2, dim=1)
self.logvar = torch.clamp(self.logvar, -30.0, 20.0)
self.deterministic = deterministic
self.std = torch.exp(0.5 * self.logvar)
self.var = torch.exp(self.logvar)
if self.deterministic:
self.var = self.std = torch.zeros_like(
self.mean, device=self.parameters.device, dtype=self.parameters.dtype
)
def sample(self, generator: torch.Generator | None = None) -> torch.Tensor:
# make sure sample is on the same device as the parameters and has same dtype
sample = randn_tensor(
self.mean.shape,
generator=generator,
device=self.parameters.device,
dtype=self.parameters.dtype,
)
x = self.mean + self.std * sample
return x
def kl(
self,
other: Optional["DiagonalGaussianDistribution"] = None,
dims: tuple[int, ...] = (1, 2, 3),
) -> torch.Tensor:
if self.deterministic:
return torch.Tensor([0.0])
else:
if other is None:
return 0.5 * torch.sum(
torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
dim=dims,
)
else:
return 0.5 * torch.sum(
torch.pow(self.mean - other.mean, 2) / other.var
+ self.var / other.var
- 1.0
- self.logvar
+ other.logvar,
dim=dims,
)
def nll(
self, sample: torch.Tensor, dims: tuple[int, ...] = (1, 2, 3)
) -> torch.Tensor:
if self.deterministic:
return torch.Tensor([0.0])
logtwopi = np.log(2.0 * np.pi)
return 0.5 * torch.sum(
logtwopi + self.logvar + torch.pow(sample - self.mean, 2) / self.var,
dim=dims,
)
def mode(self) -> torch.Tensor:
return self.mean
@@ -0,0 +1,222 @@
# SPDX-License-Identifier: Apache-2.0
"""Decoder-only audio tokenizer for the Cosmos3 sound modality."""
from __future__ import annotations
import math
from typing import Any
import torch
from torch import nn
from torch.nn.utils import weight_norm
class Snake1d(nn.Module):
def __init__(self, hidden_dim: int, logscale: bool = True) -> None:
super().__init__()
self.alpha = nn.Parameter(torch.zeros(1, hidden_dim, 1))
self.beta = nn.Parameter(torch.zeros(1, hidden_dim, 1))
self.logscale = logscale
def forward(self, x: torch.Tensor) -> torch.Tensor:
shape = x.shape
alpha = torch.exp(self.alpha) if self.logscale else self.alpha
beta = torch.exp(self.beta) if self.logscale else self.beta
x = x.reshape(shape[0], shape[1], -1)
x = x + (beta + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
return x.reshape(shape)
class OobleckResidualUnit(nn.Module):
def __init__(self, dim: int, dilation: int = 1) -> None:
super().__init__()
pad = ((7 - 1) * dilation) // 2
self.snake1 = Snake1d(dim)
self.conv1 = weight_norm(
nn.Conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad)
)
self.snake2 = Snake1d(dim)
self.conv2 = weight_norm(nn.Conv1d(dim, dim, kernel_size=1))
def forward(self, x: torch.Tensor) -> torch.Tensor:
y = self.conv1(self.snake1(x))
y = self.conv2(self.snake2(y))
pad = (x.shape[-1] - y.shape[-1]) // 2
if pad > 0:
x = x[..., pad:-pad]
return x + y
class OobleckDecoderBlock(nn.Module):
def __init__(
self,
input_dim: int,
output_dim: int,
stride: int,
output_padding: int,
) -> None:
super().__init__()
self.snake1 = Snake1d(input_dim)
self.conv_t1 = weight_norm(
nn.ConvTranspose1d(
input_dim,
output_dim,
kernel_size=2 * stride,
stride=stride,
padding=math.ceil(stride / 2),
output_padding=output_padding,
)
)
self.res_unit1 = OobleckResidualUnit(output_dim, dilation=1)
self.res_unit2 = OobleckResidualUnit(output_dim, dilation=3)
self.res_unit3 = OobleckResidualUnit(output_dim, dilation=9)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.snake1(x)
x = self.conv_t1(x)
x = self.res_unit1(x)
x = self.res_unit2(x)
return self.res_unit3(x)
class OobleckDecoder(nn.Module):
def __init__(
self,
channels: int,
input_channels: int,
audio_channels: int,
upsampling_ratios: list[int],
channel_multiples: list[int],
) -> None:
super().__init__()
strides = upsampling_ratios
mults = [1] + list(channel_multiples)
self.conv1 = weight_norm(
nn.Conv1d(input_channels, channels * mults[-1], kernel_size=7, padding=3)
)
blocks = []
for i, stride in enumerate(strides):
blocks.append(
OobleckDecoderBlock(
input_dim=channels * mults[len(strides) - i],
output_dim=channels * mults[len(strides) - i - 1],
stride=stride,
output_padding=stride % 2,
)
)
self.block = nn.ModuleList(blocks)
self.snake1 = Snake1d(channels)
self.conv2 = weight_norm(
nn.Conv1d(channels, audio_channels, kernel_size=7, padding=3, bias=False)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.conv1(x)
for layer in self.block:
x = layer(x)
x = self.snake1(x)
return self.conv2(x)
def _cfg(config: dict[str, Any], *keys: str, default: Any = None) -> Any:
for k in keys:
v = config.get(k)
if v is not None:
return v
return default
class Cosmos3AVAEAudioTokenizer(nn.Module):
"""Cosmos3 audio tokenizer: latents → waveform via an Oobleck decoder stack."""
def __init__(self, config: dict[str, Any]) -> None:
super().__init__()
self.sample_rate = int(
_cfg(config, "sampling_rate", "sample_rate", default=48000)
)
self.audio_channels = int(
_cfg(
config,
"dec_out_channels",
"audio_channels",
default=2 if bool(config.get("stereo", True)) else 1,
)
)
self.latent_channels = int(
_cfg(config, "vocoder_input_dim", "io_channels", "latent_ch", default=64)
)
dec_strides = [
int(s) for s in _cfg(config, "dec_strides", default=[2, 4, 5, 6, 8])
]
self.hop_size = int(
_cfg(
config,
"hop_size",
default=math.prod(dec_strides) if dec_strides else 1920,
)
)
stride_product = math.prod(dec_strides)
if stride_product != self.hop_size:
raise ValueError(
"Cosmos3 AVAE dec_strides product must equal hop_size: "
f"product={stride_product}, hop_size={self.hop_size}."
)
norm = str(_cfg(config, "normalization_type", default="none"))
if bool(_cfg(config, "normalize_latents", default=False)) and norm == "none":
norm = "tanh"
self.normalization_type = norm
self.tanh_input_scale = float(_cfg(config, "tanh_input_scale", default=1.5))
self.tanh_output_scale = float(_cfg(config, "tanh_output_scale", default=3.5))
self.tanh_clamp = float(_cfg(config, "tanh_clamp", default=0.995))
self.decoder = OobleckDecoder(
channels=int(_cfg(config, "dec_dim", default=320)),
input_channels=self.latent_channels,
audio_channels=self.audio_channels,
upsampling_ratios=list(reversed(dec_strides)),
channel_multiples=list(
_cfg(config, "dec_c_mults", default=[1, 2, 4, 8, 16])
),
)
@property
def temporal_compression_factor(self) -> int:
return self.hop_size
def get_latent_num_samples(self, num_audio_samples: int) -> int:
return int(num_audio_samples) // self.hop_size
def get_audio_num_samples(self, num_latent_samples: int) -> int:
return int(num_latent_samples) * self.hop_size
def _denormalize_latent(self, latent: torch.Tensor) -> torch.Tensor:
if self.normalization_type == "tanh":
in_dtype = latent.dtype
x = torch.clamp(
latent.float() / self.tanh_output_scale,
-self.tanh_clamp,
self.tanh_clamp,
)
return (torch.atanh(x) * self.tanh_input_scale).to(in_dtype)
if self.normalization_type != "none":
raise ValueError(
f"Unsupported AVAE normalization_type={self.normalization_type!r}."
)
return latent
@torch.no_grad()
def decode(self, latent: torch.Tensor) -> torch.Tensor:
squeeze = latent.ndim == 2
if squeeze:
latent = latent.unsqueeze(0)
decoder_dtype = next(self.decoder.parameters()).dtype
decoder_device = next(self.decoder.parameters()).device
z = self._denormalize_latent(latent.to(decoder_device)).to(decoder_dtype)
audio = self.decoder(z).clamp(-1.0, 1.0).to(latent.dtype)
return audio.squeeze(0) if squeeze else audio
EntryClass = Cosmos3AVAEAudioTokenizer
@@ -0,0 +1,646 @@
# Copied and adapted from: https://github.com/descriptinc/descript-audio-codec
# SPDX-License-Identifier: MIT
import math
from bisect import bisect_right
from typing import Union
import torch
import torch.nn.functional as F
from einops import rearrange
from torch import nn
from sglang.multimodal_gen.configs.models.vaes.dac import DacVAEConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
DiagonalGaussianDistribution,
)
def _snake(x, alpha):
shape = x.shape
x = x.reshape(shape[0], shape[1], -1)
x = x + (alpha + 1e-9).reciprocal() * torch.sin(alpha * x).pow(2)
x = x.reshape(shape)
return x
# Scripting this brings model speed up 1.4x
snake = torch.jit.script(_snake)
# ROCm HIPRTC can fail to compile the scripted bf16 Snake kernel.
def _should_use_eager_snake_on_rocm_bf16(x: torch.Tensor, alpha: torch.Tensor) -> bool:
return (
torch.version.hip is not None
and (x.is_cuda or alpha.is_cuda)
and (x.dtype == torch.bfloat16 or alpha.dtype == torch.bfloat16)
)
class Snake1d(nn.Module):
def __init__(self, channels):
super().__init__()
self.alpha = nn.Parameter(torch.ones(1, channels, 1))
def forward(self, x):
if _should_use_eager_snake_on_rocm_bf16(x, self.alpha):
return _snake(x, self.alpha)
return snake(x, self.alpha)
class VectorQuantize(nn.Module):
"""
Implementation of VQ similar to Karpathy's repo:
https://github.com/karpathy/deep-vector-quantization
Additionally uses following tricks from Improved VQGAN
(https://arxiv.org/pdf/2110.04627.pdf):
1. Factorized codes: Perform nearest neighbor lookup in low-dimensional space
for improved codebook usage
2. l2-normalized codes: Converts euclidean distance to cosine similarity which
improves training stability
"""
def __init__(self, input_dim: int, codebook_size: int, codebook_dim: int):
super().__init__()
self.codebook_size = codebook_size
self.codebook_dim = codebook_dim
self.in_proj = nn.Conv1d(input_dim, codebook_dim, kernel_size=1)
self.out_proj = nn.Conv1d(codebook_dim, input_dim, kernel_size=1)
self.codebook = nn.Embedding(codebook_size, codebook_dim)
def forward(self, z):
"""Quantize the input tensor using a fixed codebook and return the corresponding codebook vectors.
Args:
z (torch.Tensor): Input tensor with shape ``[B, D, T]``.
Returns:
tuple: A tuple containing:
- z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
- commitment_loss (torch.Tensor): Commitment loss scalar to train encoder to predict
vectors closer to codebook entries.
- codebook_loss (torch.Tensor): Codebook loss scalar to update the codebook.
- indices (torch.Tensor): Codebook indices (quantized discrete representation) with shape ``[B, T]``.
- z_e (torch.Tensor): Projected latents (continuous representation before quantization) with shape ``[B, D, T]``.
"""
# Factorized codes (ViT-VQGAN) Project input into low-dimensional space
z_e = self.in_proj(z) # z_e : (B x D x T)
z_q, indices = self.decode_latents(z_e)
commitment_loss = F.mse_loss(z_e, z_q.detach(), reduction="none").mean([1, 2])
codebook_loss = F.mse_loss(z_q, z_e.detach(), reduction="none").mean([1, 2])
z_q = (
z_e + (z_q - z_e).detach()
) # noop in forward pass, straight-through gradient estimator in backward pass
z_q = self.out_proj(z_q)
return z_q, commitment_loss, codebook_loss, indices, z_e
def embed_code(self, embed_id):
return F.embedding(embed_id, self.codebook.weight)
def decode_code(self, embed_id):
return self.embed_code(embed_id).transpose(1, 2)
def decode_latents(self, latents):
encodings = rearrange(latents, "b d t -> (b t) d")
codebook = self.codebook.weight # codebook: (N x D)
# L2 normalize encodings and codebook (ViT-VQGAN)
encodings = F.normalize(encodings)
codebook = F.normalize(codebook)
# Compute euclidean distance with codebook
dist = (
encodings.pow(2).sum(1, keepdim=True)
- 2 * encodings @ codebook.t()
+ codebook.pow(2).sum(1, keepdim=True).t()
)
indices = rearrange((-dist).max(1)[1], "(b t) -> b t", b=latents.size(0))
z_q = self.decode_code(indices)
return z_q, indices
class ResidualVectorQuantize(nn.Module):
"""
Introduced in SoundStream: An end2end neural audio codec
https://arxiv.org/abs/2107.03312
"""
def __init__(
self,
input_dim: int = 512,
n_codebooks: int = 9,
codebook_size: int = 1024,
codebook_dim: Union[int, list] = 8,
quantizer_dropout: float = 0.0,
):
super().__init__()
if isinstance(codebook_dim, int):
codebook_dim = [codebook_dim for _ in range(n_codebooks)]
self.n_codebooks = n_codebooks
self.codebook_dim = codebook_dim
self.codebook_size = codebook_size
dim_offsets = [0]
for dim in self.codebook_dim:
dim_offsets.append(dim_offsets[-1] + dim)
self._codebook_dim_offsets = tuple(dim_offsets)
self.quantizers = nn.ModuleList(
[
VectorQuantize(input_dim, codebook_size, codebook_dim[i])
for i in range(n_codebooks)
]
)
self.quantizer_dropout = quantizer_dropout
def forward(self, z, n_quantizers: int = None):
"""Quantize the input tensor using a fixed set of codebooks and return the corresponding codebook vectors.
Args:
z (torch.Tensor): Input tensor with shape ``[B, D, T]``.
n_quantizers (int, optional): Number of quantizers to use. If ``None``,
all quantizers are used. When ``n_quantizers`` < ``self.n_codebooks``,
quantizer dropout is applied. Note: if ``self.quantizer_dropout`` > 0
and in training mode, this argument is ignored and a random number of
quantizers is used.
Returns:
tuple: A tuple containing:
- z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
- codes (torch.Tensor): Codebook indices for each codebook with shape ``[B, N, T]``
(quantized discrete representation of input).
- latents (torch.Tensor): Projected latents with shape ``[B, N*D, T]``
(continuous representation before quantization).
- commitment_loss (torch.Tensor): Commitment loss scalar to train encoder to predict
vectors closer to codebook entries.
- codebook_loss (torch.Tensor): Codebook loss scalar to update the codebook.
"""
z_q = 0
residual = z
commitment_loss = 0
codebook_loss = 0
codebook_indices = []
latents = []
if n_quantizers is None:
n_quantizers = self.n_codebooks
quantizers = self.quantizers
if self.training:
batch_size = z.shape[0]
device = z.device
n_quantizers = torch.full(
(batch_size,),
self.n_codebooks + 1,
device=device,
dtype=torch.long,
)
if self.quantizer_dropout > 0:
dropout = torch.randint(
1,
self.n_codebooks + 1,
(batch_size,),
device=device,
)
n_dropout = int(batch_size * self.quantizer_dropout)
if n_dropout > 0:
n_quantizers[:n_dropout] = dropout[:n_dropout]
for i, quantizer in enumerate(quantizers):
z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(
residual
)
# Create mask to apply quantizer dropout
mask = i < n_quantizers
z_q = z_q + z_q_i * mask[:, None, None]
residual = residual - z_q_i
# Sum losses
commitment_loss += (commitment_loss_i * mask).mean()
codebook_loss += (codebook_loss_i * mask).mean()
codebook_indices.append(indices_i)
latents.append(z_e_i)
else:
for i, quantizer in enumerate(quantizers):
if i >= n_quantizers:
break
z_q_i, commitment_loss_i, codebook_loss_i, indices_i, z_e_i = quantizer(
residual
)
z_q = z_q + z_q_i
residual = residual - z_q_i
commitment_loss += commitment_loss_i.mean()
codebook_loss += codebook_loss_i.mean()
codebook_indices.append(indices_i)
latents.append(z_e_i)
codes = torch.stack(codebook_indices, dim=1)
latents = torch.cat(latents, dim=1)
return z_q, codes, latents, commitment_loss, codebook_loss
def from_codes(self, codes: torch.Tensor):
"""Reconstruct the continuous representation from quantized codes.
Args:
codes (torch.Tensor): Quantized discrete representation with shape ``[B, N, T]``.
Returns:
tuple: A tuple containing:
- z_q (torch.Tensor): Quantized continuous representation with shape ``[B, D, T]``.
- z_p (torch.Tensor): Concatenated latent space representation with shape ``[B, N*D, T]``.
- codes (torch.Tensor): Original input codebook indices with shape ``[B, N, T]``.
"""
z_q = 0.0
z_p = []
n_codebooks = codes.shape[1]
for i in range(n_codebooks):
z_p_i = self.quantizers[i].decode_code(codes[:, i, :])
z_p.append(z_p_i)
z_q_i = self.quantizers[i].out_proj(z_p_i)
z_q = z_q + z_q_i
return z_q, torch.cat(z_p, dim=1), codes
def from_latents(self, latents: torch.Tensor):
"""Reconstruct the continuous representation from unquantized latents.
Args:
latents (torch.Tensor): Continuous representation after projection with shape ``[B, N*D, T]``.
Returns:
tuple: A tuple containing:
- z_q (torch.Tensor): Quantized representation of full-projected space with shape ``[B, D, T]``.
- z_p (torch.Tensor): Quantized representation of latent space with shape ``[B, N*D, T]``.
- codes (torch.Tensor): Codebook indices with shape ``[B, N, T]``.
"""
z_q = 0
z_p = []
codes = []
dims = self._codebook_dim_offsets
n_codebooks = bisect_right(dims, latents.shape[1]) - 1
for i in range(n_codebooks):
j, k = dims[i], dims[i + 1]
z_p_i, codes_i = self.quantizers[i].decode_latents(latents[:, j:k, :])
z_p.append(z_p_i)
codes.append(codes_i)
z_q_i = self.quantizers[i].out_proj(z_p_i)
z_q = z_q + z_q_i
return z_q, torch.cat(z_p, dim=1), torch.stack(codes, dim=1)
class ResidualUnit(nn.Module):
def __init__(self, dim: int = 16, dilation: int = 1):
super().__init__()
pad = ((7 - 1) * dilation) // 2
self.block = nn.Sequential(
Snake1d(dim),
nn.Conv1d(dim, dim, kernel_size=7, dilation=dilation, padding=pad),
Snake1d(dim),
nn.Conv1d(dim, dim, kernel_size=1),
)
def forward(self, x):
y = self.block(x)
pad = (x.shape[-1] - y.shape[-1]) // 2
if pad > 0:
x = x[..., pad:-pad]
return x + y
class EncoderBlock(nn.Module):
def __init__(self, dim: int = 16, stride: int = 1):
super().__init__()
self.block = nn.Sequential(
ResidualUnit(dim // 2, dilation=1),
ResidualUnit(dim // 2, dilation=3),
ResidualUnit(dim // 2, dilation=9),
Snake1d(dim // 2),
nn.Conv1d(
dim // 2,
dim,
kernel_size=2 * stride,
stride=stride,
padding=math.ceil(stride / 2),
),
)
def forward(self, x):
return self.block(x)
class Encoder(nn.Module):
def __init__(
self,
d_model: int = 64,
strides: list = [2, 4, 8, 8],
d_latent: int = 64,
):
super().__init__()
# Create first convolution
self.block = [nn.Conv1d(1, d_model, kernel_size=7, padding=3)]
# Create EncoderBlocks that double channels as they downsample by `stride`
for stride in strides:
d_model *= 2
self.block += [EncoderBlock(d_model, stride=stride)]
# Create last convolution
self.block += [
Snake1d(d_model),
nn.Conv1d(d_model, d_latent, kernel_size=3, padding=1),
]
# Wrap black into nn.Sequential
self.block = nn.Sequential(*self.block)
self.enc_dim = d_model
def forward(self, x):
return self.block(x)
class DecoderBlock(nn.Module):
def __init__(self, input_dim: int = 16, output_dim: int = 8, stride: int = 1):
super().__init__()
self.block = nn.Sequential(
Snake1d(input_dim),
nn.ConvTranspose1d(
input_dim,
output_dim,
kernel_size=2 * stride,
stride=stride,
padding=math.ceil(stride / 2),
output_padding=stride % 2,
),
ResidualUnit(output_dim, dilation=1),
ResidualUnit(output_dim, dilation=3),
ResidualUnit(output_dim, dilation=9),
)
def forward(self, x):
return self.block(x)
class Decoder(nn.Module):
def __init__(
self,
input_channel,
channels,
rates,
d_out: int = 1,
):
super().__init__()
# Add first conv layer
layers = [nn.Conv1d(input_channel, channels, kernel_size=7, padding=3)]
# Add upsampling + MRF blocks
for i, stride in enumerate(rates):
input_dim = channels // 2**i
output_dim = channels // 2 ** (i + 1)
layers += [DecoderBlock(input_dim, output_dim, stride)]
# Add final conv layer
layers += [
Snake1d(output_dim),
nn.Conv1d(output_dim, d_out, kernel_size=7, padding=3),
nn.Tanh(),
]
self.model = nn.Sequential(*layers)
def forward(self, x):
return self.model(x)
class DAC(nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = ["encoder.block", "decoder.model"]
def __init__(
self,
config: DacVAEConfig,
):
super().__init__()
self.continuous = config.continuous
self.decoder_dim = config.decoder_dim
self.decoder_rates = config.decoder_rates
self.encoder_dim = config.encoder_dim
self.encoder_rates = config.encoder_rates
self.hop_length = math.prod(config.encoder_rates)
self.sample_rate = config.sample_rate
if config.latent_dim is None:
latent_dim = config.encoder_dim * (2 ** len(config.encoder_rates))
else:
latent_dim = config.latent_dim
self.latent_dim = latent_dim
if config.load_encoder:
self.encoder = Encoder(config.encoder_dim, config.encoder_rates, latent_dim)
if not config.continuous:
self.n_codebooks = config.n_codebooks
self.codebook_size = config.codebook_size
self.codebook_dim = config.codebook_dim
self.quantizer = ResidualVectorQuantize(
input_dim=latent_dim,
n_codebooks=config.n_codebooks,
codebook_size=config.codebook_size,
codebook_dim=config.codebook_dim,
quantizer_dropout=config.quantizer_dropout,
)
else:
self.quant_conv = torch.nn.Conv1d(latent_dim, 2 * latent_dim, 1)
self.post_quant_conv = torch.nn.Conv1d(latent_dim, latent_dim, 1)
if config.load_decoder:
self.decoder = Decoder(
latent_dim,
config.decoder_dim,
config.decoder_rates,
)
self.apply(self.init_weights)
@staticmethod
def init_weights(m):
if isinstance(m, nn.Conv1d):
nn.init.trunc_normal_(m.weight, std=0.02)
nn.init.constant_(m.bias, 0)
@property
def dtype(self):
return next(self.parameters()).dtype
@property
def device(self):
return next(self.parameters()).device
def preprocess(self, audio_data, sample_rate):
if sample_rate is None:
sample_rate = self.sample_rate
assert sample_rate == self.sample_rate
length = audio_data.shape[-1]
right_pad = math.ceil(length / self.hop_length) * self.hop_length - length
audio_data = nn.functional.pad(audio_data, (0, right_pad))
return audio_data
def encode(
self,
audio_data: torch.Tensor,
n_quantizers: int = None,
):
"""Encode audio data into latent representations.
This method processes audio through the encoder network and optionally applies
vector quantization (in VQ mode) or projects to a Gaussian distribution (in
continuous mode) to produce latent representations.
Args:
audio_data (torch.Tensor): Audio data to encode, with shape ``[B, 1, T]``.
n_quantizers (int, optional): Number of quantizers to use. If ``None``,
all quantizers are used. Only applicable in VQ mode (``continuous=False``).
Returns:
tuple: A tuple containing:
- z (torch.Tensor): Encoded representation. In VQ mode, this is the
quantized continuous representation with shape ``[B, D, T]``. In
continuous mode, this is a ``DiagonalGaussianDistribution`` object.
- codes (torch.Tensor or None): Codebook indices with shape ``[B, N, T]``
in VQ mode, ``None`` in continuous mode.
- latents (torch.Tensor or None): Projected latents with shape ``[B, N*D, T]``
in VQ mode, ``None`` in continuous mode.
- commitment_loss (torch.Tensor): Commitment loss scalar.
- codebook_loss (torch.Tensor): Codebook loss scalar.
Note:
In continuous mode, the encoded representation is projected through a
quantization convolution layer and wrapped in a ``DiagonalGaussianDistribution``
for VAE training.
"""
z = self.encoder(audio_data) # [B x D x T]
if not self.continuous:
z, codes, latents, commitment_loss, codebook_loss = self.quantizer(
z, n_quantizers
)
else:
z = self.quant_conv(z) # [B x 2D x T]
z = DiagonalGaussianDistribution(z)
codes, latents, commitment_loss, codebook_loss = None, None, 0, 0
return z, codes, latents, commitment_loss, codebook_loss
def decode(self, z: torch.Tensor):
"""Decode latent representations back to audio waveforms.
This method takes latent representations (either quantized from VQ mode or sampled
from the posterior in continuous mode) and reconstructs the corresponding audio
through the decoder network.
Args:
z (torch.Tensor): Latent representation to decode, with shape ``[B, D, T]``.
In VQ mode (``continuous=False``), this is the quantized continuous
representation. In continuous mode (``continuous=True``), this is sampled
from the posterior distribution.
Returns:
torch.Tensor: Decoded audio data with shape ``[B, 1, T']``. The output length
T' is determined by the decoder's upsampling rates and may differ from the
input temporal dimension T.
Note:
In continuous mode (``continuous=True``), the input is first passed through
a post-quantization convolution layer before being fed to the decoder.
"""
if not self.continuous:
audio = self.decoder(z)
else:
z = self.post_quant_conv(z)
audio = self.decoder(z)
return audio
def forward(
self,
audio_data: torch.Tensor,
sample_rate: int = None,
n_quantizers: int = None,
):
"""Model forward pass.
Args:
audio_data (torch.Tensor): Audio to encode, shape [B, 1, T].
sample_rate (int, optional): Sample rate in Hz. Defaults to
``self.sample_rate`` when ``None``.
n_quantizers (int, optional): Number of quantizers to use. When ``None``,
all quantizers are used. Only used in VQ mode (``continuous=False``).
Returns:
dict: A dictionary containing different keys depending on the mode:
**VQ Mode (``continuous=False``):**
- "audio" (torch.Tensor): Decoded audio, shape [B, 1, length].
- "z" (torch.Tensor): Quantized continuous representation, shape [B, D, T].
- "codes" (torch.Tensor): Codebook indices, shape [B, N, T].
- "latents" (torch.Tensor): Projected latents, shape [B, N*D, T].
- "vq/commitment_loss" (torch.Tensor): Commitment loss.
- "vq/codebook_loss" (torch.Tensor): Codebook loss.
**Continuous Mode (``continuous=True``):**
- "audio" (torch.Tensor): Decoded audio, shape [B, 1, length].
- "z" (torch.Tensor): Latent representation, shape [B, D, T].
- "kl_loss" (torch.Tensor): KL divergence loss (for VAE training).
"""
length = audio_data.shape[-1]
audio_data = self.preprocess(audio_data, sample_rate)
if not self.continuous:
z, codes, latents, commitment_loss, codebook_loss = self.encode(
audio_data, n_quantizers
)
x = self.decode(z)
return {
"audio": x[..., :length],
"z": z,
"codes": codes,
"latents": latents,
"vq/commitment_loss": commitment_loss,
"vq/codebook_loss": codebook_loss,
}
else:
posterior, _, _, _, _ = self.encode(audio_data, n_quantizers)
z = posterior.sample()
x = self.decode(z)
kl_loss = posterior.kl(dims=(1, 2))
kl_loss = kl_loss.mean()
return {
"audio": x[..., :length],
"z": z,
"kl_loss": kl_loss,
}
EntryClass = DAC
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,978 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0
# Adapted from diffusers
# Copyright 2024 The Hunyuan Team, The HuggingFace Team and The sglang-diffusion Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.jit_kernel.diffusion.group_norm_silu import apply_group_norm_silu
from sglang.multimodal_gen.configs.models.vaes import HunyuanVAEConfig
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.activation import get_act_fn
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelConv3d,
chunk_height_by_sizes,
disable_spatial_parallel_decode,
gather_and_trim_height,
gather_variable_height,
split_height_for_parallel_decode,
)
from sglang.multimodal_gen.runtime.models.vaes.common import (
ParallelTiledVAE,
can_install_spatial_shard_parallel_decode,
should_run_spatial_shard_parallel_decode,
)
def prepare_causal_attention_mask(
num_frames: int,
height_width: int,
dtype: torch.dtype,
device: torch.device,
batch_size: int | None = None,
) -> torch.Tensor:
indices = torch.arange(1, num_frames + 1, dtype=torch.int32, device=device)
indices_blocks = indices.repeat_interleave(height_width)
x, y = torch.meshgrid(indices_blocks, indices_blocks, indexing="xy")
mask = torch.where(x <= y, 0, -float("inf")).to(dtype=dtype)
if batch_size is not None:
mask = mask.unsqueeze(0).expand(batch_size, -1, -1)
return mask
def _make_spatial_parallel_conv3d(
conv: nn.Conv3d,
*,
height_pad: int,
width_pad: int,
padding_mode: str,
) -> SpatialParallelConv3d:
spatial_conv = SpatialParallelConv3d(
in_channels=conv.in_channels,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=(0, height_pad, width_pad),
dilation=conv.dilation,
groups=conv.groups,
bias=conv.bias is not None,
padding_mode=padding_mode,
)
spatial_conv.weight = conv.weight
spatial_conv.bias = conv.bias
return spatial_conv
def _apply_group_norm_silu(
hidden_states: torch.Tensor,
norm: nn.GroupNorm,
nonlinearity: nn.Module,
spatial_parallel: bool,
) -> torch.Tensor:
if not spatial_parallel:
return apply_group_norm_silu(hidden_states, norm, nonlinearity)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = apply_group_norm_silu(
hidden_states.contiguous(), norm, nonlinearity
)
return chunk_height_by_sizes(hidden_states, heights)
class HunyuanVAEAttention(nn.Module):
def __init__(
self, in_channels, heads, dim_head, eps, norm_num_groups, bias
) -> None:
super().__init__()
self.in_channels = in_channels
self.heads = heads
self.dim_head = dim_head
self.eps = eps
self.norm_num_groups = norm_num_groups
self.bias = bias
inner_dim = heads * dim_head
# Define the projection layers
self.to_q = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_k = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_v = nn.Linear(in_channels, inner_dim, bias=bias)
self.to_out = nn.Sequential(nn.Linear(inner_dim, in_channels, bias=bias))
# Optional normalization layers
self.group_norm = nn.GroupNorm(
norm_num_groups, in_channels, eps=eps, affine=True
)
def forward(
self, hidden_states: torch.Tensor, attention_mask: torch.Tensor | None = None
) -> torch.Tensor:
residual = hidden_states
batch_size, sequence_length, _ = hidden_states.shape
hidden_states = self.group_norm(hidden_states.transpose(1, 2)).transpose(1, 2)
# Project to query, key, value
query = self.to_q(hidden_states)
key = self.to_k(hidden_states)
value = self.to_v(hidden_states)
# Reshape for multi-head attention
head_dim = self.dim_head
query = query.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
key = key.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
value = value.view(batch_size, -1, self.heads, head_dim).transpose(1, 2)
# Perform scaled dot-product attention
hidden_states = F.scaled_dot_product_attention(
query, key, value, attn_mask=attention_mask, dropout_p=0.0, is_causal=False
)
# Reshape back
hidden_states = hidden_states.transpose(1, 2).reshape(
batch_size, -1, self.heads * head_dim
)
hidden_states = hidden_states.to(query.dtype)
# Linear projection
hidden_states = self.to_out(hidden_states)
# Residual connection and rescale
hidden_states = hidden_states + residual
return hidden_states
class HunyuanVideoCausalConv3d(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
kernel_size: int | tuple[int, int, int] = 3,
stride: int | tuple[int, int, int] = 1,
padding: int | tuple[int, int, int] = 0,
dilation: int | tuple[int, int, int] = 1,
bias: bool = True,
pad_mode: str = "replicate",
) -> None:
super().__init__()
kernel_size = (
(kernel_size, kernel_size, kernel_size)
if isinstance(kernel_size, int)
else kernel_size
)
self.pad_mode = pad_mode
self.time_causal_padding = (
kernel_size[0] // 2,
kernel_size[0] // 2,
kernel_size[1] // 2,
kernel_size[1] // 2,
kernel_size[2] - 1,
0,
)
self.spatial_parallel_time_padding = (
0,
0,
0,
0,
kernel_size[2] - 1,
0,
)
self.spatial_parallel = False
self.conv = nn.Conv3d(
in_channels, out_channels, kernel_size, stride, padding, dilation, bias=bias
)
def enable_spatial_parallel(self) -> None:
if isinstance(self.conv, SpatialParallelConv3d):
self.spatial_parallel = True
return
if self.conv.kernel_size == (1, 1, 1):
self.spatial_parallel = True
return
self.conv = _make_spatial_parallel_conv3d(
self.conv,
height_pad=self.time_causal_padding[2],
width_pad=self.time_causal_padding[0],
padding_mode=self.pad_mode,
)
self.spatial_parallel = True
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
padding = (
self.spatial_parallel_time_padding
if self.spatial_parallel
else self.time_causal_padding
)
if any(padding):
hidden_states = F.pad(hidden_states, padding, mode=self.pad_mode)
return self.conv(hidden_states)
class HunyuanVideoUpsampleCausal3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int | None = None,
kernel_size: int = 3,
stride: int = 1,
bias: bool = True,
upsample_factor: tuple[int, ...] = (2, 2, 2),
) -> None:
super().__init__()
out_channels = out_channels or in_channels
self.upsample_factor = upsample_factor
self.conv = HunyuanVideoCausalConv3d(
in_channels, out_channels, kernel_size, stride, bias=bias
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
num_frames = hidden_states.size(2)
first_frame, other_frames = hidden_states.split((1, num_frames - 1), dim=2)
first_frame = F.interpolate(
first_frame.squeeze(2),
scale_factor=self.upsample_factor[1:],
mode="nearest",
).unsqueeze(2)
if num_frames > 1:
# See: https://github.com/pytorch/pytorch/issues/81665
# Unless you have a version of pytorch where non-contiguous implementation of F.interpolate
# is fixed, this will raise either a runtime error, or fail silently with bad outputs.
# If you are encountering an error here, make sure to try running encoding/decoding with
# `vae.enable_tiling()` first. If that doesn't work, open an issue at:
# https://github.com/huggingface/diffusers/issues
other_frames = other_frames.contiguous()
other_frames = F.interpolate(
other_frames, scale_factor=self.upsample_factor, mode="nearest"
)
hidden_states = torch.cat((first_frame, other_frames), dim=2)
else:
hidden_states = first_frame
hidden_states = self.conv(hidden_states)
return hidden_states
class HunyuanVideoDownsampleCausal3D(nn.Module):
def __init__(
self,
channels: int,
out_channels: int | None = None,
padding: int = 1,
kernel_size: int = 3,
bias: bool = True,
stride=2,
) -> None:
super().__init__()
out_channels = out_channels or channels
self.conv = HunyuanVideoCausalConv3d(
channels, out_channels, kernel_size, stride, padding, bias=bias
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv(hidden_states)
return hidden_states
class HunyuanVideoResnetBlockCausal3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int | None = None,
dropout: float = 0.0,
groups: int = 32,
eps: float = 1e-6,
non_linearity: str = "silu",
) -> None:
super().__init__()
out_channels = out_channels or in_channels
self.nonlinearity = get_act_fn(non_linearity)
self.norm1 = nn.GroupNorm(groups, in_channels, eps=eps, affine=True)
self.conv1 = HunyuanVideoCausalConv3d(in_channels, out_channels, 3, 1, 0)
self.norm2 = nn.GroupNorm(groups, out_channels, eps=eps, affine=True)
self.dropout = nn.Dropout(dropout)
self.conv2 = HunyuanVideoCausalConv3d(out_channels, out_channels, 3, 1, 0)
self.conv_shortcut = None
if in_channels != out_channels:
self.conv_shortcut = HunyuanVideoCausalConv3d(
in_channels, out_channels, 1, 1, 0
)
self.spatial_parallel = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = hidden_states.contiguous()
residual = hidden_states
hidden_states = _apply_group_norm_silu(
hidden_states, self.norm1, self.nonlinearity, self.spatial_parallel
)
hidden_states = self.conv1(hidden_states)
hidden_states = _apply_group_norm_silu(
hidden_states, self.norm2, self.nonlinearity, self.spatial_parallel
)
hidden_states = self.dropout(hidden_states)
hidden_states = self.conv2(hidden_states)
if self.conv_shortcut is not None:
residual = self.conv_shortcut(residual)
hidden_states = hidden_states + residual
return hidden_states
class HunyuanVideoMidBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_attention: bool = True,
attention_head_dim: int = 1,
) -> None:
super().__init__()
resnet_groups = (
resnet_groups if resnet_groups is not None else min(in_channels // 4, 32)
)
self.add_attention = add_attention
# There is always at least one resnet
resnets = [
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=in_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
]
attentions: list[HunyuanVAEAttention | None] = []
for _ in range(num_layers):
if self.add_attention:
attentions.append(
HunyuanVAEAttention(
in_channels,
heads=in_channels // attention_head_dim,
dim_head=attention_head_dim,
eps=resnet_eps,
norm_num_groups=resnet_groups,
bias=True,
)
)
else:
attentions.append(None)
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=in_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.attentions = nn.ModuleList(attentions)
self.resnets = nn.ModuleList(resnets)
self.spatial_parallel = False
self.gradient_checkpointing = False
def _run_attention(
self, attn: HunyuanVAEAttention, hidden_states: torch.Tensor
) -> torch.Tensor:
heights = None
if self.spatial_parallel:
hidden_states, heights = gather_variable_height(hidden_states)
batch_size, num_channels, num_frames, height, width = hidden_states.shape
hidden_states = hidden_states.permute(0, 2, 3, 4, 1).flatten(1, 3)
attention_mask = prepare_causal_attention_mask(
num_frames,
height * width,
hidden_states.dtype,
hidden_states.device,
batch_size=batch_size,
)
hidden_states = attn(hidden_states, attention_mask=attention_mask)
hidden_states = hidden_states.unflatten(1, (num_frames, height, width)).permute(
0, 4, 1, 2, 3
)
if heights is not None:
hidden_states = chunk_height_by_sizes(hidden_states, heights)
return hidden_states
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
hidden_states = self._gradient_checkpointing_func(
self.resnets[0], hidden_states
)
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
hidden_states = self._run_attention(attn, hidden_states)
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
hidden_states = self.resnets[0](hidden_states)
for attn, resnet in zip(self.attentions, self.resnets[1:], strict=True):
if attn is not None:
hidden_states = self._run_attention(attn, hidden_states)
hidden_states = resnet(hidden_states)
return hidden_states
class HunyuanVideoDownBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_downsample: bool = True,
downsample_stride: tuple[int, ...] | int = 2,
downsample_padding: int = 1,
) -> None:
super().__init__()
resnets = []
for i in range(num_layers):
in_channels = in_channels if i == 0 else out_channels
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=in_channels,
out_channels=out_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.resnets = nn.ModuleList(resnets)
if add_downsample:
self.downsamplers = nn.ModuleList(
[
HunyuanVideoDownsampleCausal3D(
out_channels,
out_channels=out_channels,
padding=downsample_padding,
stride=downsample_stride,
)
]
)
else:
self.downsamplers = None
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
for resnet in self.resnets:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
for resnet in self.resnets:
hidden_states = resnet(hidden_states)
if self.downsamplers is not None:
for downsampler in self.downsamplers:
hidden_states = downsampler(hidden_states)
return hidden_states
class HunyuanVideoUpBlock3D(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
dropout: float = 0.0,
num_layers: int = 1,
resnet_eps: float = 1e-6,
resnet_act_fn: str = "silu",
resnet_groups: int = 32,
add_upsample: bool = True,
upsample_scale_factor: tuple[int, ...] = (2, 2, 2),
) -> None:
super().__init__()
resnets = []
for i in range(num_layers):
input_channels = in_channels if i == 0 else out_channels
resnets.append(
HunyuanVideoResnetBlockCausal3D(
in_channels=input_channels,
out_channels=out_channels,
eps=resnet_eps,
groups=resnet_groups,
dropout=dropout,
non_linearity=resnet_act_fn,
)
)
self.resnets = nn.ModuleList(resnets)
if add_upsample:
self.upsamplers = nn.ModuleList(
[
HunyuanVideoUpsampleCausal3D(
out_channels,
out_channels=out_channels,
upsample_factor=upsample_scale_factor,
)
]
)
else:
self.upsamplers = None
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
if torch.is_grad_enabled() and self.gradient_checkpointing:
for resnet in self.resnets:
hidden_states = self._gradient_checkpointing_func(resnet, hidden_states)
else:
for resnet in self.resnets:
hidden_states = resnet(hidden_states)
if self.upsamplers is not None:
for upsampler in self.upsamplers:
hidden_states = upsampler(hidden_states)
return hidden_states
class HunyuanVideoEncoder3D(nn.Module):
r"""
Causal encoder for 3D video-like data introduced in [Hunyuan Video](https://huggingface.co/papers/2412.03603).
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
down_block_types: tuple[str, ...] = (
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
"HunyuanVideoDownBlock3D",
),
block_out_channels: tuple[int, ...] = (128, 256, 512, 512),
layers_per_block: int = 2,
norm_num_groups: int = 32,
act_fn: str = "silu",
double_z: bool = True,
mid_block_add_attention=True,
temporal_compression_ratio: int = 4,
spatial_compression_ratio: int = 8,
) -> None:
super().__init__()
self.conv_in = HunyuanVideoCausalConv3d(
in_channels, block_out_channels[0], kernel_size=3, stride=1
)
self.mid_block: HunyuanVideoMidBlock3D | None = None
self.down_blocks = nn.ModuleList([])
output_channel = block_out_channels[0]
for i, down_block_type in enumerate(down_block_types):
if down_block_type != "HunyuanVideoDownBlock3D":
raise ValueError(f"Unsupported down_block_type: {down_block_type}")
input_channel = output_channel
output_channel = block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
num_spatial_downsample_layers = int(np.log2(spatial_compression_ratio))
num_time_downsample_layers = int(np.log2(temporal_compression_ratio))
if temporal_compression_ratio == 4:
add_spatial_downsample = bool(i < num_spatial_downsample_layers)
add_time_downsample = bool(
i >= (len(block_out_channels) - 1 - num_time_downsample_layers)
and not is_final_block
)
elif temporal_compression_ratio == 8:
add_spatial_downsample = bool(i < num_spatial_downsample_layers)
add_time_downsample = bool(i < num_time_downsample_layers)
else:
raise ValueError(
f"Unsupported time_compression_ratio: {temporal_compression_ratio}"
)
downsample_stride_HW = (2, 2) if add_spatial_downsample else (1, 1)
downsample_stride_T = (2,) if add_time_downsample else (1,)
downsample_stride = tuple(downsample_stride_T + downsample_stride_HW)
down_block = HunyuanVideoDownBlock3D(
num_layers=layers_per_block,
in_channels=input_channel,
out_channels=output_channel,
add_downsample=bool(add_spatial_downsample or add_time_downsample),
resnet_eps=1e-6,
resnet_act_fn=act_fn,
resnet_groups=norm_num_groups,
downsample_stride=downsample_stride,
downsample_padding=0,
)
self.down_blocks.append(down_block)
self.mid_block = HunyuanVideoMidBlock3D(
in_channels=block_out_channels[-1],
resnet_eps=1e-6,
resnet_act_fn=act_fn,
attention_head_dim=block_out_channels[-1],
resnet_groups=norm_num_groups,
add_attention=mid_block_add_attention,
)
self.conv_norm_out = nn.GroupNorm(
num_channels=block_out_channels[-1], num_groups=norm_num_groups, eps=1e-6
)
self.conv_act = nn.SiLU()
conv_out_channels = 2 * out_channels if double_z else out_channels
self.conv_out = HunyuanVideoCausalConv3d(
block_out_channels[-1], conv_out_channels, kernel_size=3
)
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv_in(hidden_states)
if torch.is_grad_enabled() and self.gradient_checkpointing:
for down_block in self.down_blocks:
hidden_states = self._gradient_checkpointing_func(
down_block, hidden_states
)
hidden_states = self._gradient_checkpointing_func(
self.mid_block, hidden_states
)
else:
for down_block in self.down_blocks:
hidden_states = down_block(hidden_states)
assert self.mid_block is not None
hidden_states = self.mid_block(hidden_states)
hidden_states = apply_group_norm_silu(
hidden_states, self.conv_norm_out, self.conv_act
)
hidden_states = self.conv_out(hidden_states)
return hidden_states
class HunyuanVideoDecoder3D(nn.Module):
r"""
Causal decoder for 3D video-like data introduced in [Hunyuan Video](https://huggingface.co/papers/2412.03603).
"""
def __init__(
self,
in_channels: int = 3,
out_channels: int = 3,
up_block_types: tuple[str, ...] = (
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
"HunyuanVideoUpBlock3D",
),
block_out_channels: tuple[int, ...] = (128, 256, 512, 512),
layers_per_block: int = 2,
norm_num_groups: int = 32,
act_fn: str = "silu",
mid_block_add_attention=True,
time_compression_ratio: int = 4,
spatial_compression_ratio: int = 8,
):
super().__init__()
self.layers_per_block = layers_per_block
self.conv_in = HunyuanVideoCausalConv3d(
in_channels, block_out_channels[-1], kernel_size=3, stride=1
)
self.up_blocks = nn.ModuleList([])
# mid
self.mid_block = HunyuanVideoMidBlock3D(
in_channels=block_out_channels[-1],
resnet_eps=1e-6,
resnet_act_fn=act_fn,
attention_head_dim=block_out_channels[-1],
resnet_groups=norm_num_groups,
add_attention=mid_block_add_attention,
)
# up
reversed_block_out_channels = list(reversed(block_out_channels))
output_channel = reversed_block_out_channels[0]
for i, up_block_type in enumerate(up_block_types):
if up_block_type != "HunyuanVideoUpBlock3D":
raise ValueError(f"Unsupported up_block_type: {up_block_type}")
prev_output_channel = output_channel
output_channel = reversed_block_out_channels[i]
is_final_block = i == len(block_out_channels) - 1
num_spatial_upsample_layers = int(np.log2(spatial_compression_ratio))
num_time_upsample_layers = int(np.log2(time_compression_ratio))
if time_compression_ratio == 4:
add_spatial_upsample = bool(i < num_spatial_upsample_layers)
add_time_upsample = bool(
i >= len(block_out_channels) - 1 - num_time_upsample_layers
and not is_final_block
)
else:
raise ValueError(
f"Unsupported time_compression_ratio: {time_compression_ratio}"
)
upsample_scale_factor_HW = (2, 2) if add_spatial_upsample else (1, 1)
upsample_scale_factor_T = (2,) if add_time_upsample else (1,)
upsample_scale_factor = tuple(
upsample_scale_factor_T + upsample_scale_factor_HW
)
up_block = HunyuanVideoUpBlock3D(
num_layers=self.layers_per_block + 1,
in_channels=prev_output_channel,
out_channels=output_channel,
add_upsample=bool(add_spatial_upsample or add_time_upsample),
upsample_scale_factor=upsample_scale_factor,
resnet_eps=1e-6,
resnet_act_fn=act_fn,
resnet_groups=norm_num_groups,
)
self.up_blocks.append(up_block)
prev_output_channel = output_channel
# out
self.conv_norm_out = nn.GroupNorm(
num_channels=block_out_channels[0], num_groups=norm_num_groups, eps=1e-6
)
self.conv_act = nn.SiLU()
self.conv_out = HunyuanVideoCausalConv3d(
block_out_channels[0], out_channels, kernel_size=3
)
self.spatial_parallel = False
self.gradient_checkpointing = False
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
hidden_states = self.conv_in(hidden_states)
if torch.is_grad_enabled() and self.gradient_checkpointing:
hidden_states = self._gradient_checkpointing_func(
self.mid_block, hidden_states
)
for up_block in self.up_blocks:
hidden_states = self._gradient_checkpointing_func(
up_block, hidden_states
)
else:
hidden_states = self.mid_block(hidden_states)
for up_block in self.up_blocks:
hidden_states = up_block(hidden_states)
# post-process
hidden_states = _apply_group_norm_silu(
hidden_states,
self.conv_norm_out,
self.conv_act,
self.spatial_parallel,
)
hidden_states = self.conv_out(hidden_states)
return hidden_states
def _enable_hunyuan_decoder_spatial_parallel(decoder: nn.Module) -> None:
for module in decoder.modules():
if isinstance(module, HunyuanVideoCausalConv3d):
module.enable_spatial_parallel()
elif isinstance(module, HunyuanVideoResnetBlockCausal3D):
module.spatial_parallel = True
elif isinstance(module, HunyuanVideoMidBlock3D):
module.spatial_parallel = True
elif isinstance(module, HunyuanVideoDecoder3D):
module.spatial_parallel = True
class AutoencoderKLHunyuanVideo(ParallelTiledVAE):
r"""
A VAE model with KL loss for encoding videos into latents and decoding latent representations into videos.
Introduced in [HunyuanVideo](https://huggingface.co/papers/2412.03603).
This model inherits from [`ModelMixin`]. Check the superclass documentation for it's generic methods implemented
for all models (such as downloading or saving).
"""
_supports_gradient_checkpointing = True
def __init__(
self,
config: HunyuanVAEConfig,
) -> None:
nn.Module.__init__(self)
ParallelTiledVAE.__init__(self, config)
# TODO(will): only pass in config. We do this by manually defining a
# config for hunyuan vae
self.block_out_channels = config.block_out_channels
if config.load_encoder:
self.encoder = HunyuanVideoEncoder3D(
in_channels=config.in_channels,
out_channels=config.latent_channels,
down_block_types=config.down_block_types,
block_out_channels=config.block_out_channels,
layers_per_block=config.layers_per_block,
norm_num_groups=config.norm_num_groups,
act_fn=config.act_fn,
double_z=True,
mid_block_add_attention=config.mid_block_add_attention,
temporal_compression_ratio=config.temporal_compression_ratio,
spatial_compression_ratio=config.spatial_compression_ratio,
)
self.quant_conv = nn.Conv3d(
2 * config.latent_channels, 2 * config.latent_channels, kernel_size=1
)
if config.load_decoder:
self.decoder = HunyuanVideoDecoder3D(
in_channels=config.latent_channels,
out_channels=config.out_channels,
up_block_types=config.up_block_types,
block_out_channels=config.block_out_channels,
layers_per_block=config.layers_per_block,
norm_num_groups=config.norm_num_groups,
act_fn=config.act_fn,
time_compression_ratio=config.temporal_compression_ratio,
spatial_compression_ratio=config.spatial_compression_ratio,
mid_block_add_attention=config.mid_block_add_attention,
)
self.post_quant_conv = nn.Conv3d(
config.latent_channels, config.latent_channels, kernel_size=1
)
self._spatial_parallel_decode_enabled = False
if can_install_spatial_shard_parallel_decode(self.config):
_enable_hunyuan_decoder_spatial_parallel(self.decoder)
self._spatial_parallel_decode_enabled = True
def _should_use_spatial_parallel_decode(self, z: torch.Tensor) -> bool:
return (
self._spatial_parallel_decode_enabled
and should_run_spatial_shard_parallel_decode(self.config, z)
)
def _encode(self, x: torch.Tensor) -> torch.Tensor:
x = self.encoder(x)
enc = self.quant_conv(x)
return enc
def _decode(self, z: torch.Tensor) -> torch.Tensor:
z = self.post_quant_conv(z)
if self._should_use_spatial_parallel_decode(z):
expected_height = (
z.shape[-2] * self.config.arch_config.spatial_compression_ratio
)
z, expected_height = split_height_for_parallel_decode(
z,
expected_height=expected_height,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
dec = gather_and_trim_height(self.decoder(z), expected_height)
elif self._spatial_parallel_decode_enabled:
with disable_spatial_parallel_decode():
dec = self.decoder(z)
else:
dec = self.decoder(z)
return dec
def forward(
self,
sample: torch.Tensor,
sample_posterior: bool = False,
generator: torch.Generator | None = None,
) -> torch.Tensor:
r"""
Args:
sample (`torch.Tensor`): Input sample.
sample_posterior (`bool`, *optional*, defaults to `False`):
Whether to sample from the posterior.
"""
x = sample
posterior = self.encode(x).latent_dist
if sample_posterior:
z = posterior.sample(generator=generator)
else:
z = posterior.mode()
dec = self.decode(z)
return dec
EntryClass = AutoencoderKLHunyuanVideo
@@ -0,0 +1,210 @@
from typing import Any
import torch
import torch.nn as nn
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
from sglang.multimodal_gen.runtime.models.vaes.ltx_2_vae import (
LTX2VideoCausalConv3d,
LTX2VideoResnetBlock3d,
LTXVideoDownsampler3d,
)
def _patchify_video(sample: torch.Tensor, patch_size: int) -> torch.Tensor:
if patch_size == 1:
return sample
batch_size, channels, num_frames, height, width = sample.shape
sample = sample.reshape(
batch_size,
channels,
num_frames,
1,
height // patch_size,
patch_size,
width // patch_size,
patch_size,
)
return sample.permute(0, 1, 3, 7, 5, 2, 4, 6).flatten(1, 4)
class LTX23VideoPixelNorm(nn.Module):
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 LTX23PerChannelStatistics(nn.Module):
def __init__(self, latent_channels: int) -> None:
super().__init__()
self.register_buffer("std-of-means", torch.empty(latent_channels))
self.register_buffer("mean-of-means", torch.empty(latent_channels))
def normalize(self, x: torch.Tensor) -> torch.Tensor:
mean = self.get_buffer("mean-of-means").view(1, -1, 1, 1, 1).to(x)
std = self.get_buffer("std-of-means").view(1, -1, 1, 1, 1).to(x)
return (x - mean) / std
class LTX23VideoResBlockStack(nn.Module):
def __init__(
self, channels: int, num_layers: int, spatial_padding_mode: str
) -> None:
super().__init__()
self.res_blocks = nn.ModuleList(
[
LTX2VideoResnetBlock3d(
in_channels=channels,
out_channels=channels,
spatial_padding_mode=spatial_padding_mode,
)
for _ in range(num_layers)
]
)
def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
for res_block in self.res_blocks:
hidden_states = res_block(hidden_states, causal=True)
return hidden_states
def _make_ltx23_encoder_block(
block_name: str,
block_config: dict[str, Any],
in_channels: int,
spatial_padding_mode: str,
) -> tuple[nn.Module, int]:
if block_name == "res_x":
return (
LTX23VideoResBlockStack(
channels=in_channels,
num_layers=int(block_config["num_layers"]),
spatial_padding_mode=spatial_padding_mode,
),
in_channels,
)
multiplier = int(block_config.get("multiplier", 2))
stride_map = {
"compress_space_res": (1, 2, 2),
"compress_time_res": (2, 1, 1),
"compress_all_res": (2, 2, 2),
}
stride = stride_map.get(block_name)
if stride is None:
raise ValueError(f"Unsupported LTX-2.3 encoder block: {block_name}")
out_channels = in_channels * multiplier
return (
LTXVideoDownsampler3d(
in_channels=in_channels,
out_channels=out_channels,
stride=stride,
spatial_padding_mode=spatial_padding_mode,
),
out_channels,
)
class LTX23VideoConditionEncoder(nn.Module, LayerwiseOffloadableModuleMixin):
layerwise_offload_dit_group_enabled = False
layer_names = ["down_blocks"]
def __init__(self, config: dict[str, Any]) -> None:
super().__init__()
vae_config = config.get("vae", config)
latent_channels = int(vae_config["latent_channels"])
patch_size = int(vae_config.get("patch_size", 4))
spatial_padding_mode = str(vae_config.get("spatial_padding_mode", "zeros"))
encoder_blocks = list(vae_config["encoder_blocks"])
latent_log_var = str(vae_config.get("latent_log_var", "uniform"))
self.patch_size = patch_size
self.latency_channels = latent_channels
self.latent_log_var = latent_log_var
self.per_channel_statistics = LTX23PerChannelStatistics(latent_channels)
feature_channels = latent_channels
self.conv_in = LTX2VideoCausalConv3d(
in_channels=int(vae_config.get("in_channels", 3)) * patch_size**2,
out_channels=feature_channels,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
self.down_blocks = nn.ModuleList()
for block_name, block_params in encoder_blocks:
block_config = (
{"num_layers": block_params}
if isinstance(block_params, int)
else dict(block_params)
)
block, feature_channels = _make_ltx23_encoder_block(
block_name=block_name,
block_config=block_config,
in_channels=feature_channels,
spatial_padding_mode=spatial_padding_mode,
)
self.down_blocks.append(block)
self.conv_norm_out = LTX23VideoPixelNorm(dim=1, eps=1e-8)
self.conv_act = nn.SiLU()
conv_out_channels = latent_channels
if latent_log_var == "per_channel":
conv_out_channels *= 2
elif latent_log_var in {"uniform", "constant"}:
conv_out_channels += 1
elif latent_log_var != "none":
raise ValueError(f"Unsupported latent_log_var: {latent_log_var}")
self.conv_out = LTX2VideoCausalConv3d(
in_channels=feature_channels,
out_channels=conv_out_channels,
kernel_size=3,
stride=1,
spatial_padding_mode=spatial_padding_mode,
)
def forward(self, sample: torch.Tensor) -> torch.Tensor:
frames_count = int(sample.shape[2])
if (frames_count - 1) % 8 != 0:
frames_to_crop = (frames_count - 1) % 8
sample = sample[:, :, :-frames_to_crop, ...]
hidden_states = _patchify_video(sample, self.patch_size)
hidden_states = self.conv_in(hidden_states, causal=True)
for block in self.down_blocks:
hidden_states = block(hidden_states)
hidden_states = self.conv_norm_out(hidden_states)
hidden_states = self.conv_act(hidden_states)
hidden_states = self.conv_out(hidden_states, causal=True)
if self.latent_log_var == "uniform":
means = hidden_states[:, :-1, ...]
logvar = hidden_states[:, -1:, ...]
hidden_states = torch.cat(
[
means,
logvar.repeat(1, means.shape[1], *([1] * (means.ndim - 2))),
],
dim=1,
)
elif self.latent_log_var == "constant":
means = hidden_states[:, :-1, ...]
logvar = torch.full_like(means, -30.0)
hidden_states = torch.cat([means, logvar], dim=1)
means, _ = torch.chunk(hidden_states, 2, dim=1)
return self.per_channel_statistics.normalize(means)
@@ -0,0 +1,918 @@
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.ones((base_channels,))
latents_mean = torch.zeros((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
# stage containers are not called directly, so hooks attach to called lists
self.layer_names = [
layer_name
for prefix, num_resolutions in (
("encoder.down", self.encoder.num_resolutions),
("decoder.up", self.decoder.num_resolutions),
)
for level in range(num_resolutions)
for layer_name in (
f"{prefix}.{level}.block",
f"{prefix}.{level}.attn",
)
]
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,109 @@
import torch
import torch.nn as nn
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
get_decode_parallel_rank,
get_decode_parallel_world_size,
)
from sglang.multimodal_gen.runtime.layers.parallel_conv import (
SpatialParallelConv2d,
chunk_height_by_sizes,
gather_and_trim_height,
gather_variable_height,
split_for_parallel_decode,
)
def count_decoder_spatial_upsamples(decoder: nn.Module) -> int:
return sum(
len(upsamplers)
for block in getattr(decoder, "up_blocks", [])
if (upsamplers := getattr(block, "upsamplers", None)) is not None
)
def enable_diffusers_decoder_spatial_parallel(decoder: nn.Module) -> int:
_replace_conv2d_modules(decoder)
_patch_groupnorm_modules(decoder)
_patch_attention_modules(decoder)
return count_decoder_spatial_upsamples(decoder)
def spatial_parallel_diffusers_decode(
decoder: nn.Module, z: torch.Tensor, upsample_count: int
) -> torch.Tensor:
z, expected_height = split_for_parallel_decode(
z,
upsample_count=upsample_count,
world_size=get_decode_parallel_world_size(),
rank=get_decode_parallel_rank(),
)
return gather_and_trim_height(decoder(z), expected_height)
def _replace_conv2d_modules(module: nn.Module) -> None:
for name, child in list(module.named_children()):
if type(child) is nn.Conv2d:
setattr(module, name, _make_spatial_conv2d(child))
else:
_replace_conv2d_modules(child)
def _make_spatial_conv2d(conv: nn.Conv2d) -> SpatialParallelConv2d:
spatial_conv = SpatialParallelConv2d(
in_channels=conv.in_channels,
out_channels=conv.out_channels,
kernel_size=conv.kernel_size,
stride=conv.stride,
padding=conv.padding,
dilation=conv.dilation,
groups=conv.groups,
bias=conv.bias is not None,
padding_mode=conv.padding_mode,
)
spatial_conv.weight = conv.weight
spatial_conv.bias = conv.bias
return spatial_conv
def _patch_attention_modules(module: nn.Module) -> None:
for child in module.children():
if child.__class__.__name__ == "Attention":
_patch_attention_forward(child)
_patch_attention_modules(child)
def _patch_groupnorm_modules(module: nn.Module) -> None:
for child in module.children():
if type(child) is nn.GroupNorm:
_patch_groupnorm_forward(child)
else:
_patch_groupnorm_modules(child)
def _patch_groupnorm_forward(norm: nn.GroupNorm) -> None:
original_forward = norm.forward
def spatial_parallel_forward(hidden_states):
if hidden_states.dim() < 4:
return original_forward(hidden_states)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = hidden_states.contiguous()
hidden_states = original_forward(hidden_states)
return chunk_height_by_sizes(hidden_states, heights)
norm.forward = spatial_parallel_forward
def _patch_attention_forward(attn: nn.Module) -> None:
original_forward = attn.forward
def spatial_parallel_forward(hidden_states, *args, **kwargs):
if hidden_states.dim() != 4:
return original_forward(hidden_states, *args, **kwargs)
hidden_states, heights = gather_variable_height(hidden_states)
hidden_states = hidden_states.contiguous()
hidden_states = original_forward(hidden_states, *args, **kwargs)
return chunk_height_by_sizes(hidden_states, heights)
attn.forward = spatial_parallel_forward
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,5 @@
# SPDX-License-Identifier: Apache-2.0
from sglang.multimodal_gen.runtime.models.vlas.pi05_policy import Pi05PolicyModel
__all__ = ["Pi05PolicyModel"]
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,759 @@
import math
from abc import ABC
from contextlib import nullcontext
from typing import Tuple
import einops
import torch
import torch.nn as nn
import torch.nn.functional as F
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import LTXVocoderConfig
from sglang.multimodal_gen.runtime.managers.memory_managers.layerwise_offload import (
LayerwiseOffloadableModuleMixin,
)
LRELU_SLOPE = 0.1
def get_padding(kernel_size: int, dilation: int = 1) -> int:
return int((kernel_size * dilation - dilation) / 2)
def _sinc(x: torch.Tensor) -> torch.Tensor:
return torch.where(
x == 0,
torch.tensor(1.0, device=x.device, dtype=x.dtype),
torch.sin(math.pi * x) / math.pi / x,
)
def kaiser_sinc_filter1d(
cutoff: float, half_width: float, kernel_size: int
) -> torch.Tensor:
even = kernel_size % 2 == 0
half_size = kernel_size // 2
delta_f = 4 * half_width
amplitude = 2.285 * (half_size - 1) * math.pi * delta_f + 7.95
if amplitude > 50.0:
beta = 0.1102 * (amplitude - 8.7)
elif amplitude >= 21.0:
beta = 0.5842 * (amplitude - 21) ** 0.4 + 0.07886 * (amplitude - 21.0)
else:
beta = 0.0
window = torch.kaiser_window(kernel_size, beta=beta, periodic=False)
time = (
torch.arange(-half_size, half_size) + 0.5
if even
else torch.arange(kernel_size) - half_size
)
if cutoff == 0:
filter_ = torch.zeros_like(time)
else:
filter_ = 2 * cutoff * window * _sinc(2 * cutoff * time)
filter_ /= filter_.sum()
return filter_.view(1, 1, kernel_size)
class LowPassFilter1d(nn.Module):
def __init__(
self,
cutoff: float = 0.5,
half_width: float = 0.6,
stride: int = 1,
padding: bool = True,
padding_mode: str = "replicate",
kernel_size: int = 12,
):
super().__init__()
self.kernel_size = kernel_size
self.even = kernel_size % 2 == 0
self.pad_left = kernel_size // 2 - int(self.even)
self.pad_right = kernel_size // 2
self.stride = stride
self.padding = padding
self.padding_mode = padding_mode
self.register_buffer(
"filter", kaiser_sinc_filter1d(cutoff, half_width, kernel_size)
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
_, channels, _ = x.shape
if self.padding:
x = F.pad(x, (self.pad_left, self.pad_right), mode=self.padding_mode)
return F.conv1d(
x,
self.filter.expand(channels, -1, -1),
stride=self.stride,
groups=channels,
)
class UpSample1d(nn.Module):
def __init__(
self,
ratio: int = 2,
kernel_size: int | None = None,
persistent: bool = True,
window_type: str = "kaiser",
):
super().__init__()
self.ratio = ratio
self.stride = ratio
if window_type == "hann":
rolloff = 0.99
lowpass_filter_width = 6
width = math.ceil(lowpass_filter_width / rolloff)
self.kernel_size = 2 * width * ratio + 1
self.pad = width
self.pad_left = 2 * width * ratio
self.pad_right = self.kernel_size - ratio
time_axis = (torch.arange(self.kernel_size) / ratio - width) * rolloff
time_clamped = time_axis.clamp(-lowpass_filter_width, lowpass_filter_width)
window = torch.cos(time_clamped * math.pi / lowpass_filter_width / 2) ** 2
sinc_filter = (torch.sinc(time_axis) * window * rolloff / ratio).view(
1, 1, -1
)
else:
self.kernel_size = (
int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size
)
self.pad = self.kernel_size // ratio - 1
self.pad_left = (
self.pad * self.stride + (self.kernel_size - self.stride) // 2
)
self.pad_right = (
self.pad * self.stride + (self.kernel_size - self.stride + 1) // 2
)
sinc_filter = kaiser_sinc_filter1d(
cutoff=0.5 / ratio,
half_width=0.6 / ratio,
kernel_size=self.kernel_size,
)
self.register_buffer("filter", sinc_filter, persistent=persistent)
def forward(self, x: torch.Tensor) -> torch.Tensor:
_, channels, _ = x.shape
x = F.pad(x, (self.pad, self.pad), mode="replicate")
filt = self.filter.to(dtype=x.dtype, device=x.device).expand(channels, -1, -1)
x = self.ratio * F.conv_transpose1d(
x, filt, stride=self.stride, groups=channels
)
return x[..., self.pad_left : -self.pad_right]
class DownSample1d(nn.Module):
def __init__(self, ratio: int = 2, kernel_size: int | None = None):
super().__init__()
self.lowpass = LowPassFilter1d(
cutoff=0.5 / ratio,
half_width=0.6 / ratio,
stride=ratio,
kernel_size=int(6 * ratio // 2) * 2 if kernel_size is None else kernel_size,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.lowpass(x)
class Activation1d(nn.Module):
def __init__(
self,
activation: nn.Module,
up_ratio: int = 2,
down_ratio: int = 2,
up_kernel_size: int = 12,
down_kernel_size: int = 12,
):
super().__init__()
self.act = activation
self.upsample = UpSample1d(up_ratio, up_kernel_size)
self.downsample = DownSample1d(down_ratio, down_kernel_size)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.upsample(x)
x = self.act(x)
return self.downsample(x)
class Snake(nn.Module):
def __init__(
self,
in_features: int,
alpha: float = 1.0,
alpha_trainable: bool = True,
alpha_logscale: bool = True,
):
super().__init__()
self.alpha_logscale = alpha_logscale
self.alpha = nn.Parameter(
torch.zeros(in_features)
if alpha_logscale
else torch.ones(in_features) * alpha
)
self.alpha.requires_grad = alpha_trainable
self.eps = 1e-9
def forward(self, x: torch.Tensor) -> torch.Tensor:
alpha = self.alpha.unsqueeze(0).unsqueeze(-1)
if self.alpha_logscale:
alpha = torch.exp(alpha)
return x + (1.0 / (alpha + self.eps)) * torch.sin(x * alpha).pow(2)
class SnakeBeta(nn.Module):
def __init__(
self,
in_features: int,
alpha: float = 1.0,
alpha_trainable: bool = True,
alpha_logscale: bool = True,
):
super().__init__()
self.alpha_logscale = alpha_logscale
self.alpha = nn.Parameter(
torch.zeros(in_features)
if alpha_logscale
else torch.ones(in_features) * alpha
)
self.alpha.requires_grad = alpha_trainable
self.beta = nn.Parameter(
torch.zeros(in_features)
if alpha_logscale
else torch.ones(in_features) * alpha
)
self.beta.requires_grad = alpha_trainable
self.eps = 1e-9
def forward(self, x: torch.Tensor) -> torch.Tensor:
alpha = self.alpha.unsqueeze(0).unsqueeze(-1)
beta = self.beta.unsqueeze(0).unsqueeze(-1)
if self.alpha_logscale:
alpha = torch.exp(alpha)
beta = torch.exp(beta)
return x + (1.0 / (beta + self.eps)) * torch.sin(x * alpha).pow(2)
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 AMPBlock1(nn.Module):
def __init__(
self,
channels: int,
kernel_size: int = 3,
dilation: tuple[int, int, int] = (1, 3, 5),
activation: str = "snake",
):
super().__init__()
act_cls = SnakeBeta if activation == "snakebeta" else Snake
self.convs1 = nn.ModuleList(
[
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[0],
padding=get_padding(kernel_size, dilation[0]),
),
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[1],
padding=get_padding(kernel_size, dilation[1]),
),
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=dilation[2],
padding=get_padding(kernel_size, dilation[2]),
),
]
)
self.convs2 = nn.ModuleList(
[
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
),
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
),
nn.Conv1d(
channels,
channels,
kernel_size,
1,
dilation=1,
padding=get_padding(kernel_size, 1),
),
]
)
self.acts1 = nn.ModuleList(
[Activation1d(act_cls(channels)) for _ in range(len(self.convs1))]
)
self.acts2 = nn.ModuleList(
[Activation1d(act_cls(channels)) for _ in range(len(self.convs2))]
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
for conv1, conv2, act1, act2 in zip(
self.convs1, self.convs2, self.acts1, self.acts2
):
xt = act1(x)
xt = conv1(xt)
xt = act2(xt)
xt = conv2(xt)
x = x + xt
return x
class LTX23MelSTFT(nn.Module):
class STFTFn(nn.Module):
def __init__(self, filter_length: int, hop_length: int, win_length: int):
super().__init__()
self.hop_length = hop_length
self.win_length = win_length
n_freqs = filter_length // 2 + 1
self.register_buffer(
"forward_basis", torch.zeros(n_freqs * 2, 1, filter_length)
)
self.register_buffer(
"inverse_basis", torch.zeros(n_freqs * 2, 1, filter_length)
)
def forward(self, y: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
if y.dim() == 2:
y = y.unsqueeze(1)
left_pad = max(0, self.win_length - self.hop_length)
y = F.pad(y, (left_pad, 0))
spec = F.conv1d(y, self.forward_basis, stride=self.hop_length, padding=0)
n_freqs = spec.shape[1] // 2
real, imag = spec[:, :n_freqs], spec[:, n_freqs:]
magnitude = torch.sqrt(real**2 + imag**2)
phase = torch.atan2(imag.float(), real.float()).to(real.dtype)
return magnitude, phase
def __init__(
self, filter_length: int, hop_length: int, win_length: int, n_mel_channels: int
):
super().__init__()
self.stft_fn = self.STFTFn(filter_length, hop_length, win_length)
n_freqs = filter_length // 2 + 1
self.register_buffer("mel_basis", torch.zeros(n_mel_channels, n_freqs))
def mel_spectrogram(
self, y: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
magnitude, phase = self.stft_fn(y)
energy = torch.norm(magnitude, dim=1)
mel = torch.matmul(self.mel_basis.to(magnitude.dtype), magnitude)
log_mel = torch.log(torch.clamp(mel, min=1e-5))
return log_mel, magnitude, phase, energy
class LTX23VocoderCore(nn.Module):
def __init__( # noqa: PLR0913
self,
resblock_kernel_sizes: list[int] | None = None,
upsample_rates: list[int] | None = None,
upsample_kernel_sizes: list[int] | None = None,
resblock_dilation_sizes: list[list[int]] | None = None,
upsample_initial_channel: int = 1024,
resblock: str = "1",
output_sampling_rate: int = 24000,
activation: str = "snake",
use_tanh_at_final: bool = True,
apply_final_activation: bool = True,
use_bias_at_final: bool = True,
):
super().__init__()
if resblock_kernel_sizes is None:
resblock_kernel_sizes = [3, 7, 11]
if upsample_rates is None:
upsample_rates = [6, 5, 2, 2, 2]
if upsample_kernel_sizes is None:
upsample_kernel_sizes = [16, 15, 8, 4, 4]
if resblock_dilation_sizes is None:
resblock_dilation_sizes = [[1, 3, 5], [1, 3, 5], [1, 3, 5]]
self.output_sampling_rate = output_sampling_rate
self.num_kernels = len(resblock_kernel_sizes)
self.num_upsamples = len(upsample_rates)
self.use_tanh_at_final = use_tanh_at_final
self.apply_final_activation = apply_final_activation
self.is_amp = resblock == "AMP1"
self.conv_pre = nn.Conv1d(
in_channels=128,
out_channels=upsample_initial_channel,
kernel_size=7,
stride=1,
padding=3,
)
self.ups = nn.ModuleList(
nn.ConvTranspose1d(
upsample_initial_channel // (2**i),
upsample_initial_channel // (2 ** (i + 1)),
kernel_size,
stride,
padding=(kernel_size - stride) // 2,
)
for i, (stride, kernel_size) in enumerate(
zip(upsample_rates, upsample_kernel_sizes, strict=True)
)
)
final_channels = upsample_initial_channel // (2 ** len(upsample_rates))
self.resblocks = nn.ModuleList()
for i in range(len(upsample_rates)):
channels = upsample_initial_channel // (2 ** (i + 1))
for kernel_size, dilations in zip(
resblock_kernel_sizes, resblock_dilation_sizes, strict=True
):
if self.is_amp:
self.resblocks.append(
AMPBlock1(
channels,
kernel_size,
tuple(dilations),
activation=activation,
)
)
else:
self.resblocks.append(
ResBlock(
channels,
kernel_size=kernel_size,
dilations=tuple(dilations),
leaky_relu_negative_slope=LRELU_SLOPE,
padding_mode=get_padding(kernel_size, 1),
)
)
self.act_post = (
Activation1d(SnakeBeta(final_channels)) if self.is_amp else nn.LeakyReLU()
)
self.conv_post = nn.Conv1d(
in_channels=final_channels,
out_channels=2,
kernel_size=7,
stride=1,
padding=3,
bias=use_bias_at_final,
)
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = x.transpose(2, 3)
if x.dim() == 4:
assert x.shape[1] == 2, "Input must have 2 channels for stereo"
x = einops.rearrange(x, "b s c t -> b (s c) t")
x = self.conv_pre(x)
for i in range(self.num_upsamples):
if not self.is_amp:
x = F.leaky_relu(x, LRELU_SLOPE)
x = self.ups[i](x)
start = i * self.num_kernels
end = start + self.num_kernels
block_outputs = torch.stack(
[self.resblocks[idx](x) for idx in range(start, end)],
dim=0,
)
x = block_outputs.mean(dim=0)
x = self.act_post(x)
x = self.conv_post(x)
if self.apply_final_activation:
x = torch.tanh(x) if self.use_tanh_at_final else torch.clamp(x, -1, 1)
return x
class LTX2Vocoder(ABC, nn.Module, LayerwiseOffloadableModuleMixin):
r"""
LTX 2.0 vocoder for converting generated mel spectrograms back to audio waveforms.
"""
layerwise_offload_dit_group_enabled = False
layer_names = [
"upsamplers",
"resnets",
"vocoder.ups",
"vocoder.resblocks",
"bwe_generator.ups",
"bwe_generator.resblocks",
]
def __init__(
self,
config: LTXVocoderConfig,
):
super().__init__()
self.config = config
nested_vocoder_cfg = getattr(config.arch_config, "vocoder", None)
if isinstance(nested_vocoder_cfg, dict) and "bwe" in nested_vocoder_cfg:
vocoder_cfg = nested_vocoder_cfg.get("vocoder", {})
bwe_cfg = nested_vocoder_cfg["bwe"]
self.vocoder = LTX23VocoderCore(
resblock_kernel_sizes=vocoder_cfg.get("resblock_kernel_sizes"),
upsample_rates=vocoder_cfg.get("upsample_rates"),
upsample_kernel_sizes=vocoder_cfg.get("upsample_kernel_sizes"),
resblock_dilation_sizes=vocoder_cfg.get("resblock_dilation_sizes"),
upsample_initial_channel=vocoder_cfg.get(
"upsample_initial_channel", 1024
),
resblock=vocoder_cfg.get("resblock", "1"),
output_sampling_rate=bwe_cfg["input_sampling_rate"],
activation=vocoder_cfg.get("activation", "snake"),
use_tanh_at_final=vocoder_cfg.get("use_tanh_at_final", True),
apply_final_activation=vocoder_cfg.get("apply_final_activation", True),
use_bias_at_final=vocoder_cfg.get("use_bias_at_final", True),
)
self.bwe_generator = LTX23VocoderCore(
resblock_kernel_sizes=bwe_cfg.get("resblock_kernel_sizes"),
upsample_rates=bwe_cfg.get("upsample_rates"),
upsample_kernel_sizes=bwe_cfg.get("upsample_kernel_sizes"),
resblock_dilation_sizes=bwe_cfg.get("resblock_dilation_sizes"),
upsample_initial_channel=bwe_cfg.get("upsample_initial_channel", 1024),
resblock=bwe_cfg.get("resblock", "1"),
output_sampling_rate=bwe_cfg["output_sampling_rate"],
activation=bwe_cfg.get("activation", "snake"),
use_tanh_at_final=bwe_cfg.get("use_tanh_at_final", True),
apply_final_activation=bwe_cfg.get("apply_final_activation", True),
use_bias_at_final=bwe_cfg.get("use_bias_at_final", True),
)
self.mel_stft = LTX23MelSTFT(
filter_length=bwe_cfg["n_fft"],
hop_length=bwe_cfg["hop_length"],
win_length=bwe_cfg.get("win_size", bwe_cfg["n_fft"]),
n_mel_channels=bwe_cfg["num_mels"],
)
self.input_sampling_rate = bwe_cfg["input_sampling_rate"]
self.output_sampling_rate = bwe_cfg["output_sampling_rate"]
self.hop_length = bwe_cfg["hop_length"]
with torch.device("cpu"):
self.resampler = UpSample1d(
ratio=self.output_sampling_rate // self.input_sampling_rate,
persistent=False,
window_type="hann",
)
self.sample_rate = self.output_sampling_rate
return
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)
or getattr(config.arch_config, "output_sampling_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 _compute_ltx23_mel(self, audio: torch.Tensor) -> torch.Tensor:
batch, channels, _ = audio.shape
flat = audio.reshape(batch * channels, -1)
mel, _, _, _ = self.mel_stft.mel_spectrogram(flat)
return mel.reshape(batch, channels, mel.shape[1], mel.shape[2])
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)
"""
if hasattr(self, "bwe_generator"):
input_dtype = hidden_states.dtype
autocast_ctx = (
torch.autocast(
device_type=hidden_states.device.type, dtype=torch.float32
)
if hidden_states.device.type != "cpu"
else nullcontext()
)
with autocast_ctx:
waveform = self.vocoder(hidden_states.float())
length_low_rate = waveform.shape[-1]
output_length = (
length_low_rate
* self.output_sampling_rate
// self.input_sampling_rate
)
remainder = length_low_rate % self.hop_length
if remainder != 0:
waveform = F.pad(waveform, (0, self.hop_length - remainder))
mel = self._compute_ltx23_mel(waveform)
residual = self.bwe_generator(mel.transpose(2, 3))
skip = self.resampler(waveform)
assert residual.shape == skip.shape
waveform = torch.clamp(residual + skip, -1, 1)[..., :output_length]
return waveform.to(input_dtype)
# 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