Repository navigation
MOSS-Image: the image decoder on Metal - #1687
Open
BobClawblaw wants to merge 5 commits into
Open
BobClawblaw wants to merge 5 commits into
BobClawblaw wants to merge 5 commits into
Conversation
encoder.zig runs the checkpoint's text_encoder (Qwen3.5's text model: Gated DeltaNet and gated full attention, hidden 4096) as the pipeline's encode_prompt does: the prompt in its chat format, one causal forward, the final norm's rows. bf16 weights as published; fp32 sums, DeltaNet state and attention; the other steps rounded where the torch code rounds. tf-moss-image --prompt encodes both prompts natively, tf-moss-image-encoder checks a case from moss_image_case.py encode, and start --no-encoder makes a case without torch's encoder. The tokenizer stays outside the engine module: the test programs get it. Nine kernels in moss_image_encoder.metal, each through the contract's gates 1-5 with eight controls.
The check passed on repeatability alone. Now each layer and the final norm run alone (Encoder.steps) on the float32 run's input for that step, rounded to bf16, and each step's rows must be within twice torch bf16's distance from float32 on the same input, or plain bf16 rounding's when larger, in cosine error and in its worst row. moss_image_case.py encode keeps every layer's float32 rows and torch bf16's rows for each step alone. The whole prompt's rows are still compared with torch bf16's and float32 but not gated: in bf16 the layers magnify each implementation's rounding differently, so two correct runs part by more than any one step's error. The second encoding must still repeat the first byte for byte. reference.zig holds the comparison and the gate, and its tests plant a row 5% off that must fail with an identical repeat.
decoder.zig runs the checkpoint's vae/ decoder as the pipeline's decode does, in float32: the latents' batch norm undone and the patches unpacked, the mid block with its attention, the up blocks, then the pipeline's 8-bit conversion. Weights as published (float32); fp32 sums, GroupNorm statistics and attention. The 3x3 convolutions run as Winograd F(2x2, 3x3): the weights' transform in float64 at load, rounded once, the input's and output's transforms in fp32 in the kernel. tf-moss-image decodes after the transformer frees and writes picture.png. tf-moss-image-decoder gates a decode against moss_image_case.py vae's float64 one: within twice torch float32's distance (reference.within) in cosine error and worst pixel row, and repeated byte for byte, or it fails. Nine kernels in moss_image_decoder.metal, each through the contract's gates 1-5 with nine controls.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this changes
Fixes #1504. MOSS-Image's image decoder on Metal, inside the
moss_imagefamily (#1457, #1512). The decoder is the checkpoint'svae/, FLUX.2's small VAE decoder (AutoencoderKLFlux2), run as the pipeline's decode does, in float32: the latents' batch norm undone with the saved statistics, the 2x2 patches unpacked,post_quant_conv,conv_in, the middle block with its single-head attention, four upsampling stages,conv_out, then the pipeline's own 8-bit conversion.tf-moss-imagenow frees the transformer and writespicture.pngitself, so torch no longer decodes.Stacked on #1512: the first three commits are #1512's. This PR's own are the last two,
602714801(code) and7597295ff(docs). Once #1512 lands, I'll rebase this onto main.decoder.zig(the releasedvae/config.jsongeometry, checked; the float32 weights as published; the forward and its scratch) andpng.zig(the picture's PNG encoder).zig/kernels/metal/moss_image_decoder.metal:md_unpack,md_winoandmd_wino_32(the 3x3 convolutions as Winograd F(2x2, 3x3)),md_conv(1x1),md_norm(GroupNorm, with or without SiLU),md_group_partandmd_group_finish(its statistics),md_attentionandmd_pixels. No shared kernel changes.tf-moss-imagedecodes the final latents after the transformer is freed and writespicture.pngbesidelatents.f32.tf-moss-image-decoder MODEL_DIR LATENTS WIDTH HEIGHT REFERENCE TORCH OUTchecks one decode. It must be within twice torch float32's distance from float64, in cosine error and in its worst row of pixels, and a second decode must repeat the first byte for byte. It exits 1 otherwise. The gate isreference.within, beside the encoder's, with a test that plants a row further off than the reference allows.vae, which writes the publisher's decoder's output before the clamp in float64 on the CPU and in float32 on MPS, as the pipeline runs it.Decoder.loadrefuses a GPU without tensor units and names it, likeModel.loadandEncoder.load.Receipt
Environment:
602714801(code) on MOSS-Image: the prompt encoder on Metal #1512's92c829887, on maindd0a9cc5e.OpenMOSS-Team/MOSS-Image-Base@85975793d8b3d432f1b6e27f352abaa4c2a4c0d3.pipeline_moss_image.py(sha256-checked) and diffusers 0.42.0'sAutoencoderKLFlux2, on torch 2.14.1.Precision: float32 throughout, as the pipeline decodes. Weights as published (float32), fp32 sums in every convolution and in the attention, GroupNorm's mean and variance in two fp32 passes, and the pipeline's own 8-bit conversion (clamp,
* 0.5 + 0.5,* 255, round half to even). No half, int8 or lossy weight.Exactness, on the transformer's final latents of the fox case (1024x1024) and the card's sticky-note case (1200x880), against the publisher's decoder in float64 on the CPU:
tf-moss-image's latents equal the pinned run's byte for byte; with--prompt, they equal MOSS-Image: the prompt encoder on Metal #1512's byte for byte. The decoded picture is unchanged across rebases.Speed, back to back in one session with no other GPU work, the median of three:
Memory: the decoder's scratch is five float32 picture tensors at the widest stage, 4.7 GB at 1024x1024, allocated after the transformer is freed.
tf-moss-imagepeaks at an 11.3 GB footprint from a case's rows, and at 16.4 GB with--prompt, while the encoder is loaded.Prompt speed: n/a, not a token family.
Kernel contract,
tf-kernel-contract --only "moss image"on the M5 Ultra:md_winoandmd_wino_32run as a plain 3x3, a 3x3 read nearest from half size, and a 3x3 with a residual.md_convruns with and without a residual,md_normwith and without SiLU, andmd_group_partplain and centered.md_pixelsis checked byte for byte against the same fp32 steps, with every tie between two bytes among its inputs.moss_image_records.zig.Tests run: all passed.
zig build test,test-golden,test-kernel-contract,test-server-security test-pull-security,fuzz-cortheon,dev-tools -Doptimize=Debugandcheck-release-paths.zig fmt --check build.zig zig.tools/zig/lean_check.py: 426 problems, the same as MOSS-Image: the prompt encoder on Metal #1512's head.Not run, and why:
Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo. (n/a: no drafts, prompt state or concurrency)SUMMARYline is in the receipt.