Skip to content

ltx2

LTX-2 transformer implementation

Classes

fastvideo.models.dits.ltx2.AdaLayerNormSingle

AdaLayerNormSingle(embedding_dim: int, embedding_coefficient: int = 6)

Bases: Module

AdaLN-single modulation that emits scale/shift/gates for LTX-2.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, embedding_dim: int, embedding_coefficient: int = 6):
    super().__init__()
    self.emb = PixArtAlphaCombinedTimestepSizeEmbeddings(
        embedding_dim,
        size_emb_dim=embedding_dim // 3,
    )
    self.silu = torch.nn.SiLU()
    self.linear = torch.nn.Linear(embedding_dim, embedding_coefficient * embedding_dim, bias=True)

fastvideo.models.dits.ltx2.AudioLatentPatchifier

AudioLatentPatchifier(patch_size: int, sample_rate: int, hop_length: int, audio_latent_downsample_factor: int, is_causal: bool = True, shift: int = 0)

Patchify/unpatchify audio latents and compute timing bounds.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    patch_size: int,
    sample_rate: int,
    hop_length: int,
    audio_latent_downsample_factor: int,
    is_causal: bool = True,
    shift: int = 0,
) -> None:
    self.hop_length = hop_length
    self.sample_rate = sample_rate
    self.audio_latent_downsample_factor = audio_latent_downsample_factor
    self.is_causal = is_causal
    self.shift = shift
    self._patch_size = (1, patch_size, patch_size)

Methods:

fastvideo.models.dits.ltx2.AudioLatentPatchifier.get_patch_grid_bounds
get_patch_grid_bounds(output_shape: AudioLatentShape, device: Optional[device] = None) -> Tensor

Get patch grid bounds for audio RoPE computation.

Parameters:

Name Type Description Default
output_shape AudioLatentShape

Shape of the audio latent tensor

required
device Optional[device]

Device to create tensors on

None
Source code in fastvideo/models/dits/ltx2.py
def get_patch_grid_bounds(
    self,
    output_shape: AudioLatentShape,
    device: Optional[torch.device] = None,
) -> torch.Tensor:
    """Get patch grid bounds for audio RoPE computation.

    Args:
        output_shape: Shape of the audio latent tensor
        device: Device to create tensors on
    """
    start_timings = self._get_audio_latent_time_in_sec(
        self.shift,
        output_shape.frames + self.shift,
        torch.float32,
        device,
    )
    start_timings = start_timings.unsqueeze(0).expand(output_shape.batch,
                                                      -1).unsqueeze(1)

    end_timings = self._get_audio_latent_time_in_sec(
        self.shift + 1,
        output_shape.frames + self.shift + 1,
        torch.float32,
        device,
    )
    end_timings = end_timings.unsqueeze(0).expand(output_shape.batch,
                                                  -1).unsqueeze(1)

    return torch.stack([start_timings, end_timings], dim=-1)

fastvideo.models.dits.ltx2.AudioLatentShape

Bases: tuple

Helper for (B, C, T, F) audio latent shapes.

fastvideo.models.dits.ltx2.BasicAVTransformerBlock

BasicAVTransformerBlock(idx: int, video: TransformerConfig | None = None, audio: TransformerConfig | None = None, rope_type: LTXRopeType = INTERLEAVED, norm_eps: float = 1e-06, use_distributed_attention: bool = False, cross_attention_adaln: bool = False, stg_block_idx: int = 29, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

LTX-2 transformer block (audio/video + cross attention + FFN).

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    idx: int,
    video: TransformerConfig | None = None,
    audio: TransformerConfig | None = None,
    rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
    norm_eps: float = 1e-6,
    use_distributed_attention: bool = False,
    cross_attention_adaln: bool = False,
    stg_block_idx: int = 29,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
):
    super().__init__()
    self.idx = idx
    self.use_distributed_attention = use_distributed_attention
    # LTX-2.3 cross-attention AdaLN + per-sample STG (defaults reproduce 2.0).
    self.cross_attention_adaln = cross_attention_adaln
    self.stg_block_idx = stg_block_idx

    # Choose attention class based on SP mode
    # Self-attention and audio-video cross-attention use DistributedAttention when SP > 1
    # Text cross-attention uses LocalAttention (text embeddings are replicated)
    SelfAttnCls = LTXDistributedSelfAttention if use_distributed_attention else LTXSelfAttention
    CrossAttnCls = LTXSelfAttention  # Text cross-attention is always local
    video_self_attn_backends = (
        AttentionBackendEnum.FLASH_ATTN,
        AttentionBackendEnum.TORCH_SDPA,
        AttentionBackendEnum.VIDEO_SPARSE_ATTN,
    )
    dense_attn_backends = (
        AttentionBackendEnum.FLASH_ATTN,
        AttentionBackendEnum.TORCH_SDPA,
    )

    if video is not None:
        # Video self-attention - use distributed when SP > 1
        self.attn1 = SelfAttnCls(
            query_dim=video.dim,
            context_dim=None,
            heads=video.heads,
            dim_head=video.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=video_self_attn_backends,
            apply_gated_attention=video.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.attn1",
        )
        # Text cross-attention - always local (text is replicated)
        self.attn2 = CrossAttnCls(
            query_dim=video.dim,
            context_dim=video.context_dim,
            heads=video.heads,
            dim_head=video.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=dense_attn_backends,
            apply_gated_attention=video.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.attn2",
        )
        self.ff = FeedForward(
            video.dim,
            dim_out=video.dim,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}",
        )
        # 6 rows for LTX-2.0; 9 rows when cross_attention_adaln adds the
        # text cross-attention shift/scale/gate.
        video_sst_size = adaln_embedding_coefficient(cross_attention_adaln)
        self.scale_shift_table = torch.nn.Parameter(
            torch.empty(video_sst_size, video.dim))

    if audio is not None:
        # Audio self-attention - use distributed when SP > 1
        self.audio_attn1 = SelfAttnCls(
            query_dim=audio.dim,
            context_dim=None,
            heads=audio.heads,
            dim_head=audio.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=dense_attn_backends,
            apply_gated_attention=audio.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.audio_attn1",
        )
        # Text cross-attention - always local (text is replicated)
        self.audio_attn2 = CrossAttnCls(
            query_dim=audio.dim,
            context_dim=audio.context_dim,
            heads=audio.heads,
            dim_head=audio.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=dense_attn_backends,
            apply_gated_attention=audio.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.audio_attn2",
        )
        self.audio_ff = FeedForward(
            audio.dim,
            dim_out=audio.dim,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.audio",
        )
        audio_sst_size = adaln_embedding_coefficient(cross_attention_adaln)
        self.audio_scale_shift_table = torch.nn.Parameter(
            torch.empty(audio_sst_size, audio.dim))

    if audio is not None and video is not None:
        # Audio-to-video cross-attention
        # Uses local attention - context is gathered from all SP ranks in forward()
        self.audio_to_video_attn = LTXSelfAttention(
            query_dim=video.dim,
            context_dim=audio.dim,
            heads=audio.heads,
            dim_head=audio.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=dense_attn_backends,
            apply_gated_attention=video.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.audio_to_video_attn",
        )
        # Video-to-audio cross-attention
        # Uses local attention - context is gathered from all SP ranks in forward()
        self.video_to_audio_attn = LTXSelfAttention(
            query_dim=audio.dim,
            context_dim=video.dim,
            heads=audio.heads,
            dim_head=audio.d_head,
            norm_eps=norm_eps,
            rope_type=rope_type,
            supported_attention_backends=dense_attn_backends,
            apply_gated_attention=audio.apply_gated_attention,
            quant_config=quant_config,
            prefix=f"{prefix}.blocks.{idx}.video_to_audio_attn",
        )
        self.scale_shift_table_a2v_ca_audio = torch.nn.Parameter(torch.empty(5, audio.dim))
        self.scale_shift_table_a2v_ca_video = torch.nn.Parameter(torch.empty(5, video.dim))

    # LTX-2.3 cross-attention AdaLN prompt scale/shift tables (only created
    # when cross_attention_adaln is enabled, so absent for LTX-2.0).
    if self.cross_attention_adaln and video is not None:
        self.prompt_scale_shift_table = torch.nn.Parameter(
            torch.empty(2, video.dim))
    if self.cross_attention_adaln and audio is not None:
        self.audio_prompt_scale_shift_table = torch.nn.Parameter(
            torch.empty(2, audio.dim))

    self.norm_eps = norm_eps

Methods:

fastvideo.models.dits.ltx2.BasicAVTransformerBlock.forward
forward(video: TransformerArgs | None, audio: TransformerArgs | None, video_original_seq_len: int | None = None, audio_original_seq_len: int | None = None, skip_cross_modal_attn: bool = False, skip_video_self_attn: bool | Tensor = False, skip_audio_self_attn: bool | Tensor = False) -> tuple[TransformerArgs | None, TransformerArgs | None]

Forward pass for transformer block.

Parameters:

Name Type Description Default
video TransformerArgs | None

Video transformer args

required
audio TransformerArgs | None

Audio transformer args

required
video_original_seq_len int | None

Original unpadded video sequence length

None
audio_original_seq_len int | None

Original unpadded audio sequence length

None
skip_cross_modal_attn bool

If True, skip A2V and V2A cross-modal attention (used for the modality-isolated CFG pass).

False
skip_video_self_attn bool | Tensor

If True, skip video self-attention in this block (used for STG perturbed pass).

False
skip_audio_self_attn bool | Tensor

If True, skip audio self-attention in this block (used for STG perturbed pass).

