[diffusion] model: support MOVA (#17704)
Co-authored-by: gaoyang07 <Gary1546308416AL@gmail.com> Co-authored-by: cms42 <c@cms42.top> Co-authored-by: cms42 <44895820+cms42@users.noreply.github.com> Co-authored-by: Ruixiao Li <cgruixiao@outlook.com> Co-authored-by: Li Ruixiao(SII) <80368770+Li-dongyang@users.noreply.github.com>
This commit is contained in:
co-authored by
gaoyang07
cms42
cms42
Ruixiao Li
Li Ruixiao
parent
3fcda00e8c
commit
09a9147f59
@@ -0,0 +1,7 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
|
||||||
|
MOVADualTowerConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = ["MOVADualTowerConfig"]
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""Configuration for MOVA dual tower bridge model."""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.dits.base import DiTArchConfig, DiTConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _is_conditioner_block(name: str, module) -> bool:
|
||||||
|
"""Check if module is a ConditionalCrossAttentionBlock."""
|
||||||
|
return "ConditionalCrossAttentionBlock" in type(module).__name__
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVADualTowerArchConfig(DiTArchConfig):
|
||||||
|
_fsdp_shard_conditions: list = field(
|
||||||
|
default_factory=lambda: [_is_conditioner_block]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Model architecture parameters
|
||||||
|
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
|
||||||
|
eps: float = 1e-6
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.hidden_size = self.visual_hidden_dim
|
||||||
|
self.num_attention_heads = self.visual_hidden_dim // self.head_dim
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVADualTowerConfig(DiTConfig):
|
||||||
|
arch_config: DiTArchConfig = field(default_factory=MOVADualTowerArchConfig)
|
||||||
@@ -1,6 +1,13 @@
|
|||||||
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
||||||
|
|
||||||
from sglang.multimodal_gen.configs.models.dits.hunyuanvideo import HunyuanVideoConfig
|
from sglang.multimodal_gen.configs.models.dits.hunyuanvideo import HunyuanVideoConfig
|
||||||
|
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.wanvideo import WanVideoConfig
|
from sglang.multimodal_gen.configs.models.dits.wanvideo import WanVideoConfig
|
||||||
|
|
||||||
__all__ = ["HunyuanVideoConfig", "WanVideoConfig"]
|
__all__ = [
|
||||||
|
"HunyuanVideoConfig",
|
||||||
|
"MOVAAudioConfig",
|
||||||
|
"MOVAVideoConfig",
|
||||||
|
"WanVideoConfig",
|
||||||
|
]
|
||||||
|
|||||||
@@ -0,0 +1,66 @@
|
|||||||
|
# Copied and adapted from: mossVG/mova/diffusion/models/wan_audio_dit.py
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.dits.base import DiTArchConfig, DiTConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _is_blocks(n: str, m) -> bool:
|
||||||
|
return "blocks" in n and str.isdigit(n.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVAAudioArchConfig(DiTArchConfig):
|
||||||
|
_fsdp_shard_conditions: list = field(default_factory=lambda: [_is_blocks])
|
||||||
|
|
||||||
|
param_names_mapping: dict = field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
r"^blocks\.(\d+)\.ffn\.0\.(.*)$": r"blocks.\1.ffn.fc_in.\2",
|
||||||
|
r"^blocks\.(\d+)\.ffn\.2\.(.*)$": r"blocks.\1.ffn.fc_out.\2",
|
||||||
|
r"^text_embedding\.0\.(.*)$": r"text_embedding.fc_in.\1",
|
||||||
|
r"^text_embedding\.2\.(.*)$": r"text_embedding.fc_out.\1",
|
||||||
|
r"^time_embedding\.0\.(.*)$": r"time_embedding.fc_in.\1",
|
||||||
|
r"^time_embedding\.2\.(.*)$": r"time_embedding.fc_out.\1",
|
||||||
|
r"^img_emb\.proj\.1\.(.*)$": r"img_emb.fc_in.\1",
|
||||||
|
r"^img_emb\.proj\.3\.(.*)$": r"img_emb.fc_out.\1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reverse_param_names_mapping: dict = field(default_factory=dict)
|
||||||
|
lora_param_names_mapping: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
dim: int = 1536
|
||||||
|
in_dim: int = 128
|
||||||
|
ffn_dim: int = 6144
|
||||||
|
out_dim: int = 128
|
||||||
|
text_dim: int = 4096
|
||||||
|
freq_dim: int = 256
|
||||||
|
eps: float = 1e-6
|
||||||
|
patch_size: tuple[int, int, int] = (1, 2, 2)
|
||||||
|
num_heads: int = 12
|
||||||
|
num_layers: int = 30
|
||||||
|
has_image_input: bool = False
|
||||||
|
has_image_pos_emb: bool = False
|
||||||
|
has_ref_conv: bool = False
|
||||||
|
add_control_adapter: bool = False
|
||||||
|
in_dim_control_adapter: int = 24
|
||||||
|
seperated_timestep: bool = False
|
||||||
|
require_vae_embedding: bool = False
|
||||||
|
require_clip_embedding: bool = False
|
||||||
|
fuse_vae_embedding_in_latents: bool = False
|
||||||
|
vae_type: str = "dac"
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.hidden_size = self.dim
|
||||||
|
self.num_attention_heads = self.num_heads
|
||||||
|
self.num_channels_latents = self.out_dim
|
||||||
|
assert (
|
||||||
|
not self.has_image_input
|
||||||
|
), "has_image_input must be False; it's a config from Diffsynth Studio, which means the model uses CLIP for image encoding (we don't)."
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVAAudioConfig(DiTConfig):
|
||||||
|
arch_config: DiTArchConfig = field(default_factory=MOVAAudioArchConfig)
|
||||||
|
prefix: str = "mova_audio"
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Copied and adapted from: mossVG/mova/diffusion/models/wan_video_dit.py
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.dits.base import DiTArchConfig, DiTConfig
|
||||||
|
|
||||||
|
|
||||||
|
def _is_blocks(n: str, m) -> bool:
|
||||||
|
return "blocks" in n and str.isdigit(n.split(".")[-1])
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVAVideoArchConfig(DiTArchConfig):
|
||||||
|
_fsdp_shard_conditions: list = field(default_factory=lambda: [_is_blocks])
|
||||||
|
|
||||||
|
param_names_mapping: dict = field(
|
||||||
|
default_factory=lambda: {
|
||||||
|
r"^blocks\.(\d+)\.ffn\.0\.(.*)$": r"blocks.\1.ffn.fc_in.\2",
|
||||||
|
r"^blocks\.(\d+)\.ffn\.2\.(.*)$": r"blocks.\1.ffn.fc_out.\2",
|
||||||
|
r"^text_embedding\.0\.(.*)$": r"text_embedding.fc_in.\1",
|
||||||
|
r"^text_embedding\.2\.(.*)$": r"text_embedding.fc_out.\1",
|
||||||
|
r"^time_embedding\.0\.(.*)$": r"time_embedding.fc_in.\1",
|
||||||
|
r"^time_embedding\.2\.(.*)$": r"time_embedding.fc_out.\1",
|
||||||
|
r"^img_emb\.proj\.1\.(.*)$": r"img_emb.fc_in.\1",
|
||||||
|
r"^img_emb\.proj\.3\.(.*)$": r"img_emb.fc_out.\1",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
reverse_param_names_mapping: dict = field(default_factory=dict)
|
||||||
|
lora_param_names_mapping: dict = field(default_factory=dict)
|
||||||
|
|
||||||
|
dim: int = 5120
|
||||||
|
in_dim: int = 16
|
||||||
|
ffn_dim: int = 13824
|
||||||
|
out_dim: int = 16
|
||||||
|
text_dim: int = 4096
|
||||||
|
freq_dim: int = 256
|
||||||
|
eps: float = 1e-6
|
||||||
|
patch_size: tuple[int, int, int] = (1, 2, 2)
|
||||||
|
num_heads: int = 40
|
||||||
|
num_layers: int = 40
|
||||||
|
has_image_input: bool = False
|
||||||
|
has_image_pos_emb: bool = False
|
||||||
|
has_ref_conv: bool = False
|
||||||
|
add_control_adapter: bool = False
|
||||||
|
in_dim_control_adapter: int = 24
|
||||||
|
seperated_timestep: bool = False
|
||||||
|
require_vae_embedding: bool = True
|
||||||
|
require_clip_embedding: bool = True
|
||||||
|
fuse_vae_embedding_in_latents: bool = False
|
||||||
|
|
||||||
|
def __post_init__(self):
|
||||||
|
super().__post_init__()
|
||||||
|
self.hidden_size = self.dim
|
||||||
|
self.num_attention_heads = self.num_heads
|
||||||
|
self.num_channels_latents = self.out_dim
|
||||||
|
assert (
|
||||||
|
not self.has_image_input
|
||||||
|
), "has_image_input must be False; it's a config from Diffsynth Studio, which means the model uses CLIP for image encoding (we don't)."
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVAVideoConfig(DiTConfig):
|
||||||
|
arch_config: DiTArchConfig = field(default_factory=MOVAVideoArchConfig)
|
||||||
|
prefix: str = "mova_video"
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.vaes.dac import DacVAEConfig
|
||||||
from sglang.multimodal_gen.configs.models.vaes.hunyuanvae import HunyuanVAEConfig
|
from sglang.multimodal_gen.configs.models.vaes.hunyuanvae import HunyuanVAEConfig
|
||||||
from sglang.multimodal_gen.configs.models.vaes.wanvae import WanVAEConfig
|
from sglang.multimodal_gen.configs.models.vaes.wanvae import WanVAEConfig
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
|
"DacVAEConfig",
|
||||||
"HunyuanVAEConfig",
|
"HunyuanVAEConfig",
|
||||||
"WanVAEConfig",
|
"WanVAEConfig",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# Copied and adapted from: mossVG/mova/diffusion/models/dac_vae.py
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.base import ArchConfig, ModelConfig
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DacVAEArchConfig(ArchConfig):
|
||||||
|
codebook_dim: int = 8
|
||||||
|
codebook_size: int = 1024
|
||||||
|
continuous: bool = True
|
||||||
|
decoder_dim: int = 2048
|
||||||
|
decoder_rates: List[int] = field(default_factory=lambda: [8, 5, 4, 3, 2])
|
||||||
|
encoder_dim: int = 128
|
||||||
|
encoder_rates: List[int] = field(default_factory=lambda: [2, 3, 4, 5, 8])
|
||||||
|
hop_length: int = 3840
|
||||||
|
latent_dim: int = 128
|
||||||
|
n_codebooks: int = 9
|
||||||
|
quantizer_dropout: bool = False
|
||||||
|
sample_rate: int = 48000
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DacVAEConfig(ModelConfig):
|
||||||
|
arch_config: DacVAEArchConfig = field(default_factory=DacVAEArchConfig)
|
||||||
|
load_encoder: bool = True
|
||||||
|
load_decoder: bool = True
|
||||||
@@ -20,6 +20,7 @@ from sglang.multimodal_gen.configs.pipeline_configs.hunyuan import (
|
|||||||
HunyuanConfig,
|
HunyuanConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import LTX2PipelineConfig
|
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import LTX2PipelineConfig
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.mova import MOVAPipelineConfig
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.wan import (
|
from sglang.multimodal_gen.configs.pipeline_configs.wan import (
|
||||||
SelfForcingWanT2V480PConfig,
|
SelfForcingWanT2V480PConfig,
|
||||||
WanI2V480PConfig,
|
WanI2V480PConfig,
|
||||||
@@ -39,6 +40,7 @@ __all__ = [
|
|||||||
"Flux2FinetunedPipelineConfig",
|
"Flux2FinetunedPipelineConfig",
|
||||||
"PipelineConfig",
|
"PipelineConfig",
|
||||||
"SlidingTileAttnConfig",
|
"SlidingTileAttnConfig",
|
||||||
|
"MOVAPipelineConfig",
|
||||||
"WanT2V480PConfig",
|
"WanT2V480PConfig",
|
||||||
"WanI2V480PConfig",
|
"WanI2V480PConfig",
|
||||||
"WanT2V720PConfig",
|
"WanT2V720PConfig",
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""
|
||||||
|
MOVA pipeline configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import torch
|
||||||
|
import torch.nn.functional as F
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.models.dits import MOVAAudioConfig, MOVAVideoConfig
|
||||||
|
from sglang.multimodal_gen.configs.models.encoders import T5Config
|
||||||
|
from sglang.multimodal_gen.configs.models.vaes import DacVAEConfig, WanVAEConfig
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.base import (
|
||||||
|
ModelTaskType,
|
||||||
|
PipelineConfig,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.wan import t5_postprocess_text
|
||||||
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVAPipelineConfig(PipelineConfig):
|
||||||
|
"""Configuration for MOVA (text+image -> video+audio) pipelines."""
|
||||||
|
|
||||||
|
task_type: ModelTaskType = ModelTaskType.T2V
|
||||||
|
|
||||||
|
# Model configs
|
||||||
|
dit_config: MOVAVideoConfig = field(default_factory=MOVAVideoConfig)
|
||||||
|
audio_dit_config: MOVAAudioConfig = field(default_factory=MOVAAudioConfig)
|
||||||
|
|
||||||
|
# Video VAE (Wan) + Audio VAE (DAC)
|
||||||
|
vae_config: WanVAEConfig = field(default_factory=WanVAEConfig)
|
||||||
|
audio_vae_config: DacVAEConfig = field(default_factory=DacVAEConfig)
|
||||||
|
audio_vae_precision: str = "fp32"
|
||||||
|
|
||||||
|
# Text encoder (UMT5 compatible)
|
||||||
|
text_encoder_configs: tuple = field(default_factory=lambda: (T5Config(),))
|
||||||
|
postprocess_text_funcs: tuple = field(
|
||||||
|
default_factory=lambda: (t5_postprocess_text,)
|
||||||
|
)
|
||||||
|
|
||||||
|
# MOVA specific
|
||||||
|
audio_vae_type: str = "dac"
|
||||||
|
boundary_ratio: float | None = 0.9
|
||||||
|
|
||||||
|
# temporal alignment: MOVA expects (num_frames - 1) % 4 == 0
|
||||||
|
time_division_factor: int = 4
|
||||||
|
time_division_remainder: int = 1
|
||||||
|
|
||||||
|
def _center_crop_and_resize(
|
||||||
|
self, image: torch.Tensor | Image.Image, target_height: int, target_width: int
|
||||||
|
) -> torch.Tensor | Image.Image:
|
||||||
|
if not isinstance(image, (Image.Image, torch.Tensor)):
|
||||||
|
raise TypeError(f"Unsupported image type: {type(image)}")
|
||||||
|
if isinstance(image, Image.Image):
|
||||||
|
image = torch.from_numpy(np.array(image))
|
||||||
|
|
||||||
|
if image.ndim == 2:
|
||||||
|
image = image[..., None]
|
||||||
|
|
||||||
|
if not image.dtype.is_floating_point:
|
||||||
|
image = image.to(torch.float32).div(255.0)
|
||||||
|
|
||||||
|
if image.ndim == 3:
|
||||||
|
if image.shape[0] in (1, 3, 4) and image.shape[-1] not in (1, 3, 4):
|
||||||
|
image = image.unsqueeze(0)
|
||||||
|
else:
|
||||||
|
image = image.permute(2, 0, 1).unsqueeze(0)
|
||||||
|
elif image.ndim == 4:
|
||||||
|
if image.shape[1] not in (1, 3, 4) and image.shape[-1] in (1, 3, 4):
|
||||||
|
image = image.permute(0, 3, 1, 2)
|
||||||
|
|
||||||
|
image_height, image_width = image.shape[-2], image.shape[-1]
|
||||||
|
if image_height == target_height and image_width == target_width:
|
||||||
|
return image
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Center cropping and resizing image to %dx%d", target_width, target_height
|
||||||
|
)
|
||||||
|
|
||||||
|
if image_height * target_width < image_width * target_height:
|
||||||
|
cropped_width = (image_height * target_width) // target_height
|
||||||
|
left = (image_width - cropped_width) // 2
|
||||||
|
image = image[..., :, left : left + cropped_width]
|
||||||
|
else:
|
||||||
|
cropped_height = (image_width * target_height) // target_width
|
||||||
|
top = (image_height - cropped_height) // 2
|
||||||
|
image = image[..., top : top + cropped_height, :]
|
||||||
|
|
||||||
|
image = F.interpolate(
|
||||||
|
image,
|
||||||
|
size=(target_height, target_width),
|
||||||
|
mode="bilinear",
|
||||||
|
align_corners=False,
|
||||||
|
antialias=True,
|
||||||
|
)
|
||||||
|
return image
|
||||||
|
|
||||||
|
def adjust_num_frames(self, num_frames: int) -> int:
|
||||||
|
if num_frames is None:
|
||||||
|
return num_frames
|
||||||
|
if num_frames % self.time_division_factor != self.time_division_remainder:
|
||||||
|
adjusted = (
|
||||||
|
(num_frames + self.time_division_factor - 1)
|
||||||
|
// self.time_division_factor
|
||||||
|
* self.time_division_factor
|
||||||
|
+ self.time_division_remainder
|
||||||
|
)
|
||||||
|
logger.warning(
|
||||||
|
"`num_frames` (%s) is not compatible with MOVA temporal constraints. "
|
||||||
|
"Rounding to %s.",
|
||||||
|
num_frames,
|
||||||
|
adjusted,
|
||||||
|
)
|
||||||
|
return adjusted
|
||||||
|
return num_frames
|
||||||
|
|
||||||
|
def preprocess_condition_image(
|
||||||
|
self, image, target_width, target_height, _vae_image_processor
|
||||||
|
):
|
||||||
|
image = self._center_crop_and_resize(image, target_height, target_width)
|
||||||
|
return image, (target_width, target_height)
|
||||||
|
|
||||||
|
def prepare_latent_shape(self, batch, batch_size, num_frames):
|
||||||
|
spatial = self.vae_config.arch_config.spatial_compression_ratio
|
||||||
|
length = (num_frames - 1) // self.time_division_factor + 1
|
||||||
|
shape = (
|
||||||
|
batch_size,
|
||||||
|
self.dit_config.arch_config.out_dim,
|
||||||
|
length,
|
||||||
|
batch.height // spatial,
|
||||||
|
batch.width // spatial,
|
||||||
|
)
|
||||||
|
return shape
|
||||||
|
|
||||||
|
def prepare_audio_latent_shape(self, batch_size, num_samples, audio_vae):
|
||||||
|
latent_T = (num_samples + audio_vae.hop_length - 1) // audio_vae.hop_length
|
||||||
|
return (batch_size, audio_vae.latent_dim, latent_T)
|
||||||
|
|
||||||
|
def normalize_video_latents(self, latents: torch.Tensor, video_vae) -> torch.Tensor:
|
||||||
|
latents_mean = getattr(video_vae.config, "latents_mean", None)
|
||||||
|
latents_std = getattr(video_vae.config, "latents_std", None)
|
||||||
|
if latents_mean is None or latents_std is None:
|
||||||
|
return latents
|
||||||
|
mean = torch.tensor(
|
||||||
|
latents_mean, device=latents.device, dtype=latents.dtype
|
||||||
|
).view(1, video_vae.config.z_dim, 1, 1, 1)
|
||||||
|
inv_std = (
|
||||||
|
1.0 / torch.tensor(latents_std, device=latents.device, dtype=latents.dtype)
|
||||||
|
).view(1, video_vae.config.z_dim, 1, 1, 1)
|
||||||
|
return (latents - mean) * inv_std
|
||||||
|
|
||||||
|
def denormalize_video_latents(
|
||||||
|
self, latents: torch.Tensor, video_vae
|
||||||
|
) -> torch.Tensor:
|
||||||
|
latents_mean = getattr(video_vae.config, "latents_mean", None)
|
||||||
|
latents_std = getattr(video_vae.config, "latents_std", None)
|
||||||
|
if latents_mean is None or latents_std is None:
|
||||||
|
return latents
|
||||||
|
mean = torch.tensor(
|
||||||
|
latents_mean, device=latents.device, dtype=latents.dtype
|
||||||
|
).view(1, video_vae.config.z_dim, 1, 1, 1)
|
||||||
|
std = torch.tensor(
|
||||||
|
latents_std, device=latents.device, dtype=latents.dtype
|
||||||
|
).view(1, video_vae.config.z_dim, 1, 1, 1)
|
||||||
|
return latents * std + mean
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVA360PConfig(MOVAPipelineConfig):
|
||||||
|
"""Configuration for MOVA 360P (text+image -> video+audio) pipelines."""
|
||||||
|
|
||||||
|
max_area: int = 352 * 640
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVA720PConfig(MOVAPipelineConfig):
|
||||||
|
"""Configuration for MOVA 720P (text+image -> video+audio) pipelines."""
|
||||||
|
|
||||||
|
max_area: int = 720 * 1280
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVASamplingParams(SamplingParams):
|
||||||
|
# Video parameters (MOVA defaults)
|
||||||
|
height: int = 352
|
||||||
|
width: int = 640
|
||||||
|
num_frames: int = 193
|
||||||
|
fps: int = 24
|
||||||
|
|
||||||
|
# Denoising stage
|
||||||
|
guidance_scale: float = 5.0
|
||||||
|
num_inference_steps: int = 50
|
||||||
|
sigma_shift: float = 5.0
|
||||||
|
visual_shift: float = 5.0
|
||||||
|
audio_shift: float = 5.0
|
||||||
|
|
||||||
|
negative_prompt: str = (
|
||||||
|
"色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,"
|
||||||
|
"整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,"
|
||||||
|
"画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,"
|
||||||
|
"静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVA_360P_SamplingParams(MOVASamplingParams):
|
||||||
|
# Video parameters (MOVA 360P)
|
||||||
|
height: int = 352
|
||||||
|
width: int = 640
|
||||||
|
|
||||||
|
# MOVA 360P supported resolutions
|
||||||
|
supported_resolutions: list[tuple[int, int]] = field(
|
||||||
|
default_factory=lambda: [
|
||||||
|
(352, 640),
|
||||||
|
(640, 352),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class MOVA_720P_SamplingParams(MOVASamplingParams):
|
||||||
|
# Video parameters (MOVA 720P)
|
||||||
|
height: int = 720
|
||||||
|
width: int = 1280
|
||||||
|
|
||||||
|
# MOVA 720P supported resolutions
|
||||||
|
supported_resolutions: list[tuple[int, int]] = field(
|
||||||
|
default_factory=lambda: [
|
||||||
|
(720, 1280),
|
||||||
|
(1280, 720),
|
||||||
|
]
|
||||||
|
)
|
||||||
@@ -46,6 +46,10 @@ from sglang.multimodal_gen.configs.pipeline_configs.glm_image import (
|
|||||||
GlmImagePipelineConfig,
|
GlmImagePipelineConfig,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import LTX2PipelineConfig
|
from sglang.multimodal_gen.configs.pipeline_configs.ltx_2 import LTX2PipelineConfig
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.mova import (
|
||||||
|
MOVA360PConfig,
|
||||||
|
MOVA720PConfig,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
|
from sglang.multimodal_gen.configs.pipeline_configs.qwen_image import (
|
||||||
QwenImageEditPipelineConfig,
|
QwenImageEditPipelineConfig,
|
||||||
QwenImageEditPlus_2511_PipelineConfig,
|
QwenImageEditPlus_2511_PipelineConfig,
|
||||||
@@ -72,6 +76,10 @@ from sglang.multimodal_gen.configs.sample.hunyuan import (
|
|||||||
HunyuanSamplingParams,
|
HunyuanSamplingParams,
|
||||||
)
|
)
|
||||||
from sglang.multimodal_gen.configs.sample.ltx_2 import LTX2SamplingParams
|
from sglang.multimodal_gen.configs.sample.ltx_2 import LTX2SamplingParams
|
||||||
|
from sglang.multimodal_gen.configs.sample.mova import (
|
||||||
|
MOVA_360P_SamplingParams,
|
||||||
|
MOVA_720P_SamplingParams,
|
||||||
|
)
|
||||||
from sglang.multimodal_gen.configs.sample.qwenimage import (
|
from sglang.multimodal_gen.configs.sample.qwenimage import (
|
||||||
QwenImage2512SamplingParams,
|
QwenImage2512SamplingParams,
|
||||||
QwenImageEditPlusSamplingParams,
|
QwenImageEditPlusSamplingParams,
|
||||||
@@ -543,6 +551,21 @@ def _register_configs():
|
|||||||
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
"FastVideo/FastWan2.1-T2V-1.3B-Diffusers",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
# MOVA
|
||||||
|
register_configs(
|
||||||
|
sampling_param_cls=MOVA_360P_SamplingParams,
|
||||||
|
pipeline_config_cls=MOVA360PConfig,
|
||||||
|
model_detectors=[
|
||||||
|
lambda hf_id: "mova" in hf_id.lower() and "360p" in hf_id.lower()
|
||||||
|
],
|
||||||
|
)
|
||||||
|
register_configs(
|
||||||
|
sampling_param_cls=MOVA_720P_SamplingParams,
|
||||||
|
pipeline_config_cls=MOVA720PConfig,
|
||||||
|
model_detectors=[
|
||||||
|
lambda hf_id: "mova" in hf_id.lower() and "720p" in hf_id.lower()
|
||||||
|
],
|
||||||
|
)
|
||||||
# FLUX
|
# FLUX
|
||||||
register_configs(
|
register_configs(
|
||||||
sampling_param_cls=FluxSamplingParams,
|
sampling_param_cls=FluxSamplingParams,
|
||||||
|
|||||||
@@ -268,18 +268,31 @@ class ComponentLoader(ABC):
|
|||||||
"processor": (AutoProcessorLoader, "transformers"),
|
"processor": (AutoProcessorLoader, "transformers"),
|
||||||
"vision_language_encoder": (VisionLanguageEncoderLoader, "transformers"),
|
"vision_language_encoder": (VisionLanguageEncoderLoader, "transformers"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# Loaders for audio/video specific components that might vary
|
# Loaders for audio/video specific components that might vary
|
||||||
av_module_loaders = {
|
av_module_loaders = {
|
||||||
|
"audio_dit": (TransformerLoader, "diffusers"),
|
||||||
"audio_vae": (VAELoader, "diffusers"),
|
"audio_vae": (VAELoader, "diffusers"),
|
||||||
"vocoder": (VocoderLoader, "diffusers"),
|
|
||||||
"connectors": (AdapterLoader, "diffusers"),
|
"connectors": (AdapterLoader, "diffusers"),
|
||||||
|
"dual_tower_bridge": (BridgeLoader, "diffusers"),
|
||||||
|
"video_dit": (TransformerLoader, "diffusers"),
|
||||||
|
"video_vae": (VAELoader, "diffusers"),
|
||||||
|
"vocoder": (VocoderLoader, "diffusers"),
|
||||||
}
|
}
|
||||||
|
|
||||||
# NOTE(FlamingoPg): special for LTX-2 models
|
# NOTE(FlamingoPg): special for LTX-2 models
|
||||||
if module_type == "vocoder" or module_type == "connectors":
|
if module_type == "vocoder" or module_type == "connectors":
|
||||||
transformers_or_diffusers = "diffusers"
|
transformers_or_diffusers = "diffusers"
|
||||||
|
|
||||||
|
# NOTE(CloudRipple): special for MOVA models
|
||||||
|
# TODO(CloudRipple): remove most of these special cases after unifying the loading logic
|
||||||
|
if module_type in [
|
||||||
|
"audio_vae",
|
||||||
|
"audio_dit",
|
||||||
|
"dual_tower_bridge",
|
||||||
|
"video_dit",
|
||||||
|
]:
|
||||||
|
transformers_or_diffusers = "diffusers"
|
||||||
|
|
||||||
if module_type in module_loaders:
|
if module_type in module_loaders:
|
||||||
loader_cls, expected_library = module_loaders[module_type]
|
loader_cls, expected_library = module_loaders[module_type]
|
||||||
# Assert that the library matches what's expected for this module type
|
# Assert that the library matches what's expected for this module type
|
||||||
@@ -616,7 +629,7 @@ class TokenizerLoader(ComponentLoader):
|
|||||||
|
|
||||||
|
|
||||||
class VAELoader(ComponentLoader):
|
class VAELoader(ComponentLoader):
|
||||||
"""Loader for VAE."""
|
"""Shared loader for (video/audio) VAE modules."""
|
||||||
|
|
||||||
def should_offload(
|
def should_offload(
|
||||||
self, server_args: ServerArgs, model_config: ModelConfig | None = None
|
self, server_args: ServerArgs, model_config: ModelConfig | None = None
|
||||||
@@ -636,15 +649,18 @@ class VAELoader(ComponentLoader):
|
|||||||
server_args.model_paths[module_name] = component_model_path
|
server_args.model_paths[module_name] = component_model_path
|
||||||
|
|
||||||
logger.debug("HF model config: %s", config)
|
logger.debug("HF model config: %s", config)
|
||||||
if module_name == "audio_vae":
|
if module_name in ("vae", "video_vae"):
|
||||||
vae_config = server_args.pipeline_config.audio_vae_config
|
pipeline_vae_config_attr = "vae_config"
|
||||||
vae_precision = server_args.pipeline_config.audio_vae_precision
|
pipeline_vae_precision = "vae_precision"
|
||||||
|
elif module_name in ("audio_vae",):
|
||||||
|
pipeline_vae_config_attr = "audio_vae_config"
|
||||||
|
pipeline_vae_precision = "audio_vae_precision"
|
||||||
else:
|
else:
|
||||||
vae_config = server_args.pipeline_config.vae_config
|
raise ValueError(f"Unsupported module name for VAE loader: {module_name}")
|
||||||
vae_precision = server_args.pipeline_config.vae_precision
|
vae_config = getattr(server_args.pipeline_config, pipeline_vae_config_attr)
|
||||||
|
vae_precision = getattr(server_args.pipeline_config, pipeline_vae_precision)
|
||||||
vae_config.update_model_arch(config)
|
vae_config.update_model_arch(config)
|
||||||
|
if hasattr(vae_config, "post_init"):
|
||||||
# NOTE: some post init logics are only available after updated with config
|
# NOTE: some post init logics are only available after updated with config
|
||||||
vae_config.post_init()
|
vae_config.post_init()
|
||||||
|
|
||||||
@@ -685,6 +701,16 @@ class VAELoader(ComponentLoader):
|
|||||||
), f"Found {len(safetensors_list)} safetensors files in {component_model_path}"
|
), f"Found {len(safetensors_list)} safetensors files in {component_model_path}"
|
||||||
loaded = safetensors_load_file(safetensors_list[0])
|
loaded = safetensors_load_file(safetensors_list[0])
|
||||||
vae.load_state_dict(loaded, strict=False)
|
vae.load_state_dict(loaded, strict=False)
|
||||||
|
|
||||||
|
state_keys = set(vae.state_dict().keys())
|
||||||
|
loaded_keys = set(loaded.keys())
|
||||||
|
missing_keys = sorted(state_keys - loaded_keys)
|
||||||
|
unexpected_keys = sorted(loaded_keys - state_keys)
|
||||||
|
if missing_keys:
|
||||||
|
logger.warning("VAE missing keys: %s", missing_keys)
|
||||||
|
if unexpected_keys:
|
||||||
|
logger.warning("VAE unexpected keys: %s", unexpected_keys)
|
||||||
|
|
||||||
return vae.eval()
|
return vae.eval()
|
||||||
|
|
||||||
|
|
||||||
@@ -753,11 +779,96 @@ class VocoderLoader(ComponentLoader):
|
|||||||
return vocoder.eval()
|
return vocoder.eval()
|
||||||
|
|
||||||
|
|
||||||
class TransformerLoader(ComponentLoader):
|
class BridgeLoader(ComponentLoader):
|
||||||
"""Loader for transformer."""
|
"""Loader for MOVA dual tower bridge with FSDP support."""
|
||||||
|
|
||||||
|
pipeline_bridge_config_attr: str = "bridge_config"
|
||||||
|
|
||||||
def load_customized(
|
def load_customized(
|
||||||
self, component_model_path: str, server_args: ServerArgs, *args
|
self, component_model_path: str, server_args: ServerArgs, module_name: str
|
||||||
|
):
|
||||||
|
config = get_diffusers_component_config(model_path=component_model_path)
|
||||||
|
hf_config = deepcopy(config)
|
||||||
|
class_name = config.pop("_class_name", None)
|
||||||
|
if class_name is None:
|
||||||
|
raise ValueError(
|
||||||
|
"Model config does not contain a _class_name attribute. "
|
||||||
|
"Only diffusers format is supported."
|
||||||
|
)
|
||||||
|
server_args.model_paths[module_name] = component_model_path
|
||||||
|
|
||||||
|
# Try to get bridge config from pipeline config, fallback to creating one
|
||||||
|
bridge_config = getattr(
|
||||||
|
server_args.pipeline_config, self.pipeline_bridge_config_attr, None
|
||||||
|
)
|
||||||
|
if bridge_config is not None:
|
||||||
|
bridge_config.update_model_arch(config)
|
||||||
|
else:
|
||||||
|
# Create a minimal config from hf_config
|
||||||
|
from sglang.multimodal_gen.configs.models.bridges.mova_dual_tower import (
|
||||||
|
MOVADualTowerConfig,
|
||||||
|
)
|
||||||
|
|
||||||
|
bridge_config = MOVADualTowerConfig()
|
||||||
|
bridge_config.update_model_arch(config)
|
||||||
|
|
||||||
|
model_cls, _ = ModelRegistry.resolve_model_cls(class_name)
|
||||||
|
|
||||||
|
# Find all safetensors files
|
||||||
|
safetensors_list = _list_safetensors_files(component_model_path)
|
||||||
|
if not safetensors_list:
|
||||||
|
raise ValueError(f"No safetensors files found in {component_model_path}")
|
||||||
|
|
||||||
|
default_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.dit_precision]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Loading %s from %s safetensors files, default_dtype: %s",
|
||||||
|
class_name,
|
||||||
|
len(safetensors_list),
|
||||||
|
default_dtype,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check if FSDP loading is available
|
||||||
|
if (
|
||||||
|
server_args.hsdp_shard_dim is not None
|
||||||
|
and hasattr(model_cls, "_fsdp_shard_conditions")
|
||||||
|
and model_cls._fsdp_shard_conditions
|
||||||
|
):
|
||||||
|
# Load with FSDP support
|
||||||
|
model = maybe_load_fsdp_model(
|
||||||
|
model_cls=model_cls,
|
||||||
|
init_params={"config": bridge_config, "hf_config": hf_config},
|
||||||
|
weight_dir_list=safetensors_list,
|
||||||
|
device=get_local_torch_device(),
|
||||||
|
hsdp_replicate_dim=server_args.hsdp_replicate_dim,
|
||||||
|
hsdp_shard_dim=server_args.hsdp_shard_dim,
|
||||||
|
cpu_offload=server_args.dit_cpu_offload,
|
||||||
|
pin_cpu_memory=server_args.pin_cpu_memory,
|
||||||
|
fsdp_inference=server_args.use_fsdp_inference,
|
||||||
|
default_dtype=default_dtype,
|
||||||
|
param_dtype=torch.bfloat16,
|
||||||
|
reduce_dtype=torch.float32,
|
||||||
|
output_dtype=None,
|
||||||
|
strict=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
# Fallback to simple loading (for non-FSDP or legacy models)
|
||||||
|
model = model_cls.from_pretrained(
|
||||||
|
component_model_path, torch_dtype=default_dtype
|
||||||
|
)
|
||||||
|
model = model.to(device=get_local_torch_device(), dtype=default_dtype)
|
||||||
|
|
||||||
|
total_params = sum(p.numel() for p in model.parameters())
|
||||||
|
logger.info("Loaded bridge model with %.2fM parameters", total_params / 1e6)
|
||||||
|
|
||||||
|
return model.eval()
|
||||||
|
|
||||||
|
|
||||||
|
class TransformerLoader(ComponentLoader):
|
||||||
|
"""Shared loader for (video/audio) DiT transformers."""
|
||||||
|
|
||||||
|
def load_customized(
|
||||||
|
self, component_model_path: str, server_args: ServerArgs, module_name: str
|
||||||
):
|
):
|
||||||
"""Load the transformer based on the model path, and inference args."""
|
"""Load the transformer based on the model path, and inference args."""
|
||||||
config = get_diffusers_component_config(model_path=component_model_path)
|
config = get_diffusers_component_config(model_path=component_model_path)
|
||||||
@@ -769,10 +880,17 @@ class TransformerLoader(ComponentLoader):
|
|||||||
"Only diffusers format is supported."
|
"Only diffusers format is supported."
|
||||||
)
|
)
|
||||||
|
|
||||||
server_args.model_paths["transformer"] = component_model_path
|
module_name = _normalize_module_type(module_name)
|
||||||
|
server_args.model_paths[module_name] = component_model_path
|
||||||
|
|
||||||
|
if module_name in ("transformer", "video_dit"):
|
||||||
|
pipeline_dit_config_attr = "dit_config"
|
||||||
|
elif module_name in ("audio_dit",):
|
||||||
|
pipeline_dit_config_attr = "audio_dit_config"
|
||||||
|
else:
|
||||||
|
raise ValueError(f"Invalid module name: {module_name}")
|
||||||
# Config from Diffusers supersedes sgl_diffusion's model config
|
# Config from Diffusers supersedes sgl_diffusion's model config
|
||||||
dit_config = server_args.pipeline_config.dit_config
|
dit_config = getattr(server_args.pipeline_config, pipeline_dit_config_attr)
|
||||||
dit_config.update_model_arch(config)
|
dit_config.update_model_arch(config)
|
||||||
|
|
||||||
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
|
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
|
||||||
|
|||||||
@@ -98,6 +98,9 @@ class GPUWorker:
|
|||||||
[
|
[
|
||||||
self.pipeline.get_module("transformer"),
|
self.pipeline.get_module("transformer"),
|
||||||
self.pipeline.get_module("transformer_2"),
|
self.pipeline.get_module("transformer_2"),
|
||||||
|
self.pipeline.get_module("video_dit"),
|
||||||
|
self.pipeline.get_module("video_dit_2"),
|
||||||
|
self.pipeline.get_module("audio_dit"),
|
||||||
],
|
],
|
||||||
):
|
):
|
||||||
if isinstance(dit, OffloadableDiTMixin):
|
if isinstance(dit, OffloadableDiTMixin):
|
||||||
@@ -164,6 +167,8 @@ class GPUWorker:
|
|||||||
if isinstance(result, Req):
|
if isinstance(result, Req):
|
||||||
output_batch = OutputBatch(
|
output_batch = OutputBatch(
|
||||||
output=result.output,
|
output=result.output,
|
||||||
|
audio=getattr(result, "audio", None),
|
||||||
|
audio_sample_rate=getattr(result, "audio_sample_rate", None),
|
||||||
timings=result.timings,
|
timings=result.timings,
|
||||||
trajectory_timesteps=getattr(result, "trajectory_timesteps", None),
|
trajectory_timesteps=getattr(result, "trajectory_timesteps", None),
|
||||||
trajectory_latents=getattr(result, "trajectory_latents", None),
|
trajectory_latents=getattr(result, "trajectory_latents", None),
|
||||||
|
|||||||
@@ -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,671 @@
|
|||||||
|
# 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.models.dits.base import CachableDiT
|
||||||
|
from sglang.multimodal_gen.runtime.utils.layerwise_offload import OffloadableDiTMixin
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
|
||||||
|
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,
|
||||||
|
OffloadableDiTMixin,
|
||||||
|
):
|
||||||
|
"""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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
_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_v = f_v * h * w
|
||||||
|
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,306 @@
|
|||||||
|
# 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.models.dits.base import CachableDiT
|
||||||
|
from sglang.multimodal_gen.runtime.utils.layerwise_offload import OffloadableDiTMixin
|
||||||
|
|
||||||
|
# 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, OffloadableDiTMixin):
|
||||||
|
_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]) -> 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
|
||||||
|
add_control_adapter = config.add_control_adapter
|
||||||
|
in_dim_control_adapter = config.in_dim_control_adapter
|
||||||
|
seperated_timestep = config.seperated_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.seperated_timestep = seperated_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"
|
||||||
|
)
|
||||||
|
self.time_embedding = MLP(freq_dim, dim, output_dim=dim, act_type="silu")
|
||||||
|
# Preserve state_dict keys (time_projection.1.weight/bias).
|
||||||
|
self.time_projection = nn.Sequential(nn.SiLU(), ReplicatedLinear(dim, dim * 6))
|
||||||
|
self.blocks = nn.ModuleList(
|
||||||
|
[DiTBlock(dim, num_heads, ffn_dim, eps) 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__()
|
||||||
|
if add_control_adapter:
|
||||||
|
from .wan_video_camera_controller import SimpleAdapter
|
||||||
|
|
||||||
|
self.control_adapter = SimpleAdapter(
|
||||||
|
in_dim_control_adapter,
|
||||||
|
dim,
|
||||||
|
kernel_size=patch_size[1:],
|
||||||
|
stride=patch_size[1:],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.control_adapter = None
|
||||||
|
|
||||||
|
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)
|
||||||
|
if (
|
||||||
|
self.control_adapter is not None
|
||||||
|
and control_camera_latents_input is not None
|
||||||
|
):
|
||||||
|
y_camera = self.control_adapter(control_camera_latents_input)
|
||||||
|
if isinstance(x, list):
|
||||||
|
x = [u + v for u, v in zip(x, y_camera)]
|
||||||
|
x = x[0].unsqueeze(0)
|
||||||
|
else:
|
||||||
|
if isinstance(y_camera, list):
|
||||||
|
x = x + y_camera[0]
|
||||||
|
else:
|
||||||
|
x = x + y_camera
|
||||||
|
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,
|
||||||
|
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor] | None = None,
|
||||||
|
guidance=None,
|
||||||
|
use_gradient_checkpointing: bool = False,
|
||||||
|
use_gradient_checkpointing_offload: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
) -> 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_custom_forward(module):
|
||||||
|
def custom_forward(*inputs):
|
||||||
|
return module(*inputs)
|
||||||
|
|
||||||
|
return custom_forward
|
||||||
|
|
||||||
|
for block in self.blocks:
|
||||||
|
if self.training and use_gradient_checkpointing:
|
||||||
|
if use_gradient_checkpointing_offload:
|
||||||
|
with torch.autograd.graph.save_on_cpu():
|
||||||
|
x = torch.utils.checkpoint.checkpoint(
|
||||||
|
create_custom_forward(block),
|
||||||
|
x,
|
||||||
|
context,
|
||||||
|
t_mod,
|
||||||
|
freqs,
|
||||||
|
use_reentrant=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
x = torch.utils.checkpoint.checkpoint(
|
||||||
|
create_custom_forward(block),
|
||||||
|
x,
|
||||||
|
context,
|
||||||
|
t_mod,
|
||||||
|
freqs,
|
||||||
|
use_reentrant=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
x = block(x, context, t_mod, freqs)
|
||||||
|
|
||||||
|
x = self.head(x, t)
|
||||||
|
x = self.unpatchify(x, (f,))
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
EntryClass = WanAudioModel
|
||||||
@@ -0,0 +1,570 @@
|
|||||||
|
# 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 (
|
||||||
|
RMSNorm,
|
||||||
|
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.models.dits.base import CachableDiT
|
||||||
|
from sglang.multimodal_gen.runtime.utils.layerwise_offload import OffloadableDiTMixin
|
||||||
|
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):
|
||||||
|
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)
|
||||||
|
self.k = ColumnParallelLinear(dim, dim, bias=True, gather_output=False)
|
||||||
|
self.v = ColumnParallelLinear(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(
|
||||||
|
# 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):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
|
||||||
|
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
|
||||||
|
out = self.attn(q, k, v)
|
||||||
|
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):
|
||||||
|
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)
|
||||||
|
self.k = ColumnParallelLinear(dim, dim, bias=True, gather_output=False)
|
||||||
|
self.v = ColumnParallelLinear(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)
|
||||||
|
|
||||||
|
# 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 GateModule(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
def forward(self, x, gate, residual):
|
||||||
|
return x + gate * residual
|
||||||
|
|
||||||
|
|
||||||
|
class DiTBlock(nn.Module):
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
dim: int,
|
||||||
|
num_heads: int,
|
||||||
|
ffn_dim: int,
|
||||||
|
eps: float = 1e-6,
|
||||||
|
):
|
||||||
|
super().__init__()
|
||||||
|
self.dim = dim
|
||||||
|
self.num_heads = num_heads
|
||||||
|
self.ffn_dim = ffn_dim
|
||||||
|
|
||||||
|
self.self_attn = SelfAttention(dim, num_heads, eps)
|
||||||
|
self.cross_attn = CrossAttention(dim, num_heads, eps)
|
||||||
|
self.norm1 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
||||||
|
self.norm2 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
||||||
|
self.norm3 = nn.LayerNorm(dim, eps=eps)
|
||||||
|
self.ffn = MLP(dim, ffn_dim, output_dim=dim, act_type="gelu_pytorch_tanh")
|
||||||
|
self.modulation = nn.Parameter(torch.randn(1, 6, dim) / dim**0.5)
|
||||||
|
self.gate = GateModule()
|
||||||
|
|
||||||
|
def forward(self, x, context, t_mod, freqs):
|
||||||
|
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),
|
||||||
|
)
|
||||||
|
input_x = modulate(self.norm1(x), shift_msa, scale_msa)
|
||||||
|
x = self.gate(x, gate_msa, self.self_attn(input_x, freqs))
|
||||||
|
x = x + self.cross_attn(self.norm3(x), context)
|
||||||
|
input_x = modulate(self.norm2(x), shift_mlp, scale_mlp)
|
||||||
|
x = self.gate(x, gate_mlp, self.ffn(input_x))
|
||||||
|
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 = nn.LayerNorm(dim, eps=eps, elementwise_affine=False)
|
||||||
|
# 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) * (1 + scale.squeeze(2)) + shift.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) * (1 + scale) + shift)
|
||||||
|
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, OffloadableDiTMixin):
|
||||||
|
_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]) -> 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
|
||||||
|
add_control_adapter = config.add_control_adapter
|
||||||
|
in_dim_control_adapter = config.in_dim_control_adapter
|
||||||
|
seperated_timestep = config.seperated_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.seperated_timestep = seperated_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"
|
||||||
|
)
|
||||||
|
self.time_embedding = MLP(freq_dim, dim, output_dim=dim, act_type="silu")
|
||||||
|
# Preserve state_dict keys (time_projection.1.weight/bias).
|
||||||
|
self.time_projection = nn.Sequential(nn.SiLU(), ReplicatedLinear(dim, dim * 6))
|
||||||
|
self.blocks = nn.ModuleList(
|
||||||
|
[DiTBlock(dim, num_heads, ffn_dim, eps) 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__()
|
||||||
|
if add_control_adapter:
|
||||||
|
from .wan_video_camera_controller import SimpleAdapter
|
||||||
|
|
||||||
|
self.control_adapter = SimpleAdapter(
|
||||||
|
in_dim_control_adapter,
|
||||||
|
dim,
|
||||||
|
kernel_size=patch_size[1:],
|
||||||
|
stride=patch_size[1:],
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.control_adapter = None
|
||||||
|
|
||||||
|
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
|
||||||
|
):
|
||||||
|
# NOTE(dhyu): avoid slow_conv
|
||||||
|
x = x.contiguous(memory_format=torch.channels_last_3d)
|
||||||
|
x = self.patch_embedding(x)
|
||||||
|
if (
|
||||||
|
self.control_adapter is not None
|
||||||
|
and control_camera_latents_input is not None
|
||||||
|
):
|
||||||
|
y_camera = self.control_adapter(control_camera_latents_input)
|
||||||
|
if isinstance(x, list):
|
||||||
|
x = [u + v for u, v in zip(x, y_camera)]
|
||||||
|
x = x[0].unsqueeze(0)
|
||||||
|
else:
|
||||||
|
# Some adapters may return a list even when x is a Tensor.
|
||||||
|
if isinstance(y_camera, list):
|
||||||
|
x = x + y_camera[0]
|
||||||
|
else:
|
||||||
|
x = x + y_camera
|
||||||
|
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,
|
||||||
|
encoder_hidden_states_image: torch.Tensor | list[torch.Tensor],
|
||||||
|
y: torch.Tensor,
|
||||||
|
guidance=None,
|
||||||
|
use_gradient_checkpointing: bool = False,
|
||||||
|
use_gradient_checkpointing_offload: bool = False,
|
||||||
|
**kwargs,
|
||||||
|
) -> 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)
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_custom_forward(module):
|
||||||
|
def custom_forward(*inputs):
|
||||||
|
return module(*inputs)
|
||||||
|
|
||||||
|
return custom_forward
|
||||||
|
|
||||||
|
for block in self.blocks:
|
||||||
|
if self.training and use_gradient_checkpointing:
|
||||||
|
if use_gradient_checkpointing_offload:
|
||||||
|
with torch.autograd.graph.save_on_cpu():
|
||||||
|
x = torch.utils.checkpoint.checkpoint(
|
||||||
|
create_custom_forward(block),
|
||||||
|
x,
|
||||||
|
context,
|
||||||
|
t_mod,
|
||||||
|
freqs,
|
||||||
|
use_reentrant=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
x = torch.utils.checkpoint.checkpoint(
|
||||||
|
create_custom_forward(block),
|
||||||
|
x,
|
||||||
|
context,
|
||||||
|
t_mod,
|
||||||
|
freqs,
|
||||||
|
use_reentrant=False,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
x = block(x, context, t_mod, freqs)
|
||||||
|
|
||||||
|
x = self.head(x, t)
|
||||||
|
x = self.unpatchify(x, (f, h, w))
|
||||||
|
return x
|
||||||
|
|
||||||
|
|
||||||
|
EntryClass = WanModel
|
||||||
@@ -0,0 +1,911 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""
|
||||||
|
MOVA-specific pipeline stages.
|
||||||
|
|
||||||
|
Sequence Parallelism (SP) Support:
|
||||||
|
- Video latents are sharded along the sequence dimension (T*H*W) after patchify
|
||||||
|
- Audio latents are sharded along the sequence dimension (L) after patchify
|
||||||
|
- USPAttention handles all-to-all communication internally
|
||||||
|
- Latents are gathered before unpatchify to restore full sequence
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import functools
|
||||||
|
import inspect
|
||||||
|
import os
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
|
import torch
|
||||||
|
from diffusers.utils.torch_utils import randn_tensor
|
||||||
|
from tqdm.auto import tqdm
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.runtime.distributed import (
|
||||||
|
get_local_torch_device,
|
||||||
|
get_world_group,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.distributed.communication_op import (
|
||||||
|
cfg_model_parallel_all_reduce,
|
||||||
|
sequence_model_parallel_all_gather,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.distributed.parallel_state import (
|
||||||
|
get_cfg_group,
|
||||||
|
get_classifier_free_guidance_rank,
|
||||||
|
get_sp_parallel_rank,
|
||||||
|
get_sp_world_size,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.managers.forward_context import set_forward_context
|
||||||
|
|
||||||
|
# Both audio and video DiT use the same sinusoidal_embedding_1d function
|
||||||
|
# Import from mova_video_dit where it's defined (mova_audio_dit re-exports it)
|
||||||
|
from sglang.multimodal_gen.runtime.models.dits.mova_video_dit import (
|
||||||
|
sinusoidal_embedding_1d,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Create aliases for backward compatibility
|
||||||
|
video_sinusoidal_embedding_1d = sinusoidal_embedding_1d
|
||||||
|
audio_sinusoidal_embedding_1d = sinusoidal_embedding_1d
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import OutputBatch, Req
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import (
|
||||||
|
PipelineStage,
|
||||||
|
StageParallelismType,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.decoding import (
|
||||||
|
_ensure_tensor_decode_output,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||||
|
StageValidators as V,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.validators import (
|
||||||
|
VerificationResult,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.platforms import current_platform
|
||||||
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs, get_global_server_args
|
||||||
|
from sglang.multimodal_gen.runtime.utils.layerwise_offload import OffloadableDiTMixin
|
||||||
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
from sglang.multimodal_gen.runtime.utils.perf_logger import StageProfiler
|
||||||
|
from sglang.multimodal_gen.runtime.utils.profiler import SGLDiffusionProfiler
|
||||||
|
from sglang.multimodal_gen.utils import PRECISION_TO_TYPE
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MOVALatentPreparationStage(PipelineStage):
|
||||||
|
"""Prepare video/audio noise latents for MOVA."""
|
||||||
|
|
||||||
|
def __init__(self, audio_vae, require_vae_embedding: bool = True) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.audio_vae = audio_vae
|
||||||
|
self.require_vae_embedding = require_vae_embedding
|
||||||
|
|
||||||
|
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||||
|
batch_size = batch.batch_size
|
||||||
|
num_frames = batch.num_frames
|
||||||
|
if num_frames is None:
|
||||||
|
raise ValueError("num_frames is required for MOVA")
|
||||||
|
|
||||||
|
audio_num_samples = int(self.audio_vae.sample_rate * num_frames / batch.fps)
|
||||||
|
|
||||||
|
video_shape = server_args.pipeline_config.prepare_latent_shape(
|
||||||
|
batch, batch_size, num_frames
|
||||||
|
)
|
||||||
|
audio_shape = server_args.pipeline_config.prepare_audio_latent_shape(
|
||||||
|
batch_size, audio_num_samples, self.audio_vae
|
||||||
|
)
|
||||||
|
|
||||||
|
device = get_local_torch_device()
|
||||||
|
generator = batch.generator
|
||||||
|
if isinstance(generator, list) and len(generator) != batch_size:
|
||||||
|
raise ValueError(
|
||||||
|
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
|
||||||
|
f" size of {batch_size}. Make sure the batch size matches the length of the generators."
|
||||||
|
)
|
||||||
|
|
||||||
|
dit_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.dit_precision]
|
||||||
|
batch.latents = randn_tensor(
|
||||||
|
video_shape, generator=generator, device=device, dtype=dit_dtype
|
||||||
|
)
|
||||||
|
batch.audio_latents = randn_tensor(
|
||||||
|
audio_shape, generator=generator, device=device, dtype=dit_dtype
|
||||||
|
)
|
||||||
|
|
||||||
|
if batch.image_latent is not None:
|
||||||
|
batch.y = batch.image_latent.to(device=device, dtype=dit_dtype)
|
||||||
|
elif self.require_vae_embedding:
|
||||||
|
raise ValueError("MOVA requires reference image latents for denoising")
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
class MOVATimestepPreparationStage(PipelineStage):
|
||||||
|
"""Prepare paired timesteps for MOVA."""
|
||||||
|
|
||||||
|
def __init__(self, scheduler) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.scheduler = scheduler
|
||||||
|
|
||||||
|
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||||
|
self.scheduler.set_timesteps(
|
||||||
|
batch.num_inference_steps,
|
||||||
|
denoising_strength=1.0,
|
||||||
|
shift=getattr(batch, "sigma_shift", self.scheduler.shift),
|
||||||
|
)
|
||||||
|
self.scheduler.set_pair_postprocess_by_name(
|
||||||
|
"dual_sigma_shift",
|
||||||
|
visual_shift=getattr(batch, "visual_shift", 5.0),
|
||||||
|
audio_shift=getattr(batch, "audio_shift", 5.0),
|
||||||
|
)
|
||||||
|
paired = self.scheduler.get_pairs()
|
||||||
|
batch.paired_timesteps = paired
|
||||||
|
batch.timesteps = paired
|
||||||
|
return batch
|
||||||
|
|
||||||
|
|
||||||
|
class MOVADenoisingStage(PipelineStage):
|
||||||
|
"""Run MOVA dual-tower denoising loop."""
|
||||||
|
|
||||||
|
def __init__(self, video_dit, video_dit_2, audio_dit, dual_tower_bridge, scheduler):
|
||||||
|
super().__init__()
|
||||||
|
self.video_dit = video_dit
|
||||||
|
self.video_dit_2 = video_dit_2
|
||||||
|
self.audio_dit = audio_dit
|
||||||
|
self.dual_tower_bridge = dual_tower_bridge
|
||||||
|
self.scheduler = scheduler
|
||||||
|
self._cache_dit_enabled = False
|
||||||
|
self._cached_num_steps = None
|
||||||
|
self._torch_compiled = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parallelism_type(self) -> StageParallelismType:
|
||||||
|
if get_global_server_args().enable_cfg_parallel:
|
||||||
|
return StageParallelismType.CFG_PARALLEL
|
||||||
|
return StageParallelismType.REPLICATED
|
||||||
|
|
||||||
|
def _predict(
|
||||||
|
self,
|
||||||
|
visual_dit,
|
||||||
|
visual_latents,
|
||||||
|
audio_latents,
|
||||||
|
y,
|
||||||
|
context,
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
video_fps,
|
||||||
|
timestep_index: int,
|
||||||
|
attn_metadata,
|
||||||
|
forward_batch: Req | None = None,
|
||||||
|
):
|
||||||
|
# Set forward context for distributed attention (USPAttention)
|
||||||
|
with set_forward_context(
|
||||||
|
current_timestep=timestep_index,
|
||||||
|
attn_metadata=attn_metadata,
|
||||||
|
forward_batch=forward_batch,
|
||||||
|
):
|
||||||
|
return self.inference_single_step(
|
||||||
|
visual_dit=visual_dit,
|
||||||
|
visual_latents=visual_latents,
|
||||||
|
audio_latents=audio_latents,
|
||||||
|
y=y,
|
||||||
|
context=context,
|
||||||
|
timestep=timestep,
|
||||||
|
audio_timestep=audio_timestep,
|
||||||
|
video_fps=video_fps,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _cfg_combine(self, pos, neg, guidance_scale, cfg_rank, enable_cfg_parallel):
|
||||||
|
if not enable_cfg_parallel:
|
||||||
|
return neg + guidance_scale * (pos - neg)
|
||||||
|
if cfg_rank == 0:
|
||||||
|
partial = guidance_scale * pos
|
||||||
|
else:
|
||||||
|
partial = (1 - guidance_scale) * neg
|
||||||
|
return cfg_model_parallel_all_reduce(partial)
|
||||||
|
|
||||||
|
def compile_module_with_torch_compile(self, module, server_args: ServerArgs):
|
||||||
|
if not server_args.enable_torch_compile or module is None:
|
||||||
|
return module
|
||||||
|
if not hasattr(module, "forward"):
|
||||||
|
return module
|
||||||
|
try:
|
||||||
|
import torch._inductor.config as _inductor_cfg
|
||||||
|
|
||||||
|
_inductor_cfg.reorder_for_compute_comm_overlap = True
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
mode = os.environ.get("SGLANG_TORCH_COMPILE_MODE", "max-autotune-no-cudagraphs")
|
||||||
|
logger.info("Compiling %s with mode: %s", module.__class__.__name__, mode)
|
||||||
|
compiled_forward = torch.compile(getattr(module, "forward"), mode=mode)
|
||||||
|
setattr(module, "forward", compiled_forward)
|
||||||
|
return module
|
||||||
|
|
||||||
|
def _maybe_compile_dits(self, server_args: ServerArgs):
|
||||||
|
if self._torch_compiled or not server_args.enable_torch_compile:
|
||||||
|
return
|
||||||
|
for module in filter(None, [self.video_dit, self.video_dit_2, self.audio_dit]):
|
||||||
|
self.compile_module_with_torch_compile(module, server_args)
|
||||||
|
self._torch_compiled = True
|
||||||
|
|
||||||
|
def verify_input(self, batch: Req, server_args: ServerArgs) -> VerificationResult:
|
||||||
|
"""Verify denoising stage inputs."""
|
||||||
|
result = VerificationResult()
|
||||||
|
result.add_check("y", batch.y, V.is_tensor)
|
||||||
|
result.add_check("paired_timesteps", batch.paired_timesteps, V.is_tensor)
|
||||||
|
result.add_check("latents", batch.latents, V.is_tensor)
|
||||||
|
result.add_check("audio_latents", batch.audio_latents, V.is_tensor)
|
||||||
|
result.add_check("prompt_embeds", batch.prompt_embeds, V.list_not_empty)
|
||||||
|
result.add_check(
|
||||||
|
"negative_prompt_embeds",
|
||||||
|
batch.negative_prompt_embeds,
|
||||||
|
lambda x: not batch.do_classifier_free_guidance or V.list_not_empty(x),
|
||||||
|
)
|
||||||
|
result.add_check(
|
||||||
|
"num_inference_steps", batch.num_inference_steps, V.positive_int
|
||||||
|
)
|
||||||
|
result.add_check("guidance_scale", batch.guidance_scale, V.non_negative_float)
|
||||||
|
result.add_check(
|
||||||
|
"guidance_rescale", batch.guidance_rescale, V.non_negative_float
|
||||||
|
)
|
||||||
|
result.add_check(
|
||||||
|
"do_classifier_free_guidance",
|
||||||
|
batch.do_classifier_free_guidance,
|
||||||
|
V.bool_value,
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def verify_output(self, batch: Req, server_args: ServerArgs) -> VerificationResult:
|
||||||
|
"""Verify denoising stage outputs."""
|
||||||
|
result = VerificationResult()
|
||||||
|
result.add_check("latents", batch.latents, V.is_tensor)
|
||||||
|
result.add_check("audio_latents", batch.audio_latents, V.is_tensor)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def progress_bar(
|
||||||
|
self, iterable: Iterable | None = None, total: int | None = None
|
||||||
|
) -> tqdm:
|
||||||
|
"""
|
||||||
|
Create a progress bar for the denoising process.
|
||||||
|
"""
|
||||||
|
local_rank = get_world_group().local_rank
|
||||||
|
disable = local_rank != 0
|
||||||
|
return tqdm(iterable=iterable, total=total, disable=disable)
|
||||||
|
|
||||||
|
def step_profile(self):
|
||||||
|
profiler = SGLDiffusionProfiler.get_instance()
|
||||||
|
if profiler:
|
||||||
|
profiler.step_denoising_step()
|
||||||
|
|
||||||
|
def rescale_noise_cfg(
|
||||||
|
self, noise_cfg, noise_pred_text, guidance_rescale=0.0
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Rescale noise prediction according to guidance_rescale.
|
||||||
|
|
||||||
|
Based on findings of "Common Diffusion Noise Schedules and Sample Steps are Flawed"
|
||||||
|
(https://arxiv.org/pdf/2305.08891.pdf), Section 3.4.
|
||||||
|
"""
|
||||||
|
std_text = noise_pred_text.std(
|
||||||
|
dim=list(range(1, noise_pred_text.ndim)), keepdim=True
|
||||||
|
)
|
||||||
|
std_cfg = noise_cfg.std(dim=list(range(1, noise_cfg.ndim)), keepdim=True)
|
||||||
|
noise_pred_rescaled = noise_cfg * (std_text / std_cfg)
|
||||||
|
noise_cfg = (
|
||||||
|
guidance_rescale * noise_pred_rescaled + (1 - guidance_rescale) * noise_cfg
|
||||||
|
)
|
||||||
|
return noise_cfg
|
||||||
|
|
||||||
|
def prepare_extra_func_kwargs(self, func, kwargs) -> dict[str, object]:
|
||||||
|
if not kwargs:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
if isinstance(func, functools.partial) and func.args:
|
||||||
|
func = getattr(func.args[0], "_original_forward", func)
|
||||||
|
|
||||||
|
target_func = inspect.unwrap(func)
|
||||||
|
params = inspect.signature(target_func).parameters
|
||||||
|
return {k: v for k, v in kwargs.items() if k in params}
|
||||||
|
|
||||||
|
def _build_attn_metadata(
|
||||||
|
self, i: int, batch: Req, server_args: ServerArgs
|
||||||
|
) -> object | None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _manage_device_placement(
|
||||||
|
self,
|
||||||
|
model_to_use: torch.nn.Module | None,
|
||||||
|
model_to_offload: torch.nn.Module | None,
|
||||||
|
server_args: ServerArgs,
|
||||||
|
):
|
||||||
|
if not server_args.dit_cpu_offload:
|
||||||
|
return
|
||||||
|
|
||||||
|
if (
|
||||||
|
model_to_offload is not None
|
||||||
|
and next(model_to_offload.parameters()).device.type == "cuda"
|
||||||
|
):
|
||||||
|
model_to_offload.to("cpu")
|
||||||
|
|
||||||
|
if (
|
||||||
|
model_to_use is not None
|
||||||
|
and next(model_to_use.parameters()).device.type == "cpu"
|
||||||
|
):
|
||||||
|
model_to_use.to(get_local_torch_device())
|
||||||
|
|
||||||
|
def _select_visual_dit(
|
||||||
|
self, timestep: float, boundary_ratio: float | None, server_args: ServerArgs
|
||||||
|
):
|
||||||
|
if boundary_ratio is None or self.video_dit_2 is None:
|
||||||
|
self._manage_device_placement(self.video_dit, None, server_args)
|
||||||
|
return self.video_dit
|
||||||
|
|
||||||
|
boundary_timestep = boundary_ratio * self.scheduler.num_train_timesteps
|
||||||
|
if timestep >= boundary_timestep:
|
||||||
|
current_model = self.video_dit
|
||||||
|
model_to_offload = self.video_dit_2
|
||||||
|
else:
|
||||||
|
current_model = self.video_dit_2
|
||||||
|
model_to_offload = self.video_dit
|
||||||
|
|
||||||
|
self._manage_device_placement(current_model, model_to_offload, server_args)
|
||||||
|
return current_model
|
||||||
|
|
||||||
|
def _apply_guidance_rescale(
|
||||||
|
self,
|
||||||
|
noise_pred,
|
||||||
|
noise_pred_text,
|
||||||
|
guidance_rescale,
|
||||||
|
cfg_rank,
|
||||||
|
enable_cfg_parallel,
|
||||||
|
):
|
||||||
|
if guidance_rescale <= 0.0:
|
||||||
|
return noise_pred
|
||||||
|
if enable_cfg_parallel:
|
||||||
|
std_cfg = noise_pred.std(dim=list(range(1, noise_pred.ndim)), keepdim=True)
|
||||||
|
if cfg_rank == 0:
|
||||||
|
assert noise_pred_text is not None
|
||||||
|
std_text = noise_pred_text.std(
|
||||||
|
dim=list(range(1, noise_pred_text.ndim)), keepdim=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
std_text = torch.empty_like(std_cfg)
|
||||||
|
std_text = get_cfg_group().broadcast(std_text, src=0)
|
||||||
|
noise_pred_rescaled = noise_pred * (std_text / std_cfg)
|
||||||
|
return guidance_rescale * noise_pred_rescaled + (1 - guidance_rescale) * (
|
||||||
|
noise_pred
|
||||||
|
)
|
||||||
|
return self.rescale_noise_cfg(noise_pred, noise_pred_text, guidance_rescale)
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def forward(self, batch: Req, server_args: ServerArgs) -> Req:
|
||||||
|
self._maybe_compile_dits(server_args)
|
||||||
|
self._manage_device_placement(self.audio_dit, None, server_args)
|
||||||
|
|
||||||
|
paired_timesteps = batch.paired_timesteps
|
||||||
|
if paired_timesteps is None:
|
||||||
|
raise ValueError("paired_timesteps must be set for MOVA")
|
||||||
|
|
||||||
|
y = batch.y if batch.y is not None else batch.image_latent
|
||||||
|
if getattr(self.video_dit, "require_vae_embedding", False) and y is None:
|
||||||
|
raise ValueError("MOVA requires reference image latents for denoising")
|
||||||
|
|
||||||
|
boundary_ratio = server_args.pipeline_config.boundary_ratio
|
||||||
|
total_steps = paired_timesteps.shape[0]
|
||||||
|
cfg_rank = get_classifier_free_guidance_rank()
|
||||||
|
enable_cfg_parallel = server_args.enable_cfg_parallel
|
||||||
|
|
||||||
|
is_warmup = batch.is_warmup
|
||||||
|
extra_step_kwargs = self.prepare_extra_func_kwargs(
|
||||||
|
self.scheduler.step_from_to,
|
||||||
|
getattr(batch, "extra_step_kwargs", None) or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
timings = getattr(batch, "timings", None)
|
||||||
|
perf_dump_path_provided = getattr(batch, "perf_dump_path", None) is not None
|
||||||
|
|
||||||
|
with self.progress_bar(total=total_steps) as progress_bar:
|
||||||
|
for idx_step in range(total_steps):
|
||||||
|
with StageProfiler(
|
||||||
|
f"denoising_step_{idx_step}",
|
||||||
|
logger=logger,
|
||||||
|
timings=timings,
|
||||||
|
perf_dump_path_provided=perf_dump_path_provided,
|
||||||
|
):
|
||||||
|
pair_t = paired_timesteps[idx_step]
|
||||||
|
if getattr(pair_t, "shape", None) == (2,):
|
||||||
|
timestep, audio_timestep = pair_t
|
||||||
|
else:
|
||||||
|
timestep = pair_t
|
||||||
|
audio_timestep = pair_t
|
||||||
|
|
||||||
|
cur_visual_dit = self._select_visual_dit(
|
||||||
|
timestep.item(), boundary_ratio, server_args
|
||||||
|
)
|
||||||
|
|
||||||
|
timestep = timestep.unsqueeze(0).to(device=get_local_torch_device())
|
||||||
|
audio_timestep = audio_timestep.unsqueeze(0).to(
|
||||||
|
device=get_local_torch_device()
|
||||||
|
)
|
||||||
|
|
||||||
|
attn_metadata = self._build_attn_metadata(
|
||||||
|
idx_step, batch, server_args
|
||||||
|
)
|
||||||
|
|
||||||
|
if not batch.do_classifier_free_guidance:
|
||||||
|
visual_noise_pred, audio_noise_pred = self._predict(
|
||||||
|
cur_visual_dit,
|
||||||
|
batch.latents,
|
||||||
|
batch.audio_latents,
|
||||||
|
y,
|
||||||
|
batch.prompt_embeds[0],
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
batch.fps,
|
||||||
|
idx_step,
|
||||||
|
attn_metadata,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if enable_cfg_parallel:
|
||||||
|
if cfg_rank == 0:
|
||||||
|
pos = self._predict(
|
||||||
|
cur_visual_dit,
|
||||||
|
batch.latents,
|
||||||
|
batch.audio_latents,
|
||||||
|
y,
|
||||||
|
batch.prompt_embeds[0],
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
batch.fps,
|
||||||
|
idx_step,
|
||||||
|
attn_metadata,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
neg = (None, None)
|
||||||
|
else:
|
||||||
|
pos = (None, None)
|
||||||
|
neg = self._predict(
|
||||||
|
cur_visual_dit,
|
||||||
|
batch.latents,
|
||||||
|
batch.audio_latents,
|
||||||
|
y,
|
||||||
|
batch.negative_prompt_embeds[0],
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
batch.fps,
|
||||||
|
idx_step,
|
||||||
|
attn_metadata,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
pos = self._predict(
|
||||||
|
cur_visual_dit,
|
||||||
|
batch.latents,
|
||||||
|
batch.audio_latents,
|
||||||
|
y,
|
||||||
|
batch.prompt_embeds[0],
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
batch.fps,
|
||||||
|
idx_step,
|
||||||
|
attn_metadata,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
neg = self._predict(
|
||||||
|
cur_visual_dit,
|
||||||
|
batch.latents,
|
||||||
|
batch.audio_latents,
|
||||||
|
y,
|
||||||
|
batch.negative_prompt_embeds[0],
|
||||||
|
timestep,
|
||||||
|
audio_timestep,
|
||||||
|
batch.fps,
|
||||||
|
idx_step,
|
||||||
|
attn_metadata,
|
||||||
|
batch,
|
||||||
|
)
|
||||||
|
|
||||||
|
visual_noise_pred = self._cfg_combine(
|
||||||
|
pos[0] if pos[0] is not None else neg[0],
|
||||||
|
neg[0] if neg[0] is not None else pos[0],
|
||||||
|
batch.guidance_scale,
|
||||||
|
cfg_rank,
|
||||||
|
enable_cfg_parallel,
|
||||||
|
)
|
||||||
|
audio_noise_pred = self._cfg_combine(
|
||||||
|
pos[1] if pos[1] is not None else neg[1],
|
||||||
|
neg[1] if neg[1] is not None else pos[1],
|
||||||
|
batch.guidance_scale,
|
||||||
|
cfg_rank,
|
||||||
|
enable_cfg_parallel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if batch.guidance_rescale > 0.0:
|
||||||
|
visual_noise_pred = self._apply_guidance_rescale(
|
||||||
|
visual_noise_pred,
|
||||||
|
pos[0] if pos[0] is not None else None,
|
||||||
|
batch.guidance_rescale,
|
||||||
|
cfg_rank,
|
||||||
|
enable_cfg_parallel,
|
||||||
|
)
|
||||||
|
audio_noise_pred = self._apply_guidance_rescale(
|
||||||
|
audio_noise_pred,
|
||||||
|
pos[1] if pos[1] is not None else None,
|
||||||
|
batch.guidance_rescale,
|
||||||
|
cfg_rank,
|
||||||
|
enable_cfg_parallel,
|
||||||
|
)
|
||||||
|
|
||||||
|
if idx_step + 1 < total_steps:
|
||||||
|
next_pair_t = paired_timesteps[idx_step + 1]
|
||||||
|
if getattr(next_pair_t, "shape", None) == (2,):
|
||||||
|
next_timestep, next_audio_timestep = next_pair_t
|
||||||
|
else:
|
||||||
|
next_timestep = next_pair_t
|
||||||
|
next_audio_timestep = next_pair_t
|
||||||
|
else:
|
||||||
|
next_timestep = None
|
||||||
|
next_audio_timestep = None
|
||||||
|
|
||||||
|
batch.latents = self.scheduler.step_from_to(
|
||||||
|
visual_noise_pred,
|
||||||
|
timestep,
|
||||||
|
next_timestep,
|
||||||
|
batch.latents,
|
||||||
|
**extra_step_kwargs,
|
||||||
|
)
|
||||||
|
batch.audio_latents = self.scheduler.step_from_to(
|
||||||
|
audio_noise_pred,
|
||||||
|
audio_timestep,
|
||||||
|
next_audio_timestep,
|
||||||
|
batch.audio_latents,
|
||||||
|
**extra_step_kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if progress_bar is not None:
|
||||||
|
progress_bar.update()
|
||||||
|
if not is_warmup and hasattr(self, "step_profile"):
|
||||||
|
self.step_profile()
|
||||||
|
|
||||||
|
for dit in filter(None, [self.video_dit, self.video_dit_2, self.audio_dit]):
|
||||||
|
if isinstance(dit, OffloadableDiTMixin):
|
||||||
|
dit.prepare_for_next_denoise()
|
||||||
|
|
||||||
|
return batch
|
||||||
|
|
||||||
|
def _shard_sequence_for_sp(
|
||||||
|
self, x: torch.Tensor, dim: int = 1
|
||||||
|
) -> tuple[torch.Tensor, int]:
|
||||||
|
"""
|
||||||
|
Shard tensor along sequence dimension for Sequence Parallelism.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Input tensor
|
||||||
|
dim: Dimension to shard along
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(sharded_tensor, pad_len)
|
||||||
|
"""
|
||||||
|
sp_size = get_sp_world_size()
|
||||||
|
if sp_size <= 1:
|
||||||
|
return x, 0
|
||||||
|
|
||||||
|
sp_rank = get_sp_parallel_rank()
|
||||||
|
seq_len = x.shape[dim]
|
||||||
|
|
||||||
|
# Pad if needed
|
||||||
|
pad_len = (sp_size - (seq_len % sp_size)) % sp_size
|
||||||
|
if pad_len > 0:
|
||||||
|
pad_shape = list(x.shape)
|
||||||
|
pad_shape[dim] = pad_len
|
||||||
|
pad = torch.zeros(pad_shape, dtype=x.dtype, device=x.device)
|
||||||
|
x = torch.cat([x, pad], dim=dim)
|
||||||
|
|
||||||
|
# Shard
|
||||||
|
chunk_size = x.shape[dim] // sp_size
|
||||||
|
start = sp_rank * chunk_size
|
||||||
|
end = start + chunk_size
|
||||||
|
idx = [slice(None)] * x.dim()
|
||||||
|
idx[dim] = slice(start, end)
|
||||||
|
return x[tuple(idx)], pad_len
|
||||||
|
|
||||||
|
def _gather_sequence_from_sp(
|
||||||
|
self, x: torch.Tensor, pad_len: int, dim: int = 1
|
||||||
|
) -> torch.Tensor:
|
||||||
|
"""
|
||||||
|
Gather tensor along sequence dimension after Sequence Parallelism.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
x: Sharded tensor
|
||||||
|
pad_len: Padding length that was added during sharding
|
||||||
|
dim: Dimension to gather along
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Gathered tensor with padding removed
|
||||||
|
"""
|
||||||
|
sp_size = get_sp_world_size()
|
||||||
|
if sp_size <= 1:
|
||||||
|
return x
|
||||||
|
|
||||||
|
gathered = sequence_model_parallel_all_gather(x, dim=dim)
|
||||||
|
if pad_len > 0:
|
||||||
|
idx = [slice(None)] * gathered.dim()
|
||||||
|
idx[dim] = slice(0, gathered.shape[dim] - pad_len)
|
||||||
|
gathered = gathered[tuple(idx)]
|
||||||
|
return gathered
|
||||||
|
|
||||||
|
def inference_single_step(
|
||||||
|
self,
|
||||||
|
visual_dit,
|
||||||
|
visual_latents: torch.Tensor,
|
||||||
|
audio_latents: torch.Tensor,
|
||||||
|
y,
|
||||||
|
context: torch.Tensor,
|
||||||
|
timestep: torch.Tensor,
|
||||||
|
audio_timestep: torch.Tensor,
|
||||||
|
video_fps: float,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Single inference step for MOVA dual-tower denoising.
|
||||||
|
|
||||||
|
Supports Sequence Parallelism (SP):
|
||||||
|
- After patchify, sequences are sharded across SP ranks
|
||||||
|
- USPAttention handles distributed attention communication
|
||||||
|
- Before unpatchify, sequences are gathered back
|
||||||
|
"""
|
||||||
|
model_dtype = visual_dit.time_embedding.fc_in.weight.dtype
|
||||||
|
device = visual_latents.device
|
||||||
|
|
||||||
|
visual_context = context.to(device=device, dtype=model_dtype)
|
||||||
|
audio_context = context.to(device=device, dtype=model_dtype)
|
||||||
|
with torch.autocast(
|
||||||
|
device_type=current_platform.device_type, dtype=torch.float32
|
||||||
|
):
|
||||||
|
visual_t = visual_dit.time_embedding(
|
||||||
|
video_sinusoidal_embedding_1d(visual_dit.freq_dim, timestep)
|
||||||
|
)
|
||||||
|
visual_t_mod, _ = visual_dit.time_projection(visual_t)
|
||||||
|
visual_t_mod = visual_t_mod.unflatten(1, (6, visual_dit.dim))
|
||||||
|
|
||||||
|
audio_t = self.audio_dit.time_embedding(
|
||||||
|
audio_sinusoidal_embedding_1d(self.audio_dit.freq_dim, audio_timestep)
|
||||||
|
)
|
||||||
|
audio_t_mod, _ = self.audio_dit.time_projection(audio_t)
|
||||||
|
audio_t_mod = audio_t_mod.unflatten(1, (6, self.audio_dit.dim))
|
||||||
|
|
||||||
|
visual_t = visual_t.to(model_dtype)
|
||||||
|
visual_t_mod = visual_t_mod.to(model_dtype)
|
||||||
|
audio_t = audio_t.to(model_dtype)
|
||||||
|
audio_t_mod = audio_t_mod.to(model_dtype)
|
||||||
|
|
||||||
|
visual_context_emb = visual_dit.text_embedding(visual_context)
|
||||||
|
audio_context_emb = self.audio_dit.text_embedding(audio_context)
|
||||||
|
|
||||||
|
visual_x = visual_latents.to(model_dtype)
|
||||||
|
audio_x = audio_latents.to(model_dtype)
|
||||||
|
|
||||||
|
if getattr(visual_dit, "require_vae_embedding", False):
|
||||||
|
visual_x = torch.cat([visual_x, y], dim=1)
|
||||||
|
|
||||||
|
# Patchify visual latents
|
||||||
|
visual_x, (t, h, w) = visual_dit.patchify(visual_x)
|
||||||
|
grid_size = (t, h, w)
|
||||||
|
full_visual_seq_len = t * h * w
|
||||||
|
|
||||||
|
# Build visual freqs for full sequence
|
||||||
|
visual_dit._init_freqs()
|
||||||
|
visual_freqs = tuple(freq.to(visual_x.device) for freq in visual_dit.freqs)
|
||||||
|
visual_freqs = (
|
||||||
|
torch.cat(
|
||||||
|
[
|
||||||
|
visual_freqs[0][:t].view(t, 1, 1, -1).expand(t, h, w, -1),
|
||||||
|
visual_freqs[1][:h].view(1, h, 1, -1).expand(t, h, w, -1),
|
||||||
|
visual_freqs[2][:w].view(1, 1, w, -1).expand(t, h, w, -1),
|
||||||
|
],
|
||||||
|
dim=-1,
|
||||||
|
)
|
||||||
|
.reshape(full_visual_seq_len, 1, -1)
|
||||||
|
.to(visual_x.device)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Patchify audio latents
|
||||||
|
audio_x, (f,) = self.audio_dit.patchify(audio_x, None)
|
||||||
|
full_audio_seq_len = f
|
||||||
|
|
||||||
|
# Build audio freqs for full sequence
|
||||||
|
self.audio_dit._init_freqs()
|
||||||
|
audio_freqs = (
|
||||||
|
torch.cat(
|
||||||
|
[
|
||||||
|
self.audio_dit.freqs[0][:f].view(f, -1).expand(f, -1),
|
||||||
|
self.audio_dit.freqs[1][:f].view(f, -1).expand(f, -1),
|
||||||
|
self.audio_dit.freqs[2][:f].view(f, -1).expand(f, -1),
|
||||||
|
],
|
||||||
|
dim=-1,
|
||||||
|
)
|
||||||
|
.reshape(full_audio_seq_len, 1, -1)
|
||||||
|
.to(audio_x.device)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Shard sequences for SP
|
||||||
|
visual_x, visual_pad_len = self._shard_sequence_for_sp(visual_x, dim=1)
|
||||||
|
audio_x, audio_pad_len = self._shard_sequence_for_sp(audio_x, dim=1)
|
||||||
|
|
||||||
|
# Shard freqs to match local sequence length
|
||||||
|
visual_freqs, _ = self._shard_sequence_for_sp(visual_freqs, dim=0)
|
||||||
|
audio_freqs, _ = self._shard_sequence_for_sp(audio_freqs, dim=0)
|
||||||
|
|
||||||
|
# Forward through dual-tower DiT
|
||||||
|
visual_x, audio_x = self.forward_dual_tower_dit(
|
||||||
|
visual_dit=visual_dit,
|
||||||
|
visual_x=visual_x,
|
||||||
|
audio_x=audio_x,
|
||||||
|
visual_context=visual_context_emb,
|
||||||
|
audio_context=audio_context_emb,
|
||||||
|
visual_t_mod=visual_t_mod,
|
||||||
|
audio_t_mod=audio_t_mod,
|
||||||
|
visual_freqs=visual_freqs,
|
||||||
|
audio_freqs=audio_freqs,
|
||||||
|
grid_size=grid_size,
|
||||||
|
video_fps=video_fps,
|
||||||
|
full_visual_seq_len=full_visual_seq_len,
|
||||||
|
full_audio_seq_len=full_audio_seq_len,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Gather sequences back from SP before head/unpatchify
|
||||||
|
visual_x = self._gather_sequence_from_sp(visual_x, visual_pad_len, dim=1)
|
||||||
|
audio_x = self._gather_sequence_from_sp(audio_x, audio_pad_len, dim=1)
|
||||||
|
|
||||||
|
visual_output = visual_dit.head(visual_x, visual_t)
|
||||||
|
visual_output = visual_dit.unpatchify(visual_output, grid_size)
|
||||||
|
|
||||||
|
audio_output = self.audio_dit.head(audio_x, audio_t)
|
||||||
|
audio_output = self.audio_dit.unpatchify(audio_output, (f,))
|
||||||
|
|
||||||
|
return visual_output.float(), audio_output.float()
|
||||||
|
|
||||||
|
def forward_dual_tower_dit(
|
||||||
|
self,
|
||||||
|
visual_dit,
|
||||||
|
visual_x: torch.Tensor,
|
||||||
|
audio_x: torch.Tensor,
|
||||||
|
visual_context: torch.Tensor,
|
||||||
|
audio_context: torch.Tensor,
|
||||||
|
visual_t_mod: torch.Tensor,
|
||||||
|
audio_t_mod: torch.Tensor,
|
||||||
|
visual_freqs: torch.Tensor,
|
||||||
|
audio_freqs: torch.Tensor,
|
||||||
|
grid_size: tuple[int, int, int],
|
||||||
|
video_fps: float,
|
||||||
|
full_visual_seq_len: int,
|
||||||
|
full_audio_seq_len: int,
|
||||||
|
condition_scale: float | None = 1.0,
|
||||||
|
a2v_condition_scale: float | None = None,
|
||||||
|
v2a_condition_scale: float | None = None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Forward pass through dual-tower DiT with cross-modal interaction.
|
||||||
|
|
||||||
|
Sequence Parallelism (SP) Support:
|
||||||
|
- visual_x and audio_x are already sharded along sequence dimension
|
||||||
|
- visual_freqs and audio_freqs match the local sequence length
|
||||||
|
- USPAttention in self-attention handles distributed communication
|
||||||
|
- LocalAttention in cross-attention operates on local sequence vs replicated context
|
||||||
|
- Cross-modal attention (dual_tower_bridge) uses LocalAttention (no SP communication)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
full_visual_seq_len: Full visual sequence length before SP sharding
|
||||||
|
full_audio_seq_len: Full audio sequence length before SP sharding
|
||||||
|
"""
|
||||||
|
min_layers = min(len(visual_dit.blocks), len(self.audio_dit.blocks))
|
||||||
|
visual_layers = len(visual_dit.blocks)
|
||||||
|
sp_size = get_sp_world_size()
|
||||||
|
|
||||||
|
# Build RoPE frequencies for cross-attention if needed (only used when SP == 1)
|
||||||
|
# When SP > 1, we rebuild freqs inside the loop after gathering full sequences
|
||||||
|
visual_rope_cos_sin, audio_rope_cos_sin = (
|
||||||
|
self.dual_tower_bridge.build_aligned_freqs(
|
||||||
|
video_fps=video_fps,
|
||||||
|
grid_size=grid_size,
|
||||||
|
audio_steps=full_audio_seq_len,
|
||||||
|
device=visual_x.device,
|
||||||
|
dtype=visual_x.dtype,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
if visual_rope_cos_sin is not None:
|
||||||
|
visual_rope_cos_sin = [
|
||||||
|
self._shard_sequence_for_sp(rope_cos_sin, dim=1)[0]
|
||||||
|
for rope_cos_sin in visual_rope_cos_sin
|
||||||
|
]
|
||||||
|
if audio_rope_cos_sin is not None:
|
||||||
|
audio_rope_cos_sin = [
|
||||||
|
self._shard_sequence_for_sp(rope_cos_sin, dim=1)[0]
|
||||||
|
for rope_cos_sin in audio_rope_cos_sin
|
||||||
|
]
|
||||||
|
|
||||||
|
for layer_idx in range(min_layers):
|
||||||
|
visual_block = visual_dit.blocks[layer_idx]
|
||||||
|
audio_block = self.audio_dit.blocks[layer_idx]
|
||||||
|
|
||||||
|
# Cross-modal interaction via dual tower bridge
|
||||||
|
# Bridge operations (PerFrameAttentionPooling, RoPE) expect full sequences
|
||||||
|
# When SP is enabled, we need to gather before bridge and shard after
|
||||||
|
if self.dual_tower_bridge.should_interact(layer_idx, "a2v"):
|
||||||
|
visual_x, audio_x = self.dual_tower_bridge(
|
||||||
|
layer_idx,
|
||||||
|
visual_x,
|
||||||
|
audio_x,
|
||||||
|
x_freqs=visual_rope_cos_sin,
|
||||||
|
y_freqs=audio_rope_cos_sin,
|
||||||
|
a2v_condition_scale=a2v_condition_scale,
|
||||||
|
v2a_condition_scale=v2a_condition_scale,
|
||||||
|
condition_scale=condition_scale,
|
||||||
|
video_grid_size=grid_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Self-attention and FFN in DiT blocks
|
||||||
|
visual_x = visual_block(
|
||||||
|
visual_x, visual_context, visual_t_mod, visual_freqs
|
||||||
|
)
|
||||||
|
audio_x = audio_block(audio_x, audio_context, audio_t_mod, audio_freqs)
|
||||||
|
|
||||||
|
# Process remaining visual layers (if visual has more layers than audio)
|
||||||
|
for layer_idx in range(min_layers, visual_layers):
|
||||||
|
visual_block = visual_dit.blocks[layer_idx]
|
||||||
|
visual_x = visual_block(
|
||||||
|
visual_x, visual_context, visual_t_mod, visual_freqs
|
||||||
|
)
|
||||||
|
|
||||||
|
return visual_x, audio_x
|
||||||
|
|
||||||
|
|
||||||
|
class MOVADecodingStage(PipelineStage):
|
||||||
|
"""Decode video and audio outputs for MOVA."""
|
||||||
|
|
||||||
|
def __init__(self, video_vae, audio_vae) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.video_vae = video_vae
|
||||||
|
self.audio_vae = audio_vae
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parallelism_type(self) -> StageParallelismType:
|
||||||
|
if get_global_server_args().enable_cfg_parallel:
|
||||||
|
return StageParallelismType.MAIN_RANK_ONLY
|
||||||
|
return StageParallelismType.REPLICATED
|
||||||
|
|
||||||
|
@torch.no_grad()
|
||||||
|
def forward(self, batch: Req, server_args: ServerArgs) -> OutputBatch:
|
||||||
|
self.video_vae = self.video_vae.to(get_local_torch_device())
|
||||||
|
self.audio_vae = self.audio_vae.to(get_local_torch_device())
|
||||||
|
|
||||||
|
video_latents = server_args.pipeline_config.denormalize_video_latents(
|
||||||
|
batch.latents, self.video_vae
|
||||||
|
)
|
||||||
|
|
||||||
|
vae_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
|
||||||
|
vae_autocast_enabled = (
|
||||||
|
vae_dtype != torch.float32
|
||||||
|
) and not server_args.disable_autocast
|
||||||
|
|
||||||
|
with torch.autocast(
|
||||||
|
device_type=current_platform.device_type,
|
||||||
|
dtype=vae_dtype,
|
||||||
|
enabled=vae_autocast_enabled,
|
||||||
|
):
|
||||||
|
if server_args.pipeline_config.vae_tiling:
|
||||||
|
self.video_vae.enable_tiling()
|
||||||
|
if not vae_autocast_enabled:
|
||||||
|
video_latents = video_latents.to(vae_dtype)
|
||||||
|
decode_output = self.video_vae.decode(video_latents)
|
||||||
|
video = _ensure_tensor_decode_output(decode_output)
|
||||||
|
|
||||||
|
video = (video / 2 + 0.5).clamp(0, 1)
|
||||||
|
|
||||||
|
with torch.autocast(
|
||||||
|
device_type=current_platform.device_type, dtype=torch.float32
|
||||||
|
):
|
||||||
|
audio = self.audio_vae.decode(batch.audio_latents)
|
||||||
|
output_batch = OutputBatch(
|
||||||
|
output=video,
|
||||||
|
audio=audio,
|
||||||
|
audio_sample_rate=getattr(self.audio_vae, "sample_rate", None),
|
||||||
|
timings=batch.timings,
|
||||||
|
)
|
||||||
|
return output_batch
|
||||||
@@ -0,0 +1,501 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
# Copied and adapted from: mossVG/mova/diffusion/schedulers/flow_match.py and flow_match_pair.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import math
|
||||||
|
|
||||||
|
import torch
|
||||||
|
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 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)
|
||||||
|
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
|
||||||
|
# 第一次设置 train_timesteps
|
||||||
|
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, SchedulerMixin, ConfigMixin):
|
||||||
|
"""
|
||||||
|
在 FlowMatchScheduler 的基础上,提供便捷的配对接口:
|
||||||
|
- 默认返回形状为 [num_timesteps, 2] 的张量,每一行为 (t, t)
|
||||||
|
- 允许通过 set_pair_postprocess(fn) 设置一个后处理函数以修改配对行为
|
||||||
|
"""
|
||||||
|
|
||||||
|
@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,
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
设置一个后处理函数,用于在默认配对生成后进行自定义修改。
|
||||||
|
要求:
|
||||||
|
- fn(pairs: torch.Tensor) -> torch.Tensor
|
||||||
|
- 返回的张量必须与输入 pairs 形状一致,否则直接 raise。
|
||||||
|
"""
|
||||||
|
if fn is not None and not callable(fn):
|
||||||
|
raise TypeError("pair_postprocess 必须是可调用对象或 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("调度器未初始化,请先调用 set_timesteps()")
|
||||||
|
self._refresh_pair_cache()
|
||||||
|
|
||||||
|
def set_pair_postprocess_by_name(self, name: str | None, **kwargs):
|
||||||
|
"""
|
||||||
|
通过名称快速配置后处理函数。
|
||||||
|
支持:
|
||||||
|
- None/"none"/"off"/"false"/"no": 关闭
|
||||||
|
- "quadratic_perp_bulge_swap": x2=x+d, y2=x-d,其中 d=4*amp*s*(1-s), s=t/T
|
||||||
|
- "v2a_sequential": 假设原 pairs 为 (t,t),用列0步长2采样一半序列,先让列0按该序列变化,再让列1按该序列变化
|
||||||
|
- "a2v_sequential": 同上,先列1后列0
|
||||||
|
- "dual_sigma_shift": 仅使用 timestep 数量,重新按照 FlowMatchScheduler 的 sigma 变换逻辑为两列独立构建调度,可配置 visual_shift/audio_shift
|
||||||
|
|
||||||
|
额外参数:
|
||||||
|
- amp: 浮点幅度,默认 150.0
|
||||||
|
"""
|
||||||
|
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 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
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 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
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 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
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 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
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 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
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 必须是 torch.Tensor")
|
||||||
|
if pairs.ndim != 2 or pairs.shape[1] != 2:
|
||||||
|
raise ValueError("pairs 必须是形状 [N, 2] 的 torch.Tensor")
|
||||||
|
if pairs.shape[0] == 0:
|
||||||
|
raise ValueError("pairs 的长度必须大于 0")
|
||||||
|
if source not in ("timesteps", "sigmas"):
|
||||||
|
raise ValueError("source 仅支持 'timesteps' 或 '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 必须为正数")
|
||||||
|
if denoising_strength <= 0:
|
||||||
|
raise ValueError("denoising_strength 必须为正数")
|
||||||
|
|
||||||
|
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 但未提供 exponential_shift_mu"
|
||||||
|
)
|
||||||
|
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)
|
||||||
|
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:
|
||||||
|
"""根据给定的 timestep(标量)返回对应的 sigma(按最近邻在 self.timesteps 中查找)。"""
|
||||||
|
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:
|
||||||
|
"""
|
||||||
|
使用显式给定的 (from, to) timestep 对,按照对应的 sigma 差进行一步更新:
|
||||||
|
x_{to} = x_{from} + model_output * (sigma(to) - sigma(from))
|
||||||
|
该方法可用于两个模态分别沿着它们各自列序列推进。
|
||||||
|
"""
|
||||||
|
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("调度器未初始化,请先调用 set_timesteps()")
|
||||||
|
|
||||||
|
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 返回值必须是 torch.Tensor")
|
||||||
|
if modified.shape != pairs.shape:
|
||||||
|
raise ValueError("pair_postprocess 返回的张量形状必须与输入一致")
|
||||||
|
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
|
||||||
@@ -611,7 +611,9 @@ class DiagonalGaussianDistribution:
|
|||||||
return x
|
return x
|
||||||
|
|
||||||
def kl(
|
def kl(
|
||||||
self, other: Optional["DiagonalGaussianDistribution"] = None
|
self,
|
||||||
|
other: Optional["DiagonalGaussianDistribution"] = None,
|
||||||
|
dims: tuple[int, ...] = (1, 2, 3),
|
||||||
) -> torch.Tensor:
|
) -> torch.Tensor:
|
||||||
if self.deterministic:
|
if self.deterministic:
|
||||||
return torch.Tensor([0.0])
|
return torch.Tensor([0.0])
|
||||||
@@ -619,7 +621,7 @@ class DiagonalGaussianDistribution:
|
|||||||
if other is None:
|
if other is None:
|
||||||
return 0.5 * torch.sum(
|
return 0.5 * torch.sum(
|
||||||
torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
|
torch.pow(self.mean, 2) + self.var - 1.0 - self.logvar,
|
||||||
dim=[1, 2, 3],
|
dim=dims,
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
return 0.5 * torch.sum(
|
return 0.5 * torch.sum(
|
||||||
@@ -628,7 +630,7 @@ class DiagonalGaussianDistribution:
|
|||||||
- 1.0
|
- 1.0
|
||||||
- self.logvar
|
- self.logvar
|
||||||
+ other.logvar,
|
+ other.logvar,
|
||||||
dim=[1, 2, 3],
|
dim=dims,
|
||||||
)
|
)
|
||||||
|
|
||||||
def nll(
|
def nll(
|
||||||
|
|||||||
@@ -0,0 +1,627 @@
|
|||||||
|
# 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.models.vaes.common import (
|
||||||
|
DiagonalGaussianDistribution,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Scripting this brings model speed up 1.4x
|
||||||
|
@torch.jit.script
|
||||||
|
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
|
||||||
|
|
||||||
|
|
||||||
|
class Snake1d(nn.Module):
|
||||||
|
def __init__(self, channels):
|
||||||
|
super().__init__()
|
||||||
|
self.alpha = nn.Parameter(torch.ones(1, channels, 1))
|
||||||
|
|
||||||
|
def forward(self, x):
|
||||||
|
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):
|
||||||
|
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
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
"""
|
||||||
|
MOVA pipeline integration (native SGLang pipeline).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.mova import MOVAPipelineConfig
|
||||||
|
from sglang.multimodal_gen.configs.sample.mova import MOVASamplingParams
|
||||||
|
from sglang.multimodal_gen.runtime.models.model_stages.mova import (
|
||||||
|
MOVADecodingStage,
|
||||||
|
MOVADenoisingStage,
|
||||||
|
MOVALatentPreparationStage,
|
||||||
|
MOVATimestepPreparationStage,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.composed_pipeline_base import (
|
||||||
|
ComposedPipelineBase,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.pipelines_core.stages import (
|
||||||
|
ConditioningStage,
|
||||||
|
ImageVAEEncodingStage,
|
||||||
|
InputValidationStage,
|
||||||
|
TextEncodingStage,
|
||||||
|
)
|
||||||
|
from sglang.multimodal_gen.runtime.server_args import ServerArgs
|
||||||
|
from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
|
||||||
|
|
||||||
|
logger = init_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class MOVAPipeline(ComposedPipelineBase):
|
||||||
|
"""MOVA pipeline with SGLang stage orchestration."""
|
||||||
|
|
||||||
|
pipeline_name = "MOVA"
|
||||||
|
is_video_pipeline = True
|
||||||
|
_required_config_modules = [
|
||||||
|
"video_vae",
|
||||||
|
"audio_vae",
|
||||||
|
"text_encoder",
|
||||||
|
"tokenizer",
|
||||||
|
"scheduler",
|
||||||
|
"video_dit",
|
||||||
|
"video_dit_2",
|
||||||
|
"audio_dit",
|
||||||
|
"dual_tower_bridge",
|
||||||
|
]
|
||||||
|
pipeline_config_cls = MOVAPipelineConfig
|
||||||
|
sampling_params_cls = MOVASamplingParams
|
||||||
|
|
||||||
|
def initialize_pipeline(self, server_args: ServerArgs) -> None:
|
||||||
|
"""
|
||||||
|
Initialize the pipeline.
|
||||||
|
|
||||||
|
MOVA supports Context Parallel (sequence parallel) through USPAttention,
|
||||||
|
which uses Ulysses-style all-to-all communication for distributed attention.
|
||||||
|
"""
|
||||||
|
if server_args.sp_degree > 1:
|
||||||
|
logger.info(
|
||||||
|
"MOVA Context Parallel enabled with sp_degree=%d. "
|
||||||
|
"Using USPAttention for distributed self-attention.",
|
||||||
|
server_args.sp_degree,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_pipeline_stages(self, server_args: ServerArgs) -> None:
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="input_validation_stage", stage=InputValidationStage()
|
||||||
|
)
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="prompt_encoding_stage",
|
||||||
|
stage=TextEncodingStage(
|
||||||
|
text_encoders=[self.get_module("text_encoder")],
|
||||||
|
tokenizers=[self.get_module("tokenizer")],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.add_stage(stage_name="conditioning_stage", stage=ConditioningStage())
|
||||||
|
if getattr(self.get_module("video_dit"), "require_vae_embedding", True):
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="image_latent_preparation_stage",
|
||||||
|
stage=ImageVAEEncodingStage(vae=self.get_module("video_vae")),
|
||||||
|
)
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="mova_latent_preparation_stage",
|
||||||
|
stage=MOVALatentPreparationStage(
|
||||||
|
audio_vae=self.get_module("audio_vae"),
|
||||||
|
require_vae_embedding=getattr(
|
||||||
|
self.get_module("video_dit"), "require_vae_embedding", True
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="mova_timestep_preparation_stage",
|
||||||
|
stage=MOVATimestepPreparationStage(
|
||||||
|
scheduler=self.get_module("scheduler"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="mova_denoising_stage",
|
||||||
|
stage=MOVADenoisingStage(
|
||||||
|
video_dit=self.get_module("video_dit"),
|
||||||
|
video_dit_2=self.get_module("video_dit_2"),
|
||||||
|
audio_dit=self.get_module("audio_dit"),
|
||||||
|
dual_tower_bridge=self.get_module("dual_tower_bridge"),
|
||||||
|
scheduler=self.get_module("scheduler"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
self.add_stage(
|
||||||
|
stage_name="mova_decoding_stage",
|
||||||
|
stage=MOVADecodingStage(
|
||||||
|
video_vae=self.get_module("video_vae"),
|
||||||
|
audio_vae=self.get_module("audio_vae"),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MOVAPipelineAlias(MOVAPipeline):
|
||||||
|
pipeline_name = "MOVAPipeline"
|
||||||
|
|
||||||
|
|
||||||
|
EntryClass = [MOVAPipeline, MOVAPipelineAlias]
|
||||||
@@ -182,12 +182,25 @@ class ComposedPipelineBase(ABC):
|
|||||||
"boundary_ratio" in model_index
|
"boundary_ratio" in model_index
|
||||||
and model_index["boundary_ratio"] is not None
|
and model_index["boundary_ratio"] is not None
|
||||||
):
|
):
|
||||||
|
has_transformer = (
|
||||||
|
"transformer" in model_index
|
||||||
|
or "transformer_2" in model_index
|
||||||
|
or "transformer" in self.required_config_modules
|
||||||
|
or "transformer_2" in self.required_config_modules
|
||||||
|
)
|
||||||
|
if has_transformer:
|
||||||
logger.info(
|
logger.info(
|
||||||
"MoE pipeline detected. Adding transformer_2 to self.required_config_modules..."
|
"MoE pipeline detected. Adding transformer_2 to self.required_config_modules..."
|
||||||
)
|
)
|
||||||
|
if "transformer_2" not in self.required_config_modules:
|
||||||
self.required_config_modules.append("transformer_2")
|
self.required_config_modules.append("transformer_2")
|
||||||
|
else:
|
||||||
logger.info(
|
logger.info(
|
||||||
"MoE pipeline detected. Setting boundary ratio to %s",
|
"Boundary ratio found in model_index.json without transformers; "
|
||||||
|
"using it for pipeline config only."
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
"Setting boundary ratio to %s",
|
||||||
model_index["boundary_ratio"],
|
model_index["boundary_ratio"],
|
||||||
)
|
)
|
||||||
server_args.pipeline_config.dit_config.boundary_ratio = model_index[
|
server_args.pipeline_config.dit_config.boundary_ratio = model_index[
|
||||||
|
|||||||
@@ -90,11 +90,13 @@ class Req:
|
|||||||
|
|
||||||
# Latent tensors
|
# Latent tensors
|
||||||
latents: torch.Tensor | None = None
|
latents: torch.Tensor | None = None
|
||||||
|
y: torch.Tensor | None = None
|
||||||
# Flux-2
|
# Flux-2
|
||||||
latent_ids: torch.Tensor | None = None
|
latent_ids: torch.Tensor | None = None
|
||||||
|
|
||||||
# Audio Latents (LTX-2)
|
# Audio Latents
|
||||||
audio_latents: torch.Tensor | None = None
|
audio_latents: torch.Tensor | None = None
|
||||||
|
audio_noise: torch.Tensor | None = None
|
||||||
raw_audio_latent_shape: tuple[int, ...] | None = None
|
raw_audio_latent_shape: tuple[int, ...] | None = None
|
||||||
|
|
||||||
# Audio Parameters
|
# Audio Parameters
|
||||||
@@ -114,6 +116,7 @@ class Req:
|
|||||||
|
|
||||||
# Timesteps
|
# Timesteps
|
||||||
timesteps: torch.Tensor | None = None
|
timesteps: torch.Tensor | None = None
|
||||||
|
paired_timesteps: torch.Tensor | None = None
|
||||||
timestep: torch.Tensor | float | int | None = None
|
timestep: torch.Tensor | float | int | None = None
|
||||||
step_index: int | None = None
|
step_index: int | None = None
|
||||||
|
|
||||||
@@ -154,6 +157,8 @@ class Req:
|
|||||||
|
|
||||||
# results
|
# results
|
||||||
output: torch.Tensor | None = None
|
output: torch.Tensor | None = None
|
||||||
|
audio: torch.Tensor | None = None
|
||||||
|
audio_sample_rate: int | None = None
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
# Initialize dataclass fields
|
# Initialize dataclass fields
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ from PIL import Image
|
|||||||
|
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs import WanI2V480PConfig
|
from sglang.multimodal_gen.configs.pipeline_configs import WanI2V480PConfig
|
||||||
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
from sglang.multimodal_gen.configs.pipeline_configs.base import ModelTaskType
|
||||||
|
from sglang.multimodal_gen.configs.pipeline_configs.mova import MOVAPipelineConfig
|
||||||
from sglang.multimodal_gen.runtime.models.vision_utils import load_image, load_video
|
from sglang.multimodal_gen.runtime.models.vision_utils import load_image, load_video
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
from sglang.multimodal_gen.runtime.pipelines_core.schedule_batch import Req
|
||||||
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import PipelineStage
|
from sglang.multimodal_gen.runtime.pipelines_core.stages.base import PipelineStage
|
||||||
@@ -46,6 +47,25 @@ class InputValidationStage(PipelineStage):
|
|||||||
super().__init__()
|
super().__init__()
|
||||||
self.vae_image_processor = vae_image_processor
|
self.vae_image_processor = vae_image_processor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _calculate_dimensions_from_area(
|
||||||
|
max_area: float, aspect_ratio: float, mod_value: int
|
||||||
|
) -> tuple[int, int]:
|
||||||
|
"""
|
||||||
|
Calculate output dimensions based on maximum area and aspect ratio.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
max_area: Maximum area constraint for the output
|
||||||
|
aspect_ratio: Target aspect ratio (height/width)
|
||||||
|
mod_value: Value to round dimensions to (typically vae_scale * patch_size)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Tuple of (width, height) rounded to multiples of mod_value
|
||||||
|
"""
|
||||||
|
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
|
||||||
|
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
|
||||||
|
return width, height
|
||||||
|
|
||||||
def _generate_seeds(self, batch: Req, server_args: ServerArgs):
|
def _generate_seeds(self, batch: Req, server_args: ServerArgs):
|
||||||
"""Generate seeds for the inference"""
|
"""Generate seeds for the inference"""
|
||||||
seed = batch.seed
|
seed = batch.seed
|
||||||
@@ -168,13 +188,45 @@ class InputValidationStage(PipelineStage):
|
|||||||
server_args.pipeline_config.vae_config.arch_config.scale_factor_spatial
|
server_args.pipeline_config.vae_config.arch_config.scale_factor_spatial
|
||||||
* server_args.pipeline_config.dit_config.arch_config.patch_size[1]
|
* server_args.pipeline_config.dit_config.arch_config.patch_size[1]
|
||||||
)
|
)
|
||||||
height = round(np.sqrt(max_area * aspect_ratio)) // mod_value * mod_value
|
width, height = self._calculate_dimensions_from_area(
|
||||||
width = round(np.sqrt(max_area / aspect_ratio)) // mod_value * mod_value
|
max_area, aspect_ratio, mod_value
|
||||||
|
)
|
||||||
|
|
||||||
batch.condition_image = batch.condition_image.resize((width, height))
|
batch.condition_image = batch.condition_image.resize((width, height))
|
||||||
batch.height = height
|
batch.height = height
|
||||||
batch.width = width
|
batch.width = width
|
||||||
|
|
||||||
|
elif issubclass(type(server_args.pipeline_config), MOVAPipelineConfig):
|
||||||
|
# resize image only, MOVA
|
||||||
|
image = batch.condition_image
|
||||||
|
if isinstance(image, list):
|
||||||
|
image = image[0] # not support multi image input yet.
|
||||||
|
|
||||||
|
max_area = server_args.pipeline_config.max_area
|
||||||
|
if hasattr(batch, "height") and hasattr(batch, "width"):
|
||||||
|
aspect_ratio = batch.height / batch.width
|
||||||
|
else:
|
||||||
|
aspect_ratio = (
|
||||||
|
batch.sampling_params.height / batch.sampling_params.width
|
||||||
|
)
|
||||||
|
mod_value = (
|
||||||
|
server_args.pipeline_config.vae_config.arch_config.scale_factor_spatial
|
||||||
|
* server_args.pipeline_config.dit_config.arch_config.patch_size[1]
|
||||||
|
)
|
||||||
|
width, height = self._calculate_dimensions_from_area(
|
||||||
|
max_area, aspect_ratio, mod_value
|
||||||
|
)
|
||||||
|
|
||||||
|
config = server_args.pipeline_config
|
||||||
|
image, (final_w, final_h) = (
|
||||||
|
server_args.pipeline_config.preprocess_condition_image(
|
||||||
|
image, width, height, self.vae_image_processor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
batch.condition_image = image
|
||||||
|
batch.width = final_w
|
||||||
|
batch.height = final_h
|
||||||
|
|
||||||
def forward(
|
def forward(
|
||||||
self,
|
self,
|
||||||
batch: Req,
|
batch: Req,
|
||||||
|
|||||||
@@ -342,6 +342,11 @@ class ServerArgs:
|
|||||||
default_factory=lambda: {
|
default_factory=lambda: {
|
||||||
"transformer": True,
|
"transformer": True,
|
||||||
"vae": True,
|
"vae": True,
|
||||||
|
"video_vae": True,
|
||||||
|
"audio_vae": True,
|
||||||
|
"video_dit": True,
|
||||||
|
"audio_dit": True,
|
||||||
|
"dual_tower_bridge": True,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -412,20 +412,6 @@ def verify_model_config_and_directory(model_path: str) -> dict[str, Any]:
|
|||||||
"Only HuggingFace diffusers format is supported."
|
"Only HuggingFace diffusers format is supported."
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check for transformer and vae directories
|
|
||||||
transformer_dir = os.path.join(model_path, "transformer")
|
|
||||||
vae_dir = os.path.join(model_path, "vae")
|
|
||||||
|
|
||||||
if not os.path.exists(transformer_dir):
|
|
||||||
raise ValueError(
|
|
||||||
f"Model directory {model_path} does not contain a transformer/ directory."
|
|
||||||
)
|
|
||||||
|
|
||||||
if not os.path.exists(vae_dir):
|
|
||||||
raise ValueError(
|
|
||||||
f"Model directory {model_path} does not contain a vae/ directory."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Load the config
|
# Load the config
|
||||||
with open(config_path) as f:
|
with open(config_path) as f:
|
||||||
config = json.load(f)
|
config = json.load(f)
|
||||||
@@ -435,6 +421,37 @@ def verify_model_config_and_directory(model_path: str) -> dict[str, Any]:
|
|||||||
raise ValueError("model_index.json does not contain _diffusers_version")
|
raise ValueError("model_index.json does not contain _diffusers_version")
|
||||||
|
|
||||||
logger.info("Diffusers version: %s", config["_diffusers_version"])
|
logger.info("Diffusers version: %s", config["_diffusers_version"])
|
||||||
|
|
||||||
|
component_keys = [
|
||||||
|
key
|
||||||
|
for key, value in config.items()
|
||||||
|
if isinstance(value, (list, tuple))
|
||||||
|
and len(value) == 2
|
||||||
|
and all(isinstance(item, str) for item in value)
|
||||||
|
]
|
||||||
|
if component_keys:
|
||||||
|
missing_components = [
|
||||||
|
component_key
|
||||||
|
for component_key in component_keys
|
||||||
|
if not os.path.exists(os.path.join(model_path, component_key))
|
||||||
|
]
|
||||||
|
if missing_components:
|
||||||
|
missing_str = ", ".join(missing_components)
|
||||||
|
raise ValueError(
|
||||||
|
f"Model directory {model_path} is missing required component "
|
||||||
|
f"directories: {missing_str}."
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
transformer_dir = os.path.join(model_path, "transformer")
|
||||||
|
vae_dir = os.path.join(model_path, "vae")
|
||||||
|
if not os.path.exists(transformer_dir):
|
||||||
|
raise ValueError(
|
||||||
|
f"Model directory {model_path} does not contain a transformer/ directory."
|
||||||
|
)
|
||||||
|
if not os.path.exists(vae_dir):
|
||||||
|
raise ValueError(
|
||||||
|
f"Model directory {model_path} does not contain a vae/ directory."
|
||||||
|
)
|
||||||
return cast(dict[str, Any], config)
|
return cast(dict[str, Any], config)
|
||||||
|
|
||||||
|
|
||||||
@@ -543,14 +560,35 @@ def maybe_download_model(
|
|||||||
|
|
||||||
def _verify_model_complete(path: str) -> bool:
|
def _verify_model_complete(path: str) -> bool:
|
||||||
"""Check if model directory has required subdirectories."""
|
"""Check if model directory has required subdirectories."""
|
||||||
|
config_path = os.path.join(path, "model_index.json")
|
||||||
|
if not os.path.exists(config_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(config_path) as config_file:
|
||||||
|
model_index = json.load(config_file)
|
||||||
|
except Exception as exc:
|
||||||
|
logger.warning(
|
||||||
|
"Failed to read model_index.json at %s: %s", config_path, exc
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
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 component_keys:
|
||||||
|
return all(
|
||||||
|
os.path.exists(os.path.join(path, component_key))
|
||||||
|
for component_key in component_keys
|
||||||
|
)
|
||||||
|
|
||||||
transformer_dir = os.path.join(path, "transformer")
|
transformer_dir = os.path.join(path, "transformer")
|
||||||
vae_dir = os.path.join(path, "vae")
|
vae_dir = os.path.join(path, "vae")
|
||||||
config_path = os.path.join(path, "model_index.json")
|
return os.path.exists(transformer_dir) and os.path.exists(vae_dir)
|
||||||
return (
|
|
||||||
os.path.exists(config_path)
|
|
||||||
and os.path.exists(transformer_dir)
|
|
||||||
and os.path.exists(vae_dir)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 1. Local path check: if path exists locally, verify it's complete (skip for LoRA)
|
# 1. Local path check: if path exists locally, verify it's complete (skip for LoRA)
|
||||||
if os.path.exists(model_name_or_path):
|
if os.path.exists(model_name_or_path):
|
||||||
@@ -584,7 +622,7 @@ def maybe_download_model(
|
|||||||
return model_name_or_path
|
return model_name_or_path
|
||||||
else:
|
else:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
"Local model at %s appears incomplete (missing transformer/ or vae/), "
|
"Local model at %s appears incomplete (missing required components), "
|
||||||
"will attempt re-download",
|
"will attempt re-download",
|
||||||
model_name_or_path,
|
model_name_or_path,
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user