Skip to content

feat(models): add LoRA support for Qwen-Image-2.1 - #9793

Open
Pfannkuchensack wants to merge 2 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/qwen-image-2-1-lora
Open

Pfannkuchensack wants to merge 2 commits into
invoke-ai:mainfrom
Pfannkuchensack:feat/qwen-image-2-1-lora

Conversation

@Pfannkuchensack

@Pfannkuchensack Pfannkuchensack commented Oct 10, 2026 •

Copy link
Copy Markdown
Member

Summary

Adds LoRA support to Qwen-Image-2.1.

Stacked on #9787. This PR's own change is the last commit; the one before it is #9787 (#9784 is merged).

Layouts. The published LoRAs target the diffusers module names under a diffusion_model. (ComfyUI), transformer. / base_model.model. (diffusers/PEFT) or lora_unet_ (Kohya) prefix. One exception: ComfyUI fuses each block's gated MLP input into img_mlp.gate_up. It is split by its output rows into gate_layer (first half) and proj (second half), as the base checkpoint converter does. The split covers low-rank layers, PEFT/ai-toolkit DoRA (output-dim magnitude) and full (diff) layers. Full diffs on the RMSNorm weights (attn.norm_q/norm_k, txt_in.text_norm), which ComfyUI's official Turbo LoRA carries, are merged into those weights on every format, since no sidecar can patch a norm.

All or nothing. Identification and conversion share one key table. A LoRA with a layer this transformer lacks, or with a gate_up layer that cannot be split, is not installed as a Qwen-Image-2.1 LoRA. That covers LoKR, LoHA and LyCORIS DoRA, which mix both halves. Applying only the part that fits would pass a partial LoRA off as a whole one.

Backend

  • New LoRA_LyCORIS_QwenImage21_Config: the gated MLP's names (gate_up, gate_layer, proj, out) or the 4096 width of the block attention (to_q/k/v, to_out.0, low-rank or LoKR), excluding FLUX.2 LoRAs and anything with unsupported keys. Qwen-Image LoRA configs no longer claim such files, attention-only 2.1 LoKr included.
  • FLUX's Kohya detector now accepts only FLUX's own block names (double_blocks, single_blocks); a Kohya Qwen LoRA whose every key is on the MLP no longer reads as FLUX.
  • New qwen_image_2_1_lora_loader and qwen_image_2_1_lora_collection_loader nodes. They refuse non-LoRAs and LoRAs of other bases by name, with a hint that Qwen Image LoRAs do not fit 2.1, and skip LoRAs already applied.
  • qwen_image_2_1_denoise patches the transformer for the whole sampling run, before the prefix KV cache is built:
    • plain weights are patched in place and restored afterwards;
    • GGUF/int8 weights get sidecars, and fp8 ones per layer;
    • working memory is reserved only for sidecars: the LoRA sizes plus one MLP-wide update per forward.

webv2: qwen-image-2-1 joins the LoRA-supported bases. The graph routes the transformer through the collection loader.

Docs: a LoRA section on the Qwen-Image-2.1 page, plus the generate-panel and model-families rows.

Related Issues / Discussions

Part of #9594. Depends on #9787.

QA Instructions

  • Python: the affected suites pass locally, ruff clean; the full suite runs in CI.
  • webv2: pnpm lint passes; the generation suites pass.
  • Docs: the build passes, internal links are valid.
  • Covered on CPU:
    • comfy and Kohya layouts across every block and top-level module, against a meta-device transformer;
    • the exact gate_up split, including DoRA magnitudes and full layers;
    • all prefixes, and alpha;
    • refusals: LoKR/LyCORIS DoRA on gate_up, foreign layers;
    • identification against every other LoRA config, the FLUX.2 Klein 9B cases, and a stripped real LoRA fixture;
    • the denoise path with a real Linear, merged and as a sidecar: the update is applied from the first step and the weight is unchanged afterwards;
    • collection dedupe and refusal messages; the webv2 graph.
  • GPU (RTX 4090):
    • ComfyUI's Turbo LoRA (avg. rank 178, with norm diffs) on the Base model at 8 steps turns the unfinished image into a sharp one on bf16 (merged), GGUF and int8 (sidecars); the sidecar runs land within bf16 rounding of the merged one.
    • A Manga-to-Photo LoRA changes GGUF txt2img and Diffusers edits; the Diffusers weights are bit-identical before and after a LoRA run.
    • Three community 2.1 LoRAs and the Turbo LoRA install as qwen-image-2-1.