False
Source code in fastvideo/models/dits/ltx2.py
def forward(
    self,
    video: TransformerArgs | None,
    audio: TransformerArgs | None,
    video_original_seq_len: int | None = None,
    audio_original_seq_len: int | None = None,
    skip_cross_modal_attn: bool = False,
    skip_video_self_attn: bool | torch.Tensor = False,
    skip_audio_self_attn: bool | torch.Tensor = False,
) -> tuple[TransformerArgs | None, TransformerArgs | None]:
    """Forward pass for transformer block.

    Args:
        video: Video transformer args
        audio: Audio transformer args
        video_original_seq_len: Original unpadded video sequence length
        audio_original_seq_len: Original unpadded audio sequence length
        skip_cross_modal_attn: If True, skip A2V and V2A cross-modal
            attention (used for the modality-isolated CFG pass).
        skip_video_self_attn: If True, skip video self-attention
            in this block (used for STG perturbed pass).
        skip_audio_self_attn: If True, skip audio self-attention
            in this block (used for STG perturbed pass).
    """
    vx = video.x if video is not None else None
    ax = audio.x if audio is not None else None

    run_vx = video is not None and video.enabled and vx.numel() > 0
    run_ax = audio is not None and audio.enabled and ax.numel() > 0
    run_a2v = run_vx and run_ax
    run_v2a = run_ax and run_vx

    def _build_attn_keep_mask(
        values: torch.Tensor,
        skip_flag: bool | torch.Tensor,
    ) -> torch.Tensor:
        """STG keep-mask for self-attention.

        Returns a per-sample multiplier (1 keep, 0 perturb) broadcastable
        over ``values``. Only the configured ``stg_block_idx`` is perturbed.
        Supports both the LTX-2.0 bool flag and an LTX-2.3 per-sample
        tensor ``[B]`` (1/True == perturb). When skip_flag is False/0 the
        multiplier is all ones, reproducing the un-skipped LTX-2.0 path.
        """
        bsz = values.shape[0]
        keep = torch.ones((bsz, ), device=values.device, dtype=values.dtype)
        if self.idx == self.stg_block_idx:
            if torch.is_tensor(skip_flag):
                if skip_flag.ndim == 0:
                    perturb = skip_flag.reshape(1).expand(bsz)
                else:
                    if skip_flag.shape[0] != bsz:
                        raise ValueError(
                            "Per-sample STG mask batch size mismatch: "
                            f"got {skip_flag.shape[0]}, expected {bsz}")
                    perturb = skip_flag.reshape(bsz)
                keep = 1.0 - perturb.to(device=values.device,
                                        dtype=values.dtype)
            elif bool(skip_flag):
                keep.zero_()
        return keep.view(bsz, *([1] * (values.ndim - 1)))

    if run_vx:
        vshift_msa, vscale_msa, vgate_msa = self.get_ada_values(
            self.scale_shift_table, vx.shape[0], video.timesteps, slice(0, 3)
        )
        norm_vx = _rms_norm_dispatch(vx, eps=self.norm_eps) * (
            1 + vscale_msa) + vshift_msa
        video_self_attn_mask = _build_attn_keep_mask(vx, skip_video_self_attn)
        if self.use_distributed_attention:
            vx_attn1_out = self.attn1(
                norm_vx,
                pe=video.positional_embeddings,
                original_seq_len=video_original_seq_len,
            )
        else:
            vx_attn1_out = self.attn1(norm_vx, pe=video.positional_embeddings)
        vx = vx + vx_attn1_out * vgate_msa * video_self_attn_mask
        # Text cross-attention: no SP mask needed (text is replicated).
        vx = vx + self._apply_text_cross_attention(
            x=vx,
            context=video.context,
            attn=self.attn2,
            scale_shift_table=self.scale_shift_table,
            prompt_scale_shift_table=getattr(
                self, "prompt_scale_shift_table", None),
            timestep=video.timesteps,
            prompt_timestep=video.prompt_timestep,
            context_mask=video.context_mask,
        )

    if run_ax:
        ashift_msa, ascale_msa, agate_msa = self.get_ada_values(
            self.audio_scale_shift_table, ax.shape[0], audio.timesteps, slice(0, 3)
        )
        norm_ax = _rms_norm_dispatch(ax, eps=self.norm_eps) * (
            1 + ascale_msa) + ashift_msa
        audio_self_attn_mask = _build_attn_keep_mask(ax, skip_audio_self_attn)
        if self.use_distributed_attention:
            ax_attn1_out = self.audio_attn1(
                norm_ax,
                pe=audio.positional_embeddings,
                original_seq_len=audio_original_seq_len,
            )
        else:
            ax_attn1_out = self.audio_attn1(norm_ax, pe=audio.positional_embeddings)
        ax = ax + ax_attn1_out * agate_msa * audio_self_attn_mask
        # Text cross-attention: no SP mask needed (text is replicated).
        ax = ax + self._apply_text_cross_attention(
            x=ax,
            context=audio.context,
            attn=self.audio_attn2,
            scale_shift_table=self.audio_scale_shift_table,
            prompt_scale_shift_table=getattr(
                self, "audio_prompt_scale_shift_table", None),
            timestep=audio.timesteps,
            prompt_timestep=audio.prompt_timestep,
            context_mask=audio.context_mask,
        )

    if (run_a2v or run_v2a) and not skip_cross_modal_attn:
        vx_norm3 = _rms_norm_dispatch(vx, eps=self.norm_eps)
        ax_norm3 = _rms_norm_dispatch(ax, eps=self.norm_eps)

        (
            scale_ca_audio_hidden_states_a2v,
            shift_ca_audio_hidden_states_a2v,
            scale_ca_audio_hidden_states_v2a,
            shift_ca_audio_hidden_states_v2a,
            gate_out_v2a,
        ) = self.get_av_ca_ada_values(
            self.scale_shift_table_a2v_ca_audio,
            ax.shape[0],
            audio.cross_scale_shift_timestep,
            audio.cross_gate_timestep,
        )

        (
            scale_ca_video_hidden_states_a2v,
            shift_ca_video_hidden_states_a2v,
            scale_ca_video_hidden_states_v2a,
            shift_ca_video_hidden_states_v2a,
            gate_out_a2v,
        ) = self.get_av_ca_ada_values(
            self.scale_shift_table_a2v_ca_video,
            vx.shape[0],
            video.cross_scale_shift_timestep,
            video.cross_gate_timestep,
        )

        if run_a2v:
            vx_scaled = vx_norm3 * (1 + scale_ca_video_hidden_states_a2v) + shift_ca_video_hidden_states_a2v
            ax_scaled = ax_norm3 * (1 + scale_ca_audio_hidden_states_a2v) + shift_ca_audio_hidden_states_a2v
            # Audio-to-video cross-attention: Q from video (sharded), K/V from audio
            # For SP, gather audio context from all ranks before cross-attention
            if self.use_distributed_attention:
                # Gather audio context from all SP ranks
                ax_context = sequence_model_parallel_all_gather(ax_scaled, dim=1)
                # Video Q: video tokens divide evenly (adjusted upstream), so simple slicing works
                video_q_pe = None
                if video.cross_positional_embeddings is not None:
                    sp_rank = get_sp_parallel_rank()
                    local_seq_len = vx_scaled.shape[1]
                    pe_tuple = video.cross_positional_embeddings
                    start_idx = sp_rank * local_seq_len
                    end_idx = start_idx + local_seq_len
                    video_q_pe = tuple(pe[:, :, start_idx:end_idx, :] for pe in pe_tuple)
                # Audio K: gathered context may have padding, trim to original length
                audio_k_pe = audio.cross_positional_embeddings
                if audio_k_pe is not None:
                    original_audio_len = audio_k_pe[0].shape[2]
                    ax_context = ax_context[:, :original_audio_len, :]
                vx = vx + (
                    self.audio_to_video_attn(
                        vx_scaled,
                        context=ax_context,
                        pe=video_q_pe,
                        k_pe=audio_k_pe,
                    )
                    * gate_out_a2v
                )
            else:
                ax_context = ax_scaled
                video_q_pe = video.cross_positional_embeddings
                audio_k_pe = audio.cross_positional_embeddings
                vx = vx + (
                    self.audio_to_video_attn(
                        vx_scaled,
                        context=ax_context,
                        pe=video_q_pe,
                        k_pe=audio_k_pe,
                    )
                    * gate_out_a2v
                )

        if run_v2a:
            ax_scaled = ax_norm3 * (1 + scale_ca_audio_hidden_states_v2a) + shift_ca_audio_hidden_states_v2a
            vx_scaled = vx_norm3 * (1 + scale_ca_video_hidden_states_v2a) + shift_ca_video_hidden_states_v2a
            # Video-to-audio cross-attention: Q from audio, K/V from video
            # For SP, gather both modalities and run cross-attention on full sequences
            # (audio is short, so no need to keep it sharded for cross-attention)
            if self.use_distributed_attention:
                # Gather both modalities to full sequences
                ax_full = sequence_model_parallel_all_gather(ax_scaled, dim=1)
                vx_context = sequence_model_parallel_all_gather(vx_scaled, dim=1)
                # Use full positional embeddings directly
                audio_q_pe = audio.cross_positional_embeddings
                video_k_pe = video.cross_positional_embeddings
                # Trim gathered tensors to original lengths (may have padding from sharding)
                if audio_q_pe is not None:
                    original_audio_len = audio_q_pe[0].shape[2]
                    ax_full = ax_full[:, :original_audio_len, :]
                if video_k_pe is not None:
                    original_video_len = video_k_pe[0].shape[2]
                    vx_context = vx_context[:, :original_video_len, :]
                # Run cross-attention on full audio
                v2a_out_full = self.video_to_audio_attn(
                    ax_full,
                    context=vx_context,
                    pe=audio_q_pe,
                    k_pe=video_k_pe,
                )
                # Shard the output back to match ax's sharding
                v2a_out, _ = sequence_model_parallel_shard(v2a_out_full, dim=1)
                ax = ax + v2a_out * gate_out_v2a
            else:
                vx_context = vx_scaled
                audio_q_pe = audio.cross_positional_embeddings
                video_k_pe = video.cross_positional_embeddings
                ax = ax + (
                    self.video_to_audio_attn(
                        ax_scaled,
                        context=vx_context,
                        pe=audio_q_pe,
                        k_pe=video_k_pe,
                    )
                    * gate_out_v2a
                )

    if run_vx:
        # slice(3, 6) selects the FFN shift/scale/gate. Identical to
        # slice(3, None) for the 6-row LTX-2.0 table, but correct for the
        # 9-row cross_attention_adaln table (rows 6..9 are the cross-attn).
        vshift_mlp, vscale_mlp, vgate_mlp = self.get_ada_values(
            self.scale_shift_table, vx.shape[0], video.timesteps, slice(3, 6)
        )
        vx_scaled = _rms_norm_dispatch(vx, eps=self.norm_eps) * (
            1 + vscale_mlp) + vshift_mlp
        vx = vx + self.ff(vx_scaled) * vgate_mlp

    if run_ax:
        ashift_mlp, ascale_mlp, agate_mlp = self.get_ada_values(
            self.audio_scale_shift_table, ax.shape[0], audio.timesteps, slice(3, 6)
        )
        ax_scaled = _rms_norm_dispatch(ax, eps=self.norm_eps) * (
            1 + ascale_mlp) + ashift_mlp
        ax = ax + self.audio_ff(ax_scaled) * agate_mlp

    if os.getenv("LTX2_PIPELINE_DEBUG_LOG", "0") == "1":
        video_sum = vx.float().sum().item() if vx is not None else 0.0
        audio_sum = ax.float().sum().item() if ax is not None else 0.0
        _debug_block_log_line(
            f"fastvideo:block={self.idx}:video_sum={video_sum:.6f} "
            f"audio_sum={audio_sum:.6f}"
        )

    # Register FSDP2 backward hooks on output tensors (module-level hooks don't
    # fire for dataclass outputs, so we must hook the tensors directly)
    self._register_fsdp_backward_hooks_on_output(vx, ax)

    return (
        replace(video, x=vx) if video is not None else None,
        replace(audio, x=ax) if audio is not None else None,
    )

