mirror of
https://github.com/vladmandic/automatic
synced 2026-08-31 17:41:06 +02:00
25b7961e4e
A delta that does not fit its target module cannot apply, and applying only the layers that do fit leaves the model in a state nothing was trained for, so try_load_chain drops the whole file when any family reports a mismatch. Bias deltas were never checked against the target bias and could only surface at apply time; a module with no bias stays a non-mismatch, since whole architectures are built bias=False. - check bias deltas against the module bias in the lora, norm and full loaders - carry the mismatch count on the network so the chain can refuse the file - record refused writes in the infotext so a partial apply is not read as clean - point the krea2 full-diff test at a module that has a bias
739 lines
29 KiB
Python
739 lines
29 KiB
Python
#!/usr/bin/env python
|
|
"""
|
|
Offline unit tests for Krea 2 native adapter loaders.
|
|
|
|
Krea 2 is the one native arch whose transformer keeps checkpoint-style module
|
|
names (``blocks.N.attn.wq``, ``txtfusion.*``, ``first``, ``last`` ...) that
|
|
differ from upstream ``diffusers.Krea2Transformer2DModel``
|
|
(``transformer_blocks.N.attn.to_q``, ``text_fusion.*``, ``img_in`` ...). So
|
|
``pipelines.krea2.krea2_lora`` carries a diffusers -> checkpoint rename that the
|
|
other native arches (which load the diffusers class) do not need. These tests
|
|
pin that rename plus the resolver-first verbatim fallback in
|
|
``modules.lora.native_adapter.resolve_group_targets``.
|
|
|
|
Save formats exercised, cross-referenced against real Krea 2 LoRAs:
|
|
|
|
- diffusers-PEFT (``transformer.transformer_blocks.0.attn.to_q.lora_A.weight``):
|
|
the official ``krea/Krea-2-LoRA-*`` releases (renamed to checkpoint names).
|
|
- bare diffusers (``transformer_blocks.0.attn.to_q.lora_A.weight``): what
|
|
``Krea2Transformer2DModel.save_lora_adapter()`` emits (renamed).
|
|
- comfy checkpoint (``diffusion_model.blocks.14.attn.wq.lora_A.weight``): the
|
|
CivitAI ecosystem's checkpoint-named LoRAs (bound verbatim).
|
|
- kohya (``lora_unet_blocks_0_attn_wq.lora_down.weight``): checkpoint-named,
|
|
reconstructed to dotted paths.
|
|
- OneTrainer (``lora_transformer_blocks_0_mlp_up.*``): sdnext's own
|
|
network-key namespace, passthrough-bound.
|
|
|
|
The reference module tree is the real ``Krea2Transformer2DModel`` at tiny dims,
|
|
so module names are authoritative rather than hand-mirrored. Target shapes are
|
|
read from that instance, so the loader's shape-match gate is exercised too.
|
|
|
|
No running server required.
|
|
|
|
Usage:
|
|
python test/test-krea2-native-adapters.py
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
import time
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
import safetensors.torch
|
|
|
|
script_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
sys.path.insert(0, script_dir)
|
|
os.chdir(script_dir)
|
|
|
|
os.environ['SD_INSTALL_QUIET'] = '1'
|
|
|
|
# Bootstrap cmd_args before any module that pulls in shared.py.
|
|
import modules.cmd_args # pylint: disable=wrong-import-position
|
|
import installer # pylint: disable=wrong-import-position
|
|
_orig_argv = sys.argv
|
|
sys.argv = [sys.argv[0]]
|
|
try:
|
|
modules.cmd_args.parse_args()
|
|
finally:
|
|
sys.argv = _orig_argv
|
|
installer.add_args(modules.cmd_args.parser)
|
|
modules.cmd_args.parsed, _ = modules.cmd_args.parser.parse_known_args([])
|
|
|
|
from modules.errors import log # pylint: disable=wrong-import-position
|
|
from modules import shared # pylint: disable=wrong-import-position
|
|
from modules.lora import ( # pylint: disable=wrong-import-position
|
|
network, network_lora, network_lokr, network_hada, network_oft,
|
|
)
|
|
from modules.lora import native_adapter # pylint: disable=wrong-import-position
|
|
from pipelines.krea2 import krea2_lora as K # pylint: disable=wrong-import-position
|
|
from pipelines.krea2.transformer_krea2 import Krea2Transformer2DModel # pylint: disable=wrong-import-position
|
|
|
|
|
|
# ============================================================
|
|
# Test infrastructure
|
|
# ============================================================
|
|
|
|
results: dict[str, dict] = {}
|
|
|
|
|
|
def category(name: str):
|
|
if name not in results:
|
|
results[name] = {'passed': 0, 'failed': 0, 'tests': []}
|
|
return name
|
|
|
|
|
|
def record(cat: str, passed: bool, name: str, detail: str = ''):
|
|
status = 'PASS' if passed else 'FAIL'
|
|
results[cat]['passed' if passed else 'failed'] += 1
|
|
results[cat]['tests'].append((status, name))
|
|
msg = f' {status}: {name}'
|
|
if detail:
|
|
msg += f' ({detail})'
|
|
if passed:
|
|
log.info(msg)
|
|
else:
|
|
log.error(msg)
|
|
|
|
|
|
def run_test(cat: str, fn):
|
|
name = fn.__name__
|
|
try:
|
|
ok = fn()
|
|
if ok is False:
|
|
record(cat, False, name)
|
|
else:
|
|
record(cat, True, name)
|
|
except AssertionError as e:
|
|
record(cat, False, name, str(e))
|
|
except Exception as e: # pylint: disable=broad-except
|
|
record(cat, False, name, f'exception: {e}')
|
|
import traceback
|
|
traceback.print_exc()
|
|
|
|
|
|
# ============================================================
|
|
# Reference Krea 2 transformer (real class, tiny dims)
|
|
# ============================================================
|
|
# Upstream: features=6144, heads=48, kvheads=12, multiplier=4, layers=28,
|
|
# txtdim=2560, txtlayers=12. Test scale keeps the proportions that matter for
|
|
# module naming and shape-arithmetic while staying instant to build.
|
|
|
|
FEATURES = 64
|
|
HEADS = 4
|
|
KVHEADS = 2
|
|
LAYERS = 2
|
|
|
|
REF = Krea2Transformer2DModel(
|
|
features=FEATURES, tdim=16, txtdim=32, heads=HEADS, kvheads=KVHEADS,
|
|
multiplier=4, layers=LAYERS, patch=2, channels=16,
|
|
txtlayers=12, txtheads=2, txtkvheads=2,
|
|
)
|
|
|
|
# {checkpoint dotted name: (out, in)} for every Linear, so synthesizers can size
|
|
# adapter tensors to the real module and the loader's shape gate is real.
|
|
LINEAR_SHAPES = {name: tuple(m.weight.shape) for name, m in REF.named_modules() if isinstance(m, nn.Linear)}
|
|
# {network key: module} exactly as assign_network_names_to_compvis_modules stamps it.
|
|
LINEAR_NETKEYS = {'lora_transformer_' + name.replace('.', '_') for name in LINEAR_SHAPES}
|
|
|
|
|
|
def ckpt_shape(path: str):
|
|
assert path in LINEAR_SHAPES, f'reference has no Linear {path!r}'
|
|
return LINEAR_SHAPES[path]
|
|
|
|
|
|
# ============================================================
|
|
# Mock pipeline wrapping the real transformer
|
|
# ============================================================
|
|
|
|
|
|
class _MockKrea2Pipeline:
|
|
def __init__(self, transformer):
|
|
self.transformer = transformer
|
|
self.text_encoder = None
|
|
|
|
|
|
class _MockKrea2SdModel:
|
|
def __init__(self, pipe):
|
|
self.pipe = pipe
|
|
self.network_layer_mapping = {}
|
|
self.embedding_db = None
|
|
self.__class__.__name__ = 'Krea2Pipeline'
|
|
|
|
|
|
def install_mock_pipe():
|
|
"""Point shared.sd_model at a mock exposing the reference Krea 2 transformer.
|
|
|
|
Re-installed per load so prior network_layer_name stamps do not leak.
|
|
"""
|
|
pipe = _MockKrea2Pipeline(REF)
|
|
sd_model = _MockKrea2SdModel(pipe)
|
|
from modules.modeldata import model_data
|
|
model_data.sd_model = sd_model
|
|
return sd_model
|
|
|
|
|
|
# ============================================================
|
|
# Adapter-tensor synthesizers (sized to the reference modules)
|
|
# ============================================================
|
|
|
|
RANK = 8
|
|
LOKR_DIM = 8
|
|
|
|
|
|
def lora_pair(key_base, ckpt_path, suffix=('lora_A.weight', 'lora_B.weight'), alpha=False):
|
|
out, inp = ckpt_shape(ckpt_path)
|
|
sd = {
|
|
f'{key_base}.{suffix[0]}': torch.randn(RANK, inp),
|
|
f'{key_base}.{suffix[1]}': torch.randn(out, RANK),
|
|
}
|
|
if alpha:
|
|
sd[f'{key_base}.alpha'] = torch.tensor(float(RANK))
|
|
return sd
|
|
|
|
|
|
def lokr_pair(key_base, ckpt_path):
|
|
out, inp = ckpt_shape(ckpt_path)
|
|
assert out % LOKR_DIM == 0 and inp % LOKR_DIM == 0
|
|
return {
|
|
f'{key_base}.lokr_w1': torch.randn(LOKR_DIM, LOKR_DIM),
|
|
f'{key_base}.lokr_w2': torch.randn(out // LOKR_DIM, inp // LOKR_DIM),
|
|
f'{key_base}.alpha': torch.tensor(float(LOKR_DIM)),
|
|
}
|
|
|
|
|
|
def loha_pair(key_base, ckpt_path):
|
|
out, inp = ckpt_shape(ckpt_path)
|
|
return {
|
|
f'{key_base}.hada_w1_a': torch.randn(out, RANK),
|
|
f'{key_base}.hada_w1_b': torch.randn(RANK, inp),
|
|
f'{key_base}.hada_w2_a': torch.randn(out, RANK),
|
|
f'{key_base}.hada_w2_b': torch.randn(RANK, inp),
|
|
f'{key_base}.alpha': torch.tensor(float(RANK)),
|
|
}
|
|
|
|
|
|
def oft_pair(key_base, ckpt_path, num_blocks=4):
|
|
out, _inp = ckpt_shape(ckpt_path)
|
|
assert out % num_blocks == 0
|
|
bs = out // num_blocks
|
|
return {
|
|
f'{key_base}.oft_blocks': torch.randn(num_blocks, bs, bs) * 0.01,
|
|
f'{key_base}.oft_diag': torch.ones(num_blocks, bs),
|
|
f'{key_base}.alpha': torch.tensor(0.001),
|
|
}
|
|
|
|
|
|
# ============================================================
|
|
# Helpers
|
|
# ============================================================
|
|
|
|
|
|
class TempLora:
|
|
"""Context manager: writes a state dict to a temp safetensors file."""
|
|
|
|
def __init__(self, state_dict, name='test'):
|
|
self.state_dict = state_dict
|
|
self.name = name
|
|
self.path = None
|
|
|
|
def __enter__(self):
|
|
sd = {k: v.contiguous() if isinstance(v, torch.Tensor) else v for k, v in self.state_dict.items()}
|
|
fd, self.path = tempfile.mkstemp(suffix='.safetensors', prefix=f'{self.name}_')
|
|
os.close(fd)
|
|
safetensors.torch.save_file(sd, self.path)
|
|
return _MockNetworkOnDisk(self.path, self.name)
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
if self.path and os.path.exists(self.path):
|
|
os.unlink(self.path)
|
|
|
|
|
|
class _MockNetworkOnDisk:
|
|
def __init__(self, filename, name):
|
|
self.filename = filename
|
|
self.name = name
|
|
self.shorthash = ''
|
|
self.sd_version = 'unknown'
|
|
|
|
|
|
def assert_shape(t: torch.Tensor, expected_shape, label=''):
|
|
actual = tuple(t.shape)
|
|
assert actual == tuple(expected_shape), f'{label}: shape {actual}, expected {tuple(expected_shape)}'
|
|
|
|
|
|
def make_network_for_module(net_module: network.NetworkModule, te_mul: float = 1.0, unet_mul: float = 1.0):
|
|
net_module.network.te_multiplier = te_mul
|
|
net_module.network.unet_multiplier = unet_mul
|
|
return net_module
|
|
|
|
|
|
def _load_via(try_fn, state_dict, name='test'):
|
|
install_mock_pipe()
|
|
with TempLora(state_dict, name=name) as nod:
|
|
return try_fn(name, nod, lora_scale=1.0)
|
|
|
|
|
|
def resolve_netkeys(state_dict):
|
|
"""Return the set of network keys a state dict resolves to (rename applied)."""
|
|
groups = K.group_by_suffixes(state_dict, K.LORA_SUFFIXES)
|
|
keys = set()
|
|
for (prefix, base), _w in groups.items():
|
|
for path, _chunk in native_adapter.resolve_group_targets(K.resolve_targets, prefix, base):
|
|
keys.add('lora_transformer_' + path.replace('.', '_'))
|
|
return keys
|
|
|
|
|
|
# ============================================================
|
|
# Tests - parsing primitives
|
|
# ============================================================
|
|
|
|
CAT_PARSE = category('parse')
|
|
|
|
|
|
def test_parse_key_all_prefixes():
|
|
bd = K.BARE_DIFFUSERS_PREFIX_USED
|
|
|
|
def parse(key):
|
|
return K.parse_key(key, K.LORA_SUFFIXES)
|
|
cases = [
|
|
# diffusers-PEFT: 'transformer.' stripped, base is the diffusers path
|
|
('transformer.transformer_blocks.0.attn.to_q.lora_A.weight',
|
|
('transformer.', 'transformer_blocks.0.attn.to_q', 'lora_down.weight')),
|
|
# comfy checkpoint
|
|
('diffusion_model.blocks.0.attn.wq.lora_B.weight',
|
|
('diffusion_model.', 'blocks.0.attn.wq', 'lora_up.weight')),
|
|
# kohya checkpoint
|
|
('lora_unet_blocks_0_attn_wq.lora_down.weight',
|
|
('lora_unet_', 'blocks_0_attn_wq', 'lora_down.weight')),
|
|
# bare diffusers (save_lora_adapter output)
|
|
('transformer_blocks.0.ff.gate.lora_A.weight',
|
|
(bd, 'transformer_blocks.0.ff.gate', 'lora_down.weight')),
|
|
('text_fusion.projector.lora_A.weight',
|
|
(bd, 'text_fusion.projector', 'lora_down.weight')),
|
|
('random.unrelated.key', None),
|
|
]
|
|
for key, expected in cases:
|
|
got = parse(key)
|
|
assert got == expected, f'parse_key({key!r}) = {got}, expected {expected}'
|
|
return True
|
|
|
|
|
|
def test_marker_disambiguation():
|
|
pure_lora = {
|
|
'transformer.transformer_blocks.0.attn.to_q.lora_A.weight': torch.zeros(1, 1),
|
|
'transformer.transformer_blocks.0.attn.to_q.lora_B.weight': torch.zeros(1, 1),
|
|
}
|
|
assert K.has_marker(pure_lora, K.LORA_MARKERS)
|
|
assert not K.has_marker(pure_lora, K.LOKR_MARKERS)
|
|
assert not K.has_marker(pure_lora, K.LOHA_MARKERS)
|
|
|
|
pure_lokr = {
|
|
'diffusion_model.blocks.0.attn.wq.lokr_w1': torch.zeros(1, 1),
|
|
'diffusion_model.blocks.0.attn.wq.lokr_w2': torch.zeros(1, 1),
|
|
}
|
|
assert K.has_marker(pure_lokr, K.LOKR_MARKERS)
|
|
assert not K.has_marker(pure_lokr, K.LORA_MARKERS)
|
|
return True
|
|
|
|
|
|
# ============================================================
|
|
# Tests - rename resolution (the arch-specific crux)
|
|
# ============================================================
|
|
|
|
CAT_RESOLVE = category('resolve')
|
|
|
|
|
|
def test_resolve_block_attn_leaf_renames():
|
|
"""Diffusers attn/ff leaves rewrite to checkpoint leaves under 'transformer.'."""
|
|
pairs = {
|
|
'transformer_blocks.0.attn.to_q': 'blocks.0.attn.wq',
|
|
'transformer_blocks.0.attn.to_k': 'blocks.0.attn.wk',
|
|
'transformer_blocks.0.attn.to_v': 'blocks.0.attn.wv',
|
|
'transformer_blocks.0.attn.to_gate': 'blocks.0.attn.gate',
|
|
'transformer_blocks.1.attn.to_out.0': 'blocks.1.attn.wo',
|
|
'transformer_blocks.0.ff.gate': 'blocks.0.mlp.gate',
|
|
'transformer_blocks.0.ff.up': 'blocks.0.mlp.up',
|
|
'transformer_blocks.1.ff.down': 'blocks.1.mlp.down',
|
|
}
|
|
for diff_base, ckpt in pairs.items():
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, 'transformer.', diff_base)
|
|
assert got == [(ckpt, None)], f'{diff_base} -> {got}, expected {ckpt}'
|
|
return True
|
|
|
|
|
|
def test_resolve_text_fusion_renames():
|
|
"""text_fusion.* rewrites to txtfusion.*; projector keeps its name."""
|
|
pairs = {
|
|
'text_fusion.layerwise_blocks.0.attn.to_q': 'txtfusion.layerwise_blocks.0.attn.wq',
|
|
'text_fusion.refiner_blocks.1.ff.down': 'txtfusion.refiner_blocks.1.mlp.down',
|
|
'text_fusion.projector': 'txtfusion.projector',
|
|
}
|
|
for diff_base, ckpt in pairs.items():
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, 'transformer.', diff_base)
|
|
assert got == [(ckpt, None)], f'{diff_base} -> {got}, expected {ckpt}'
|
|
return True
|
|
|
|
|
|
def test_resolve_non_block_extras():
|
|
"""Embedders / MLP-sequential / final layer map to their checkpoint paths."""
|
|
for diff_base, ckpt in K.DIFFUSERS_EXTRA_MAP.items():
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, 'transformer.', diff_base)
|
|
assert got == [(ckpt, None)], f'{diff_base} -> {got}, expected {ckpt}'
|
|
# every renamed extra is a real Linear on the reference transformer
|
|
for ckpt in K.DIFFUSERS_EXTRA_MAP.values():
|
|
assert ckpt in LINEAR_SHAPES, f'extra target {ckpt!r} is not a reference Linear'
|
|
return True
|
|
|
|
|
|
def test_resolve_checkpoint_names_verbatim():
|
|
"""Checkpoint-named bases fall through to the shared verbatim binding."""
|
|
# comfy dotted (diffusion_model.) and transformer.-prefixed checkpoint names
|
|
for prefix in ('diffusion_model.', 'transformer.'):
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, prefix, 'blocks.14.attn.wq')
|
|
assert got == [('blocks.14.attn.wq', None)], f'{prefix} verbatim -> {got}'
|
|
# OneTrainer / lycoris passthrough
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, 'lora_transformer_', 'blocks_0_mlp_up')
|
|
assert got == [('blocks_0_mlp_up', None)], f'lora_transformer_ passthrough -> {got}'
|
|
return True
|
|
|
|
|
|
def test_resolve_every_official_module_is_real():
|
|
"""A full official-style block-0 + extras set resolves onto real Linears only."""
|
|
diff_bases = [
|
|
'transformer_blocks.0.attn.to_q', 'transformer_blocks.0.attn.to_k',
|
|
'transformer_blocks.0.attn.to_v', 'transformer_blocks.0.attn.to_gate',
|
|
'transformer_blocks.0.attn.to_out.0', 'transformer_blocks.0.ff.gate',
|
|
'transformer_blocks.0.ff.up', 'transformer_blocks.0.ff.down',
|
|
'text_fusion.layerwise_blocks.0.attn.to_q', 'text_fusion.projector',
|
|
'img_in', 'txt_in.linear_1', 'txt_in.linear_2', 'time_embed.linear_1',
|
|
'time_embed.linear_2', 'time_mod_proj', 'final_layer.linear',
|
|
]
|
|
sd = {}
|
|
for b in diff_bases:
|
|
sd.update(lora_pair(f'transformer.{b}', _diff_to_ckpt(b)))
|
|
netkeys = resolve_netkeys(sd)
|
|
missing = netkeys - LINEAR_NETKEYS
|
|
assert not missing, f'unmapped: {missing}'
|
|
assert len(netkeys) == len(diff_bases), f'got {len(netkeys)} keys for {len(diff_bases)} modules'
|
|
return True
|
|
|
|
|
|
def _diff_to_ckpt(diff_base):
|
|
"""Test-side mirror of resolve_targets, to size synthetic tensors."""
|
|
got = native_adapter.resolve_group_targets(K.resolve_targets, 'transformer.', diff_base)
|
|
return got[0][0]
|
|
|
|
|
|
# ============================================================
|
|
# Tests - loaders end-to-end
|
|
# ============================================================
|
|
|
|
CAT_LOADER = category('loader')
|
|
|
|
|
|
def test_lora_official_diffusers_renamed():
|
|
"""Official diffusers-PEFT key renames and binds onto the checkpoint module."""
|
|
sd = lora_pair('transformer.transformer_blocks.0.attn.to_q', 'blocks.0.attn.wq', alpha=True)
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and len(net.modules) == 1, f'got {net.modules if net else None}'
|
|
assert 'lora_transformer_blocks_0_attn_wq' in net.modules, f'got {set(net.modules)}'
|
|
assert isinstance(next(iter(net.modules.values())), network_lora.NetworkModuleLora)
|
|
return True
|
|
|
|
|
|
def test_lora_bias_delta_binds():
|
|
"""A diff_b sized to the module bias rides along with the weight LoRA."""
|
|
sd = lora_pair('diffusion_model.first', 'first')
|
|
sd['diffusion_model.first.diff_b'] = torch.randn(ckpt_shape('first')[0])
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_first' in net.modules, f'got {set(net.modules) if net else None}'
|
|
assert net.mismatch == 0, f'mismatch={net.mismatch}'
|
|
return True
|
|
|
|
|
|
def test_lora_bias_delta_wrong_shape_rejected():
|
|
"""A diff_b that does not fit the module bias is a mismatch, not an apply-time surprise."""
|
|
sd = lora_pair('diffusion_model.first', 'first')
|
|
sd['diffusion_model.first.diff_b'] = torch.randn(ckpt_shape('first')[0] + 3)
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is None, f'expected rejection, got {net.modules}'
|
|
return True
|
|
|
|
|
|
def test_lora_bias_delta_on_biasless_module_binds():
|
|
"""A bias delta aimed at a module built without one is not a mismatch.
|
|
|
|
The krea2 blocks are ``bias=False`` and whole arches (flux2) carry no bias
|
|
at all, so a stray delta there is one unappliable key rather than the wrong
|
|
file. It binds, and the apply pass counts it refused.
|
|
"""
|
|
sd = lora_pair('diffusion_model.blocks.0.attn.wq', 'blocks.0.attn.wq')
|
|
sd['diffusion_model.blocks.0.attn.wq.diff_b'] = torch.randn(ckpt_shape('blocks.0.attn.wq')[0])
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and net.mismatch == 0, f'got {net.mismatch if net else None}'
|
|
return True
|
|
|
|
|
|
def test_chain_refuses_whole_network_on_mismatch():
|
|
"""One bad delta refuses the file rather than applying the layers that fit."""
|
|
sd = lora_pair('diffusion_model.blocks.0.attn.wq', 'blocks.0.attn.wq')
|
|
sd.update(lora_pair('diffusion_model.blocks.0.mlp.up', 'blocks.0.mlp.up'))
|
|
sd['diffusion_model.blocks.0.mlp.up.lora_A.weight'] = torch.randn(RANK, ckpt_shape('blocks.0.mlp.up')[1] + 8)
|
|
net = _load_via(K.try_load, sd)
|
|
assert net is None, f'expected refusal, got {set(net.modules)}'
|
|
return True
|
|
|
|
|
|
def test_lora_bare_diffusers_renamed():
|
|
"""Bare diffusers key (save_lora_adapter output) renames and binds."""
|
|
sd = lora_pair('transformer_blocks.1.ff.up', 'blocks.1.mlp.up')
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_blocks_1_mlp_up' in net.modules, f'got {set(net.modules) if net else None}'
|
|
return True
|
|
|
|
|
|
def test_lora_comfy_checkpoint_verbatim():
|
|
"""CivitAI comfy checkpoint key binds verbatim (no rename)."""
|
|
sd = lora_pair('diffusion_model.blocks.0.attn.gate', 'blocks.0.attn.gate')
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_attn_gate' in net.modules, f'got {set(net.modules) if net else None}'
|
|
return True
|
|
|
|
|
|
def test_lora_kohya_checkpoint():
|
|
"""Kohya checkpoint underscore key reconstructs to the dotted path and binds."""
|
|
out, inp = ckpt_shape('blocks.0.attn.wq')
|
|
sd = {
|
|
'lora_unet_blocks_0_attn_wq.lora_down.weight': torch.randn(RANK, inp),
|
|
'lora_unet_blocks_0_attn_wq.lora_up.weight': torch.randn(out, RANK),
|
|
'lora_unet_blocks_0_attn_wq.alpha': torch.tensor(float(RANK)),
|
|
}
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_attn_wq' in net.modules, f'got {set(net.modules) if net else None}'
|
|
return True
|
|
|
|
|
|
def test_lora_kohya_compound_block_names():
|
|
"""Kohya reconstruction protects the compound layerwise_blocks / refiner_blocks names."""
|
|
out, inp = ckpt_shape('txtfusion.refiner_blocks.1.mlp.down')
|
|
sd = {
|
|
'lora_unet_txtfusion_refiner_blocks_1_mlp_down.lora_down.weight': torch.randn(RANK, inp),
|
|
'lora_unet_txtfusion_refiner_blocks_1_mlp_down.lora_up.weight': torch.randn(out, RANK),
|
|
}
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_txtfusion_refiner_blocks_1_mlp_down' in net.modules, f'got {set(net.modules) if net else None}'
|
|
return True
|
|
|
|
|
|
def test_lora_onetrainer_passthrough():
|
|
"""OneTrainer lora_transformer_ keys bind via the shared passthrough, no rename."""
|
|
out, inp = ckpt_shape('blocks.0.mlp.up')
|
|
sd = {
|
|
'lora_transformer_blocks_0_mlp_up.lora_down.weight': torch.randn(RANK, inp),
|
|
'lora_transformer_blocks_0_mlp_up.lora_up.weight': torch.randn(out, RANK),
|
|
}
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_mlp_up' in net.modules, f'got {set(net.modules) if net else None}'
|
|
return True
|
|
|
|
|
|
def test_lora_extras_renamed():
|
|
"""Non-block extras rename and bind (img_in->first, time_embed->tmlp, final_layer->last)."""
|
|
sd = {}
|
|
sd.update(lora_pair('transformer.img_in', 'first'))
|
|
sd.update(lora_pair('transformer.time_embed.linear_1', 'tmlp.0'))
|
|
sd.update(lora_pair('transformer.final_layer.linear', 'last.linear'))
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and len(net.modules) == 3, f'got {set(net.modules) if net else None}'
|
|
assert set(net.modules) == {
|
|
'lora_transformer_first', 'lora_transformer_tmlp_0', 'lora_transformer_last_linear',
|
|
}, f'got {set(net.modules)}'
|
|
return True
|
|
|
|
|
|
def test_lora_dora_threading():
|
|
"""dora_scale flows onto NetworkModuleLora.dora_scale after the rename."""
|
|
out, _inp = ckpt_shape('blocks.0.mlp.down')
|
|
sd = lora_pair('transformer.transformer_blocks.0.ff.down', 'blocks.0.mlp.down')
|
|
sd['transformer.transformer_blocks.0.ff.down.dora_scale'] = torch.randn(out)
|
|
net = _load_via(K.try_load_lora, sd)
|
|
assert net is not None and len(net.modules) == 1
|
|
assert next(iter(net.modules.values())).dora_scale is not None
|
|
return True
|
|
|
|
|
|
def test_lokr_diffusers_renamed():
|
|
"""Diffusers-named LoKR renames and binds via NetworkModuleLokr (no chunk variant)."""
|
|
sd = lokr_pair('transformer.transformer_blocks.0.ff.gate', 'blocks.0.mlp.gate')
|
|
net = _load_via(K.try_load_lokr, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_mlp_gate' in net.modules, f'got {set(net.modules) if net else None}'
|
|
mod = next(iter(net.modules.values()))
|
|
assert isinstance(mod, network_lokr.NetworkModuleLokr)
|
|
assert not isinstance(mod, network_lokr.NetworkModuleLokrChunk)
|
|
return True
|
|
|
|
|
|
def test_loha_diffusers_renamed():
|
|
"""Diffusers-named LoHA renames and binds via NetworkModuleHada."""
|
|
sd = loha_pair('transformer.transformer_blocks.0.attn.to_q', 'blocks.0.attn.wq')
|
|
net = _load_via(K.try_load_loha, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_attn_wq' in net.modules, f'got {set(net.modules) if net else None}'
|
|
assert isinstance(next(iter(net.modules.values())), network_hada.NetworkModuleHada)
|
|
return True
|
|
|
|
|
|
def test_oft_diffusers_renamed():
|
|
"""Diffusers-named OFT renames and binds via NetworkModuleOFT without NoneType errors."""
|
|
sd = oft_pair('transformer.transformer_blocks.0.attn.to_out.0', 'blocks.0.attn.wo')
|
|
net = _load_via(K.try_load_oft, sd)
|
|
assert net is not None and 'lora_transformer_blocks_0_attn_wo' in net.modules, f'got {set(net.modules) if net else None}'
|
|
assert isinstance(next(iter(net.modules.values())), network_oft.NetworkModuleOFT)
|
|
return True
|
|
|
|
|
|
def test_full_diff_chain():
|
|
"""Full-diff extraction loads through the try_load chain and yields finite updown.
|
|
|
|
Targets ``img_in``/``first`` rather than a block attention leaf: the blocks are
|
|
built ``bias=False``, so a diff_b aimed at one is a delta with nothing to land on.
|
|
"""
|
|
out, inp = ckpt_shape('first')
|
|
sd = {
|
|
'transformer.img_in.diff': torch.randn(out, inp),
|
|
'transformer.img_in.diff_b': torch.randn(out),
|
|
}
|
|
net = _load_via(K.try_load, sd)
|
|
assert net is not None and 'lora_transformer_first' in net.modules, f'got {set(net.modules) if net else None}'
|
|
mod = next(iter(net.modules.values()))
|
|
updown, ex_bias = mod.calc_updown(mod.sd_module.weight)
|
|
assert tuple(updown.shape) == (out, inp) and torch.isfinite(updown).all()
|
|
assert ex_bias is not None and tuple(ex_bias.shape) == (out,)
|
|
return True
|
|
|
|
|
|
def test_chain_merges_multiple_families():
|
|
"""try_load merges LoRA + LoKR groups from one file onto distinct modules."""
|
|
sd = {}
|
|
sd.update(lora_pair('transformer.transformer_blocks.0.attn.to_q', 'blocks.0.attn.wq'))
|
|
sd.update(lokr_pair('transformer.transformer_blocks.0.ff.gate', 'blocks.0.mlp.gate'))
|
|
net = _load_via(K.try_load, sd)
|
|
assert net is not None and len(net.modules) == 2, f'got {set(net.modules) if net else None}'
|
|
assert set(net.modules) == {'lora_transformer_blocks_0_attn_wq', 'lora_transformer_blocks_0_mlp_gate'}
|
|
return True
|
|
|
|
|
|
# ============================================================
|
|
# Tests - calc_updown shape sanity
|
|
# ============================================================
|
|
|
|
CAT_MATH = category('math')
|
|
|
|
|
|
def test_lora_calc_updown_shape():
|
|
net = _load_via(K.try_load_lora, lora_pair('transformer.transformer_blocks.0.attn.to_q', 'blocks.0.attn.wq'))
|
|
mod = make_network_for_module(next(iter(net.modules.values())))
|
|
target = torch.randn(*ckpt_shape('blocks.0.attn.wq'))
|
|
updown, _ = mod.calc_updown(target)
|
|
assert_shape(updown, target.shape, label='LoRA calc_updown')
|
|
return True
|
|
|
|
|
|
def test_lokr_calc_updown_shape():
|
|
net = _load_via(K.try_load_lokr, lokr_pair('transformer.transformer_blocks.0.ff.gate', 'blocks.0.mlp.gate'))
|
|
mod = make_network_for_module(next(iter(net.modules.values())))
|
|
target = torch.randn(*ckpt_shape('blocks.0.mlp.gate'))
|
|
updown, _ = mod.calc_updown(target)
|
|
assert_shape(updown, target.shape, label='LoKR calc_updown')
|
|
return True
|
|
|
|
|
|
def test_loha_calc_updown_shape():
|
|
net = _load_via(K.try_load_loha, loha_pair('transformer.transformer_blocks.0.attn.to_q', 'blocks.0.attn.wq'))
|
|
mod = make_network_for_module(next(iter(net.modules.values())))
|
|
target = torch.randn(*ckpt_shape('blocks.0.attn.wq'))
|
|
updown, _ = mod.calc_updown(target)
|
|
assert_shape(updown, target.shape, label='LoHA calc_updown')
|
|
return True
|
|
|
|
|
|
def test_oft_calc_updown_shape():
|
|
net = _load_via(K.try_load_oft, oft_pair('transformer.transformer_blocks.0.attn.to_out.0', 'blocks.0.attn.wo'))
|
|
mod = make_network_for_module(next(iter(net.modules.values())))
|
|
target = torch.randn(*ckpt_shape('blocks.0.attn.wo'))
|
|
updown, _ = mod.calc_updown(target)
|
|
assert_shape(updown, target.shape, label='OFT calc_updown')
|
|
return True
|
|
|
|
|
|
# ============================================================
|
|
# Test runner
|
|
# ============================================================
|
|
|
|
|
|
def run_tests():
|
|
t0 = time.time()
|
|
|
|
log.warning('=== Parsing primitives ===')
|
|
for fn in [test_parse_key_all_prefixes, test_marker_disambiguation]:
|
|
run_test(CAT_PARSE, fn)
|
|
|
|
log.warning('=== Rename resolution ===')
|
|
for fn in [
|
|
test_resolve_block_attn_leaf_renames,
|
|
test_resolve_text_fusion_renames,
|
|
test_resolve_non_block_extras,
|
|
test_resolve_checkpoint_names_verbatim,
|
|
test_resolve_every_official_module_is_real,
|
|
]:
|
|
run_test(CAT_RESOLVE, fn)
|
|
|
|
log.warning('=== Loaders ===')
|
|
for fn in [
|
|
test_lora_official_diffusers_renamed,
|
|
test_lora_bias_delta_binds,
|
|
test_lora_bias_delta_wrong_shape_rejected,
|
|
test_lora_bias_delta_on_biasless_module_binds,
|
|
test_chain_refuses_whole_network_on_mismatch,
|
|
test_lora_bare_diffusers_renamed,
|
|
test_lora_comfy_checkpoint_verbatim,
|
|
test_lora_kohya_checkpoint,
|
|
test_lora_kohya_compound_block_names,
|
|
test_lora_onetrainer_passthrough,
|
|
test_lora_extras_renamed,
|
|
test_lora_dora_threading,
|
|
test_lokr_diffusers_renamed,
|
|
test_loha_diffusers_renamed,
|
|
test_oft_diffusers_renamed,
|
|
test_full_diff_chain,
|
|
test_chain_merges_multiple_families,
|
|
]:
|
|
run_test(CAT_LOADER, fn)
|
|
|
|
log.warning('=== calc_updown shape sanity ===')
|
|
for fn in [
|
|
test_lora_calc_updown_shape,
|
|
test_lokr_calc_updown_shape,
|
|
test_loha_calc_updown_shape,
|
|
test_oft_calc_updown_shape,
|
|
]:
|
|
run_test(CAT_MATH, fn)
|
|
|
|
elapsed = time.time() - t0
|
|
log.warning('=== Results ===')
|
|
total_pass = 0
|
|
total_fail = 0
|
|
for cat, info in results.items():
|
|
status = 'PASS' if info['failed'] == 0 else 'FAIL'
|
|
log.info(f' {cat}: {info["passed"]} passed, {info["failed"]} failed [{status}]')
|
|
total_pass += info['passed']
|
|
total_fail += info['failed']
|
|
log.warning(f'Total: {total_pass} passed, {total_fail} failed in {elapsed:.2f}s')
|
|
return total_fail == 0
|
|
|
|
|
|
if __name__ == '__main__':
|
|
ok = run_tests()
|
|
sys.exit(0 if ok else 1)
|