~/satyajit

Qwen-Image-2.1: the 7B is 28% of the pipeline, and the licence is not Apache-2.0

mdjsonmcp

2026-09-20 · 68 min · image-generation · diffusion · qwen · inference · kv-cache · quantization · benchmarks

Why read this

Essentialtop 10%

Reads Qwen-Image-2.1's weights: an exact 7.1B DiT in a 25.6B pipeline, a prefix cache exact by design, a non-commercial licence, and 4-core CPU runs.

  • Original, source-checked analysis
  • Interactive explanations
  • A guide you can follow today

Image & video generationRuns on a laptop CPUNon-commercial licencePractitioner model

How this was scored
Is it new?
2 of 3: A real new idea, method or capability
Can I trust it?
3 of 3: Reproduces the headline result, or shows from primary files it is wrong
Can I run it?
2 of 3: Open code or weights with real limits
Will I understand it?
3 of 3: Mechanism carried by interactives built from real code or data
Can I act on it?
3 of 3: A decision guide a practitioner can follow today
Will it last?
2 of 3: A reference for a year or more
Does it affect many?
2 of 3: A widely used model, tool or lab release
Only here?
3 of 3: The only place this analysis exists

Score 90 of 100, ranked 3 of 666 rated articles. Each question is answered 0–3 by hand, and a 3 is rare. How articles are scored

Two months ago Qwen shipped Qwen-Image-3.0 as an API and published no architecture, no parameter count and no benchmark table — a capabilities announcement, which is what the piece on it here had to work with. On 20 September 2026 they shipped Qwen-Image-2.1: a lower version number, released after 3.0, with the weights attached.

So this time there are files to read, and I read them rather than the blog. Every number below comes from Qwen/Qwen-Image-2.1 and its ModelScope mirror — config.json per component, the safetensors indexes, the tensor headers pulled over HTTP range reads, and the LICENSE file. They do not all say what the announcement says, and the gaps are worth knowing before you plan around this model.

Two days on, other people have run it, and this piece has grown to take their numbers: a measured 2.55x for the prefix cache, native-resolution RGBA samples the release never published, and the same pipeline run at 3.05 GiB of VRAM and claimed at 2 GB. Where a measurement tests something I derived from the files, I have said which way it went. And since every one of those numbers was taken on a GPU, I ran the thing on four CPU cores with no GPU at all, twice. The first pass, at 8 steps, showed only that it runs. The second used the model's own settings, and at 512 x 512 it gets every word on the board right in about an hour, on 9.90 GB of 4-bit weights, with a gallery beside it.

Qwen/Qwen-Image-2.1@790c926 · snapshot 2026-09-22
announced
7B visual generation component, 32 Single-Stream DiT layers, open-source
measured
7,115,124,736
parameters
7.12B
repo size
47.36 GB
task
text-to-image
library
diffusers
license
other
safetensors
7 shards
largest file
9.97 GB
files
28
downloads
16.2K
likes
1.7K
parameters by dtype
BF167.12B
diffusersqwenimage-generationimage-editingrgba

Measured from the safetensors headers: the denoiser is 7,115,124,736 parameters in 297 tensors, all BF16, with no bias tensors anywhere — the claim holds exactly. The pipeline around it does not: 8,767,123,696 in the Qwen3-VL text encoder, 337,740,404 in the FP32 autoencoder, and a further 9,409,813,744 if you use the prompt rewriter the README recommends. The LICENSE is the Qwen Research License Agreement, non-commercial, dated 20 September 2026 — not Apache-2.0, which every previous open Qwen image model carried.

repo last modified 2026-09-21

qwenlm/qwen-image-2.1@fb7ae1d · snapshot 2026-09-22
tracked files
20
license
custom
branch
main
tests
none found
source
42.2 kB
commit date
2026-09-20
source by language
Python38.6 kB(4)Shell3.6 kB(1)

by size of tracked source at this commit, file counts in brackets; docs, data and vendored trees excluded

The weights and the code ship under different terms: the Hub repo carries the Qwen Research License, while the inference code here is Apache-2.0. Both are needed to run it.

local clone, 2026-09-22 at fb7ae1d — branch, commit, commitDate, fileCount, hasTests, languages, license, licenseFile, shallow

shallow clone: counts describe the pinned tree, not the history

The denoiser stops being an MMDiT

Qwen-Image 1.0 and its December refresh were both QwenImageTransformer2DModel: 60 joint blocks, 24 heads of 128 (3,072 wide), a separate text stream with its own modulation and its own MLP, and a text encoder whose states arrived at joint_attention_dim: 3584. That is an MMDiT, in the MM-DiT-from-SD3 sense, and it weighed 20,430,401,088 parameters.

2.1 is QwenImage21Transformer2DModel, and the tensor names give it away before the config does. There is no txt_mlp, no add_q_proj, no to_add_out — no second stream at all. Text is projected once by txt_in into the same 4,096-wide space, condition-image latents are substituted into the sequence at the slots the vision-language encoder reserved for them, the target image's tokens are appended, and one stack of 32 blocks runs over the lot.

Qwen-Image 1.0 / 2512Qwen-Image-2.1
denoiser classQwenImageTransformer2DModelQwenImage21Transformer2DModel
blocks60, dual-stream32, single-stream
width24 x 128 = 3,07232 x 128 = 4,096
mlp_ratio4 (implicit)3
modulationper blockone shared projection
patch_size21
denoiser params20,430,401,0887,115,124,736
text encoderQwen2.5-VL-7BQwen3-VL-8B
VAEz_dim 16, 8x spatial, RGBz_dim 64, 16x spatial, RGBA
licenceApache-2.0Qwen Research (non-commercial)

Two of those rows do most of the shrinking, and neither touches depth or width. mlp_ratio: 3 is unusual — nearly every transformer since 2017 uses 4 — and the modulation is a single top-level Linear(4096 → 16384) that every block slices its scales and gates out of, rather than 32 copies of it. The diffusers port says so in as many words: "blocks hold no modulation parameters of their own." There are no bias tensors, and the final AdaLN emits scale only, no shift.

transformer/ — 297 tensors, all BF16, 7,115,124,736 parameters
img_mlp x32
67.9%
attn x32
30.2%
modulation
0.9%
txt_in
0.5%
norm_out + time_embed
0.5%
img_in + proj_out + norms
0.0%
  • img_mlp x32 — 4,831,838,208 params. SwiGLU, 4,096 <-> 12,288, three matrices per block
  • attn x32 — 2,147,483,648 params. q/k/v/o, one shared stream over text + images
  • modulation — 67,108,864 params. one Linear(4,096 -> 16,384) for every block: scale and gate, twice, no shift
  • txt_in — 33,558,528 params. RMSNorm + GELU MLP lifting Qwen3-VL's 4,096-wide states into the stream
  • norm_out + time_embed — 34,603,008 params. scale-only final AdaLN, sinusoidal timestep projection
  • img_in + proj_out + norms — 532,480 params. 64-channel latents in and out, per-block q/k norms

the same 32 x 4,096 stack, built the ordinary way

as shipped
7.12B
+ modulation per block, not shared
+2.08B
+ mlp_ratio 4, not 3
+1.61B
would weigh
10.81B

Two config lines — mlp_ratio: 3 and a single shared modulation projection — take 3.69B off the denoiser, which is 34.2% of what the conventional arrangement would have cost. Depth and width are unchanged.

what the “7B” sits inside

transformer
7.12B
text_encoder
8.77B
vae
0.34B
PE rewriter
9.41B

25,629,802,580 parameters end to end on the path the README recommends. The denoiser everybody quotes is 27.8% of it. Drop the optional prompt rewriter and the pipeline you still have to download is 16.22B.

The 7B claim is exact. The framing around it is the part to watch: the README's "just 7B parameters in its visual generation component" is true of the component and not of the thing you download, which is 33.1 GB of BF16 and FP32 weights before you add the rewriter. Some of that is dead: the Qwen3-VL text encoder ships its 622,329,856-parameter lm_head, tie_word_embeddings is false, and the pipeline never generates a token. vLLM-Omni's recipe notes the same thing from the other direction — it excludes "its vision tower and unused lm_head" when quantising.

Block-causal attention, and why the cache is exact

The interesting engineering is in how the sequence is masked. The release calls it "mixed-granularity attention"; the implementation is one line:

# diffusers/models/transformers/transformer_qwenimage21.py
allowed = ((q_idx >= kv_idx) | same_image_block) & key_valid[batch_idx, kv_idx]

The joint sequence is causal, the way a language model is causal — except that every image block, each reference image and the target, is internally bidirectional. Text tokens get token-level granularity; image tokens get chunk-level. The release's own figure is the mask itself, which is the right way to draw it:

An attention mask drawn as a grid of query rows against key columns, divided into four segments — system prefix, optional input image, edit instruction, target image. The text segments form causal staircases; the image segments are solid blocks, bidirectional within themselves. A bracket labels the first three segments 'Compute once, Reuse K/V' and the target image segment 'Compute each step'.
The mixed-granularity mask: causal across the sequence, bidirectional inside each image block (Qwen-Image-2.1 release, architecture figure).

