[diffusion] model: support LongLive 2.0 T2V and I2V inference (#27639)

Co-authored-by: Yihao Wang <42559837+AgainstEntropy@users.noreply.github.com>
Co-authored-by: Mick <mickjagger19@icloud.com>
This commit is contained in:
Rabinovich
2026-07-13 17:39:30 -07:00
committed by GitHub
co-authored by Yihao Wang Mick
parent 2f79d334f2
commit cfe4eefabb
22 changed files with 2215 additions and 54 deletions
@@ -8,6 +8,7 @@ from sglang.multimodal_gen.configs.models.dits.ideogram import Ideogram4DiTConfi
from sglang.multimodal_gen.configs.models.dits.lingbot_world import (
LingBotWorldVideoConfig,
)
from sglang.multimodal_gen.configs.models.dits.longlive2 import LongLive2VideoConfig
from sglang.multimodal_gen.configs.models.dits.mova_audio import MOVAAudioConfig
from sglang.multimodal_gen.configs.models.dits.mova_video import MOVAVideoConfig
from sglang.multimodal_gen.configs.models.dits.stablediffusion3 import (
@@ -21,6 +22,7 @@ __all__ = [
"HunyuanVideoConfig",
"Ideogram4DiTConfig",
"LingBotWorldVideoConfig",
"LongLive2VideoConfig",
"WanVideoConfig",
"Hunyuan3DDiTConfig",
"MOVAAudioConfig",
@@ -0,0 +1,85 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from https://github.com/NVlabs/LongLive
from dataclasses import dataclass, field
from sglang.multimodal_gen.configs.models.dits.base import DiTArchConfig
from sglang.multimodal_gen.configs.models.dits.wanvideo import (
WanVideoArchConfig,
WanVideoConfig,
)
@dataclass
class LongLive2ArchConfig(WanVideoArchConfig):
param_names_mapping: dict = field(
default_factory=lambda: {
r"^model\.patch_embedding\.(.*)$": r"patch_embedding.proj.\1",
r"^model\.text_embedding\.0\.(.*)$": r"condition_embedder.text_embedder.fc_in.\1",
r"^model\.text_embedding\.2\.(.*)$": r"condition_embedder.text_embedder.fc_out.\1",
r"^model\.time_embedding\.0\.(.*)$": r"condition_embedder.time_embedder.mlp.fc_in.\1",
r"^model\.time_embedding\.2\.(.*)$": r"condition_embedder.time_embedder.mlp.fc_out.\1",
r"^model\.time_projection\.1\.(.*)$": r"condition_embedder.time_modulation.linear.\1",
r"^model\.blocks\.(\d+)\.modulation$": r"blocks.\1.scale_shift_table",
r"^model\.blocks\.(\d+)\.self_attn\.q\.(.*)$": r"blocks.\1.to_q.\2",
r"^model\.blocks\.(\d+)\.self_attn\.k\.(.*)$": r"blocks.\1.to_k.\2",
r"^model\.blocks\.(\d+)\.self_attn\.v\.(.*)$": r"blocks.\1.to_v.\2",
r"^model\.blocks\.(\d+)\.self_attn\.o\.(.*)$": r"blocks.\1.to_out.\2",
r"^model\.blocks\.(\d+)\.self_attn\.norm_q\.(.*)$": r"blocks.\1.norm_q.\2",
r"^model\.blocks\.(\d+)\.self_attn\.norm_k\.(.*)$": r"blocks.\1.norm_k.\2",
r"^model\.blocks\.(\d+)\.norm3\.(.*)$": r"blocks.\1.self_attn_residual_norm.norm.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.q\.(.*)$": r"blocks.\1.attn2.to_q.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.k\.(.*)$": r"blocks.\1.attn2.to_k.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.v\.(.*)$": r"blocks.\1.attn2.to_v.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.o\.(.*)$": r"blocks.\1.attn2.to_out.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.norm_q\.(.*)$": r"blocks.\1.attn2.norm_q.\2",
r"^model\.blocks\.(\d+)\.cross_attn\.norm_k\.(.*)$": r"blocks.\1.attn2.norm_k.\2",
r"^model\.blocks\.(\d+)\.ffn\.0\.(.*)$": r"blocks.\1.ffn.fc_in.\2",
r"^model\.blocks\.(\d+)\.ffn\.2\.(.*)$": r"blocks.\1.ffn.fc_out.\2",
r"^model\.head\.modulation$": r"scale_shift_table",
r"^model\.head\.head\.(.*)$": r"proj_out.\1",
}
)
reverse_param_names_mapping: dict = field(
default_factory=lambda: {
r"^patch_embedding\.proj\.(.*)$": r"model.patch_embedding.\1",
r"^condition_embedder\.text_embedder\.fc_in\.(.*)$": r"model.text_embedding.0.\1",
r"^condition_embedder\.text_embedder\.fc_out\.(.*)$": r"model.text_embedding.2.\1",
r"^condition_embedder\.time_embedder\.mlp\.fc_in\.(.*)$": r"model.time_embedding.0.\1",
r"^condition_embedder\.time_embedder\.mlp\.fc_out\.(.*)$": r"model.time_embedding.2.\1",
r"^condition_embedder\.time_modulation\.linear\.(.*)$": r"model.time_projection.1.\1",
r"^blocks\.(\d+)\.scale_shift_table$": r"model.blocks.\1.modulation",
r"^blocks\.(\d+)\.to_q\.(.*)$": r"model.blocks.\1.self_attn.q.\2",
r"^blocks\.(\d+)\.to_k\.(.*)$": r"model.blocks.\1.self_attn.k.\2",
r"^blocks\.(\d+)\.to_v\.(.*)$": r"model.blocks.\1.self_attn.v.\2",
r"^blocks\.(\d+)\.to_out\.(.*)$": r"model.blocks.\1.self_attn.o.\2",
r"^blocks\.(\d+)\.norm_q\.(.*)$": r"model.blocks.\1.self_attn.norm_q.\2",
r"^blocks\.(\d+)\.norm_k\.(.*)$": r"model.blocks.\1.self_attn.norm_k.\2",
r"^blocks\.(\d+)\.self_attn_residual_norm\.norm\.(.*)$": r"model.blocks.\1.norm3.\2",
r"^blocks\.(\d+)\.attn2\.to_q\.(.*)$": r"model.blocks.\1.cross_attn.q.\2",
r"^blocks\.(\d+)\.attn2\.to_k\.(.*)$": r"model.blocks.\1.cross_attn.k.\2",
r"^blocks\.(\d+)\.attn2\.to_v\.(.*)$": r"model.blocks.\1.cross_attn.v.\2",
r"^blocks\.(\d+)\.attn2\.to_out\.(.*)$": r"model.blocks.\1.cross_attn.o.\2",
r"^blocks\.(\d+)\.attn2\.norm_q\.(.*)$": r"model.blocks.\1.cross_attn.norm_q.\2",
r"^blocks\.(\d+)\.attn2\.norm_k\.(.*)$": r"model.blocks.\1.cross_attn.norm_k.\2",
r"^blocks\.(\d+)\.ffn\.fc_in\.(.*)$": r"model.blocks.\1.ffn.0.\2",
r"^blocks\.(\d+)\.ffn\.fc_out\.(.*)$": r"model.blocks.\1.ffn.2.\2",
r"^scale_shift_table$": r"model.head.modulation",
r"^proj_out\.(.*)$": r"model.head.head.\1",
}
)
num_attention_heads: int = 24
attention_head_dim: int = 128
in_channels: int = 48
out_channels: int = 48
ffn_dim: int = 14336
num_layers: int = 30
local_attn_size: int = 32
sink_size: int = 8
num_frames_per_block: int = 8
sliding_window_num_frames: int = 32
@dataclass
class LongLive2VideoConfig(WanVideoConfig):
arch_config: DiTArchConfig = field(default_factory=LongLive2ArchConfig)
@@ -0,0 +1,59 @@
# SPDX-License-Identifier: Apache-2.0
# Adapted from https://github.com/NVlabs/LongLive
from dataclasses import dataclass, field
from sglang.multimodal_gen.configs.models import DiTConfig
from sglang.multimodal_gen.configs.models.dits.longlive2 import LongLive2VideoConfig
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
from sglang.multimodal_gen.configs.pipeline_configs.wan import Wan2_2_TI2V_5B_Config
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__)
@dataclass
class LongLive2T2VConfig(Wan2_2_TI2V_5B_Config):
is_causal: bool = True
task_type: ModelTaskType = ModelTaskType.TI2V
vae_precision: str = "bf16"
flow_shift: float | None = 5.0
dmd_denoising_steps: list[int] | None = field(
default_factory=lambda: [1000, 750, 500, 250]
)
expand_timesteps: bool = False
dit_config: DiTConfig = field(default_factory=LongLive2VideoConfig)
def adjust_num_frames(self, num_frames: int) -> int:
num_frames = super().adjust_num_frames(num_frames)
vae_scale_factor_temporal = self.vae_config.arch_config.scale_factor_temporal
latent_frames = (num_frames - 1) // vae_scale_factor_temporal + 1
block_size = self.dit_config.arch_config.num_frames_per_block
if latent_frames % block_size == 0:
return num_frames
adjusted_latent_frames = max(
block_size, latent_frames // block_size * block_size
)
adjusted_num_frames = (
adjusted_latent_frames - 1
) * vae_scale_factor_temporal + 1
logger.warning(
"`num_frames` must map to latent frames divisible by %s for "
"LongLive2 causal denoising. Rounding from %s to %s.",
block_size,
num_frames,
adjusted_num_frames,
)
return adjusted_num_frames
def postprocess_image_latent(self, latent_condition, batch):
return latent_condition[:, :, :1]
def __post_init__(self) -> None:
super().__post_init__()
self.vae_config.load_encoder = True
self.vae_config.load_decoder = True
@@ -0,0 +1,74 @@
# SPDX-License-Identifier: Apache-2.0
from dataclasses import dataclass, field
from sglang.multimodal_gen.configs.sample.wan import Wan2_2_TI2V_5B_SamplingParam
@dataclass
class LongLive2SamplingParams(Wan2_2_TI2V_5B_SamplingParam):
height: int = 704
width: int = 1280
fps: int = 24
num_inference_steps: int = 4
guidance_scale: float = 1.0
num_frames: int = 61
shot_prompts: list[str] | None = field(
default=None, metadata={"batch_sig_exclude": True}
)
shot_durations: list[int] | None = field(
default=None, metadata={"batch_sig_exclude": True}
)
chunks_per_shot: int = field(default=0, metadata={"batch_sig_exclude": True})
scene_cut_prefix: str = field(
default="The scene transitions. ", metadata={"batch_sig_exclude": True}
)
multi_shot_sink: bool = field(default=True, metadata={"batch_sig_exclude": True})
multi_shot_rope_offset: float = field(
default=8.0, metadata={"batch_sig_exclude": True}
)
supported_resolutions: list[tuple[int, int]] | None = field(
default_factory=lambda: [
(1280, 704),
(704, 1280),
(832, 480),
(480, 832),
]
)
def _validate(self):
super()._validate()
if self.shot_prompts is not None:
if not isinstance(self.shot_prompts, list) or not self.shot_prompts:
raise ValueError("shot_prompts must be a non-empty list of strings")
if not all(
isinstance(prompt, str) and prompt for prompt in self.shot_prompts
):
raise ValueError("shot_prompts must contain non-empty strings")
if self.shot_durations is not None:
if not isinstance(self.shot_durations, list) or not self.shot_durations:
raise ValueError("shot_durations must be a non-empty list of ints")
if self.shot_prompts is not None and len(self.shot_durations) != len(
self.shot_prompts
):
raise ValueError("shot_durations must match shot_prompts length")
if not all(
isinstance(duration, int) and duration > 0
for duration in self.shot_durations
):
raise ValueError("shot_durations must contain positive ints")
if self.chunks_per_shot < 0:
raise ValueError("chunks_per_shot must be non-negative")
if self.scene_cut_prefix is None:
self.scene_cut_prefix = ""
if self.multi_shot_rope_offset < 0:
raise ValueError("multi_shot_rope_offset must be non-negative")
def _adjust(self, server_args):
if self.shot_prompts is not None and self.prompt is None:
self.prompt = self.shot_prompts[0]
super()._adjust(server_args)
+11
View File
@@ -68,6 +68,7 @@ from sglang.multimodal_gen.configs.pipeline_configs.joy_image import (
JoyImageEditPipelineConfig,
)
from sglang.multimodal_gen.configs.pipeline_configs.krea2 import Krea2PipelineConfig
from sglang.multimodal_gen.configs.pipeline_configs.longlive2 import LongLive2T2VConfig
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import (
LTX2PipelineConfig,
LTX23PipelineConfig,
@@ -129,6 +130,7 @@ from sglang.multimodal_gen.configs.sample.krea2 import (
from sglang.multimodal_gen.configs.sample.lingbot_world import (
LingBotWorldSamplingParams,
)
from sglang.multimodal_gen.configs.sample.longlive2 import LongLive2SamplingParams
from sglang.multimodal_gen.configs.sample.ltx_2 import (
LTX2SamplingParams,
LTX23HQSamplingParams,
@@ -788,6 +790,15 @@ def _register_configs():
"robbyant/lingbot-world-v2-14b-causal-fast-diffusers",
],
)
register_configs(
sampling_param_cls=LongLive2SamplingParams,
pipeline_config_cls=LongLive2T2VConfig,
hf_model_paths=[
# Since LongLive-2.0-5B does not have official diffusers release
"Rabinovich/LongLive-2.0-5B-Diffusers",
"Efficient-Large-Model/LongLive-2.0-5B",
],
)
register_configs(
sampling_param_cls=FastWanT2V480PConfig,
pipeline_config_cls=FastWan2_1_T2V_480P_Config,
@@ -32,6 +32,9 @@ class CausalSelfAttentionKVCache:
sink_tokens: int = 0
attention_window_size: int = 0
allow_growth: bool = False
global_sink_tokens: int = 0
pinned_start: int = -1
pinned_len: int = 0
def __post_init__(self) -> None:
if self.cache_size == 0:
@@ -46,6 +49,29 @@ class CausalSelfAttentionKVCache:
self.global_end_index_int = 0
if self.local_end_index_int is not None:
self.local_end_index_int = 0
self.reset_pinned_sink()
def reset_pinned_sink(self) -> None:
self.pinned_start = -1
self.pinned_len = 0
def pin_current_chunk(self, current_num_tokens: int) -> None:
if self.sink_tokens <= 0 or current_num_tokens <= 0:
self.reset_pinned_sink()
return
_, local_end_index = self._read_indices()
self.pinned_start = local_end_index - current_num_tokens
self.pinned_len = min(self.sink_tokens, current_num_tokens)
def _has_pinned_sink(self) -> bool:
return self.pinned_start >= 0 and self.pinned_len > 0
def _effective_sink_tokens(self) -> int:
if self._has_pinned_sink():
if self.pinned_start == self.global_sink_tokens:
return self.global_sink_tokens + self.pinned_len
return self.global_sink_tokens
return max(self.global_sink_tokens, self.sink_tokens)
def _read_indices(self) -> tuple[int, int]:
global_end_index = self.global_end_index_int
@@ -141,7 +167,7 @@ class CausalSelfAttentionKVCache:
)
current_chunk_end = current_chunk_start + num_new_tokens
kv_cache_size = self.cache_size
sink_tokens = self.sink_tokens
sink_tokens = self._effective_sink_tokens()
global_end_index, local_end_index_prev = self._read_indices()
# local_start(/end)_index: the local position of the start/end of current chunk
@@ -236,6 +262,9 @@ class CausalSelfAttentionKVCache:
:,
].clone()
if self._has_pinned_sink() and self.pinned_start >= sink_tokens:
self.pinned_start -= num_evicted_tokens
# if we move the minimum number of tokens, the right bound of the append token would be aligned with end of the buffer
local_end_index = kv_cache_size
else:
@@ -329,72 +358,159 @@ class CausalSelfAttentionKVCache:
heads.
"""
if recent_window_tokens is None:
if cache_head_slice is None:
return (
self.k[:, attn_start_index:updated_local_end],
self.v[:, attn_start_index:updated_local_end],
if self.global_sink_tokens > 0 or self._has_pinned_sink():
return self._pinned_attention_view(
attn_start_index=attn_start_index,
updated_local_end=updated_local_end,
cache_head_slice=cache_head_slice,
)
return (
self.k[:, attn_start_index:updated_local_end, cache_head_slice, :],
self.v[:, attn_start_index:updated_local_end, cache_head_slice, :],
return self._cache_slice(
slice(attn_start_index, updated_local_end),
cache_head_slice=cache_head_slice,
)
if recent_window_tokens < 0:
raise ValueError("recent_window_tokens must be non-negative or None")
sink_end = min(self.sink_tokens, updated_local_end)
sink_end = min(self._effective_sink_tokens(), updated_local_end)
recent_start = max(sink_end, local_start_index - recent_window_tokens)
if recent_start <= sink_end:
if cache_head_slice is None:
return self.k[:, :updated_local_end], self.v[:, :updated_local_end]
return (
self.k[:, :updated_local_end, cache_head_slice, :],
self.v[:, :updated_local_end, cache_head_slice, :],
)
if sink_end <= 0:
if cache_head_slice is None:
return (
self.k[:, recent_start:updated_local_end],
self.v[:, recent_start:updated_local_end],
)
return (
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
return self._cache_slice(
slice(0, updated_local_end),
cache_head_slice=cache_head_slice,
)
cache_slices = []
if sink_end > 0:
cache_slices.append(slice(0, sink_end))
if (
self._has_pinned_sink()
and self.pinned_start >= sink_end
and self.pinned_start < recent_start
):
cache_slices.append(
slice(self.pinned_start, self.pinned_start + self.pinned_len)
)
cache_slices.append(slice(recent_start, updated_local_end))
return self._cat_cache_slices(
cache_slices,
cache_head_slice=cache_head_slice,
)
def _cache_slice(
self,
cache_slice: slice,
*,
cache_head_slice: slice | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
if cache_head_slice is None:
return self.k[:, cache_slice], self.v[:, cache_slice]
return (
self.k[:, cache_slice, cache_head_slice, :],
self.v[:, cache_slice, cache_head_slice, :],
)
def _cat_cache_slices(
self,
cache_slices: list[slice],
*,
cache_head_slice: slice | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
if len(cache_slices) == 1:
return self._cache_slice(
cache_slices[0],
cache_head_slice=cache_head_slice,
)
if cache_head_slice is None:
return (
torch.cat(
[
self.k[:, :sink_end],
self.k[:, recent_start:updated_local_end],
],
dim=1,
[self.k[:, cache_slice] for cache_slice in cache_slices], dim=1
),
torch.cat(
[
self.v[:, :sink_end],
self.v[:, recent_start:updated_local_end],
],
dim=1,
[self.v[:, cache_slice] for cache_slice in cache_slices], dim=1
),
)
return (
torch.cat(
[
self.k[:, :sink_end, cache_head_slice, :],
self.k[:, recent_start:updated_local_end, cache_head_slice, :],
self.k[:, cache_slice, cache_head_slice, :]
for cache_slice in cache_slices
],
dim=1,
),
torch.cat(
[
self.v[:, :sink_end, cache_head_slice, :],
self.v[:, recent_start:updated_local_end, cache_head_slice, :],
self.v[:, cache_slice, cache_head_slice, :]
for cache_slice in cache_slices
],
dim=1,
),
)
def _pinned_attention_view(
self,
*,
attn_start_index: int,
updated_local_end: int,
cache_head_slice: slice | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
effective_sink_tokens = self._effective_sink_tokens()
prepend_sink = effective_sink_tokens > 0 and attn_start_index > 0
prepend_pinned = (
self._has_pinned_sink()
and self.pinned_start >= effective_sink_tokens
and self.pinned_start < attn_start_index
)
if prepend_sink and prepend_pinned:
extra_tokens = effective_sink_tokens + self.pinned_len
local_window_size = max(0, self.attention_window_size - extra_tokens)
local_window_start = max(
effective_sink_tokens,
updated_local_end - local_window_size,
)
cache_slices = [
slice(0, effective_sink_tokens),
slice(self.pinned_start, self.pinned_start + self.pinned_len),
slice(local_window_start, updated_local_end),
]
return self._cat_cache_slices(
cache_slices,
cache_head_slice=cache_head_slice,
)
if prepend_sink:
local_window_size = max(
0,
self.attention_window_size - effective_sink_tokens,
)
local_window_start = max(
effective_sink_tokens,
updated_local_end - local_window_size,
)
return self._cat_cache_slices(
[
slice(0, effective_sink_tokens),
slice(local_window_start, updated_local_end),
],
cache_head_slice=cache_head_slice,
)
if prepend_pinned:
local_window_size = max(0, self.attention_window_size - self.pinned_len)
local_window_start = max(0, updated_local_end - local_window_size)
return self._cat_cache_slices(
[
slice(self.pinned_start, self.pinned_start + self.pinned_len),
slice(local_window_start, updated_local_end),
],
cache_head_slice=cache_head_slice,
)
return self._cache_slice(
slice(attn_start_index, updated_local_end),
cache_head_slice=cache_head_slice,
)
@dataclass(slots=True)
class CrossAttentionKVCache:
@@ -521,7 +521,9 @@ class CausalWanTransformer3DModel(BaseDiT, LayerwiseOffloadableModuleMixin):
# 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
# Block size is bounded only by the causal block-mask construction, which
# supports any positive value.
assert self.num_frame_per_block >= 1
self.independent_first_frame = False
self.__post_init__()
@@ -0,0 +1,190 @@
# SPDX-License-Identifier: Apache-2.0
from typing import Any
import torch
import torch.nn as nn
from torch.nn.attention.flex_attention import BlockMask
from sglang.multimodal_gen.configs.models.dits.longlive2 import LongLive2VideoConfig
from sglang.multimodal_gen.runtime.layers.kvcache.causal_attention_cache import (
CausalSelfAttentionKVCache,
CrossAttentionKVCache,
)
from sglang.multimodal_gen.runtime.layers.layernorm import (
LayerNormScaleShift,
tensor_parallel_rms_norm,
)
from sglang.multimodal_gen.runtime.layers.quantization.configs.base_config import (
QuantizationConfig,
)
from sglang.multimodal_gen.runtime.models.dits.causal_wanvideo import (
CausalWanTransformer3DModel,
CausalWanTransformerBlock,
)
class LongLive2CausalWanTransformerBlock(CausalWanTransformerBlock):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.norm1 = LayerNormScaleShift(
self.hidden_dim,
eps=self.norm1.eps,
elementwise_affine=False,
dtype=torch.float32,
)
def _cross_attn_with_cache(
self,
hidden_states: torch.Tensor,
encoder_hidden_states: torch.Tensor,
crossattn_cache: CrossAttentionKVCache | None,
) -> torch.Tensor:
attn2 = self.attn2
q, _ = attn2.to_q(hidden_states)
if attn2.tp_rmsnorm:
q = tensor_parallel_rms_norm(q, attn2.norm_q)
else:
q = attn2.norm_q(q)
q = q.unflatten(2, (attn2.local_num_heads, attn2.head_dim))
if crossattn_cache is not None and crossattn_cache.is_init:
k = crossattn_cache.k
v = crossattn_cache.v
else:
k, _ = attn2.to_k(encoder_hidden_states)
if attn2.tp_rmsnorm:
k = tensor_parallel_rms_norm(k, attn2.norm_k)
else:
k = attn2.norm_k(k)
k = k.unflatten(2, (attn2.local_num_heads, attn2.head_dim))
v, _ = attn2.to_v(encoder_hidden_states)
v = v.unflatten(2, (attn2.local_num_heads, attn2.head_dim))
if crossattn_cache is not None:
crossattn_cache.store(k, v)
hidden_states = attn2.attn(q, k, v)
hidden_states = hidden_states.flatten(2)
hidden_states, _ = attn2.to_out(hidden_states)
return hidden_states
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:
if hidden_states.dim() == 4:
hidden_states = hidden_states.squeeze(1)
num_frames = temb.shape[1]
bs, _, _ = hidden_states.shape
orig_dtype = hidden_states.dtype
e = self.scale_shift_table + temb.float()
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
)
assert shift_msa.dtype == torch.float32
norm_hidden_states = self.norm1(hidden_states, shift_msa, scale_msa)
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:
query = self.norm_q(query)
if self.norm_k is not None:
key = self.norm_k(key)
query = query.squeeze(1).unflatten(2, (self.num_attention_heads, -1))
key = key.squeeze(1).unflatten(2, (self.num_attention_heads, -1))
value = value.squeeze(1).unflatten(2, (self.num_attention_heads, -1))
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)
attn_output = self._cross_attn_with_cache(
norm_hidden_states,
encoder_hidden_states,
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)
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 LongLive2Transformer3DModel(CausalWanTransformer3DModel):
_fsdp_shard_conditions = LongLive2VideoConfig()._fsdp_shard_conditions
_compile_conditions = LongLive2VideoConfig()._compile_conditions
_supported_attention_backends = LongLive2VideoConfig()._supported_attention_backends
param_names_mapping = LongLive2VideoConfig().param_names_mapping
reverse_param_names_mapping = LongLive2VideoConfig().reverse_param_names_mapping
lora_param_names_mapping = LongLive2VideoConfig().lora_param_names_mapping
def __init__(
self,
config: LongLive2VideoConfig,
hf_config: dict[str, Any],
quant_config: QuantizationConfig | None = None,
) -> None:
super().__init__(config=config, hf_config=hf_config, quant_config=quant_config)
inner_dim = config.num_attention_heads * config.attention_head_dim
self.blocks = nn.ModuleList(
[
LongLive2CausalWanTransformerBlock(
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)
]
)
EntryClass = LongLive2Transformer3DModel
@@ -0,0 +1,61 @@
# SPDX-License-Identifier: Apache-2.0
from sglang.multimodal_gen.configs.pipeline_configs.longlive2 import LongLive2T2VConfig
from sglang.multimodal_gen.configs.sample.longlive2 import LongLive2SamplingParams
from sglang.multimodal_gen.runtime.models.schedulers.scheduling_flow_unipc_multistep import (
FlowUniPCMultistepScheduler,
)
from sglang.multimodal_gen.runtime.pipelines.wan_causal_dmd_pipeline import (
WanCausalDMDPipeline,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages import InputValidationStage
from sglang.multimodal_gen.runtime.pipelines_core.stages.model_specific_stages.longlive2 import (
LongLive2CausalDenoisingStage,
LongLive2ImageVAEEncodingStage,
LongLive2LatentPreparationStage,
LongLive2TextEncodingStage,
)
from sglang.multimodal_gen.runtime.server_args import ServerArgs
class LongLive2Pipeline(WanCausalDMDPipeline):
pipeline_name = "LongLive2Pipeline"
pipeline_config_cls = LongLive2T2VConfig
sampling_params_cls = LongLive2SamplingParams
def initialize_pipeline(self, server_args: ServerArgs):
self.modules["scheduler"] = FlowUniPCMultistepScheduler(
num_train_timesteps=1000,
shift=1,
use_dynamic_shifting=False,
)
def create_pipeline_stages(self, server_args: ServerArgs) -> None:
self.add_stage(InputValidationStage())
self.add_stage(
LongLive2TextEncodingStage(
text_encoders=[self.get_module("text_encoder")],
tokenizers=[self.get_module("tokenizer")],
)
)
self.add_stage(
LongLive2ImageVAEEncodingStage(
vae=self.get_module("vae"),
component_name="vae",
)
)
self.add_stage(
LongLive2LatentPreparationStage(
scheduler=self.get_module("scheduler"),
transformer=self.get_module("transformer"),
)
)
self.add_stage(
LongLive2CausalDenoisingStage(
transformer=self.get_module("transformer"),
scheduler=self.get_module("scheduler"),
),
)
self.add_standard_decoding_stage()
EntryClass = LongLive2Pipeline
@@ -41,6 +41,65 @@ from sglang.multimodal_gen.runtime.utils.precision import (
logger = init_logger(__name__)
CAUSAL_BLOCK_PROMPTS_KEY = "causal_block_prompts"
CAUSAL_SCENE_CUT_MASK_KEY = "causal_scene_cut_mask"
CAUSAL_SHOT_INDICES_KEY = "causal_shot_indices"
def expand_causal_block_prompts(
shot_prompts: list[str],
*,
num_blocks: int,
shot_durations: list[int] | None = None,
chunks_per_shot: int = 0,
scene_cut_prefix: str = "",
) -> tuple[list[str], list[bool], list[int]]:
if not shot_prompts:
raise ValueError("shot_prompts must be non-empty")
if num_blocks <= 0:
raise ValueError("num_blocks must be positive")
if shot_durations is not None and len(shot_durations) != len(shot_prompts):
raise ValueError("shot_durations must match shot_prompts length")
if shot_durations is not None:
durations = shot_durations[: len(shot_prompts)]
elif chunks_per_shot > 0:
durations = [chunks_per_shot] * len(shot_prompts)
else:
base, extra = divmod(num_blocks, len(shot_prompts))
durations = [base + (1 if i < extra else 0) for i in range(len(shot_prompts))]
clamped: list[int] = []
remaining = num_blocks
for duration in durations:
if remaining <= 0:
break
take = min(int(duration), remaining)
clamped.append(take)
remaining -= take
if remaining > 0 and clamped:
clamped[-1] += remaining
if not clamped:
clamped = [num_blocks]
block_prompts: list[str] = []
scene_cut_mask: list[bool] = []
shot_indices: list[int] = []
for shot_idx, (caption, duration) in enumerate(zip(shot_prompts, clamped)):
for block_in_shot in range(duration):
is_scene_cut = shot_idx > 0 and block_in_shot == 0
if is_scene_cut and scene_cut_prefix:
block_prompts.append(scene_cut_prefix + caption)
else:
block_prompts.append(caption)
scene_cut_mask.append(is_scene_cut)
shot_indices.append(shot_idx)
return (
block_prompts[:num_blocks],
scene_cut_mask[:num_blocks],
shot_indices[:num_blocks],
)
@dataclass(slots=True)
class CausalDMDForwardContext:
@@ -89,6 +148,8 @@ class CausalDMDDenoisingStage(DenoisingStage):
# KV and cross-attention cache state (initialized on first forward)
self.causal_kv_cache: list | None = None
self.crossattn_cache: list | None = None
self.causal_kv_cache_neg: list | None = None
self.crossattn_cache_neg: list | None = None
# Model-dependent constants (aligned with causal_inference.py assumptions)
self.num_transformer_blocks = self.transformer.config.arch_config.num_layers
self.num_frames_per_block = (
@@ -189,6 +250,85 @@ class CausalDMDDenoisingStage(DenoisingStage):
assert torch.isnan(prompt_embeds[0]).sum() == 0
return prompt_embeds
@staticmethod
def _block_prompt_count(batch: Req) -> int | None:
block_prompts = batch.extra.get(CAUSAL_BLOCK_PROMPTS_KEY)
if block_prompts is None:
return None
return len(block_prompts)
@classmethod
def _select_block_conditioning(cls, value, block_index: int, block_count: int):
if isinstance(value, torch.Tensor) and value.shape[:1] == (block_count,):
return value[block_index : block_index + 1]
if isinstance(value, list):
return [
cls._select_block_conditioning(item, block_index, block_count)
for item in value
]
if isinstance(value, tuple):
return tuple(
cls._select_block_conditioning(item, block_index, block_count)
for item in value
)
if isinstance(value, dict):
return {
key: cls._select_block_conditioning(item, block_index, block_count)
for key, item in value.items()
}
return value
@classmethod
def _select_block_prompt_embeds(
cls,
batch: Req,
prompt_embeds,
block_index: int,
):
block_count = cls._block_prompt_count(batch)
if block_count is None:
return prompt_embeds
return cls._select_block_conditioning(prompt_embeds, block_index, block_count)
@classmethod
def _select_block_cond_kwargs(
cls,
batch: Req,
cond_kwargs: dict[str, Any],
block_index: int,
) -> dict[str, Any]:
block_count = cls._block_prompt_count(batch)
if block_count is None:
return cond_kwargs
return {
key: cls._select_block_conditioning(value, block_index, block_count)
for key, value in cond_kwargs.items()
}
def _reset_crossattn_cache_for_block(self, batch: Req, *caches) -> None:
if self._block_prompt_count(batch) is None:
return
for cache in caches:
if cache is not None:
self._reset_crossattn_cache(cache)
def _validate_block_prompt_count(self, batch: Req, block_sizes: list[int]) -> None:
block_count = self._block_prompt_count(batch)
if block_count is None:
return
if block_count != len(block_sizes):
raise ValueError(
"causal block prompt count must match causal block count, "
f"got {block_count} prompts and {len(block_sizes)} blocks"
)
@staticmethod
def _shot_index(batch: Req, block_index: int) -> int:
shot_indices = batch.extra.get(CAUSAL_SHOT_INDICES_KEY)
if not isinstance(shot_indices, list) or block_index >= len(shot_indices):
return 0
return int(shot_indices[block_index])
def _prepare_causal_dmd_forward_context(
self,
batch: Req,
@@ -853,6 +993,93 @@ class CausalDMDDenoisingStage(DenoisingStage):
for cache_block in kv_cache:
cache_block.reset_indices()
def _causal_kv_cache_global_sink_tokens_for_batch(self, batch: Req) -> int:
return 0
def _causal_kv_cache_kwargs_for_batch(
self,
batch: Req,
) -> dict[str, Any] | None:
global_sink_tokens = self._causal_kv_cache_global_sink_tokens_for_batch(batch)
if global_sink_tokens <= 0:
return None
return {"global_sink_tokens": global_sink_tokens}
def _cache_needs_reinit_for_batch(self, kv_cache, batch: Req) -> bool:
if kv_cache is None or len(kv_cache) != self.num_transformer_blocks:
return True
expected_global_sink_tokens = (
self._causal_kv_cache_global_sink_tokens_for_batch(batch)
)
return kv_cache[0].global_sink_tokens != expected_global_sink_tokens
def _pin_current_chunk(self, kv_cache, current_num_frames: int) -> None:
if kv_cache is None:
return
current_num_tokens = current_num_frames * self.num_token_per_frame
for cache_block in kv_cache:
cache_block.pin_current_chunk(current_num_tokens)
def _is_scene_cut(self, batch: Req, block_index: int) -> bool:
scene_cut_mask = batch.extra.get(CAUSAL_SCENE_CUT_MASK_KEY)
if not isinstance(scene_cut_mask, list) or block_index >= len(scene_cut_mask):
return False
return bool(scene_cut_mask[block_index])
def _new_causal_cache_pair(
self,
*,
batch_size: int,
max_text_len: int,
dtype: torch.dtype,
device: torch.device,
kv_cache_kwargs: dict[str, Any] | None = None,
) -> tuple[list, list]:
prev_kv_cache = self.causal_kv_cache
prev_crossattn_cache = self.crossattn_cache
try:
return self._initialize_causal_caches(
batch_size=batch_size,
max_text_len=max_text_len,
dtype=dtype,
device=device,
kv_cache_kwargs=kv_cache_kwargs,
)
finally:
self.causal_kv_cache = prev_kv_cache
self.crossattn_cache = prev_crossattn_cache
def _reset_or_init_negative_caches(
self,
*,
batch: Req,
batch_size: int,
max_text_len: int,
dtype: torch.dtype,
device: torch.device,
kv_cache_kwargs: dict[str, Any] | None = None,
) -> tuple[list, list]:
if (
self._cache_needs_reinit_for_batch(self.causal_kv_cache_neg, batch)
or self.crossattn_cache_neg is None
):
(
self.causal_kv_cache_neg,
self.crossattn_cache_neg,
) = self._new_causal_cache_pair(
batch_size=batch_size,
max_text_len=max_text_len,
dtype=dtype,
device=device,
kv_cache_kwargs=kv_cache_kwargs,
)
else:
self._reset_causal_caches(
kv_cache=self.causal_kv_cache_neg,
crossattn_cache=self.crossattn_cache_neg,
)
return self.causal_kv_cache_neg, self.crossattn_cache_neg
def _get_causal_kv_cache_size(
self,
*,
@@ -879,6 +1106,7 @@ class CausalDMDDenoisingStage(DenoisingStage):
device,
use_int_indices: bool = False,
sink_tokens: int = 0,
global_sink_tokens: int = 0,
attention_window_size: int | None = None,
allow_growth: bool = False,
) -> list[CausalSelfAttentionKVCache]:
@@ -915,6 +1143,7 @@ class CausalDMDDenoisingStage(DenoisingStage):
local_end_index_int=int_index,
cache_size=kv_cache_size,
sink_tokens=sink_tokens,
global_sink_tokens=global_sink_tokens,
attention_window_size=attention_window_size,
allow_growth=allow_growth,
)
@@ -1095,6 +1324,7 @@ class CausalDMDDenoisingStage(DenoisingStage):
*,
sequence_shard_enabled: bool = False,
kv_cache_size: int | None = None,
global_sink_tokens: int = 0,
) -> None:
"""
Initialize (but not fill) a Per-GPU KV cache aligned with the model assumptions.
@@ -1118,6 +1348,7 @@ class CausalDMDDenoisingStage(DenoisingStage):
sequence_shard_enabled=sequence_shard_enabled
),
sink_tokens=self._get_causal_sink_tokens(),
global_sink_tokens=global_sink_tokens,
attention_window_size=self._get_causal_attention_window_size(kv_cache_size),
)
@@ -0,0 +1,902 @@
# SPDX-License-Identifier: Apache-2.0
from collections.abc import Callable
from typing import Any
import torch
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
from sglang.multimodal_gen.runtime.pipelines_core.stages.causal_denoising import (
CAUSAL_BLOCK_PROMPTS_KEY,
CAUSAL_SCENE_CUT_MASK_KEY,
CAUSAL_SHOT_INDICES_KEY,
CausalDMDDenoisingStage,
expand_causal_block_prompts,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.image_encoding import (
ImageVAEEncodingStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.latent_preparation import (
LatentPreparationSpec,
LatentPreparationStage,
)
from sglang.multimodal_gen.runtime.pipelines_core.stages.text_encoding import (
TextEncodingStage,
)
from sglang.multimodal_gen.runtime.server_args import ServerArgs
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
from sglang.multimodal_gen.runtime.utils.perf_logger import StageProfiler
logger = init_logger(__name__)
LONG_LIVE2_DEFAULT_SCENE_CUT_PREFIX = "The scene transitions. "
def _latent_frame_count(batch: Req, server_args: ServerArgs) -> int:
num_frames = batch.num_frames
vae_config = server_args.pipeline_config.vae_config
if vae_config.use_temporal_scaling_frames:
temporal_scale_factor = vae_config.arch_config.temporal_compression_ratio
num_frames = (num_frames - 1) // temporal_scale_factor + 1
return int(num_frames)
def _causal_block_count(batch: Req, server_args: ServerArgs) -> int:
latent_frames = _latent_frame_count(batch, server_args)
block_size = server_args.pipeline_config.dit_config.arch_config.num_frames_per_block
if latent_frames % block_size != 0:
raise ValueError(
"LongLive2 latent frames must be divisible by num_frames_per_block "
f"({block_size}), got {latent_frames}"
)
return latent_frames // block_size
def expand_longlive2_shot_prompts(
shot_prompts: list[str],
*,
num_blocks: int,
shot_durations: list[int] | None = None,
chunks_per_shot: int = 0,
scene_cut_prefix: str = LONG_LIVE2_DEFAULT_SCENE_CUT_PREFIX,
) -> list[str]:
return expand_causal_block_prompts(
shot_prompts,
num_blocks=num_blocks,
shot_durations=shot_durations,
chunks_per_shot=chunks_per_shot,
scene_cut_prefix=scene_cut_prefix,
)[0]
class LongLive2TextEncodingStage(TextEncodingStage):
def build_dedup_fingerprint(self, batch: Req, server_args: ServerArgs):
base = super().build_dedup_fingerprint(batch, server_args)
return (
base,
self.freeze_for_dedup(getattr(batch, "shot_prompts", None)),
self.freeze_for_dedup(getattr(batch, "shot_durations", None)),
int(getattr(batch, "chunks_per_shot", 0) or 0),
getattr(batch, "scene_cut_prefix", None),
)
def _block_prompts(self, batch: Req, server_args: ServerArgs) -> list[str] | None:
shot_prompts = getattr(batch, "shot_prompts", None)
if shot_prompts is None:
return None
if isinstance(batch.prompt, list):
raise ValueError("LongLive2 shot_prompts supports one video per request")
block_prompts, scene_cut_mask, shot_indices = expand_causal_block_prompts(
shot_prompts,
num_blocks=_causal_block_count(batch, server_args),
shot_durations=getattr(batch, "shot_durations", None),
chunks_per_shot=int(getattr(batch, "chunks_per_shot", 0) or 0),
scene_cut_prefix=(
LONG_LIVE2_DEFAULT_SCENE_CUT_PREFIX
if getattr(batch, "scene_cut_prefix", None) is None
else getattr(batch, "scene_cut_prefix")
),
)
batch.extra[CAUSAL_BLOCK_PROMPTS_KEY] = block_prompts
batch.extra[CAUSAL_SCENE_CUT_MASK_KEY] = scene_cut_mask
batch.extra[CAUSAL_SHOT_INDICES_KEY] = shot_indices
return block_prompts
@torch.no_grad()
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
block_prompts = self._block_prompts(batch, server_args)
if block_prompts is None:
return super().forward(batch, server_args)
original_prompt = batch.prompt
batch.prompt = block_prompts
try:
return super().forward(batch, server_args)
finally:
batch.prompt = original_prompt
class LongLive2ImageVAEEncodingStage(ImageVAEEncodingStage):
def preprocess(self, image):
image = super().preprocess(image)
if image.ndim == 5:
image = image.squeeze(2)
return image
class LongLive2LatentPreparationStage(LatentPreparationStage):
def get_latent_preparation_spec(
self,
batch: Req,
server_args: ServerArgs,
batch_size: int,
num_frames: int,
device: torch.device | str,
) -> LatentPreparationSpec:
b, c, t, h, w = server_args.pipeline_config.prepare_latent_shape(
batch, batch_size, num_frames
)
return LatentPreparationSpec(
shape=(b, t, c, h, w),
dtype=self._get_latent_dtype(batch, server_args),
device=device,
prepare_latent_ids=False,
pack_latents=False,
)
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
batch = super().forward(batch, server_args)
return self._normalize_latent_layout(batch, server_args)
def _prepare_grouped_latents(
self,
batches: list[Req],
server_args: ServerArgs,
) -> Req:
batch = super()._prepare_grouped_latents(batches, server_args)
return self._normalize_latent_layout(batch, server_args)
@staticmethod
def _expected_latent_channels(batch: Req, server_args: ServerArgs) -> int:
shape = server_args.pipeline_config.prepare_latent_shape(
batch,
batch.batch_size,
batch.latents.shape[1],
)
return int(shape[1])
def _normalize_latent_layout(self, batch: Req, server_args: ServerArgs) -> Req:
latents = batch.latents
if latents is None or latents.ndim != 5:
return batch
expected_channels = self._expected_latent_channels(batch, server_args)
if (
latents.shape[1] != expected_channels
and latents.shape[2] == expected_channels
):
latents = latents.permute(0, 2, 1, 3, 4).contiguous()
batch.latents = latents
batch.raw_latent_shape = latents.shape
return batch
class LongLive2CausalDenoisingStage(CausalDMDDenoisingStage):
def __init__(self, transformer, scheduler) -> None:
super().__init__(transformer, scheduler)
self._rope_temporal_offset = 0.0
self._i2v_image_latent: torch.Tensor | None = None
def _get_causal_dmd_latents(self, batch: Req) -> torch.Tensor:
latents = super()._get_causal_dmd_latents(batch)
if torch.is_inference(latents):
latents = latents.clone()
batch.latents = latents
return latents
@torch.no_grad()
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
return self._forward_one_shot_common(
batch, server_args, use_cfg=self._use_cfg(batch)
)
@staticmethod
def _i2v_clamp_active(batch: Req) -> bool:
image_latent = getattr(batch, "image_latent", None)
return image_latent is not None and image_latent.shape[2] == 1
def _prepare_i2v_clamp(self, current_latents, start_frame):
clamp_latent = self._i2v_image_latent if start_frame == 0 else None
if clamp_latent is None:
return None, 0
clamp_latent = clamp_latent.to(
device=current_latents.device, dtype=current_latents.dtype
)
return clamp_latent, clamp_latent.shape[2]
@staticmethod
def _use_cfg(batch: Req) -> bool:
return bool(getattr(batch, "do_classifier_free_guidance", False))
@staticmethod
def _guidance_scale(batch: Req) -> float:
return float(getattr(batch, "guidance_scale", 1.0))
@staticmethod
def _denoise_step_profiler(batch: Req, start_frame: int, step_index: int):
return StageProfiler(
f"denoising_step_{start_frame}_{step_index}",
logger=logger,
metrics=batch.metrics,
perf_dump_path_provided=batch.perf_dump_path is not None,
record_as_step=True,
)
@staticmethod
def _get_negative_prompt_embeds(batch: Req):
negative_prompt_embeds = getattr(batch, "negative_prompt_embeds", None)
if negative_prompt_embeds is None or (
isinstance(negative_prompt_embeds, list)
and len(negative_prompt_embeds) == 0
):
raise ValueError(
"LongLive2 classifier-free guidance requires negative_prompt_embeds"
)
return negative_prompt_embeds
def _prepare_causal_dmd_neg_cond_kwargs(
self,
batch: Req,
server_args: ServerArgs,
target_dtype: torch.dtype,
) -> dict[str, Any]:
return self.prepare_extra_func_kwargs(
self.transformer.forward,
{
"encoder_attention_mask": batch.negative_attention_mask,
},
)
def _multi_shot_sink_enabled(self, batch: Req) -> bool:
return (
self._block_prompt_count(batch) is not None
and bool(getattr(batch, "multi_shot_sink", True))
and self.sink_size > 0
)
def _causal_kv_cache_global_sink_tokens_for_batch(self, batch: Req) -> int:
if not self._multi_shot_sink_enabled(batch):
return 0
return self._get_causal_sink_tokens()
def _is_scene_cut(self, batch: Req, block_index: int) -> bool:
if not self._multi_shot_sink_enabled(batch):
return False
return super()._is_scene_cut(batch, block_index)
def _set_rope_temporal_offset(self, batch: Req, shot_index: int) -> None:
offset = float(getattr(batch, "multi_shot_rope_offset", 8.0) or 0.0)
self._rope_temporal_offset = shot_index * offset
def _forward_one_shot_common(
self, batch: Req, server_args: ServerArgs, *, use_cfg: bool
) -> Req:
ctx = self._prepare_causal_dmd_forward_context(batch, server_args)
target_dtype = ctx.target_dtype
autocast_enabled = ctx.autocast_enabled
scheduler = ctx.scheduler
device = ctx.device
timesteps = ctx.timesteps
image_kwargs = ctx.image_kwargs
pos_cond_kwargs = ctx.pos_cond_kwargs
latents = ctx.latents
prompt_embeds = ctx.prompt_embeds
t, h, w = ctx.num_frames, ctx.height, ctx.width
negative_prompt_embeds = None
neg_cond_kwargs = None
if use_cfg:
neg_cond_kwargs = self._prepare_causal_dmd_neg_cond_kwargs(
batch, server_args, target_dtype
)
negative_prompt_embeds = self._get_negative_prompt_embeds(batch)
independent_first_frame = self.transformer.independent_first_frame
max_text_len = self._get_max_text_len(server_args)
kv_cache_kwargs = self._causal_kv_cache_kwargs_for_batch(batch)
self._rope_temporal_offset = 0.0
if self._cache_needs_reinit_for_batch(self.causal_kv_cache, batch):
self._initialize_causal_caches(
batch_size=latents.shape[0],
max_text_len=max_text_len,
dtype=target_dtype,
device=latents.device,
kv_cache_kwargs=kv_cache_kwargs,
)
else:
assert self.crossattn_cache is not None
self._reset_causal_caches(
kv_cache=self.causal_kv_cache,
crossattn_cache=self.crossattn_cache,
)
kv_cache_neg = None
crossattn_cache_neg = None
if use_cfg:
kv_cache_neg, crossattn_cache_neg = self._reset_or_init_negative_caches(
batch=batch,
batch_size=latents.shape[0],
max_text_len=max_text_len,
dtype=target_dtype,
device=latents.device,
kv_cache_kwargs=kv_cache_kwargs,
)
current_start_frame = 0
clamp_i2v = self._i2v_clamp_active(batch)
self._i2v_image_latent = batch.image_latent if clamp_i2v else None
if getattr(batch, "image_latent", None) is not None and not clamp_i2v:
image_latent = batch.image_latent
assert image_latent is not None
input_frames = image_latent.shape[2]
warmup_prompt_embeds = self._select_block_prompt_embeds(
batch, prompt_embeds, 0
)
warmup_pos_cond_kwargs = self._select_block_cond_kwargs(
batch, pos_cond_kwargs, 0
)
warmup_neg_prompt_embeds = (
self._select_block_prompt_embeds(batch, negative_prompt_embeds, 0)
if use_cfg
else None
)
warmup_neg_cond_kwargs = (
self._select_block_cond_kwargs(batch, neg_cond_kwargs, 0)
if use_cfg
else None
)
def warm_up(context_input, start_frame):
self._warm_up_causal_context_cache(
batch,
server_args,
context_input=context_input,
prompt_embeds=warmup_prompt_embeds,
kv_cache=self.causal_kv_cache,
crossattn_cache=self.crossattn_cache,
current_start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=warmup_pos_cond_kwargs,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
if use_cfg:
self._warm_up_causal_context_cache(
batch,
server_args,
context_input=context_input,
prompt_embeds=warmup_neg_prompt_embeds,
kv_cache=kv_cache_neg,
crossattn_cache=crossattn_cache_neg,
current_start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=warmup_neg_cond_kwargs,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
if independent_first_frame and input_frames >= 1:
warm_up(image_latent[:, :, :1, :, :], current_start_frame)
current_start_frame += 1
remaining_frames = input_frames - 1
else:
remaining_frames = input_frames
while remaining_frames > 0:
block = min(self.num_frames_per_block, remaining_frames)
warm_up(
image_latent[
:, :, current_start_frame : current_start_frame + block, :, :
],
current_start_frame,
)
current_start_frame += block
remaining_frames -= block
pos_start_base = current_start_frame
if not independent_first_frame or (
independent_first_frame and batch.image_latent is not None
):
if t % self.num_frames_per_block != 0:
raise ValueError(
"num_frames must be divisible by num_frames_per_block for causal DMD denoising"
)
num_blocks = t // self.num_frames_per_block
block_sizes = [self.num_frames_per_block] * num_blocks
else:
if (t - 1) % self.num_frames_per_block != 0:
raise ValueError(
"(num_frames - 1) must be divisible by num_frame_per_block when independent_first_frame=True"
)
num_blocks = (t - 1) // self.num_frames_per_block
block_sizes = [1] + [self.num_frames_per_block] * num_blocks
start_index = 0
self._validate_block_prompt_count(batch, block_sizes)
def prepare_context_input(current_latents):
return current_latents
with self.progress_bar(total=len(block_sizes) * len(timesteps)) as progress_bar:
for block_index, current_num_frames in enumerate(block_sizes):
self._set_rope_temporal_offset(
batch, self._shot_index(batch, block_index)
)
is_scene_cut = self._is_scene_cut(batch, block_index)
current_latents = latents[
:, :, start_index : start_index + current_num_frames, :, :
]
current_prompt_embeds = self._select_block_prompt_embeds(
batch, prompt_embeds, block_index
)
current_pos_cond_kwargs = self._select_block_cond_kwargs(
batch, pos_cond_kwargs, block_index
)
caches = [self.crossattn_cache]
if use_cfg:
caches.append(crossattn_cache_neg)
self._reset_crossattn_cache_for_block(batch, *caches)
def prepare_model_input(current_latents):
latent_model_input = current_latents
if (
batch.image_latent is not None
and independent_first_frame
and start_index == 0
):
latent_model_input = torch.cat(
[latent_model_input, batch.image_latent], dim=2
)
return latent_model_input
current_start_tokens = (
pos_start_base + start_index
) * self.num_token_per_frame
block_kwargs = dict(
chunk_latents=current_latents,
scheduler=scheduler,
timesteps=timesteps,
prompt_embeds=current_prompt_embeds,
kv_cache=self.causal_kv_cache,
crossattn_cache=self.crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=start_index,
image_kwargs=image_kwargs,
pos_cond_kwargs=current_pos_cond_kwargs,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
device=device,
attn_raw_latent_shape=(current_num_frames, h, w),
prepare_model_input=prepare_model_input,
prepare_context_input=prepare_context_input,
progress_bar=progress_bar,
)
if use_cfg:
current_latents = self._denoise_and_update_causal_block_cfg(
batch,
server_args,
negative_prompt_embeds=self._select_block_prompt_embeds(
batch, negative_prompt_embeds, block_index
),
kv_cache_neg=kv_cache_neg,
crossattn_cache_neg=crossattn_cache_neg,
neg_cond_kwargs=self._select_block_cond_kwargs(
batch, neg_cond_kwargs, block_index
),
**block_kwargs,
)
else:
current_latents = self._denoise_and_update_causal_block(
batch, server_args, **block_kwargs
)
if is_scene_cut:
self._pin_current_chunk(self.causal_kv_cache, current_num_frames)
if use_cfg:
self._pin_current_chunk(kv_cache_neg, current_num_frames)
latents[:, :, start_index : start_index + current_num_frames, :, :] = (
current_latents
)
start_index += current_num_frames
self._rope_temporal_offset = 0.0
batch.latents = latents
return batch
def _forward_causal_transformer(
self,
batch: Req,
*,
latent_model_input: torch.Tensor,
prompt_embeds,
timestep: torch.Tensor,
kv_cache,
crossattn_cache,
current_start_tokens: int,
start_frame: int,
image_kwargs: dict,
pos_cond_kwargs: dict,
current_timestep: int,
attn_metadata,
target_dtype: torch.dtype,
autocast_enabled: bool,
) -> torch.Tensor:
self._manage_dit_use_site(self.transformer, "transformer", batch)
rope_start_frame = start_frame
if self._rope_temporal_offset != 0.0:
rope_start_frame = start_frame + self._rope_temporal_offset
return super()._forward_causal_transformer(
batch,
latent_model_input=latent_model_input,
prompt_embeds=prompt_embeds,
timestep=timestep,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=rope_start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
current_timestep=current_timestep,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
def _prepare_causal_dmd_timesteps(
self,
batch: Req,
server_args: ServerArgs,
scheduler,
device: torch.device,
) -> torch.Tensor:
scheduler.set_timesteps(
batch.num_inference_steps,
device=device,
shift=server_args.pipeline_config.flow_shift,
)
return scheduler.timesteps.to(device)
def _denoise_causal_dmd_chunk(
self,
batch: Req,
server_args: ServerArgs,
*,
chunk_latents: torch.Tensor,
scheduler,
timesteps: torch.Tensor,
prompt_embeds,
kv_cache,
crossattn_cache,
current_start_tokens: int,
start_frame: int,
image_kwargs: dict,
pos_cond_kwargs: dict,
target_dtype: torch.dtype,
autocast_enabled: bool,
device: torch.device,
attn_raw_latent_shape: tuple[int, int, int],
prepare_model_input: Callable[[torch.Tensor], torch.Tensor],
progress_bar=None,
) -> tuple[torch.Tensor, Any | None]:
scheduler.set_timesteps(
len(timesteps),
device=device,
shift=server_args.pipeline_config.flow_shift,
)
timesteps = scheduler.timesteps.to(device)
current_latents = chunk_latents
attn_metadata = None
clamp_latent, context_frames = self._prepare_i2v_clamp(
current_latents, start_frame
)
if clamp_latent is not None:
current_latents = current_latents.clone()
for current_timestep, timestep in enumerate(timesteps):
with self._denoise_step_profiler(batch, start_frame, current_timestep):
if clamp_latent is not None:
current_latents[:, :, :context_frames] = clamp_latent
latent_model_input = prepare_model_input(current_latents).to(
target_dtype
)
attn_metadata = self._build_causal_attn_metadata(
batch,
server_args,
current_timestep=current_timestep,
raw_latent_shape=attn_raw_latent_shape,
device=device,
)
batch_size = latent_model_input.shape[0]
timestep_2d = (
timestep.reshape(1)
.to(device=latent_model_input.device, dtype=torch.float32)
.expand(batch_size, latent_model_input.shape[2])
)
if clamp_latent is not None:
timestep_2d = timestep_2d.clone()
timestep_2d[:, :context_frames] = 0
flow_pred = self._forward_causal_transformer(
batch,
latent_model_input=latent_model_input,
prompt_embeds=prompt_embeds,
timestep=timestep_2d,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
current_timestep=current_timestep,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
next_latents = scheduler.step(
flow_pred,
timestep,
current_latents,
return_dict=False,
)[0]
current_latents = next_latents
if clamp_latent is not None:
current_latents[:, :, :context_frames] = clamp_latent
if progress_bar is not None:
progress_bar.update()
return current_latents, attn_metadata
def _denoise_causal_dmd_chunk_cfg(
self,
batch: Req,
server_args: ServerArgs,
*,
chunk_latents: torch.Tensor,
scheduler,
timesteps: torch.Tensor,
prompt_embeds,
negative_prompt_embeds,
kv_cache,
crossattn_cache,
kv_cache_neg,
crossattn_cache_neg,
current_start_tokens: int,
start_frame: int,
image_kwargs: dict,
pos_cond_kwargs: dict,
neg_cond_kwargs: dict,
target_dtype: torch.dtype,
autocast_enabled: bool,
device: torch.device,
attn_raw_latent_shape: tuple[int, int, int],
prepare_model_input: Callable[[torch.Tensor], torch.Tensor],
progress_bar=None,
) -> tuple[torch.Tensor, Any | None]:
scheduler.set_timesteps(
len(timesteps),
device=device,
shift=server_args.pipeline_config.flow_shift,
)
timesteps = scheduler.timesteps.to(device)
current_latents = chunk_latents
attn_metadata = None
guidance_scale = self._guidance_scale(batch)
clamp_latent, context_frames = self._prepare_i2v_clamp(
current_latents, start_frame
)
if clamp_latent is not None:
current_latents = current_latents.clone()
for current_timestep, timestep in enumerate(timesteps):
with self._denoise_step_profiler(batch, start_frame, current_timestep):
if clamp_latent is not None:
current_latents[:, :, :context_frames] = clamp_latent
latent_model_input = prepare_model_input(current_latents).to(
target_dtype
)
attn_metadata = self._build_causal_attn_metadata(
batch,
server_args,
current_timestep=current_timestep,
raw_latent_shape=attn_raw_latent_shape,
device=device,
)
batch_size = latent_model_input.shape[0]
timestep_2d = (
timestep.reshape(1)
.to(device=latent_model_input.device, dtype=torch.float32)
.expand(batch_size, latent_model_input.shape[2])
)
if clamp_latent is not None:
timestep_2d = timestep_2d.clone()
timestep_2d[:, :context_frames] = 0
flow_pred_cond = self._forward_causal_transformer(
batch,
latent_model_input=latent_model_input,
prompt_embeds=prompt_embeds,
timestep=timestep_2d,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
current_timestep=current_timestep,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
flow_pred_uncond = self._forward_causal_transformer(
batch,
latent_model_input=latent_model_input,
prompt_embeds=negative_prompt_embeds,
timestep=timestep_2d,
kv_cache=kv_cache_neg,
crossattn_cache=crossattn_cache_neg,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=neg_cond_kwargs,
current_timestep=current_timestep,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
flow_pred = flow_pred_uncond + guidance_scale * (
flow_pred_cond - flow_pred_uncond
)
next_latents = scheduler.step(
flow_pred,
timestep,
current_latents,
return_dict=False,
)[0]
current_latents = next_latents
if clamp_latent is not None:
current_latents[:, :, :context_frames] = clamp_latent
if progress_bar is not None:
progress_bar.update()
return current_latents, attn_metadata
def _denoise_and_update_causal_block_cfg(
self,
batch: Req,
server_args: ServerArgs,
*,
chunk_latents: torch.Tensor,
scheduler,
timesteps: torch.Tensor,
prompt_embeds,
negative_prompt_embeds,
kv_cache,
crossattn_cache,
kv_cache_neg,
crossattn_cache_neg,
current_start_tokens: int,
start_frame: int,
image_kwargs: dict,
pos_cond_kwargs: dict,
neg_cond_kwargs: dict,
target_dtype: torch.dtype,
autocast_enabled: bool,
device: torch.device,
attn_raw_latent_shape: tuple[int, int, int],
prepare_model_input: Callable[[torch.Tensor], torch.Tensor],
prepare_context_input: Callable[[torch.Tensor], torch.Tensor],
progress_bar=None,
) -> torch.Tensor:
current_latents, attn_metadata = self._denoise_causal_dmd_chunk_cfg(
batch,
server_args,
chunk_latents=chunk_latents,
scheduler=scheduler,
timesteps=timesteps,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
kv_cache_neg=kv_cache_neg,
crossattn_cache_neg=crossattn_cache_neg,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
neg_cond_kwargs=neg_cond_kwargs,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
device=device,
attn_raw_latent_shape=attn_raw_latent_shape,
prepare_model_input=prepare_model_input,
progress_bar=progress_bar,
)
context_input = prepare_context_input(current_latents)
self._update_causal_context_cache(
batch,
server_args,
context_input=context_input,
prompt_embeds=prompt_embeds,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
self._update_causal_context_cache(
batch,
server_args,
context_input=context_input,
prompt_embeds=negative_prompt_embeds,
kv_cache=kv_cache_neg,
crossattn_cache=crossattn_cache_neg,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=neg_cond_kwargs,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
return current_latents
def _update_causal_context_cache(
self,
batch: Req,
server_args: ServerArgs,
*,
context_input: torch.Tensor,
prompt_embeds,
kv_cache,
crossattn_cache,
current_start_tokens: int,
start_frame: int,
image_kwargs: dict,
pos_cond_kwargs: dict,
attn_metadata,
target_dtype: torch.dtype,
autocast_enabled: bool,
) -> None:
context_noise = getattr(server_args.pipeline_config, "context_noise", 0)
timestep = torch.full(
(context_input.shape[0], context_input.shape[2]),
float(context_noise),
device=context_input.device,
dtype=torch.float32,
)
self._forward_causal_transformer(
batch,
latent_model_input=context_input.to(target_dtype),
prompt_embeds=prompt_embeds,
timestep=timestep,
kv_cache=kv_cache,
crossattn_cache=crossattn_cache,
current_start_tokens=current_start_tokens,
start_frame=start_frame,
image_kwargs=image_kwargs,
pos_cond_kwargs=pos_cond_kwargs,
current_timestep=0,
attn_metadata=attn_metadata,
target_dtype=target_dtype,
autocast_enabled=autocast_enabled,
)
@@ -57,6 +57,80 @@ from sglang.utils import is_in_ci
logger = init_logger(__name__)
_NON_WEIGHT_DIFFUSERS_COMPONENT_HINTS = (
"tokenizer",
"scheduler",
"processor",
"feature_extractor",
)
_WEIGHT_FILE_PATTERNS = (
"*.safetensors",
"*.bin",
"*.pt",
"*.pth",
"*.ckpt",
)
def _is_diffusers_component_entry(value: Any) -> bool:
return (
isinstance(value, (list, tuple))
and len(value) == 2
and all(item is None or isinstance(item, str) for item in value)
)
def _is_weight_bearing_diffusers_component(key: str, value: Any) -> bool:
if (
key.startswith("_")
or not _is_diffusers_component_entry(value)
or not any(item is not None for item in value)
):
return False
key_lower = key.lower()
return not any(hint in key_lower for hint in _NON_WEIGHT_DIFFUSERS_COMPONENT_HINTS)
def _get_declared_weight_component_dirs(model_path: str) -> list[str]:
model_index_path = os.path.join(model_path, "model_index.json")
if not os.path.exists(model_index_path):
return []
try:
with open(model_index_path) as f:
model_index = json.load(f)
except Exception as exc:
logger.warning(
"Failed to read model_index.json at %s: %s", model_index_path, exc
)
return []
return [
key
for key, value in model_index.items()
if _is_weight_bearing_diffusers_component(key, value)
]
def _has_local_weight_files(component_path: str) -> bool:
return any(
glob.glob(os.path.join(component_path, pattern))
for pattern in _WEIGHT_FILE_PATTERNS
)
def _get_missing_declared_weight_components(model_path: str) -> list[str]:
missing_files = []
for component_dir in _get_declared_weight_component_dirs(model_path):
component_path = os.path.join(model_path, component_dir)
if not os.path.isdir(component_path):
missing_files.append(f"{component_dir}/")
elif not _has_local_weight_files(component_path):
missing_files.append(f"{component_dir}/<weights>")
return missing_files
def _check_index_files_for_missing_shards(
model_path: str,
) -> tuple[bool, list[str], list[str]]:
@@ -74,6 +148,15 @@ def _check_index_files_for_missing_shards(
"""
missing_files = []
checked_subdirs = []
checked_subdir_set = set()
def _record_checked_subdir(dir_path: str) -> None:
subdir = os.path.basename(dir_path)
if not subdir:
subdir = "."
if subdir not in checked_subdir_set:
checked_subdirs.append(subdir)
checked_subdir_set.add(subdir)
# Add common subdirectories for diffusers models
try:
@@ -85,6 +168,10 @@ def _check_index_files_for_missing_shards(
# Check the root directory and all subdirectories that might contain model weights
dirs_to_check = [model_path]
for component_dir in _get_declared_weight_component_dirs(model_path):
_record_checked_subdir(os.path.join(model_path, component_dir))
missing_files.extend(_get_missing_declared_weight_components(model_path))
for subdir in subdirs:
subdir_path = os.path.join(model_path, subdir)
if os.path.isdir(subdir_path):
@@ -95,7 +182,7 @@ def _check_index_files_for_missing_shards(
index_files = glob.glob(os.path.join(dir_path, "*.safetensors.index.json"))
for index_file in index_files:
checked_subdirs.append(os.path.basename(dir_path))
_record_checked_subdir(dir_path)
try:
with open(index_file) as f:
index_data = json.load(f)
@@ -227,12 +314,13 @@ def _verify_diffusers_model_complete(path: str) -> bool:
component_keys = [
key
for key, value in model_index.items()
if isinstance(value, (list, tuple))
and len(value) == 2
and all(isinstance(item, str) for item in value)
if _is_diffusers_component_entry(value)
and any(item is not None for item in value)
]
if component_keys:
return all(os.path.exists(os.path.join(path, key)) for key in component_keys)
return all(
os.path.exists(os.path.join(path, key)) for key in component_keys
) and not _get_missing_declared_weight_components(path)
return os.path.exists(os.path.join(path, "transformer")) and os.path.exists(
os.path.join(path, "vae")
@@ -22,7 +22,8 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
DiffusionTestCase,
IDEOGRAM4_CI_sampling_params,
JOY_ECHO_T2V_CI_sampling_params,
LINGBOT_WORLD_REALTIME_sampling_params,
LONGLIVE2_I2V_CI_sampling_params,
LONGLIVE2_T2V_CI_sampling_params,
MODELOPT_QWEN_IMAGE_2512_NVFP4_CI_sampling_params,
MODELOPT_T2I_CI_sampling_params,
MODELOPT_T2V_CI_sampling_params,
@@ -31,6 +32,7 @@ from sglang.multimodal_gen.test.server.testcase_configs import (
MULTI_IMAGE_TI2I_sampling_params,
MULTI_IMAGE_TI2I_UPLOAD_sampling_params,
PI05_ACTION_CI_sampling_params,
REALTIME_MODEL_sampling_params,
SANA_WM_TI2V_CI_sampling_params,
T2I_sampling_params,
T2V_sampling_params,
@@ -259,6 +261,15 @@ ONE_GPU_CASES: list[DiffusionTestCase] = [
run_consistency_check=True,
run_component_accuracy_check=False,
),
DiffusionTestCase(
"longlive2_t2v",
DiffusionServerArgs(
model_path="Rabinovich/LongLive-2.0-5B-Diffusers",
modality="video",
),
LONGLIVE2_T2V_CI_sampling_params,
run_component_accuracy_check=False,
),
# TeaCache acceleration test for Wan video model
DiffusionTestCase(
"wan2_1_t2v_1.3b_teacache_enabled",
@@ -380,6 +391,17 @@ ONE_GPU_CASES: list[DiffusionTestCase] = [
run_models_api_check=False,
run_t2v_input_reference_check=False,
),
DiffusionTestCase(
"longlive2_i2v",
DiffusionServerArgs(
model_path="Rabinovich/LongLive-2.0-5B-Diffusers",
modality="video",
),
LONGLIVE2_I2V_CI_sampling_params,
run_component_accuracy_check=False,
run_models_api_check=False,
run_t2v_input_reference_check=False,
),
# flaky
# === Helios T2V ===
# DiffusionTestCase(
@@ -439,7 +461,7 @@ ONE_GPU_CASES: list[DiffusionTestCase] = [
],
text_encoder_cpu_offload=True,
),
LINGBOT_WORLD_REALTIME_sampling_params,
REALTIME_MODEL_sampling_params,
run_component_accuracy_check=False,
run_models_api_check=False,
run_t2v_input_reference_check=False,
@@ -2602,6 +2602,38 @@
"expected_median_denoise_ms": 242.79,
"estimated_full_test_time_s": 170.0
},
"longlive2_t2v": {
"stages_ms": {
"InputValidationStage": 0.05,
"LongLive2TextEncodingStage": 328.32,
"LongLive2ImageVAEEncodingStage": 0.0,
"LongLive2LatentPreparationStage": 0.17,
"LongLive2CausalDenoisingStage": 4879.98,
"DecodingStage": 1397.62,
"per_frame_generation": null
},
"denoise_step_ms": {},
"expected_e2e_ms": 6610.71,
"expected_avg_denoise_ms": 650.0,
"expected_median_denoise_ms": 650.0,
"estimated_full_test_time_s": 153.1
},
"longlive2_i2v": {
"stages_ms": {
"InputValidationStage": 23.02,
"LongLive2TextEncodingStage": 327.98,
"LongLive2ImageVAEEncodingStage": 1048.81,
"LongLive2LatentPreparationStage": 0.12,
"LongLive2CausalDenoisingStage": 4975.28,
"DecodingStage": 3051.96,
"per_frame_generation": null
},
"denoise_step_ms": {},
"expected_e2e_ms": 9431.65,
"expected_avg_denoise_ms": 800.0,
"expected_median_denoise_ms": 800.0,
"estimated_full_test_time_s": 149.4
},
"lingbot_world_realtime_plastic_beach": {
"stages_ms": {},
"denoise_step_ms": {},
@@ -315,7 +315,13 @@ class DiffusionTestCase:
)
LINGBOT_WORLD_REALTIME_sampling_params = DiffusionSamplingParams(
_REALTIME_MODEL_COMMON_EXTRAS = {
"seed": 42,
"num_inference_steps": 4,
"guidance_scale": 1.0,
}
REALTIME_MODEL_sampling_params = DiffusionSamplingParams(
prompt=(
"A slow aerial orbit around a pastel floating island hotel in the open "
"ocean, hazy sunlight, turquoise water, toy-like architectural detail, "
@@ -336,9 +342,7 @@ LINGBOT_WORLD_REALTIME_sampling_params = DiffusionSamplingParams(
},
realtime_perf_ignore_initial_chunks=2,
extras={
"seed": 42,
"num_inference_steps": 4,
"guidance_scale": 1.0,
**_REALTIME_MODEL_COMMON_EXTRAS,
"realtime_causal_sink_size": 9,
"realtime_causal_kv_cache_num_frames": 18,
"condition_inputs": {
@@ -607,6 +611,28 @@ SANA_WM_TI2V_CI_sampling_params = DiffusionSamplingParams(
extras={"num_inference_steps": 12, "seed": 0, "guidance_scale": 4.5},
)
LONGLIVE2_T2V_CI_sampling_params = replace(
REALTIME_MODEL_sampling_params,
image_path=None,
num_frames=61,
realtime_num_chunks=None,
realtime_events=[],
realtime_perf_thresholds={},
realtime_perf_ignore_initial_chunks=0,
extras=dict(_REALTIME_MODEL_COMMON_EXTRAS),
)
LONGLIVE2_I2V_CI_sampling_params = replace(
REALTIME_MODEL_sampling_params,
output_size="960x928",
num_frames=61,
realtime_num_chunks=None,
realtime_events=[],
realtime_perf_thresholds={},
realtime_perf_ignore_initial_chunks=0,
extras=dict(_REALTIME_MODEL_COMMON_EXTRAS),
)
TURBOWAN_I2V_sampling_params = DiffusionSamplingParams(
prompt="The man in the picture slowly turns his head, his expression enigmatic and otherworldly. The camera performs a slow, cinematic dolly out, focusing on his face. Moody lighting, neon signs glowing in the background, shallow depth of field.",
image_path="https://is1-ssl.mzstatic.com/image/thumb/Music114/v4/5f/fa/56/5ffa56c2-ea1f-7a17-6bad-192ff9b6476d/825646124206.jpg/600x600bb.jpg",
@@ -34,7 +34,7 @@ if TYPE_CHECKING:
logger = init_logger(__name__)
SGL_TEST_FILES_CI_DATA_REVISION = "9a64abec5a7517a9f2b04ac1b4eab4173adb2d38"
SGL_TEST_FILES_CI_DATA_REVISION = "d51ca9623e0bb27087da243a44c942fdda5aafe5"
if current_platform.is_npu():
SGL_TEST_FILES_CI_DATA_REVISION = "6b62f4b6825c76a25fd2ba28248df68f2b400e65"
@@ -31,7 +31,9 @@ from sglang.multimodal_gen.test.server.realtime_consistency import (
from sglang.multimodal_gen.test.server.test_server_utils import get_generate_fn
from sglang.multimodal_gen.test.server.testcase_configs import (
DiffusionSamplingParams,
LINGBOT_WORLD_REALTIME_sampling_params,
LONGLIVE2_I2V_CI_sampling_params,
LONGLIVE2_T2V_CI_sampling_params,
REALTIME_MODEL_sampling_params,
)
# Request construction
@@ -493,8 +495,8 @@ def test_realtime_sampling_params_route_to_realtime_video_generator():
assert generate_fn.__name__ == "generate_realtime_video"
def test_lingbot_realtime_plastic_beach_params_are_lossless_gt_ready():
params = LINGBOT_WORLD_REALTIME_sampling_params
def test_realtime_model_params_are_lossless_gt_ready():
params = REALTIME_MODEL_sampling_params
assert "floating island hotel" in params.prompt
assert "825646291038" in str(params.image_path)
@@ -521,6 +523,33 @@ def test_lingbot_realtime_plastic_beach_params_are_lossless_gt_ready():
]
def test_longlive2_cases_share_realtime_model_sampling_profile():
for params in (
LONGLIVE2_T2V_CI_sampling_params,
LONGLIVE2_I2V_CI_sampling_params,
):
assert params.prompt == REALTIME_MODEL_sampling_params.prompt
assert params.fps == REALTIME_MODEL_sampling_params.fps
assert params.extras == {
"seed": 42,
"num_inference_steps": 4,
"guidance_scale": 1.0,
}
assert params.realtime_num_chunks is None
assert params.realtime_perf_thresholds == {}
assert LONGLIVE2_T2V_CI_sampling_params.image_path is None
assert (
LONGLIVE2_T2V_CI_sampling_params.output_size
== REALTIME_MODEL_sampling_params.output_size
)
assert (
LONGLIVE2_I2V_CI_sampling_params.image_path
== REALTIME_MODEL_sampling_params.image_path
)
assert LONGLIVE2_I2V_CI_sampling_params.output_size == "960x928"
def test_lingbot_realtime_case_is_registered_by_default():
from sglang.multimodal_gen.test.server.gpu_cases import ONE_GPU_CASES
@@ -0,0 +1,72 @@
import json
from sglang.multimodal_gen.runtime.utils.hf_diffusers_utils import (
_check_index_files_for_missing_shards,
_verify_diffusers_model_complete,
)
def _write_model_index(root):
(root / "model_index.json").write_text(
json.dumps(
{
"_class_name": "LongLive2Pipeline",
"_diffusers_version": "0.34.0",
"scheduler": ["diffusers", "FlowMatchEulerDiscreteScheduler"],
"text_encoder": ["transformers", "T5EncoderModel"],
"tokenizer": ["transformers", "T5TokenizerFast"],
"transformer": ["diffusers", "LongLive2Transformer3DModel"],
"transformer_2": [None, None],
"vae": ["diffusers", "AutoencoderKLWan"],
}
)
)
def test_diffusers_cache_validation_rejects_declared_component_without_weights(
tmp_path,
):
_write_model_index(tmp_path)
for subdir in ("scheduler", "text_encoder", "tokenizer", "transformer", "vae"):
(tmp_path / subdir).mkdir()
(tmp_path / "text_encoder" / "model.safetensors").write_bytes(b"weights")
(tmp_path / "vae" / "diffusion_pytorch_model.bin").write_bytes(b"weights")
assert not _verify_diffusers_model_complete(str(tmp_path))
is_valid, missing_files, checked_subdirs = _check_index_files_for_missing_shards(
str(tmp_path)
)
assert not is_valid
assert "transformer/<weights>" in missing_files
assert "transformer" in checked_subdirs
def test_diffusers_cache_validation_checks_declared_component_shards(tmp_path):
_write_model_index(tmp_path)
for subdir in ("scheduler", "text_encoder", "tokenizer", "transformer", "vae"):
(tmp_path / subdir).mkdir()
(tmp_path / subdir / "model.safetensors").write_bytes(b"weights")
index_path = (
tmp_path / "transformer" / "diffusion_pytorch_model.safetensors.index.json"
)
index_path.write_text(
json.dumps(
{
"weight_map": {
"block.0.weight": "model.safetensors",
"block.1.weight": "missing.safetensors",
}
}
)
)
assert _verify_diffusers_model_complete(str(tmp_path))
is_valid, missing_files, checked_subdirs = _check_index_files_for_missing_shards(
str(tmp_path)
)
assert not is_valid
assert "transformer/missing.safetensors" in missing_files
assert "transformer" in checked_subdirs
@@ -0,0 +1,22 @@
# SPDX-License-Identifier: Apache-2.0
import unittest
from sglang.multimodal_gen.configs.pipeline_configs.longlive2 import LongLive2T2VConfig
class TestLongLive2AdjustNumFrames(unittest.TestCase):
def setUp(self):
self.config = LongLive2T2VConfig()
def test_reuses_wan_temporal_frame_adjustment(self):
self.assertEqual(self.config.adjust_num_frames(62), 61)
def test_keeps_frames_when_latents_match_causal_block(self):
self.assertEqual(self.config.adjust_num_frames(93), 93)
def test_rounds_to_causal_block_aligned_latents(self):
self.assertEqual(self.config.adjust_num_frames(65), 61)
if __name__ == "__main__":
unittest.main()