[feat] LTX-2.3 transformer support (config-gated extension of LTX-2) - #1397
Conversation
Port the LTX-2.3 DiT + Gemma text-connector deltas onto the upstream LTX-2.0 implementation as config-gated extensions. Every new field defaults OFF so that, with all flags at default, the model reproduces LTX-2.0 behavior (same param layout, same numerics). - configs/models/dits/ltx2.py: add cross_attention_adaln, caption_proj_before_connector, apply_gated_attention, caption/connector fields, audio_connector_*, and stg_block_idx (resolves to 29 for 2.0 / 28 for 2.3 in __post_init__); prepend the four to_gate_compress -> to_gate_logits weight-rename rules. - models/dits/ltx2.py: gated self-attention (to_gate_logits, distinct from the VSA-QAT to_gate_compress gate), cross-attention AdaLN (adaln_embedding_coefficient, prompt_scale_shift_table, apply_cross_attention_adaln), per-sample STG keep-mask alongside the existing bool skip path, and StageAwareRMSNorm/_rms_norm_dispatch with a defensive QuACK import (falls back to torch rms_norm when unavailable). Keeps upstream's BaseDiT base class and SP (original_seq_len) path. - models/encoders/gemma.py + config: connector gated attention, per-token RMS feature extractor (caption_proj_before_connector), separate audio connector config, and the 2.3 weight-name renames. - pipelines/basic/ltx2/presets.py: add LTX2_3_BASE (30 steps, CFG 3.0, STG block 28) to ALL_PRESETS. - registry.py: register an LTX-2.3 base entry and extend the distilled detector to match 2.3 aliases.
Merge ProtectionsYour pull request matches the following merge protections and will not be merged until they are valid. 🔴 PR merge requirementsWaiting for
This rule is failing.
|
There was a problem hiding this comment.
Code Review
This pull request introduces support for LTX-2.3 models, adding features such as gated self-attention, cross-attention AdaLN, and separate video/audio feature extractors in the Gemma text encoder, while maintaining backward compatibility with LTX-2.0. The code review identified several critical and high-severity issues, including a bug where modality.sigma is never populated (which will cause runtime crashes when cross_attention_adaln is enabled), a backward compatibility break for LTX-2.0 VSA checkpoints due to unconditional parameter renaming, a potential crash with single-element 1D STG masks, and potential runtime errors from calling .view() on non-contiguous tensors.
| timestep, embedded_timestep = self._prepare_timestep( | ||
| modality.timesteps, x.shape[0], modality.latent.dtype, self.adaln) | ||
| prompt_timestep = None | ||
| if self.prompt_adaln is not None and modality.sigma is not None: |
There was a problem hiding this comment.
Critical Bug: modality.sigma is never populated
In LTX2Transformer3DModel.forward (lines 2962 and 3007), video_modality and audio_modality are instantiated without passing the sigma parameter. As a result, modality.sigma always defaults to None.
This means prompt_timestep will always be None here, which will cause apply_cross_attention_adaln to raise a ValueError at runtime whenever cross_attention_adaln is enabled.
Fix: Update LTX2Transformer3DModel.forward to accept sigma (e.g., from kwargs or as an explicit argument) and pass it when instantiating the Modality objects.
| r"^model\.diffusion_model\.(.*)\.to_gate_compress\.(.*)$": r"model.\1.to_gate_logits.\2", | ||
| r"^diffusion_model\.(.*)\.to_gate_compress\.(.*)$": r"model.\1.to_gate_logits.\2", | ||
| r"^model\.(.*)\.to_gate_compress\.(.*)$": r"model.\1.to_gate_logits.\2", | ||
| r"^(.*)\.to_gate_compress\.(.*)$": r"model.\1.to_gate_logits.\2", |
There was a problem hiding this comment.
High Severity: Backward Compatibility Break for LTX-2.0 VSA Checkpoints
Renaming to_gate_compress to to_gate_logits unconditionally in param_names_mapping will break backward compatibility for LTX-2.0 checkpoints that use Video Sparse Attention (VSA).
In LTX-2.0 VSA, to_gate_compress is a valid parameter representing the VSA gate. Unconditional renaming will attempt to load these weights into to_gate_logits, causing shape mismatches and runtime errors.
Recommendation: Perform this renaming conditionally during weight loading (e.g., in load_weights by checking the model config or parameter shapes) rather than using a global static regex mapping.
| 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) |
There was a problem hiding this comment.
Medium Severity: Potential Crash with Single-Element 1D STG Mask
If skip_flag is a 1D tensor of shape [1], skip_flag.shape[0] != bsz will raise a ValueError when bsz > 1. In PyTorch, single-element 1D tensors are often used as broadcastable scalars.
It is safer to check skip_flag.numel() == 1 and expand it to bsz to handle this common edge case.
| 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) | |
| if skip_flag.numel() == 1: | |
| 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) |
| # LTX-2.3 gated attention: scale the attention output per-head by | ||
| # 2*sigmoid(gate_logits). No-op for LTX-2.0 (gate_logits is None). | ||
| if gate_logits is not None: | ||
| out = out.view(b, q_len, self.heads, self.dim_head) |
There was a problem hiding this comment.
Medium Severity: Potential View Crash on Non-Contiguous Tensor
If out is not contiguous (which can happen depending on the attention backend or sequence parallel operations), out.view will raise a RuntimeError. Using reshape is safer as it automatically handles non-contiguous tensors by copying them if necessary.
| out = out.view(b, q_len, self.heads, self.dim_head) | |
| out = out.reshape(b, q_len, self.heads, self.dim_head) |
|
|
||
| # 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) |
There was a problem hiding this comment.
Medium Severity: Potential View Crash on Non-Contiguous Tensor
If out is not contiguous, out.view will raise a RuntimeError. Using reshape is safer as it automatically handles non-contiguous tensors.
| out = out.view(b, q_len, self.heads, self.dim_head) | |
| out = out.reshape(b, q_len, self.heads, self.dim_head) |
Port the per-sample sigma threading from the internal LTX-2.3 implementation so cross_attention_adaln's prompt_timestep is populated at runtime: - top-level transformer forward now accepts video_sigma/audio_sigma and sets them onto the video/audio Modality (defaulting from timestep when unset, matching internal). - ltx2_denoising computes sigma_batch per step and passes video_sigma/audio_sigma on every guidance-pass transformer call. Backward-compatible: sigma only feeds prompt_adaln, which is None for LTX-2.0, so the cross_attn_adaln path stays skipped there. Also keeps the ltx2vae compress_space/compress_time decoder channel-scheme fix.
|
Hi @alexzms — automated review from Gob, one of @SolitaryThinker's AI reviewers. Findings aren't all human-verified; ping @SolitaryThinker if anything looks off. TL;DRSolid config-gated extension — default-off everywhere, the LTX-2.0 path is empirically preserved by the existing 35-test suite, and the new arch flags / preset / registry / pipeline plumbing all line up cleanly. ship-with-fixes: 1 × S1, 3 × S2, 2 × S3. The dominant concern is the new top-of-dict The rest is test gaps (zero new tests for ~575 LoC of new model code) and reviewer-attention items you already flagged (SP / compile / official-2.3-weights). S1-1 —
|
…_attention Addresses Gob's S1 finding on hao-ai-lab#1397: the 4 ``to_gate_compress`` -> ``to_gate_logits`` rules in ``LTX2VideoArchConfig.param_names_mapping`` fire unconditionally, but LTX-2.0 attention modules already legitimately carry a ``to_gate_compress`` parameter on two paths: - The VSA-QAT gate (``fastvideo/models/dits/ltx2.py``), created whenever the attention backend is ``VIDEO_SPARSE_ATTN``. - The default ``lora_target_modules`` list (``fastvideo/train/utils/lora.py:36`` and ``fastvideo/pipelines/lora_pipeline.py:171``), so every LoRA trained against defaults ships ``*.to_gate_compress.lora_A/B.weight`` keys. Unconditional rename silently retargets both: LTX-2.0 VSA checkpoints have their VSA gate weight rewritten to a non-existent ``to_gate_logits`` slot, and default-target LoRAs in the wild get their ``to_gate_compress`` LoRA pair rewritten too. The PR's "no-ops for LTX-2.0 checkpoints, which contain no ``to_gate_compress`` weights" comment was incorrect on this point. Fix: move the 4 gate-rename rules out of the static default factory and inject them at the front of ``param_names_mapping`` in ``__post_init__`` only when ``apply_gated_attention=True``. With the flag off, the rename never runs and LTX-2.0 VSA + default-target LoRA load behavior is byte-identical to ``origin/main``. With the flag on, the rules fire before the generic prefix-strip rules (first-match-wins iteration in ``get_param_names_mapping``). Adds ``fastvideo/tests/api/test_ltx2_param_mapping.py`` (14 parametrized tests covering both flag states + rule ordering). All 62 existing LTX-2 api tests pass alongside the new ones.
Pre-commit checks failedHi @alexzms, the pre-commit checks have failed. To fix them locally: # Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install
# Run all checks and auto-fix what's possible
pre-commit run --all-filesCommon fixes:
After fixing, commit and push the changes. The checks will re-run automatically. For future commits, |
|
Pushed Diagnosis verified, with one path-shape correction: the LTX-2.0 Going with Option A: moved the 4 rename rules out of the static Regression test: |
|
/merge |
Pre-commit checks failedHi @alexzms, the pre-commit checks have failed. To fix them locally: # Install pre-commit if you haven't already
uv pip install pre-commit
pre-commit install
# Run all checks and auto-fix what's possible
pre-commit run --all-filesCommon fixes:
After fixing, commit and push the changes. The checks will re-run automatically. For future commits, |
hao-ai-lab#1397 added the ltx2_3_base preset but test_ltx2_presets_registered still asserts only 3 presets, so it fails on current main. Drive-by fix to unblock CI for this PR. Pre-existing bug, surfaced because this PR also touches CI-running test files.
hao-ai-lab#1397 added the ltx2_3_base preset but test_ltx2_presets_registered still asserts only 3 presets, so it fails on current main. Drive-by fix to unblock CI for this PR. Pre-existing bug, surfaced because this PR also touches CI-running test files.
Add `examples/inference/basic/basic_ltx2_3_distilled_i2v.py`: a single-GPU
LTX-2.3 distilled image-to-video example that loads the registered
`FastVideo/LTX-2.3-Distilled-Diffusers` snapshot, runs the i2v path with
the production recipe (8 denoise + 3 refine, CFG=1, 832x1280 portrait,
121 frames @ 24fps), and prints a per-stage timing breakdown averaged
over two measured runs.
Defaults to eager so it works out of the box on `main`. The docstring
documents how to opt into `torch.compile` via `LTX23_ENABLE_COMPILE=1`
and names the two upstream sites where fullgraph compile currently fails
under recent PyTorch:
- `fastvideo/models/encoders/gemma.py` (`_replace_padded_with_learnable_
registers`): boolean-mask indexing produces a data-dependent shape
that `dynamic=False` cannot trace.
- `fastvideo/models/dits/ltx2.py` nested `_build_attn_keep_mask`: the
`bool | torch.Tensor` annotation is evaluated every outer-forward
call, producing a `types.UnionType` that dynamo's SourcelessBuilder
rejects.
Both have small workarounds (see docstring) that can be addressed in
follow-up PRs; flipping `LTX23_ENABLE_COMPILE=1` here will then exercise
the compile path end-to-end.
Fills a gap on `main`: the existing `basic_ltx2*.py` examples are t2v
only and target LTX-2.0 distilled. After hao-ai-lab#1397 merged LTX-2.3, there
was no copy-paste example for the LTX-2.3 i2v path.
Validated end-to-end on a single GB200: 2 warmup + 2 measured runs,
e2e ~8.7s eager per 5s clip; stage sum matches e2e (no hidden overhead).
The conditioning image is supplied via `LTX23_I2V_IMAGE` (the script
errors out with a usage message if unset) so no binary asset is added
to the tree.
Add `examples/inference/basic/basic_ltx2_3_distilled_i2v.py`: a single-GPU LTX-2.3 distilled image-to-video example with torch.compile fully enabled, two warmup runs to settle Inductor's per-shape autotune, two measured runs, and a per-stage timing breakdown. Fills a gap on `main`: the existing `basic_ltx2*.py` examples are t2v-only and target LTX-2.0 distilled. After PR hao-ai-lab#1397 merged LTX-2.3, there was no copy-paste example for the LTX-2.3 i2v path with compile + benchmark plumbing wired in. The script reads the conditioning image from `LTX23_I2V_IMAGE` (errors out with a helpful message if unset) and uses a generic fashion-runway prompt that the user can override via `LTX23_I2V_PROMPT`. Defaults match the production recipe documented in the docstring: 8 denoise + 3 refine steps, CFG=1, 832x1280 portrait, 121 frames @ 24fps. Includes a comment calling out the Blackwell `shape_padding=False` requirement and the `env -u LD_LIBRARY_PATH` launch tip from prior experience on GB200.
hao-ai-lab#1428 fixed this exact assertion in fastvideo/tests/api/test_presets.py but missed the identical one in tests/local_tests/. Since local_tests are excluded from CI, test_ltx2_typed_surface_preflight has been failing since ltx2_3_base was registered in hao-ai-lab#1397.
hao-ai-lab#1428 fixed this exact assertion in fastvideo/tests/api/test_presets.py but missed the identical one in tests/local_tests/. Since local_tests are excluded from CI, test_ltx2_typed_surface_preflight has been failing since ltx2_3_base was registered in hao-ai-lab#1397.
Summary
Adds LTX-2.3 transformer support on top of the existing LTX-2 implementation as a config-gated extension — every new field defaults off, so with defaults the model reproduces LTX-2.0 exactly.
Changes
models/dits/ltx2.py,configs/models/dits/ltx2.py): per-head gated attention (apply_gated_attention→to_gate_logits, distinct from upstream'sto_gate_compressVSA gate); cross-attention AdaLN (cross_attention_adaln→ 6→9-row scale_shift table + prompt-side modulation, fed by asigma/prompt_timesteppath); STG block 28 (stg_block_idx) + per-sample STG mask;StageAwareRMSNormrefine fast-path (defensive QuACK import → torch rms_norm fallback).models/encoders/gemma.py, config):caption_proj_before_connector, gated connector, per-modality feature extractor.models/dits/ltx2.pyforward +pipelines/basic/ltx2/stages/ltx2_denoising.py):video_sigma/audio_sigmapopulateModality.sigmato drive the prompt AdaLN. No-op for 2.0 (sigmaunused whenprompt_adaln is None).models/vaes/ltx2vae.py, +8 lines): apply the existingcompress_allchannel-multiplier scheme tocompress_space/compress_timetoo (the 2.3 base VAE grows decoder channels 128→1024 via these). No-op whenmultiplier=1(2.0).presets.py,registry.py):LTX2_3_BASEpreset (30 steps, CFG 3.0 + negative prompt, STG [28]) + 2.3 model entry + distilled detector aliases.Backward compatibility
Every 2.3 path is gated by a config flag (default off) or no-ops for 2.0 (sigma/VAE multipliers). Existing LTX2 tests pass on this branch.
Test plan — what IS validated
test_ltx2_stage_overrides(12),test_ltx2_continuation(17),test_nvfp4_ltx2_wiring(6)num_gpus=1,enable_torch_compile=False), e2e ≈ 103s, output matches promptNOT yet validated (reviewer attention)
original_seq_lenSP attention-mask path and does not port internal's diverged scheme (create_attention_mask_for_padding,skip_*_self_attn_blockslists). The new gated-attn / cross-attn-AdaLN / STG-mask paths are untested under SP — needs a multi-GPU run.fullgraph=True): compile-enable showed no setup-time graph break, but a full warmup+measured compiled run was not completed — graph-break behavior of the new 2.3 paths is unverified.Review notes
to_gate_logits(2.3 per-head gate onx) is distinct fromto_gate_compress(VSA gate oncontext) — kept separate, not cross-wired.if not skip_video_self_attnbranch became an always-compute multiply-mask (skip→×0 equivalent), soattn1always runs on the perturbed pass; confirm acceptable.compress_space/compress_timenow mirror the existingcompress_allchannel-multiplier convention.Excludes: i2v conditioning + LoRA fix + NVFP4 per-stage profile (already upstream), training/distillation, demo UI. Audio BWE is the companion PR (#1398).