Qwen-Image Mixed Precision on a 32 GB Mac

Denis Ineshin · 2026-07-24

A case study from mlx-teacache's Qwen-Image showcase: diagnosing a visual artifact in a uniform 4-bit build and removing it for about 1.9 GiB of additional MLX allocation

Qwen-Image has about 20 billion parameters. Its bf16 weights alone need roughly 40 GiB, so a 32 GB Mac calls for lower precision. Our first baseline, mflux with quantize=4, produced a portrait with a cracked skin texture. Changes to resolution, schedule, prompt suffix, VAE settings, and guidance did not remove it. A mixed-precision recipe did. It protected a small set of layers and added about 1.9 GiB to peak MLX allocation. This paper documents that one-prompt, one-seed result and does not claim a general Qwen-Image sensitivity map.

Evidence scope

1. The symptom

We constructed mflux with quantize=4, which applies uniform 4-bit affine quantization to each eligible module. The test portrait showed fine cracks across the skin, worst near the center. The pattern appeared with the official 50-step CFG recipe and with shorter schedules. At 2× crop, it looks reticulated, almost like dried mud rather than ordinary image grain.

The problem first appeared in a TeaCache comparison, so TeaCache was the first suspect. Vanilla and wrapped generation on the same build showed the same reticulation. We therefore ran the remaining experiments with vanilla mflux and treated model construction as the source.

2. The hypotheses that did not survive

Several simpler explanations fit the symptom. We tested them directly or checked them against the official implementation before changing quantization:

Hypothesis Verdict Evidence
Output resolution too low rejected artifact present at 512², 768², and 1024²
Too few denoising steps rejected present at 20 and at 50 steps
Missing official prompt suffix marginal the official positive_magic suffix barely moved it
Wrong flow-match scheduler shift rejected mflux's shift parameters and resolution-dependent mu match the official Qwen values
VAE decode or latent normalization rejected mflux's 16-channel latent mean/std constants match the official VAE; decode denormalizes correctly
VAE tiling seams rejected 1024² tiled decode with overlap showed no grid-aligned seams
Guidance/CFG misconfiguration rejected artifact persists across guidance settings at the official true_cfg_scale=4.0

The scheduler and VAE checks used official Qwen-Image source, not only generated images. Both had plausible community theories behind them, but neither matched the implementation. The uniform-q4 baseline still left one obvious variable untouched: layer-level weight precision.

3. Why quantization was the credible suspect

Prior work shows that low-bit quantization does not affect every part of a diffusion transformer equally. Mixed-precision methods respond by spending more bits on the parts they find most sensitive. That gave us a plausible direction for the next build.

Position and conditioning paths can be sensitive

Qua²SeDiMo (AAAI 2025, arXiv:2412.14628) builds per-layer sensitivity maps for diffusion denoisers. It reports that "the final output layer of DiT models are more sensitive to quantization than their U-Net counterparts." Its analysis also gives the timestep-embedding block a high-sensitivity signature. SVDQuant (ICLR 2025, arXiv:2411.05007), a 4-bit method for FLUX-class models, keeps the inputs of the adaptive-normalization linear layers at 16 bits rather than quantizing them with everything else.

Block position appears in two other sources. SQ-DM (arXiv:2501.15448) states the block-position case directly. It says that "only the first and last few blocks are generally more sensitive to quantization." It then keeps those blocks at MXINT8 and uses 4-bit precision elsewhere. TreeQ (arXiv:2512.06353), a December 2025 preprint on mixed-precision search for diffusion transformers, treats keeping "the embedding and final layers ... in full precision" as the shared baseline of every method it compares.

For MMDiT block roles, Unraveling MMDiT Blocks (arXiv:2601.02211) is a training-free analysis rather than a quantization study. It finds that early blocks carry semantic structure while later blocks render fine detail. That result is consistent with protecting the edge blocks.

The observed texture resembles reported low-bit failures

PTQ4DiT (NeurIPS 2024, arXiv:2405.16005) describes an outlier-driven mechanism. It locates the error in "channels with significantly high absolute values" rather than spreading it uniformly. TreeQ reports losses in fine-grained, high-frequency detail and structured local features. Its spatial-feature metrics deteriorate even while global fidelity metrics stay reasonable. A reticulated skin texture on an otherwise coherent portrait resembles that failure class, although this study has no higher-precision reference that would establish which output is more faithful.

Community builders reached a similar position-protection design for Qwen-Image. city96's Qwen-Image GGUF builds keep the first and last layer at high precision in their lower-bit variants. The model card describes this as new dynamic quantization logic. QuantStack's Qwen-Image GGUF conversions ship the same K-quant family for Qwen-Image.

The evidence does not transfer cleanly to this case. Some methods differentiate precision by operation type rather than block position. HQ-DiT (arXiv:2405.19751) applies FP4 to every fully-connected layer in every block, so position protection is an established convention, not a settled law.

