diag(lora): say which file dropped weights, and how much of it

The drop message named three keys and a count with no file attached, and
note_on_change keyed it globally, so in a stack only the first LoRA's shapes
were ever printed. Each file now carries its name into its own log key, and
the count is reported as dropped-of-matched: 51 of 400 is a variant mismatch,
51 of 51 is a file that contributes nothing.

Adds one stack-level line. Every LoRA dropping every weight is not a property
of the LoRAs but of the model they were all measured against, and that is the
reading worth making explicit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-08-25 11:42:22 +03:00
co-authored by Claude Opus 5
parent 494894ef3b
commit c22207c1f9
2 changed files with 120 additions and 18 deletions
+68 -18
View File
@@ -350,9 +350,21 @@ def _adaln_curve_note(model, bad):
f"ComfyUI's H3 checkpoint.")
def resolve_lora_patches(model, lora, clip=None):
_DROPPED_FRAGMENT = re.compile(r"\| (\d+)/(\d+) DROPPED")
def _dropped_all_of(line):
"""True when a status line says every weight this LoRA matched was dropped for shape."""
return any(int(m.group(1)) == int(m.group(2)) for m in _DROPPED_FRAGMENT.finditer(line))
def resolve_lora_patches(model, lora, clip=None, name=None):
"""Match a LoRA against a model, trying known wrapper prefixes. -> (patches, note).
`name` is the file this LoRA came from. It is carried into the log lines because the
answer to "is this one bad LoRA or the loader" is only visible across a whole stack:
one file dropping keys is that file, every file dropping every key is the loader.
ComfyUI's converters handle the tensor-naming dialects (lora_up/lora_A/lora.up/PEFT
defaults); what they do not handle is a whole state dict nested under a wrapper the
model's key map never mentions, which is how a diffusers-trained LoRA ends up applying
@@ -374,11 +386,14 @@ def resolve_lora_patches(model, lora, clip=None):
if count > best_count:
best, best_count, best_note = variant, count, f"stripped {prefix}"
who = f"{name}: " if name else ""
patches = comfy.lora.load_lora(best, key_map)
if not patches:
logging.warning("[FunPack] LoRA matched no weights in this model - wrong model family?")
logging.warning(
f"[FunPack] {who}LoRA matched no weights in this model - wrong model family?")
return patches, "MATCHED NOTHING"
note = f"keys={len(patches)} fmt={best_note}"
matched = len(patches)
note = f"keys={matched} fmt={best_note}"
# Matched by NAME is not applied: a mismatched pair is dropped during the merge, and
# a LoRA that reports success while a third of it never lands is the worst of both.
bad = _mismatched_lora_keys(model, patches)
@@ -394,7 +409,7 @@ def resolve_lora_patches(model, lora, clip=None):
patches.pop(key, None)
curve = _adaln_curve_note(model, bad)
if curve:
_log.note_on_change("lora:adaln_curve", "FunPack", curve)
_log.note_on_change(f"lora:adaln_curve:{name or '?'}", "FunPack", who + curve)
note += f" | {len(bad)} adaLN adapters DROPPED (curve-form — see log)"
else:
# WITH the numbers. "trained against a different variant" is not actionable on
@@ -404,13 +419,17 @@ def resolve_lora_patches(model, lora, clip=None):
sample = "; ".join(f"{k}: LoRA {d[0]}x{d[1]} into weight {w[0]}x{w[1]}"
for k, d, w in bad[:3])
_log.note_on_change(
"lora:shape", "FunPack",
f"{len(bad)} LoRA weights do not have the shape of the weight they name, so "
f"they are DROPPED before the merge. {sample}. The LoRA was trained against a "
f"different variant of this architecture. Kept, they would not error — comfy "
f"reshapes any delta with the right element count and adds it scrambled, which "
f"shows up later as an all-NaN latent, not as a load failure.")
note += f" | {len(bad)} DROPPED (shape mismatch)"
f"lora:shape:{name or '?'}", "FunPack",
f"{who}{len(bad)} of {matched} matched weights do not have the shape of the "
f"weight they name, so they are DROPPED before the merge. {sample}. Kept, they "
f"would not error — comfy reshapes any delta with the right element count and "
f"adds it scrambled, which shows up later as an all-NaN latent, not as a load "
f"failure. {len(bad)} of {matched} means "
+ ("this LoRA was trained against a different variant of this architecture."
if len(bad) < matched else
"NOTHING from this file applies — if every LoRA in the stack says the same, "
"suspect the checkpoint the loader built, not the LoRAs."))
note += f" | {len(bad)}/{matched} DROPPED (shape mismatch)"
return patches, note
@@ -761,17 +780,18 @@ class FunPackLoraLoader:
image_model,
)
def _load_model_lora_patches(self, model, lora, lora_cache_key, model_cache_key=None):
def _load_model_lora_patches(self, model, lora, lora_cache_key, model_cache_key=None,
name=None):
"""(patches, format note). The note says how the LoRA's keys had to be read."""
if lora_cache_key is None:
return resolve_lora_patches(model, lora)
return resolve_lora_patches(model, lora, name=name)
cache_key = (lora_cache_key, model_cache_key or self._model_cache_key(model))
cached = self._cache_get(self.model_patch_cache, cache_key)
if cached is not None:
return cached
result = resolve_lora_patches(model, lora)
result = resolve_lora_patches(model, lora, name=name)
self._cache_put(self.model_patch_cache, cache_key, result, LORA_PATCH_CACHE_SIZE)
return result
@@ -1038,9 +1058,9 @@ class FunPackLoraLoader:
parts.insert(-1, f"anchor_q={quality:.2f}")
return " ".join(parts)
def _load_lora_per_block(self, model, lora, model_weight):
def _load_lora_per_block(self, model, lora, model_weight, name=None):
lora_cache_key = None
loaded, fmt = self._load_model_lora_patches(model, lora, lora_cache_key)
loaded, fmt = self._load_model_lora_patches(model, lora, lora_cache_key, name=name)
profile = self._lora_block_profile({}, loaded, model_weight)
if not profile:
return None, f"per-block fallback=global {fmt}"
@@ -1089,7 +1109,8 @@ class FunPackLoraLoader:
lora, lora_cache_key = self._load_lora_file(entry["name"])
if self._per_block_supported(model, lora_stack, entry):
patch_cache_key = (lora_cache_key, model_cache_key)
loaded, fmt = self._load_model_lora_patches(model, lora, lora_cache_key, model_cache_key)
loaded, fmt = self._load_model_lora_patches(
model, lora, lora_cache_key, model_cache_key, name=entry.get("name"))
profile = self._lora_block_profile(entry, loaded, model_weight, patch_cache_key)
if profile is not None:
item = {
@@ -1155,7 +1176,8 @@ class FunPackLoraLoader:
# Same resolver as the per-block path: comfy's own converters, plus the
# wrapper-prefix search, so an unusual format is not silently a no-op.
loaded, fmt = self._load_model_lora_patches(
model, item["lora"], item.get("cache_key"), model_cache_key)
model, item["lora"], item.get("cache_key"), model_cache_key,
name=entry.get("name"))
model, _, _ = self._apply_model_patches(model, loaded, model_weight)
apply_status = f"{item['status']} {fmt}"
@@ -1167,5 +1189,33 @@ class FunPackLoraLoader:
if loaded_count == 0:
lines.append("No LoRAs were applied.")
else:
verdict = self._stack_shape_verdict(lines)
if verdict:
_log.note_on_change("lora:stack_shape", "FunPack", verdict)
lines.append(verdict)
return (model, clip, lora_stack, "\n".join(lines))
@staticmethod
def _stack_shape_verdict(lines):
"""One line about the STACK, because the per-file lines cannot answer the question.
"3 of 400 weights dropped" is a property of one LoRA. "every weight of every LoRA
dropped" is a property of the model they were all measured against — the same reading
the user makes by eye across the status lines, made once here so it is not missed.
"""
applied = [ln for ln in lines if " mode=" in ln]
if not applied:
return None
total = [ln for ln in applied if "DROPPED (shape mismatch)" in ln]
if not total:
return None
whole = [ln for ln in total if _dropped_all_of(ln)]
if len(whole) == len(applied) and len(applied) > 1:
return (f"all {len(applied)} LoRAs in this stack matched this model by name and "
f"NONE by shape. That is one property shared by every file, so suspect the "
f"checkpoint the loader built (a repack whose layout was mis-inferred) "
f"before suspecting the LoRAs.")
return (f"{len(total)} of {len(applied)} LoRAs dropped weights for shape; the rest "
f"applied. Per-file numbers are on the lines above.")
+52
View File
@@ -195,3 +195,55 @@ def test_the_dropped_message_carries_both_shapes(mm, monkeypatch, capsys):
key, dims, want = bad[0]
assert dims == (5376, 5376)
assert want == (16128, 5376)
def _status_line(slot, name, fragment):
return f"lora_{slot}: {name} applied=+1.000 source=base mode=keys=400 fmt=as-is{fragment}"
def test_the_stack_verdict_blames_the_checkpoint_when_nothing_fits(mm):
"""The question the per-file lines cannot answer. One LoRA dropping everything is that
file; every LoRA dropping everything is the one thing they share — the model."""
lines = [_status_line(0, "a.safetensors", " | 400/400 DROPPED (shape mismatch)"),
_status_line(1, "b.safetensors", " | 128/128 DROPPED (shape mismatch)")]
verdict = mm.FunPackLoraLoader._stack_shape_verdict(lines)
assert "all 2 LoRAs" in verdict
assert "checkpoint" in verdict
def test_a_partial_drop_does_not_blame_the_checkpoint(mm):
lines = [_status_line(0, "a.safetensors", " | 51/400 DROPPED (shape mismatch)"),
_status_line(1, "b.safetensors", "")]
verdict = mm.FunPackLoraLoader._stack_shape_verdict(lines)
assert "1 of 2 LoRAs" in verdict
assert "checkpoint" not in verdict
def test_one_lora_dropping_everything_is_not_a_stack_verdict(mm):
"""A single file says nothing about what every file has in common."""
lines = [_status_line(0, "a.safetensors", " | 400/400 DROPPED (shape mismatch)")]
assert "checkpoint" not in (mm.FunPackLoraLoader._stack_shape_verdict(lines) or "")
def test_a_clean_stack_says_nothing(mm):
lines = [_status_line(0, "a.safetensors", ""), _status_line(1, "b.safetensors", "")]
assert mm.FunPackLoraLoader._stack_shape_verdict(lines) is None
def test_the_log_line_names_the_file_and_the_share(mm, monkeypatch, capsys):
"""Which file, and how much of it. "51 dropped" out of 400 is a variant mismatch; out of
51 it is the whole LoRA, and the difference decides where to look next."""
import comfy.lora
model = _Model({"w": (768, 768)})
monkeypatch.setattr(comfy.lora, "model_lora_keys_unet", lambda *a, **k: {})
monkeypatch.setattr(comfy.lora, "load_lora",
lambda *a, **k: {"w": _Adapter(384, 1536)})
monkeypatch.setattr(mm._log, "_last_by_key", {}, raising=False)
_, note = mm.resolve_lora_patches(model, {}, name="turbo.safetensors")
out = capsys.readouterr().out
assert "turbo.safetensors" in out
assert "1 of 1" in out
assert "NOTHING from this file applies" in out
assert "1/1 DROPPED" in note