Causality alone would not buy anything during sampling, because a diffusion step changes every token's activations. What makes the prefix reusable is a second switch, causal_condition: true, and it is the detail worth carrying away from this release. Text and condition-image tokens are modulated from t = 0 instead of from the sampled timestep — the code builds a modulation tensor with batch_size + 1 rows and routes the prefix to the extra one. Their activations therefore do not depend on the step, so their K and V do not either, and caching them is exact rather than approximate. The library refuses to let you get this wrong:

kv_cache requires `causal_condition=True`. The cache is only valid because text and
condition-image tokens modulate from t=0, which makes their activations independent of
the denoising step.

Whether that is worth anything depends entirely on how much prefix there is, and the arithmetic is simple enough to do exactly. patch_size is 1 and the VAE compresses 16x, so an image is exactly (H/16) × (W/16) tokens: a 1024px reference costs 4,096 of them, a 2048px target costs 16,384.

At four references and a 1024px target that puts 19.8% of the sequence in the recomputed part, against the "about a fifth" the vLLM-Omni recipe reports for the same configuration — which is the cross-check I wanted, because it means the model matches the serving stack's behaviour rather than my reading of the code. Drag the references to zero and the saving collapses to nothing, which is also what the recipe says: "plain short-prompt text-to-image has almost nothing to cache." This is an editing optimisation that a text-to-image benchmark cannot see.

The cache, as three other people measured it

First the mechanics, from diffusers rather than from the announcement, because the interesting part is that nothing here is trained to be cacheable. On the first denoising step every layer projects Q, K and V for the whole joint sequence and keeps only the prefix half — post-RoPE, shaped (batch, prefix_tokens, heads, head_dim), one pair per layer. On every later step the model cuts the sequence down before the blocks run at all:

# diffusers/models/transformers/transformer_qwenimage21.py
if kv_cache_mode == "cached":
    joint_hidden_states = joint_hidden_states[:, prefix_len:]
    rotary_emb = rotary_emb[prefix_len:]
    modulation_mask = modulation_mask[prefix_len:]

so the target positions are the only ones projected at all, and each attention layer puts the prefix back with torch.cat([cached_k, key], dim=1). Target queries attend over [fixed prefix + current target]. use_kv_cache defaults to True in the pipeline; with true_cfg_scale > 1 there are two caches, one per branch, so guidance doubles the cache as well as the work.

One line in there is worth stealing on its own. The cache is written with .clone(), not contiguous(), and the comment says why: at batch size 1 the prefix slice already counts as contiguous, so contiguous() hands back the same view and the cache pins the entire prefill K and V for every step of the loop. On the configuration below that is 6.13 GiB retained instead of 4.13 GiB. One word, two gigabytes.

measured speedup vs. what the mask arithmetic predicts

A100, warmed, 2 references, 1024px, 40 steps — DiT time only

predicted
2.62x
measured
2.55x

50.57s → 19.86s. The measurement is 97.3% of the arithmetic bound.

RTX 5070 Ti, 1 reference, 512px, 4 steps — whole job

predicted, DiT
1.69x
measured, job
1.30x

9.82s → 7.57s, end to end. The text encoder and the VAE are outside the cache’s reach, so the two numbers measure different things; reconciling them puts the DiT at 56% of that job.

RTX 3090, ComfyUI INT8, 1024px, 40 steps — no before/after, so read it as an exclusion

jobobservedif cachedif nottext to image1.00x1.00x1.00xone-reference edit0.99x1.16x2.18xtwo-reference edit1.19x1.32x3.45x

Each column is that job’s cost relative to plain text-to-image on the same server. The runs were sequential and differ in cache warmth, so the observed column is an acceptance record rather than a benchmark — but adding a second reference costs 1.19x, and without the cache it could not have been cheaper than 3.45x.

effective throughput, prefix recomputed182 TFLOPS (58% of peak)effective throughput, prefix cached177 TFLOPS (57% of peak)
K and V per prefix token, all 32 layers, bf16524,288 bytes = 0.5 MiBprefix of the A100 run (8,448 tokens)4.13 GiBmeasured memory increase3.7 G = 90% of it
The timings are the three sources’, quoted. The predictions are derived from the shipped config the same way PrefixCache derives its token shares: exact block-causal pair counts, 13d² linear MACs per token per layer, one full prefill step and 39 cached ones, ignoring normalisation, the VAE and the text encoder. A prompt is assumed at 256 tokens and a reference at its stated resolution, so a different prompt length moves the predictions by well under a percent.

2.55x is not a suspicious number. It is very close to the only number this architecture allows. Two 1024px references and a 1024px target make a 12,544-token sequence of which the target is 32.7%; one full step plus 39 cached ones works out to 2.62x fewer multiply-accumulates, and the measurement lands at 97% of that. The cleaner way to see it is to divide back out: the recomputing run does 9.18 PFLOP in 50.57 s and the cached run does 3.51 PFLOP in 19.86 s, which is 182 and 177 TFLOPS — 58% and 57% of the A100's BF16 peak. The cache does not make the machine more efficient. It gives it less to do, and the saving arrives where the mask says it should.

The memory goes the other way, and the arithmetic is just as tight. K and V for one prefix token, across all 32 layers in bf16, is exactly 2 * 4096 * 2 * 32 bytes — half a mebibyte, a number worth memorising. An 8,448-token prefix is therefore 4.13 GiB of cache, and the measured peak rose by 3.7 G. That is 90% of the arithmetic, and the gap is the allocator reusing what the shorter sequence stopped needing.

Two caveats, because 2.55x is the best case in the set. It is DiT time on a warmed A100, not a job: the second measurement in the ledger is end-to-end on an RTX 5070 Ti and comes out at 1.30x, because the text encoder and the VAE are outside the cache's reach and four steps give it only three to work with. And at zero references the widget above still says 1.05x. None of this touches plain short-prompt text-to-image, which remains the case the published benchmark measures.

The alpha channel is real; the published evidence is 684 pixels wide

The feature that actually distinguishes this checkpoint is in vae/config.json, one line from the top: in_channels: 4, out_channels: 4. The autoencoder takes and returns RGBA. z_dim is 64, scale_factor_spatial is 16, decoder_base_dim is 144 against a base_dim of 96 — so the decoder carries 259,044,052 of the VAE's 337,740,404 parameters and the encoder only 78,675,680, a 3.3:1 split. It is stored in FP32, alone among the three components, which is why 337M parameters occupy 1.35 GB.

This folds in the December 2025 Qwen-Image-Layered model as a capability of the base checkpoint rather than a separate download, and the prompt format is a blunt instrument: you ask for it in English, in the prompt, with a sentence the model card spells out — This is an RGBA image with transparency. … The image has alpha channel and the background is transparent.

A cartoon dragon in Chinese new-year dress riding a lion-dance lion, holding a lantern and a firecracker, with no background. The empty area is drawn as a pale grey and white checkerboard, showing that those pixels are transparent rather than white.
A published RGBA sample, composited here onto a checkerboard rather than white so the alpha channel survives the page (Qwen-Image-2.1 release, transparency showcase).

I measured that file's alpha channel rather than taking the claim. All 256 levels are present, so it is not a one-bit matte — but the histogram is sharply bimodal: 53.1% of pixels fully transparent, 23.5% fully opaque, 22.6% within 31 levels of one of those two extremes, and 0.8% of the image anywhere between alpha 32 and 223. That is a clean cutout with an anti-aliased fringe, not translucency. It is also a 684 x 685 web asset, well under the 2048px the card gives as the default generation size, and downsampling manufactures fringe alpha on its own — so even the fringe is not safely attributable to the model. The release publishes no native-resolution RGBA sample, which means the one claim you most want to check from the artifacts is the one you cannot.

Two days later you can. Someone validating a local installation generated the transparency workflow twice at 1024 x 1024 — once through the official INT8 weights and once through a 4-bit GGUF conversion of the same denoiser — and committed both PNGs. I ran the same histogram over them.

where the alpha channel actually sits
release showcasemidrange 0.80%

684 x 685, published RGBA web asset, downscaled by someone before publication

INT8 ConvRotmidrange 0.55%

1024 x 1024 native, official Comfy-Org INT8 weights, 40 steps

GGUF Q4_K_Mmidrange 0.48%

1024 x 1024 native, community 4-bit conversion of the same denoiser, 40 steps

  • alpha 0
  • 1-31, 224-254
  • 32-223
  • alpha 255
INT8 ConvRot — within 7 levels of an extreme99.10%GGUF Q4_K_M — within 7 levels of an extreme99.15%
A matting model puts mass in the middle: hair, glass, smoke, a soft shadow. All three of these put essentially none there. The two native files are not mine either — they come from someone else’s installation validation, generated on an RTX 3090 through two different quantisations of the same denoiser, which is why they are worth more than one more sample from one more pipeline.

The midrange share at native resolution is 0.55% and 0.48%, against 0.8% in the downscaled release asset. It went down, which is the direction the resampling argument predicted, and it stayed down under 4-bit quantisation. Put the other way: in both native files, 99.1% of pixels sit within seven levels of fully transparent or fully opaque. This is a matte generator. It is a good one — the edges are clean and the shape is right — but if you are planning around glass, smoke or a soft shadow, plan on compositing them yourself.

