Instructions to use Shriramnag/ShivAI-Image-to-Video with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use Shriramnag/ShivAI-Image-to-Video with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("Shriramnag/ShivAI-Image-to-Video", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- LICENSE +381 -0
- README.md +123 -3
- packages/ltx-core/README.md +413 -0
- packages/ltx-core/pyproject.toml +55 -0
- packages/ltx-core/src/ltx_core/__init__.py +0 -0
- packages/ltx-core/src/ltx_core/batch_split.py +95 -0
- packages/ltx-core/src/ltx_core/block_streaming/__init__.py +19 -0
- packages/ltx-core/src/ltx_core/block_streaming/builder.py +351 -0
- packages/ltx-core/src/ltx_core/block_streaming/disk.py +148 -0
- packages/ltx-core/src/ltx_core/block_streaming/pool.py +75 -0
- packages/ltx-core/src/ltx_core/block_streaming/provider.py +147 -0
- packages/ltx-core/src/ltx_core/block_streaming/source.py +102 -0
- packages/ltx-core/src/ltx_core/block_streaming/utils.py +134 -0
- packages/ltx-core/src/ltx_core/block_streaming/wrapper.py +96 -0
- packages/ltx-core/src/ltx_core/components/__init__.py +10 -0
- packages/ltx-core/src/ltx_core/components/diffusion_steps.py +186 -0
- packages/ltx-core/src/ltx_core/components/guiders.py +364 -0
- packages/ltx-core/src/ltx_core/components/noisers.py +35 -0
- packages/ltx-core/src/ltx_core/components/patchifiers.py +353 -0
- packages/ltx-core/src/ltx_core/components/protocols.py +101 -0
- packages/ltx-core/src/ltx_core/components/schedulers.py +130 -0
- packages/ltx-core/src/ltx_core/conditioning/__init__.py +21 -0
- packages/ltx-core/src/ltx_core/conditioning/exceptions.py +4 -0
- packages/ltx-core/src/ltx_core/conditioning/item.py +20 -0
- packages/ltx-core/src/ltx_core/conditioning/mask_utils.py +210 -0
- packages/ltx-core/src/ltx_core/conditioning/types/__init__.py +15 -0
- packages/ltx-core/src/ltx_core/conditioning/types/attention_strength_wrapper.py +71 -0
- packages/ltx-core/src/ltx_core/conditioning/types/keyframe_cond.py +83 -0
- packages/ltx-core/src/ltx_core/conditioning/types/latent_cond.py +44 -0
- packages/ltx-core/src/ltx_core/conditioning/types/noise_mask_cond.py +45 -0
- packages/ltx-core/src/ltx_core/conditioning/types/reference_audio_cond.py +59 -0
- packages/ltx-core/src/ltx_core/conditioning/types/reference_video_cond.py +91 -0
- packages/ltx-core/src/ltx_core/guidance/__init__.py +15 -0
- packages/ltx-core/src/ltx_core/guidance/perturbations.py +79 -0
- packages/ltx-core/src/ltx_core/hdr.py +71 -0
- packages/ltx-core/src/ltx_core/loader/__init__.py +56 -0
- packages/ltx-core/src/ltx_core/loader/fuse_loras.py +162 -0
- packages/ltx-core/src/ltx_core/loader/helpers.py +61 -0
- packages/ltx-core/src/ltx_core/loader/kernels.py +79 -0
- packages/ltx-core/src/ltx_core/loader/module_ops.py +14 -0
- packages/ltx-core/src/ltx_core/loader/primitives.py +158 -0
- packages/ltx-core/src/ltx_core/loader/registry.py +84 -0
- packages/ltx-core/src/ltx_core/loader/sd_ops.py +139 -0
- packages/ltx-core/src/ltx_core/loader/sft_loader.py +66 -0
- packages/ltx-core/src/ltx_core/loader/single_gpu_model_builder.py +162 -0
- packages/ltx-core/src/ltx_core/modality_tiling.py +234 -0
- packages/ltx-core/src/ltx_core/model/__init__.py +8 -0
- packages/ltx-core/src/ltx_core/model/audio_vae/__init__.py +29 -0
- packages/ltx-core/src/ltx_core/model/audio_vae/attention.py +71 -0
- packages/ltx-core/src/ltx_core/model/audio_vae/audio_vae.py +508 -0
LICENSE
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
LTX-2 Community License Agreement
|
| 2 |
+
License date: January 5, 2026
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
By using or distributing any portion or element of LTX-2, you agree
|
| 6 |
+
to be bound by this Agreement.
|
| 7 |
+
|
| 8 |
+
1. Definitions.
|
| 9 |
+
|
| 10 |
+
"Agreement" means the terms and conditions for the license, use,
|
| 11 |
+
reproduction, and distribution of LTX-2 and the Complementary
|
| 12 |
+
Materials, as specified in this document.
|
| 13 |
+
|
| 14 |
+
"Control" means the direct or indirect ownership of more than
|
| 15 |
+
fifty percent (50%) of the voting securities or other ownership
|
| 16 |
+
interests, or the power to direct the management and policies of
|
| 17 |
+
such Entity through voting rights, contract, or otherwise.
|
| 18 |
+
|
| 19 |
+
"Data" means a collection of information and/or content extracted
|
| 20 |
+
from the dataset used with LTX-2, including to train, pretrain,
|
| 21 |
+
or otherwise evaluate LTX-2. The Data is not licensed under this
|
| 22 |
+
Agreement.
|
| 23 |
+
|
| 24 |
+
"Derivatives of LTX-2" means all modifications to LTX-2, works
|
| 25 |
+
based on LTX-2, or any other model which is created or initialized
|
| 26 |
+
by transfer of patterns of the weights, parameters, activations or
|
| 27 |
+
output of LTX-2, to the other model, in order to cause the other
|
| 28 |
+
model to perform similarly to LTX-2, including – but not limited
|
| 29 |
+
to - distillation methods entailing the use of intermediate data
|
| 30 |
+
representations or methods based on the generation of synthetic
|
| 31 |
+
data by LTX-2 for training the other model. For clarity, Derivatives
|
| 32 |
+
of LTX-2 include: (i) any fine-tuned or adapted weights, parameters,
|
| 33 |
+
or checkpoints derived from LTX-2; (ii) derivative model architectures
|
| 34 |
+
that incorporate or are based upon LTX-2's architecture; and
|
| 35 |
+
(iii) any modified or extended versions of the Complementary
|
| 36 |
+
Materials. All intellectual property rights in Derivatives of LTX-2
|
| 37 |
+
shall be subject to the terms of this Agreement, and you may not
|
| 38 |
+
claim exclusive ownership rights in any Derivatives of LTX-2 that
|
| 39 |
+
would restrict the rights granted herein.
|
| 40 |
+
|
| 41 |
+
"Entity" means any individual, corporation, partnership, limited
|
| 42 |
+
liability company, or other legal entity. For purposes of this
|
| 43 |
+
Agreement, an Entity shall be deemed to include, on an aggregative
|
| 44 |
+
basis, all subsidiaries, affiliates, and other companies under
|
| 45 |
+
common Control with such Entity. When determining whether an Entity
|
| 46 |
+
meets any threshold under this Agreement (including revenue
|
| 47 |
+
thresholds), all subsidiaries, affiliates, and companies under
|
| 48 |
+
common Control shall be considered collectively.
|
| 49 |
+
|
| 50 |
+
"Harm" includes but is not limited to physical, mental,
|
| 51 |
+
psychological, financial and reputational damage, pain, or loss.
|
| 52 |
+
|
| 53 |
+
"Licensor" or "Lightricks" means the owner that is granting the
|
| 54 |
+
license under this Agreement. For the purposes of this Agreement,
|
| 55 |
+
the Licensor is Lightricks Ltd.
|
| 56 |
+
|
| 57 |
+
"LTX-2" means the large language models, text/image/video/audio/3D
|
| 58 |
+
generation models, and multimodal large language models and their
|
| 59 |
+
software and algorithms, including trained model weights, parameters
|
| 60 |
+
(including optimizer states), machine-learning model code,
|
| 61 |
+
inference-enabling code, training-enabling code, fine-tuning
|
| 62 |
+
enabling code, accompanying source code, scripts, documentation,
|
| 63 |
+
tutorials, examples, and all other elements of the foregoing
|
| 64 |
+
distributed and made publicly available by Lightricks (including,
|
| 65 |
+
for example, at https://github.com/Lightricks/LTX-2) for the LTX-2
|
| 66 |
+
model released on January 5, 2026. This license is applicable to
|
| 67 |
+
all LTX-2 versions released since January 5, 2026, and all future
|
| 68 |
+
releases of LTX-2 under this license.
|
| 69 |
+
|
| 70 |
+
"Output" means the results of operating LTX-2 as embodied in
|
| 71 |
+
informational content resulting therefrom.
|
| 72 |
+
|
| 73 |
+
"you" (or "your") means an individual or legal Entity licensing
|
| 74 |
+
LTX-2 in accordance with this Agreement and/or making use of LTX-2
|
| 75 |
+
for whichever purpose and in any field of use, including usage of
|
| 76 |
+
LTX-2 in an end-use application - e.g. chatbot, translator, image
|
| 77 |
+
generator.
|
| 78 |
+
|
| 79 |
+
2. Grant of License. Subject to the terms and conditions of this
|
| 80 |
+
Agreement, you are granted a non-exclusive, worldwide,
|
| 81 |
+
non-transferable and royalty-free limited license under Licensor's
|
| 82 |
+
intellectual property or other rights owned by Licensor embodied
|
| 83 |
+
in LTX-2 to use, reproduce, prepare, distribute, publicly display,
|
| 84 |
+
publicly perform, sublicense, copy, create derivative works of,
|
| 85 |
+
and make modifications to LTX-2, for any purpose, subject to the
|
| 86 |
+
restrictions set forth in Attachment A; provided however, that
|
| 87 |
+
Entities with annual revenues of at least $10,000,000 (the
|
| 88 |
+
"Commercial Entities") are required to obtain a paid commercial
|
| 89 |
+
use license in order to use LTX-2 and Derivatives of LTX-2,
|
| 90 |
+
subject to the terms and provisions of a different license (the
|
| 91 |
+
"Commercial Use Agreement"), as will be provided by the Licensor.
|
| 92 |
+
Commercial Entities interested in such a commercial license are
|
| 93 |
+
required to [contact Licensor](https://ltx.io/model/licensing).
|
| 94 |
+
Any commercial use of LTX-2 or Derivatives of LTX-2 by the
|
| 95 |
+
Commercial Entities not in accordance with this Agreement and/or
|
| 96 |
+
the Commercial Use Agreement is strictly prohibited and shall be
|
| 97 |
+
deemed a material breach of this Agreement. Such material breach
|
| 98 |
+
will be subject, in addition to any license fees owed to Licensor
|
| 99 |
+
for the period such Commercial Entity used LTX-2 (as will be
|
| 100 |
+
determined by Licensor), to liquidated damages, which will be paid
|
| 101 |
+
to Licensor immediately upon demand, in an amount equal to double
|
| 102 |
+
the amount that would otherwise have been paid by you for the
|
| 103 |
+
relevant period of time. Such amount reflects a reasonable estimation
|
| 104 |
+
of the losses and administrative costs incurred due to such breach.
|
| 105 |
+
You agree and understand that this remedy does not limit the Licensor's
|
| 106 |
+
right to pursue other remedies available at law or equity.
|
| 107 |
+
|
| 108 |
+
3. Distribution and Redistribution. You may host for third parties
|
| 109 |
+
remote access purposes (e.g. software-as-a-service), reproduce
|
| 110 |
+
and distribute copies of LTX-2 or Derivatives of LTX-2 thereof in
|
| 111 |
+
any medium, with or without modifications, provided that you meet
|
| 112 |
+
the following conditions:
|
| 113 |
+
|
| 114 |
+
(a) Use-based restrictions as referenced in paragraph 4 and all
|
| 115 |
+
provisions of Attachment A MUST be included as an enforceable
|
| 116 |
+
provision by you in any type of legal agreement (e.g. a
|
| 117 |
+
license) governing the use and/or distribution of LTX-2 or
|
| 118 |
+
Derivatives of LTX-2, and you shall give notice to subsequent
|
| 119 |
+
users you distribute to, that LTX-2 or Derivatives of LTX-2
|
| 120 |
+
are subject to paragraph 4 and Attachment A in their entirety,
|
| 121 |
+
including all use restrictions and acceptable use policies;
|
| 122 |
+
|
| 123 |
+
(b) You must provide any third party recipients of LTX-2 or
|
| 124 |
+
Derivatives of LTX-2 a copy of this Agreement, including all
|
| 125 |
+
attachments and use policies. Any Derivative of LTX-2 (as
|
| 126 |
+
defined in Section 1, including but not limited to fine-tuned
|
| 127 |
+
weights, modified training code, models trained on Outputs, or
|
| 128 |
+
any other derivative) must be distributed exclusively under
|
| 129 |
+
the terms of this Agreement with a complete copy of this
|
| 130 |
+
license included;
|
| 131 |
+
|
| 132 |
+
(c) You must cause any modified files to carry prominent notices
|
| 133 |
+
stating that you changed the files;
|
| 134 |
+
|
| 135 |
+
(d) You must retain all copyright, patent, trademark, and
|
| 136 |
+
attribution notices excluding those notices that do not
|
| 137 |
+
pertain to any part of LTX-2, Derivatives of LTX-2.
|
| 138 |
+
|
| 139 |
+
You may add your own copyright statement to your modifications and
|
| 140 |
+
may provide additional or different license terms and conditions -
|
| 141 |
+
respecting paragraph 3(a) - for use, reproduction, or distribution
|
| 142 |
+
of your modifications, or for any such Derivatives of LTX-2 as a
|
| 143 |
+
whole, provided your use, reproduction, and distribution of LTX-2
|
| 144 |
+
otherwise complies with the conditions stated in this Agreement,
|
| 145 |
+
and you provide a complete copy of this Agreement with any such
|
| 146 |
+
use, reproduction and distribution of LTX-2 and any Derivatives
|
| 147 |
+
thereof.
|
| 148 |
+
|
| 149 |
+
4. Use-based restrictions. The restrictions set forth in Attachment A
|
| 150 |
+
are considered Use-based restrictions. Therefore, you cannot use
|
| 151 |
+
LTX-2 and the Derivatives of LTX-2 in violation of the specified
|
| 152 |
+
restricted uses. You may use LTX-2 subject to this Agreement,
|
| 153 |
+
including only for lawful purposes and in accordance with the
|
| 154 |
+
Agreement. "Use" may include creating any content with, fine-tuning,
|
| 155 |
+
updating, running, training, evaluating and/or re-parametrizing
|
| 156 |
+
LTX-2. You shall require all of your users who use LTX-2 or a
|
| 157 |
+
Derivative of LTX-2 to comply with the terms of this paragraph 4.
|
| 158 |
+
|
| 159 |
+
5. The Output You Generate. Except as set forth herein, Licensor
|
| 160 |
+
claims no rights in the Output you generate using LTX-2. You are
|
| 161 |
+
accountable for input you insert into LTX-2, the Output you
|
| 162 |
+
generate and its subsequent uses. No use of the Output can
|
| 163 |
+
contravene any provision as stated in the Agreement.
|
| 164 |
+
|
| 165 |
+
6. Updates and Runtime Restrictions. To the maximum extent permitted
|
| 166 |
+
by law, Licensor reserves the right to restrict (remotely or
|
| 167 |
+
otherwise) usage of LTX-2 in violation of this Agreement, update
|
| 168 |
+
LTX-2 through electronic means, or modify the Output of LTX-2
|
| 169 |
+
based on updates. You shall undertake reasonable efforts to use
|
| 170 |
+
the latest version of LTX-2. Any use of the non-current version
|
| 171 |
+
of LTX-2 is done solely at your risk.
|
| 172 |
+
|
| 173 |
+
7. Export Controls and Sanctions Compliance. You acknowledge that
|
| 174 |
+
LTX-2, Derivatives of LTX-2 may be subject to export control laws
|
| 175 |
+
and regulations, including but not limited to the U.S. Export
|
| 176 |
+
Administration Regulations and sanctions programs administered by
|
| 177 |
+
the Office of Foreign Assets Control (OFAC). You represent and
|
| 178 |
+
warrant that you and any users of LTX-2 are not (i) located in,
|
| 179 |
+
organized under the laws of, or ordinarily resident in any country
|
| 180 |
+
or territory subject to comprehensive sanctions; (ii) identified
|
| 181 |
+
on any U.S. government restricted party list, including the
|
| 182 |
+
Specially Designated Nationals and Blocked Persons List; or
|
| 183 |
+
(iii) otherwise prohibited from receiving LTX-2 under applicable
|
| 184 |
+
law. You shall not export, re-export, or transfer LTX-2, directly
|
| 185 |
+
or indirectly, in violation of any applicable export control or
|
| 186 |
+
sanctions laws or regulations. You agree to comply with all
|
| 187 |
+
applicable trade control laws and shall indemnify and hold
|
| 188 |
+
Licensor harmless from any claims arising from your failure to
|
| 189 |
+
comply with such laws.
|
| 190 |
+
|
| 191 |
+
8. Trademarks and related. Nothing in this Agreement permits you to
|
| 192 |
+
make use of Licensor's trademarks, trade names, logos or to
|
| 193 |
+
otherwise suggest endorsement or misrepresent the relationship
|
| 194 |
+
between the parties; and any rights not expressly granted herein
|
| 195 |
+
are reserved by the Licensor.
|
| 196 |
+
|
| 197 |
+
9. Disclaimer of Warranty. Unless required by applicable law or
|
| 198 |
+
agreed to in writing, Licensor provides LTX-2 on an "AS IS" BASIS,
|
| 199 |
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
| 200 |
+
implied, including, without limitation, any warranties or
|
| 201 |
+
conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS
|
| 202 |
+
FOR A PARTICULAR PURPOSE. You are solely responsible for
|
| 203 |
+
determining the appropriateness of using or redistributing LTX-2
|
| 204 |
+
and Derivatives of LTX-2 and assume any risks associated with
|
| 205 |
+
your exercise of permissions under this Agreement.
|
| 206 |
+
|
| 207 |
+
10. Limitation of Liability. In no event and under no legal theory,
|
| 208 |
+
whether in tort (including negligence), contract, or otherwise,
|
| 209 |
+
unless required by applicable law (such as deliberate and grossly
|
| 210 |
+
negligent acts) or agreed to in writing, shall Licensor be liable
|
| 211 |
+
to you for damages, including any direct, indirect, special,
|
| 212 |
+
incidental, or consequential damages of any character arising as
|
| 213 |
+
a result of this Agreement or out of the use or inability to use
|
| 214 |
+
LTX-2 (including but not limited to damages for loss of goodwill,
|
| 215 |
+
work stoppage, computer failure or malfunction, or any and all
|
| 216 |
+
other commercial damages or losses), even if Licensor has been
|
| 217 |
+
advised of the possibility of such damages.
|
| 218 |
+
|
| 219 |
+
11. Accepting Warranty or Additional Liability. While redistributing
|
| 220 |
+
LTX-2 and Derivatives of LTX-2, you may, provided you do not
|
| 221 |
+
violate the terms of this Agreement, choose to offer and charge
|
| 222 |
+
a fee for, acceptance of support, warranty, indemnity, or other
|
| 223 |
+
liability obligations. However, in accepting such obligations,
|
| 224 |
+
you may act only on your own behalf and on your sole
|
| 225 |
+
responsibility, not on behalf of Licensor, and only if you agree
|
| 226 |
+
to indemnify, defend, and hold Licensor harmless for any liability
|
| 227 |
+
incurred by, or claims asserted against Licensor, by reason of
|
| 228 |
+
your accepting any such warranty or additional liability.
|
| 229 |
+
|
| 230 |
+
12. Governing Law. This Agreement and all relations, disputes, claims
|
| 231 |
+
and other matters arising hereunder (including non-contractual
|
| 232 |
+
disputes or claims) will be governed exclusively by, and construed
|
| 233 |
+
exclusively in accordance with, the laws of the State of New York.
|
| 234 |
+
To the extent permitted by law, choice of laws rules and the
|
| 235 |
+
United Nations Convention on Contracts for the International Sale
|
| 236 |
+
of Goods will not apply. For the purposes of adjudicating any
|
| 237 |
+
action or proceeding to enforce the terms of this Agreement, you
|
| 238 |
+
hereby irrevocably consent to the exclusive jurisdiction of, and
|
| 239 |
+
venue in, the federal and state courts located in the County of
|
| 240 |
+
New York within the State of New York. The prevailing party in
|
| 241 |
+
any claim or dispute between the parties under this Agreement
|
| 242 |
+
will be entitled to reimbursement of its reasonable attorneys'
|
| 243 |
+
fees and costs. You hereby waive the right to a trial by jury,
|
| 244 |
+
to participate in a class or representative action (including in
|
| 245 |
+
arbitration), or to combine individual proceedings in court or
|
| 246 |
+
in arbitration without the consent of all parties.
|
| 247 |
+
|
| 248 |
+
13. Term and Termination. This Agreement is effective upon your
|
| 249 |
+
acceptance and continues until terminated. Licensor may terminate
|
| 250 |
+
this Agreement immediately upon written notice to you if you
|
| 251 |
+
breach any provision of this Agreement, including but not limited
|
| 252 |
+
to violations of the use restrictions in Attachment A or
|
| 253 |
+
unauthorized commercial use. Upon termination: (a) all rights
|
| 254 |
+
granted to you under this Agreement will immediately cease;
|
| 255 |
+
(b) you must immediately cease all use of LTX-2 and Derivatives
|
| 256 |
+
of LTX-2; (c) you must delete or destroy all copies of LTX-2
|
| 257 |
+
and Derivatives of LTX-2 in your possession or control; and
|
| 258 |
+
(d) you must notify any third parties to whom you distributed
|
| 259 |
+
LTX-2 or Derivatives of LTX-2 of the termination. Sections 8-13,
|
| 260 |
+
and Section 15 shall survive termination of this Agreement.
|
| 261 |
+
Termination does not relieve you of any obligations incurred
|
| 262 |
+
prior to termination, including payment obligations under
|
| 263 |
+
Section 2. In addition, if You commence a lawsuit or other
|
| 264 |
+
proceedings (including a cross-claim or counterclaim in a lawsuit)
|
| 265 |
+
against Licensor or any person or entity alleging that LTX-2 or
|
| 266 |
+
any Output, or any portion of any of the foregoing, infringe any
|
| 267 |
+
intellectual property or other right owned or licensable by you,
|
| 268 |
+
then all licenses granted to you under this Agreement shall
|
| 269 |
+
terminate as of the date such lawsuit or other proceeding is filed.
|
| 270 |
+
|
| 271 |
+
14. Disputes and Arbitration. All disputes arising in connection with
|
| 272 |
+
this Agreement shall be finally settled by arbitration under the
|
| 273 |
+
Rules of Arbitration of the International Chamber of Commerce
|
| 274 |
+
("ICC Rules"), by one (1) arbitrator appointed in accordance with
|
| 275 |
+
the ICC Rules. The seat of arbitration shall be New York, NY, USA,
|
| 276 |
+
and the proceedings shall be conducted in English. The arbitrator
|
| 277 |
+
shall be empowered to grant any relief that a court could grant.
|
| 278 |
+
Judgment on the arbitration award may be entered by any court
|
| 279 |
+
having jurisdiction thereof. Each party waives its right to a
|
| 280 |
+
trial by jury and to participate in any class or representative
|
| 281 |
+
action.
|
| 282 |
+
|
| 283 |
+
15. If any provision of this Agreement is held to be
|
| 284 |
+
invalid, illegal
|
| 285 |
+
or unenforceable, the remaining provisions shall be unaffected
|
| 286 |
+
thereby and remain valid as if such provision had not been set
|
| 287 |
+
forth herein.
|
| 288 |
+
|
| 289 |
+
END OF TERMS AND CONDITIONS
|
| 290 |
+
|
| 291 |
+
ATTACHMENT A: Use Restrictions
|
| 292 |
+
|
| 293 |
+
When using the Outputs, LTX-2 and any Derivatives thereof, you
|
| 294 |
+
will comply with the Acceptable Use Policy. In addition, you
|
| 295 |
+
agree not to use the Outputs, LTX-2 or its Derivatives in any
|
| 296 |
+
of the following ways:
|
| 297 |
+
|
| 298 |
+
1. In any way that violates any applicable national, federal,
|
| 299 |
+
state, local or international law or regulation;
|
| 300 |
+
|
| 301 |
+
2. For the purpose of exploiting, Harming or attempting to
|
| 302 |
+
exploit or Harm minors in any way;
|
| 303 |
+
|
| 304 |
+
3. To generate or disseminate false information and/or content
|
| 305 |
+
with the purpose of Harming others;
|
| 306 |
+
|
| 307 |
+
4. To generate or disseminate personal identifiable information
|
| 308 |
+
that can be used to Harm an individual;
|
| 309 |
+
|
| 310 |
+
5. To generate or disseminate information and/or content (e.g.
|
| 311 |
+
images, code, posts, articles), and place the information
|
| 312 |
+
and/or content in any context (e.g. bot generating tweets)
|
| 313 |
+
without expressly and intelligibly disclaiming that the
|
| 314 |
+
information and/or content is machine generated;
|
| 315 |
+
|
| 316 |
+
6. To defame, disparage or otherwise harass others;
|
| 317 |
+
|
| 318 |
+
7. To impersonate or attempt to impersonate (e.g. deepfakes)
|
| 319 |
+
others without their consent;
|
| 320 |
+
|
| 321 |
+
8. For fully automated decision making that adversely impacts an
|
| 322 |
+
individual's legal rights or otherwise creates or modifies a
|
| 323 |
+
binding, enforceable obligation;
|
| 324 |
+
|
| 325 |
+
9. For any use intended to or which has the effect of
|
| 326 |
+
discriminating against or Harming individuals or groups based
|
| 327 |
+
on online or offline social behavior or known or predicted
|
| 328 |
+
personal or personality characteristics;
|
| 329 |
+
|
| 330 |
+
10. To exploit any of the vulnerabilities of a specific group of
|
| 331 |
+
persons based on their age, social, physical or mental
|
| 332 |
+
characteristics, in order to materially distort the behavior
|
| 333 |
+
of a person pertaining to that group in a manner that causes
|
| 334 |
+
or is likely to cause that person or another person physical
|
| 335 |
+
or psychological Harm;
|
| 336 |
+
|
| 337 |
+
11. For any use intended to or which has the effect of
|
| 338 |
+
discriminating against individuals or groups based on legally
|
| 339 |
+
protected characteristics or categories;
|
| 340 |
+
|
| 341 |
+
12. To provide medical advice and medical results interpretation;
|
| 342 |
+
|
| 343 |
+
13. To generate or disseminate information for the purpose to be
|
| 344 |
+
used for administration of justice, law enforcement,
|
| 345 |
+
immigration or asylum processes, such as predicting an
|
| 346 |
+
individual will commit fraud/crime commitment (e.g. by text
|
| 347 |
+
profiling, drawing causal relationships between assertions
|
| 348 |
+
made in documents, indiscriminate and arbitrarily-targeted use);
|
| 349 |
+
|
| 350 |
+
14. To generate and/or disseminate malware (including – but not
|
| 351 |
+
limited to – ransomware) or any other content to be used for
|
| 352 |
+
the purpose of harming electronic systems;
|
| 353 |
+
|
| 354 |
+
15. To engage in, promote, incite, or facilitate discrimination
|
| 355 |
+
or other unlawful or harmful conduct in the provision of
|
| 356 |
+
employment, employment benefits, credit, housing, or other
|
| 357 |
+
essential goods and services;
|
| 358 |
+
|
| 359 |
+
16. To engage in, promote, incite, or facilitate the harassment,
|
| 360 |
+
abuse, threatening, or bullying of individuals or groups of
|
| 361 |
+
individuals;
|
| 362 |
+
|
| 363 |
+
17. For military, warfare, nuclear industries or applications,
|
| 364 |
+
weapons development, or any use in connection with activities
|
| 365 |
+
that may cause death, personal injury, or severe physical or
|
| 366 |
+
environmental damage;
|
| 367 |
+
|
| 368 |
+
18. For commercial use only: To train, improve, or fine-tune any
|
| 369 |
+
other machine learning model, artificial intelligence system,
|
| 370 |
+
or competing model, except for Derivatives of LTX-2 as
|
| 371 |
+
expressly permitted under this Agreement;
|
| 372 |
+
|
| 373 |
+
19. To circumvent, disable, or interfere with any technical
|
| 374 |
+
limitations, safety features, content filters, or use
|
| 375 |
+
restrictions implemented in LTX-2 by Licensor;
|
| 376 |
+
|
| 377 |
+
20. To use LTX-2 or Derivatives of LTX-2 in any product, service,
|
| 378 |
+
or application that directly competes with Licensor's
|
| 379 |
+
commercial products or services, or is designed to replace or
|
| 380 |
+
substitute Licensor's offerings in the market, without
|
| 381 |
+
obtaining a separate commercial license from Licensor.
|
README.md
CHANGED
|
@@ -1,3 +1,123 @@
|
|
| 1 |
-
-
|
| 2 |
-
|
| 3 |
-
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LTX-2
|
| 2 |
+
|
| 3 |
+
[](https://ltx.io)
|
| 4 |
+
[](https://huggingface.co/Lightricks/LTX-2.3)
|
| 5 |
+
[](https://console.ltx.video/playground)
|
| 6 |
+
[](https://arxiv.org/abs/2601.03233)
|
| 7 |
+
[](https://discord.gg/ltxplatform)
|
| 8 |
+
|
| 9 |
+
**LTX-2** is the first DiT-based audio-video foundation model that contains all core capabilities of modern video generation in one model: synchronized audio and video, high fidelity, multiple performance modes, production-ready outputs, API access, and open access.
|
| 10 |
+
|
| 11 |
+
<div align="center">
|
| 12 |
+
<video src="https://github.com/user-attachments/assets/4414adc0-086c-43de-b367-9362eeb20228" width="70%" poster=""> </video>
|
| 13 |
+
</div>
|
| 14 |
+
|
| 15 |
+
## 🚀 Quick Start
|
| 16 |
+
|
| 17 |
+
```bash
|
| 18 |
+
# Clone the repository
|
| 19 |
+
git clone https://github.com/Lightricks/LTX-2.git
|
| 20 |
+
cd LTX-2
|
| 21 |
+
|
| 22 |
+
# Set up the environment
|
| 23 |
+
uv sync --frozen
|
| 24 |
+
source .venv/bin/activate
|
| 25 |
+
```
|
| 26 |
+
|
| 27 |
+
### Required Models
|
| 28 |
+
|
| 29 |
+
Download the following models from the [LTX-2.3 HuggingFace repository](https://huggingface.co/Lightricks/LTX-2.3):
|
| 30 |
+
|
| 31 |
+
**LTX-2.3 Model Checkpoint** (choose and download one of the following)
|
| 32 |
+
* [`ltx-2.3-22b-dev.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-dev.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-dev.safetensors)
|
| 33 |
+
* [`ltx-2.3-22b-distilled-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-distilled-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-1.1.safetensors)
|
| 34 |
+
|
| 35 |
+
**Spatial Upscaler** - Required for current two-stage pipeline implementations in this repository
|
| 36 |
+
* [`ltx-2.3-spatial-upscaler-x2-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x2-1.1.safetensors)
|
| 37 |
+
* [`ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-spatial-upscaler-x1.5-1.0.safetensors)
|
| 38 |
+
|
| 39 |
+
**Temporal Upscaler** - Supported by the model and will be required for future pipeline implementations
|
| 40 |
+
* [`ltx-2.3-temporal-upscaler-x2-1.0.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-temporal-upscaler-x2-1.0.safetensors)
|
| 41 |
+
|
| 42 |
+
**Distilled LoRA** - Required for current two-stage pipeline implementations in this repository (except DistilledPipeline, ICLoraPipeline, and LipDubPipeline)
|
| 43 |
+
* [`ltx-2.3-22b-distilled-lora-384-1.1.safetensors`](https://huggingface.co/Lightricks/LTX-2.3/blob/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors) - [Download](https://huggingface.co/Lightricks/LTX-2.3/resolve/main/ltx-2.3-22b-distilled-lora-384-1.1.safetensors)
|
| 44 |
+
|
| 45 |
+
**Gemma Text Encoder** (download all assets from the repository)
|
| 46 |
+
* [`Gemma 3`](https://huggingface.co/google/gemma-3-12b-it-qat-q4_0-unquantized/tree/main)
|
| 47 |
+
|
| 48 |
+
**LoRAs**
|
| 49 |
+
* [`LTX-2.3-22b-IC-LoRA-Union-Control`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control) - [Download](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Union-Control/resolve/main/ltx-2.3-22b-ic-lora-union-control-ref0.5.safetensors)
|
| 50 |
+
* [`LTX-2.3-22b-IC-LoRA-Motion-Track-Control`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control) - [Download](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-Motion-Track-Control/resolve/main/ltx-2.3-22b-ic-lora-motion-track-control-ref0.5.safetensors)
|
| 51 |
+
* [`LTX-2-19b-IC-LoRA-Detailer`](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Detailer/resolve/main/ltx-2-19b-ic-lora-detailer.safetensors)
|
| 52 |
+
* [`LTX-2-19b-IC-LoRA-Pose-Control`](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-IC-LoRA-Pose-Control/resolve/main/ltx-2-19b-ic-lora-pose-control.safetensors)
|
| 53 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Dolly-In`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In/resolve/main/ltx-2-19b-lora-camera-control-dolly-in.safetensors)
|
| 54 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Dolly-Left`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Left/resolve/main/ltx-2-19b-lora-camera-control-dolly-left.safetensors)
|
| 55 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Dolly-Out`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Out/resolve/main/ltx-2-19b-lora-camera-control-dolly-out.safetensors)
|
| 56 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Dolly-Right`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-Right/resolve/main/ltx-2-19b-lora-camera-control-dolly-right.safetensors)
|
| 57 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Jib-Down`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Down/resolve/main/ltx-2-19b-lora-camera-control-jib-down.safetensors)
|
| 58 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Jib-Up`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Jib-Up/resolve/main/ltx-2-19b-lora-camera-control-jib-up.safetensors)
|
| 59 |
+
* [`LTX-2-19b-LoRA-Camera-Control-Static`](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static) - [Download](https://huggingface.co/Lightricks/LTX-2-19b-LoRA-Camera-Control-Static/resolve/main/ltx-2-19b-lora-camera-control-static.safetensors)
|
| 60 |
+
* [`LTX-2.3-22b-IC-LoRA-HDR`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-HDR) - HDR IC-LoRA and pre-computed text embeddings for `HDRICLoraPipeline`
|
| 61 |
+
* [`LTX-2.3-22b-IC-LoRA-LipDub`](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-LipDub) - [Download](https://huggingface.co/Lightricks/LTX-2.3-22b-IC-LoRA-LipDub/resolve/main/ltx-2.3-22b-ic-lora-lipdub-0.9.safetensors)
|
| 62 |
+
|
| 63 |
+
### Available Pipelines
|
| 64 |
+
|
| 65 |
+
* **[TI2VidTwoStagesPipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages.py)** - Production-quality text/image-to-video with 2x upsampling (recommended)
|
| 66 |
+
* **[TI2VidTwoStagesHQPipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages_hq.py)** - Same two-stage flow as above but uses the res_2s second-order sampler (fewer steps, better quality)
|
| 67 |
+
* **[TI2VidOneStagePipeline](packages/ltx-pipelines/src/ltx_pipelines/ti2vid_one_stage.py)** - Single-stage generation for quick prototyping
|
| 68 |
+
* **[DistilledPipeline](packages/ltx-pipelines/src/ltx_pipelines/distilled.py)** - Fastest inference with 8 predefined sigmas
|
| 69 |
+
* **[ICLoraPipeline](packages/ltx-pipelines/src/ltx_pipelines/ic_lora.py)** - Video-to-video and image-to-video transformations (uses distilled model.)
|
| 70 |
+
* **[KeyframeInterpolationPipeline](packages/ltx-pipelines/src/ltx_pipelines/keyframe_interpolation.py)** - Interpolate between keyframe images
|
| 71 |
+
* **[A2VidPipelineTwoStage](packages/ltx-pipelines/src/ltx_pipelines/a2vid_two_stage.py)** - Audio-to-video generation conditioned on an input audio file
|
| 72 |
+
* **[RetakePipeline](packages/ltx-pipelines/src/ltx_pipelines/retake.py)** - Regenerate a specific time region of an existing video
|
| 73 |
+
* **[HDRICLoraPipeline](packages/ltx-pipelines/src/ltx_pipelines/hdr_ic_lora.py)** - Video-to-video with HDR output (linear float frames via LogC3 inverse decode, suitable for EXR export and tonemapping)
|
| 74 |
+
* **[LipDubPipeline](packages/ltx-pipelines/src/ltx_pipelines/lipdub.py)** - Lip dubbing, rephrasing, matching speaker identity (distilled model, single IC-LoRA, Two stages).
|
| 75 |
+
|
| 76 |
+
### ⚡ Optimization Tips
|
| 77 |
+
|
| 78 |
+
* **Use DistilledPipeline** - Fastest inference with only 8 predefined sigmas (8 steps stage 1, 4 steps stage 2)
|
| 79 |
+
* **Enable FP8 quantization** - Enables lower memory footprint: `--quantization fp8-cast` (CLI) or `quantization=QuantizationPolicy.fp8_cast()` (Python). Fp8-cast should be used with bf16 checkpoints, it shall downcast them on the fly. For Hopper GPUs with TensorRT-LLM, use `--quantization fp8-scaled-mm` for FP8 scaled matrix multiplication. Fp8-scaled-mm should be used with fp8 checkpoints.
|
| 80 |
+
* **Install attention optimizations** - Use xFormers (`uv sync --extra xformers`) or [Flash Attention 3](https://github.com/Dao-AILab/flash-attention) for Hopper GPUs
|
| 81 |
+
* **Use gradient estimation** - Reduce inference steps from 40 to 20-30 while maintaining quality (see [pipeline documentation](packages/ltx-pipelines/README.md#denoising-loop-optimization))
|
| 82 |
+
* **Skip memory cleanup** - If you have sufficient VRAM, disable automatic memory cleanup between stages for faster processing
|
| 83 |
+
* **Choose single-stage pipeline** - Use `TI2VidOneStagePipeline` for faster generation when high resolution isn't required
|
| 84 |
+
|
| 85 |
+
## ✍️ Prompting for LTX-2
|
| 86 |
+
|
| 87 |
+
When writing prompts, focus on detailed, chronological descriptions of actions and scenes. Include specific movements, appearances, camera angles, and environmental details - all in a single flowing paragraph. Start directly with the action, and keep descriptions literal and precise. Think like a cinematographer describing a shot list. Keep within 200 words. For best results, build your prompts using this structure:
|
| 88 |
+
|
| 89 |
+
- Start with main action in a single sentence
|
| 90 |
+
- Add specific details about movements and gestures
|
| 91 |
+
- Describe character/object appearances precisely
|
| 92 |
+
- Include background and environment details
|
| 93 |
+
- Specify camera angles and movements
|
| 94 |
+
- Describe lighting and colors
|
| 95 |
+
- Note any changes or sudden events
|
| 96 |
+
|
| 97 |
+
For additional guidance on writing a prompt please refer to <https://ltx.video/blog/how-to-prompt-for-ltx-2>
|
| 98 |
+
|
| 99 |
+
### Automatic Prompt Enhancement
|
| 100 |
+
|
| 101 |
+
LTX-2 pipelines support automatic prompt enhancement via an `enhance_prompt` parameter.
|
| 102 |
+
|
| 103 |
+
## 🔌 ComfyUI Integration
|
| 104 |
+
|
| 105 |
+
To use our model with ComfyUI, please follow the instructions at <https://github.com/Lightricks/ComfyUI-LTXVideo/>.
|
| 106 |
+
|
| 107 |
+
## 📦 Packages
|
| 108 |
+
|
| 109 |
+
This repository is organized as a monorepo with three main packages:
|
| 110 |
+
|
| 111 |
+
* **[ltx-core](packages/ltx-core/)** - Core model implementation, inference stack, and utilities
|
| 112 |
+
* **[ltx-pipelines](packages/ltx-pipelines/)** - High-level pipeline implementations for text-to-video, image-to-video, and other generation modes
|
| 113 |
+
* **[ltx-trainer](packages/ltx-trainer/)** - Training and fine-tuning tools for LoRA, full fine-tuning, and IC-LoRA
|
| 114 |
+
|
| 115 |
+
Each package has its own README and documentation. See the [Documentation](#-documentation) section below.
|
| 116 |
+
|
| 117 |
+
## 📚 Documentation
|
| 118 |
+
|
| 119 |
+
Each package includes comprehensive documentation:
|
| 120 |
+
|
| 121 |
+
* **[LTX-Core README](packages/ltx-core/README.md)** - Core model implementation, inference stack, and utilities
|
| 122 |
+
* **[LTX-Pipelines README](packages/ltx-pipelines/README.md)** - High-level pipeline implementations and usage guides
|
| 123 |
+
* **[LTX-Trainer README](packages/ltx-trainer/README.md)** - Training and fine-tuning documentation with detailed guides
|
packages/ltx-core/README.md
ADDED
|
@@ -0,0 +1,413 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LTX-Core
|
| 2 |
+
|
| 3 |
+
The foundational library for the LTX-2 Audio-Video generation model. This package contains the raw model definitions, component implementations, and loading logic used by `ltx-pipelines` and `ltx-trainer`.
|
| 4 |
+
|
| 5 |
+
## 📦 What's Inside?
|
| 6 |
+
|
| 7 |
+
- **`components/`**: Modular diffusion components (Schedulers, Guiders, Noisers, Patchifiers) following standard protocols
|
| 8 |
+
- **`conditioning/`**: Tools for preparing latent states and applying conditioning (image, video, keyframes)
|
| 9 |
+
- **`guidance/`**: Perturbation system for fine-grained control over attention mechanisms
|
| 10 |
+
- **`loader/`**: Utilities for loading weights from `.safetensors`, fusing LoRAs, and managing memory
|
| 11 |
+
- **`model/`**: PyTorch implementations of the LTX-2 Transformer, Video VAE, Audio VAE, Vocoder and Upscaler
|
| 12 |
+
- **`text_encoders/gemma`**: Gemma text encoder implementation with tokenizers, feature extractors, and separate encoders for audio-video and video-only generation
|
| 13 |
+
- **`quantization/`**: FP8 quantization backends (FP8-TensorRT-LLM scaled MM, FP8 cast) for reduced memory footprint.
|
| 14 |
+
|
| 15 |
+
## 🚀 Quick Start
|
| 16 |
+
|
| 17 |
+
`ltx-core` provides the building blocks (models, components, and utilities) needed to construct inference flows. For ready-made inference pipelines use [`ltx-pipelines`](../ltx-pipelines/) or [`ltx-trainer`](../ltx-trainer/) for training.
|
| 18 |
+
|
| 19 |
+
## 🔧 Installation
|
| 20 |
+
|
| 21 |
+
```bash
|
| 22 |
+
# From the repository root
|
| 23 |
+
uv sync --frozen
|
| 24 |
+
|
| 25 |
+
# Or install as a package
|
| 26 |
+
pip install -e packages/ltx-core
|
| 27 |
+
```
|
| 28 |
+
|
| 29 |
+
## Building Blocks Overview
|
| 30 |
+
|
| 31 |
+
`ltx-core` provides modular components that can be combined to build custom inference flows:
|
| 32 |
+
|
| 33 |
+
### Core Models
|
| 34 |
+
|
| 35 |
+
- **Transformer** ([`model/transformer/`](src/ltx_core/model/transformer/)): The asymmetric dual-stream LTX-2 transformer (14B-parameter video stream, 5B-parameter audio stream) with bidirectional cross-modal attention for joint audio-video processing. Expects inputs in [`Modality`](src/ltx_core/model/transformer/modality.py) format
|
| 36 |
+
- **Video VAE** ([`model/video_vae/`](src/ltx_core/model/video_vae/)): Encodes/decodes video pixels to/from latent space with temporal and spatial compression
|
| 37 |
+
- **Audio VAE** ([`model/audio_vae/`](src/ltx_core/model/audio_vae/)): Encodes/decodes audio spectrograms to/from latent space
|
| 38 |
+
- **Vocoder** ([`model/audio_vae/`](src/ltx_core/model/audio_vae/)): Neural vocoder that converts mel spectrograms to audio waveforms
|
| 39 |
+
- **Text Encoder** ([`text_encoders/`](src/ltx_core/text_encoders/)): Gemma 3-based multilingual encoder with multi-layer feature extraction and thinking tokens that produces separate embeddings for video and audio conditioning
|
| 40 |
+
- **Spatial Upscaler** ([`model/upsampler/`](src/ltx_core/model/upsampler/)): Upsamples latent representations for higher-resolution generation
|
| 41 |
+
|
| 42 |
+
### Diffusion Components
|
| 43 |
+
|
| 44 |
+
- **Schedulers** ([`components/schedulers.py`](src/ltx_core/components/schedulers.py)): Noise schedules (LTX2Scheduler, LinearQuadratic, Beta) that control the denoising process
|
| 45 |
+
- **Guiders** ([`components/guiders.py`](src/ltx_core/components/guiders.py)): Guidance strategies (CFG, STG, APG) for controlling generation quality and adherence to prompts
|
| 46 |
+
- **Noisers** ([`components/noisers.py`](src/ltx_core/components/noisers.py)): Add noise to latents according to the diffusion schedule
|
| 47 |
+
- **Patchifiers** ([`components/patchifiers.py`](src/ltx_core/components/patchifiers.py)): Convert between spatial latents `[B, C, F, H, W]` and sequence format `[B, seq_len, dim]` for transformer processing
|
| 48 |
+
|
| 49 |
+
### Conditioning & Control
|
| 50 |
+
|
| 51 |
+
- **Conditioning** ([`conditioning/`](src/ltx_core/conditioning/)): Tools for preparing and applying various conditioning types (image, video, keyframes)
|
| 52 |
+
- **Guidance** ([`guidance/`](src/ltx_core/guidance/)): Perturbation system for fine-grained control over attention mechanisms (e.g., skipping specific attention layers)
|
| 53 |
+
|
| 54 |
+
### Utilities
|
| 55 |
+
|
| 56 |
+
- **Loader** ([`loader/`](src/ltx_core/loader/)): Model loading from `.safetensors`, LoRA fusion, weight remapping, and memory management
|
| 57 |
+
- **Quantization** ([`quantization/`](src/ltx_core/quantization/)): FP8 quantization backends for reduced memory footprint and faster inference
|
| 58 |
+
|
| 59 |
+
### Loader
|
| 60 |
+
|
| 61 |
+
The `loader/` module provides `SingleGPUModelBuilder`, a frozen dataclass that loads a PyTorch model from `.safetensors` checkpoints and optionally fuses one or more LoRA adapters.
|
| 62 |
+
|
| 63 |
+
#### Basic usage
|
| 64 |
+
|
| 65 |
+
```python
|
| 66 |
+
from ltx_core.loader import SingleGPUModelBuilder
|
| 67 |
+
|
| 68 |
+
builder = SingleGPUModelBuilder(
|
| 69 |
+
model_class_configurator=MyModelConfigurator,
|
| 70 |
+
model_path="/path/to/model.safetensors",
|
| 71 |
+
)
|
| 72 |
+
model = builder.build(device=torch.device("cuda"))
|
| 73 |
+
```
|
| 74 |
+
|
| 75 |
+
#### Loading LoRA adapters
|
| 76 |
+
|
| 77 |
+
Use the `.lora()` method to attach one or more LoRA adapters before calling `.build()`:
|
| 78 |
+
|
| 79 |
+
```python
|
| 80 |
+
from ltx_core.loader import SDOps
|
| 81 |
+
|
| 82 |
+
lora_sd_ops = SDOps(name="identity").with_matching() # or a model-specific key-renaming SDOps
|
| 83 |
+
|
| 84 |
+
builder = (
|
| 85 |
+
SingleGPUModelBuilder(
|
| 86 |
+
model_class_configurator=MyModelConfigurator,
|
| 87 |
+
model_path="/path/to/model.safetensors",
|
| 88 |
+
)
|
| 89 |
+
.lora("/path/to/lora_a.safetensors", 0.8, lora_sd_ops)
|
| 90 |
+
.lora("/path/to/lora_b.safetensors", 0.5, lora_sd_ops)
|
| 91 |
+
)
|
| 92 |
+
model = builder.build(device=torch.device("cuda"))
|
| 93 |
+
```
|
| 94 |
+
|
| 95 |
+
#### Memory-efficient LoRA loading (`lora_load_device`)
|
| 96 |
+
|
| 97 |
+
By default, LoRA weights are loaded onto the **CPU** (`lora_load_device=torch.device("cpu")`). This means each LoRA adapter is kept in CPU memory and transferred to the GPU sequentially during weight fusion, which keeps peak GPU memory low even when fusing large adapters.
|
| 98 |
+
|
| 99 |
+
If all adapters fit comfortably in GPU memory you can skip the CPU staging by setting `lora_load_device` to the target CUDA device:
|
| 100 |
+
|
| 101 |
+
```python
|
| 102 |
+
import torch
|
| 103 |
+
from ltx_core.loader import SingleGPUModelBuilder
|
| 104 |
+
|
| 105 |
+
# Load LoRA weights directly onto the GPU (faster, but uses more GPU memory)
|
| 106 |
+
builder = SingleGPUModelBuilder(
|
| 107 |
+
model_class_configurator=MyModelConfigurator,
|
| 108 |
+
model_path="/path/to/model.safetensors",
|
| 109 |
+
lora_load_device=torch.device("cuda"),
|
| 110 |
+
).lora("/path/to/lora.safetensors", 1.0, lora_sd_ops)
|
| 111 |
+
|
| 112 |
+
model = builder.build(device=torch.device("cuda"))
|
| 113 |
+
```
|
| 114 |
+
|
| 115 |
+
### Quantization
|
| 116 |
+
|
| 117 |
+
The `quantization/` module provides FP8 quantization support for the LTX-2 transformer, significantly reducing memory usage while maintaining quality. Two backends are available:
|
| 118 |
+
|
| 119 |
+
#### FP8 Scaled MM (TensorRT-LLM)
|
| 120 |
+
|
| 121 |
+
Uses NVIDIA TensorRT-LLM's `cublas_scaled_mm` for efficient FP8 matrix multiplication. Weights are stored in FP8 format with per-tensor scaling, and inputs are quantized dynamically (or statically with calibration data).
|
| 122 |
+
|
| 123 |
+
**Requirements**: `uv sync --frozen --extra fp8-trtllm`
|
| 124 |
+
|
| 125 |
+
**Usage with QuantizationPolicy:**
|
| 126 |
+
|
| 127 |
+
```python
|
| 128 |
+
from ltx_core.quantization import QuantizationPolicy
|
| 129 |
+
|
| 130 |
+
# Dynamic input quantization (no calibration needed)
|
| 131 |
+
policy = QuantizationPolicy.fp8_scaled_mm()
|
| 132 |
+
|
| 133 |
+
# Static input quantization with calibration file
|
| 134 |
+
policy = QuantizationPolicy.fp8_scaled_mm(calibration_amax_path="/path/to/amax.json")
|
| 135 |
+
```
|
| 136 |
+
|
| 137 |
+
The policy provides `sd_ops` and `module_ops` that can be passed to the model builder:
|
| 138 |
+
|
| 139 |
+
```python
|
| 140 |
+
from ltx_core.loader import SingleGPUModelBuilder
|
| 141 |
+
|
| 142 |
+
builder = SingleGPUModelBuilder(
|
| 143 |
+
model=model,
|
| 144 |
+
device=device,
|
| 145 |
+
sd_ops=policy.sd_ops,
|
| 146 |
+
module_ops=policy.module_ops,
|
| 147 |
+
)
|
| 148 |
+
builder.load(checkpoint_path)
|
| 149 |
+
```
|
| 150 |
+
|
| 151 |
+
**Calibration File Format** (for static input quantization):
|
| 152 |
+
|
| 153 |
+
```json
|
| 154 |
+
{
|
| 155 |
+
"amax_values": {
|
| 156 |
+
"transformer_blocks.0.attn.to_q.input_quantizer": 12.5,
|
| 157 |
+
"transformer_blocks.0.attn.to_k.input_quantizer": 8.3,
|
| 158 |
+
...
|
| 159 |
+
}
|
| 160 |
+
}
|
| 161 |
+
```
|
| 162 |
+
|
| 163 |
+
#### FP8 Cast
|
| 164 |
+
|
| 165 |
+
A simpler approach that casts weights to FP8 for storage and upcasts during inference:
|
| 166 |
+
|
| 167 |
+
```python
|
| 168 |
+
policy = QuantizationPolicy.fp8_cast()
|
| 169 |
+
```
|
| 170 |
+
|
| 171 |
+
For complete, production-ready pipeline implementations that combine these building blocks, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 172 |
+
|
| 173 |
+
---
|
| 174 |
+
|
| 175 |
+
# Architecture Overview
|
| 176 |
+
|
| 177 |
+
This section provides a deep dive into the internal architecture of the LTX-2 Audio-Video generation model.
|
| 178 |
+
|
| 179 |
+
## Table of Contents
|
| 180 |
+
|
| 181 |
+
1. [High-Level Architecture](#high-level-architecture)
|
| 182 |
+
2. [The Transformer](#the-transformer)
|
| 183 |
+
3. [Video VAE](#video-vae)
|
| 184 |
+
4. [Audio VAE](#audio-vae)
|
| 185 |
+
5. [Text Encoding (Gemma)](#text-encoding-gemma)
|
| 186 |
+
6. [Spatial Upscaler](#spatial-upsampler)
|
| 187 |
+
7. [Data Flow](#data-flow)
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## High-Level Architecture
|
| 192 |
+
|
| 193 |
+
LTX-2 is an **asymmetric dual-stream diffusion transformer** that jointly models the text-conditioned distribution of video and audio signals, capturing true joint dependencies (unlike sequential T2V→V2A pipelines).
|
| 194 |
+
|
| 195 |
+
### Key Design Principles
|
| 196 |
+
|
| 197 |
+
- **Decoupled Latent Representations**: Separate modality-specific VAEs enable 3D RoPE (video) vs 1D RoPE (audio), independent compression optimization, and native V2A/A2V editing workflows
|
| 198 |
+
- **Asymmetric Dual-Stream**: 14B-parameter video stream (spatiotemporal dynamics) + 5B-parameter audio stream (1D temporal), sharing 48 transformer blocks but differing in width
|
| 199 |
+
- **Bidirectional Cross-Modal Attention**: 1D temporal RoPE enables sub-frame alignment, mapping visual cues to auditory events (lip-sync, foley, environmental acoustics)
|
| 200 |
+
- **Cross-Modality AdaLN**: Scaling/shift parameters conditioned on the other modality's hidden states for synchronization across differing diffusion timesteps/temporal resolutions
|
| 201 |
+
|
| 202 |
+
```text
|
| 203 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 204 |
+
│ INPUT PREPARATION │
|
| 205 |
+
│ │
|
| 206 |
+
│ Video Pixels → Video VAE Encoder → Video Latents │
|
| 207 |
+
│ Audio Waveform → Audio VAE Encoder → Audio Latents │
|
| 208 |
+
│ Text Prompt → Gemma 3 Encoder → Text Embeddings │
|
| 209 |
+
└─────────────────────────────────────────────────────────────┘
|
| 210 |
+
↓
|
| 211 |
+
┌──────────────────────���──────────────────────────────────────┐
|
| 212 |
+
│ LTX-2 ASYMMETRIC DUAL-STREAM TRANSFORMER (48 Blocks) │
|
| 213 |
+
│ │
|
| 214 |
+
│ ┌──────────────────────┐ ┌──────────────────────┐ │
|
| 215 |
+
│ │ Video Stream (14B) │ │ Audio Stream (5B) │ │
|
| 216 |
+
│ │ │ │ │ │
|
| 217 |
+
│ │ 3D RoPE (x,y,t) │ │ 1D RoPE (temporal) │ │
|
| 218 |
+
│ │ │ │ │ │
|
| 219 |
+
│ │ Self-Attn │ │ Self-Attn │ │
|
| 220 |
+
│ │ Text Cross-Attn │ │ Text Cross-Attn │ │
|
| 221 |
+
│ │ │◄────►│ │ │
|
| 222 |
+
│ │ A↔V Cross-Attn │ │ A↔V Cross-Attn │ │
|
| 223 |
+
│ │ (1D temporal RoPE) │ │ (1D temporal RoPE) │ │
|
| 224 |
+
│ │ Cross-modality │ │ Cross-modality │ │
|
| 225 |
+
│ │ AdaLN │ │ AdaLN │ │
|
| 226 |
+
│ │ Feed-Forward │ │ Feed-Forward │ │
|
| 227 |
+
│ └──────────────────────┘ └──────────────────────┘ │
|
| 228 |
+
└─────────────────────────────────────────────────────────────┘
|
| 229 |
+
↓
|
| 230 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 231 |
+
│ OUTPUT DECODING │
|
| 232 |
+
│ │
|
| 233 |
+
│ Video Latents → Video VAE Decoder → Video Pixels │
|
| 234 |
+
│ Audio Latents → Audio VAE Decoder → Mel Spectrogram │
|
| 235 |
+
│ Mel Spectrogram → Vocoder → Audio Waveform (24 kHz) │
|
| 236 |
+
└─────────────────────────────────────────────────────────────┘
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
---
|
| 240 |
+
|
| 241 |
+
## The Transformer
|
| 242 |
+
|
| 243 |
+
The core of LTX-2 is an **asymmetric dual-stream diffusion transformer** with 48 layers that processes both video and audio tokens simultaneously. The architecture allocates 14B parameters to the video stream and 5B parameters to the audio stream, reflecting the different information densities of the two modalities.
|
| 244 |
+
|
| 245 |
+
### Model Structure
|
| 246 |
+
|
| 247 |
+
**Source**: [`src/ltx_core/model/transformer/model.py`](src/ltx_core/model/transformer/model.py)
|
| 248 |
+
|
| 249 |
+
The `LTXModel` class implements the transformer. It supports both video-only and audio-video generation modes. For actual usage, see the [`ltx-pipelines`](../ltx-pipelines/) package which handles model loading and initialization.
|
| 250 |
+
|
| 251 |
+
### Transformer Block Architecture
|
| 252 |
+
|
| 253 |
+
**Source**: [`src/ltx_core/model/transformer/transformer.py`](src/ltx_core/model/transformer/transformer.py)
|
| 254 |
+
|
| 255 |
+
Each dual-stream block performs four operations sequentially:
|
| 256 |
+
|
| 257 |
+
1. **Self-Attention**: Within-modality attention for each stream
|
| 258 |
+
2. **Text Cross-Attention**: Textual prompt conditioning for both streams
|
| 259 |
+
3. **Audio-Visual Cross-Attention**: Bidirectional inter-modal exchange
|
| 260 |
+
4. **Feed-Forward Network (FFN)**: Feature refinement
|
| 261 |
+
|
| 262 |
+
```text
|
| 263 |
+
┌─────────────────────────────────────────────────────────────┐
|
| 264 |
+
│ TRANSFORMER BLOCK │
|
| 265 |
+
│ │
|
| 266 |
+
│ VIDEO (14B): Input → RMSNorm → AdaLN → Self-Attn → │
|
| 267 |
+
│ RMSNorm → Text Cross-Attn → │
|
| 268 |
+
│ RMSNorm → AdaLN → A↔V Cross-Attn (1D RoPE) → │
|
| 269 |
+
│ RMSNorm → AdaLN → FFN → Output │
|
| 270 |
+
│ │
|
| 271 |
+
│ AUDIO (5B): Input → RMSNorm → AdaLN → Self-Attn → │
|
| 272 |
+
│ RMSNorm → Text Cross-Attn → │
|
| 273 |
+
│ RMSNorm → AdaLN → A↔V Cross-Attn (1D RoPE) → │
|
| 274 |
+
│ RMSNorm → AdaLN → FFN → Output │
|
| 275 |
+
│ │
|
| 276 |
+
│ RoPE: Video=3D (x,y,t), Audio=1D (t), Cross-Attn=1D (t) │
|
| 277 |
+
│ AdaLN: Timestep-conditioned, cross-modality for A↔V CA │
|
| 278 |
+
└─────────────────────────────────────────────────────────────┘
|
| 279 |
+
```
|
| 280 |
+
|
| 281 |
+
### Audio-Visual Cross-Attention Details
|
| 282 |
+
|
| 283 |
+
Bidirectional cross-attention enables tight temporal alignment: video and audio streams exchange information bidirectionally using 1D temporal RoPE (synchronization only, no spatial alignment). AdaLN gates condition on each modality's timestep for cross-modal synchronization.
|
| 284 |
+
|
| 285 |
+
### Perturbations
|
| 286 |
+
|
| 287 |
+
The transformer supports [**perturbations**](src/ltx_core/guidance/perturbations.py) that selectively skip attention operations.
|
| 288 |
+
|
| 289 |
+
Perturbations allow you to disable specific attention mechanisms during inference, which is useful for guidance techniques like STG (Spatio-Temporal Guidance).
|
| 290 |
+
|
| 291 |
+
**Supported Perturbation Types**:
|
| 292 |
+
|
| 293 |
+
- `SKIP_VIDEO_SELF_ATTN`: Skip video self-attention
|
| 294 |
+
- `SKIP_AUDIO_SELF_ATTN`: Skip audio self-attention
|
| 295 |
+
- `SKIP_A2V_CROSS_ATTN`: Skip audio-to-video cross-attention
|
| 296 |
+
- `SKIP_V2A_CROSS_ATTN`: Skip video-to-audio cross-attention
|
| 297 |
+
|
| 298 |
+
Perturbations are used internally by guidance mechanisms like STG (Spatio-Temporal Guidance). For usage examples, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 299 |
+
|
| 300 |
+
---
|
| 301 |
+
|
| 302 |
+
## Video VAE
|
| 303 |
+
|
| 304 |
+
The Video VAE ([`src/ltx_core/model/video_vae/`](src/ltx_core/model/video_vae/)) encodes video pixels into latent representations and decodes them back.
|
| 305 |
+
|
| 306 |
+
### Architecture
|
| 307 |
+
|
| 308 |
+
- **Encoder**: Compresses `[B, 3, F, H, W]` pixels → `[B, 128, F', H/32, W/32]` latents
|
| 309 |
+
- Where `F' = 1 + (F-1)/8` (frame count must satisfy `(F-1) % 8 == 0`)
|
| 310 |
+
- Example: `[B, 3, 33, 512, 512]` → `[B, 128, 5, 16, 16]`
|
| 311 |
+
- **Decoder**: Expands `[B, 128, F, H, W]` latents → `[B, 3, F', H*32, W*32]` pixels
|
| 312 |
+
- Where `F' = 1 + (F-1)*8`
|
| 313 |
+
- Example: `[B, 128, 5, 16, 16]` → `[B, 3, 33, 512, 512]`
|
| 314 |
+
|
| 315 |
+
The Video VAE is used internally by pipelines for encoding video pixels to latents and decoding latents back to pixels. For usage examples, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 316 |
+
|
| 317 |
+
---
|
| 318 |
+
|
| 319 |
+
## Audio VAE
|
| 320 |
+
|
| 321 |
+
The Audio VAE ([`src/ltx_core/model/audio_vae/`](src/ltx_core/model/audio_vae/)) processes audio spectrograms.
|
| 322 |
+
|
| 323 |
+
### Audio VAE Architecture
|
| 324 |
+
|
| 325 |
+
Compact neural audio representation optimized for diffusion-based training. Natively supports stereo: processes two-channel mel-spectrograms (16 kHz input) with channel concatenation before encoding.
|
| 326 |
+
|
| 327 |
+
- **Encoder**: `[B, mel_bins, T]` → `[B, 8, T/4, 16]` latents (4× temporal downsampling, 8 channels, 16 mel bins in latent space, ~1/25s per token, 128-dim feature vector)
|
| 328 |
+
- **Decoder**: `[B, 8, T, 16]` → `[B, mel_bins, T*4]` mel spectrogram
|
| 329 |
+
- **Vocoder**: HiFi-GAN-based, modified for stereo synthesis and upsampling (16 kHz mel → 24 kHz waveform, doubled generator capacity for stereo)
|
| 330 |
+
|
| 331 |
+
**Downsampling**:
|
| 332 |
+
|
| 333 |
+
- Temporal: 4× (time steps)
|
| 334 |
+
- Frequency: Variable (input mel_bins → fixed 16 in latent space)
|
| 335 |
+
|
| 336 |
+
The Audio VAE is used internally by pipelines for encoding mel spectrograms to latents and decoding latents back to mel spectrograms. The vocoder converts mel spectrograms to audio waveforms. For usage examples, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 337 |
+
|
| 338 |
+
---
|
| 339 |
+
|
| 340 |
+
## Text Encoding (Gemma)
|
| 341 |
+
|
| 342 |
+
LTX-2 uses **Gemma 3** (Gemma 3-12B) as the multilingual text encoder backbone, located in [`src/ltx_core/text_encoders/gemma/`](src/ltx_core/text_encoders/gemma/). Advanced text understanding is critical not only for global language support but for the phonetic and semantic accuracy of generated speech.
|
| 343 |
+
|
| 344 |
+
### Text Encoder Architecture
|
| 345 |
+
|
| 346 |
+
The text conditioning pipeline consists of three stages:
|
| 347 |
+
|
| 348 |
+
1. **Gemma 3 Backbone**: Decoder-only LLM processes text tokens → embeddings across all layers `[B, T, D, L]`
|
| 349 |
+
2. **Multi-Layer Feature Extractor**: Aggregates features from all decoder layers (not just final layer), applies mean-centered scaling, flattens to `[B, T, D×L]`, and projects via learnable matrix W (jointly optimized with LTX-2, LLM weights frozen)
|
| 350 |
+
3. **Text Connector**: Bidirectional transformer blocks with learnable registers (replacing padded positions, also referred to as "thinking tokens" in the paper) for contextual mixing. Separate connectors for video and audio streams (`Embeddings1DConnector`)
|
| 351 |
+
|
| 352 |
+
**Encoders**:
|
| 353 |
+
|
| 354 |
+
- `AVGemmaTextEncoderModel`: Audio-video generation (two connectors → `AVGemmaEncoderOutput` with separate video/audio contexts)
|
| 355 |
+
- `VideoGemmaTextEncoderModel`: Video-only generation (single connector → `VideoGemmaEncoderOutput`)
|
| 356 |
+
|
| 357 |
+
### System Prompts
|
| 358 |
+
|
| 359 |
+
System prompts are also used to enhance user's prompts.
|
| 360 |
+
|
| 361 |
+
- **Text-to-Video**: [`gemma_t2v_system_prompt.txt`](src/ltx_core/text_encoders/gemma/encoders/prompts/gemma_t2v_system_prompt.txt)
|
| 362 |
+
- **Image-to-Video**: [`gemma_i2v_system_prompt.txt`](src/ltx_core/text_encoders/gemma/encoders/prompts/gemma_i2v_system_prompt.txt)
|
| 363 |
+
|
| 364 |
+
**Important**: Video and audio receive **different** context embeddings, even from the same prompt. This allows better modality-specific conditioning and enables the model to synthesize speech that is synchronized with visual lip movement while being natural in cadence, accent, and emotional tone.
|
| 365 |
+
|
| 366 |
+
**Output Format**:
|
| 367 |
+
|
| 368 |
+
- Video context: `[B, seq_len, 4096]` - Video-specific text embeddings
|
| 369 |
+
- Audio context: `[B, seq_len, 2048]` - Audio-specific text embeddings
|
| 370 |
+
|
| 371 |
+
The text encoder is used internally by pipelines. For usage examples, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 372 |
+
|
| 373 |
+
---
|
| 374 |
+
|
| 375 |
+
## Upscaler
|
| 376 |
+
|
| 377 |
+
The Upscaler ([`src/ltx_core/model/upsampler/`](src/ltx_core/model/upsampler/)) upsamples latent representations for higher-resolution output.
|
| 378 |
+
|
| 379 |
+
The spatial upsampler is used internally by two-stage pipelines (e.g., [`TI2VidTwoStagesPipeline`](../ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages.py), [`ICLoraPipeline`](../ltx-pipelines/src/ltx_pipelines/ic_lora.py)) to upsample low-resolution latents before final VAE decoding. For usage examples, see the [`ltx-pipelines`](../ltx-pipelines/) package.
|
| 380 |
+
|
| 381 |
+
---
|
| 382 |
+
|
| 383 |
+
## Data Flow
|
| 384 |
+
|
| 385 |
+
### Complete Generation Pipeline
|
| 386 |
+
|
| 387 |
+
Here's how all the components work together conceptually ([`src/ltx_core/components/`](src/ltx_core/components/)):
|
| 388 |
+
|
| 389 |
+
**Pipeline Steps**:
|
| 390 |
+
|
| 391 |
+
1. **Text Encoding**: Text prompt → Gemma encoder → separate video/audio embeddings
|
| 392 |
+
2. **Latent Initialization**: Initialize noise latents in spatial format `[B, C, F, H, W]`
|
| 393 |
+
3. **Patchification**: Convert spatial latents to sequence format `[B, seq_len, dim]` for transformer
|
| 394 |
+
4. **Sigma Schedule**: Generate noise schedule (adapts to token count)
|
| 395 |
+
5. **Denoising Loop**: Iteratively denoise using transformer predictions
|
| 396 |
+
- Create Modality inputs with per-token timesteps and RoPE positions
|
| 397 |
+
- Forward pass through transformer (conditional and unconditional for CFG)
|
| 398 |
+
- Apply guidance (CFG, STG, etc.)
|
| 399 |
+
- Update latents using diffusion step (Euler, etc.)
|
| 400 |
+
6. **Unpatchification**: Convert sequence back to spatial format
|
| 401 |
+
7. **VAE Decoding**: Decode latents to pixel space (with optional upsampling for two-stage)
|
| 402 |
+
|
| 403 |
+
- [`TI2VidTwoStagesPipeline`](../ltx-pipelines/src/ltx_pipelines/ti2vid_two_stages.py) - Two-stage text-to-video (recommended)
|
| 404 |
+
- [`ICLoraPipeline`](../ltx-pipelines/src/ltx_pipelines/ic_lora.py) - Video-to-video with IC-LoRA control
|
| 405 |
+
- [`DistilledPipeline`](../ltx-pipelines/src/ltx_pipelines/distilled.py) - Fast inference with distilled model
|
| 406 |
+
- [`KeyframeInterpolationPipeline`](../ltx-pipelines/src/ltx_pipelines/keyframe_interpolation.py) - Keyframe-based interpolation
|
| 407 |
+
|
| 408 |
+
See the [ltx-pipelines README](../ltx-pipelines/README.md) for usage examples.
|
| 409 |
+
|
| 410 |
+
## 🔗 Related Projects
|
| 411 |
+
|
| 412 |
+
- **[ltx-pipelines](../ltx-pipelines/)** - High-level pipeline implementations for text-to-video, image-to-video, and video-to-video
|
| 413 |
+
- **[ltx-trainer](../ltx-trainer/)** - Training and fine-tuning tools
|
packages/ltx-core/pyproject.toml
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "ltx-core"
|
| 3 |
+
version = "1.1.3"
|
| 4 |
+
description = "Core implementation of Lightricks' LTX-2 model"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.10"
|
| 7 |
+
dependencies = [
|
| 8 |
+
"torch~=2.7",
|
| 9 |
+
"torchaudio",
|
| 10 |
+
"einops",
|
| 11 |
+
"numpy",
|
| 12 |
+
"transformers>=4.52",
|
| 13 |
+
"safetensors",
|
| 14 |
+
"accelerate",
|
| 15 |
+
"scipy>=1.14",
|
| 16 |
+
]
|
| 17 |
+
|
| 18 |
+
[project.optional-dependencies]
|
| 19 |
+
xformers = ["xformers"]
|
| 20 |
+
fp8-trtllm = [
|
| 21 |
+
"tensorrt-llm==1.0.0",
|
| 22 |
+
"onnx>=1.16.0,<1.20.0",
|
| 23 |
+
"openmpi",
|
| 24 |
+
]
|
| 25 |
+
|
| 26 |
+
[tool.uv]
|
| 27 |
+
conflicts = [
|
| 28 |
+
[
|
| 29 |
+
{ extra = "xformers" },
|
| 30 |
+
{ extra = "fp8-trtllm" },
|
| 31 |
+
],
|
| 32 |
+
]
|
| 33 |
+
|
| 34 |
+
[tool.uv.sources]
|
| 35 |
+
xformers = { index = "pytorch" }
|
| 36 |
+
tensorrt-llm = { index = "nvidia" }
|
| 37 |
+
|
| 38 |
+
[[tool.uv.index]]
|
| 39 |
+
name = "pytorch"
|
| 40 |
+
url = "https://download.pytorch.org/whl/cu129"
|
| 41 |
+
explicit = true
|
| 42 |
+
|
| 43 |
+
[[tool.uv.index]]
|
| 44 |
+
name = "nvidia"
|
| 45 |
+
url = "https://pypi.nvidia.com/"
|
| 46 |
+
explicit = true
|
| 47 |
+
|
| 48 |
+
[build-system]
|
| 49 |
+
requires = ["uv_build>=0.9.8,<0.10.0"]
|
| 50 |
+
build-backend = "uv_build"
|
| 51 |
+
|
| 52 |
+
[dependency-groups]
|
| 53 |
+
dev = [
|
| 54 |
+
"scikit-image>=0.25.2",
|
| 55 |
+
]
|
packages/ltx-core/src/ltx_core/__init__.py
ADDED
|
File without changes
|
packages/ltx-core/src/ltx_core/batch_split.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Batch-splitting adapter for the transformer.
|
| 2 |
+
Wraps an ``X0Model`` (or ``BlockStreamingWrapper``) and splits batched inputs
|
| 3 |
+
into smaller chunks before forwarding, then concatenates the results. This
|
| 4 |
+
controls peak activation memory at the cost of more forward passes.
|
| 5 |
+
The adapter is transparent — it has the same ``forward`` signature as
|
| 6 |
+
``X0Model`` and proxies attribute access to the wrapped model.
|
| 7 |
+
Example
|
| 8 |
+
-------
|
| 9 |
+
>>> from ltx_core.batch_split import BatchSplitAdapter
|
| 10 |
+
>>> adapter = BatchSplitAdapter(model, max_batch_size=1)
|
| 11 |
+
>>> # Receives B=4, runs 4xB=1 internally, returns B=4
|
| 12 |
+
>>> denoised_video, denoised_audio = adapter(video=v_b4, audio=a_b4, perturbations=ptb)
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import annotations
|
| 16 |
+
|
| 17 |
+
from typing import Any
|
| 18 |
+
|
| 19 |
+
import torch
|
| 20 |
+
from torch import nn
|
| 21 |
+
|
| 22 |
+
from ltx_core.guidance.perturbations import BatchedPerturbationConfig
|
| 23 |
+
from ltx_core.model.transformer.modality import Modality
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def _split_perturbations(config: BatchedPerturbationConfig, sizes: list[int]) -> list[BatchedPerturbationConfig]:
|
| 27 |
+
"""Split a ``BatchedPerturbationConfig`` along the batch dimension."""
|
| 28 |
+
it = iter(config.perturbations)
|
| 29 |
+
return [BatchedPerturbationConfig([next(it) for _ in range(s)]) for s in sizes]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _merge_tensors(tensors: list[torch.Tensor | None]) -> torch.Tensor | None:
|
| 33 |
+
"""Concatenate tensors along batch dim, or return None if all are None."""
|
| 34 |
+
non_none = [t for t in tensors if t is not None]
|
| 35 |
+
if not non_none:
|
| 36 |
+
return None
|
| 37 |
+
return torch.cat(non_none, dim=0)
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class BatchSplitAdapter(nn.Module):
|
| 41 |
+
"""Wraps a model and splits batched forward calls into smaller chunks.
|
| 42 |
+
Has the same ``forward`` signature as ``X0Model``:
|
| 43 |
+
``(video, audio, perturbations) -> (denoised_video, denoised_audio)``.
|
| 44 |
+
Args:
|
| 45 |
+
model: The model to wrap (``X0Model``, ``BlockStreamingWrapper``, etc.).
|
| 46 |
+
max_batch_size: Maximum batch size per forward pass. Input batches
|
| 47 |
+
larger than this are split into sequential chunks.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
def __init__(self, model: nn.Module, max_batch_size: int) -> None:
|
| 51 |
+
if max_batch_size < 1:
|
| 52 |
+
raise ValueError(f"max_batch_size must be >= 1, got {max_batch_size}")
|
| 53 |
+
super().__init__()
|
| 54 |
+
self._model = model
|
| 55 |
+
self._max_batch_size = max_batch_size
|
| 56 |
+
|
| 57 |
+
def _get_chunk_sizes(self, batch_size: int) -> list[int]:
|
| 58 |
+
full, remainder = divmod(batch_size, self._max_batch_size)
|
| 59 |
+
sizes = [self._max_batch_size] * full
|
| 60 |
+
if remainder:
|
| 61 |
+
sizes.append(remainder)
|
| 62 |
+
return sizes
|
| 63 |
+
|
| 64 |
+
def forward(
|
| 65 |
+
self,
|
| 66 |
+
video: Modality | None,
|
| 67 |
+
audio: Modality | None,
|
| 68 |
+
perturbations: BatchedPerturbationConfig,
|
| 69 |
+
) -> tuple[torch.Tensor | None, torch.Tensor | None]:
|
| 70 |
+
batch_size = (video or audio).latent.shape[0]
|
| 71 |
+
|
| 72 |
+
if batch_size <= self._max_batch_size:
|
| 73 |
+
return self._model(video=video, audio=audio, perturbations=perturbations)
|
| 74 |
+
|
| 75 |
+
sizes = self._get_chunk_sizes(batch_size)
|
| 76 |
+
n = len(sizes)
|
| 77 |
+
|
| 78 |
+
v_chunks = video.split(sizes) if video is not None else [None] * n
|
| 79 |
+
a_chunks = audio.split(sizes) if audio is not None else [None] * n
|
| 80 |
+
p_chunks = _split_perturbations(perturbations, sizes)
|
| 81 |
+
|
| 82 |
+
chunk_results = [
|
| 83 |
+
self._model(video=vc, audio=ac, perturbations=pc)
|
| 84 |
+
for vc, ac, pc in zip(v_chunks, a_chunks, p_chunks, strict=True)
|
| 85 |
+
]
|
| 86 |
+
|
| 87 |
+
results_v, results_a = zip(*chunk_results, strict=True)
|
| 88 |
+
return _merge_tensors(list(results_v)), _merge_tensors(list(results_a))
|
| 89 |
+
|
| 90 |
+
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
| 91 |
+
"""Proxy attribute access to the wrapped model."""
|
| 92 |
+
try:
|
| 93 |
+
return super().__getattr__(name)
|
| 94 |
+
except AttributeError:
|
| 95 |
+
return getattr(self._model, name)
|
packages/ltx-core/src/ltx_core/block_streaming/__init__.py
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Block streaming: memory-efficient sequential-block inference.
|
| 2 |
+
Streams transformer blocks from safetensors to GPU one at a time.
|
| 3 |
+
Block weights are provided by a :class:`WeightsProvider` which handles
|
| 4 |
+
CPU-to-GPU copies, caching, and stream synchronization. Two weight
|
| 5 |
+
source strategies are available:
|
| 6 |
+
- **RAM streaming** (default): all blocks pre-loaded into pinned CPU
|
| 7 |
+
buffers with LoRA fusion at build time. Fast, higher CPU memory.
|
| 8 |
+
- **Disk streaming** (``cpu_slots < num_blocks``): blocks read from
|
| 9 |
+
disk on demand with FIFO eviction. Slower, lower CPU memory.
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
from ltx_core.block_streaming.builder import DISK_CPU_SLOTS, StreamingModelBuilder
|
| 13 |
+
from ltx_core.block_streaming.wrapper import BlockStreamingWrapper
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"DISK_CPU_SLOTS",
|
| 17 |
+
"BlockStreamingWrapper",
|
| 18 |
+
"StreamingModelBuilder",
|
| 19 |
+
]
|
packages/ltx-core/src/ltx_core/block_streaming/builder.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Builder that constructs a BlockStreamingWrapper from safetensors checkpoints."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import logging
|
| 6 |
+
from collections.abc import Callable
|
| 7 |
+
from dataclasses import dataclass, field, replace
|
| 8 |
+
from typing import Generic
|
| 9 |
+
|
| 10 |
+
import safetensors
|
| 11 |
+
import torch
|
| 12 |
+
from torch import nn
|
| 13 |
+
|
| 14 |
+
from ltx_core.block_streaming.disk import DiskBlockReader, DiskTensorReader, LoraSource
|
| 15 |
+
from ltx_core.block_streaming.pool import WeightPool
|
| 16 |
+
from ltx_core.block_streaming.provider import WeightsProvider
|
| 17 |
+
from ltx_core.block_streaming.source import DiskWeightSource, PinnedWeightSource, WeightSource
|
| 18 |
+
from ltx_core.block_streaming.utils import allocate_layout_views, derive_layout, make_block_key, resolve_attr
|
| 19 |
+
from ltx_core.block_streaming.wrapper import BlockStreamingWrapper
|
| 20 |
+
from ltx_core.loader.fuse_loras import aggregate_lora_products, fuse_lora_weights
|
| 21 |
+
from ltx_core.loader.helpers import create_meta_model, load_state_dict, read_model_config
|
| 22 |
+
from ltx_core.loader.module_ops import ModuleOps
|
| 23 |
+
from ltx_core.loader.primitives import (
|
| 24 |
+
LoraPathStrengthAndSDOps,
|
| 25 |
+
LoraStateDictWithStrength,
|
| 26 |
+
ModelBuilderProtocol,
|
| 27 |
+
StateDictLoader,
|
| 28 |
+
)
|
| 29 |
+
from ltx_core.loader.registry import DummyRegistry, Registry
|
| 30 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 31 |
+
from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader
|
| 32 |
+
from ltx_core.model.model_protocol import ModelConfigurator, ModelType
|
| 33 |
+
|
| 34 |
+
logger = logging.getLogger(__name__)
|
| 35 |
+
|
| 36 |
+
DISK_CPU_SLOTS = 2
|
| 37 |
+
_DEFAULT_GPU_SLOTS = 2
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@dataclass(frozen=True)
|
| 41 |
+
class StreamingModelBuilder(Generic[ModelType], ModelBuilderProtocol[ModelType]):
|
| 42 |
+
"""Immutable builder for :class:`BlockStreamingWrapper`.
|
| 43 |
+
Reads block weights from safetensors on demand. ``cpu_slots`` and
|
| 44 |
+
``gpu_slots`` control the memory/speed trade-off (see :meth:`build`).
|
| 45 |
+
Args:
|
| 46 |
+
model_class_configurator: Creates the model from a config dict.
|
| 47 |
+
model_path: One or more ``.safetensors`` checkpoint paths.
|
| 48 |
+
model_sd_ops: Key remapping applied to safetensors keys.
|
| 49 |
+
module_ops: Module-level mutations for the meta model.
|
| 50 |
+
loras: LoRA adapters fused into weights at load time.
|
| 51 |
+
model_loader: Strategy for reading checkpoint metadata.
|
| 52 |
+
registry: Shared cache for loaded state dicts.
|
| 53 |
+
blocks_attr: Dotted path to the ``nn.ModuleList`` (e.g.
|
| 54 |
+
``"velocity_model.transformer_blocks"``).
|
| 55 |
+
blocks_prefix: State-dict key prefix for block weights
|
| 56 |
+
(e.g. ``"transformer_blocks"``).
|
| 57 |
+
state_dict_prefix: Wrapper offset prepended to keys when loading into
|
| 58 |
+
the meta model (e.g. ``"velocity_model."`` when wrapped by ``X0Model``).
|
| 59 |
+
model_wrapper: Optional callable wrapping the model
|
| 60 |
+
(e.g. ``X0Model``).
|
| 61 |
+
"""
|
| 62 |
+
|
| 63 |
+
model_class_configurator: type[ModelConfigurator[ModelType]]
|
| 64 |
+
model_path: str | tuple[str, ...]
|
| 65 |
+
model_sd_ops: SDOps | None = None
|
| 66 |
+
module_ops: tuple[ModuleOps, ...] = field(default_factory=tuple)
|
| 67 |
+
loras: tuple[LoraPathStrengthAndSDOps, ...] = field(default_factory=tuple)
|
| 68 |
+
model_loader: StateDictLoader = field(default_factory=SafetensorsModelStateDictLoader)
|
| 69 |
+
registry: Registry = field(default_factory=DummyRegistry)
|
| 70 |
+
|
| 71 |
+
# Streaming-specific
|
| 72 |
+
blocks_attr: str = ""
|
| 73 |
+
blocks_prefix: str = ""
|
| 74 |
+
state_dict_prefix: str = ""
|
| 75 |
+
model_wrapper: Callable[[ModelType], nn.Module] | None = None
|
| 76 |
+
|
| 77 |
+
def with_sd_ops(self, sd_ops: SDOps | None) -> StreamingModelBuilder:
|
| 78 |
+
return replace(self, model_sd_ops=sd_ops)
|
| 79 |
+
|
| 80 |
+
def with_module_ops(self, module_ops: tuple[ModuleOps, ...]) -> StreamingModelBuilder:
|
| 81 |
+
return replace(self, module_ops=module_ops)
|
| 82 |
+
|
| 83 |
+
def with_loras(self, loras: tuple[LoraPathStrengthAndSDOps, ...]) -> StreamingModelBuilder:
|
| 84 |
+
return replace(self, loras=loras)
|
| 85 |
+
|
| 86 |
+
def model_config(self) -> dict:
|
| 87 |
+
"""Read model configuration from the checkpoint metadata."""
|
| 88 |
+
return read_model_config(self.model_path, self.model_loader)
|
| 89 |
+
|
| 90 |
+
def meta_model(self, config: dict, module_ops: tuple[ModuleOps, ...]) -> ModelType:
|
| 91 |
+
"""Create a model on the meta device and apply module operations."""
|
| 92 |
+
return create_meta_model(self.model_class_configurator, config, module_ops)
|
| 93 |
+
|
| 94 |
+
def build(
|
| 95 |
+
self,
|
| 96 |
+
target_device: torch.device,
|
| 97 |
+
dtype: torch.dtype,
|
| 98 |
+
cpu_slots_count: int | None = None,
|
| 99 |
+
gpu_slots_count: int | None = None,
|
| 100 |
+
**_kwargs: object,
|
| 101 |
+
) -> BlockStreamingWrapper:
|
| 102 |
+
"""Build and return a ready-to-use :class:`BlockStreamingWrapper`.
|
| 103 |
+
Args:
|
| 104 |
+
target_device: GPU device for compute.
|
| 105 |
+
dtype: Weight dtype (e.g. ``torch.bfloat16``).
|
| 106 |
+
cpu_slots_count: Number of pinned CPU buffer slots.
|
| 107 |
+
``None`` = RAM streaming (all blocks pre-loaded with LoRA fusion).
|
| 108 |
+
gpu_slots_count: Number of GPU buffer slots.
|
| 109 |
+
``None`` = ``_DEFAULT_GPU_SLOTS`` (2).
|
| 110 |
+
"""
|
| 111 |
+
if not self.blocks_prefix:
|
| 112 |
+
raise ValueError("blocks_prefix must be non-empty for streaming")
|
| 113 |
+
|
| 114 |
+
config = read_model_config(self.model_path, self.model_loader)
|
| 115 |
+
meta_model: nn.Module = create_meta_model(self.model_class_configurator, config, self.module_ops)
|
| 116 |
+
if self.model_wrapper is not None:
|
| 117 |
+
meta_model = self.model_wrapper(meta_model)
|
| 118 |
+
meta_model.eval()
|
| 119 |
+
|
| 120 |
+
blocks = resolve_attr(meta_model, self.blocks_attr)
|
| 121 |
+
|
| 122 |
+
checkpoint_paths = list(self.model_path) if isinstance(self.model_path, tuple) else [self.model_path]
|
| 123 |
+
block_key_map, non_block_keys = _scan_checkpoint_keys(checkpoint_paths, self.model_sd_ops, self.blocks_prefix)
|
| 124 |
+
|
| 125 |
+
cpu_slots_count = cpu_slots_count if cpu_slots_count is not None else len(blocks)
|
| 126 |
+
gpu_slots_count = gpu_slots_count if gpu_slots_count is not None else _DEFAULT_GPU_SLOTS
|
| 127 |
+
|
| 128 |
+
if cpu_slots_count >= len(blocks):
|
| 129 |
+
source, lora_sources = self._build_pinned_source(
|
| 130 |
+
meta_model, target_device, dtype, cpu_slots_count, block_key_map, non_block_keys
|
| 131 |
+
)
|
| 132 |
+
else:
|
| 133 |
+
reader = DiskTensorReader(checkpoint_paths)
|
| 134 |
+
source, lora_sources = self._build_disk_source(
|
| 135 |
+
meta_model, target_device, dtype, cpu_slots_count, reader, block_key_map, non_block_keys
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
copy_stream = torch.cuda.Stream(device=target_device)
|
| 139 |
+
gpu_pool = WeightPool(
|
| 140 |
+
source.block_layout,
|
| 141 |
+
gpu_slots_count,
|
| 142 |
+
target_device,
|
| 143 |
+
reuse_barrier=lambda event: copy_stream.wait_event(event),
|
| 144 |
+
)
|
| 145 |
+
provider = WeightsProvider(gpu_pool, copy_stream, target_device, source, lora_sources, self.blocks_prefix)
|
| 146 |
+
return BlockStreamingWrapper(
|
| 147 |
+
model=meta_model,
|
| 148 |
+
blocks=blocks,
|
| 149 |
+
provider=provider,
|
| 150 |
+
target_device=target_device,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
def _build_pinned_source(
|
| 154 |
+
self,
|
| 155 |
+
meta_model: nn.Module,
|
| 156 |
+
target_device: torch.device,
|
| 157 |
+
dtype: torch.dtype,
|
| 158 |
+
cpu_slots_count: int,
|
| 159 |
+
block_key_map: dict[int, list[tuple[str, str]]],
|
| 160 |
+
non_block_keys: list[tuple[str, str]],
|
| 161 |
+
) -> tuple[WeightSource, list[LoraSource]]:
|
| 162 |
+
"""Pre-load all blocks into pinned CPU buffers with LoRA fusion."""
|
| 163 |
+
model_sd = load_state_dict(
|
| 164 |
+
self.model_path, self.model_loader, self.registry, torch.device("cpu"), self.model_sd_ops
|
| 165 |
+
)
|
| 166 |
+
|
| 167 |
+
lora_sd_and_strengths = [
|
| 168 |
+
LoraStateDictWithStrength(
|
| 169 |
+
load_state_dict([lora.path], self.model_loader, self.registry, torch.device("cpu"), lora.sd_ops),
|
| 170 |
+
lora.strength,
|
| 171 |
+
)
|
| 172 |
+
for lora in self.loras
|
| 173 |
+
]
|
| 174 |
+
|
| 175 |
+
for block_idx in block_key_map:
|
| 176 |
+
if block_idx >= cpu_slots_count:
|
| 177 |
+
raise ValueError(
|
| 178 |
+
f"Pinned source requires one CPU slot per block; "
|
| 179 |
+
f"got block index {block_idx} with only {cpu_slots_count} slots."
|
| 180 |
+
)
|
| 181 |
+
|
| 182 |
+
blocks = resolve_attr(meta_model, self.blocks_attr)
|
| 183 |
+
block_tensors: dict[str, torch.Tensor] = {}
|
| 184 |
+
for block_idx, entries in block_key_map.items():
|
| 185 |
+
block_params = dict(blocks[block_idx].named_parameters())
|
| 186 |
+
for _sft_key, param_name in entries:
|
| 187 |
+
key = make_block_key(self.blocks_prefix, block_idx, param_name)
|
| 188 |
+
block_tensors[key] = block_params[param_name]
|
| 189 |
+
blocks_layout = derive_layout(block_tensors, dtype)
|
| 190 |
+
pinned_blocks = allocate_layout_views(blocks_layout, pin_memory=True)
|
| 191 |
+
|
| 192 |
+
should_sync = False
|
| 193 |
+
for key, fused in fuse_lora_weights(model_sd, lora_sd_and_strengths, dtype=None, preserve_input_device=False):
|
| 194 |
+
if key in pinned_blocks:
|
| 195 |
+
pinned_blocks[key].copy_(fused, non_blocking=True)
|
| 196 |
+
model_sd.sd[key] = None
|
| 197 |
+
should_sync = True
|
| 198 |
+
else:
|
| 199 |
+
model_sd.sd[key] = fused
|
| 200 |
+
if should_sync:
|
| 201 |
+
torch.cuda.synchronize()
|
| 202 |
+
|
| 203 |
+
# Fill remaining pinned keys from the source state dict.
|
| 204 |
+
for key in blocks_layout:
|
| 205 |
+
if model_sd.sd[key] is None:
|
| 206 |
+
continue
|
| 207 |
+
pinned_blocks[key].copy_(model_sd.sd[key])
|
| 208 |
+
model_sd.sd[key] = None
|
| 209 |
+
|
| 210 |
+
pinned: dict[int, dict[str, torch.Tensor]] = {
|
| 211 |
+
block_idx: {
|
| 212 |
+
param_name: pinned_blocks[make_block_key(self.blocks_prefix, block_idx, param_name)]
|
| 213 |
+
for _sft_key, param_name in entries
|
| 214 |
+
}
|
| 215 |
+
for block_idx, entries in block_key_map.items()
|
| 216 |
+
}
|
| 217 |
+
|
| 218 |
+
non_block_sd: dict[str, torch.Tensor] = {
|
| 219 |
+
self.state_dict_prefix + model_key: model_sd.sd[model_key].to(device=target_device, dtype=dtype)
|
| 220 |
+
for _sft_key, model_key in non_block_keys
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
meta_model.load_state_dict(non_block_sd, strict=False, assign=True)
|
| 224 |
+
|
| 225 |
+
return PinnedWeightSource(pinned), []
|
| 226 |
+
|
| 227 |
+
def _build_disk_source(
|
| 228 |
+
self,
|
| 229 |
+
meta_model: nn.Module,
|
| 230 |
+
target_device: torch.device,
|
| 231 |
+
dtype: torch.dtype,
|
| 232 |
+
cpu_slots_count: int,
|
| 233 |
+
reader: DiskTensorReader,
|
| 234 |
+
block_key_map: dict[int, list[tuple[str, str]]],
|
| 235 |
+
non_block_keys: list[tuple[str, str]],
|
| 236 |
+
) -> tuple[WeightSource, list[LoraSource]]:
|
| 237 |
+
"""Create a DiskWeightSource backed by a DiskBlockReader for lazy loading.
|
| 238 |
+
Derives the shared pool layout from the meta model's block 0 — this
|
| 239 |
+
relies on module_ops (e.g. fp8_cast) leaving the meta param dtype in
|
| 240 |
+
sync with the post-sd_ops checkpoint dtype.
|
| 241 |
+
"""
|
| 242 |
+
lora_sources = [LoraSource(lora.path, lora.sd_ops, lora.strength) for lora in self.loras]
|
| 243 |
+
|
| 244 |
+
self._load_non_block_weights(
|
| 245 |
+
reader,
|
| 246 |
+
non_block_keys,
|
| 247 |
+
meta_model,
|
| 248 |
+
target_device,
|
| 249 |
+
dtype,
|
| 250 |
+
sd_ops=self.model_sd_ops,
|
| 251 |
+
key_prefix=self.state_dict_prefix,
|
| 252 |
+
lora_sources=lora_sources,
|
| 253 |
+
)
|
| 254 |
+
|
| 255 |
+
blocks = resolve_attr(meta_model, self.blocks_attr)
|
| 256 |
+
layout = derive_layout(dict(blocks[0].named_parameters()), dtype)
|
| 257 |
+
|
| 258 |
+
cpu_pool = WeightPool(
|
| 259 |
+
layout,
|
| 260 |
+
cpu_slots_count,
|
| 261 |
+
torch.device("cpu"),
|
| 262 |
+
reuse_barrier=lambda event: event.synchronize(),
|
| 263 |
+
pin_memory=True,
|
| 264 |
+
)
|
| 265 |
+
block_reader = DiskBlockReader(
|
| 266 |
+
reader=reader,
|
| 267 |
+
block_key_map=block_key_map,
|
| 268 |
+
sd_ops=self.model_sd_ops,
|
| 269 |
+
blocks_prefix=self.blocks_prefix,
|
| 270 |
+
)
|
| 271 |
+
source = DiskWeightSource(cpu_pool, block_reader)
|
| 272 |
+
return source, lora_sources
|
| 273 |
+
|
| 274 |
+
# ------------------------------------------------------------------
|
| 275 |
+
# Helpers
|
| 276 |
+
# ------------------------------------------------------------------
|
| 277 |
+
|
| 278 |
+
@staticmethod
|
| 279 |
+
def _fuse_lora_delta(
|
| 280 |
+
model_key: str,
|
| 281 |
+
tensor: torch.Tensor,
|
| 282 |
+
lora_sources: list[LoraSource],
|
| 283 |
+
) -> torch.Tensor:
|
| 284 |
+
"""Add all matching LoRA deltas to *tensor* in-place via ``addmm_``."""
|
| 285 |
+
if not lora_sources or not model_key.endswith(".weight"):
|
| 286 |
+
return tensor
|
| 287 |
+
prefix = model_key[: -len(".weight")]
|
| 288 |
+
products = (
|
| 289 |
+
ab
|
| 290 |
+
for ab in (s.get_ab(prefix, device=tensor.device, dtype=tensor.dtype) for s in lora_sources)
|
| 291 |
+
if ab is not None
|
| 292 |
+
)
|
| 293 |
+
aggregate_lora_products(products, out=tensor)
|
| 294 |
+
return tensor
|
| 295 |
+
|
| 296 |
+
@staticmethod
|
| 297 |
+
@torch.inference_mode()
|
| 298 |
+
def _load_non_block_weights(
|
| 299 |
+
reader: DiskTensorReader,
|
| 300 |
+
non_block_keys: list[tuple[str, str]],
|
| 301 |
+
model: nn.Module,
|
| 302 |
+
device: torch.device,
|
| 303 |
+
dtype: torch.dtype,
|
| 304 |
+
sd_ops: SDOps | None = None,
|
| 305 |
+
key_prefix: str = "",
|
| 306 |
+
lora_sources: list[LoraSource] | None = None,
|
| 307 |
+
) -> None:
|
| 308 |
+
"""Load non-block weights into *model* on *device*."""
|
| 309 |
+
state_dict: dict[str, torch.Tensor] = {}
|
| 310 |
+
sources = lora_sources or []
|
| 311 |
+
for sft_key, model_key in non_block_keys:
|
| 312 |
+
tensor = reader.get_tensor(sft_key).to(device=device, dtype=dtype)
|
| 313 |
+
tensor = StreamingModelBuilder._fuse_lora_delta(model_key, tensor, sources)
|
| 314 |
+
if sd_ops is not None:
|
| 315 |
+
for kv in sd_ops.apply_to_key_value(model_key, tensor):
|
| 316 |
+
state_dict[key_prefix + kv.new_key] = kv.new_value
|
| 317 |
+
continue
|
| 318 |
+
state_dict[key_prefix + model_key] = tensor
|
| 319 |
+
model.load_state_dict(state_dict, strict=False, assign=True)
|
| 320 |
+
|
| 321 |
+
|
| 322 |
+
def _scan_checkpoint_keys(
|
| 323 |
+
checkpoint_paths: list[str],
|
| 324 |
+
sd_ops: SDOps | None,
|
| 325 |
+
blocks_prefix: str,
|
| 326 |
+
) -> tuple[dict[int, list[tuple[str, str]]], list[tuple[str, str]]]:
|
| 327 |
+
"""Partition checkpoint keys into per-block and non-block lists.
|
| 328 |
+
Opens the safetensors files for header-only key enumeration; no tensor data
|
| 329 |
+
is read.
|
| 330 |
+
"""
|
| 331 |
+
block_key_map: dict[int, list[tuple[str, str]]] = {}
|
| 332 |
+
non_block_keys: list[tuple[str, str]] = []
|
| 333 |
+
prefix_dot = blocks_prefix + "."
|
| 334 |
+
for path in checkpoint_paths:
|
| 335 |
+
with safetensors.safe_open(path, framework="pt", device="cpu") as handle:
|
| 336 |
+
for sft_key in handle.keys(): # noqa: SIM118
|
| 337 |
+
model_key = sd_ops.apply_to_key(sft_key) if sd_ops else sft_key
|
| 338 |
+
if model_key is None:
|
| 339 |
+
continue
|
| 340 |
+
if model_key.startswith(prefix_dot):
|
| 341 |
+
rest = model_key[len(prefix_dot) :]
|
| 342 |
+
idx_str, _, param_name = rest.partition(".")
|
| 343 |
+
try:
|
| 344 |
+
block_idx = int(idx_str)
|
| 345 |
+
except ValueError:
|
| 346 |
+
non_block_keys.append((sft_key, model_key))
|
| 347 |
+
continue
|
| 348 |
+
block_key_map.setdefault(block_idx, []).append((sft_key, param_name))
|
| 349 |
+
else:
|
| 350 |
+
non_block_keys.append((sft_key, model_key))
|
| 351 |
+
return block_key_map, non_block_keys
|
packages/ltx-core/src/ltx_core/block_streaming/disk.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Safetensors I/O and LoRA fusion for block streaming."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections.abc import Iterator
|
| 6 |
+
|
| 7 |
+
import safetensors
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from ltx_core.block_streaming.utils import allocate_layout_views, make_block_key
|
| 11 |
+
from ltx_core.loader.fuse_loras import LoraProduct
|
| 12 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 13 |
+
|
| 14 |
+
_SAFETENSORS_DTYPE_TO_TORCH: dict[str, torch.dtype] = {
|
| 15 |
+
"F64": torch.float64,
|
| 16 |
+
"F32": torch.float32,
|
| 17 |
+
"F16": torch.float16,
|
| 18 |
+
"BF16": torch.bfloat16,
|
| 19 |
+
}
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class DiskTensorReader:
|
| 23 |
+
"""Key-based tensor accessor over one or more safetensors files."""
|
| 24 |
+
|
| 25 |
+
def __init__(self, paths: list[str]) -> None:
|
| 26 |
+
self._handles: list[safetensors.safe_open] = []
|
| 27 |
+
self._key_to_handle_idx: dict[str, int] = {}
|
| 28 |
+
for path in paths:
|
| 29 |
+
handle = safetensors.safe_open(path, framework="pt", device="cpu")
|
| 30 |
+
handle_idx = len(self._handles)
|
| 31 |
+
self._handles.append(handle)
|
| 32 |
+
for sft_key in handle.keys(): # noqa: SIM118
|
| 33 |
+
self._key_to_handle_idx[sft_key] = handle_idx
|
| 34 |
+
|
| 35 |
+
def get_tensor(self, key: str) -> torch.Tensor:
|
| 36 |
+
return self._handles[self._key_to_handle_idx[key]].get_tensor(key)
|
| 37 |
+
|
| 38 |
+
def close(self) -> None:
|
| 39 |
+
self._handles.clear()
|
| 40 |
+
self._key_to_handle_idx.clear()
|
| 41 |
+
|
| 42 |
+
def __contains__(self, key: str) -> bool:
|
| 43 |
+
return key in self._key_to_handle_idx
|
| 44 |
+
|
| 45 |
+
def __iter__(self) -> Iterator[str]:
|
| 46 |
+
return iter(self._key_to_handle_idx)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
class DiskBlockReader:
|
| 50 |
+
"""Reads one block at a time from safetensors into provided buffers."""
|
| 51 |
+
|
| 52 |
+
def __init__(
|
| 53 |
+
self,
|
| 54 |
+
reader: DiskTensorReader,
|
| 55 |
+
block_key_map: dict[int, list[tuple[str, str]]],
|
| 56 |
+
sd_ops: SDOps | None = None,
|
| 57 |
+
blocks_prefix: str = "",
|
| 58 |
+
) -> None:
|
| 59 |
+
self._reader = reader
|
| 60 |
+
self._block_key_map = block_key_map
|
| 61 |
+
self._sd_ops = sd_ops
|
| 62 |
+
self._blocks_prefix = blocks_prefix
|
| 63 |
+
|
| 64 |
+
def read_into(self, target: dict[str, torch.Tensor], block_idx: int) -> None:
|
| 65 |
+
block_prefix = make_block_key(self._blocks_prefix, block_idx, "")
|
| 66 |
+
for sft_key, param_name in self._block_key_map[block_idx]:
|
| 67 |
+
tensor = self._reader.get_tensor(sft_key)
|
| 68 |
+
if self._sd_ops is None:
|
| 69 |
+
target[param_name].copy_(tensor)
|
| 70 |
+
continue
|
| 71 |
+
full_key = make_block_key(self._blocks_prefix, block_idx, param_name)
|
| 72 |
+
for result in self._sd_ops.apply_to_key_value(full_key, tensor):
|
| 73 |
+
if not result.new_key.startswith(block_prefix):
|
| 74 |
+
raise ValueError(
|
| 75 |
+
f"SDOps output key '{result.new_key}' is outside block {block_idx} "
|
| 76 |
+
f"(expected prefix '{block_prefix}'); cannot route to a per-block buffer."
|
| 77 |
+
)
|
| 78 |
+
target[result.new_key[len(block_prefix) :]].copy_(result.new_value)
|
| 79 |
+
|
| 80 |
+
def cleanup(self) -> None:
|
| 81 |
+
self._reader.close()
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
class LoraSource:
|
| 85 |
+
"""Pinned-memory cache of matched LoRA A/B factors backed by a single buffer."""
|
| 86 |
+
|
| 87 |
+
def __init__(self, path: str, sd_ops: SDOps | None, strength: float) -> None:
|
| 88 |
+
self.strength = strength
|
| 89 |
+
self._pinned_ab: dict[str, tuple[torch.Tensor, torch.Tensor]] = {}
|
| 90 |
+
|
| 91 |
+
a_keys: dict[str, str] = {}
|
| 92 |
+
b_keys: dict[str, str] = {}
|
| 93 |
+
with safetensors.safe_open(path, framework="pt", device="cpu") as handle:
|
| 94 |
+
for sft_key in handle.keys(): # noqa: SIM118
|
| 95 |
+
model_key = sd_ops.apply_to_key(sft_key) if sd_ops is not None else sft_key
|
| 96 |
+
if model_key is None:
|
| 97 |
+
continue
|
| 98 |
+
if model_key.endswith(".lora_A.weight"):
|
| 99 |
+
a_keys[model_key[: -len(".lora_A.weight")]] = sft_key
|
| 100 |
+
elif model_key.endswith(".lora_B.weight"):
|
| 101 |
+
b_keys[model_key[: -len(".lora_B.weight")]] = sft_key
|
| 102 |
+
|
| 103 |
+
matched_prefixes = list(a_keys.keys() & b_keys.keys())
|
| 104 |
+
|
| 105 |
+
# Build the layout from safetensors header metadata only — no tensor data is read.
|
| 106 |
+
layout: dict[str, tuple[torch.Size, torch.dtype]] = {}
|
| 107 |
+
for prefix in matched_prefixes:
|
| 108 |
+
a_slice_view = handle.get_slice(a_keys[prefix])
|
| 109 |
+
b_slice_view = handle.get_slice(b_keys[prefix])
|
| 110 |
+
layout[f"{prefix}.A"] = (
|
| 111 |
+
torch.Size(a_slice_view.get_shape()),
|
| 112 |
+
_SAFETENSORS_DTYPE_TO_TORCH[a_slice_view.get_dtype()],
|
| 113 |
+
)
|
| 114 |
+
layout[f"{prefix}.B"] = (
|
| 115 |
+
torch.Size(b_slice_view.get_shape()),
|
| 116 |
+
_SAFETENSORS_DTYPE_TO_TORCH[b_slice_view.get_dtype()],
|
| 117 |
+
)
|
| 118 |
+
|
| 119 |
+
all_views = allocate_layout_views(layout, pin_memory=True)
|
| 120 |
+
|
| 121 |
+
for prefix in matched_prefixes:
|
| 122 |
+
a_view = all_views[f"{prefix}.A"]
|
| 123 |
+
b_view = all_views[f"{prefix}.B"]
|
| 124 |
+
a_view.copy_(handle.get_tensor(a_keys[prefix]))
|
| 125 |
+
b_view.copy_(handle.get_tensor(b_keys[prefix]))
|
| 126 |
+
self._pinned_ab[prefix] = (a_view, b_view)
|
| 127 |
+
|
| 128 |
+
def get_ab(
|
| 129 |
+
self,
|
| 130 |
+
param_prefix: str,
|
| 131 |
+
device: torch.device | None = None,
|
| 132 |
+
dtype: torch.dtype | None = None,
|
| 133 |
+
) -> LoraProduct | None:
|
| 134 |
+
"""Return the :class:`LoraProduct` for *param_prefix*, or ``None``."""
|
| 135 |
+
pair = self._pinned_ab.get(param_prefix)
|
| 136 |
+
if pair is None:
|
| 137 |
+
return None
|
| 138 |
+
a, b = pair
|
| 139 |
+
if device is not None and device.type == "cuda":
|
| 140 |
+
a = a.to(device=device, non_blocking=True)
|
| 141 |
+
b = b.to(device=device, non_blocking=True)
|
| 142 |
+
if dtype is not None:
|
| 143 |
+
a = a.to(dtype=dtype)
|
| 144 |
+
b = b.to(dtype=dtype)
|
| 145 |
+
return LoraProduct(a, b, self.strength)
|
| 146 |
+
|
| 147 |
+
def cleanup(self) -> None:
|
| 148 |
+
self._pinned_ab.clear()
|
packages/ltx-core/src/ltx_core/block_streaming/pool.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Weight buffer pool for block streaming."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections import deque
|
| 6 |
+
from typing import Callable
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from ltx_core.block_streaming.utils import allocate_layout_views
|
| 11 |
+
from ltx_core.loader.primitives import TensorLayout
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class WeightPool:
|
| 15 |
+
"""Fixed pool of pre-allocated weight buffers with event-based reuse.
|
| 16 |
+
All slots share a single buffer (CPU or GPU); each slot is a
|
| 17 |
+
contiguous slice carved out of it via :func:`allocate_layout_views`.
|
| 18 |
+
Args:
|
| 19 |
+
buffer_layout: ``{name: (shape, dtype)}`` for each buffer.
|
| 20 |
+
capacity: Number of buffers to pre-allocate.
|
| 21 |
+
device: Device for allocation.
|
| 22 |
+
reuse_barrier: Called with the pending event before a buffer is reused.
|
| 23 |
+
pin_memory: Pin buffers (for async H2D copies from CPU).
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def __init__(
|
| 27 |
+
self,
|
| 28 |
+
buffer_layout: TensorLayout,
|
| 29 |
+
capacity: int,
|
| 30 |
+
device: torch.device,
|
| 31 |
+
reuse_barrier: Callable[[torch.cuda.Event], None],
|
| 32 |
+
pin_memory: bool = False,
|
| 33 |
+
) -> None:
|
| 34 |
+
self._buffer_layout = buffer_layout
|
| 35 |
+
self._capacity = capacity
|
| 36 |
+
self._free: deque[dict[str, torch.Tensor]] = deque()
|
| 37 |
+
self._events: dict[int, torch.cuda.Event] = {}
|
| 38 |
+
self._reuse_barrier = reuse_barrier
|
| 39 |
+
memory_layout = {
|
| 40 |
+
_make_key(slot, name): (shape, dtype)
|
| 41 |
+
for slot in range(capacity)
|
| 42 |
+
for name, (shape, dtype) in buffer_layout.items()
|
| 43 |
+
}
|
| 44 |
+
all_views = allocate_layout_views(memory_layout, device=device, pin_memory=pin_memory)
|
| 45 |
+
for slot in range(capacity):
|
| 46 |
+
self._free.append({name: all_views[_make_key(slot, name)] for name in buffer_layout})
|
| 47 |
+
|
| 48 |
+
@property
|
| 49 |
+
def capacity(self) -> int:
|
| 50 |
+
return self._capacity
|
| 51 |
+
|
| 52 |
+
@property
|
| 53 |
+
def buffer_layout(self) -> TensorLayout:
|
| 54 |
+
return self._buffer_layout
|
| 55 |
+
|
| 56 |
+
def acquire(self) -> dict[str, torch.Tensor]:
|
| 57 |
+
"""Take a free buffer, waiting any pending event before returning."""
|
| 58 |
+
weights = self._free.popleft()
|
| 59 |
+
event = self._events.pop(id(weights), None)
|
| 60 |
+
if event is not None:
|
| 61 |
+
self._reuse_barrier(event)
|
| 62 |
+
return weights
|
| 63 |
+
|
| 64 |
+
def release(self, weights: dict[str, torch.Tensor], event: torch.cuda.Event | None = None) -> None:
|
| 65 |
+
"""Return a buffer to the free list.
|
| 66 |
+
If *event* is given it is waited on the next :meth:`acquire`
|
| 67 |
+
of this buffer, ensuring the prior operation has completed.
|
| 68 |
+
"""
|
| 69 |
+
if event is not None:
|
| 70 |
+
self._events[id(weights)] = event
|
| 71 |
+
self._free.append(weights)
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
def _make_key(slot: int, name: str) -> str:
|
| 75 |
+
return f"{slot}/{name}"
|
packages/ltx-core/src/ltx_core/block_streaming/provider.py
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""GPU weights provider for block streaming."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections import OrderedDict
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
from ltx_core.block_streaming.disk import LoraSource
|
| 10 |
+
from ltx_core.block_streaming.pool import WeightPool
|
| 11 |
+
from ltx_core.block_streaming.source import WeightSource
|
| 12 |
+
from ltx_core.block_streaming.utils import FP8_DTYPES
|
| 13 |
+
from ltx_core.loader.fuse_loras import aggregate_lora_products, fuse_cast_fp8_weight
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def _contiguous_byte_view(weights: dict[str, torch.Tensor]) -> torch.Tensor | None:
|
| 17 |
+
"""Return a ``uint8`` view spanning every tensor in *weights*, or ``None`` if
|
| 18 |
+
they don't share one contiguous storage region."""
|
| 19 |
+
tensors = list(weights.values())
|
| 20 |
+
if not tensors:
|
| 21 |
+
return None
|
| 22 |
+
storage = tensors[0].untyped_storage()
|
| 23 |
+
storage_ptr = storage.data_ptr()
|
| 24 |
+
start = end = tensors[0].storage_offset() * tensors[0].element_size()
|
| 25 |
+
for t in tensors:
|
| 26 |
+
if t.untyped_storage().data_ptr() != storage_ptr or not t.is_contiguous():
|
| 27 |
+
return None
|
| 28 |
+
offset = t.storage_offset() * t.element_size()
|
| 29 |
+
nbytes = t.numel() * t.element_size()
|
| 30 |
+
start = min(start, offset)
|
| 31 |
+
end = max(end, offset + nbytes)
|
| 32 |
+
view = torch.empty(0, dtype=torch.uint8, device=tensors[0].device)
|
| 33 |
+
view.set_(storage, start, (end - start,), (1,))
|
| 34 |
+
return view
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
class WeightsProvider:
|
| 38 |
+
"""Provides GPU-ready block weights via H2D copy from a pinned CPU weight source.
|
| 39 |
+
Args:
|
| 40 |
+
pool: Pre-allocated GPU weight buffer pool.
|
| 41 |
+
copy_stream: Dedicated CUDA stream for async H2D copies.
|
| 42 |
+
target_device: GPU device for compute.
|
| 43 |
+
source: Pinned CPU weight source.
|
| 44 |
+
lora_sources: LoRA adapters fused on H2D copy.
|
| 45 |
+
blocks_prefix: State-dict prefix for LoRA key matching.
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def __init__(
|
| 49 |
+
self,
|
| 50 |
+
pool: WeightPool,
|
| 51 |
+
copy_stream: torch.cuda.Stream,
|
| 52 |
+
target_device: torch.device,
|
| 53 |
+
source: WeightSource,
|
| 54 |
+
lora_sources: list[LoraSource] | None = None,
|
| 55 |
+
blocks_prefix: str = "",
|
| 56 |
+
) -> None:
|
| 57 |
+
self._copy_stream = copy_stream
|
| 58 |
+
self._pool = pool
|
| 59 |
+
self._cache: OrderedDict[int, dict[str, torch.Tensor]] = OrderedDict()
|
| 60 |
+
self._events: dict[int, torch.cuda.Event] = {}
|
| 61 |
+
self._target_device = target_device
|
| 62 |
+
self._source = source
|
| 63 |
+
self._lora_sources = lora_sources or []
|
| 64 |
+
self._blocks_prefix = blocks_prefix
|
| 65 |
+
|
| 66 |
+
def get(self, idx: int) -> dict[str, torch.Tensor]:
|
| 67 |
+
"""Return GPU weights for block *idx*. Does H2D copy on miss."""
|
| 68 |
+
if idx in self._cache:
|
| 69 |
+
return self._cache[idx]
|
| 70 |
+
|
| 71 |
+
# Evict oldest GPU buffer if at capacity.
|
| 72 |
+
if len(self._cache) >= self._pool.capacity:
|
| 73 |
+
evicted_idx, evicted_weights = self._cache.popitem(last=False)
|
| 74 |
+
self._pool.release(evicted_weights, event=self._events.pop(evicted_idx, None))
|
| 75 |
+
|
| 76 |
+
gpu_weights = self._pool.acquire()
|
| 77 |
+
cpu_weights = self._source.get(idx)
|
| 78 |
+
|
| 79 |
+
h2d_event = self._copy_to_gpu(idx, gpu_weights, cpu_weights)
|
| 80 |
+
self._source.release(idx, event=h2d_event)
|
| 81 |
+
|
| 82 |
+
self._cache[idx] = gpu_weights
|
| 83 |
+
return gpu_weights
|
| 84 |
+
|
| 85 |
+
def _copy_to_gpu(
|
| 86 |
+
self,
|
| 87 |
+
idx: int,
|
| 88 |
+
gpu_weights: dict[str, torch.Tensor],
|
| 89 |
+
cpu_weights: dict[str, torch.Tensor],
|
| 90 |
+
) -> torch.cuda.Event:
|
| 91 |
+
"""Enqueue H2D copy + LoRA fusion on the copy stream and wait on compute.
|
| 92 |
+
The wait is intentionally inside this method so callers -- and
|
| 93 |
+
instrumentation regions wrapping it -- observe the full transfer time.
|
| 94 |
+
"""
|
| 95 |
+
with torch.cuda.stream(self._copy_stream):
|
| 96 |
+
gpu_view = _contiguous_byte_view(gpu_weights)
|
| 97 |
+
cpu_view = _contiguous_byte_view(cpu_weights)
|
| 98 |
+
if gpu_view is not None and cpu_view is not None and gpu_view.numel() == cpu_view.numel():
|
| 99 |
+
gpu_view.copy_(cpu_view, non_blocking=True)
|
| 100 |
+
else:
|
| 101 |
+
for name, gpu_tensor in gpu_weights.items():
|
| 102 |
+
gpu_tensor.copy_(cpu_weights[name], non_blocking=True)
|
| 103 |
+
if self._lora_sources:
|
| 104 |
+
self._fuse_block_loras(idx, gpu_weights)
|
| 105 |
+
h2d_event = torch.cuda.Event()
|
| 106 |
+
h2d_event.record(self._copy_stream)
|
| 107 |
+
|
| 108 |
+
torch.cuda.current_stream(self._target_device).wait_event(h2d_event)
|
| 109 |
+
return h2d_event
|
| 110 |
+
|
| 111 |
+
def release(self, idx: int, event: torch.cuda.Event) -> None:
|
| 112 |
+
"""Attach a compute-done event -- waited before this buffer is recycled."""
|
| 113 |
+
self._events[idx] = event
|
| 114 |
+
|
| 115 |
+
def cleanup(self) -> None:
|
| 116 |
+
"""Synchronize streams and release all resources."""
|
| 117 |
+
self._copy_stream.synchronize()
|
| 118 |
+
torch.cuda.current_stream(self._target_device).synchronize()
|
| 119 |
+
self._cache.clear()
|
| 120 |
+
self._events.clear()
|
| 121 |
+
self._source.cleanup()
|
| 122 |
+
for lora in self._lora_sources:
|
| 123 |
+
lora.cleanup()
|
| 124 |
+
|
| 125 |
+
def __len__(self) -> int:
|
| 126 |
+
return len(self._cache)
|
| 127 |
+
|
| 128 |
+
def _fuse_block_loras(self, idx: int, weights: dict[str, torch.Tensor]) -> None:
|
| 129 |
+
"""Fuse LoRA deltas directly into GPU block weights."""
|
| 130 |
+
for name, tensor in weights.items():
|
| 131 |
+
if not name.endswith(".weight"):
|
| 132 |
+
continue
|
| 133 |
+
prefix = f"{self._blocks_prefix}.{idx}.{name}".removesuffix(".weight")
|
| 134 |
+
is_fp8 = tensor.dtype in FP8_DTYPES
|
| 135 |
+
agg_dtype = torch.bfloat16 if is_fp8 else tensor.dtype
|
| 136 |
+
products = (
|
| 137 |
+
ab
|
| 138 |
+
for ab in (s.get_ab(prefix, device=self._target_device, dtype=agg_dtype) for s in self._lora_sources)
|
| 139 |
+
if ab is not None
|
| 140 |
+
)
|
| 141 |
+
aggregated = aggregate_lora_products(products, agg_dtype)
|
| 142 |
+
if aggregated is None:
|
| 143 |
+
continue
|
| 144 |
+
if is_fp8:
|
| 145 |
+
tensor.copy_(fuse_cast_fp8_weight(aggregated, tensor, tensor.dtype))
|
| 146 |
+
else:
|
| 147 |
+
tensor.add_(aggregated)
|
packages/ltx-core/src/ltx_core/block_streaming/source.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Weight sources for block streaming: protocol and implementations."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from collections import OrderedDict
|
| 6 |
+
from typing import Protocol
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
|
| 10 |
+
from ltx_core.block_streaming.disk import DiskBlockReader
|
| 11 |
+
from ltx_core.block_streaming.pool import WeightPool
|
| 12 |
+
from ltx_core.loader.primitives import TensorLayout
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class WeightSource(Protocol):
|
| 16 |
+
"""Provides pinned CPU weights for a given block index.
|
| 17 |
+
Assumes all buffers share an identical layout across all block indices.
|
| 18 |
+
"""
|
| 19 |
+
|
| 20 |
+
@property
|
| 21 |
+
def block_layout(self) -> TensorLayout:
|
| 22 |
+
"""Shared per-block buffer layout (shape + dtype for each param)."""
|
| 23 |
+
...
|
| 24 |
+
|
| 25 |
+
def get(self, idx: int) -> dict[str, torch.Tensor]:
|
| 26 |
+
"""Return CPU weights for block *idx*."""
|
| 27 |
+
...
|
| 28 |
+
|
| 29 |
+
def release(self, idx: int, event: torch.cuda.Event) -> None:
|
| 30 |
+
"""Signal that an async operation using these weights is guarded by *event*."""
|
| 31 |
+
...
|
| 32 |
+
|
| 33 |
+
def cleanup(self) -> None:
|
| 34 |
+
"""Release all resources (buffers, readers, events)."""
|
| 35 |
+
...
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class DiskWeightSource(WeightSource):
|
| 39 |
+
"""Reads block weights from disk into pinned CPU buffers on demand."""
|
| 40 |
+
|
| 41 |
+
def __init__(self, pool: WeightPool, reader: DiskBlockReader) -> None:
|
| 42 |
+
self._pool = pool
|
| 43 |
+
self._cache: OrderedDict[int, dict[str, torch.Tensor]] = OrderedDict()
|
| 44 |
+
self._events: dict[int, torch.cuda.Event] = {}
|
| 45 |
+
self._reader = reader
|
| 46 |
+
|
| 47 |
+
@property
|
| 48 |
+
def block_layout(self) -> TensorLayout:
|
| 49 |
+
return self._pool.buffer_layout
|
| 50 |
+
|
| 51 |
+
def get(self, idx: int) -> dict[str, torch.Tensor]:
|
| 52 |
+
"""Return CPU weights for block *idx*. Reads from disk on miss."""
|
| 53 |
+
if idx in self._cache:
|
| 54 |
+
return self._cache[idx]
|
| 55 |
+
|
| 56 |
+
if len(self._cache) >= self._pool.capacity:
|
| 57 |
+
evicted_idx, evicted_weights = self._cache.popitem(last=False)
|
| 58 |
+
self._pool.release(evicted_weights, event=self._events.pop(evicted_idx, None))
|
| 59 |
+
|
| 60 |
+
weights = self._pool.acquire()
|
| 61 |
+
self._reader.read_into(weights, idx)
|
| 62 |
+
self._cache[idx] = weights
|
| 63 |
+
return weights
|
| 64 |
+
|
| 65 |
+
def release(self, idx: int, event: torch.cuda.Event) -> None:
|
| 66 |
+
"""Attach an H2D event -- waited before this buffer is recycled."""
|
| 67 |
+
self._events[idx] = event
|
| 68 |
+
|
| 69 |
+
def cleanup(self) -> None:
|
| 70 |
+
"""Clear cache and close the disk reader."""
|
| 71 |
+
self._cache.clear()
|
| 72 |
+
self._events.clear()
|
| 73 |
+
self._reader.cleanup()
|
| 74 |
+
|
| 75 |
+
def __len__(self) -> int:
|
| 76 |
+
return len(self._cache)
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
class PinnedWeightSource(WeightSource):
|
| 80 |
+
"""Pre-loaded pinned CPU weights."""
|
| 81 |
+
|
| 82 |
+
def __init__(self, weights: dict[int, dict[str, torch.Tensor]]) -> None:
|
| 83 |
+
if not weights:
|
| 84 |
+
raise ValueError("PinnedWeightSource requires at least one block")
|
| 85 |
+
self._weights = weights
|
| 86 |
+
|
| 87 |
+
@property
|
| 88 |
+
def block_layout(self) -> TensorLayout:
|
| 89 |
+
first_block = self._weights[min(self._weights)]
|
| 90 |
+
return {name: (t.shape, t.dtype) for name, t in first_block.items()}
|
| 91 |
+
|
| 92 |
+
def get(self, idx: int) -> dict[str, torch.Tensor]:
|
| 93 |
+
return self._weights[idx]
|
| 94 |
+
|
| 95 |
+
def release(self, idx: int, event: torch.cuda.Event) -> None:
|
| 96 |
+
pass
|
| 97 |
+
|
| 98 |
+
def cleanup(self) -> None:
|
| 99 |
+
self._weights.clear()
|
| 100 |
+
|
| 101 |
+
def __len__(self) -> int:
|
| 102 |
+
return len(self._weights)
|
packages/ltx-core/src/ltx_core/block_streaming/utils.py
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared utilities for the block_streaming package."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import math
|
| 6 |
+
import weakref
|
| 7 |
+
from dataclasses import dataclass
|
| 8 |
+
from typing import Any
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import nn
|
| 12 |
+
|
| 13 |
+
from ltx_core.loader.primitives import TensorLayout
|
| 14 |
+
|
| 15 |
+
FP8_DTYPES = frozenset({torch.float8_e4m3fn, torch.float8_e5m2})
|
| 16 |
+
|
| 17 |
+
_BUFFER_ALIGN = 16
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def make_block_key(blocks_prefix: str, block_idx: int, param_name: str) -> str:
|
| 21 |
+
"""Return the state-dict key for *param_name* under block *block_idx*."""
|
| 22 |
+
return f"{blocks_prefix}.{block_idx}.{param_name}"
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
def resolve_attr(module: nn.Module, dotted_path: str) -> nn.ModuleList:
|
| 26 |
+
"""Resolve a dotted attribute path like ``'model.language_model.layers'``."""
|
| 27 |
+
obj: Any = module
|
| 28 |
+
for part in dotted_path.split("."):
|
| 29 |
+
obj = getattr(obj, part)
|
| 30 |
+
if not isinstance(obj, nn.ModuleList):
|
| 31 |
+
raise TypeError(f"Expected nn.ModuleList at '{dotted_path}', got {type(obj).__name__}")
|
| 32 |
+
return obj
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
def assign_tensor_to_module(root: nn.Module, dotted_name: str, tensor: torch.Tensor) -> None:
|
| 36 |
+
"""Assign *tensor* to the parameter/buffer at *dotted_name* inside *root*.
|
| 37 |
+
Unlike ``param.data = tensor``, this works even when the existing parameter
|
| 38 |
+
lives on the ``meta`` device (which has an incompatible storage type).
|
| 39 |
+
"""
|
| 40 |
+
parts = dotted_name.split(".")
|
| 41 |
+
parent = root
|
| 42 |
+
for part in parts[:-1]:
|
| 43 |
+
parent = getattr(parent, part)
|
| 44 |
+
leaf = parts[-1]
|
| 45 |
+
if leaf in parent._parameters:
|
| 46 |
+
parent._parameters[leaf] = nn.Parameter(tensor, requires_grad=False)
|
| 47 |
+
elif leaf in parent._buffers:
|
| 48 |
+
parent._buffers[leaf] = tensor
|
| 49 |
+
else:
|
| 50 |
+
raise AttributeError(f"{leaf} is not a parameter or buffer of {type(parent).__name__}")
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
def derive_layout(tensors: dict[str, torch.Tensor], dtype: torch.dtype | None = None) -> TensorLayout:
|
| 54 |
+
"""Derive a layout from a ``{name: tensor}`` dict.
|
| 55 |
+
If ``dtype`` is given, non-FP8 dtypes are coerced to it (FP8 preserved). If
|
| 56 |
+
``None``, the source dtype is preserved as-is.
|
| 57 |
+
"""
|
| 58 |
+
return {
|
| 59 |
+
name: (t.shape, t.dtype if dtype is None or t.dtype in FP8_DTYPES else dtype) for name, t in tensors.items()
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
|
| 63 |
+
def _align_up(offset: int, alignment: int) -> int:
|
| 64 |
+
return (offset + alignment - 1) & ~(alignment - 1)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def _alloc_pinned_exact(nbytes: int) -> torch.Tensor | None:
|
| 68 |
+
"""Allocate exactly ``nbytes`` of pinned host memory via ``cudaHostRegister``.
|
| 69 |
+
Bypasses PyTorch's ``CachingHostAllocator``, which rounds every
|
| 70 |
+
``pin_memory=True`` request up to ``PowerOf2Ceil(N)`` (see
|
| 71 |
+
``aten/src/ATen/core/CachingHostAllocator.h``). Returns ``None`` if
|
| 72 |
+
registration fails. The unregister hook is bound to the storage (not the
|
| 73 |
+
tensor) so views of the buffer keep the registration alive until the
|
| 74 |
+
memory is actually freed. Caller is responsible for ensuring CUDA is
|
| 75 |
+
available.
|
| 76 |
+
"""
|
| 77 |
+
cudart = torch.cuda.cudart()
|
| 78 |
+
buf = torch.empty(nbytes, dtype=torch.uint8)
|
| 79 |
+
ptr = buf.data_ptr()
|
| 80 |
+
err = int(cudart.cudaHostRegister(ptr, nbytes, 0))
|
| 81 |
+
if err != 0:
|
| 82 |
+
return None
|
| 83 |
+
weakref.finalize(buf.untyped_storage(), lambda p=ptr: cudart.cudaHostUnregister(p))
|
| 84 |
+
return buf
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def _alloc_buffer(nbytes: int, device: torch.device | None, pin_memory: bool) -> torch.Tensor:
|
| 88 |
+
"""Allocate one ``uint8`` buffer for :func:`allocate_layout_views`.
|
| 89 |
+
For pinned host buffers, prefer ``cudaHostRegister`` to dodge the caching
|
| 90 |
+
allocator's power-of-2 rounding. Falls back to the caching allocator if
|
| 91 |
+
registration fails. Raises if pinning is requested without a CUDA runtime,
|
| 92 |
+
since pinning is fundamentally a CUDA driver operation.
|
| 93 |
+
"""
|
| 94 |
+
if pin_memory and (device is None or torch.device(device).type == "cpu"):
|
| 95 |
+
if not torch.cuda.is_available():
|
| 96 |
+
raise RuntimeError("pin_memory=True requires CUDA, which is not available")
|
| 97 |
+
buf = _alloc_pinned_exact(nbytes)
|
| 98 |
+
if buf is not None:
|
| 99 |
+
return buf
|
| 100 |
+
return torch.empty(nbytes, dtype=torch.uint8, device=device, pin_memory=pin_memory)
|
| 101 |
+
|
| 102 |
+
|
| 103 |
+
@dataclass(frozen=True)
|
| 104 |
+
class _TensorSlice:
|
| 105 |
+
"""Location of a single tensor view within the buffer."""
|
| 106 |
+
|
| 107 |
+
offset: int
|
| 108 |
+
shape: torch.Size
|
| 109 |
+
dtype: torch.dtype
|
| 110 |
+
|
| 111 |
+
def size(self) -> int:
|
| 112 |
+
return math.prod(self.shape) * self.dtype.itemsize
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def allocate_layout_views(
|
| 116 |
+
layout: TensorLayout,
|
| 117 |
+
device: torch.device | None = None,
|
| 118 |
+
pin_memory: bool = False,
|
| 119 |
+
) -> dict[str, torch.Tensor]:
|
| 120 |
+
"""Allocate a single ``uint8`` buffer and return per-key tensor views into it.
|
| 121 |
+
All keys in *layout* live in one contiguous allocation; each returned
|
| 122 |
+
tensor is a non-overlapping slice of that buffer reinterpreted at the
|
| 123 |
+
requested shape and dtype. The views keep the underlying storage alive
|
| 124 |
+
via PyTorch refcounting — drop them all to release the memory.
|
| 125 |
+
"""
|
| 126 |
+
slices: dict[str, _TensorSlice] = {}
|
| 127 |
+
cursor = 0
|
| 128 |
+
for key, (shape, dtype) in layout.items():
|
| 129 |
+
cursor = _align_up(cursor, _BUFFER_ALIGN)
|
| 130 |
+
slices[key] = _TensorSlice(offset=cursor, shape=shape, dtype=dtype)
|
| 131 |
+
cursor += slices[key].size()
|
| 132 |
+
# Allocate at least one byte so empty layouts still produce a valid buffer.
|
| 133 |
+
buffer = _alloc_buffer(max(_align_up(cursor, _BUFFER_ALIGN), 1), device, pin_memory)
|
| 134 |
+
return {key: buffer[s.offset : s.offset + s.size()].view(s.dtype).view(s.shape) for key, s in slices.items()}
|
packages/ltx-core/src/ltx_core/block_streaming/wrapper.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Block streaming wrapper: streams transformer blocks through a WeightsProvider."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import itertools
|
| 6 |
+
from typing import Any
|
| 7 |
+
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
|
| 11 |
+
from ltx_core.block_streaming.provider import WeightsProvider
|
| 12 |
+
from ltx_core.block_streaming.utils import assign_tensor_to_module
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class BlockStreamingWrapper(nn.Module):
|
| 16 |
+
"""Streams sequential model blocks through GPU buffer caches.
|
| 17 |
+
The wrapper delegates all weight management to a :class:`WeightsProvider`
|
| 18 |
+
which handles CPU-to-GPU copies, caching, LoRA fusion, and stream
|
| 19 |
+
synchronization internally.
|
| 20 |
+
Use :class:`StreamingModelBuilder` to construct this wrapper -- it
|
| 21 |
+
handles checkpoint parsing, source selection, and provider creation.
|
| 22 |
+
Args:
|
| 23 |
+
model: The wrapped model (non-block params already on GPU).
|
| 24 |
+
blocks: Sequential blocks to stream (``nn.ModuleList``).
|
| 25 |
+
provider: Provides GPU-ready weights on demand.
|
| 26 |
+
target_device: GPU device for compute.
|
| 27 |
+
"""
|
| 28 |
+
|
| 29 |
+
def __init__(
|
| 30 |
+
self,
|
| 31 |
+
model: nn.Module,
|
| 32 |
+
blocks: nn.ModuleList,
|
| 33 |
+
provider: WeightsProvider,
|
| 34 |
+
target_device: torch.device,
|
| 35 |
+
) -> None:
|
| 36 |
+
super().__init__()
|
| 37 |
+
self._model = model
|
| 38 |
+
self._blocks = blocks
|
| 39 |
+
self._target_device = target_device
|
| 40 |
+
self._provider = provider
|
| 41 |
+
|
| 42 |
+
self._hooks: list[torch.utils.hooks.RemovableHandle] = []
|
| 43 |
+
self._register_hooks()
|
| 44 |
+
|
| 45 |
+
# ------------------------------------------------------------------
|
| 46 |
+
# Hook registration
|
| 47 |
+
# ------------------------------------------------------------------
|
| 48 |
+
|
| 49 |
+
def _pre_hook(self, block_idx: int) -> None:
|
| 50 |
+
"""Load GPU weights for a block and inject them into its parameters."""
|
| 51 |
+
gpu_weights = self._provider.get(block_idx)
|
| 52 |
+
|
| 53 |
+
block = self._blocks[block_idx]
|
| 54 |
+
for name, _param in itertools.chain(block.named_parameters(), block.named_buffers()):
|
| 55 |
+
assign_tensor_to_module(block, name, gpu_weights[name])
|
| 56 |
+
|
| 57 |
+
def _post_hook(self, block_idx: int) -> None:
|
| 58 |
+
"""Record a compute-done event and release the block weights."""
|
| 59 |
+
compute_done = torch.cuda.Event()
|
| 60 |
+
compute_done.record(torch.cuda.current_stream(self._target_device))
|
| 61 |
+
self._provider.release(block_idx, event=compute_done)
|
| 62 |
+
|
| 63 |
+
def _register_hooks(self) -> None:
|
| 64 |
+
for idx, block in enumerate(self._blocks):
|
| 65 |
+
pre = block.register_forward_pre_hook(
|
| 66 |
+
lambda _mod, _args, *, idx=idx: self._pre_hook(idx),
|
| 67 |
+
)
|
| 68 |
+
post = block.register_forward_hook(
|
| 69 |
+
lambda _mod, _args, _out, *, idx=idx: self._post_hook(idx),
|
| 70 |
+
)
|
| 71 |
+
self._hooks.extend([pre, post])
|
| 72 |
+
|
| 73 |
+
# ------------------------------------------------------------------
|
| 74 |
+
# Teardown
|
| 75 |
+
# ------------------------------------------------------------------
|
| 76 |
+
|
| 77 |
+
def teardown(self) -> None:
|
| 78 |
+
"""Remove hooks and release all resources."""
|
| 79 |
+
for h in self._hooks:
|
| 80 |
+
h.remove()
|
| 81 |
+
self._hooks.clear()
|
| 82 |
+
self._provider.cleanup()
|
| 83 |
+
|
| 84 |
+
# ------------------------------------------------------------------
|
| 85 |
+
# Forward and attribute delegation
|
| 86 |
+
# ------------------------------------------------------------------
|
| 87 |
+
|
| 88 |
+
def forward(self, *args: Any, **kwargs: Any) -> Any: # noqa: ANN401
|
| 89 |
+
return self._model(*args, **kwargs)
|
| 90 |
+
|
| 91 |
+
def __getattr__(self, name: str) -> Any: # noqa: ANN401
|
| 92 |
+
"""Proxy attribute access to the wrapped model."""
|
| 93 |
+
try:
|
| 94 |
+
return super().__getattr__(name)
|
| 95 |
+
except AttributeError:
|
| 96 |
+
return getattr(self._model, name)
|
packages/ltx-core/src/ltx_core/components/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Diffusion pipeline components.
|
| 3 |
+
Submodules:
|
| 4 |
+
diffusion_steps - Diffusion stepping algorithms (EulerDiffusionStep)
|
| 5 |
+
guiders - Guidance strategies (CFGGuider, STGGuider, APG variants)
|
| 6 |
+
noisers - Noise samplers (GaussianNoiser)
|
| 7 |
+
patchifiers - Latent patchification (VideoLatentPatchifier, AudioPatchifier)
|
| 8 |
+
protocols - Protocol definitions (Patchifier, etc.)
|
| 9 |
+
schedulers - Sigma schedulers (LTX2Scheduler, LinearQuadraticScheduler)
|
| 10 |
+
"""
|
packages/ltx-core/src/ltx_core/components/diffusion_steps.py
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from ltx_core.components.protocols import DiffusionStepProtocol
|
| 4 |
+
from ltx_core.utils import to_velocity
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
def _get_ancestral_step(
|
| 8 |
+
sigma_from: torch.Tensor,
|
| 9 |
+
sigma_to: torch.Tensor,
|
| 10 |
+
eta: float = 1.0,
|
| 11 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 12 |
+
"""Compute ``(sigma_down, sigma_up)`` for one DDIM ancestral sampling step.
|
| 13 |
+
Both inputs are in the rescaled parameterization ``sigma / alpha``.
|
| 14 |
+
Returns ``sigma_down`` (deterministic component) and ``sigma_up``
|
| 15 |
+
(stochastic component) in the same rescaled space.
|
| 16 |
+
"""
|
| 17 |
+
if not eta:
|
| 18 |
+
return sigma_to, torch.zeros_like(sigma_to)
|
| 19 |
+
variance = sigma_to**2 * (sigma_from**2 - sigma_to**2).clamp(min=0) / sigma_from**2
|
| 20 |
+
sigma_up = (eta * variance**0.5).clamp(max=sigma_to)
|
| 21 |
+
sigma_down = (sigma_to**2 - sigma_up**2).clamp(min=0) ** 0.5
|
| 22 |
+
return sigma_down, sigma_up
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class EulerDiffusionStep(DiffusionStepProtocol):
|
| 26 |
+
"""
|
| 27 |
+
First-order Euler method for diffusion sampling.
|
| 28 |
+
Takes a single step from the current noise level (sigma) to the next by
|
| 29 |
+
computing velocity from the denoised prediction and applying: sample + velocity * dt.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def step(
|
| 33 |
+
self, sample: torch.Tensor, denoised_sample: torch.Tensor, sigmas: torch.Tensor, step_index: int, **_kwargs
|
| 34 |
+
) -> torch.Tensor:
|
| 35 |
+
sigma = sigmas[step_index]
|
| 36 |
+
sigma_next = sigmas[step_index + 1]
|
| 37 |
+
dt = sigma_next - sigma
|
| 38 |
+
velocity = to_velocity(sample, sigma, denoised_sample)
|
| 39 |
+
|
| 40 |
+
return (sample.to(torch.float32) + velocity.to(torch.float32) * dt).to(sample.dtype)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class Res2sDiffusionStep(DiffusionStepProtocol):
|
| 44 |
+
"""
|
| 45 |
+
Second-order diffusion step for res_2s sampling with SDE noise injection.
|
| 46 |
+
Used by the res_2s denoising loop. Advances the sample from the current
|
| 47 |
+
sigma to the next by mixing a deterministic update (from the denoised
|
| 48 |
+
prediction) with injected noise via ``get_sde_coeff``, producing
|
| 49 |
+
variance-preserving transitions.
|
| 50 |
+
"""
|
| 51 |
+
|
| 52 |
+
@staticmethod
|
| 53 |
+
def get_sde_coeff(
|
| 54 |
+
sigma_next: torch.Tensor,
|
| 55 |
+
sigma_up: torch.Tensor | None = None,
|
| 56 |
+
sigma_down: torch.Tensor | None = None,
|
| 57 |
+
sigma_max: torch.Tensor | None = None,
|
| 58 |
+
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
| 59 |
+
"""
|
| 60 |
+
Compute SDE coefficients (alpha_ratio, sigma_down, sigma_up) for the step.
|
| 61 |
+
Given either ``sigma_down`` or ``sigma_up``, returns the mixing
|
| 62 |
+
coefficients used for variance-preserving noise injection. If
|
| 63 |
+
``sigma_up`` is provided, ``sigma_down`` and ``alpha_ratio`` are
|
| 64 |
+
derived; if ``sigma_down`` is provided, ``sigma_up`` and
|
| 65 |
+
``alpha_ratio`` are derived.
|
| 66 |
+
"""
|
| 67 |
+
if sigma_down is not None:
|
| 68 |
+
alpha_ratio = (1 - sigma_next) / (1 - sigma_down)
|
| 69 |
+
sigma_up = (sigma_next**2 - sigma_down**2 * alpha_ratio**2).clamp(min=0) ** 0.5
|
| 70 |
+
elif sigma_up is not None:
|
| 71 |
+
# Fallback to avoid sqrt(neg_num)
|
| 72 |
+
sigma_up.clamp_(max=sigma_next * 0.9999)
|
| 73 |
+
sigmax = sigma_max if sigma_max is not None else torch.ones_like(sigma_next)
|
| 74 |
+
sigma_signal = sigmax - sigma_next
|
| 75 |
+
sigma_residual = (sigma_next**2 - sigma_up**2).clamp(min=0) ** 0.5
|
| 76 |
+
alpha_ratio = sigma_signal + sigma_residual
|
| 77 |
+
sigma_down = sigma_residual / alpha_ratio
|
| 78 |
+
else:
|
| 79 |
+
alpha_ratio = torch.ones_like(sigma_next)
|
| 80 |
+
sigma_down = sigma_next
|
| 81 |
+
sigma_up = torch.zeros_like(sigma_next)
|
| 82 |
+
|
| 83 |
+
sigma_up = torch.nan_to_num(sigma_up if sigma_up is not None else torch.zeros_like(sigma_next), 0.0)
|
| 84 |
+
# Replace NaNs in sigma_down with corresponding sigma_next elements (float32)
|
| 85 |
+
nan_mask = torch.isnan(sigma_down)
|
| 86 |
+
sigma_down[nan_mask] = sigma_next[nan_mask].to(sigma_down.dtype)
|
| 87 |
+
alpha_ratio = torch.nan_to_num(alpha_ratio, 1.0)
|
| 88 |
+
|
| 89 |
+
return alpha_ratio, sigma_down, sigma_up
|
| 90 |
+
|
| 91 |
+
def step(
|
| 92 |
+
self,
|
| 93 |
+
sample: torch.Tensor,
|
| 94 |
+
denoised_sample: torch.Tensor,
|
| 95 |
+
sigmas: torch.Tensor,
|
| 96 |
+
step_index: int,
|
| 97 |
+
noise: torch.Tensor,
|
| 98 |
+
eta: float = 0.5,
|
| 99 |
+
) -> torch.Tensor:
|
| 100 |
+
"""Advance one step with SDE noise injection via get_sde_coeff.
|
| 101 |
+
Args:
|
| 102 |
+
sample: Current noisy sample.
|
| 103 |
+
denoised_sample: Denoised prediction from the model.
|
| 104 |
+
sigmas: Noise schedule tensor.
|
| 105 |
+
step_index: Current step index in the schedule.
|
| 106 |
+
noise: Random noise tensor for stochastic injection.
|
| 107 |
+
eta: Controls stochastic noise injection strength (0=deterministic, 1=maximum). Default 0.5.
|
| 108 |
+
Returns:
|
| 109 |
+
Next sample with SDE noise injection applied.
|
| 110 |
+
"""
|
| 111 |
+
sigma = sigmas[step_index]
|
| 112 |
+
sigma_next = sigmas[step_index + 1]
|
| 113 |
+
alpha_ratio, sigma_down, sigma_up = self.get_sde_coeff(sigma_next, sigma_up=sigma_next * eta)
|
| 114 |
+
output_dtype = denoised_sample.dtype
|
| 115 |
+
if torch.any(sigma_up == 0) or torch.any(sigma_next == 0):
|
| 116 |
+
return denoised_sample
|
| 117 |
+
|
| 118 |
+
# Extract epsilon prediction
|
| 119 |
+
eps_next = (sample - denoised_sample) / (sigma - sigma_next)
|
| 120 |
+
denoised_next = sample - sigma * eps_next
|
| 121 |
+
|
| 122 |
+
# Mix deterministic and stochastic components
|
| 123 |
+
x_noised = alpha_ratio * (denoised_next + sigma_down * eps_next) + sigma_up * noise
|
| 124 |
+
return x_noised.to(output_dtype)
|
| 125 |
+
|
| 126 |
+
|
| 127 |
+
class EulerCfgPpDiffusionStep(DiffusionStepProtocol):
|
| 128 |
+
"""Euler step using the CFG++ correction for the ODE derivative.
|
| 129 |
+
Instead of the standard velocity formula, the ODE derivative is computed
|
| 130 |
+
from the unconditioned prediction, keeping the conditioned prediction as
|
| 131 |
+
the target denoised state. Ancestral (DDIM) noise injection is applied
|
| 132 |
+
in the rescaled sigma parameterization (sigma / alpha).
|
| 133 |
+
All diffusion quantities (alpha, ODE derivative, ancestral coefficients)
|
| 134 |
+
are computed internally from ``sigmas`` and ``uncond_denoised``.
|
| 135 |
+
Reference: CFG++ (https://arxiv.org/abs/2406.08070).
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
def __init__(self, eta: float = 1.0, s_noise: float = 1.0) -> None:
|
| 139 |
+
self.eta = eta
|
| 140 |
+
self.s_noise = s_noise
|
| 141 |
+
|
| 142 |
+
def step(
|
| 143 |
+
self,
|
| 144 |
+
sample: torch.Tensor,
|
| 145 |
+
denoised_sample: torch.Tensor,
|
| 146 |
+
sigmas: torch.Tensor,
|
| 147 |
+
step_index: int,
|
| 148 |
+
uncond_denoised: torch.Tensor,
|
| 149 |
+
noise: torch.Tensor | None = None,
|
| 150 |
+
**_kwargs,
|
| 151 |
+
) -> torch.Tensor:
|
| 152 |
+
"""Advance one CFG++ Euler step.
|
| 153 |
+
Args:
|
| 154 |
+
sample: Current noisy latent x_t.
|
| 155 |
+
denoised_sample: Conditioned denoised prediction x_0^cond.
|
| 156 |
+
sigmas: Full sigma schedule tensor.
|
| 157 |
+
step_index: Current step index.
|
| 158 |
+
uncond_denoised: Unconditioned denoised prediction x_0^uncond,
|
| 159 |
+
used to compute the ODE derivative direction.
|
| 160 |
+
noise: Noise tensor for stochastic injection; ignored when
|
| 161 |
+
``eta=0`` or ``s_noise=0``.
|
| 162 |
+
Returns:
|
| 163 |
+
Updated latent x_{t-1}.
|
| 164 |
+
"""
|
| 165 |
+
sigma_s = sigmas[step_index].to(torch.float32)
|
| 166 |
+
sigma_t = sigmas[step_index + 1].to(torch.float32)
|
| 167 |
+
_eps = torch.finfo(torch.float32).eps
|
| 168 |
+
# Clamp to avoid division by zero when sigma == 1.0 exactly.
|
| 169 |
+
alpha_s = (1.0 - sigma_s).clamp(min=_eps)
|
| 170 |
+
alpha_t = (1.0 - sigma_t).clamp(min=_eps)
|
| 171 |
+
|
| 172 |
+
x = sample.to(torch.float32)
|
| 173 |
+
denoised = denoised_sample.to(torch.float32)
|
| 174 |
+
uncond = uncond_denoised.to(torch.float32)
|
| 175 |
+
|
| 176 |
+
# ODE derivative: direction toward noise using uncond prediction (CFG++ correction)
|
| 177 |
+
d = (x - alpha_s * uncond) / sigma_s
|
| 178 |
+
|
| 179 |
+
# Ancestral step in rescaled sigma space (sigma / alpha)
|
| 180 |
+
sigma_down, sigma_up = _get_ancestral_step(sigma_s / alpha_s, sigma_t / alpha_t, eta=self.eta)
|
| 181 |
+
sigma_down = alpha_t * sigma_down
|
| 182 |
+
|
| 183 |
+
x_next = alpha_t * denoised + sigma_down * d
|
| 184 |
+
if noise is not None and self.eta > 0 and self.s_noise > 0:
|
| 185 |
+
x_next = x_next + alpha_t * noise.to(torch.float32) * self.s_noise * sigma_up
|
| 186 |
+
return x_next.to(sample.dtype)
|
packages/ltx-core/src/ltx_core/components/guiders.py
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from collections.abc import Mapping, Sequence
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from ltx_core.components.protocols import GuiderProtocol
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
@dataclass(frozen=True)
|
| 11 |
+
class CFGGuider(GuiderProtocol):
|
| 12 |
+
"""
|
| 13 |
+
Classifier-free guidance (CFG) guider.
|
| 14 |
+
Computes the guidance delta as (scale - 1) * (cond - uncond), steering the
|
| 15 |
+
denoising process toward the conditioned prediction.
|
| 16 |
+
Attributes:
|
| 17 |
+
scale: Guidance strength. 1.0 means no guidance, higher values increase
|
| 18 |
+
adherence to the conditioning.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
scale: float
|
| 22 |
+
|
| 23 |
+
def delta(self, cond: torch.Tensor, uncond: torch.Tensor) -> torch.Tensor:
|
| 24 |
+
return (self.scale - 1) * (cond - uncond)
|
| 25 |
+
|
| 26 |
+
def enabled(self) -> bool:
|
| 27 |
+
return self.scale != 1.0
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
@dataclass(frozen=True)
|
| 31 |
+
class CFGStarRescalingGuider(GuiderProtocol):
|
| 32 |
+
"""
|
| 33 |
+
Calculates the CFG delta between conditioned and unconditioned samples.
|
| 34 |
+
To minimize offset in the denoising direction and move mostly along the
|
| 35 |
+
conditioning axis within the distribution, the unconditioned sample is
|
| 36 |
+
rescaled in accordance with the norm of the conditioned sample.
|
| 37 |
+
Attributes:
|
| 38 |
+
scale (float):
|
| 39 |
+
Global guidance strength. A value of 1.0 corresponds to no extra
|
| 40 |
+
guidance beyond the base model prediction. Values > 1.0 increase
|
| 41 |
+
the influence of the conditioned sample relative to the
|
| 42 |
+
unconditioned one.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
scale: float
|
| 46 |
+
|
| 47 |
+
def delta(self, cond: torch.Tensor, uncond: torch.Tensor) -> torch.Tensor:
|
| 48 |
+
rescaled_neg = projection_coef(cond, uncond) * uncond
|
| 49 |
+
return (self.scale - 1) * (cond - rescaled_neg)
|
| 50 |
+
|
| 51 |
+
def enabled(self) -> bool:
|
| 52 |
+
return self.scale != 1.0
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
@dataclass(frozen=True)
|
| 56 |
+
class STGGuider(GuiderProtocol):
|
| 57 |
+
"""
|
| 58 |
+
Calculates the STG delta between conditioned and perturbed denoised samples.
|
| 59 |
+
Perturbed samples are the result of the denoising process with perturbations,
|
| 60 |
+
e.g. attentions acting as passthrough for certain layers and modalities.
|
| 61 |
+
Attributes:
|
| 62 |
+
scale (float):
|
| 63 |
+
Global strength of the STG guidance. A value of 0.0 disables the
|
| 64 |
+
guidance. Larger values increase the correction applied in the
|
| 65 |
+
direction of (pos_denoised - perturbed_denoised).
|
| 66 |
+
"""
|
| 67 |
+
|
| 68 |
+
scale: float
|
| 69 |
+
|
| 70 |
+
def delta(self, pos_denoised: torch.Tensor, perturbed_denoised: torch.Tensor) -> torch.Tensor:
|
| 71 |
+
return self.scale * (pos_denoised - perturbed_denoised)
|
| 72 |
+
|
| 73 |
+
def enabled(self) -> bool:
|
| 74 |
+
return self.scale != 0.0
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclass(frozen=True)
|
| 78 |
+
class LtxAPGGuider(GuiderProtocol):
|
| 79 |
+
"""
|
| 80 |
+
Calculates the APG (adaptive projected guidance) delta between conditioned
|
| 81 |
+
and unconditioned samples.
|
| 82 |
+
To minimize offset in the denoising direction and move mostly along the
|
| 83 |
+
conditioning axis within the distribution, the (cond - uncond) delta is
|
| 84 |
+
decomposed into components parallel and orthogonal to the conditioned
|
| 85 |
+
sample. The `eta` parameter weights the parallel component, while `scale`
|
| 86 |
+
is applied to the orthogonal component. Optionally, a norm threshold can
|
| 87 |
+
be used to suppress guidance when the magnitude of the correction is small.
|
| 88 |
+
Attributes:
|
| 89 |
+
scale (float):
|
| 90 |
+
Strength applied to the component of the guidance that is orthogonal
|
| 91 |
+
to the conditioned sample. Controls how aggressively we move in
|
| 92 |
+
directions that change semantics but stay consistent with the
|
| 93 |
+
conditioning manifold.
|
| 94 |
+
eta (float):
|
| 95 |
+
Weight of the component of the guidance that is parallel to the
|
| 96 |
+
conditioned sample. A value of 1.0 keeps the full parallel
|
| 97 |
+
component; values in [0, 1] attenuate it, and values > 1.0 amplify
|
| 98 |
+
motion along the conditioning direction.
|
| 99 |
+
norm_threshold (float):
|
| 100 |
+
Minimum L2 norm of the guidance delta below which the guidance
|
| 101 |
+
can be reduced or ignored (depending on implementation).
|
| 102 |
+
This is useful for avoiding noisy or unstable updates when the
|
| 103 |
+
guidance signal is very small.
|
| 104 |
+
"""
|
| 105 |
+
|
| 106 |
+
scale: float
|
| 107 |
+
eta: float = 1.0
|
| 108 |
+
norm_threshold: float = 0.0
|
| 109 |
+
|
| 110 |
+
def delta(self, cond: torch.Tensor, uncond: torch.Tensor) -> torch.Tensor:
|
| 111 |
+
guidance = cond - uncond
|
| 112 |
+
if self.norm_threshold > 0:
|
| 113 |
+
ones = torch.ones_like(guidance)
|
| 114 |
+
guidance_norm = guidance.norm(p=2, dim=[-1, -2, -3], keepdim=True)
|
| 115 |
+
scale_factor = torch.minimum(ones, self.norm_threshold / guidance_norm)
|
| 116 |
+
guidance = guidance * scale_factor
|
| 117 |
+
proj_coeff = projection_coef(guidance, cond)
|
| 118 |
+
g_parallel = proj_coeff * cond
|
| 119 |
+
g_orth = guidance - g_parallel
|
| 120 |
+
g_apg = g_parallel * self.eta + g_orth
|
| 121 |
+
|
| 122 |
+
return g_apg * (self.scale - 1)
|
| 123 |
+
|
| 124 |
+
def enabled(self) -> bool:
|
| 125 |
+
return self.scale != 1.0
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
@dataclass(frozen=False)
|
| 129 |
+
class LegacyStatefulAPGGuider(GuiderProtocol):
|
| 130 |
+
"""
|
| 131 |
+
Calculates the APG (adaptive projected guidance) delta between conditioned
|
| 132 |
+
and unconditioned samples.
|
| 133 |
+
To minimize offset in the denoising direction and move mostly along the
|
| 134 |
+
conditioning axis within the distribution, the (cond - uncond) delta is
|
| 135 |
+
decomposed into components parallel and orthogonal to the conditioned
|
| 136 |
+
sample. The `eta` parameter weights the parallel component, while `scale`
|
| 137 |
+
is applied to the orthogonal component. Optionally, a norm threshold can
|
| 138 |
+
be used to suppress guidance when the magnitude of the correction is small.
|
| 139 |
+
Attributes:
|
| 140 |
+
scale (float):
|
| 141 |
+
Strength applied to the component of the guidance that is orthogonal
|
| 142 |
+
to the conditioned sample. Controls how aggressively we move in
|
| 143 |
+
directions that change semantics but stay consistent with the
|
| 144 |
+
conditioning manifold.
|
| 145 |
+
eta (float):
|
| 146 |
+
Weight of the component of the guidance that is parallel to the
|
| 147 |
+
conditioned sample. A value of 1.0 keeps the full parallel
|
| 148 |
+
component; values in [0, 1] attenuate it, and values > 1.0 amplify
|
| 149 |
+
motion along the conditioning direction.
|
| 150 |
+
norm_threshold (float):
|
| 151 |
+
Minimum L2 norm of the guidance delta below which the guidance
|
| 152 |
+
can be reduced or ignored (depending on implementation).
|
| 153 |
+
This is useful for avoiding noisy or unstable updates when the
|
| 154 |
+
guidance signal is very small.
|
| 155 |
+
momentum (float):
|
| 156 |
+
Exponential moving-average coefficient for accumulating guidance
|
| 157 |
+
over time. running_avg = momentum * running_avg + guidance
|
| 158 |
+
"""
|
| 159 |
+
|
| 160 |
+
scale: float
|
| 161 |
+
eta: float
|
| 162 |
+
norm_threshold: float = 5.0
|
| 163 |
+
momentum: float = 0.0
|
| 164 |
+
# it is user's responsibility not to use same APGGuider for several denoisings or different modalities
|
| 165 |
+
# in order not to share accumulated average across different denoisings or modalities
|
| 166 |
+
running_avg: torch.Tensor | None = None
|
| 167 |
+
|
| 168 |
+
def delta(self, cond: torch.Tensor, uncond: torch.Tensor) -> torch.Tensor:
|
| 169 |
+
guidance = cond - uncond
|
| 170 |
+
if self.momentum != 0:
|
| 171 |
+
if self.running_avg is None:
|
| 172 |
+
self.running_avg = guidance.clone()
|
| 173 |
+
else:
|
| 174 |
+
self.running_avg = self.momentum * self.running_avg + guidance
|
| 175 |
+
guidance = self.running_avg
|
| 176 |
+
|
| 177 |
+
if self.norm_threshold > 0:
|
| 178 |
+
ones = torch.ones_like(guidance)
|
| 179 |
+
guidance_norm = guidance.norm(p=2, dim=[-1, -2, -3], keepdim=True)
|
| 180 |
+
scale_factor = torch.minimum(ones, self.norm_threshold / guidance_norm)
|
| 181 |
+
guidance = guidance * scale_factor
|
| 182 |
+
|
| 183 |
+
proj_coeff = projection_coef(guidance, cond)
|
| 184 |
+
g_parallel = proj_coeff * cond
|
| 185 |
+
g_orth = guidance - g_parallel
|
| 186 |
+
g_apg = g_parallel * self.eta + g_orth
|
| 187 |
+
|
| 188 |
+
return g_apg * self.scale
|
| 189 |
+
|
| 190 |
+
def enabled(self) -> bool:
|
| 191 |
+
return self.scale != 0.0
|
| 192 |
+
|
| 193 |
+
|
| 194 |
+
@dataclass(frozen=True)
|
| 195 |
+
class MultiModalGuiderParams:
|
| 196 |
+
"""
|
| 197 |
+
Parameters for the multi-modal guider.
|
| 198 |
+
"""
|
| 199 |
+
|
| 200 |
+
cfg_scale: float = 1.0
|
| 201 |
+
"CFG (Classifier-free guidance) scale controlling how strongly the model adheres to the prompt."
|
| 202 |
+
stg_scale: float = 0.0
|
| 203 |
+
"STG (Spatio-Temporal Guidance) scale controls how strongly the model reacts to the perturbation of the modality."
|
| 204 |
+
stg_blocks: list[int] | None = field(default_factory=list)
|
| 205 |
+
"Which transformer blocks to perturb for STG."
|
| 206 |
+
rescale_scale: float = 0.0
|
| 207 |
+
"Rescale scale controlling how strongly the model rescales the modality after applying other guidance."
|
| 208 |
+
modality_scale: float = 1.0
|
| 209 |
+
"Modality scale controlling how strongly the model reacts to the perturbation of the modality."
|
| 210 |
+
skip_step: int = 0
|
| 211 |
+
"Skip step controlling how often the model skips the step."
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def _params_for_sigma_from_sorted_dict(
|
| 215 |
+
sigma: float, params_by_sigma: Sequence[tuple[float, MultiModalGuiderParams]]
|
| 216 |
+
) -> MultiModalGuiderParams:
|
| 217 |
+
"""
|
| 218 |
+
Return params for the given sigma from a sorted (sigma_upper_bound -> params) structure.
|
| 219 |
+
Keys are sorted descending (bin upper bounds). Bin i is (key_{i+1}, key_i].
|
| 220 |
+
Get all keys >= sigma; use last in list (smallest such key = upper bound of bin containing sigma),
|
| 221 |
+
or last entry in the sequence if list is empty (sigma above max key).
|
| 222 |
+
"""
|
| 223 |
+
if not params_by_sigma:
|
| 224 |
+
raise ValueError("params_by_sigma must be non-empty")
|
| 225 |
+
sigma = float(sigma)
|
| 226 |
+
keys_desc = [k for k, _ in params_by_sigma]
|
| 227 |
+
keys_ge_sigma = [k for k in keys_desc if k >= sigma]
|
| 228 |
+
# sigma above all keys: use first bin (max key)
|
| 229 |
+
key = keys_ge_sigma[-1] if keys_ge_sigma else keys_desc[0]
|
| 230 |
+
return next(p for k, p in params_by_sigma if k == key)
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
@dataclass(frozen=True)
|
| 234 |
+
class MultiModalGuider:
|
| 235 |
+
"""
|
| 236 |
+
Multi-modal guider with constant params per instance.
|
| 237 |
+
For sigma-dependent params, use MultiModalGuiderFactory.build_from_sigma(sigma) to
|
| 238 |
+
obtain a guider for each step.
|
| 239 |
+
"""
|
| 240 |
+
|
| 241 |
+
params: MultiModalGuiderParams
|
| 242 |
+
negative_context: torch.Tensor | None = None
|
| 243 |
+
|
| 244 |
+
def calculate(
|
| 245 |
+
self,
|
| 246 |
+
cond: torch.Tensor,
|
| 247 |
+
uncond_text: torch.Tensor | float,
|
| 248 |
+
uncond_perturbed: torch.Tensor | float,
|
| 249 |
+
uncond_modality: torch.Tensor | float,
|
| 250 |
+
) -> torch.Tensor:
|
| 251 |
+
"""
|
| 252 |
+
The guider calculates the guidance delta as (scale - 1) * (cond - uncond) for cfg and modality cfg,
|
| 253 |
+
and as scale * (cond - uncond) for stg, steering the denoising process away from the unconditioned
|
| 254 |
+
prediction.
|
| 255 |
+
"""
|
| 256 |
+
pred = (
|
| 257 |
+
cond
|
| 258 |
+
+ (self.params.cfg_scale - 1) * (cond - uncond_text)
|
| 259 |
+
+ self.params.stg_scale * (cond - uncond_perturbed)
|
| 260 |
+
+ (self.params.modality_scale - 1) * (cond - uncond_modality)
|
| 261 |
+
)
|
| 262 |
+
|
| 263 |
+
if self.params.rescale_scale != 0:
|
| 264 |
+
factor = cond.std() / pred.std()
|
| 265 |
+
factor = self.params.rescale_scale * factor + (1 - self.params.rescale_scale)
|
| 266 |
+
pred = pred * factor
|
| 267 |
+
|
| 268 |
+
return pred
|
| 269 |
+
|
| 270 |
+
def do_unconditional_generation(self) -> bool:
|
| 271 |
+
"""Returns True if the guider is doing unconditional generation."""
|
| 272 |
+
return not math.isclose(self.params.cfg_scale, 1.0)
|
| 273 |
+
|
| 274 |
+
def do_perturbed_generation(self) -> bool:
|
| 275 |
+
"""Returns True if the guider is doing perturbed generation."""
|
| 276 |
+
return not math.isclose(self.params.stg_scale, 0.0)
|
| 277 |
+
|
| 278 |
+
def do_isolated_modality_generation(self) -> bool:
|
| 279 |
+
"""Returns True if the guider is doing isolated modality generation."""
|
| 280 |
+
return not math.isclose(self.params.modality_scale, 1.0)
|
| 281 |
+
|
| 282 |
+
def should_skip_step(self, step: int) -> bool:
|
| 283 |
+
"""Returns True if the guider should skip the step."""
|
| 284 |
+
if self.params.skip_step == 0:
|
| 285 |
+
return False
|
| 286 |
+
return step % (self.params.skip_step + 1) != 0
|
| 287 |
+
|
| 288 |
+
|
| 289 |
+
@dataclass(frozen=True)
|
| 290 |
+
class MultiModalGuiderFactory:
|
| 291 |
+
"""
|
| 292 |
+
Factory that creates a MultiModalGuider for a given sigma.
|
| 293 |
+
Single source of truth: _params_by_sigma (schedule). Use constant() for
|
| 294 |
+
one params for all sigma, from_dict() for sigma-binned params.
|
| 295 |
+
"""
|
| 296 |
+
|
| 297 |
+
negative_context: torch.Tensor | None = None
|
| 298 |
+
_params_by_sigma: tuple[tuple[float, MultiModalGuiderParams], ...] = ()
|
| 299 |
+
|
| 300 |
+
@classmethod
|
| 301 |
+
def constant(
|
| 302 |
+
cls,
|
| 303 |
+
params: MultiModalGuiderParams,
|
| 304 |
+
negative_context: torch.Tensor | None = None,
|
| 305 |
+
) -> "MultiModalGuiderFactory":
|
| 306 |
+
"""Build a factory with constant params (same guider for all sigma)."""
|
| 307 |
+
return cls(
|
| 308 |
+
negative_context=negative_context,
|
| 309 |
+
_params_by_sigma=((float("inf"), params),),
|
| 310 |
+
)
|
| 311 |
+
|
| 312 |
+
@classmethod
|
| 313 |
+
def from_dict(
|
| 314 |
+
cls,
|
| 315 |
+
sigma_to_params: Mapping[float, MultiModalGuiderParams],
|
| 316 |
+
negative_context: torch.Tensor | None = None,
|
| 317 |
+
) -> "MultiModalGuiderFactory":
|
| 318 |
+
"""
|
| 319 |
+
Build a factory from a dict of sigma_value -> MultiModalGuiderParams.
|
| 320 |
+
Keys are sorted descending and used for bin lookup in params(sigma).
|
| 321 |
+
"""
|
| 322 |
+
if not sigma_to_params:
|
| 323 |
+
raise ValueError("sigma_to_params must be non-empty")
|
| 324 |
+
sorted_items = tuple(sorted(sigma_to_params.items(), key=lambda x: x[0], reverse=True))
|
| 325 |
+
return cls(negative_context=negative_context, _params_by_sigma=sorted_items)
|
| 326 |
+
|
| 327 |
+
def params(self, sigma: float | torch.Tensor) -> MultiModalGuiderParams:
|
| 328 |
+
"""Return params effective for the given sigma (getter; single source of truth)."""
|
| 329 |
+
sigma_val = float(sigma.item() if isinstance(sigma, torch.Tensor) else sigma)
|
| 330 |
+
return _params_for_sigma_from_sorted_dict(sigma_val, self._params_by_sigma)
|
| 331 |
+
|
| 332 |
+
def build_from_sigma(self, sigma: float | torch.Tensor) -> MultiModalGuider:
|
| 333 |
+
"""Return a MultiModalGuider with params effective for the given sigma."""
|
| 334 |
+
return MultiModalGuider(
|
| 335 |
+
params=self.params(sigma),
|
| 336 |
+
negative_context=self.negative_context,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def create_multimodal_guider_factory(
|
| 341 |
+
params: MultiModalGuiderParams | MultiModalGuiderFactory,
|
| 342 |
+
negative_context: torch.Tensor | None = None,
|
| 343 |
+
) -> MultiModalGuiderFactory:
|
| 344 |
+
"""
|
| 345 |
+
Create or return a MultiModalGuiderFactory. Pass constant params for a
|
| 346 |
+
single-params factory (uses MultiModalGuiderFactory.constant), or an existing
|
| 347 |
+
MultiModalGuiderFactory. When given a factory, returns it as-is unless
|
| 348 |
+
negative_context is provided. For sigma-dependent params use
|
| 349 |
+
MultiModalGuiderFactory.from_dict(...) and pass that as params.
|
| 350 |
+
"""
|
| 351 |
+
if isinstance(params, MultiModalGuiderFactory):
|
| 352 |
+
if negative_context is not None and params.negative_context is not negative_context:
|
| 353 |
+
return MultiModalGuiderFactory.from_dict(dict(params._params_by_sigma), negative_context=negative_context)
|
| 354 |
+
return params
|
| 355 |
+
return MultiModalGuiderFactory.constant(params, negative_context=negative_context)
|
| 356 |
+
|
| 357 |
+
|
| 358 |
+
def projection_coef(to_project: torch.Tensor, project_onto: torch.Tensor) -> torch.Tensor:
|
| 359 |
+
batch_size = to_project.shape[0]
|
| 360 |
+
positive_flat = to_project.reshape(batch_size, -1)
|
| 361 |
+
negative_flat = project_onto.reshape(batch_size, -1)
|
| 362 |
+
dot_product = torch.sum(positive_flat * negative_flat, dim=1, keepdim=True)
|
| 363 |
+
squared_norm = torch.sum(negative_flat**2, dim=1, keepdim=True) + 1e-8
|
| 364 |
+
return dot_product / squared_norm
|
packages/ltx-core/src/ltx_core/components/noisers.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import replace
|
| 2 |
+
from typing import Protocol
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from ltx_core.types import LatentState
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class Noiser(Protocol):
|
| 10 |
+
"""Protocol for adding noise to a latent state during diffusion."""
|
| 11 |
+
|
| 12 |
+
def __call__(self, latent_state: LatentState, noise_scale: float) -> LatentState: ...
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class GaussianNoiser(Noiser):
|
| 16 |
+
"""Adds Gaussian noise to a latent state, scaled by the denoise mask."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, generator: torch.Generator):
|
| 19 |
+
super().__init__()
|
| 20 |
+
|
| 21 |
+
self.generator = generator
|
| 22 |
+
|
| 23 |
+
def __call__(self, latent_state: LatentState, noise_scale: float = 1.0) -> LatentState:
|
| 24 |
+
noise = torch.randn(
|
| 25 |
+
*latent_state.latent.shape,
|
| 26 |
+
device=latent_state.latent.device,
|
| 27 |
+
dtype=latent_state.latent.dtype,
|
| 28 |
+
generator=self.generator,
|
| 29 |
+
)
|
| 30 |
+
scaled_mask = latent_state.denoise_mask * noise_scale
|
| 31 |
+
latent = noise * scaled_mask + latent_state.latent * (1 - scaled_mask)
|
| 32 |
+
return replace(
|
| 33 |
+
latent_state,
|
| 34 |
+
latent=latent.to(latent_state.latent.dtype),
|
| 35 |
+
)
|
packages/ltx-core/src/ltx_core/components/patchifiers.py
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from typing import Optional, Tuple
|
| 3 |
+
|
| 4 |
+
import einops
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from ltx_core.components.protocols import Patchifier
|
| 8 |
+
from ltx_core.types import AudioLatentShape, SpatioTemporalScaleFactors, VideoLatentShape
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class VideoLatentPatchifier(Patchifier):
|
| 12 |
+
def __init__(self, patch_size: int):
|
| 13 |
+
# Patch sizes for video latents.
|
| 14 |
+
self._patch_size = (
|
| 15 |
+
1, # temporal dimension
|
| 16 |
+
patch_size, # height dimension
|
| 17 |
+
patch_size, # width dimension
|
| 18 |
+
)
|
| 19 |
+
|
| 20 |
+
@property
|
| 21 |
+
def patch_size(self) -> Tuple[int, int, int]:
|
| 22 |
+
return self._patch_size
|
| 23 |
+
|
| 24 |
+
def get_token_count(self, tgt_shape: VideoLatentShape) -> int:
|
| 25 |
+
return math.prod(tgt_shape.to_torch_shape()[2:]) // math.prod(self._patch_size)
|
| 26 |
+
|
| 27 |
+
def patchify(
|
| 28 |
+
self,
|
| 29 |
+
latents: torch.Tensor,
|
| 30 |
+
) -> torch.Tensor:
|
| 31 |
+
latents = einops.rearrange(
|
| 32 |
+
latents,
|
| 33 |
+
"b c (f p1) (h p2) (w p3) -> b (f h w) (c p1 p2 p3)",
|
| 34 |
+
p1=self._patch_size[0],
|
| 35 |
+
p2=self._patch_size[1],
|
| 36 |
+
p3=self._patch_size[2],
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
return latents
|
| 40 |
+
|
| 41 |
+
def unpatchify(
|
| 42 |
+
self,
|
| 43 |
+
latents: torch.Tensor,
|
| 44 |
+
output_shape: VideoLatentShape,
|
| 45 |
+
) -> torch.Tensor:
|
| 46 |
+
assert self._patch_size[0] == 1, "Temporal patch size must be 1 for symmetric patchifier"
|
| 47 |
+
|
| 48 |
+
patch_grid_frames = output_shape.frames // self._patch_size[0]
|
| 49 |
+
patch_grid_height = output_shape.height // self._patch_size[1]
|
| 50 |
+
patch_grid_width = output_shape.width // self._patch_size[2]
|
| 51 |
+
|
| 52 |
+
latents = einops.rearrange(
|
| 53 |
+
latents,
|
| 54 |
+
"b (f h w) (c p q) -> b c f (h p) (w q)",
|
| 55 |
+
f=patch_grid_frames,
|
| 56 |
+
h=patch_grid_height,
|
| 57 |
+
w=patch_grid_width,
|
| 58 |
+
p=self._patch_size[1],
|
| 59 |
+
q=self._patch_size[2],
|
| 60 |
+
)
|
| 61 |
+
|
| 62 |
+
return latents
|
| 63 |
+
|
| 64 |
+
def get_patch_grid_bounds(
|
| 65 |
+
self,
|
| 66 |
+
output_shape: AudioLatentShape | VideoLatentShape,
|
| 67 |
+
device: Optional[torch.device] = None,
|
| 68 |
+
) -> torch.Tensor:
|
| 69 |
+
"""
|
| 70 |
+
Return the per-dimension bounds [inclusive start, exclusive end) for every
|
| 71 |
+
patch produced by `patchify`. The bounds are expressed in the original
|
| 72 |
+
video grid coordinates: frame/time, height, and width.
|
| 73 |
+
The resulting tensor is shaped `[batch_size, 3, num_patches, 2]`, where:
|
| 74 |
+
- axis 1 (size 3) enumerates (frame/time, height, width) dimensions
|
| 75 |
+
- axis 3 (size 2) stores `[start, end)` indices within each dimension
|
| 76 |
+
Args:
|
| 77 |
+
output_shape: Video grid description containing frames, height, and width.
|
| 78 |
+
device: Device of the latent tensor.
|
| 79 |
+
"""
|
| 80 |
+
if not isinstance(output_shape, VideoLatentShape):
|
| 81 |
+
raise ValueError("VideoLatentPatchifier expects VideoLatentShape when computing coordinates")
|
| 82 |
+
|
| 83 |
+
frames = output_shape.frames
|
| 84 |
+
height = output_shape.height
|
| 85 |
+
width = output_shape.width
|
| 86 |
+
batch_size = output_shape.batch
|
| 87 |
+
|
| 88 |
+
# Validate inputs to ensure positive dimensions
|
| 89 |
+
assert frames > 0, f"frames must be positive, got {frames}"
|
| 90 |
+
assert height > 0, f"height must be positive, got {height}"
|
| 91 |
+
assert width > 0, f"width must be positive, got {width}"
|
| 92 |
+
assert batch_size > 0, f"batch_size must be positive, got {batch_size}"
|
| 93 |
+
|
| 94 |
+
# Generate grid coordinates for each dimension (frame, height, width)
|
| 95 |
+
# We use torch.arange to create the starting coordinates for each patch.
|
| 96 |
+
# indexing='ij' ensures the dimensions are in the order (frame, height, width).
|
| 97 |
+
grid_coords = torch.meshgrid(
|
| 98 |
+
torch.arange(start=0, end=frames, step=self._patch_size[0], device=device),
|
| 99 |
+
torch.arange(start=0, end=height, step=self._patch_size[1], device=device),
|
| 100 |
+
torch.arange(start=0, end=width, step=self._patch_size[2], device=device),
|
| 101 |
+
indexing="ij",
|
| 102 |
+
)
|
| 103 |
+
|
| 104 |
+
# Stack the grid coordinates to create the start coordinates tensor.
|
| 105 |
+
# Shape becomes (3, grid_f, grid_h, grid_w)
|
| 106 |
+
patch_starts = torch.stack(grid_coords, dim=0)
|
| 107 |
+
|
| 108 |
+
# Create a tensor containing the size of a single patch:
|
| 109 |
+
# (frame_patch_size, height_patch_size, width_patch_size).
|
| 110 |
+
# Reshape to (3, 1, 1, 1) to enable broadcasting when adding to the start coordinates.
|
| 111 |
+
patch_size_delta = torch.tensor(
|
| 112 |
+
self._patch_size,
|
| 113 |
+
device=patch_starts.device,
|
| 114 |
+
dtype=patch_starts.dtype,
|
| 115 |
+
).view(3, 1, 1, 1)
|
| 116 |
+
|
| 117 |
+
# Calculate end coordinates: start + patch_size
|
| 118 |
+
# Shape becomes (3, grid_f, grid_h, grid_w)
|
| 119 |
+
patch_ends = patch_starts + patch_size_delta
|
| 120 |
+
|
| 121 |
+
# Stack start and end coordinates together along the last dimension
|
| 122 |
+
# Shape becomes (3, grid_f, grid_h, grid_w, 2), where the last dimension is [start, end]
|
| 123 |
+
latent_coords = torch.stack((patch_starts, patch_ends), dim=-1)
|
| 124 |
+
|
| 125 |
+
# Broadcast to batch size and flatten all spatial/temporal dimensions into one sequence.
|
| 126 |
+
# Final Shape: (batch_size, 3, num_patches, 2)
|
| 127 |
+
latent_coords = einops.repeat(
|
| 128 |
+
latent_coords,
|
| 129 |
+
"c f h w bounds -> b c (f h w) bounds",
|
| 130 |
+
b=batch_size,
|
| 131 |
+
bounds=2,
|
| 132 |
+
)
|
| 133 |
+
|
| 134 |
+
return latent_coords
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
def get_pixel_coords(
|
| 138 |
+
latent_coords: torch.Tensor,
|
| 139 |
+
scale_factors: SpatioTemporalScaleFactors,
|
| 140 |
+
causal_fix: bool = False,
|
| 141 |
+
) -> torch.Tensor:
|
| 142 |
+
"""
|
| 143 |
+
Map latent-space `[start, end)` coordinates to their pixel-space equivalents by scaling
|
| 144 |
+
each axis (frame/time, height, width) with the corresponding VAE downsampling factors.
|
| 145 |
+
Optionally compensate for causal encoding that keeps the first frame at unit temporal scale.
|
| 146 |
+
Args:
|
| 147 |
+
latent_coords: Tensor of latent bounds shaped `(batch, 3, num_patches, 2)`.
|
| 148 |
+
scale_factors: SpatioTemporalScaleFactors tuple `(temporal, height, width)` with integer scale factors applied
|
| 149 |
+
per axis.
|
| 150 |
+
causal_fix: When True, rewrites the temporal axis of the first frame so causal VAEs
|
| 151 |
+
that treat frame zero differently still yield non-negative timestamps.
|
| 152 |
+
"""
|
| 153 |
+
# Broadcast the VAE scale factors so they align with the `(batch, axis, patch, bound)` layout.
|
| 154 |
+
# Axis 1 of `latent_coords` is ordered (frame/time, height, width) — match that explicitly by
|
| 155 |
+
# pulling fields from the NamedTuple rather than relying on tuple iteration order.
|
| 156 |
+
broadcast_shape = [1] * latent_coords.ndim
|
| 157 |
+
broadcast_shape[1] = -1 # axis dimension corresponds to (frame/time, height, width)
|
| 158 |
+
scale_tensor = torch.tensor(
|
| 159 |
+
[scale_factors.time, scale_factors.height, scale_factors.width],
|
| 160 |
+
device=latent_coords.device,
|
| 161 |
+
).view(*broadcast_shape)
|
| 162 |
+
|
| 163 |
+
# Apply per-axis scaling to convert latent bounds into pixel-space coordinates.
|
| 164 |
+
pixel_coords = latent_coords * scale_tensor
|
| 165 |
+
|
| 166 |
+
if causal_fix:
|
| 167 |
+
# VAE temporal stride for the very first frame is 1 instead of `scale_factors.time`.
|
| 168 |
+
# Shift and clamp to keep the first-frame timestamps causal and non-negative.
|
| 169 |
+
pixel_coords[:, 0, ...] = (pixel_coords[:, 0, ...] + 1 - scale_factors.time).clamp(min=0)
|
| 170 |
+
|
| 171 |
+
return pixel_coords
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
class AudioPatchifier(Patchifier):
|
| 175 |
+
def __init__(
|
| 176 |
+
self,
|
| 177 |
+
patch_size: int,
|
| 178 |
+
sample_rate: int = 16000,
|
| 179 |
+
hop_length: int = 160,
|
| 180 |
+
audio_latent_downsample_factor: int = 4,
|
| 181 |
+
is_causal: bool = True,
|
| 182 |
+
shift: int = 0,
|
| 183 |
+
):
|
| 184 |
+
"""
|
| 185 |
+
Patchifier tailored for spectrogram/audio latents.
|
| 186 |
+
Args:
|
| 187 |
+
patch_size: Number of mel bins combined into a single patch. This
|
| 188 |
+
controls the resolution along the frequency axis.
|
| 189 |
+
sample_rate: Original waveform sampling rate. Used to map latent
|
| 190 |
+
indices back to seconds so downstream consumers can align audio
|
| 191 |
+
and video cues.
|
| 192 |
+
hop_length: Window hop length used for the spectrogram. Determines
|
| 193 |
+
how many real-time samples separate two consecutive latent frames.
|
| 194 |
+
audio_latent_downsample_factor: Ratio between spectrogram frames and
|
| 195 |
+
latent frames; compensates for additional downsampling inside the
|
| 196 |
+
VAE encoder.
|
| 197 |
+
is_causal: When True, timing is shifted to account for causal
|
| 198 |
+
receptive fields so timestamps do not peek into the future.
|
| 199 |
+
shift: Integer offset applied to the latent indices. Enables
|
| 200 |
+
constructing overlapping windows from the same latent sequence.
|
| 201 |
+
"""
|
| 202 |
+
self.hop_length = hop_length
|
| 203 |
+
self.sample_rate = sample_rate
|
| 204 |
+
self.audio_latent_downsample_factor = audio_latent_downsample_factor
|
| 205 |
+
self.is_causal = is_causal
|
| 206 |
+
self.shift = shift
|
| 207 |
+
self._patch_size = (1, patch_size, patch_size)
|
| 208 |
+
|
| 209 |
+
@property
|
| 210 |
+
def patch_size(self) -> Tuple[int, int, int]:
|
| 211 |
+
return self._patch_size
|
| 212 |
+
|
| 213 |
+
def get_token_count(self, tgt_shape: AudioLatentShape) -> int:
|
| 214 |
+
return tgt_shape.frames
|
| 215 |
+
|
| 216 |
+
def _get_audio_latent_time_in_sec(
|
| 217 |
+
self,
|
| 218 |
+
start_latent: int,
|
| 219 |
+
end_latent: int,
|
| 220 |
+
dtype: torch.dtype,
|
| 221 |
+
device: Optional[torch.device] = None,
|
| 222 |
+
) -> torch.Tensor:
|
| 223 |
+
"""
|
| 224 |
+
Converts latent indices into real-time seconds while honoring causal
|
| 225 |
+
offsets and the configured hop length.
|
| 226 |
+
Args:
|
| 227 |
+
start_latent: Inclusive start index inside the latent sequence. This
|
| 228 |
+
sets the first timestamp returned.
|
| 229 |
+
end_latent: Exclusive end index. Determines how many timestamps get
|
| 230 |
+
generated.
|
| 231 |
+
dtype: Floating-point dtype used for the returned tensor, allowing
|
| 232 |
+
callers to control precision.
|
| 233 |
+
device: Target device for the timestamp tensor. When omitted the
|
| 234 |
+
computation occurs on CPU to avoid surprising GPU allocations.
|
| 235 |
+
"""
|
| 236 |
+
if device is None:
|
| 237 |
+
device = torch.device("cpu")
|
| 238 |
+
|
| 239 |
+
audio_latent_frame = torch.arange(start_latent, end_latent, dtype=dtype, device=device)
|
| 240 |
+
|
| 241 |
+
audio_mel_frame = audio_latent_frame * self.audio_latent_downsample_factor
|
| 242 |
+
|
| 243 |
+
if self.is_causal:
|
| 244 |
+
# Frame offset for causal alignment.
|
| 245 |
+
# The "+1" ensures the timestamp corresponds to the first sample that is fully available.
|
| 246 |
+
causal_offset = 1
|
| 247 |
+
audio_mel_frame = (audio_mel_frame + causal_offset - self.audio_latent_downsample_factor).clip(min=0)
|
| 248 |
+
|
| 249 |
+
return audio_mel_frame * self.hop_length / self.sample_rate
|
| 250 |
+
|
| 251 |
+
def _compute_audio_timings(
|
| 252 |
+
self,
|
| 253 |
+
batch_size: int,
|
| 254 |
+
num_steps: int,
|
| 255 |
+
device: Optional[torch.device] = None,
|
| 256 |
+
) -> torch.Tensor:
|
| 257 |
+
"""
|
| 258 |
+
Builds a `(B, 1, T, 2)` tensor containing timestamps for each latent frame.
|
| 259 |
+
This helper method underpins `get_patch_grid_bounds` for the audio patchifier.
|
| 260 |
+
Args:
|
| 261 |
+
batch_size: Number of sequences to broadcast the timings over.
|
| 262 |
+
num_steps: Number of latent frames (time steps) to convert into timestamps.
|
| 263 |
+
device: Device on which the resulting tensor should reside.
|
| 264 |
+
"""
|
| 265 |
+
resolved_device = device
|
| 266 |
+
if resolved_device is None:
|
| 267 |
+
resolved_device = torch.device("cpu")
|
| 268 |
+
|
| 269 |
+
start_timings = self._get_audio_latent_time_in_sec(
|
| 270 |
+
self.shift,
|
| 271 |
+
num_steps + self.shift,
|
| 272 |
+
torch.float32,
|
| 273 |
+
resolved_device,
|
| 274 |
+
)
|
| 275 |
+
start_timings = start_timings.unsqueeze(0).expand(batch_size, -1).unsqueeze(1)
|
| 276 |
+
|
| 277 |
+
end_timings = self._get_audio_latent_time_in_sec(
|
| 278 |
+
self.shift + 1,
|
| 279 |
+
num_steps + self.shift + 1,
|
| 280 |
+
torch.float32,
|
| 281 |
+
resolved_device,
|
| 282 |
+
)
|
| 283 |
+
end_timings = end_timings.unsqueeze(0).expand(batch_size, -1).unsqueeze(1)
|
| 284 |
+
|
| 285 |
+
return torch.stack([start_timings, end_timings], dim=-1)
|
| 286 |
+
|
| 287 |
+
def patchify(
|
| 288 |
+
self,
|
| 289 |
+
audio_latents: torch.Tensor,
|
| 290 |
+
) -> torch.Tensor:
|
| 291 |
+
"""
|
| 292 |
+
Flattens the audio latent tensor along time. Use `get_patch_grid_bounds`
|
| 293 |
+
to derive timestamps for each latent frame based on the configured hop
|
| 294 |
+
length and downsampling.
|
| 295 |
+
Args:
|
| 296 |
+
audio_latents: Latent tensor to patchify.
|
| 297 |
+
Returns:
|
| 298 |
+
Flattened patch tokens tensor. Use `get_patch_grid_bounds` to compute the
|
| 299 |
+
corresponding timing metadata when needed.
|
| 300 |
+
"""
|
| 301 |
+
audio_latents = einops.rearrange(
|
| 302 |
+
audio_latents,
|
| 303 |
+
"b c t f -> b t (c f)",
|
| 304 |
+
)
|
| 305 |
+
|
| 306 |
+
return audio_latents
|
| 307 |
+
|
| 308 |
+
def unpatchify(
|
| 309 |
+
self,
|
| 310 |
+
audio_latents: torch.Tensor,
|
| 311 |
+
output_shape: AudioLatentShape,
|
| 312 |
+
) -> torch.Tensor:
|
| 313 |
+
"""
|
| 314 |
+
Restores the `(B, C, T, F)` spectrogram tensor from flattened patches.
|
| 315 |
+
Use `get_patch_grid_bounds` to recompute the timestamps that describe each
|
| 316 |
+
frame's position in real time.
|
| 317 |
+
Args:
|
| 318 |
+
audio_latents: Latent tensor to unpatchify.
|
| 319 |
+
output_shape: Shape of the unpatched output tensor.
|
| 320 |
+
Returns:
|
| 321 |
+
Unpatched latent tensor. Use `get_patch_grid_bounds` to compute the timing
|
| 322 |
+
metadata associated with the restored latents.
|
| 323 |
+
"""
|
| 324 |
+
# audio_latents shape: (batch, time, freq * channels)
|
| 325 |
+
audio_latents = einops.rearrange(
|
| 326 |
+
audio_latents,
|
| 327 |
+
"b t (c f) -> b c t f",
|
| 328 |
+
c=output_shape.channels,
|
| 329 |
+
f=output_shape.mel_bins,
|
| 330 |
+
)
|
| 331 |
+
|
| 332 |
+
return audio_latents
|
| 333 |
+
|
| 334 |
+
def get_patch_grid_bounds(
|
| 335 |
+
self,
|
| 336 |
+
output_shape: AudioLatentShape | VideoLatentShape,
|
| 337 |
+
device: Optional[torch.device] = None,
|
| 338 |
+
) -> torch.Tensor:
|
| 339 |
+
"""
|
| 340 |
+
Return the temporal bounds `[inclusive start, exclusive end)` for every
|
| 341 |
+
patch emitted by `patchify`. For audio this corresponds to timestamps in
|
| 342 |
+
seconds aligned with the original spectrogram grid.
|
| 343 |
+
The returned tensor has shape `[batch_size, 1, time_steps, 2]`, where:
|
| 344 |
+
- axis 1 (size 1) represents the temporal dimension
|
| 345 |
+
- axis 3 (size 2) stores the `[start, end)` timestamps per patch
|
| 346 |
+
Args:
|
| 347 |
+
output_shape: Audio grid specification describing the number of time steps.
|
| 348 |
+
device: Target device for the returned tensor.
|
| 349 |
+
"""
|
| 350 |
+
if not isinstance(output_shape, AudioLatentShape):
|
| 351 |
+
raise ValueError("AudioPatchifier expects AudioLatentShape when computing coordinates")
|
| 352 |
+
|
| 353 |
+
return self._compute_audio_timings(output_shape.batch, output_shape.frames, device)
|
packages/ltx-core/src/ltx_core/components/protocols.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Protocol, Tuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from ltx_core.types import AudioLatentShape, VideoLatentShape
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class Patchifier(Protocol):
|
| 9 |
+
"""
|
| 10 |
+
Protocol for patchifiers that convert latent tensors into patches and assemble them back.
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
def patchify(
|
| 14 |
+
self,
|
| 15 |
+
latents: torch.Tensor,
|
| 16 |
+
) -> torch.Tensor:
|
| 17 |
+
...
|
| 18 |
+
"""
|
| 19 |
+
Convert latent tensors into flattened patch tokens.
|
| 20 |
+
Args:
|
| 21 |
+
latents: Latent tensor to patchify.
|
| 22 |
+
Returns:
|
| 23 |
+
Flattened patch tokens tensor.
|
| 24 |
+
"""
|
| 25 |
+
|
| 26 |
+
def unpatchify(
|
| 27 |
+
self,
|
| 28 |
+
latents: torch.Tensor,
|
| 29 |
+
output_shape: AudioLatentShape | VideoLatentShape,
|
| 30 |
+
) -> torch.Tensor:
|
| 31 |
+
"""
|
| 32 |
+
Converts latent tensors between spatio-temporal formats and flattened sequence representations.
|
| 33 |
+
Args:
|
| 34 |
+
latents: Patch tokens that must be rearranged back into the latent grid constructed by `patchify`.
|
| 35 |
+
output_shape: Shape of the output tensor. Note that output_shape is either AudioLatentShape or
|
| 36 |
+
VideoLatentShape.
|
| 37 |
+
Returns:
|
| 38 |
+
Dense latent tensor restored from the flattened representation.
|
| 39 |
+
"""
|
| 40 |
+
|
| 41 |
+
@property
|
| 42 |
+
def patch_size(self) -> Tuple[int, int, int]:
|
| 43 |
+
...
|
| 44 |
+
"""
|
| 45 |
+
Returns the patch size as a tuple of (temporal, height, width) dimensions
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def get_patch_grid_bounds(
|
| 49 |
+
self,
|
| 50 |
+
output_shape: AudioLatentShape | VideoLatentShape,
|
| 51 |
+
device: torch.device | None = None,
|
| 52 |
+
) -> torch.Tensor:
|
| 53 |
+
...
|
| 54 |
+
"""
|
| 55 |
+
Compute metadata describing where each latent patch resides within the
|
| 56 |
+
grid specified by `output_shape`.
|
| 57 |
+
Args:
|
| 58 |
+
output_shape: Target grid layout for the patches.
|
| 59 |
+
device: Target device for the returned tensor.
|
| 60 |
+
Returns:
|
| 61 |
+
Tensor containing patch coordinate metadata such as spatial or temporal intervals.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
class SchedulerProtocol(Protocol):
|
| 66 |
+
"""
|
| 67 |
+
Protocol for schedulers that provide a sigmas schedule tensor for a
|
| 68 |
+
given number of steps. Device is cpu.
|
| 69 |
+
"""
|
| 70 |
+
|
| 71 |
+
def execute(self, steps: int, **kwargs) -> torch.FloatTensor: ...
|
| 72 |
+
|
| 73 |
+
|
| 74 |
+
class GuiderProtocol(Protocol):
|
| 75 |
+
"""
|
| 76 |
+
Protocol for guiders that compute a delta tensor given conditioning inputs.
|
| 77 |
+
The returned delta should be added to the conditional output (cond), enabling
|
| 78 |
+
multiple guiders to be chained together by accumulating their deltas.
|
| 79 |
+
"""
|
| 80 |
+
|
| 81 |
+
scale: float
|
| 82 |
+
|
| 83 |
+
def delta(self, cond: torch.Tensor, uncond: torch.Tensor) -> torch.Tensor: ...
|
| 84 |
+
|
| 85 |
+
def enabled(self) -> bool:
|
| 86 |
+
"""
|
| 87 |
+
Returns whether the corresponding perturbation is enabled. E.g. for CFG, this should return False if the scale
|
| 88 |
+
is 1.0.
|
| 89 |
+
"""
|
| 90 |
+
...
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
class DiffusionStepProtocol(Protocol):
|
| 94 |
+
"""
|
| 95 |
+
Protocol for diffusion steps that provide a next sample tensor for a given current sample tensor,
|
| 96 |
+
current denoised sample tensor, and sigmas tensor.
|
| 97 |
+
"""
|
| 98 |
+
|
| 99 |
+
def step(
|
| 100 |
+
self, sample: torch.Tensor, denoised_sample: torch.Tensor, sigmas: torch.Tensor, step_index: int, **kwargs
|
| 101 |
+
) -> torch.Tensor: ...
|
packages/ltx-core/src/ltx_core/components/schedulers.py
ADDED
|
@@ -0,0 +1,130 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import math
|
| 2 |
+
from functools import lru_cache
|
| 3 |
+
|
| 4 |
+
import numpy
|
| 5 |
+
import scipy
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from ltx_core.components.protocols import SchedulerProtocol
|
| 9 |
+
|
| 10 |
+
BASE_SHIFT_ANCHOR = 1024
|
| 11 |
+
MAX_SHIFT_ANCHOR = 4096
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class LTX2Scheduler(SchedulerProtocol):
|
| 15 |
+
"""
|
| 16 |
+
Default scheduler for LTX-2 diffusion sampling.
|
| 17 |
+
Generates a sigma schedule with token-count-dependent shifting and optional
|
| 18 |
+
stretching to a terminal value.
|
| 19 |
+
"""
|
| 20 |
+
|
| 21 |
+
def execute(
|
| 22 |
+
self,
|
| 23 |
+
steps: int,
|
| 24 |
+
latent: torch.Tensor | None = None,
|
| 25 |
+
max_shift: float = 2.05,
|
| 26 |
+
base_shift: float = 0.95,
|
| 27 |
+
stretch: bool = True,
|
| 28 |
+
terminal: float = 0.1,
|
| 29 |
+
default_number_of_tokens: int = MAX_SHIFT_ANCHOR,
|
| 30 |
+
**_kwargs,
|
| 31 |
+
) -> torch.FloatTensor:
|
| 32 |
+
tokens = math.prod(latent.shape[2:]) if latent is not None else default_number_of_tokens
|
| 33 |
+
sigmas = torch.linspace(1.0, 0.0, steps + 1)
|
| 34 |
+
|
| 35 |
+
x1 = BASE_SHIFT_ANCHOR
|
| 36 |
+
x2 = MAX_SHIFT_ANCHOR
|
| 37 |
+
mm = (max_shift - base_shift) / (x2 - x1)
|
| 38 |
+
b = base_shift - mm * x1
|
| 39 |
+
sigma_shift = (tokens) * mm + b
|
| 40 |
+
|
| 41 |
+
power = 1
|
| 42 |
+
sigmas = torch.where(
|
| 43 |
+
sigmas != 0,
|
| 44 |
+
math.exp(sigma_shift) / (math.exp(sigma_shift) + (1 / sigmas - 1) ** power),
|
| 45 |
+
0,
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# Stretch sigmas so that its final value matches the given terminal value.
|
| 49 |
+
if stretch:
|
| 50 |
+
non_zero_mask = sigmas != 0
|
| 51 |
+
non_zero_sigmas = sigmas[non_zero_mask]
|
| 52 |
+
one_minus_z = 1.0 - non_zero_sigmas
|
| 53 |
+
scale_factor = one_minus_z[-1] / (1.0 - terminal)
|
| 54 |
+
stretched = 1.0 - (one_minus_z / scale_factor)
|
| 55 |
+
sigmas[non_zero_mask] = stretched
|
| 56 |
+
|
| 57 |
+
return sigmas.to(torch.float32)
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
class LinearQuadraticScheduler(SchedulerProtocol):
|
| 61 |
+
"""
|
| 62 |
+
Scheduler with linear steps followed by quadratic steps.
|
| 63 |
+
Produces a sigma schedule that transitions linearly up to a threshold,
|
| 64 |
+
then follows a quadratic curve for the remaining steps.
|
| 65 |
+
"""
|
| 66 |
+
|
| 67 |
+
def execute(
|
| 68 |
+
self, steps: int, threshold_noise: float = 0.025, linear_steps: int | None = None, **_kwargs
|
| 69 |
+
) -> torch.FloatTensor:
|
| 70 |
+
if steps == 1:
|
| 71 |
+
return torch.FloatTensor([1.0, 0.0])
|
| 72 |
+
|
| 73 |
+
if linear_steps is None:
|
| 74 |
+
linear_steps = steps // 2
|
| 75 |
+
linear_sigma_schedule = [i * threshold_noise / linear_steps for i in range(linear_steps)]
|
| 76 |
+
threshold_noise_step_diff = linear_steps - threshold_noise * steps
|
| 77 |
+
quadratic_steps = steps - linear_steps
|
| 78 |
+
quadratic_sigma_schedule = []
|
| 79 |
+
if quadratic_steps > 0:
|
| 80 |
+
quadratic_coef = threshold_noise_step_diff / (linear_steps * quadratic_steps**2)
|
| 81 |
+
linear_coef = threshold_noise / linear_steps - 2 * threshold_noise_step_diff / (quadratic_steps**2)
|
| 82 |
+
const = quadratic_coef * (linear_steps**2)
|
| 83 |
+
quadratic_sigma_schedule = [
|
| 84 |
+
quadratic_coef * (i**2) + linear_coef * i + const for i in range(linear_steps, steps)
|
| 85 |
+
]
|
| 86 |
+
sigma_schedule = linear_sigma_schedule + quadratic_sigma_schedule + [1.0]
|
| 87 |
+
sigma_schedule = [1.0 - x for x in sigma_schedule]
|
| 88 |
+
return torch.FloatTensor(sigma_schedule)
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class BetaScheduler(SchedulerProtocol):
|
| 92 |
+
"""
|
| 93 |
+
Scheduler using a beta distribution to sample timesteps.
|
| 94 |
+
Based on: https://arxiv.org/abs/2407.12173
|
| 95 |
+
"""
|
| 96 |
+
|
| 97 |
+
shift = 2.37
|
| 98 |
+
timesteps_length = 10000
|
| 99 |
+
|
| 100 |
+
def execute(self, steps: int, alpha: float = 0.6, beta: float = 0.6) -> torch.FloatTensor:
|
| 101 |
+
"""
|
| 102 |
+
Execute the beta scheduler.
|
| 103 |
+
Args:
|
| 104 |
+
steps: The number of steps to execute the scheduler for.
|
| 105 |
+
alpha: The alpha parameter for the beta distribution.
|
| 106 |
+
beta: The beta parameter for the beta distribution.
|
| 107 |
+
Warnings:
|
| 108 |
+
The number of steps within `sigmas` theoretically might be less than `steps+1`,
|
| 109 |
+
because of the deduplication of the identical timesteps
|
| 110 |
+
Returns:
|
| 111 |
+
A tensor of sigmas.
|
| 112 |
+
"""
|
| 113 |
+
model_sampling_sigmas = _precalculate_model_sampling_sigmas(self.shift, self.timesteps_length)
|
| 114 |
+
total_timesteps = len(model_sampling_sigmas) - 1
|
| 115 |
+
ts = 1 - numpy.linspace(0, 1, steps, endpoint=False)
|
| 116 |
+
ts = numpy.rint(scipy.stats.beta.ppf(ts, alpha, beta) * total_timesteps).tolist()
|
| 117 |
+
ts = list(dict.fromkeys(ts))
|
| 118 |
+
|
| 119 |
+
sigmas = [float(model_sampling_sigmas[int(t)]) for t in ts] + [0.0]
|
| 120 |
+
return torch.FloatTensor(sigmas)
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
@lru_cache(maxsize=5)
|
| 124 |
+
def _precalculate_model_sampling_sigmas(shift: float, timesteps_length: int) -> torch.Tensor:
|
| 125 |
+
timesteps = torch.arange(1, timesteps_length + 1, 1) / timesteps_length
|
| 126 |
+
return torch.Tensor([flux_time_shift(shift, 1.0, t) for t in timesteps])
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def flux_time_shift(mu: float, sigma: float, t: float) -> float:
|
| 130 |
+
return math.exp(mu) / (math.exp(mu) + (1 / t - 1) ** sigma)
|
packages/ltx-core/src/ltx_core/conditioning/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conditioning utilities: latent state, tools, and conditioning types."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.conditioning.exceptions import ConditioningError
|
| 4 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 5 |
+
from ltx_core.conditioning.types import (
|
| 6 |
+
AudioConditionByReferenceLatent,
|
| 7 |
+
ConditioningItemAttentionStrengthWrapper,
|
| 8 |
+
VideoConditionByKeyframeIndex,
|
| 9 |
+
VideoConditionByLatentIndex,
|
| 10 |
+
VideoConditionByReferenceLatent,
|
| 11 |
+
)
|
| 12 |
+
|
| 13 |
+
__all__ = [
|
| 14 |
+
"AudioConditionByReferenceLatent",
|
| 15 |
+
"ConditioningError",
|
| 16 |
+
"ConditioningItem",
|
| 17 |
+
"ConditioningItemAttentionStrengthWrapper",
|
| 18 |
+
"VideoConditionByKeyframeIndex",
|
| 19 |
+
"VideoConditionByLatentIndex",
|
| 20 |
+
"VideoConditionByReferenceLatent",
|
| 21 |
+
]
|
packages/ltx-core/src/ltx_core/conditioning/exceptions.py
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
class ConditioningError(Exception):
|
| 2 |
+
"""
|
| 3 |
+
Class for conditioning-related errors.
|
| 4 |
+
"""
|
packages/ltx-core/src/ltx_core/conditioning/item.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Protocol
|
| 2 |
+
|
| 3 |
+
from ltx_core.tools import LatentTools
|
| 4 |
+
from ltx_core.types import LatentState
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
class ConditioningItem(Protocol):
|
| 8 |
+
"""Protocol for conditioning items that modify latent state during diffusion."""
|
| 9 |
+
|
| 10 |
+
def apply_to(self, latent_state: LatentState, latent_tools: LatentTools) -> LatentState:
|
| 11 |
+
"""
|
| 12 |
+
Apply the conditioning to the latent state.
|
| 13 |
+
Args:
|
| 14 |
+
latent_state: The latent state to apply the conditioning to. This is state always patchified.
|
| 15 |
+
Returns:
|
| 16 |
+
The latent state after the conditioning has been applied.
|
| 17 |
+
IMPORTANT: If the conditioning needs to add extra tokens to the latent, it should add them to the end of the
|
| 18 |
+
latent.
|
| 19 |
+
"""
|
| 20 |
+
...
|
packages/ltx-core/src/ltx_core/conditioning/mask_utils.py
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Utilities for building 2D self-attention masks for conditioning items."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import TYPE_CHECKING
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
|
| 9 |
+
if TYPE_CHECKING:
|
| 10 |
+
from ltx_core.types import LatentState
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def resolve_cross_mask(
|
| 14 |
+
attention_mask: float | int | torch.Tensor,
|
| 15 |
+
num_new_tokens: int,
|
| 16 |
+
batch_size: int,
|
| 17 |
+
device: torch.device,
|
| 18 |
+
dtype: torch.dtype,
|
| 19 |
+
) -> torch.Tensor:
|
| 20 |
+
"""Convert an attention_mask (scalar or tensor) to a (B, M) cross_mask tensor.
|
| 21 |
+
Args:
|
| 22 |
+
attention_mask: Scalar value applied uniformly, 1D tensor of shape (M,)
|
| 23 |
+
broadcast across batch, or 2D tensor of shape (B, M).
|
| 24 |
+
num_new_tokens: Number of new conditioning tokens M.
|
| 25 |
+
batch_size: Batch size B.
|
| 26 |
+
device: Device for the output tensor.
|
| 27 |
+
dtype: Data type for the output tensor.
|
| 28 |
+
Returns:
|
| 29 |
+
Cross-mask tensor of shape (B, M).
|
| 30 |
+
"""
|
| 31 |
+
if isinstance(attention_mask, (int, float)):
|
| 32 |
+
return torch.full(
|
| 33 |
+
(batch_size, num_new_tokens),
|
| 34 |
+
fill_value=float(attention_mask),
|
| 35 |
+
device=device,
|
| 36 |
+
dtype=dtype,
|
| 37 |
+
)
|
| 38 |
+
mask = attention_mask.to(device=device, dtype=dtype)
|
| 39 |
+
|
| 40 |
+
# Handle scalar (0-D) tensor like a Python scalar.
|
| 41 |
+
if mask.dim() == 0:
|
| 42 |
+
return torch.full(
|
| 43 |
+
(batch_size, num_new_tokens),
|
| 44 |
+
fill_value=float(mask.item()),
|
| 45 |
+
device=device,
|
| 46 |
+
dtype=dtype,
|
| 47 |
+
)
|
| 48 |
+
|
| 49 |
+
if mask.dim() == 1:
|
| 50 |
+
if mask.shape[0] != num_new_tokens:
|
| 51 |
+
raise ValueError(
|
| 52 |
+
f"1-D attention_mask length must equal num_new_tokens ({num_new_tokens}), got shape {tuple(mask.shape)}"
|
| 53 |
+
)
|
| 54 |
+
mask = mask.unsqueeze(0).expand(batch_size, -1)
|
| 55 |
+
elif mask.dim() == 2:
|
| 56 |
+
b, m = mask.shape
|
| 57 |
+
if m != num_new_tokens:
|
| 58 |
+
raise ValueError(
|
| 59 |
+
f"2-D attention_mask second dimension must equal num_new_tokens ({num_new_tokens}), "
|
| 60 |
+
f"got shape {tuple(mask.shape)}"
|
| 61 |
+
)
|
| 62 |
+
if b not in (batch_size, 1):
|
| 63 |
+
raise ValueError(
|
| 64 |
+
f"2-D attention_mask batch dimension must equal batch_size ({batch_size}) or 1, "
|
| 65 |
+
f"got shape {tuple(mask.shape)}"
|
| 66 |
+
)
|
| 67 |
+
if b == 1 and batch_size > 1:
|
| 68 |
+
mask = mask.expand(batch_size, -1)
|
| 69 |
+
else:
|
| 70 |
+
raise ValueError(
|
| 71 |
+
f"attention_mask tensor must be 0-D, 1-D, or 2-D, got {mask.dim()}-D with shape {tuple(mask.shape)}"
|
| 72 |
+
)
|
| 73 |
+
return mask
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
def update_attention_mask(
|
| 77 |
+
latent_state: LatentState,
|
| 78 |
+
attention_mask: float | torch.Tensor | None,
|
| 79 |
+
num_noisy_tokens: int,
|
| 80 |
+
num_new_tokens: int,
|
| 81 |
+
batch_size: int,
|
| 82 |
+
device: torch.device,
|
| 83 |
+
dtype: torch.dtype,
|
| 84 |
+
) -> torch.Tensor | None:
|
| 85 |
+
"""Build or update the self-attention mask for newly appended conditioning tokens.
|
| 86 |
+
If *attention_mask* is ``None`` and no existing mask is present, returns
|
| 87 |
+
``None``. If *attention_mask* is ``None`` but an existing mask is present,
|
| 88 |
+
the mask is expanded with full attention (1s) for the new tokens so that
|
| 89 |
+
its dimensions stay consistent with the growing latent sequence. Otherwise,
|
| 90 |
+
resolves *attention_mask* to a per-token cross-mask and expands the 2-D
|
| 91 |
+
attention mask via :func:`build_attention_mask`.
|
| 92 |
+
Args:
|
| 93 |
+
latent_state: Current latent state (provides the existing mask and total
|
| 94 |
+
existing-token count).
|
| 95 |
+
attention_mask: Per-token attention weight. Scalar, 1-D ``(M,)``, 2-D
|
| 96 |
+
``(B, M)`` tensor, or ``None`` (no-op).
|
| 97 |
+
num_noisy_tokens: Number of original noisy tokens (from
|
| 98 |
+
``latent_tools.target_shape.token_count()``).
|
| 99 |
+
num_new_tokens: Number of new conditioning tokens being appended.
|
| 100 |
+
batch_size: Batch size.
|
| 101 |
+
device: Device for the output tensor.
|
| 102 |
+
dtype: Data type for the output tensor.
|
| 103 |
+
Returns:
|
| 104 |
+
Updated attention mask of shape ``(B, N+M, N+M)``, or ``None`` if no
|
| 105 |
+
masking is needed.
|
| 106 |
+
"""
|
| 107 |
+
if attention_mask is None:
|
| 108 |
+
if latent_state.attention_mask is None:
|
| 109 |
+
return None
|
| 110 |
+
# Existing mask present but no new mask requested: pad with 1s (full
|
| 111 |
+
# attention) so the mask dimensions stay consistent with the growing
|
| 112 |
+
# latent sequence.
|
| 113 |
+
cross_mask = torch.ones(batch_size, num_new_tokens, device=device, dtype=dtype)
|
| 114 |
+
return build_attention_mask(
|
| 115 |
+
existing_mask=latent_state.attention_mask,
|
| 116 |
+
num_noisy_tokens=num_noisy_tokens,
|
| 117 |
+
num_new_tokens=num_new_tokens,
|
| 118 |
+
num_existing_tokens=latent_state.latent.shape[1],
|
| 119 |
+
cross_mask=cross_mask,
|
| 120 |
+
device=device,
|
| 121 |
+
dtype=dtype,
|
| 122 |
+
)
|
| 123 |
+
|
| 124 |
+
cross_mask = resolve_cross_mask(attention_mask, num_new_tokens, batch_size, device, dtype)
|
| 125 |
+
return build_attention_mask(
|
| 126 |
+
existing_mask=latent_state.attention_mask,
|
| 127 |
+
num_noisy_tokens=num_noisy_tokens,
|
| 128 |
+
num_new_tokens=num_new_tokens,
|
| 129 |
+
num_existing_tokens=latent_state.latent.shape[1],
|
| 130 |
+
cross_mask=cross_mask,
|
| 131 |
+
device=device,
|
| 132 |
+
dtype=dtype,
|
| 133 |
+
)
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def build_attention_mask(
|
| 137 |
+
existing_mask: torch.Tensor | None,
|
| 138 |
+
num_noisy_tokens: int,
|
| 139 |
+
num_new_tokens: int,
|
| 140 |
+
num_existing_tokens: int,
|
| 141 |
+
cross_mask: torch.Tensor,
|
| 142 |
+
device: torch.device,
|
| 143 |
+
dtype: torch.dtype,
|
| 144 |
+
) -> torch.Tensor:
|
| 145 |
+
"""
|
| 146 |
+
Expand the attention mask to include newly appended conditioning tokens.
|
| 147 |
+
Each conditioning item appends M new reference tokens to the sequence. This function
|
| 148 |
+
builds a (B, N+M, N+M) attention mask with the following block structure:
|
| 149 |
+
noisy prev_ref new_ref
|
| 150 |
+
(N_noisy) (N-N_noisy) (M)
|
| 151 |
+
┌───────────┬───────────┬───────────┐
|
| 152 |
+
noisy │ │ │ │
|
| 153 |
+
(N_noisy) │ existing │ existing │ cross │
|
| 154 |
+
│ │ │ │
|
| 155 |
+
├───────────┼───────────┼───────────┤
|
| 156 |
+
prev_ref │ │ │ │
|
| 157 |
+
(N-N_noisy)│ existing │ existing │ 0 │
|
| 158 |
+
│ │ │ │
|
| 159 |
+
├───────────┼───────────┼───────────┤
|
| 160 |
+
new_ref │ │ │ │
|
| 161 |
+
(M) │ cross │ 0 │ 1 │
|
| 162 |
+
│ │ │ │
|
| 163 |
+
└───────────┴───────────┴───────────┘
|
| 164 |
+
Where:
|
| 165 |
+
- **existing**: preserved from the previous mask (or 1.0 if first conditioning)
|
| 166 |
+
- **cross**: values from *cross_mask* (shape B, M), in [0, 1]
|
| 167 |
+
- **0**: no attention between different reference groups
|
| 168 |
+
Args:
|
| 169 |
+
existing_mask: Current attention mask of shape (B, N, N), or None if no mask exists yet.
|
| 170 |
+
When None, the top-left NxN block is filled with 1s (full attention between all
|
| 171 |
+
existing tokens including any prior reference tokens that had no mask).
|
| 172 |
+
num_noisy_tokens: Number of original noisy tokens (always at positions [0:num_noisy_tokens]).
|
| 173 |
+
num_new_tokens: Number of new conditioning tokens M being appended.
|
| 174 |
+
num_existing_tokens: Total number of current tokens N (noisy + any prior conditioning tokens).
|
| 175 |
+
cross_mask: Per-token attention weight of shape (B, M) controlling attention between
|
| 176 |
+
new reference tokens and noisy tokens. Values in [0, 1].
|
| 177 |
+
device: Device for the output tensor.
|
| 178 |
+
dtype: Data type for the output tensor.
|
| 179 |
+
Returns:
|
| 180 |
+
Attention mask of shape (B, N+M, N+M) with values in [0, 1].
|
| 181 |
+
"""
|
| 182 |
+
batch_size = cross_mask.shape[0]
|
| 183 |
+
total = num_existing_tokens + num_new_tokens
|
| 184 |
+
|
| 185 |
+
# Start with zeros
|
| 186 |
+
mask = torch.zeros((batch_size, total, total), device=device, dtype=dtype)
|
| 187 |
+
|
| 188 |
+
# Top-left: preserve existing mask or fill with 1s for noisy tokens
|
| 189 |
+
if existing_mask is not None:
|
| 190 |
+
mask[:, :num_existing_tokens, :num_existing_tokens] = existing_mask
|
| 191 |
+
else:
|
| 192 |
+
mask[:, :num_existing_tokens, :num_existing_tokens] = 1.0
|
| 193 |
+
|
| 194 |
+
# Bottom-right: new reference tokens fully attend to themselves
|
| 195 |
+
mask[:, num_existing_tokens:, num_existing_tokens:] = 1.0
|
| 196 |
+
|
| 197 |
+
# Cross-attention between noisy tokens and new reference tokens
|
| 198 |
+
# cross_mask shape: (B, M) -> broadcast to (B, N_noisy, M) and (B, M, N_noisy)
|
| 199 |
+
|
| 200 |
+
# Noisy tokens attending to new reference tokens: [0:N_noisy, N:N+M]
|
| 201 |
+
# Each column j in this block gets cross_mask[:, j]
|
| 202 |
+
mask[:, :num_noisy_tokens, num_existing_tokens:] = cross_mask.unsqueeze(1)
|
| 203 |
+
|
| 204 |
+
# New reference tokens attending to noisy tokens: [N:N+M, 0:N_noisy]
|
| 205 |
+
# Each row i in this block gets cross_mask[:, i]
|
| 206 |
+
mask[:, num_existing_tokens:, :num_noisy_tokens] = cross_mask.unsqueeze(2)
|
| 207 |
+
|
| 208 |
+
# [N_noisy:N, N:N+M] and [N:N+M, N_noisy:N] remain 0 (no cross-ref attention)
|
| 209 |
+
|
| 210 |
+
return mask
|
packages/ltx-core/src/ltx_core/conditioning/types/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Conditioning type implementations."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.conditioning.types.attention_strength_wrapper import ConditioningItemAttentionStrengthWrapper
|
| 4 |
+
from ltx_core.conditioning.types.keyframe_cond import VideoConditionByKeyframeIndex
|
| 5 |
+
from ltx_core.conditioning.types.latent_cond import VideoConditionByLatentIndex
|
| 6 |
+
from ltx_core.conditioning.types.reference_audio_cond import AudioConditionByReferenceLatent
|
| 7 |
+
from ltx_core.conditioning.types.reference_video_cond import VideoConditionByReferenceLatent
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"AudioConditionByReferenceLatent",
|
| 11 |
+
"ConditioningItemAttentionStrengthWrapper",
|
| 12 |
+
"VideoConditionByKeyframeIndex",
|
| 13 |
+
"VideoConditionByLatentIndex",
|
| 14 |
+
"VideoConditionByReferenceLatent",
|
| 15 |
+
]
|
packages/ltx-core/src/ltx_core/conditioning/types/attention_strength_wrapper.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Wrapper conditioning item that adds attention masking to any inner conditioning."""
|
| 2 |
+
|
| 3 |
+
from dataclasses import replace
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 8 |
+
from ltx_core.conditioning.mask_utils import update_attention_mask
|
| 9 |
+
from ltx_core.tools import LatentTools
|
| 10 |
+
from ltx_core.types import LatentState
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class ConditioningItemAttentionStrengthWrapper(ConditioningItem):
|
| 14 |
+
"""Wraps a conditioning item to add an attention mask for its tokens.
|
| 15 |
+
Separates the *attention-masking* concern from the underlying conditioning
|
| 16 |
+
logic (token layout, positional encoding, denoise strength). The inner
|
| 17 |
+
conditioning item appends tokens to the latent sequence as usual, and this
|
| 18 |
+
wrapper then builds or updates the self-attention mask so that the newly
|
| 19 |
+
added tokens interact with the noisy tokens according to *attention_mask*.
|
| 20 |
+
Args:
|
| 21 |
+
conditioning: Any conditioning item that appends tokens to the latent.
|
| 22 |
+
attention_mask: Per-token attention weight controlling how strongly the
|
| 23 |
+
new conditioning tokens attend to/from noisy tokens. Can be a
|
| 24 |
+
scalar (float) applied uniformly, or a tensor of shape ``(B, M)``
|
| 25 |
+
for spatial control, where ``M = F * H * W`` is the number of
|
| 26 |
+
patchified conditioning tokens. Values in ``[0, 1]``.
|
| 27 |
+
Example::
|
| 28 |
+
cond = ConditioningItemAttentionStrengthWrapper(
|
| 29 |
+
VideoConditionByReferenceLatent(latent=ref, strength=1.0),
|
| 30 |
+
attention_mask=0.5,
|
| 31 |
+
)
|
| 32 |
+
state = cond.apply_to(latent_state, latent_tools)
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
def __init__(
|
| 36 |
+
self,
|
| 37 |
+
conditioning: ConditioningItem,
|
| 38 |
+
attention_mask: float | torch.Tensor,
|
| 39 |
+
):
|
| 40 |
+
self.conditioning = conditioning
|
| 41 |
+
self.attention_mask = attention_mask
|
| 42 |
+
|
| 43 |
+
def apply_to(
|
| 44 |
+
self,
|
| 45 |
+
latent_state: LatentState,
|
| 46 |
+
latent_tools: LatentTools,
|
| 47 |
+
) -> LatentState:
|
| 48 |
+
"""Apply inner conditioning, then build the attention mask for its tokens."""
|
| 49 |
+
# Snapshot the original state for mask building
|
| 50 |
+
original_state = latent_state
|
| 51 |
+
|
| 52 |
+
# Inner conditioning appends tokens (positions, denoise mask, etc.)
|
| 53 |
+
new_state = self.conditioning.apply_to(latent_state, latent_tools)
|
| 54 |
+
|
| 55 |
+
num_new_tokens = new_state.latent.shape[1] - original_state.latent.shape[1]
|
| 56 |
+
if num_new_tokens == 0:
|
| 57 |
+
return new_state
|
| 58 |
+
|
| 59 |
+
# Build the attention mask using the *original* state as the reference
|
| 60 |
+
# so that the block structure is computed correctly.
|
| 61 |
+
new_attention_mask = update_attention_mask(
|
| 62 |
+
latent_state=original_state,
|
| 63 |
+
attention_mask=self.attention_mask,
|
| 64 |
+
num_noisy_tokens=latent_tools.target_shape.token_count(),
|
| 65 |
+
num_new_tokens=num_new_tokens,
|
| 66 |
+
batch_size=new_state.latent.shape[0],
|
| 67 |
+
device=new_state.latent.device,
|
| 68 |
+
dtype=new_state.latent.dtype,
|
| 69 |
+
)
|
| 70 |
+
|
| 71 |
+
return replace(new_state, attention_mask=new_attention_mask)
|
packages/ltx-core/src/ltx_core/conditioning/types/keyframe_cond.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from ltx_core.components.patchifiers import get_pixel_coords
|
| 4 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 5 |
+
from ltx_core.conditioning.mask_utils import update_attention_mask
|
| 6 |
+
from ltx_core.tools import VideoLatentTools
|
| 7 |
+
from ltx_core.types import LatentState, VideoLatentShape
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class VideoConditionByKeyframeIndex(ConditioningItem):
|
| 11 |
+
"""
|
| 12 |
+
Conditions video generation on keyframe latents at a specific frame index.
|
| 13 |
+
Appends keyframe tokens to the latent state with positions offset by frame_idx,
|
| 14 |
+
and sets denoise strength according to the strength parameter.
|
| 15 |
+
To add attention masking, wrap with :class:`ConditioningItemAttentionStrengthWrapper`.
|
| 16 |
+
Args:
|
| 17 |
+
keyframes: Keyframe latents [B, C, F, H, W].
|
| 18 |
+
frame_idx: Frame index offset for positional encoding.
|
| 19 |
+
strength: Conditioning strength (1.0 = clean, 0.0 = fully denoised).
|
| 20 |
+
num_pixel_frames: Number of pixel frames the keyframe latent originally encodes.
|
| 21 |
+
"""
|
| 22 |
+
|
| 23 |
+
def __init__(
|
| 24 |
+
self,
|
| 25 |
+
keyframes: torch.Tensor,
|
| 26 |
+
frame_idx: int,
|
| 27 |
+
strength: float,
|
| 28 |
+
num_pixel_frames: int = 1,
|
| 29 |
+
):
|
| 30 |
+
self.keyframes = keyframes
|
| 31 |
+
self.frame_idx = frame_idx
|
| 32 |
+
self.strength = strength
|
| 33 |
+
self.num_pixel_frames = num_pixel_frames
|
| 34 |
+
|
| 35 |
+
def apply_to(
|
| 36 |
+
self,
|
| 37 |
+
latent_state: LatentState,
|
| 38 |
+
latent_tools: VideoLatentTools,
|
| 39 |
+
) -> LatentState:
|
| 40 |
+
tokens = latent_tools.patchifier.patchify(self.keyframes)
|
| 41 |
+
latent_coords = latent_tools.patchifier.get_patch_grid_bounds(
|
| 42 |
+
output_shape=VideoLatentShape.from_torch_shape(self.keyframes.shape),
|
| 43 |
+
device=self.keyframes.device,
|
| 44 |
+
)
|
| 45 |
+
positions = get_pixel_coords(
|
| 46 |
+
latent_coords=latent_coords,
|
| 47 |
+
scale_factors=latent_tools.scale_factors,
|
| 48 |
+
causal_fix=latent_tools.causal_fix if self.frame_idx == 0 else False,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
positions[:, 0, ...] += self.frame_idx
|
| 52 |
+
# If the keyframe latent encodes a single pixel frame,
|
| 53 |
+
# narrow the temporal end to [start, start + 1) instead of the
|
| 54 |
+
# VAE-scaled range.
|
| 55 |
+
if self.num_pixel_frames == 1:
|
| 56 |
+
positions[:, 0, ..., 1:] = positions[:, 0, ..., :1] + 1
|
| 57 |
+
positions = positions.to(dtype=torch.float32)
|
| 58 |
+
positions[:, 0, ...] /= latent_tools.fps
|
| 59 |
+
|
| 60 |
+
denoise_mask = torch.full(
|
| 61 |
+
size=(*tokens.shape[:2], 1),
|
| 62 |
+
fill_value=1.0 - self.strength,
|
| 63 |
+
device=self.keyframes.device,
|
| 64 |
+
dtype=self.keyframes.dtype,
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
new_attention_mask = update_attention_mask(
|
| 68 |
+
latent_state=latent_state,
|
| 69 |
+
attention_mask=None,
|
| 70 |
+
num_noisy_tokens=latent_tools.target_shape.token_count(),
|
| 71 |
+
num_new_tokens=tokens.shape[1],
|
| 72 |
+
batch_size=tokens.shape[0],
|
| 73 |
+
device=self.keyframes.device,
|
| 74 |
+
dtype=self.keyframes.dtype,
|
| 75 |
+
)
|
| 76 |
+
|
| 77 |
+
return LatentState(
|
| 78 |
+
latent=torch.cat([latent_state.latent, tokens], dim=1),
|
| 79 |
+
denoise_mask=torch.cat([latent_state.denoise_mask, denoise_mask], dim=1),
|
| 80 |
+
positions=torch.cat([latent_state.positions, positions], dim=2),
|
| 81 |
+
clean_latent=torch.cat([latent_state.clean_latent, tokens], dim=1),
|
| 82 |
+
attention_mask=new_attention_mask,
|
| 83 |
+
)
|
packages/ltx-core/src/ltx_core/conditioning/types/latent_cond.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import torch
|
| 2 |
+
|
| 3 |
+
from ltx_core.conditioning.exceptions import ConditioningError
|
| 4 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 5 |
+
from ltx_core.tools import LatentTools
|
| 6 |
+
from ltx_core.types import LatentState
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
class VideoConditionByLatentIndex(ConditioningItem):
|
| 10 |
+
"""
|
| 11 |
+
Conditions video generation by injecting latents at a specific latent frame index.
|
| 12 |
+
Replaces tokens in the latent state at positions corresponding to latent_idx,
|
| 13 |
+
and sets denoise strength according to the strength parameter.
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
def __init__(self, latent: torch.Tensor, strength: float, latent_idx: int):
|
| 17 |
+
self.latent = latent
|
| 18 |
+
self.strength = strength
|
| 19 |
+
self.latent_idx = latent_idx
|
| 20 |
+
|
| 21 |
+
def apply_to(self, latent_state: LatentState, latent_tools: LatentTools) -> LatentState:
|
| 22 |
+
cond_batch, cond_channels, _, cond_height, cond_width = self.latent.shape
|
| 23 |
+
tgt_batch, tgt_channels, tgt_frames, tgt_height, tgt_width = latent_tools.target_shape.to_torch_shape()
|
| 24 |
+
|
| 25 |
+
if (cond_batch, cond_channels, cond_height, cond_width) != (tgt_batch, tgt_channels, tgt_height, tgt_width):
|
| 26 |
+
raise ConditioningError(
|
| 27 |
+
f"Can't apply image conditioning item to latent with shape {latent_tools.target_shape}, expected "
|
| 28 |
+
f"shape is ({tgt_batch}, {tgt_channels}, {tgt_frames}, {tgt_height}, {tgt_width}). Make sure "
|
| 29 |
+
"the image and latent have the same spatial shape."
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
tokens = latent_tools.patchifier.patchify(self.latent)
|
| 33 |
+
start_token = latent_tools.patchifier.get_token_count(
|
| 34 |
+
latent_tools.target_shape._replace(frames=self.latent_idx)
|
| 35 |
+
)
|
| 36 |
+
stop_token = start_token + tokens.shape[1]
|
| 37 |
+
|
| 38 |
+
latent_state = latent_state.clone()
|
| 39 |
+
|
| 40 |
+
latent_state.latent[:, start_token:stop_token] = tokens
|
| 41 |
+
latent_state.clean_latent[:, start_token:stop_token] = tokens
|
| 42 |
+
latent_state.denoise_mask[:, start_token:stop_token] = 1.0 - self.strength
|
| 43 |
+
|
| 44 |
+
return latent_state
|
packages/ltx-core/src/ltx_core/conditioning/types/noise_mask_cond.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
|
| 3 |
+
from ltx_core.components.patchifiers import get_pixel_coords
|
| 4 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 5 |
+
from ltx_core.tools import LatentTools, SpatioTemporalScaleFactors
|
| 6 |
+
from ltx_core.types import AudioLatentShape, LatentState, VideoLatentShape
|
| 7 |
+
|
| 8 |
+
|
| 9 |
+
@dataclass(frozen=True)
|
| 10 |
+
class TemporalRegionMask(ConditioningItem):
|
| 11 |
+
"""Conditioning item that sets ``denoise_mask = 0`` outside a time range
|
| 12 |
+
and ``1`` inside, so only the specified temporal region is regenerated.
|
| 13 |
+
Uses ``start_time`` and ``end_time`` in seconds. Works in *patchified*
|
| 14 |
+
(token) space using the patchifier's ``get_patch_grid_bounds``: for video
|
| 15 |
+
coords are latent frame indices (converted from seconds via ``fps``), for
|
| 16 |
+
audio coords are already in seconds.
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
start_time: float # seconds, inclusive
|
| 20 |
+
end_time: float # seconds, exclusive
|
| 21 |
+
fps: float
|
| 22 |
+
|
| 23 |
+
def apply_to(self, latent_state: LatentState, latent_tools: LatentTools) -> LatentState:
|
| 24 |
+
coords = latent_tools.patchifier.get_patch_grid_bounds(
|
| 25 |
+
latent_tools.target_shape, device=latent_state.denoise_mask.device
|
| 26 |
+
)
|
| 27 |
+
if isinstance(latent_tools.target_shape, AudioLatentShape):
|
| 28 |
+
# Audio: patchifier get_patch_grid_bounds returns seconds
|
| 29 |
+
t_boundaries = coords[:, 0]
|
| 30 |
+
elif isinstance(latent_tools.target_shape, VideoLatentShape):
|
| 31 |
+
# Video: patchifier get_patch_grid_bounds returns latent bounds, converting to frame numbers & pixel bounds
|
| 32 |
+
scale_factors = getattr(latent_tools, "scale_factors", SpatioTemporalScaleFactors.default())
|
| 33 |
+
pixel_bounds = get_pixel_coords(coords, scale_factors, causal_fix=getattr(latent_tools, "causal_fix", True))
|
| 34 |
+
# converting frame numbers to seconds
|
| 35 |
+
t_boundaries = pixel_bounds[:, 0] / self.fps
|
| 36 |
+
else:
|
| 37 |
+
raise ValueError("Unsupported LatentShape type, expected AudioLatentShape or VideoLatentShape")
|
| 38 |
+
t_start, t_end = t_boundaries.unbind(dim=-1) # [B, N]
|
| 39 |
+
in_region = (t_end > self.start_time) & (t_start < self.end_time)
|
| 40 |
+
state = latent_state.clone()
|
| 41 |
+
mask_val = in_region.to(state.denoise_mask.dtype)
|
| 42 |
+
if state.denoise_mask.dim() == 3:
|
| 43 |
+
mask_val = mask_val.unsqueeze(-1)
|
| 44 |
+
state.denoise_mask.copy_(mask_val)
|
| 45 |
+
return state
|
packages/ltx-core/src/ltx_core/conditioning/types/reference_audio_cond.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio reference conditioning items."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
|
| 7 |
+
from ltx_core.conditioning.mask_utils import update_attention_mask
|
| 8 |
+
from ltx_core.tools import LatentTools
|
| 9 |
+
from ltx_core.types import LatentState
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class AudioConditionByReferenceLatent:
|
| 13 |
+
"""Append patchified reference audio tokens after the target audio sequence.
|
| 14 |
+
Mirrors :class:`ltx_core.conditioning.types.reference_video_cond.VideoConditionByReferenceLatent`
|
| 15 |
+
but for audio. The reference tokens are appended so the target audio tokens stay
|
| 16 |
+
in the first ``num_noisy_tokens`` positions and can be kept by
|
| 17 |
+
:meth:`ltx_core.tools.LatentTools.clear_conditioning`.
|
| 18 |
+
Args:
|
| 19 |
+
patchified: Patchified reference latent ``[B, T_ref, C]``.
|
| 20 |
+
positions: RoPE positions for reference tokens, ``[B, 1, T_ref, 2]``.
|
| 21 |
+
strength: 1.0 keeps reference clean; 0.0 would fully denoise it.
|
| 22 |
+
"""
|
| 23 |
+
|
| 24 |
+
def __init__(
|
| 25 |
+
self,
|
| 26 |
+
patchified: torch.Tensor,
|
| 27 |
+
positions: torch.Tensor,
|
| 28 |
+
strength: float = 1.0,
|
| 29 |
+
) -> None:
|
| 30 |
+
self.patchified = patchified
|
| 31 |
+
self.positions = positions.to(dtype=torch.float32)
|
| 32 |
+
self.strength = strength
|
| 33 |
+
|
| 34 |
+
def apply_to(self, latent_state: LatentState, latent_tools: LatentTools) -> LatentState:
|
| 35 |
+
tokens = self.patchified
|
| 36 |
+
denoise_mask = torch.full(
|
| 37 |
+
size=(*tokens.shape[:2], 1),
|
| 38 |
+
fill_value=1.0 - self.strength,
|
| 39 |
+
device=tokens.device,
|
| 40 |
+
dtype=tokens.dtype,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
new_attention_mask = update_attention_mask(
|
| 44 |
+
latent_state=latent_state,
|
| 45 |
+
attention_mask=None,
|
| 46 |
+
num_noisy_tokens=latent_tools.patchifier.get_token_count(latent_tools.target_shape),
|
| 47 |
+
num_new_tokens=tokens.shape[1],
|
| 48 |
+
batch_size=tokens.shape[0],
|
| 49 |
+
device=tokens.device,
|
| 50 |
+
dtype=tokens.dtype,
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
return LatentState(
|
| 54 |
+
latent=torch.cat([latent_state.latent, tokens], dim=1),
|
| 55 |
+
denoise_mask=torch.cat([latent_state.denoise_mask, denoise_mask], dim=1),
|
| 56 |
+
positions=torch.cat([latent_state.positions, self.positions], dim=2),
|
| 57 |
+
clean_latent=torch.cat([latent_state.clean_latent, tokens], dim=1),
|
| 58 |
+
attention_mask=new_attention_mask,
|
| 59 |
+
)
|
packages/ltx-core/src/ltx_core/conditioning/types/reference_video_cond.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Reference video conditioning for IC-LoRA inference."""
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from ltx_core.components.patchifiers import get_pixel_coords
|
| 6 |
+
from ltx_core.conditioning.item import ConditioningItem
|
| 7 |
+
from ltx_core.conditioning.mask_utils import update_attention_mask
|
| 8 |
+
from ltx_core.tools import VideoLatentTools
|
| 9 |
+
from ltx_core.types import LatentState, VideoLatentShape
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class VideoConditionByReferenceLatent(ConditioningItem):
|
| 13 |
+
"""
|
| 14 |
+
Conditions video generation on a reference video latent for IC-LoRA inference.
|
| 15 |
+
IC-LoRAs are trained by concatenating reference (control signal) and target tokens,
|
| 16 |
+
learning to attend across both. This class replicates that setup at inference by
|
| 17 |
+
appending reference tokens to the latent sequence.
|
| 18 |
+
IC-LoRAs can be trained with lower-resolution references than the target (e.g., 384px
|
| 19 |
+
reference for 768px output) for efficiency and better generalization. The
|
| 20 |
+
`downscale_factor` scales reference positions to match target coordinates, preserving
|
| 21 |
+
the learned positional relationships. This must match the factor used during training
|
| 22 |
+
(stored in LoRA metadata).
|
| 23 |
+
To add attention masking, wrap with :class:`ConditioningItemAttentionStrengthWrapper`.
|
| 24 |
+
Args:
|
| 25 |
+
latent: Reference video latents [B, C, F, H, W]
|
| 26 |
+
downscale_factor: Target/reference resolution ratio (e.g., 2 = half-resolution
|
| 27 |
+
reference). Spatial positions are scaled by this factor.
|
| 28 |
+
strength: Conditioning strength. 1.0 = full (reference kept clean),
|
| 29 |
+
0.0 = none (reference denoised). Default 1.0.
|
| 30 |
+
"""
|
| 31 |
+
|
| 32 |
+
def __init__(
|
| 33 |
+
self,
|
| 34 |
+
latent: torch.Tensor,
|
| 35 |
+
downscale_factor: int = 1,
|
| 36 |
+
strength: float = 1.0,
|
| 37 |
+
):
|
| 38 |
+
self.latent = latent
|
| 39 |
+
self.downscale_factor = downscale_factor
|
| 40 |
+
self.strength = strength
|
| 41 |
+
|
| 42 |
+
def apply_to(
|
| 43 |
+
self,
|
| 44 |
+
latent_state: LatentState,
|
| 45 |
+
latent_tools: VideoLatentTools,
|
| 46 |
+
) -> LatentState:
|
| 47 |
+
"""Append reference video tokens with scaled positions."""
|
| 48 |
+
tokens = latent_tools.patchifier.patchify(self.latent)
|
| 49 |
+
|
| 50 |
+
# Compute positions for the reference video's actual dimensions
|
| 51 |
+
latent_coords = latent_tools.patchifier.get_patch_grid_bounds(
|
| 52 |
+
output_shape=VideoLatentShape.from_torch_shape(self.latent.shape),
|
| 53 |
+
device=self.latent.device,
|
| 54 |
+
)
|
| 55 |
+
positions = get_pixel_coords(
|
| 56 |
+
latent_coords=latent_coords,
|
| 57 |
+
scale_factors=latent_tools.scale_factors,
|
| 58 |
+
causal_fix=latent_tools.causal_fix,
|
| 59 |
+
)
|
| 60 |
+
positions = positions.to(dtype=torch.float32)
|
| 61 |
+
positions[:, 0, ...] /= latent_tools.fps
|
| 62 |
+
|
| 63 |
+
# Scale spatial positions to match target coordinate space
|
| 64 |
+
if self.downscale_factor != 1:
|
| 65 |
+
positions[:, 1, ...] *= self.downscale_factor # height axis
|
| 66 |
+
positions[:, 2, ...] *= self.downscale_factor # width axis
|
| 67 |
+
|
| 68 |
+
denoise_mask = torch.full(
|
| 69 |
+
size=(*tokens.shape[:2], 1),
|
| 70 |
+
fill_value=1.0 - self.strength,
|
| 71 |
+
device=self.latent.device,
|
| 72 |
+
dtype=self.latent.dtype,
|
| 73 |
+
)
|
| 74 |
+
|
| 75 |
+
new_attention_mask = update_attention_mask(
|
| 76 |
+
latent_state=latent_state,
|
| 77 |
+
attention_mask=None,
|
| 78 |
+
num_noisy_tokens=latent_tools.target_shape.token_count(),
|
| 79 |
+
num_new_tokens=tokens.shape[1],
|
| 80 |
+
batch_size=tokens.shape[0],
|
| 81 |
+
device=self.latent.device,
|
| 82 |
+
dtype=self.latent.dtype,
|
| 83 |
+
)
|
| 84 |
+
|
| 85 |
+
return LatentState(
|
| 86 |
+
latent=torch.cat([latent_state.latent, tokens], dim=1),
|
| 87 |
+
denoise_mask=torch.cat([latent_state.denoise_mask, denoise_mask], dim=1),
|
| 88 |
+
positions=torch.cat([latent_state.positions, positions], dim=2),
|
| 89 |
+
clean_latent=torch.cat([latent_state.clean_latent, tokens], dim=1),
|
| 90 |
+
attention_mask=new_attention_mask,
|
| 91 |
+
)
|
packages/ltx-core/src/ltx_core/guidance/__init__.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Guidance and perturbation utilities for attention manipulation."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.guidance.perturbations import (
|
| 4 |
+
BatchedPerturbationConfig,
|
| 5 |
+
Perturbation,
|
| 6 |
+
PerturbationConfig,
|
| 7 |
+
PerturbationType,
|
| 8 |
+
)
|
| 9 |
+
|
| 10 |
+
__all__ = [
|
| 11 |
+
"BatchedPerturbationConfig",
|
| 12 |
+
"Perturbation",
|
| 13 |
+
"PerturbationConfig",
|
| 14 |
+
"PerturbationType",
|
| 15 |
+
]
|
packages/ltx-core/src/ltx_core/guidance/perturbations.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass
|
| 2 |
+
from enum import Enum
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
from torch._prims_common import DeviceLikeType
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class PerturbationType(Enum):
|
| 9 |
+
"""Types of attention perturbations for STG (Spatio-Temporal Guidance)."""
|
| 10 |
+
|
| 11 |
+
SKIP_A2V_CROSS_ATTN = "skip_a2v_cross_attn"
|
| 12 |
+
SKIP_V2A_CROSS_ATTN = "skip_v2a_cross_attn"
|
| 13 |
+
SKIP_VIDEO_SELF_ATTN = "skip_video_self_attn"
|
| 14 |
+
SKIP_AUDIO_SELF_ATTN = "skip_audio_self_attn"
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@dataclass(frozen=True)
|
| 18 |
+
class Perturbation:
|
| 19 |
+
"""A single perturbation specifying which attention type to skip and in which blocks."""
|
| 20 |
+
|
| 21 |
+
type: PerturbationType
|
| 22 |
+
blocks: list[int] | None # None means all blocks
|
| 23 |
+
|
| 24 |
+
def is_perturbed(self, perturbation_type: PerturbationType, block: int) -> bool:
|
| 25 |
+
if self.type != perturbation_type:
|
| 26 |
+
return False
|
| 27 |
+
|
| 28 |
+
if self.blocks is None:
|
| 29 |
+
return True
|
| 30 |
+
|
| 31 |
+
return block in self.blocks
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
@dataclass(frozen=True)
|
| 35 |
+
class PerturbationConfig:
|
| 36 |
+
"""Configuration holding a list of perturbations for a single sample."""
|
| 37 |
+
|
| 38 |
+
perturbations: list[Perturbation] | None
|
| 39 |
+
|
| 40 |
+
def is_perturbed(self, perturbation_type: PerturbationType, block: int) -> bool:
|
| 41 |
+
if self.perturbations is None:
|
| 42 |
+
return False
|
| 43 |
+
|
| 44 |
+
return any(perturbation.is_perturbed(perturbation_type, block) for perturbation in self.perturbations)
|
| 45 |
+
|
| 46 |
+
@staticmethod
|
| 47 |
+
def empty() -> "PerturbationConfig":
|
| 48 |
+
return PerturbationConfig([])
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@dataclass(frozen=True)
|
| 52 |
+
class BatchedPerturbationConfig:
|
| 53 |
+
"""Perturbation configurations for a batch, with utilities for generating attention masks."""
|
| 54 |
+
|
| 55 |
+
perturbations: list[PerturbationConfig]
|
| 56 |
+
|
| 57 |
+
def mask(
|
| 58 |
+
self, perturbation_type: PerturbationType, block: int, device: DeviceLikeType, dtype: torch.dtype
|
| 59 |
+
) -> torch.Tensor:
|
| 60 |
+
mask = torch.ones((len(self.perturbations),), device=device, dtype=dtype)
|
| 61 |
+
for batch_idx, perturbation in enumerate(self.perturbations):
|
| 62 |
+
if perturbation.is_perturbed(perturbation_type, block):
|
| 63 |
+
mask[batch_idx] = 0
|
| 64 |
+
|
| 65 |
+
return mask
|
| 66 |
+
|
| 67 |
+
def mask_like(self, perturbation_type: PerturbationType, block: int, values: torch.Tensor) -> torch.Tensor:
|
| 68 |
+
mask = self.mask(perturbation_type, block, values.device, values.dtype)
|
| 69 |
+
return mask.view(mask.numel(), *([1] * len(values.shape[1:])))
|
| 70 |
+
|
| 71 |
+
def any_in_batch(self, perturbation_type: PerturbationType, block: int) -> bool:
|
| 72 |
+
return any(perturbation.is_perturbed(perturbation_type, block) for perturbation in self.perturbations)
|
| 73 |
+
|
| 74 |
+
def all_in_batch(self, perturbation_type: PerturbationType, block: int) -> bool:
|
| 75 |
+
return all(perturbation.is_perturbed(perturbation_type, block) for perturbation in self.perturbations)
|
| 76 |
+
|
| 77 |
+
@staticmethod
|
| 78 |
+
def empty(batch_size: int) -> "BatchedPerturbationConfig":
|
| 79 |
+
return BatchedPerturbationConfig([PerturbationConfig.empty() for _ in range(batch_size)])
|
packages/ltx-core/src/ltx_core/hdr.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""HDR utilities: LogC3 compression for HDR IC-LoRA training and inference.
|
| 2 |
+
Provides compress/decompress and postprocess helpers for HDR video generation.
|
| 3 |
+
Used by ltx-pipelines for HDR IC-LoRA and by ltx-trainer for HDR validation.
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
from __future__ import annotations
|
| 7 |
+
|
| 8 |
+
from typing import Literal
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import Tensor
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class LogC3:
|
| 15 |
+
"""ARRI LogC3 (EI 800) HDR compression.
|
| 16 |
+
Maps linear [0, ∞) <-> LogC3 [0, 1] via the camera log curve. The log
|
| 17 |
+
curve allocates more precision to shadows/midtones and compresses
|
| 18 |
+
highlights smoothly. Callers are responsible for mapping the [0, 1]
|
| 19 |
+
output to the VAE's [-1, 1] input range.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
name = "LogC3"
|
| 23 |
+
A = 5.555556
|
| 24 |
+
B = 0.052272
|
| 25 |
+
C = 0.247190
|
| 26 |
+
D = 0.385537
|
| 27 |
+
E = 5.367655
|
| 28 |
+
F = 0.092809
|
| 29 |
+
CUT = 0.010591
|
| 30 |
+
|
| 31 |
+
def compress(self, hdr: Tensor) -> Tensor:
|
| 32 |
+
"""Compress linear HDR [0, ∞) → LogC3 [0, 1]."""
|
| 33 |
+
x = torch.clamp(hdr, min=0.0)
|
| 34 |
+
log_part = self.C * torch.log10(self.A * x + self.B) + self.D
|
| 35 |
+
lin_part = self.E * x + self.F
|
| 36 |
+
logc = torch.where(x >= self.CUT, log_part, lin_part)
|
| 37 |
+
return torch.clamp(logc, 0.0, 1.0)
|
| 38 |
+
|
| 39 |
+
def compress_ldr(self, ldr: Tensor) -> Tensor:
|
| 40 |
+
"""Compress LDR [0, 1] → [0, 1] (no log curve, just clamp)."""
|
| 41 |
+
return torch.clamp(ldr, 0.0, 1.0)
|
| 42 |
+
|
| 43 |
+
def decompress(self, logc: Tensor) -> Tensor:
|
| 44 |
+
"""Decompress LogC3 [0, 1] → linear HDR [0, ∞)."""
|
| 45 |
+
logc = torch.clamp(logc, 0.0, 1.0)
|
| 46 |
+
cut_log = self.E * self.CUT + self.F
|
| 47 |
+
lin_from_log = (torch.pow(10.0, (logc - self.D) / self.C) - self.B) / self.A
|
| 48 |
+
lin_from_lin = (logc - self.F) / self.E
|
| 49 |
+
return torch.where(logc >= cut_log, lin_from_log, lin_from_lin)
|
| 50 |
+
|
| 51 |
+
def decompress_ldr(self, logc: Tensor) -> Tensor:
|
| 52 |
+
"""Decompress [0, 1] → LDR [0, 1] (identity clamp)."""
|
| 53 |
+
return torch.clamp(logc, 0.0, 1.0)
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
def apply_hdr_decode_postprocess(
|
| 57 |
+
decoded_video: Tensor,
|
| 58 |
+
transform: Literal["logc3"] = "logc3",
|
| 59 |
+
) -> Tensor:
|
| 60 |
+
"""Apply HDR decompress to VAE decode output for HDR recovery.
|
| 61 |
+
Args:
|
| 62 |
+
decoded_video: Tensor from VAE decode in [0, 1], shape [B, C, F, H, W].
|
| 63 |
+
Must be float32 for sufficient color resolution.
|
| 64 |
+
transform: "logc3".
|
| 65 |
+
Returns:
|
| 66 |
+
HDR video tensor float32.
|
| 67 |
+
"""
|
| 68 |
+
decoded_video = decoded_video.float()
|
| 69 |
+
if transform == "logc3":
|
| 70 |
+
return LogC3().decompress(decoded_video)
|
| 71 |
+
raise ValueError(f"Unsupported HDR transform: {transform}")
|
packages/ltx-core/src/ltx_core/loader/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Loader utilities for model weights, LoRAs, and safetensor operations."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.loader.fuse_loras import apply_loras
|
| 4 |
+
from ltx_core.loader.helpers import (
|
| 5 |
+
create_meta_model,
|
| 6 |
+
load_state_dict,
|
| 7 |
+
read_model_config,
|
| 8 |
+
)
|
| 9 |
+
from ltx_core.loader.module_ops import ModuleOps
|
| 10 |
+
from ltx_core.loader.primitives import (
|
| 11 |
+
LoRAAdaptableProtocol,
|
| 12 |
+
LoraPathStrengthAndSDOps,
|
| 13 |
+
LoraStateDictWithStrength,
|
| 14 |
+
ModelBuilderProtocol,
|
| 15 |
+
StateDict,
|
| 16 |
+
StateDictLoader,
|
| 17 |
+
)
|
| 18 |
+
from ltx_core.loader.registry import DummyRegistry, Registry, StateDictRegistry
|
| 19 |
+
from ltx_core.loader.sd_ops import (
|
| 20 |
+
LTXV_LORA_COMFY_RENAMING_MAP,
|
| 21 |
+
ContentMatching,
|
| 22 |
+
ContentReplacement,
|
| 23 |
+
KeyValueOperation,
|
| 24 |
+
KeyValueOperationResult,
|
| 25 |
+
SDKeyValueOperation,
|
| 26 |
+
SDOps,
|
| 27 |
+
)
|
| 28 |
+
from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader, SafetensorsStateDictLoader
|
| 29 |
+
from ltx_core.loader.single_gpu_model_builder import SingleGPUModelBuilder
|
| 30 |
+
|
| 31 |
+
__all__ = [
|
| 32 |
+
"LTXV_LORA_COMFY_RENAMING_MAP",
|
| 33 |
+
"ContentMatching",
|
| 34 |
+
"ContentReplacement",
|
| 35 |
+
"DummyRegistry",
|
| 36 |
+
"KeyValueOperation",
|
| 37 |
+
"KeyValueOperationResult",
|
| 38 |
+
"LoRAAdaptableProtocol",
|
| 39 |
+
"LoraPathStrengthAndSDOps",
|
| 40 |
+
"LoraStateDictWithStrength",
|
| 41 |
+
"ModelBuilderProtocol",
|
| 42 |
+
"ModuleOps",
|
| 43 |
+
"Registry",
|
| 44 |
+
"SDKeyValueOperation",
|
| 45 |
+
"SDOps",
|
| 46 |
+
"SafetensorsModelStateDictLoader",
|
| 47 |
+
"SafetensorsStateDictLoader",
|
| 48 |
+
"SingleGPUModelBuilder",
|
| 49 |
+
"StateDict",
|
| 50 |
+
"StateDictLoader",
|
| 51 |
+
"StateDictRegistry",
|
| 52 |
+
"apply_loras",
|
| 53 |
+
"create_meta_model",
|
| 54 |
+
"load_state_dict",
|
| 55 |
+
"read_model_config",
|
| 56 |
+
]
|
packages/ltx-core/src/ltx_core/loader/fuse_loras.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from collections.abc import Iterable, Iterator
|
| 2 |
+
from typing import NamedTuple
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from ltx_core.loader.kernels import TRITON_AVAILABLE
|
| 7 |
+
from ltx_core.loader.primitives import LoraStateDictWithStrength, StateDict
|
| 8 |
+
from ltx_core.quantization.fp8_cast import fused_add_round_launch
|
| 9 |
+
from ltx_core.quantization.fp8_scaled_mm import quantize_weight_to_fp8_per_tensor
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class LoraProduct(NamedTuple):
|
| 13 |
+
"""A LoRA's ``A``, ``B`` factors and its strength scalar."""
|
| 14 |
+
|
| 15 |
+
a: torch.Tensor
|
| 16 |
+
b: torch.Tensor
|
| 17 |
+
strength: float
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
def _get_device() -> torch.device:
|
| 21 |
+
if torch.cuda.is_available():
|
| 22 |
+
return torch.device("cuda", torch.cuda.current_device())
|
| 23 |
+
return torch.device("cpu")
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def aggregate_lora_products(
|
| 27 |
+
products: Iterable[LoraProduct],
|
| 28 |
+
dtype: torch.dtype | None = None,
|
| 29 |
+
*,
|
| 30 |
+
out: torch.Tensor | None = None,
|
| 31 |
+
) -> torch.Tensor | None:
|
| 32 |
+
"""Accumulate ``sum((B * strength) @ A)`` across :class:`LoraProduct` items.
|
| 33 |
+
If ``out`` is provided, ``addmm_`` accumulates directly into it — caller
|
| 34 |
+
ensures A/B dtypes and devices match ``out``. Otherwise the first product
|
| 35 |
+
materializes the ``(out, in)``-shape aggregator at ``dtype``; subsequent
|
| 36 |
+
products use ``addmm_`` to avoid allocating the full intermediate delta.
|
| 37 |
+
Returns ``out`` (or the new aggregator), or ``None`` if ``products`` was empty
|
| 38 |
+
and ``out`` was not given.
|
| 39 |
+
"""
|
| 40 |
+
aggregated = out
|
| 41 |
+
for product in products:
|
| 42 |
+
if aggregated is None:
|
| 43 |
+
aggregated = torch.matmul(product.b * product.strength, product.a).to(dtype=dtype)
|
| 44 |
+
else:
|
| 45 |
+
aggregated.addmm_(product.b, product.a, alpha=product.strength)
|
| 46 |
+
return aggregated
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def fuse_cast_fp8_weight(
|
| 50 |
+
delta_bf16: torch.Tensor,
|
| 51 |
+
weight_fp8: torch.Tensor,
|
| 52 |
+
target_dtype: torch.dtype,
|
| 53 |
+
) -> torch.Tensor:
|
| 54 |
+
"""Return ``(delta_bf16 + dequantize(weight_fp8)).to(target_dtype)``.
|
| 55 |
+
CUDA with Triton uses stochastic rounding; otherwise uses a deterministic bf16 add.
|
| 56 |
+
``delta_bf16`` is the bf16 accumulator and is mutated in place.
|
| 57 |
+
"""
|
| 58 |
+
if delta_bf16.dtype != torch.bfloat16:
|
| 59 |
+
raise ValueError(f"delta_bf16 must be bfloat16, got {delta_bf16.dtype}")
|
| 60 |
+
if str(weight_fp8.device).startswith("cuda") and TRITON_AVAILABLE:
|
| 61 |
+
fused_add_round_launch(delta_bf16, weight_fp8, seed=0)
|
| 62 |
+
else:
|
| 63 |
+
delta_bf16.add_(weight_fp8.to(dtype=torch.bfloat16))
|
| 64 |
+
return delta_bf16.to(dtype=target_dtype)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
def fuse_lora_weights(
|
| 68 |
+
model_sd: StateDict,
|
| 69 |
+
lora_sd_and_strengths: list[LoraStateDictWithStrength],
|
| 70 |
+
dtype: torch.dtype | None = None,
|
| 71 |
+
preserve_input_device: bool = True,
|
| 72 |
+
) -> Iterator[tuple[str, torch.Tensor]]:
|
| 73 |
+
"""Yield ``(key, fused_tensor)`` for each weight modified by at least one LoRA.
|
| 74 |
+
For scaled-FP8 weights, this includes both the updated ``.weight`` tensor
|
| 75 |
+
and its corresponding ``.weight_scale`` tensor.
|
| 76 |
+
When ``preserve_input_device`` is False, fused tensors are yielded on the device
|
| 77 |
+
used for fusion; caller is responsible for moving them to their final
|
| 78 |
+
destination.
|
| 79 |
+
"""
|
| 80 |
+
for key, original_weight in model_sd.sd.items():
|
| 81 |
+
if original_weight is None or key.endswith(".weight_scale"):
|
| 82 |
+
continue
|
| 83 |
+
original_device = original_weight.device
|
| 84 |
+
weight = original_weight.to(device=_get_device())
|
| 85 |
+
target_dtype = dtype if dtype is not None else weight.dtype
|
| 86 |
+
deltas_dtype = target_dtype if target_dtype not in [torch.float8_e4m3fn, torch.float8_e5m2] else torch.bfloat16
|
| 87 |
+
|
| 88 |
+
deltas = _aggregate_deltas(lora_sd_and_strengths, key, deltas_dtype, weight.device)
|
| 89 |
+
if deltas is None:
|
| 90 |
+
continue
|
| 91 |
+
|
| 92 |
+
scale_key = key.replace(".weight", ".weight_scale") if key.endswith(".weight") else None
|
| 93 |
+
is_scaled_fp8 = scale_key is not None and scale_key in model_sd.sd
|
| 94 |
+
|
| 95 |
+
if weight.dtype == torch.float8_e4m3fn:
|
| 96 |
+
if is_scaled_fp8:
|
| 97 |
+
fused = _fuse_delta_with_scaled_fp8(deltas, weight, key, scale_key, model_sd)
|
| 98 |
+
else:
|
| 99 |
+
fused = {key: fuse_cast_fp8_weight(deltas, weight, target_dtype)}
|
| 100 |
+
elif weight.dtype == torch.bfloat16:
|
| 101 |
+
deltas.add_(weight)
|
| 102 |
+
fused = {key: deltas.to(dtype=target_dtype)}
|
| 103 |
+
else:
|
| 104 |
+
raise ValueError(f"Unsupported dtype: {weight.dtype}")
|
| 105 |
+
|
| 106 |
+
for k, v in fused.items():
|
| 107 |
+
yield k, v.to(device=original_device) if preserve_input_device else v
|
| 108 |
+
|
| 109 |
+
|
| 110 |
+
def apply_loras(
|
| 111 |
+
model_sd: StateDict,
|
| 112 |
+
lora_sd_and_strengths: list[LoraStateDictWithStrength],
|
| 113 |
+
dtype: torch.dtype | None = None,
|
| 114 |
+
destination_sd: StateDict | None = None,
|
| 115 |
+
) -> StateDict:
|
| 116 |
+
"""Fuse LoRAs into ``model_sd`` and place the results in ``destination_sd``.
|
| 117 |
+
When ``destination_sd`` is provided, the fused tensors are placed directly into it.
|
| 118 |
+
"""
|
| 119 |
+
if destination_sd is not None:
|
| 120 |
+
for key, fused in fuse_lora_weights(model_sd, lora_sd_and_strengths, dtype):
|
| 121 |
+
destination_sd.sd[key] = fused
|
| 122 |
+
return destination_sd
|
| 123 |
+
|
| 124 |
+
fused = dict(fuse_lora_weights(model_sd, lora_sd_and_strengths, dtype))
|
| 125 |
+
sd = {k: (fused[k] if k in fused else v.clone()) for k, v in model_sd.sd.items()}
|
| 126 |
+
return StateDict(sd, model_sd.device, model_sd.size, model_sd.dtype)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def _aggregate_deltas(
|
| 130 |
+
lora_sd_and_strengths: list[LoraStateDictWithStrength], key: str, dtype: torch.dtype, device: torch.device
|
| 131 |
+
) -> torch.Tensor | None:
|
| 132 |
+
prefix = key[: -len(".weight")]
|
| 133 |
+
key_a = f"{prefix}.lora_A.weight"
|
| 134 |
+
key_b = f"{prefix}.lora_B.weight"
|
| 135 |
+
|
| 136 |
+
def _ab_products() -> Iterator[LoraProduct]:
|
| 137 |
+
for lsd, coef in lora_sd_and_strengths:
|
| 138 |
+
if key_a not in lsd.sd or key_b not in lsd.sd:
|
| 139 |
+
continue
|
| 140 |
+
a = lsd.sd[key_a].to(device=device, dtype=dtype, non_blocking=True)
|
| 141 |
+
b = lsd.sd[key_b].to(device=device, dtype=dtype, non_blocking=True)
|
| 142 |
+
yield LoraProduct(a, b, coef)
|
| 143 |
+
|
| 144 |
+
return aggregate_lora_products(_ab_products(), dtype)
|
| 145 |
+
|
| 146 |
+
|
| 147 |
+
def _fuse_delta_with_scaled_fp8(
|
| 148 |
+
deltas: torch.Tensor,
|
| 149 |
+
weight: torch.Tensor,
|
| 150 |
+
key: str,
|
| 151 |
+
scale_key: str,
|
| 152 |
+
model_sd: StateDict,
|
| 153 |
+
) -> dict[str, torch.Tensor]:
|
| 154 |
+
"""Dequantize scaled FP8 weight, add LoRA delta, and re-quantize."""
|
| 155 |
+
weight_scale = model_sd.sd[scale_key]
|
| 156 |
+
|
| 157 |
+
original_weight = weight.to(torch.float32) * weight_scale
|
| 158 |
+
|
| 159 |
+
new_weight = original_weight + deltas.to(torch.float32)
|
| 160 |
+
|
| 161 |
+
new_fp8_weight, new_weight_scale = quantize_weight_to_fp8_per_tensor(new_weight)
|
| 162 |
+
return {key: new_fp8_weight, scale_key: new_weight_scale}
|
packages/ltx-core/src/ltx_core/loader/helpers.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared model-construction helpers used by both SingleGPUModelBuilder and StreamingModelBuilder."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
from typing import TypeVar
|
| 6 |
+
|
| 7 |
+
import torch
|
| 8 |
+
from torch import nn
|
| 9 |
+
|
| 10 |
+
from ltx_core.loader.module_ops import ModuleOps
|
| 11 |
+
from ltx_core.loader.primitives import StateDict, StateDictLoader
|
| 12 |
+
from ltx_core.loader.registry import Registry
|
| 13 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 14 |
+
from ltx_core.model.model_protocol import ModelConfigurator
|
| 15 |
+
|
| 16 |
+
_M = TypeVar("_M", bound=nn.Module)
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
def load_state_dict(
|
| 20 |
+
paths: str | tuple[str, ...] | list[str],
|
| 21 |
+
loader: StateDictLoader,
|
| 22 |
+
registry: Registry,
|
| 23 |
+
device: torch.device | None,
|
| 24 |
+
sd_ops: SDOps | None = None,
|
| 25 |
+
) -> StateDict:
|
| 26 |
+
"""Load a state dict from disk, using registry caching."""
|
| 27 |
+
if isinstance(paths, str):
|
| 28 |
+
path_list = [paths]
|
| 29 |
+
elif isinstance(paths, tuple):
|
| 30 |
+
path_list = list(paths)
|
| 31 |
+
else:
|
| 32 |
+
path_list = paths
|
| 33 |
+
cached = registry.get(path_list, sd_ops)
|
| 34 |
+
if cached is not None:
|
| 35 |
+
return cached
|
| 36 |
+
result = loader.load(path_list, sd_ops=sd_ops, device=device)
|
| 37 |
+
registry.add(path_list, sd_ops=sd_ops, state_dict=result)
|
| 38 |
+
return result
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
def read_model_config(
|
| 42 |
+
model_path: str | tuple[str, ...],
|
| 43 |
+
loader: StateDictLoader,
|
| 44 |
+
) -> dict:
|
| 45 |
+
"""Read metadata from the first shard of a checkpoint."""
|
| 46 |
+
first = model_path[0] if isinstance(model_path, tuple) else model_path
|
| 47 |
+
return loader.metadata(first)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def create_meta_model(
|
| 51 |
+
configurator: type[ModelConfigurator[_M]],
|
| 52 |
+
config: dict,
|
| 53 |
+
module_ops: tuple[ModuleOps, ...] = (),
|
| 54 |
+
) -> _M:
|
| 55 |
+
"""Create a model on the meta device and apply module operations."""
|
| 56 |
+
with torch.device("meta"):
|
| 57 |
+
model = configurator.from_config(config)
|
| 58 |
+
for op in module_ops:
|
| 59 |
+
if op.matcher(model):
|
| 60 |
+
model = op.mutator(model)
|
| 61 |
+
return model
|
packages/ltx-core/src/ltx_core/loader/kernels.py
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# ruff: noqa: ANN001, ANN201, ERA001, N803, N806
|
| 2 |
+
try:
|
| 3 |
+
import triton
|
| 4 |
+
import triton.language as tl
|
| 5 |
+
|
| 6 |
+
TRITON_AVAILABLE = True
|
| 7 |
+
except (ImportError, OSError):
|
| 8 |
+
TRITON_AVAILABLE = False
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
if TRITON_AVAILABLE:
|
| 12 |
+
|
| 13 |
+
@triton.jit
|
| 14 |
+
def fused_add_round_kernel(
|
| 15 |
+
x_ptr,
|
| 16 |
+
output_ptr, # contents will be added to the output
|
| 17 |
+
seed,
|
| 18 |
+
n_elements,
|
| 19 |
+
EXPONENT_BIAS,
|
| 20 |
+
MANTISSA_BITS,
|
| 21 |
+
BLOCK_SIZE: tl.constexpr,
|
| 22 |
+
):
|
| 23 |
+
"""
|
| 24 |
+
A kernel to upcast 8bit quantized weights to bfloat16 with stochastic rounding
|
| 25 |
+
and add them to bfloat16 output weights. Might be used to upcast original model weights
|
| 26 |
+
and to further add them to precalculated deltas coming from LoRAs.
|
| 27 |
+
"""
|
| 28 |
+
# Get program ID and compute offsets
|
| 29 |
+
pid = tl.program_id(axis=0)
|
| 30 |
+
block_start = pid * BLOCK_SIZE
|
| 31 |
+
offsets = block_start + tl.arange(0, BLOCK_SIZE)
|
| 32 |
+
mask = offsets < n_elements
|
| 33 |
+
|
| 34 |
+
# Load data
|
| 35 |
+
x = tl.load(x_ptr + offsets, mask=mask)
|
| 36 |
+
rand_vals = tl.rand(seed, offsets) - 0.5
|
| 37 |
+
|
| 38 |
+
x = tl.cast(x, tl.float16)
|
| 39 |
+
delta = tl.load(output_ptr + offsets, mask=mask)
|
| 40 |
+
delta = tl.cast(delta, tl.float16)
|
| 41 |
+
x = x + delta
|
| 42 |
+
|
| 43 |
+
x_bits = tl.cast(x, tl.int16, bitcast=True)
|
| 44 |
+
|
| 45 |
+
# Calculate the exponent. Unbiased fp16 exponent is ((x_bits & 0x7C00) >> 10) - 15 for
|
| 46 |
+
# normal numbers and -14 for subnormals.
|
| 47 |
+
fp16_exponent_bits = (x_bits & 0x7C00) >> 10
|
| 48 |
+
fp16_normals = fp16_exponent_bits > 0
|
| 49 |
+
fp16_exponent = tl.where(fp16_normals, fp16_exponent_bits - 15, -14)
|
| 50 |
+
|
| 51 |
+
# Add the target dtype's exponent bias and clamp to the target dtype's exponent range.
|
| 52 |
+
exponent = fp16_exponent + EXPONENT_BIAS
|
| 53 |
+
MAX_EXPONENT = 2 * EXPONENT_BIAS + 1
|
| 54 |
+
exponent = tl.where(exponent > MAX_EXPONENT, MAX_EXPONENT, exponent)
|
| 55 |
+
exponent = tl.where(exponent < 0, 0, exponent)
|
| 56 |
+
|
| 57 |
+
# Normal ULP exponent, expressed as an fp16 exponent field:
|
| 58 |
+
# (exponent - EXPONENT_BIAS - MANTISSA_BITS) + 15
|
| 59 |
+
# Simplifies to: fp16_exponent - MANTISSA_BITS + 15
|
| 60 |
+
# See https://en.wikipedia.org/wiki/Unit_in_the_last_place
|
| 61 |
+
eps_exp = tl.maximum(0, tl.minimum(31, exponent - EXPONENT_BIAS - MANTISSA_BITS + 15))
|
| 62 |
+
|
| 63 |
+
# Calculate epsilon in the target dtype
|
| 64 |
+
eps_normal = tl.cast(tl.cast(eps_exp << 10, tl.int16), tl.float16, bitcast=True)
|
| 65 |
+
|
| 66 |
+
# Subnormal ULP: 2^(1 - EXPONENT_BIAS - MANTISSA_BITS) ->
|
| 67 |
+
# fp16 exponent bits: (1 - EXPONENT_BIAS - MANTISSA_BITS) + 15 =
|
| 68 |
+
# 16 - EXPONENT_BIAS - MANTISSA_BITS
|
| 69 |
+
eps_subnormal = tl.cast(tl.cast((16 - EXPONENT_BIAS - MANTISSA_BITS) << 10, tl.int16), tl.float16, bitcast=True)
|
| 70 |
+
eps = tl.where(exponent > 0, eps_normal, eps_subnormal)
|
| 71 |
+
|
| 72 |
+
# Apply zero mask to epsilon
|
| 73 |
+
eps = tl.where(x == 0, 0.0, eps)
|
| 74 |
+
|
| 75 |
+
# Apply stochastic rounding
|
| 76 |
+
output = tl.cast(x + rand_vals * eps, tl.bfloat16)
|
| 77 |
+
|
| 78 |
+
# Store the result
|
| 79 |
+
tl.store(output_ptr + offsets, output, mask=mask)
|
packages/ltx-core/src/ltx_core/loader/module_ops.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Callable, NamedTuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class ModuleOps(NamedTuple):
|
| 7 |
+
"""
|
| 8 |
+
Defines a named operation for matching and mutating PyTorch modules.
|
| 9 |
+
Used to selectively transform modules in a model (e.g., replacing layers with quantized versions).
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
name: str
|
| 13 |
+
matcher: Callable[[torch.nn.Module], bool]
|
| 14 |
+
mutator: Callable[[torch.nn.Module], torch.nn.Module]
|
packages/ltx-core/src/ltx_core/loader/primitives.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from __future__ import annotations
|
| 2 |
+
|
| 3 |
+
from dataclasses import dataclass
|
| 4 |
+
from typing import TYPE_CHECKING, NamedTuple, Protocol
|
| 5 |
+
|
| 6 |
+
import torch
|
| 7 |
+
|
| 8 |
+
from ltx_core.loader.module_ops import ModuleOps
|
| 9 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 10 |
+
from ltx_core.model.model_protocol import ModelType
|
| 11 |
+
|
| 12 |
+
if TYPE_CHECKING:
|
| 13 |
+
from ltx_core.loader.registry import Registry
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
# Per-key shape and dtype description for a flat collection of tensors.
|
| 17 |
+
TensorLayout = dict[str, tuple[torch.Size, torch.dtype]]
|
| 18 |
+
|
| 19 |
+
|
| 20 |
+
@dataclass(frozen=True)
|
| 21 |
+
class StateDict:
|
| 22 |
+
"""
|
| 23 |
+
Immutable container for a PyTorch state dictionary.
|
| 24 |
+
Contains:
|
| 25 |
+
- sd: Dictionary of tensors (weights, buffers, etc.)
|
| 26 |
+
- device: Device where tensors are stored
|
| 27 |
+
- size: Total memory footprint in bytes
|
| 28 |
+
- dtype: Set of tensor dtypes present
|
| 29 |
+
"""
|
| 30 |
+
|
| 31 |
+
sd: dict
|
| 32 |
+
device: torch.device
|
| 33 |
+
size: int
|
| 34 |
+
dtype: set[torch.dtype]
|
| 35 |
+
|
| 36 |
+
def footprint(self) -> tuple[int, torch.device]:
|
| 37 |
+
return self.size, self.device
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
class StateDictLoader(Protocol):
|
| 41 |
+
"""
|
| 42 |
+
Protocol for loading state dictionaries from various sources.
|
| 43 |
+
Implementations must provide:
|
| 44 |
+
- metadata: Extract model metadata from a single path
|
| 45 |
+
- load: Load state dict from path(s) and apply SDOps transformations
|
| 46 |
+
"""
|
| 47 |
+
|
| 48 |
+
def metadata(self, path: str) -> dict:
|
| 49 |
+
"""
|
| 50 |
+
Load metadata from path
|
| 51 |
+
"""
|
| 52 |
+
|
| 53 |
+
def load(self, path: str | list[str], sd_ops: SDOps | None = None, device: torch.device | None = None) -> StateDict:
|
| 54 |
+
"""
|
| 55 |
+
Load state dict from path or paths (for sharded model storage) and apply sd_ops
|
| 56 |
+
"""
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class BuilderProtocol(Protocol[ModelType]):
|
| 60 |
+
"""Protocol for model builders that produce a model via ``build()``."""
|
| 61 |
+
|
| 62 |
+
def build(
|
| 63 |
+
self, device: torch.device | None = None, dtype: torch.dtype | None = None, **kwargs: object
|
| 64 |
+
) -> ModelType: ...
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
class ModelBuilderProtocol(BuilderProtocol[ModelType], Protocol[ModelType]):
|
| 68 |
+
"""
|
| 69 |
+
Protocol for building PyTorch models from configuration dictionaries.
|
| 70 |
+
Implementations must provide:
|
| 71 |
+
- meta_model: Create a model from configuration dictionary and apply module operations
|
| 72 |
+
- build: Create and initialize a model from state dictionary and apply dtype transformations
|
| 73 |
+
"""
|
| 74 |
+
|
| 75 |
+
model_sd_ops: SDOps | None
|
| 76 |
+
module_ops: tuple[ModuleOps, ...]
|
| 77 |
+
loras: tuple["LoraPathStrengthAndSDOps", ...]
|
| 78 |
+
registry: "Registry"
|
| 79 |
+
|
| 80 |
+
def meta_model(self, config: dict, module_ops: list[ModuleOps] | None = None) -> ModelType:
|
| 81 |
+
"""
|
| 82 |
+
Create a model on the meta device from a configuration dictionary.
|
| 83 |
+
This decouples model creation from weight loading, allowing the model
|
| 84 |
+
architecture to be instantiated without allocating memory for parameters.
|
| 85 |
+
Args:
|
| 86 |
+
config: Model configuration dictionary.
|
| 87 |
+
module_ops: Optional list of module operations to apply (e.g., quantization).
|
| 88 |
+
Returns:
|
| 89 |
+
Model instance on meta device (no actual memory allocated for parameters).
|
| 90 |
+
"""
|
| 91 |
+
...
|
| 92 |
+
|
| 93 |
+
def with_sd_ops(self, sd_ops: SDOps | None) -> "ModelBuilderProtocol[ModelType]":
|
| 94 |
+
"""Return a copy of this builder with the given state-dict key remapping ops."""
|
| 95 |
+
...
|
| 96 |
+
|
| 97 |
+
def with_module_ops(self, module_ops: tuple[ModuleOps, ...]) -> "ModelBuilderProtocol[ModelType]":
|
| 98 |
+
"""Return a copy of this builder with the given module operations (e.g. quantization)."""
|
| 99 |
+
...
|
| 100 |
+
|
| 101 |
+
def with_loras(self, loras: tuple["LoraPathStrengthAndSDOps", ...]) -> "ModelBuilderProtocol[ModelType]":
|
| 102 |
+
"""Return a copy of this builder with the given LoRAs to fuse at build time."""
|
| 103 |
+
...
|
| 104 |
+
|
| 105 |
+
def with_registry(self, registry: "Registry") -> "ModelBuilderProtocol[ModelType]":
|
| 106 |
+
"""Return a copy of this builder using the given weight registry for allocation."""
|
| 107 |
+
...
|
| 108 |
+
|
| 109 |
+
def with_lora_load_device(self, device: torch.device) -> "ModelBuilderProtocol[ModelType]":
|
| 110 |
+
"""Return a copy of this builder that loads LoRA weights onto the given device."""
|
| 111 |
+
...
|
| 112 |
+
|
| 113 |
+
def build(
|
| 114 |
+
self, device: torch.device | None = None, dtype: torch.dtype | None = None, **kwargs: object
|
| 115 |
+
) -> ModelType:
|
| 116 |
+
"""
|
| 117 |
+
Build the model
|
| 118 |
+
Args:
|
| 119 |
+
device: Target device for the model
|
| 120 |
+
dtype: Target dtype for the model, if None, uses the dtype of the model_path model
|
| 121 |
+
Returns:
|
| 122 |
+
Model instance
|
| 123 |
+
"""
|
| 124 |
+
...
|
| 125 |
+
|
| 126 |
+
def model_config(self) -> dict:
|
| 127 |
+
"""Return the model configuration dictionary extracted from the checkpoint metadata."""
|
| 128 |
+
...
|
| 129 |
+
|
| 130 |
+
|
| 131 |
+
class LoRAAdaptableProtocol(Protocol):
|
| 132 |
+
"""
|
| 133 |
+
Protocol for models that can be adapted with LoRAs.
|
| 134 |
+
Implementations must provide:
|
| 135 |
+
- lora: Add a LoRA to the model
|
| 136 |
+
"""
|
| 137 |
+
|
| 138 |
+
def lora(self, lora_path: str, strength: float) -> "LoRAAdaptableProtocol":
|
| 139 |
+
pass
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class LoraPathStrengthAndSDOps(NamedTuple):
|
| 143 |
+
"""
|
| 144 |
+
Tuple containing a LoRA path, strength, and SDOps for applying to the LoRA state dict.
|
| 145 |
+
"""
|
| 146 |
+
|
| 147 |
+
path: str
|
| 148 |
+
strength: float
|
| 149 |
+
sd_ops: SDOps
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
class LoraStateDictWithStrength(NamedTuple):
|
| 153 |
+
"""
|
| 154 |
+
Tuple containing a LoRA state dict and strength for applying to the model.
|
| 155 |
+
"""
|
| 156 |
+
|
| 157 |
+
state_dict: StateDict
|
| 158 |
+
strength: float
|
packages/ltx-core/src/ltx_core/loader/registry.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import hashlib
|
| 2 |
+
import threading
|
| 3 |
+
from dataclasses import dataclass, field
|
| 4 |
+
from pathlib import Path
|
| 5 |
+
from typing import Protocol
|
| 6 |
+
|
| 7 |
+
from ltx_core.loader.primitives import StateDict
|
| 8 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
class Registry(Protocol):
|
| 12 |
+
"""
|
| 13 |
+
Protocol for managing state dictionaries in a registry.
|
| 14 |
+
It is used to store state dictionaries and reuse them later without loading them again.
|
| 15 |
+
Implementations must provide:
|
| 16 |
+
- add: Add a state dictionary to the registry
|
| 17 |
+
- pop: Remove a state dictionary from the registry
|
| 18 |
+
- get: Retrieve a state dictionary from the registry
|
| 19 |
+
- clear: Clear all state dictionaries from the registry
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
def add(self, paths: list[str], sd_ops: SDOps | None, state_dict: StateDict) -> None: ...
|
| 23 |
+
|
| 24 |
+
def pop(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None: ...
|
| 25 |
+
|
| 26 |
+
def get(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None: ...
|
| 27 |
+
|
| 28 |
+
def clear(self) -> None: ...
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
class DummyRegistry(Registry):
|
| 32 |
+
"""
|
| 33 |
+
Dummy registry that does not store state dictionaries.
|
| 34 |
+
"""
|
| 35 |
+
|
| 36 |
+
def add(self, paths: list[str], sd_ops: SDOps | None, state_dict: StateDict) -> None:
|
| 37 |
+
pass
|
| 38 |
+
|
| 39 |
+
def pop(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None:
|
| 40 |
+
pass
|
| 41 |
+
|
| 42 |
+
def get(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None:
|
| 43 |
+
pass
|
| 44 |
+
|
| 45 |
+
def clear(self) -> None:
|
| 46 |
+
pass
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@dataclass
|
| 50 |
+
class StateDictRegistry(Registry):
|
| 51 |
+
"""
|
| 52 |
+
Registry that stores state dictionaries in a dictionary.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
_state_dicts: dict[str, StateDict] = field(default_factory=dict)
|
| 56 |
+
_lock: threading.Lock = field(default_factory=threading.Lock)
|
| 57 |
+
|
| 58 |
+
def _generate_id(self, paths: list[str], sd_ops: SDOps) -> str:
|
| 59 |
+
m = hashlib.sha256()
|
| 60 |
+
parts = [str(Path(p).resolve()) for p in paths]
|
| 61 |
+
if sd_ops is not None:
|
| 62 |
+
parts.append(sd_ops.name)
|
| 63 |
+
m.update("\0".join(parts).encode("utf-8"))
|
| 64 |
+
return m.hexdigest()
|
| 65 |
+
|
| 66 |
+
def add(self, paths: list[str], sd_ops: SDOps | None, state_dict: StateDict) -> str:
|
| 67 |
+
sd_id = self._generate_id(paths, sd_ops)
|
| 68 |
+
with self._lock:
|
| 69 |
+
if sd_id in self._state_dicts:
|
| 70 |
+
raise ValueError(f"State dict retrieved from {paths} with {sd_ops} already added, check with get first")
|
| 71 |
+
self._state_dicts[sd_id] = state_dict
|
| 72 |
+
return sd_id
|
| 73 |
+
|
| 74 |
+
def pop(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None:
|
| 75 |
+
with self._lock:
|
| 76 |
+
return self._state_dicts.pop(self._generate_id(paths, sd_ops), None)
|
| 77 |
+
|
| 78 |
+
def get(self, paths: list[str], sd_ops: SDOps | None) -> StateDict | None:
|
| 79 |
+
with self._lock:
|
| 80 |
+
return self._state_dicts.get(self._generate_id(paths, sd_ops), None)
|
| 81 |
+
|
| 82 |
+
def clear(self) -> None:
|
| 83 |
+
with self._lock:
|
| 84 |
+
self._state_dicts.clear()
|
packages/ltx-core/src/ltx_core/loader/sd_ops.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from dataclasses import dataclass, replace
|
| 2 |
+
from typing import NamedTuple, Protocol
|
| 3 |
+
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
@dataclass(frozen=True, slots=True)
|
| 8 |
+
class ContentReplacement:
|
| 9 |
+
"""
|
| 10 |
+
Represents a content replacement operation.
|
| 11 |
+
Used to replace a specific content with a replacement in a state dict key.
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
content: str
|
| 15 |
+
replacement: str
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
@dataclass(frozen=True, slots=True)
|
| 19 |
+
class ContentMatching:
|
| 20 |
+
"""
|
| 21 |
+
Represents a content matching operation.
|
| 22 |
+
Used to match a specific prefix and suffix in a state dict key.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
prefix: str = ""
|
| 26 |
+
suffix: str = ""
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class KeyValueOperationResult(NamedTuple):
|
| 30 |
+
"""
|
| 31 |
+
Represents the result of a key-value operation.
|
| 32 |
+
Contains the new key and value after the operation has been applied.
|
| 33 |
+
"""
|
| 34 |
+
|
| 35 |
+
new_key: str
|
| 36 |
+
new_value: torch.Tensor
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
class KeyValueOperation(Protocol):
|
| 40 |
+
"""
|
| 41 |
+
Protocol for key-value operations.
|
| 42 |
+
Used to apply operations to a specific key and value in a state dict.
|
| 43 |
+
"""
|
| 44 |
+
|
| 45 |
+
def __call__(self, tensor_key: str, tensor_value: torch.Tensor) -> list[KeyValueOperationResult]: ...
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
@dataclass(frozen=True, slots=True)
|
| 49 |
+
class SDKeyValueOperation:
|
| 50 |
+
"""
|
| 51 |
+
Represents a key-value operation.
|
| 52 |
+
Used to apply operations to a specific key and value in a state dict.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
key_matcher: ContentMatching
|
| 56 |
+
kv_operation: KeyValueOperation
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
@dataclass(frozen=True, slots=True)
|
| 60 |
+
class SDOps:
|
| 61 |
+
"""Immutable class representing state dict key operations."""
|
| 62 |
+
|
| 63 |
+
name: str
|
| 64 |
+
mapping: tuple[
|
| 65 |
+
ContentReplacement | ContentMatching | SDKeyValueOperation, ...
|
| 66 |
+
] = () # Immutable tuple of (key, value) pairs
|
| 67 |
+
allowed_keys: frozenset[str] | None = None
|
| 68 |
+
|
| 69 |
+
def with_replacement(self, content: str, replacement: str) -> "SDOps":
|
| 70 |
+
"""Create a new SDOps instance with the specified replacement added to the mapping."""
|
| 71 |
+
|
| 72 |
+
new_mapping = (*self.mapping, ContentReplacement(content, replacement))
|
| 73 |
+
return replace(self, mapping=new_mapping)
|
| 74 |
+
|
| 75 |
+
def with_matching(self, prefix: str = "", suffix: str = "") -> "SDOps":
|
| 76 |
+
"""Create a new SDOps instance with the specified prefix and suffix matching added to the mapping."""
|
| 77 |
+
|
| 78 |
+
new_mapping = (*self.mapping, ContentMatching(prefix, suffix))
|
| 79 |
+
return replace(self, mapping=new_mapping)
|
| 80 |
+
|
| 81 |
+
def with_additional_allowed_keys(self, keys: frozenset[str]) -> "SDOps":
|
| 82 |
+
"""Create a new SDOps instance that only passes keys present in *keys* (post-replacement).
|
| 83 |
+
If allowed_keys already exists, the sets are merged via union.
|
| 84 |
+
"""
|
| 85 |
+
merged = frozenset(keys) | self.allowed_keys if self.allowed_keys is not None else frozenset(keys)
|
| 86 |
+
return replace(self, allowed_keys=merged)
|
| 87 |
+
|
| 88 |
+
def with_kv_operation(
|
| 89 |
+
self,
|
| 90 |
+
operation: KeyValueOperation,
|
| 91 |
+
key_prefix: str = "",
|
| 92 |
+
key_suffix: str = "",
|
| 93 |
+
) -> "SDOps":
|
| 94 |
+
"""Create a new SDOps instance with the specified value operation added to the mapping."""
|
| 95 |
+
key_matcher = ContentMatching(key_prefix, key_suffix)
|
| 96 |
+
sd_kv_operation = SDKeyValueOperation(key_matcher, operation)
|
| 97 |
+
new_mapping = (*self.mapping, sd_kv_operation)
|
| 98 |
+
return replace(self, mapping=new_mapping)
|
| 99 |
+
|
| 100 |
+
def apply_to_key(self, key: str) -> str | None:
|
| 101 |
+
"""Apply the mapping to the given name."""
|
| 102 |
+
matchers = [content for content in self.mapping if isinstance(content, ContentMatching)]
|
| 103 |
+
valid = any(key.startswith(f.prefix) and key.endswith(f.suffix) for f in matchers)
|
| 104 |
+
if not valid:
|
| 105 |
+
return None
|
| 106 |
+
|
| 107 |
+
for replacement in self.mapping:
|
| 108 |
+
if not isinstance(replacement, ContentReplacement):
|
| 109 |
+
continue
|
| 110 |
+
if replacement.content in key:
|
| 111 |
+
key = key.replace(replacement.content, replacement.replacement)
|
| 112 |
+
|
| 113 |
+
if self.allowed_keys is not None and key not in self.allowed_keys:
|
| 114 |
+
return None
|
| 115 |
+
|
| 116 |
+
return key
|
| 117 |
+
|
| 118 |
+
def apply_to_key_value(self, key: str, value: torch.Tensor) -> list[KeyValueOperationResult]:
|
| 119 |
+
"""Apply the value operation to the given name and associated value."""
|
| 120 |
+
for operation in self.mapping:
|
| 121 |
+
if not isinstance(operation, SDKeyValueOperation):
|
| 122 |
+
continue
|
| 123 |
+
if key.startswith(operation.key_matcher.prefix) and key.endswith(operation.key_matcher.suffix):
|
| 124 |
+
return operation.kv_operation(key, value)
|
| 125 |
+
return [KeyValueOperationResult(key, value)]
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
# Predefined SDOps instances
|
| 129 |
+
LTXV_LORA_COMFY_RENAMING_MAP = (
|
| 130 |
+
SDOps("LTXV_LORA_COMFY_PREFIX_MAP").with_matching().with_replacement("diffusion_model.", "")
|
| 131 |
+
)
|
| 132 |
+
|
| 133 |
+
LTXV_LORA_COMFY_TARGET_MAP = (
|
| 134 |
+
SDOps("LTXV_LORA_COMFY_TARGET_MAP")
|
| 135 |
+
.with_matching()
|
| 136 |
+
.with_replacement("diffusion_model.", "")
|
| 137 |
+
.with_replacement(".lora_A.weight", ".weight")
|
| 138 |
+
.with_replacement(".lora_B.weight", ".weight")
|
| 139 |
+
)
|
packages/ltx-core/src/ltx_core/loader/sft_loader.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import json
|
| 2 |
+
|
| 3 |
+
import safetensors
|
| 4 |
+
import torch
|
| 5 |
+
|
| 6 |
+
from ltx_core.loader.primitives import StateDict, StateDictLoader
|
| 7 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SafetensorsStateDictLoader(StateDictLoader):
|
| 11 |
+
"""
|
| 12 |
+
Loads weights from safetensors files without metadata support.
|
| 13 |
+
Use this for loading raw weight files. For model files that include
|
| 14 |
+
configuration metadata, use SafetensorsModelStateDictLoader instead.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
def metadata(self, path: str) -> dict:
|
| 18 |
+
raise NotImplementedError("Not implemented")
|
| 19 |
+
|
| 20 |
+
def load(self, path: str | list[str], sd_ops: SDOps, device: torch.device | None = None) -> StateDict:
|
| 21 |
+
"""
|
| 22 |
+
Load state dict from path or paths (for sharded model storage) and apply sd_ops
|
| 23 |
+
"""
|
| 24 |
+
sd = {}
|
| 25 |
+
size = 0
|
| 26 |
+
dtype = set()
|
| 27 |
+
device = device or torch.device("cpu")
|
| 28 |
+
model_paths = path if isinstance(path, list) else [path]
|
| 29 |
+
for shard_path in model_paths:
|
| 30 |
+
with safetensors.safe_open(shard_path, framework="pt", device=str(device)) as f:
|
| 31 |
+
safetensor_keys = f.keys()
|
| 32 |
+
for name in safetensor_keys:
|
| 33 |
+
expected_name = name if sd_ops is None else sd_ops.apply_to_key(name)
|
| 34 |
+
if expected_name is None:
|
| 35 |
+
continue
|
| 36 |
+
value = f.get_tensor(name).to(device=device, non_blocking=True, copy=False)
|
| 37 |
+
key_value_pairs = ((expected_name, value),)
|
| 38 |
+
if sd_ops is not None:
|
| 39 |
+
key_value_pairs = sd_ops.apply_to_key_value(expected_name, value)
|
| 40 |
+
for key, value in key_value_pairs:
|
| 41 |
+
size += value.nbytes
|
| 42 |
+
dtype.add(value.dtype)
|
| 43 |
+
sd[key] = value
|
| 44 |
+
|
| 45 |
+
return StateDict(sd=sd, device=device, size=size, dtype=dtype)
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class SafetensorsModelStateDictLoader(StateDictLoader):
|
| 49 |
+
"""
|
| 50 |
+
Loads weights and configuration metadata from safetensors model files.
|
| 51 |
+
Unlike SafetensorsStateDictLoader, this loader can read model configuration
|
| 52 |
+
from the safetensors file metadata via the metadata() method.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(self, weight_loader: SafetensorsStateDictLoader | None = None):
|
| 56 |
+
self.weight_loader = weight_loader if weight_loader is not None else SafetensorsStateDictLoader()
|
| 57 |
+
|
| 58 |
+
def metadata(self, path: str) -> dict:
|
| 59 |
+
with safetensors.safe_open(path, framework="pt") as f:
|
| 60 |
+
meta = f.metadata()
|
| 61 |
+
if meta is None or "config" not in meta:
|
| 62 |
+
return {}
|
| 63 |
+
return json.loads(meta["config"])
|
| 64 |
+
|
| 65 |
+
def load(self, path: str | list[str], sd_ops: SDOps | None = None, device: torch.device | None = None) -> StateDict:
|
| 66 |
+
return self.weight_loader.load(path, sd_ops, device)
|
packages/ltx-core/src/ltx_core/loader/single_gpu_model_builder.py
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import logging
|
| 2 |
+
from dataclasses import dataclass, field, replace
|
| 3 |
+
from typing import Generic
|
| 4 |
+
|
| 5 |
+
import torch
|
| 6 |
+
from torch import nn
|
| 7 |
+
|
| 8 |
+
from ltx_core.loader.fuse_loras import apply_loras
|
| 9 |
+
from ltx_core.loader.helpers import create_meta_model, load_state_dict, read_model_config
|
| 10 |
+
from ltx_core.loader.module_ops import ModuleOps
|
| 11 |
+
from ltx_core.loader.primitives import (
|
| 12 |
+
LoRAAdaptableProtocol,
|
| 13 |
+
LoraPathStrengthAndSDOps,
|
| 14 |
+
LoraStateDictWithStrength,
|
| 15 |
+
ModelBuilderProtocol,
|
| 16 |
+
StateDict,
|
| 17 |
+
StateDictLoader,
|
| 18 |
+
)
|
| 19 |
+
from ltx_core.loader.registry import DummyRegistry, Registry
|
| 20 |
+
from ltx_core.loader.sd_ops import SDOps
|
| 21 |
+
from ltx_core.loader.sft_loader import SafetensorsModelStateDictLoader
|
| 22 |
+
from ltx_core.model.model_protocol import ModelConfigurator, ModelType
|
| 23 |
+
|
| 24 |
+
logger: logging.Logger = logging.getLogger(__name__)
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
def _check_uninitialized(model: nn.Module) -> list[str]:
|
| 28 |
+
"""Return names of any parameters/buffers still on meta device."""
|
| 29 |
+
names = []
|
| 30 |
+
for name, param in model.named_parameters():
|
| 31 |
+
if str(param.device) == "meta":
|
| 32 |
+
names.append(name)
|
| 33 |
+
for name, buf in model.named_buffers():
|
| 34 |
+
if str(buf.device) == "meta":
|
| 35 |
+
names.append(name)
|
| 36 |
+
return names
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _load_model_weights(
|
| 40 |
+
meta_model: nn.Module,
|
| 41 |
+
model_path: str | tuple[str, ...],
|
| 42 |
+
loras: tuple[LoraPathStrengthAndSDOps, ...],
|
| 43 |
+
loader: StateDictLoader,
|
| 44 |
+
registry: Registry,
|
| 45 |
+
device: torch.device,
|
| 46 |
+
dtype: torch.dtype | None,
|
| 47 |
+
model_sd_ops: SDOps | None = None,
|
| 48 |
+
lora_load_device: torch.device | None = None,
|
| 49 |
+
) -> None:
|
| 50 |
+
"""Load base weights and fuse LoRAs into *meta_model* in-place."""
|
| 51 |
+
if lora_load_device is None:
|
| 52 |
+
lora_load_device = device
|
| 53 |
+
|
| 54 |
+
model_sd = load_state_dict(model_path, loader, registry, device, model_sd_ops)
|
| 55 |
+
|
| 56 |
+
lora_strengths = [lora.strength for lora in loras]
|
| 57 |
+
if not lora_strengths or (min(lora_strengths) == 0 and max(lora_strengths) == 0):
|
| 58 |
+
sd = model_sd.sd
|
| 59 |
+
if dtype is not None:
|
| 60 |
+
sd = {key: value.to(dtype=dtype) for key, value in model_sd.sd.items()}
|
| 61 |
+
meta_model.load_state_dict(sd, strict=False, assign=True)
|
| 62 |
+
return
|
| 63 |
+
|
| 64 |
+
lora_state_dicts = [load_state_dict([lora.path], loader, registry, lora_load_device, lora.sd_ops) for lora in loras]
|
| 65 |
+
lora_sd_and_strengths = [
|
| 66 |
+
LoraStateDictWithStrength(sd, strength) for sd, strength in zip(lora_state_dicts, lora_strengths, strict=True)
|
| 67 |
+
]
|
| 68 |
+
final_sd = apply_loras(
|
| 69 |
+
model_sd=model_sd,
|
| 70 |
+
lora_sd_and_strengths=lora_sd_and_strengths,
|
| 71 |
+
dtype=dtype,
|
| 72 |
+
destination_sd=model_sd if isinstance(registry, DummyRegistry) else None,
|
| 73 |
+
)
|
| 74 |
+
meta_model.load_state_dict(final_sd.sd, strict=False, assign=True)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
@dataclass(frozen=True)
|
| 78 |
+
class SingleGPUModelBuilder(Generic[ModelType], ModelBuilderProtocol[ModelType], LoRAAdaptableProtocol):
|
| 79 |
+
"""
|
| 80 |
+
Builder for PyTorch models residing on a single GPU.
|
| 81 |
+
Attributes:
|
| 82 |
+
model_class_configurator: Class responsible for constructing the model from a config dict.
|
| 83 |
+
model_path: Path (or tuple of shard paths) to the model's `.safetensors` checkpoint(s).
|
| 84 |
+
model_sd_ops: Optional state-dict operations applied when loading the model weights.
|
| 85 |
+
module_ops: Sequence of module-level mutations applied to the meta model before weight loading.
|
| 86 |
+
loras: Sequence of LoRA adapters (path, strength, optional sd_ops) to fuse into the model.
|
| 87 |
+
model_loader: Strategy for loading state dicts from disk. Defaults to
|
| 88 |
+
:class:`SafetensorsModelStateDictLoader`.
|
| 89 |
+
registry: Cache for already-loaded state dicts. Defaults to :class:`DummyRegistry` (no caching).
|
| 90 |
+
lora_load_device: Device used when loading LoRA weight tensors from disk. Defaults to
|
| 91 |
+
``torch.device("cpu")``, which keeps LoRA weights in CPU memory and transfers them to
|
| 92 |
+
the target GPU sequentially during fusion, reducing peak GPU memory usage compared to
|
| 93 |
+
loading all LoRA weights directly onto the GPU at once.
|
| 94 |
+
"""
|
| 95 |
+
|
| 96 |
+
model_class_configurator: type[ModelConfigurator[ModelType]]
|
| 97 |
+
model_path: str | tuple[str, ...]
|
| 98 |
+
model_sd_ops: SDOps | None = None
|
| 99 |
+
module_ops: tuple[ModuleOps, ...] = field(default_factory=tuple)
|
| 100 |
+
loras: tuple[LoraPathStrengthAndSDOps, ...] = field(default_factory=tuple)
|
| 101 |
+
model_loader: StateDictLoader = field(default_factory=SafetensorsModelStateDictLoader)
|
| 102 |
+
registry: Registry = field(default_factory=DummyRegistry)
|
| 103 |
+
lora_load_device: torch.device = field(default_factory=lambda: torch.device("cpu"))
|
| 104 |
+
|
| 105 |
+
def lora(self, lora_path: str, strength: float, sd_ops: SDOps) -> "SingleGPUModelBuilder":
|
| 106 |
+
return replace(self, loras=(*self.loras, LoraPathStrengthAndSDOps(lora_path, strength, sd_ops)))
|
| 107 |
+
|
| 108 |
+
def with_sd_ops(self, sd_ops: SDOps | None) -> "SingleGPUModelBuilder":
|
| 109 |
+
return replace(self, model_sd_ops=sd_ops)
|
| 110 |
+
|
| 111 |
+
def with_module_ops(self, module_ops: tuple[ModuleOps, ...]) -> "SingleGPUModelBuilder":
|
| 112 |
+
return replace(self, module_ops=module_ops)
|
| 113 |
+
|
| 114 |
+
def with_loras(self, loras: tuple[LoraPathStrengthAndSDOps, ...]) -> "SingleGPUModelBuilder":
|
| 115 |
+
return replace(self, loras=loras)
|
| 116 |
+
|
| 117 |
+
def with_registry(self, registry: Registry) -> "SingleGPUModelBuilder":
|
| 118 |
+
return replace(self, registry=registry)
|
| 119 |
+
|
| 120 |
+
def with_lora_load_device(self, device: torch.device) -> "SingleGPUModelBuilder":
|
| 121 |
+
return replace(self, lora_load_device=device)
|
| 122 |
+
|
| 123 |
+
def model_config(self) -> dict:
|
| 124 |
+
return read_model_config(self.model_path, self.model_loader)
|
| 125 |
+
|
| 126 |
+
def meta_model(self, config: dict, module_ops: tuple[ModuleOps, ...]) -> ModelType:
|
| 127 |
+
return create_meta_model(self.model_class_configurator, config, module_ops)
|
| 128 |
+
|
| 129 |
+
def load_sd(
|
| 130 |
+
self, paths: list[str], registry: Registry, device: torch.device | None, sd_ops: SDOps | None = None
|
| 131 |
+
) -> StateDict:
|
| 132 |
+
return load_state_dict(paths, self.model_loader, registry, device, sd_ops)
|
| 133 |
+
|
| 134 |
+
def _return_model(self, meta_model: ModelType, device: torch.device) -> ModelType:
|
| 135 |
+
uninitialized = _check_uninitialized(meta_model)
|
| 136 |
+
if uninitialized:
|
| 137 |
+
logger.warning(f"Uninitialized parameters or buffers: {uninitialized}")
|
| 138 |
+
return meta_model
|
| 139 |
+
return meta_model.to(device)
|
| 140 |
+
|
| 141 |
+
def build(
|
| 142 |
+
self,
|
| 143 |
+
device: torch.device | None = None,
|
| 144 |
+
dtype: torch.dtype | None = None,
|
| 145 |
+
**kwargs: object, # noqa: ARG002
|
| 146 |
+
) -> ModelType:
|
| 147 |
+
device = torch.device("cuda") if device is None else device
|
| 148 |
+
config = self.model_config()
|
| 149 |
+
meta_model = self.meta_model(config, self.module_ops)
|
| 150 |
+
|
| 151 |
+
_load_model_weights(
|
| 152 |
+
meta_model=meta_model,
|
| 153 |
+
model_path=self.model_path,
|
| 154 |
+
loras=self.loras,
|
| 155 |
+
loader=self.model_loader,
|
| 156 |
+
registry=self.registry,
|
| 157 |
+
device=device,
|
| 158 |
+
dtype=dtype,
|
| 159 |
+
model_sd_ops=self.model_sd_ops,
|
| 160 |
+
lora_load_device=self.lora_load_device,
|
| 161 |
+
)
|
| 162 |
+
return self._return_model(meta_model, device)
|
packages/ltx-core/src/ltx_core/modality_tiling.py
ADDED
|
@@ -0,0 +1,234 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Video modality tiling helpers.
|
| 2 |
+
Provides :class:`VideoModalityTilingHelper` — a stateless helper that
|
| 3 |
+
tiles and blends video :class:`Modality` token sequences by
|
| 4 |
+
spatial/temporal region. Tile geometry is represented by the existing
|
| 5 |
+
:class:`Tile` NamedTuple from :mod:`ltx_core.tiling`; no distributed
|
| 6 |
+
primitives are required.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from __future__ import annotations
|
| 10 |
+
|
| 11 |
+
from dataclasses import dataclass, replace
|
| 12 |
+
|
| 13 |
+
import torch
|
| 14 |
+
|
| 15 |
+
from ltx_core.model.transformer.modality import Modality
|
| 16 |
+
from ltx_core.tiling import Tile, TileCountConfig, create_tiles, identity_mapping_operation, split_by_count
|
| 17 |
+
from ltx_core.tools import VideoLatentTools
|
| 18 |
+
from ltx_core.types import VideoLatentShape
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
@dataclass(frozen=True)
|
| 22 |
+
class TilingContext:
|
| 23 |
+
"""Opaque context produced by :meth:`VideoModalityTilingHelper.tile_modality`.
|
| 24 |
+
Carries the token-level keep mask and per-conditioning-token blend
|
| 25 |
+
weights needed by :meth:`~VideoModalityTilingHelper.blend`.
|
| 26 |
+
"""
|
| 27 |
+
|
| 28 |
+
keep_mask: torch.Tensor
|
| 29 |
+
cond_blend_weights: torch.Tensor | None
|
| 30 |
+
"""``(num_kept_cond,)`` — weight for each kept conditioning token,
|
| 31 |
+
equal to ``1 / num_tiles_that_keep_this_token``. ``None`` when
|
| 32 |
+
there are no conditioning tokens."""
|
| 33 |
+
|
| 34 |
+
|
| 35 |
+
class VideoModalityTilingHelper:
|
| 36 |
+
"""Stateless helper that tiles and blends video :class:`Modality` sequences.
|
| 37 |
+
Constructed once with a :class:`TileCountConfig` and
|
| 38 |
+
:class:`VideoLatentTools`. Tiles are computed at construction and
|
| 39 |
+
available via the :attr:`tiles` property. Use :meth:`tile_modality`
|
| 40 |
+
and :meth:`blend` with any tile from that list.
|
| 41 |
+
Usage::
|
| 42 |
+
helper = VideoModalityTilingHelper(tiling, video_tools)
|
| 43 |
+
for tile in helper.tiles:
|
| 44 |
+
tiled_mod, ctx = helper.tile_modality(modality, tile)
|
| 45 |
+
result = run_model(tiled_mod)
|
| 46 |
+
helper.blend(result, tile, ctx, output=output)
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
def __init__(self, tiling: TileCountConfig, video_tools: VideoLatentTools) -> None:
|
| 50 |
+
self._patchifier = video_tools.patchifier
|
| 51 |
+
self._latent_shape = video_tools.target_shape
|
| 52 |
+
self._num_generated_tokens = self._patchifier.get_token_count(self._latent_shape)
|
| 53 |
+
self._tiles = create_tiles(
|
| 54 |
+
torch.Size([self._latent_shape.frames, self._latent_shape.height, self._latent_shape.width]),
|
| 55 |
+
splitters=[
|
| 56 |
+
split_by_count(tiling.frames.num_tiles, tiling.frames.overlap),
|
| 57 |
+
split_by_count(tiling.height.num_tiles, tiling.height.overlap),
|
| 58 |
+
split_by_count(tiling.width.num_tiles, tiling.width.overlap),
|
| 59 |
+
],
|
| 60 |
+
mappers=[identity_mapping_operation] * 3,
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
@property
|
| 64 |
+
def tiles(self) -> list[Tile]:
|
| 65 |
+
"""All tiles for the configured tiling layout."""
|
| 66 |
+
return self._tiles
|
| 67 |
+
|
| 68 |
+
# -- tile modality -----------------------------------------------------
|
| 69 |
+
|
| 70 |
+
def tile_modality(
|
| 71 |
+
self, modality: Modality, tile: Tile, *, normalize_positions: bool = True
|
| 72 |
+
) -> tuple[Modality, TilingContext]:
|
| 73 |
+
"""Slice *modality* to the tokens covered by *tile*.
|
| 74 |
+
Selects generated tokens belonging to the tile's spatial region
|
| 75 |
+
and conditioning tokens that overlap with the tile (or have
|
| 76 |
+
negative time coordinates).
|
| 77 |
+
Args:
|
| 78 |
+
normalize_positions: When True, shift all positions so the
|
| 79 |
+
tile's generated tokens start at zero in every dimension.
|
| 80 |
+
Returns:
|
| 81 |
+
A ``(tiled_modality, context)`` tuple. Pass *context* to
|
| 82 |
+
:meth:`blend` together with the model output.
|
| 83 |
+
"""
|
| 84 |
+
keep_mask = self._keep_mask(modality, tile)
|
| 85 |
+
|
| 86 |
+
tile_attention_mask = None
|
| 87 |
+
if modality.attention_mask is not None:
|
| 88 |
+
keep_indices = keep_mask.nonzero(as_tuple=False).squeeze(1)
|
| 89 |
+
tile_attention_mask = modality.attention_mask[:, keep_indices, :][:, :, keep_indices]
|
| 90 |
+
|
| 91 |
+
positions = modality.positions[:, :, keep_mask, :]
|
| 92 |
+
if normalize_positions:
|
| 93 |
+
num_tile_gen = self._tile_generated_token_count(tile)
|
| 94 |
+
gen_pos = positions[:, :, :num_tile_gen, :] # (B, 3, num_tile_gen, 2)
|
| 95 |
+
offset = gen_pos[..., 0].amin(dim=2, keepdim=True).unsqueeze(-1) # (B, 3, 1, 1)
|
| 96 |
+
positions = positions - offset
|
| 97 |
+
|
| 98 |
+
tiled = replace(
|
| 99 |
+
modality,
|
| 100 |
+
latent=modality.latent[:, keep_mask, :],
|
| 101 |
+
timesteps=modality.timesteps[:, keep_mask],
|
| 102 |
+
positions=positions,
|
| 103 |
+
attention_mask=tile_attention_mask,
|
| 104 |
+
)
|
| 105 |
+
|
| 106 |
+
cond_blend_weights = None
|
| 107 |
+
num_total = modality.latent.shape[1]
|
| 108 |
+
if num_total > self._num_generated_tokens:
|
| 109 |
+
cond_keep = keep_mask[self._num_generated_tokens :]
|
| 110 |
+
# Count how many tiles keep each conditioning token.
|
| 111 |
+
cond_counts = torch.zeros(cond_keep.sum(), dtype=torch.float32)
|
| 112 |
+
for t in self._tiles:
|
| 113 |
+
other_mask = self._keep_mask(modality, t)
|
| 114 |
+
other_cond = other_mask[self._num_generated_tokens :]
|
| 115 |
+
# Map other tile's kept cond tokens into this tile's kept subset.
|
| 116 |
+
cond_counts += other_cond[cond_keep].float()
|
| 117 |
+
cond_blend_weights = 1.0 / cond_counts
|
| 118 |
+
|
| 119 |
+
return tiled, TilingContext(keep_mask=keep_mask, cond_blend_weights=cond_blend_weights)
|
| 120 |
+
|
| 121 |
+
# -- blend -------------------------------------------------------------
|
| 122 |
+
|
| 123 |
+
def blend(
|
| 124 |
+
self,
|
| 125 |
+
tile_to_blend: torch.Tensor,
|
| 126 |
+
tile: Tile,
|
| 127 |
+
context: TilingContext,
|
| 128 |
+
output: torch.Tensor | None = None,
|
| 129 |
+
) -> torch.Tensor:
|
| 130 |
+
"""Blend-weight tile results and accumulate into the full token space.
|
| 131 |
+
Premultiplied (blend-weighted) data is **added** to *output*,
|
| 132 |
+
allowing multiple tiles to be accumulated into the same buffer.
|
| 133 |
+
Args:
|
| 134 |
+
tile_to_blend: Denoised tile tensor ``(B, num_tile_tokens, D)``,
|
| 135 |
+
where the first ``_tile_generated_token_count(tile)``
|
| 136 |
+
entries are generated tokens and the remainder are
|
| 137 |
+
conditioning tokens.
|
| 138 |
+
tile: The :class:`Tile` that was used in :meth:`tile_modality`.
|
| 139 |
+
context: The :class:`TilingContext` returned by :meth:`tile_modality`.
|
| 140 |
+
output: Optional pre-allocated output tensor. When provided
|
| 141 |
+
its shape must be ``(B, num_total_tokens, D)`` and the
|
| 142 |
+
blended tile is **added** into it. When ``None`` a new
|
| 143 |
+
zero-filled tensor is created.
|
| 144 |
+
Returns:
|
| 145 |
+
The output tensor with the blended tile added at the correct
|
| 146 |
+
positions.
|
| 147 |
+
"""
|
| 148 |
+
batch, _, dim = tile_to_blend.shape
|
| 149 |
+
num_tile_gen = self._tile_generated_token_count(tile)
|
| 150 |
+
gen_indices = self._generated_token_indices(tile)
|
| 151 |
+
|
| 152 |
+
num_total_tokens = context.keep_mask.shape[0]
|
| 153 |
+
expected_shape = (batch, num_total_tokens, dim)
|
| 154 |
+
|
| 155 |
+
if output is not None:
|
| 156 |
+
if output.shape != expected_shape:
|
| 157 |
+
raise ValueError(f"Expected output shape {expected_shape}, got {output.shape}")
|
| 158 |
+
result = output
|
| 159 |
+
else:
|
| 160 |
+
result = torch.zeros(*expected_shape, device=tile_to_blend.device, dtype=tile_to_blend.dtype)
|
| 161 |
+
|
| 162 |
+
# Blend mask is (tile_F, tile_H, tile_W) — one weight per token in row-major order.
|
| 163 |
+
blend_weights = tile.blend_mask.reshape(-1).to(device=tile_to_blend.device, dtype=tile_to_blend.dtype)
|
| 164 |
+
tile_gen = tile_to_blend[:, :num_tile_gen, :] * blend_weights[None, :, None]
|
| 165 |
+
|
| 166 |
+
result[:, gen_indices, :] += tile_gen
|
| 167 |
+
|
| 168 |
+
# Scatter kept conditioning tokens, weighted by 1/N where N is
|
| 169 |
+
# the number of tiles that keep each token (so they sum to 1).
|
| 170 |
+
if num_total_tokens > self._num_generated_tokens and context.cond_blend_weights is not None:
|
| 171 |
+
cond_keep = context.keep_mask[self._num_generated_tokens :]
|
| 172 |
+
cond_indices = self._num_generated_tokens + cond_keep.nonzero(as_tuple=False).squeeze(1)
|
| 173 |
+
weights = context.cond_blend_weights.to(device=tile_to_blend.device, dtype=tile_to_blend.dtype)
|
| 174 |
+
result[:, cond_indices, :] += tile_to_blend[:, num_tile_gen:, :] * weights[None, :, None]
|
| 175 |
+
|
| 176 |
+
return result
|
| 177 |
+
|
| 178 |
+
# -- private -----------------------------------------------------------
|
| 179 |
+
|
| 180 |
+
def _tile_generated_token_count(self, tile: Tile) -> int:
|
| 181 |
+
"""Number of generated tokens in *tile*."""
|
| 182 |
+
frame_slice, height_slice, width_slice = tile.in_coords
|
| 183 |
+
tile_shape = VideoLatentShape(
|
| 184 |
+
batch=self._latent_shape.batch,
|
| 185 |
+
channels=self._latent_shape.channels,
|
| 186 |
+
frames=frame_slice.stop - frame_slice.start,
|
| 187 |
+
height=height_slice.stop - height_slice.start,
|
| 188 |
+
width=width_slice.stop - width_slice.start,
|
| 189 |
+
)
|
| 190 |
+
return self._patchifier.get_token_count(tile_shape)
|
| 191 |
+
|
| 192 |
+
def _generated_token_indices(self, tile: Tile) -> torch.Tensor:
|
| 193 |
+
"""Flat token indices of *tile*'s generated tokens in the full sequence."""
|
| 194 |
+
frame_slice, height_slice, width_slice = tile.in_coords
|
| 195 |
+
f = torch.arange(frame_slice.start, frame_slice.stop)
|
| 196 |
+
h = torch.arange(height_slice.start, height_slice.stop)
|
| 197 |
+
w = torch.arange(width_slice.start, width_slice.stop)
|
| 198 |
+
return (
|
| 199 |
+
f[:, None, None] * self._latent_shape.height * self._latent_shape.width
|
| 200 |
+
+ h[None, :, None] * self._latent_shape.width
|
| 201 |
+
+ w[None, None, :]
|
| 202 |
+
).reshape(-1)
|
| 203 |
+
|
| 204 |
+
def _keep_mask(self, modality: Modality, tile: Tile) -> torch.Tensor:
|
| 205 |
+
"""Boolean mask ``(num_total_tokens,)`` — True for tokens the tile processes.
|
| 206 |
+
Generated tokens are selected by grid position. Conditioning
|
| 207 |
+
tokens are kept when their ``[start, end)`` intervals overlap
|
| 208 |
+
the tile in all three dimensions, or when they have a negative
|
| 209 |
+
time coordinate (reference tokens).
|
| 210 |
+
"""
|
| 211 |
+
num_total = modality.latent.shape[1]
|
| 212 |
+
mask = torch.zeros(num_total, dtype=torch.bool)
|
| 213 |
+
|
| 214 |
+
gen_indices = self._generated_token_indices(tile)
|
| 215 |
+
mask[gen_indices] = True
|
| 216 |
+
|
| 217 |
+
if num_total > self._num_generated_tokens:
|
| 218 |
+
gen_positions = modality.positions[:, :, gen_indices, :] # (B, 3, num_tile_gen, 2)
|
| 219 |
+
tile_start = gen_positions[..., 0].amin(dim=2) # (B, 3)
|
| 220 |
+
tile_end = gen_positions[..., 1].amax(dim=2) # (B, 3)
|
| 221 |
+
|
| 222 |
+
cond_positions = modality.positions[:, :, self._num_generated_tokens :, :] # (B, 3, num_cond, 2)
|
| 223 |
+
|
| 224 |
+
overlaps = (cond_positions[..., 0] < tile_end.unsqueeze(2)) & (
|
| 225 |
+
cond_positions[..., 1] > tile_start.unsqueeze(2)
|
| 226 |
+
) # (B, 3, num_cond)
|
| 227 |
+
overlaps_all_dims = overlaps.all(dim=1) # (B, num_cond)
|
| 228 |
+
|
| 229 |
+
has_negative_time = cond_positions[:, 0, :, 0] < 0 # (B, num_cond)
|
| 230 |
+
|
| 231 |
+
keep_cond = (overlaps_all_dims | has_negative_time).any(dim=0) # (num_cond,)
|
| 232 |
+
mask[self._num_generated_tokens :] = keep_cond
|
| 233 |
+
|
| 234 |
+
return mask
|
packages/ltx-core/src/ltx_core/model/__init__.py
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Model definitions for LTX-2."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.model.model_protocol import ModelConfigurator, ModelType
|
| 4 |
+
|
| 5 |
+
__all__ = [
|
| 6 |
+
"ModelConfigurator",
|
| 7 |
+
"ModelType",
|
| 8 |
+
]
|
packages/ltx-core/src/ltx_core/model/audio_vae/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Audio VAE model components."""
|
| 2 |
+
|
| 3 |
+
from ltx_core.model.audio_vae.audio_vae import AudioDecoder, AudioEncoder, decode_audio, encode_audio
|
| 4 |
+
from ltx_core.model.audio_vae.model_configurator import (
|
| 5 |
+
AUDIO_VAE_DECODER_COMFY_KEYS_FILTER,
|
| 6 |
+
AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER,
|
| 7 |
+
VOCODER_COMFY_KEYS_FILTER,
|
| 8 |
+
AudioDecoderConfigurator,
|
| 9 |
+
AudioEncoderConfigurator,
|
| 10 |
+
VocoderConfigurator,
|
| 11 |
+
)
|
| 12 |
+
from ltx_core.model.audio_vae.ops import AudioProcessor
|
| 13 |
+
from ltx_core.model.audio_vae.vocoder import Vocoder, VocoderWithBWE
|
| 14 |
+
|
| 15 |
+
__all__ = [
|
| 16 |
+
"AUDIO_VAE_DECODER_COMFY_KEYS_FILTER",
|
| 17 |
+
"AUDIO_VAE_ENCODER_COMFY_KEYS_FILTER",
|
| 18 |
+
"VOCODER_COMFY_KEYS_FILTER",
|
| 19 |
+
"AudioDecoder",
|
| 20 |
+
"AudioDecoderConfigurator",
|
| 21 |
+
"AudioEncoder",
|
| 22 |
+
"AudioEncoderConfigurator",
|
| 23 |
+
"AudioProcessor",
|
| 24 |
+
"Vocoder",
|
| 25 |
+
"VocoderConfigurator",
|
| 26 |
+
"VocoderWithBWE",
|
| 27 |
+
"decode_audio",
|
| 28 |
+
"encode_audio",
|
| 29 |
+
]
|
packages/ltx-core/src/ltx_core/model/audio_vae/attention.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from enum import Enum
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
|
| 5 |
+
from ltx_core.model.common.normalization import NormType, build_normalization_layer
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class AttentionType(Enum):
|
| 9 |
+
"""Enum for specifying the attention mechanism type."""
|
| 10 |
+
|
| 11 |
+
VANILLA = "vanilla"
|
| 12 |
+
LINEAR = "linear"
|
| 13 |
+
NONE = "none"
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class AttnBlock(torch.nn.Module):
|
| 17 |
+
def __init__(
|
| 18 |
+
self,
|
| 19 |
+
in_channels: int,
|
| 20 |
+
norm_type: NormType = NormType.GROUP,
|
| 21 |
+
) -> None:
|
| 22 |
+
super().__init__()
|
| 23 |
+
self.in_channels = in_channels
|
| 24 |
+
|
| 25 |
+
self.norm = build_normalization_layer(in_channels, normtype=norm_type)
|
| 26 |
+
self.q = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
| 27 |
+
self.k = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
| 28 |
+
self.v = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
| 29 |
+
self.proj_out = torch.nn.Conv2d(in_channels, in_channels, kernel_size=1, stride=1, padding=0)
|
| 30 |
+
|
| 31 |
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
| 32 |
+
h_ = x
|
| 33 |
+
h_ = self.norm(h_)
|
| 34 |
+
q = self.q(h_)
|
| 35 |
+
k = self.k(h_)
|
| 36 |
+
v = self.v(h_)
|
| 37 |
+
|
| 38 |
+
# compute attention
|
| 39 |
+
b, c, h, w = q.shape
|
| 40 |
+
q = q.reshape(b, c, h * w).contiguous()
|
| 41 |
+
q = q.permute(0, 2, 1).contiguous() # b,hw,c
|
| 42 |
+
k = k.reshape(b, c, h * w).contiguous() # b,c,hw
|
| 43 |
+
w_ = torch.bmm(q, k).contiguous() # b,hw,hw w[b,i,j]=sum_c q[b,i,c]k[b,c,j]
|
| 44 |
+
w_ = w_ * (int(c) ** (-0.5))
|
| 45 |
+
w_ = torch.nn.functional.softmax(w_, dim=2)
|
| 46 |
+
|
| 47 |
+
# attend to values
|
| 48 |
+
v = v.reshape(b, c, h * w).contiguous()
|
| 49 |
+
w_ = w_.permute(0, 2, 1).contiguous() # b,hw,hw (first hw of k, second of q)
|
| 50 |
+
h_ = torch.bmm(v, w_).contiguous() # b, c,hw (hw of q) h_[b,c,j] = sum_i v[b,c,i] w_[b,i,j]
|
| 51 |
+
h_ = h_.reshape(b, c, h, w).contiguous()
|
| 52 |
+
|
| 53 |
+
h_ = self.proj_out(h_)
|
| 54 |
+
|
| 55 |
+
return x + h_
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def make_attn(
|
| 59 |
+
in_channels: int,
|
| 60 |
+
attn_type: AttentionType = AttentionType.VANILLA,
|
| 61 |
+
norm_type: NormType = NormType.GROUP,
|
| 62 |
+
) -> torch.nn.Module:
|
| 63 |
+
match attn_type:
|
| 64 |
+
case AttentionType.VANILLA:
|
| 65 |
+
return AttnBlock(in_channels, norm_type=norm_type)
|
| 66 |
+
case AttentionType.NONE:
|
| 67 |
+
return torch.nn.Identity()
|
| 68 |
+
case AttentionType.LINEAR:
|
| 69 |
+
raise NotImplementedError(f"Attention type {attn_type.value} is not supported yet.")
|
| 70 |
+
case _:
|
| 71 |
+
raise ValueError(f"Unknown attention type: {attn_type}")
|
packages/ltx-core/src/ltx_core/model/audio_vae/audio_vae.py
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from typing import Set, Tuple
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn.functional as F
|
| 5 |
+
|
| 6 |
+
from ltx_core.components.patchifiers import AudioPatchifier
|
| 7 |
+
from ltx_core.model.audio_vae.attention import AttentionType, make_attn
|
| 8 |
+
from ltx_core.model.audio_vae.causal_conv_2d import make_conv2d
|
| 9 |
+
from ltx_core.model.audio_vae.causality_axis import CausalityAxis
|
| 10 |
+
from ltx_core.model.audio_vae.downsample import build_downsampling_path
|
| 11 |
+
from ltx_core.model.audio_vae.ops import AudioProcessor, PerChannelStatistics
|
| 12 |
+
from ltx_core.model.audio_vae.resnet import ResnetBlock
|
| 13 |
+
from ltx_core.model.audio_vae.upsample import build_upsampling_path
|
| 14 |
+
from ltx_core.model.audio_vae.vocoder import Vocoder
|
| 15 |
+
from ltx_core.model.common.normalization import NormType, build_normalization_layer
|
| 16 |
+
from ltx_core.types import Audio, AudioLatentShape
|
| 17 |
+
|
| 18 |
+
LATENT_DOWNSAMPLE_FACTOR = 4
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def build_mid_block(
|
| 22 |
+
channels: int,
|
| 23 |
+
temb_channels: int,
|
| 24 |
+
dropout: float,
|
| 25 |
+
norm_type: NormType,
|
| 26 |
+
causality_axis: CausalityAxis,
|
| 27 |
+
attn_type: AttentionType,
|
| 28 |
+
add_attention: bool,
|
| 29 |
+
) -> torch.nn.Module:
|
| 30 |
+
"""Build the middle block with two ResNet blocks and optional attention."""
|
| 31 |
+
mid = torch.nn.Module()
|
| 32 |
+
mid.block_1 = ResnetBlock(
|
| 33 |
+
in_channels=channels,
|
| 34 |
+
out_channels=channels,
|
| 35 |
+
temb_channels=temb_channels,
|
| 36 |
+
dropout=dropout,
|
| 37 |
+
norm_type=norm_type,
|
| 38 |
+
causality_axis=causality_axis,
|
| 39 |
+
)
|
| 40 |
+
mid.attn_1 = make_attn(channels, attn_type=attn_type, norm_type=norm_type) if add_attention else torch.nn.Identity()
|
| 41 |
+
mid.block_2 = ResnetBlock(
|
| 42 |
+
in_channels=channels,
|
| 43 |
+
out_channels=channels,
|
| 44 |
+
temb_channels=temb_channels,
|
| 45 |
+
dropout=dropout,
|
| 46 |
+
norm_type=norm_type,
|
| 47 |
+
causality_axis=causality_axis,
|
| 48 |
+
)
|
| 49 |
+
return mid
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def run_mid_block(mid: torch.nn.Module, features: torch.Tensor) -> torch.Tensor:
|
| 53 |
+
"""Run features through the middle block."""
|
| 54 |
+
features = mid.block_1(features, temb=None)
|
| 55 |
+
features = mid.attn_1(features)
|
| 56 |
+
return mid.block_2(features, temb=None)
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class AudioEncoder(torch.nn.Module):
|
| 60 |
+
"""
|
| 61 |
+
Encoder that compresses audio spectrograms into latent representations.
|
| 62 |
+
The encoder uses a series of downsampling blocks with residual connections,
|
| 63 |
+
attention mechanisms, and configurable causal convolutions.
|
| 64 |
+
"""
|
| 65 |
+
|
| 66 |
+
def __init__( # noqa: PLR0913
|
| 67 |
+
self,
|
| 68 |
+
*,
|
| 69 |
+
ch: int,
|
| 70 |
+
ch_mult: Tuple[int, ...] = (1, 2, 4, 8),
|
| 71 |
+
num_res_blocks: int,
|
| 72 |
+
attn_resolutions: Set[int],
|
| 73 |
+
dropout: float = 0.0,
|
| 74 |
+
resamp_with_conv: bool = True,
|
| 75 |
+
in_channels: int,
|
| 76 |
+
resolution: int,
|
| 77 |
+
z_channels: int,
|
| 78 |
+
double_z: bool = True,
|
| 79 |
+
attn_type: AttentionType = AttentionType.VANILLA,
|
| 80 |
+
mid_block_add_attention: bool = True,
|
| 81 |
+
norm_type: NormType = NormType.GROUP,
|
| 82 |
+
causality_axis: CausalityAxis = CausalityAxis.WIDTH,
|
| 83 |
+
sample_rate: int = 16000,
|
| 84 |
+
mel_hop_length: int = 160,
|
| 85 |
+
n_fft: int = 1024,
|
| 86 |
+
is_causal: bool = True,
|
| 87 |
+
mel_bins: int = 64,
|
| 88 |
+
**_ignore_kwargs,
|
| 89 |
+
) -> None:
|
| 90 |
+
"""
|
| 91 |
+
Initialize the Encoder.
|
| 92 |
+
Args:
|
| 93 |
+
Arguments are configuration parameters, loaded from the audio VAE checkpoint config
|
| 94 |
+
(audio_vae.model.params.ddconfig):
|
| 95 |
+
ch: Base number of feature channels used in the first convolution layer.
|
| 96 |
+
ch_mult: Multiplicative factors for the number of channels at each resolution level.
|
| 97 |
+
num_res_blocks: Number of residual blocks to use at each resolution level.
|
| 98 |
+
attn_resolutions: Spatial resolutions (e.g., in time/frequency) at which to apply attention.
|
| 99 |
+
resolution: Input spatial resolution of the spectrogram (height, width).
|
| 100 |
+
z_channels: Number of channels in the latent representation.
|
| 101 |
+
norm_type: Normalization layer type to use within the network (e.g., group, batch).
|
| 102 |
+
causality_axis: Axis along which convolutions should be causal (e.g., time axis).
|
| 103 |
+
sample_rate: Audio sample rate in Hz for the input signals.
|
| 104 |
+
mel_hop_length: Hop length used when computing the mel spectrogram.
|
| 105 |
+
n_fft: FFT size used to compute the spectrogram.
|
| 106 |
+
mel_bins: Number of mel-frequency bins in the input spectrogram.
|
| 107 |
+
in_channels: Number of channels in the input spectrogram tensor.
|
| 108 |
+
double_z: If True, predict both mean and log-variance (doubling latent channels).
|
| 109 |
+
is_causal: If True, use causal convolutions suitable for streaming setups.
|
| 110 |
+
dropout: Dropout probability used in residual and mid blocks.
|
| 111 |
+
attn_type: Type of attention mechanism to use in attention blocks.
|
| 112 |
+
resamp_with_conv: If True, perform resolution changes using strided convolutions.
|
| 113 |
+
mid_block_add_attention: If True, add an attention block in the mid-level of the encoder.
|
| 114 |
+
"""
|
| 115 |
+
super().__init__()
|
| 116 |
+
|
| 117 |
+
self.per_channel_statistics = PerChannelStatistics(latent_channels=ch)
|
| 118 |
+
self.sample_rate = sample_rate
|
| 119 |
+
self.mel_hop_length = mel_hop_length
|
| 120 |
+
self.n_fft = n_fft
|
| 121 |
+
self.is_causal = is_causal
|
| 122 |
+
self.mel_bins = mel_bins
|
| 123 |
+
|
| 124 |
+
self.patchifier = AudioPatchifier(
|
| 125 |
+
patch_size=1,
|
| 126 |
+
audio_latent_downsample_factor=LATENT_DOWNSAMPLE_FACTOR,
|
| 127 |
+
sample_rate=sample_rate,
|
| 128 |
+
hop_length=mel_hop_length,
|
| 129 |
+
is_causal=is_causal,
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
self.ch = ch
|
| 133 |
+
self.temb_ch = 0
|
| 134 |
+
self.num_resolutions = len(ch_mult)
|
| 135 |
+
self.num_res_blocks = num_res_blocks
|
| 136 |
+
self.resolution = resolution
|
| 137 |
+
self.in_channels = in_channels
|
| 138 |
+
self.z_channels = z_channels
|
| 139 |
+
self.double_z = double_z
|
| 140 |
+
self.norm_type = norm_type
|
| 141 |
+
self.causality_axis = causality_axis
|
| 142 |
+
self.attn_type = attn_type
|
| 143 |
+
|
| 144 |
+
# downsampling
|
| 145 |
+
self.conv_in = make_conv2d(
|
| 146 |
+
in_channels,
|
| 147 |
+
self.ch,
|
| 148 |
+
kernel_size=3,
|
| 149 |
+
stride=1,
|
| 150 |
+
causality_axis=self.causality_axis,
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
self.non_linearity = torch.nn.SiLU()
|
| 154 |
+
|
| 155 |
+
self.down, block_in = build_downsampling_path(
|
| 156 |
+
ch=ch,
|
| 157 |
+
ch_mult=ch_mult,
|
| 158 |
+
num_resolutions=self.num_resolutions,
|
| 159 |
+
num_res_blocks=num_res_blocks,
|
| 160 |
+
resolution=resolution,
|
| 161 |
+
temb_channels=self.temb_ch,
|
| 162 |
+
dropout=dropout,
|
| 163 |
+
norm_type=self.norm_type,
|
| 164 |
+
causality_axis=self.causality_axis,
|
| 165 |
+
attn_type=self.attn_type,
|
| 166 |
+
attn_resolutions=attn_resolutions,
|
| 167 |
+
resamp_with_conv=resamp_with_conv,
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
self.mid = build_mid_block(
|
| 171 |
+
channels=block_in,
|
| 172 |
+
temb_channels=self.temb_ch,
|
| 173 |
+
dropout=dropout,
|
| 174 |
+
norm_type=self.norm_type,
|
| 175 |
+
causality_axis=self.causality_axis,
|
| 176 |
+
attn_type=self.attn_type,
|
| 177 |
+
add_attention=mid_block_add_attention,
|
| 178 |
+
)
|
| 179 |
+
|
| 180 |
+
self.norm_out = build_normalization_layer(block_in, normtype=self.norm_type)
|
| 181 |
+
self.conv_out = make_conv2d(
|
| 182 |
+
block_in,
|
| 183 |
+
2 * z_channels if double_z else z_channels,
|
| 184 |
+
kernel_size=3,
|
| 185 |
+
stride=1,
|
| 186 |
+
causality_axis=self.causality_axis,
|
| 187 |
+
)
|
| 188 |
+
|
| 189 |
+
def forward(self, spectrogram: torch.Tensor) -> torch.Tensor:
|
| 190 |
+
"""
|
| 191 |
+
Encode audio spectrogram into latent representations.
|
| 192 |
+
Args:
|
| 193 |
+
spectrogram: Input spectrogram of shape (batch, channels, time, frequency)
|
| 194 |
+
Returns:
|
| 195 |
+
Encoded latent representation of shape (batch, channels, frames, mel_bins)
|
| 196 |
+
"""
|
| 197 |
+
h = self.conv_in(spectrogram)
|
| 198 |
+
h = self._run_downsampling_path(h)
|
| 199 |
+
h = run_mid_block(self.mid, h)
|
| 200 |
+
h = self._finalize_output(h)
|
| 201 |
+
|
| 202 |
+
return self._normalize_latents(h)
|
| 203 |
+
|
| 204 |
+
def _run_downsampling_path(self, h: torch.Tensor) -> torch.Tensor:
|
| 205 |
+
for level in range(self.num_resolutions):
|
| 206 |
+
stage = self.down[level]
|
| 207 |
+
for block_idx in range(self.num_res_blocks):
|
| 208 |
+
h = stage.block[block_idx](h, temb=None)
|
| 209 |
+
if stage.attn:
|
| 210 |
+
h = stage.attn[block_idx](h)
|
| 211 |
+
|
| 212 |
+
if level != self.num_resolutions - 1:
|
| 213 |
+
h = stage.downsample(h)
|
| 214 |
+
|
| 215 |
+
return h
|
| 216 |
+
|
| 217 |
+
def _finalize_output(self, h: torch.Tensor) -> torch.Tensor:
|
| 218 |
+
h = self.norm_out(h)
|
| 219 |
+
h = self.non_linearity(h)
|
| 220 |
+
return self.conv_out(h)
|
| 221 |
+
|
| 222 |
+
def _normalize_latents(self, latent_output: torch.Tensor) -> torch.Tensor:
|
| 223 |
+
"""
|
| 224 |
+
Normalize encoder latents using per-channel statistics.
|
| 225 |
+
When the encoder is configured with ``double_z=True``, the final
|
| 226 |
+
convolution produces twice the number of latent channels, typically
|
| 227 |
+
interpreted as two concatenated tensors along the channel dimension
|
| 228 |
+
(e.g., mean and variance or other auxiliary parameters).
|
| 229 |
+
This method intentionally uses only the first half of the channels
|
| 230 |
+
(the "mean" component) as input to the patchifier and normalization
|
| 231 |
+
logic. The remaining channels are left unchanged by this method and
|
| 232 |
+
are expected to be consumed elsewhere in the VAE pipeline.
|
| 233 |
+
If ``double_z=False``, the encoder output already contains only the
|
| 234 |
+
mean latents and the chunking operation simply returns that tensor.
|
| 235 |
+
"""
|
| 236 |
+
means = torch.chunk(latent_output, 2, dim=1)[0]
|
| 237 |
+
latent_shape = AudioLatentShape(
|
| 238 |
+
batch=means.shape[0],
|
| 239 |
+
channels=means.shape[1],
|
| 240 |
+
frames=means.shape[2],
|
| 241 |
+
mel_bins=means.shape[3],
|
| 242 |
+
)
|
| 243 |
+
latent_patched = self.patchifier.patchify(means)
|
| 244 |
+
latent_normalized = self.per_channel_statistics.normalize(latent_patched)
|
| 245 |
+
return self.patchifier.unpatchify(latent_normalized, latent_shape)
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def encode_audio(
|
| 249 |
+
audio: Audio,
|
| 250 |
+
audio_encoder: AudioEncoder,
|
| 251 |
+
audio_processor: AudioProcessor | None = None,
|
| 252 |
+
) -> torch.Tensor:
|
| 253 |
+
"""Encode audio waveform into latent representation.
|
| 254 |
+
Args:
|
| 255 |
+
audio: Audio container with waveform tensor of shape (batch, channels, samples) and sampling rate.
|
| 256 |
+
audio_encoder: Audio encoder model
|
| 257 |
+
audio_processor: Audio processor model (optional, if not provided, it will be created from the audio encoder)
|
| 258 |
+
"""
|
| 259 |
+
dtype = next(audio_encoder.parameters()).dtype
|
| 260 |
+
device = next(audio_encoder.parameters()).device
|
| 261 |
+
|
| 262 |
+
if audio_processor is None:
|
| 263 |
+
audio_processor = AudioProcessor(
|
| 264 |
+
target_sample_rate=audio_encoder.sample_rate,
|
| 265 |
+
mel_bins=audio_encoder.mel_bins,
|
| 266 |
+
mel_hop_length=audio_encoder.mel_hop_length,
|
| 267 |
+
n_fft=audio_encoder.n_fft,
|
| 268 |
+
).to(device=device)
|
| 269 |
+
|
| 270 |
+
mel_spectrogram = audio_processor.waveform_to_mel(audio.to(device=device))
|
| 271 |
+
|
| 272 |
+
latent = audio_encoder(mel_spectrogram.to(dtype=dtype))
|
| 273 |
+
return latent
|
| 274 |
+
|
| 275 |
+
|
| 276 |
+
class AudioDecoder(torch.nn.Module):
|
| 277 |
+
"""
|
| 278 |
+
Symmetric decoder that reconstructs audio spectrograms from latent features.
|
| 279 |
+
The decoder mirrors the encoder structure with configurable channel multipliers,
|
| 280 |
+
attention resolutions, and causal convolutions.
|
| 281 |
+
"""
|
| 282 |
+
|
| 283 |
+
def __init__( # noqa: PLR0913
|
| 284 |
+
self,
|
| 285 |
+
*,
|
| 286 |
+
ch: int,
|
| 287 |
+
out_ch: int,
|
| 288 |
+
ch_mult: Tuple[int, ...] = (1, 2, 4, 8),
|
| 289 |
+
num_res_blocks: int,
|
| 290 |
+
attn_resolutions: Set[int],
|
| 291 |
+
resolution: int,
|
| 292 |
+
z_channels: int,
|
| 293 |
+
norm_type: NormType = NormType.GROUP,
|
| 294 |
+
causality_axis: CausalityAxis = CausalityAxis.WIDTH,
|
| 295 |
+
dropout: float = 0.0,
|
| 296 |
+
mid_block_add_attention: bool = True,
|
| 297 |
+
sample_rate: int = 16000,
|
| 298 |
+
mel_hop_length: int = 160,
|
| 299 |
+
is_causal: bool = True,
|
| 300 |
+
mel_bins: int | None = None,
|
| 301 |
+
) -> None:
|
| 302 |
+
"""
|
| 303 |
+
Initialize the Decoder.
|
| 304 |
+
Args:
|
| 305 |
+
Arguments are configuration parameters, loaded from the audio VAE checkpoint config
|
| 306 |
+
(audio_vae.model.params.ddconfig):
|
| 307 |
+
- ch, out_ch, ch_mult, num_res_blocks, attn_resolutions
|
| 308 |
+
- resolution, z_channels
|
| 309 |
+
- norm_type, causality_axis
|
| 310 |
+
"""
|
| 311 |
+
super().__init__()
|
| 312 |
+
|
| 313 |
+
# Internal behavioural defaults that are not driven by the checkpoint.
|
| 314 |
+
resamp_with_conv = True
|
| 315 |
+
attn_type = AttentionType.VANILLA
|
| 316 |
+
|
| 317 |
+
# Per-channel statistics for denormalizing latents
|
| 318 |
+
self.per_channel_statistics = PerChannelStatistics(latent_channels=ch)
|
| 319 |
+
self.sample_rate = sample_rate
|
| 320 |
+
self.mel_hop_length = mel_hop_length
|
| 321 |
+
self.is_causal = is_causal
|
| 322 |
+
self.mel_bins = mel_bins
|
| 323 |
+
self.patchifier = AudioPatchifier(
|
| 324 |
+
patch_size=1,
|
| 325 |
+
audio_latent_downsample_factor=LATENT_DOWNSAMPLE_FACTOR,
|
| 326 |
+
sample_rate=sample_rate,
|
| 327 |
+
hop_length=mel_hop_length,
|
| 328 |
+
is_causal=is_causal,
|
| 329 |
+
)
|
| 330 |
+
|
| 331 |
+
self.ch = ch
|
| 332 |
+
self.temb_ch = 0
|
| 333 |
+
self.num_resolutions = len(ch_mult)
|
| 334 |
+
self.num_res_blocks = num_res_blocks
|
| 335 |
+
self.resolution = resolution
|
| 336 |
+
self.out_ch = out_ch
|
| 337 |
+
self.give_pre_end = False
|
| 338 |
+
self.tanh_out = False
|
| 339 |
+
self.norm_type = norm_type
|
| 340 |
+
self.z_channels = z_channels
|
| 341 |
+
self.channel_multipliers = ch_mult
|
| 342 |
+
self.attn_resolutions = attn_resolutions
|
| 343 |
+
self.causality_axis = causality_axis
|
| 344 |
+
self.attn_type = attn_type
|
| 345 |
+
|
| 346 |
+
base_block_channels = ch * self.channel_multipliers[-1]
|
| 347 |
+
base_resolution = resolution // (2 ** (self.num_resolutions - 1))
|
| 348 |
+
self.z_shape = (1, z_channels, base_resolution, base_resolution)
|
| 349 |
+
|
| 350 |
+
self.conv_in = make_conv2d(
|
| 351 |
+
z_channels, base_block_channels, kernel_size=3, stride=1, causality_axis=self.causality_axis
|
| 352 |
+
)
|
| 353 |
+
self.non_linearity = torch.nn.SiLU()
|
| 354 |
+
self.mid = build_mid_block(
|
| 355 |
+
channels=base_block_channels,
|
| 356 |
+
temb_channels=self.temb_ch,
|
| 357 |
+
dropout=dropout,
|
| 358 |
+
norm_type=self.norm_type,
|
| 359 |
+
causality_axis=self.causality_axis,
|
| 360 |
+
attn_type=self.attn_type,
|
| 361 |
+
add_attention=mid_block_add_attention,
|
| 362 |
+
)
|
| 363 |
+
self.up, final_block_channels = build_upsampling_path(
|
| 364 |
+
ch=ch,
|
| 365 |
+
ch_mult=ch_mult,
|
| 366 |
+
num_resolutions=self.num_resolutions,
|
| 367 |
+
num_res_blocks=num_res_blocks,
|
| 368 |
+
resolution=resolution,
|
| 369 |
+
temb_channels=self.temb_ch,
|
| 370 |
+
dropout=dropout,
|
| 371 |
+
norm_type=self.norm_type,
|
| 372 |
+
causality_axis=self.causality_axis,
|
| 373 |
+
attn_type=self.attn_type,
|
| 374 |
+
attn_resolutions=attn_resolutions,
|
| 375 |
+
resamp_with_conv=resamp_with_conv,
|
| 376 |
+
initial_block_channels=base_block_channels,
|
| 377 |
+
)
|
| 378 |
+
|
| 379 |
+
self.norm_out = build_normalization_layer(final_block_channels, normtype=self.norm_type)
|
| 380 |
+
self.conv_out = make_conv2d(
|
| 381 |
+
final_block_channels, out_ch, kernel_size=3, stride=1, causality_axis=self.causality_axis
|
| 382 |
+
)
|
| 383 |
+
|
| 384 |
+
def forward(self, sample: torch.Tensor) -> torch.Tensor:
|
| 385 |
+
"""
|
| 386 |
+
Decode latent features back to audio spectrograms.
|
| 387 |
+
Args:
|
| 388 |
+
sample: Encoded latent representation of shape (batch, channels, frames, mel_bins)
|
| 389 |
+
Returns:
|
| 390 |
+
Reconstructed audio spectrogram of shape (batch, channels, time, frequency)
|
| 391 |
+
"""
|
| 392 |
+
sample, target_shape = self._denormalize_latents(sample)
|
| 393 |
+
|
| 394 |
+
h = self.conv_in(sample)
|
| 395 |
+
h = run_mid_block(self.mid, h)
|
| 396 |
+
h = self._run_upsampling_path(h)
|
| 397 |
+
h = self._finalize_output(h)
|
| 398 |
+
|
| 399 |
+
return self._adjust_output_shape(h, target_shape)
|
| 400 |
+
|
| 401 |
+
def _denormalize_latents(self, sample: torch.Tensor) -> tuple[torch.Tensor, AudioLatentShape]:
|
| 402 |
+
latent_shape = AudioLatentShape(
|
| 403 |
+
batch=sample.shape[0],
|
| 404 |
+
channels=sample.shape[1],
|
| 405 |
+
frames=sample.shape[2],
|
| 406 |
+
mel_bins=sample.shape[3],
|
| 407 |
+
)
|
| 408 |
+
|
| 409 |
+
sample_patched = self.patchifier.patchify(sample)
|
| 410 |
+
sample_denormalized = self.per_channel_statistics.un_normalize(sample_patched)
|
| 411 |
+
sample = self.patchifier.unpatchify(sample_denormalized, latent_shape)
|
| 412 |
+
|
| 413 |
+
target_frames = latent_shape.frames * LATENT_DOWNSAMPLE_FACTOR
|
| 414 |
+
if self.causality_axis != CausalityAxis.NONE:
|
| 415 |
+
target_frames = max(target_frames - (LATENT_DOWNSAMPLE_FACTOR - 1), 1)
|
| 416 |
+
|
| 417 |
+
target_shape = AudioLatentShape(
|
| 418 |
+
batch=latent_shape.batch,
|
| 419 |
+
channels=self.out_ch,
|
| 420 |
+
frames=target_frames,
|
| 421 |
+
mel_bins=self.mel_bins if self.mel_bins is not None else latent_shape.mel_bins,
|
| 422 |
+
)
|
| 423 |
+
|
| 424 |
+
return sample, target_shape
|
| 425 |
+
|
| 426 |
+
def _adjust_output_shape(
|
| 427 |
+
self,
|
| 428 |
+
decoded_output: torch.Tensor,
|
| 429 |
+
target_shape: AudioLatentShape,
|
| 430 |
+
) -> torch.Tensor:
|
| 431 |
+
"""
|
| 432 |
+
Adjust output shape to match target dimensions for variable-length audio.
|
| 433 |
+
This function handles the common case where decoded audio spectrograms need to be
|
| 434 |
+
resized to match a specific target shape.
|
| 435 |
+
Args:
|
| 436 |
+
decoded_output: Tensor of shape (batch, channels, time, frequency)
|
| 437 |
+
target_shape: AudioLatentShape describing (batch, channels, time, mel bins)
|
| 438 |
+
Returns:
|
| 439 |
+
Tensor adjusted to match target_shape exactly
|
| 440 |
+
"""
|
| 441 |
+
# Current output shape: (batch, channels, time, frequency)
|
| 442 |
+
_, _, current_time, current_freq = decoded_output.shape
|
| 443 |
+
target_channels = target_shape.channels
|
| 444 |
+
target_time = target_shape.frames
|
| 445 |
+
target_freq = target_shape.mel_bins
|
| 446 |
+
|
| 447 |
+
# Step 1: Crop first to avoid exceeding target dimensions
|
| 448 |
+
decoded_output = decoded_output[
|
| 449 |
+
:, :target_channels, : min(current_time, target_time), : min(current_freq, target_freq)
|
| 450 |
+
]
|
| 451 |
+
|
| 452 |
+
# Step 2: Calculate padding needed for time and frequency dimensions
|
| 453 |
+
time_padding_needed = target_time - decoded_output.shape[2]
|
| 454 |
+
freq_padding_needed = target_freq - decoded_output.shape[3]
|
| 455 |
+
|
| 456 |
+
# Step 3: Apply padding if needed
|
| 457 |
+
if time_padding_needed > 0 or freq_padding_needed > 0:
|
| 458 |
+
# PyTorch padding format: (pad_left, pad_right, pad_top, pad_bottom)
|
| 459 |
+
# For audio: pad_left/right = frequency, pad_top/bottom = time
|
| 460 |
+
padding = (
|
| 461 |
+
0,
|
| 462 |
+
max(freq_padding_needed, 0), # frequency padding (left, right)
|
| 463 |
+
0,
|
| 464 |
+
max(time_padding_needed, 0), # time padding (top, bottom)
|
| 465 |
+
)
|
| 466 |
+
decoded_output = F.pad(decoded_output, padding)
|
| 467 |
+
|
| 468 |
+
# Step 4: Final safety crop to ensure exact target shape
|
| 469 |
+
decoded_output = decoded_output[:, :target_channels, :target_time, :target_freq]
|
| 470 |
+
|
| 471 |
+
return decoded_output
|
| 472 |
+
|
| 473 |
+
def _run_upsampling_path(self, h: torch.Tensor) -> torch.Tensor:
|
| 474 |
+
for level in reversed(range(self.num_resolutions)):
|
| 475 |
+
stage = self.up[level]
|
| 476 |
+
for block_idx, block in enumerate(stage.block):
|
| 477 |
+
h = block(h, temb=None)
|
| 478 |
+
if stage.attn:
|
| 479 |
+
h = stage.attn[block_idx](h)
|
| 480 |
+
|
| 481 |
+
if level != 0 and hasattr(stage, "upsample"):
|
| 482 |
+
h = stage.upsample(h)
|
| 483 |
+
|
| 484 |
+
return h
|
| 485 |
+
|
| 486 |
+
def _finalize_output(self, h: torch.Tensor) -> torch.Tensor:
|
| 487 |
+
if self.give_pre_end:
|
| 488 |
+
return h
|
| 489 |
+
|
| 490 |
+
h = self.norm_out(h)
|
| 491 |
+
h = self.non_linearity(h)
|
| 492 |
+
h = self.conv_out(h)
|
| 493 |
+
return torch.tanh(h) if self.tanh_out else h
|
| 494 |
+
|
| 495 |
+
|
| 496 |
+
def decode_audio(latent: torch.Tensor, audio_decoder: "AudioDecoder", vocoder: "Vocoder") -> Audio:
|
| 497 |
+
"""
|
| 498 |
+
Decode an audio latent representation using the provided audio decoder and vocoder.
|
| 499 |
+
Args:
|
| 500 |
+
latent: Input audio latent tensor.
|
| 501 |
+
audio_decoder: Model to decode the latent to waveform features.
|
| 502 |
+
vocoder: Model to convert decoded features to audio waveform.
|
| 503 |
+
Returns:
|
| 504 |
+
Decoded audio with waveform and sampling rate.
|
| 505 |
+
"""
|
| 506 |
+
decoded_audio = audio_decoder(latent)
|
| 507 |
+
waveform = vocoder(decoded_audio).squeeze(0).float()
|
| 508 |
+
return Audio(waveform=waveform, sampling_rate=vocoder.output_sampling_rate)
|