Skip to content

Commit d42bf9c

Browse files
fix(model-manager): add Z-Image LoRA/DoRA detection support (#8709)
## Summary Fix Z-Image LoRA/DoRA model detection failing during installation. Z-Image LoRAs use different key patterns than SD/SDXL LoRAs. The base `LoRA_LyCORIS_Config_Base` class only checked for key suffixes like `lora_A.weight` and `lora_B.weight`, but Z-Image LoRAs (especially those in DoRA format) use: - `lora_down.weight` / `lora_up.weight` (standard LoRA format) - `dora_scale` (DoRA weight decomposition) This PR overrides `_validate_looks_like_lora` in `LoRA_LyCORIS_ZImage_Config` to recognize Z-Image specific patterns: - Keys starting with `diffusion_model.layers.` (Z-Image S3-DiT architecture) - Keys ending with `lora_down.weight`, `lora_up.weight`, `lora_A.weight`, `lora_B.weight`, or `dora_scale` ## Related Issues / Discussions Fixes installation of Z-Image LoRAs trained with DoRA (Weight-Decomposed Low-Rank Adaptation). ## QA Instructions 1. Download a Z-Image LoRA in DoRA format (e.g., from CivitAI with keys like `diffusion_model.layers.X.attention.to_k.lora_down.weight`) 2. Try to install the LoRA via Model Manager 3. Verify the model is recognized as a Z-Image LoRA and installs successfully 4. Verify the LoRA can be applied when generating with Z-Image ## Merge Plan Standard merge, no special considerations. ## Checklist - [x] _The PR has a short but descriptive title, suitable for a changelog_ - [ ] _Tests added / updated (if applicable)_ - [ ] _❗Changes to a redux slice have a corresponding migration_ - [ ] _Documentation added / updated (if applicable)_ - [ ] _Updated `What's New` copy (if doing a release after this PR)_
2 parents 0b1befa + d403587 commit d42bf9c

File tree

2 files changed

+75
-5
lines changed

2 files changed

+75
-5
lines changed

invokeai/backend/model_manager/configs/lora.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,42 @@ class LoRA_LyCORIS_ZImage_Config(LoRA_LyCORIS_Config_Base, Config_Base):
227227

228228
base: Literal[BaseModelType.ZImage] = Field(default=BaseModelType.ZImage)
229229

230+
@classmethod
231+
def _validate_looks_like_lora(cls, mod: ModelOnDisk) -> None:
232+
"""Z-Image LoRAs have different key patterns than SD/SDXL LoRAs.
233+
234+
Z-Image LoRAs use keys like:
235+
- diffusion_model.layers.X.attention.to_k.lora_down.weight (DoRA format)
236+
- diffusion_model.layers.X.attention.to_k.lora_A.weight (PEFT format)
237+
- diffusion_model.layers.X.attention.to_k.dora_scale (DoRA scale)
238+
"""
239+
state_dict = mod.load_state_dict()
240+
241+
# Check for Z-Image specific LoRA patterns
242+
has_z_image_lora_keys = state_dict_has_any_keys_starting_with(
243+
state_dict,
244+
{
245+
"diffusion_model.layers.", # Z-Image S3-DiT layer pattern
246+
},
247+
)
248+
249+
# Also check for LoRA weight suffixes (various formats)
250+
has_lora_suffix = state_dict_has_any_keys_ending_with(
251+
state_dict,
252+
{
253+
"lora_A.weight",
254+
"lora_B.weight",
255+
"lora_down.weight",
256+
"lora_up.weight",
257+
"dora_scale",
258+
},
259+
)
260+
261+
if has_z_image_lora_keys and has_lora_suffix:
262+
return
263+
264+
raise NotAMatchError("model does not match Z-Image LoRA heuristics")
265+
230266
@classmethod
231267
def _get_base_or_raise(cls, mod: ModelOnDisk) -> BaseModelType:
232268
"""Z-Image LoRAs are identified by their diffusion_model.layers structure.

invokeai/backend/patches/lora_conversions/z_image_lora_conversion_utils.py

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,16 +140,50 @@ def _get_lora_layer_values(layer_dict: dict[str, torch.Tensor], alpha: float | N
140140

141141

142142
def _group_by_layer(state_dict: Dict[str, torch.Tensor]) -> dict[str, dict[str, torch.Tensor]]:
143-
"""Groups the keys in the state dict by layer."""
143+
"""Groups the keys in the state dict by layer.
144+
145+
Z-Image LoRAs have keys like:
146+
- diffusion_model.layers.17.attention.to_k.alpha
147+
- diffusion_model.layers.17.attention.to_k.dora_scale
148+
- diffusion_model.layers.17.attention.to_k.lora_down.weight
149+
- diffusion_model.layers.17.attention.to_k.lora_up.weight
150+
151+
We need to group these by the full layer path (e.g., diffusion_model.layers.17.attention.to_k)
152+
and extract the suffix (alpha, dora_scale, lora_down.weight, lora_up.weight).
153+
"""
144154
layer_dict: dict[str, dict[str, torch.Tensor]] = {}
155+
156+
# Known suffixes that indicate the end of a layer name
157+
known_suffixes = [
158+
".lora_A.weight",
159+
".lora_B.weight",
160+
".lora_down.weight",
161+
".lora_up.weight",
162+
".dora_scale",
163+
".alpha",
164+
]
165+
145166
for key in state_dict:
146167
if not isinstance(key, str):
147168
continue
148-
# Split the 'lora_A.weight' or 'lora_B.weight' suffix from the layer name.
149-
parts = key.rsplit(".", maxsplit=2)
150-
layer_name = parts[0]
151-
key_name = ".".join(parts[1:])
169+
170+
# Try to find a known suffix
171+
layer_name = None
172+
key_name = None
173+
for suffix in known_suffixes:
174+
if key.endswith(suffix):
175+
layer_name = key[: -len(suffix)]
176+
key_name = suffix[1:] # Remove leading dot
177+
break
178+
179+
if layer_name is None:
180+
# Fallback to original logic for unknown formats
181+
parts = key.rsplit(".", maxsplit=2)
182+
layer_name = parts[0]
183+
key_name = ".".join(parts[1:])
184+
152185
if layer_name not in layer_dict:
153186
layer_dict[layer_name] = {}
154187
layer_dict[layer_name][key_name] = state_dict[key]
188+
155189
return layer_dict

0 commit comments

Comments
 (0)