Much of the sensitivity evidence also concerns joint weight-and-activation quantization (W4A4/W4A8), not the weight-only 4-bit setup used here. Both regimes protect similar layers, but applying that convention to weight-only quantization remains an assumption. None of this proves that uniform q4 causes this artifact on this model. We found no peer-reviewed, Qwen-Image-specific quantization-sensitivity study published by July 2026. The literature made quantization worth testing, so the next experiment changed the model build.

4. The recipe

mlx.nn.quantize accepts a class_predicate callable. The callable receives each module's path and instance. It can return:

mflux routes model construction through QwenWeightDefinition.quantization_predicate, which quantizes every quantizable module uniformly when the caller requests quantization (return hasattr(module, "to_quantized")). mflux itself defaults quantize to None; uniform q4 is produced only when the caller selects quantize=4. Overriding the predicate before constructing that model yields mixed precision with no changes to mflux itself.

Qwen-Image has 60 transformer blocks. The recipe assigns precision as follows:

Precision Modules
bf16 (skipped by the predicate) img_in, txt_in, time_text_embed, proj_out, norm_out
8-bit, group size 64 transformer blocks 0 through 5 and 54 through 59 (first six + last six)
4-bit (default) the middle 48 blocks
Qwen-Image mixed-precision allocationTransformer: 60 blocksBlocks 0 through 5first 6 blocks8-bit, group size 64Blocks 6 through 53middle 48 blocks4-bit requested defaultBlocks 54 through 59last 6 blocks8-bit, group size 64Inputs and conditioningimg_in, txt_in, time_text_embedbf16Output pathnorm_out, proj_outbf16Load report for the complete build:bf16: 6 quantizable modules8-bit: 168 modules4-bit: 672 modulesConceptual precision map. This is not a sensitivity measurement or ablation result, and it is not to scale.
Figure 1. Conceptual map of the precision recipe. The block ranges and precision assignments come from the construction predicate; the module totals come from its load report. This is not a layer-sensitivity measurement or ablation result, and the blocks are not drawn to scale. The editable PlantUML source is published with the paper.

At load time the build reported 6 modules kept in bf16, 168 quantized to 8-bit, and 672 to 4-bit. Analogous work informed the bf16 set. img_in and txt_in are the embeddings; proj_out is the final projection. time_text_embed and norm_out sit on the timestep and output-normalization paths. The cited studies do not establish that these specific Qwen-Image modules are sensitive under weight-only quantization. The construction snippet is adapted from the recipe shipped in the public variant documentation:

from mflux.models.qwen.weights.qwen_weight_definition import QwenWeightDefinition
from mflux.models.qwen.variants.txt2img.qwen_image import QwenImage
from mflux.models.common.config.model_config import ModelConfig

_PROTECT = set(range(6)) | set(range(54, 60))           # first 6 + last 6 of 60 blocks -> q8
_BF16 = ("img_in", "txt_in", "time_text_embed", "proj_out", "norm_out")  # -> bf16

def _mixed(path, module):
    if not hasattr(module, "to_quantized"):
        return False
    if any(path == p or path.startswith(p + ".") for p in _BF16):
        return False                                    # keep bf16
    if path.startswith("transformer_blocks."):
        idx = int(path.split(".")[1])
        if idx in _PROTECT:
            return {"group_size": 64, "bits": 8}        # q8
    return True                                         # default q4

_original = QwenWeightDefinition.__dict__["quantization_predicate"]
QwenWeightDefinition.quantization_predicate = staticmethod(_mixed)
try:
    flux = QwenImage(quantize=4, model_config=ModelConfig.qwen_image())
finally:
    QwenWeightDefinition.quantization_predicate = _original

This mechanism has a practical caveat. It monkeypatches a class attribute because mflux 0.17.5 offers no per-instance way to inject a predicate into QwenImage construction. The finally block restores the original predicate after construction. During construction, however, the override remains process-global. Another thread must not construct a Qwen model at the same time. The hook is also fragile across mflux refactors, so a native mflux option would be safer than this snippet.

5. Results

The 512×512 experiment kept the generation inputs fixed and changed the construction predicate. It used mflux 0.17.5, seed 42, 50 steps, and guidance=4.0 on the 32 GB M1 Max. The exact portrait prompt and MLX version were not retained, which limits reproduction of this pair.

Build Source-reported peak Author's visual assessment
uniform q4 (quantize=4) 27.6 GiB cracked / reticulated
mixed precision (recipe above) 29.5 GiB reticulation largely absent; preferred result

The 512² peaks and visual comparison are source-reported observations. The repository's committed benchmark artifacts cover the separate 768×768 recipe discussed below.