fastvideo.models.dits.ltx2.FeedForward

FeedForward(dim: int, dim_out: int, mult: int = 4, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

LTX-2 FFN: GELUApprox -> Identity -> Linear.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    dim: int,
    dim_out: int,
    mult: int = 4,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
) -> None:
    super().__init__()
    inner_dim = int(dim * mult)
    project_in = GELUApprox(
        dim,
        inner_dim,
        quant_config=quant_config,
        prefix=f"{prefix}.ffn",
    )
    project_out = ReplicatedLinear(
        inner_dim,
        dim_out,
        quant_config=quant_config,
        prefix=f"{prefix}.ffn.fc_out",
    )
    self.net = nn.ModuleList([project_in, nn.Identity(), project_out])

fastvideo.models.dits.ltx2.GELUApprox

GELUApprox(in_features: int, out_features: int, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

Linear + tanh-approximate GELU used by LTX-2 FFN.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    in_features: int,
    out_features: int,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
):
    super().__init__()
    self.proj = ReplicatedLinear(
        in_features,
        out_features,
        quant_config=quant_config,
        prefix=f"{prefix}.fc_in",
    )
    self.act = nn.GELU(approximate="tanh")

fastvideo.models.dits.ltx2.LTX2Transformer3DModel

LTX2Transformer3DModel(config: LTX2VideoConfig, hf_config: dict[str, Any])

Bases: BaseDiT

LTX-2 transformer using native FastVideo LTX-2 modules.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, config: LTX2VideoConfig, hf_config: dict[str, Any]):
    super().__init__(config=config, hf_config=hf_config)

    arch = config.arch_config

    # Get SP world size for distributed attention
    sp_world_size = get_sp_world_size()
    use_vsa_backend = os.getenv("FASTVIDEO_ATTENTION_BACKEND",
                                "") == "VIDEO_SPARSE_ATTN"
    use_distributed_attention = sp_world_size > 1 or use_vsa_backend

    # Validate that attention heads are divisible by SP world size
    if sp_world_size > 1:
        assert arch.num_attention_heads % sp_world_size == 0, (
            f"The number of video attention heads ({arch.num_attention_heads}) "
            f"must be divisible by the sequence parallel size ({sp_world_size})"
        )
        assert arch.audio_num_attention_heads % sp_world_size == 0, (
            f"The number of audio attention heads ({arch.audio_num_attention_heads}) "
            f"must be divisible by the sequence parallel size ({sp_world_size})"
        )
        logger.info(
            f"LTX2 sequence parallelism enabled with SP world size {sp_world_size}"
        )
    elif use_vsa_backend:
        logger.info(
            "LTX2 VSA enabled with SP world size 1; using distributed "
            "attention path for VSA compatibility")

    model_type = LTXModelType.AudioVideo
    self.model = LTXModel(
        model_type=model_type,
        num_attention_heads=arch.num_attention_heads,
        attention_head_dim=arch.attention_head_dim,
        in_channels=arch.in_channels,
        out_channels=arch.out_channels,
        num_layers=arch.num_layers,
        cross_attention_dim=arch.cross_attention_dim,
        norm_eps=arch.norm_eps,
        caption_channels=arch.caption_channels,
        positional_embedding_theta=arch.positional_embedding_theta,
        positional_embedding_max_pos=arch.positional_embedding_max_pos,
        timestep_scale_multiplier=arch.timestep_scale_multiplier,
        use_middle_indices_grid=arch.use_middle_indices_grid,
        rope_type=LTXRopeType(arch.rope_type),
        double_precision_rope=arch.double_precision_rope,
        audio_num_attention_heads=arch.audio_num_attention_heads,
        audio_attention_head_dim=arch.audio_attention_head_dim,
        audio_in_channels=arch.audio_in_channels,
        audio_out_channels=arch.audio_out_channels,
        audio_cross_attention_dim=arch.audio_cross_attention_dim,
        audio_positional_embedding_max_pos=arch.audio_positional_embedding_max_pos,
        av_ca_timestep_scale_multiplier=arch.av_ca_timestep_scale_multiplier,
        cross_attention_adaln=arch.cross_attention_adaln,
        caption_proj_before_connector=arch.caption_proj_before_connector,
        apply_gated_attention=arch.apply_gated_attention,
        stg_block_idx=arch.stg_block_idx,
        use_distributed_attention=use_distributed_attention,
        quant_config=config.quant_config,
        prefix=config.prefix,
    )

    self.patchifier = VideoLatentPatchifier(patch_size=arch.patch_size[1])
    self.audio_patchifier = AudioLatentPatchifier(
        patch_size=DEFAULT_LTX2_AUDIO_MEL_BINS,
        sample_rate=DEFAULT_LTX2_AUDIO_SAMPLE_RATE,
        hop_length=DEFAULT_LTX2_AUDIO_HOP_LENGTH,
        audio_latent_downsample_factor=DEFAULT_LTX2_AUDIO_DOWNSAMPLE,
        is_causal=True,
        shift=0,
    )

    self.hidden_size = arch.num_attention_heads * arch.attention_head_dim
    self.num_attention_heads = arch.num_attention_heads
    self.num_channels_latents = arch.num_channels_latents

    if os.getenv("LTX2_DEBUG_DETAIL", "0") == "1":
        detail_path = os.getenv("LTX2_PIPELINE_DEBUG_DETAIL_PATH", "")
        if detail_path:
            self._attach_debug_detail_hooks(detail_path)

fastvideo.models.dits.ltx2.LTXDistributedAttention

LTXDistributedAttention(num_heads: int, head_size: int, rope_type: LTXRopeType, num_kv_heads: int | None = None, softmax_scale: float | None = None, causal: bool = False, supported_attention_backends: tuple[AttentionBackendEnum, ...] | None = None, prefix: str = '', **extra_impl_args)

Bases: DistributedAttention

LTX-2 specialized DistributedAttention that handles LTX-style RoPE internally.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    num_heads: int,
    head_size: int,
    rope_type: LTXRopeType,
    num_kv_heads: int | None = None,
    softmax_scale: float | None = None,
    causal: bool = False,
    supported_attention_backends: tuple[AttentionBackendEnum, ...] | None = None,
    prefix: str = "",
    **extra_impl_args,
) -> None:
    super().__init__(
        num_heads=num_heads,
        head_size=head_size,
        num_kv_heads=num_kv_heads,
        softmax_scale=softmax_scale,
        causal=causal,
        supported_attention_backends=supported_attention_backends,
        prefix=prefix,
        **extra_impl_args,
    )
    self.rope_type = rope_type

Methods:

fastvideo.models.dits.ltx2.LTXDistributedAttention.forward
forward(q: Tensor, k: Tensor, v: Tensor, original_seq_len: int, replicated_q: Tensor | None = None, replicated_k: Tensor | None = None, replicated_v: Tensor | None = None, gate_compress: Tensor | None = None, ltx_freqs_cis: tuple[Tensor, Tensor] | None = None) -> tuple[Tensor, Tensor | None]

Forward pass with LTX-2 style RoPE application.

Parameters:

Name Type Description Default
q Tensor

Query tensor [batch_size, seq_len, num_heads, head_dim]

required
k Tensor

Key tensor [batch_size, seq_len, num_heads, head_dim]

required
v Tensor

Value tensor [batch_size, seq_len, num_heads, head_dim]

required
replicated_q Tensor | None

Replicated query tensor for text tokens

None
replicated_k Tensor | None

Replicated key tensor

None
replicated_v Tensor | None

Replicated value tensor

None
original_seq_len int

Original (unpadded) full sequence length

required
ltx_freqs_cis tuple[Tensor, Tensor] | None

LTX-2 style RoPE (cos, sin) with shape [B, H, T, D]

None

Returns:

Type Description
tuple[Tensor, Tensor | None]

Tuple of output tensor and optional replicated output

