Uh oh!
There was an error while loading. Please reload this page.
[modular] LTX-2.5: two-stage generation as one pipeline - #14612
Conversation
Add `LTX25TwoStageBlocks` / `LTX25TwoStageModularPipeline`: the distilled two-stage recipe (first pass, 2x latent upsample, second pass, diffusion decode) as a single modular pipeline for every workflow `LTX25AutoBlocks` supports. The stages are ordinary blocks that can be popped and run on their own. - split the shared LTX-2 leaves into first-pass / second-pass blocks (`LTX2Stage2PrepareLatentsStep`, `LTX2Stage2PrepareAudioLatentsStep`, `LTX2ConditionStage2PrepareLatentsStep`) with `sigmas_name` / `sigmas_default` init arguments instead of branching on `latents` inside one block - every core-denoise group takes and leaves latents in the VAE form: `LTX2UnpackLatentsStep` closes each group, encoders normalize and decoders denormalize, `LTX2LatentUpsampleStep` bridges the passes - `modular_blocks_ltx25.py` is self-contained (no imports from the LTX-2 preset), with the distilled schedules as defaults and no `num_inference_steps`; `LTX25ModularPipeline` carries the LTX-2.5 latent statistics as the fallback for stages run without an autoencoder - geometry and statistics come from pipeline properties instead of declaring `vae` / `audio_vae` in denoise-side blocks; `use_cross_timestep` is a pipeline property; `batch_size` / `dtype` come from the text input step; the in-context attention mask is built inside the prepare-latents block - agent guide: gotcha on latent form across block boundaries Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
HuggingFaceDocBuilderDev
commented
Sep 6, 2026
The docs for this PR live here. All of your documentation changes will be reflected on that endpoint. The docs are available until 30 days after the last update. |
…ted patterns Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… the official recipe Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
| generator = block_state.generator[0] if isinstance(block_state.generator, list) else block_state.generator | ||
| all_latents, all_coords, all_cross_masks, token_counts = [], [], [], [] | ||
| reference_latents = [] |
There was a problem hiding this comment.
LTX2ReferenceEncoderStep did these 4 things:
(1) encode each reference -> reference latent (normalized)
(2) pack + concat
(2) compute reference_coords
(3) build reference_cross_mask
we only keep the step(1) here in encoder step, move the packing and the rest of the stuff to core denoise blocks (i.e.LTX2InContextPrepareLatentsStep)
| return noise_scale * noise + (1 - noise_scale) * latents | ||
| def _downsample_mask_to_latent( |
There was a problem hiding this comment.
moved from LTX2ReferenceEncoderStep see https://github.com/huggingface/diffusers/pull/14612/changes#r3954943536
| return latents | ||
| def _normalize_audio_latents( |
There was a problem hiding this comment.
We only need to accept normalized latents now - so this function is not needed here
norm/denorm should be part of vae blocks (encoders.py and decoders.py)
this changes what stage-2 accepts: it no longer takes denormalized latents (the standard pipeline output for output_type="latents")
| # auto_docstring | ||
| class LTX2AutoBuildVideoSelfAttentionMaskStep(ConditionalPipelineBlocks): |
There was a problem hiding this comment.
this is absorbed into LTX2InContextPrepareLatentsStep
| return latent_mask.reshape(b, latent_num_frames * latent_height * latent_width) | ||
| def _build_video_self_attention_mask( |
There was a problem hiding this comment.
this is the old LTX2BuildVideoSelfAttentionMaskStep + the cross-mask computation moved from reference encoder; now called from LTX2InContextPrepareLatentsStep
| "non-`None` default across its blocks, so a literal 0.0 would shadow the condition workflow's " | ||
| "`None -> sigmas[0] or 1.0` resolution wherever the two share a blockset (`LTX2AutoBlocks`). The " | ||
| "resolved value is written back to state for `LTX2PrepareAudioLatentsStep`." | ||
| "Samples the packed video noise latents for a first pass of text-to-video generation. Refining " |
There was a problem hiding this comment.
split LTX2PrepareLatentsStep into two blocks:
this one is only for stage1 now, so basically just generate the initial noise, "latents" input now means the pre-generated initial noise consistent with how we define this input in our other pipelines
it no longer takes the stage1 output as latents and re-noises it (that's moved to LTX2Stage2PrepareLatentsStep
…latents input Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCdbvRpL9fv3h3WwSUPpfS
…cstring Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HCdbvRpL9fv3h3WwSUPpfS
| return components, state | ||
| class LTX2Stage2PrepareAudioLatentsStep(ModularPipelineBlocks): |
There was a problem hiding this comment.
LTX2PrepareAudioLatentsStep also split into stage1 + stage2
Adds
LTX25TwoStageBlocksthe distilled two-stage recipe as a single modular pipeline:for every workflow
LTX25AutoBlockssupports (t2v / i2v / condition / in-context). It is assembled from the same leaf blocks asLTX25AutoBlocks, and the stages are ordinary blocks, so you can pop them and run a pass on its own.setup
example usage1: Single stage, everything at its default
pipe.blocks.get_workflow("text2video").inputs— the distilled schedule is the default,num_framesis predicted by the duration head when omitted, and there is nonum_inference_steps: the checkpoint runs a fixed sigma schedule, so there is no step count to choose.promptnegative_prompt,max_sequence_lengthNone,1024num_frames,min_seconds,max_seconds,frame_rateNone(auto),1.0,20.0,24.0sigmas,timestepsDISTILLED_SIGMA_VALUES,Noneheight,width512,704num_videos_per_prompt,generator,attention_kwargs,output_type1,None,None,"pil"example usage 2: Two stages as one call
pipe.blocks.get_workflow("text2video").inputs— everything at its default again. The second pass reads its schedule under its own names (stage_2_*) so both passes can sit in one pipeline, and takes itsheight/width/num_framesfrom the upsampled latents rather than as inputs:promptnegative_prompt,max_sequence_lengthNone,1024num_frames,min_seconds,max_seconds,frame_rateNone(auto),1.0,20.0,24.0sigmas,timestepsDISTILLED_SIGMA_VALUES,Noneheight,width512,704(the first pass; the output is 2x)stage_2_sigmas,stage_2_timestepsSTAGE_2_DISTILLED_SIGMA_VALUES,Nonenoise_scaleNone→stage_2_sigmas[0], the level the upsampled latents are re-noised tonum_videos_per_prompt,generator,attention_kwargs,output_type1,None,None,"pil"example usage3: Two stages separately
you can pop each stage into their own pipelines and hand the state along. For instance, preview the first pass (and re-run it as many times as you like) before spending the second pass on it:
stage_2.inputs— what the popped second pass takes on its own (this isLTX25AutoStage2CoreDenoiseStep, so the inputs are the union of its t2v / i2v / condition branches). Everything comes from the first pass's state; its own settings are at their defaults:latents,audio_latentsstage_1(latentsthroughupsample)connector_prompt_embeds,connector_audio_prompt_embeds,connector_attention_masktext_encoder+inputnegative_connector_*(3)Nonebatch_size,dtypeinputstage_2_sigmas,stage_2_timestepsSTAGE_2_DISTILLED_SIGMA_VALUES,Nonenoise_scaleNone→stage_2_sigmas[0]frame_rate,num_videos_per_prompt,generator,attention_kwargs24.0,1,None,Noneimage_latents/condition_latents,condition_strengths,condition_indices,condition_pixel_framesstage_2_*encodersNoneNo
height/width/num_frames: the second pass reads them off the latents. (upsamplealone takes justlatents;decodetakeslatents,audio_latents,generator,output_type.)