Support single-file Wan 2.2 checkpoints - #9503
Conversation
The A14B expert tag is a filename heuristic, but the loader treated it as
authoritative and aborted with "A Wan A14B GGUF expert pair must contain one
high and one low expert" whenever it couldn't tag both files.
Trust the explicit wiring instead. Which slot the user picked is a stronger
signal than the filename: an untagged file is taken at its wired position, or
inferred as the complement of a tagged partner. Only a genuine conflict — both
files claiming the same expert — is an error, now with a message naming the
expert. Untagged pairs log a warning.
The same reasoning applies to an unpaired transformer, so the "an unpaired A14B
GGUF must be the high-noise expert" hard rejection is gone too: a single wired
transformer is intent as much as a pair is, and readiness.ts already promises
the low-noise partner is optional. It degrades to the existing single-expert
quality warning, with an extra hint when the file is tagged low.
Two guards for the cases the relaxation opens up:
- Swapping the experts on the strength of a filename tag now logs a warning
naming both files and their tags, so a mistagged file can't invert the pair
invisibly.
- The same model wired to both slots is rejected. It used to fail the
{high, low} pair check; without a guard it would unload and reload the same
multi-GB expert at every boundary crossing.
Existing installs need no re-probe; the loader covers records stored with
expert='none'. The filename heuristic itself is left untouched — invoke-ai#9503
replaces it wholesale with a shared, better-guarded detector.
…#9505) * fix(wan): pair Wan 2.2 A14B GGUF experts by wiring, not just filename The A14B expert tag is a filename heuristic, but the loader treated it as authoritative and aborted with "A Wan A14B GGUF expert pair must contain one high and one low expert" whenever it couldn't tag both files. Two fixes: - Broaden the filename heuristic. It only matched `high_noise` / `low_noise` and variants, so community finetunes tagged with a bare marker (e.g. `Finetune_q5High.gguf`) resolved to 'none'. A bare `high` / `low` is now matched as a whole word, on separator *or* camelCase boundaries, so `q5High` is recognised while `Flowstate`, `slowmotion` and `highres` are not. Names carrying both markers stay ambiguous ('none'). - Trust the explicit wiring in the loader. Which slot the user picked is a stronger signal than the filename: an untagged file is taken at its wired position, or inferred as the complement of a tagged partner. Only a genuine conflict — both files claiming the same expert — is an error, now with a message naming the expert. Untagged pairs log a warning. Existing installs need no re-probe; the loader change covers records already stored with expert='none'. * fix(wan): pair Wan 2.2 A14B GGUF experts by wiring, not just filename The A14B expert tag is a filename heuristic, but the loader treated it as authoritative and aborted with "A Wan A14B GGUF expert pair must contain one high and one low expert" whenever it couldn't tag both files. Trust the explicit wiring instead. Which slot the user picked is a stronger signal than the filename: an untagged file is taken at its wired position, or inferred as the complement of a tagged partner. Only a genuine conflict — both files claiming the same expert — is an error, now with a message naming the expert. Untagged pairs log a warning. The same reasoning applies to an unpaired transformer, so the "an unpaired A14B GGUF must be the high-noise expert" hard rejection is gone too: a single wired transformer is intent as much as a pair is, and readiness.ts already promises the low-noise partner is optional. It degrades to the existing single-expert quality warning, with an extra hint when the file is tagged low. Two guards for the cases the relaxation opens up: - Swapping the experts on the strength of a filename tag now logs a warning naming both files and their tags, so a mistagged file can't invert the pair invisibly. - The same model wired to both slots is rejected. It used to fail the {high, low} pair check; without a guard it would unload and reload the same multi-GB expert at every boundary crossing. Existing installs need no re-probe; the loader covers records stored with expert='none'. The filename heuristic itself is left untouched — #9503 replaces it wholesale with a shared, better-guarded detector.
Closes invoke-ai#9463. Wan 2.2 main models could only be imported as a Diffusers folder or a GGUF file. The community ships fine-tunes as a single `.safetensors` per transformer (CivitAI, ComfyUI-oriented HF repos), and no config class matched those, so the model manager reported "unidentified model". - Add `Main_Checkpoint_Wan_Config`: probes for Wan transformer keys in either the native upstream or the diffusers key layout, tolerating the ComfyUI `model.diffusion_model.` prefix, and records variant + MoE expert. Wan 2.1 is rejected on architecture (CLIP image embedder, 1536-dim transformer, VACE control blocks) rather than by demanding the filename say "wan2.2" — community fine-tunes routinely drop the version from the name, and rejecting those was the reported bug. The same architectural check is now also applied to the GGUF probe, which previously only had the filename gate. - Add `WanCheckpointModel` loader: strips the ComfyUI prefix, dequantizes ComfyUI `fp8_scaled` weights, converts native keys to the diffusers layout, and derives the `WanTransformer3DModel` config from the weights themselves. The shape inference is factored out of the GGUF loader so both share it. - Broaden the A14B expert filename heuristic to bare `high` / `low` tokens (matched at token boundaries so "slow"/"flow"/"highway" can't trip it). - Teach `wan_model_loader` that single-file means GGUF *or* checkpoint, for both the main model and the low-noise expert slot; the two experts may mix formats. Its pairing errors now name the offending models and explain that the expert comes from the filename. - Move the shared ComfyUI single-file helpers out of the Qwen Image loader into `comfyui_state_dict_utils` rather than adding a third copy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Addresses three defects found by an adversarial review of the previous commit.
1. The broadened expert filename heuristic fired on any bare `high`/`low`
token, so `...-4step-low-cfg-merge`, `..._lowVRAM`, `...HighQuality` and
friends were labelled as MoE experts. That is worse than not guessing:
'none' raises a clear pairing error, but a mislabelled expert satisfies the
{high, low} pair check, gets swapped into the wrong slot, and silently runs
the same expert for both denoise phases.
A marker now has to include `noise` — adjacent (`high noise`, `noise_high`)
or fused (`highnoise`). Matching is per token rather than by substring, so
`slow_noise` and `flownoise` no longer match either; the original substring
heuristic got those two wrong as well.
2. A Wan LoRA can carry a full replacement `patch_embedding` (I2V adapters
change in_channels 16->36) plus the text projection, which is everything
`_has_wan_keys` looks for. Because Main outranks LoRA in `matches_sort_key`,
such a file was pulled out of the LoRA pickers into the main-model dropdown,
where it could only fail to load. The probe now also requires an undecorated
`blocks.0.<attn>.<q>.weight`, which a LoRA never has.
Deliberately a positive structural test rather than a "reject anything with
lora_A keys" exclusion — main models with merged-in LoRA weights sometimes
retain those keys.
3. `.ckpt`/`.pt`/`.pth`/`.bin` files were claimed by the probe but the loader
reads safetensors unconditionally, so they installed cleanly and then died
with an opaque header error at generation time. Restricted to `.safetensors`.
Also reworded the VACE rejection: Wan 2.2 VACE variants exist, so calling it a
Wan 2.1 marker was wrong. The refusal stands — this loader builds a plain
WanTransformer3DModel with no control branch — but the reason now says so.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-two adversarial review found the previous commit over-corrected. Requiring
`noise` in the expert marker turned off detection for the convention used by the
most widely mirrored single-file Wan 2.2 releases — the Kijai fp8 catalogue names
every file `Wan2_2-T2V-A14B-HIGH_fp8_e4m3fn_scaled_KJ.safetensors` — leaving them
unpairable, with no UI to set the expert after install.
Scored against 108 real filenames pulled from the Kijai, Comfy-Org and QuantStack
repo trees:
pre-branch main missed=25 mislabelled=0
bare-token (1st attempt) missed= 0 mislabelled=0
noise-required (2nd) missed=25 mislabelled=0
this commit missed= 0 mislabelled=0
So a bare `high`/`low` token counts again, but only when no neighbouring token
marks it as an adjective about something else (`lowVRAM`, `low-cfg`, `highRes`).
That disqualifier list is deliberately short: real releases put markers next to
plenty of unrelated words (`..._LOW_lightning_edition`), and a false negative is
what this whole change exists to avoid. An explicit `...noise` marker anywhere in
the name still outranks a bare token found earlier.
The review's suggested remedy — read the expert from safetensors metadata, as the
GGUF probe reads `general.name` — does not work: sampling the Kijai catalogue,
0 of 8 files carry any `__metadata__` at all.
Two mislabel cases from the review are now handled structurally instead: TI2V-5B
pins `expert='none'` because it is single-transformer, so `...5B-lowVRAM` can no
longer leak into the low-noise expert picker.
Also in this commit:
- Wan Animate is now refused with an accurate reason. It is 36-channel with
undecorated block weights and no VACE blocks, so nothing else turned it away,
and `strict=False` would silently drop its 127 face-adapter/motion-encoder keys.
Checked before the Wan 2.1 markers, since Animate carries `img_emb` too and was
otherwise reported as a Wan 2.1 I2V model.
- Two frontend format gates that the first commit missed: `MainModelPicker` still
hid only GGUF low-noise experts from the primary dropdown, and the readiness
pre-flight skipped the VAE/encoder check for checkpoint mains, so Invoke was
enabled for a graph that could only fail in the loader. Both now go through one
shared `isWanSingleFileMainModelConfig` guard so they cannot drift again.
- `Main_GGUF_Wan_Config` gained the LoRA-vs-transformer check for symmetry.
- The Wan LoRA probe now shares `_detect_wan_expert` instead of carrying a stale
copy of the old heuristic under a comment claiming the two matched. Side effect
worth a maintainer's eye: an expert-specific LoRA named with a bare HIGH/LOW is
now tagged rather than left untagged, so it is applied to that expert alone
instead of to both.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…variants Round three. Two independent fresh-context reviewers, one on the identification layer and one on everything else. Findings below; the corpus figures are theirs, built from ~4,700 unique HuggingFace filenames, not from this branch's tests. **My "no metadata" claim was wrong, and the previous commit message repeated it.** Every Wan 2.2 safetensors in Kijai/WanVideo_comfy_fp8_scaled carries `__metadata__["model_type"]` naming the expert. My check missed it because it range-fetched a fixed 128 KB window while those headers are ~146 KB, and silently treated a truncated read as "no metadata". Comfy-Org's files genuinely have none, so the filename heuristic is still required — but metadata is now consulted as a fallback when the name yields nothing. Filename first, deliberately: renaming is the only lever a user has to correct a mis-detection, and letting an embedded `model_type` outrank it would take that away. Expert heuristic, three real defects: - The fused-marker test was an equality check, so `WAN2.2t2vLOWNOISEFP8` (a real 14 GB transformer) tokenized to `lownoisefp8` and matched nothing. It is now anchored with startswith/endswith — which is still not a bare substring test, so `slownoise` stays unmatched. - First-bare-marker-wins was backwards. Real names put descriptors first and the expert tag last, so `Extream Low Angle HIGH` resolved to 'low' on a HIGH file. Last marker wins now. - Files serving *both* experts (`... I2V HIGH+LOW ...`, seven real examples, all physically 2x the size of their single-expert siblings) were tagged with one of them. A surviving high/low conflict now yields 'none' — which for a LoRA means "apply to both", the correct answer. Also `angle` joins the disqualifier list, and disqualifiers now only count when they *follow* the marker: "low angle" is a camera angle, but "Angle HIGH" is the high-noise expert of a camera-angle LoRA. Two more unsupported Wan variants refused, both verified against real headers: - **S2V** (`audio_injector`, `casual_audio_encoder`, `cond_encoder`, `frame_packer` — 165 of 1260 keys) was importing as plain T2V-A14B. - **Fun-Control-Camera** (`control_adapter`) was importing as I2V-A14B. This one was the dangerous case: it ships as a correctly tagged high/low pair, so the expert-pairing check passed and it would have rendered as an ordinary I2V while silently ignoring every camera input. And a generic backstop, since enumerating families by name will always lag: both Wan loaders now refuse any state dict with keys the transformer has nowhere to put, instead of letting `strict=False` discard them. Every supported release checked yields zero unexpected keys, so there is no benign case being blocked. Frontend: `modelSelected.ts` still auto-filled the VAE/encoder slots for GGUF mains only, so selecting a checkpoint main populated nothing and immediately blocked Invoke — the readiness check demanded components that nothing offered to fill. It now shares `isWanSingleFileMainModelConfig` with readiness, and the two carry comments pointing at each other. Adds the first Wan cases to readiness.test.ts, which had none. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nt + guards Round four. Three fresh-context reviewers on split scopes (identification, loaders, frontend). Five blockers, each fix mutation-verified by reverting it and confirming its test fails. 1. The unexpected-key backstop was a regression on the GGUF path. Before this branch, `WanGGUFCheckpointModel` raised only on missing keys and let `strict=False` drop the rest; now any extra key raises. The "all-in-one" packaging convention bundles transformer + VAE + CLIP in one file so ComfyUI's `Load Checkpoint` node can supply all three — Phr00t/WAN2.2-14B- Rapid-AllInOne documents exactly that, and befox/WAN2.2-14B-Rapid-AllInOne- GGUF mirrors ~110 conversions of it. Those installed and generated fine on main and died at the first Invoke here, blaming Animate/S2V/Fun-Camera. 2. Worse, the backstop contradicted this branch's own probe. The docstring on `_has_wan_transformer_block_weights` says main models with merged-in LoRA weights sometimes retain those keys, and uses a positive structural test precisely so they aren't turned away — and then the loader turned them away. Both are fixed by classifying extra keys instead of blanket-refusing them: bundled components and merged-LoRA residue are dropped with a log line, anything else still raises. The generic backstop's purpose is intact — an unenumerated conditioning branch still fails loudly, with a test to prove the allowlist didn't switch it off. 3. `_detect_wan_variant_from_state_dict` mapped the variant from in_channels alone while the transformer width sat unused in the same tuple (and is already read two functions away by the Wan 2.1 marker). The wider Wan family reuses these channel counts at other widths, so a 5120-wide 48-channel derivative was labelled TI2V-5B — which pins expert='none', selects TI2V-5B default settings and hides the low-noise partner picker. A14B is uniquely 5120-wide and TI2V-5B uniquely 3072-wide; require both to agree so an unsupported derivative falls through to unidentified rather than mislabelled. 4. readiness.ts claimed the low-noise A14B partner was optional. It is optional only for expert='high'; 'low' and 'none' are a hard ValueError in the loader, and 'none' is routine because the tag is a filename heuristic with no UI to correct it. Invoke was enabled for a graph that could only fail. The pre-flight now checks it, and the two copies of the Wan block are one shared helper so they cannot drift. Relaxable once the loader takes pairing from the wiring — see invoke-ai#9505, which the comment points at. 5. `isWanSingleFileMainModelConfig` took an all-optional structural type and returned plain boolean. That is a weak type: TypeScript accepts any object sharing one property, so passing `ModelIdentifierField` (base + type, no format) compiled clean and silently returned false, disabling every gate below it. The bare `format === 'gguf_quantized'` it replaced was at least a compile error there. Now takes AnyModelConfigWithExternal and returns a real type predicate — which immediately surfaced a latent mismatch at both readiness call sites. Also: the invoke-blocked string still said "GGUF Wan 2.2 models" while being shown for checkpoints, and WAN_SINGLE_FILE_FORMATS was an export with no external consumer. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…spellings `low_high_noise` names one file holding both A14B experts — moriqqe/Mabrle_wan2.2_low_high_noise and Chromatraining/v1_FGO_nitocris_morgan_wan2.2_t2v_low_high_noise_14B_fp16 are real releases — but the heuristic returned on the first marker adjacent to a `noise` token and reported it as the high-noise expert. The bare spelling of the same meaning (`...HIGH-LOW`) already returned 'none', so the two disagreed. A `noise` token now qualifies the whole run of adjacent markers rather than the one it happens to touch, and the explicit and bare tiers are reconciled the same way: one distinct marker wins, both means the file serves both, so 'none'. Two more from the same pass: - A disqualifier following the run now outranks an adjacent `noise`, which was previously short-circuited: `noise_LOW_VRAM` is describing VRAM. - The docstring advertised "the last surviving bare marker wins". That rule was unreachable — the return was guarded on the marker set being a singleton, so `bare[-1]` was always `bare[0]` — and the test offered as cover for it passes identically with either, because the `angle` disqualifier is what actually resolves that name. Documented what the code does instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Wan auto-fill could build a graph the loader is guaranteed to reject, and then readiness would pass it. Three separate ways: - It fell back to *any* installed Wan Diffusers model as the Component Source when no variant match existed — twelve lines below a comment explaining that a mismatched source "would silently load the wrong VAE and produce broken images". `_validate_component_source_vae` raises on exactly that. - The standalone VAE was first-match out of an unsorted entity adapter, on the stated grounds that "the standalone VAE / encoder configs don't carry variant info". The VAE configs do: `latent_channels` is 16 or 48, and `_validate_standalone_vae` compares against it. - Every write was gated on the slot being empty, and nothing anywhere clears these slots — paramsSlice carries all four across a base change and modelsLoaded has no Wan handler. So the variant matching only ever ran on a fresh slot. Selecting A14B then TI2V-5B left the 16-channel VAE wired, and because the loader prefers a standalone VAE over a Diffusers main's own, that stale slot also broke the next self-contained Diffusers model the user picked. Slots are now re-validated rather than only filled, the VAE is chosen by latent_channels, and there is no mismatched fallback — if nothing compatible is installed the slot is cleared, which reads as "pick one" instead of looking handled. Resolving the wired identifiers against the installed models also means a slot pointing at a deleted model is treated as empty. Extracted as a pure `getWanComponentUpdates`, following krea2ComponentSync, so it can be tested without driving the listener. Each of the three defects above is covered by a test that fails when the old behaviour is restored. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…els filter MainModelPicker hid Wan low-noise experts from the main dropdown, but it was the only one of three places a primary main gets chosen. The other two were unfiltered: - InitialStateMainModelPicker (the launchpad "Select your model" picker) used the raw useMainModels() list. - modelsLoaded's handleMainModels auto-selects allMainModels[0] whenever the current selection becomes unavailable, filtered only by isNonRefinerMainModelConfig. A user whose only Wan single-file is the low-noise expert had it selected for them, silently. Either way the loader then refuses it — "An unpaired Wan A14B model must be the high-noise expert" — so the model was reachable but unusable. All three now share `isSelectableAsPrimaryMainModel`, which names the concept so a fourth entry point has something to reach for. Also pins the dequantization behaviour that `test_scale_bookkeeping_never_reaches_the_model` was quietly relying on. Its fixture pairs a bf16 weight with a scale, so the loader multiplies it, and the test asserted only that the scale *keys* were gone — it constructed a 4x-scaled weight and said nothing about it. Now asserted, with the reason there is no fp8 gate written down in `_dequantize_comfyui_fp8`: not every checkpoint using these keys stores fp8 weights, and skipping the multiply for those is as wrong as applying a stale scale. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s survived
Mutation-tested the branch's own tests by breaking each thing they claim to
cover. Four survived untouched:
- `out_channels` was inferred from `proj_out.weight` with a five-line comment
explaining why it must not be assumed equal to `in_channels` — and every
fixture was 16-in/16-out, so `out_channels = in_channels` kept the suite
green. Now covered by an asymmetric 36-in/16-out model, the I2V-A14B shape
the comment is about.
- `Main_GGUF_Wan_Config` gained the branch-family refusal and the
LoRA-vs-transformer guard for parity with the checkpoint probe, and neither
had a test: deleting both left all 526 config tests passing. The checkpoint
side has tests for all five behaviours, so the omission was asymmetry rather
than intent.
- `layer_count_fallback` was unreachable. `num_layers == 0` means no key starts
with `blocks.`, and `require("blocks.0.ffn.net.0.proj.weight")` has already
raised by then; setting the fallback to 999 changed nothing. It was also the
only use of the `variant` argument, so dropping it makes
`_build_wan_transformer_config` derive the config purely from the weights,
which is what its docstring says it is for.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e frontend invoke-ai#9505 made the loader take the A14B expert pairing from the wiring rather than the filename tag: an unpaired or untagged A14B now runs with a warning instead of raising, and the only hard error left is two files claiming the same expert. Three things on this branch were still enforcing the old contract. - `readiness.ts` blocked Invoke when an unpaired A14B main was tagged anything but 'high'. That would now stop a generation the backend is happy to run — and 'none' is the common case for community checkpoints, which is what this branch exists to support. Rule and its string removed; the VAE/encoder rule stays. - The checkpoint path in `wan_model_loader.py` carried its own copy of the same rejection (the conflict this rebase had to resolve). It now shares invoke-ai#9505's wiring-first logic, with the messages generalised from "GGUF" to "single-file" since checkpoints reach all of them, and the variant-mismatch error extended to name both models and their variants — it was the only pairing error that named neither. - `isSelectableAsPrimaryMainModel` hid every Wan low-noise expert from all three primary-main entry points, justified in its own comment by "the loader refuses it". That justification is gone. Hiding is still the right steer — a low expert belongs in the Transformer (Low Noise) slot and running it alone looks worse — but unconditional hiding would leave someone whose only Wan file is a low expert with their model missing from every picker and no way to reach it. Replaced with `selectPrimaryMainModelOptions`, which hides a low expert only while a partner of the same variant is installed, so the list degrades instead of dead-ending. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… two rounds Two independent reviewers on split scopes. Six real defects, three of them regressions introduced by the very commits meant to fix things. Regressions: - `_detect_wan_expert` let a disqualifier consume the whole run of adjacent markers instead of only the one it qualifies, so `...-A14B-HIGH_lowVRAM_fp8_scaled_KJ` went from 'high' to 'none'. That is not a safe default: for a main it disables both pair checks, and for a LoRA it applies a single-expert distill to both experts. - `handleMainModels` tested the *selected* model against the visibility-filtered list, conflating "hidden" with "uninstalled". Installing the high-noise partner therefore read as "your model vanished" and swapped the user onto an unrelated model — firing the whole base-changed cascade (LoRAs disabled, VAE cleared, bbox resized) for what was only a file install. Availability is now tested against what exists; only the auto-pick uses the filtered list. - The `wan_model_loader.py` conflict resolution duplicated the unpaired-A14B warning block, logging it twice. The test used `any(...)` and could not see it. Gaps: - The merged-LoRA allowlist covered kohya and PEFT but not LoKr, LoHa, DoRA or OFT, which `LoRA_LyCORIS_Wan_Config` does accept — so the probe took a merged file the loader then refused, blaming Animate/S2V/Fun-Camera. It now tracks the same families, and matches path segments rather than substrings so a future branch named `..._lora_adapter` still trips the backstop instead of being swallowed. - Benign extras were classified but never removed, so an all-in-one checkpoint's bundled VAE and UMT5-XXL were dequantized, upcast to bf16 and reserved in the RAM cache before `load_state_dict` discarded them — several GB for the exact family this branch added support for. They are now dropped straight after the prefix strip, ahead of all three costs. - Readiness checked only that the Wan slots were *populated*, while the Advanced comboboxes offer every Wan VAE and Diffusers main with no variant filter and nothing re-runs the auto-fill on a hand-picked slot. Four loader errors were reachable with Invoke enabled. It now shares the compatibility predicates with the auto-fill rather than restating them, and covers the duplicate-transformer case that became reachable once low experts could appear in the main picker. Also: a self-contained Diffusers main no longer gets a standalone VAE force-wired over its own (the loader ranks the wired one higher, and clearing it just refilled on the next selection); a low-noise partner left over from another variant is cleared; the encoder slot is resolved against installed models like the other two; and the variant-mismatch error prints a value rather than a raw enum. Every fix is mutation-verified. Two tests that asserted the old behaviour were rewritten, and the tautological `hasattr(model, "vae")` assertions were replaced with ones that read the dict actually handed to `load_state_dict`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
57ae57b to
e73f233
Compare
FindingsMedium: The LoRA probe now calls the shared Chain: probe tags a 5B LoRA Trigger, executed against the PR branch: To expose this issue, add a test that probes a TI2V-5B LoRA whose stem contains a bare Medium:
Chain: select a TI2V-5B main -> open Advanced -> pick anything in "Transformer (Low Noise)". The Wan pre-flight tests exercise this only with an A14B main ( To expose this issue, add a test that wires a low-noise partner while the main is TI2V-5B, and one with a Diffusers main, and asserts no reason is pushed. Low:
To expose this issue, add a test that runs the GGUF loader over a native-layout state dict carrying one key the rename table does not map, and pin the intended outcome (refuse, or extend Low:
To expose this issue, add a test that asserts the low-noise picker predicate accepts an untagged single-file A14B main when the primary picker is showing its partner. |
…#9503 1. TI2V-5B LoRAs were regressed to inert. Widening the LoRA probe to the shared `_detect_wan_expert` made it read the bare high/low token convention, but it did not carry over the structural pin `_resolve_wan_expert` applies on the main-model side. A 5B LoRA whose stem happens to contain a standalone `low` ("Wan2.2_TI2V_5B_low_light_v2") was tagged, routed by `_resolve_target("auto")` into `loras_low_noise` alone, and then never read — the single-transformer 5B denoise path consumes only the primary list. The generation succeeded with the LoRA silently absent. Variant is now resolved first and only A14B gets a tag. 2. Readiness blocked Invoke on a slot the loader ignores. `pushWanReasons` judged the low-noise partner for every Wan main, but the loader reads it only for a single-file A14B: it logs "ignored for the single-expert TI2V-5B variant" and skips the pairing block, and the Diffusers branch never looks at the slot. For a TI2V-5B main this was unavoidable rather than occasional — the partner picker can only offer A14Bs, so every possible pick failed the variant check, and the error text does not name the combobox to clear. 3. Pinned the GGUF side of the unexpected-key backstop. The gate is applied to the pre-existing GGUF loader, which previously checked `missing_keys` only. Its diffusers-layout behaviour was already covered; the native-layout path, where an unmapped key is possible at all, was not. 4. An untagged expert pair was unwireable from the linear UI. The partner picker required `expert === 'low'`, so a pair probing to none/none appeared twice in the main picker and never in the low-noise one. Since invoke-ai#9505 the wiring is authoritative and the tag advisory, so the picker now takes anything single-file that is not tagged `high` and not TI2V-5B. `expert` is absent from `ModelRecordChanges` and records are never re-probed, so this was permanent for anything already installed. Every fix is mutation-verified: each new test fails against the unfixed code.
Two adversarial reviewers on split scopes. The frontend gate and picker survived an enumeration of every 1-3 model library over a 17-model universe (0 false positives, 0 models invisible in both pickers). The backend found a real hole. - The TI2V-5B expert pin only fires when the variant was detected, and `detect_wan_lora_variant` reads the inner dim off an `attn1.to_q` LoRA pair. A LoKr/LoHa adapter, or one patching only `to_k`/`to_v`, comes back `variant=None`, keeps its `low` tag, and then sails past `_assert_lora_variant_matches_main`, which returns early on an unknown variant. Records written before the pin are in the same position. Confirmed by probing: a LoKr 5B LoRA named `Wan2.2_TI2V_5B_low_light_v2` still yields `expert='low'`. `_warn_if_low_routing_is_inert` becomes `_correct_inert_low_routing`: where the main is TI2V-5B and the routing came out low-only, apply the LoRA to the single transformer instead of warning that it will do nothing. The main's variant is the one signal that cannot be wrong. - The GGUF native-layout test pinned a scenario the probe forecloses. `_find_unsupported_wan_variant_marker` rejects `vace_blocks.` with `NotAMatchError` at identification, so a VACE GGUF never reaches a loader. Switched to an un-enumerated branch, which is what the backstop is actually for. The bundle test's stated mechanism was also wrong: benign extras are dropped before the rename table runs, so the two passes never interact. - `selectPrimaryMainModelOptions` had no test that could catch it being keyed on the wide partner predicate — with two untagged models the wide test classes both as low experts, so neither has a partner and the mistake hides behind itself. Added the `[high, untagged]` case, which fails against that mutation. - Corrected a false claim in the picker comment: models *can* be re-probed, via the Reidentify endpoints. Re-probing an untagged file just returns `none` again, which is the actual reason the tag cannot be corrected. Mutation-verified: reverting the re-route fails 3 tests, reverting the primary filter to the wide predicate fails the new guard.
|
Thanks — all four confirmed, all four fixed, and the first one led somewhere worse than reported. Fixes are in 1. TI2V-5B LoRAs regressed to inert — confirmed, and the fix needed to go further than the probeReproduced exactly as you described. Probing against the branch: This is my regression: widening the LoRA probe to the shared I first fixed it where you pointed, by resolving the variant before the expert tag and skipping the tag for
Records written before the pin existed are in the same position, and So the fix now lives in both places. Tests: One small correction to the report: only the 2. Readiness blocks a slot the loader ignores — confirmedRight on every point, including that this is unavoidable rather than occasional for a TI2V-5B main: the combobox renders for every Wan main (
A fresh-context reviewer then enumerated every 1–3 model library over a 17-model universe (2 single-file formats × 3 variants × 3 expert tags, plus 3 Diffusers variants) against a transcription of the loader's pairing block: 0 false positives and 0 false negatives introduced. It did surface one pre-existing hole that neither of us named — the loader's 3. GGUF unexpected-key gate — behaviour change confirmed, coverage claim not quiteThe behaviour change is real and deliberate: Two corrections on the supporting evidence, though. The gate is not untested through the GGUF path — I'd also put the blast radius lower than "existing, working installs". An unmapped key that should have become a real parameter also leaves that parameter unfilled, which the pre-existing Worth noting for whoever reads this next: the family names one reaches for here are all foreclosed upstream. 4. Untagged expert pair unwireable from the linear UI — confirmedFixed. Your test suggestion is in, and the review caught that it wasn't enough on its own: with two untagged models the wide predicate classes both as low experts, so neither has a partner and neither is hidden — the mistake hides behind itself. There's now also a One correction here too, on my side rather than yours. My code comment claimed installed records "are never re-probed"; that's wrong — Every fix is mutation-verified — reverting each one fails the test that covers it. Reverting the re-route fails 3 tests; reverting the primary filter to the wide predicate fails the new guard. Full suites green locally (4711 backend, 1859 frontend), ruff 0.11.2 and all five frontend lints clean. |
FindingsLow:
Confirmed by execution against the branch, two TI2V-5B single-file configs, one tagged The tagged file is offered nowhere in the linear UI. Trigger: a Wan GGUF probed before this branch whose stem contained The narrow fix is one condition: To expose this issue, add a test that passes a TI2V-5B config with Low:
The behaviour is defensible: a single-transformer model has one list, and applying with a warning beats a silent no-op. The contract shown to the user has not followed it. The field description still reads "'both'/'high'/'low' override it", and the routing table comment at No test needed; amend the description and the comment to say that |
…pickers Two low-severity findings from Pfannkuchensack's second round. - `isWanLowNoisePartnerOption` excludes every TI2V-5B from the partner picker, but `selectPrimaryMainModelOptions` still hid any single-file main tagged `low` once `hasPartner` found another single-file Wan main of the same variant — and it matched `ti2v_5b` against `ti2v_5b` like anything else. A TI2V-5B carrying `expert='low'` satisfied both exclusions at once and was offered nowhere in the linear UI. Reproduced: with two TI2V-5B single-file configs, one tagged `low`, the primary picker returned only the untagged one and the partner picker returned nothing. Such a record is reachable, which is where my previous round's "0 models invisible in both pickers" claim went wrong: that enumeration assumed the TI2V pin made the combination impossible, but the pin is new here. `main`'s `_detect_wan_gguf_expert` applies the tag without consulting the variant, so a 5B named `...-low_noise.gguf` installed before this branch still carries `expert='low'` today. Both predicates now share one `isWanTi2v5bConfig` test, so they cannot drift apart again — the failure mode here was precisely the two disagreeing. - `_correct_inert_low_routing` re-points an explicit `target="low"` as well as an inferred one, which is intended, but the contract shown to the user still described the old behaviour. The `target` field description (rendered in the node editor) and the routing-table comment now say that `low` is applied to the single transformer on TI2V-5B. schema.ts and openapi.json regenerated for the description change; both diffs are that one line. Mutation-verified: dropping the 5B exclusion from the hide test fails the new `never leaves a TI2V-5B invisible in both pickers`.
…A routing Follow-up from a fresh-context review of the previous commit, which fixed one user-facing string and left two others contradicting it. Invocation class docstrings are rendered in the workflow editor — they reach `openapi.json` as the schema `description`, which `parseSchema.ts` puts on `template.description` and `InvocationNodeInfoIcon` displays. So hovering the "Apply LoRA - Wan 2.2" node's info icon said a low-only routing "logs a warning" and is inert, while the Target field one row down said it is applied to the transformer. Both classes now describe the correction, including `WanLoRACollectionLoader`, which is the node the linear UI actually emits. Also narrowed the routing-table comment: it claimed the correction "overrides all four" targets, but `_correct_inert_low_routing` returns early unless the routing came out low-only, so `both` and `high` are never touched. Text only — no behaviour change. schema.ts and openapi.json regenerated.
|
Both confirmed, both fixed — 1. A TI2V-5B tagged
|
Thanks so much! |
Brings in the three commits this branch was behind: Intel XPU device support (invoke-ai#9401), single-file Wan 2.2 checkpoints (invoke-ai#9503) and the opt-in Wan low-VRAM mode (invoke-ai#9462). Only uv.lock conflicted. This branch lifts fastapi to 0.141.1, which adds annotated-doc and typing-inspection as dependencies, while invoke-ai#9401 rewrote every marker string in the file to carry the new xpu extra. Regenerated with 'uv lock' instead of hand-merging those marker chains; the result has both (fastapi 0.141.1, 776 xpu markers). openapi.json and schema.ts merged without conflict, and were checked rather than assumed: both are supersets of main (143 -> 145 paths, 870 -> 877 schemas, 187 -> 189 operations), so nothing from main was dropped.
Summary
Closes #9463.
Wan 2.2 main models could only be imported as a Diffusers folder or a GGUF file. The community ships fine-tunes as a single
.safetensorsper transformer — that's what CivitAI and ComfyUI-oriented Hugging Face repos distribute — and no config class matched that shape, so the model manager reported "unidentified model" for all of them.Reproduced before fixing: synthetic checkpoints matching the three models named in the issue all fell through to
Unknown_Config. This also explains the reporter's observation that the Wan VAEs and the umt5 encoder imported fine — those already have checkpoint configs.Opening as a draft: the filename heuristic below went through three adversarial review rounds and I'd like a second pair of eyes on the judgement calls before this is considered ready.
What's in it
Main_Checkpoint_Wan_Config— probes for Wan transformer keys in either the native/upstream or the diffusers key layout, tolerating the ComfyUImodel.diffusion_model.prefix. Records the variant (T2V-A14B / I2V-A14B / TI2V-5B, frompatch_embeddingin_channels) and the MoE expert.WanCheckpointModelloader — strips the ComfyUI prefix, dequantizes ComfyUIfp8_scaledweights, converts native keys to the diffusers layout, and derives theWanTransformer3DModelconstructor kwargs from the weights themselves rather than from a table of known repos. The shape inference is factored out of the GGUF loader so both share one implementation; the extracted helper is behaviourally identical to the code it replaced.wan_model_loadernow treats "single-file" as GGUF or checkpoint, for both the main model and the low-noise expert slot. The two experts may mix formats — both loaders produce a plainWanTransformer3DModel, andwan_denoisealready computes the sidecar-LoRA decision per expert. Pairing errors now name the offending models and explain that the expert is detected from the filename.Shared ComfyUI single-file helpers moved out of the Qwen Image loader into
comfyui_state_dict_utilsrather than adding a third copy.qwen_imagere-exports them, so its tests are untouched.Two things worth a maintainer's judgement
1. The A14B expert heuristic. The expert can't be read off the weights, so it comes from the filename, with declared metadata as a fallback. Two conventions are both common and both have to work:
high_noise/low_noiseand its spellings — Comfy-Org's repackaged repos.HIGH/LOWtoken — the Kijai fp8 catalogue and many CivitAI fine-tunes.Scored against ~4,700 unique filenames enumerated from HuggingFace, the rules land at 0 missed and 0 mislabelled on real A14B transformers. The tradeoffs encoded:
highandlowconflict yieldsnonerather than a guess — files serving both experts are real, and for a LoRAnonecorrectly means "apply to both".vram,cfg,steps,angle, …) suppresses bare markers used as adjectives, and only counts when it follows the marker — "low angle" is a camera angle, "Angle HIGH" is the high-noise expert of a camera-angle LoRA.This is the least certain part of the change and it rests on conventions that can shift. The durable fix is to make it non-load-bearing:
expertcurrently can't be set at all (ModelRecordChangeshas no such field andbuild_common_fieldswhitelists it out), so a mis-detection means renaming the file and reimporting. I'd like to file exposing it as a follow-up.2. The Wan LoRA probe now shares that heuristic instead of carrying a stale copy under a comment claiming the two matched. Side effect: an expert-specific LoRA named with a bare
HIGH/LOWis now tagged rather than left untagged, so it applies to that expert alone instead of to both. I believe that's a fix — mis-routing Lightning distills to both experts degrades output — but it changes existing behaviour and is adjacent to the reported issue. Happy to drop that commit if you'd rather keep this narrow.What is deliberately refused
Wan 2.1 is rejected on architecture rather than on the filename — community fine-tunes routinely drop the version from the name, and rejecting those was the reported bug:
img_emb.*/condition_embedder.image_embedder.*patch_embeddinginner dim 1536The one irreducibly ambiguous case is Wan 2.1 T2V-14B, shape-identical to a Wan 2.2 A14B expert; it's caught by an explicit
wan2.1in the name and otherwise imports as A14B. The same architectural check now also applies to the GGUF probe, which previously had only a filename gate and would accept a misnamed Wan 2.1 I2V.Wan variants built on extra conditioning branches are refused with a reason naming the family — Animate (
face_adapter,motion_encoder), S2V (audio_injector,cond_encoder, …), Fun-Control / Fun-Camera (control_adapter), VACE (vace_blocks). Each would otherwise build a correctly-shaped transformer, report zero missing keys, and generate with its entire conditioning branch silently absent. Fun-Camera was the worst: it ships as a properly tagged high/low pair, so the expert-pairing check passed too.Because enumerating families by name will always lag, both loaders now also refuse any state dict carrying keys the transformer has nowhere to put, rather than letting
strict=Falsediscard them. Every supported release checked yields zero unexpected keys, so nothing benign is blocked.diffusersdoes shipWanVACETransformer3DModelandWanAnimateTransformer3DModel, so VACE and Animate are tractable follow-ups; S2V has no transformer class at all.Verification
tsc, eslint, prettier, knip, dpdm clean.openapi.jsonandschema.tsregenerated per the typegen/openapi CI procedure and confirmed byte-identical to the generator output.WanTransformer3DModel:missing=0, unexpected=0, every parameter bf16, correctin_channels16/36/48 andnum_layers40/40/30._build_wan_transformer_configdiffed againstWanTransformer3DModel.__init__and against the upstreamtransformer/config.jsonfor all three Wan-AI Diffusers repos: every kwarg the family varies is derived from shapes; everything left at a default is identical across variants.out_channelsinference, both new GGUF probe gates, and an unreachable layer-count fallback) and are covered now.Upgrade note
Existing installs are not re-probed. Anyone who hit #9463 has a persisted
Unknown_Configrow for their Wan checkpoint, and nothing in this branch repairs it — there is no re-probe endpoint and/scan_folderonly reportsis_installed. Those models have to be deleted and re-imported to pick up the new config class.The same applies to
expert: it is recorded at probe time and cannot be set through any API (ModelRecordChangeshas no such field), so a model installed before this branch keeps whatever tag it was given. Round four found and fixed two heuristic defects, but only new imports benefit.Not included
No node version bump. Nothing stored in a workflow changed — only the live template's model-picker filter widened — so bumping would flag every saved Wan workflow as needing an update for no benefit.