Source code in fastvideo/models/dits/ltx2.py
def forward(
    self,
    q: torch.Tensor,
    k: torch.Tensor,
    v: torch.Tensor,
    original_seq_len: int,
    replicated_q: torch.Tensor | None = None,
    replicated_k: torch.Tensor | None = None,
    replicated_v: torch.Tensor | None = None,
    gate_compress: torch.Tensor | None = None,
    ltx_freqs_cis: tuple[torch.Tensor, torch.Tensor] | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
    """Forward pass with LTX-2 style RoPE application.

    Args:
        q: Query tensor [batch_size, seq_len, num_heads, head_dim]
        k: Key tensor [batch_size, seq_len, num_heads, head_dim]
        v: Value tensor [batch_size, seq_len, num_heads, head_dim]
        replicated_q: Replicated query tensor for text tokens
        replicated_k: Replicated key tensor
        replicated_v: Replicated value tensor
        original_seq_len: Original (unpadded) full sequence length
        ltx_freqs_cis: LTX-2 style RoPE (cos, sin) with shape [B, H, T, D]

    Returns:
        Tuple of output tensor and optional replicated output
    """
    assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"
    batch_size, _, num_heads, _ = q.shape
    local_rank = get_sp_parallel_rank()
    world_size = get_sp_world_size()

    forward_context: ForwardContext = get_forward_context()
    ctx_attn_metadata = forward_context.attn_metadata

    use_vsa = self.backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN

    if use_vsa:
        assert (
            replicated_q is None and replicated_k is None
            and replicated_v is None
        ), "Replicated QKV is not supported for VSA now"
        if gate_compress is None:
            raise ValueError(
                "gate_compress must be provided when using VIDEO_SPARSE_ATTN"
            )

        qkvg = torch.cat([q, k, v, gate_compress],
                         dim=0)  # [4*batch, seq_len, heads, head_dim]
        qkvg = sequence_model_parallel_all_to_all_4D(qkvg,
                                                     scatter_dim=2,
                                                     gather_dim=1)
        pad_seq_len = qkvg.shape[1] - original_seq_len
        qkvg = qkvg[:, :original_seq_len, :, :]

        if ltx_freqs_cis is not None:
            cos, sin = ltx_freqs_cis
            heads_per_rank = num_heads // world_size
            head_start = local_rank * heads_per_rank
            head_end = head_start + heads_per_rank
            cos_local = cos[:, head_start:head_end, :original_seq_len, :]
            sin_local = sin[:, head_start:head_end, :original_seq_len, :]
            qk_part = qkvg[:batch_size * 2].transpose(1, 2)
            qk_part = apply_ltx_rotary_emb_4d(
                qk_part, (cos_local, sin_local), self.rope_type)
            qkvg[:batch_size * 2] = qk_part.transpose(1, 2)

        qkvg = self.attn_impl.preprocess_qkv(qkvg, ctx_attn_metadata)
        q, k, v, gate_compress = qkvg.chunk(4, dim=0)
        output = self.attn_impl.forward(  # type: ignore[call-arg]
            q, k, v, gate_compress, ctx_attn_metadata)
        output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)
        output = torch.nn.functional.pad(output, (0, 0, 0, 0, 0, pad_seq_len))

        output = sequence_model_parallel_all_to_all_4D(output,
                                                       scatter_dim=1,
                                                       gather_dim=2)
        return output, None

    # Stack QKV
    qkv = torch.cat([q, k, v], dim=0)  # [3*batch, seq_len, num_heads, head_dim]

    # Redistribute heads across sequence dimension
    qkv = sequence_model_parallel_all_to_all_4D(qkv, scatter_dim=2, gather_dim=1)

    # After all-to-all, each rank has the full sequence but only a subset of heads
    pad_seq_len = qkv.shape[1] - original_seq_len
    qkv = qkv[:, :original_seq_len, :, :]

    # Apply LTX-2 style RoPE after all-to-all (when we have full sequence)
    if ltx_freqs_cis is not None:
        cos, sin = ltx_freqs_cis
        heads_per_rank = num_heads // world_size
        head_start = local_rank * heads_per_rank
        head_end = head_start + heads_per_rank
        # Slice to this rank's heads: [B, H/SP, T, D]
        cos_local = cos[:, head_start:head_end, :original_seq_len, :]
        sin_local = sin[:, head_start:head_end, :original_seq_len, :]

        # Apply RoPE to Q and K together (first 2*batch_size in dim 0)
        qk_part = qkv[:batch_size * 2]
        # Transpose to [2*B, H/SP, T, D] for LTX RoPE application
        qk_part = qk_part.transpose(1, 2)
        qk_part = apply_ltx_rotary_emb_4d(qk_part, (cos_local, sin_local), self.rope_type)
        # Transpose back to [2*B, T, H/SP, D]
        qkv[:batch_size * 2] = qk_part.transpose(1, 2)

    # Apply backend-specific preprocess_qkv
    qkv = self.attn_impl.preprocess_qkv(qkv, ctx_attn_metadata)

    # Concatenate with replicated QKV if provided
    if replicated_q is not None:
        assert replicated_k is not None and replicated_v is not None
        replicated_qkv = torch.cat(
            [replicated_q, replicated_k, replicated_v],
            dim=0)  # [3, seq_len, num_heads, head_dim]
        heads_per_rank = num_heads // world_size
        replicated_qkv = replicated_qkv[:, :, local_rank *
                                        heads_per_rank:(local_rank + 1) *
                                        heads_per_rank]
        qkv = torch.cat([qkv, replicated_qkv], dim=1)

    q, k, v = qkv.chunk(3, dim=0)

    output = self.attn_impl.forward(q, k, v, ctx_attn_metadata)

    # Redistribute back if using sequence parallelism
    replicated_output = None
    if replicated_q is not None:
        split_idx = original_seq_len
        replicated_output = output[:, split_idx:]
        output = output[:, :split_idx]
        replicated_output = sequence_model_parallel_all_gather(
            replicated_output.contiguous(), dim=2)

    # Apply backend-specific postprocess_output
    output = self.attn_impl.postprocess_output(output, ctx_attn_metadata)

    output = torch.nn.functional.pad(output, (0, 0, 0, 0, 0, pad_seq_len))

    output = sequence_model_parallel_all_to_all_4D(output, scatter_dim=1, gather_dim=2)

    return output, replicated_output

fastvideo.models.dits.ltx2.LTXDistributedSelfAttention

LTXDistributedSelfAttention(query_dim: int, context_dim: int | None, heads: int, dim_head: int, norm_eps: float, rope_type: LTXRopeType, supported_attention_backends: tuple[AttentionBackendEnum, ...], apply_gated_attention: bool = False, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

LTX-2 attention block with RMSNorm + LTXDistributedAttention for SP.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    query_dim: int,
    context_dim: int | None,
    heads: int,
    dim_head: int,
    norm_eps: float,
    rope_type: LTXRopeType,
    supported_attention_backends: tuple[AttentionBackendEnum, ...],
    apply_gated_attention: bool = False,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
) -> None:
    super().__init__()
    inner_dim = dim_head * heads
    context_dim = query_dim if context_dim is None else context_dim

    self.heads = heads
    self.dim_head = dim_head
    self.rope_type = rope_type

    # StageAwareRMSNorm == torch.nn.RMSNorm outside the LTX-2.3 refine
    # stage, preserving LTX-2.0 numerics by default.
    self.q_norm = StageAwareRMSNorm(inner_dim, eps=norm_eps)
    self.k_norm = StageAwareRMSNorm(inner_dim, eps=norm_eps)
    self.to_q = ReplicatedLinear(
        query_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_q",
    )
    self.to_k = ReplicatedLinear(
        context_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_k",
    )
    self.to_v = ReplicatedLinear(
        context_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_v",
    )
    # LTX-2.3 gated attention (distinct from VSA-QAT to_gate_compress).
    self.to_gate_logits: ReplicatedLinear | None = None
    if apply_gated_attention:
        self.to_gate_logits = ReplicatedLinear(
            query_dim,
            heads,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_gate_logits",
        )
    self.to_out = nn.ModuleList([
        ReplicatedLinear(
            inner_dim,
            query_dim,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_out",
        ),
        nn.Identity(),
    ])

    self.attn = LTXDistributedAttention(
        num_heads=heads,
        head_size=dim_head,
        rope_type=rope_type,
        causal=False,
        supported_attention_backends=supported_attention_backends,
        prefix=f"{prefix}.attn",
    )
    self.to_gate_compress: ReplicatedLinear | None = None
    if self.attn.backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN:
        self.to_gate_compress = ReplicatedLinear(
            context_dim,
            inner_dim,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_gate_compress",
        )

Methods:

fastvideo.models.dits.ltx2.LTXDistributedSelfAttention.forward
forward(x: Tensor, context: Tensor | None = None, pe: tuple[Tensor, Tensor] | None = None, k_pe: tuple[Tensor, Tensor] | None = None, original_seq_len: int | None = None) -> Tensor

Forward pass for distributed self-attention.

Parameters:

Name Type Description Default
x Tensor

Query tensor [B, L, C]

required
context Tensor | None

Key/Value tensor [B, L, C], or None for self-attention

None
pe tuple[Tensor, Tensor] | None

Rotary position embeddings for Q (cos, sin) with shape [B, H, T_full, D] NOTE: For SP, this must be the FULL sequence RoPE, not sharded!

None
k_pe tuple[Tensor, Tensor] | None

Rotary position embeddings for K (cos, sin), or None to use pe

None
original_seq_len int | None

Original (unpadded) full sequence length

