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.
- task
- text-to-image
- library
- diffusers
- license
- other
- safetensors
- 7 shards
- largest file
- 9.97 GB
- files
- 28
- downloads
- 16.2K
- likes
- 1.7K
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
- license
- custom
- branch
- main
- tests
- none found
- source
- 42.2 kB
- commit date
- 2026-09-20
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 / 2512 | Qwen-Image-2.1 | |
|---|---|---|
| denoiser class | QwenImageTransformer2DModel | QwenImage21Transformer2DModel |
| blocks | 60, dual-stream | 32, single-stream |
| width | 24 x 128 = 3,072 | 32 x 128 = 4,096 |
mlp_ratio | 4 (implicit) | 3 |
| modulation | per block | one shared projection |
patch_size | 2 | 1 |
| denoiser params | 20,430,401,088 | 7,115,124,736 |
| text encoder | Qwen2.5-VL-7B | Qwen3-VL-8B |
| VAE | z_dim 16, 8x spatial, RGB | z_dim 64, 16x spatial, RGBA |
| licence | Apache-2.0 | Qwen 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.
- 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
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:

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.
A100, warmed, 2 references, 1024px, 40 steps — DiT time only
50.57s → 19.86s. The measurement is 97.3% of the arithmetic bound.
RTX 5070 Ti, 1 reference, 512px, 4 steps — whole job
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
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.
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.

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.
684 x 685, published RGBA web asset, downscaled by someone before publication
1024 x 1024 native, official Comfy-Org INT8 weights, 40 steps
1024 x 1024 native, community 4-bit conversion of the same denoiser, 40 steps
- alpha 0
- 1-31, 224-254
- 32-223
- alpha 255
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:

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:
| Repo | Released | Licence |
|---|---|---|
Qwen/Qwen-Image | Aug 2025 | Apache-2.0 |
Qwen/Qwen-Image-Edit | Aug 2025 | Apache-2.0 |
Qwen/Qwen-Image-Edit-2509 | Sep 2025 | Apache-2.0 |
Qwen/Qwen-Image-Edit-2511 | Dec 2025 | Apache-2.0 |
Qwen/Qwen-Image-Layered | Dec 2025 | Apache-2.0 |
Qwen/Qwen-Image-2512 | Dec 2025 | Apache-2.0 |
Qwen/Qwen-Image-Bench | May 2026 | Apache-2.0 |
Qwen/Qwen-Image-2.1 | Sep 2026 | Qwen 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.

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:
- 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.
- The human-agreement figure is a model-level rank correlation. Spearman is impressive and it is computed over 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.
- 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:
| Config | Peak memory | Time per image |
|---|---|---|
| BF16 | 34.0 GB | 3,282-4,492 ms |
DiT FP8, img_mlp kept BF16 | 32.0-32.7 GB | 3,356-4,706 ms |
| Text-encoder FP8 | 27.5-28.1 GB | 3,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:
| Component | Repo · file | Bytes | Bits/param |
|---|---|---|---|
| Denoiser, pass 2 | leejet/Qwen-Image-2.1-GGUF · qwen_image_2.1-Q4_K.gguf | 4,197,494,816 | 4.72 |
| Denoiser, both passes | leejet/Qwen-Image-2.1-GGUF · qwen_image_2.1-Q2_K.gguf | 2,561,716,256 | 2.88 |
| Denoiser, comparison | unsloth/Qwen-Image-2.1-GGUF · qwen-image-2.1-Q2_K.gguf | 2,466,137,824 | 2.77 |
| Text encoder | Qwen/Qwen3-VL-8B-Instruct-GGUF · Qwen3VL-8B-Instruct-Q4_K_M.gguf | 5,027,784,800 | 4.59 |
| VAE | Comfy-Org/Qwen-Image-2.1 · vae/qwen_image_2.1_vae_bf16.safetensors | 675,509,688 | 16.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.
- Qwen's model card. Every example calls the pipeline with
num_inference_steps=40and seed 42, the text-to-image ones at 2048 x 2048, and none of them passes a guidance argument. - The diffusers pipeline.
QwenImage21Pipeline.__call__defaults tonum_inference_steps: int = 40andtrue_cfg_scale: float = 1.0, and guidance switches on only whentrue_cfg_scale > 1and a negative prompt is supplied. So the vendor's default is 40 steps with classifier-free guidance off — one denoiser pass per step, not two. The scheduler isFlowMatchEulerDiscreteSchedulerwith a resolution-dependent shift:base_shift0.5,max_shift0.9, anchored at 256 and 8,192 latent tokens, exponential time shift, andshift_terminal: 0.02. - Unsloth's guide gives two recipes and says to use the one for the backend actually running the model. For diffusers: 40 steps, CFG 1.0 ("CFG disabled"), keep the pipeline's scheduler — which agrees with the model card. For native stable-diffusion.cpp: 20 steps ("CLI default and Unsloth's recorded test"), cfg 6.0 ("upstream sd.cpp example"), Euler, and an automatic flow shift. Unsloth's own GGUF samples were rendered at 1024², 20 steps, cfg 6.0.
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

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.




