fix(mm): stop the SDNQ guards from aborting folder-encoder identification - #9561
Open
Pfannkuchensack wants to merge 4 commits into
Open
fix(mm): stop the SDNQ guards from aborting folder-encoder identification#9561Pfannkuchensack wants to merge 4 commits into
Pfannkuchensack wants to merge 4 commits into
Conversation
…ck to unknown
The starter-model download `black-forest-labs/FLUX.2-klein-{4B,9B}::text_encoder+tokenizer`
lands the Qwen3 encoder as several `model-0000N-of-0000M.safetensors` shards. The SDNQ
rejection guard in `Qwen3Encoder_Qwen3Encoder_Config` called `mod.load_state_dict()`, which
raises `ValueError("Multiple weight files found for this model")` - not a `NotAMatchError` -
when a folder holds more than one weight file. That aborted this config's probe entirely, so
no candidate matched and the encoder was stored as `unknown`.
Make the SDNQ key check shard-safe: read tensor *names* from the safetensors headers per
shard instead of loading a state dict. That is cheap (no tensor data is materialized), works
for any number of shards, and keeps detecting SDNQ weight+scale pairs even when they are
split across shards. The mirrored fallback in `Qwen3Encoder_SDNQ_Folder_Config` had the same
crash for sharded SDNQ folders and now uses the same helper, with its file scope unchanged.
Verified against a real `FLUX.2-klein-9B::text_encoder+tokenizer` install, which now
identifies as `Qwen3Encoder_Qwen3Encoder_Config` / variant `qwen3_8b`.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pfannkuchensack
requested review from
JPPhoto,
blessedcoolant,
dunkeroni and
lstein
as code owners
August 30, 2026 19:44
…tion The SDNQ support added an "is this folder SDNQ-quantized?" guard to Qwen3Encoder_Qwen3Encoder_Config - a folder config that until then never touched weights - and implemented it with `mod.load_state_dict()`. That call refuses to pick a file when a directory holds more than one weight file and raises ValueError, which is not a NotAMatchError, so the factory records it as an error, no candidate matches and the model falls back to Unknown_Config. Every folder-layout Qwen3 encoder we ship as a starter model is sharded (FLUX.2 Klein 9B: 4, Klein 4B: 2, Z-Image text_encoder: 3), so all three install as "Unable to identify model". The SDNQ checks in identification now read tensor names from the safetensors headers via safetensors_tensor_names() / safetensors_have_sdnq_keys() in the sdnq detection module - the module the SDNQ PR created so this question has one implementation. Reading headers is per file, so it works for any number of shards, and it resolves weight/scale pairs across the union of all of them: sharding splits a checkpoint by tensor order and routinely separates a weight from its scale. Three further gaps closed while here: - Qwen3Encoder_Qwen3Encoder_Config and Qwen3Encoder_SDNQ_Folder_Config must partition folders but asked different questions: the unquantized one checked the marker in text_encoder/ too and keys across every shard, the SDNQ one only the root and only safetensors directly in it. A markerless SDNQ encoder in the nested layout was rejected by both. Both now call one shared predicate. - A corrupt quantization_config.json aborted the SDNQ probe with a JSONDecodeError instead of falling through to the key check. - The Qwen3-only q_norm/k_norm fallback read the state dict inside a bare except, so a sharded folder yielded no signal and was rejected. Single-file and GGUF configs keep using the state dict, which is correct for them. Verified against a real FLUX.2-klein-9B::text_encoder+tokenizer install: it now identifies as Qwen3Encoder_Qwen3Encoder_Config / variant qwen3_8b, and still does with ModelOnDisk.load_state_dict patched to raise. Closes invoke-ai#9567
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
fix(mm): stop the SDNQ guards from aborting folder-encoder identification
The SDNQ support in #9228 added an "is this folder SDNQ-quantized?" guard to
Qwen3Encoder_Qwen3Encoder_Config— a folder config that until then never touched weights — andimplemented it with
mod.load_state_dict(). That call refuses to pick a file when a directory holdsmore than one weight file and raises
ValueError, which is not aNotAMatchError, so thefactory records it as an error and the config never matches. With no candidate left, the model falls
back to
Unknown_Config.Every folder-layout Qwen3 encoder we ship as a starter model is sharded, so all three install as
"Unable to identify model":
black-forest-labs/FLUX.2-klein-9B::text_encoder+tokenizerblack-forest-labs/FLUX.2-klein-4B::text_encoder+tokenizerTongyi-MAI/Z-Image-Turbo::text_encoder+tokenizerWhat changed
The SDNQ checks in identification read tensor names from safetensors headers.
safetensors_tensor_names()/safetensors_have_sdnq_keys()live inbackend/quantization/sdnq/detection.py— the module the SDNQ PR created precisely so thisquestion has one implementation — and
folder_has_sdnq_keys()now delegates to them. Readingheaders is per file, so it works for any number of shards, and it resolves weight/scale pairs
across the union of all of them: sharding splits a checkpoint by tensor order and routinely
separates a weight from its scale.
The two Qwen3 folder configs now ask the same question.
Qwen3Encoder_Qwen3Encoder_Config(rejects SDNQ) and
Qwen3Encoder_SDNQ_Folder_Config(requires it) must partition folders, but theylooked at different places: the unquantized one checked the marker in
text_encoder/too and keysacross every shard, the SDNQ one only the root and only safetensors sitting directly in it. A
markerless SDNQ encoder in the nested
text_encoder/layout was therefore rejected by both —the exact shape that lands a model in
unknown. Both now call one shared predicate.A corrupt
quantization_config.jsonno longer aborts the probe.Qwen3Encoder_SDNQ_Folder_Configcalledjson.load()unguarded; it now falls through to the keycheck, like every other marker read.
The Qwen3-only
q_norm/k_normfallback works for sharded folders. It read the state dictinside a bare
except, so a sharded folder yielded no signal at all and was rejected. It readsheader names now.
No behaviour changes for single-file checkpoints or GGUFs — those paths still use the state dict,
which is correct for them.
Related Issues / Discussions
Closes #9567
QA Instructions
Verified against a real
black-forest-labs/FLUX.2-klein-9B::text_encoder+tokenizerinstall (4 shards)on Windows:
ModelConfigFactory.from_model_on_disk()→Unknown_Config, withQwen3Encoder_Qwen3Encoder_Config: ValueError: Multiple weight files found for this modelin thedetails.
Qwen3Encoder_Qwen3Encoder_Config,variant=qwen3_8b, and it is selectable for FLUX.2Klein 9B.
ModelOnDisk.load_state_dictpatched to raise, identification still succeeds.To reproduce in the UI: Model Manager → Starter Models → install "FLUX.2 Klein Qwen3 8B Encoder"
(or the 4B / Z-Image encoders). It should register as a Qwen3 encoder, not as Unknown.
New tests:
load_state_dict()is called at alltext_encoder/is honouredq_normkeysMerge Plan
Normal merge. Supersedes the narrower first pass on this branch.
Checklist
What's Newcopy (if doing a release after this PR)