mirror of
https://github.com/vladmandic/automatic
synced 2026-09-14 10:38:42 +02:00
b434eb0c1b
The parser no longer takes reference-name prefixes to tell bare reference keys from bare diffusers keys. Any bare key carries the sentinel and the arch resolver renames what it knows and passes the rest through. Flux2 keeps its list for file-format detection only.
1141 lines
49 KiB
Python
1141 lines
49 KiB
Python
"""Shared scaffolding for native adapter loaders.
|
|
|
|
Each per-arch native adapter loader implements the same algorithm:
|
|
|
|
1. Read the safetensors state dict
|
|
2. Test for family-specific markers; bail out if absent
|
|
3. Resolve the diffusers ``network_layer_mapping``
|
|
4. Group state-dict entries by ``(prefix, base)``
|
|
5. For each group, ask the arch to resolve targets (diffusers paths + chunk specs)
|
|
6. Instantiate a :class:`network.NetworkModule*` per resolved target
|
|
7. Return a populated :class:`network.Network` (or ``None`` if no matches)
|
|
|
|
This module holds the parts of that algorithm that don't vary between
|
|
architectures. Constants (suffix and marker tables), helpers (``has_marker``,
|
|
``resolve_mapping``, ``new_network``, ``finalize_network``, ``shapes_match``),
|
|
the parameterized parsing primitives (``parse_key``, ``group_by_suffixes``),
|
|
and the two cross-arch key normalizations (``unwrap_peft_wrapper`` and
|
|
``strip_peft_adapter_name``) live here.
|
|
|
|
The variance that does remain is captured by ``ChunkSpec`` (how a row range
|
|
of a fused weight is described) and the per-arch ``resolve_targets`` callable
|
|
each loader passes in (how a parsed ``(prefix, base)`` maps to one or more
|
|
diffusers paths plus optional chunk descriptors).
|
|
|
|
Per-arch loader modules import this module and pass their own ``prefixes``
|
|
and ``resolve_targets`` to the generic helpers.
|
|
"""
|
|
|
|
import os
|
|
import time
|
|
from dataclasses import dataclass
|
|
|
|
import torch
|
|
|
|
from modules.sd_models import read_state_dict # pylint: disable=unused-import
|
|
from modules.logger import log
|
|
from modules.lora import (
|
|
lora_convert, network, network_boft, network_full, network_glora,
|
|
network_hada, network_ia3, network_lokr, network_lora, network_norm,
|
|
network_oft,
|
|
)
|
|
from modules.lora import lora_common as l
|
|
|
|
|
|
# Universal prefix list shared by every native arch loader. Per-arch loaders
|
|
# extend this with arch-specific entries when their files use additional
|
|
# vendor-specific naming conventions. ``lora_transformer_`` is not a vendor
|
|
# format but sdnext's own internal transformer namespace; files already saved
|
|
# in it (e.g. OneTrainer) pass through verbatim, see :func:`resolve_group_targets`.
|
|
# ``lycoris_`` is the LyCORIS-standalone save format: the wrapped diffusers
|
|
# module path with dots rendered as underscores. It is arch-independent by
|
|
# construction (the wrapped layout equals the target layout), so it lives here
|
|
# rather than in any arch's prefix list.
|
|
KNOWN_PREFIXES_DEFAULT = ("diffusion_model.", "transformer.", "lora_unet_", "lora_transformer_", "lycoris_")
|
|
|
|
|
|
# Sentinel ``prefix_used`` value emitted by :func:`parse_key` for a bare path,
|
|
# one that matched no arch prefix. A loader ``resolve_targets`` may dispatch on
|
|
# this string to rewrite the base path; when it declines,
|
|
# :func:`resolve_group_targets` binds the path verbatim, and a path naming no
|
|
# live module counts as unmapped instead of vanishing.
|
|
BARE_DIFFUSERS_PREFIX_USED = "bare_diffusers"
|
|
|
|
|
|
# Default network-key prefix. Single-component arches keep this default;
|
|
# multi-component arches (those with separate text-encoder or adapter
|
|
# components alongside the transformer) pass a callable that picks per
|
|
# ``prefix_used``.
|
|
NETWORK_PREFIX_DEFAULT = "lora_transformer_"
|
|
|
|
|
|
# Prefixes whose parsed ``base`` is normally already a network-key tail
|
|
# (``arch_prefix + base.replace(".", "_")`` matches the stamped module name).
|
|
# :func:`resolve_group_targets` binds them verbatim unless the arch's
|
|
# ``resolve_targets`` claims the group first (needed when the arch's module
|
|
# tree diverges from the names these formats carry). ``lycoris_`` bases are
|
|
# already underscored, so the loader's ``.replace(".", "_")`` is a no-op on them.
|
|
PASSTHROUGH_PREFIXES_DEFAULT = ("transformer.", BARE_DIFFUSERS_PREFIX_USED, "lora_transformer_", "lycoris_")
|
|
|
|
|
|
def _resolve_prefix(network_prefix, prefix_used):
|
|
"""Return the network-key prefix for one parsed group.
|
|
|
|
``network_prefix`` is either a literal string (single-component arches) or
|
|
a ``Callable[[str | None], str]`` that picks based on which arch prefix
|
|
was matched (multi-component arches route to ``lora_te_`` / ``lora_llm_adapter_``
|
|
/ ``lora_transformer_`` etc.).
|
|
"""
|
|
return network_prefix(prefix_used) if callable(network_prefix) else network_prefix
|
|
|
|
|
|
SUFFIX_NORMALIZE = {
|
|
"lora_A.weight": "lora_down.weight",
|
|
"lora_B.weight": "lora_up.weight",
|
|
}
|
|
|
|
|
|
# === Family suffix tables ===
|
|
# Alpha / scale / bias / dora_scale flow into ``weights.w`` via the base
|
|
# ``network.NetworkModule.__init__`` and are listed here so they survive the
|
|
# suffix-filter pass in :func:`parse_key`.
|
|
|
|
LORA_SUFFIXES = (
|
|
".lora_down.weight", ".lora_up.weight", ".lora_mid.weight",
|
|
".lora_A.weight", ".lora_B.weight",
|
|
# diff_b: bias delta some saves pair with the weight LoRA, applied as ex_bias.
|
|
# magnitude / lora_magnitude_vector: DoRA row norms (ai-toolkit / PEFT key
|
|
# names); converted onto the dora_scale path by try_load_lora.
|
|
# bias_indices/values/size: LyCORIS extraction sparse residual triplet,
|
|
# reconstructed into the dense-bias path by network.NetworkModule.
|
|
".alpha", ".dora_scale", ".magnitude", ".lora_magnitude_vector",
|
|
".bias", ".bias_indices", ".bias_values", ".bias_size", ".diff_b", ".scale",
|
|
)
|
|
LOKR_SUFFIXES = (
|
|
".lokr_w1", ".lokr_w2",
|
|
".lokr_w1_a", ".lokr_w1_b",
|
|
".lokr_w2_a", ".lokr_w2_b",
|
|
".lokr_t2",
|
|
".alpha", ".dora_scale", ".bias", ".scale",
|
|
)
|
|
LOHA_SUFFIXES = (
|
|
".hada_w1_a", ".hada_w1_b",
|
|
".hada_w2_a", ".hada_w2_b",
|
|
".hada_t1", ".hada_t2",
|
|
".alpha", ".dora_scale", ".bias", ".scale",
|
|
)
|
|
OFT_SUFFIXES = (
|
|
".oft_blocks", ".oft_diag",
|
|
".alpha", ".dora_scale", ".bias", ".scale",
|
|
)
|
|
IA3_SUFFIXES = (
|
|
".weight", ".on_input",
|
|
".alpha", ".scale",
|
|
)
|
|
GLORA_SUFFIXES = (
|
|
".a1.weight", ".a2.weight",
|
|
".b1.weight", ".b2.weight",
|
|
".alpha", ".dora_scale", ".scale",
|
|
)
|
|
NORM_SUFFIXES = (
|
|
".w_norm", ".b_norm",
|
|
".alpha", ".scale",
|
|
)
|
|
FULL_SUFFIXES = (
|
|
".diff", ".diff_b",
|
|
".alpha", ".scale",
|
|
)
|
|
|
|
|
|
# === Family marker tables ===
|
|
# Presence of any marker substring anywhere in a key triggers a try-load attempt
|
|
# for that family. Markers are deliberately narrower than suffixes (e.g. IA3's
|
|
# ``.on_input`` rather than ``.weight``) so :func:`has_marker` does not light up
|
|
# on accidental overlaps with other families.
|
|
|
|
LORA_MARKERS = (
|
|
".lora_down.weight", ".lora_up.weight",
|
|
".lora_A.weight", ".lora_B.weight",
|
|
# PEFT named-adapter saves embed the slot name as ``.lora_A.<name>.weight``;
|
|
# the trailing-dot forms catch every variant.
|
|
".lora_A.", ".lora_B.",
|
|
)
|
|
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")
|
|
NORM_MARKERS = (".w_norm",)
|
|
FULL_MARKERS = (".diff",)
|
|
|
|
|
|
# === Chunk descriptor ===
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ChunkSpec:
|
|
"""How to take a fused weight's rows along dim 0 for one target module.
|
|
|
|
Three forms; the reorder composes with either slice:
|
|
|
|
- Equal chunks (``idx`` + ``total``): fused QKV split into Q/K/V via
|
|
``torch.chunk(up, total, dim=0)[idx]``. Used by flux2 / z-image where
|
|
Q, K and V have the same ``out_features``.
|
|
- Row range (``start`` + ``end``): asymmetric partition via
|
|
``up[start:end]``. Used by chroma's single-block ``linear1`` which
|
|
fuses Q / K / V / proj_mlp at unequal sizes
|
|
(``[3072, 3072, 3072, 12288]``).
|
|
- Row reorder (``reorder``): the module lays out equal row blocks in a
|
|
different order from the save; ``(1, 0)`` swaps the halves of a fused
|
|
SwiGLU projection saved ``[gate; value]`` onto a ``[value; gate]``
|
|
module. Applied to the rows the slice selects. Only the LoRA family
|
|
permutes rows; the others skip a reordered target.
|
|
|
|
Generic loaders check :attr:`is_equal_chunks` to decide between the two
|
|
forms and select the appropriate ``NetworkModule*Chunk`` /
|
|
``NetworkModule*SliceChunk`` variant.
|
|
"""
|
|
idx: int | None = None
|
|
total: int | None = None
|
|
start: int | None = None
|
|
end: int | None = None
|
|
reorder: tuple[int, ...] | None = None
|
|
|
|
@property
|
|
def is_equal_chunks(self) -> bool:
|
|
return self.idx is not None and self.total is not None
|
|
|
|
@property
|
|
def is_slice(self) -> bool:
|
|
return self.is_equal_chunks or self.start is not None
|
|
|
|
|
|
# === Key normalizations (applied universally by parse_key) ===
|
|
|
|
|
|
def unwrap_peft_wrapper(key):
|
|
"""Strip the ``base_model.model.`` prefix added by ``peft.save_pretrained``.
|
|
|
|
PeftModel.save_pretrained prepends this wrapper to every adapter key. The
|
|
content underneath can be any of the standard prefixes (BFL, diffusers PEFT,
|
|
kohya, bare); a single strip lets the rest of :func:`parse_key` handle the
|
|
unwrapped key normally. Mirrors the diffusers ``Flux2LoraLoaderMixin``
|
|
behavior of renaming ``base_model.model.`` to ``diffusion_model.`` before
|
|
feeding the converter.
|
|
"""
|
|
if key.startswith("base_model.model."):
|
|
return key[len("base_model.model."):]
|
|
return key
|
|
|
|
|
|
def strip_peft_adapter_name(key):
|
|
"""Normalize ``.lora_[AB].<adapter_name>.weight`` to ``.lora_[AB].weight``.
|
|
|
|
``peft.PeftModel`` and the diffusers ``save_lora_adapter`` exporter embed
|
|
the adapter slot name into the saved key (``"default"`` when not explicitly
|
|
set). Strip a single non-dotted name segment so the suffix table matches
|
|
without having to list every plausible adapter name.
|
|
"""
|
|
for inner in (".lora_A.", ".lora_B."):
|
|
idx = key.find(inner)
|
|
if idx == -1:
|
|
continue
|
|
rest = key[idx + len(inner):]
|
|
if rest == "weight" or not rest.endswith(".weight"):
|
|
continue
|
|
adapter_name = rest[:-len(".weight")]
|
|
if adapter_name and "." not in adapter_name:
|
|
return key[:idx] + inner + "weight"
|
|
return key
|
|
|
|
|
|
# === Core helpers ===
|
|
|
|
|
|
def has_marker(state_dict, markers):
|
|
"""Substring scan: does any key in ``state_dict`` contain any marker?"""
|
|
return any(any(m in k for m in markers) for k in state_dict)
|
|
|
|
|
|
def resolve_mapping():
|
|
from modules import shared
|
|
"""Ensure ``network_layer_mapping`` is populated, return it (or empty dict)."""
|
|
sd_model = getattr(shared.sd_model, "pipe", shared.sd_model)
|
|
lora_convert.assign_network_names_to_compvis_modules(sd_model)
|
|
return getattr(shared.sd_model, "network_layer_mapping", {}) or {}
|
|
|
|
|
|
def new_network(name, network_on_disk):
|
|
"""Construct an empty :class:`network.Network` with the file's mtime stamped."""
|
|
net = network.Network(name, network_on_disk)
|
|
net.mtime = os.path.getmtime(network_on_disk.filename)
|
|
return net
|
|
|
|
|
|
def finalize_network(net, name, family, lora_scale, t0, unmapped=0, mismatch=0, skipped=0):
|
|
"""Emit the standard debug log line and return the populated network (or ``None``).
|
|
|
|
Returns ``None`` when no modules were bound. Records ``mismatch`` on the
|
|
network so :func:`try_load_chain` can refuse the file as a whole.
|
|
"""
|
|
net.mismatch = mismatch
|
|
if len(net.modules) == 0:
|
|
if mismatch:
|
|
log.error(
|
|
f'Network load: type={family} name="{name}" native no-match'
|
|
f' unmapped={unmapped} mismatch={mismatch} skipped={skipped}'
|
|
)
|
|
elif unmapped or skipped:
|
|
log.debug(
|
|
f'Network load: type={family} name="{name}" native no-match'
|
|
f' unmapped={unmapped} skipped={skipped}'
|
|
)
|
|
return None
|
|
log.debug(
|
|
f'Network load: type={family} name="{name}" native modules={len(net.modules)}'
|
|
f' unmapped={unmapped} mismatch={mismatch} skipped={skipped} scale={lora_scale}'
|
|
)
|
|
l.timer.activate += time.time() - t0
|
|
return net
|
|
|
|
|
|
def shapes_match(sd_module, down_w: torch.Tensor, up_w: torch.Tensor) -> bool:
|
|
"""LoRA-style rank-and-dim sanity check against the live module weight.
|
|
|
|
Honors SDNQ-quantized modules by reading the original shape from the
|
|
dequantizer rather than the packed weight tensor.
|
|
"""
|
|
if not hasattr(sd_module, "weight"):
|
|
return False
|
|
if hasattr(sd_module, "sdnq_dequantizer"):
|
|
mod_shape = sd_module.sdnq_dequantizer.original_shape
|
|
else:
|
|
mod_shape = sd_module.weight.shape
|
|
if len(mod_shape) < 2 or len(down_w.shape) < 2 or len(up_w.shape) < 2:
|
|
return False
|
|
return down_w.shape[1] == mod_shape[1] and up_w.shape[0] == mod_shape[0]
|
|
|
|
|
|
def lokr_kron_shape(w):
|
|
"""Return the ``(out, in_flat)`` shape of ``kron(w1, w2)`` from stored factors.
|
|
|
|
Each factor is either full (``lokr_w1``/``lokr_w2``) or rank-decomposed
|
|
(``lokr_w1_a @ lokr_w1_b``); a Tucker-rebuilt w2 (conv-only) stores its
|
|
parts as ``(rank, part)`` with the kernel dims carried by ``lokr_t2``.
|
|
Kernel dims are folded into ``in_flat``, matching how the delta reshapes
|
|
onto a conv weight.
|
|
"""
|
|
w1 = w.get("lokr_w1")
|
|
r1 = w1.shape[0] if w1 is not None else w["lokr_w1_a"].shape[0]
|
|
c1 = w1.shape[1] if w1 is not None else w["lokr_w1_b"].shape[1]
|
|
w2 = w.get("lokr_w2")
|
|
t2 = w.get("lokr_t2")
|
|
if w2 is not None:
|
|
r2 = w2.shape[0]
|
|
c2_flat = w2.numel() // r2
|
|
elif t2 is not None:
|
|
r2 = w["lokr_w2_a"].shape[1]
|
|
c2_flat = w["lokr_w2_b"].shape[1]
|
|
for d in t2.shape[2:]:
|
|
c2_flat *= d
|
|
else:
|
|
r2 = w["lokr_w2_a"].shape[0]
|
|
w2b = w["lokr_w2_b"]
|
|
c2_flat = w2b.numel() // w2b.shape[0]
|
|
return r1 * r2, c1 * c2_flat
|
|
|
|
|
|
def bias_delta_fits(sd_module, bias_w: torch.Tensor) -> bool:
|
|
"""Bias-delta sanity check against the live module bias.
|
|
|
|
A module with no bias is not a mismatch: whole architectures are built
|
|
``bias=False`` (flux2 has not one biased module), so a stray delta there is
|
|
one unappliable key and the apply pass counts it refused.
|
|
"""
|
|
bias = getattr(sd_module, "bias", None)
|
|
if bias is None:
|
|
return True
|
|
return tuple(bias_w.shape) == tuple(bias.shape)
|
|
|
|
|
|
def lokr_shapes_match(sd_module, kron_shape, chunk: ChunkSpec | None) -> bool:
|
|
"""Kron-vs-module dim check, honoring SDNQ original shapes and chunk rows.
|
|
|
|
The input dim is never chunked; the output dim must cover the full fused
|
|
weight for chunked targets (``total * out`` for equal chunks, ``end <=
|
|
kron_out`` with an exact row-range for slices).
|
|
"""
|
|
if not hasattr(sd_module, "weight"):
|
|
return False
|
|
if hasattr(sd_module, "sdnq_dequantizer"):
|
|
mod_shape = sd_module.sdnq_dequantizer.original_shape
|
|
else:
|
|
mod_shape = sd_module.weight.shape
|
|
if len(mod_shape) < 2:
|
|
return False
|
|
mod_in_flat = 1
|
|
for d in mod_shape[1:]:
|
|
mod_in_flat *= d
|
|
kron_out, kron_in_flat = kron_shape
|
|
if kron_in_flat != mod_in_flat:
|
|
return False
|
|
if chunk is None or not chunk.is_slice:
|
|
return kron_out == mod_shape[0]
|
|
if chunk.is_equal_chunks:
|
|
return kron_out == mod_shape[0] * chunk.total
|
|
return (chunk.end - chunk.start) == mod_shape[0] and kron_out >= chunk.end
|
|
|
|
|
|
# === Parsing primitives ===
|
|
|
|
|
|
def parse_key(key, suffixes, *, prefixes=KNOWN_PREFIXES_DEFAULT):
|
|
"""Return ``(prefix_used, base, suffix_normalized)`` or ``None``.
|
|
|
|
``prefix_used`` is the matched element of ``prefixes``, or
|
|
``BARE_DIFFUSERS_PREFIX_USED`` for a bare key, which the loader offers to
|
|
the resolver and counts as unmapped when nothing binds. ``base`` is the
|
|
path with prefix and suffix removed. ``suffix_normalized`` is the suffix
|
|
(without the leading dot) after applying :data:`SUFFIX_NORMALIZE` (e.g.
|
|
``lora_A.weight`` becomes ``lora_down.weight``).
|
|
|
|
Always applies :func:`unwrap_peft_wrapper` and :func:`strip_peft_adapter_name`
|
|
to the raw key before format detection so callers do not have to opt in.
|
|
"""
|
|
key = unwrap_peft_wrapper(key)
|
|
key = strip_peft_adapter_name(key)
|
|
prefix_used = None
|
|
stripped = key
|
|
for p in prefixes:
|
|
if key.startswith(p):
|
|
prefix_used = p
|
|
stripped = key[len(p):]
|
|
break
|
|
if prefix_used is None:
|
|
prefix_used = BARE_DIFFUSERS_PREFIX_USED
|
|
|
|
matched_suffix = None
|
|
split_at = -1
|
|
for marker in suffixes:
|
|
if stripped.endswith(marker):
|
|
split_at = len(stripped) - len(marker)
|
|
matched_suffix = marker.lstrip(".")
|
|
break
|
|
if split_at < 0:
|
|
return None
|
|
|
|
base = stripped[:split_at]
|
|
if not base:
|
|
return None
|
|
|
|
suffix = SUFFIX_NORMALIZE.get(matched_suffix, matched_suffix)
|
|
return prefix_used, base, suffix
|
|
|
|
|
|
def group_by_suffixes(state_dict, suffixes, *, prefixes=KNOWN_PREFIXES_DEFAULT):
|
|
"""Group state-dict entries by ``(prefix_used, base)``.
|
|
|
|
Returns ``{(prefix_used, base): {suffix: tensor, ...}}`` where each suffix
|
|
is the normalized form produced by :func:`parse_key`. Per-family loaders
|
|
apply their own key-presence gates on each group (e.g. LoRA requires both
|
|
``lora_down.weight`` and ``lora_up.weight``).
|
|
"""
|
|
groups: dict[tuple, dict[str, torch.Tensor]] = {}
|
|
for key, value in state_dict.items():
|
|
parsed = parse_key(key, suffixes, prefixes=prefixes)
|
|
if parsed is None:
|
|
continue
|
|
prefix_used, base, suffix = parsed
|
|
slot = groups.get((prefix_used, base))
|
|
if slot is None:
|
|
slot = {}
|
|
groups[(prefix_used, base)] = slot
|
|
slot[suffix] = value
|
|
return groups
|
|
|
|
|
|
def resolve_group_targets(resolve_targets, prefix_used, base):
|
|
"""Map a parsed ``(prefix_used, base)`` group to ``[(diffusers_path, chunk), ...]``.
|
|
|
|
The arch's ``resolve_targets`` is consulted first for every group.
|
|
Passthrough prefixes (:data:`PASSTHROUGH_PREFIXES_DEFAULT`) fall back to
|
|
verbatim binding when the resolver declines (returns empty), so arches
|
|
whose module tree matches the stamped names need no rewrite branch, while
|
|
arches with a divergent tree (e.g. krea2, whose transformer keeps checkpoint
|
|
names that differ from the ecosystem's diffusers names) can rewrite them.
|
|
"""
|
|
if prefix_used in PASSTHROUGH_PREFIXES_DEFAULT:
|
|
return resolve_targets(prefix_used, base) or [(base, None)]
|
|
return resolve_targets(prefix_used, base)
|
|
|
|
|
|
# === Generic family loaders ===
|
|
#
|
|
# Each loader takes a per-arch ``resolve_targets`` callable returning a list of
|
|
# ``(diffusers_path, ChunkSpec | None)`` tuples for each parsed group. The
|
|
# loader builds the network key as ``network_prefix + path.replace(".", "_")``
|
|
# where ``network_prefix`` defaults to ``"lora_transformer_"`` and may be
|
|
# either a literal string (single-component arches) or a callable that picks
|
|
# per ``prefix_used`` (multi-component arches such as Anima that route to
|
|
# ``lora_te_`` or ``lora_llm_adapter_`` based on which arch prefix matched).
|
|
# The loader then instantiates the appropriate ``network.NetworkModule*`` subclass.
|
|
#
|
|
# Fused-target handling per family:
|
|
#
|
|
# - LoRA: chunk at load time (lora_up is sliced along dim 0). Both equal and
|
|
# unequal ChunkSpec shapes are supported.
|
|
# - LoKR: defer to NetworkModuleLokrChunk (equal) or NetworkModuleLokrSliceChunk
|
|
# (unequal) which materialize the Kronecker product once and return the
|
|
# designated slice.
|
|
# - LoHA: only equal ChunkSpec is supported via NetworkModuleHadaChunk. Unequal
|
|
# slices and Tucker-decomposed LoHAs on fused targets are skipped with a
|
|
# warning (no slice variant exists, and Tucker keys cannot arise on Linear
|
|
# layers per LyCORIS upstream — see network_hada.NetworkModuleHadaChunk).
|
|
#
|
|
# DoRA on fused targets (LoRA/LoKR/LoHA): per-output dora_scale rows are
|
|
# sliced with the chunk via slice_dora_scale (exact, row norms are
|
|
# independent); per-input dora_scale couples the chunks through shared
|
|
# column norms and the group is skipped with a warning.
|
|
#
|
|
# Bias companions on fused targets: per-output diff_b deltas slice with the
|
|
# chunk (LoRA only; no other family's save format pairs them). The legacy
|
|
# weight-shaped "bias" key has no defined partition and skips the group.
|
|
# - OFT/BOFT: no chunk variant exists; fused targets are skipped with a
|
|
# warning. Discrimination is by ``oft_blocks.ndim`` (3-D OFT, 4-D BOFT),
|
|
# mirroring upstream LyCORIS ``algo_check``.
|
|
|
|
|
|
def slice_chunk_rows(t, chunk: ChunkSpec):
|
|
"""Slice dim 0 of ``t`` per ``chunk``, then lay the selected rows out in the chunk's order.
|
|
|
|
Equal-chunks form uses ``torch.chunk`` (faster for the symmetric case);
|
|
row-range form uses tensor slicing for arbitrary partitions.
|
|
"""
|
|
if chunk.is_equal_chunks:
|
|
t = torch.chunk(t, chunk.total, dim=0)[chunk.idx]
|
|
elif chunk.start is not None:
|
|
t = t[chunk.start:chunk.end]
|
|
if chunk.reorder is not None:
|
|
blocks = torch.chunk(t, len(chunk.reorder), dim=0)
|
|
t = torch.cat([blocks[i] for i in chunk.reorder], dim=0)
|
|
return t.contiguous()
|
|
|
|
|
|
def _slice_lora_chunk(w, chunk: ChunkSpec):
|
|
"""Return a shallow copy of ``w`` with ``lora_up.weight`` sliced per ``chunk``; a dense bias follows a pure reorder."""
|
|
out = dict(w)
|
|
out["lora_up.weight"] = slice_chunk_rows(w["lora_up.weight"], chunk)
|
|
if "bias" in w and not chunk.is_slice:
|
|
out["bias"] = slice_chunk_rows(w["bias"], chunk)
|
|
return out
|
|
|
|
|
|
def slice_dora_scale(w, chunk: ChunkSpec, fused_out):
|
|
"""Slice a per-output ``dora_scale`` to the chunk rows; ``None`` if unsliceable.
|
|
|
|
LyCORIS ``wd_on_out=True`` (the LoKr/LoHA default) stores per-output
|
|
magnitudes of shape ``(out, 1)`` (1-D ``(out,)`` also seen); the rows
|
|
partition exactly with the fused weight, so the chunk slice preserves the
|
|
DoRA math (row norms are independent across rows). ``wd_on_out=False``
|
|
stores per-input magnitudes whose column norms span every fused row,
|
|
coupling the chunks; no exact per-chunk equivalent exists and the caller
|
|
must skip the group.
|
|
"""
|
|
ds = w.get("dora_scale")
|
|
if ds is None:
|
|
return w
|
|
if ds.ndim >= 1 and ds.shape[0] == fused_out:
|
|
out = dict(w)
|
|
out["dora_scale"] = slice_chunk_rows(ds, chunk)
|
|
return out
|
|
return None
|
|
|
|
|
|
def slice_bias_delta(w, chunk: ChunkSpec, fused_out):
|
|
"""Slice a per-output ``diff_b`` bias delta to the chunk rows; ``None`` if unsliceable.
|
|
|
|
``diff_b`` stores one bias value per output feature, so it partitions with
|
|
the fused rows exactly like the up-weight.
|
|
"""
|
|
db = w.get("diff_b")
|
|
if db is None:
|
|
return w
|
|
if db.ndim >= 1 and db.shape[0] == fused_out:
|
|
out = dict(w)
|
|
out["diff_b"] = slice_chunk_rows(db, chunk)
|
|
return out
|
|
return None
|
|
|
|
|
|
def try_load_lora(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
network_alpha=None,
|
|
arch_name="generic"):
|
|
"""Generic LoRA loader (handles DoRA via the universal ``finalize_updown`` hook).
|
|
|
|
Fused targets are chunked at load time by slicing ``lora_up`` along dim 0;
|
|
the down-side is shared across the resolved targets.
|
|
|
|
``network_alpha`` is a file-level alpha for files without alpha tensors;
|
|
a file carrying any alpha of its own keeps those and ignores it.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, LORA_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, LORA_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
if network_alpha is not None and any("alpha" in w for w in groups.values()):
|
|
network_alpha = None
|
|
|
|
unmapped = 0
|
|
mismatch = 0
|
|
skipped = 0
|
|
for (prefix, base), w in groups.items():
|
|
if "lora_down.weight" not in w or "lora_up.weight" not in w:
|
|
continue
|
|
if network_alpha is not None:
|
|
w = dict(w)
|
|
w["alpha"] = torch.tensor(float(network_alpha))
|
|
# DoRA magnitude vectors: ai-toolkit saves `magnitude`, PEFT/diffusers
|
|
# `lora_magnitude_vector`. Both are 1-D per-output row norms with
|
|
# dora_scale semantics; reshape to (out, 1) so the apply-time
|
|
# orientation detection cannot misread square layers as per-input.
|
|
for mag_key in ("magnitude", "lora_magnitude_vector"):
|
|
mag = w.get(mag_key)
|
|
if mag is not None and "dora_scale" not in w:
|
|
w = dict(w)
|
|
w.pop(mag_key)
|
|
w["dora_scale"] = mag.reshape(-1, 1) if mag.ndim == 1 else mag
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, chunk in resolve_group_targets(resolve_targets, prefix, base):
|
|
network_key = arch_prefix + diffusers_path.replace(".", "_")
|
|
sd_module = mapping.get(network_key)
|
|
if sd_module is None:
|
|
unmapped += 1
|
|
continue
|
|
|
|
target_w = w
|
|
if chunk is not None:
|
|
if "bias_indices" in w or ("bias" in w and chunk.is_slice):
|
|
# Weight-shaped bias residuals (dense or LyCORIS sparse
|
|
# triplet) are not partitioned onto fused targets.
|
|
log.warning(f'Network load: type=LoRA name="{name}" arch={arch_name} key={network_key} weight-shaped bias on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
fused_out = w["lora_up.weight"].shape[0]
|
|
target_w = _slice_lora_chunk(w, chunk)
|
|
target_w = slice_dora_scale(target_w, chunk, fused_out)
|
|
if target_w is None:
|
|
log.warning(f'Network load: type=LoRA name="{name}" arch={arch_name} key={network_key} per-input DoRA on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
target_w = slice_bias_delta(target_w, chunk, fused_out)
|
|
if target_w is None:
|
|
log.warning(f'Network load: type=LoRA name="{name}" arch={arch_name} key={network_key} non-per-output diff_b on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
|
|
if not shapes_match(sd_module, target_w["lora_down.weight"], target_w["lora_up.weight"]):
|
|
log.warning(
|
|
f'Network load: type=LoRA name="{name}" arch={arch_name} key={network_key}'
|
|
f' lora={target_w["lora_down.weight"].shape[1]}x{target_w["lora_up.weight"].shape[0]}'
|
|
f' module={getattr(sd_module, "weight", None).shape if hasattr(sd_module, "weight") else "?"}'
|
|
f' shape mismatch'
|
|
)
|
|
mismatch += 1
|
|
continue
|
|
|
|
if "diff_b" in target_w and not bias_delta_fits(sd_module, target_w["diff_b"]):
|
|
log.warning(
|
|
f'Network load: type=LoRA name="{name}" arch={arch_name} key={network_key}'
|
|
f' bias={tuple(target_w["diff_b"].shape)} module={tuple(sd_module.bias.shape)}'
|
|
f' bias shape mismatch'
|
|
)
|
|
mismatch += 1
|
|
continue
|
|
|
|
nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=target_w, sd_module=sd_module)
|
|
net.modules[network_key] = network_lora.NetworkModuleLora(net, nw)
|
|
|
|
return finalize_network(net, name, "LoRA", lora_scale, t0, unmapped=unmapped, mismatch=mismatch, skipped=skipped)
|
|
|
|
|
|
def try_load_lokr(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic LoKR loader.
|
|
|
|
Stores only the compact LoKR factors and dispatches to
|
|
:class:`network_lokr.NetworkModuleLokrChunk` (equal chunks) or
|
|
:class:`network_lokr.NetworkModuleLokrSliceChunk` (row range) at apply
|
|
time. Both materialize ``kron(w1, w2)`` once per forward pass and return
|
|
the designated slice; full materialization happens lazily inside the
|
|
module rather than at load.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, LOKR_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, LOKR_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
mismatch = 0
|
|
skipped = 0
|
|
for (prefix, base), w in groups.items():
|
|
has_1 = "lokr_w1" in w or ("lokr_w1_a" in w and "lokr_w1_b" in w)
|
|
has_2 = "lokr_w2" in w or ("lokr_w2_a" in w and "lokr_w2_b" in w)
|
|
if not (has_1 and has_2):
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
kron_shape = lokr_kron_shape(w)
|
|
for diffusers_path, chunk in resolve_group_targets(resolve_targets, prefix, base):
|
|
network_key = arch_prefix + diffusers_path.replace(".", "_")
|
|
sd_module = mapping.get(network_key)
|
|
if sd_module is None:
|
|
unmapped += 1
|
|
continue
|
|
if not lokr_shapes_match(sd_module, kron_shape, chunk):
|
|
log.warning(
|
|
f'Network load: type=LoKR name="{name}" arch={arch_name} key={network_key}'
|
|
f' kron={kron_shape[0]}x{kron_shape[1]}'
|
|
f' module={getattr(sd_module, "weight", None).shape if hasattr(sd_module, "weight") else "?"}'
|
|
f' shape mismatch'
|
|
)
|
|
mismatch += 1
|
|
continue
|
|
target_w = w
|
|
if chunk is not None:
|
|
if chunk.reorder is not None:
|
|
log.warning(f'Network load: type=LoKR name="{name}" arch={arch_name} key={network_key} row reorder on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
if "bias" in w:
|
|
log.warning(f'Network load: type=LoKR name="{name}" arch={arch_name} key={network_key} weight-shaped bias on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
# Kron rows = w1 rows * w2 rows; the tucker w2_a orientation
|
|
# differs but tucker is conv-only and chunks are Linear-only.
|
|
w1 = w.get("lokr_w1")
|
|
w2 = w.get("lokr_w2")
|
|
w1_rows = w1.shape[0] if w1 is not None else w["lokr_w1_a"].shape[0]
|
|
w2_rows = w2.shape[0] if w2 is not None else w["lokr_w2_a"].shape[0]
|
|
target_w = slice_dora_scale(w, chunk, fused_out=w1_rows * w2_rows)
|
|
if target_w is None:
|
|
log.warning(f'Network load: type=LoKR name="{name}" arch={arch_name} key={network_key} per-input DoRA on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=target_w, sd_module=sd_module)
|
|
if chunk is None:
|
|
net.modules[network_key] = network_lokr.NetworkModuleLokr(net, nw)
|
|
elif chunk.is_equal_chunks:
|
|
net.modules[network_key] = network_lokr.NetworkModuleLokrChunk(net, nw, chunk.idx, chunk.total)
|
|
else:
|
|
net.modules[network_key] = network_lokr.NetworkModuleLokrSliceChunk(net, nw, chunk.start, chunk.end)
|
|
|
|
return finalize_network(net, name, "LoKR", lora_scale, t0, unmapped=unmapped, mismatch=mismatch, skipped=skipped)
|
|
|
|
|
|
def try_load_loha(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic LoHA (Hadamard product) loader.
|
|
|
|
Standard non-Tucker LoHA on fused targets uses
|
|
:class:`network_hada.NetworkModuleHadaChunk` for equal-chunks dispatch.
|
|
Tucker-decomposed LoHAs on fused targets and any unequal-chunks dispatch
|
|
are skipped with a warning: no slice variant exists, and Tucker keys
|
|
cannot arise on Linear layers per LyCORIS upstream.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, LOHA_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, LOHA_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
skipped = 0
|
|
for (prefix, base), w in groups.items():
|
|
if not all(k in w for k in ("hada_w1_a", "hada_w1_b", "hada_w2_a", "hada_w2_b")):
|
|
continue
|
|
is_tucker = "hada_t1" in w or "hada_t2" in w
|
|
targets = resolve_group_targets(resolve_targets, prefix, base)
|
|
is_fused = any(t[1] is not None for t in targets)
|
|
if is_fused and is_tucker:
|
|
log.warning(f'Network load: type=LoHA name="{name}" arch={arch_name} key={base} Tucker fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, chunk in targets:
|
|
network_key = arch_prefix + diffusers_path.replace(".", "_")
|
|
sd_module = mapping.get(network_key)
|
|
if sd_module is None:
|
|
unmapped += 1
|
|
continue
|
|
target_w = w
|
|
if chunk is not None:
|
|
if chunk.reorder is not None:
|
|
log.warning(f'Network load: type=LoHA name="{name}" arch={arch_name} key={network_key} row reorder on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
if "bias" in w:
|
|
log.warning(f'Network load: type=LoHA name="{name}" arch={arch_name} key={network_key} weight-shaped bias on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
target_w = slice_dora_scale(w, chunk, fused_out=w["hada_w1_a"].shape[0])
|
|
if target_w is None:
|
|
log.warning(f'Network load: type=LoHA name="{name}" arch={arch_name} key={network_key} per-input DoRA on fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=target_w, sd_module=sd_module)
|
|
if chunk is None:
|
|
net.modules[network_key] = network_hada.NetworkModuleHada(net, nw)
|
|
elif chunk.is_equal_chunks:
|
|
net.modules[network_key] = network_hada.NetworkModuleHadaChunk(net, nw, chunk.idx, chunk.total)
|
|
else:
|
|
log.warning(f'Network load: type=LoHA name="{name}" arch={arch_name} key={network_key} unequal fused chunks unsupported')
|
|
skipped += 1
|
|
|
|
return finalize_network(net, name, "LoHA", lora_scale, t0, unmapped=unmapped, skipped=skipped)
|
|
|
|
|
|
def try_load_oft(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic OFT/BOFT loader.
|
|
|
|
OFT and BOFT share the ``oft_blocks`` save key and are discriminated by
|
|
tensor dimensionality (3-D OFT, 4-D BOFT), mirroring upstream
|
|
``algo_check``. Both kohya (``oft_blocks`` + alpha-as-constraint) and
|
|
LyCORIS (``oft_diag``) OFT layouts route through
|
|
:class:`network_oft.NetworkModuleOFT`; BOFT routes through
|
|
:class:`network_boft.NetworkModuleBOFT`.
|
|
|
|
Fused targets are skipped with a warning for both algorithms: OFT block
|
|
structure (and BOFT's per-stage block partition) is tied to the target
|
|
module's ``out_features``, so a per-Q/K/V split would require re-deriving
|
|
the rotations per chunk.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, OFT_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, OFT_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
skipped = 0
|
|
for (prefix, base), w in groups.items():
|
|
if not ("oft_blocks" in w or "oft_diag" in w):
|
|
continue
|
|
is_boft = "oft_blocks" in w and w["oft_blocks"].ndim == 4
|
|
targets = resolve_group_targets(resolve_targets, prefix, base)
|
|
if any(t[1] is not None for t in targets):
|
|
log.warning(f'Network load: type={"BOFT" if is_boft else "OFT"} name="{name}" arch={arch_name} key={base} fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, _ in targets:
|
|
network_key = arch_prefix + 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)
|
|
if is_boft:
|
|
net.modules[network_key] = network_boft.NetworkModuleBOFT(net, nw)
|
|
else:
|
|
net.modules[network_key] = network_oft.NetworkModuleOFT(net, nw)
|
|
|
|
return finalize_network(net, name, "OFT", lora_scale, t0, unmapped=unmapped, skipped=skipped)
|
|
|
|
|
|
def try_load_ia3(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic IA3 loader.
|
|
|
|
IA3 stores a per-row or per-column scale vector keyed under ``.weight``
|
|
plus an ``.on_input`` flag selecting which axis. ``.weight`` alone is too
|
|
generic for the marker scan (it overlaps every other family's
|
|
``.lora_down.weight`` / ``.hada_w*`` keys), so the marker gate insists on
|
|
``.on_input`` while the suffix table includes both.
|
|
|
|
Fused targets are skipped with a warning. There is no real-world IA3-on-DiT
|
|
prevalence to justify the asymmetry between ``on_input=True`` (which would
|
|
replicate cleanly across Q/K/V) and ``on_input=False`` (which would need
|
|
output-axis slicing).
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, IA3_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, IA3_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
skipped = 0
|
|
for (prefix, base), w in groups.items():
|
|
if not ("weight" in w and "on_input" in w):
|
|
continue
|
|
targets = resolve_group_targets(resolve_targets, prefix, base)
|
|
if any(t[1] is not None for t in targets):
|
|
log.warning(f'Network load: type=IA3 name="{name}" arch={arch_name} key={base} fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, _ in targets:
|
|
network_key = arch_prefix + 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_ia3.NetworkModuleIa3(net, nw)
|
|
|
|
return finalize_network(net, name, "IA3", lora_scale, t0, unmapped=unmapped, skipped=skipped)
|
|
|
|
|
|
def try_load_glora(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic GLoRA loader.
|
|
|
|
GLoRA stores four low-rank components (``a1`` / ``a2`` / ``b1`` / ``b2``)
|
|
and computes ``W_delta = w2b @ w1b + (target @ w2a) @ w1a``; the second
|
|
term is target-dependent. Fused targets are skipped: the target-dependent
|
|
term doesn't slice cleanly across projections without redirecting
|
|
calc_updown to a fused proxy weight, and the file pattern is vanishingly
|
|
rare on DiT architectures.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = 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_fn(
|
|
state_dict, GLORA_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
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_group_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}" arch={arch_name} key={base} fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, _ in targets:
|
|
network_key = arch_prefix + 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)
|
|
|
|
|
|
def try_load_norm(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"): # pylint: disable=unused-argument
|
|
"""Generic Norm (LayerNorm / RMSNorm weight + bias delta) loader.
|
|
|
|
Norm targets are never fused, so the chunk dispatch is dropped.
|
|
|
|
Loader-local stamping: ``lora_convert.assign_network_names_to_compvis_modules``
|
|
deliberately skips setting ``module.network_layer_name`` for transformer
|
|
norm modules (except SD3) because of legacy CompVis UNet collisions. This
|
|
loader bypasses the guard locally: for each target it actually binds, it
|
|
sets ``network_layer_name`` directly on the host module so
|
|
``network_activate`` will apply the delta. Stamping is idempotent and only
|
|
touches modules a Norm adapter explicitly targets.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, NORM_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, NORM_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
mismatch = 0
|
|
for (prefix, base), w in groups.items():
|
|
if "w_norm" not in w:
|
|
continue
|
|
targets = resolve_group_targets(resolve_targets, prefix, base)
|
|
if not targets:
|
|
unmapped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, chunk in targets:
|
|
if chunk is not None:
|
|
continue # norm targets are not fused
|
|
network_key = arch_prefix + diffusers_path.replace(".", "_")
|
|
sd_module = mapping.get(network_key)
|
|
if sd_module is None:
|
|
unmapped += 1
|
|
continue
|
|
if "b_norm" in w and not bias_delta_fits(sd_module, w["b_norm"]):
|
|
log.warning(
|
|
f'Network load: type=Norm name="{name}" arch={arch_name} key={network_key}'
|
|
f' bias={tuple(w["b_norm"].shape)} module={tuple(sd_module.bias.shape)}'
|
|
f' bias shape mismatch'
|
|
)
|
|
mismatch += 1
|
|
continue
|
|
if not getattr(sd_module, "network_layer_name", None):
|
|
sd_module.network_layer_name = network_key
|
|
nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=w, sd_module=sd_module)
|
|
net.modules[network_key] = network_norm.NetworkModuleNorm(net, nw)
|
|
|
|
return finalize_network(net, name, "Norm", lora_scale, t0, unmapped=unmapped, mismatch=mismatch)
|
|
|
|
|
|
def try_load_full(name, network_on_disk, lora_scale, *,
|
|
resolve_targets, prefixes=KNOWN_PREFIXES_DEFAULT,
|
|
network_prefix=NETWORK_PREFIX_DEFAULT,
|
|
group_by_suffixes_fn=group_by_suffixes,
|
|
arch_name="generic"):
|
|
"""Generic Full (full-rank weight delta) loader.
|
|
|
|
Full adapters carry a complete weight delta (``diff``, same shape as the
|
|
host weight) and an optional bias delta (``diff_b``). Fused targets are
|
|
skipped with a warning: ``diff`` has the host weight's full shape and
|
|
row-slicing across three projections is well-defined arithmetically, but
|
|
no chunk class exists.
|
|
"""
|
|
t0 = time.time()
|
|
state_dict = read_state_dict(network_on_disk.filename, what="network")
|
|
if not has_marker(state_dict, FULL_MARKERS):
|
|
return None
|
|
|
|
mapping = resolve_mapping()
|
|
net = new_network(name, network_on_disk)
|
|
groups = group_by_suffixes_fn(
|
|
state_dict, FULL_SUFFIXES,
|
|
prefixes=prefixes,
|
|
)
|
|
|
|
unmapped = 0
|
|
skipped = 0
|
|
mismatch = 0
|
|
for (prefix, base), w in groups.items():
|
|
if "diff" not in w:
|
|
continue
|
|
targets = resolve_group_targets(resolve_targets, prefix, base)
|
|
if any(t[1] is not None for t in targets):
|
|
log.warning(f'Network load: type=Full name="{name}" arch={arch_name} key={base} fused target skipped (unsupported)')
|
|
skipped += 1
|
|
continue
|
|
arch_prefix = _resolve_prefix(network_prefix, prefix)
|
|
for diffusers_path, _ in targets:
|
|
network_key = arch_prefix + diffusers_path.replace(".", "_")
|
|
sd_module = mapping.get(network_key)
|
|
if sd_module is None:
|
|
unmapped += 1
|
|
continue
|
|
if "diff_b" in w and not bias_delta_fits(sd_module, w["diff_b"]):
|
|
log.warning(
|
|
f'Network load: type=Full name="{name}" arch={arch_name} key={network_key}'
|
|
f' bias={tuple(w["diff_b"].shape)} module={tuple(sd_module.bias.shape)}'
|
|
f' bias shape mismatch'
|
|
)
|
|
mismatch += 1
|
|
continue
|
|
# Loader-local stamping, same as try_load_norm: a full-weight extraction carries the
|
|
# norm weights too, and assign_network_names_to_compvis_modules puts norms in the
|
|
# mapping but never stamps network_layer_name on them, which is what the apply pass
|
|
# keys off. Without this they bind and then silently never apply.
|
|
if not getattr(sd_module, "network_layer_name", None):
|
|
sd_module.network_layer_name = network_key
|
|
nw = network.NetworkWeights(network_key=network_key, sd_key=network_key, w=w, sd_module=sd_module)
|
|
net.modules[network_key] = network_full.NetworkModuleFull(net, nw)
|
|
|
|
return finalize_network(net, name, "Full", lora_scale, t0, unmapped=unmapped, mismatch=mismatch, skipped=skipped)
|
|
|
|
|
|
# === Per-arch umbrella ===
|
|
|
|
|
|
def try_load_chain(name, network_on_disk, lora_scale, family_loaders):
|
|
"""Run each family loader in order and merge any non-None results.
|
|
|
|
Per-arch loader modules expose a ``try_load(name, nod, scale)`` entry point
|
|
that the dispatcher in ``modules.lora.lora_load`` calls. That entry point
|
|
is a thin wrapper around this helper: it passes ``family_loaders`` as a
|
|
tuple of partial-applied generic loaders, each already bound to the arch's
|
|
``resolve_targets`` and prefix tuples.
|
|
"""
|
|
from modules import sd_models_utils
|
|
sd_models_utils.state_dict_cache.enable()
|
|
net = None
|
|
mismatch = 0
|
|
for try_fn in family_loaders:
|
|
sub = try_fn(name, network_on_disk, lora_scale)
|
|
if sub is None:
|
|
continue
|
|
mismatch += getattr(sub, 'mismatch', 0)
|
|
if net is None:
|
|
net = sub
|
|
else:
|
|
net.modules.update(sub.modules)
|
|
sd_models_utils.state_dict_cache.disable()
|
|
if net is not None and mismatch > 0: # applying only the layers that fit leaves the model in a state nothing was trained for
|
|
log.error(f'Network load: type=LoRA name="{name}" modules={len(net.modules)} mismatch={mismatch} shapes do not match the loaded model')
|
|
return None
|
|
return net
|