There is a second thing in those files that the model card does not mention, and it will cost somebody an afternoon:

Three panels of the same 1024 by 1024 generated teapot. Left: the RGBA file composited onto a grey and white checkerboard, showing a red teapot on transparency. Middle: the alpha channel alone, an almost binary white silhouette on black with no grey. Right: the RGB channels with the alpha discarded, showing the same teapot on a saturated purple field with faint horizontal banding along the bottom.
The same file three ways. The alpha channel is a matte, not a gradient; the RGB underneath the transparent region is a decoded purple field, not blank (generated by a third party's installation validation on an RTX 3090 through the official INT8 weights; the checkerboard and the panel layout are mine).

The transparent pixels are not empty. The VAE decodes RGB there too — a saturated purple, mean (153, 56, 195) over the pixels below alpha 8, with visible horizontal banding near the bottom edge. Anything that reads the PNG without asking for the alpha channel gets a purple rectangle with a teapot on it: cv2.imread without IMREAD_UNCHANGED, a JPEG export, a downstream model fed the file directly. The guide that shipped these samples warns about it in one line; the model card does not.

The licence is the news

Every open Qwen image model before this one is Apache-2.0. I checked all of them on the Hub:

RepoReleasedLicence
Qwen/Qwen-ImageAug 2025Apache-2.0
Qwen/Qwen-Image-EditAug 2025Apache-2.0
Qwen/Qwen-Image-Edit-2509Sep 2025Apache-2.0
Qwen/Qwen-Image-Edit-2511Dec 2025Apache-2.0
Qwen/Qwen-Image-LayeredDec 2025Apache-2.0
Qwen/Qwen-Image-2512Dec 2025Apache-2.0
Qwen/Qwen-Image-BenchMay 2026Apache-2.0
Qwen/Qwen-Image-2.1Sep 2026Qwen Research License

The LICENSE file is dated 20 September 2026 and says what it says:

You are granted a non-exclusive, worldwide, non-transferable and royalty-free limited license … to use, reproduce, distribute, copy, create derivative works of, and make modifications to the Materials FOR NON-COMMERCIAL PURPOSES ONLY.

with "Non-Commercial" defined in clause 1(i) as "for research or evaluation purposes only", and commercial use routed to an email address. Both prompt-rewriting checkpoints carry the same terms. So does the VAE — there is no separate licence for it, which matters because autoencoders get lifted out of releases and reused more often than denoisers do, and this is the first 16x RGBA one worth lifting.

This is not a use-restriction licence in the RAIL sense; there is no acceptable-use annex and no list of forbidden applications. It is a plain commercial restriction, with Chinese governing law and exclusive jurisdiction in the Hangzhou courts. "Open-source" is the word the blog, the README and the model card all use. The OSI definition has said no field-of-endeavour restrictions since 1998, and this is one.

The downstream is already inconsistent about it, within hours of release. Of the two FlagOS repackages that Qwen's own README links, FlagRelease/Qwen-Image-2.1-BF16-zhenwu-FlagOS is labelled apache-2.0 on ModelScope, which is not a licence that repo is in a position to grant. Among the community GGUF conversions that appeared the same day, two carry qwen-research and one carries no licence field at all. None of that is Qwen's doing, and all of it is the predictable consequence of shipping a research licence into a family people have spent a year treating as Apache.

Two days on, the picture is better than that paragraph suggests, and the correction belongs here rather than in a footnote. Every redistribution people are actually installing from carries the right terms. The official ComfyUI repack (Comfy-Org/Qwen-Image-2.1) declares license_name: qwen-research and links back to Qwen's own LICENSE file, which is the most correct form any of them takes. Both abenzerps GGUF conversions declare qwen-research with base_model set, missing only the link. The ncnn conversion (nihui-szyl/qwen-image-ncnn) ships the Qwen Research License verbatim, says in the card that the files are "for research and evaluation purposes only", that commercial use needs a separate licence, and that the inference tool's own Apache-2.0 "does not change the licensing terms of the Qwen-Image-2.1 model files" — a distinction most repackagers do not bother to draw. Its one gap is metadata rather than terms: the card carries no license: field, so the Hub shows the repository with no licence tag and it will not appear in a licence filter. The installers are cleaner still, because they redistribute nothing: MIT scripts that download the official weights by pinned revision and verify them by SHA-256. The FlagOS apache-2.0 label remains the outlier, not the pattern.

Three more repositories belong in that audit, because the CPU runs further down use or measure them. Unsloth's two (unsloth/Qwen-Image-2.1-GGUF and unsloth/Qwen-Image-2.1-FP8) declare license_name: qwen-research, link license_link to Qwen's own LICENSE and set base_model — the same, most complete form as the ComfyUI repack. leejet's leejet/Qwen-Image-2.1-GGUF, whose files produced every CPU image in this article, says in its README body that the quantised files follow the Qwen Research License and links it, but carries no license: field, so the Hub shows it with no licence tag — the ncnn repository's gap exactly, terms right and metadata missing.

And one finding that cuts the other way. The text encoder inside Qwen/Qwen-Image-2.1 is, as far as I can check without downloading 17.5 GB twice, stock Qwen/Qwen3-VL-8B-Instruct: all 750 tensors match it in name, shape, dtype and byte size, 17,534,247,392 bytes on both sides, and the six tensors I sampled — first and last language layers, a middle MLP, a vision block, the embeddings and lm_head — are byte-identical over their first 4 MB. That model is Qwen's own Apache-2.0 release. So the research licence on this directory covers the denoiser and the VAE in any sense that matters: 8.77B of the pipeline's 16.2B parameters are obtainable from Qwen under Apache-2.0 already, and the GGUF encoder the CPU runs below use is Qwen's own Apache-2.0 conversion of it, Qwen/Qwen3-VL-8B-Instruct-GGUF.

What the one published eval measures

There is no technical report. arXiv has no Qwen-Image-2.1 paper; the most recent Qwen image paper is the benchmark's, from May. The GitHub repo is a README, a LICENSE, and a prompt_rewrite/ directory — no inference code of its own, no training code, no evaluation harness. The entire quantitative record of this release is one chart.

A bar chart of 29 image models ranked by a single total score from 67.01 down to 46.17, with Qwen Image 2.1 highlighted in green at 60.28 in seventh place. Below each bar, a second row shows parameter counts: open models are labelled with sizes from 6B to 80B, while closed models show a padlock icon on a full-height bar and a footnote that closed models' parameter counts are not disclosed.
The only quantitative claim in the release: one aggregate score across 29 models, on Qwen's own benchmark (Qwen-Image-2.1 release, Qwen-Image-Bench evaluation comparison).

Name the denominators, because the chart does not. Qwen-Image-Bench is 1,000 expert-written bilingual prompts, scored across a three-level rubric — 5 pillars, 23 sub-capabilities, 56 verifiable facets. Each facet is judged Fail, Pass or Excel, mapped to 0, 60 and 100, N/A excluded, and averaged up the tree. The judge is Q-Judger, a Qwen3.6-27B fine-tune trained on 130,000+ annotated pairs from 80 art-academy annotators, run at temperature 0, seed 42, thinking on.

So: Qwen's model, on Qwen's prompts, scored by Qwen's judge, against competitors Qwen ran itself. That is not disqualifying — the benchmark, the judge and the dataset are all published under Apache-2.0, which is more than most vendors do, and you can rerun it. It is also not third-party, and the chart does not say so.

Three things the chart leaves out that its own sources state:

  1. The leaderboard it reuses is the Chinese-prompt run. Eighteen of the 29 bars match the benchmark's published leaderboard to the hundredth, and that leaderboard carries a footnote: "results are computed based on Q-Judger's evaluation of images generated from Chinese prompts. We will release the results for image generation from English prompts soon." The chart inherits the denominator and does not mention it.
  2. The human-agreement figure is a model-level rank correlation. Spearman ρ=0.92\rho = 0.92 is impressive and it is computed over N=18N = 18 models, not over images. It says the judge orders a field the way experts do. It does not say the judge is right about any particular picture, and it says nothing at all about whether a 0.46-point gap between two adjacent bars is a real difference.
  3. It is a text-to-image benchmark. Transparency, ten-reference composition, circle- and mask-guided local editing, identity preservation — everything this release actually added — is measured by none of it. The chart scores the one capability 2.1 did not change.

The rubric is worth putting the totals back onto, because 60 is not an arbitrary number on that scale. It is the score of a model that passes every facet and excels at none.

That gives the chart a reading it does not advertise. Qwen-Image-2.1 at 60.28 is the lowest bar on it that could, in principle, have zero Fails — everything below is carrying a floor on how often the judge said the image failed outright, up to 23.1% for the last bar. Against its own line the improvement is real and large: 49.23 for Qwen-Image 1.0, 52.06 for 2512, 60.28 here, at roughly a third of 1.0's denoiser size. Against the field it is seventh of 29, below Qwen's own Qwen Image 3 Pro at 62.36, and the six models above it are all closed.

What it costs to run, from someone other than the vendor

The release makes an efficiency argument — "compact", "lightweight", "low computational cost" — and publishes no latency, no throughput and no memory figure to support it. On the day it landed the only measured numbers anywhere were third-party, in the vLLM-Omni recipe, taken on one NVIDIA GB300 at 1024 x 1024 and 40 steps:

ConfigPeak memoryTime per image
BF1634.0 GB3,282-4,492 ms
DiT FP8, img_mlp kept BF1632.0-32.7 GB3,356-4,706 ms
Text-encoder FP827.5-28.1 GB3,427-4,770 ms

Two things follow. 34 GB peak means a "7B" model does not fit a 24 GB card without offloading — the pipeline is 33.1 GB of weights before activations, of which the denoiser is 14.2 GB. And FP8 on Blackwell buys memory, not speed: none of the quantised rows is faster, which is what you expect when the bottleneck is bandwidth rather than tensor cores. Qwen ships no quantised export of its own; every FP8, W8A8 and GGUF build is a partner's or the community's.

Down the ladder: 33.1 GB, then 3.05 GiB, then 2 GB

Two days of community work produced two much smaller numbers than that table, and they are smaller in completely different ways.

The ladder carries three more kinds of row than it did. Unsloth's GGUF and FP8 repositories are there with byte counts read from the Hub; the FP8 row is a GPU route. Unsloth's guide states 11 GB of VRAM for the GGUF route, 24 GB for FP8 or INT8 (6 GB with offloading, at what it says is less than twice the time), and 12–16 GB of RAM for CPU-only, and says of all of them that "memory figures are estimates, not tested minimums" — which is why the host-memory view draws them dashed. The last two rows are this article's own runs on a four-core CPU, measured further down: 9.86 and 11.40 GiB of peak RSS at 512², under the 15.21 GiB the 3090's tightest profile needed on the host, and on the time view bars that dwarf every GPU row, which is the honest shape of doing this without one.

The 3.05 GiB configuration is the ComfyUI GGUF route, and it trades four separate things, all of them stated. Precision first: the denoiser goes from bf16 to Q4_K_M, which is 5.18 bits per parameter and takes 14.23 GB to 4.60 GB; the text encoder goes to an INT8 ConvRot export at 8.53 bits including scales, 17.53 GB to 9.35 GB; the VAE is cast from fp32 to bf16, 1.35 GB to 0.68 GB. The download drops from 33.1 GB to 14.63 GB. Second, the prompt rewriter is gone — 9.41B parameters and 18.82 GB of the path Qwen's README recommends — and the validation report says plainly that no prompt-enhancer integration was tested. A distilled 0.75B replacement for that rewriter has since appeared, and the piece on it here checks what it keeps. Third, placement: the text encoder runs on the CPU, the VAE decodes on the CPU, and the server log shows 2,186 MiB of denoiser weights on the GPU against 2,301 MiB offloaded, so less than half the 4-bit model is resident and the rest streams in per step.

Fourth, and this is the number that belongs next to the footprint: time. At 512 x 512 and 40 steps the same package goes 111.75 s to 195.28 s as the budget tightens from 5.55 GiB to 3.05 GiB. At 1024 x 1024 it goes 60.35 s to 145.90 s for 15.39 GiB down to 6.14 GiB. And the memory does not disappear, it relocates: process RSS in the tightest profile is 15.21 GiB, against 2.4 GiB in the ordinary ones. You are buying VRAM with system RAM and wall-clock: at 1024 that is 2.4x the time for 40% of the memory.

The 2 GB claim is a different animal, and the arithmetic is the interesting part. It comes from an ncnn/Vulkan port — a portable executable, no CUDA, no PyTorch, no Python, running on NVIDIA, AMD, Intel and Apple Silicon — and unlike the GGUF route it claims full bf16 with no quantisation. That claim checks out, from eight bytes. The first four bytes of each shipped .bin are 83 8b 34 01, and ncnn/src/modelbin.cpp reads tag 0x01348B83 as bfloat16; all three components carry it. The denoiser reconciles to the release exactly: 14,230,275,488 bytes against the safetensors index's 14,230,249,472, 26 KB apart on 14.2 GB, which is ncnn's own per-layer headers.

What is missing from the conversion is the part this article already flagged as dead. The whole package is 31,191,738,224 bytes, which is 1.92 GB under the 33.1 GB release, and the shortfall is two things and only two: the 622,329,856-parameter lm_head that the pipeline never generates a token with (1.24 GB), and the VAE halved from fp32 (0.68 GB). Subtract both and you land within 3.6 MB — 0.01% — of the shipped total. Somebody else read the same headers and drew the same conclusion, and then acted on it.

So where does 2 GB come from? Not from a smaller model. The project's own requirements section asks for 16 GB of RAM on Linux and macOS, and on Windows for (half of system RAM) + (GPU memory) >= 16GB, because WDDM lets a Vulkan application map only half the host's RAM. 2 GB is the GPU's share of a 16 GB budget, and that 16 GB is not arbitrary either: the denoiser, the vision tower and the VAE are 14.23 + 1.16 + 0.66 = 16.05 GB, which is the sampling stage once the 15.14 GB text encoder has run and been released. The weights live in host memory and the GPU reads them as it goes.

What that costs is not published. One release, dated 22 September, no benchmark table, sample images "TBA". The floor is easy to compute and worth stating: if only about 2 GB of a 14.23 GB denoiser is resident, each of 40 steps drags most of it across the host link, so on PCIe 4.0 x16 at a realistic 25 GB/s that is about 0.57 s per step and 23 s per image of pure transfer before any arithmetic happens. On unified memory — Apple Silicon, an integrated GPU — there is no link to cross at all, which is presumably why the target is Vulkan and not CUDA. A memory figure with no latency figure next to it is a claim about where the weights sit, not about how fast they run.

One last thing puts the two halves of this article on the same axis. The prefix cache buys its 2.55x by spending memory: half a mebibyte per prefix token, 4.13 GiB at two 1024px references and 8.13 GiB at four. That is more than the entire GPU budget of the 3.05 GiB profile. The configuration that makes this model fit a small card and the optimisation that makes multi-reference editing fast are the same trade run in opposite directions, and on one card you do not get both.

Running it on four CPU cores, with no GPU at all

Every figure in the section above was taken on a GPU — a GB300, an A100, a 3090 told to pretend it was smaller — and the two most-quoted numbers are both claims about where the weights sit rather than about what it takes to have them. 3.05 GiB is a placement hint on a 24 GiB card. 2 GB is the GPU's share of a 16 GB budget the host has to supply. Neither answers the question a reader without a GPU actually has, which is whether a GPU is the floor at all.

So I ran it on the box that builds this site: four cores of an Intel Xeon at 2.80GHz, 15 GB of RAM, no /dev/dri, no CUDA, no Vulkan, no accelerator of any kind. It generates images. I did it twice. The first pass was a smoke test — 8 sampling steps, a fifth of what the vendor uses — and its pictures were fair evidence that the model runs and unfair evidence of what it draws; the site owner looked at them and said as much. The second pass uses the model's own settings, and pulls apart the three things the first pass had tangled together: the step budget, the noise schedule and the quantisation. The second pass comes first here, because it is the one that describes the model.

The files

The runner is stable-diffusion.cpp at commit 2bb7294, built with -DGGML_NATIVE=ON against AVX-512F/BW/DQ/VL and AVX512-VNNI — the project whose author also published the Qwen-Image-2.1 GGUFs. Every weight file was fetched at a pinned revision, and every one matches the SHA-256 the Hub records for it:

ComponentRepo · fileBytesBits/param
Denoiser, pass 2leejet/Qwen-Image-2.1-GGUF · qwen_image_2.1-Q4_K.gguf4,197,494,8164.72
Denoiser, both passesleejet/Qwen-Image-2.1-GGUF · qwen_image_2.1-Q2_K.gguf2,561,716,2562.88
Denoiser, comparisonunsloth/Qwen-Image-2.1-GGUF · qwen-image-2.1-Q2_K.gguf2,466,137,8242.77
Text encoderQwen/Qwen3-VL-8B-Instruct-GGUF · Qwen3VL-8B-Instruct-Q4_K_M.gguf5,027,784,8004.59
VAEComfy-Org/Qwen-Image-2.1 · vae/qwen_image_2.1_vae_bf16.safetensors675,509,68816.00

Bits per parameter are read from each GGUF's own tensor table, not from the file name: every one of these denoisers holds exactly 7,115,124,736 parameters, the same count the safetensors headers gave at the top of this article. The Q4_K pipeline is 9,900,789,304 bytes, the Q2_K one 8,265,010,744 — against 14.63 GB for the 3090 profile and 33.1 GB for the path the README recommends. Note which component moved. The denoiser is the 7B everybody talks about, and at Q2_K it is the smallest file in the pipeline: the 4-bit text encoder is 1.96x its size. That is this article's opening ratio surviving all the way down the quantisation ladder.

On the first pass disk was the binding constraint — 10.93 GB free, which is why it went straight to Q2_K. On the second it was not, and something else was.

What "the model's own settings" are

Three sources, and they do not all say the same thing.

So the vendor and the sd.cpp example disagree about the one setting that doubles the cost. The vendor runs one denoiser pass per step; the sd.cpp example runs two, at a guidance scale the vendor's pipeline never applies by default. My first pass borrowed guidance from the second camp for one of its runs, and that is the run whose exposure collapsed.

The schedule disagrees too, and nobody's guide mentions it. For this model sd.cpp picks its generic Flux schedule — base_shift 0.5, max_shift 1.15, anchored at 256 and 4,096 tokens, no terminal stretch — which I read out of src/runtime/denoiser.hpp and src/pipeline/request.cpp at the commit I built. At 512 x 512 that gives a shift of 0.630 against the vendor's 0.539. The bigger difference is at the far end. The vendor stretches the schedule so the last non-zero noise level is 0.02; sd.cpp's default ends at 0.211 at 8 steps, 0.090 at 20 and 0.046 at 40, and then jumps to zero in one Euler step. At 8 steps the final step is asked to remove a fifth of the full noise level in one move, and I expected that to be why the first pass looked unfinished. It was not, and the run that shows it is below.

Either way, for the second pass I did not trust "automatic". Every run hands sd.cpp the vendor's exact sigma schedule through --sigmas, computed from the model's own scheduler_config.json with the formula diffusers uses, and the schedules are in the data file beside the images.

Memory: a ceiling I did not know was there

The first attempt at the second pass died. Q4_K at 512 x 512 ran two sampling steps and was killed during the VAE decode with signal 9, and the kernel log is specific about why: not the machine's 15.7 GB but a memory cgroup. The sandbox these commands run in caps its shell at 14,345,842,688 bytes — 13.36 GiB — and in a cgroup the page cache counts. Without memory mapping, sd.cpp reads each weight file into the page cache and then copies it into its own buffers, so 9.90 GB of weights were charged twice until reclaim caught up. The process was killed at 10.27 GiB of anonymous memory, having spent 413 seconds of system time — against 789 of user time — fighting the limit; the cgroup had hit it 8,373,181 times since the container booted 18 minutes earlier. Its two sampling steps took 119.76 s and 115.78 s.

--mmap fixes it. The weights are mapped straight from the GGUF files, charged once, and evictable, so pages nothing is touching — the text encoder's, once conditioning is done — can be dropped rather than held in a private copy. The same 2-step run then finished: 89.39 s and 90.28 s per step, 9.9 s of system time, the cgroup never at its limit, and a peak of 12.08 GiB against the 13.36 GiB cap (sampled every half second). Every second-pass run uses it. The lesson travels: inside a container, a memory figure is a figure for charged memory, and whether the runner reads or maps its weights moves it by roughly the size of the weights.

With it, peak RSS was 11.40 GiB for the Q4_K runs and 9.86 GiB for Q2_K at 512², counting the mapped weights, with a sampled cgroup peak of 13.34 GiB — page cache filling the space it is allowed, not the process needing it. sd.cpp's own compute buffers at 512² are small beside the weights and lopsided among themselves: 7.18 MB for the text encoder, 234.03 MB for the denoiser and 2,450.50 MB for the VAE, the one component that ever materialises anything at full pixel resolution.

At the vendor's own settings

A 512-pixel-square generated image of a classroom chalkboard in a wooden frame. Four lines of clean white lettering read '4 CPU', 'cores,', 'no', 'GPU', every word and the comma correct. Warm light comes through a window on the left, where a small potted plant sits on the sill beside the board; wooden school desks fill the foreground.
Generated by me on four CPU cores with no GPU: 512 x 512, 40 Euler steps on the vendor's own sigma schedule, cfg 1.0 (guidance off, as the diffusers pipeline defaults), seed 42, leejet's Q4_K denoiser (4.72 bits per parameter) and Qwen's Q4_K_M Qwen3-VL encoder. 3,863 seconds wall-clock. A quarter of the pixels of the pipeline's 1024 px default and a sixteenth of the model card's 2048 px, on 4-bit weights, so still not the full-quality model — but the first image here that uses its recommended settings.

Same prompt, same seed, same box. Every word on the board is right, the comma included; the plant is beside it on the windowsill, the light is warm and the frame is properly exposed. If anything the lettering is too clean — it reads as a typeface with a chalk texture rather than as handwriting — but it is exactly the text that was asked for, which is the capability this release is built around and the one its only published benchmark measures. It took 64 minutes and 23 seconds of wall-clock on a box other jobs were sharing — about 59 on a quiet one, from the timings further down.

That one image retires two conclusions the first pass drew. Text rendering did not need guidance: this is guidance off, which is what the vendor's pipeline does by default. And the dark, underexposed frame the first pass got with guidance on was not something to trade against legibility; at a fair step budget, with no guidance at all, neither problem appears. What the first pass had actually measured, as the next runs show, was 8 steps.

Steps, schedule and bits, one at a time

The first pass differed from the vendor in three ways at once — 8 steps instead of 40, sd.cpp's schedule instead of the vendor's, 2.88-bit weights — so its pictures could not say which one mattered. These runs change one thing at a time. All of them are 512 x 512, seed 42, guidance off, the same prompt, generated on this box's four CPU cores.

A dark chalkboard filling the frame with four lines of smudged, unreadable chalk marks and a potted plant on a wooden ledge at the lower right.
Baseline, from the first pass: leejet Q2_K, 8 steps, sd.cpp's default schedule, cfg 1.0, 512², seed 42. Generated on CPU. Unreadable.
Almost the same image as the baseline: a dark chalkboard with four smudged, unreadable lines and a plant at the lower right, slightly darker overall.
Change the schedule only: leejet Q2_K, 8 steps, the vendor's sigmas, cfg 1.0, 512², seed 42. Generated on CPU. Still unreadable.
The same chalkboard composition, now with four lines of clean white lettering reading '4 CPU', 'cores,', 'no', 'GPU', a potted plant on the ledge at the lower right and a blotchy white smear on the left edge of the board.
Then the steps only: leejet Q2_K, 20 steps, vendor sigmas, cfg 1.0, 512², seed 42. Generated on CPU. Every word right, at 2.88 bits per parameter.
A different composition: a framed chalkboard lit by a window on the left, four lines reading '4 CPU', 'cores,', 'no', 'GPU', a plant on the sill and wooden desks in the foreground.
Then the bits only: leejet Q4_K, 20 steps, vendor sigmas, cfg 1.0, 512², seed 42. Generated on CPU. Every word right; a different picture.

Read left to right and down, and the confound comes apart:

ChangedFromToThe words on the board
Schedulesd.cpp default, Q2_K, 8 stepsvendor, Q2_K, 8 stepsunreadable, then unreadable
StepsQ2_K, 8 stepsQ2_K, 20 stepsunreadable, then all correct
BitsQ2_K, 20 stepsQ4_K, 20 stepsall correct, then all correct
StepsQ4_K, 20 stepsQ4_K, 40 stepsall correct, then all correct

The step budget did it. Changing the schedule at 8 steps changed almost nothing — the 0.211 terminal jump I suspected turns out not to be what smudges the text. Going from 8 steps to 20 at the same 2.88 bits fixed every word. Going from Q2_K to Q4_K at 20 steps did not change legibility at all; it changed the picture. The two quantisations land on different compositions from the same seed — the 2-bit weights keep the first pass's close-up board and ledge, the 4-bit ones pull back to a window-lit room — and the Q2_K frame has a blotchy smear along its left edge and a flatter finish that the Q4_K one does not. From 20 steps to 40, at Q4_K, the image barely moves: the same composition and the same words, with the chalk a little softer at 20.

So the first pass's central claim is withdrawn rather than adjusted. It said guidance bought text fidelity and resolution did not, and hedged that 8 steps at 1,024 latent tokens might simply be too few. That hedge was the finding. At 20 steps, 512 x 512 renders the text with guidance off, at either quantisation.

Where the bits go

A "Q2_K" is not one thing. Reading the tensor tables of every GGUF in Unsloth's repo over HTTP range requests — no download — shows files whose names describe their largest ambition rather than their contents:

Unsloth rungBytesBits/paramWhat the tensor table actually holds, by share of parametersShared modulation
Q2_K2,466,137,8242.7799.0% Q2_KQ2_K
Q3_K_S2,724,742,8803.0669.1% Q2_K, 25.7% Q3_K, 4.0% Q4_KQ4_K
Q3_K_M3,168,290,5283.5665.8% Q3_K, 20.3% Q2_K, 11.8% Q4_K, 1.2% Q5_KQ4_K
Q3_K_XL3,612,493,5364.0655.4% Q3_K, 32.3% Q4_K, 7.8% Q5_K, 3.3% Q2_KQ5_K
Q4_K_S3,906,356,9604.3954.2% Q4_K, 33.5% Q3_K, 10.8% Q5_KQ5_K
Q4_K_M4,199,565,0244.7290.8% Q4_K, 7.8% Q5_KQ5_K
Q5_K_S4,501,948,1285.0664.6% Q4_K, 30.9% Q5_K, 2.9% Q8_0Q5_K
Q5_K_M5,390,223,0726.0665.3% Q5_K, 29.5% Q6_K, 4.5% Q8_0Q5_K
Q6_K6,271,551,2007.0579.5% Q6_K, 19.8% Q8_0Q8_0
Q6_K_XL6,718,506,7207.5553.5% Q6_K, 45.8% Q8_0Q8_0
Q8_07,640,860,3848.5999.3% Q8_0Q8_0
F1614,230,275,80816.0099.5% F16, 0.5% BF16F16
leejet Q2_K2,561,716,2562.8898.1% Q2_K, the rest BF16BF16
leejet Q4_K4,197,494,8164.7298.1% Q4_K, the rest BF16BF16

Unsloth describes these as Dynamic 2.0 quants, "upcasted to higher precision, per tensor, from a measured sensitivity scan", and the tables bear that out, with two surprises. The rung labelled Q3_K_S is mostly Q2_K by parameter count, and Q4_K_S has a third of its parameters at Q3_K. And one tensor gets treated differently at the bottom than everywhere else. This denoiser has a single modulation projection shared by all 32 blocks — modulation.1.weight, 4,096 x 16,384, 67,108,864 parameters, 0.94% of the model — which this article flagged as a place the architecture saved parameters. leejet's seven rungs are uniform — the same 192 tensors at the named type and the same 73 in BF16 in every file, from Q2_K to Q8_0 — so it keeps that tensor in BF16 throughout. Unsloth stores it at Q4_K or better everywhere else — Q4_K, Q5_K, Q8_0 or F16, depending on the rung — and quantises it to 2 bits in its Q2_K.

That makes leejet's Q2_K against Unsloth's Q2_K a fair test of whether where the bits go matters at the bottom: 2,561,716,256 bytes against 2,466,137,824, and the same 192 tensors at Q2_K in both. Diffing the two tensor tables leaves exactly 69 differences. Sixty-five go up in Unsloth's file — the 64 query/key norms and the output modulation, BF16 to F32 — and three small ones go down to Q8_0: the output projection and the two timestep-embedding layers. The one that goes all the way down is the shared modulation, BF16 to Q2_K.

A chalkboard in a white frame with four lines of hand-lettered chalk reading '4 CPU', 'cores', 'no', 'GPU' — the comma after 'cores' is missing — and a potted plant on a white shelf at the lower left, in cool grey light.
Unsloth's Q2_K (2.77 bits per parameter, shared modulation at 2 bits), 20 steps, vendor sigmas, cfg 1.0, 512², seed 42. Generated on CPU, 1,669.06 s. Legible, one comma short, and a third composition from the same seed.

sd.cpp loads Unsloth's files without complaint, although they name every tensor with a model.diffusion_model. prefix that leejet's do not carry. The text survives the 2-bit shared modulation — all four words, in a more hand-lettered chalk than either leejet file produced — but the comma after "cores" does not, and the seed lands on yet another room: a white-framed board, the plant on a shelf at the left, cooler light. Three weight files, one seed, three compositions. On one prompt that is not evidence that the modulation tensor is fragile, and I would not claim it. What it does show is that at 2 to 3 bits, which file you download changes which picture a given seed produces — the same seed, prompt and settings gave three different rooms across three files.

Where the wall clock goes

Second pass — 512², vendor sigmas, guidance off, --mmap

Q4_K · 40 steps3863.00 s

the model's own settings; the headline image — wall-clock from the job queue's timestamps; sd.cpp's own log for this run was lost (see the text), and other jobs overlapped it

Q4_K · 20 steps1822.84 s · 87.09 s/step median · 11.40 GiB peak RSS

the gallery's setting

Q2_K · 20 steps1656.77 s · 79.43 s/step median · 9.86 GiB peak RSS

leejet's 2.88-bit rung, same everything else

Unsloth Q2_K · 20 steps1669.06 s · 79.64 s/step median · 9.79 GiB peak RSS

2.77 bits, the shared modulation quantised too

Q2_K · 8 steps665.60 s · 76.50 s/step median · 9.86 GiB peak RSS

the first pass's step count, on the vendor's schedule

First pass — sd.cpp's default schedule, no --mmap

256² · 1 step58.28 s · 33.63 s/step median · 7.91 GiB peak RSS

Q2_K, cfg 1.0 — the smoke test

256² · 8 steps203.32 s · 22.18 s/step median · 7.92 GiB peak RSS

Q2_K, cfg 1.0

512² · 8 steps901.72 s · 100.27 s/step median · 9.73 GiB peak RSS

Q2_K, cfg 1.0 — sharing the box with other jobs

256² · 8 steps474.60 s · 53.17 s/step median · 7.92 GiB peak RSS

Q2_K, cfg 4.0 — guidance on, two denoiser passes per step

text encodesamplingVAE decodetotal only
Measured on this site’s build box: 4 cores of an Intel Xeon at 2.80GHz, 15 GB of RAM, no GPU of any kind. Each pass is drawn to its own longest run. Each stage includes reading that component’s weights, which is why the segments sum to the total. The per-step figure is a median, because other jobs shared this box during some runs. Peak RSS is VmHWM, sampled from /proc while the process ran, and in the second pass it counts the memory-mapped weights.

Five things from the second pass, all at 512 x 512 with guidance off:

One chalkboard is a legibility test, not a look at the model. So here is a gallery at one setting — leejet's Q4_K, 512 x 512, 20 steps on the vendor's schedule, guidance off, seed 42 — with prompts chosen to hit what this release claims to be good at: bilingual text, a dense layout, counting and spatial relations, a photographic portrait, a named illustration style, fine detail, and a transparent subject. I wrote the seven new prompts down before any of them had run. Six ran, and every image they produced is here beside the chalkboard, including the ones that went wrong.

Twenty steps rather than forty is a budget decision — half an hour an image instead of an hour — and it rests on evidence rather than convenience: on the chalkboard the two are the same picture, and the poster, the densest text in the set, came out exact at 20. I started a 40-step poster as a further check and cancelled it after 26 seconds once the 20-step one had rendered every element, because it could only have confirmed what the 20-step image already showed, at the cost of an hour the next job was waiting for.

A framed classroom chalkboard lit by a window on the left, four lines of white lettering reading '4 CPU', 'cores,', 'no', 'GPU', a potted plant on the sill and wooden desks in the foreground.
'a chalkboard in a classroom, the words "4 CPU cores, no GPU" written in white chalk, a small potted plant beside it' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 1,822.84 s. Every word right.
A minimalist cream poster. At the top, bold black text reads 'EDGE AI SUMMIT' with 'Bengaluru, 14 November 2026' in smaller type beneath it; along the bottom, three words in a row read 'Talks', 'Workshops', 'Demos'. The middle of the poster is empty.
'A minimalist conference poster on a cream background. At the top, bold black text reads "EDGE AI SUMMIT". Below it, smaller text reads "Bengaluru, 14 November 2026". At the bottom, three words in a row read "Talks", "Workshops", "Demos"' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 2,097.40 s. All five text elements exact.
A small shop front at night under a glowing red sign. The sign reads 面馆 in white characters, then a garbled glyph, then 'BAR'; the word 'NOODLE' is missing. Steam drifts out of the lit doorway and the wet street in front reflects the light.
'A small noodle shop at night with a glowing red sign above the door that reads "面馆 NOODLE BAR" in large letters, steam rising from the doorway, the wet street reflecting the sign' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 2,166.00 s. The Chinese is right; 'NOODLE' is missing, a garbled glyph in its place.
On a glossy white surface, three red cups on the left fused into a single three-lobed object with shared walls, and two separate blue bowls on the right, each reflected in the surface.
'Three red cups to the left of two blue bowls on a plain white table, photographed from the front' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 2,047.88 s. Count and placement right; the three cups are fused into one piece.
A photographic close portrait of an elderly man with a deeply lined, weathered face, grey hair and a full grey beard, wearing a worn yellow raincoat with the hood up, in flat overcast light against a blurred harbour background.
'A photorealistic portrait of an elderly fisherman with a weathered face and a grey beard, wearing a yellow raincoat, soft overcast light, shallow depth of field' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 1,871.32 s. Every element of the prompt, and no visible artifacts at this size.
Two panels. Left: a green glass bottle of olive oil with a cork stopper, composited over a grey checkerboard, but the background is solid white and the checkerboard shows only in a thin fringe around the bottle and one small blob at the lower right. Right: the image's alpha channel, white almost everywhere, with a thin black outline around the bottle and the same small black blob.
'This is an RGBA image with transparency. A clear glass bottle of green olive oil with a cork stopper. The image has alpha channel and the background is transparent.' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 1,819.53 s. Left, the output over a checkerboard; right, its alpha channel, white meaning opaque. The transparency failed: the background came back opaque white, and only a fringe around the bottle, 1.30% of the pixels, falls below alpha 32.
Two hands knitting a chunky grey-brown wool piece in warm light from a window behind, fuzzy fibres visible along the yarn, the knit worked in a thick cable pattern; two short metal needle tips meet between the hands and disappear into the wool.
'A close-up photograph of two hands knitting a chunky wool scarf, individual yarn fibres and stitches visible, warm window light' — Q4_K, 20 steps, cfg 1.0, 512², seed 42, on CPU, 1,743.39 s. Plausible hands and visible fibres; the needles are short stubs that vanish into the wool.

Right: the chalkboard; the poster, whose five text elements — a date among them — are all exact; the Chinese on the noodle-shop sign; the count and the left-right placement of the cups and bowls; and a portrait that takes every element of its prompt. Wrong: the sign's English lost a word, a garbled glyph where NOODLE should be; the three red cups came out fused into one three-lobed object; and the transparency request came back with an opaque white background, the one outright failure. The knitting is the subtle one: plausible hands with no extra fingers and fuzzy fibres on the wool, but needles that are short stubs vanishing into the yarn instead of running through the hands.

I pre-registered seven new prompts and the gallery shows six. The seventh — An ukiyo-e woodblock print of a cat sitting on a wooden bridge in the rain, with Mount Fuji in the background — was cut for time before it ran, when the box slowed to 146 s a step; no image of it was produced or discarded, and the gallery has no test of a named illustration style as a result.

Each image took 1,819.53 to 2,166.00 s, with per-step medians from 88.57 to 103.90 s. The two slowest are the two longest prompts, so some of that is text length, but not all of it: step times drifted from the high 80s to 146 s inside single runs while nothing else ran in the VM, steal time read 1–2%, and the samples I took showed no I/O wait and no memory reclaim. Whatever that was, it was outside the machine I could see, and it is why every per-step figure in this section is a median.

The transparency failure needs more than a shrug, because the model can do this: the two 1024² samples in the alpha section above came back 24.97% and 33.65% fully transparent, one of them through a 4-bit GGUF. So I went down the list of causes. It is not the prompt: I used the model card's exact wording, and the diffusers pipeline has no other switch for it — no output mode, no different system prompt, all four VAE channels post-processed the same way. And it is not sd.cpp's alpha path, which I could test for eight minutes of CPU. The same prompt, seed, weights and binary at 256 x 256 came back with most of its background transparent:

Two panels at 256 pixels. Left: a tall green glass bottle with a cork over a grey checkerboard, which shows through most of the background, interrupted by several irregular opaque white blobs to either side of the bottle. Right: the alpha channel, black for most of the background, white for the bottle and for the same scattered blobs.
A diagnostic, not part of the gallery: the glass prompt at 256 x 256, Q4_K, 20 steps, cfg 1.0, seed 42, on CPU, 496.52 s. Left over a checkerboard, right the alpha channel. 47.23% of pixels at alpha 5 or below, the most common value 1 rather than 0, with saturated purple beneath — so sd.cpp's alpha path works — but opaque white blobs are painted over the background and the glass itself is opaque.

47.23% of the pixels are at alpha 5 or below, though only 1.41% at exactly zero — the most common value is 1 — and under them the RGB is saturated purple, the same signature this article found under the teapot's transparent pixels. So where the model makes transparency, sd.cpp carries it through. What went wrong in both of my glass images is something else: opaque white painted over the transparent background, in blobs at 256² and across the whole frame at 512². That is in what the model generated, not in how it was decoded, and what remains unseparated is everything that differs between my runs and the two that worked: 20 steps rather than 40, 512 or 256 pixels rather than 1024, leejet's Q4_K rather than the community Q4_K_M or the official INT8, and a different prompt and seed. A second seed at 256² would split the seed from the rest in eight minutes; I stopped short of it because another job was waiting for the CPU. So I do not know which it is, and nothing here says the model cannot do transparency on a CPU — one configuration of it did not.

The alpha floor deserves a line of its own, because another model shows it too. In the probe the transparent region sits almost entirely between 1 and 31 — 46.32% of the pixels, most of them at exactly 1 — with only 1.41% at 0, where the two 1024² samples that worked had a quarter and a third of the frame at exactly 0. Ming-Image's transparent backgrounds, measured in the piece on Ming-Image-0.1-Design, also sit at alpha 1 rather than 0. That model ships Qwen-Image-Layered's RGBA autoencoder, and it is not the same autoencoder as this one — not by hash, which would prove nothing against a bf16 conversion, but by config: Qwen-Image-Layered's has 16 latent channels at 8x compression, Qwen-Image-2.1's has 64 at 16x. Two different RGBA autoencoders both put a transparent background at 1. Whether that floor comes from the models or from how a decoded value is rounded to 8 bits I have not checked, and the practical consequence is the same either way: code that treats alpha > 0 as content will find a full-canvas object.

The first pass, and what it got wrong

The first pass ran three configurations at 8 steps on sd.cpp's default schedule, and the two that are not already in the grid above are here as the baseline they are.

A 256-pixel-square generated image of a dark classroom chalkboard. The words '4 CPU' are legible in white chalk at the top; two further lines of chalk writing are word-shaped but not readable. Below the board is a wooden ledge with a small potted plant on the left and a stack of books on the right.
First pass: leejet Q2_K, 256 x 256, 8 Euler steps, sd.cpp's default schedule, cfg 1.0, seed 42. Generated on CPU in 203.32 s. '4 CPU' legible, the rest not.
A 256-pixel-square generated image of a very dark chalkboard carrying two lines of faint blue-white chalk: the first reads '4 CPU cores' with the words run together, the second reads 'no GPU'. A small potted plant stands on a wooden ledge below.
First pass: the same run with guidance at cfg 4.0 — a setting the vendor's pipeline does not use by default. 474.60 s on CPU. Both lines discernible, the frame badly underexposed.

It drew three conclusions, and the second pass is the test of each.

What it got right survives: the pipeline runs end to end with no GPU at all, the text encoder costs almost nothing, and the VAE's activation memory at small resolutions dwarfs the denoiser's — 613.19 MB against 65.24 MB at 256², 2,450.50 MB at 512².

What this does and does not settle

It settles the floor, and it moves the answer from "it runs" to "it works". No GPU anywhere in these runs, no Vulkan, no unified memory: 9.90 GB on disk and 11.40 GiB resident for the 4-bit pipeline, and at the vendor's own settings a 512² image in an hour that gets every word on the board right. At 20 steps it is half an hour. The 2 GB and 3.05 GiB figures above ask how little of a GPU you can get away with; this asks whether you need one, and at 512² the answer is no, if you have an hour and the text is what you care about. The gallery says the same thing more broadly and less kindly: at 20 steps the dense poster and the Chinese sign text held, and the portrait took every element of its prompt. It failed on three things — a dropped English word, three objects fused into one, and a transparency request that did not come back transparent — and one seed each cannot pin those on the weights, the resolution or the model.

What it does not settle is how far below the full model this is. Every image here is 4-bit or 2-bit, at a quarter of the pipeline's default pixel count and a sixteenth of the model card's, and nobody has published a bf16 image at the same seed to hold them against — the 3090 report has no quality comparison either. It does not settle guidance at a fair step budget, because I did not run cfg above 1.0 past 8 steps. It is one seed per prompt throughout, so no single success or failure here is a rate. And it does not reach native resolution: the VAE's compute buffer is 2,450.50 MB at 512² and grows with the pixel count, so on this box's 13.36 GiB cgroup a 1024² decode would likely need sd.cpp's tiled VAE — an inference from the buffer sizes, not something I ran. The time that would cost is in the falsifiers, as an extrapolation rather than a number.

What is missing

Shortest useful list, all verified by absence:

The take

Qwen-Image-2.1 is a good piece of engineering and a worse release than the six that preceded it. The architecture work is genuine and legible from the files: a single-stream DiT at a third of the previous denoiser's size, a shared modulation projection and mlp_ratio: 3 that together account for 3.69B of the parameters it does not have, a 16x RGBA autoencoder that makes transparency a property of the latent space instead of a second model, and a block-causal mask whose prefix cache is exact because someone thought to modulate the condition from t = 0. That last one is the idea I would steal, and two days of other people's benchmarks have not dented it: 2.55x measured against 2.62x predicted, bit-exact output, and the same effective throughput on both paths. The engineering is the part of this release that holds up under someone else's stopwatch.

What surrounds it is thinner than the version number suggests. One aggregate score on the vendor's own benchmark, on the vendor's own judge, on the Chinese-prompt run, measuring the capability that did not change. No report, no data, no numbers for the new features, no first-party quantisation. And a licence that moves a year-old Apache-2.0 family to non-commercial research terms while the announcement keeps saying "open-source" — which is the one change here that decides whether you can use it at all, and the only one the blog post does not mention.

What would change my mind

13 claims above, and what would falsify each

  1. The denoiser is exactly 7,115,124,736 parameters and contains no second text stream.

    Sum the shapes in transformer/diffusion_pytorch_model.safetensors.index.json and the two shard headers. A txt_mlp, add_q_proj or to_add_out tensor anywhere in those 297 names, or a total that does not reconcile with the index's total_size of 14,230,249,472 bytes at two bytes per BF16 parameter, falsifies it.

  2. The published quantitative comparison is the vendor's benchmark, judged by the vendor's model, on Chinese prompts.

    A third-party evaluation of this checkpoint on a harness Qwen did not write, or an English-prompt Qwen-Image-Bench leaderboard that reproduces the chart's ordering, would change how much weight the 60.28 carries. The footnote I am relying on is on the dataset card at huggingface.co/datasets/Qwen/Qwen-Image-Bench; if it is edited or the English run lands, this section is stale.

  3. The prefix KV cache is worth little for plain text-to-image and a lot for multi-reference editing.

    Originally I asked for a measured step-time breakdown showing the cached path saving substantially less than the token share predicts. Three now exist and none of them do; the A100 one lands at 97% of the arithmetic bound. What would still falsify this is the other tail: a measured 2-reference DiT speedup materially above 2.62x at 1024px and 40 steps, which the mask does not permit and would mean the cache is doing something my pair counts do not model. A text-to-image-only speedup above about 1.05x would do the same.

  4. 2.55x reconciles with the architecture because the DiT is compute-bound at these sequence lengths.

    The whole reconciliation rests on 50.57 s and 19.86 s being DiT time on a warmed A100 at 2 references, 1024px and 40 steps, with no classifier-free guidance. If any of those is wrong — if the timing is end-to-end, if guidance was on and doubled the work, if the references were resized to something other than about a megapixel — the predicted ratio moves and the agreement is a coincidence. The same run at a much larger target, where the target dominates the sequence, should converge toward 1.0x; if it does not, my model of where the saving comes from is wrong.

  5. Qwen-Image-2.1 is the first Qwen image model under a non-commercial licence.

    A Qwen image checkpoint on the Hub or ModelScope, dated before 20 September 2026, whose LICENSE is not Apache-2.0. I checked the eight repos in the table and nothing else.

  6. Qwen-Image-2.1's transparency is a matte, not translucency — in native-resolution output, not just the published web asset.

    Two 1024px generations, through two different quantisations, put 0.55% and 0.48% of pixels between alpha 32 and 223 and 99.1% within seven levels of an extreme. Both are opaque subjects on empty backgrounds, which is the easy case. A native 2048px glass, smoke or hair prompt with meaningful midrange mass would falsify this, and is the test I still want; so would a matting metric against a ground-truth alpha, which nobody has run on this checkpoint. My own glass prompt, on four CPU cores at Q4_K, 512² and 20 steps, did not get as far as testing it: the background came back opaque white rather than transparent, with 87.47% of pixels at alpha 255 and 10.75% at 224–254, 1.30% below 32 in a fringe around the bottle, and 0.48% in the midrange. The glass body is opaque in the alpha channel too, but when the model has not made the background transparent that says nothing about translucency. Every other CPU image, none of which asked for transparency, also came back RGBA and at least 99.9% within five levels of opaque.

  7. The first pass's smudged text was its 8-step budget — not the 2.88-bit weights, and not sd.cpp's default schedule.

    One prompt and one seed, changing one thing at a time: the schedule at 8 steps changed nothing, 8 steps to 20 at the same Q2_K fixed every word, and Q2_K to Q4_K at 20 steps did not change legibility. What would falsify it is the same experiment on other seeds going the other way — a Q2_K image at 20 steps with broken text where Q4_K's is clean, or an 8-step image on the vendor's schedule that reads correctly where sd.cpp's default does not. What it cannot say is how either quantisation compares with bf16 at the same seed; nobody has run that reference, and I did not have the memory to.

  8. A 1024 x 1024, 40-step image at the vendor's settings would take roughly 3.5 to 4.5 hours on these four cores.

    An extrapolation, not a measurement, and it replaces the "ten hours" the first pass gave, which assumed guidance the vendor does not use. It is 87.09 s per step at 512² and Q4_K, multiplied by the 256-to-512 step-cost ratio — 3.58x on clean numbers, 4.53x on the first pass's shared-box ones — then by 40 steps, plus encode and decode. Attention grows faster than the rest with sequence length, so the upper end is the likelier one. A measured 1024² step on this box under about 280 s or over about 450 s would mean the scaling is not the shape I assumed.

  9. Inside a memory-limited container, reading rather than mapping the weights is what broke the Q4_K pipeline, not the pipeline's size.

    One OOM kill without --mmap and one clean run with it, same weights, same resolution, same cgroup. The mechanism I have given — page cache and a private copy both charged — fits the kernel log and the 413 s of system time, but I did not isolate it further. A read-mode run that succeeds in the same cgroup once the page cache is dropped beforehand would show the kill was about cache timing rather than double-charging, and would soften the advice.

  10. The text encoder inside Qwen/Qwen-Image-2.1 is stock Qwen3-VL-8B-Instruct, which Qwen also publishes under Apache-2.0.

    Evidence by sampling, not exhaustion: all 750 tensor names, shapes, dtypes and byte sizes match, and six tensors spread across the model are byte-identical over their first 4 MB. Any tensor whose bytes differ falsifies it, and a fine-tune of a few layers would hide from my sample. Hashing every tensor in both repositories over range requests would settle it for about 35 GB of transfer, which I did not spend.

  11. At 512 x 512, 20 steps gives up little against the vendor's 40.

    Measured side by side on one prompt, the chalkboard, where the two are the same picture; the poster, the densest text in the gallery, came out exact at 20, but I cancelled its 40-step twin. A gallery prompt that renders visibly better text, anatomy or structure at 40 steps than at 20, same seed, would falsify it and would make the gallery an under-sampled view of the model.

  12. The failed transparent background is in what the model generated, not in how sd.cpp decoded it.

    At 256² the same prompt, seed, weights and binary put 47.23% of pixels at alpha 5 or below with purple beneath, so the fourth channel survives the decode; the 512² failure is opaque white painted over the background. The same prompt, seed and step count at 512² through diffusers or ComfyUI coming back transparent would falsify it, and would put the fault back in sd.cpp's path after all.

  13. The ncnn conversion is the full-width release minus the unused lm_head and a halved VAE.

    Sum the file sizes at huggingface.co/nihui-szyl/qwen-image-ncnn and subtract 1,244,659,712 bytes for the lm_head and 675,480,808 for the VAE's fp32-to-2-byte cast from the release's 33,115,458,480. I get 31,195,317,960 against a shipped 31,191,738,224, which is 0.01% apart. A different residual — or a bin whose leading tag is not 0x01348B83 — means it dropped or converted something else, and the "full bf16, no quantisation" claim needs restating.

Update — 2 October 2026

Two ecosystem pieces landed after the 23 September update, both on the autoencoder this article measured rather than the denoiser. madebyollin posted two of them. texture-fix-vae-for-qwen-image-2.1 is the release's own VAE finetuned "to produce cleaner textures with no checkerboard artifacts" (reported) — a drop-in replacement on the same latent API, so the 337M-parameter autoencoder this article measured gets a better decoder without touching the 7.1B denoiser. taeqi2_1 (MIT) is a tiny distilled autoencoder on the same 16× / 64-channel / RGBA latent API — the exact geometry this article read off the config — meant for real-time preview during sampling and for resource-constrained encode/decode. Both confirm the article's aside that autoencoders get lifted out of releases and reused more than denoisers do: here the VAE is simultaneously fixed and shrunk while the denoiser is left alone.

The second item is nihui's ncnn/Vulkan port, whose earlier "2 GB" claim this article already took apart above — 2 GB is the GPU's share of a host budget, not the model's footprint. A follow-up post reports the port now rendering full BF16 at 1024×1024 with speed unchanged, on 2 GB of VRAM and 4 GB of system RAM (reported). The native-size result closes the open question the project's own requirements had left, but the 4 GB RAM figure is lower than the ~16 GB host budget this article computed from those same requirements, so it is a fresh, unreconciled claim from the author's teaser, not a number reproduced here.

Sources, all read directly: the Hugging Face repo (model_index.json, transformer/config.json, text_encoder/config.json, vae/config.json, scheduler/scheduler_config.json, both safetensors indexes, the tensor headers by range read, and LICENSE); the ModelScope mirror, whose card is byte-identical to the Hub's; the GitHub repo; the release blog; the diffusers implementation of QwenImage21Transformer2DModel and AutoencoderKLQwenImage21; the Qwen-Image-Bench dataset and Q-Judger model cards, and their paper (arXiv:2605.28091); and the third-party vLLM-Omni recipe.

Added 22 September, all likewise read rather than summarised: the diffusers implementation (transformer_qwenimage21.py, pipelines/qwenimage21/pipeline_qwenimage21.py and both API doc pages) for the cache mechanics and the use_kv_cache default; a public thread by Sayak Paul for the A100 DiT timings and the memory delta; flyweight PR #91 for the RTX 5070 Ti end-to-end numbers and the bit-exactness check; the deployment guide and its GGUF companion, whose install/models.json manifests, docs/validation.md reports and committed example PNGs are the source of every 3090 figure and both alpha histograms; abenzerps/Qwen-Image-2.1-GGUF and Comfy-Org/Qwen-Image-2.1 for licence metadata; and nihui/qwenimage-ncnn-vulkan with nihui-szyl/qwen-image-ncnn, whose file sizes came from the Hub API and whose bf16 tag came from an 8-byte range read checked against ncnn/src/modelbin.cpp. The first three figures are the release's own, served locally and flattened; the fourth is my own three-panel composite of a PNG from that validation run, and both checkerboards are mine. I measured the alpha histograms myself.

Cite this article

For attribution, please use the following reference or BibTeX:

Satyajit Ghana, "Qwen-Image-2.1: the 7B is 28% of the pipeline, and the licence is not Apache-2.0", ai.thesatyajit.com, September 2026.

bibtex
@misc{ghana2026qwenimage21,
  author = {Satyajit Ghana},
  title  = {Qwen-Image-2.1: the 7B is 28% of the pipeline, and the licence is not Apache-2.0},
  url    = {https://ai.thesatyajit.com/articles/qwen-image-2-1},
  year   = {2026}
}
share