Skip to content

Feat: flux2 dev support - #9234

Merged
lstein merged 52 commits into
invoke-ai:mainfrom
Pfannkuchensack:feature/flux2-dev-support
Aug 7, 2026
Merged

Feat: flux2 dev support#9234
lstein merged 52 commits into
invoke-ai:mainfrom
Pfannkuchensack:feature/flux2-dev-support

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented May 25, 2026

Copy link
Copy Markdown
Member

Summary

Adds end-to-end support for FLUX.2 [dev] alongside the existing FLUX.2 Klein implementation. Dev uses a Mistral Small 3 text encoder (hidden 5120 → joint_attention_dim 15360, hidden states sampled at layers 10/20/30) instead of Klein's Qwen3. It shares the 32-channel AutoencoderKLFlux2 VAE and the 4D-RoPE sampling backend with Klein, so most existing infrastructure is reused — only the Mistral encoder loaders/configs and the Dev graph wiring are new.

Two Mistral encoder packagings are auto-detected: the 40-layer Mistral that ships in the black-forest-labs/FLUX.2-dev diffusers repo, and the 30-layer cow-mistral3-small distillation (Comfy-Org safetensors, gguf-org GGUFs). Single-file and GGUF encoders embed the Tekken tokenizer, so no separate tokenizer download is needed.

What works

  • Generation modes: txt2img, img2img, inpaint, outpaint (canvas)
  • Multi-reference image editing via the FLUX Kontext ref-image UI (same as Klein)
  • LoRAs for FLUX.2 [dev]
  • Model formats: diffusers folder, single-file safetensors, and GGUF transformers; Mistral encoder as diffusers / single-file (bf16 / fp8 / fp4) / GGUF, with standalone VAE + encoder selection in Advanced Settings
  • Metadata recall / Remix: Dev VAE + Mistral encoder restore correctly, disambiguated from Klein's VAE / Qwen3 slice
  • Starter models for the Dev transformer, VAE, and Mistral encoders

Limitations / notes

  • Memory: the Mistral encoder is large (fp8 ~18 GB, bf16 ~35 GB) — plan VRAM/RAM accordingly.
  • Encoder precision matters: very low-bit GGUF Mistral encoders (e.g. Q2/Q3) noticeably weaken prompt adherence; prefer fp8 / bf16 or Q6/Q8.
  • Non-BFL Mistral: upstream Mistral Small 3.1 / 3.2 (40-layer, non-BFL) is accepted but off-distribution for FLUX.2 and logs a load-time warning; it is not offered as a starter model.
  • Tokenizer fallback: encoders that don't embed Tekken (older cow GGUFs, bare diffusers folders without tokenizer/) fall back to fetching black-forest-labs/FLUX.2-dev:tokenizer from HF; offline with no HF cache this errors with documented workarounds.
  • License: FLUX.2 [dev] is Non-Commercial (flagged in isNonCommercialMainModelConfig).
  • Applying a Klein LoRA on a Dev model logs a variant-mismatch warning (no crash).

How to test

Automated:

# backend (fast, no model load)
uv run --extra cuda pytest tests/test_imports.py tests/model_identification/test_identification.py

pyproject.toml gained mistral-common; uv.lock is regenerated (uv lock --locked passes).

