mirror of
https://github.com/vladmandic/automatic
synced 2026-09-19 17:24:32 +02:00
fix(lora): refuse a network whose deltas do not fit the model
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
This commit is contained in:
@@ -443,6 +443,49 @@ def test_lora_official_diffusers_renamed():
|
||||
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')
|
||||
@@ -551,14 +594,18 @@ def test_oft_diffusers_renamed():
|
||||
|
||||
|
||||
def test_full_diff_chain():
|
||||
"""Full-diff extraction loads through the try_load chain and yields finite updown."""
|
||||
out, inp = ckpt_shape('blocks.0.attn.wq')
|
||||
"""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.transformer_blocks.0.attn.to_q.diff': torch.randn(out, inp),
|
||||
'transformer.transformer_blocks.0.attn.to_q.diff_b': torch.randn(out),
|
||||
'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_blocks_0_attn_wq' in net.modules, f'got {set(net.modules) if net else None}'
|
||||
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()
|
||||
@@ -645,6 +692,10 @@ def run_tests():
|
||||
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,
|
||||
|
||||
+24
-2
@@ -101,9 +101,13 @@ def make_linear(out_features: int, in_features: int, seed: int = 0):
|
||||
|
||||
|
||||
def stamp_fuse(module):
|
||||
"""Mark the module as network_backup_weights leaves it in fuse mode: no tensor backup."""
|
||||
"""Mark the module as network_backup_weights leaves it in fuse mode: no tensor backup.
|
||||
|
||||
The bias flag is only set when the module has one, same as the loader does.
|
||||
"""
|
||||
module.network_weights_backup = True
|
||||
module.network_bias_backup = True
|
||||
if getattr(module, 'bias', None) is not None:
|
||||
module.network_bias_backup = True
|
||||
|
||||
|
||||
def stamp_backup(module, weight, bias):
|
||||
@@ -187,6 +191,23 @@ def test_fuse_mismatched_bias_delta_is_refused():
|
||||
return True
|
||||
|
||||
|
||||
def test_fuse_bias_delta_without_a_bias_is_refused():
|
||||
"""A delta aimed at a bias the module does not have is counted, not silently dropped.
|
||||
|
||||
The loader lets this through on purpose: whole architectures are built
|
||||
bias=False, so a stray diff_b is one unappliable key rather than the wrong
|
||||
file. The apply pass is where it has to become visible.
|
||||
"""
|
||||
module = torch.nn.Linear(8, 32, bias=False)
|
||||
w0 = module.weight.detach().clone()
|
||||
stamp_fuse(module)
|
||||
written = network_apply_direct(module, torch.full_like(w0, 0.5), torch.full((32,), 0.25), device=CPU)
|
||||
assert written == (True, False), f'reported {written}'
|
||||
assert module.bias is None, 'a bias appeared on a module built without one'
|
||||
assert_close(module.weight.detach(), w0 + 0.5, 'weight')
|
||||
return True
|
||||
|
||||
|
||||
def test_fuse_mismatched_weight_delta_is_refused():
|
||||
"""A weight delta that does not fit is dropped while the bias delta still lands."""
|
||||
module, w0, b0 = make_linear(32, 8)
|
||||
@@ -248,6 +269,7 @@ def run_tests():
|
||||
test_fuse_deactivate_restores,
|
||||
test_fuse_mismatched_bias_delta_is_refused,
|
||||
test_fuse_mismatched_weight_delta_is_refused,
|
||||
test_fuse_bias_delta_without_a_bias_is_refused,
|
||||
]:
|
||||
run_test(CAT_FUSE, fn)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user