Feat: flux2 dev support - #9234
Conversation
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
…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.
…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).
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.
…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
left a comment
There was a problem hiding this comment.
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)
- Migration seed (blocker 1) —
flux2DevMistralEncoderModel = nullseeded in the migration; the rebuilt fixture genuinely covers the regression (deleting the seed fails the test). ✅ - 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. ✅ - 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.) ✅ - Tokenizer structure (blocker 4) — dead fallback and duplicate pre-try removed,
ValueErrorcaught, single helper used by all three loaders. (AutoProcessor-only gap above is the only issue.) ✅ - Collection LoRA base check —
ValueErrorbefore dedup/config fetch; FLUX.1 rejected; round-1 Klein-variant rejection intact; Klein side symmetric. ✅ - 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. ✅ rope_theta— single_reinit_inv_freqwith therope_parameters/rope_scalingfallback; verified on transformers 5.5.4 thatrope_thetaset viaMistralConfig(rope_theta=…)lands exactly where the fallback reads it. ✅- mistral-common pin —
>=1.5.4,<2in 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. ✅ - Vision tower drop —
setattr(model, …, None)removes the tower/projector fromstate_dict()/named_parameters(), so cache accounting and VRAM transfers exclude it;.language_modelaccess and index-based extraction unaffected. Residual, non-blocking: the sibling full-pipeline path (flux.pyFlux2DiffusersModel._load_model, used for diffusers mains andmistral_source_model) still loads and carries the ~0.8 GB tower — worth the same treatment in a follow-up. ✅ - 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.pysays 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.
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
left a comment
There was a problem hiding this comment.
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_metadatamistral_encoderdeclaration + 2.2.0 bump looks good.
Housekeeping: I pushed two commits
7179712— merge of currentmain. Conflicts:addRegions.ts(main refactored the posCond unions into a sharedRegionalPositiveConditioningalias for Krea-2 regional prompting; I addedflux2_dev_text_encoderto the alias and took main's shape), andschema.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_encodernode declaresidle_gpu_offloadable=True, andflux2_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/appis 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
- 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 yourtest_dev_loader_extracts_vae_from_a_klein_pipelineproves 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. _validate_encoder_sourcein the Klein loader only rejectsDev, not Klein-family mismatches, while its comment claims the opaque-matmul class is closed. Klein 4B GGUF main + Klein 9B diffusers asqwen3_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.
lstein
left a comment
There was a problem hiding this comment.
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 (
AttributeErrorescaping a probe rung): fixed. The probes now catch broadly with_TOKENIZER_LOAD_ERRORSdemoted 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_pretrainedlayout that used to kill the load now falls through cleanly (and in fact resolves correctly via the new tekken rung, since that layout carries atekken.json). - Blocker 2 (mistral-common tokenizer silently mis-encoding the template markers): fixed on both paths. The standalone-
tekken.jsonrung ahead of the transformers probes handles the official mistralai layout, and_normalize_tokenizercatches anything that still arrives mistral-common-backed. I verified the re-wrap against the real transformers 5.5.4MistralCommonBackend: the MRO module-name detection fires,.tokenizeris the underlyingMistralTokenizer, 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 BFLFLUX.2-dev/tokenizerfolder ships notekken.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_VARIANTmap 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; thehasattr→getattrfix in_validate_qwen3_encoder_variantand 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)
_normalize_tokenizeronly inspects the top-level object, so a processor whose inner.tokenizeris mistral-common-backed would pass through unnormalized. Unreachable today (any local dir withtekken.jsonis 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._TekkenRawTextAdapter._resolve_special_idsreturns{}when the wrapped tokenizer exposes neither accessor, and_encodethen BPE-encodes the markers with no warning — the very failure mode this commit eliminates elsewhere. Pre-existing, but a one-line warning when_special_idscomes 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.
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_dim15360, hidden states sampled at layers 10/20/30) instead of Klein's Qwen3. It shares the 32-channelAutoencoderKLFlux2VAE 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-devdiffusers repo, and the 30-layercow-mistral3-smalldistillation (Comfy-Org safetensors, gguf-org GGUFs). Single-file and GGUF encoders embed the Tekken tokenizer, so no separate tokenizer download is needed.What works
Limitations / notes
tokenizer/) fall back to fetchingblack-forest-labs/FLUX.2-dev:tokenizerfrom HF; offline with no HF cache this errors with documented workarounds.isNonCommercialMainModelConfig).How to test
Automated:
# backend (fast, no model load) uv run --extra cuda pytest tests/test_imports.py tests/model_identification/test_identification.pypyproject.tomlgainedmistral-common;uv.lockis regenerated (uv lock --lockedpasses).End-to-end:
gguf-org/flux2-dev-ggufquant — ~20 GB for Q4_K_M — or the fullblack-forest-labs/FLUX.2-devdiffusers folder), the FLUX.2 VAE, and a Mistral encoder (Comfy-Org fp8 recommended, or a Q6/Q8 cow GGUF).Loaded embedded Tekken tokenizer from <file>(no HF fetch).Checklist
What's Newcopy (if doing a release after this PR)Closes #8668