qwen-image-mnn
Run Qwen-Image-2.1 text-to-image on-device with the MNN weights — no torch, no
diffusers, no 33 GB checkpoint. One command through uvx:
uvx qwen-image-mnn "a red panda riding a bicycle" --steps 40 --out panda.png
The CLI downloads the converted MNN weights on first use (~30 GB for --quant fp16) into
~/.cache/qwen-image-mnn/<quant>, then renders offline. The official pipeline defaults are used
unless you say otherwise: 40 steps, 1024 px, true_cfg_scale=1.0.
What runs where
| piece | graph | fp16 | int8 | int4 |
|---|---|---|---|---|
| Qwen3-VL text encoder (language model, text-only path) | text_encoder.mnn |
15.1 GB | 11.2 GB | 7.7 GB |
| Qwen-Image-2.1 DiT (32 blocks, 4096 dim) | dit.mnn |
14.2 GB | 8.9 GB | 5.3 GB |
| VAE (decoder + encoder) | vae_decoder.mnn, vae_encoder.mnn |
1.3 GB | 1.3 GB | 1.3 GB |
| download | 30.7 GB | 21.4 GB | 14.4 GB |
int8/int4 are weight-only quantizations (mnnconvert --weightQuantBits … --weightQuantBlock 32 --hqq) of the same graphs; the VAE stays fp32 at every level. int8 stays inside the model's own
dtype noise — the fp16-vs-bf16 reference run drifts 6.3e-3 → 2.0e-1 while int8 drifts
1.4e-3 → 2.4e-2, image PSNR 52 dB against the reference render (fp16: 43.6 dB). int4 is
measurably worse (4.4e-1, PSNR 28.5 dB): use it when 7 GB matters more than the difference.
Every graph is exported from the released diffusers checkpoint to ONNX and then converted with
mnnconvert. The tokenizer, the flow-match scheduler and all the joint-sequence bookkeeping
(3-axis RoPE, block-causal mask, modulation rows) are plain numpy in this package, because they
are cheap — the transformer itself is the only part that needs MNN.
Install / run
uvx qwen-image-mnn --help # from PyPI
uvx --from /path/to/qwen-image-mnn qwen-image-mnn "a photo of a cat" --seed 7 # or a checkout
Options that matter:
--quant fp16 weight precision to download: fp16 | int8 | int4
--size 1024 square side in pixels (or --height/--width)
--steps 40 denoising steps (40 is the released pipeline's default)
--seed 0 seed for the initial latents
--true-cfg-scale 1.0 >1 with --negative-prompt enables classifier-free guidance
--backend cpu the only backend the weights were verified on; metal/opencl are
experimental on MNN 3.6.1/macOS
--precision high keep this: `low` swaps the CPU kernels to fp16 *arithmetic*, which
overflows the 32-block residual stream (absmax 2.6e36, output full of
inf). `high` and `normal` are bit-identical, so there is nothing to gain.
--model-dir DIR use a local converted-model directory instead of downloading
--latents X.npy start from fixed packed latents (reproducible comparisons)
--dump-latents X.npy save every step's latents
--prompt-embeds X.npy supply prompt embeddings (e.g. saved from the reference pipeline) and
skip the text encoder -- this isolates the DiT + VAE when comparing
Speed
CPU only (Metal numbers below), precision=high, 10 threads, on this 15-core Mac. These are warm
numbers — the minimum of repeated calls inside one process, which is what a multi-step render
actually pays:
| stage | fp16 | int8 | int4 |
|---|---|---|---|
| text encoder, one 28-token prompt | 0.39 s | 0.38 s | 0.38 s |
| DiT step, 512 px (T=1039) | 10.1 s | 10.4 s | 10.4 s |
| 40 steps + VAE, 1024 px (official default) | 80 min | — | — |
Weight-only quantization buys size, not speed: the weights are dequantized as they are paged in, so every precision runs the same multiply-accumulates and the three columns land inside noise of each other. What the first call pays instead is MNN's per-process dynamic-shape planning — ~23 s to resize the DiT session plus ~35 s for the first step (against 10 s warm), and ~20–30 s for the text encoder's first prompt (against 0.4 s warm). Quantization does not remove that either, though it does shrink it a little on the smaller graphs.
The 1024 px row is a real 40-step render (4817 s wall clock, ~120 s per step at T=4111); it was measured on a machine carrying other work, so treat it as a ballpark.
How the numbers were verified
Each graph was run against its source with identical inputs:
| graph | vs its ONNX source | vs the released model |
|---|---|---|
| text encoder | 8.4e-4 | 6.0e-3 |
| DiT | 1.7e-2 (max-rel, T=1120) | 3.0e-2 (vs a torch fp16 forward, T=4192) |
| VAE decoder | 1.0e-3 … 4.2e-3 | — |
| VAE encoder | 2.1e-3 … 3.6e-3 | — |
End to end, with both sides starting from the same latents: the MNN pipeline's latents drift
5.0e-3 (step 1) … 3.1e-2 (step 8) relative to the released pipeline's own fp16 run, image
PSNR 43.6 dB. The released model compared against itself across dtypes (fp16 vs bf16, same
seed and latents) drifts up to 2.0e-1 in the same job — the conversion is closer to the model
than the model is to itself once the dtype moves. tools/ref_noise_floor.sh reproduces that
floor, tools/compare_e2e.py is the comparison and its tolerance is sized from it.
The fp32 VAE is what the published numbers were measured with; the fp16 VAE variants kept in
tools/ are for the record only (the decoder holds PSNR 43 dB, the encoder's mean error grows
to 9e-3).
Text-only conversion notes, the text-encoder bisect (block-level taps that found the MNN
importer bug) and the fixups that rewrite the opset-18 dynamo output into the opset-17 shape
MNN's importer actually reads correctly live in tools/ (this package's sibling repo):
export_dit_onnx.py, export_text_encoder_onnx.py, export_vae_onnx.py, onnx_fixups.py.
Current scope
Text-to-image (T2I) on the CPU backend. Image editing (the "创改一体" path) needs the vision tower and the deepstack merge, which is not part of these graphs yet.
Metal on macOS 26 with MNN 3.6.1 miscomputes: the Metal tensor-API capability probe fails to
compile against the new MetalPerformancePrimitives headers and the fallback kernels return
garbage. MNN master fixes the probe, and a locally built master library computes all three graphs
correctly on Metal (VAE max rel 1.5e-3, DiT 2.2e-3) — but only the VAE gets faster (3.1x); the
DiT is ~4x slower than CPU in the same probe, so CPU is the fastest full path
even once an upstream release ships the fix. The pip wheel cannot load a locally built
libMNN.dylib either, so none of this is reachable from this package today.
中文速览
uvx qwen-image-mnn "提示词" --steps 40 --out out.png 即可出图:首次运行按 --quant 把权
重下载到 ~/.cache/qwen-image-mnn/<quant>(fp16 30.7 GB / int8 21.4 GB / int4 14.4 GB),之后
完全离线。默认参数与官方一致:40 步、1024 px。pip 版 MNN 上请用默认 --backend cpu --precision high:low 精度会溢出,Metal 在 MNN 3.6.1 结果异常(上游 master 已修探针,但实测
只有 VAE 变快 3.1 倍,DiT 反而比 CPU 慢 4 倍,且 pip 包无法加载自编译库)。int8 与 fp16 同处于
模型自身 dtype 噪声之内,推荐替代;int4 有可测差异(PSNR 28.5 dB vs 52.0 dB),仅在体积敏感时
使用。量化只省体积不省时间:三档稳态步时相同(512 px 约 10.1/10.4/10.4 s),进程内首次调用还要
一次性付 MNN 的动态 shape 规划(DiT 约 23 s resizeSession + 35 s 首步),详见 Speed 一节。当前覆盖文生图;图生图/图像编辑还需要视觉塔与 deepstack 融合,尚未包含。
Release files for qwen-image-mnn 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| qwen_image_mnn-0.1.1.tar.gz | 16.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| qwen_image_mnn-0.1.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 37.0 kB
Release files / qwen_image_mnn-0.1.1.tar.gz
| Download URL | qwen_image_mnn-0.1.1.tar.gz |
|---|---|
| Size | 16.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b0f76186e47dfce890469608101f4c82b4499c69b6f251d91a7c0d9baf4cd679
|
|
BLAKE2b-256 checksum How to use checksums |
26e38d82e5cc3613199e2df18f3bbb8d0415939f71e459925b38a831c1fd1c85
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|
Release files / qwen_image_mnn-0.1.1-py3-none-any.whl
| Download URL | qwen_image_mnn-0.1.1-py3-none-any.whl |
|---|---|
| Size | 20.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
f9a72b39339538bb1dc4c888e833a4dab2a88398db93b5d47d1338e0f982a80e
|
|
BLAKE2b-256 checksum How to use checksums |
c0451b0955d973549f3ce018d4917d7ab8073b08684902372376c2005b638b1f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.12.5 {"installer":{"name":"uv","version":"0.12.5","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}
|