Repository navigation
MOSS-Image: the prompt encoder on Metal - #1512
BobClawblaw wants to merge 3 commits into
Conversation
|
Thanks @BobClawblaw for the prompt encoder on Metal. Every new function has its kernel contract probes, and there are rounding and state controls for the whole family suite. Two asks before a device run:
|
6f05bcd to
198ed90
Compare
|
Thanks. Pushed 198ed90 (one commit on the two, rebased onto main 1b9a987): 1. The check now fails on a mismatch. The gate is per step, not on the whole prompt; point 2 is why.
2. The sticky note's row 58. Row 58 is
Pixels, the fox on this head:
Final pixels can't equal the torch pipeline's bit for bit: the encoder's rows differ from torch's by bf16 rounding. On this head: The description is updated with the gate, the numbers and the head. |
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.
198ed90 to
92c8298
Compare
What this changes
Fixes #1459. MOSS-Image's prompt encoder on Metal, inside the
moss_imagefamily (#1457). The encoder is the checkpoint'stext_encoder/, Qwen3.5's text model: 32 layers, three Gated DeltaNet layers to each gated full-attention one, hidden 4,096, bf16 as published. It runs as the pipeline'sencode_promptdoes: the prompt in the pipeline's chat format, one causal forward, and the final norm's rows. With--prompt,tf-moss-imagemakes both prompts' rows itself, so torch no longer loads the encoder.encoder_config.zig(the released geometry, checked),encoder_weights.zig(thelanguage_model.tensors; the vision tower is not read),encoder.zig(the chat format, the token ids, the forward), and the launchers inkernels.zig.zig/kernels/metal/moss_image_encoder.metal:me_norm,me_head,me_attention,me_gate,me_silu_mul,me_conv,me_gdn_prep,me_gdnandme_gated_norm. The projections reuse the Qwen-VL tower'stfp::gemm_nax, as the transformer's do, and the residual adds reusemi_gate_add. No shared kernel changes.tokenizer/tokenizer.json. The engine module has no tokenizer, so the build gives it to the test programs (one import added toengine_programsinmetal_targets.zig). No program's behaviour changes.tf-moss-image --prompt FILE [--negative FILE]encodes both prompts natively and frees the encoder before the transformer loads.tf-moss-image-encoderchecks a case frommoss_image_case.py encode. The ids must match. Each layer and the final norm, run alone (Encoder.steps) on the float32 run's input rounded to bf16, must be within twice torch bf16's distance from float32 on the same input, in cosine error and in its worst row. The whole prompt must repeat byte for byte. It exits 1 otherwise.reference.zigholds the gate, and its tests plant a mismatch that must fail with an identical repeat.encode,start --no-encoderandexact --fp32-encoder. The last is the float32 ruler the docs quoted for moss_image: MOSS-Image-Base's transformer on the Metal runtime (new family, denoiser) #1457.Encoder.loadrefuses a GPU without tensor units and names it, likeModel.load.Receipt
f9651de30(code) and92c829887(the gate) on maindd0a9cc5e.OpenMOSS-Team/MOSS-Image-Base@85975793d8b3d432f1b6e27f352abaa4c2a4c0d3.pipeline_moss_image.py(sha256-checked) on torch 2.14.1 (MPS, bf16), diffusers 0.42.0, transformers 5.21.0.Tokens: the ids equal the publisher's tokenizer's on all three (10, 34 and 63 tokens).
Each step alone, on 9 prompts (10 to 75 tokens), every layer and the final norm on the float32 run's input in bf16: our distance from float32 is a mean of 0.50 of the gate's limit, which is torch bf16's own distance, and at most 0.65 (the fox, layer 13's worst row). All 297 gates pass. A row planted 5% off at layer 20 fails at layers 20 and 21, with an identical repeat.
Every layer of the whole prompt against torch bf16: cosine 0.99974 or better.
The final rows against a float32 run of the encoder, with torch bf16's own:
These are not gated. Over 32 layers in bf16, each implementation's rounding is magnified differently, so two correct runs part by more than any one step's error. On the 75-token prompt, torch's own variants (MPS or CPU; chunks of 8, 16, 32 and 64, or the recurrent form; equal in exact arithmetic) end 0.86 to 4.97 times its default's cosine error from float32, and ours 1.82 times.
Pictures: with our rows, the transformer's latents through the pipeline's decoder are 47.07 dB PSNR from the pipeline's picture for the fox, and 28.71 dB for the sticky note. With torch's rows they are 48.52 and 28.93 dB. The ruler is a float32 run of the whole schedule, the encoder included, over 8 cases (both prompts, 4 seeds each):
Ours with our rows is the closer of the last two in 4 of the 8 cases.
Repeats: an encode run twice gives identical bytes.
Pixels, the fox case on this head:
2e3100e8…);--prompt, the latents equal the first head's (6f05bcd94) byte for byte (sha2569ba4fd4c…), so the decoded picture is the same.tf-kernel-contract --only "moss image"on the M5 Ultra:me_norm(the transformer'sNormtest, generalized to plain, modulated and zero-centered),me_head,me_gate,me_silu_mul,me_conv,me_gated_norm,me_gdn_prep,me_gdnandme_attention.me_gdn: checked against the recurrence in float64. Each output's allowance is its two sums' rounding over the state's magnitude, plus every earlier step's error, which a decay and a unit-key projection never grow.me_gdn, one-hot keys write j, 2^20 and -2^20 to three state rows, which its query sums back to j.me_convuses taps of 256, 1, 1 and -256 over rows that cancel to 2. The attentions use equal scores over values whose sums reach about 2^22 and cancel.--offlinecannot compile the tensor-op source for M1/M2, and the family refuses those chips.zig build test,test-golden,test-kernel-contract,test-server-security test-pull-security,fuzz-cortheon,dev-toolsanddev-tools -Doptimize=Debug.zig fmt --check build.zig zig.tools/zig/lean_check.py: 425 problems, the same as main.atc overlap: every open PR it test-merged merges cleanly. Its conflicts are with local worktrees only: the sharedmetal_targets.zigandsources.ziglists, and older MOSS-Image branches.A recipe branch that builds this PR's first head (
6f05bcd94), whose latents this head reproduces byte for byte: https://github.com/BobClawblaw/moss-image-base-bf16-m5-ultra/tree/native-encoderChecklist
"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.