AINovice2005 commited on
Commit
d80ed59
·
verified ·
1 Parent(s): 4b180cd

Upload folder using huggingface_hub

Browse files
__init__.py ADDED
File without changes
__pycache__/block.cpython-312.pyc ADDED
Binary file (1.91 kB). View file
 
__pycache__/modeling_prunavae.cpython-312.pyc ADDED
Binary file (21.7 kB). View file
 
block.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from diffusers.modular_pipelines import ModularPipelineBlocks, PipelineState
2
+ from diffusers.modular_pipelines.modular_pipeline_utils import ComponentSpec, InputParam, OutputParam
3
+
4
+ try:
5
+ from .modeling_prunavae import PrunaAutoencoderKLLTX2Video
6
+ except ImportError:
7
+ from modeling_prunavae import PrunaAutoencoderKLLTX2Video
8
+
9
+
10
+ class LoadPrunaVAE(ModularPipelineBlocks):
11
+ model_name = "PrunaVAED"
12
+
13
+ @property
14
+ def description(self) -> str:
15
+ return "Declares the Pruna LTX-2 VAE as an expected pipeline component."
16
+
17
+ @property
18
+ def expected_components(self) -> list[ComponentSpec]:
19
+ return [
20
+ ComponentSpec(
21
+ "vae",
22
+ PrunaAutoencoderKLLTX2Video,
23
+ pretrained_model_name_or_path="PrunaAI/PrunaVAED",
24
+ subfolder="vae",
25
+ )
26
+ ]
27
+
28
+ @property
29
+ def inputs(self) -> list[InputParam]:
30
+ return []
31
+
32
+ @property
33
+ def intermediate_outputs(self) -> list[OutputParam]:
34
+ return []
35
+
36
+ def __call__(self, components, state: PipelineState):
37
+ # Nothing to compute here -- this block's only role is to make
38
+ # `components.vae` (a PrunaAutoencoderKLLTX2Video) available to
39
+ # every block downstream in the pipeline. Blocks that actually
40
+ # need it (e.g. a decode step) read it directly off `components`,
41
+ # not off `state`.
42
+ return components, state
modeling_prunavae.py ADDED
@@ -0,0 +1,672 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2025 The Lightricks team, The HuggingFace Team, and Pruna AI.
2
+ # All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """
16
+ Pruna variant of the LTX-2 video decoder / autoencoder.
17
+
18
+ This module is intentionally kept structurally identical to upstream
19
+ Diffusers' ``autoencoder_kl_ltx2.py``. It exists to support checkpoints
20
+ produced by ``PrunaVAED``, which prunes the internal ResNet width of the
21
+ decoder's up-blocks while preserving wider skip connections between decoder
22
+ stages than stock LTX-2 assumes.
23
+
24
+ There are exactly three intentional deviations from upstream, each isolated
25
+ to a single class so that future rebases against upstream Diffusers can diff
26
+ each class independently:
27
+
28
+ 1. ``PrunaLTX2VideoUpBlock3d``
29
+ The ``conv_in`` projection is constructed against the pre-upsampler
30
+ channel width (``out_channels * upscale_factor``) rather than the
31
+ ResNet width (``out_channels``). Upstream implicitly assumes
32
+ ``in_channels == out_channels`` is the only case that needs no
33
+ projection; Pruna's decoder keeps wider skip tensors between stages, so
34
+ that assumption no longer holds.
35
+
36
+ 2. ``PrunaLTX2VideoDecoder3d``
37
+ Up-block input widths are tracked via an explicit ``current_channels``
38
+ accumulator (the true width of the tensor leaving the previous stage)
39
+ instead of being re-derived from ``block_out_channels[i] //
40
+ upsample_factor[i]``. This is the direct consequence of deviation (1):
41
+ once skip widths are no longer implicitly recoverable from
42
+ ``block_out_channels`` alone, the decoder must track them explicitly.
43
+ It also instantiates ``PrunaLTX2VideoUpBlock3d`` in place of the
44
+ upstream ``LTX2VideoUpBlock3d``.
45
+
46
+ 3. ``PrunaAutoencoderKLLTX2Video``
47
+ The constructor is otherwise identical to
48
+ ``AutoencoderKLLTX2Video.__init__``; the only change is that
49
+ ``self.decoder`` is built from ``PrunaLTX2VideoDecoder3d`` instead of
50
+ ``LTX2VideoDecoder3d``. Every other method (``encode``, ``decode``,
51
+ ``forward``, ``tiled_encode``, ``tiled_decode``, etc.) is inherited
52
+ unchanged.
53
+
54
+ ``forward()`` is unchanged in every class below relative to upstream: none
55
+ of the three deviations touch execution semantics, only module
56
+ construction. This checkpoint topology matches what ``PrunaVAED`` produces
57
+ while remaining forward-compatible with the original LTX-2 decoder.
58
+ """
59
+
60
+ from __future__ import annotations
61
+
62
+ import torch
63
+ import torch.nn as nn
64
+
65
+ from diffusers.configuration_utils import register_to_config
66
+ from diffusers.models.autoencoders.autoencoder_kl_ltx2 import (
67
+ AutoencoderKLLTX2Video,
68
+ LTX2VideoCausalConv3d,
69
+ LTX2VideoMidBlock3d,
70
+ LTX2VideoResnetBlock3d,
71
+ LTX2VideoUpsampler3d,
72
+ PerChannelRMSNorm,
73
+ )
74
+ from diffusers.models.embeddings import PixArtAlphaCombinedTimestepSizeEmbeddings
75
+
76
+
77
+ # Deliberately NOT imported, since this module replaces them:
78
+ # LTX2VideoDecoder3d, LTX2VideoUpBlock3d
79
+
80
+
81
+ class PrunaLTX2VideoUpBlock3d(nn.Module):
82
+ r"""
83
+ Pruna variant of ``LTX2VideoUpBlock3d``.
84
+
85
+ This implementation differs from the upstream Diffusers version in one
86
+ important way:
87
+
88
+ The optional ``conv_in`` projection operates on the **pre-upsampler**
89
+ channel width rather than the ResNet width.
90
+
91
+ Upstream compares
92
+
93
+ in_channels != out_channels
94
+
95
+ which assumes the incoming tensor has already been pruned down to the
96
+ block's internal ResNet width before it arrives.
97
+
98
+ Pruna preserves wider skip connections between decoder stages and only
99
+ prunes the internal ResNet channels, so we compare against the
100
+ pre-upsampler width instead:
101
+
102
+ pre_upsample_channels = out_channels * upscale_factor
103
+
104
+ Example
105
+ -------
106
+ incoming tensor : 384 channels
107
+ ResNet width : 128 channels
108
+ upscale_factor : 2
109
+
110
+ The ResNet therefore expects a 256-channel tensor before the upsampler,
111
+ requiring a 384 -> 256 projection that upstream's narrower comparison
112
+ would never trigger.
113
+
114
+ This exactly matches the checkpoint topology produced by ``PrunaVAED``
115
+ while remaining forward-compatible with the original LTX-2 decoder.
116
+
117
+ Args:
118
+ in_channels (`int`):
119
+ Number of input channels.
120
+ out_channels (`int`, *optional*):
121
+ Number of output channels. If None, defaults to `in_channels`.
122
+ num_layers (`int`, defaults to `1`):
123
+ Number of resnet layers.
124
+ dropout (`float`, defaults to `0.0`):
125
+ Dropout rate.
126
+ resnet_eps (`float`, defaults to `1e-6`):
127
+ Epsilon value for normalization layers.
128
+ resnet_act_fn (`str`, defaults to `"swish"`):
129
+ Activation function to use.
130
+ spatio_temporal_scale (`bool`, defaults to `True`):
131
+ Whether or not to use an upsampling layer. If not used, output
132
+ dimension would be same as input dimension.
133
+ upscale_factor (`int`, defaults to `1`):
134
+ Channel upscale factor applied by the upsampler.
135
+ """
136
+
137
+ _supports_gradient_checkpointing = True
138
+
139
+ def __init__(
140
+ self,
141
+ in_channels: int,
142
+ out_channels: int | None = None,
143
+ num_layers: int = 1,
144
+ dropout: float = 0.0,
145
+ resnet_eps: float = 1e-6,
146
+ resnet_act_fn: str = "swish",
147
+ spatio_temporal_scale: bool = True,
148
+ upsample_type: str = "spatiotemporal",
149
+ inject_noise: bool = False,
150
+ timestep_conditioning: bool = False,
151
+ upsample_residual: bool = False,
152
+ upscale_factor: int = 1,
153
+ spatial_padding_mode: str = "zeros",
154
+ ):
155
+ super().__init__()
156
+
157
+ out_channels = out_channels or in_channels
158
+
159
+ #
160
+ # ------------------------------------------------------------------
161
+ # PRUNA CHANGE (1 of 1 in this class)
162
+ #
163
+ # Width immediately before the upsampler.
164
+ #
165
+ # Stock Diffusers compares:
166
+ #
167
+ # in_channels != out_channels
168
+ #
169
+ # which assumes the incoming tensor has already been pruned.
170
+ #
171
+ # Pruna preserves wider skip tensors between decoder stages.
172
+ # Therefore we compare against the pre-upsampler width instead.
173
+ # ------------------------------------------------------------------
174
+ #
175
+ pre_upsample_channels = out_channels * upscale_factor
176
+
177
+ self.time_embedder = None
178
+ if timestep_conditioning:
179
+ self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(in_channels * 4, 0)
180
+
181
+ self.conv_in = None
182
+ if in_channels != pre_upsample_channels:
183
+ self.conv_in = LTX2VideoResnetBlock3d(
184
+ in_channels=in_channels,
185
+ out_channels=pre_upsample_channels,
186
+ dropout=dropout,
187
+ eps=resnet_eps,
188
+ non_linearity=resnet_act_fn,
189
+ inject_noise=inject_noise,
190
+ timestep_conditioning=timestep_conditioning,
191
+ spatial_padding_mode=spatial_padding_mode,
192
+ )
193
+
194
+ self.upsamplers = None
195
+ if spatio_temporal_scale:
196
+ self.upsamplers = nn.ModuleList()
197
+
198
+ if upsample_type == "spatial":
199
+ upsample_stride = (1, 2, 2)
200
+ elif upsample_type == "temporal":
201
+ upsample_stride = (2, 1, 1)
202
+ elif upsample_type == "spatiotemporal":
203
+ upsample_stride = (2, 2, 2)
204
+ else:
205
+ # Upstream leaves this branch implicit; making the failure
206
+ # explicit improves debuggability without changing behavior
207
+ # for any valid configuration.
208
+ raise ValueError(f"Unknown upsample_type: {upsample_type}")
209
+
210
+ self.upsamplers.append(
211
+ LTX2VideoUpsampler3d(
212
+ in_channels=pre_upsample_channels,
213
+ stride=upsample_stride,
214
+ residual=upsample_residual,
215
+ upscale_factor=upscale_factor,
216
+ spatial_padding_mode=spatial_padding_mode,
217
+ )
218
+ )
219
+
220
+ resnets = []
221
+ for _ in range(num_layers):
222
+ resnets.append(
223
+ LTX2VideoResnetBlock3d(
224
+ in_channels=out_channels,
225
+ out_channels=out_channels,
226
+ dropout=dropout,
227
+ eps=resnet_eps,
228
+ non_linearity=resnet_act_fn,
229
+ inject_noise=inject_noise,
230
+ timestep_conditioning=timestep_conditioning,
231
+ spatial_padding_mode=spatial_padding_mode,
232
+ )
233
+ )
234
+ self.resnets = nn.ModuleList(resnets)
235
+
236
+ self.gradient_checkpointing = False
237
+
238
+ # Identical to upstream `LTX2VideoUpBlock3d.forward` -- the Pruna change
239
+ # is purely in module construction (`__init__`), not execution.
240
+ def forward(
241
+ self,
242
+ hidden_states: torch.Tensor,
243
+ temb: torch.Tensor | None = None,
244
+ generator: torch.Generator | None = None,
245
+ causal: bool = True,
246
+ ) -> torch.Tensor:
247
+ if self.conv_in is not None:
248
+ hidden_states = self.conv_in(hidden_states, temb, generator, causal=causal)
249
+
250
+ if self.time_embedder is not None:
251
+ temb = self.time_embedder(
252
+ timestep=temb.flatten(),
253
+ resolution=None,
254
+ aspect_ratio=None,
255
+ batch_size=hidden_states.size(0),
256
+ hidden_dtype=hidden_states.dtype,
257
+ )
258
+ temb = temb.view(hidden_states.size(0), -1, 1, 1, 1)
259
+
260
+ if self.upsamplers is not None:
261
+ for upsampler in self.upsamplers:
262
+ hidden_states = upsampler(hidden_states, causal=causal)
263
+
264
+ for resnet in self.resnets:
265
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
266
+ hidden_states = self._gradient_checkpointing_func(
267
+ resnet, hidden_states, temb, generator, causal
268
+ )
269
+ else:
270
+ hidden_states = resnet(hidden_states, temb, generator, causal=causal)
271
+
272
+ return hidden_states
273
+
274
+
275
+ class PrunaLTX2VideoDecoder3d(nn.Module):
276
+ r"""
277
+ Pruna variant of ``LTX2VideoDecoder3d``.
278
+
279
+ Deliberately **not** a subclass of ``LTX2VideoDecoder3d``: the entire
280
+ `__init__` would be overridden anyway (the up-block construction loop
281
+ must change), so subclassing would only add coupling to the upstream
282
+ constructor's private attributes while saving nothing but the ~50-line
283
+ `forward()` method, which is reproduced verbatim below instead.
284
+
285
+ There are exactly two semantic changes relative to upstream:
286
+
287
+ 1. Up-block input widths are tracked with an explicit
288
+ ``current_channels`` accumulator -- the true width of the tensor
289
+ leaving the previous stage -- rather than being re-derived from
290
+ ``block_out_channels[i] // upsample_factor[i]``. Upstream's
291
+ re-derivation implicitly assumes stage skip widths collapse to the
292
+ ResNet width; Pruna's decoder does not make that assumption.
293
+ 2. Each stage instantiates ``PrunaLTX2VideoUpBlock3d`` instead of
294
+ ``LTX2VideoUpBlock3d``.
295
+
296
+ Everything else -- ``conv_in``, ``mid_block``, ``norm_out``,
297
+ ``conv_out``, timestep conditioning, and ``forward()`` -- is copied
298
+ unchanged from upstream.
299
+
300
+ Args:
301
+ in_channels (`int`, defaults to 128):
302
+ Number of latent channels.
303
+ out_channels (`int`, defaults to 3):
304
+ Number of output channels.
305
+ block_out_channels (`tuple[int, ...]`, defaults to `(256, 512, 1024)`):
306
+ The number of output channels for each block.
307
+ spatio_temporal_scaling (`tuple[bool, ...]`, defaults to `(True, True, True)`):
308
+ Whether a block should contain spatio-temporal upscaling layers or not.
309
+ layers_per_block (`tuple[int, ...]`, defaults to `(5, 5, 5, 5)`):
310
+ The number of layers per block.
311
+ patch_size (`int`, defaults to `4`):
312
+ The size of spatial patches.
313
+ patch_size_t (`int`, defaults to `1`):
314
+ The size of temporal patches.
315
+ resnet_norm_eps (`float`, defaults to `1e-6`):
316
+ Epsilon value for ResNet normalization layers.
317
+ is_causal (`bool`, defaults to `False`):
318
+ Whether this layer behaves causally (future frames depend only on past frames) or not.
319
+ timestep_conditioning (`bool`, defaults to `False`):
320
+ Whether to condition the model on timesteps.
321
+ """
322
+
323
+ _supports_gradient_checkpointing = True
324
+
325
+ def __init__(
326
+ self,
327
+ in_channels: int = 128,
328
+ out_channels: int = 3,
329
+ block_out_channels: tuple[int, ...] = (256, 512, 1024),
330
+ spatio_temporal_scaling: bool | tuple[bool, ...] = (True, True, True),
331
+ layers_per_block: tuple[int, ...] = (5, 5, 5, 5),
332
+ upsample_type: tuple[str, ...] = ("spatiotemporal", "spatiotemporal", "spatiotemporal"),
333
+ patch_size: int = 4,
334
+ patch_size_t: int = 1,
335
+ resnet_norm_eps: float = 1e-6,
336
+ is_causal: bool = False,
337
+ inject_noise: bool | tuple[bool, ...] = (False, False, False),
338
+ timestep_conditioning: bool = False,
339
+ upsample_residual: bool | tuple[bool, ...] = (True, True, True),
340
+ upsample_factor: tuple[int, ...] = (2, 2, 2),
341
+ spatial_padding_mode: str = "reflect",
342
+ ) -> None:
343
+ super().__init__()
344
+ num_decoder_blocks = len(layers_per_block)
345
+ if isinstance(spatio_temporal_scaling, bool):
346
+ spatio_temporal_scaling = (spatio_temporal_scaling,) * (num_decoder_blocks - 1)
347
+ if isinstance(inject_noise, bool):
348
+ inject_noise = (inject_noise,) * num_decoder_blocks
349
+ if isinstance(upsample_residual, bool):
350
+ upsample_residual = (upsample_residual,) * (num_decoder_blocks - 1)
351
+
352
+ self.patch_size = patch_size
353
+ self.patch_size_t = patch_size_t
354
+ self.out_channels = out_channels * patch_size**2
355
+ self.is_causal = is_causal
356
+
357
+ block_out_channels = tuple(reversed(block_out_channels))
358
+ spatio_temporal_scaling = tuple(reversed(spatio_temporal_scaling))
359
+ layers_per_block = tuple(reversed(layers_per_block))
360
+ inject_noise = tuple(reversed(inject_noise))
361
+ upsample_residual = tuple(reversed(upsample_residual))
362
+ upsample_factor = tuple(reversed(upsample_factor))
363
+ output_channel = block_out_channels[0]
364
+
365
+ self.conv_in = LTX2VideoCausalConv3d(
366
+ in_channels=in_channels,
367
+ out_channels=output_channel,
368
+ kernel_size=3,
369
+ stride=1,
370
+ spatial_padding_mode=spatial_padding_mode,
371
+ )
372
+
373
+ self.mid_block = LTX2VideoMidBlock3d(
374
+ in_channels=output_channel,
375
+ num_layers=layers_per_block[0],
376
+ resnet_eps=resnet_norm_eps,
377
+ inject_noise=inject_noise[0],
378
+ timestep_conditioning=timestep_conditioning,
379
+ spatial_padding_mode=spatial_padding_mode,
380
+ )
381
+
382
+ # up blocks
383
+ num_block_out_channels = len(block_out_channels)
384
+ self.up_blocks = nn.ModuleList([])
385
+
386
+ #
387
+ # ------------------------------------------------------------------
388
+ # PRUNA CHANGE (1 of 2 in this class)
389
+ #
390
+ # Upstream re-derives each stage's input width from
391
+ # `block_out_channels[i] // upsample_factor[i]`, which implicitly
392
+ # assumes the tensor leaving a stage is exactly that stage's ResNet
393
+ # width. Pruna's decoder keeps wider skip connections between
394
+ # stages, so we instead track the *actual* channel width of the
395
+ # tensor as it flows from stage to stage.
396
+ #
397
+ # After `conv_in` + `mid_block`, that width is `output_channel`
398
+ # (== block_out_channels[0]); after each up-block it becomes that
399
+ # block's `resnet_width`.
400
+ # ------------------------------------------------------------------
401
+ #
402
+ current_channels = output_channel
403
+
404
+ for i in range(num_block_out_channels):
405
+ resnet_width = block_out_channels[i] // upsample_factor[i]
406
+
407
+ #
408
+ # ------------------------------------------------------------------
409
+ # PRUNA CHANGE (2 of 2 in this class)
410
+ #
411
+ # Instantiate the Pruna up-block, which projects from the true
412
+ # incoming skip width (`current_channels`) rather than assuming
413
+ # it already equals the ResNet width.
414
+ # ------------------------------------------------------------------
415
+ #
416
+ up_block = PrunaLTX2VideoUpBlock3d(
417
+ in_channels=current_channels,
418
+ out_channels=resnet_width,
419
+ num_layers=layers_per_block[i + 1],
420
+ resnet_eps=resnet_norm_eps,
421
+ spatio_temporal_scale=spatio_temporal_scaling[i],
422
+ upsample_type=upsample_type[i],
423
+ inject_noise=inject_noise[i + 1],
424
+ timestep_conditioning=timestep_conditioning,
425
+ upsample_residual=upsample_residual[i],
426
+ upscale_factor=upsample_factor[i],
427
+ spatial_padding_mode=spatial_padding_mode,
428
+ )
429
+
430
+ self.up_blocks.append(up_block)
431
+ current_channels = resnet_width
432
+
433
+ output_channel = current_channels
434
+
435
+ # out
436
+ self.norm_out = PerChannelRMSNorm()
437
+ self.conv_act = nn.SiLU()
438
+ self.conv_out = LTX2VideoCausalConv3d(
439
+ in_channels=output_channel,
440
+ out_channels=self.out_channels,
441
+ kernel_size=3,
442
+ stride=1,
443
+ spatial_padding_mode=spatial_padding_mode,
444
+ )
445
+
446
+ # timestep embedding
447
+ self.time_embedder = None
448
+ self.scale_shift_table = None
449
+ self.timestep_scale_multiplier = None
450
+ if timestep_conditioning:
451
+ self.timestep_scale_multiplier = nn.Parameter(torch.tensor(1000.0, dtype=torch.float32))
452
+ self.time_embedder = PixArtAlphaCombinedTimestepSizeEmbeddings(output_channel * 2, 0)
453
+ self.scale_shift_table = nn.Parameter(torch.randn(2, output_channel) / output_channel**0.5)
454
+
455
+ self.gradient_checkpointing = False
456
+
457
+ # Identical to upstream `LTX2VideoDecoder3d.forward` -- both Pruna
458
+ # changes above are construction-time only.
459
+ def forward(
460
+ self,
461
+ hidden_states: torch.Tensor,
462
+ temb: torch.Tensor | None = None,
463
+ causal: bool | None = None,
464
+ ) -> torch.Tensor:
465
+ causal = causal or self.is_causal
466
+
467
+ hidden_states = self.conv_in(hidden_states, causal=causal)
468
+
469
+ if self.timestep_scale_multiplier is not None:
470
+ temb = temb * self.timestep_scale_multiplier
471
+
472
+ if torch.is_grad_enabled() and self.gradient_checkpointing:
473
+ hidden_states = self._gradient_checkpointing_func(self.mid_block, hidden_states, temb, None, causal)
474
+
475
+ for up_block in self.up_blocks:
476
+ hidden_states = self._gradient_checkpointing_func(up_block, hidden_states, temb, None, causal)
477
+ else:
478
+ hidden_states = self.mid_block(hidden_states, temb, causal=causal)
479
+
480
+ for up_block in self.up_blocks:
481
+ hidden_states = up_block(hidden_states, temb, causal=causal)
482
+
483
+ hidden_states = self.norm_out(hidden_states)
484
+
485
+ if self.time_embedder is not None:
486
+ temb = self.time_embedder(
487
+ timestep=temb.flatten(),
488
+ resolution=None,
489
+ aspect_ratio=None,
490
+ batch_size=hidden_states.size(0),
491
+ hidden_dtype=hidden_states.dtype,
492
+ )
493
+ temb = temb.view(hidden_states.size(0), -1, 1, 1, 1).unflatten(1, (2, -1))
494
+ temb = temb + self.scale_shift_table[None, ..., None, None, None]
495
+ shift, scale = temb.unbind(dim=1)
496
+ hidden_states = hidden_states * (1 + scale) + shift
497
+
498
+ hidden_states = self.conv_act(hidden_states)
499
+ hidden_states = self.conv_out(hidden_states, causal=causal)
500
+
501
+ p = self.patch_size
502
+ p_t = self.patch_size_t
503
+
504
+ batch_size, num_channels, num_frames, height, width = hidden_states.shape
505
+ hidden_states = hidden_states.reshape(batch_size, -1, p_t, p, p, num_frames, height, width)
506
+ hidden_states = hidden_states.permute(0, 1, 5, 2, 6, 4, 7, 3).flatten(6, 7).flatten(4, 5).flatten(2, 3)
507
+
508
+ return hidden_states
509
+
510
+
511
+ class PrunaAutoencoderKLLTX2Video(AutoencoderKLLTX2Video):
512
+ r"""
513
+ Pruna variant of ``AutoencoderKLLTX2Video``.
514
+
515
+ Differs from upstream in exactly one constructor line: ``self.decoder``
516
+ is built from :class:`PrunaLTX2VideoDecoder3d` instead of
517
+ ``LTX2VideoDecoder3d``. The encoder, buffers, tiling configuration, and
518
+ framewise decoding setup are all copied verbatim from upstream.
519
+
520
+ Every other method -- ``encode``, ``decode``, ``forward``,
521
+ ``tiled_encode``, ``tiled_decode``, ``enable_tiling``,
522
+ ``enable_slicing``, etc. -- is inherited unchanged from
523
+ ``AutoencoderKLLTX2Video``, since none of them depend on the decoder's
524
+ internal channel-width bookkeeping.
525
+ """
526
+
527
+ _supports_gradient_checkpointing = True
528
+
529
+ @register_to_config
530
+ def __init__(
531
+ self,
532
+ in_channels: int = 3,
533
+ out_channels: int = 3,
534
+ latent_channels: int = 128,
535
+ block_out_channels: tuple[int, ...] = (256, 512, 1024, 2048),
536
+ down_block_types: tuple[str, ...] = (
537
+ "LTX2VideoDownBlock3D",
538
+ "LTX2VideoDownBlock3D",
539
+ "LTX2VideoDownBlock3D",
540
+ "LTX2VideoDownBlock3D",
541
+ ),
542
+ decoder_block_out_channels: tuple[int, ...] = (256, 512, 1024),
543
+ layers_per_block: tuple[int, ...] = (4, 6, 6, 2, 2),
544
+ decoder_layers_per_block: tuple[int, ...] = (5, 5, 5, 5),
545
+ spatio_temporal_scaling: bool | tuple[bool, ...] = (True, True, True, True),
546
+ decoder_spatio_temporal_scaling: bool | tuple[bool, ...] = (True, True, True),
547
+ decoder_inject_noise: bool | tuple[bool, ...] = (False, False, False, False),
548
+ downsample_type: tuple[str, ...] = ("spatial", "temporal", "spatiotemporal", "spatiotemporal"),
549
+ upsample_type: tuple[str, ...] = ("spatiotemporal", "spatiotemporal", "spatiotemporal"),
550
+ upsample_residual: bool | tuple[bool, ...] = (True, True, True),
551
+ upsample_factor: tuple[int, ...] = (2, 2, 2),
552
+ timestep_conditioning: bool = False,
553
+ patch_size: int = 4,
554
+ patch_size_t: int = 1,
555
+ resnet_norm_eps: float = 1e-6,
556
+ scaling_factor: float = 1.0,
557
+ encoder_causal: bool = True,
558
+ decoder_causal: bool = True,
559
+ encoder_spatial_padding_mode: str = "zeros",
560
+ decoder_spatial_padding_mode: str = "reflect",
561
+ spatial_compression_ratio: int = None,
562
+ temporal_compression_ratio: int = None,
563
+ ) -> None:
564
+ # Bypass AutoencoderKLLTX2Video.__init__ (and its `self.decoder =
565
+ # LTX2VideoDecoder3d(...)` line) entirely; go straight to
566
+ # nn.Module.__init__ via the mixin chain, exactly as upstream does.
567
+ super(AutoencoderKLLTX2Video, self).__init__()
568
+
569
+ num_encoder_blocks = len(layers_per_block)
570
+ num_decoder_blocks = len(decoder_layers_per_block)
571
+ if isinstance(spatio_temporal_scaling, bool):
572
+ spatio_temporal_scaling = (spatio_temporal_scaling,) * (num_encoder_blocks - 1)
573
+ if isinstance(decoder_spatio_temporal_scaling, bool):
574
+ decoder_spatio_temporal_scaling = (decoder_spatio_temporal_scaling,) * (num_decoder_blocks - 1)
575
+ if isinstance(decoder_inject_noise, bool):
576
+ decoder_inject_noise = (decoder_inject_noise,) * num_decoder_blocks
577
+ if isinstance(upsample_residual, bool):
578
+ upsample_residual = (upsample_residual,) * (num_decoder_blocks - 1)
579
+
580
+ # Import the encoder + downstream block type lazily from upstream so
581
+ # this file never needs to redefine anything on the encoder side.
582
+ from diffusers.models.autoencoders.autoencoder_kl_ltx2 import LTX2VideoEncoder3d
583
+
584
+ self.encoder = LTX2VideoEncoder3d(
585
+ in_channels=in_channels,
586
+ out_channels=latent_channels,
587
+ block_out_channels=block_out_channels,
588
+ down_block_types=down_block_types,
589
+ spatio_temporal_scaling=spatio_temporal_scaling,
590
+ layers_per_block=layers_per_block,
591
+ downsample_type=downsample_type,
592
+ patch_size=patch_size,
593
+ patch_size_t=patch_size_t,
594
+ resnet_norm_eps=resnet_norm_eps,
595
+ is_causal=encoder_causal,
596
+ spatial_padding_mode=encoder_spatial_padding_mode,
597
+ )
598
+
599
+ #
600
+ # ------------------------------------------------------------------
601
+ # PRUNA CHANGE (the only change in this class)
602
+ #
603
+ # Instantiate PrunaLTX2VideoDecoder3d instead of LTX2VideoDecoder3d.
604
+ # Every argument passed is identical to upstream.
605
+ # ------------------------------------------------------------------
606
+ #
607
+ self.decoder = PrunaLTX2VideoDecoder3d(
608
+ in_channels=latent_channels,
609
+ out_channels=out_channels,
610
+ block_out_channels=decoder_block_out_channels,
611
+ spatio_temporal_scaling=decoder_spatio_temporal_scaling,
612
+ layers_per_block=decoder_layers_per_block,
613
+ upsample_type=upsample_type,
614
+ patch_size=patch_size,
615
+ patch_size_t=patch_size_t,
616
+ resnet_norm_eps=resnet_norm_eps,
617
+ is_causal=decoder_causal,
618
+ timestep_conditioning=timestep_conditioning,
619
+ inject_noise=decoder_inject_noise,
620
+ upsample_residual=upsample_residual,
621
+ upsample_factor=upsample_factor,
622
+ spatial_padding_mode=decoder_spatial_padding_mode,
623
+ )
624
+
625
+ latents_mean = torch.zeros((latent_channels,), requires_grad=False)
626
+ latents_std = torch.ones((latent_channels,), requires_grad=False)
627
+ self.register_buffer("latents_mean", latents_mean, persistent=True)
628
+ self.register_buffer("latents_std", latents_std, persistent=True)
629
+
630
+ self.spatial_compression_ratio = (
631
+ patch_size * 2 ** sum(spatio_temporal_scaling)
632
+ if spatial_compression_ratio is None
633
+ else spatial_compression_ratio
634
+ )
635
+ self.temporal_compression_ratio = (
636
+ patch_size_t * 2 ** sum(spatio_temporal_scaling)
637
+ if temporal_compression_ratio is None
638
+ else temporal_compression_ratio
639
+ )
640
+
641
+ # When decoding a batch of video latents at a time, one can save memory by slicing across the batch dimension
642
+ # to perform decoding of a single video latent at a time.
643
+ self.use_slicing = False
644
+
645
+ # When decoding spatially large video latents, the memory requirement is very high. By breaking the video latent
646
+ # frames spatially into smaller tiles and performing multiple forward passes for decoding, and then blending the
647
+ # intermediate tiles together, the memory requirement can be lowered.
648
+ self.use_tiling = False
649
+
650
+ # When decoding temporally long video latents, the memory requirement is very high. By decoding latent frames
651
+ # at a fixed frame batch size (based on `self.num_latent_frames_batch_sizes`), the memory requirement can be lowered.
652
+ self.use_framewise_encoding = False
653
+ self.use_framewise_decoding = False
654
+
655
+ # This can be configured based on the amount of GPU memory available.
656
+ # `16` for sample frames and `2` for latent frames are sensible defaults for consumer GPUs.
657
+ # Setting it to higher values results in higher memory usage.
658
+ self.num_sample_frames_batch_size = 16
659
+ self.num_latent_frames_batch_size = 2
660
+
661
+ # The minimal tile height and width for spatial tiling to be used
662
+ self.tile_sample_min_height = 512
663
+ self.tile_sample_min_width = 512
664
+ self.tile_sample_min_num_frames = 16
665
+
666
+ # The minimal distance between two spatial tiles
667
+ self.tile_sample_stride_height = 448
668
+ self.tile_sample_stride_width = 448
669
+ self.tile_sample_stride_num_frames = 8
670
+
671
+ # encode(), decode(), forward(), tiled_encode(), tiled_decode(), and all
672
+ # other methods are inherited unchanged from AutoencoderKLLTX2Video.
modular_config.json ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "_class_name": "LoadPrunaVAE",
3
+ "_diffusers_version": "0.39.0",
4
+ "auto_map": {
5
+ "ModularPipelineBlocks": "block.LoadPrunaVAE"
6
+ }
7
+ }