Skip to content

Commit 69c10cf

Browse files
committed
address all review
1 parent 6c67800 commit 69c10cf

5 files changed

Lines changed: 266 additions & 171 deletions

File tree

src/diffusers/modular_pipelines/ltx/before_denoise.py

Lines changed: 48 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818
import torch
1919

2020
from ...models import LTXVideoTransformer3DModel
21-
from ...pipelines.ltx.pipeline_ltx import LTXPipeline
2221
from ...schedulers import FlowMatchEulerDiscreteScheduler
2322
from ...utils import logging
2423
from ...utils.torch_utils import randn_tensor
@@ -73,6 +72,43 @@ def retrieve_timesteps(
7372
return timesteps, num_inference_steps
7473

7574

75+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._pack_latents
76+
def _pack_latents(latents: torch.Tensor, patch_size: int = 1, patch_size_t: int = 1) -> torch.Tensor:
77+
# Unpacked latents of shape are [B, C, F, H, W] are patched into tokens of shape
78+
# [B, C, F // p_t, p_t, H // p, p, W // p, p].
79+
# The patch dimensions are then permuted and collapsed into the channel dimension of shape:
80+
# [B, F // p_t * H // p * W // p, C * p_t * p * p] (an ndim=3 tensor).
81+
# dim=0 is the batch size, dim=1 is the effective video sequence length,
82+
# dim=2 is the effective number of input features
83+
batch_size, num_channels, num_frames, height, width = latents.shape
84+
post_patch_num_frames = num_frames // patch_size_t
85+
post_patch_height = height // patch_size
86+
post_patch_width = width // patch_size
87+
latents = latents.reshape(
88+
batch_size,
89+
-1,
90+
post_patch_num_frames,
91+
patch_size_t,
92+
post_patch_height,
93+
patch_size,
94+
post_patch_width,
95+
patch_size,
96+
)
97+
latents = latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
98+
return latents
99+
100+
101+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._normalize_latents
102+
def _normalize_latents(
103+
latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
104+
) -> torch.Tensor:
105+
# Normalize latents across the channel dimension [B, C, F, H, W]
106+
latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
107+
latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
108+
latents = (latents - latents_mean) * scaling_factor / latents_std
109+
return latents
110+
111+
76112
class LTXTextInputStep(ModularPipelineBlocks):
77113
model_name = "ltx"
78114

@@ -94,7 +130,6 @@ def expected_components(self) -> list[ComponentSpec]:
94130
def inputs(self) -> list[InputParam]:
95131
return [
96132
InputParam.template("num_images_per_prompt", name="num_videos_per_prompt"),
97-
InputParam("guidance_scale", type_hint=float, default=3.0),
98133
InputParam.template("prompt_embeds", required=True),
99134
InputParam.template("prompt_embeds_mask", name="prompt_attention_mask"),
100135
InputParam.template("negative_prompt_embeds"),
@@ -112,11 +147,6 @@ def intermediate_outputs(self) -> list[OutputParam]:
112147
def __call__(self, components: LTXModularPipeline, state: PipelineState) -> PipelineState:
113148
block_state = self.get_block_state(state)
114149

115-
# Set guidance_scale on guider so CFG is configured correctly
116-
guidance_scale = getattr(block_state, "guidance_scale", 3.0)
117-
if hasattr(components, "guider") and components.guider is not None:
118-
components.guider.guidance_scale = guidance_scale
119-
120150
block_state.batch_size = block_state.prompt_embeds.shape[0]
121151
block_state.dtype = block_state.prompt_embeds.dtype
122152
num_videos = block_state.num_videos_per_prompt
@@ -257,7 +287,7 @@ def inputs(self) -> list[InputParam]:
257287
@property
258288
def intermediate_outputs(self) -> list[OutputParam]:
259289
return [
260-
OutputParam.template("latents"),
290+
OutputParam("latents", type_hint=torch.Tensor),
261291
]
262292

263293
@torch.no_grad()
@@ -279,7 +309,7 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
279309
block_state.latents = randn_tensor(
280310
shape, generator=block_state.generator, device=device, dtype=torch.float32
281311
)
282-
block_state.latents = LTXPipeline._pack_latents(
312+
block_state.latents = _pack_latents(
283313
block_state.latents,
284314
components.transformer_spatial_patch_size,
285315
components.transformer_temporal_patch_size,
@@ -289,39 +319,19 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
289319
return components, state
290320

291321

292-
# Copied from diffusers.pipelines.stable_diffusion.pipeline_stable_diffusion_img2img.retrieve_latents
293-
def retrieve_latents(
294-
encoder_output: torch.Tensor, generator: torch.Generator | None = None, sample_mode: str = "sample"
295-
):
296-
if hasattr(encoder_output, "latent_dist") and sample_mode == "sample":
297-
return encoder_output.latent_dist.sample(generator)
298-
elif hasattr(encoder_output, "latent_dist") and sample_mode == "argmax":
299-
return encoder_output.latent_dist.mode()
300-
elif hasattr(encoder_output, "latents"):
301-
return encoder_output.latents
302-
else:
303-
raise AttributeError("Could not access latents of provided encoder_output")
304-
305-
306322
class LTXImage2VideoPrepareLatentsStep(ModularPipelineBlocks):
307323
model_name = "ltx"
308324

309325
@property
310326
def description(self) -> str:
311-
return "Prepare latents step for image-to-video: encodes the first frame and creates a conditioning mask"
312-
313-
@property
314-
def expected_components(self) -> list[ComponentSpec]:
315-
from ...models import AutoencoderKLLTXVideo
316-
317-
return [
318-
ComponentSpec("vae", AutoencoderKLLTXVideo),
319-
]
327+
return (
328+
"Prepare latents step for image-to-video: takes pre-encoded image latents and creates a conditioning mask"
329+
)
320330

321331
@property
322332
def inputs(self) -> list[InputParam]:
323333
return [
324-
InputParam.template("image"),
334+
InputParam("image_latents", type_hint=torch.Tensor, required=True),
325335
InputParam.template("height", default=512),
326336
InputParam.template("width", default=704),
327337
InputParam("num_frames", type_hint=int, default=161),
@@ -335,7 +345,7 @@ def inputs(self) -> list[InputParam]:
335345
@property
336346
def intermediate_outputs(self) -> list[OutputParam]:
337347
return [
338-
OutputParam.template("latents"),
348+
OutputParam("latents", type_hint=torch.Tensor),
339349
OutputParam("conditioning_mask", type_hint=torch.Tensor),
340350
]
341351

@@ -355,7 +365,7 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
355365
if block_state.latents is not None:
356366
conditioning_mask = block_state.latents.new_zeros(mask_shape)
357367
conditioning_mask[:, :, 0] = 1.0
358-
conditioning_mask = LTXPipeline._pack_latents(
368+
conditioning_mask = _pack_latents(
359369
conditioning_mask,
360370
components.transformer_spatial_patch_size,
361371
components.transformer_temporal_patch_size,
@@ -365,38 +375,9 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
365375
self.set_block_state(state, block_state)
366376
return components, state
367377

368-
image = block_state.image
369-
if not isinstance(image, torch.Tensor):
370-
from ...video_processor import VideoProcessor
371-
372-
processor = VideoProcessor(vae_scale_factor=components.vae_spatial_compression_ratio)
373-
image = processor.preprocess(image, height=block_state.height, width=block_state.width)
374-
image = image.to(device=device, dtype=torch.float32)
375-
376-
vae_dtype = components.vae.dtype
377-
378-
num_images = image.shape[0]
379-
if isinstance(block_state.generator, list):
380-
init_latents = [
381-
retrieve_latents(
382-
components.vae.encode(image[i].unsqueeze(0).unsqueeze(2).to(vae_dtype)), block_state.generator[i]
383-
)
384-
for i in range(num_images)
385-
]
386-
else:
387-
init_latents = [
388-
retrieve_latents(
389-
components.vae.encode(img.unsqueeze(0).unsqueeze(2).to(vae_dtype)), block_state.generator
390-
)
391-
for img in image
392-
]
393-
394-
init_latents = torch.cat(init_latents, dim=0).to(torch.float32)
378+
init_latents = block_state.image_latents.to(device=device, dtype=torch.float32)
395379
if init_latents.shape[0] < batch_size:
396380
init_latents = init_latents.repeat_interleave(batch_size // init_latents.shape[0], dim=0)
397-
init_latents = LTXPipeline._normalize_latents(
398-
init_latents, components.vae.latents_mean, components.vae.latents_std
399-
)
400381
init_latents = init_latents.repeat(1, 1, num_frames, 1, 1)
401382

402383
actual_mask_shape = (
@@ -412,12 +393,12 @@ def __call__(self, components: LTXModularPipeline, state: PipelineState) -> Pipe
412393
noise = randn_tensor(init_latents.shape, generator=block_state.generator, device=device, dtype=torch.float32)
413394
latents = init_latents * conditioning_mask + noise * (1 - conditioning_mask)
414395

415-
conditioning_mask = LTXPipeline._pack_latents(
396+
conditioning_mask = _pack_latents(
416397
conditioning_mask,
417398
components.transformer_spatial_patch_size,
418399
components.transformer_temporal_patch_size,
419400
).squeeze(-1)
420-
latents = LTXPipeline._pack_latents(
401+
latents = _pack_latents(
421402
latents,
422403
components.transformer_spatial_patch_size,
423404
components.transformer_temporal_patch_size,

src/diffusers/modular_pipelines/ltx/decoders.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
from ...configuration_utils import FrozenDict
2020
from ...models import AutoencoderKLLTXVideo
21-
from ...pipelines.ltx.pipeline_ltx import LTXPipeline
2221
from ...utils import logging
2322
from ...utils.torch_utils import randn_tensor
2423
from ...video_processor import VideoProcessor
@@ -29,6 +28,31 @@
2928
logger = logging.get_logger(__name__)
3029

3130

31+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._unpack_latents
32+
def _unpack_latents(
33+
latents: torch.Tensor, num_frames: int, height: int, width: int, patch_size: int = 1, patch_size_t: int = 1
34+
) -> torch.Tensor:
35+
# Packed latents of shape [B, S, D] (S is the effective video sequence length,
36+
# D is the effective feature dimensions) are unpacked and reshaped into a video tensor
37+
# of shape [B, C, F, H, W]. This is the inverse operation of what happens in the
38+
# `_pack_latents` method.
39+
batch_size = latents.size(0)
40+
latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size)
41+
latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
42+
return latents
43+
44+
45+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._denormalize_latents
46+
def _denormalize_latents(
47+
latents: torch.Tensor, latents_mean: torch.Tensor, latents_std: torch.Tensor, scaling_factor: float = 1.0
48+
) -> torch.Tensor:
49+
# Denormalize latents across the channel dimension [B, C, F, H, W]
50+
latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
51+
latents_std = latents_std.view(1, -1, 1, 1, 1).to(latents.device, latents.dtype)
52+
latents = latents * latents_std / scaling_factor + latents_mean
53+
return latents
54+
55+
3256
class LTXVaeDecoderStep(ModularPipelineBlocks):
3357
model_name = "ltx"
3458

@@ -74,11 +98,6 @@ def __call__(self, components, state: PipelineState) -> PipelineState:
7498

7599
latents = block_state.latents
76100

77-
if block_state.output_type == "latent":
78-
block_state.videos = latents
79-
self.set_block_state(state, block_state)
80-
return components, state
81-
82101
height = block_state.height
83102
width = block_state.width
84103
num_frames = block_state.num_frames
@@ -87,17 +106,15 @@ def __call__(self, components, state: PipelineState) -> PipelineState:
87106
latent_height = height // components.vae_spatial_compression_ratio
88107
latent_width = width // components.vae_spatial_compression_ratio
89108

90-
latents = LTXPipeline._unpack_latents(
109+
latents = _unpack_latents(
91110
latents,
92111
latent_num_frames,
93112
latent_height,
94113
latent_width,
95114
components.transformer_spatial_patch_size,
96115
components.transformer_temporal_patch_size,
97116
)
98-
latents = LTXPipeline._denormalize_latents(
99-
latents, vae.latents_mean, vae.latents_std, vae.config.scaling_factor
100-
)
117+
latents = _denormalize_latents(latents, vae.latents_mean, vae.latents_std, vae.config.scaling_factor)
101118
latents = latents.to(block_state.dtype)
102119

103120
if not vae.config.timestep_conditioning:

src/diffusers/modular_pipelines/ltx/denoise.py

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,46 @@
3434
logger = logging.get_logger(__name__)
3535

3636

37+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._pack_latents
38+
def _pack_latents(latents: torch.Tensor, patch_size: int = 1, patch_size_t: int = 1) -> torch.Tensor:
39+
# Unpacked latents of shape are [B, C, F, H, W] are patched into tokens of shape
40+
# [B, C, F // p_t, p_t, H // p, p, W // p, p].
41+
# The patch dimensions are then permuted and collapsed into the channel dimension of shape:
42+
# [B, F // p_t * H // p * W // p, C * p_t * p * p] (an ndim=3 tensor).
43+
# dim=0 is the batch size, dim=1 is the effective video sequence length,
44+
# dim=2 is the effective number of input features
45+
batch_size, num_channels, num_frames, height, width = latents.shape
46+
post_patch_num_frames = num_frames // patch_size_t
47+
post_patch_height = height // patch_size
48+
post_patch_width = width // patch_size
49+
latents = latents.reshape(
50+
batch_size,
51+
-1,
52+
post_patch_num_frames,
53+
patch_size_t,
54+
post_patch_height,
55+
patch_size,
56+
post_patch_width,
57+
patch_size,
58+
)
59+
latents = latents.permute(0, 2, 4, 6, 1, 3, 5, 7).flatten(4, 7).flatten(1, 3)
60+
return latents
61+
62+
63+
# Copied from diffusers.pipelines.ltx.pipeline_ltx.LTXPipeline._unpack_latents
64+
def _unpack_latents(
65+
latents: torch.Tensor, num_frames: int, height: int, width: int, patch_size: int = 1, patch_size_t: int = 1
66+
) -> torch.Tensor:
67+
# Packed latents of shape [B, S, D] (S is the effective video sequence length,
68+
# D is the effective feature dimensions) are unpacked and reshaped into a video tensor
69+
# of shape [B, C, F, H, W]. This is the inverse operation of what happens in the
70+
# `_pack_latents` method.
71+
batch_size = latents.size(0)
72+
latents = latents.reshape(batch_size, num_frames, height, width, -1, patch_size_t, patch_size, patch_size)
73+
latents = latents.permute(0, 4, 1, 5, 2, 6, 3, 7).flatten(6, 7).flatten(4, 5).flatten(2, 3)
74+
return latents
75+
76+
3777
class LTXLoopBeforeDenoiser(ModularPipelineBlocks):
3878
model_name = "ltx"
3979

@@ -413,21 +453,19 @@ def inputs(self) -> list[InputParam]:
413453

414454
@torch.no_grad()
415455
def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: int, t: torch.Tensor):
416-
from ...pipelines.ltx.pipeline_ltx import LTXPipeline
417-
418456
latent_num_frames = (block_state.num_frames - 1) // components.vae_temporal_compression_ratio + 1
419457
latent_height = block_state.height // components.vae_spatial_compression_ratio
420458
latent_width = block_state.width // components.vae_spatial_compression_ratio
421459

422-
noise_pred = LTXPipeline._unpack_latents(
460+
noise_pred = _unpack_latents(
423461
block_state.noise_pred,
424462
latent_num_frames,
425463
latent_height,
426464
latent_width,
427465
components.transformer_spatial_patch_size,
428466
components.transformer_temporal_patch_size,
429467
)
430-
latents = LTXPipeline._unpack_latents(
468+
latents = _unpack_latents(
431469
block_state.latents,
432470
latent_num_frames,
433471
latent_height,
@@ -441,7 +479,7 @@ def __call__(self, components: LTXModularPipeline, block_state: BlockState, i: i
441479
pred_latents = components.scheduler.step(noise_pred, t, noise_latents, return_dict=False)[0]
442480

443481
latents = torch.cat([latents[:, :, :1], pred_latents], dim=2)
444-
block_state.latents = LTXPipeline._pack_latents(
482+
block_state.latents = _pack_latents(
445483
latents,
446484
components.transformer_spatial_patch_size,
447485
components.transformer_temporal_patch_size,

0 commit comments

Comments
 (0)