Center crops from two synthetic portraits generated with identical inputs: the uniform-q4 output has visible reticulation, while the mixed-precision output does not
Figure 2. Center crops at 2× from two synthetic Qwen-Image outputs generated with the same prompt, seed, steps, and guidance. Left: uniform 4-bit. Right: mixed precision. The outputs differ globally, so this pair supports only the visible before/after comparison, not identity preservation or fidelity to a higher-precision reference.
Full frames of two synthetic portraits generated with the same inputs under uniform-q4 and mixed-precision model builds
Figure 3. The full synthetic frames behind Figure 2. Changing the quantization predicate changed facial geometry, framing, hair, clothing, lighting, background, and texture. The pair therefore demonstrates a substantial output change and the disappearance of the observed reticulation, not a texture-only repair.

The project's separate 768×768 showcase used the same mixed predicate. That build completed at a 30.42 GiB peak MLX allocation, measured as mx.get_peak_memory() / 1024**3. The uniform-q4 figure is source-reported at roughly 28.5 GiB. The visual improvement is also source-reported, but no public uniform-q4/mixed image pair supports it at 768×768.

The public 768×768 benchmark used mflux 0.17.5, 50 steps, guidance=4.0, seed 42, and the portrait prompt printed in its committed report. Its memory metric covers MLX allocations, not total process or system memory. Subtracting it from 32 GB would not measure system-memory headroom.

A simple weight calculation explains the pressure. At about 20 billion parameters, 8-bit weights need roughly 20 GiB and bf16 weights roughly 40 GiB. The text encoder, VAE, activations, and quantization metadata need additional memory. We did not measure either full-precision alternative.

The mixed recipe instead protects twelve of sixty blocks at 8-bit and a handful of modules at bf16. It added about 1.9 GiB to the reported uniform-q4 peak and completed on the test machine. No precision sweep established that the recipe is unique or minimal.

The TeaCache gate coefficients shipped for Qwen-Image were calibrated on the stock uniform-q4 build. They transfer to the mixed-precision build without recalibration: the project's threshold sweep on the mixed build showed the same graceful skip ramp at high structural similarity (SSIM 0.987 at the default threshold) documented on the variant page. That is an empirical transfer result; the study did not measure how far the mixed recipe moved the latent trajectory.

The wrapper also reproduced each base build's visible behavior. This separated the model-construction effect from the cache behavior.

6. Scope and limitations

The study is narrow, so the claims need the following limits:

7. Lessons

Treat uniform quantization as a baseline

With quantize=4, mflux treated the transformer's 846 quantizable modules identically. Analogous literature and community builds motivated a concentrated protection set. In this case it cost about 1.9 GiB, roughly 7% of the reported uniform-q4 peak, and produced the author's preferred result for the tested portrait.

Check cheap hypotheses in source

Two of the seven hypotheses, scheduler shift and VAE constants, had plausible community theories attached. Both were settled by comparing the port's constants against the official release, which is faster and more conclusive than generating test images against a moving visual target.

Separate wrapper behavior from model behavior

The step-skipping cache was the first suspect because it was the newest component. One comparison exonerated it: vanilla and wrapped output shared the visible reticulation. The harness makes this vanilla-versus-wrapped check routine and inexpensive.

Put the recipe where users will find it

The mixed-precision recipe lives outside the library's scope, but its documentation is where a 32 GB Qwen-Image user will find the problem. The variant page carries the construction snippet and says plainly that stock q4 is grainy on this model.

Claims and status

In short, this case supports a narrow result. For one 512×512 image, the mixed build removed the cracks seen with uniform q4. The same build also ran at 768×768 on the test Mac. The study does not tell us whether the image is closer to full precision. It also does not identify the smallest working protection set or show that the same layers matter for other prompts.

Claim Status Source
With quantize=4, the tested Qwen-Image portrait showed visible skin reticulation on this hardware and recipe source-reported observation; also observed across the tested resolutions and step counts variant page; changelog
The mixed recipe removed the visible reticulation and produced the author's preferred 512² output one controlled prompt/seed pair; subjective assessment; no high-precision reference (Figures 2 and 3) this paper's figures
Peak 27.6 GiB (uniform q4) vs 29.5 GiB (mixed) at 512²; ~28.5 vs 30.42 GiB at 768² source-reported; public artifact corroborates the 30.42 GiB mixed-build peak as MLX allocation, not total system use variant page; comparison report
Protecting edge blocks and selected input, conditioning, and output modules is a plausible mixed-precision heuristic motivated by analogous literature and community practice; not a measured Qwen-Image sensitivity map SQ-DM; Qua²SeDiMo; SVDQuant; city96 GGUF builds
TeaCache coefficients calibrated on stock q4 transfer to the mixed build source-reported (threshold sweep on the mixed build, SSIM 0.987 at default threshold) variant page
mflux 0.17.5 exposes the predicate as a class attribute rather than a per-instance constructor option source inspection of the version used qwen_weight_definition.py at v.0.17.5

References and source notes


Prepared 2026-07-20. Last updated 2026-07-24. Denis Ineshin.

Buy me a coffee