Image Generation & Editing
Qwen-Image-2.1 turns a prompt into an image, or a prompt plus one or more reference images into an edited image. This page covers the download, the run recipe, the defaults, and the settings that decide how long an image takes.
These commands follow the same conventions as the text-model recipes — the Hugging Face CLI, a source build from the repository root, and --backend swapped to match your hardware. See Download & run, per family.
Qwen-Image-2.1 (prompt → image, prompt + image → edited image)
The shortest route is the ready-made config/qwen-image-2.1.json. It pins repository revisions and SHA-256 checksums and downloads whatever is missing — four files, about 10.29 GiB — into $TENSORSHARP_MODELS/qwen-image-2.1/, or models/qwen-image-2.1/ in the repository when TENSORSHARP_MODELS is unset. The configuration selects ggml_metal; on an NVIDIA machine with the CUDA backend built, append --backend ggml_cuda; the native CPU backend is ggml_cpu, and --backend cpu runs everything in pure C# (see On a CPU).
dotnet build TensorSharp.Cli/TensorSharp.Cli.csproj -c Release
dotnet build TensorSharp.Server.Host/TensorSharp.Server.Host.csproj -c Release
# Text-to-image
dotnet run --project TensorSharp.Cli -c Release --no-build -- \
--config config/qwen-image-2.1.json \
--prompt 'A small orange cat beside a blue ceramic vase, soft daylight, detailed photograph' \
--width 2048 --height 2048 --diffusion-steps 40 --cfg 1 \
--diffusion-seed 42 --output generated.png
# Image editing
dotnet run --project TensorSharp.Cli -c Release --no-build -- \
--config config/qwen-image-2.1.json \
--image generated.png \
--prompt 'Change the blue vase to a red vase. Preserve the cat, lighting and composition.' \
--width 2048 --height 2048 --diffusion-steps 40 --cfg 1 \
--diffusion-seed 42 --output edited.png
# Server + Web UI
dotnet run --project TensorSharp.Server.Host -c Release --no-build -- \
--config config/qwen-image-2.1.json --host 127.0.0.1 --port 5000
To fetch the four files yourself instead, pass the companions explicitly:
hf download Abiray/Qwen-Image-2.1-GGUF qwen_image_2.1_Q4_K_M.gguf --local-dir models
hf download Comfy-Org/Qwen-Image-2.1 vae/qwen_image_2.1_vae_bf16.safetensors --local-dir models
hf download Qwen/Qwen3-VL-8B-Instruct-GGUF Qwen3VL-8B-Instruct-Q4_K_M.gguf mmproj-Qwen3VL-8B-Instruct-F16.gguf --local-dir models
dotnet TensorSharp.Server.Host/bin/TensorSharp.Server.Host.dll --model models/qwen_image_2.1_Q4_K_M.gguf \
--qwen-image-vae models/vae/qwen_image_2.1_vae_bf16.safetensors \
--qwen-image-vl models/Qwen3VL-8B-Instruct-Q4_K_M.gguf \
--qwen-image-mmproj models/mmproj-Qwen3VL-8B-Instruct-F16.gguf \
--backend ggml_cuda
| Component | File | Source |
|---|---|---|
| Diffusion transformer | qwen_image_2.1_Q4_K_M.gguf | Abiray/Qwen-Image-2.1-GGUF |
| Dedicated VAE | qwen_image_2.1_vae_bf16.safetensors | Comfy-Org/Qwen-Image-2.1 |
| Text encoder | Qwen3VL-8B-Instruct-Q4_K_M.gguf | Qwen/Qwen3-VL-8B-Instruct-GGUF |
| Vision encoder for editing | mmproj-Qwen3VL-8B-Instruct-F16.gguf | Same Qwen3-VL repository |
The download size is not peak inference memory: runtime memory also includes activations, decoding buffers and working weights, and larger images and multiple references increase it.
Other Qwen-Image-2.1 GGUFs load as well. A file with no metadata at all, such as Unsloth's qwen-image-2.1-Q8_0.gguf (tensors under a model.diffusion_model. prefix included), is recognized from its tensor layout — never from its file name. The companion scan matches a qwen_image_2.1_vae safetensors file and names containing qwen3vl-8b or qwen3-vl-8b, so a projector saved as mmproj-BF16.gguf needs --qwen-image-mmproj. On an M5 Pro the Unsloth Q8_0 file generated 1024×1024 at 40 steps in 337.7 s, against 383.1 s for stable-diffusion.cpp in the same single-run comparison. Only 2.1 loads: an earlier Qwen-Image or Qwen-Image-Edit-2511 transformer is refused at load (exit code 2).
Qwen-Image-2.1-Turbo (8 steps)
Qwen-Image-2.1-Turbo is the same 7B transformer distilled to 8 Euler steps at CFG 1 on one fixed schedule; its VAE, text encoder and vision projector are the 2.1 files above. config/qwen-image-2.1-turbo.json downloads the AtomicChat Qwen-Image-2.1-Turbo-AD-Q4_K.gguf (4.20 GB) into qwen-image-2.1/ under the models root, next to the 2.1 files above under their published names, so a folder that already holds them fetches only the transformer. Qwen-Image-2.1-Turbo-Q8_0.gguf (7.59 GB) is the transformer closest to the full-precision checkpoint (the card's LPIPS against BF16: 0.037, against 0.147 for AD-Q4_K); both use the same Q4_K_M text encoder, which the card measures moving pictures about as far as AD-Q4_K does.
dotnet run --project TensorSharp.Cli -c Release --no-build -- \
--config config/qwen-image-2.1-turbo.json \
--prompt 'a neon sign that reads "OPEN LATE", rainy night' \
--width 1024 --height 1024 --diffusion-seed 42 --output turbo.png
- Declared, not detected. The GGUF has no metadata and Turbo has the base checkpoint's tensors, so the config declares it with
"qwen-image-variant": "turbo"(--qwen-image-varianton the CLI and the server,TS_QWEN_IMAGE_VARIANTin-process; TensorAgent's Turbo entries declare it themselves). Without a declaration a file name containing the wordturbois assumed to be Turbo and the load prints the assumption, because step-distillation LoRA merges into the base checkpoint are published under the same names. - Its own schedule. The
model_index.jsonsample_sigmas[1.0, 0.978453, 0.95418, 0.926626, 0.89508, 0.845148, 0.704534, 0.414568]and a final 0, used as they are at every size, at CFG 1. Other step counts are refused: Qwen has evaluated no other schedule. An explicit CFG above 1 still adds the negative pass. - LoRA plug-ins. A plug-in that brings a sampling recipe (the four step-distillation plug-ins) is refused on Turbo with the reason; plug-ins without one load. Film Stills and Grainscape were validated on Turbo (their look applies, and, as on the base checkpoint, part of the scene is redrawn), and TensorAgent offers those two plus experimental Casual Phone Photo and Voxel Style on Turbo; catalog availability of the new adapters does not establish quality across devices.
- Parity and speed. On an M5 Pro at 1024×1024 a Turbo picture takes 69 s with AD-Q4_K and 68 s with Q8_0, against 320 s for the base model's 40 steps: a Turbo step costs what a base step costs (7.6–7.9 s), and Q8_0 is the slightly faster one. On the same files, prompt, seed and noise, TensorSharp's pictures match stable-diffusion.cpp's at 39–60 dB PSNR (three prompts, one edit), and run 12–13% faster per step from words and 40% faster on the edit. Details are in the model card.
Image generation & editing (Qwen-Image-2.1)
The loaded qwen_image GGUF is the Qwen-Image-2.1 diffusion transformer. TensorSharp resolves its companions next to it — the dedicated 2.1 VAE, the Qwen3-VL-8B text encoder and, for editing, the Qwen3-VL mmproj vision encoder — or takes them from --qwen-image-vae / --qwen-image-vl / --qwen-image-mmproj (env: TS_QWEN_IMAGE_VAE / TS_QWEN_IMAGE_TE / TS_QWEN_IMAGE_MMPROJ). Native RGBA input and PNG output preserve transparency: the Qwen3-VL conditioning branch composites the reference over white while the VAE keeps alpha.
- Generation or editing. No
--imageselects generation; one or more--imagearguments select editing, with the references in the order given.--input prompt.txtcan supply the prompt instead of--prompt. - Sampling defaults. Omitted settings select 40 Euler steps and CFG 1.0, following Qwen's recommended unguided sampling — one transformer prediction per step. An explicit CFG above 1 adds the negative prediction and applies
--negative-prompt; negative prompts have no effect at CFG 1. - Geometry. Omitting dimensions selects 2048×2048 for generation, or approximately the same pixel area at the first reference's aspect ratio for editing. Set width and height together, in multiples of 32, to override it. On the server,
--widthand--height(both are needed) set that default for requests that name no size — and the Web UI names none. References are conditioned at approximately 1 megapixel each, or the output area if smaller. An edit sized from an area shrinks a picture larger than that area;--keep-source-size(keepSourceSizein a request, sent by the Web UI and TensorAgent with every edit) returns the first image's exact size instead, sampled within that area and the image's own, so an edit of an edit keeps its size. - Schedule. The official scheduler configuration: exponential dynamic shifting with the 256/0.5 and 8192/0.9 sequence-length/shift anchors, terminal stretching to 0.02, and a final Euler step to zero.
- Seeds.
--diffusion-seed(seedin the API; default 0) keys the initial noise. Text-to-image noise depends on the seed and size alone, as in stable-diffusion.cpp. An edit's noise also depends on its reference pictures (a hash of their 8-bit pixels picks a separate noise stream), so editing a picture at the seed and size it was drawn with does not start from the noise that became it — from that noise the model can retrace the picture and ignore the instruction. The same pictures and settings still give the same edit.TS_QWEN21_EDIT_NOISE=seeddraws edits from the seed alone for matched-noise comparisons. See seeds and edits. - Execution. On a GGML backend (
ggml_metal,ggml_cuda,ggml_vulkanorggml_cpu) the diffusion transformer runs a complete GGML graph with resident quantized weights; on the pure-C#cpubackend it runs a managed forward over the same file-mapped weights (see On a CPU). CUDA and Metal retain the graph between predictions (TS_QWEN21_GRAPH_REUSE=1, the default). There is no CPU weight-streaming mode; start with smaller dimensions if memory is short. The VAE decodes as one whole graph by default on CUDA and Metal — 1024×1024 in 5.0 s on an M5 Pro, against 13.0 s per convolution — and per convolution, on the device, under Vulkan, where inputs are rescaled by exact powers of two to stay within Vulkan's F16 matrix operands;TS_QWEN21_VAE_FUSED=0selects the per-convolution path anywhere.--tp Nshards the transformer across GPUs (see below). LoRA plug-ins run on all five backends (see below).
The speed levers are resolution and step count. The biggest cut in steps is a step-distillation LoRA plug-in, which replaces the 40-step default with 4–8 transformer passes: on an M5 Pro, 1024×1024 at Viggle Turbo's 6 steps took 54.5 s end to end (see LoRA plug-ins). A 2K square has four times the latent image tokens of a 1K square and more attention work, so --width 1024 --height 1024 is the draft setting. --diffusion-steps 25 --cfg 1 is the faster profile used by the official ComfyUI generation and editing workflows; 40 steps remains the default, and fewer steps are a quality/speed trade-off rather than equivalent quality. CFG 1 already means one transformer pass per step. For a quick smoke test, 256×256 and one step verifies loading and the end-to-end data path, not image quality. Editing also gains from the default-on prefix KV cache.
Run it from the CLI (--prompt, plus --image to edit), the Web UI (a prompt without an attachment generates an image; attach one or more images to edit, with live denoising previews), the HTTP API (/api/image-generate and /api/image-edit, each with a /stream variant), or in-process from C# via QwenImageModel.GenerateImage() / EditImage(). Image operations are serialized because the diffusion pipeline shares mutable working state. Full details and the validation history are in the repository's Qwen-Image-2.1 card.
Edit a selected region
In TensorAgent or the server Web UI, attach photos, choose Select area beside a thumbnail, paint or erase the region, and choose Use selection. That photo becomes the editing target; other photos remain references. Load Qwen-Image 2.1, describe the change and send. The editor supports mouse, touch and stylus, zoom/pan, undo/redo, inversion and edge softness. Compare original reviews the result; Edit again restores the photos, selection and prompt.
A saved selection preserves the target's original dimensions and exact decoded RGB/alpha pixels outside the selection. White edits and black protects in a grayscale mask; an alpha mask uses transparent pixels to edit. The mask must match the first image's dimensions. Edge feathering fades inward. Optional crop mode processes the selected region with context, then places it back on the original canvas; it can reduce work but has less surrounding context. Instruction-following inside the selection still depends on the model.
dotnet run --project TensorSharp.Cli -c Release --no-build -- \
--config config/qwen-image-2.1.json \
--image photo.png --mask selection.png --mask-mode grayscale \
--prompt 'Change the selected vase to red ceramic' \
--mask-feather 8 --mask-crop --mask-crop-padding 96 \
--width 1024 --height 1024 --diffusion-seed 42 --output edited.png
--mask-invert reverses the selection. Feathering is 0–1024 source pixels; crop padding is 0–16384 (default 64). The editor has no fixed megapixel or per-side limit; practical image sizes depend on browser memory and canvas support. HEIC/HEIF selection editing uses a separate full-resolution PNG. The same mask fields work through the JSON, multipart and SSE image-edit APIs and QwenImageParams in C#. Detailed semantics and validation limits are in the model card.
On a CPU (--backend cpu)
--backend cpu runs the whole pipeline in managed C#, with no GGML call: the transformer (8-bit activations on a multi-row integer GEMM, as ggml-cpu quantizes them; tiled flash attention; unmerged LoRA), a host-memory prefix KV cache, the Qwen3-VL text encoder on the same integer GEMM and the vision encoder on a packed F32 GEMM, and the VAE as implicit-im2col packed convolutions on a pool with one thread per logical CPU. A request that names no size renders at 1024×1024 (1 MP) on this backend; ggml_cpu and the GPU backends keep 2048×2048. --tp is refused on this backend. Measured on an i7-11800H (8 cores / 16 threads, AVX-512, 32 GB), text-to-image, seed 42:
| Run | cpu | ggml_cpu | Image vs ggml_cpu |
|---|---|---|---|
| 256×256, 2 steps | 16.9–17.1 s (VAE decode 2.8–2.9 s) | 36.1–36.4 s (VAE decode 10.8–11.2 s) | 37.7 dB PSNR, SSIM 0.984 |
| 512×512, Pruna 5-step LoRA | 118 s (17.6–21.7 s per step) | 295.7 s (VAE decode 50.3 s) | 32.9 dB PSNR, SSIM 0.972 |
The images differ because the managed transformer sums in another order than ggml-cpu and keeps F32 where ggml-cpu uses an F16 GELU table. TS_QWEN21_CPU_MATMUL=f32 (F32 activations against dequantized weight tiles, the default before) is slower (20.4 s and 169.6 s for the two runs above) and numerically steadier. Two larger runs were measured with that F32 transformer: 1024×1024 with the Pruna LoRA 756 s against 1101 s on ggml_cpu, and a 512×512 edit 224 s against 353 s. Memory is the limit on a CPU: a size whose estimated peak does not fit the machine is refused before any work (TS_QWEN_IMAGE_CPU_MEMORY_CHECK=0 overrides); a 1024×1024 Pruna generation peaked at 3.7 GiB of commit, a 2048×2048 VAE encode and decode at 10.5 GiB. The kernel switches (TS_QWEN21_CPU_*, TS_QWEN_TE_CPU_MATMUL, TS_CPU_GEMM_THREADS) are in the model card.
Prefix KV cache
The text and reference-image tokens are modulated at t = 0 and never attend to the image being generated, so their keys and values are the same at every denoising step. TensorSharp implements the official prefix KV cache: step 1 runs the whole sequence and stores each block's prefix keys and values on the device, and every later step runs only the target image's tokens. It is on by default (TS_QWEN21_PREFIX_CACHE=0 turns it off) and stores exactly what attention reads — F16 on Metal and with CUDA flash attention, F32 otherwise — so with the default storage the output PNGs were byte-identical to uncached runs. A CFG run keeps one cache per branch, and every cache is released before the VAE decode. The cost is memory: 512 KiB per prefix token per branch in F16, about 2 GiB for each 1-megapixel reference. A cache may use at most half the memory the device reports free (TS_QWEN21_PREFIX_CACHE_MAX_MIB caps it further) and is otherwise declined with a warning; TS_QWEN21_PREFIX_CACHE_TYPE=q8_0 or q8_0_v stores it in 8 bits, saving memory at 5–13% more time per step and a small rounding of the output.
It pays in proportion to the prefix's share of the sequence. A 1024² edit on an M5 Pro went from 17.97 s to 9.65–10.38 s per step with one reference, and from 33.32 s to 11.46 s with two (2.91×); on an A40 (ggml_cuda) the one-reference edit went from 2.547 s to 1.328 s. Generation, whose prefix is only the prompt, gains 1–3%, and at 2048² editing gains less because the target dominates. Details and the Vulkan numbers are in the Qwen-Image-2.1 card.
Multiple GPUs
--tp N on ggml_cuda or ggml_vulkan shards the diffusion transformer Megatron-style: each GPU holds 32/N whole attention heads and 12,288/N MLP columns, the two row-parallel products in each block are summed across GPUs, and each GPU caches the prefix of its own heads. N must divide the 32 heads — 2, 4, 8 or 16 on one machine — and each weight's quantized blocks must stay whole when it is sharded, which the load checks per weight type; only 2 GPUs have been measured. The text encoder, vision encoder and VAE stay on the first GPU, and multi-node groups are refused. On two A40s it measured 1.34× per step for 1024² generation, 1.42× for a one-reference 1024² edit and 1.54–1.57× at 2048²; under ggml_vulkan, which reduces through host memory, it was 0.86× — slower than one GPU. Partial sums are added in a different order, so outputs are not bit-identical to a single GPU: composition and quality match, while details can differ. See the card for memory per GPU and the numerical checks.
LoRA plug-ins
--lora adds a LoRA to the diffusion transformer on the CLI and the server alike: a style, an editing skill, or a step-distillation adapter that replaces the 40-step default with 4–8 transformer passes. Repeat it to stack plug-ins; --lora-scale sets the strength of the preceding --lora and --lora-config names its companion config (a TensorSharp LoRA config, a PEFT adapter_config.json or a VideoX-Fun pdd_config.json). The fourteen ready-made plug-ins in config/lora/ download and hash-check their weights on first use and carry the adapter's strength and sampling recipe.
# Viggle Turbo: 6 steps and CFG 1 from the plug-in's recipe (4–8 steps supported)
dotnet run --project TensorSharp.Cli -c Release --no-build -- \
--config config/qwen-image-2.1.json \
--lora config/lora/qwen-image-2.1-viggle-turbo.json \
--prompt 'A small orange cat beside a blue ceramic vase, soft daylight, detailed photograph' \
--width 1024 --height 1024 --diffusion-seed 42 --output turbo.png
# The server applies its startup set to every image request
dotnet run --project TensorSharp.Server.Host -c Release --no-build -- \
--config config/qwen-image-2.1.json --lora config/lora/qwen-image-2.1-pruna-8step.json
- Shipped plug-ins. Step distillation:
viggle-turbo(6 steps, 4–8 supported),pruna-8step,pruna-5stepandfun-acc-4step(a PDD bundle with per-step output heads). Styles and editing skills:film-stills,grainscape,casual-phone(triggerlbxstyle),voxel-style(attach a photo),fix(DoRA),detail-enhancer,natural-exposure,anime-consistency,object-removerandobject-mover. Each file's comments cite its model card, trigger phrase and license; several are non-commercial. - Sampling precedence. Explicit
--diffusion-steps/--cfg(or a request'ssteps/cfg) win over the plug-in's recipe, which wins over the model defaults. A step count the recipe has no schedule for is refused, naming the supported ones, and only one plug-in per run may carry a recipe — none on Turbo, whose checkpoint has its own. - Formats. Diffusers / PEFT, ComfyUI / ai-toolkit, DiffSynth / ModelScope and kohya names; alpha from per-module
.alphatensors, the PEFT metadata a diffusers saver embeds in the file, anadapter_config.jsonor the plug-in's ownalpha, and otherwise equal to the rank — kohya'sss_network_alphais training metadata and is ignored, as in ComfyUI and diffusers; DoRA; VideoX-Fun PDD bundles. LoKr, LoHa, text-encoder LoRAs and LoRAs made for other models are refused with a message; nothing is skipped silently. - Execution. Applied unmerged,
y = W x + B (A x), on top of the quantized weights, because merging a distillation LoRA's small delta into Q8_0 or Q4 weights rounds most of it away. Works with the prefix KV cache and with--tp N, where each GPU holds its slice of the factors. - Server. The startup set applies to every generation and edit; per-request LoRA selection is not implemented.
The flag reference and the plug-in config format are in the repository's USAGE.md, and the packing and tensor-parallel details in the Qwen-Image-2.1 card.