Repository navigation
feat(models): add Qwen-Image-2.1 text-to-image and canvas support - #9784
Conversation
Adds the qwen-image-2-1 base (Base/Turbo) with Diffusers, single-file and GGUF transformers, the RGBA VAE and webv2 generate/canvas graphs. Bit-identical to QwenImage21Pipeline; img2img starts by noise level, large encodes and decodes tile automatically, unsupported quantizations are refused at install. Shares the Qwen3-VL 8B encoder starter with Ideogram 4 and documents the model.
lstein
left a comment
There was a problem hiding this comment.
Approving at 37efce3. I found no blockers in an adversarial review of the inference path, model identification and the frontend/API contract.
Please fix before merge: the image-parity claim in the description
The decoded image is not bit-identical to QwenImage21Pipeline when the VAE runs in bf16, which is the default on CUDA and MPS.
qwen_image_2_1_latents_to_image.py:58denormalizes in fp32 (latents.float() * std + mean) and casts tovae.dtypeonce.- The pipeline (
pipeline_qwenimage21.py, end of__call__) first casts the latents tovae.dtype, then castslatents_mean/latents_stdto bf16 and does the multiply-add in bf16.
I loaded the real ComfyUI VAE through convert_comfy_vae_to_diffusers and decoded the same latents both ways on CPU in bf16. About 40% of the decoder input elements differ, and about 25% of output pixels differ by up to 12/255. Running the post-processing on its own gives 0 differing pixels, so the denormalization order accounts for the whole difference.
The fp32 order is arguably the more accurate one, so either fix is fine:
- match the pipeline order if exact parity matters, or
- narrow the Parity bullet and the QA line ("the image is identical at 2048²") to say the latents are bit-identical.
Non-blocking notes
-
Bundled VAE precision.
QwenImage21DiffusersModel._load_modelloads every submodel atchoose_bfloat16_safe_dtype(...), the VAE included. As a result:precision: float32is ignored for the bundled VAE.- On a GPU without bf16 the VAE gets fp16, which
_wan_family_dtypedocuments as unstable for this autoencoder.
The standalone VAE path does use
_wan_family_dtype. -
Denoise working-memory estimate. The 0.25 MiB/token activation term and the 0.5 MiB/text-token KV term in
_estimate_working_memoryassume 2-byte weights. Where the model falls back to fp32 (some MPS/XPU devices), the estimate is about half the real requirement. The VAE estimate already scales withdtype.itemsize; this one could too. -
Turbo fixture scheduler config. The Turbo stripped-model fixture contains the Base
scheduler_config.json(use_dynamic_shifting: true,shift_terminal: 0.02). The runtime is unaffected, becauseTURBO_SCHEDULER_CONFIGis hard-coded and matches the real HF file. The fixture would still mislead anything that read it later. -
No CFG > 1 coverage for the graph builder.
graph.test.tshas no qwen-image-2-1 case, and the node-type snapshot only covers cfg 1. Dropping theneg_cond→negative_conditioningedge would go unnoticed, and CFG > 1 would silently run without a negative prompt. One builder test at CFG > 1 would also be a good place to assert the GGUF/checkpointrequireComponenterrors. -
LoRA identification gap (already listed as a limitation). The
_has_qwen_image21_lora_keyswidth check only looks atlora_A/lora_downonto_q/to_k/to_v. Two kinds of 2.1 LoRA are therefore still identified asqwen-image:- a 2.1 LoRA that touches only
to_out.0plusimg_mlp.proj/img_mlp.out; - an attention-only LoKr.
Fine to leave for the LoRA PR.
- a 2.1 LoRA that touches only
-
Smaller items:
- The legacy UI lists 2.1 models but fails at Invoke with "No graph builders", like other bases it cannot build.
- A standalone Qwen3-VL 8B encoder left selected from an Ideogram 4 checkpoint overrides the encoder bundled in a 2.1 Diffusers model, and Recall doesn't clear it. Krea-2 and Ideogram 4 already behave this way.
- Turbo is detected from the filename alone for single files, so a renamed Turbo GGUF gets Base defaults.
Verified clean
-
Identification regressions. I ran
from_model_on_diskover all 197 stripped fixtures on both the base and head trees. Only the 8 new Qwen-Image-2.1 fixtures changed. I also built 26 synthetic files of realistic shape and none changed identification. Together these cover:- FLUX/Kontext/Krea/Schnell GGUFs, including prefixed ones;
- Qwen-Image 1 LoRAs in PEFT, Kohya, attention-only and LoKr formats;
- Qwen-Image/Wan/Anima/SD/FLUX VAEs.
pytest tests/model_identification tests/backend/model_manager: 2276 passed. -
Install-time refusals. An
InvalidMatchErrordoes not stop other configs from matching, and the 2.1 refusal only runs after the 2.1 key check passes. The Ideogram 4 refusal and GGUF dequant helper were moved without changes. -
Sigmas. Base sigmas match
retrieve_timestepsexactly (torch.equal) across step counts and sequence lengths. Turbo resampling is correct, and 1-step Base has no NaN. -
Strength slicing.
clip_sigmashandles every strength edge case tried. -
Other inference checks:
- CPU noise is seed-reproducible.
- The scheduler step matches the scheduler's dtype handling.
- The prefix KV caches are local to each invocation and separate for positive and negative.
- The system-prefix length is 14.
- Inpaint mask polarity and re-noising are correct.
-
Shared tiling helper. The default
spatial_scale=8keeps existing Qwen-Image and Wan callers byte-identical. -
API schema.
openapi.jsonregenerates identically from the Python, and the schema diff only adds things. -
Frontend. webv2
lint:tscpasses and vitest acrossgeneration/core,models/core,workflow/coreandworkbench/image-actionspasses (1515 tests). The graph builder's field names match the invocations. -
Starters. The encoder starter rename keeps both source-based and
previous_namesmatching working.
|
I smoke-tested this PR. With both Base and Turbo, inpaint and img2img need a denoising strength of about 0.90 before the image changes materially. On most other models, 0.75–0.85 already has a strong effect. The cause is the strength mapping, not the inpaint code. Starting sigma at 1024², computed with this PR's own schedules (
So strength 0.90 on 2.1 is roughly 0.80 on Qwen-Image 1 or Z-Image, which matches what I saw. I understand why noise-level selection was chosen for Turbo: picking by step index wastes most of the slider, since strength 0.50–0.85 all starts above sigma 0.89. My suggestion:
If one rule for both variants matters more, the alternative is to keep the current mapping and note in the model docs that 2.1 needs a higher denoising strength than Qwen-Image 1 for comparable change. Not a blocker; the approval stands. |
lstein
left a comment
There was a problem hiding this comment.
PR is still a work in progress. Withdrawing approval to avoid confusion.
…e torchao files Denormalize latents and map the decode to pixels in the VAE's dtype, as QwenImage21Pipeline does. Load the bundled VAE at the configured precision (never float16); scale the denoise estimate with the dtype. Refuse torchao-quantized single files (unsloth's FP8 build) at install; fix the Turbo fixture's scheduler config.
|
Thanks for the thorough review. Addressed in faf49ed:
Real-file and GPU results are in the descriptions; the stack is ready from my side. |
# Conflicts: # docs/src/content/docs/Users Guide/03.Models/02.model-families.mdx # docs/src/content/docs/start-here/system-requirements.mdx # invokeai/backend/model_manager/load/model_loaders/ideogram4.py # invokeai/frontend/api/schema.ts
lstein
left a comment
There was a problem hiding this comment.
Re-reviewed at 43e614e (fix faf49ed plus the two main merges). Thanks, this addresses the parity request and the non-blocking notes well.
Verified:
- Decode parity. l2i plus
to_imagenow produce exactly whatQwenImage21Pipeline+VaeImageProcessor.postprocessproduce, in bf16, fp16 and fp32, including NaN/±inf edge values (checked on CPU with synthetic tensors). The new decode test checks against diffusers' own processor, and reverting either half of the fix makes it fail. - Bundled VAE dtype. It honours
precisionand is never fp16. Note that on CPU-only setups it now runs in fp32 rather than bf16, which is correct but doubles VAE memory. - Denoise working memory now scales with the compute dtype, and quantized transformers are still sized by their compute dtype.
- Turbo fixture. Its scheduler config now equals
TURBO_SCHEDULER_CONFIG, and identification is unchanged. - CFG > 1 graph test. It catches a dropped or miswired negative-conditioning edge and an inverted CFG gate.
- torchao refusal. It only runs after the 2.1/Ideogram 4 fingerprint matches, so it can't refuse other bases, and it raises
InvalidMatchError, so other configs still get to match. - ERNIE-Image merge. Both sides are kept in
factory.py,configs/main.py, the starters and the loaders, with no duplicate registrations.openapi.json/schema.tsregenerate byte-identically. - Local runs.
tests/model_identification+tests/backend/model_manager: 2290 passed, 150 hardware/env skips.- Qwen-Image-2.1 node/VAE/config tests: 51 passed.
graph.test.ts: 83 passed.lint:tsc: clean.
Follow-ups (non-blocking)
- Img2img/inpaint strength is unchanged and undocumented.
clip_sigmasstill picks the start by noise level for both Base and Turbo, so Base needs about 0.90 strength for the change that ~0.80 gives on Qwen-Image 1, Z-Image and similar models (see my earlier comment). If noise-level selection stays for both variants, please at least note inqwen-image-2-1.mdxthat 2.1 needs a higher denoising strength than Qwen-Image 1 for comparable change. - Model Format Support page has no Qwen-Image-2.1 entries. Main added
03.model-format-support.mdx, and the merged02.model-families.mdx:45now points to it for "which format loads for which model". The family matrix, the encoder table (Qwen3-VL 8B) and the VAE table there all omit 2.1, and its torchao refusal is mentioned only on the family page. - The
requireComponentcalls atgraph.ts:1300-1301are dead code.compileGenerateGraphthrows the first policy validation reason (baseGenerationPolicies.tsmissingMessage) before the builder runs, so these never execute. Deleting both still passes the new GGUF/checkpoint test, which is really testing the policy. Their label also says "Qwen3-VL Encoder" rather than "Qwen3-VL 8B Encoder". Either drop them or make them the source of the message. - Small test gaps:
- The float32 working-memory test calls the static estimator directly, so hard-coding
2instead ofdtype.itemsizeat the call site (qwen_image_2_1_denoise.py:239) passes every test. - The CFG > 1 graph test checks only the source
node_idof theneg_cond.qwen3_vl_encoderedge, so wiring it from the wrong loader output field (e.g.vae) passes.
- The float32 working-memory test calls the static estimator directly, so hard-coding
# Conflicts: # docs/src/content/docs/configuration/Optimization/low-vram-mode.mdx # invokeai/frontend/api/schema.ts # tests/app/invocations/test_frontend_graph_node_types.py
… to the format page Base picks its start step by strength like Qwen-Image and diffusers' img2img; Turbo keeps noise-level selection. List Qwen-Image-2.1 on the Model Format Support page, and drop the builder's dead component checks. Test the denoise passes the compute dtype's width, and the encoder edge's source field.
Cover nearest-step rounding on Base, a guidance list shifted by a step on Turbo, and the bf16 width. Docs: Turbo's start in plain noise-level terms; the 2.1 VAE among those refusing quantized files.
It shared Qwen Image's cyan, so the two base badges rendered the same foreground. Adds a deeper cyan (aqua), related as FLUX.1/FLUX.2 are, and holds it to the badge contrast bar.
Summary
Adds Qwen-Image-2.1 (Base and the 8-step Turbo) as a new
qwen-image-2-1base. This covers text-to-image plus canvas img2img, inpaint and outpaint. Before this, the released files were misidentified: the GGUF as FLUX schnell, the VAE as SD1.Model support
QwenImage21Pipeline); single-file transformers in bf16, ComfyUI scaled fp8 orint8_convrot; GGUF transformers; standalone VAEs in Diffusers or ComfyUI layout.QwenImage21Pipelinebit for bit. That covers the prompt template and pre-norm hidden states, the Base shift and Turbo sigma table, CPU noise, the scheduler's bf16 step, the prefix KV cache, and the VAE's denormalization and pixel mapping, both done in the VAE's dtype as the pipeline does.clip_timestep_schedule_fractional): its table is front-loaded, so by index any strength from 0.5 to 0.85 would start above sigma 0.89.InvalidMatchError): nvfp4 files, torchao-quantized files (such as unsloth's FP8 build; loading them is [enhancement]: Load torchao-quantized single-file transformers (unsloth FP8) #9798), int8 files without the convrot marker, and Q8_CR GGUFs. The check is shared with Ideogram 4.double_blocks, so the 2.1 GGUF is no longer taken for FLUX schnell.Starter models
common.py, shared with Ideogram 4.previous_nameskeeps existing installs matched.Frontend
Docs: a new model page, plus the requirements, model-families, generate-panel, parameters, low-VRAM and FP8 pages.
Reference images (Edit) and LoRA support follow in separate PRs.
Related Issues / Discussions
Part of #9594. Builds on #9774 (diffusers 0.41.0).
QA Instructions
pnpm lintpasses. vitest: 10073 passed; 3 failures are locale-dependent tests that fail on a German-locale machine, unrelated to this change.tscshows the same 16 errors as on the base commit9d30e6a148; none are new.QwenImage21Pipelineat 1024² and 2048² (40 steps). The decoded 1024² image is RGB bit-identical to the pipeline's (it was off by up to 12/255 on a quarter of the pixels before the dtype fix); a tiled decode differs from the pipeline's untiled one by the tiling.int8_convrot37 dB; GGUF Q4_K_M 21 dB (visibly the same image). The ComfyUI Turbo bf16 file is identified as Turbo and gives clean 8-step images. unsloth's FP8 (torchao) file is refused at install. No ComfyUI scaled-fp8 2.1 build is published; that path is covered by fixtures.Review
All material findings were resolved, with regression tests:
From the maintainer review: the decode now matches the pipeline's dtype order (bit-identical image), the bundled VAE honours the configured precision and never runs in float16, the denoise estimate scales with the compute dtype, the Turbo fixture carries the real Turbo scheduler config, and the webv2 builder is tested at CFG > 1 and for the GGUF component requirements.
From the second maintainer review: Base image-to-image starts by step index (above), Qwen-Image-2.1 is listed on the Model Format Support page, the builder's unreachable component checks are gone, and tests pin the dtype width at the call site, the strength rounding, the guidance slice and the encoder edge's source field.
Remaining limitation: LoRAs that target only
to_outor the MLP halves, and attention-only LoKr, are told apart from Qwen-Image LoRAs only with #9793.Compatibility / Rollout
previous_names.openapi.jsonandschema.tsare regenerated; the changes are additive.Checklist
What's Newcopy (if doing a release after this PR)