[LoRA] add LoKr adapter support (Z-Image, Flux2/Klein)#14163
[LoRA] add LoKr adapter support (Z-Image, Flux2/Klein)#14163christopher5106 wants to merge 1 commit into
Conversation
Adds loading of LoKr (LyCORIS Kronecker product) adapters: - `load_lora_adapter` detects `lokr_` keys and injects a peft `LoKrConfig`, inferred from the tensor shapes via `_create_lokr_config` (decompose factor, per-module rank/alpha patterns). - State dict conversions for the formats in the wild: ai-toolkit Z-Image (dotted diffusers paths under `diffusion_model.`), ai-toolkit BFL Flux2 (fused qkv), LyCORIS underscore format, and bare dotted diffusers paths. - BFL fused-QKV LoKr cannot be split exactly into separate Q/K/V Kronecker factors, so `Flux2LoraLoaderMixin.load_lora_weights` fuses the model's QKV projections and maps the adapter 1:1 (exact). - Alpha follows the LyCORIS convention: scaling applies only to rank-decomposed factors and is baked into the weights at conversion. Fixes huggingface#13221
|
Will review next week but triggering an AI review as well. |
There was a problem hiding this comment.
🤗 Serge says:
Solid, well-tested addition of LoKr adapter loading for Z-Image and Flux2/Klein. The design is coherent: shape-inferred LoKrConfig, alpha baked into the left Kronecker factor per LyCORIS convention, and a fuse-then-map strategy for the fused-QKV case that avoids the lossy per-chunk re-factorization. I verified the key integration points and found no blocking issues.
Correctness (verified, no defects)
_create_lokr_config—collectionsis imported;factorization/LoKrConfigare imported lazily from peft; thedecompose_factorsearch andrank/rank_patternderivation are internally consistent and reproduced exactly by the tests._bake_lokr_alpha— rank is read from the inner dimension of the decomposed factor (w2_b.shape[0]/w1_b.shape[0]), and full-matrix modules correctly drop alpha via therank is Noneguard. Scaling matches the test expectations ((alpha/rank) * kron(w1, w2_a@w2_b)).- Flux2 fused-QKV path —
Flux2Transformer2DModelinheritsfuse_qkv_projectionsfromAttentionMixin; the"Added" in processor nameguard does not spuriously trip (Flux2 processors areFlux2AttnProcessor/Flux2KVAttnProcessor), andfuse_projectionscreatesto_qkv/to_added_qkvused by_get_fused_projectionsat inference. The.attn.to_qkv.substring check does not falsely matchto_qkv_mlp_proj.is_quantizedis a real attribute on the model. reis imported inlora_conversion_utils.py;loggerexists inlora_pipeline.py.
Minor (non-blocking)
- In
_bake_lokr_alpha, a malformed module that has alokr_w*_bbut neitherlokr_w1norlokr_w1_awould raiseKeyErrorat the final assignment. Not reachable for the four in-the-wild formats, so purely defensive — fine to leave.
Tests
Flux2LoKrTestsandZImageLoKrTestscover fused QKV (img+txt), rank-decomposed factors with meaningful alpha, full-rank factors with placeholder alpha, and the LyCORIS underscore format, assertingget_delta_weightequalskron(w1, w2). All referenced imports resolve.
LGTM.
serge v0.1.0 · model: claude-opus-4-8 · 19 LLM turns · 23 tool calls · 110.2s · 880940 in / 7039 out tokens
BenjaminBossan
left a comment
There was a problem hiding this comment.
From the PEFT perspective, this looks correct to me. I have two small comments, but I'll leave it to the Diffusers maintainers to decide if they're worth addressing.
| if not is_correct_format: | ||
| raise ValueError("Invalid LoRA checkpoint. Make sure all LoRA param names contain `'lora'` substring.") | ||
| raise ValueError( | ||
| "Invalid LoRA checkpoint. Make sure all LoRA param names contain the `'lora'` or `'lokr'` substring." |
There was a problem hiding this comment.
The message could be confusing because it still mentions "LoRA checkpoint".
How about "Invalid adapter checkpoint. We currently support LoRA and LoKr."
I wouldn't really mention substring matching, as it's an implementation detail. Same applies to the error messages below.
| # be split exactly into separate Q/K/V factors. Fuse the model's projections so the adapter maps 1:1. | ||
| needs_fused_qkv = any(".attn.to_qkv." in k or ".attn.to_added_qkv." in k for k in state_dict) | ||
| if needs_fused_qkv: | ||
| if getattr(transformer, "is_quantized", False): |
There was a problem hiding this comment.
Another potential error case would be if there is already an adapter loaded onto a non-fused q, k, or v layer, right? Maybe not terribly likely, but could be worth checking.
What does this PR do?
Adds support for loading LoKr (LyCORIS Kronecker product) adapters, for Z-Image and Flux2/Klein checkpoints in the wild, plus a generic path that works for any model whose LoKr state dict already uses diffusers module names.
Fixes #13221. Also addresses #13261 and #13137 (Flux 2 Klein LoKr).
Follows up on the discussion in #13326 — see "Why not split the fused QKV" below for measurements that motivated a different approach for the fused-QKV part. cc @CalamitousFelicitousness whose prototype this builds on.
How it works
PeftAdapterMixin.load_lora_adapterdetectslokr_keys and injects a peftLoKrConfiginstead of aLoraConfig. The new_create_lokr_configinfers the config from tensor shapes: it reconstructs each module's Kronecker factorization and finds thedecompose_factorunder which peft's ownfactorization()reproduces every shape; per-modulerank_pattern/alpha_patternmake peft recreate full vs. rank-decomposed factors exactly as stored (for full-matrix factors the rank is set tomax(lokr_w2.shape)so peft also creates full matrices).alpha / rankscaling applies only when a factor is rank-decomposed and is baked intolokr_w1at conversion; full-rank modules ignore alpha (ai-toolkit stores a ~1e10 placeholder there). The config setsalpha = rso the runtime scaling is 1.0.diffusion_model.layers.0.attention.to_q.lokr_w1)F16/z-image-turbo-flow-dpo(the checkpoint from #13221)_convert_non_diffusers_lokr_to_diffusers(paths already match the model)diffusion_model.double_blocks.0.img_attn.qkv.lokr_w1)puttmorbidly233/loraklein_snofs_v1_2.safetensors(the checkpoint from #13261)_convert_non_diffusers_flux2_lokr_to_diffuserslycoris_transformer_blocks_0_attn_to_q.lokr_w1)gattaplayer/besch-flux2-klein-9b-lokr-lion-3e-6-bs2-ga2-v02_convert_lycoris_flux2_lokr_to_diffusers.alphakeysbghira/flux2-klein-9b-distillation-lokr_convert_non_diffusers_lokr_to_diffusersWhy not split the fused QKV
BFL-format checkpoints apply LoKr to the fused QKV projection of the double blocks, while the diffusers Flux2 model has separate Q/K/V. For LoRA this split is exact (shared
lora_A, chunkedlora_B), but a Kronecker product delta over the fused projection is mathematically not a Kronecker product per chunk. #13326 approximated each chunk with a rank-1 Van Loan re-factorization; measuring that on the realklein_snofs_v1_2checkpoint gives a mean relative delta error of 69% on the 48 QKV projections (max 79%, min 32%), i.e. the adapter loads but its QKV contribution is mostly destroyed — the same class of problem that got #13997 rejected.Instead, the converter maps these modules to the model's fused
attn.to_qkv/attn.to_added_qkvprojections, andFlux2LoraLoaderMixin.load_lora_weightscallstransformer.fuse_qkv_projections()(idempotent) before injection, with an info log. Loading is then exact. Quantized transformers raise a clear error for such checkpoints since fusing would concatenate packed quantized weights.Tests / verification
ZImageLoKrTestsandFlux2LoKrTests: injectedget_delta_weightequalskron(w1, w2)exactly, covering fused QKV (img + txt streams), rank-decomposed factors with meaningful alpha, full-rank factors with placeholder alpha, and the LyCORIS format.F16/z-image-turbo-flow-dpoloaded into a real-dimension (dim=3840) Z-Image transformer: all deltas exact, scaling 1.0.tests/lora/test_lora_layers_z_image.py(36 passed) andtests/lora/test_lora_layers_flux2.py(39 passed).Who can review?
@sayakpaul @asomoza