Read left to right and down, and the confound comes apart:
| Changed | From | To | The words on the board |
|---|---|---|---|
| Schedule | sd.cpp default, Q2_K, 8 steps | vendor, Q2_K, 8 steps | unreadable, then unreadable |
| Steps | Q2_K, 8 steps | Q2_K, 20 steps | unreadable, then all correct |
| Bits | Q2_K, 20 steps | Q4_K, 20 steps | all correct, then all correct |
| Steps | Q4_K, 20 steps | Q4_K, 40 steps | all 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 rung | Bytes | Bits/param | What the tensor table actually holds, by share of parameters | Shared modulation |
|---|---|---|---|---|
Q2_K | 2,466,137,824 | 2.77 | 99.0% Q2_K | Q2_K |
Q3_K_S | 2,724,742,880 | 3.06 | 69.1% Q2_K, 25.7% Q3_K, 4.0% Q4_K | Q4_K |
Q3_K_M | 3,168,290,528 | 3.56 | 65.8% Q3_K, 20.3% Q2_K, 11.8% Q4_K, 1.2% Q5_K | Q4_K |
Q3_K_XL | 3,612,493,536 | 4.06 | 55.4% Q3_K, 32.3% Q4_K, 7.8% Q5_K, 3.3% Q2_K | Q5_K |
Q4_K_S | 3,906,356,960 | 4.39 | 54.2% Q4_K, 33.5% Q3_K, 10.8% Q5_K | Q5_K |
Q4_K_M | 4,199,565,024 | 4.72 | 90.8% Q4_K, 7.8% Q5_K | Q5_K |
Q5_K_S | 4,501,948,128 | 5.06 | 64.6% Q4_K, 30.9% Q5_K, 2.9% Q8_0 | Q5_K |
Q5_K_M | 5,390,223,072 | 6.06 | 65.3% Q5_K, 29.5% Q6_K, 4.5% Q8_0 | Q5_K |
Q6_K | 6,271,551,200 | 7.05 | 79.5% Q6_K, 19.8% Q8_0 | Q8_0 |
Q6_K_XL | 6,718,506,720 | 7.55 | 53.5% Q6_K, 45.8% Q8_0 | Q8_0 |
Q8_0 | 7,640,860,384 | 8.59 | 99.3% Q8_0 | Q8_0 |
F16 | 14,230,275,808 | 16.00 | 99.5% F16, 0.5% BF16 | F16 |
leejet Q2_K | 2,561,716,256 | 2.88 | 98.1% Q2_K, the rest BF16 | BF16 |
leejet Q4_K | 4,197,494,816 | 4.72 | 98.1% Q4_K, the rest BF16 | BF16 |
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.

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
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
the gallery's setting
leejet's 2.88-bit rung, same everything else
2.77 bits, the shared modulation quantised too
the first pass's step count, on the vendor's schedule
First pass — sd.cpp's default schedule, no --mmap
Q2_K, cfg 1.0 — the smoke test
Q2_K, cfg 1.0
Q2_K, cfg 1.0 — sharing the box with other jobs
Q2_K, cfg 4.0 — guidance on, two denoiser passes per step
/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:
- 87.09 s per step at Q4_K, 79.43 s at leejet's Q2_K, 79.64 s at Unsloth's — medians of the per-step times sd.cpp prints. Q2_K's file is 39% smaller and its steps are only 9% faster, so on this CPU a step is mostly arithmetic, not bytes; cutting two-fifths of the weights buys disk and memory, not much time. sd.cpp kept 3.76 to 3.82 of the 4 cores busy in every clean run.
- The headline took 3,863 s, and should have taken about 3,535. Its wall-clock comes from
the job queue's timestamps, because sd.cpp's own log for that run is gone: I edited the shell
script that launched it while it was still executing, bash resumed reading the edited file at
a stale offset when the run ended, and a fragment of the new script overwrote the log. The
image had already been written. Fifteen per-step readings survive from checks I made during
the run, from 86.48 s to 124.09 s, and the slow ones sit in windows where other jobs were on
the box — my own downloads and a type-check, another agent's
pnpm validate, and page cache from freshly downloaded files squeezing the mapped weights. From the clean 20-step run's medians, 40 steps is 4.92 + 40 x 87.09 + 46.06 = 3,535 s, about 59 minutes. That figure is arithmetic on measurements, not a measurement. - Text encoding is free. 4.36 to 5.85 s for 8.77B parameters of Qwen3-VL, under 0.4% of a 20-step run.
- The VAE is not. 39.60 to 46.06 s to decode at 512², and its compute buffer is 2,450.50 MB — ten times the denoiser's 234.03 MB. At 20 steps that is 2.5% of the run; the 91.36 s the first pass logged was most likely the same decode on a shared box.
- 20 steps is half of 40 and costs half. 1,822.84 s for the gallery's setting against about 3,535 for the vendor's, and on the chalkboard the two images are the same picture. Whether that holds on harder prompts is what the gallery tests.
A gallery at one fixed setting
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.







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:

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.


