Repository navigation
moss image: serve pictures on CUDA (encoder, decoder and host for #1573) - #2281
devopsfortunato wants to merge 28 commits into
Conversation
|
Thanks @devopsfortunato, and for taking the split on #1573 the way @BobClawblaw handed it over. The CUDA host is careful: recognised only by
On #1573: the issue's plan was a Metal host gated on an M5, and this is the CUDA one. That's fine as the first host. We'll note on the issue that the Metal host is still open for whoever has a Mac and the checkpoint. |
ad9acd6 to
fafccae
Compare
|
Thanks — all four were fair. Here is where each stands. 1. The rebase: done, and one correctionYou were right that the stack no longer applied. Worse than it looked from my side: I had been rebasing onto my fork's
Rebased with On the head question: 2. The reference data, with hashesThe gates read these files. Sizes and sha256:
Generated with
3. The text-family A/B: started, and blocked by the GPUThe shape of the change first, because it explains what a mismatch would even look like: Main side, on this host, served from The branch side of those three is what I still owe you. It needs the same checkpoint resident, the host's own long-running server has taken the 78 GB again (the engine then reports Gemma is the harder half, and I need your help: nothing Gemma-shaped I can reach is served by the CUDA engine. 4. The kernel contract: in progressGates 1-4 with planted controls for the sixteen kernels, and the five-SM records, are being written now as the next commit on this branch, in the order that failed twice before when attempted whole: |
The Qwen A/B, both sidesSame checkpoint (
Identical, reply for reply ( Gemma: still blocked, and I need a checkpoint nameThree tries, three refusals at load, all with the engine's own words:
So on this host nothing Gemma-shaped that I can reach is a geometry your CUDA engine serves. Please name the Gemma checkpoint you run (repo id and revision), and I will run the same three prompts through both binaries and post the table. Failing that, the golden corpus is the standing evidence for the Gemma template path: Kernel contractUnder way, one kernel end to end first as promised: |
|
Thanks @devopsfortunato for the rebase onto our main, the head table, the reference files with their hashes, and
|
|
/atc exchange f468aaab2e5bf073c3f23f586868c4e80f0a962cf42f480b328609506b9b925f |
|
Thanks @devopsfortunato. The seven decoder kernels and three of the encoder's now have gate suites, planted controls and five-SM records. Still open before this leaves draft:
|
…lines under Unreleased, and the decoder's records no longer pending (ashhart#2281, part of ashhart#1573)
…o a second device's download cannot free through a dead driver (ashhart#2281)
…picture, byte-identical to the served one (ashhart#2281, part of ashhart#1573)
|
Thanks @devopsfortunato. This push adds contracts for
|
…so all nine of its kernels carry controls and five-SM records (ashhart#2281, part of ashhart#1573)
a0e3062 to
2f2d59b
Compare
…lines under Unreleased, and the decoder's records no longer pending (ashhart#2281, part of ashhart#1573)
…o a second device's download cannot free through a dead driver (ashhart#2281)
…so all nine of its kernels carry controls and five-SM records (ashhart#2281, part of ashhart#1573)
…in their own files, so both parents stay under 600 lines (ashhart#2281)
…picture, byte-identical to the served one (ashhart#2281, part of ashhart#1573)
…s two unnecessary bf16 waivers go, so gate 4 enforces them (ashhart#2281)
… neither is a token family (ashhart#2281)
|
Thanks — all six, on head
Every new kernel has a float64 reference, gates 1-4, a planted control that comes out red as planted, and five-SM records; the limits check reads
Checks on this head: |
…and enforces fp16 instead of waiving it (ashhart#1573)
…decoder's 3x3 serves its residual launches (ashhart#1573)
… with their controls and records (ashhart#1573)
…coder's records no longer pending (ashhart#2281, part of ashhart#1573)
…so all nine of its kernels carry controls and five-SM records (ashhart#2281, part of ashhart#1573)
…in their own files, so both parents stay under 600 lines (ashhart#2281)
…picture, byte-identical to the served one (ashhart#2281, part of ashhart#1573)
… bf16 waivers, so gate 4 enforces them, and the recipe lists all 24 kernels' suites (ashhart#2281)
… neither is a token family (ashhart#2281)
…' comment names the kernels they carry (ashhart#2281)
…mports, one table lookup for all three tools (ashhart#2281)
…ed lines come back (ashhart#2281) The MOSS-Image gate and its two include lines move to moss_controls.cuh, so contract_controls.cu is 494 lines and its two packed paged-attention lines are main's again. The moved block is byte-identical.
…process (ashhart#2281) fail() logs the out-of-service case at err again. The test runner counts any err log inside a test as a failure, so the unit test keeps the device-OOM half and a new check executable captures the err line, the one finished event and the released slot.
0350a66 to
0029fa0
Compare
|
Rebased as you asked: Content proof that the rebase changed only the base: the old delta GPU-free checks on the rebased head: Description: the "Stacked on #2253" note is replaced with a line saying it is no longer stacked (28 commits onto |
|
@devopsfortunato thanks, the rebase is just what we hoped for: 28 commits on main with #2253's two dropped, Nothing else is needed from you. The release is out, so next we run our two text-model checks on this head, one on Metal and one on CUDA, and once those and CI are green it goes on the landing queue. One tiny leftover, fix it or leave it: the description's checks list still opens with "Head Thank you for seeing this through so many rounds.
|
|
Fixed the leftover: the checks list now opens with "Head |
|
Thanks @devopsfortunato, and thanks for fixing the checks-list line so quickly. The tree reads well: the device-OOM handling in Main has moved since yesterday, so two small things are left:
Nothing else is needed from you beyond the rebased head.
|
|
The RTX 5090 run for the served/tool pair, plus the contract, the encoder and the decoder, on Setup
1.
|
| picture | bytes | sha256 (served = tool) |
|---|---|---|
| 1024x1024 | 2,220,048 | 38d07c0eddb97411e09f646b4d2355fc3b358b7d708aade2657f7da8ea97ad3d |
1024x1024, negative_prompt "blurry, low quality, watermark" |
2,130,293 | e34b51d5f4a3a9234d491d0edc00ae9291b371d782d45d9f306b72e991ee1ceb |
| 1280x1024 | 2,152,762 | 2a24c3b933930322e27bc7b419568ab5a78a8e19ec76cafd631ee4a211de94a7 |
| 1408x1024 | 2,215,657 | d2ef3022737c70c000579152c877d5d71ca672e40ff32cc471a76c54cf7c7ca2 |
| 1536x1024 | tool 2,465,606; served: refused | tool 6c9f5abd1e463c2b6f322ce0c63575d574d64fa249d20aab7d4bf46d51ad16e3 |
- The 1024x1024 request was served twice, and a third time after the refused request below: the same bytes each time.
- The 1536x1024 request, three times across two server starts:
HTTP 500 {"error": {"message": "OutOfDeviceMemory"}}after about 43 s, witherror: cuMemAlloc: CUDA_ERROR_OUT_OF_MEMORY (2)andrefused images-...: OutOfDeviceMemoryin the log. The engine keeps serving: the next 1024x1024 request returns the same hash. - After loading, the server holds about 24.1 GiB (24,674 to 24,682 MiB sampled) of the card's 32,607 MiB. 1408x1024 (1,441,792 pixels) is served; 1536x1024 (1,572,864 pixels, the cap the start line reports) is not. I did not probe between the two. The failure comes at about the time the tool needs for the 50 steps (43.4 s), so I read it as the decoder's scratch allocation, but the error line itself only names
cuMemAlloc. - The tool at 1536x1024 works because it frees the transformer before it decodes (
zig/tests/moss_image_run.zig:232): transformer 43.40 s, decode 1.19 s.
3. Encoder and decoder
tf-moss-image-encoderagainst a case frommoss_image_case.py encode(the float32 run of the 9B encoder needs about 31 GiB for the weights alone, nearly all of this card's 31.8 GiB; I did not try it on the GPU): 31 tokens, the same ids as the tokenizer's; all 32 layers and the final norm pass their gates; the whole prompt against float32 at 1-cosine 8.34e-5, worst row 0.02820 (torch bf16's 6.79e-5, 0.02683; reported, not gated); encoded again in 0.012 s with the same bytes.PASS every step within its gate, repeated byte for byte.tf-moss-image-decoderon this card's own 1024x1024 latents from the run above, againstmoss_image_case.py vae(float64 on the CPU):
| mean abs diff from float64 | max | 8-bit values off by 1 (of 3,145,728) | |
|---|---|---|---|
| ours | 4.357e-7 | 1.098e-5 | 189 |
| torch float32 on this card | 6.546e-5 | 1.725e-3 | 26,079 |
The recipe's GB10 numbers are 3.766e-7, 1.568e-5 and 138 for ours (other latents). Decode: 0.787 s here.
4. Speed, for reference
1024x1024, tool: both prompts encoded in 0.077 s, 50 steps in 27.54 s (0.554 s a step, two forwards), decode 0.79 s. The recipe's 5090 line says 24.6 s for 50 guided steps (driver 615.71); I did not look into the difference; the power limit here is below the card's default, which may be part of it (not tested). At 1280x1024 the 50 steps take 33.76 s and at 1408x1024 40.28 s.
5. Gate 5 on sm_120 only
kernel-contract-limits needs all five SMs, so on a -Dsm=120 build it exits 1 for the missing images. Reading only its sm_120 lines for the 27 MOSS-Image launches: all pass, 20 say (record current) and 7 say (record stale): tf_md_gemm (record 59 registers, here 64), tf_md_conv3x3 (55, 56), tf_md_group_finish (38, 37) and the four tf_md_group_part launches. Only registers differ; all are within their bounds. nvcc 13.0 is not installed here, so this may just be 13.3.73 against the records' compiler.
Not run
- Metal; any other card.
- The transformer's teacher gate and
tf-moss-image-blocks: they need the case files from torch's float32 run, which are only posted as hashes, and I did not make them: the recipe says the float32 run of its block cases does not fit an RTX 5090, and I did not try it. - The text-model A/B.
- Whether a 5090 with another driver gives the recipe's 24.6 s.
Raw logs and the pictures are available if wanted.
What this changes
On main, the family's CUDA path is the transformer alone: the prompt encoder and the VAE decoder are Metal-only, with
moss_image_case.pyrunning them in torch. This branch ports both to CUDA and adds the host that serves a picture, sotensorfold-native serveon a GB10 answersPOST /v1/images/generationsnatively with no torch in the path:cuda_encoder.zig+moss_image_encoder.cu+tf-moss-image-encoder— the checkpoint's Qwen3.5-9B prompt encoder (32 layers, full attention every fourth, Gated DeltaNet elsewhere, final norm), one kernel per Metal kernel, driven by the sameencoder_configrefusals.cuda_decoder.zig+moss_image_decoder.cu+tf-moss-image-decoder— the FLUX.2 small VAE decoder, float32 throughout, seven kernels.moss_image_host.zig+moss_image_spec.zig— encoder, transformer and decoder resident once, one picture at a time with a stated queue,VTable.submit_image/Info.image, the checkpoint's own chat bracket, the pipeline's schedule, noise and CFG (empty negative, block 22 skipped, guidance 1 skipping the negative pass), the picture quantised and PNG-encoded into the caller's list,finishedlast, cancellation between steps.cuda.zigrecognises the checkpoint bymodel_index.json(MOSSImagePipeline) before any token family's path; a checkpoint without it is unchanged.cuda_model.ziggainsfitso a host plans one scratch and serves every geometry inside it;hf_text.ziggains atokenizer/fallback for diffusers layouts (models that keeptokenizer.jsonat their folder root never reach it).tf-moss-imagegains--prompt,--pictureand--seed, so the tool can make a picture natively instead of reading torch's rows from a case.--prompt+--picturepath) is safe on main as it stands: main'sDriver.closealready callsdownload_stage.closeDriver, which drops this thread's pinned bounce and its cached device before the library closes. This pull request's ownrelease()and its tests went with the rebase, so the stage is released once.The issue's acceptance gate: the served picture equals the tool's, byte for byte
At the default guidance, seed 42, 50 steps, 1024×1024, with the case's prompt:
POST /v1/images/generations4ade0c108d19cd12dcdaec601ab56b7b0a51af118aab037504d0ee3aee21b738tf-moss-image --prompt … --picture … --seed 424ade0c108d19cd12dcdaec601ab56b7b0a51af118aab037504d0ee3aee21b738The same comparison at the other geometry the issue names, and with a negative prompt (same checkpoint, seed 42, 50 steps, guidance 3, the prompt above; built from the earlier head
0350a662con a GB10, sm_121):POST /v1/images/generations(size: "1536x1024")f69713489e26f86a24d95a45ee8c71ab0cc94dc53ebed8592b230c4a732a8eb8tf-moss-image --prompt … --picture … --seed 42f69713489e26f86a24d95a45ee8c71ab0cc94dc53ebed8592b230c4a732a8eb8negative_prompt"blurry, low quality, watermark", served4dd293de97bf4387794c73a16487b1cd25289adef5a527866836ff5aef82a740tf-moss-image --negative …4dd293de97bf4387794c73a16487b1cd25289adef5a527866836ff5aef82a740The 1536×1024 run is the tool's start-only path (schedule and seeded noise, no torch case for that geometry): it exits 2 by design, having written the picture; the transformer took 116.09 s for 50 steps and the decoder 5.574 s. The negative-prompt pair at 1024² also matches the earlier round's served hash, so it is stable across builds.
cmpreports identical; the served hash was reproduced by two independent server builds. The tool's encoding takes 0.355 s and the decode 3.667 s. Against the publisher's own picture the served one is 27.45 dB, which is the transformer's bf16 sensitivity on this prompt, not a pipeline difference.What made the first attempt differ is worth recording: the case's
latentswere dumped by this host's aarch64 torch, while the host starts fromnoise.zig, the port of x86-AVX torch's normal fill (the family's recorded hash for seed 42 at 1024² is378421c7…). Same seed, different CPU stream, different trajectory —--seedstarts the tool from the host's own noise, and everything else already agreed.The kernel contract
Every new kernel has a float64 reference, gates 1-4, a planted control that comes out red as planted, and register/shared-memory records for sm_80, sm_86, sm_89, sm_120 and sm_121. The five-SM limits check reads
(record current)for every moss launch, includingtf_me_gdn's 255 registers / 1024 B shared at 128 threads, which is the hardware ceiling and does not spill past the limits.Three findings came out of building these gates, all in the tests, none in a kernel:
siluis not monotone — it dips to −0.278 before z ≈ −1.278 — so a monotone interval mapper inverted the bounds; the suite now uses asiluRangethat includes the knee.tf_me_convskips taps withrow+k < 3rather than multiplying by zero, so a reference doing0 * wplanted NaNs the kernel never makes.tf_me_gdn_prep'sA_LOGandDT_BIASare bf16 tensors whiletf_me_gdn'sQN/KN/G/BETAare fp32; a first reference fed fp32 and gate 1 caught it.Declarations that are deliberate rather than waivers, stated in the source where they apply:
tf_me_conv's four-tap sums cannot separate a reduced accumulator (f16 = 0, measured0×),tf_me_gdn's output followsss^-1/2so a bf16 sum's error is under an output ulp, andtf_me_conv'smagisΣ|p|(the tap terms) because that is the errorsilucarries to the output.Checks on this head
0029fa0c0: this pull request's 28 commits ontomain@8a7838a65, with server: POST /v1/images/generations for an engine with an image capability (#1573) #2253's two commits dropped by the rebase and no conflicts. The patch is the same as0350a662c's; the base moved.zig build install -Doptimize=ReleaseFast -Dnvcc=/usr/local/cuda/bin/nvcc -Dsm=121— exit 0, all fourtf-moss-image*tools built;zig build native …— exit 0.tf-cuda-test moss-contract— exit 0,SUMMARY moss image: 24 kernels, 24 pass gates 1-4, 0 fail; 21 controls: 21 red as planted, 0 missed.zig build test-kernel-contract— exit 0.zig build test-moss-out-of-service— exit 0,moss out of service: one err line with the reason, one failed event, the engine stopped. It is a real control: withfail()'s log temporarily atwarnit exits 1 namingWrongLogCount.fail()logs the out-of-service case aterragain, and the unit test keeps the device-OOM half (the stopped path is the executable's, because Zig's test runner counts anyerrlog inside a test as a failure).zig build test— exit 0,607/607 steps succeeded; 7160/7219 tests passed (59 skipped);zig build test-golden— exit 0,322/335 + 55/55 equal to server goldens, 0 unexpected differences;zig build test-server-security test-pull-security— exit 0,14/14 steps succeeded; 30/30 tests passed.zig fmt --check build.zig zig— exit 0.lean_check.py— 116 problems, the same as the base364f2f6c7(0 new).contract_controls.cu494 lines andmoss_controls.cuh112 — the MOSS gate and its two includes moved into the new header, so contract_controls.cu is back under its old size with main's unpacked paged-attention lines;hf_text.zig571,engine_api.zig528 — all within 600.Reference data, with hashes
The gates read these files, produced by
tools/zig/moss_image_case.pyfrom the checkpoint at85975793d8b3d432f1b6e27f352abaa4c2a4c0d3on torch 2.14.1+cu130, diffusers 0.42.1, transformers 5.12.1, numpy 2.5.4:case.safetensors37a4b3594788fdcdfb0d7691eee8a6ae28b5adab3543fad0006c2b3949031f1blatents.f3288061b8cbb9def5c9cea8dc7a5c001cd340a270906e7b2d718bfe6346d5a5f07exact.latents.f32134ad4f6d92df93c7240a3e424181b005a42661ed762d13bdec332e94bd14604reference.pngda244b88cb488ba3bc495dec72282bdd2482845d2e7e719107c3f544b44f999cprompt.txtd96b2fbe4d920b020e7f259a0b5383632c11dd55ec28221995f90b40f2148438encoder.safetensors(the encoder gate)2fbe7ffc5a284e1b17d4ab1d229da97f6343d0980ec550dd61413a7d08f6cd9bvaeruns in float64 on the CPU on purpose: this card's cuDNN convolutions sit at TF32's noise level (its own fp32 decode is 3.8e-3 from float64), too coarse to gate a decoder.The text-family A/B
Same checkpoint (
Qwen3.8-Flash-Next-FP4-Local), same three prompts,temperature 0, 64 tokens, one binary frommain@7269f56cfand one from this head, the three requests concurrently with each also run alone and serially (tools/bench_concurrent.py --alone --serial):token_shab8cff47e51d3b8cff47e51d377c338127f7777c338127f7760a66a4dc3e460a66a4dc3e4On both binaries every concurrent reply equals its alone and serial run (3/3 equal, 0 unequal).
For the two text models the token streams are identical on both sides, temperature 0, 32 tokens, three prompts sent concurrently with each also run alone and serially (
tools/bench_concurrent.py --alone --serial) on a GB10,main@f03e5aac1against this branch's80c6f8ec5:mlx-community/gemma-4-12B-it-4bitat73bcf09gives3649ce497d81on both, andmlx-community/Qwen3.5-9B-MLX-4bitgives315abff85f3fon both. (Qwen/Qwen3.5-0.8Bis refused by the family asUnsupportedQwenQuantization, since its config is not 4-bit affine at group 64.) For Gemma the golden corpus also carries the template path:gemma_tool,gemma_tool-single,gemma_tool-streamandgemma_tool-single-streamdrive a real Gemma tool-call template through the real routes, and the corpus is identical on both sides (322/335 + 55/55, 0 unexpected) — the BF16 Gemma release is refused by name, matching the 4-bit conversion being what main serves.What this pull request does not do
callForresolves a served shape by(rows, cols, cin), and the decoder's last level has two launches on one triple: the 96→96 block conv (which loads a residual) andconv_out96→3 (the picture). The suite serves the residual, becauseaddgates a load path nothing else covers andconv_outis exercised end to end by the byte-equal receipt above. Keying the harness oncout/flags would cover both; that is a repo-wide contract change, not this PR.mi.host.quantize, so the tool and the server share one implementation; the byte-equal receipt below still holds.Checklist