Repository navigation
feat(model-manager): support LyCORIS LoKr LoRAs for Krea-2 - #9424
Conversation
Krea-2 LoRAs in LyCORIS LoKr format were rejected at install time and could not be used. LoKr is the default output format of ai-toolkit, so this affects a fair number of community adapters. Two gaps in the Krea-2 path: - `LoRA_LyCORIS_Krea2_Config` required a complete lora_A/B or lora_down/up pair. LoKr carries Kronecker factors instead, so the probe rejected it even though the Krea-2 signature modules matched. The explicit-base override went through the same helper, so forcing the base did not help either. - `_SUFFIX_TO_VALUE_KEY` did not map the lokr_* suffixes, so `_group_by_layer` would have dropped the weights and left each layer holding only its alpha. The rest of the stack already handles LoKr: `LoKRLayer` exists and `any_lora_layer_from_state_dict` routes to it on `lokr_w1` / `lokr_w1_a`. Only the Krea-2 wiring was missing, matching its (WIP) status in invoke-ai#9304. The Krea-2 signature-module requirement is unchanged, so a LoKr targeting only generic transformer blocks is still not claimed by this base.
Resolves the import conflict in test_krea2_lora_conversion_utils.py (both sides added imports) and reconciles this branch with invoke-ai#9449, which taught the Krea-2 converter the kohya flattened layout. The two changes interact. invoke-ai#9449 rewrites a flattened module only when every suffix on it is one `_group_by_layer` knows, and left LyCORIS algorithms verbatim because `lokr_*` was not such a suffix. This branch makes it one, so a kohya-flattened LoKr module now un-flattens and applies instead of degrading to the per-layer "Failed to find module" warning. The hazard invoke-ai#9449 guards against -- the grouper's blind rsplit(".", 2) cutting inside a dotted module path -- does not apply to a suffix the grouper recognises. So `test_kohya_lycoris_algorithm_keys_do_not_abort_the_load` now carries its expected layer per case: the LoKr parametrizations expect the converted module, `hada_*` and `diff` still expect the verbatim key. The property the test guards -- that a mixed-algorithm file never aborts the whole adapter -- holds either way. Added a test covering the kohya + LoKr path end to end.
|
Merged #9449 landed just after your last merge and touched the same four files, which is where the Your per-module gate converts a flattened module only when every suffix on it is one The hazard your comment guards against — the grouper's blind Concretely: two of the five parametrizations of If you'd rather this PR left the kohya path completely alone, the alternative is to give that gate
|
Reconciles with invoke-ai#9517 (ai-toolkit DoRA magnitudes) and invoke-ai#9518 (validate kohya keys by un-flattening rather than by prefix). invoke-ai#9518 replaced the prefix-tuple argument of `_has_complete_lora_pair` with a key-filter callable, so `_has_lokr_layer` takes the same `key_filter` and the explicit-override gate now passes it `_key_is_supported_krea2_layout`. That is strictly better for LoKr too: the un-flattener rejects the `lora_unet_` files belonging to other bases that a bare prefix swept in. `_SUFFIX_TO_VALUE_KEY` merged cleanly -- invoke-ai#9517's `dora_magnitude` entries and the `lokr_*` entries are independent. Comment blocks above it and above `_KREA2_SUPPORTED_LORA_PREFIXES` keep both sides.
|
Merged Tests and ruff green, nothing else changed on this side. |
|
Merged Krea-2 tests ( |
Pfannkuchensack
left a comment
There was a problem hiding this comment.
Thanks for this. The routing works, and it fixes the Krea-2 LoKr files that are actually out there. Nothing blocking: three things I'd fix before merge, plus a few nits.
What I checked
Real-world layouts. I read only the safetensors headers of published Krea-2 LoKr adapters on HF and built files with identical keys and shapes. Between them they cover four layouts:
| Source | Layout |
|---|---|
ai-toolkit (eviligh3e/chle_krea2_lokr_v3, azikimbowe/krea2_lokr_stile) |
diffusion_model.blocks.N.attn.gate.{lokr_w1,lokr_w2,alpha} |
| ai-toolkit, factored w2 (another public adapter) | native, lokr_w1 + lokr_w2_a/lokr_w2_b |
NeedAHugNOW/City-Pop-LoKr |
kohya flat, lora_unet_blocks_0_attn_gate.lokr_w1 + lokr_w2_a/lokr_w2_b (one module with a full lokr_w2) |
Kentus/Naz_Ferra_KREA2_LoKr |
native, 264 modules including txtmlp.3, tproj.1, last.linear |
On main all five classify as Unknown. On this branch each matches LoRA_LyCORIS_Krea2_Config and nothing else. Every layer converts to a LoKRLayer whose reconstructed weight has the shape of a real nn.Linear in Krea2Transformer2DModel. LoKr files shaped like Qwen-Image, FLUX, Anima, Wan and Z-Image adapters classify exactly as they do on main; none is claimed as Krea-2.
End to end. I installed the real city-pop-texture-v2.safetensors through the model manager; it comes in as krea-2 / lora / lycoris. I then generated on Krea-2 Turbo Q4_K_M (GGUF) at 1024², 8 steps, fixed seed, on an RTX 4090. There were no Failed to find module warnings, the image changes clearly (mean absolute pixel difference 40.5 against no LoRA), and repeated runs are bit-identical.
The focused tests pass (104), and ruff check / ruff format --check are clean.
Should fix
1. An incomplete LoKr module installs, then fails at generation.
_has_lokr_layer (lora.py#L959-L984) accepts any single lokr_* key, so the "no orphaned half" comment doesn't hold. LoKRLayer needs a complete w1 side and a complete w2 side. With keys under diffusion_model.txtfusion.layerwise_blocks.0.attn.wq.:
| Module carries | Probe on this branch | At generation |
|---|---|---|
only lokr_w1 |
krea-2 / lycoris |
AssertionError in LoKRLayer.__init__ |
only lokr_w2 |
krea-2 / lycoris |
ValueError: Unsupported lora format |
lokr_w1 + lokr_w2_a (no _b) |
krea-2 / lycoris |
AssertionError |
lokr_w1 / lokr_w2 + dora_scale |
krea-2 / lycoris |
KeyError: 'lora_up.weight' (dispatched to DoRALayer) |
This config otherwise rejects that kind of file at identification time (_lora_weight_keys_are_all_paired). A per-module check, grouped by key.rsplit(".", 1)[0], would close the gap:
def _lokr_module_is_complete(suffixes: set[str]) -> bool:
has_w1 = "lokr_w1" in suffixes or {"lokr_w1_a", "lokr_w1_b"} <= suffixes
has_w2 = "lokr_w2" in suffixes or {"lokr_w2_a", "lokr_w2_b"} <= suffixes
return has_w1 and has_w2For dora_scale on a LoKr module, either reject it at probe time or strip it the way anima_lora_conversion_utils._make_layer_patch already does.
2. The explicit-override LoKr test doesn't cover the override change.
test_explicit_krea2_override_accepts_lokr_lora (test_krea2_lora_config.py#L395-L401) uses a fixture containing txtfusion, which the automatic path already accepts. With the or _has_lokr_layer(state_dict, _key_is_supported_krea2_layout) term removed, the test still passes.
Two cases would actually exercise the override:
- A LoKr without the Krea-2 signature, e.g.
transformer.transformer_blocks.0.attn.to_q.lokr_w1/w2, or kohyalora_unet_blocks_0_attn_wq.lokr_w1/w2(which auto-probes as Unknown). - A rejection case: a Wan kohya LoKr (
lora_unet_blocks_0_self_attn_q.lokr_w1) must still be refused under the override.
3. The conversion tests can't tell the factors apart.
The new LoKr conversion tests (test_krea2_lora_conversion_utils.py#L497-L587) use torch.ones and assert only shapes or is not None. kron(ones(2, 2), ones(3, 4)) and kron(ones(3, 4), ones(2, 2)) are both 6×8. So swapping lokr_w1↔lokr_w2 or _a↔_b in _SUFFIX_TO_VALUE_KEY leaves all of them green.
To pin the mapping:
- Use distinct random tensors and compare
get_weightagainsttorch.kron(w1, w2)andtorch.kron(w1_a @ w1_b, w2_a @ w2_b). - For the factored case, also assert
scale() == alpha / rank.
The code itself gets this right today; the tests just wouldn't notice a regression.
Nits
is_state_dict_likely_krea2_lora(krea2_lora_conversion_utils.py#L165-L191) has no caller outside its tests. This PR adds a third copy of the LoKr suffix list there, which already lacks.lokr_t2, plus the function's first two tests. I'd leave it untouched (or delete it) and drop those tests.- The LoKr suffixes now live in three places:
_LOKR_WEIGHT_SUFFIXES, the tuple above, and_SUFFIX_TO_VALUE_KEY. One constant inkrea2_lora_constants.pywould keep them in sync. - Some comments are stale:
- krea2_lora_conversion_utils.py#L119-L125 and #L146-L151 still say
lokr_w1modules stay verbatim. - The
_validate_looks_like_loradocstring and comment (lora.py#L1049-L1053) still describe only A/B pairs.
- krea2_lora_conversion_utils.py#L119-L125 and #L146-L151 still say
Not this PR, but worth knowing
On GGUF Krea-2, a LoKr nearly doubles denoise time. Same setup as above, one warm-up then three interleaved runs:
| Denoise (mean) | |
|---|---|
| no LoRA | 10.65 s |
| PEFT LoRA (A/B) | 11.66 s (1.09×) |
| City-Pop LoKr | 20.25 s (1.90×) |
Sidecar patching has a low-rank fast path only for LoRALayer. Every other layer type materializes its full delta weight on every forward. That's pre-existing and shared with the other bases, so it's a candidate for a separate follow-up.
…g in tests Addresses review 9424: - Identification checked only that some `lokr_*` key existed, so a module missing an operand, holding half a factored `_a`/`_b` pair, or spelling one operand both ways installed and then failed building LoKRLayer at generation time. Completeness is now checked per module, mirroring LoKRLayer's own invariants, on both the automatic and the explicit-override path. - A weight-decomposed LoKr (`dora_scale` / `magnitude` beside the factors) dispatched to DoRALayer and died on the missing lora_up/lora_down. The converter now drops the magnitude and applies plain LoKr with a warning, as the Anima converter does. - The LoKr suffix list lives once, in krea2_lora_constants, and both the identification helpers and `_SUFFIX_TO_VALUE_KEY` derive from it. - `is_state_dict_likely_krea2_lora` has no callers; its LoKr change and the two tests for it are reverted. Stale comments in the kohya pass and in `_validate_looks_like_lora` are updated. Tests: the override tests now use LoKr files the automatic probe cannot identify (dotted without the Krea-2 signature, and kohya-flattened), plus a Wan kohya LoKr that must still be refused. The conversion tests use random factors and compare against torch.kron exactly, and check scale() == alpha / rank for the factored case, so swapping w1/w2 or _a/_b in the suffix map now fails them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Thanks for the thorough review, and especially for testing against the published adapters and running it end to end. All three should-fix points and the nits are addressed in 6de2785. 1. Incomplete LoKr modules. You're right: the "no orphaned half" comment was wrong. Completeness is now checked per module (grouped by For 2. Override test. Replaced it with two cases the automatic probe returns as Unknown, dotted 3. Conversion tests. They now use random factors and compare To make sure the new tests actually guard what they claim, I injected each regression temporarily and confirmed they go red:
Nits.
Krea-2 focused tests: 123 passed. Thanks also for the GGUF timing numbers. Agreed that the missing low-rank fast path for non- |
|
The suffix mappings in this PR also address the generation-time error A minimal synthetic example of the grouping failure on state_dict = {
'diffusion_model.blocks.0.attn.wq.alpha': torch.tensor(1.0),
'diffusion_model.blocks.0.attn.wq.lokr_w1': torch.randn(2, 2),
'diffusion_model.blocks.0.attn.wq.lokr_w2': torch.randn(3, 3),
}
The seven |
Summary
Krea-2 LoRAs in LyCORIS LoKr format can't be installed at all at the moment. The probe turns them away with:
I hit this with an adapter trained in ai-toolkit, which emits LoKr by default, so I'd guess a fair few of the Krea-2 LoRAs going around are in the same boat.
The Krea-2 probe only knows about
lora_A/lora_Bandlora_down/lora_up. LoKr carries Kronecker factors instead (lokr_w1/lokr_w2, or the factored_a/_bpairs plus an optionallokr_t2), so_has_complete_lora_pair()says no and the adapter is never identified. Setting the base by hand doesn't get you anywhere either, that path goes through the same helper.There's a second one further down:
_SUFFIX_TO_VALUE_KEYdoesn't list thelokr_*suffixes, so even past the probe_group_by_layer()would drop the weights and leave every layer holding nothing but its alpha.The rest was already there.
LoKRLayerexists,any_lora_layer_from_state_dict()reaches for it the moment it seeslokr_w1, and the Qwen-Image converter handles LoKr fine. Only the Krea-2 side was never wired up, which fits it being marked WIP in #9304.So this adds the two missing pieces: a
_has_lokr_layer()helper that both config gates consult, and thelokr_*suffixes in the converter's suffix map. I left the signature-module check alone, so a LoKr that only touches generic transformer blocks still won't be claimed as Krea-2.Related Issues / Discussions
Follows on from #9304.
QA Instructions
Take any ai-toolkit LoKr for Krea-2 (keys look like
diffusion_model.txtfusion.<block>.attn.wq.lokr_w1) and install it through the Model Manager. Before, it's rejected with the error above. After, it installs asbase=krea-2, format=lycorisand turns up in the LoRA picker.I tested with a 32-module adapter on the text-fusion stage against a GGUF-quantised Krea-2 Turbo. It converts to 32
LoKRLayers, every target key resolves to a realnn.LinearinKrea2Transformer2DModel, the reconstructed weight (4×4 ⊗ 640×640 = 2560×2560) matches the layer it patches, and generation comes out visibly different with the LoRA switched on.8 new tests, covering full and factored LoKr, the native→diffusers key renaming, and that a LoKr without the Krea-2 signature modules still gets turned away.
tests/backend/patches/andtests/backend/model_manager/pass (1030 passed, 133 skipped, 1 xfailed), as do ruff check and format. I only ran them on 3.12/Windows locally, so the rest of the matrix is on CI.Merge Plan
Nothing special, backend only.
Checklist
What's Newcopy (if doing a release after this PR)