From 5978716c987a3f7ef2f03bbcb597a494e13bb103 Mon Sep 17 00:00:00 2001 From: CalamitousFelicitousness Date: Sat, 9 May 2026 16:09:43 +0100 Subject: [PATCH] feat(flux2): native GLoRA loader Loads the four ab.weight tensors plus alpha. Fused QKV is skipped: GLoRA's target-dependent term doesn't row-slice cleanly without recomputing against a fused proxy weight. --- pipelines/flux/flux2_lora.py | 52 +++++++++++++++++++++++++++++++++++- 1 file changed, 51 insertions(+), 1 deletion(-) diff --git a/pipelines/flux/flux2_lora.py b/pipelines/flux/flux2_lora.py index abfbed43c..c8f3e5d60 100644 --- a/pipelines/flux/flux2_lora.py +++ b/pipelines/flux/flux2_lora.py @@ -38,7 +38,7 @@ import time import torch from modules import shared, sd_models from modules.logger import log -from modules.lora import network, network_lora, network_lokr, network_hada, network_oft, network_ia3, lora_convert +from modules.lora import network, network_lora, network_lokr, network_hada, network_oft, network_ia3, network_glora, lora_convert from modules.lora import lora_common as l @@ -86,12 +86,18 @@ IA3_SUFFIXES = ( ".weight", ".on_input", ".alpha", ".scale", ) +GLORA_SUFFIXES = ( + ".a1.weight", ".a2.weight", + ".b1.weight", ".b2.weight", + ".alpha", ".dora_scale", ".scale", +) LORA_MARKERS = (".lora_down.weight", ".lora_up.weight", ".lora_A.weight", ".lora_B.weight") LOKR_MARKERS = (".lokr_w1", ".lokr_w2") LOHA_MARKERS = (".hada_w1_a", ".hada_w1_b", ".hada_w2_a", ".hada_w2_b") OFT_MARKERS = (".oft_blocks", ".oft_diag") IA3_MARKERS = (".on_input",) # NOT .weight — too generic, overlaps every other family +GLORA_MARKERS = (".a1.weight", ".a2.weight", ".b1.weight", ".b2.weight") # === BFL → diffusers mapping === @@ -523,6 +529,50 @@ def try_load_ia3(name, network_on_disk, lora_scale): return finalize_network(net, name, 'IA3', lora_scale, t0, unmapped=unmapped, skipped=skipped) +def try_load_glora(name, network_on_disk, lora_scale): + """Load a Flux2/Klein GLoRA adapter as native modules. + + GLoRA stores four low-rank components (``a1``/``a2``/``b1``/``b2``) and + computes ``ΔW = w2b @ w1b + (target @ w2a) @ w1a`` — the second term is + target-dependent. Fused QKV in double_blocks is skipped with a warning + because the target-dependent term doesn't slice cleanly without + redirecting calc_updown to a fused proxy weight, and zero real-world + GLoRA-on-DiT files exist. + + Depends on the ``self.dim`` initialization fix in network_glora.py so + that alpha-based ``calc_scale`` is honored. + """ + t0 = time.time() + state_dict = sd_models.read_state_dict(network_on_disk.filename, what='network') + if not has_marker(state_dict, GLORA_MARKERS): + return None + + mapping = resolve_mapping() + net = new_network(name, network_on_disk) + groups = group_by_suffixes(state_dict, GLORA_SUFFIXES) + + unmapped = 0 + skipped = 0 + for (prefix, base), w in groups.items(): + if not all(k in w for k in ('a1.weight', 'a2.weight', 'b1.weight', 'b2.weight')): + continue + targets = resolve_targets(prefix, base) + if any(t[1] is not None for t in targets): + log.warning(f'Network load: type=GLoRA name="{name}" key={base} fused QKV skipped (unsupported)') + skipped += 1 + continue + for diffusers_path, _, _ in targets: + network_key = "lora_transformer_" + diffusers_path.replace(".", "_") + sd_module = mapping.get(network_key) + if sd_module is None: + unmapped += 1 + continue + nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=w, sd_module=sd_module) + net.modules[network_key] = network_glora.NetworkModuleGLora(net, nw) + + return finalize_network(net, name, 'GLoRA', lora_scale, t0, unmapped=unmapped, skipped=skipped) + + # === Diffusers-PEFT path helpers (used when lora_force_diffusers is on) ===