Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,12 @@ starter models do, needs no name. TAEF2, TAEQI2.1 and TAEW2.2 are recognized by
FLUX.1 and FLUX.2 also accept ComfyUI/OneTrainer PEFT LoRAs whose module names start with `diffusion_model.`.
Per-layer `.alpha` values in these `diffusion_model.` files set the LoRA scale.

SimpleTuner's LyCORIS training writes its layers with the model's own module names flattened behind a `lycoris_`
prefix (for example `lycoris_single_transformer_blocks_0_attn_to_k.lokr_w1`). Files in this layout are recognized
on FLUX.1 (both `lycoris_` and `lycoris__` spellings), FLUX.2, SDXL, Z-Image and Wan, as they already were on
SD 3 / 3.5; FLUX.3's own flattened names are a different layout and are still not recognized, as noted on its
family page.

For SD 1.x / 2.x and FLUX.2, LyCORIS LoKr files may store either Kronecker operand whole or as a
factorized `_a` / `_b` pair. FLUX.2 also accepts LoHA files in the same BFL and kohya layouts.

Expand Down
35 changes: 31 additions & 4 deletions invokeai/backend/model_manager/configs/lora.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
has_non_ltx2_architecture_keys,
has_unsupported_ltx2_lora_variant_keys,
)
from invokeai.backend.patches.lora_conversions.lycoris_flat_lora_constants import lycoris_flat_family
from invokeai.backend.patches.lora_conversions.minimax_h3_lora_constants import (
find_minimax_h3_lora_defect,
has_minimax_h3_lora_keys,
Expand Down Expand Up @@ -706,6 +707,8 @@ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
"lora_te1_",
"lora_te2_",
"lora_transformer_",
# SimpleTuner's LyCORIS flattens the model's own module names behind this prefix (#10174).
"lycoris_",
},
)

Expand Down Expand Up @@ -746,6 +749,15 @@ def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
if is_sd3_lora(state_dict):
raise NotAMatchError("model looks like an SD 3 LoRA, not an SD 1/2/XL LoRA")

# SimpleTuner's LyCORIS flattens the model's own module names behind `lycoris_` (#10174). FLUX.1 and SDXL
# are the families this base detection serves; FLUX.2, Z-Image and Wan have their own probes, and names
# that settle nothing fall through to the Stable Diffusion heuristics below.
flat_family = lycoris_flat_family(state_dict)
if flat_family == "flux1":
return BaseModelType.Flux
if flat_family == "sdxl":
return BaseModelType.StableDiffusionXL

# If we've gotten here, we assume that the model is a Stable Diffusion model
token_vector_length = lora_token_vector_length(state_dict)
if token_vector_length == 768:
Expand Down Expand Up @@ -800,6 +812,10 @@ def from_model_on_disk(cls, mod: ModelOnDisk, override_fields: dict[str, Any]) -
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
if _get_flux_lora_format(mod) and _is_flux2_lora(mod):
return BaseModelType.Flux2
# SimpleTuner's LyCORIS flat keys: FLUX.2 Klein's own module names (`to_qkv_mlp_proj`, `ff.linear_in`) say
# so, whatever the tensor shapes can (#10174).
if lycoris_flat_family(mod.load_state_dict()) == "flux2":
return BaseModelType.Flux2
raise NotAMatchError("model is not a FLUX.2 LoRA")


Expand Down Expand Up @@ -839,6 +855,11 @@ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
if is_state_dict_likely_z_image_kohya_lora(state_dict):
return

# SimpleTuner's LyCORIS flattens the module names behind `lycoris_` (`lycoris_layers_0_attention_to_k`);
# the family detection resolves them and keeps Ideogram 4's same-named blocks out by width (#10174).
if lycoris_flat_family(state_dict) == "z_image":
return

# Check for Z-Image specific LoRA patterns (dot-notation formats)
has_z_image_lora_keys = state_dict_has_any_keys_starting_with(
state_dict,
Expand Down Expand Up @@ -886,6 +907,9 @@ def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
# Check for Kohya format
if is_state_dict_likely_z_image_kohya_lora(state_dict):
return BaseModelType.ZImage
# SimpleTuner's LyCORIS flat names (#10174)
if lycoris_flat_family(state_dict) == "z_image":
return BaseModelType.ZImage

# Check for Z-Image transformer layer patterns (dot-notation formats)
# Z-Image uses diffusion_model.layers.X structure (unlike Flux which uses double_blocks/single_blocks)
Expand Down Expand Up @@ -1538,7 +1562,9 @@ def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
state_dict = mod.load_state_dict()
str_keys = [k for k in state_dict.keys() if isinstance(k, str)]

has_wan_keys = has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys)
has_wan_keys = (
has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys) or lycoris_flat_family(state_dict) == "wan"
)
has_lora_suffix = state_dict_has_any_keys_ending_with(
state_dict,
{
Expand All @@ -1565,9 +1591,10 @@ def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
state_dict = mod.load_state_dict()
str_keys = [k for k in state_dict.keys() if isinstance(k, str)]

if (has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys)) and not has_non_wan_architecture_keys(
str_keys
):
has_wan_keys = (
has_wan_kohya_keys(str_keys) or has_wan_peft_keys(str_keys) or lycoris_flat_family(state_dict) == "wan"
)
if has_wan_keys and not has_non_wan_architecture_keys(str_keys):
return BaseModelType.Wan

raise NotAMatchError("model does not look like a Wan LoRA")
Expand Down
11 changes: 10 additions & 1 deletion invokeai/backend/model_manager/load/model_loaders/lora.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@
lora_model_from_krea2_state_dict,
)
from invokeai.backend.patches.lora_conversions.ltx2_lora_conversion_utils import lora_model_from_ltx2_state_dict
from invokeai.backend.patches.lora_conversions.lycoris_flat_lora_constants import lycoris_flat_family
from invokeai.backend.patches.lora_conversions.lycoris_flat_lora_conversion_utils import (
lora_model_from_lycoris_flat_state_dict,
)
from invokeai.backend.patches.lora_conversions.minimax_h3_lora_conversion_utils import (
lora_model_from_minimax_h3_state_dict,
)
Expand Down Expand Up @@ -158,8 +162,13 @@ def _load_model(

factorized_dora_layers = _count_factorized_layers_with_dora_magnitude(state_dict)

# SimpleTuner's LyCORIS flattens the model's own module names behind `lycoris_` (#10174); map them back and
# hand each family its own converter. State dicts this layout does not claim (SD 3's flat spelling, which
# `lora_model_from_sd3_state_dict` resolves already, and every other format) fall through to the dispatch.
if lycoris_flat_family(state_dict) is not None:
model = lora_model_from_lycoris_flat_state_dict(state_dict, base=self._model_base)
# Apply state_dict key conversions, if necessary.
if self._model_base == BaseModelType.StableDiffusionXL:
elif self._model_base == BaseModelType.StableDiffusionXL:
state_dict = convert_sdxl_keys_to_diffusers_format(state_dict)
model = lora_model_from_sd_state_dict(state_dict=state_dict)
elif self._model_base in (BaseModelType.Flux, BaseModelType.Flux2):
Expand Down
Loading