Skip to content

Commit 9d794bb

Browse files
committed
support loading non-diffusers Ideogram4 LoRAs
1 parent ecd7d0a commit 9d794bb

2 files changed

Lines changed: 94 additions & 1 deletion

File tree

src/diffusers/loaders/lora_conversion_utils.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2883,3 +2883,88 @@ def get_alpha_scales(down_weight, alpha_key):
28832883

28842884
converted_state_dict = {f"transformer.{k}": v for k, v in converted_state_dict.items()}
28852885
return converted_state_dict
2886+
2887+
2888+
def _convert_non_diffusers_ideogram4_lora_to_diffusers(state_dict):
2889+
"""
2890+
Convert non-diffusers Ideogram4 LoRA state dict to diffusers format.
2891+
2892+
Handles:
2893+
- `diffusion_model.` / `conditional_transformer.` prefix removal
2894+
- `lora_down`/`lora_up` (kohya) -> `lora_A`/`lora_B`, with `.alpha` folded into the weights
2895+
- fused `attention.qkv` -> split `to_q`/`to_k`/`to_v`; `attention.o` -> `to_out.0`
2896+
- `feed_forward.w1`/`w2`/`w3` and `adaln_modulation` map one-to-one
2897+
"""
2898+
for prefix in ("diffusion_model.", "conditional_transformer."):
2899+
if any(k.startswith(prefix) for k in state_dict):
2900+
state_dict = {k.removeprefix(prefix): v for k, v in state_dict.items()}
2901+
break
2902+
2903+
is_kohya = any(".lora_down.weight" in k for k in state_dict)
2904+
down_suffix = ".lora_down.weight" if is_kohya else ".lora_A.weight"
2905+
up_suffix = ".lora_up.weight" if is_kohya else ".lora_B.weight"
2906+
2907+
def get_alpha_scales(down_weight, alpha_key):
2908+
rank = down_weight.shape[0]
2909+
alpha_tensor = state_dict.pop(alpha_key, None)
2910+
if alpha_tensor is None:
2911+
return 1.0, 1.0
2912+
# LoRA is scaled by `alpha / rank` in the forward pass; split the factor between down and up.
2913+
scale = alpha_tensor.item() / rank
2914+
scale_down, scale_up = scale, 1.0
2915+
while scale_down * 2 < scale_up:
2916+
scale_down *= 2
2917+
scale_up /= 2
2918+
return scale_down, scale_up
2919+
2920+
def pull(base):
2921+
"""Pop the scaled (lora_A, lora_B) pair for a module path, or return None if absent."""
2922+
down_key = base + down_suffix
2923+
if down_key not in state_dict:
2924+
return None
2925+
down = state_dict.pop(down_key)
2926+
up = state_dict.pop(base + up_suffix)
2927+
scale_down, scale_up = get_alpha_scales(down, base + ".alpha")
2928+
return down * scale_down, up * scale_up
2929+
2930+
num_layers = 0
2931+
for k in state_dict:
2932+
match = re.match(r"layers\.(\d+)\.", k)
2933+
if match:
2934+
num_layers = max(num_layers, int(match.group(1)) + 1)
2935+
2936+
converted_state_dict = {}
2937+
for i in range(num_layers):
2938+
layer_prefix = f"layers.{i}"
2939+
2940+
# Fused qkv -> split to_q / to_k / to_v (shared down/lora_A, chunk up/lora_B in thirds).
2941+
qkv = pull(f"{layer_prefix}.attention.qkv")
2942+
if qkv is not None:
2943+
down, up = qkv
2944+
up_q, up_k, up_v = torch.chunk(up, 3, dim=0)
2945+
for proj, up_proj in (("to_q", up_q), ("to_k", up_k), ("to_v", up_v)):
2946+
converted_state_dict[f"{layer_prefix}.attention.{proj}.lora_A.weight"] = down.clone()
2947+
converted_state_dict[f"{layer_prefix}.attention.{proj}.lora_B.weight"] = up_proj.contiguous()
2948+
2949+
# attention.o -> attention.to_out.0
2950+
out = pull(f"{layer_prefix}.attention.o")
2951+
if out is not None:
2952+
down, up = out
2953+
converted_state_dict[f"{layer_prefix}.attention.to_out.0.lora_A.weight"] = down
2954+
converted_state_dict[f"{layer_prefix}.attention.to_out.0.lora_B.weight"] = up
2955+
2956+
# feed_forward.{w1,w2,w3} and adaln_modulation map one-to-one.
2957+
for module in ("feed_forward.w1", "feed_forward.w2", "feed_forward.w3", "adaln_modulation"):
2958+
pair = pull(f"{layer_prefix}.{module}")
2959+
if pair is not None:
2960+
down, up = pair
2961+
converted_state_dict[f"{layer_prefix}.{module}.lora_A.weight"] = down
2962+
converted_state_dict[f"{layer_prefix}.{module}.lora_B.weight"] = up
2963+
2964+
if len(state_dict) > 0:
2965+
raise ValueError(
2966+
f"`state_dict` should be empty at this point but has {sorted(state_dict.keys())}. "
2967+
"This may be an unsupported Ideogram4 LoRA layout."
2968+
)
2969+
2970+
return {f"transformer.{k}": v for k, v in converted_state_dict.items()}

src/diffusers/loaders/lora_pipeline.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
_convert_non_diffusers_anima_lora_to_diffusers,
5050
_convert_non_diffusers_flux2_lora_to_diffusers,
5151
_convert_non_diffusers_hidream_lora_to_diffusers,
52+
_convert_non_diffusers_ideogram4_lora_to_diffusers,
5253
_convert_non_diffusers_lora_to_diffusers,
5354
_convert_non_diffusers_ltx2_lora_to_diffusers,
5455
_convert_non_diffusers_ltxv_lora_to_diffusers,
@@ -6028,7 +6029,6 @@ class Ideogram4LoraLoaderMixin(LoraBaseMixin):
60286029

60296030
@classmethod
60306031
@validate_hf_hub_args
6031-
# Copied from diffusers.loaders.lora_pipeline.SD3LoraLoaderMixin.lora_state_dict
60326032
def lora_state_dict(
60336033
cls,
60346034
pretrained_model_name_or_path_or_dict: str | dict[str, torch.Tensor],
@@ -6078,6 +6078,14 @@ def lora_state_dict(
60786078
logger.warning(warn_msg)
60796079
state_dict = {k: v for k, v in state_dict.items() if "dora_scale" not in k}
60806080

6081+
# ai-toolkit (ostris) saves Ideogram4 LoRAs under a `diffusion_model.` prefix with a fused
6082+
# `attention.qkv` projection; convert those to the diffusers layout before loading.
6083+
is_non_diffusers_format = any(k.startswith("diffusion_model.") for k in state_dict) or any(
6084+
".attention.qkv." in k for k in state_dict
6085+
)
6086+
if is_non_diffusers_format:
6087+
state_dict = _convert_non_diffusers_ideogram4_lora_to_diffusers(state_dict)
6088+
60816089
out = (state_dict, metadata) if return_lora_metadata else state_dict
60826090
return out
60836091

0 commit comments

Comments
 (0)