None
Source code in fastvideo/models/dits/ltx2.py
def forward(
    self,
    x: torch.Tensor,
    context: torch.Tensor | None = None,
    pe: tuple[torch.Tensor, torch.Tensor] | None = None,
    k_pe: tuple[torch.Tensor, torch.Tensor] | None = None,
    original_seq_len: int | None = None,
) -> torch.Tensor:
    """Forward pass for distributed self-attention.

    Args:
        x: Query tensor [B, L, C]
        context: Key/Value tensor [B, L, C], or None for self-attention
        pe: Rotary position embeddings for Q (cos, sin) with shape [B, H, T_full, D]
            NOTE: For SP, this must be the FULL sequence RoPE, not sharded!
        k_pe: Rotary position embeddings for K (cos, sin), or None to use pe
        original_seq_len: Original (unpadded) full sequence length
    """
    context = x if context is None else context

    q_pre_quantized = (
        self.to_q.quant_method.quantize_input(x)  # type: ignore[union-attr]
        if _supports_prequantized_input(self.to_q) else None)
    kv_pre_quantized = None
    if (_supports_prequantized_input(self.to_k)
            and _supports_prequantized_input(self.to_v)):
        if context is x and q_pre_quantized is not None:
            kv_pre_quantized = q_pre_quantized
        else:
            kv_pre_quantized = self.to_k.quant_method.quantize_input(  # type: ignore[union-attr]
                context)

    q = _linear_project_with_optional_prequant(self.to_q, x, q_pre_quantized)
    k = _linear_project_with_optional_prequant(self.to_k, context, kv_pre_quantized)
    v = _linear_project_with_optional_prequant(self.to_v, context, kv_pre_quantized)
    gate_logits = (self.to_gate_logits(x)[0]
                   if self.to_gate_logits is not None else None)
    gate_compress = (self.to_gate_compress(context)[0]
                     if self.to_gate_compress is not None else None)

    q = self.q_norm(q)
    k = self.k_norm(k)

    # RoPE is applied inside LTXDistributedAttention AFTER the all-to-all,
    # when each rank has the full sequence (but subset of heads).
    b, q_len, _ = q.shape
    k_len = k.shape[1]
    q = q.view(b, q_len, self.heads, self.dim_head)
    k = k.view(b, k_len, self.heads, self.dim_head)
    v = v.view(b, k_len, self.heads, self.dim_head)
    if gate_compress is not None:
        gate_compress = gate_compress.view(b, k_len, self.heads,
                                           self.dim_head)

    # Pass full RoPE to distributed attention - it will apply after all-to-all
    # and slice to this rank's heads
    if original_seq_len is None:
        raise ValueError("original_seq_len must be provided for distributed attention")
    out, _ = self.attn(
        q,
        k,
        v,
        original_seq_len,
        gate_compress=gate_compress,
        ltx_freqs_cis=pe,
    )

    # LTX-2.3 gated attention (no-op for LTX-2.0).
    if gate_logits is not None:
        out = out.view(b, q_len, self.heads, self.dim_head)
        gates = 2.0 * torch.sigmoid(gate_logits).to(out.dtype)
        out = out * gates.unsqueeze(-1)
    out = out.reshape(b, q_len, -1)
    out = self.to_out[0](out)[0]
    return self.to_out[1](out)

fastvideo.models.dits.ltx2.LTXLocalAttention

LTXLocalAttention(num_heads: int, head_size: int, rope_type: LTXRopeType, num_kv_heads: int | None = None, softmax_scale: float | None = None, causal: bool = False, supported_attention_backends: tuple[AttentionBackendEnum, ...] | None = None, **extra_impl_args)

Bases: LocalAttention

LTX-2 specialized LocalAttention that handles LTX-style RoPE internally.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    num_heads: int,
    head_size: int,
    rope_type: LTXRopeType,
    num_kv_heads: int | None = None,
    softmax_scale: float | None = None,
    causal: bool = False,
    supported_attention_backends: tuple[AttentionBackendEnum, ...] | None = None,
    **extra_impl_args,
) -> None:
    super().__init__(
        num_heads=num_heads,
        head_size=head_size,
        num_kv_heads=num_kv_heads,
        softmax_scale=softmax_scale,
        causal=causal,
        supported_attention_backends=supported_attention_backends,
        **extra_impl_args,
    )
    self.rope_type = rope_type

Methods:

fastvideo.models.dits.ltx2.LTXLocalAttention.forward
forward(q: Tensor, k: Tensor, v: Tensor, gate_compress: Tensor | None = None, ltx_freqs_cis: tuple[Tensor, Tensor] | None = None, ltx_k_freqs_cis: tuple[Tensor, Tensor] | None = None) -> Tensor

Forward pass with LTX-2 style RoPE application.

Parameters:

Name Type Description Default
q Tensor

Query tensor [batch_size, seq_len, num_heads, head_dim]

required
k Tensor

Key tensor [batch_size, seq_len, num_heads, head_dim]

required
v Tensor

Value tensor [batch_size, seq_len, num_heads, head_dim]

required
ltx_freqs_cis tuple[Tensor, Tensor] | None

LTX-2 style RoPE (cos, sin) for Q with shape [B, H, T, D]

None
ltx_k_freqs_cis tuple[Tensor, Tensor] | None

LTX-2 style RoPE (cos, sin) for K (if different from Q)

None

Returns:

Type Description
Tensor

Output tensor after local attention

Source code in fastvideo/models/dits/ltx2.py
def forward(
    self,
    q: torch.Tensor,
    k: torch.Tensor,
    v: torch.Tensor,
    gate_compress: torch.Tensor | None = None,
    ltx_freqs_cis: tuple[torch.Tensor, torch.Tensor] | None = None,
    ltx_k_freqs_cis: tuple[torch.Tensor, torch.Tensor] | None = None,
) -> torch.Tensor:
    """Forward pass with LTX-2 style RoPE application.

    Args:
        q: Query tensor [batch_size, seq_len, num_heads, head_dim]
        k: Key tensor [batch_size, seq_len, num_heads, head_dim]
        v: Value tensor [batch_size, seq_len, num_heads, head_dim]
        ltx_freqs_cis: LTX-2 style RoPE (cos, sin) for Q with shape [B, H, T, D]
        ltx_k_freqs_cis: LTX-2 style RoPE (cos, sin) for K (if different from Q)

    Returns:
        Output tensor after local attention
    """
    assert q.dim() == 4 and k.dim() == 4 and v.dim() == 4, "Expected 4D tensors"

    forward_context: ForwardContext = get_forward_context()
    ctx_attn_metadata = forward_context.attn_metadata

    # Apply LTX-2 style RoPE
    if ltx_freqs_cis is not None:
        # Use separate K RoPE if provided (for cross-attention), otherwise use same as Q
        k_freqs = ltx_k_freqs_cis if ltx_k_freqs_cis is not None else ltx_freqs_cis
        # Transpose to [B, H, T, D] for RoPE application
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)
        q = apply_ltx_rotary_emb_4d(q, ltx_freqs_cis, self.rope_type)
        k = apply_ltx_rotary_emb_4d(k, k_freqs, self.rope_type)
        # Transpose back to [B, T, H, D]
        q = q.transpose(1, 2)
        k = k.transpose(1, 2)

    if self.backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN:
        if gate_compress is None:
            raise ValueError(
                "gate_compress must be provided when using VIDEO_SPARSE_ATTN"
            )
        output = self.attn_impl.forward(  # type: ignore[call-arg]
            q, k, v, gate_compress, ctx_attn_metadata)
    else:
        output = self.attn_impl.forward(q, k, v, ctx_attn_metadata)
    return output

fastvideo.models.dits.ltx2.LTXModel

