[diffusion] fix: further align ltx2.3 accuracy with tp (#24660)

This commit is contained in:
Mick
2026-05-11 13:42:08 +08:00
committed by GitHub
parent ed70226ec1
commit 6e5b4de01a
16 changed files with 435 additions and 311 deletions
@@ -71,7 +71,7 @@ Other deployment flags:
- `--lora-weight-name`: Select the exact safetensors file when the LoRA repository contains multiple weight files. - `--lora-weight-name`: Select the exact safetensors file when the LoRA repository contains multiple weight files.
<Note> <Note>
For native LTX-2.3 two-stage serving without a user LoRA, `resident` is the fastest high-VRAM path. When you pass `--lora-path`, SGLang still applies the user LoRA during the two-stage switch, so use `resident` on H200-class GPUs for enough VRAM, but do not expect the same premerged-stage2 benefit as the no-user-LoRA path. For native LTX-2.3 two-stage serving without a user LoRA, `resident` is the fastest high-VRAM path. LTX-2 still applies the distilled LoRA during the stage switch, so `--ltx2-two-stage-device-mode` is mainly an LTX-2.3 optimization. When you pass `--lora-path`, SGLang still applies the user LoRA during the two-stage switch, so use `resident` on H200-class GPUs for enough VRAM, but do not expect the same premerged-stage2 benefit as the no-user-LoRA path.
</Note> </Note>
### 3.3 Fast multi-GPU presets ### 3.3 Fast multi-GPU presets
@@ -80,11 +80,12 @@ For latency-oriented LTX serving, prefer CFG parallel over sequence parallelism.
| Target | Recommended server flags | Notes | | Target | Recommended server flags | Notes |
| --- | --- | --- | | --- | --- | --- |
| 1 high-VRAM GPU | `--ltx2-two-stage-device-mode resident` | Fastest two-stage setup when both DiTs fit. | | LTX-2.3, 1 high-VRAM GPU | `--ltx2-two-stage-device-mode resident` | Fastest two-stage setup when both DiTs fit. |
| 1 standard GPU | `--ltx2-two-stage-device-mode snapshot` | Lower VRAM than `resident`; use this when H100-class memory is tight. | | LTX-2.3, 1 standard GPU | `--ltx2-two-stage-device-mode snapshot` | Lower VRAM than `resident`; use this when H100-class memory is tight. |
| 2 GPUs | `--num-gpus 2 --enable-cfg-parallel --ltx2-two-stage-device-mode resident` | Fastest common 2-GPU setup. | | LTX-2, 2 GPUs | `--num-gpus 2 --enable-cfg-parallel` | Fastest verified 2-GPU setup; keep `--dit-layerwise-offload` disabled unless memory is tight. |
| 4 GPUs | `--num-gpus 4 --tp-size 2 --enable-cfg-parallel --ltx2-two-stage-device-mode resident` | Fastest common 4-GPU layout: TP2 inside each CFG branch. | | LTX-2.3, 2 GPUs | `--num-gpus 2 --enable-cfg-parallel --ltx2-two-stage-device-mode resident` | Fastest common 2-GPU setup. |
| Official comparison | `--ltx2-two-stage-device-mode original` | Use this only when matching the original stage-switch semantics matters. | | LTX-2.3, 4 GPUs | `--num-gpus 4 --tp-size 2 --enable-cfg-parallel --ltx2-two-stage-device-mode resident` | Fastest common 4-GPU layout: TP2 inside each CFG branch. |
| Official comparison | `--ltx2-two-stage-device-mode original` | Use this only when matching the original LTX-2.3 stage-switch semantics matters. |
Use `--enable-cfg-parallel` for degree-2 CFG parallel. Use `--cfg-parallel-size` only when you explicitly need a different CFG branch count. If `resident` exceeds available VRAM, keep the same parallelism preset and switch only the device mode to `snapshot`. Use `--enable-cfg-parallel` for degree-2 CFG parallel. Use `--cfg-parallel-size` only when you explicitly need a different CFG branch count. If `resident` exceeds available VRAM, keep the same parallelism preset and switch only the device mode to `snapshot`.
@@ -141,7 +141,7 @@ export const LTXDeployment = () => {
let command = `sglang serve \\\n --model-path ${config.repoId} \\\n --pipeline-class-name ${pipelineClass}`; let command = `sglang serve \\\n --model-path ${config.repoId} \\\n --pipeline-class-name ${pipelineClass}`;
command += getParallelFlags(); command += getParallelFlags();
if (values.pipeline !== 'one-stage') { if (values.model === 'ltx23' && values.pipeline !== 'one-stage') {
command += ` \\\n --ltx2-two-stage-device-mode ${getDeviceMode()}`; command += ` \\\n --ltx2-two-stage-device-mode ${getDeviceMode()}`;
} }
@@ -2,7 +2,10 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from contextlib import nullcontext
import torch import torch
from torch.nn.attention import SDPBackend, sdpa_kernel
from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata, from sglang.multimodal_gen.runtime.layers.attention.backends.attention_backend import ( # FlashAttentionMetadata,
AttentionBackend, AttentionBackend,
@@ -14,6 +17,13 @@ from sglang.multimodal_gen.runtime.utils.logging_utils import init_logger
logger = init_logger(__name__) logger = init_logger(__name__)
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS = [
SDPBackend.CUDNN_ATTENTION,
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
class SDPABackend(AttentionBackend): class SDPABackend(AttentionBackend):
@@ -51,6 +61,7 @@ class SDPAImpl(AttentionImpl):
self.causal = causal self.causal = causal
self.softmax_scale = softmax_scale self.softmax_scale = softmax_scale
self.dropout = extra_impl_args.get("dropout_p", 0.0) self.dropout = extra_impl_args.get("dropout_p", 0.0)
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
def forward( def forward(
self, self,
@@ -71,6 +82,12 @@ class SDPAImpl(AttentionImpl):
} }
if query.shape[1] != key.shape[1]: if query.shape[1] != key.shape[1]:
attn_kwargs["enable_gqa"] = True attn_kwargs["enable_gqa"] = True
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and query.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
output = torch.nn.functional.scaled_dot_product_attention( output = torch.nn.functional.scaled_dot_product_attention(
query, key, value, **attn_kwargs query, key, value, **attn_kwargs
) )
@@ -1,10 +1,12 @@
# Copied and adapted from: https://github.com/hao-ai-lab/FastVideo # Copied and adapted from: https://github.com/hao-ai-lab/FastVideo
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from contextlib import nullcontext
from typing import Type from typing import Type
import torch import torch
import torch.nn as nn import torch.nn as nn
from torch.nn.attention import SDPBackend, sdpa_kernel
from sglang.multimodal_gen.runtime.distributed.communication_op import ( from sglang.multimodal_gen.runtime.distributed.communication_op import (
sequence_model_parallel_all_gather, sequence_model_parallel_all_gather,
@@ -35,6 +37,13 @@ from sglang.multimodal_gen.runtime.managers.forward_context import (
from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum from sglang.multimodal_gen.runtime.platforms import AttentionBackendEnum
from sglang.multimodal_gen.utils import get_compute_dtype from sglang.multimodal_gen.utils import get_compute_dtype
_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS = [
SDPBackend.CUDNN_ATTENTION,
SDPBackend.FLASH_ATTENTION,
SDPBackend.EFFICIENT_ATTENTION,
SDPBackend.MATH,
]
class UlyssesAttention(nn.Module): class UlyssesAttention(nn.Module):
"""Ulysses-style SequenceParallelism attention layer.""" """Ulysses-style SequenceParallelism attention layer."""
@@ -246,6 +255,7 @@ class LocalAttention(nn.Module):
head_size, dtype, supported_attention_backends=supported_attention_backends head_size, dtype, supported_attention_backends=supported_attention_backends
) )
impl_cls = attn_backend.get_impl_cls() impl_cls = attn_backend.get_impl_cls()
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
self.attn_impl = impl_cls( self.attn_impl = impl_cls(
num_heads=num_heads, num_heads=num_heads,
head_size=head_size, head_size=head_size,
@@ -304,6 +314,12 @@ class LocalAttention(nn.Module):
mask = mask[:, None, :, :] mask = mask[:, None, :, :]
mask = (mask - 1.0) * torch.finfo(q_.dtype).max mask = (mask - 1.0) * torch.finfo(q_.dtype).max
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and q_.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
return torch.nn.functional.scaled_dot_product_attention( return torch.nn.functional.scaled_dot_product_attention(
q_, q_,
k_, k_,
@@ -373,6 +389,7 @@ class USPAttention(nn.Module):
f"Please ensure your platform supports these backends." f"Please ensure your platform supports these backends."
) )
impl_cls: Type["AttentionImpl"] = attn_backend.get_impl_cls() impl_cls: Type["AttentionImpl"] = attn_backend.get_impl_cls()
self.allow_cudnn_sdp = bool(extra_impl_args.get("allow_cudnn_sdp", False))
self.attn_impl = impl_cls( self.attn_impl = impl_cls(
num_heads=num_heads, num_heads=num_heads,
head_size=head_size, head_size=head_size,
@@ -453,6 +470,12 @@ class USPAttention(nn.Module):
k_ = k.transpose(1, 2) k_ = k.transpose(1, 2)
v_ = v.transpose(1, 2) v_ = v.transpose(1, 2)
mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device) mask = _prepare_sdpa_mask(attn_mask, dtype=q_.dtype, device=q_.device)
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and q_.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
return torch.nn.functional.scaled_dot_product_attention( return torch.nn.functional.scaled_dot_product_attention(
q_, q_,
k_, k_,
@@ -489,6 +512,12 @@ class USPAttention(nn.Module):
k_ = k.transpose(1, 2) k_ = k.transpose(1, 2)
v_ = v.transpose(1, 2) v_ = v.transpose(1, 2)
mask = _prepare_sdpa_mask(gathered_mask, dtype=q_.dtype, device=q_.device) mask = _prepare_sdpa_mask(gathered_mask, dtype=q_.dtype, device=q_.device)
sdpa_context = (
sdpa_kernel(_PYTORCH_DEFAULT_CUDA_SDP_BACKENDS)
if self.allow_cudnn_sdp and q_.device.type == "cuda"
else nullcontext()
)
with sdpa_context:
out = torch.nn.functional.scaled_dot_product_attention( out = torch.nn.functional.scaled_dot_product_attention(
q_, q_,
k_, k_,
@@ -4,11 +4,8 @@
from __future__ import annotations from __future__ import annotations
import functools
import math
from typing import Any, Optional, Tuple, Union from typing import Any, Optional, Tuple, Union
import numpy as np
import torch import torch
import torch.nn as nn import torch.nn as nn
import torch.nn.functional as F import torch.nn.functional as F
@@ -98,17 +95,6 @@ def _ltx2_build_batched_perturbation_states(
return states return states
@functools.lru_cache(maxsize=5)
def _ltx2_rope_freq_grid_np(theta: float, num_pos_dims: int, dim: int) -> torch.Tensor:
# Official LTX uses NumPy float64 for double-precision RoPE frequencies.
n_elem = 2 * num_pos_dims
pow_indices = np.power(
theta,
np.linspace(0.0, 1.0, dim // n_elem, dtype=np.float64),
)
return torch.tensor(pow_indices * math.pi / 2.0, dtype=torch.float32)
def apply_interleaved_rotary_emb( def apply_interleaved_rotary_emb(
x: torch.Tensor, freqs: Tuple[torch.Tensor, torch.Tensor] x: torch.Tensor, freqs: Tuple[torch.Tensor, torch.Tensor]
) -> torch.Tensor: ) -> torch.Tensor:
@@ -351,18 +337,16 @@ class LTX2AudioVideoRotaryPosEmbed(nn.Module):
).to(device) ).to(device)
num_rope_elems = num_pos_dims * 2 num_rope_elems = num_pos_dims * 2
if self.double_precision: # LTX-2.3 HQ is sensitive to RoPE rounding; keep frequency generation on
freqs = _ltx2_rope_freq_grid_np(self.theta, num_pos_dims, self.dim).to( # the target device instead of caching a CPU/NumPy tensor.
device=device freqs_dtype = torch.float64 if self.double_precision else torch.float32
)
else:
pow_indices = torch.pow( pow_indices = torch.pow(
self.theta, self.theta,
torch.linspace( torch.linspace(
start=0.0, start=0.0,
end=1.0, end=1.0,
steps=self.dim // num_rope_elems, steps=self.dim // num_rope_elems,
dtype=torch.float32, dtype=freqs_dtype,
device=device, device=device,
), ),
) )
@@ -647,6 +631,8 @@ class LTX2Attention(nn.Module):
causal=False, causal=False,
supported_attention_backends=supported_attention_backends, supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.attn", prefix=f"{prefix}.attn",
# official LTX2 torch_sdpa uses cuDNN; cuda setup disables it
allow_cudnn_sdp=True,
) )
else: else:
self.attn = USPAttention( self.attn = USPAttention(
@@ -658,6 +644,8 @@ class LTX2Attention(nn.Module):
causal=False, causal=False,
supported_attention_backends=supported_attention_backends, supported_attention_backends=supported_attention_backends,
prefix=f"{prefix}.attn", prefix=f"{prefix}.attn",
# official LTX2 torch_sdpa uses cuDNN; cuda setup disables it
allow_cudnn_sdp=True,
) )
def forward( def forward(
@@ -1422,9 +1410,18 @@ class LTX2VideoTransformer3DModel(CachableDiT, OffloadableDiTMixin):
if hasattr(arch.rope_type, "value") if hasattr(arch.rope_type, "value")
else str(arch.rope_type) else str(arch.rope_type)
) )
rope_double_precision = bool( frequencies_precision = hf_config.get("frequencies_precision")
if frequencies_precision is None:
frequencies_precision = getattr(arch, "frequencies_precision", None)
# diffusers/LTX configs use `frequencies_precision` for this RoPE switch
rope_double_precision = (
str(frequencies_precision) == "float64"
if frequencies_precision is not None
else bool(
hf_config.get("rope_double_precision", arch.double_precision_rope) hf_config.get("rope_double_precision", arch.double_precision_rope)
) )
)
self.quantize_video_rope_coords_to_hidden_dtype = bool( self.quantize_video_rope_coords_to_hidden_dtype = bool(
hf_config.get("quantize_video_rope_coords_to_hidden_dtype", False) hf_config.get("quantize_video_rope_coords_to_hidden_dtype", False)
) )
@@ -146,14 +146,21 @@ class Gemma3Attention(nn.Module):
prefix=f"{prefix}.o_proj", prefix=f"{prefix}.o_proj",
) )
self.layer_type = ( layer_types = getattr(config.text_config, "layer_types", None)
config.text_config.layer_types[layer_id] if layer_types:
if hasattr(config.text_config, "layer_types") self.layer_type = layer_types[layer_id]
else None self.is_sliding = self.layer_type == "sliding_attention"
else:
# official Gemma3 uses sliding_window_pattern when layer_types is absent
sliding_window_pattern = getattr(
config.text_config, "sliding_window_pattern", None
) )
self.is_sliding = ( self.is_sliding = (
config.text_config.layer_types[layer_id] == "sliding_attention" bool((layer_id + 1) % sliding_window_pattern)
if sliding_window_pattern
else False
) )
self.layer_type = "sliding_attention" if self.is_sliding else None
rope_parameters = getattr(config.text_config, "rope_parameters", None) or {} rope_parameters = getattr(config.text_config, "rope_parameters", None) or {}
layer_rope_params = {} layer_rope_params = {}
@@ -204,7 +211,7 @@ class Gemma3Attention(nn.Module):
self.sliding_window = None self.sliding_window = None
self.window_size = (-1, -1) self.window_size = (-1, -1)
self.rotary_emb = get_rope( self.rotary_pos_emb = get_rope(
self.head_dim, self.head_dim,
rotary_dim=self.head_dim, rotary_dim=self.head_dim,
max_position=config.text_config.max_position_embeddings, max_position=config.text_config.max_position_embeddings,
@@ -213,12 +220,6 @@ class Gemma3Attention(nn.Module):
is_neox_style=True, is_neox_style=True,
) )
self.rope_scaling_factor = (
float(rope_scaling["factor"])
if rope_scaling and rope_scaling.get("factor")
else None
)
# Local Attention not support attention mask, we use global attention instead. # Local Attention not support attention mask, we use global attention instead.
# self.attn = LocalAttention( # self.attn = LocalAttention(
# self.num_heads, # self.num_heads,
@@ -238,25 +239,18 @@ class Gemma3Attention(nn.Module):
dim=self.head_dim, eps=config.text_config.rms_norm_eps dim=self.head_dim, eps=config.text_config.rms_norm_eps
) )
def rotary_emb(self, positions, q, k): def _apply_rotary_pos_emb(self, positions, q, k):
"""Apply RoPE using the same device-side inv_freq materialization as LTX.""" positions_flat = positions.flatten().to(
positions_flat = positions.flatten().float() device=self.rotary_pos_emb.cos_sin_cache.device, dtype=torch.long
num_tokens = positions_flat.shape[0]
with torch.autocast(device_type=q.device.type, enabled=False):
freq_indices = (
torch.arange(
0, self.head_dim, 2, dtype=torch.int64, device=q.device
).float()
/ self.head_dim
) )
inv_freq = 1.0 / (self.rope_theta**freq_indices) cos_sin = self.rotary_pos_emb.cos_sin_cache.index_select(0, positions_flat)
if self.rope_scaling_factor is not None: cos, sin = cos_sin.chunk(2, dim=-1)
inv_freq = inv_freq / self.rope_scaling_factor # match HF Gemma3: expand half-dim freqs to full head dim before rotate_half
freqs = torch.outer(positions_flat, inv_freq) cos = torch.cat((cos, cos), dim=-1).to(device=q.device, dtype=q.dtype)
emb = freqs.repeat(1, 2) sin = torch.cat((sin, sin), dim=-1).to(device=q.device, dtype=q.dtype)
cos = emb.cos().to(q.dtype).unsqueeze(1) cos = cos.unsqueeze(1)
sin = emb.sin().to(q.dtype).unsqueeze(1) sin = sin.unsqueeze(1)
num_tokens = positions_flat.shape[0]
q = q.reshape(num_tokens, -1, self.head_dim) q = q.reshape(num_tokens, -1, self.head_dim)
k = k.reshape(num_tokens, -1, self.head_dim) k = k.reshape(num_tokens, -1, self.head_dim)
@@ -283,7 +277,7 @@ class Gemma3Attention(nn.Module):
k = self.k_norm(k) k = self.k_norm(k)
# Apply RoPE # Apply RoPE
q, k = self.rotary_emb(positions, q, k) q, k = self._apply_rotary_pos_emb(positions, q, k)
q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim) q = q.reshape(batch_size, seq_len, self.num_heads, self.head_dim)
k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim) k = k.reshape(batch_size, seq_len, self.num_kv_heads, self.head_dim)
@@ -306,7 +300,7 @@ class Gemma3Attention(nn.Module):
attn_mask = attn_mask.masked_fill(causal, False) attn_mask = attn_mask.masked_fill(causal, False)
if self.is_sliding and self.sliding_window is not None: if self.is_sliding and self.sliding_window is not None:
idx = torch.arange(seq_len, device=hidden_states.device) idx = torch.arange(seq_len, device=hidden_states.device)
dist = idx[None, :] - idx[:, None] dist = idx[:, None] - idx[None, :]
too_far = dist > self.sliding_window too_far = dist > self.sliding_window
attn_mask = attn_mask.masked_fill(too_far, False) attn_mask = attn_mask.masked_fill(too_far, False)
@@ -193,7 +193,6 @@ class LTX2SigmaPreparationStage(PipelineStage):
int(batch.num_inference_steps), int(batch.num_inference_steps),
number_of_tokens=latent_num_frames * latent_height * latent_width, number_of_tokens=latent_num_frames * latent_height * latent_width,
) )
batch.sigmas.append(0.0011)
else: else:
batch.sigmas = build_official_ltx2_sigmas( batch.sigmas = build_official_ltx2_sigmas(
int(batch.num_inference_steps) int(batch.num_inference_steps)
@@ -82,8 +82,12 @@ class ParallelExecutor(PipelineExecutor):
elif paradigm == StageParallelismType.CFG_PARALLEL: elif paradigm == StageParallelismType.CFG_PARALLEL:
obj_list = [batch] if rank == 0 else [] obj_list = [batch] if rank == 0 else []
# `dist.broadcast(src=...)` expects a global rank for process groups.
broadcasted_list = broadcast_pyobj( broadcasted_list = broadcast_pyobj(
obj_list, rank=rank, dist_group=cfg_group.cpu_group, src=0 obj_list,
rank=get_world_rank(),
dist_group=cfg_group.cpu_group,
src=cfg_group.ranks[0],
) )
if rank != 0: if rank != 0:
batch = broadcasted_list[0] batch = broadcasted_list[0]
@@ -354,12 +354,11 @@ class LTX2RefinementStage(LTX2AVDenoisingStage):
scheduler = clone_scheduler_runtime(original_batch_scheduler or self.scheduler) scheduler = clone_scheduler_runtime(original_batch_scheduler or self.scheduler)
distilled_device = scheduler.sigmas.device distilled_device = scheduler.sigmas.device
num_steps = len(self.distilled_sigmas) - 1
# Inject `0.0011` before the terminal `0.0` to avoid the # Inject `0.0011` before the terminal `0.0` to avoid the
# `sigma_next==0` singularity in res2s' `(sample - denoised) / # `sigma_next==0` singularity in res2s' `(sample - denoised) /
# (sigma - sigma_next)`. Official `res2s_denoising_loop` does this # (sigma - sigma_next)`. This changes the final sigma pair only; it
# exact injection (samplers.py:262); official `euler_denoising_loop` # must not add an extra denoising timestep.
# does NOT — it uses `sigma_next` directly. So gate on the active
# sampler, not on the model variant.
if self.sampler_name == "res2s" and self.distilled_sigmas[-1].item() == 0.0: if self.sampler_name == "res2s" and self.distilled_sigmas[-1].item() == 0.0:
scheduler_sigmas = torch.cat( scheduler_sigmas = torch.cat(
[ [
@@ -372,9 +371,10 @@ class LTX2RefinementStage(LTX2AVDenoisingStage):
scheduler_sigmas = self.distilled_sigmas scheduler_sigmas = self.distilled_sigmas
scheduler.sigmas = scheduler_sigmas scheduler.sigmas = scheduler_sigmas
num_steps = len(scheduler_sigmas) - 1
scheduler.num_inference_steps = num_steps scheduler.num_inference_steps = num_steps
scheduler.timesteps = (scheduler_sigmas[:num_steps] * 1000).to(distilled_device) scheduler.timesteps = (self.distilled_sigmas[:num_steps] * 1000).to(
distilled_device
)
scheduler._step_index = None scheduler._step_index = None
scheduler._begin_index = None scheduler._begin_index = None
@@ -422,45 +422,76 @@ class LTX2DenoisingStage(DenoisingStage):
return pred * factor return pred * factor
@classmethod @classmethod
def _ltx2_combine_guided_x0_parallel( def _ltx2_combine_guided_x0_parallel_av(
cls, cls,
*, *,
latents: torch.Tensor, video_latents: torch.Tensor,
local_velocities: dict[str, torch.Tensor], audio_latents: torch.Tensor,
sigma: float | torch.Tensor, local_video_velocities: dict[str, torch.Tensor],
cfg_scale: float, local_audio_velocities: dict[str, torch.Tensor],
stg_scale: float, video_sigma: float | torch.Tensor,
rescale_scale: float, audio_sigma: float | torch.Tensor,
modality_scale: float, video_cfg_scale: float,
) -> torch.Tensor: video_stg_scale: float,
"""Combine stage-1 guidance passes that were split across CFG ranks. video_rescale_scale: float,
video_modality_scale: float,
audio_cfg_scale: float,
audio_stg_scale: float,
audio_rescale_scale: float,
audio_modality_scale: float,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Reconstruct CFG branches once for both modalities before guider math."""
first_video_velocity = next(iter(local_video_velocities.values()))
first_audio_velocity = next(iter(local_audio_velocities.values()))
video_template = cls._ltx2_velocity_to_x0(
video_latents, first_video_velocity, video_sigma
)
audio_template = cls._ltx2_velocity_to_x0(
audio_latents, first_audio_velocity, audio_sigma
)
video_numel = video_template.numel()
branches: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
Each pass is one model forward with a different conditioning setup: for name in ("cond", "neg", "perturbed", "modality"):
positive prompt, negative prompt, attention-disabled perturbation, or if name in local_video_velocities:
audio/video cross-attention disabled. A rank only owns some passes, so local_video = cls._ltx2_velocity_to_x0(
it contributes weighted x0 terms for those passes and all-reduce video_latents, local_video_velocities[name], video_sigma
reconstructs the full guided x0 on every rank. )
""" local_audio = cls._ltx2_velocity_to_x0(
coefficients = { audio_latents, local_audio_velocities[name], audio_sigma
"cond": cfg_scale + stg_scale + modality_scale - 1.0, )
"neg": 1.0 - cfg_scale, else:
"perturbed": -stg_scale, local_video = torch.zeros_like(video_template)
"modality": 1.0 - modality_scale, local_audio = torch.zeros_like(audio_template)
} flat = torch.cat((local_video.reshape(-1), local_audio.reshape(-1)))
first_velocity = next(iter(local_velocities.values())) flat = cfg_model_parallel_all_reduce(flat)
template = cls._ltx2_velocity_to_x0(latents, first_velocity, sigma) branches[name] = (
cond_partial = torch.zeros_like(template) flat[:video_numel].reshape_as(video_template),
pred_partial = torch.zeros_like(template) flat[video_numel:].reshape_as(audio_template),
)
for name, velocity in local_velocities.items(): # folding the coefficients changes bf16 rounding and drifts from single-GPU
denoised = cls._ltx2_velocity_to_x0(latents, velocity, sigma) guided_video = cls._ltx2_calculate_guided_x0(
if name == "cond": cond=branches["cond"][0],
cond_partial = cond_partial + denoised uncond_text=branches["neg"][0],
pred_partial = pred_partial + denoised * coefficients[name] uncond_perturbed=branches["perturbed"][0],
uncond_modality=branches["modality"][0],
cond = cfg_model_parallel_all_reduce(cond_partial) cfg_scale=video_cfg_scale,
pred = cfg_model_parallel_all_reduce(pred_partial) stg_scale=video_stg_scale,
return cls._ltx2_apply_rescale(cond, pred, rescale_scale) rescale_scale=video_rescale_scale,
modality_scale=video_modality_scale,
)
guided_audio = cls._ltx2_calculate_guided_x0(
cond=branches["cond"][1],
uncond_text=branches["neg"][1],
uncond_perturbed=branches["perturbed"][1],
uncond_modality=branches["modality"][1],
cfg_scale=audio_cfg_scale,
stg_scale=audio_stg_scale,
rescale_scale=audio_rescale_scale,
modality_scale=audio_modality_scale,
)
return guided_video, guided_audio
@staticmethod @staticmethod
def _ltx2_channelwise_normalize(noise: torch.Tensor) -> torch.Tensor: def _ltx2_channelwise_normalize(noise: torch.Tensor) -> torch.Tensor:
@@ -556,7 +587,9 @@ class LTX2DenoisingStage(DenoisingStage):
dtype=sliced.dtype, dtype=sliced.dtype,
) )
sliced = torch.cat([sliced, pad], dim=1) sliced = torch.cat([sliced, pad], dim=1)
return sliced.to(dtype=reference_tensor.dtype) return sliced
# The native HQ SDE path consumes this through `.float()`; keep the
# original downcast boundary before that upcast to preserve the trajectory.
return cls._ltx2_res2s_new_noise(reference_tensor, generator).to( return cls._ltx2_res2s_new_noise(reference_tensor, generator).to(
dtype=reference_tensor.dtype dtype=reference_tensor.dtype
) )
@@ -703,6 +736,8 @@ class LTX2DenoisingStage(DenoisingStage):
update (midpoint SDE, bongmath anchor refinement, midpoint re-eval, update (midpoint SDE, bongmath anchor refinement, midpoint re-eval,
final RK2 combination with SDE noise). Mirrors the guided stage-1 res2s final RK2 combination with SDE noise). Mirrors the guided stage-1 res2s
math but without CFG/STG (stage-2 HQ uses the simple CFG path). math but without CFG/STG (stage-2 HQ uses the simple CFG path).
Raw HQ model timesteps are only inputs to the DiT call; stage-2 res2s
math stays in scheduler sigma space.
""" """
sigma_val = float(sigma.item()) sigma_val = float(sigma.item())
sigma_next_val = float(sigma_next.item()) sigma_next_val = float(sigma_next.item())
@@ -711,24 +746,10 @@ class LTX2DenoisingStage(DenoisingStage):
denoised_video = ctx.latents.float() denoised_video = ctx.latents.float()
denoised_audio = ctx.audio_latents.float() denoised_audio = ctx.audio_latents.float()
else: else:
video_sigma_for_x0 = ( denoised_video = ctx.latents.float() - sigma * model_video_velocity.float()
model_video_timestep denoised_audio = (
if ctx.use_ltx23_hq_timestep_semantics ctx.audio_latents.float() - sigma * model_audio_velocity.float()
and model_video_timestep is not None
else sigma
) )
audio_sigma_for_x0 = (
model_audio_timestep
if ctx.use_ltx23_hq_timestep_semantics
and model_audio_timestep is not None
else sigma
)
denoised_video = self._ltx2_velocity_to_x0(
ctx.latents, model_video_velocity, video_sigma_for_x0
).float()
denoised_audio = self._ltx2_velocity_to_x0(
ctx.audio_latents, model_audio_velocity, audio_sigma_for_x0
).float()
if sigma_val == 0.0 or sigma_next_val == 0.0: if sigma_val == 0.0 or sigma_next_val == 0.0:
next_video = denoised_video.to(dtype=ctx.latents.dtype) next_video = denoised_video.to(dtype=ctx.latents.dtype)
@@ -738,11 +759,6 @@ class LTX2DenoisingStage(DenoisingStage):
sigma_d = sigma.double() sigma_d = sigma.double()
sigma_next_d = sigma_next.double() sigma_next_d = sigma_next.double()
if ctx.use_ltx23_hq_timestep_semantics:
h = self._ltx2_res2s_step_size_scalar(sigma_d, sigma_next_d)
a21, b1, b2 = self._ltx2_get_res2s_coefficients_scalar(h)
h_value = h
else:
h = -torch.log(torch.clamp(sigma_next_d / sigma_d, min=1e-12)) h = -torch.log(torch.clamp(sigma_next_d / sigma_d, min=1e-12))
a21, b1, b2 = self._ltx2_get_res2s_coefficients(h) a21, b1, b2 = self._ltx2_get_res2s_coefficients(h)
h_value = float(h.item()) h_value = float(h.item())
@@ -809,22 +825,8 @@ class LTX2DenoisingStage(DenoisingStage):
midpoint_video_model_latents, midpoint_audio_model_latents, sub_sigma midpoint_video_model_latents, midpoint_audio_model_latents, sub_sigma
) )
mid_video_sigma_for_x0 = ( midpoint_denoised_video = midpoint_video_latents.float() - sub_sigma * mid_v
mid_video_timestep midpoint_denoised_audio = midpoint_audio_latents.float() - sub_sigma * mid_a
if ctx.use_ltx23_hq_timestep_semantics and mid_video_timestep is not None
else sub_sigma
)
mid_audio_sigma_for_x0 = (
mid_audio_timestep
if ctx.use_ltx23_hq_timestep_semantics and mid_audio_timestep is not None
else sub_sigma
)
midpoint_denoised_video = self._ltx2_velocity_to_x0(
midpoint_video_latents, mid_v, mid_video_sigma_for_x0
).float()
midpoint_denoised_audio = self._ltx2_velocity_to_x0(
midpoint_audio_latents, mid_a, mid_audio_sigma_for_x0
).float()
eps2_video = midpoint_denoised_video.double() - anchor_video eps2_video = midpoint_denoised_video.double() - anchor_video
eps2_audio = midpoint_denoised_audio.double() - anchor_audio eps2_audio = midpoint_denoised_audio.double() - anchor_audio
@@ -848,23 +850,19 @@ class LTX2DenoisingStage(DenoisingStage):
ctx.audio_latents, batch ctx.audio_latents, batch
).float() ).float()
) )
sde_sigma = sigma if ctx.use_ltx23_hq_timestep_semantics else sigma_d
sde_sigma_next = (
sigma_next if ctx.use_ltx23_hq_timestep_semantics else sigma_next_d
)
next_video = self._ltx2_res2s_sde_step( next_video = self._ltx2_res2s_sde_step(
sample=anchor_video, sample=anchor_video,
denoised_sample=next_video_det, denoised_sample=next_video_det,
sigma=sde_sigma, sigma=sigma_d,
sigma_next=sde_sigma_next, sigma_next=sigma_next_d,
noise=step_noise_video, noise=step_noise_video,
terminal=False, terminal=False,
) )
next_audio = self._ltx2_res2s_sde_step( next_audio = self._ltx2_res2s_sde_step(
sample=anchor_audio, sample=anchor_audio,
denoised_sample=next_audio_det, denoised_sample=next_audio_det,
sigma=sde_sigma, sigma=sigma_d,
sigma_next=sde_sigma_next, sigma_next=sigma_next_d,
noise=step_noise_audio, noise=step_noise_audio,
terminal=False, terminal=False,
) )
@@ -1574,9 +1572,19 @@ class LTX2DenoisingStage(DenoisingStage):
ctx.denoise_mask = ctx.denoise_mask.to(device) ctx.denoise_mask = ctx.denoise_mask.to(device)
if ctx.clean_latent is not None: if ctx.clean_latent is not None:
ctx.clean_latent = ctx.clean_latent.to(device) ctx.clean_latent = ctx.clean_latent.to(device)
self._move_ltx2_scheduler_tensors_to_device(ctx.scheduler, device)
self._move_ltx2_scheduler_tensors_to_device(ctx.audio_scheduler, device)
return ctx return ctx
@staticmethod
def _move_ltx2_scheduler_tensors_to_device(scheduler: object, device) -> None:
# cfg-parallel batches are broadcast from rank 0; scheduler state must be local
for name in ("sigmas", "timesteps"):
value = getattr(scheduler, name, None)
if isinstance(value, torch.Tensor):
setattr(scheduler, name, value.to(device))
def _before_denoising_loop( def _before_denoising_loop(
self, ctx: LTX2DenoisingContext, batch: Req, server_args: ServerArgs self, ctx: LTX2DenoisingContext, batch: Req, server_args: ServerArgs
) -> None: ) -> None:
@@ -1652,9 +1660,13 @@ class LTX2DenoisingStage(DenoisingStage):
) )
use_official_cfg_path = stage1_guider_params is None use_official_cfg_path = stage1_guider_params is None
if use_official_cfg_path: if use_official_cfg_path:
cfg_parallel = ( do_two_branch_cfg = batch.do_classifier_free_guidance
server_args.enable_cfg_parallel and batch.do_classifier_free_guidance if ctx.stage == "stage2" and is_ltx2_two_stage_pipeline_name(
) server_args.pipeline_class_name
):
# official two-stage stage 2 is a distilled positive-only denoiser
do_two_branch_cfg = False
cfg_parallel = server_args.enable_cfg_parallel and do_two_branch_cfg
cfg_rank = get_classifier_free_guidance_rank() if cfg_parallel else 0 cfg_rank = get_classifier_free_guidance_rank() if cfg_parallel else 0
if cfg_parallel: if cfg_parallel:
@@ -1691,7 +1703,7 @@ class LTX2DenoisingStage(DenoisingStage):
audio_encoder_hidden_states=batch.audio_prompt_embeds[0], audio_encoder_hidden_states=batch.audio_prompt_embeds[0],
encoder_attention_mask=prompt_attention_mask, encoder_attention_mask=prompt_attention_mask,
) )
if batch.do_classifier_free_guidance: if do_two_branch_cfg:
cfg_batch_size = batch_size * 2 cfg_batch_size = batch_size * 2
model_kwargs = self._repeat_ltx2_model_kwargs_batch( model_kwargs = self._repeat_ltx2_model_kwargs_batch(
model_kwargs, cfg_batch_size model_kwargs, cfg_batch_size
@@ -1735,7 +1747,7 @@ class LTX2DenoisingStage(DenoisingStage):
model_video, model_audio = self._combine_cfg_parallel_av( model_video, model_audio = self._combine_cfg_parallel_av(
model_video, model_audio, float(batch.guidance_scale), cfg_rank model_video, model_audio, float(batch.guidance_scale), cfg_rank
) )
elif batch.do_classifier_free_guidance: elif do_two_branch_cfg:
model_video_uncond, model_video_text = model_video.chunk(2) model_video_uncond, model_video_text = model_video.chunk(2)
model_audio_uncond, model_audio_text = model_audio.chunk(2) model_audio_uncond, model_audio_text = model_audio.chunk(2)
model_video = model_video_uncond + ( model_video = model_video_uncond + (
@@ -1780,7 +1792,7 @@ class LTX2DenoisingStage(DenoisingStage):
audio_encoder_hidden_states=batch.audio_prompt_embeds[0], audio_encoder_hidden_states=batch.audio_prompt_embeds[0],
encoder_attention_mask=prompt_attention_mask, encoder_attention_mask=prompt_attention_mask,
) )
if batch.do_classifier_free_guidance: if do_two_branch_cfg:
cfg_batch_size = batch_size_local * 2 cfg_batch_size = batch_size_local * 2
model_kwargs_local = self._repeat_ltx2_model_kwargs_batch( model_kwargs_local = self._repeat_ltx2_model_kwargs_batch(
model_kwargs_local, cfg_batch_size model_kwargs_local, cfg_batch_size
@@ -1827,7 +1839,7 @@ class LTX2DenoisingStage(DenoisingStage):
mid_v = mid_v.float() mid_v = mid_v.float()
mid_a = mid_a.float() mid_a = mid_a.float()
if batch.do_classifier_free_guidance: if do_two_branch_cfg:
mid_v_u, mid_v_t = mid_v.chunk(2) mid_v_u, mid_v_t = mid_v.chunk(2)
mid_a_u, mid_a_t = mid_a.chunk(2) mid_a_u, mid_a_t = mid_a.chunk(2)
mid_v = mid_v_u + batch.guidance_scale * (mid_v_t - mid_v_u) mid_v = mid_v_u + batch.guidance_scale * (mid_v_t - mid_v_u)
@@ -2202,20 +2214,43 @@ class LTX2DenoisingStage(DenoisingStage):
) * ctx.denoise_mask.squeeze(-1) ) * ctx.denoise_mask.squeeze(-1)
if stage1_cfg_parallel: if stage1_cfg_parallel:
guided_video = self._ltx2_combine_guided_x0_parallel( guided_video, guided_audio = (
latents=video_latents, self._ltx2_combine_guided_x0_parallel_av(
local_velocities={ video_latents=video_latents,
audio_latents=audio_latents,
local_video_velocities={
name: output[0] for name, output in pass_outputs.items() name: output[0] for name, output in pass_outputs.items()
}, },
sigma=video_sigma_for_x0, local_audio_velocities={
cfg_scale=float(stage1_guider_params["video_cfg_scale"]), name: output[1] for name, output in pass_outputs.items()
stg_scale=float(stage1_guider_params["video_stg_scale"]), },
rescale_scale=float( video_sigma=video_sigma_for_x0,
audio_sigma=audio_sigma_for_x0,
video_cfg_scale=float(
stage1_guider_params["video_cfg_scale"]
),
video_stg_scale=float(
stage1_guider_params["video_stg_scale"]
),
video_rescale_scale=float(
stage1_guider_params["video_rescale_scale"] stage1_guider_params["video_rescale_scale"]
), ),
modality_scale=float( video_modality_scale=float(
stage1_guider_params["video_modality_scale"] stage1_guider_params["video_modality_scale"]
), ),
audio_cfg_scale=float(
stage1_guider_params["audio_cfg_scale"]
),
audio_stg_scale=float(
stage1_guider_params["audio_stg_scale"]
),
audio_rescale_scale=float(
stage1_guider_params["audio_rescale_scale"]
),
audio_modality_scale=float(
stage1_guider_params["audio_modality_scale"]
),
)
) )
if video_skip and ctx.last_denoised_video is not None: if video_skip and ctx.last_denoised_video is not None:
denoised_video_local = ctx.last_denoised_video denoised_video_local = ctx.last_denoised_video
@@ -2224,21 +2259,6 @@ class LTX2DenoisingStage(DenoisingStage):
if update_skip_cache: if update_skip_cache:
ctx.last_denoised_video = guided_video ctx.last_denoised_video = guided_video
guided_audio = self._ltx2_combine_guided_x0_parallel(
latents=audio_latents,
local_velocities={
name: output[1] for name, output in pass_outputs.items()
},
sigma=audio_sigma_for_x0,
cfg_scale=float(stage1_guider_params["audio_cfg_scale"]),
stg_scale=float(stage1_guider_params["audio_stg_scale"]),
rescale_scale=float(
stage1_guider_params["audio_rescale_scale"]
),
modality_scale=float(
stage1_guider_params["audio_modality_scale"]
),
)
if audio_skip and ctx.last_denoised_audio is not None: if audio_skip and ctx.last_denoised_audio is not None:
denoised_audio_local = ctx.last_denoised_audio denoised_audio_local = ctx.last_denoised_audio
else: else:
@@ -614,9 +614,15 @@ class ServerArgs(DisaggArgsMixin):
if component_name is None: if component_name is None:
continue continue
key = component_name.replace("-", "_") key = component_name.replace("-", "_")
backend = self.component_attention_backends.get(key) fallback_keys = [key]
if key.endswith("_2"):
# Secondary two-stage components inherit the base component
# backend unless explicitly overridden.
fallback_keys.append(key[:-2])
for backend_key in fallback_keys:
backend = self.component_attention_backends.get(backend_key)
if backend is not None: if backend is not None:
return AttentionBackendEnum[backend.upper()], key return AttentionBackendEnum[backend.upper()], backend_key
return None, None return None, None
def _adjust_warmup(self): def _adjust_warmup(self):
@@ -110,15 +110,15 @@
"mean_abs_diff_threshold": 10.0 "mean_abs_diff_threshold": 10.0
}, },
"ltx_2.3_one_stage_ti2v": { "ltx_2.3_one_stage_ti2v": {
"clip_threshold": 0.64, "clip_threshold": 0.84,
"ssim_threshold": 0.42, "ssim_threshold": 0.78,
"psnr_threshold": 8.8, "psnr_threshold": 20.5,
"mean_abs_diff_threshold": 59.0 "mean_abs_diff_threshold": 11.0
}, },
"ltx_2.3_two_stage_t2v_2gpus": { "ltx_2.3_two_stage_t2v_2gpus": {
"clip_threshold": 0.79, "clip_threshold": 0.79,
"ssim_threshold": 0.12, "ssim_threshold": 0.12,
"psnr_threshold": 12.1, "psnr_threshold": 16.0,
"mean_abs_diff_threshold": 51.0 "mean_abs_diff_threshold": 51.0
}, },
"wan2_1_t2v_1.3b_teacache_enabled": { "wan2_1_t2v_1.3b_teacache_enabled": {
@@ -254,10 +254,10 @@
"mean_abs_diff_threshold": 45.0 "mean_abs_diff_threshold": 45.0
}, },
"ltx_2_3_two_stage_ti2v_2gpus": { "ltx_2_3_two_stage_ti2v_2gpus": {
"clip_threshold": 0.92, "clip_threshold": 0.55,
"ssim_threshold": 0.58, "ssim_threshold": 0.29,
"psnr_threshold": 17.5, "psnr_threshold": 11.7,
"mean_abs_diff_threshold": 20.0 "mean_abs_diff_threshold": 47.0
} }
}, },
"default_clip_threshold_image": 0.92, "default_clip_threshold_image": 0.92,
@@ -601,8 +601,7 @@ TWO_GPU_CASES = [
"ltx_2_two_stage_t2v", "ltx_2_two_stage_t2v",
DiffusionServerArgs( DiffusionServerArgs(
model_path="Lightricks/LTX-2", model_path="Lightricks/LTX-2",
ulysses_degree=2, cfg_parallel=True,
dit_layerwise_offload=True,
extras=["--pipeline-class-name LTX2TwoStagePipeline"], extras=["--pipeline-class-name LTX2TwoStagePipeline"],
), ),
T2V_sampling_params, T2V_sampling_params,
@@ -613,7 +612,7 @@ TWO_GPU_CASES = [
model_path="Lightricks/LTX-2.3", model_path="Lightricks/LTX-2.3",
cfg_parallel=True, cfg_parallel=True,
extras=[ extras=[
"--pipeline-class-name LTX2TwoStagePipeline --ltx2-two-stage-device-mode original" "--pipeline-class-name LTX2TwoStagePipeline --ltx2-two-stage-device-mode original",
], ],
), ),
TI2V_sampling_params, TI2V_sampling_params,
@@ -634,10 +633,10 @@ TWO_GPU_CASES = [
cfg_parallel=True, cfg_parallel=True,
extras=[ extras=[
"--pipeline-class-name LTX2TwoStagePipeline", "--pipeline-class-name LTX2TwoStagePipeline",
"--ltx2-two-stage-device-mode original", "--component-attention-backends transformer=fa",
], ],
), ),
T2V_sampling_params, DiffusionSamplingParams(prompt=T2V_PROMPT, extras={"seed": 42}),
run_component_accuracy_check=False, run_component_accuracy_check=False,
), ),
# I2V LoRA test case # I2V LoRA test case
@@ -708,7 +707,7 @@ TWO_GPU_CASES = [
"ltx_2.3_one_stage_ti2v", "ltx_2.3_one_stage_ti2v",
DiffusionServerArgs( DiffusionServerArgs(
model_path="Lightricks/LTX-2.3", model_path="Lightricks/LTX-2.3",
ulysses_degree=2, cfg_parallel=True,
), ),
TI2V_sampling_params, TI2V_sampling_params,
run_component_accuracy_check=False, run_component_accuracy_check=False,
@@ -1096,68 +1096,70 @@
}, },
"ltx_2_two_stage_t2v": { "ltx_2_two_stage_t2v": {
"stages_ms": { "stages_ms": {
"InputValidationStage": 0.1, "InputValidationStage": 0.03,
"TextEncodingStage": 1830.33, "TextEncodingStage": 1463.27,
"LTX2TextConnectorStage": 9.61, "LTX2TextConnectorStage": 12.85,
"LTX2HalveResolutionStage": 0.06, "LTX2HalveResolutionStage": 0.11,
"LTX2LoRASwitchStage": 11578.48, "LTX2LoRASwitchStage": 325.46,
"LTX2SigmaPreparationStage": 0.18, "LTX2SigmaPreparationStage": 0.25,
"TimestepPreparationStage": 19.37, "TimestepPreparationStage": 7.85,
"LTX2AVLatentPreparationStage": 0.23, "LTX2AVLatentPreparationStage": 0.34,
"LTX2AVDenoisingStage": 53177.49, "LTX2ImageEncodingStage": 0.02,
"LTX2UpsampleStage": 2104.17, "LTX2AVDenoisingStage": 7744.44,
"LTX2RefinementStage": 3947.13, "LTX2UpsampleStage": 2.98,
"LTX2AVDecodingStage": 332.07 "LTX2RefinementStage": 666.08,
"LTX2AVDecodingStage": 338.87,
"per_frame_generation": null
}, },
"denoise_step_ms": { "denoise_step_ms": {
"0": 1186.27, "0": 165.1,
"1": 1331.86, "1": 309.19,
"2": 1330.41, "2": 166.63,
"3": 1331.28, "3": 175.53,
"4": 1331.5, "4": 158.77,
"5": 1331.45, "5": 191.1,
"6": 1331.79, "6": 203.59,
"7": 1331.59, "7": 202.98,
"8": 1331.55, "8": 205.09,
"9": 1331.55, "9": 195.92,
"10": 1331.51, "10": 226.96,
"11": 1331.34, "11": 203.85,
"12": 1331.48, "12": 190.75,
"13": 1331.38, "13": 192.07,
"14": 1331.74, "14": 193.85,
"15": 1331.84, "15": 191.34,
"16": 1331.09, "16": 193.56,
"17": 1331.79, "17": 192.05,
"18": 1332.34, "18": 189.53,
"19": 1337.7, "19": 191.61,
"20": 1337.53, "20": 187.52,
"21": 1334.48, "21": 192.57,
"22": 1334.87, "22": 190.67,
"23": 1333.28, "23": 189.47,
"24": 1333.15, "24": 187.38,
"25": 1333.82, "25": 190.22,
"26": 1333.55, "26": 196.7,
"27": 1339.35, "27": 185.05,
"28": 1336.96, "28": 189.59,
"29": 1335.25, "29": 209.85,
"30": 1331.8, "30": 194.47,
"31": 1339.52, "31": 189.43,
"32": 1334.1, "32": 189.58,
"33": 1331.96, "33": 188.41,
"34": 1331.78, "34": 198.11,
"35": 1332.5, "35": 188.45,
"36": 1331.3, "36": 187.06,
"37": 1331.75, "37": 188.65,
"38": 1331.94, "38": 200.22,
"39": 1331.84, "39": 156.46,
"40": 1278.82, "40": 220.06,
"41": 1330.68, "41": 223.51,
"42": 1331.7 "42": 221.07
}, },
"expected_e2e_ms": 73463.94, "expected_e2e_ms": 10601.1,
"expected_avg_denoise_ms": 1328.22, "expected_avg_denoise_ms": 195.04,
"expected_median_denoise_ms": 1331.79, "expected_median_denoise_ms": 191.34,
"estimated_full_test_time_s": 133.1 "estimated_full_test_time_s": 345.4
}, },
"wan2_2_ti2v_5b": { "wan2_2_ti2v_5b": {
"stages_ms": { "stages_ms": {
@@ -169,3 +169,6 @@ def test_save_consistency_failure_artifact(tmp_path, monkeypatch):
assert artifact_path.suffix == ".png" assert artifact_path.suffix == ".png"
assert (tmp_path / "consistency_failures" / "summary.json").exists() assert (tmp_path / "consistency_failures" / "summary.json").exists()
assert (tmp_path / "consistency_failures" / "index.html").exists() assert (tmp_path / "consistency_failures" / "index.html").exists()
assert (
tmp_path / "consistency_failures" / "generated" / "unit_image_fail_1gpu.png"
).exists()
@@ -50,12 +50,12 @@ SGL_TEST_FILES_CONSISTENCY_GT_BASES = (
SGL_TEST_FILES_OFFICIAL_CONSISTENCY_GT_BASE, SGL_TEST_FILES_OFFICIAL_CONSISTENCY_GT_BASE,
SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE, SGL_TEST_FILES_SGLANG_CONSISTENCY_GT_BASE,
) )
# Keep non-comparable LTX CI scenarios on sglang_generated rather than hiding # LTX cases listed here compare against official-generated GT.
# remaining semantic gaps behind very loose official thresholds.
SGL_TEST_FILES_OFFICIAL_CONSISTENCY_GT_CASES = frozenset( SGL_TEST_FILES_OFFICIAL_CONSISTENCY_GT_CASES = frozenset(
{ {
"ltx_2.3_one_stage_ti2v", "ltx_2.3_one_stage_ti2v",
"ltx_2.3_two_stage_t2v_2gpus", "ltx_2.3_two_stage_t2v_2gpus",
"ltx_2_3_two_stage_ti2v_2gpus",
} }
) )
CONSISTENCY_THRESHOLD_JSON_PATH = ( CONSISTENCY_THRESHOLD_JSON_PATH = (
@@ -1458,6 +1458,7 @@ def _consistency_failure_record(
is_video: bool, is_video: bool,
output_format: str | None, output_format: str | None,
image_name: str, image_name: str,
generated_files: list[str],
gt_remote_files: list[tuple[str, str]] | None, gt_remote_files: list[tuple[str, str]] | None,
) -> dict[str, Any]: ) -> dict[str, Any]:
return { return {
@@ -1466,6 +1467,7 @@ def _consistency_failure_record(
"is_video": is_video, "is_video": is_video,
"output_format": output_format, "output_format": output_format,
"comparison_png": image_name, "comparison_png": image_name,
"generated_files": generated_files,
"metrics": { "metrics": {
"min_clip_similarity": _json_metric_value(result.min_similarity), "min_clip_similarity": _json_metric_value(result.min_similarity),
"min_ssim": _json_metric_value(result.min_ssim), "min_ssim": _json_metric_value(result.min_ssim),
@@ -1499,6 +1501,36 @@ def _consistency_failure_record(
} }
def _save_generated_artifact_images(
out_dir: Path,
case_id: str,
num_gpus: int,
output_frames: list[np.ndarray],
is_video: bool,
output_format: str | None,
) -> list[str]:
generated_dir = out_dir / "generated"
generated_dir.mkdir(parents=True, exist_ok=True)
safe_case_id = _safe_artifact_name(case_id)
if is_video:
suffixes = ("frame_0", "frame_mid", "frame_last")
filenames = [
f"{safe_case_id}_{num_gpus}gpu_{suffix}.png"
for suffix in suffixes[: len(output_frames)]
]
else:
ext = output_format_to_ext(output_format)
filenames = [f"{safe_case_id}_{num_gpus}gpu.{ext}"]
generated_files = []
for frame, filename in zip(output_frames, filenames):
path = generated_dir / filename
Image.fromarray(_ensure_rgb_uint8_image(frame)).save(path)
generated_files.append(str(path.relative_to(out_dir)))
return generated_files
def _write_consistency_failure_index( def _write_consistency_failure_index(
out_dir: Path, out_dir: Path,
records: list[dict[str, Any]], records: list[dict[str, Any]],
@@ -1508,6 +1540,15 @@ def _write_consistency_failure_index(
case_id = html.escape(record["case_id"]) case_id = html.escape(record["case_id"])
png = html.escape(record["comparison_png"]) png = html.escape(record["comparison_png"])
metrics = record["metrics"] metrics = record["metrics"]
generated_links = "".join(
f'<li><a href="{html.escape(path)}">{html.escape(path)}</a></li>'
for path in record.get("generated_files", [])
)
generated_html = (
f"<p>Generated images:</p><ul>{generated_links}</ul>"
if generated_links
else ""
)
sections.append( sections.append(
"<section>" "<section>"
f"<h2>{case_id} ({record['num_gpus']} GPU)</h2>" f"<h2>{case_id} ({record['num_gpus']} GPU)</h2>"
@@ -1518,6 +1559,7 @@ def _write_consistency_failure_index(
f"mean_abs_diff={metrics['max_mean_abs_diff']}" f"mean_abs_diff={metrics['max_mean_abs_diff']}"
"</p>" "</p>"
f'<img src="{png}" alt="{case_id} comparison">' f'<img src="{png}" alt="{case_id} comparison">'
f"{generated_html}"
"</section>" "</section>"
) )
@@ -1529,6 +1571,7 @@ def _write_consistency_failure_index(
"section{margin:0 0 28px;padding:16px;background:white;border:1px solid #ddd;border-radius:6px}" "section{margin:0 0 28px;padding:16px;background:white;border:1px solid #ddd;border-radius:6px}"
"h2{font-size:18px;margin:0 0 8px}" "h2{font-size:18px;margin:0 0 8px}"
"p{margin:0 0 12px;color:#444}" "p{margin:0 0 12px;color:#444}"
"ul{margin:0 0 12px;padding-left:20px}"
"img{max-width:100%;height:auto;border:1px solid #ddd}" "img{max-width:100%;height:auto;border:1px solid #ddd}"
"</style></head><body>" "</style></head><body>"
"<h1>Diffusion consistency failures</h1>" + "".join(sections) + "</body></html>" "<h1>Diffusion consistency failures</h1>" + "".join(sections) + "</body></html>"
@@ -1566,6 +1609,15 @@ def save_consistency_failure_artifact(
) )
comparison.save(image_path) comparison.save(image_path)
generated_files = _save_generated_artifact_images(
out_dir=out_dir,
case_id=case_id,
num_gpus=num_gpus,
output_frames=output_frames,
is_video=is_video,
output_format=output_format,
)
record = _consistency_failure_record( record = _consistency_failure_record(
case_id=case_id, case_id=case_id,
num_gpus=num_gpus, num_gpus=num_gpus,
@@ -1573,6 +1625,7 @@ def save_consistency_failure_artifact(
is_video=is_video, is_video=is_video,
output_format=output_format, output_format=output_format,
image_name=image_name, image_name=image_name,
generated_files=generated_files,
gt_remote_files=gt_remote_files, gt_remote_files=gt_remote_files,
) )
case_json_path = out_dir / f"{safe_case_id}.json" case_json_path = out_dir / f"{safe_case_id}.json"