-
Notifications
You must be signed in to change notification settings - Fork 442
Expand file tree
/
Copy pathltx2.py
More file actions
3094 lines (2776 loc) · 125 KB
/
Copy pathltx2.py
File metadata and controls
3094 lines (2776 loc) · 125 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-License-Identifier: Apache-2.0
"""
LTX-2 transformer implementation
"""
from dataclasses import dataclass, replace
from enum import Enum
import functools
import math
import os
from pathlib import Path
from typing import Any, Optional, Tuple, Callable
import numpy as np
import torch
import torch.nn as nn
from einops import rearrange, repeat
from fastvideo.attention.backends.sdpa import SDPAMetadata
from fastvideo.attention.layer import DistributedAttention, LocalAttention
from fastvideo.configs.models.dits import LTX2VideoConfig
from fastvideo.distributed.communication_op import (
sequence_model_parallel_all_gather,
sequence_model_parallel_all_gather_with_unpad,
sequence_model_parallel_all_to_all_4D,
sequence_model_parallel_shard,
)
from fastvideo.distributed.parallel_state import get_sp_parallel_rank, get_sp_world_size
from fastvideo.forward_context import ForwardContext, get_forward_context, set_forward_context
from fastvideo.layers.linear import ReplicatedLinear
from fastvideo.layers.quantization.base_config import QuantizationConfig
from fastvideo.logger import init_logger
from fastvideo.models.dits.base import BaseDiT
from fastvideo.platforms import AttentionBackendEnum
logger = init_logger(__name__)
# QuACK provides a fast NVFP4 RMSNorm used only for the LTX-2.3 refine stage.
# Import defensively: upstream installs without QuACK must still work, simply
# falling back to ``torch.nn.functional.rms_norm`` (the non-refine path).
try:
from quack import rmsnorm as _quack_rmsnorm
except Exception: # pragma: no cover - optional dependency
_quack_rmsnorm = None
def _is_ltx2_refine_stage() -> bool:
"""Return True when running LTX-2 stage-2 refinement denoising.
Only the refine stage uses the QuACK fast path; everything else (and any
install lacking QuACK) uses the standard Torch RMSNorm, which reproduces
LTX-2.0 numerics exactly.
"""
if _quack_rmsnorm is None:
return False
try:
forward_ctx = get_forward_context()
except AssertionError:
return False
forward_batch = forward_ctx.forward_batch
if forward_batch is None:
return False
return forward_batch.extra.get("ltx2_fp4_stage_profile") == "refine"
def _rms_norm_dispatch(
x: torch.Tensor,
eps: float,
weight: torch.Tensor | None = None,
) -> torch.Tensor:
"""Use QuACK RMSNorm only for LTX-2 refine stage, else Torch RMSNorm."""
if _is_ltx2_refine_stage():
return _quack_rmsnorm(x, weight=weight, eps=eps)
return torch.nn.functional.rms_norm(
x, (x.shape[-1], ), weight=weight, eps=eps)
class StageAwareRMSNorm(nn.RMSNorm):
"""Use torch.nn.RMSNorm for base stage and QuACK for refine stage.
When QuACK is unavailable or outside the refine stage this behaves
identically to ``torch.nn.RMSNorm`` (LTX-2.0 q_norm/k_norm behavior).
"""
def __init__(self, hidden_size: int, eps: float = 1e-6) -> None:
super().__init__(hidden_size, eps=eps)
def forward(self, x: torch.Tensor) -> torch.Tensor:
if _is_ltx2_refine_stage():
return _quack_rmsnorm(
x,
weight=self.weight,
eps=self.eps,
)
return super().forward(x)
def _supports_prequantized_input(linear: ReplicatedLinear) -> bool:
"""Whether ``linear``'s quant method is willing to accept a
pre-quantized ``(x_fp4, x_scale, x_global_sf)`` input tuple.
Used by the LTX-2 attention forward path so that a single input
tensor can be quantized once and reused across q/k/v projections
when they share the same source (self-attention).
"""
quant_method = getattr(linear, "quant_method", None)
if not callable(getattr(quant_method, "quantize_input", None)):
return False
wants_prequant = getattr(quant_method, "wants_prequantized_input", None)
if callable(wants_prequant):
try:
return bool(wants_prequant())
except Exception:
return False
return True
def _linear_project_with_optional_prequant(
linear: ReplicatedLinear,
x: torch.Tensor,
pre_quantized: tuple[torch.Tensor, torch.Tensor, torch.Tensor] | None,
) -> torch.Tensor:
"""Project ``x`` through ``linear``, optionally bypassing the
in-method quantize step when ``pre_quantized`` is supplied.
When the linear's quant method does not support pre-quantized
inputs (e.g. ``UnquantizedLinearMethod``), falls back to the
standard ``linear(x)`` call and discards the bias-pass-through
tuple element.
"""
if pre_quantized is None or not _supports_prequantized_input(linear):
return linear(x)[0]
bias = linear.bias if not linear.skip_bias_add else None
return linear.quant_method.apply( # type: ignore[union-attr]
linear,
x,
bias=bias,
pre_quantized=pre_quantized,
)
def get_timestep_embedding(
timesteps: torch.Tensor,
embedding_dim: int,
flip_sin_to_cos: bool = False,
downscale_freq_shift: float = 1,
scale: float = 1,
max_period: int = 10000,
) -> torch.Tensor:
"""Sinusoidal timestep embedding used by LTX-2 AdaLN."""
if len(timesteps.shape) != 1:
raise ValueError("Timesteps should be a 1d-array")
half_dim = embedding_dim // 2
exponent = -math.log(max_period) * torch.arange(start=0, end=half_dim, dtype=torch.float32, device=timesteps.device)
exponent = exponent / (half_dim - downscale_freq_shift)
emb = torch.exp(exponent)
emb = timesteps[:, None].float() * emb[None, :]
emb = scale * emb
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=-1)
if flip_sin_to_cos:
emb = torch.cat([emb[:, half_dim:], emb[:, :half_dim]], dim=-1)
if embedding_dim % 2 == 1:
emb = torch.nn.functional.pad(emb, (0, 1, 0, 0))
return emb
class TimestepEmbedding(torch.nn.Module):
"""Two-layer MLP to project timestep embeddings."""
def __init__(
self,
in_channels: int,
time_embed_dim: int,
out_dim: int | None = None,
post_act_fn: str | None = None,
cond_proj_dim: int | None = None,
sample_proj_bias: bool = True,
):
super().__init__()
self.linear_1 = torch.nn.Linear(in_channels, time_embed_dim, sample_proj_bias)
self.cond_proj = torch.nn.Linear(cond_proj_dim, in_channels, bias=False) if cond_proj_dim is not None else None
self.act = torch.nn.SiLU()
time_embed_dim_out = out_dim if out_dim is not None else time_embed_dim
self.linear_2 = torch.nn.Linear(time_embed_dim, time_embed_dim_out, sample_proj_bias)
self.post_act = None if post_act_fn is None else None
def forward(self, sample: torch.Tensor, condition: torch.Tensor | None = None) -> torch.Tensor:
if condition is not None:
sample = sample + self.cond_proj(condition)
sample = self.linear_1(sample)
if self.act is not None:
sample = self.act(sample)
sample = self.linear_2(sample)
if self.post_act is not None:
sample = self.post_act(sample)
return sample
class Timesteps(torch.nn.Module):
"""Sinusoidal timestep embedding wrapper with scaling knobs."""
def __init__(self, num_channels: int, flip_sin_to_cos: bool, downscale_freq_shift: float, scale: int = 1):
super().__init__()
self.num_channels = num_channels
self.flip_sin_to_cos = flip_sin_to_cos
self.downscale_freq_shift = downscale_freq_shift
self.scale = scale
def forward(self, timesteps: torch.Tensor) -> torch.Tensor:
return get_timestep_embedding(
timesteps,
self.num_channels,
flip_sin_to_cos=self.flip_sin_to_cos,
downscale_freq_shift=self.downscale_freq_shift,
scale=self.scale,
)
class PixArtAlphaCombinedTimestepSizeEmbeddings(torch.nn.Module):
"""PixArt-Alpha timestep embedding used by LTX-2 AdaLN."""
def __init__(self, embedding_dim: int, size_emb_dim: int):
super().__init__()
self.outdim = size_emb_dim
self.time_proj = Timesteps(num_channels=256, flip_sin_to_cos=True, downscale_freq_shift=0)
self.timestep_embedder = TimestepEmbedding(in_channels=256, time_embed_dim=embedding_dim)
def forward(self, timestep: torch.Tensor, hidden_dtype: torch.dtype) -> torch.Tensor:
timesteps_proj = self.time_proj(timestep)
timesteps_emb = self.timestep_embedder(timesteps_proj.to(dtype=hidden_dtype))
return timesteps_emb
class AdaLayerNormSingle(torch.nn.Module):
"""AdaLN-single modulation that emits scale/shift/gates for LTX-2."""
def __init__(self, embedding_dim: int, embedding_coefficient: int = 6):
super().__init__()
self.emb = PixArtAlphaCombinedTimestepSizeEmbeddings(
embedding_dim,
size_emb_dim=embedding_dim // 3,
)
self.silu = torch.nn.SiLU()
self.linear = torch.nn.Linear(embedding_dim, embedding_coefficient * embedding_dim, bias=True)
def forward(
self,
timestep: torch.Tensor,
hidden_dtype: Optional[torch.dtype] = None,
) -> Tuple[torch.Tensor, torch.Tensor]:
embedded_timestep = self.emb(timestep, hidden_dtype=hidden_dtype)
return self.linear(self.silu(embedded_timestep)), embedded_timestep
# Number of AdaLN scale/shift/gate rows. LTX-2.0 emits 6 (shift/scale/gate for
# self-attn and FFN). LTX-2.3 with cross_attention_adaln emits 3 extra rows for
# the text cross-attention shift/scale/gate.
ADALN_NUM_BASE_PARAMS = 6
ADALN_NUM_CROSS_ATTN_PARAMS = 3
def adaln_embedding_coefficient(cross_attention_adaln: bool) -> int:
return ADALN_NUM_BASE_PARAMS + (ADALN_NUM_CROSS_ATTN_PARAMS
if cross_attention_adaln else 0)
class PixArtAlphaTextProjection(torch.nn.Module):
"""Caption projection MLP used by LTX-2."""
def __init__(self, in_features: int, hidden_size: int, out_features: int | None = None, act_fn: str = "gelu_tanh"):
super().__init__()
if out_features is None:
out_features = hidden_size
self.linear_1 = torch.nn.Linear(in_features=in_features, out_features=hidden_size, bias=True)
if act_fn == "gelu_tanh":
self.act_1 = torch.nn.GELU(approximate="tanh")
elif act_fn == "silu":
self.act_1 = torch.nn.SiLU()
else:
raise ValueError(f"Unknown activation function: {act_fn}")
self.linear_2 = torch.nn.Linear(in_features=hidden_size, out_features=out_features, bias=True)
def forward(self, caption: torch.Tensor) -> torch.Tensor:
hidden_states = self.linear_1(caption)
hidden_states = self.act_1(hidden_states)
hidden_states = self.linear_2(hidden_states)
return hidden_states
class GELUApprox(nn.Module):
"""Linear + tanh-approximate GELU used by LTX-2 FFN."""
def __init__(
self,
in_features: int,
out_features: int,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.proj = ReplicatedLinear(
in_features,
out_features,
quant_config=quant_config,
prefix=f"{prefix}.fc_in",
)
self.act = nn.GELU(approximate="tanh")
def forward(self, x: torch.Tensor) -> torch.Tensor:
return self.act(self.proj(x)[0])
class FeedForward(nn.Module):
"""LTX-2 FFN: GELUApprox -> Identity -> Linear."""
def __init__(
self,
dim: int,
dim_out: int,
mult: int = 4,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
) -> None:
super().__init__()
inner_dim = int(dim * mult)
project_in = GELUApprox(
dim,
inner_dim,
quant_config=quant_config,
prefix=f"{prefix}.ffn",
)
project_out = ReplicatedLinear(
inner_dim,
dim_out,
quant_config=quant_config,
prefix=f"{prefix}.ffn.fc_out",
)
self.net = nn.ModuleList([project_in, nn.Identity(), project_out])
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.net[0](x)
x = self.net[1](x)
x = self.net[2](x)[0]
return x
class VideoLatentShape(tuple):
"""Helper for (B, C, T, H, W) latent shapes."""
@property
def batch(self) -> int:
return self[0]
@property
def channels(self) -> int:
return self[1]
@property
def frames(self) -> int:
return self[2]
@property
def height(self) -> int:
return self[3]
@property
def width(self) -> int:
return self[4]
@staticmethod
def from_torch_shape(shape: torch.Size) -> "VideoLatentShape":
return VideoLatentShape(shape)
def to_torch_shape(self) -> torch.Size:
return torch.Size(self)
class AudioLatentShape(tuple):
"""Helper for (B, C, T, F) audio latent shapes."""
@property
def batch(self) -> int:
return self[0]
@property
def channels(self) -> int:
return self[1]
@property
def frames(self) -> int:
return self[2]
@property
def mel_bins(self) -> int:
return self[3]
def to_torch_shape(self) -> torch.Size:
return torch.Size(self)
@staticmethod
def from_torch_shape(shape: torch.Size) -> "AudioLatentShape":
return AudioLatentShape(shape)
@staticmethod
def from_duration(
batch: int,
duration: float,
channels: int,
mel_bins: int,
sample_rate: int,
hop_length: int,
audio_latent_downsample_factor: int,
) -> "AudioLatentShape":
latents_per_second = float(sample_rate) / float(
hop_length) / float(audio_latent_downsample_factor)
return AudioLatentShape(
(batch, channels, round(duration * latents_per_second), mel_bins))
class VideoLatentPatchifier:
"""Patchify/unpatchify latent tokens for LTX-2."""
def __init__(self, patch_size: int):
self._patch_size = (1, patch_size, patch_size)
@property
def patch_size(self) -> Tuple[int, int, int]:
return self._patch_size
def get_token_count(self, tgt_shape: VideoLatentShape) -> int:
return math.prod(tgt_shape.to_torch_shape()[2:]) // math.prod(self._patch_size)
def patchify(self, latents: torch.Tensor) -> torch.Tensor:
return rearrange(
latents,
"b c (f p1) (h p2) (w p3) -> b (f h w) (c p1 p2 p3)",
p1=self._patch_size[0],
p2=self._patch_size[1],
p3=self._patch_size[2],
)
def unpatchify(self, latents: torch.Tensor, output_shape: VideoLatentShape) -> torch.Tensor:
patch_grid_frames = output_shape.frames // self._patch_size[0]
patch_grid_height = output_shape.height // self._patch_size[1]
patch_grid_width = output_shape.width // self._patch_size[2]
return rearrange(
latents,
"b (f h w) (c p q) -> b c f (h p) (w q)",
f=patch_grid_frames,
h=patch_grid_height,
w=patch_grid_width,
p=self._patch_size[1],
q=self._patch_size[2],
)
def get_patch_grid_bounds(
self,
output_shape: VideoLatentShape,
device: Optional[torch.device] = None,
) -> torch.Tensor:
"""Get patch grid bounds for RoPE computation.
Args:
output_shape: Shape of the video latent tensor
device: Device to create tensors on
"""
frames = output_shape.frames
height = output_shape.height
width = output_shape.width
batch_size = output_shape.batch
grid_coords = torch.meshgrid(
torch.arange(start=0, end=frames, step=self._patch_size[0], device=device),
torch.arange(start=0, end=height, step=self._patch_size[1], device=device),
torch.arange(start=0, end=width, step=self._patch_size[2], device=device),
indexing="ij",
)
patch_starts = torch.stack(grid_coords, dim=0)
patch_size_delta = torch.tensor(
self._patch_size,
device=patch_starts.device,
dtype=patch_starts.dtype,
).view(3, 1, 1, 1)
patch_ends = patch_starts + patch_size_delta
latent_coords = torch.stack((patch_starts, patch_ends), dim=-1)
latent_coords = repeat(
latent_coords,
"c f h w bounds -> b c (f h w) bounds",
b=batch_size,
bounds=2,
)
return latent_coords
class AudioLatentPatchifier:
"""Patchify/unpatchify audio latents and compute timing bounds."""
def __init__(
self,
patch_size: int,
sample_rate: int,
hop_length: int,
audio_latent_downsample_factor: int,
is_causal: bool = True,
shift: int = 0,
) -> None:
self.hop_length = hop_length
self.sample_rate = sample_rate
self.audio_latent_downsample_factor = audio_latent_downsample_factor
self.is_causal = is_causal
self.shift = shift
self._patch_size = (1, patch_size, patch_size)
def get_token_count(self, tgt_shape: AudioLatentShape) -> int:
return tgt_shape.frames
def patchify(self, latents: torch.Tensor) -> torch.Tensor:
return rearrange(latents, "b c t f -> b t (c f)")
def unpatchify(
self,
latents: torch.Tensor,
output_shape: AudioLatentShape,
) -> torch.Tensor:
return rearrange(
latents,
"b t (c f) -> b c t f",
c=output_shape.channels,
f=output_shape.mel_bins,
)
def get_patch_grid_bounds(
self,
output_shape: AudioLatentShape,
device: Optional[torch.device] = None,
) -> torch.Tensor:
"""Get patch grid bounds for audio RoPE computation.
Args:
output_shape: Shape of the audio latent tensor
device: Device to create tensors on
"""
start_timings = self._get_audio_latent_time_in_sec(
self.shift,
output_shape.frames + self.shift,
torch.float32,
device,
)
start_timings = start_timings.unsqueeze(0).expand(output_shape.batch,
-1).unsqueeze(1)
end_timings = self._get_audio_latent_time_in_sec(
self.shift + 1,
output_shape.frames + self.shift + 1,
torch.float32,
device,
)
end_timings = end_timings.unsqueeze(0).expand(output_shape.batch,
-1).unsqueeze(1)
return torch.stack([start_timings, end_timings], dim=-1)
def _get_audio_latent_time_in_sec(
self,
start_latent: int,
end_latent: int,
dtype: torch.dtype,
device: Optional[torch.device],
) -> torch.Tensor:
resolved_device = device or torch.device("cpu")
audio_latent_frame = torch.arange(
start_latent, end_latent, dtype=dtype, device=resolved_device)
audio_mel_frame = audio_latent_frame * self.audio_latent_downsample_factor
if self.is_causal:
causal_offset = 1
audio_mel_frame = (
audio_mel_frame + causal_offset -
self.audio_latent_downsample_factor).clamp(min=0)
return audio_mel_frame * self.hop_length / self.sample_rate
def _get_pixel_coords(
latent_coords: torch.Tensor,
scale_factors: tuple[int, int, int],
fps: float | None,
causal_fix: bool = True,
) -> torch.Tensor:
broadcast_shape = [1] * latent_coords.ndim
broadcast_shape[1] = -1
scale_tensor = torch.tensor(
scale_factors,
device=latent_coords.device,
dtype=torch.float32,
).view(*broadcast_shape)
pixel_coords = latent_coords.to(torch.float32) * scale_tensor
if causal_fix:
pixel_coords[:, 0, ...] = (
pixel_coords[:, 0, ...] + 1 - scale_factors[0]).clamp(min=0)
if fps:
pixel_coords[:, 0, ...] = pixel_coords[:, 0, ...] / fps
return pixel_coords
def _to_denoised(
sample: torch.Tensor,
velocity: torch.Tensor,
sigma: torch.Tensor,
calc_dtype: torch.dtype = torch.float32,
) -> torch.Tensor:
if isinstance(sigma, torch.Tensor):
sigma = sigma.to(calc_dtype)
while sigma.ndim < sample.ndim:
sigma = sigma.unsqueeze(-1)
return (sample.to(calc_dtype) - velocity.to(calc_dtype) * sigma).to(sample.dtype)
def _debug_block_log_line(message: str) -> None:
if os.getenv("LTX2_PIPELINE_DEBUG_LOG", "0") != "1":
return
log_path = os.getenv("LTX2_PIPELINE_DEBUG_PATH", "")
if not log_path:
return
log_dir = os.path.dirname(log_path)
if log_dir:
os.makedirs(log_dir, exist_ok=True)
with open(log_path, "a", encoding="utf-8") as f:
f.write(message + "\n")
def _debug_transformer_args(prefix: str, args: "TransformerArgs | None") -> None:
if os.getenv("LTX2_PIPELINE_DEBUG_LOG", "0") != "1" or args is None:
return
pe_cos, pe_sin = args.positional_embeddings
cross_cos = None
cross_sin = None
if args.cross_positional_embeddings is not None:
cross_cos, cross_sin = args.cross_positional_embeddings
mask = args.context_mask
if mask is None:
mask_summary = "mask=None"
else:
finite = torch.isfinite(mask)
finite_sum = mask[finite].sum().item() if finite.any() else 0.0
mask_summary = (
f"mask_min={mask.min().item():.6f} "
f"mask_max={mask.max().item():.6f} "
f"mask_finite_sum={finite_sum:.6f} "
f"mask_finite_count={finite.sum().item()}"
)
_debug_block_log_line(
f"{prefix}:x_sum={args.x.float().sum().item():.6f} "
f"context_sum={args.context.float().sum().item():.6f} "
f"t_sum={args.timesteps.float().sum().item():.6f} "
f"emb_sum={args.embedded_timestep.float().sum().item():.6f} "
f"pe_cos_sum={pe_cos.float().sum().item():.6f} "
f"pe_sin_sum={pe_sin.float().sum().item():.6f} "
f"cross_pe_cos_sum={(cross_cos.float().sum().item() if cross_cos is not None else 0.0):.6f} "
f"cross_pe_sin_sum={(cross_sin.float().sum().item() if cross_sin is not None else 0.0):.6f} "
f"{mask_summary}"
)
class LTXRopeType(Enum):
"""LTX-2 rotary variants (interleaved vs split)."""
INTERLEAVED = "interleaved"
SPLIT = "split"
DEFAULT_LTX2_SCALE_FACTORS = (8, 32, 32)
DEFAULT_LTX2_AUDIO_CHANNELS = 8
DEFAULT_LTX2_AUDIO_MEL_BINS = 16
DEFAULT_LTX2_AUDIO_SAMPLE_RATE = 16000
DEFAULT_LTX2_AUDIO_HOP_LENGTH = 160
DEFAULT_LTX2_AUDIO_DOWNSAMPLE = 4
# The vocoder upsamples from mel spectrograms (16kHz sample rate) to 24kHz audio
DEFAULT_LTX2_VOCODER_OUTPUT_SAMPLE_RATE = 24000
def apply_ltx_rotary_emb(
input_tensor: torch.Tensor,
freqs_cis: tuple[torch.Tensor, torch.Tensor],
rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
) -> torch.Tensor:
"""Apply LTX-2 rotary embeddings to a tensor."""
if rope_type == LTXRopeType.INTERLEAVED:
return _apply_ltx_interleaved_rotary_emb(input_tensor, *freqs_cis)
if rope_type == LTXRopeType.SPLIT:
return _apply_ltx_split_rotary_emb(input_tensor, *freqs_cis)
raise ValueError(f"Invalid rope type: {rope_type}")
def apply_ltx_rotary_emb_4d(
input_tensor: torch.Tensor,
freqs_cis: tuple[torch.Tensor, torch.Tensor],
rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
) -> torch.Tensor:
"""Apply LTX-2 rotary embeddings to a 4D tensor [B, H, T, D].
This is used for applying RoPE after all-to-all in distributed attention,
where the tensor is already in [B, H, T, D] format.
"""
cos_freqs, sin_freqs = freqs_cis
if rope_type == LTXRopeType.INTERLEAVED:
# For interleaved, cos/sin have shape [B, T, inner_dim]
# Need to reshape to [B, H, T, D] format
# Actually interleaved doesn't have per-head rotations, so we broadcast
t_dup = rearrange(input_tensor, "... (d r) -> ... d r", r=2)
t1, t2 = t_dup.unbind(dim=-1)
t_dup = torch.stack((-t2, t1), dim=-1)
input_tensor_rot = rearrange(t_dup, "... d r -> ... (d r)")
return input_tensor * cos_freqs + input_tensor_rot * sin_freqs
if rope_type == LTXRopeType.SPLIT:
# For split, cos/sin already have shape [B, H, T, D/2]
# input_tensor is [B, H, T, D]
split_input = rearrange(input_tensor, "... (d r) -> ... d r", d=2)
first_half_input = split_input[..., :1, :]
second_half_input = split_input[..., 1:, :]
output = split_input * cos_freqs.unsqueeze(-2)
first_half_output = output[..., :1, :]
second_half_output = output[..., 1:, :]
first_half_output.addcmul_(-sin_freqs.unsqueeze(-2), second_half_input)
second_half_output.addcmul_(sin_freqs.unsqueeze(-2), first_half_input)
return rearrange(output, "... d r -> ... (d r)")
raise ValueError(f"Invalid rope type: {rope_type}")
def _apply_ltx_interleaved_rotary_emb(
input_tensor: torch.Tensor, cos_freqs: torch.Tensor, sin_freqs: torch.Tensor
) -> torch.Tensor:
t_dup = rearrange(input_tensor, "... (d r) -> ... d r", r=2)
t1, t2 = t_dup.unbind(dim=-1)
t_dup = torch.stack((-t2, t1), dim=-1)
input_tensor_rot = rearrange(t_dup, "... d r -> ... (d r)")
return input_tensor * cos_freqs + input_tensor_rot * sin_freqs
def _apply_ltx_split_rotary_emb(
input_tensor: torch.Tensor, cos_freqs: torch.Tensor, sin_freqs: torch.Tensor
) -> torch.Tensor:
needs_reshape = False
if input_tensor.ndim != 4 and cos_freqs.ndim == 4:
b, h, t, _ = cos_freqs.shape
input_tensor = input_tensor.reshape(b, t, h, -1).swapaxes(1, 2)
needs_reshape = True
split_input = rearrange(input_tensor, "... (d r) -> ... d r", d=2)
first_half_input = split_input[..., :1, :]
second_half_input = split_input[..., 1:, :]
output = split_input * cos_freqs.unsqueeze(-2)
first_half_output = output[..., :1, :]
second_half_output = output[..., 1:, :]
first_half_output.addcmul_(-sin_freqs.unsqueeze(-2), second_half_input)
second_half_output.addcmul_(sin_freqs.unsqueeze(-2), first_half_input)
output = rearrange(output, "... d r -> ... (d r)")
if needs_reshape:
output = output.swapaxes(1, 2).reshape(b, t, -1)
return output
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_np(
positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
"""Generate LTX-2 rotary frequencies with high-precision numpy."""
theta = positional_embedding_theta
start = 1
end = theta
n_elem = 2 * positional_embedding_max_pos_count
pow_indices = np.power(
theta,
np.linspace(
np.log(start) / np.log(theta),
np.log(end) / np.log(theta),
inner_dim // n_elem,
dtype=np.float64,
),
)
return torch.tensor(pow_indices * math.pi / 2, dtype=torch.float32)
@functools.lru_cache(maxsize=5)
def generate_ltx_freq_grid_pytorch(
positional_embedding_theta: float, positional_embedding_max_pos_count: int, inner_dim: int
) -> torch.Tensor:
"""Generate LTX-2 rotary frequencies in torch for speed."""
theta = positional_embedding_theta
start = 1
end = theta
n_elem = 2 * positional_embedding_max_pos_count
indices = theta ** (
torch.linspace(
math.log(start, theta),
math.log(end, theta),
inner_dim // n_elem,
dtype=torch.float32,
)
)
indices = indices.to(dtype=torch.float32)
return indices * math.pi / 2
def _ltx_get_fractional_positions(indices_grid: torch.Tensor, max_pos: list[int]) -> torch.Tensor:
n_pos_dims = indices_grid.shape[1]
if n_pos_dims != len(max_pos):
raise ValueError(
f"Number of position dimensions ({n_pos_dims}) must match max_pos length ({len(max_pos)})"
)
return torch.stack(
[indices_grid[:, i] / max_pos[i] for i in range(n_pos_dims)],
dim=-1,
)
def _ltx_generate_freqs(
indices: torch.Tensor, indices_grid: torch.Tensor, max_pos: list[int], use_middle_indices_grid: bool
) -> torch.Tensor:
if use_middle_indices_grid:
indices_grid_start, indices_grid_end = indices_grid[..., 0], indices_grid[..., 1]
indices_grid = (indices_grid_start + indices_grid_end) / 2.0
elif len(indices_grid.shape) == 4:
indices_grid = indices_grid[..., 0]
fractional_positions = _ltx_get_fractional_positions(indices_grid, max_pos)
indices = indices.to(device=fractional_positions.device)
freqs = (indices * (fractional_positions.unsqueeze(-1) * 2 - 1)).transpose(-1, -2).flatten(2)
return freqs
def _ltx_split_freqs_cis(
freqs: torch.Tensor, pad_size: int, num_attention_heads: int
) -> tuple[torch.Tensor, torch.Tensor]:
cos_freq = freqs.cos()
sin_freq = freqs.sin()
if pad_size != 0:
cos_padding = torch.ones_like(cos_freq[:, :, :pad_size])
sin_padding = torch.zeros_like(sin_freq[:, :, :pad_size])
cos_freq = torch.concatenate([cos_padding, cos_freq], axis=-1)
sin_freq = torch.concatenate([sin_padding, sin_freq], axis=-1)
b = cos_freq.shape[0]
t = cos_freq.shape[1]
cos_freq = cos_freq.reshape(b, t, num_attention_heads, -1)
sin_freq = sin_freq.reshape(b, t, num_attention_heads, -1)
cos_freq = torch.swapaxes(cos_freq, 1, 2)
sin_freq = torch.swapaxes(sin_freq, 1, 2)
return cos_freq, sin_freq
def _ltx_interleaved_freqs_cis(freqs: torch.Tensor, pad_size: int) -> tuple[torch.Tensor, torch.Tensor]:
cos_freq = freqs.cos().repeat_interleave(2, dim=-1)
sin_freq = freqs.sin().repeat_interleave(2, dim=-1)
if pad_size != 0:
cos_padding = torch.ones_like(cos_freq[:, :, :pad_size])
sin_padding = torch.zeros_like(cos_freq[:, :, :pad_size])
cos_freq = torch.cat([cos_padding, cos_freq], dim=-1)
sin_freq = torch.cat([sin_padding, sin_freq], dim=-1)
return cos_freq, sin_freq
def precompute_ltx_freqs_cis(
indices_grid: torch.Tensor,
dim: int,
out_dtype: torch.dtype,
theta: float = 10000.0,
max_pos: list[int] | None = None,
use_middle_indices_grid: bool = False,
num_attention_heads: int = 32,
rope_type: LTXRopeType = LTXRopeType.INTERLEAVED,
freq_grid_generator: Callable[[float, int, int], torch.Tensor] = generate_ltx_freq_grid_pytorch,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Precompute LTX-2 rotary cos/sin grids for (t, x, y) positions."""
if max_pos is None:
max_pos = [20, 2048, 2048]
indices = freq_grid_generator(theta, indices_grid.shape[1], dim)
freqs = _ltx_generate_freqs(indices, indices_grid, max_pos, use_middle_indices_grid)
if rope_type == LTXRopeType.SPLIT:
expected_freqs = dim // 2
current_freqs = freqs.shape[-1]
pad_size = expected_freqs - current_freqs
cos_freq, sin_freq = _ltx_split_freqs_cis(freqs, pad_size, num_attention_heads)
else:
n_elem = 2 * indices_grid.shape[1]
cos_freq, sin_freq = _ltx_interleaved_freqs_cis(freqs, dim % n_elem)
return cos_freq.to(out_dtype), sin_freq.to(out_dtype)
@dataclass(frozen=True)
class TransformerArgs:
"""Pack transformer inputs for LTX-2 blocks."""
x: torch.Tensor
context: torch.Tensor
context_mask: torch.Tensor | None
timesteps: torch.Tensor
embedded_timestep: torch.Tensor
positional_embeddings: torch.Tensor
cross_positional_embeddings: torch.Tensor | None
cross_scale_shift_timestep: torch.Tensor | None
cross_gate_timestep: torch.Tensor | None
enabled: bool
# LTX-2.3 cross-attention AdaLN prompt timestep embedding (None for 2.0).
prompt_timestep: torch.Tensor | None = None
@dataclass(frozen=True)
class Modality:
"""Lightweight modality container for LTX-2 inputs."""
enabled: bool
latent: torch.Tensor
timesteps: torch.Tensor
positions: torch.Tensor
context: torch.Tensor
context_mask: torch.Tensor | None = None
# LTX-2.3 cross-attention AdaLN sigma timestep (None for 2.0).
sigma: torch.Tensor | None = None
class TransformerArgsPreprocessor:
"""Prepare LTX-2 transformer inputs (patchify, AdaLN, rope)."""
def __init__( # noqa: PLR0913
self,
patchify_proj: torch.nn.Linear,
adaln: AdaLayerNormSingle,
caption_projection: PixArtAlphaTextProjection,
inner_dim: int,
max_pos: list[int],
num_attention_heads: int,
use_middle_indices_grid: bool,
timestep_scale_multiplier: int,
double_precision_rope: bool,
positional_embedding_theta: float,
rope_type: LTXRopeType,
prompt_adaln: AdaLayerNormSingle | None = None,
) -> None:
self.patchify_proj = patchify_proj
self.adaln = adaln
self.caption_projection = caption_projection
self.inner_dim = inner_dim
self.max_pos = max_pos
self.num_attention_heads = num_attention_heads
self.use_middle_indices_grid = use_middle_indices_grid
self.timestep_scale_multiplier = timestep_scale_multiplier
self.double_precision_rope = double_precision_rope
self.positional_embedding_theta = positional_embedding_theta
self.rope_type = rope_type
# LTX-2.3 cross-attention AdaLN prompt timestep embedder (None for 2.0).
self.prompt_adaln = prompt_adaln
def _prepare_timestep(
self,
timestep: torch.Tensor,
batch_size: int,
hidden_dtype: torch.dtype,
adaln: AdaLayerNormSingle | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
adaln = self.adaln if adaln is None else adaln
timestep = timestep * self.timestep_scale_multiplier
timestep, embedded_timestep = adaln(timestep.flatten(), hidden_dtype=hidden_dtype)
timestep = timestep.view(batch_size, -1, timestep.shape[-1])
embedded_timestep = embedded_timestep.view(batch_size, -1, embedded_timestep.shape[-1])
return timestep, embedded_timestep
def _prepare_context(
self,
context: torch.Tensor,
x: torch.Tensor,
attention_mask: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor | None]:
batch_size = x.shape[0]
if context.device != x.device:
context = context.to(x.device)
if context.dtype != x.dtype:
context = context.to(x.dtype)
if attention_mask is not None and attention_mask.device != x.device:
attention_mask = attention_mask.to(x.device)
# When ``caption_proj_before_connector`` is set the caption projection
# lives in the text encoder, so it is None here and we skip it.
if self.caption_projection is not None:
context = self.caption_projection(context)
context = context.view(batch_size, -1, x.shape[-1])
return context, attention_mask
def _prepare_attention_mask(self, attention_mask: torch.Tensor | None, x_dtype: torch.dtype) -> torch.Tensor | None:
if attention_mask is None or torch.is_floating_point(attention_mask):
return attention_mask
return (attention_mask - 1).to(x_dtype).reshape(
(attention_mask.shape[0], 1, -1, attention_mask.shape[-1])
) * torch.finfo(x_dtype).max
def _prepare_positional_embeddings(
self,
positions: torch.Tensor,
inner_dim: int,
max_pos: list[int],
use_middle_indices_grid: bool,