LTXModel(*, model_type: LTXModelType = AudioVideo, num_attention_heads: int = 32, attention_head_dim: int = 128, in_channels: int = 128, out_channels: int = 128, num_layers: int = 48, cross_attention_dim: int = 4096, norm_eps: float = 1e-06, caption_channels: int = 3840, positional_embedding_theta: float = 10000.0, positional_embedding_max_pos: list[int] | None = None, timestep_scale_multiplier: int = 1000, use_middle_indices_grid: bool = True, audio_num_attention_heads: int = 32, audio_attention_head_dim: int = 64, audio_in_channels: int = 128, audio_out_channels: int = 128, audio_cross_attention_dim: int = 2048, audio_positional_embedding_max_pos: list[int] | None = None, av_ca_timestep_scale_multiplier: int = 1, rope_type: LTXRopeType = INTERLEAVED, double_precision_rope: bool = False, cross_attention_adaln: bool = False, caption_proj_before_connector: bool = False, apply_gated_attention: bool = False, stg_block_idx: int = 29, use_distributed_attention: bool = False, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

Core LTX-2 transformer stack for audio/video latents.

Source code in fastvideo/models/dits/ltx2.py
def __init__(  # noqa: PLR0913
    self,
    *,
    model_type: LTXModelType = LTXModelType.AudioVideo,
    num_attention_heads: int = 32,
    attention_head_dim: int = 128,
    in_channels: int = 128,
    out_channels: int = 128,
    num_layers: int = 48,
    cross_attention_dim: int = 4096,
    norm_eps: float = 1e-06,
    caption_channels: int = 3840,
    positional_embedding_theta: float = 10000.0,
    positional_embedding_max_pos: list[int] | None = None,
    timestep_scale_multiplier: int = 1000,
    use_middle_indices_grid: bool = True,
    audio_num_attention_heads: int = 32,
    audio_attention_head_dim: int = 64,
    audio_in_channels: int = 128,
    audio_out_channels: int = 128,
    audio_cross_attention_dim: int = 2048,
    audio_positional_embedding_max_pos: list[int] | None = None,
    av_ca_timestep_scale_multiplier: int = 1,
    rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
    double_precision_rope: bool = False,
    cross_attention_adaln: bool = False,
    caption_proj_before_connector: bool = False,
    apply_gated_attention: bool = False,
    stg_block_idx: int = 29,
    use_distributed_attention: bool = False,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
):
    super().__init__()
    self._enable_gradient_checkpointing = False
    # LTX-2.3 gated extensions (all default OFF == LTX-2.0 behavior).
    self.cross_attention_adaln = cross_attention_adaln
    self.caption_proj_before_connector = caption_proj_before_connector
    self.apply_gated_attention = apply_gated_attention
    self.stg_block_idx = stg_block_idx
    self.use_middle_indices_grid = use_middle_indices_grid
    self.rope_type = rope_type
    self.double_precision_rope = double_precision_rope
    self.timestep_scale_multiplier = timestep_scale_multiplier
    self.positional_embedding_theta = positional_embedding_theta
    self.model_type = model_type
    cross_pe_max_pos = None

    if model_type.is_video_enabled():
        if positional_embedding_max_pos is None:
            positional_embedding_max_pos = [20, 2048, 2048]
        self.positional_embedding_max_pos = positional_embedding_max_pos
        self.num_attention_heads = num_attention_heads
        self.inner_dim = num_attention_heads * attention_head_dim
        self._init_video(
            in_channels=in_channels,
            out_channels=out_channels,
            caption_channels=caption_channels,
            norm_eps=norm_eps,
            create_caption_projection=not caption_proj_before_connector,
        )

    if model_type.is_audio_enabled():
        if audio_positional_embedding_max_pos is None:
            audio_positional_embedding_max_pos = [20]
        self.audio_positional_embedding_max_pos = audio_positional_embedding_max_pos
        self.audio_num_attention_heads = audio_num_attention_heads
        self.audio_inner_dim = self.audio_num_attention_heads * audio_attention_head_dim
        self._init_audio(
            in_channels=audio_in_channels,
            out_channels=audio_out_channels,
            caption_channels=caption_channels,
            norm_eps=norm_eps,
            create_caption_projection=not caption_proj_before_connector,
        )

    if model_type.is_video_enabled() and model_type.is_audio_enabled():
        cross_pe_max_pos = max(self.positional_embedding_max_pos[0], self.audio_positional_embedding_max_pos[0])
        self.av_ca_timestep_scale_multiplier = av_ca_timestep_scale_multiplier
        self.audio_cross_attention_dim = audio_cross_attention_dim
        self._init_audio_video(num_scale_shift_values=4)

    self._init_preprocessors(cross_pe_max_pos)
    self._init_transformer_blocks(
        num_layers=num_layers,
        attention_head_dim=attention_head_dim if model_type.is_video_enabled() else 0,
        cross_attention_dim=cross_attention_dim,
        audio_attention_head_dim=audio_attention_head_dim if model_type.is_audio_enabled() else 0,
        audio_cross_attention_dim=audio_cross_attention_dim,
        norm_eps=norm_eps,
        use_distributed_attention=use_distributed_attention,
        quant_config=quant_config,
        prefix=prefix,
    )

Methods:

fastvideo.models.dits.ltx2.LTXModel.forward
forward(video: Modality | None, audio: Modality | None, video_original_seq_len: int | None = None, audio_original_seq_len: int | None = None, skip_cross_modal_attn: bool = False, skip_video_self_attn_blocks: list[int] | None = None, skip_audio_self_attn_blocks: list[int] | None = None) -> tuple[Tensor | None, Tensor | None]

Forward pass through the LTX model.

Parameters:

Name Type Description Default
video Modality | None

Video modality input

required
audio Modality | None

Audio modality input

required
video_original_seq_len int | None

Original unpadded video sequence length

None
audio_original_seq_len int | None

Original unpadded audio sequence length

None
skip_cross_modal_attn bool

If True, skip A2V and V2A cross-modal attention in all transformer blocks (modality-isolated CFG pass).

False
skip_video_self_attn_blocks list[int] | None

Block indices where video self-attention is skipped (STG perturbed pass).

None
skip_audio_self_attn_blocks list[int] | None

Block indices where audio self-attention is skipped (STG perturbed pass).

None
Source code in fastvideo/models/dits/ltx2.py
def forward(
    self,
    video: Modality | None,
    audio: Modality | None,
    video_original_seq_len: int | None = None,
    audio_original_seq_len: int | None = None,
    skip_cross_modal_attn: bool = False,
    skip_video_self_attn_blocks: list[int] | None = None,
    skip_audio_self_attn_blocks: list[int] | None = None,
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
    """Forward pass through the LTX model.

    Args:
        video: Video modality input
        audio: Audio modality input
        video_original_seq_len: Original unpadded video sequence length
        audio_original_seq_len: Original unpadded audio sequence length
        skip_cross_modal_attn: If True, skip A2V and V2A cross-modal
            attention in all transformer blocks (modality-isolated
            CFG pass).
        skip_video_self_attn_blocks: Block indices where video
            self-attention is skipped (STG perturbed pass).
        skip_audio_self_attn_blocks: Block indices where audio
            self-attention is skipped (STG perturbed pass).
    """
    if os.getenv("LTX2_PIPELINE_DEBUG_LOG", "0") == "1":
        _debug_block_log_line(
            "fastvideo:patchify_proj"
            f":video_w_sum={self.patchify_proj.weight.float().sum().item():.6f} "
            f"video_b_sum={self.patchify_proj.bias.float().sum().item():.6f} "
            f"audio_w_sum={self.audio_patchify_proj.weight.float().sum().item():.6f} "
            f"audio_b_sum={self.audio_patchify_proj.bias.float().sum().item():.6f}"
        )
    if not self.model_type.is_video_enabled() and video is not None:
        raise ValueError("Video is not enabled for this model")
    if not self.model_type.is_audio_enabled() and audio is not None:
        raise ValueError("Audio is not enabled for this model")

    video_args = self.video_args_preprocessor.prepare(video) if video is not None else None
    audio_args = self.audio_args_preprocessor.prepare(audio) if audio is not None else None
    _debug_transformer_args("fastvideo:prep_video", video_args)
    _debug_transformer_args("fastvideo:prep_audio", audio_args)
    video_out, audio_out = self._process_transformer_blocks(
        video_args,
        audio_args,
        video_original_seq_len=video_original_seq_len,
        audio_original_seq_len=audio_original_seq_len,
        skip_cross_modal_attn=skip_cross_modal_attn,
        skip_video_self_attn_blocks=skip_video_self_attn_blocks,
        skip_audio_self_attn_blocks=skip_audio_self_attn_blocks,
    )

    vx = (
        self._process_output(
            self.scale_shift_table, self.norm_out, self.proj_out, video_out.x, video_out.embedded_timestep
        )
        if video_out is not None
        else None
    )
    ax = (
        self._process_output(
            self.audio_scale_shift_table,
            self.audio_norm_out,
            self.audio_proj_out,
            audio_out.x,
            audio_out.embedded_timestep,
        )
        if audio_out is not None
        else None
    )
    return vx, ax

fastvideo.models.dits.ltx2.LTXModelType

Bases: Enum

Model type flags for LTX-2.

fastvideo.models.dits.ltx2.LTXRopeType

Bases: Enum

LTX-2 rotary variants (interleaved vs split).

fastvideo.models.dits.ltx2.LTXSelfAttention

LTXSelfAttention(query_dim: int, context_dim: int | None, heads: int, dim_head: int, norm_eps: float, rope_type: LTXRopeType, supported_attention_backends: tuple[AttentionBackendEnum, ...], apply_gated_attention: bool = False, quant_config: QuantizationConfig | None = None, prefix: str = '')

Bases: Module

LTX-2 attention block with RMSNorm + FastVideo LocalAttention.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    query_dim: int,
    context_dim: int | None,
    heads: int,
    dim_head: int,
    norm_eps: float,
    rope_type: LTXRopeType,
    supported_attention_backends: tuple[AttentionBackendEnum, ...],
    apply_gated_attention: bool = False,
    quant_config: QuantizationConfig | None = None,
    prefix: str = "",
) -> None:
    super().__init__()
    inner_dim = dim_head * heads
    context_dim = query_dim if context_dim is None else context_dim

    self.heads = heads
    self.dim_head = dim_head
    self.rope_type = rope_type

    # StageAwareRMSNorm is identical to torch.nn.RMSNorm outside the
    # LTX-2.3 refine stage, so default-off behavior matches LTX-2.0.
    self.q_norm = StageAwareRMSNorm(inner_dim, eps=norm_eps)
    self.k_norm = StageAwareRMSNorm(inner_dim, eps=norm_eps)
    self.to_q = ReplicatedLinear(
        query_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_q",
    )
    self.to_k = ReplicatedLinear(
        context_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_k",
    )
    self.to_v = ReplicatedLinear(
        context_dim,
        inner_dim,
        bias=True,
        quant_config=quant_config,
        prefix=f"{prefix}.to_v",
    )
    # LTX-2.3 gated attention. DISTINCT from the VSA-QAT to_gate_compress
    # gate below: this gate multiplies the *self-attention output* by
    # 2*sigmoid(to_gate_logits(x)). None (default) == LTX-2.0 behavior.
    self.to_gate_logits: ReplicatedLinear | None = None
    if apply_gated_attention:
        self.to_gate_logits = ReplicatedLinear(
            query_dim,
            heads,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_gate_logits",
        )
    self.to_out = nn.ModuleList([
        ReplicatedLinear(
            inner_dim,
            query_dim,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_out",
        ),
        nn.Identity(),
    ])

    self.attn = LTXLocalAttention(
        num_heads=heads,
        head_size=dim_head,
        rope_type=rope_type,
        dropout_rate=0,
        softmax_scale=None,
        causal=False,
        supported_attention_backends=supported_attention_backends,
    )
    self.to_gate_compress: ReplicatedLinear | None = None
    if self.attn.backend == AttentionBackendEnum.VIDEO_SPARSE_ATTN:
        self.to_gate_compress = ReplicatedLinear(
            context_dim,
            inner_dim,
            bias=True,
            quant_config=quant_config,
            prefix=f"{prefix}.to_gate_compress",
        )
    self.attn_masked = LTXLocalAttention(
        num_heads=heads,
        head_size=dim_head,
        rope_type=rope_type,
        dropout_rate=0,
        softmax_scale=None,
        causal=False,
        supported_attention_backends=(AttentionBackendEnum.TORCH_SDPA,),
    )

fastvideo.models.dits.ltx2.Modality dataclass

Modality(enabled: bool, latent: Tensor, timesteps: Tensor, positions: Tensor, context: Tensor, context_mask: Tensor | None = None, sigma: Tensor | None = None)

Lightweight modality container for LTX-2 inputs.

fastvideo.models.dits.ltx2.MultiModalTransformerArgsPreprocessor

MultiModalTransformerArgsPreprocessor(patchify_proj: Linear, adaln: AdaLayerNormSingle, caption_projection: PixArtAlphaTextProjection, cross_scale_shift_adaln: AdaLayerNormSingle, cross_gate_adaln: AdaLayerNormSingle, inner_dim: int, max_pos: list[int], num_attention_heads: int, cross_pe_max_pos: int, use_middle_indices_grid: bool, audio_cross_attention_dim: int, timestep_scale_multiplier: int, double_precision_rope: bool, positional_embedding_theta: float, rope_type: LTXRopeType, av_ca_timestep_scale_multiplier: int, prompt_adaln: AdaLayerNormSingle | None = None)

Prepare transformer args for audio/video cross-attention paths.

Source code in fastvideo/models/dits/ltx2.py
def __init__(  # noqa: PLR0913
    self,
    patchify_proj: torch.nn.Linear,
    adaln: AdaLayerNormSingle,
    caption_projection: PixArtAlphaTextProjection,
    cross_scale_shift_adaln: AdaLayerNormSingle,
    cross_gate_adaln: AdaLayerNormSingle,
    inner_dim: int,
    max_pos: list[int],
    num_attention_heads: int,
    cross_pe_max_pos: int,
    use_middle_indices_grid: bool,
    audio_cross_attention_dim: int,
    timestep_scale_multiplier: int,
    double_precision_rope: bool,
    positional_embedding_theta: float,
    rope_type: LTXRopeType,
    av_ca_timestep_scale_multiplier: int,
    prompt_adaln: AdaLayerNormSingle | None = None,
) -> None:
    self.simple_preprocessor = TransformerArgsPreprocessor(
        patchify_proj=patchify_proj,
        adaln=adaln,
        caption_projection=caption_projection,
        inner_dim=inner_dim,
        max_pos=max_pos,
        num_attention_heads=num_attention_heads,
        use_middle_indices_grid=use_middle_indices_grid,
        timestep_scale_multiplier=timestep_scale_multiplier,
        double_precision_rope=double_precision_rope,
        positional_embedding_theta=positional_embedding_theta,
        rope_type=rope_type,
        prompt_adaln=prompt_adaln,
    )
    self.cross_scale_shift_adaln = cross_scale_shift_adaln
    self.cross_gate_adaln = cross_gate_adaln
    self.cross_pe_max_pos = cross_pe_max_pos
    self.audio_cross_attention_dim = audio_cross_attention_dim
    self.av_ca_timestep_scale_multiplier = av_ca_timestep_scale_multiplier

fastvideo.models.dits.ltx2.PixArtAlphaCombinedTimestepSizeEmbeddings

PixArtAlphaCombinedTimestepSizeEmbeddings(embedding_dim: int, size_emb_dim: int)

Bases: Module

PixArt-Alpha timestep embedding used by LTX-2 AdaLN.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, embedding_dim: int, size_emb_dim: int):
    super().__init__()
    self.outdim = size_emb_dim
    self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0)
    self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)

