Multiple improvements and hardening across the Power LoRA stack: - adaln: stronger caching (file-version keys), device-safe grid/basis computation, normalized tensor hashing for basis cache, more robust table-to-table rebasing with residual checks and fallback, better detection of adaLN weight name variants. - apply: validate numeric strengths, reject unsupported passengers, handle loading errors, detect shape mismatches, track rejected/skipped/quantized merges, improved PDD handoff and reporting. - pdd: introduce movable PddBank/PddState, register banks as additional models, thread-local PDD context, strength-scheduled blending and safer interval clamping. - keymap/detect/gain/modality/nodes/branch: add support for varied LoRA key spellings, more validation, skip incomplete adapters, and small bugfixes. - server: surface partial calibration note. - pyproject/README/.gitignore: packaging, docs, and ignore cleanup; add Python >=3.10 requirement and publisher repo fix. - web UI: fix balance button behavior, safer auto-balance error handling and factor sanitization. - tests: add unit tests for adaln, modality and pdd. Overall: better robustness to varied LoRA formats, mixed devices, malformed inputs, and improved user-facing error/reporting.
131 lines
4.9 KiB
Python
131 lines
4.9 KiB
Python
"""Key normalisation for MiniMax H3 LoRAs.
|
|
|
|
Every H3 LoRA in the wild is one of a handful of naming conventions. Observed
|
|
across the local collection:
|
|
|
|
* ai-toolkit / diffusers ``diffusion_model.blocks.0.attn.qkv_proj.lora_A.weight``
|
|
* same, no prefix ``blocks.0.attn.qkv_proj.lora_A.weight``
|
|
* kohya / musubi ``lora_unet_blocks_0_attn_qkv_proj.lora_down.weight`` + ``.alpha``
|
|
* lycoris ``lycoris_blocks_0_attn_qkv_proj.*``
|
|
* peft ``base_model.model.blocks.0...`` / ``transformer.blocks.0...``
|
|
|
|
``comfy.lora.model_lora_keys_unet`` already covers the ``diffusion_model.``-
|
|
prefixed and ``lora_unet_`` forms, but not the bare form, and it gives us no
|
|
handle for rewriting adaLN tensors before they are matched. So we normalise
|
|
everything ourselves onto the canonical ``diffusion_model.<path>`` module key,
|
|
resolving underscore forms through the model's own state dict rather than
|
|
guessing where the word boundaries are.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
|
|
# suffixes comfy.lora.load_lora understands, longest first so that
|
|
# ``.lora_down.weight`` is not truncated to ``.weight``
|
|
_LORA_SUFFIXES = (
|
|
".lora_linear_layer.down.weight", ".lora_linear_layer.up.weight",
|
|
"_lora.down.weight", "_lora.up.weight",
|
|
".lora.down.weight", ".lora.up.weight",
|
|
".lora_down.weight", ".lora_up.weight",
|
|
".lora_A.weight", ".lora_B.weight",
|
|
".lora_A.default.weight", ".lora_B.default.weight",
|
|
".lora_A", ".lora_B",
|
|
".lora_mid.weight", ".dora_scale",
|
|
".diff_b", ".diff", ".set_weight", ".alpha",
|
|
".hada_w1_a", ".hada_w1_b", ".hada_w2_a", ".hada_w2_b", ".hada_t1", ".hada_t2",
|
|
".lokr_w1", ".lokr_w2", ".lokr_w1_a", ".lokr_w1_b", ".lokr_w2_a", ".lokr_w2_b",
|
|
".lokr_t2", ".reshape_weight", ".oft_blocks", ".boft_blocks", ".rescale",
|
|
".w_norm", ".b_norm",
|
|
)
|
|
|
|
_STRIP_PREFIXES = (
|
|
"diffusion_model.", "transformer.", "base_model.model.", "pipe.dit.",
|
|
"model.diffusion_model.", "unet.",
|
|
)
|
|
|
|
_UNDERSCORE_PREFIXES = ("lora_unet_", "lycoris_", "lora_transformer_")
|
|
|
|
|
|
def split_suffix(key: str):
|
|
"""``blocks.0.mlp.fc1.lora_A.weight`` -> ``('blocks.0.mlp.fc1', '.lora_A.weight')``."""
|
|
for suffix in sorted(_LORA_SUFFIXES, key=len, reverse=True):
|
|
if key.endswith(suffix):
|
|
return key[: -len(suffix)], suffix
|
|
return None, None
|
|
|
|
|
|
def build_module_index(model_sd_keys) -> dict:
|
|
"""Map every spelling of a DiT module path to its canonical form.
|
|
|
|
Canonical form is the state-dict key minus ``.weight``, e.g.
|
|
``diffusion_model.blocks.0.attn.qkv_proj``. Both the dotted path and the
|
|
underscored path (which is what kohya trainers emit) resolve to it, and the
|
|
underscore mapping is derived from the real keys so no heuristic is needed
|
|
to decide whether ``qkv_proj`` is one token or two.
|
|
"""
|
|
index: dict[str, str] = {}
|
|
for k in model_sd_keys:
|
|
if not k.startswith("diffusion_model.") or not k.endswith(".weight"):
|
|
continue
|
|
canonical = k[: -len(".weight")]
|
|
bare = canonical[len("diffusion_model."):]
|
|
index[canonical] = canonical
|
|
index[bare] = canonical
|
|
index[bare.replace(".", "_")] = canonical
|
|
return index
|
|
|
|
|
|
def _strip_prefixes(path: str) -> str:
|
|
changed = True
|
|
while changed:
|
|
changed = False
|
|
for pre in _STRIP_PREFIXES:
|
|
if path.startswith(pre):
|
|
path = path[len(pre):]
|
|
changed = True
|
|
break
|
|
for pre in _UNDERSCORE_PREFIXES:
|
|
if path.startswith(pre):
|
|
path = path[len(pre):]
|
|
break
|
|
return path
|
|
|
|
|
|
def canonical_module(key_body: str, index: dict):
|
|
"""Resolve one LoRA key body to a canonical module path, or None."""
|
|
body = _strip_prefixes(key_body)
|
|
hit = index.get(body)
|
|
if hit is not None:
|
|
return hit
|
|
# kohya writes ``lora_unet__blocks_0_...`` (double underscore) and some
|
|
# trainers keep a dotted tail after the underscored head
|
|
squashed = body.replace(".", "_")
|
|
hit = index.get(squashed) or index.get(re.sub(r"_{2,}", "_", squashed))
|
|
if hit is not None:
|
|
return hit
|
|
return None
|
|
|
|
|
|
def normalize(lora_sd: dict, index: dict):
|
|
"""Rewrite a LoRA state dict onto canonical module keys.
|
|
|
|
Returns ``(normalized_sd, unmatched_keys)``. Non-LoRA passenger tensors
|
|
(``adaln_t_table`` and full ``adaln_proj.linear.weight`` replacements found
|
|
in "complete pruned" LoRA files) are passed through untouched so the caller
|
|
can deal with them.
|
|
"""
|
|
out: dict = {}
|
|
unmatched: list[str] = []
|
|
for key, value in lora_sd.items():
|
|
body, suffix = split_suffix(key)
|
|
if body is None:
|
|
out[key] = value # passenger tensor, handled upstream
|
|
continue
|
|
module = canonical_module(body, index)
|
|
if module is None:
|
|
unmatched.append(key)
|
|
continue
|
|
out[module + suffix] = value
|
|
return out, unmatched
|