+
Weights: pretrained Keras weights live on Hugging Face under
+
zeromodels/<variant>
+(each repo carries
zm_config.json + sharded
+
*.weights.json /
*.weights.h5 +
+
tokenizer.json). Load with
from_weights("zeromodels/<variant>").
+
+
+Qwen-Image-2.1, ported to pure Keras 3: latent text-to-image flow-matching with a
+32-layer **single-stream** block-causal DiT, a residual 64-channel KL autoencoder
+(16× spatial, RGBA), and the Qwen3-VL text tower. The whole model is **one
+container**, `QwenImage21Model`. `QwenImage21TextToImage` adds `generate`.
+
+The weights are converted once, offline, and hosted: on-the-fly `hf:` conversion
+is deliberately **not supported** for diffusion models.
+
+Key facts of the port:
+
+- **Unpatched latents**: the denoiser sees `(B, H · W, 64)` tokens (VAE scale 16;
+ no 2×2 packing). At 1024px that is a `(B, 4096, 64)` sequence.
+- **Block-causal attention**: text is causal; the target image block is
+ bidirectional and can attend to all preceding text.
+- **`causal_condition`**: text tokens modulate from `t = 0` (timestep-independent),
+ matching Diffusers' KV-cache-ready conditioning.
+- **True CFG optional**: Diffusers defaults to `true_cfg_scale=1.0` (no guidance).
+ Pass `guidance_scale > 1` with a negative prompt to enable dual forwards.
+- **Pre-norm text features**: the text tower returns decoder outputs *before* the
+ final RMSNorm, matching Diffusers' forward hook on the language-model norm.
+- **Schedulers match Diffusers**: `FlowMatchEulerDiscreteScheduler` with dynamic
+ resolution shifting (`mu` from image sequence length) and `shift_terminal`.
+
+Links:
+
+- Source: [`Qwen/Qwen-Image-2.1`](https://huggingface.co/Qwen/Qwen-Image-2.1)
+- Reference: [diffusers `QwenImage21Pipeline`](https://huggingface.co/docs/diffusers/api/pipelines/qwenimage)
+- License: [Qwen Research License](https://huggingface.co/Qwen/Qwen-Image-2.1)
+- See also [qwen_image.md](qwen_image.md) (1.0 double-stream / packed),
+ [qwen3_vl.md](qwen3_vl.md) (text tower)
+
+## Variants
+
+Preconverted, bfloat16 weights are hosted under `zeromodels/`. Load with
+`from_weights("zeromodels/