Skip to content

Commit eae38c4

Browse files
Merge pull request #425 from tsvikas:ltx2-3-lora-support
PiperOrigin-RevId: 938164510
2 parents 39af0e2 + 0fbf767 commit eae38c4

3 files changed

Lines changed: 42 additions & 19 deletions

File tree

src/maxdiffusion/configs/ltx2_3_video.yml

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,18 +142,20 @@ enable_lora: False
142142

143143
# Distilled LoRA
144144
# lora_config: {
145-
# lora_model_name_or_path: ["Lightricks/LTX-2"],
146-
# weight_name: ["ltx-2-19b-distilled-lora-384.safetensors"],
145+
# lora_model_name_or_path: ["Lightricks/LTX-2.3"],
146+
# weight_name: ["ltx-2.3-22b-distilled-lora-384.safetensors"],
147147
# adapter_name: ["distilled-lora-384"],
148+
# # placeholder - the real value is mixed per-layer ranks: 32/128/256/384
149+
# # and the loader reads each layer's REAL rank from the LoRA tensor shapes
148150
# rank: [384]
149151
# }
150152

151153
# Standard LoRA
152154
lora_config: {
153-
lora_model_name_or_path: ["Lightricks/LTX-2-19b-LoRA-Camera-Control-Dolly-In"],
154-
weight_name: ["ltx-2-19b-lora-camera-control-dolly-in.safetensors"],
155-
adapter_name: ["camera-control-dolly-in"],
156-
rank: [32]
155+
lora_model_name_or_path: ["Lightricks/LTX-2.3-22b-IC-LoRA-Colorization"],
156+
weight_name: ["ltx-2.3-22b-ic-lora-colorization-0.9.safetensors"],
157+
adapter_name: ["colorization"],
158+
rank: [128]
157159
}
158160

159161

src/maxdiffusion/loaders/lora_conversion_utils.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,31 +716,37 @@ def translate_ltx2_nnx_path_to_diffusers_lora(nnx_path_str, scan_layers=False):
716716
"attn1.to_k": "attn1.to_k",
717717
"attn1.to_v": "attn1.to_v",
718718
"attn1.to_out": "attn1.to_out.0",
719+
"attn1.to_gate_logits": "attn1.to_gate_logits",
719720
# Audio Self Attention (audio_attn1)
720721
"audio_attn1.to_q": "audio_attn1.to_q",
721722
"audio_attn1.to_k": "audio_attn1.to_k",
722723
"audio_attn1.to_v": "audio_attn1.to_v",
723724
"audio_attn1.to_out": "audio_attn1.to_out.0",
725+
"audio_attn1.to_gate_logits": "audio_attn1.to_gate_logits",
724726
# Audio Cross Attention (audio_attn2)
725727
"audio_attn2.to_q": "audio_attn2.to_q",
726728
"audio_attn2.to_k": "audio_attn2.to_k",
727729
"audio_attn2.to_v": "audio_attn2.to_v",
728730
"audio_attn2.to_out": "audio_attn2.to_out.0",
731+
"audio_attn2.to_gate_logits": "audio_attn2.to_gate_logits",
729732
# Cross Attention (attn2)
730733
"attn2.to_q": "attn2.to_q",
731734
"attn2.to_k": "attn2.to_k",
732735
"attn2.to_v": "attn2.to_v",
733736
"attn2.to_out": "attn2.to_out.0",
737+
"attn2.to_gate_logits": "attn2.to_gate_logits",
734738
# Audio to Video Cross Attention
735739
"audio_to_video_attn.to_q": "audio_to_video_attn.to_q",
736740
"audio_to_video_attn.to_k": "audio_to_video_attn.to_k",
737741
"audio_to_video_attn.to_v": "audio_to_video_attn.to_v",
738742
"audio_to_video_attn.to_out": "audio_to_video_attn.to_out.0",
743+
"audio_to_video_attn.to_gate_logits": "audio_to_video_attn.to_gate_logits",
739744
# Video to Audio Cross Attention
740745
"video_to_audio_attn.to_q": "video_to_audio_attn.to_q",
741746
"video_to_audio_attn.to_k": "video_to_audio_attn.to_k",
742747
"video_to_audio_attn.to_v": "video_to_audio_attn.to_v",
743748
"video_to_audio_attn.to_out": "video_to_audio_attn.to_out.0",
749+
"video_to_audio_attn.to_gate_logits": "video_to_audio_attn.to_gate_logits",
744750
# Feed Forward
745751
"ff.net_0": "ff.net.0.proj",
746752
"ff.net_2": "ff.net.2",
@@ -778,6 +784,13 @@ def translate_ltx2_nnx_path_to_diffusers_lora(nnx_path_str, scan_layers=False):
778784
"caption_projection.linear_2": "diffusion_model.caption_projection.linear_2",
779785
"audio_caption_projection.linear_1": "diffusion_model.audio_caption_projection.linear_1",
780786
"audio_caption_projection.linear_2": "diffusion_model.audio_caption_projection.linear_2",
787+
# Prompt-conditioned AdaLN
788+
"prompt_adaln.linear": "diffusion_model.prompt_adaln_single.linear",
789+
"prompt_adaln.emb.timestep_embedder.linear_1": "diffusion_model.prompt_adaln_single.emb.timestep_embedder.linear_1",
790+
"prompt_adaln.emb.timestep_embedder.linear_2": "diffusion_model.prompt_adaln_single.emb.timestep_embedder.linear_2",
791+
"audio_prompt_adaln.linear": "diffusion_model.audio_prompt_adaln_single.linear",
792+
"audio_prompt_adaln.emb.timestep_embedder.linear_1": "diffusion_model.audio_prompt_adaln_single.emb.timestep_embedder.linear_1",
793+
"audio_prompt_adaln.emb.timestep_embedder.linear_2": "diffusion_model.audio_prompt_adaln_single.emb.timestep_embedder.linear_2",
781794
# Connectors
782795
"feature_extractor.linear": "text_embedding_projection.aggregate_embed",
783796
}