fastvideo.models.dits.ltx2.PixArtAlphaTextProjection

PixArtAlphaTextProjection(in_features: int, hidden_size: int, out_features: int | None = None, act_fn: str = 'gelu_tanh')

Bases: Module

Caption projection MLP used by LTX-2.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, in_features: int, hidden_size: int, out_features: int | None = None, act_fn: str = "gelu_tanh"):
    super().__init__()
    if out_features is None:
        out_features = hidden_size
    self.linear_1 = torch.nn.Linear(in_features=in_features, out_features=hidden_size, bias=True)
    if act_fn == "gelu_tanh":
        self.act_1 = torch.nn.GELU(approximate="tanh")
    elif act_fn == "silu":
        self.act_1 = torch.nn.SiLU()
    else:
        raise ValueError(f"Unknown activation function: {act_fn}")
    self.linear_2 = torch.nn.Linear(in_features=hidden_size, out_features=out_features, bias=True)

fastvideo.models.dits.ltx2.StageAwareRMSNorm

StageAwareRMSNorm(hidden_size: int, eps: float = 1e-06)

Bases: RMSNorm

Use torch.nn.RMSNorm for base stage and QuACK for refine stage.

When QuACK is unavailable or outside the refine stage this behaves identically to torch.nn.RMSNorm (LTX-2.0 q_norm/k_norm behavior).

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
    super().__init__(hidden_size, eps=eps)

fastvideo.models.dits.ltx2.TimestepEmbedding

TimestepEmbedding(in_channels: int, time_embed_dim: int, out_dim: int | None = None, post_act_fn: str | None = None, cond_proj_dim: int | None = None, sample_proj_bias: bool = True)

Bases: Module

Two-layer MLP to project timestep embeddings.

Source code in fastvideo/models/dits/ltx2.py
def __init__(
    self,
    in_channels: int,
    time_embed_dim: int,
    out_dim: int | None = None,
    post_act_fn: str | None = None,
    cond_proj_dim: int | None = None,
    sample_proj_bias: bool = True,
):
    super().__init__()
    self.linear_1 = torch.nn.Linear(in_channels, time_embed_dim, sample_proj_bias)
    self.cond_proj = torch.nn.Linear(cond_proj_dim, in_channels, bias=False) if cond_proj_dim is not None else None
    self.act = torch.nn.SiLU()
    time_embed_dim_out = out_dim if out_dim is not None else time_embed_dim
    self.linear_2 = torch.nn.Linear(time_embed_dim, time_embed_dim_out, sample_proj_bias)
    self.post_act = None if post_act_fn is None else None

fastvideo.models.dits.ltx2.Timesteps

Timesteps(num_channels: int, flip_sin_to_cos: bool, downscale_freq_shift: float, scale: int = 1)

Bases: Module

Sinusoidal timestep embedding wrapper with scaling knobs.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, num_channels: int, flip_sin_to_cos: bool, downscale_freq_shift: float, scale: int = 1):
    super().__init__()
    self.num_channels = num_channels
    self.flip_sin_to_cos = flip_sin_to_cos
    self.downscale_freq_shift = downscale_freq_shift
    self.scale = scale

fastvideo.models.dits.ltx2.TransformerArgs dataclass

TransformerArgs(x: Tensor, context: Tensor, context_mask: Tensor | None, timesteps: Tensor, embedded_timestep: Tensor, positional_embeddings: Tensor, cross_positional_embeddings: Tensor | None, cross_scale_shift_timestep: Tensor | None, cross_gate_timestep: Tensor | None, enabled: bool, prompt_timestep: Tensor | None = None)

Pack transformer inputs for LTX-2 blocks.

fastvideo.models.dits.ltx2.TransformerArgsPreprocessor

TransformerArgsPreprocessor(patchify_proj: Linear, adaln: AdaLayerNormSingle, caption_projection: PixArtAlphaTextProjection, inner_dim: int, max_pos: list[int], num_attention_heads: int, use_middle_indices_grid: bool, timestep_scale_multiplier: int, double_precision_rope: bool, positional_embedding_theta: float, rope_type: LTXRopeType, prompt_adaln: AdaLayerNormSingle | None = None)

Prepare LTX-2 transformer inputs (patchify, AdaLN, rope).

Source code in fastvideo/models/dits/ltx2.py
def __init__(  # noqa: PLR0913
    self,
    patchify_proj: torch.nn.Linear,
    adaln: AdaLayerNormSingle,
    caption_projection: PixArtAlphaTextProjection,
    inner_dim: int,
    max_pos: list[int],
    num_attention_heads: int,
    use_middle_indices_grid: bool,
    timestep_scale_multiplier: int,
    double_precision_rope: bool,
    positional_embedding_theta: float,
    rope_type: LTXRopeType,
    prompt_adaln: AdaLayerNormSingle | None = None,
) -> None:
    self.patchify_proj = patchify_proj
    self.adaln = adaln
    self.caption_projection = caption_projection
    self.inner_dim = inner_dim
    self.max_pos = max_pos
    self.num_attention_heads = num_attention_heads
    self.use_middle_indices_grid = use_middle_indices_grid
    self.timestep_scale_multiplier = timestep_scale_multiplier
    self.double_precision_rope = double_precision_rope
    self.positional_embedding_theta = positional_embedding_theta
    self.rope_type = rope_type
    # LTX-2.3 cross-attention AdaLN prompt timestep embedder (None for 2.0).
    self.prompt_adaln = prompt_adaln

fastvideo.models.dits.ltx2.TransformerConfig dataclass

TransformerConfig(dim: int, heads: int, d_head: int, context_dim: int, apply_gated_attention: bool = False, cross_attention_adaln: bool = False)

Attention/FFN dims for LTX-2 transformer blocks.

fastvideo.models.dits.ltx2.VideoLatentPatchifier

VideoLatentPatchifier(patch_size: int)

Patchify/unpatchify latent tokens for LTX-2.

Source code in fastvideo/models/dits/ltx2.py
def __init__(self, patch_size: int):
    self._patch_size = (1, patch_size, patch_size)

Methods:

fastvideo.models.dits.ltx2.VideoLatentPatchifier.get_patch_grid_bounds
get_patch_grid_bounds(output_shape: VideoLatentShape, device: Optional[device] = None) -> Tensor

Get patch grid bounds for RoPE computation.

Parameters:

Name Type Description Default
output_shape VideoLatentShape

Shape of the video latent tensor

required
device Optional[device]

Device to create tensors on

None
Source code in fastvideo/models/dits/ltx2.py
def get_patch_grid_bounds(
    self,
    output_shape: VideoLatentShape,
    device: Optional[torch.device] = None,
) -> torch.Tensor:
    """Get patch grid bounds for RoPE computation.

    Args:
        output_shape: Shape of the video latent tensor
        device: Device to create tensors on
    """
    frames = output_shape.frames
    height = output_shape.height
    width = output_shape.width
    batch_size = output_shape.batch

    grid_coords = torch.meshgrid(
        torch.arange(start=0, end=frames, step=self._patch_size[0], device=device),
        torch.arange(start=0, end=height, step=self._patch_size[1], device=device),
        torch.arange(start=0, end=width, step=self._patch_size[2], device=device),
        indexing="ij",
    )

    patch_starts = torch.stack(grid_coords, dim=0)
    patch_size_delta = torch.tensor(
        self._patch_size,
        device=patch_starts.device,
        dtype=patch_starts.dtype,
    ).view(3, 1, 1, 1)
    patch_ends = patch_starts + patch_size_delta
    latent_coords = torch.stack((patch_starts, patch_ends), dim=-1)
    latent_coords = repeat(
        latent_coords,
        "c f h w bounds -> b c (f h w) bounds",
        b=batch_size,
        bounds=2,
    )
    return latent_coords

fastvideo.models.dits.ltx2.VideoLatentShape

Bases: tuple

Helper for (B, C, T, H, W) latent shapes.

Functions:

fastvideo.models.dits.ltx2.apply_cross_attention_adaln

apply_cross_attention_adaln(x: Tensor, context: Tensor, attn: Callable, q_shift: Tensor, q_scale: Tensor, q_gate: Tensor, prompt_scale_shift_table: Tensor | None, prompt_timestep: Tensor | None, context_mask: Tensor | None = None, norm_eps: float = 1e-06) -> Tensor