Review

All material findings were resolved, with regression tests:

  • LoRAs with foreign or unsplittable layers were applied partially; they are now refused at install time and at conversion;
  • the gate_up split now splits every row-indexed value (low-rank up, DoRA magnitude, full diff), not just the low-rank pair;
  • working memory was reserved for direct patching too; it is now sized only for sidecars;
  • the collection loader could apply a LoRA twice;
  • from GPU testing: the official Turbo LoRA installed as unknown (norm diffs);
  • from the PR1 review: 2.1 LoRAs on only to_out.0 or the MLP halves, and attention-only LoKr, were filed as Qwen-Image LoRAs.

Remaining limitation: a LoRA on only to_q/to_k/to_v of FLUX.2 Klein 9B's first blocks has exactly the keys and shapes of a Qwen-Image-2.1 one and is identified as the latter. This is pinned in a test and noted in the docs.

Compatibility / Rollout

  • Generated API files: openapi.json and schema.ts are regenerated; the changes are additive: two new nodes and a new LoRA config. qwen_image_2_1_denoise moves to 1.2.0 without new fields.
  • Model identification: the new config only claims LoRAs that no other config claimed before.

Checklist

  • The PR has a short but descriptive title, suitable for a changelog
  • Meaningful regression coverage added / updated where needed; obsolete tests/code removed
  • Persisted-state and API changes include required migrations / compatibility validation
  • Relevant performance/efficiency opportunities considered; material claims have evidence
  • Material review findings resolved and relevant checks rerun
  • Documentation added / updated (if applicable)
  • Updated What's New copy (if doing a release after this PR)

@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 python-tests PRs that change python tests docs PRs that change docs labels Oct 10, 2026
@Pfannkuchensack
Pfannkuchensack force-pushed the feat/qwen-image-2-1-lora branch from 57cb054 to 2184ae7 Compare October 10, 2026 23:56
@Pfannkuchensack
Pfannkuchensack marked this pull request as draft October 11, 2026 00:20
@Pfannkuchensack
Pfannkuchensack force-pushed the feat/qwen-image-2-1-lora branch from 2184ae7 to e23bbbc Compare October 11, 2026 00:27
@Pfannkuchensack
Pfannkuchensack marked this pull request as ready for review October 11, 2026 01:01
lstein added a commit that referenced this pull request Oct 16, 2026
)

## Summary