End-to-end:

  1. In the Model Manager install a Dev transformer (a gguf-org/flux2-dev-gguf quant — ~20 GB for Q4_K_M — or the full black-forest-labs/FLUX.2-dev diffusers folder), the FLUX.2 VAE, and a Mistral encoder (Comfy-Org fp8 recommended, or a Q6/Q8 cow GGUF).
  2. Select the Dev transformer. Advanced Settings should show FLUX.2 [dev] VAE and FLUX.2 [dev] Mistral Encoder dropdowns (not Klein's Qwen3 selector). A missing VAE/encoder should block queueing with a Dev-specific message.
  3. Defaults: 28 steps, guidance 3.5, CFG 1.0, 1024×1024. Generate — the first run with Comfy / cow files logs Loaded embedded Tekken tokenizer from <file> (no HF fetch).
  4. Exercise img2img, inpaint / outpaint on canvas, and a multi-reference edit.
  5. Apply a FLUX.2 [dev] LoRA and generate.
  6. Recall: open a generated Dev image → Remix; Dev VAE + Mistral encoder should restore. Repeat with a Klein image and confirm Klein VAE + Qwen3 still recall (no cross-contamination).

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
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

Closes #8668

Adds end-to-end support for FLUX.2 [dev] alongside the existing Klein
implementation. Dev uses Mistral Small 3.1 (24B) as its sole text encoder
instead of Klein's Qwen3, with joint_attention_dim=15360 and the
guidance-distilled 32B transformer.

Backend
- taxonomy: Flux2VariantType.Dev, ModelType.MistralEncoder,
  ModelFormat.MistralEncoder, MistralVariantType
- configs: probe dev via context_in_dim=15360 (main + LoRA); new
  mistral_encoder.py with Diffusers / Checkpoint / GGUF configs;
  Main_Diffusers_Flux2_Config accepts Flux2Pipeline class name
- loaders: new mistral_encoder.py (AutoModel for Diffusers folder,
  MistralModel for single-file + GGUF with llama.cpp key conversion).
  Existing Klein transformer loaders are generic enough for dev
- ModelRecordChanges.variant union extended with MistralVariantType

Invocations
- flux2_dev_model_loader, flux2_dev_text_encoder (Mistral chat-template
  with FLUX2_DEV_SYSTEM_MESSAGE and layer-stacking 10/20/30),
  flux2_dev_lora_loader (+ collection variant)
- MistralEncoderField on model.py; flux2_denoise / flux2_vae_decode /
  flux2_vae_encode reused unchanged (already model-agnostic)

Frontend
- types/hooks/selectors for MistralEncoder, isFlux2DevMainModelConfig,
  selectFlux2DevDiffusersModels, useMistralEncoderModels
- params slice fields flux2DevVaeModel / flux2DevMistralEncoderModel /
  flux2DevSourceModel + reducers, selectIsFlux2Dev / selectIsFlux2Klein
- ParamFlux2DevModelSelect component, wired into AdvancedSettingsAccordion
- buildFLUXGraph dev branch with full txt2img / img2img / inpaint /
  outpaint + multi-reference image editing (same flux_kontext +
  collect chain as Klein, since Flux2RefImageExtension is model-agnostic)
- addFlux2DevLoRAs helper for dev LoRA wiring
- zModelType / zModelFormat / zFlux2VariantType extended for
  mistral_encoder / mistral_small_3_1 / dev
- OpenAPI schema regenerated, TS types updated

Starter models
- FLUX.2 [dev] Diffusers (bf16 + NF4), three GGUFs (Q4/Q6/Q8), Mistral
  encoder (bf16 + NF4)
Follow-up fixes after first end-to-end run with FLUX.2 [dev] GGUF +
Mistral 3.x GGUF + standalone FLUX.2 VAE.

Frontend
- buildFLUXGraph: wire dev model loader's vae into both flux2_denoise
  (required for BN statistics / inpaint) and flux2_vae_decode; missing
  edge was raising RequiredConnectionException at runtime
- readiness.ts: variant-aware FLUX.2 readiness check — dev requires
  flux2DevVaeModel + flux2DevMistralEncoderModel (or a Dev diffusers
  source); Klein keeps Qwen3/VAE check. Threads
  hasFlux2DevDiffusersSource through generate + canvas tabs and updates
  buildGenerateTabArg / buildCanvasTabArg test helpers
- en.json: noFlux2DevVaeModelSelected, noFlux2DevMistralEncoderModelSelected

Mistral encoder loader (GGUF / single-file)
- Fix "Cannot copy out of meta tensor": llama.cpp conversion produced
  `model.*` keys but loader instantiated bare MistralModel (no `model.`
  prefix). Add _convert_for_bare_mistral_model to strip the prefix and
  drop lm_head before load_state_dict
- _materialize_remaining_meta_tensors: after load_state_dict, replace any
  still-meta parameters (norms→ones, others→zeros) and buffers so the
  cache→VRAM move can't fail on partial state dicts, with a warning
  listing what was missing
- llama.cpp converter: map attn_q_norm/attn_k_norm (Mistral 3.x qk-norm
  variants), with ordering before attn_q/attn_k to avoid bad rewrites

Tokenizer / processor fallback
- _load_processor_with_offline_fallback walks a list of sources
  (black-forest-labs/FLUX.2-dev tokenizer subfolder, then
  mistralai/Mistral-Small-3.1-… and 3.2-…), trying AutoProcessor then
  AutoTokenizer for each, cache-first then online. Final error spells
  out the three workarounds (install Diffusers folder, set HF_ENDPOINT,
  pre-cache the tokenizer)
- flux2_dev_text_encoder: try multimodal `[{type, text}]` chat template
  first (PixtralProcessor / Mistral3Processor), fall back to plain
  string content (AutoTokenizer), then to manual [INST]…[/INST]

Qwen3 encoder probe strictness
- _get_qwen3_variant_from_state_dict and _get_variant_from_config now
  return None / raise NotAMatchError for unknown hidden_size instead of
  silently defaulting to qwen3_4b. The old fallback meant any llama.cpp
  GGUF causal LM (Mistral, Llama, …) was wrongly classified as Qwen3 —
  visible when the Mistral 3.x GGUF was identified as a Qwen3-4B encoder
- Checkpoint / GGUF / Diffusers loaders propagate the strictness
@github-actions github-actions Bot added python PRs that change python files invocations PRs that change invocations backend PRs that change backend files services PRs that change app services frontend PRs that change frontend files labels May 25, 2026
@lstein lstein self-assigned this May 27, 2026
@lstein lstein added the 6.14.0 label May 27, 2026
@lstein lstein moved this to 6.14.x Theme: USER EXPERIENCE in Invoke - Community Roadmap May 27, 2026
…andlers

Upstream Mistral Small 3.1/3.2 (40 layers) produces off-distribution embeddings
under FLUX.2's static (10, 20, 30) hidden-state extraction. The joint attention
was actually trained against BFL's 30-layer cow-mistral3-small distillation —
both Comfy-Org's safetensors and gguf-org's cow GGUFs ship the same 30-layer
weights, just packaged differently.

- Probing (configs/mistral_encoder.py) now rejects non-cow Mistrals across all
  three formats (Diffusers / Checkpoint / GGUF) with a clear error.
- Loader (load/model_loaders/mistral_encoder.py) extracts the embedded Tekken
  tokenizer from the `tekken_model` U8 (safetensors) / fp16-per-byte (cow GGUF)
  tensor via mistral_common, falling back to the BFL HF tokenizer. Removes the
  INVOKEAI_MISTRAL_TOKENIZER_SOURCE env var.
- Starter models: drop upstream Mistral 3.x entries, add Comfy-Org bf16/fp8/fp4
  variants alongside the cow GGUFs.
- MistralVariantType: drop Small3_1, keep only Cow.
- pyproject.toml: add mistral-common dependency.

Frontend recall:
- Add Flux2DevVAEModel + Flux2DevMistralEncoderModel handlers, disambiguating
  Klein vs dev via presence of `mistral_encoder` / `qwen3_encoder` metadata
  fields (both bases are `flux2`).
- Wire both into the Recall Parameters panel (hardcoded list was missing them).
- Add `metadata.mistralEncoder` i18n key + colocated tests.
@github-actions github-actions Bot added Root python-deps PRs that change python dependencies labels Jun 6, 2026
…encoders

After studying ComfyUI's `Flux2Tokenizer` / `Mistral3_24BModel` reference
implementation, align the FLUX.2 [dev] text-encoder path with their setup:

- Probing now accepts both 30-layer (cow distillation) and 40-layer (Mistral
  Small 3, BFL canonical / upstream) Mistrals. Re-adds `MistralVariantType.Mistral24B`
  alongside `Cow`. All three configs (Diffusers / Checkpoint / GGUF) updated.

- Loaders strip `model.norm` (replace with Identity) when the loaded weights
  are the 30-layer cow distillation. Matches Comfy's `final_norm=False` for
  the pruned variant; for transformers' `MistralModel` the final RMSNorm is
  always built but the cow was trained against the raw post-layer-29 state.

- 40-layer loads now log a clear warning that upstream Mistral 3.1 / 3.2 is
  NOT what FLUX.2's joint attention was trained against and recommends the
  Comfy-Org bf16/fp8/fp4 or gguf-org cow GGUF variants. BFL's canonical
  bundled text_encoder is also 40-layer so we don't hard-reject; the warning
  is opt-in self-discipline.

- Text encoder invocation switches from `apply_chat_template(messages, ...)`
  to a raw text template `[SYSTEM_PROMPT]{sys}[/SYSTEM_PROMPT][INST]{prompt}[/INST]`
  fed straight to the tokenizer — byte-for-byte matches Comfy's
  `Flux2Tokenizer.llama_template.format(text)`. System prompt now includes
  the literal `\n` between "object" and "attribution" Comfy ships.

- `_TekkenChatTemplateAdapter` renamed to `_TekkenRawTextAdapter` and exposes
  a `__call__(text, padding_side='left', ...)` interface that Tekken-encodes
  the raw string (BOS=1, no EOS) and left-pads with token id 11. Matches
  Comfy's `pad_left=True` / `pad_token=11` settings.

Frontend types extended for the new `mistral3_24b` variant
(zMistralVariantType, MODEL_VARIANT_TO_LONG_NAME, schema.ts).

@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.

Frontend build is failing. pytests are failing.

lstein and others added 5 commits July 9, 2026 20:52
Knip reported 6 unused exports. Each was dead code rather than incomplete
wiring, verified against the actual consumers:

- Drop the vestigial `flux2DevSourceModel` param end-to-end (state field,
  default, migration, reducer, action, selector, test). The FLUX graph
  builder auto-picks the diffusers source itself and never read this param;
  no UI set it. Mirrors how the Klein path already works.
- Delete `selectIsFlux2Klein`; the graph builder computes this locally and
  only `selectIsFlux2Dev` is consumed.
- Un-export `zMistralVariantType`; used only in the local `zAnyModelVariant`
  union, like `zQwenImageVariantType`.
- Delete `selectMistralEncoderModels`; components use the
  `useMistralEncoderModels` hook instead.
- Un-export `isFlux2DevMainModelConfig`; used only within types.ts, like its
  `isFluxDevMainModelConfig` / `isFlux2Klein9BMainModelConfig` siblings.
Pfannkuchensack and others added 7 commits July 28, 2026 20:24
…upport

Resolves conflicts with upstream video generation (invoke-ai#9163) and Ideogram 4
(invoke-ai#9303).

Conflict resolutions:
- Kept both new encoder types side by side (mistral_encoder for FLUX.2 [dev],
  wan_t5_encoder for Wan 2.2) across taxonomy, invocation fields, model
  manager and node types
- isNonCommercialMainModelConfig now covers FLUX dev, FLUX.2 Klein 9B,
  FLUX.2 dev and Ideogram 4
- Regenerated uv.lock from the merged pyproject (adds imageio/imageio-ffmpeg)
Brings in the round-2 review fixes and the tightened mistral-common pin on
top of the local merge of upstream video generation (invoke-ai#9163).

Conflict resolutions:
- modelSelected: kept the Wan 2.2 auto-default block alongside origin's
  reworded FLUX.2 variant-switch comment covering both encoder slots
…port

# Conflicts:
#	invokeai/app/services/model_records/model_records_base.py
#	invokeai/backend/model_manager/configs/factory.py
#	invokeai/backend/model_manager/taxonomy.py
#	invokeai/frontend/web/openapi.json
#	invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts
#	invokeai/frontend/web/src/features/controlLayers/store/types.ts
#	invokeai/frontend/web/src/features/modelManagerV2/models.ts
#	invokeai/frontend/web/src/features/nodes/types/common.ts
#	invokeai/frontend/web/src/features/nodes/util/graph/generation/buildFLUXGraph.ts
#	invokeai/frontend/web/src/services/api/hooks/modelsByType.ts
#	invokeai/frontend/web/src/services/api/schema.ts
…ision

main and this branch both shipped _version 4 with different new keys (PiD
fields vs the flux2 VAE merge + Mistral encoder slot), so a v4 blob written
by either parent would fail zParamsState.parse() after the merge and wipe
the whole params slice. Keep main's v3->v4 step verbatim and move the flux2
slot merge + Mistral seed to a new v4->v5 step with conditional seeding for
both v4 shapes.

Also seed the five Wan component fields in v3->v4: they were added to the
schema without a version bump while releases were still writing v3 blobs,
so a genuine released-build (v6.13.x) v3 blob fails parse() on them today
- same wipe, inherited from main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

I've re-verified the branch at 3e62aa8 against my second review. Thank you for the thorough fix commit (5026c02) — 8 of the 10 findings are correctly and completely fixed, including all the hard parts (the migration seed with a field-accurate test fixture, model-resolving guidance recall, the _reinit_inv_freq extraction validated against transformers 5.5.4, and the vision-tower drop). Two of the fixes need a follow-up, which is why this is another change request rather than an approval. Details below.

Requested changes

🔴 1. The new Klein-loader variant guard broke the VAE-only fallback (regression from the blocker-3 fix)

flux2_klein_model_loader.py calls _validate_diffusers_format on both call sites — the encoder-extraction path (:154) and the VAE-only extraction path (:132). The encoder-side rejection is exactly right, but the 32-channel AutoencoderKLFlux2 is shared between Klein and [dev], and the linear UI deliberately relies on that: buildFLUXGraph.ts falls back to any FLUX.2 diffusers pipeline when only the VAE is needed ("Fall back to any diffusers model if only the VAE is needed"), and isFlux2DiffusersMainModelConfig / readiness don't filter by variant.

Triggering sequence: install a Klein GGUF main + a standalone Qwen3 encoder + a FLUX.2 [dev] diffusers pipeline as the only FLUX.2 diffusers model (no standalone VAE) → readiness enables Invoke → the builder sets qwen3_source_model to the [dev] pipeline for VAE extraction → the new guard raises ValueError claiming the VAE is "incompatible with the Klein transformer" — which worked before this fix, and the message is factually wrong for the VAE.

Suggested fix: apply the variant guard only at the tokenizer/encoder extraction call sites ([dev] loader :153, Klein loader :154), not the VAE ones — or, if you prefer to reject cross-variant VAE sourcing on principle, the frontend readiness + source-selection fallback must be variant-filtered to match, so users get a disabled Invoke button with a reason instead of a runtime error.

🔴 2. The root-directory tokenizer step still fails offline for the exact layout blocker 4 described

The structural cleanup is all there (dead fallback gone, duplicated pre-try gone, ValueError in every except tuple, all three loaders share the helper). But the new step 3 (mistral_encoder.py:676-681) tries AutoProcessor only. With the pinned transformers 5.5.4, AutoProcessor.from_pretrained on a directory whose config.json is model_type: "mistral3" — which is precisely the BFL-style standalone-encoder layout this step targets — raises OSError: Can't load image processor … (it resolves mistral3 to a multimodal processor and requires preprocessor_config.json) before reaching any tokenizer fallback. The exception is swallowed and the ladder falls through to the HF fetch, which raises RuntimeError offline.

Reproduced empirically against the project venv with HF_HUB_OFFLINE=1: a root directory with a mistral3 config.json plus a valid tekken.json (or tokenizer.json + tokenizer_config.json) fails via AutoProcessor, while AutoTokenizer.from_pretrained on the same directory succeeds and returns a working backend. So the tokenizer is loadable — only the loader-class choice loses it. The step only succeeds today when the root config is text-only model_type: "mistral" or a full processor set including preprocessor_config.json is present.

Suggested fix: loop (AutoProcessor, AutoTokenizer) in steps 2 and 3, exactly as _load_tokenizer_from_hf already does (:616). Note if you do: probing showed AutoTokenizer can raise KeyError: 'special_tokens' on a directory containing only tekken.json, so the except tuple there needs KeyError too (or that escapes loudly).

Note on the two commits I just pushed (merge of main + migration v5 bump)

I've pushed 567cba9 (merge of latest main, which brings in the PiD decoder work — the flux2 PiD wiring is adapted into this branch's addFlux2Features helper and verified behavior-identical to main's) and d3c7bde. The second commit fixes a semantic conflict the textual merge couldn't see, and it wasn't your bug: main (after the PiD PR, 802de41) and this branch both bumped the params persist schema to _version: 4 with different new keys (main: pidMode/pidDecoderModel/gemma2EncoderModel/pidSteps, still with kleinVaeModel; this branch: flux2VaeModel + flux2DevMistralEncoderModel). After a plain merge, a v4 blob written by either parent is missing the other side's keys, skips every migration branch, fails zParamsState.parse(), and silently wipes the whole params slice — the same failure mode as blocker 1, reintroduced by the version collision.

The fix keeps main's v3→v4 verbatim and moves this branch's flux2 slot-merge + Mistral seed into a new v4→v5 step with conditional (?? null) seeding for both v4 shapes, with tests covering both. It also seeds the five wan* keys in v3→v4: main added them to the schema without a version bump while v6.13.x releases were still writing v3 blobs, so genuine released-build v3 blobs fail parse on those keys today (empirically confirmed — inherited from main, needs the same fix there).

Verified fixed (no action needed)

  1. Migration seed (blocker 1)flux2DevMistralEncoderModel = null seeded in the migration; the rebuilt fixture genuinely covers the regression (deleting the seed fails the test). ✅
  2. Guidance for [dev] (blocker 2) — the parse gate resolves the image's own model and exempts variant === 'dev'; Klein still skips; slider renders for dev only via the same predicate the graph builder uses, so visibility and consumption can't drift; generate → metadata → recall → regenerate round-trips. Uninstalled-model dev images fall back to skipping guidance — reasonable fail-safe. ✅
  3. Encoder-side variant guards (blocker 3) — the reported Klein-encoder-into-dev-transformer path (and mirror) is closed on every reachable route with clear ValueErrors, and the linear UI filters dev sources by variant. (VAE-path over-reach above is the only issue.) ✅
  4. Tokenizer structure (blocker 4) — dead fallback and duplicate pre-try removed, ValueError caught, single helper used by all three loaders. (AutoProcessor-only gap above is the only issue.) ✅
  5. Collection LoRA base checkValueError before dedup/config fetch; FLUX.1 rejected; round-1 Klein-variant rejection intact; Klein side symmetric. ✅
  6. Mistral slot clearing — cleared on base switch (with toast accounting) and intra-flux2 variant switch, including when the Klein encoder is unset; recall path covered via modelSelected. ✅
  7. rope_theta — single _reinit_inv_freq with the rope_parameters/rope_scaling fallback; verified on transformers 5.5.4 that rope_theta set via MistralConfig(rope_theta=…) lands exactly where the fallback reads it. ✅
  8. mistral-common pin>=1.5.4,<2 in the right block, lock resolves 1.11.6, and both private-API usages (_special_tokens_reverse_vocab, Tekkenizer.encode(s, bos, eos)) verified present in-range with defensive fallbacks. ✅
  9. Vision tower dropsetattr(model, …, None) removes the tower/projector from state_dict()/named_parameters(), so cache accounting and VRAM transfers exclude it; .language_model access and index-based extraction unaffected. Residual, non-blocking: the sibling full-pipeline path (flux.py Flux2DiffusersModel._load_model, used for diffusers mains and mistral_source_model) still loads and carries the ~0.8 GB tower — worth the same treatment in a follow-up. ✅
  10. 40-layer docs — all four sources now agree on the operational contract (accepted, warned, indices 10/20/30, final norm kept for 40-layer / stripped for cow). Nit, non-blocking: starter_models.py says BFL's own 40-layer has weaker adherence due to index depth, while the loader warning implies only non-BFL 40-layer weights degrade — pick one story. ✅

Pre-existing, out of scope for this PR (noting for completeness): the workflow editor can still feed a Klein 9B-family source into a 4B main (no cross-Klein variant check on the source-extraction path — same wrong-width class, linear UI is protected); the Klein loader never validates its main model's variant.

🤖 Generated with Claude Code

…n, widen the tokenizer ladder

The FLUX.2 loaders ran one validator on both the VAE- and the encoder-extraction
call site, so the cross-variant check also rejected VAE-only sourcing. Klein and
[dev] share the same 32-channel AutoencoderKLFlux2 and the linear UI relies on
that -- buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the
VAE is needed, and readiness does not filter by variant. A Klein GGUF main plus a
standalone Qwen3 encoder plus a [dev] pipeline as the only diffusers model
therefore hit a ValueError behind an enabled Invoke button. Split the validator:
format-only for the VAE path, format + variant for the encoder path.

The Mistral tokenizer ladder's local-directory rungs tried AutoProcessor only.
On transformers 5.5.4 that raises OSError for a mistral3 config.json without
preprocessor_config.json -- exactly the BFL-style standalone-encoder layout the
rungs target -- so the ladder fell through to the HF fetch and failed offline.
Both rungs now loop (AutoProcessor, AutoTokenizer), with KeyError in the except
tuple for tekken-only directories.
@github-actions github-actions Bot added the python-tests PRs that change python tests label Jul 31, 2026
@Pfannkuchensack
Pfannkuchensack requested a review from lstein July 31, 2026 03:54
Pfannkuchensack and others added 4 commits July 31, 2026 06:15
FLUX.2 [dev] recorded its Mistral text encoder as an undeclared extra key,
relying on the node's `extra='allow'`, while the Klein counterpart
`qwen3_encoder` is a proper field. Declare it so it lands in the OpenAPI
schema and is typed in the frontend instead of `unknown`.

Also bumps the node version, which was left at 2.1.0 across several model
integrations that widened the node: `ideogram4_caption` (invoke-ai#9303) and the
generation modes for FLUX.2, Anima, Qwen-Image, Ideogram 4, Wan, Krea-2 and
Ernie. All changes are additive, so this is a minor bump - saved workflows
carrying a core_metadata node now auto-update to the current field set on
load rather than silently keeping a stale one.
Conflicts:
- addRegions.ts: adopted main's RegionalPositiveConditioning alias and added
  flux2_dev_text_encoder to it
- schema.ts/openapi.json: regenerated from the merged backend

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Main's invoke-ai#9428 marked every text-encoder node idle_gpu_offloadable and added a
registry guard asserting the marker on all *_text_encoder nodes; the merge
brought that guard onto this branch where flux2_dev_text_encoder (which
neither parent knew about) fails it.

The flag alone would be wrong: the marker's contract is that the saved
conditioning is CPU-backed, because the borrowed GPU's pool lock is released
the moment the node returns. Move the Mistral embeds to CPU before save
(the placeholder clip_embeds follows their device), add the marker, bump to
1.0.1, and add the same output-device regression test the Klein encoder has.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@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.

Round 4 review (head c2b7778)

Thanks for the quick turnaround on both round-3 blockers. I re-verified each fix in code and with fresh-context adversarial passes (including mutation-testing your new tests — they do catch reverts). Summary:

  • Round-3 blocker 1 (cross-variant guard also rejected VAE-only sourcing): fixed correctly. The split is right, every source-based encoder extraction goes through _validate_encoder_source, no VAE branch retains variant logic, and the linear UI can't wire a cross-variant source into an encoder slot in any main-format × standalone-VAE × standalone-encoder combination I enumerated.
  • Round-3 blocker 2 (AutoProcessor-only local rungs): the offline gap itself is fixed — both local rungs now loop (AutoProcessor, AutoTokenizer). Unfortunately the widened AutoTokenizer rung introduces two new problems, one of which silently corrupts conditioning. Details below; both reproduced empirically against transformers 5.5.4 with real files, not hypothesized.
  • The core_metadata mistral_encoder declaration + 2.2.0 bump looks good.

Housekeeping: I pushed two commits

  • 7179712 — merge of current main. Conflicts: addRegions.ts (main refactored the posCond unions into a shared RegionalPositiveConditioning alias for Krea-2 regional prompting; I added flux2_dev_text_encoder to the alias and took main's shape), and schema.ts/openapi.json (regenerated from the merged backend rather than hand-merged).
  • c2b7778 — the merge surfaced a semantic conflict git can't see: main's #9428 added a registry guard asserting every *_text_encoder node declares idle_gpu_offloadable=True, and flux2_dev_text_encoder (which neither parent knew about) failed it, so python-tests would have gone red. The flag alone would have been wrong — the marker's contract is that saved conditioning is CPU-backed, because the borrowed GPU's pool lock is released the moment the node returns. I mirrored the Klein encoder: detach().to("cpu") before save, marker added, version 1.0.1, plus the same output-device regression test Klein has. tests/app is fully green at head (2161 passed), as are the frontend suite (1736) and lints.

Blocker 1: AttributeError escapes the ladder and crashes the whole load

_TOKENIZER_LOAD_ERRORS = (OSError, EnvironmentError, ValueError, KeyError) (mistral_encoder.py:623) doesn't cover a failure mode of the very rung you added. In transformers 5.5.4, AutoTokenizer.from_pretrained on a directory whose tokenizer_config.json names a tokenizer_class unknown to 5.5.4 hits tokenizer_class_from_name(...) → None and raises AttributeError: 'NoneType' object has no attribute 'from_pretrained' (tokenization_auto.py:723 has no None-guard).

Triggering sequence (reproduced): encoder dir with config.json (model_type: "mistral3"), tekken.json, and a tokenizer_config.json containing "tokenizer_class": "MistralCommonTokenizer" — exactly what transformers 4.5x's MistralCommonTokenizer.save_pretrained writes. Rung 3's AutoProcessor raises OSError (caught), AutoTokenizer raises AttributeError → escapes _try_load_tokenizer_from_dir_load_tokenizer_for_model crashes instead of falling through to the HF rung. Before this commit the same directory worked whenever rung 4 could succeed.

Given the ladder's whole contract is graceful fallthrough, I'd catch broadly in _try_load_tokenizer_from_dir (add AttributeError, or catch Exception and log) — an exotic exception from a probe rung should never be able to kill the load while a working fallback remains.

Blocker 2: root tekken.json + config.json now loads MistralCommonBackend, which tokenizes the template markers wrong — silently

The commit comment assumes tekken-carrying directories fail with KeyError: 'special_tokens'. That's only true when config.json is absent. With config.json (model_type: "mistral3") + a real tekken.json — the layout of an official mistralai Mistral-Small download, and the standalone-encoder layout these loaders explicitly accept — AutoTokenizer succeeds and returns MistralCommonBackend (mistral-common is a hard dependency, so this holds in every standard install).

That object BPE-encodes [SYSTEM_PROMPT]/[INST] as literal text instead of splicing them as single Tekken special ids. Verified against this module's own _TekkenRawTextAdapter on the same prompt: reference ends ..., 1, 17, 33153, 5117, 18, 3, 1097, 7990, 4 (markers = ids 17/18/3/4); MistralCommonBackend produces 1, 1091, 101289, 58343, ... (literal [, SY, STEM…). Every structural token is off-distribution, the encode "works", and generations silently degrade. Before this commit, that directory fell through to rung 4 and got the correct BFL processor whenever online/cached — so this regresses the online case too, and offline it trades an actionable RuntimeError for silent corruption.

Your ladder tests missed it because the fixture's tekken.json is a small fake that raises during parse; a real one parses fine and short-circuits.

Suggested fix: the ideal handling for a root tekken.json already exists in this file — wrap it with _TekkenRawTextAdapter (same as the embedded-tekken rung) before ever handing the directory to AutoTokenizer; and/or treat a mistral-common-backed result from AutoTokenizer as "not usable, keep falling". Either way, please add a fixture with a structurally valid tekken model file so the test exercises the success path, not just the raise path.

Non-blockers

  1. The [dev]-side justification of the guard split overclaims, and the [dev] half is UI-dead. "buildFLUXGraph falls back to any FLUX.2 diffusers pipeline when only the VAE is needed" is true only of the Klein branch. The dev branch selects from dev-only pipelines (selectFlux2DevDiffusersModels) and readiness requires a dev diffusers source, so the config your test_dev_loader_extracts_vae_from_a_klein_pipeline proves the backend accepts (dev GGUF + standalone Mistral encoder + Klein diffusers as the only VAE source) still shows a disabled Invoke button. Safe direction, but either relax the dev builder/readiness VAE-sourcing to mirror Klein or trim the docstrings to the Klein case.
  2. _validate_encoder_source in the Klein loader only rejects Dev, not Klein-family mismatches, while its comment claims the opaque-matmul class is closed. Klein 4B GGUF main + Klein 9B diffusers as qwen3_source_model (workflow editor) passes the guard and hits exactly that matmul error; the standalone-encoder path (_validate_qwen3_encoder_variant) and the frontend both enforce the family match — only the source path doesn't. Pre-existing, but worth closing while you're here: check the Klein→Qwen3 family map in the source path too. Related asymmetry: the dev guard rejects any non-dev variant, the Klein guard only dev — a future third FLUX.2 variant would be rejected on one side and silently accepted on the other.

Requesting changes for the two tokenizer-ladder blockers.

…silently mis-encoding Tekken

Round-4 review blockers, both reproduced against transformers 5.5.4 with real files before fixing.

1. An AttributeError from a probe rung killed the whole load. `AutoTokenizer.from_pretrained` on a
   directory whose `tokenizer_config.json` names a `tokenizer_class` the installed transformers does
   not know resolves that class to None and dereferences it without a guard — exactly the layout
   `MistralCommonTokenizer.save_pretrained` writes. That is not in `_TOKENIZER_LOAD_ERRORS`, so it
   escaped `_try_load_tokenizer_from_dir` and crashed a load the HF rung would have completed. The
   probes now catch broadly; the expected-error tuple only selects the log level, so an unexpected
   failure is still logged loudly instead of being swallowed.

2. A root `tekken.json` next to `config.json` did not fail in `AutoTokenizer` — it resolved to a
   mistral-common-backed tokenizer that BPE-encodes `[SYSTEM_PROMPT]`/`[INST]` as literal text
   instead of splicing them as single Tekken ids. The encode "worked" and conditioning was silently
   off-distribution. Fixed on two independent paths: the ladder now reads a standalone `tekken.json`
   itself, ahead of the transformers probes, and any mistral-common-backed result is re-wrapped in
   `_TekkenRawTextAdapter` through its underlying `MistralTokenizer` rather than used as-is. The
   vocab is fine — only its `__call__` is wrong — so re-wrapping beats discarding, which would have
   traded silent corruption for an offline RuntimeError. Verified: the re-wrapped ids are identical
   to the reference adapter's.

Also closes both non-blockers:

- `_validate_encoder_source` in the Klein loader rejected only [dev], so a Klein 9B pipeline passed
  as `qwen3_source_model` for a Klein 4B transformer and hit the very matmul error the guard exists
  to prevent (the frontend and the standalone-encoder path both enforce the family match; the
  workflow editor's source field was the only way in). It is now an allowlist keyed on a shared
  `_KLEIN_TO_QWEN3_VARIANT` map — mirroring the frontend's `KLEIN_TO_QWEN3_VARIANT_MAP` — and checks
  the source's Qwen3 family against the main model, so a future third FLUX.2 variant fails closed on
  the Klein side too, not just on [dev]. `_validate_qwen3_encoder_variant` shares that map and now
  uses `getattr` instead of `hasattr`, which turned a None variant into an AttributeError in the
  error path rather than the intended ValueError.

- The [dev] loader's `_validate_diffusers_format` docstring claimed the linear UI relies on the
  permissive VAE path. That holds for Klein, but the [dev] builder sources from dev-only pipelines
  and readiness gates on one, so there the cross-variant VAE case is reachable through the workflow
  editor only. The justification now states what actually holds: the 32-channel AutoencoderKLFlux2
  is shared (the repo ships the Klein-sourced `flux2_vae` as a dependency of every [dev] GGUF
  starter), and `mistral_source_model` is not variant-filtered in the editor.

Tests: a structurally valid Tekken fixture, so the ladder exercises the success path rather than
only the raise path the previous fake produced; regression tests for both blockers on the directory
and HF rungs; Klein-family coverage including same-family acceptance and the standalone-encoder
guard's negative path, which had no coverage at all. All new tests mutation-verified — reverting the
broad catch, the tekken rung, the re-wrap, the family check, or the allowlist each fails at least
one.

tests/app + tests/backend/model_manager: 3010 passed. The 9 failures are the pre-existing
network-dependent ones in test_model_install / test_load_api / test_download_queue.
@Pfannkuchensack
Pfannkuchensack requested a review from lstein August 6, 2026 03:58

@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.

Round 5 review (head e74568d) — approved

Both round-4 blockers are fixed, and I verified each empirically by re-running my original repro directories through the real production code (not just the test fakes), plus a fresh-context adversarial pass over the commit.

  • Blocker 1 (AttributeError escaping a probe rung): fixed. The probes now catch broadly with _TOKENIZER_LOAD_ERRORS demoted to log-level selection — the right shape: an unexpected failure is logged loudly instead of either crashing the load or being silently swallowed. The 4.5x-save_pretrained layout that used to kill the load now falls through cleanly (and in fact resolves correctly via the new tekken rung, since that layout carries a tekken.json).
  • Blocker 2 (mistral-common tokenizer silently mis-encoding the template markers): fixed on both paths. The standalone-tekken.json rung ahead of the transformers probes handles the official mistralai layout, and _normalize_tokenizer catches anything that still arrives mistral-common-backed. I verified the re-wrap against the real transformers 5.5.4 MistralCommonBackend: the MRO module-name detection fires, .tokenizer is the underlying MistralTokenizer, and both paths produce token ids identical to the reference adapter — markers spliced as single Tekken ids. Re-wrapping instead of discarding was the right call for exactly the reason your commit message gives. I also confirmed the BFL FLUX.2-dev/tokenizer folder ships no tekken.json, so the new rung cannot shadow the canonical diffusers layout.
  • Both non-blockers closed too. The Klein guard's allowlist on the shared _KLEIN_TO_QWEN3_VARIANT map covers all four Klein variants, fails closed for anything else (including a future variant), and shuts the 9B-source-into-4B-main workflow-editor gap; the hasattrgetattr fix in _validate_qwen3_encoder_variant and the corrected [dev] docstring are both right. Thanks also for the structurally valid tekken fixture — the ladder's success path is finally exercised, and I confirmed all five headline mutations (broad catch, tekken rung, re-wrap, family check, allowlist) each fail at least one new test.

CI is green at this head and the affected suites pass locally (944 tests).

Non-blocking residuals (fine as follow-ups or not at all)

  1. _normalize_tokenizer only inspects the top-level object, so a processor whose inner .tokenizer is mistral-common-backed would pass through unnormalized. Unreachable today (any local dir with tekken.json is intercepted by the tekken rung first, and the BFL fallback resolves a fast tokenizer), and it behaved identically before this commit — noting it only so the assumption is written down.
  2. _TekkenRawTextAdapter._resolve_special_ids returns {} when the wrapped tokenizer exposes neither accessor, and _encode then BPE-encodes the markers with no warning — the very failure mode this commit eliminates elsewhere. Pre-existing, but a one-line warning when _special_ids comes up empty would close the last silent path.

Approving. Thanks for the thorough turnaround across all five rounds — the tokenizer ladder is in much better shape than where this started.

@lstein
lstein enabled auto-merge (squash) August 7, 2026 13:55
@lstein
lstein merged commit f10d2a4 into invoke-ai:main Aug 7, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

6.14.0 backend PRs that change backend files frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-deps PRs that change python dependencies python-tests PRs that change python tests Root services PRs that change app services

Projects

Status: 6.14.x Theme: USER EXPERIENCE

Development

Successfully merging this pull request may close these issues.

[enhancement]: Support for Flux.2 model

2 participants