It drew three conclusions, and the second pass is the test of each.
- "Guidance buys text fidelity and resolution does not." Withdrawn. At 8 steps that was what the pictures showed; at 20 steps, with guidance off, 512 x 512 renders every word at either quantisation. The underexposed cfg 4.0 frame was not a price legibility requires; at a fair step budget legibility comes with guidance off. I did not run cfg 4.0 past 8 steps, so whether that darkness came from the steps or the guidance I cannot say.
- "4.53x the per-step time for 4x the tokens — close to linear, slightly over." Wrong in its detail. That 512² run was sharing the box: sd.cpp averaged 2.91 busy cores out of 4, against 3.51 in the 256² run beside it. Against the clean second-pass median for the same Q2_K weights at 512², 79.43 s, the ratio is 3.58x. The 256² run itself only had 3.51 cores busy, so the honest reading is roughly linear, not over.
- "Disk, not RAM, was the binding constraint." True for that pass, on 10.93 GB of free disk. On the second, disk was not the limit and a memory cgroup was.
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:
- No technical report. No arXiv preprint, no PDF, no architecture section beyond eight bullet points in a README.
- No training data disclosure. Nothing about corpus, scale, filtering, or the licensing of what it learned from. The flagship multi-reference demo is a generated group photograph assembled from six publicity portraits of identifiable real people, and the release has nothing to say about that either.
- No eval for anything new. The benchmark covers text-to-image; the transparency, editing and reference-composition claims are supported by curated examples only. No GEdit, no ImgEdit, no alpha-matting metric, no identity-similarity number.
- No first-party quantisation. BF16 shards only, plus an FP32 VAE.
- No inference code in the repo. The GitHub repository ships the prompt rewriter and nothing else; the model runs through third-party libraries from day one, which is a reasonable choice and worth knowing before you file an issue.
- No limitations, bias or intended-use section in the model card. Not one line.
- No separate licence for the VAE.
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
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.jsonand the two shard headers. Atxt_mlp,add_q_projorto_add_outtensor anywhere in those 297 names, or a total that does not reconcile with the index'stotal_sizeof 14,230,249,472 bytes at two bytes per BF16 parameter, falsifies it.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.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.
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.
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
LICENSEis not Apache-2.0. I checked the eight repos in the table and nothing else.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.
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.
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.
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
--mmapand 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.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.
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.
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.
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-ncnnand subtract 1,244,659,712 bytes for thelm_headand 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 not0x01348B83— 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.