src/maxdiffusion/loaders/ltx2_lora_nnx_loader.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -50,26 +50,34 @@ def load_lora_weights(
5050
def translate_fn(nnx_path_str):
5151
return lora_conversion_utils.translate_ltx2_nnx_path_to_diffusers_lora(nnx_path_str, scan_layers=scan_layers)
5252

53-
h_state_dict = None
54-
if hasattr(pipeline, "transformer") and transformer_weight_name:
53+
if not transformer_weight_name:
54+
max_logging.log("No LoRA weight name provided; skipping LoRA load.")
55+
return pipeline
56+
57+
h_state_dict, _ = lora_loader.lora_state_dict(lora_model_path, weight_name=transformer_weight_name, **kwargs)
58+
transformer_state_dict = {}
59+
connector_state_dict = {}
60+
if hasattr(pipeline, "transformer"):
5561
max_logging.log(f"Merging LoRA into transformer with rank={rank}")
56-
h_state_dict, _ = lora_loader.lora_state_dict(lora_model_path, weight_name=transformer_weight_name, **kwargs)
5762
# Filter state dict for transformer keys to avoid confusing warnings
58-
transformer_state_dict = {k: v for k, v in h_state_dict.items() if k.startswith("diffusion_model")}
63+
transformer_state_dict = {k: v for k, v in h_state_dict.items() if k.startswith("diffusion_model.")}
5964
merge_fn(pipeline.transformer, transformer_state_dict, rank, scale, translate_fn, dtype=dtype)
6065
else:
61-
max_logging.log("transformer not found or no weight name provided for LoRA.")
66+
max_logging.log("transformer not found.")
6267

6368
if hasattr(pipeline, "connectors"):
6469
max_logging.log(f"Merging LoRA into connectors with rank={rank}")
65-
if h_state_dict is None and transformer_weight_name:
66-
h_state_dict, _ = lora_loader.lora_state_dict(lora_model_path, weight_name=transformer_weight_name, **kwargs)
70+
connector_state_dict = {k: v for k, v in h_state_dict.items() if k.startswith("text_embedding_projection.")}
71+
merge_fn(pipeline.connectors, connector_state_dict, rank, scale, translate_fn, dtype=dtype)
72+
else:
73+
max_logging.log("connectors not found.")
6774

68-
if h_state_dict is not None:
69-
# Filter state dict for connector keys to avoid confusing warnings
70-
connector_state_dict = {k: v for k, v in h_state_dict.items() if k.startswith("text_embedding_projection")}
71-
merge_fn(pipeline.connectors, connector_state_dict, rank, scale, translate_fn, dtype=dtype)
72-
else:
73-
max_logging.log("Could not load LoRA state dict for connectors.")
75+
# Warn if there are keys routed to no target.
76+
# the merge_fn warns about unmatched keys in each dict, so we only warn about any leftovers
77+
unmatched_keys = set(h_state_dict) - set(transformer_state_dict) - set(connector_state_dict)
78+
if unmatched_keys:
79+
max_logging.log(
80+
f"{len(unmatched_keys)} key(s) in LoRA dictionary routed to no merge target: {unmatched_keys}"
81+
)
7482

7583
return pipeline

0 commit comments

Comments
 (0)