fix(model loaders): ignore unexpected checkpoint keys, report them at debug only - #9581
Open
lstein wants to merge 1 commit into
Open
fix(model loaders): ignore unexpected checkpoint keys, report them at debug only#9581lstein wants to merge 1 commit into
lstein wants to merge 1 commit into
Conversation
… debug only Single-file loaders disagreed, loader by loader, about what an unexpected key from `load_state_dict(strict=False)` means: some raised, one warned, most ignored it silently, and the rest used `strict=True` and let torch raise. The ones that hard-failed turned any harmless extra tensor an exporter happened to serialize into a user-facing crash that needed a code change and a release — Anima went through this twice (invoke-ai#9201, invoke-ai#9402) for tensors the model does not need and the official checkpoint does not contain. Per the team decision on invoke-ai#9437, extra keys are now reported at DEBUG and otherwise ignored everywhere. New `invokeai/backend/util/state_dict_loading.py` holds the single policy: - `log_unexpected_keys()` — DEBUG only, never raises. - `load_state_dict_ignoring_extras()` — a drop-in for `strict=True` that keeps the strictness that matters (every required parameter must be filled, shape mismatches still raise) and drops the strictness that only produces whack-a-mole. - `reject_incomplete_load()` — the meta-device completeness sweep, generalized out of krea2. Stronger than `missing_keys` for models built under `init_empty_weights()`: immune to non-persistent buffers and tied weights. Every previously existing missing-key guard is preserved exactly; only the unexpected-key policy changed. `flux.py`'s bare `assert len(unexpected_keys) == 0` — which carried no message and was stripped entirely under `python -O` — is gone with it. Two consequences worth calling out: - `configs/pid_decoder.py` rejected unexpected keys at *identification* time, deliberately mirroring the loader ("both are fatal there"). Left alone, the PiD relaxation would have been unreachable and the installer would refuse a file that now loads fine. It keeps refusing non-string keys, which `load_state_dict` genuinely cannot survive. - Anima's unexpected-key `RuntimeError` was its only hard load-time guard, so it is replaced with the meta-device sweep rather than dropped — otherwise an incomplete checkpoint would fail mid-inference with "Cannot copy out of meta tensor" instead of at load time. `wan.py::_raise_for_incompatible_keys` deliberately keeps raising: Wan derivatives (Animate, S2V, Fun-Camera) are supersets whose extra branches are the feature the checkpoint exists for, not exporter noise, and it strips the benign extras before that check. Closes invoke-ai#9437 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01V4nLTdWBQaaDUFVLx9TyZH
lstein
requested review from
JPPhoto,
Pfannkuchensack,
blessedcoolant and
dunkeroni
as code owners
September 10, 2026 02:16
mohithvardhan002
left a comment
There was a problem hiding this comment.
I found one potential issue with the new state-dict loading flow. Please take a look at the duplicate load mentioned above.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #9437, implementing the resolution from the 2026-08-03 comment: unexpected keys in a checkpoint are reported at
DEBUGand otherwise ignored silently. They never fail a load.Single-file loaders disagreed, loader by loader, about what an unexpected key from
load_state_dict(strict=False)means — some raised, one warned, most ignored it silently, and the rest usedstrict=Trueand let torch raise. The ones that hard-failed turned any harmless extra tensor an exporter happened to serialize into a user-facing crash that needed a code change and a release. Anima went through this twice (#9201, #9402) for tensors the model does not need and the official checkpoint does not contain.What changed
New
invokeai/backend/util/state_dict_loading.pyholds the single policy:log_unexpected_keys(source, keys)DEBUGonly, never raisesload_state_dict_ignoring_extras(model, sd, ...)strict=True: keeps the strictness that matters (every required parameter must be filled; shape mismatches still raise from torch) and drops the strictness that only produces whack-a-molereject_incomplete_load(model, what=...)krea2.py(which now delegates to it)~35 call sites are routed through it across
anima,flux,z_image,krea2,qwen_image,vae,ideogram4,gemma2_encoderandmistral_encoder, pluspid/decode.py,quantization/sdnq/loaders.pyandideogram4/quantized_loading.py.Every previously existing missing-key guard is preserved exactly; only the unexpected-key policy changed. All eight sites that now pass
allow_missing=Truewere alreadystrict=Falsebefore, so nothing was silently weakened.flux.py's bareassert len(unexpected_keys) == 0— which carried no message and was stripped entirely underpython -O— goes with it.Two consequences worth reviewing carefully
1. The identification gate had to move with the loader.
configs/pid_decoder.pyrejected unexpected keys at identification time, deliberately mirroring the loader ("Missing and unexpected keys are fatal here because both are fatal there, which is what makes installation and loading accept the same set of files"). Relaxing onlypid/decode.pywould have left the PiD relaxation unreachable — the installer would still refuse a file that now loads fine, so #9437 would not have been fixed for PiD at all. The gate now refuses only non-string keys, whichload_state_dictgenuinely cannot survive (it calls.startswith()on every key).2. Anima's unexpected-key
RuntimeErrorwas its only hard load-time guard — missing keys were merely a warning there. Dropping it outright would have turned an incomplete checkpoint from a load-time error into aCannot copy out of meta tensorcrash mid-inference, so it is replaced withreject_incomplete_loadrather than removed. Verified empirically on accelerate 1.14 that this cannot false-positive:init_empty_weights()defaults toinclude_buffers=False, soAnimaTransformer's 685 parameters are on meta but its threepersistent=Falsebuffers are materialized by the constructor.Deliberately not converted
wan.py::_raise_for_incompatible_keysstill raises on unexpected keys, and I'd argue it should. Wan 2.2 derivatives (Animate, S2V, Fun-Camera) are supersets of the plain transformer whose extraaudio_injector/face_adapter/control_adapterbranches are the entire feature the checkpoint exists for, not exporter noise — they report zero missing keys and would generate with that conditioning silently absent. That loader also strips the genuinely benign extras (bundled VAE/text-encoder weights, merged-LoRA residue) before the check, so what reaches it is a named-variant signal. #9437's own table likewise lists Wan only under missing-key handling. The new module docstring records this exception and why it isn't a counter-example. Happy to convert it if the team disagrees.Testing
tests/backend/util/test_state_dict_loading.pycovers all three helpers, including the two cases the oldmissing_keys-based reasoning got wrong: a non-persistent buffer must not be a false positive, and a persistent buffer left on meta must be caught.WARNINGinstead ofDEBUGfails 4 tests; re-rejecting unexpected keys at PiD identification fails 2.mainproduces on my rig (4test_flux2_working_memory, 1test_pid_chunked_equivalence— both ROCm-environment, unrelated to this change).Adversarial review
The diff was put through a fresh-context adversarial review whose brief was to prove the "missing-key strictness is preserved exactly" invariant false. It confirmed the invariant across all 32 converted call sites (tied weights, non-persistent buffers, GGUF deliberate-later-fill,
init_empty_weights+assign=True, return-value comparisons, error-message matching, import cycles) and surfaced the two issues above, both of which are fixed here.🤖 Generated with Claude Code
https://claude.ai/code/session_01V4nLTdWBQaaDUFVLx9TyZH