Adds Qwen-Image-2.1 (Base and the 8-step Turbo) as a new
`qwen-image-2-1` base. 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**
- **Formats:** Diffusers folders (`QwenImage21Pipeline`); single-file
transformers in bf16, ComfyUI scaled fp8 or `int8_convrot`; GGUF
transformers; standalone VAEs in Diffusers or ComfyUI layout.
- **Nodes:** model loader, Qwen3-VL 8B text encoder, denoise, i2l and
l2i.
- **Parity:** the denoise and the decode reproduce `QwenImage21Pipeline`
bit 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.
- **Strength:** on Base, img2img and inpaint pick the start step by
strength (step index), as Qwen-Image, Z-Image and diffusers' img2img
pipelines do, so a strength changes the image about as much as on those
models (0.75 starts at sigma ~0.85 at 40 steps). Turbo picks by noise
level (`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.
- **RGBA VAE:** a decode is saved as RGB when at most 0.1% of pixels
have alpha < 224, otherwise as RGBA. Encodes treat the input as opaque.
The VAE, bundled or standalone, runs at the configured precision, except
float16, which is unstable for it.
- **Memory:** encodes and decodes tile up front when the untiled frame
would not fit (1024/768/512 px tiles, via the shared Qwen-Image tiling
helper at 16x). The working-memory estimates cover the KV cache, CFG,
SDPA scores and the GGUF dequant transient, and scale with the VAE and
compute dtypes.
- **Refused at install** (`InvalidMatchError`): nvfp4 files,
torchao-quantized files (such as unsloth's FP8 build; loading them is
#9798), int8 files without the convrot marker, and Q8_CR GGUFs. The
check is shared with Ideogram 4.
- **Identification:** Qwen-Image LoRA configs no longer claim 2.1 LoRAs;
attention-only ones are told apart by their 4096 width. The FLUX GGUF
config now requires `double_blocks`, so the 2.1 GGUF is no longer taken
for FLUX schnell.

**Starter models**
- Diffusers Base and Turbo, the Q4_K_M GGUF and the ComfyUI VAE.
- The Qwen3-VL 8B encoder starter moves to `common.py`, shared with
Ideogram 4. `previous_names` keeps existing installs matched.

**Frontend**
- webv2: base identity, generation policy (VAE and Qwen3-VL 8B encoder
are required for non-Diffusers models), graph builder, canvas mapping
and metadata modes.
- webv1 enums and labels for shared-schema compatibility.

**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

- **Python:** the affected suites pass locally, ruff clean; the full
suite runs in CI.
- **webv2:** `pnpm lint` passes. vitest: 10073 passed; 3 failures are
locale-dependent tests that fail on a German-locale machine, unrelated
to this change.
- **webv1:** `tsc` shows the same 16 errors as on the base commit
`9d30e6a148`; none are new.
- **Docs:** the build passes and all internal links are valid.
- **Parity (RTX 4090):** final latents are bit-identical to
`QwenImage21Pipeline` at 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.
- **Browser (webv2, GGUF Q4_K_M + fp8 encoder + ComfyUI VAE):**
  - txt2img, about 47 s for 40 steps at 1024².
  - CFG 4 with a negative prompt.
- Recall All, which restores model, VAE, encoder, CFG, seed and prompts.
  - Canvas img2img, inpaint and outpaint.
  - A 2048² img2img, where the encode tiles.
- **Real files (RTX 4090, 1024², 40 steps, same seed, against the
Diffusers transformer):** ComfyUI bf16 is bit-identical; `int8_convrot`
37 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:
- the Turbo strength mapping;
- NaN sigmas at one Base step;
- missing encode auto-tiling;
- the dtype-blind VAE estimate;
- install-time refusal of nvfp4 and unmarked int8;
- attention-only LoRA misidentification;
- the webv1 enum.

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_out` or the MLP halves,
and attention-only LoKr, are told apart from Qwen-Image LoRAs only with
#9793.

## Compatibility / Rollout

- **Models already installed:** Qwen-Image-2.1 files installed before
this PR keep their wrong records (FLUX/SD1) until they are removed and
reinstalled. There is no migration.
- **Starter rename:** the 8B encoder starter is renamed "Qwen3-VL 8B
Encoder" and keeps its old name in `previous_names`.
- **Generated API files:** `openapi.json` and `schema.ts` are
regenerated; the changes are additive.

## Checklist

- [x] _The PR has a short but descriptive title, suitable for a
changelog_
- [x] _Meaningful regression coverage added / updated where needed;
obsolete tests/code removed_
- [x] _Persisted-state and API changes include required migrations /
compatibility validation_
- [x] _Relevant performance/efficiency opportunities considered;
material claims have evidence_
- [x] _Material review findings resolved and relevant checks rerun_
- [x] _Documentation added / updated (if applicable)_
- [ ] _Updated `What's New` copy (if doing a release after this PR)_
The prompt encoder reads up to four references through the vision tower; their latents fill the image slots.
The encoder reserves memory for the references' attention; the denoise checks slot grids before loading.
Reference images are offered only on Diffusers models, which keep the vision tower.
Load ComfyUI, diffusers/PEFT and Kohya LoRAs, split ComfyUI's fused gate_up, and merge norm diffs on every format.
Identify 2.1 LoRAs by their gated MLP or 4096-wide attention; refuse any that would apply only in part.
Apply them to the denoise transformer through a collection loader, as sidecars on GGUF, int8 and fp8 weights.
@Pfannkuchensack
Pfannkuchensack force-pushed the feat/qwen-image-2-1-lora branch from e04ef43 to 56fe808 Compare October 19, 2026 04:51

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

backend PRs that change backend files docs PRs that change docs frontend PRs that change frontend files invocations PRs that change invocations python PRs that change python files python-tests PRs that change python tests services PRs that change app services

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant