# Few-step Qwen-Image-2.1: two DMD LoRAs, and a speedup that is steps alone

> Satyajit Ghana — Head of Engineering @ Inkers Technology
> canonical: https://ai.thesatyajit.com/articles/qwen-image-2-1-few-step
> date: 2026-09-26
> tags: image-generation, diffusion, flow-matching, distillation, lora, qwen, open-weights, inference-optimization, benchmarks, explainer

Qwen-Image-2.1 samples an image in 40 denoising steps. [The piece on the release](/articles/qwen-image-2-1)
covers the architecture, a 7B single-stream DiT with an exact prefix KV cache, and I will not
repeat it here. This one is about what arrived in the week after: two few-step students.

- **Viggle** published `Viggle/Qwen-Image-2.1-viggle-turbo` on 22 September and was at v0.2.1
  two days later. AK's post on v0.2: "Text-to-image and image editing in 6 steps", "about 5×
  faster than the 40-step Qwen-Image-2.1".
- **Pruna** published `PrunaAI/Pruna-Qwen-Image-2.1` on 23 September: "a set of a few-step LoRA
  adapters that make Qwen-Image-2.1 … up to 6.3× faster". Its two short links resolve to the Hub
  repo and to a dashboard login; there is no blog post.

The tidy framing is two routes: a full distilled checkpoint against LoRA adapters. That was
true for about a day. I read the repositories: safetensors headers over HTTP range requests,
scheduler configs, both cards, and the `cases.json` behind Viggle's comparison Space, which holds 32
pairs of end-to-end timings. **Measured** means I computed it from a file.
**Reported** means it is the publisher's number and I could not re-run it. **Reasoned** is my
arithmetic on the other two. The first version of this piece ran no weights. The section on
[four CPU cores](#prunas-adapter-on-four-cpu-cores), added the same day, runs Pruna's adapters and
generates images, and its timings are labelled by how busy the box was.

<ModelCard
  repo="Viggle/Qwen-Image-2.1-viggle-turbo"
  claimed="6 steps, no CFG, about 5x faster than the 40-step base"
  note="Measured from the headers: v0.2.1 is a rank-256 LoRA of 679,542,784 parameters, 9.55% of the denoiser. The 7.1B the Hub reports is the superseded v0.1 full fine-tune. The 5.02x sums 32 end-to-end pairs on one B200. Qwen Research License, non-commercial."
/>

<ModelCard
  repo="PrunaAI/Pruna-Qwen-Image-2.1"
  claimed="5 or 8 steps, no CFG, up to 6.3x faster"
  note="Measured from the headers: two rank-64, alpha-128 LoRAs of 167,772,160 parameters, 2.36% of the denoiser, on attention and image MLP only. The 6.3x is the 5-step adapter at 2048²; the recommended 8-step one at 1024² is 3.8x. No quality metric. Qwen Research License, non-commercial."
/>

## Why the base model takes 40 steps

Qwen-Image-2.1 is a flow-matching model. Training mixes an image $x_0$ with Gaussian noise
$\epsilon$ as $x_\sigma = (1-\sigma)\,x_0 + \sigma\,\epsilon$ and teaches the denoiser $v_\theta$ to
predict the velocity $\epsilon - x_0$. Sampling runs it backwards: start at $\sigma = 1$, pure
noise, and take Euler steps down a list of noise levels to $\sigma = 0$. That is what
`FlowMatchEulerDiscreteScheduler.step` does:

$$
x_{\sigma_{i+1}} = x_{\sigma_i} + (\sigma_{i+1} - \sigma_i)\, v_\theta(x_{\sigma_i}, \sigma_i)
$$

Each step is one forward pass of the 7B denoiser, and the step count is set by how curved the
path is. The ideal velocity at any point is an average, $v^\star(x, \sigma) = \mathbb{E}[\epsilon - x_0 \mid x_\sigma = x]$.
At pure noise every image the prompt allows is still possible, so the velocity points at their
average, and one Euler step from there lands on the average image: a blur. As $\sigma$ falls the
posterior narrows, the direction swings toward one image, and the path bends. Euler holds the
direction fixed across a step, so it overshoots every bend by an amount that grows with the step.
Forty small steps keep that error small. Six large ones do not, unless the network is retrained
to take them.

<FlowToy />

The toy's velocity field is exact, so its error is the sampler's alone. At N = 1 the teacher
lands on the average of the three clusters. From 4 to 8 steps it heads for the right cluster and
misses the 400-step endpoint; at 40 it is close. A few-step student is trained to close that gap.

### The second cost, which this base does not pay

Classifier-free guidance runs the denoiser twice per step, with and without the prompt, and
extrapolates between the two. It doubles the passes. Qwen-Image 1.0 and Qwen-Image-2512 both call
their pipelines with a negative prompt, `num_inference_steps=50` and `true_cfg_scale=4.0`: 100
forward passes an image. Qwen-Image-2.1's card calls its pipeline with 40 steps and no guidance
argument. The diffusers pipeline defaults to `true_cfg_scale=1.0`, and switches guidance on only
when the scale exceeds 1 *and* a negative prompt is passed. The denoiser has no guidance embedder
either: its only conditioning on the step is `time_text_embed.timestep_embedder` (measured, from
the tensor names). So the 40-step baseline is 40 passes, not 80.

This shapes every speed claim below. Distillation usually removes two costs at once, because the
student is trained to reproduce the teacher's *guided* output in one pass per step. Here the
guidance half was already gone before either student arrived, and the budget is the step ratio
alone: 40/6 = 6.67x for Viggle's default, 40/8 = 5x and 40/5 = 8x for Pruna's adapters. Those are
ceilings, before the text encoder and the VAE, which no step count touches.

## The distillation families, and where these two sit

Every few-step method trains a student to do in N big steps what the teacher does in many small
ones. They differ in what "the same" means.

| Family | The student is trained to | Examples | Used here |
|---|---|---|---|
| Consistency | map any point on the teacher's trajectory to that trajectory's endpoint | [CM](https://arxiv.org/abs/2303.01469), [LCM](https://arxiv.org/abs/2310.04378), [LCM-LoRA](https://arxiv.org/abs/2311.05556) | no |
| Continuous-time consistency | the same in the limit of tiny steps, through a Jacobian-vector-product loss | [sCM](https://arxiv.org/abs/2410.11081); [rCM](https://arxiv.org/abs/2510.08431) adds score distillation as a regulariser | no |
| Rectified-flow reflow | regenerate noise-image pairs with the teacher and retrain on straight lines between them | [Rectified flow](https://arxiv.org/abs/2209.03003), [InstaFlow](https://arxiv.org/abs/2309.06380) | no |
| Adversarial | fool a discriminator on its samples | [ADD](https://arxiv.org/abs/2311.17042) (DINOv2 features), [LADD](https://arxiv.org/abs/2403.12015) (the teacher's latent features; SD3-Turbo in four unguided steps) | not stated |
| Distribution matching | produce samples the teacher's score cannot tell from its own | [DMD](https://arxiv.org/abs/2311.18828), [DMD2](https://arxiv.org/abs/2405.14867), [SenseFlow](https://arxiv.org/abs/2506.00523) | **both** |
| Guidance distillation | reproduce the two-pass guided output in one pass, with the scale as an input | [Meng et al.](https://arxiv.org/abs/2210.03142) | moot: no CFG to remove |

The first three are trajectory methods. They care which image a given noise becomes, which is the
dashed straight path in the toy. Distribution matching does not. DMD keeps two score estimates:
the teacher's, and a "fake" one trained online on the student's own outputs. It re-noises a student
sample, asks both where it should go, and pushes the student along the difference, which is the
gradient of the reverse KL divergence $\mathrm{KL}(p_\text{student} \,\|\, p_\text{teacher})$. DMD2 dropped
the first paper's regression loss on teacher-generated pairs, added a two-time-scale update for
the critic and a GAN term on real images, and made the student multi-step by simulating its own
inference during training. The failure this family invites is mode collapse, because reverse KL
is mode-seeking; the rCM paper reports matching DMD2's quality "while mitigating mode collapse".

**Viggle** says what it did (reported): "DMD2 (Yin et al., 2024) / SenseFlow-style distribution
matching", plus SenseFlow's intra-segment guidance, in which "the student's velocity over each of
its segments is regressed on the teacher's", with teacher targets made from rewritten prompts. The
weights are the step-700 EMA student of a run named `v6_isg`, trained on a 4-step schedule. Whether
a GAN term survived, the card does not say.

It also measured the collapse. Its "sample diversity" is the mean DINOv2 patch distance between 8
seeds of one prompt, over 32 prompts, as a ratio to the base model. The v0.1 students scored
**0.75** (LoRA) and **0.72** (full fine-tune): seeds landing on near-identical layouts. v0.2.1
scores **0.98**. These are Viggle's numbers on its own prompts, and the most useful on either
card: they test for the failure the method is known for.

**Pruna** says one sentence: "Training is based on DMD." No loss terms, no teacher settings, no
code. The sentence after it, "Improved using Qwen", reads like a method and is a licence
obligation: clause 4(b) of the Qwen Research License says anyone who uses the model or its outputs
to train or improve a model they distribute "shall prominently display 'Built with Qwen' or
'Improved using Qwen'". Viggle's card uses the other phrase.

## What is actually in each repository

Measured from the safetensors headers:

| File | Rank | Alpha | Tensors | Parameters | Share of denoiser | Targets | Size |
|---|---:|---:|---:|---:|---:|---|---:|
| Viggle v0.2.1, 6-step | 256 | 256 | 454 | 679,542,784 | 9.55% | q, k, v, out, 3 MLP, shared modulation, timestep embedder | 1.36 GB |
| Viggle v0.2.1, rank cut | 128 | 128 | 454 | 339,771,392 | 4.78% | same | 0.68 GB |
| Viggle v0.1 LoRA, 4-step | 64 | 64 | 454 | 169,885,696 | 2.39% | same | 0.34 GB |
| Viggle v0.1 `transformer/` | full | n/a | 297 | 7,115,124,736 | 100% | every weight | 14.23 GB |
| Pruna v0.1, 8-step and 5-step | 64 | 128 | 448 | 167,772,160 | 2.36% | q, k, v, out, 3 MLP | 0.34 GB each |

There is no full-checkpoint route any more. Viggle shipped one as v0.1 on 22 September, a
fine-tune of all 7,115,124,736 weights beside a rank-64 LoRA, moved to a rank-256 LoRA a day later,
and now says of the old files "there is no reason to prefer them". I compared the first 64 KB of
seven of the full fine-tune's tensors with the base's, and every one had changed. The share of bf16 values that differ is 31% in block 0's query
projection, 56% in block 31's MLP output, 75% in the shared modulation and 84% in the timestep
embedder (measured). The step-conditioning path moved most. That is the part a sampler that only
ever sees four noise levels has most reason to relearn (reasoned).

The two adapters sit on the same 32 blocks and differ in two ways.

- **Scope.** Viggle's also reaches the shared `modulation.1` projection, 4,096 to 16,384, which
  every block slices its scales and gates from, and both timestep-embedder layers. Pruna's touches
  only the attention and image-MLP projections inside the blocks, so it can change what the blocks
  do with the noise level but not how the model reads it.
- **Rank.** Viggle's is four times Pruna's. Its card also ships a rank-128 cut made by per-layer
  SVD, keeping 83–100% of each update's energy. On the v0.2 pair, the card reports, images from
  the cut differ from the rank-256 adapter's by LPIPS 0.0244, against 0.0239 for re-factoring the
  rank-256 adapter at full rank, which changes nothing but round-off.

Pruna's alpha is twice its rank, so "keep the LoRA strength at 1.0" applies the update at scale 2.

Why not merge the update into the weights and pay nothing per step? Viggle measured that too:
round-to-nearest into bf16 keeps about 70% of its update on average, and about 40% in some early
MLP layers, because the update is small against a bf16 weight's rounding step (reported). Both
cards therefore run the adapter unmerged, as a side branch $Wx + BAx$ on every call. That is exact,
and it is not free; the speed section finds the bill.

## Both students run the same clock

The schedules are where the two releases turn out closest. Viggle passes raw nodes
`[1.0, 0.9375, 0.875, 0.75, 0.5, 0.25]` through the pipeline's own resolution-dependent shift.
Pruna switches the shift off and passes nodes already shifted, $2t/(1+t)$ on evenly spaced $t$: a
fixed shift of 2. At 1024 x 1024 the pipeline's own shift is $e^{\mu} = 2.0008$, so the two land
on one grid.

<ScheduleRuler />

Three things follow, reasoned from the configs.

- **Both spend extra steps at the high-noise end.** Viggle trained on `linspace(1, 1/4, 4)` and
  samples with its first segment cut into three. Pruna's 5-step schedule is the 4-step shift-2 grid
  plus one node at 0.94. Viggle's card says why: "The composition is
  decided between raw sigma 1 and 0.875", and one big Euler step there "ghosts and drifts the
  layout". Its 5-step launch schedule changed the layout on 4% of 96 held-out requests; 6 steps
  changed none (reported). A raw 0.875 becomes 0.933 after the shift, so the decisive span is the
  first 6.7% of the noise range by the pipeline's formula; the card says "roughly the first 5%".
- **Both switch off `shift_terminal`.** The base config stretches its schedule so the last non-zero
  node is 0.02. Applied to a student whose last node is 0.4, the stretch would move every node, and
  Viggle's card says it "would wreck the last step".
- **At 2048 they part.** Viggle's nodes follow the pipeline's shift to 3.72 and spend more of the
  budget at high noise, as the base does. Pruna's stay at shift 2, the schedule it trained at 1K.
  Its card calls 2K outside the training coverage and says the 2K timings "show inference speed,
  not a guarantee of quality".

## Checking the speed claims

Both claims are end-to-end, and they are not on the same GPU.

- **Viggle** (reported): 32 of the 37 examples in Qwen's own demo Space, rendered once by the base
  at 40 steps and once by v0.2.1 at 6, same prompt, inputs and seed 42, on one NVIDIA B200, timing
  "text encoding, denoising and VAE decoding" after warm-up. Text-to-image outputs are about 4 MP,
  edits 1536² in area.
- **Pruna** (reported): the card's own prompt, BF16, batch 1, on one H100 80GB, median of 3 after a
  warm-up, at 1024² and 2048². Base at 40 steps with the KV cache **on**; adapters with it **off**,
  unmerged, no compilation.

<SpeedLedger />

<div className="grid gap-x-4 sm:grid-cols-2">

<Figure
  src="/articles/qwen-image-2-1-few-step/fig3.png"
  alt="A dark bar chart titled Text-to-Image Generation Latency at 1024 by 1024 on one H100 80GB, BF16, batch size 1. Qwen-Image-2.1 base at 40 steps takes 6.10 seconds; the 8-step LoRA takes 1.61 seconds, labelled 3.8 times faster; the 5-step LoRA takes 1.06 seconds, labelled 5.7 times faster."
  caption="At the training resolution: 3.8x for the recommended 8-step adapter (Pruna-Qwen-Image-2.1 model card, Figure 1)."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/fig4.png"
  alt="The same chart at 2048 by 2048. The base at 40 steps takes 31.44 seconds; the 8-step LoRA 7.60 seconds, 4.1 times faster; the 5-step LoRA 4.98 seconds, 6.3 times faster."
  caption="Where the 6.3x comes from: the 5-step adapter at 2K, outside its training coverage (Pruna-Qwen-Image-2.1 model card, Figure 2)."
/>

</div>

**Viggle's "about 5x" holds.** Summing all 32 pairs gives 633.7 s against 126.3 s, **5.02x**
(measured, from `cases.json`). By task it is 5.55x for text-to-image (median 26.1 s against 4.7 s),
83% of the 6.67x ceiling, and 4.7x for single-reference edits (median 14.2 s against 3.0 s).

**Pruna's "up to 6.3x" is one cell of eight.** It is the 5-step adapter at 2048², 31.44 s against
4.98 s: the adapter its card calls "visibly worse", at a resolution its card says it did not train
at. The recommended default, the 8-step adapter at 1K, is **3.8x** for text-to-image and **3.5x**
for editing (reported). Every ratio on the four charts reproduces from its bar labels, with one
rounding slip: 6.10/1.06 is 5.75, charted as 5.7x.

**The adapters' steps cost more than the base's.** Pruna's 5- and 8-step runs share their fixed
work, so their difference is three steps. That gives the adapter's per-step cost, the fixed
encode-and-decode cost, and from those the base's per-step cost (reasoned; tick "per-step fit"
above). At 1K an adapter step is 0.183 s against the base's 0.149 s, **23% dearer**; at 2K, 0.873 s
against 0.771 s, 13%. The arms differ in the unmerged side branch and the KV cache. The branch adds
only 2.4% to the multiply-accumulates of the seven projections it wraps, and in text-to-image the
cached prefix is just the prompt, so most of the gap is not FLOPs. My guess is the fourteen small
matmuls per block, each rereading its input; nothing published separates the causes. At the base's
per-step cost, the 5-step run at 1K would take about 0.89 s: **6.9x** rather than 5.75x.

**Editing gains less, and the KV cache is why.** The base caches the keys and values of the prompt
and reference images after its first step ([the mechanism](/articles/qwen-image-2-1#block-causal-attention-and-why-the-cache-is-exact),
and [how it measured](/articles/qwen-image-2-1#the-cache-as-three-other-people-measured-it)). Its
first step is a full pass over references and target; the other 39 run the target only. A 6-step
student gets five cheap steps, so the full step and the reference encoding are a far larger share
of its time. Viggle's worst case shows it: an edit with ten references, about 41,000 condition
tokens against 9,216 target tokens, is 36.6 s against 9.9 s, **3.7x**. Solving those two timings
for a fixed cost and a per-step cost puts 5 to 6 s of the student's 9.9 s outside the loop
(reasoned; the range is whether a student step costs the same as a base step or 15% more).

Pruna's edit rows hold a small puzzle. The fitted edit step at 1K is only 5.5% dearer than the
text-to-image step, yet an uncached edit with a megapixel reference would process twice the tokens.
Either those runs had the cache on, as the card's examples do, or the reference was small. The card
does not say (reasoned).

## What the quality claims measure

Viggle's card says the student is "very competitive with it in quality: on the official Qwen
examples the two are hard to tell apart on most prompts". The evidence is of two kinds. The first
is the comparison set: 32 pairs, one sample each, no seed picking. I looked at two of the five
dense-text examples, where the card says the student is weakest.

<Figure
  src="/articles/qwen-image-2-1-few-step/fig1.png"
  alt="Two nearly identical architecture presentation boards for Olavinlinna Castle side by side: an aerial view of a stone castle on a rocky island, an elevation with dimension lines, detail panels and Chinese labels. Layout, castle and panels match; the right one is slightly softer."
  caption="Same prompt, same seed, same layout. Left: Qwen-Image-2.1 at 40 steps, 27.6 s. Right: viggle-turbo v0.2.1 at 6 steps, 4.9 s. Both on one B200 at 2048 x 2048; the side-by-side layout is mine (Viggle demo Space, Comparison tab, architecture presentation board)."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/fig2.png"
  alt="Three crops of the same three-line figure caption of generated small print. Top, clean and fully legible. Middle, the first two lines are doubled and smeared like a double exposure, with several words unreadable. Bottom, the first line is mostly clean again and the second line is still partly garbled."
  caption="Small Latin text at native resolution. Top: base, 40 steps. Middle: v0.2.1 at 6 steps, the double exposure Viggle's notes describe. Bottom: v0.2.1 at 8 steps. Crops are mine (Viggle demo Space, Comparison tab, academic infographic)."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/fig6.png"
  alt="Three enlarged crops of a numbered Chinese label reading 第一庭院 next to a castle tower. Top and bottom, the four characters are clean black strokes. Middle, the last character is covered by a green blotch."
  caption="Small Chinese strokes. Top: base, 40 steps. Middle: 6 steps, a colour blotch on the last character. Bottom: 8 steps, clean. Crops enlarged 2x by me (Viggle demo Space, Comparison tab, architecture presentation board)."
/>

Both failures are the ones Viggle's own notes in the Space describe, to its credit: at 6 steps
"small Latin text can print twice, like a double exposure (decided in the 0.75 → 0.5 step), and
small Chinese strokes can get colour blotches (the last 0.25 → 0 step)". Its OCR check puts numbers
on them. Word recall on the infographic's caption rises from 0.71 at 6 steps to 0.83 at 8 over 16
seeds, and on the board's Chinese labels from 0.76 to 0.90 over 4 seeds, against 0.95 for the base
on both (reported).

The second kind is the metrics table: sample diversity, centroid drift and the share of prompts
whose layout differs from the base's, over 96 held-out user requests and 32 prompts, plus
Laplacian sharpness (0.0199 for v0.2.1, 0.0187 for v0.2). Every one measures distance from the
teacher. None measures whether the picture does what the prompt asked. The card is plain about it:
"no standard benchmark is claimed", and "2K output is compared with the base model only by eye".
Its numbers were also taken with Qwen's prompt rewriters on, which
[the piece on the pocket rewriter](/articles/qwen-image-2-1-pocket-rewriter) found made rendered
images less faithful to the request, not more.

Pruna publishes no quality number at all. Its card says v0.1 "does not yet match the visual quality
of the base model", that the 5-step adapter's images are "visibly worse", and that short prompts
do worse. The pictures it ships have no base-model counterpart and no stated settings.

<Figure
  src="/articles/qwen-image-2-1-few-step/fig5.png"
  alt="A grid of image edits joined by purple arrows and plus signs: a knitted dragon turned to gold and to ice, a runner's silhouette combined with a traffic policeman, a figure moved into a night scene with a helicopter, a skier turned into a surfer, a flame-haired portrait turned to frost, a man in a suit made broader, a felt puppet given a smile, sunglasses and a hat, and a woman's helmet and suit restyled in knitted dragon wool."
  caption="Pruna's editing samples, including two-reference compositions. The card does not say which adapter, step count or resolution produced them, and shows no base-model output beside them (Pruna-Qwen-Image-2.1 repository, edit_grid.jpg)."
/>

The rest of the checklist. Both edit: Viggle trained with 1 to 3 references and says 5, 6 and 10
references, RGBA output and mask-guided edits "work … but are not measured"; Pruna trained single-
and multi-image editing at 1K with up to three. Both run at `true_cfg_scale=1.0` with no negative
prompt, and Viggle's card adds that CFG does not help. Both `LICENSE` files are byte-identical to
Qwen's (measured): the Qwen Research License, non-commercial, "for research or evaluation purposes
only".

## Pruna's adapter on four CPU cores

Every timing above is someone else's, on a B200 or an H100. The release piece ran the base model
[on the four-core Xeon that builds this site](/articles/qwen-image-2-1#running-it-on-four-cpu-cores-with-no-gpu-at-all),
with no GPU at all: a 512² image at the vendor's 40 steps took about an hour. A few-step adapter is
the obvious thing to try there. I ran Pruna's, because its pair gives a 5-step and an 8-step point
on one base; at 90 seconds a step, every image here is ten minutes or more of the box.

The setup is the release piece's: stable-diffusion.cpp at commit `2bb7294`, leejet's Q4_K GGUF of
the denoiser, Qwen's Q4_K_M encoder, 512 x 512, Euler, guidance off, seed 42, four threads. The
sigmas are Pruna's own lists, passed verbatim with no shift on top:
`1.0, 0.9333333, 0.8571429, 0.7692308, 0.6666667, 0.5454545, 0.4, 0.2222222, 0` for 8 steps and
`1.0, 0.94, 0.8571429, 0.6666667, 0.4, 0` for 5. LoRA strength 1.0, as the card says.

### The adapter does not load as shipped

Loaded as published, sd.cpp applied 320 of the file's 448 tensors, and logged as much. Two things
do not line up.

- **The MLP names.** leejet's GGUF fuses the image MLP's two input projections,
  `img_mlp.gate_layer` and `img_mlp.proj`, into one `img_mlp.gate_up` matrix, gate rows first. The
  adapter's 128 tensors for those layers, 32 blocks times two layers times A and B, name weights
  the file does not have.
- **The scale.** PEFT scales an update by alpha over rank, 128/64 = 2, and keeps alpha in the
  file's metadata. sd.cpp reads a scale only from an `.alpha` tensor and otherwise uses 1, so even
  the matched layers would get half the update.

The fix is a rewrite of the file. Multiply every B by 2, a power of two and so exact in bf16. For
each block, stack the gate and up A matrices into one rank-128 A, and put the two Bs on the diagonal
of one B, so the product is the gate update over the up update. The rewritten adapter has 384
tensors, sd.cpp applies all 384, and the largest difference between its updates and PEFT's, over
all 32 blocks, is 0.0 (measured). It is 436 MB against 0.34 GB, because the block-diagonal B stores
its zeros. Half of this is already fixed upstream: a stable-diffusion.cpp change merged on 25
September, [#2057](https://github.com/leejet/stable-diffusion.cpp/pull/2057), maps the two MLP
names onto the fused weight. The scale is still read only from an `.alpha` or `.scale` tensor, so a
PEFT file still needs its B matrices doubled by hand ([the stable-diffusion.cpp piece](/articles/stable-diffusion-cpp#a-lora-that-loaded-320-of-its-448-tensors)
has the code path).

### What it costs

| Run | Box | Per step | Sampling | VAE decode | Total | Peak RSS |
|---|---|---:|---:|---:|---:|---:|
| Base, 40 steps (release piece) | shared | 87.09 s median | | 46.06 s | 3,863 s | |
| Pruna, 8 steps | quiet | 89.89–91.64 s | 725.60 s | 39.56 s | 771.68 s | 11.59 GiB |
| Pruna, 5 steps | shared | 98.42–124.30 s | 532.66 s | 76.11 s | 615.85 s | 10.23 GiB |
| Base, 8 steps on Pruna's sigmas | shared | 90.39–127.90 s | 829.02 s | 66.00 s | 900.86 s | 10.36 GiB |

"Quiet" means the load average stayed near the job's own four threads; "shared" means other work,
mostly other agents building this site, was on the box. The quiet run is the one to read.

- **The 8-step adapter is 4.6x faster than the base at its own settings.** 771.68 s against the
  release piece's arithmetic figure of 3,535 s for 40 clean steps, or 5.0x against its 3,863 s
  wall-clock. Steps alone allow 5x; the 46.08 s outside the loop, mostly the VAE decode, is the
  difference (measured, and reasoned from the release piece's medians).
- **An adapter step costs what a base step costs.** The quantised weights mean sd.cpp cannot merge
  the update, so its `auto` mode applies it at runtime, as the unmerged side branch the cards
  describe. The quiet run's eight steps took 89.89 to 91.64 s; the base's first two steps in the
  control, before the box filled up, took 90.39 and 90.50 s, and the release piece's median base
  step was 87.09 s. The branch adds 2.4% to the multiply-accumulates of the projections it wraps,
  which is inside this box's run-to-run spread. On the H100, [Pruna's own numbers](#checking-the-speed-claims)
  put an adapter step 23% above a base step. On a CPU that does the arithmetic and little else, the
  premium disappears, which fits the reading that the GPU's gap is not FLOPs (reasoned).
- **The 5-step run only looks slow.** It ran on a shared box, and its steps were 8 to 37% slower
  than the quiet run's. At the quiet run's 90.70 s a step and 46.08 s outside the loop, 5 steps is
  about 500 s, 7.1x the 40-step base (reasoned).

### What it draws

<div className="grid gap-x-4 sm:grid-cols-2">

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-512-p8.png"
  alt="A classroom chalkboard in a wooden frame, lit from a window on the left. Four lines of white chalk read '4 CPU', 'cores,', 'no', 'GPU', every word and the comma correct. A small potted plant sits on the ledge at the lower right."
  caption="Pruna's 8-step adapter, rewritten for sd.cpp: Q4_K, 512², guidance off, seed 42. Generated by me on four CPU cores, 771.68 s. Every word right."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-512-base8.png"
  alt="The same chalkboard composition, slightly flatter and darker: four lines reading '4 CPU', 'cores,', 'no', 'GPU', a plant on the ledge at the lower left and a desk in the foreground."
  caption="The control: the base model with no adapter, the same 8 sigmas, seed and settings. Generated on CPU, 900.86 s on a shared box. Every word right too."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-512-p5.png"
  alt="A chalkboard seen at an angle, with a window on the left, a plant on the sill and a wooden desk in the foreground. Four lines read '4 CPU', 'cores,', 'no', 'GPU', correct, with the lettering slightly tilted."
  caption="Pruna's 5-step adapter, the same settings on its 5 sigmas. Generated on CPU, 615.85 s on a shared box. Correct, in a different composition."
/>

<Figure
  src="/articles/qwen-image-2-1/cpu2-q4k-512-s40-cfg1.png"
  alt="A framed chalkboard by a window, four lines reading '4 CPU', 'cores,', 'no', 'GPU', a plant on the sill and desks in the foreground."
  caption="The reference: the base at 40 steps on the vendor's schedule, from the release piece. Generated on CPU, 3,863 s."
/>

</div>

All four get every word and the comma. The chalkboard is easy enough that the base at 8 steps on
Pruna's schedule gets it too, so on this prompt the adapter does not buy legibility. The release
piece's 8-step runs were unreadable, but those were at Q2_K on the vendor's schedule; two things
changed at once, so the pair says nothing about which one mattered.

What the adapter buys is finish. On the grey-level images, its 8-step picture has the 40-step base's
contrast, a standard deviation of 60.6 against 60.7, and nearly its sharpness, a Laplacian variance
of 512 against 524. The base at 8 steps is flatter and softer, at 56.6 and 467 (measured). That is
one prompt, one seed and two crude statistics: evidence that the distillation did its job here, and
not a quality benchmark.

<div className="grid gap-x-4 sm:grid-cols-2">

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-512-poster-p8.png"
  alt="A minimalist cream poster. Large bold black capitals read 'EDGE AI SUMMIT', a line below reads 'Bengaluru, 14 November 2026', and three words along the bottom read 'Talks', 'Workshops', 'Demos'. All text is correct."
  caption="A layout prompt with the 8-step adapter: 512², guidance off, seed 42. Generated on CPU, 1,802.70 s on a busy box. Every word right."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-512-poster-base8.png"
  alt="The same poster with smaller, lighter type: 'EDGE AI SUMMIT', 'Bengaluru, 14 November 2026', and 'Talks', 'Workshops', 'Demos' along the bottom, all correct."
  caption="The base at the same 8 sigmas, no adapter. Generated on CPU, 1,738.58 s on a busier box. Every word right, in thinner type."
/>

</div>

The poster repeats the pattern. Both spell everything, the date included. The adapter sets it
larger and heavier, and its grey-level standard deviation is 40.8 against 28.6. Both leave the
middle empty, which is what "minimalist" asked for. Their wall-clocks are twice the quiet run's
because the box was running other jobs; they measure the box, not the model.

### At the adapter's own resolution

512² is a quarter of the pixels Pruna trained at, so the last four images are at 1024 x 1024 with
the same rewritten 8-step adapter, guidance off, seed 42, and `--vae-tiling`, which decodes the
latent in tiles instead of all at once. The prompts are longer on purpose: a scene with signage,
a dense illustration with a line of text, a labelled diagram, and a photograph with fine texture.

Only the hummingbird had the box nearly to itself, so it is the one to time. Its eight steps took
370.47 to 377.67 s, 4.1 times a 512² step for 4 times the pixels, and the whole image 3,303.93 s,
55 minutes, with a peak RSS of 11.88 GiB. The other three shared the box and took 4,551.41 to
8,581.52 s; those times measure the neighbours as much as the model. At the same step cost, 40 base
steps at 1024² would take about 15,240 s, over four hours, so the adapter's 4.6x holds at this size
too (reasoned: the base was not run at 1024² here).

<div className="grid gap-x-4 sm:grid-cols-2">

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-1024-neon-alley.jpg"
  alt="A rain-wet alley at night. Above a small shop a neon sign reads 'CHAI & CODE' in orange tubes, with 'open till 2 am' in blue beneath it. Steam rises from a brass kettle on a stall, fairy lights zigzag overhead, pink and cyan light reflects off the paving, and a ginger cat sleeps on a stack of wooden crates."
  caption="'CHAI & CODE' and 'open till 2 am', both exactly as asked, with the kettle, the steam, the fairy lights and the cat on the crates. Pruna's 8-step adapter at 1024², generated on four CPU cores, 4,551.41 s on a shared box."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-1024-diffusion-poster.jpg"
  alt="A cream poster titled 'HOW A DIFFUSION MODEL PAINTS'. Four tall panels in a row joined by arrows, labelled '1. Pure noise', '2. Rough shapes', '3. Colours settle' and '4. Fine detail', each showing a snowy mountain above a lake with pine trees. The first panel is grainy; the other three are finished paintings with different skies."
  caption="Every word right: the title and all four labels. The pictures do not show what the labels say. Panel 1 is the finished scene under grain, and panels 2 to 4 are the same finished painting with different skies. 1024², 8 steps, 4,671.67 s on a shared box."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-1024-cafe.jpg"
  alt="An isometric cutaway of a small bookshop cafe. On the ground floor a barista stands behind a counter with glass pastry cases under a chalkboard reading 'TODAY: CARDAMON BUN'. Above, a balcony with bookshelves and ladders, and a rooftop with potted lemon trees, string lights and a green armchair where a dog sleeps."
  caption="One letter wrong: the prompt asked for 'TODAY: CARDAMOM BUN' and the board says CARDAMON. The small menu boards are not words, and the reading nook with the sleeping dog ended up on the roof beside the lemon trees. 1024², 8 steps, 8,581.52 s on a busy box."
/>

<Figure
  src="/articles/qwen-image-2-1-few-step/cpu-1024-hummingbird.jpg"
  alt="A hummingbird hovering beside a single red hibiscus flower, wings spread, a green back and a ruby throat, with a small drop of nectar on the tip of its beak, against a plain olive-green background."
  caption="Sharp feathers, the ruby throat and the drop on the beak; one hibiscus rather than a cluster, and a flat olive ground rather than the creamy bokeh asked for. 1024², 8 steps, 3,303.93 s on a nearly quiet box."
/>

</div>

Across the four, text holds up at 8 steps: the two strings on the sign, the poster's title and four
labels, and all but one letter of the chalkboard. What slips is composition. A prompt with more
parts than the picture makes room for loses some of them: the poster's four stages collapse into
one finished painting, the café's reading nook moves to the roof, and the hummingbird gets one
flower instead of a cluster. One seed each, so this is a look at the adapter, not a measurement
of it.

## The take

The step count is the whole story here. Qwen-Image-2.1 had already dropped classifier-free
guidance by default, so there was no second factor for distillation to remove. A 6-step student can
be at most 6.67x faster, and an unmerged adapter gives some of that back on every step. Viggle's
5.02x on a B200 is three quarters of that ceiling, measured on Qwen's own examples, and its card
names the price: small text, complicated edits and seed diversity, the last of which DMD is known
to spend.

Pruna's release is a v0.1 and says so; the headline does not. "Up to 6.3x" is the adapter the card
calls visibly worse, at a resolution it did not train at, and the recommended configuration is
3.8x. The card is precise about its timing method, down to which arm had the KV cache on, and
publishes nothing about image quality.

The finding I would carry away is the schedule. Two teams, distilling the same model separately,
put their students on the same shift-2 grid and spent their extra steps at the high-noise end,
where the composition gets decided. A few-step release for this model that lands its nodes
elsewhere will be worth reading closely.

<ChangeMyMind>
  <Falsifier claim="The 40-step Qwen-Image-2.1 baseline runs one forward pass per step, so a few-step student's speedup is bounded by 40 / N.">
    Rests on the pipeline default `true_cfg_scale=1.0`, the card's examples and the tensor names.
    A Qwen-recommended setting with guidance on, or either baseline timed with CFG, would double the
    baseline and change every ratio here.
  </Falsifier>

  <Falsifier claim="Both releases are distribution-matching (DMD-family) LoRAs, not full checkpoints.">
    Rests on both cards and every adapter header. A new recommended full-weight file, or training
    code whose main loss is a consistency or adversarial objective, would change it.
  </Falsifier>

  <Falsifier claim="Unmerged adapter steps cost 13-23% more than base steps in Pruna's runs, mostly not because of the adapter's FLOPs.">
    A two-point fit on medians of three, assuming both arms share one fixed cost. A merged or
    cached 5-step timing at 1024² near 6.9x would confirm it; one still near 5.75x would put the
    overhead somewhere else.
  </Falsifier>

  <Falsifier claim="Viggle's quality metrics measure distance from the teacher, not prompt adherence.">
    The card's own words and metric definitions. A GenEval, DPG or Qwen-Image-Bench run on either
    student against the 40-step base would replace this with an actual number.
  </Falsifier>
</ChangeMyMind>

---

*Sources, read directly: [`Viggle/Qwen-Image-2.1-viggle-turbo`](https://huggingface.co/Viggle/Qwen-Image-2.1-viggle-turbo)
(card, `NOTICE`, `LICENSE`, scheduler and adapter configs, `comfyui/viggle_turbo.py`, and all nine
safetensors headers by range request) and its
[demo Space](https://huggingface.co/spaces/Viggle/Qwen-Image-2.1-viggle-turbo) (`compare.py`,
`compare/cases.json`, the renders); [`PrunaAI/Pruna-Qwen-Image-2.1`](https://huggingface.co/PrunaAI/Pruna-Qwen-Image-2.1)
(card, `LICENSE`, both adapter headers, the latency charts);
[`Qwen/Qwen-Image-2.1`](https://huggingface.co/Qwen/Qwen-Image-2.1) (card, `LICENSE`, configs, shard
headers); the diffusers `QwenImage21Pipeline` at the commit Viggle pins; and the Qwen-Image and
Qwen-Image-2512 cards. Figures 3 to 5 are Pruna's and 1, 2 and 6 Viggle's, served locally; the
crops and layouts of 1, 2 and 6 are mine.*
