Repository navigation
Conversation
|
Thank you, @drowzeys, the native Qwen-Image denoiser and retained prompt KV are useful first components of the image family. We checked fa73745. The config accepts an epsilon it never validates against the fixed kernel constant, and Scratch.of multiplies unbounded shapes. We can add semantic/bounds refusals and failure regressions before exposing requests. We will land the qualified denoiser first, then native text encoding, noise/schedules, VAE and picture output under shared job admission/cancellation. The recipe currently leaves encoding and decoding to mflux. Please keep same-input step/pixel references and multiple-resolution controls for that completion, alongside numerical kernel adapters and exact-source memory measurements. No Python/framework service should remain required for the final native request. Posted by Ash's AI Assistant Synthena |
zig/src/families/qwen_image runs Qwen-Image-2.1's 7B transformer from the released bf16 diffusers checkpoint. The prompt rows run once a prompt, with causal attention and each block's keys and values kept. The image rows run at each denoising step. Activations are bf16 and every sum is fp32; there is no int8. The projections reuse the Qwen-VL tower's tfp::gemm_nax, and attention reuses the prefill tfp::attention_nax template; full (non-causal) entry points are added for the image rows. The family's own kernels are the modulated layer norm, the prompt RMSNorm, gelu, the q/k norm and rotary, the prefix copy, the gated residual and SwiGLU. Gate 5 holds all 15 of its launches. tf-qwen-image checks the transformer against a case from tools/zig/qwen_image_case.py, which runs mflux's prompt encoder, transformer and decoder as the reference. The case holds every step's velocity on the reference's own input, then the whole schedule is run and timed. docs/recipes/qwen-image-2.1.md has the receipts.
fa73745 to
0267610
Compare
|
Rebased on main |
The kernels run every norm with the fixed cfg.eps (1e-6), but check() accepted the config's eps without reading it. It now requires eps, compares it at f32 with the constant, and refuses another value or a non-number with error.EpsilonMismatch (a missing eps is error.BadConfig). Host test: the released value written two ways passes; 1e-05, 0, 1, a string and null are refused.
…atent grid Scratch.of multiplied the prompt and image rows into byte counts with no bound. Every product and the 256-aligned total are now checked (std.math.mul/add) and refused with error.ScratchTooLarge, as is a key count past the kernels' u32 row arguments. The prompt store's bytes are computed there too, and grid() refuses a width x height whose latent rows overflow (error.BadImageSize). Host tests cover each overflow.
|
Thank you. Both points are fixed; the head is now
Checks on the M5 Ultra:
These are build and host tests only; I didn't do a device run. The same-input step and pixel references and the multiple-resolution controls are all kept. |
|
Thanks @drowzeys for the epsilon and overflow controls, including the unchanged 533,978,112-byte real-shape case. These are the semantic and allocation checks requested in the review; our integration carries those boundaries too. The reviewed landing unit remains the native denoiser component. The host tests and build do not qualify its same-input device steps or make the external text encoder and VAE native. Keep the numerical adapters, resource checks, multiple-resolution controls and reference comparisons on the final integrated source. The new head's lack of a device rerun should stay explicit. Posted by Ash's AI Assistant Synthena |
|
Thank you, @drowzeys, for Qwen-Image-2.1's Metal transformer. 1.1.0 includes #948 ( Posted by Ash's AI Assistant Synthena |
|
The Qwen-Image Metal denoiser component shipped in 1.1.0 as Posted by Ash's AI Assistant Synthena |
…he Metal runtime
Thank you, Ash, for the invitation to send this for review, and for the Metal runtime and kernels it builds on.
This is the first image model on TensorFold's Zig runtime. Qwen-Image-2.1's 7B transformer now runs on TensorFold's Metal runtime from the released bf16 diffusers checkpoint, with no MLX and no Python in the transformer. Prompt rows go in, and each denoising step's velocity comes out. It reuses your kernels rather than adding a parallel stack: the projections are the Qwen-VL tower's
tfp::gemm_nax, and attention is the prefilltfp::attention_naxtemplate. This is the case for TensorFold beyond text: the same runtime, kernel contract and lean code carry a model whose step is a denoising pass over an image rather than a token.On a Mac Studio M5 Ultra,
Qwen/Qwen-Image-2.1-Turbo, 1344x768 (4,032 image rows), 8 steps, compared with mflux (MLX):What comes next, for the gate we agree on:
attention_naxtemplate.tensorfoldentry point. That means agenerate-style command, or an endpoint, once we agree where a denoiser sits.I did not open an issue first, since this came by invitation. Happy to move the gate discussion into one if you prefer.
Ours (left) and mflux (right), same prompt rows and starting noise, both decoded by mflux:
What it adds
These are new files, plus minimal wiring: 1 line in
build.zig, 2 inzig/kernels/metal/sources.zig, 2 inzig/src/tensorfold.zig, 3 in the gate-5 runner and 15 records inregistry.zig.zig/src/families/qwen_image/(Zig over the Metal runtime):config.zigcheckstransformer/config.jsonand refuses anything butQwenImage21Transformer2DModelat the released geometry.weights.zigreads the diffusers shards into shared Metal buffers throughcore/checkpoint_metal.zig. It checks that the checkpoint holds 297 tensors, and the dtype and shape of each one it reads.host.zigholds the rotary tables (three axes: prompt rows on the diagonal, the image centred at the prompt's frame), the timestep embedding, and the modulation every block shares.kernels.zigholds the pipelines. The projections areqwv_gemm_bf16_nt_*(the tower'stfp::gemm_nax). Attention istfp::attention_nax, causal for the prompt rows and full for the image rows.model.zigholds the prompt pass, whose keys and values are kept for each block, and the per-step image pass.zig/kernels/metal/qwen_image.metalholds seven row kernels and eightattention_naxentry points: full and causal, by alignment.zig build tf-qwen-imageruns the transformer on a reference case:run.json.tools/zig/qwen_image_case.py(development tool, using mflux and MLX):casewrites the case: prompt rows, noise levels, starting latents, mflux's per-step inputs and velocities, and optional fp32 velocities.decodeturns our latents into a picture.comparegives PSNR and a side-by-side image.docs/recipes/qwen-image-2.1.mdhas how to run it and the receipts.How it fits the engine, and how it does not
families.detect, and touches nothing in the lane engine, the server or any existing family.tensorfold.zig, driven today by atf-*program. A served text-to-image endpoint would be a separate request type, not a lane. Image towers already run as one-shot passes outside the lane rounds, and a denoiser would sit in the same place.Receipts
Environment:
46a08c18.Qwen/Qwen-Image-2.1-Turbo.add5164on MLX 0.32.3, bf16.Request:
sample_sigmas, given the resolution shift: 1.0, 0.989079, 0.976488, 0.961812, 0.944488, 0.915859, 0.826254, 0.585455, then 0.Velocity, each step on mflux's own input (
tf-qwen-imageon the case):The same step run twice gave identical bytes.
Image. Our free-running 8 steps, decoded by mflux's float32 decoder, against mflux's own image: PSNR 43.35 dB, mean |diff| 0.84 of 255. A side-by-side image is attached.
Speed, one session under a GPU lock shared with the machine's other jobs, run in the order mflux, ours, mflux:
add5164), run 1add5164), run 2Where the GPU time goes, measured by switching off one stage class at a time in a local build. That session's full forward took 0.805 s on the GPU, so these are shares rather than absolute times:
I tried instantiating the gemm with K-alignment, a single change, and it did not move it: 0.729 s against 0.730 s.
Earlier alternated pairs ran while another job had a 35B model loaded in Ollama on the same GPU, which was not part of this work. Under that matched contention, mflux and this branch were within about 3% of each other: 2.13–2.48 s against 2.24–2.36 s. The table above is the uncontended session.
Memory:
/usr/bin/time -l), with the bf16 weights in shared Metal buffers.Host tests,
zig build test -Dcpu=apple_m1:Kernel contract,
zig build kernel-contract -Dcpu=apple_m1:The 2 known findings are
affine_mm's documented ones. With--only "qwen image", all 15 new launches pass the limits check on this GPU: 256 threads, or 128 for attention; 144 threadgroup bytes for the two norms and 0 for the rest.Lean check,
python3 tools/zig/lean_check.py: 517 problems on the branch, equal to main's baseline of 517. That is 0 new, and none of the 517 are in this branch's files.Not tested, or not in this pull request
nax.h's simdgroup fallback, as the Qwen-VL tower does, but I have not run them there.--offline) were not run.Qwen/Qwen-Image-2.1checkpoint (40 steps) has the same transformer and is untested.Licenses
Qwen/Qwen-Image-2.1and-Turboweights: the Qwen Research License, which is non-commercial. They are not redistributed here, and no weight bytes or fixtures from them are in the tree.tools/zig/qwen_image_case.py, which imports it at run time. No mflux or diffusers code is copied.Reproducing
python3 tools/zig/qwen_image_case.py case MODEL_DIR case/ --prompt-file prompt.txt --fp32-steps 8 zig build tf-qwen-image -Dcpu=apple_m1 zig-out/bin/tf-qwen-image MODEL_DIR/transformer case/case.safetensors case/ python3 tools/zig/qwen_image_case.py decode MODEL_DIR case/latents.f32 case/zig.png python3 tools/zig/qwen_image_case.py compare case/reference.png case/zig.png case/side.pngThe Studio that uses this model is #941.
🤖 Generated with Claude Code
https://claude.ai/code/session_01PfjmufXm4L7qsVNFyRw9ZK