Skip to content

fix: remove FP4 Mistral starter, fix model rename validation, limit Z-Image to Qwen3 4B encoders - #9587

Open
Pfannkuchensack wants to merge 8 commits into
invoke-ai:mainfrom
Pfannkuchensack:fix/small_v6_fixes
Open

Pfannkuchensack wants to merge 8 commits into
invoke-ai:mainfrom
Pfannkuchensack:fix/small_v6_fixes

Conversation

@Pfannkuchensack

Copy link
Copy Markdown
Member

Summary

Three small, independent fixes, one commit each.

1. Remove the FLUX.2 [dev] FP4 Mistral encoder starter model (#9565)
The Mistral loader has no FP4 support. Users downloaded 12.3 GB and got a shape mismatch at the first generation. This removes the starter entry and drops FP4 from the recommended encoders in the loader's 40-layer Mistral warning. The GGUF and FP8/BF16 variants are unaffected.

2. Model rename: accept 3-character names and re-enable Save (#9364)
Adopted from #9419 by @leepokai, who closed it because they could not follow up. The change is unmodified and they are credited as co-author.

  • The name validator required length > 3 while its message says "at least 3 characters". It now uses >= 3.
  • The Save button was gated on the number of keys in formState.errors, which could stay stale after clearing and retyping the name. It is now gated on !formState.isValid, the same pattern SystemPromptForm uses.

3. Only offer Qwen3 4B encoders for Z-Image (#9526)
Z-Image consumes 2560-wide embeddings. Three places also accepted Klein 9B's 8B encoder (4096 wide), which fails at the first denoise step:

  • the encoder picker (useQwen3EncoderModels)
  • the auto-default when switching to Z-Image (selectQwen3EncoderModels)
  • metadata recall (isQwen3EncoderModelConfig)

All three now use a new isZImageQwen3EncoderModelConfig guard (variant === 'qwen3_4b'), with a matching hook and selector. variant is required on all Qwen3 encoder configs, so no installed encoder is hidden by mistake.

Users who already have an 8B encoder in the Z-Image slot would see an empty picker while generation still fails. For them, readiness now reports "Selected Qwen3 Encoder is incompatible with Z-Image". The slot's config is resolved in the caller, following the existing wanWiredConfigs pattern. The now unused selectQwen3EncoderModels export is removed. The new i18n key is added to en.json only.

Related Issues / Discussions

Closes #9565
Closes #9364
Closes #9526

Supersedes #9419

QA Instructions

Automated (run locally):

  • tsc --noEmit, eslint, prettier --check, knip: clean
  • vitest on readiness.test.ts, parsing.test.tsx, modelSelected.test.ts: 226/226 passing. New tests:
    • readiness: an 8B encoder in the Z-Image slot produces the incompatibility reason, a 4B encoder does not
    • metadata: ZImageQwen3EncoderModel rejects an 8B encoder
    • listener: the Z-Image encoder slot defaults from the 4B pool, and stays empty when that pool is empty
  • ruff check and ruff format --check on the two Python files: clean. The starter list imports with no dangling reference.

I have not tested these changes manually in the running app. Suggested manual checks:

  1. FP4 starter: Model Manager → Starter Models → FLUX.2 [dev]. The "Mistral Encoder (Comfy FP4 mixed)" entry is gone.
  2. Rename: Model Manager → select a model → Edit → delete the whole name → type a new name of 3 or more characters. Save is enabled, and a 3-character name saves.
  3. Z-Image encoder: Install the "FLUX.2 Klein Qwen3 8B Encoder" and a Z-Image Qwen3 4B encoder.
    • Select a Z-Image model: the encoder picker lists only the 4B encoder.
    • With no Z-Image diffusers model installed, switch from another base to Z-Image: the encoder slot defaults to the 4B encoder.
    • With an 8B encoder already in the slot from a previous version, the Invoke button shows the incompatibility reason.

Merge Plan

No special considerations. The redux state shape is unchanged (no slice or migration changes). A slot that still holds an 8B encoder keeps its value and is only flagged by readiness.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Tests added / updated (if applicable)
  • ❗Changes to a redux slice have a corresponding migration (n/a, no slice changes)
  • Documentation added / updated (if applicable) (n/a)
  • Updated What's New copy (if doing a release after this PR)

Pfannkuchensack and others added 3 commits September 13, 2026 22:55
The loader has no FP4 support, so the 12.3 GB download failed with a
shape mismatch at the first generation. Also drop FP4 from the
recommended encoders in the 40-layer Mistral warning.

Closes invoke-ai#9565
… clearing the name

The name validator required more than 3 characters while its message
says "at least 3". Save was gated on the error count, which could stay
stale after clearing and retyping the name; gate it on isValid instead.

Adopted from invoke-ai#9419.

Closes invoke-ai#9364

Co-authored-by: leepokai <109857817+leepokai@users.noreply.github.com>
Z-Image consumes 2560-wide embeddings, but the encoder picker, the
auto-default on model switch and metadata recall all accepted Klein
9B's 8B encoder (4096 wide), which fails at the first denoise step.

Narrow all three to qwen3_4b, and add a readiness reason for a slot
that still holds an 8B encoder from before this change - the picker
no longer shows it, so the slot would otherwise look valid.

Closes invoke-ai#9526
@github-actions github-actions Bot added python PRs that change python files backend PRs that change backend files frontend PRs that change frontend files labels Sep 13, 2026
@Pfannkuchensack Pfannkuchensack moved this from 6.14.2 to 6.14.1: Bug fixes to 6.14.0 in Invoke - Community Roadmap Sep 13, 2026
@Pfannkuchensack Pfannkuchensack moved this from 6.14.1: Bug fixes to 6.14.0 to 6.14.2 in Invoke - Community Roadmap Sep 13, 2026
The auto-default on switching to Z-Image dispatched only key, name and
base. The reducer parses with zModelIdentifierField, which requires hash
and type, so it dropped the payload and the encoder slot stayed empty.
Send the full identifier like the VAE and Anima defaults do, and type
the reducer's payload as ModelIdentifierField so the compiler catches
this shape.

@lstein lstein left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adversarial review at f13282dbbb. I tried to break each of the three fixes; all three are correct in the paths they name, and the tests they add are load-bearing. One gap in the new readiness check lets the exact failure from #9526 through, so I am requesting changes for that alone. The fix is a two-line move.

Verified locally on the branch: tsc --noEmit, eslint, prettier --check clean; vitest 226/226 on the three touched suites; ruff check/format clean on the two Python files; the starter list imports (195 entries, no FP4 Mistral). knip reports 7 unused exported types, all present on main and untouched by this diff.

Blocker

B1. The incompatibility check is skipped exactly when the loader still uses the stale encoder — self-contained SDNQ pipeline mains.

readiness.ts:542 and :1302 sit inside if (!mainIsSelfContainedPipeline). But buildZImageGraph.ts:102 passes qwen3_encoder_model: zImageQwen3EncoderModel ?? undefined unconditionally, and ZImageModelLoaderInvocation resolves the encoder as standalone slot → Qwen3 Source → self-contained main (z_image_model_loader.py:122-134). So a populated slot always wins over the pipeline's own encoder.

Triggering sequence:

  1. Pre-PR state: Z-Image slot holds Klein 9B's 8B encoder, VAE slot holds a FLUX.1 VAE.
  2. Upgrade; select an SDNQ Z-Image pipeline main. modelSelected.ts:145 sees zImageVaeModel && zImageQwen3EncoderModelhasValidConfig → leaves both slots alone.
  3. Readiness: mainIsSelfContainedPipeline is true → the whole block, including the new else if, is skipped. Invoke is enabled.
  4. Loader takes the 8B slot ahead of the pipeline's 4B encoder → 4096 vs 2560 at the first denoise step — the same error as #9526, from the one class of main the PR's new safety net does not cover.

I confirmed this with a probe in readiness.test.ts (fails on the branch):

it('errors when the standalone encoder is an 8B encoder even with a self-contained SDNQ main', () => {
  const reasons = getReasonsWhyCannotEnqueueGenerateTab(
    buildZImageTabArg({
      model: zImageSdnqPipelineModel,
      zImageQwen3EncoderModel: { key: 'enc' },
      zImageQwen3EncoderConfig: { key: 'enc', type: 'qwen3_encoder', variant: 'qwen3_8b' } as AnyModelConfig,
    })
  );
  expect(hasZImageQwen3IncompatibleReason(reasons)).toBe(true);
});

Fix: hoist the check out of the !mainIsSelfContainedPipeline branch in both tabs, so it runs whenever zImageQwen3EncoderConfig resolves — the slot is consulted by the loader regardless of the main's format. Please add the probe (and its canvas twin) so the hole stays closed.

Non-blocking

N1. First-load race: the new reason can be missing until something else changes. useReadinessWatcher (readiness.ts:249-289) does not depend on the model-configs query. On a fresh page load params rehydrates before getModelConfigs resolves, so the first debouncedUpdateReasons run sees query.data === undefined, selectZImageQwen3EncoderConfig returns null, and no reason is pushed. It only appears after the next dependency change (socket connect, any param edit). wanWiredConfigs has the same exposure, so this is a pre-existing pattern rather than something this PR introduced — worth a follow-up that makes the resolved configs a watcher dependency.

N2. FP4 is still "accepted" in three docstrings and by the probe. mistral_encoder.py:7 and :348 (loader) and configs/mistral_encoder.py:269 still advertise Comfy-Org fp4. _drop_quantization_metadata does weight * scale on the packed uint8 nibbles, which is where the shape mismatch in #9565 comes from — it was never FP4 dequantization. Removing the starter stops the 12 GB download, but a user who installs the file by hand still gets the same crash at first generation. Suggest fixing the docstrings here and, as a follow-up, rejecting *_fp4_mixed at probe or load with a clear message.

N3. isValid runs full-form validation on mount, and _updateValid does not populate errors. Two consequences worth knowing: a model whose current name is under 3 characters, or an external model whose provider_id/provider_model_id is under 3 characters, now opens with Save disabled and no visible error until the offending field is touched. Previously Save was enabled until the user edited the name. Rare enough to leave as-is; noting so the change in behaviour is deliberate. I reproduced with react-hook-form 7.69 that !isValid tracks ''abcdefghabcab correctly (disabled/enabled/enabled/disabled), so the new gate does fix the "delete everything then retype" path in #9364.

Attacks that did not land

  • Guard correctness. variant is derived from hidden_size in every Qwen3 config class (qwen3_encoder.py:134-190, :364-382, :638-660), so variant === 'qwen3_4b' is exactly "2560 wide". The field is required on all four config classes; no installed encoder can lack it.
  • Loader vs readiness precedence. When both the standalone slot and Qwen3 Source are set, the loader uses the slot (:122), so flagging the slot is not a false positive.
  • Payload-type change on zImageQwen3EncoderModelSelected. Every dispatch site (parsing.tsx:1621, :1705, the picker, the listener) already sends a full ModelIdentifierField or null; tsc agrees. The listener test's toEqual on the five identifier fields is what catches the old dropped-payload bug.
  • Metadata recall. The Klein handler (parsing.tsx:2006-2011) keeps isQwen3EncoderModelConfig, so Klein 9B recall is unaffected; the Z-Image handler's base gate still separates Klein 4B from Z-Image.
  • Starter removal. No other reference to flux2_dev_comfy_mistral_fp4 anywhere in the tree (bundles included).
  • Klein 4B + 8B encoder is the mirror of this bug in ParamFlux2KleinModelSelect, but it is out of scope for the linked issues.

Review follow-up. The new Qwen3 encoder compatibility check sat inside the
`!mainIsSelfContainedPipeline` branch in both tabs, but ZImageModelLoaderInvocation
resolves the encoder as standalone slot -> Qwen3 Source -> self-contained main, and
buildZImageGraph passes the slot unconditionally. A populated slot therefore wins
even when the main ships its own encoder, so the check skipped exactly the class of
main it is meant to protect: selecting an SDNQ Z-Image pipeline with a leftover
Klein 9B 8B encoder in the slot left Invoke enabled and failed at the first denoise
step with 4096 vs 2560 - the invoke-ai#9526 crash.

The check now runs whenever zImageQwen3EncoderConfig resolves, in both tabs. It
cannot double-report: a resolved config means the slot is set, so the
"no encoder source" reason does not fire alongside it.

Five tests cover it, including the canvas twin; the two self-contained-SDNQ cases
fail without this change while the 4B and GGUF cases pass either way, so the fix is
not over-correcting.

Also drops FP4 from the three Mistral encoder docstrings that still advertised it.
_drop_quantization_metadata multiplies the packed uint8 by its scale, which is
correct for FP8 and wrong for FP4, where a byte holds two nibbles that must be
unpacked first - that is where the shape mismatch in invoke-ai#9565 comes from. Rejecting
`*_fp4_mixed` at probe or load stays a follow-up.
The FP4 correction in the previous commit edited the class docstring of
MistralEncoder_Checkpoint_Config, which pydantic publishes as the schema
description, so schema.ts went stale and typegen-checks failed on "compare files".

Also moves the four-line explanation of why FP4 cannot be dequantized out of that
docstring and into a comment above the class. It is a note about packed nibbles for
whoever picks up the follow-up, not something every API client should receive as a
field description - and keeping it out means the regenerated schema is a one-line
change instead of five.
Companion to the schema.ts regeneration. openapi.json is checked in too and
openapi-checks regenerates and diffs it separately, so the same one-line
description change had to land in both artifacts.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend PRs that change backend files frontend PRs that change frontend files python PRs that change python files

Projects

Status: 6.14.2

2 participants