LTX-2.3 cross-attention AdaLN: modulate query (x) and key/value (context) by AdaLN scale/shift and gate the output.

Only reached when cross_attention_adaln is enabled, so it never affects the LTX-2.0 path.

Source code in fastvideo/models/dits/ltx2.py
def apply_cross_attention_adaln(
    x: torch.Tensor,
    context: torch.Tensor,
    attn: Callable,
    q_shift: torch.Tensor,
    q_scale: torch.Tensor,
    q_gate: torch.Tensor,
    prompt_scale_shift_table: torch.Tensor | None,
    prompt_timestep: torch.Tensor | None,
    context_mask: torch.Tensor | None = None,
    norm_eps: float = 1e-6,
) -> torch.Tensor:
    """LTX-2.3 cross-attention AdaLN: modulate query (x) and key/value
    (context) by AdaLN scale/shift and gate the output.

    Only reached when ``cross_attention_adaln`` is enabled, so it never
    affects the LTX-2.0 path.
    """
    if prompt_scale_shift_table is None or prompt_timestep is None:
        raise ValueError(
            "cross_attention_adaln requires prompt scale/shift tables and prompt_timestep."
        )
    batch_size = x.shape[0]
    shift_kv, scale_kv = (
        prompt_scale_shift_table[None, None].to(device=x.device, dtype=x.dtype)
        + prompt_timestep.reshape(batch_size, prompt_timestep.shape[1], 2, -1)
    ).unbind(dim=2)
    attn_input = _rms_norm_dispatch(x, eps=norm_eps) * (1 + q_scale) + q_shift
    encoder_hidden_states = context * (1 + scale_kv) + shift_kv
    return attn(attn_input, context=encoder_hidden_states,
                mask=context_mask) * q_gate

fastvideo.models.dits.ltx2.apply_ltx_rotary_emb

apply_ltx_rotary_emb(input_tensor: Tensor, freqs_cis: tuple[Tensor, Tensor], rope_type: LTXRopeType = INTERLEAVED) -> Tensor

Apply LTX-2 rotary embeddings to a tensor.

Source code in fastvideo/models/dits/ltx2.py
def apply_ltx_rotary_emb(
    input_tensor: torch.Tensor,
    freqs_cis: tuple[torch.Tensor, torch.Tensor],
    rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
) -> torch.Tensor:
    """Apply LTX-2 rotary embeddings to a tensor."""
    if rope_type == LTXRopeType.INTERLEAVED:
        return _apply_ltx_interleaved_rotary_emb(input_tensor, *freqs_cis)
    if rope_type == LTXRopeType.SPLIT:
        return _apply_ltx_split_rotary_emb(input_tensor, *freqs_cis)
    raise ValueError(f"Invalid rope type: {rope_type}")

fastvideo.models.dits.ltx2.apply_ltx_rotary_emb_4d

apply_ltx_rotary_emb_4d(input_tensor: Tensor, freqs_cis: tuple[Tensor, Tensor], rope_type: LTXRopeType = INTERLEAVED) -> Tensor

Apply LTX-2 rotary embeddings to a 4D tensor [B, H, T, D].

This is used for applying RoPE after all-to-all in distributed attention, where the tensor is already in [B, H, T, D] format.

Source code in fastvideo/models/dits/ltx2.py
def apply_ltx_rotary_emb_4d(
    input_tensor: torch.Tensor,
    freqs_cis: tuple[torch.Tensor, torch.Tensor],
    rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
) -> torch.Tensor:
    """Apply LTX-2 rotary embeddings to a 4D tensor [B, H, T, D].

    This is used for applying RoPE after all-to-all in distributed attention,
    where the tensor is already in [B, H, T, D] format.
    """
    cos_freqs, sin_freqs = freqs_cis
    if rope_type == LTXRopeType.INTERLEAVED:
        # For interleaved, cos/sin have shape [B, T, inner_dim]
        # Need to reshape to [B, H, T, D] format
        # Actually interleaved doesn't have per-head rotations, so we broadcast
        t_dup = rearrange(input_tensor, "... (d r) -> ... d r", r=2)
        t1, t2 = t_dup.unbind(dim=-1)
        t_dup = torch.stack((-t2, t1), dim=-1)
        input_tensor_rot = rearrange(t_dup, "... d r -> ... (d r)")
        return input_tensor * cos_freqs + input_tensor_rot * sin_freqs
    if rope_type == LTXRopeType.SPLIT:
        # For split, cos/sin already have shape [B, H, T, D/2]
        # input_tensor is [B, H, T, D]
        split_input = rearrange(input_tensor, "... (d r) -> ... d r", d=2)
        first_half_input = split_input[..., :1, :]
        second_half_input = split_input[..., 1:, :]

        output = split_input * cos_freqs.unsqueeze(-2)
        first_half_output = output[..., :1, :]
        second_half_output = output[..., 1:, :]

        first_half_output.addcmul_(-sin_freqs.unsqueeze(-2), second_half_input)
        second_half_output.addcmul_(sin_freqs.unsqueeze(-2), first_half_input)

        return rearrange(output, "... d r -> ... (d r)")
    raise ValueError(f"Invalid rope type: {rope_type}")

fastvideo.models.dits.ltx2.generate_ltx_freq_grid_np cached

generate_ltx_freq_grid_np(positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int) -> Tensor

Generate LTX-2 rotary frequencies with high-precision numpy.

Source code in fastvideo/models/dits/ltx2.py
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_np(
    positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
    """Generate LTX-2 rotary frequencies with high-precision numpy."""
    theta = positional_embedding_theta
    start = 1
    end = theta
    n_elem = 2 * positional_embedding_max_pos_count
    pow_indices = np.power(
        theta,
        np.linspace(
            np.log(start) / np.log(theta),
            np.log(end) / np.log(theta),
            inner_dim // n_elem,
            dtype=np.float64,
        ),
    )
    return torch.tensor(pow_indices * math.pi / 2, dtype=torch.float32)

fastvideo.models.dits.ltx2.generate_ltx_freq_grid_pytorch cached

generate_ltx_freq_grid_pytorch(positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int) -> Tensor

Generate LTX-2 rotary frequencies in torch for speed.

Source code in fastvideo/models/dits/ltx2.py
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_pytorch(
    positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
    """Generate LTX-2 rotary frequencies in torch for speed."""
    theta = positional_embedding_theta
    start = 1
    end = theta
    n_elem = 2 * positional_embedding_max_pos_count
    indices = theta ** (
        torch.linspace(
            math.log(start, theta),
            math.log(end, theta),
            inner_dim // n_elem,
            dtype=torch.float32,
        )
    )
    indices = indices.to(dtype=torch.float32)
    return indices * math.pi / 2

fastvideo.models.dits.ltx2.get_timestep_embedding

get_timestep_embedding(timesteps: Tensor, embedding_dim: int, flip_sin_to_cos: bool = False, downscale_freq_shift: float = 1, scale: float = 1, max_period: int = 10000) -> Tensor

Sinusoidal timestep embedding used by LTX-2 AdaLN.

Source code in fastvideo/models/dits/ltx2.py
def get_timestep_embedding(
    timesteps: torch.Tensor,
    embedding_dim: int,
    flip_sin_to_cos: bool = False,
    downscale_freq_shift: float = 1,
    scale: float = 1,
    max_period: int = 10000,
) -> torch.Tensor:
    """Sinusoidal timestep embedding used by LTX-2 AdaLN."""
    if len(timesteps.shape) != 1:
        raise ValueError("Timesteps should be a 1d-array")

    half_dim = embedding_dim // 2
    exponent = -math.log(max_period) * torch.arange(start=0, end=half_dim, dtype=torch.float32, device=timesteps.device)
    exponent = exponent / (half_dim - downscale_freq_shift)

    emb = torch.exp(exponent)
    emb = timesteps[:, None].float() * emb[None, :]
    emb = scale * emb
    emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)

    if flip_sin_to_cos:
        emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)

    if embedding_dim % 2 == 1:
        emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
    return emb

fastvideo.models.dits.ltx2.precompute_ltx_freqs_cis

precompute_ltx_freqs_cis(indices_grid: Tensor, dim: int, out_dtype: dtype, theta: float = 10000.0, max_pos: list[int] | None = None, use_middle_indices_grid: bool = False, num_attention_heads: int = 32, rope_type: LTXRopeType = INTERLEAVED, freq_grid_generator: Callable[[float, int, int], Tensor] = generate_ltx_freq_grid_pytorch) -> tuple[Tensor, Tensor]

Precompute LTX-2 rotary cos/sin grids for (t, x, y) positions.

Source code in fastvideo/models/dits/ltx2.py
def precompute_ltx_freqs_cis(
    indices_grid: torch.Tensor,
    dim: int,
    out_dtype: torch.dtype,
    theta: float = 10000.0,
    max_pos: list[int] | None = None,
    use_middle_indices_grid: bool = False,
    num_attention_heads: int = 32,
    rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
    freq_grid_generator: Callable[[float, int, int], torch.Tensor] = generate_ltx_freq_grid_pytorch,
) -> tuple[torch.Tensor, torch.Tensor]:
    """Precompute LTX-2 rotary cos/sin grids for (t, x, y) positions."""
    if max_pos is None:
        max_pos = [20, 2048, 2048]

    indices = freq_grid_generator(theta, indices_grid.shape[1], dim)
    freqs = _ltx_generate_freqs(indices, indices_grid, max_pos, use_middle_indices_grid)

    if rope_type == LTXRopeType.SPLIT:
        expected_freqs = dim // 2
        current_freqs = freqs.shape[-1]
        pad_size = expected_freqs - current_freqs
        cos_freq, sin_freq = _ltx_split_freqs_cis(freqs, pad_size, num_attention_heads)
    else:
        n_elem = 2 * indices_grid.shape[1]
        cos_freq, sin_freq = _ltx_interleaved_freqs_cis(freqs, dim % n_elem)
    return cos_freq.to(out_dtype), sin_freq.to(out_dtype)