[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>
|
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.)