[diffusion] model: LTX-2 (1/2) (#17495)

Co-authored-by: FlamingoPg <1106310035@qq.com>
This commit is contained in:
GMI Xiao Jin
2026-01-24 11:59:48 +08:00
committed by GitHub
co-authored by FlamingoPg
parent 894928a951
commit 797a9811a2
9 changed files with 519 additions and 29 deletions
@@ -14,8 +14,12 @@ import time
from typing import Any, List, Union from typing import Any, List, Union
import numpy as np import numpy as np
import torch
from sglang.multimodal_gen.configs.sample.sampling_params import SamplingParams from sglang.multimodal_gen.configs.sample.sampling_params import (
DataType,
SamplingParams,
)
from sglang.multimodal_gen.runtime.entrypoints.openai.utils import ( from sglang.multimodal_gen.runtime.entrypoints.openai.utils import (
ListLorasReq, ListLorasReq,
MergeLoraWeightsReq, MergeLoraWeightsReq,
@@ -231,8 +235,27 @@ class DiffGenerator:
request_idx + 1, request_idx + 1,
) )
continue continue
audio_sample_rate = output_batch.audio_sample_rate
for output_idx, sample in enumerate(output_batch.output): for output_idx, sample in enumerate(output_batch.output):
num_outputs = len(output_batch.output) num_outputs = len(output_batch.output)
audio = output_batch.audio
if req.data_type == DataType.VIDEO:
if isinstance(audio, torch.Tensor) and audio.ndim >= 2:
audio = (
audio[output_idx]
if audio.shape[0] > output_idx
else None
)
elif isinstance(audio, np.ndarray) and audio.ndim >= 2:
audio = (
audio[output_idx]
if audio.shape[0] > output_idx
else None
)
if audio is not None and not (
isinstance(sample, (tuple, list)) and len(sample) == 2
):
sample = (sample, audio)
frames = post_process_sample( frames = post_process_sample(
sample, sample,
fps=req.fps, fps=req.fps,
@@ -242,11 +265,13 @@ class DiffGenerator:
num_outputs, output_idx num_outputs, output_idx
), ),
data_type=req.data_type, data_type=req.data_type,
audio_sample_rate=audio_sample_rate,
) )
result_item: dict[str, Any] = { result_item: dict[str, Any] = {
"samples": sample, "samples": sample,
"frames": frames, "frames": frames,
"audio": audio,
"prompts": req.prompt, "prompts": req.prompt,
"size": (req.height, req.width, req.num_frames), "size": (req.height, req.width, req.num_frames),
"generation_time": timer.duration, "generation_time": timer.duration,
@@ -118,12 +118,28 @@ async def forward_to_scheduler(req_obj, sp):
raise RuntimeError("Model generation returned no output.") raise RuntimeError("Model generation returned no output.")
output_file_path = sp.output_file_path() output_file_path = sp.output_file_path()
sample = response.output[0]
try:
audio = response.audio
except AttributeError:
audio = None
if isinstance(audio, torch.Tensor) and audio.ndim >= 2:
audio = audio[0]
if audio is not None and not (
isinstance(sample, (tuple, list)) and len(sample) == 2
):
sample = (sample, audio)
post_process_sample( post_process_sample(
sample=response.output[0], sample=sample,
data_type=sp.data_type, data_type=sp.data_type,
fps=sp.fps or 24, fps=sp.fps or 24,
save_output=True, save_output=True,
save_file_path=output_file_path, save_file_path=output_file_path,
audio_sample_rate=(
response.audio_sample_rate
if hasattr(response, "audio_sample_rate")
else None
),
) )
if hasattr(response, "model_dump"): if hasattr(response, "model_dump"):
@@ -7,6 +7,7 @@ import time
from typing import Any, List, Optional, Union from typing import Any, List, Optional, Union
import httpx import httpx
import torch
from fastapi import UploadFile from fastapi import UploadFile
from sglang.multimodal_gen.configs.sample.sampling_params import DataType from sglang.multimodal_gen.configs.sample.sampling_params import DataType
@@ -211,22 +212,32 @@ async def process_generation_batch(
result = await scheduler_client.forward([batch]) result = await scheduler_client.forward([batch])
if result.output is None: if result.output is None:
error_msg = getattr(result, "error", "Unknown error") error_msg = result.error or "Unknown error"
raise RuntimeError( raise RuntimeError(
f"Model generation returned no output. Error from scheduler: {error_msg}" f"Model generation returned no output. Error from scheduler: {error_msg}"
) )
save_file_path_list = [] save_file_path_list = []
audio_sample_rate = result.audio_sample_rate
if batch.data_type == DataType.VIDEO: if batch.data_type == DataType.VIDEO:
for idx, output in enumerate(result.output): for idx, output in enumerate(result.output):
save_file_path = str( save_file_path = str(
os.path.join(batch.output_path, batch.output_file_name) os.path.join(batch.output_path, batch.output_file_name)
) )
sample = result.output[idx]
audio = result.audio
if isinstance(audio, torch.Tensor) and audio.ndim >= 2:
audio = audio[idx] if audio.shape[0] > idx else None
if audio is not None and not (
isinstance(sample, (tuple, list)) and len(sample) == 2
):
sample = (sample, audio)
post_process_sample( post_process_sample(
result.output[idx], sample,
batch.data_type, batch.data_type,
batch.fps, batch.fps,
batch.save_output, batch.save_output,
save_file_path, save_file_path,
audio_sample_rate=audio_sample_rate,
) )
save_file_path_list.append(save_file_path) save_file_path_list.append(save_file_path)
else: else:
@@ -242,6 +253,7 @@ async def process_generation_batch(
batch.fps, batch.fps,
batch.save_output, batch.save_output,
save_file_path, save_file_path,
audio_sample_rate=audio_sample_rate,
) )
save_file_path_list.append(save_file_path) save_file_path_list.append(save_file_path)
@@ -9,10 +9,25 @@ diffusion models.
""" """
import os import os
import shutil
import subprocess
import tempfile
from typing import Any, Optional
import imageio import imageio
import numpy as np
import torch import torch
try:
import scipy.io.wavfile as scipy_wavfile
except ImportError: # pragma: no cover
scipy_wavfile = None
try:
import imageio_ffmpeg as _imageio_ffmpeg
except ImportError: # pragma: no cover
_imageio_ffmpeg = None
from sglang.multimodal_gen.configs.sample.sampling_params import ( from sglang.multimodal_gen.configs.sample.sampling_params import (
DataType, DataType,
SamplingParams, SamplingParams,
@@ -24,6 +39,171 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import CYAN, RESET, init_
logger = init_logger(__name__) logger = init_logger(__name__)
def _normalize_audio_to_numpy(audio: Any) -> np.ndarray | None:
"""Convert audio (torch / numpy) into a float32 numpy array in [-1, 1], best-effort."""
if audio is None:
return None
if isinstance(audio, torch.Tensor):
audio_np = audio.detach().float().clamp(-1.0, 1.0).cpu().numpy()
elif isinstance(audio, np.ndarray):
audio_np = audio.astype(np.float32, copy=False)
audio_np = np.clip(audio_np, -1.0, 1.0)
else:
return None
# 1. Squeeze leading singleton dimensions (Batch, etc.)
while audio_np.ndim > 1 and audio_np.shape[0] == 1:
audio_np = audio_np.squeeze(0)
# 2. Handle (C, L) -> (L, C)
if audio_np.ndim == 2 and audio_np.shape[0] < audio_np.shape[1]:
audio_np = audio_np.transpose(1, 0)
# 3. Final safety check: if still 2D and channels (dim 1) is huge, something is wrong
if audio_np.ndim == 2 and audio_np.shape[1] > 256 and audio_np.shape[0] == 1:
audio_np = audio_np.flatten()
return audio_np
def _pick_audio_sample_rate(
*,
audio_np: np.ndarray,
audio_sample_rate: Optional[int],
fps: int,
num_frames: int,
) -> int:
"""Pick a plausible sample rate, falling back to inferring from video duration."""
selected_sr = int(audio_sample_rate) if audio_sample_rate is not None else None
if selected_sr is None or not (8000 <= selected_sr <= 192000):
selected_sr = 24000
try:
duration_s = float(num_frames) / float(fps) if fps else 0.0
if duration_s > 0:
audio_len = (
int(audio_np.shape[0])
if audio_np.ndim == 2
else int(audio_np.shape[-1])
)
inferred_sr = int(round(float(audio_len) / duration_s))
if 8000 <= inferred_sr <= 192000:
selected_sr = inferred_sr
except Exception:
pass
return selected_sr
def _resolve_ffmpeg_exe() -> str:
ffmpeg_exe = "ffmpeg"
ffmpeg_on_path = shutil.which("ffmpeg")
if ffmpeg_on_path:
ffmpeg_exe = ffmpeg_on_path
try:
if _imageio_ffmpeg is not None:
ffmpeg_exe = _imageio_ffmpeg.get_ffmpeg_exe()
except Exception:
pass
ffmpeg_ok = False
if ffmpeg_exe:
if os.path.isabs(ffmpeg_exe):
ffmpeg_ok = os.path.exists(ffmpeg_exe)
else:
ffmpeg_ok = shutil.which(ffmpeg_exe) is not None
if not ffmpeg_ok:
raise RuntimeError("ffmpeg not found")
return ffmpeg_exe
def _mux_audio_np_into_mp4(
*,
save_file_path: str,
audio_np: np.ndarray,
sample_rate: int,
ffmpeg_exe: str,
) -> None:
merged_path = save_file_path.rsplit(".", 1)[0] + ".tmp_mux.mp4"
tmp_wav_path = None
try:
if scipy_wavfile is None:
raise RuntimeError(
"scipy is required to mux audio into mp4 (pip install scipy)"
)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_wav_path = f.name
scipy_wavfile.write(tmp_wav_path, sample_rate, audio_np)
subprocess.run(
[
ffmpeg_exe,
"-y",
"-i",
save_file_path,
"-i",
tmp_wav_path,
"-c:v",
"copy",
"-c:a",
"aac",
"-strict",
"experimental",
merged_path,
],
check=True,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
)
os.replace(merged_path, save_file_path)
finally:
if tmp_wav_path:
try:
os.remove(tmp_wav_path)
except OSError:
pass
if os.path.exists(merged_path):
try:
os.remove(merged_path)
except OSError:
pass
def _maybe_mux_audio_into_mp4(
*,
save_file_path: str,
audio: Any,
frames: list,
fps: int,
audio_sample_rate: Optional[int],
) -> None:
"""Best-effort mux audio into an already-written mp4 at save_file_path.
Any failure should keep the silent video and only log a warning.
"""
audio_np = _normalize_audio_to_numpy(audio)
if audio_np is None:
return
selected_sr = _pick_audio_sample_rate(
audio_np=audio_np,
audio_sample_rate=audio_sample_rate,
fps=fps,
num_frames=len(frames),
)
try:
ffmpeg_exe = _resolve_ffmpeg_exe()
_mux_audio_np_into_mp4(
save_file_path=save_file_path,
audio_np=audio_np,
sample_rate=selected_sr,
ffmpeg_exe=ffmpeg_exe,
)
logger.info(f"Merged video saved to {CYAN}{save_file_path}{RESET}")
except Exception as e:
logger.warning(
"Failed to mux audio into mp4 (saved silent video): %s",
str(e),
)
def prepare_request( def prepare_request(
server_args: ServerArgs, server_args: ServerArgs,
sampling_params: SamplingParams, sampling_params: SamplingParams,
@@ -32,7 +212,10 @@ def prepare_request(
Create a Req object with sampling_params as a parameter. Create a Req object with sampling_params as a parameter.
""" """
req = Req(sampling_params=sampling_params, VSA_sparsity=server_args.VSA_sparsity) req = Req(sampling_params=sampling_params, VSA_sparsity=server_args.VSA_sparsity)
diffusers_kwargs = getattr(sampling_params, "diffusers_kwargs", None) try:
diffusers_kwargs = sampling_params.diffusers_kwargs
except AttributeError:
diffusers_kwargs = None
if diffusers_kwargs: if diffusers_kwargs:
req.extra["diffusers_kwargs"] = diffusers_kwargs req.extra["diffusers_kwargs"] = diffusers_kwargs
@@ -49,27 +232,51 @@ def prepare_request(
def post_process_sample( def post_process_sample(
sample: torch.Tensor, sample: Any,
data_type: DataType, data_type: DataType,
fps: int, fps: int,
save_output: bool = True, save_output: bool = True,
save_file_path: str = None, save_file_path: Optional[str] = None,
audio_sample_rate: Optional[int] = None,
): ):
""" """
Process sample output and save video if necessary Process sample output and save video if necessary
""" """
# 1. Vectorized processing on GPU/CPU tensor audio = None
if sample.dim() == 3: if isinstance(sample, (tuple, list)) and len(sample) == 2:
# for images, dim t is missing sample, audio = sample
sample = sample.unsqueeze(1)
# Convert to uint8 and move to CPU in bulk frames = None
# Shape: [C, T, H, W] -> [T, H, W, C] if isinstance(sample, torch.Tensor):
if sample.dim() == 3:
sample = sample.unsqueeze(1)
sample = (sample * 255).clamp(0, 255).to(torch.uint8) sample = (sample * 255).clamp(0, 255).to(torch.uint8)
videos = sample.permute(1, 2, 3, 0).cpu().numpy() videos = sample.permute(1, 2, 3, 0).cpu().numpy()
# Convert to list of frames for imageio
frames = list(videos) frames = list(videos)
else:
if not isinstance(sample, np.ndarray):
raise TypeError(f"Unsupported sample type: {type(sample)}")
arr = sample
if arr.ndim == 3:
if arr.shape[-1] in (1, 3, 4):
arr = arr[None, ...]
else:
arr = arr[..., None]
if arr.ndim != 4:
raise ValueError(f"Unexpected numpy sample shape: {tuple(arr.shape)}")
if arr.shape[-1] not in (1, 3, 4) and arr.shape[0] in (1, 3, 4):
t = torch.from_numpy(arr)
if t.dim() == 3:
t = t.unsqueeze(1)
t = (t * 255).clamp(0, 255).to(torch.uint8)
videos = t.permute(1, 2, 3, 0).cpu().numpy()
frames = list(videos)
else:
if arr.dtype != np.uint8:
arr = (np.clip(arr, 0.0, 1.0) * 255.0).astype(np.uint8)
frames = list(arr)
# 2. Save outputs if requested # 2. Save outputs if requested
if save_output: if save_output:
@@ -86,6 +293,15 @@ def post_process_sample(
codec="libx264", codec="libx264",
quality=quality, quality=quality,
) )
_maybe_mux_audio_into_mp4(
save_file_path=save_file_path,
audio=audio,
frames=frames,
fps=fps,
audio_sample_rate=audio_sample_rate,
)
else: else:
quality = 75 quality = 75
if len(frames) > 1: if len(frames) > 1:
@@ -257,6 +257,38 @@ class RotaryEmbedding(CustomOp):
return s return s
class LinearScalingRotaryEmbedding(RotaryEmbedding):
def __init__(
self,
head_size: int,
rotary_dim: int,
max_position_embeddings: int,
base: int | float,
is_neox_style: bool,
dtype: torch.dtype,
scaling_factor: float,
) -> None:
self.scaling_factor = float(scaling_factor)
super().__init__(
head_size=head_size,
rotary_dim=rotary_dim,
max_position_embeddings=max_position_embeddings,
base=base,
is_neox_style=is_neox_style,
dtype=dtype,
)
def _compute_cos_sin_cache(self) -> torch.Tensor:
inv_freq = self._compute_inv_freq(self.base)
t = torch.arange(self.max_position_embeddings, dtype=torch.float)
t = t / self.scaling_factor
freqs = torch.einsum("i,j -> ij", t, inv_freq)
cos = freqs.cos()
sin = freqs.sin()
cache = torch.cat((cos, sin), dim=-1)
return cache
class OneDRotaryEmbedding(torch.nn.Module): class OneDRotaryEmbedding(torch.nn.Module):
"""1D rotary positional embedding with caching.""" """1D rotary positional embedding with caching."""
@@ -949,10 +981,23 @@ def get_rope(
rope_scaling_args = None rope_scaling_args = None
if partial_rotary_factor < 1.0: if partial_rotary_factor < 1.0:
rotary_dim = int(rotary_dim * partial_rotary_factor) rotary_dim = int(rotary_dim * partial_rotary_factor)
max_position_embeddings = max_position
rope_type = None
if rope_scaling is not None:
rope_type = rope_scaling.get("rope_type", rope_scaling.get("type", None))
if rope_type in (None, "default"):
rope_scaling = None
elif rope_type == "linear":
factor = float(rope_scaling.get("factor", 1.0))
original_max = rope_scaling.get("original_max_position_embeddings", None)
if original_max is not None:
max_position_embeddings = max(
max_position_embeddings, int(float(original_max) * factor)
)
key = ( key = (
head_size, head_size,
rotary_dim, rotary_dim,
max_position, max_position_embeddings,
base, base,
is_neox_style, is_neox_style,
rope_scaling_args, rope_scaling_args,
@@ -963,7 +1008,19 @@ def get_rope(
if rope_scaling is None: if rope_scaling is None:
rotary_emb = RotaryEmbedding( rotary_emb = RotaryEmbedding(
head_size, rotary_dim, max_position, base, is_neox_style, dtype head_size, rotary_dim, max_position_embeddings, base, is_neox_style, dtype
)
else:
if rope_type == "linear":
factor = float(rope_scaling.get("factor", 1.0))
rotary_emb = LinearScalingRotaryEmbedding(
head_size=head_size,
rotary_dim=rotary_dim,
max_position_embeddings=max_position_embeddings,
base=base,
is_neox_style=is_neox_style,
dtype=dtype,
scaling_factor=factor,
) )
else: else:
raise ValueError(f"Unknown RoPE scaling {rope_scaling}") raise ValueError(f"Unknown RoPE scaling {rope_scaling}")
@@ -269,6 +269,17 @@ class ComponentLoader(ABC):
"vision_language_encoder": (VisionLanguageEncoderLoader, "transformers"), "vision_language_encoder": (VisionLanguageEncoderLoader, "transformers"),
} }
# Loaders for audio/video specific components that might vary
av_module_loaders = {
"audio_vae": (VAELoader, "diffusers"),
"vocoder": (VocoderLoader, "diffusers"),
"connectors": (AdapterLoader, "diffusers"),
}
# NOTE(FlamingoPg): special for LTX-2 models
if module_type == "vocoder" or module_type == "connectors":
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
@@ -277,6 +288,11 @@ class ComponentLoader(ABC):
), f"{module_type} must be loaded from {expected_library}, got {transformers_or_diffusers}" ), f"{module_type} must be loaded from {expected_library}, got {transformers_or_diffusers}"
return loader_cls() return loader_cls()
if module_type in av_module_loaders:
loader_cls, expected_library = av_module_loaders[module_type]
if transformers_or_diffusers == expected_library:
return loader_cls()
# For unknown module types, use a generic loader # For unknown module types, use a generic loader
logger.warning( logger.warning(
"No specific loader found for module type: %s. Using generic loader.", "No specific loader found for module type: %s. Using generic loader.",
@@ -484,6 +500,10 @@ class TextEncoderLoader(ComponentLoader):
self._get_all_weights(model, model_path, to_cpu=should_offload) self._get_all_weights(model, model_path, to_cpu=should_offload)
) )
# Explicitly move model to target device after loading weights
if not should_offload:
model = model.to(local_torch_device)
if should_offload: if should_offload:
# Disable FSDP for MPS as it's not compatible # Disable FSDP for MPS as it's not compatible
if current_platform.is_mps(): if current_platform.is_mps():
@@ -604,7 +624,7 @@ class VAELoader(ComponentLoader):
return server_args.vae_cpu_offload return server_args.vae_cpu_offload
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
): ):
"""Load the VAE based on the model path, and inference args.""" """Load the VAE 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)
@@ -613,10 +633,16 @@ class VAELoader(ComponentLoader):
class_name is not None class_name is not None
), "Model config does not contain a _class_name attribute. Only diffusers format is supported." ), "Model config does not contain a _class_name attribute. Only diffusers format is supported."
server_args.model_paths["vae"] = 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":
vae_config = server_args.pipeline_config.audio_vae_config
vae_precision = server_args.pipeline_config.audio_vae_precision
else:
vae_config = server_args.pipeline_config.vae_config vae_config = server_args.pipeline_config.vae_config
vae_precision = server_args.pipeline_config.vae_precision
vae_config.update_model_arch(config) vae_config.update_model_arch(config)
# NOTE: some post init logics are only available after updated with config # NOTE: some post init logics are only available after updated with config
@@ -635,7 +661,7 @@ class VAELoader(ComponentLoader):
custom_module = importlib.util.module_from_spec(spec) custom_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(custom_module) spec.loader.exec_module(custom_module)
vae_cls = getattr(custom_module, cls_name) vae_cls = getattr(custom_module, cls_name)
vae_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision] vae_dtype = PRECISION_TO_TYPE[vae_precision]
with set_default_torch_dtype(vae_dtype): with set_default_torch_dtype(vae_dtype):
vae = vae_cls.from_pretrained( vae = vae_cls.from_pretrained(
component_model_path, component_model_path,
@@ -647,9 +673,7 @@ class VAELoader(ComponentLoader):
# Load from ModelRegistry (standard VAE classes) # Load from ModelRegistry (standard VAE classes)
with ( with (
set_default_torch_dtype( set_default_torch_dtype(PRECISION_TO_TYPE[vae_precision]),
PRECISION_TO_TYPE[server_args.pipeline_config.vae_precision]
),
skip_init_modules(), skip_init_modules(),
): ):
vae_cls, _ = ModelRegistry.resolve_model_cls(class_name) vae_cls, _ = ModelRegistry.resolve_model_cls(class_name)
@@ -664,6 +688,71 @@ class VAELoader(ComponentLoader):
return vae.eval() return vae.eval()
class VocoderLoader(ComponentLoader):
def should_offload(
self, server_args: ServerArgs, model_config: ModelConfig | None = None
):
return server_args.vae_cpu_offload
def load_customized(
self, component_model_path: str, server_args: ServerArgs, module_name: str
):
config = get_diffusers_component_config(model_path=component_model_path)
class_name = config.pop("_class_name", None)
assert (
class_name is not None
), "Model config does not contain a _class_name attribute. Only diffusers format is supported."
server_args.model_paths[module_name] = component_model_path
from sglang.multimodal_gen.configs.models.vocoder.ltx_vocoder import (
LTXVocoderConfig,
)
vocoder_config = LTXVocoderConfig()
vocoder_config.update_model_arch(config)
try:
vocoder_precision = server_args.pipeline_config.audio_vae_precision
except AttributeError:
vocoder_precision = "fp32"
vocoder_dtype = PRECISION_TO_TYPE[vocoder_precision]
should_offload = self.should_offload(server_args)
target_device = self.target_device(should_offload)
with set_default_torch_dtype(vocoder_dtype), skip_init_modules():
vocoder_cls, _ = ModelRegistry.resolve_model_cls(class_name)
vocoder = vocoder_cls(vocoder_config).to(target_device)
safetensors_list = _list_safetensors_files(component_model_path)
assert (
len(safetensors_list) == 1
), f"Found {len(safetensors_list)} safetensors files in {component_model_path}"
loaded = safetensors_load_file(safetensors_list[0])
incompatible = vocoder.load_state_dict(loaded, strict=False)
missing_keys = []
unexpected_keys = []
try:
missing_keys = incompatible.missing_keys
unexpected_keys = incompatible.unexpected_keys
except AttributeError:
# Best-effort fallback in case older torch returns a tuple-like.
try:
missing_keys = incompatible[0]
unexpected_keys = incompatible[1]
except Exception:
pass
if missing_keys or unexpected_keys:
logger.warning(
"Loaded vocoder with missing_keys=%d unexpected_keys=%d",
len(missing_keys),
len(unexpected_keys),
)
return vocoder.eval()
class TransformerLoader(ComponentLoader): class TransformerLoader(ComponentLoader):
"""Loader for transformer.""" """Loader for transformer."""
@@ -755,6 +844,58 @@ class TransformerLoader(ComponentLoader):
return model return model
class AdapterLoader(ComponentLoader):
"""Loader for small adapter-style modules (e.g., LTX-2 connectors).
This loader intentionally avoids FSDP sharding and just:
1) Instantiates the module from `config.json`.
2) Loads a single safetensors state_dict.
"""
def load_customized(
self, component_model_path: str, server_args: ServerArgs, *args
):
config = get_diffusers_component_config(model_path=component_model_path)
cls_name = config.pop("_class_name", None)
if cls_name is None:
raise ValueError(
"Model config does not contain a _class_name attribute. "
"Only diffusers format is supported."
)
config.pop("_diffusers_version", None)
config.pop("_name_or_path", None)
server_args.model_paths["connectors"] = component_model_path
model_cls, _ = ModelRegistry.resolve_model_cls(cls_name)
target_device = get_local_torch_device()
default_dtype = PRECISION_TO_TYPE[server_args.pipeline_config.dit_precision]
from types import SimpleNamespace
with set_default_torch_dtype(default_dtype), skip_init_modules():
connector_cfg = SimpleNamespace(**config)
model = model_cls(connector_cfg).to(
device=target_device, dtype=default_dtype
)
safetensors_list = _list_safetensors_files(component_model_path)
if not safetensors_list:
raise ValueError(f"No safetensors files found in {component_model_path}")
if len(safetensors_list) != 1:
raise ValueError(
f"Found {len(safetensors_list)} safetensors files in {component_model_path}, expected 1"
)
loaded = safetensors_load_file(safetensors_list[0])
model.load_state_dict(loaded, strict=False)
return model.eval()
class SchedulerLoader(ComponentLoader): class SchedulerLoader(ComponentLoader):
"""Loader for scheduler.""" """Loader for scheduler."""
@@ -82,11 +82,25 @@ class Req:
# Tracking if embeddings are already processed # Tracking if embeddings are already processed
is_prompt_processed: bool = False is_prompt_processed: bool = False
# Audio Embeddings (LTX-2)
audio_prompt_embeds: list[torch.Tensor] | torch.Tensor = field(default_factory=list)
negative_audio_prompt_embeds: list[torch.Tensor] | torch.Tensor = field(
default_factory=list
)
# Latent tensors # Latent tensors
latents: torch.Tensor | None = None latents: 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: torch.Tensor | None = None
raw_audio_latent_shape: tuple[int, ...] | None = None
# Audio Parameters
fps: float = 24.0
generate_audio: bool = True
raw_latent_shape: torch.Tensor | None = None raw_latent_shape: torch.Tensor | None = None
noise_pred: torch.Tensor | None = None noise_pred: torch.Tensor | None = None
# vae-encoded condition image # vae-encoded condition image
@@ -115,6 +129,7 @@ class Req:
trajectory_timesteps: list[torch.Tensor] | None = None trajectory_timesteps: list[torch.Tensor] | None = None
trajectory_latents: torch.Tensor | None = None trajectory_latents: torch.Tensor | None = None
trajectory_audio_latents: torch.Tensor | None = None
# Extra parameters that might be needed by specific pipeline implementations # Extra parameters that might be needed by specific pipeline implementations
extra: dict[str, Any] = field(default_factory=dict) extra: dict[str, Any] = field(default_factory=dict)
@@ -297,6 +312,8 @@ class OutputBatch:
""" """
output: torch.Tensor | None = None output: torch.Tensor | None = None
audio: torch.Tensor | None = None
audio_sample_rate: int | None = None
trajectory_timesteps: list[torch.Tensor] | None = None trajectory_timesteps: list[torch.Tensor] | None = None
trajectory_latents: torch.Tensor | None = None trajectory_latents: torch.Tensor | None = None
trajectory_decoded: list[torch.Tensor] | None = None trajectory_decoded: list[torch.Tensor] | None = None
@@ -77,7 +77,9 @@ class TextEncodingStage(PipelineStage):
for pe in pooler_embeds_list: for pe in pooler_embeds_list:
batch.pooled_embeds.append(pe) batch.pooled_embeds.append(pe)
if batch.prompt_attention_mask is not None:
if batch.prompt_attention_mask is None:
batch.prompt_attention_mask = []
for am in prompt_masks_list: for am in prompt_masks_list:
batch.prompt_attention_mask.append(am) batch.prompt_attention_mask.append(am)
@@ -98,7 +100,8 @@ class TextEncodingStage(PipelineStage):
for pe in neg_pooler_embeds_list: for pe in neg_pooler_embeds_list:
batch.neg_pooled_embeds.append(pe) batch.neg_pooled_embeds.append(pe)
if batch.negative_attention_mask is not None: if batch.negative_attention_mask is None:
batch.negative_attention_mask = []
for nm in neg_masks_list: for nm in neg_masks_list:
batch.negative_attention_mask.append(nm) batch.negative_attention_mask.append(nm)
@@ -74,6 +74,7 @@ class TimestepPreparationStage(PipelineStage):
n_tokens = batch.n_tokens n_tokens = batch.n_tokens
sigmas = server_args.pipeline_config.prepare_sigmas(sigmas, num_inference_steps) sigmas = server_args.pipeline_config.prepare_sigmas(sigmas, num_inference_steps)
batch.sigmas = sigmas
# Prepare extra kwargs for set_timesteps # Prepare extra kwargs for set_timesteps
extra_set_timesteps_kwargs = {} extra_set_timesteps_kwargs = {}
@@ -87,6 +88,8 @@ class TimestepPreparationStage(PipelineStage):
key, value = callee(batch, server_args) key, value = callee(batch, server_args)
assert isinstance(key, str) assert isinstance(key, str)
extra_set_timesteps_kwargs[key] = value extra_set_timesteps_kwargs[key] = value
if key == "mu":
batch.extra["mu"] = value
# Handle custom timesteps or sigmas # Handle custom timesteps or sigmas
if timesteps is not None and sigmas is not None: if timesteps is not None and sigmas is not None: