Repository navigation
refactor(preflight): platform names a (backend, runtime) pair, and mlx-cuda exists - #172
Conversation
…x-cuda exists The platform field held three naming schemes in four values: "cuda" and "rocm" named the BACKEND, "apple-silicon" named the HARDWARE, and "apple-silicon-mlx" folded the RUNTIME into the same field. That is not a cosmetic inconsistency — it is why a missing CUDA-side MLX profile read as a category error rather than a gap. "apple-silicon-mlx" makes MLX look like an Apple concept when the build ships mlx_cuda_v13 for Linux and Windows (cmake/local.cmake). Platform is now backend x runtime, with the MLX runtime prefixing its backend to match the payload directories the build produces: backend llama.cpp MLX metal metal mlx-metal (mlx_metal_v4) cuda cuda mlx-cuda (mlx_cuda_v13) rocm rocm -- no MLX ROCm backend upstream cpu cpu -- no mlx_cpu target is built Renames: apple-silicon-metal -> metal, apple-silicon-mlx -> mlx-metal, apple-silicon-mlx-0-32-14 -> mlx-metal-0-32-14, apple-silicon-cpu -> cpu. The old --platform values are accepted as deprecated aliases and print a line saying what they resolved to, so nothing in flight breaks. ADDS THE mlx-cuda PROFILE, never measured and declared so rather than absent. Its three arches carry status = "unmeasured" with reasons naming the known packaging fault: the payload ships with no ELF RPATH and libquadmath unbundled, so it loads only where the host already provides CUDA, cuDNN, NCCL and libquadmath. preflight exits 4 rather than implying a pass — verified. Fixes a bug the rename exposed: --platform's choices were a HARDCODED list, so the new names were rejected by argparse before resolve_profile — the single place that owns this — ever ran. Choices are now derived from the expectations file plus the aliases, so the two cannot drift again. Verified: verdict gate 34/34 (it caught a missing `reason` on the new unmeasured blocks and was right to); mlx-metal resolves and passes 10/10 on this host; the deprecated alias resolves to mlx-metal-0-32-14 with a warning; mlx-cuda exits 4. Historical campaign docs keep the old names — they describe runs that used them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Reviewing as consolidator. The taxonomy is right and worth landing — Two corrections, both in your favour, plus data. 1. The
|
| metric | mlx-cuda (today) | mlx-metal (2026-08-08) |
|---|---|---|
| scene bbox IoU | 0.962 | 0.964 |
| boxes / labels / colors | 6/6 · 6/6 · 6/6 | 6/6 · 6/6 · 6/6 |
| serial | ✅ | ✅ |
| invoice items · qty+price · total | 5/5 · 5/5 · ✅ | 5/5 · 5/5 · ✅ |
| name_bbox hits | 4 | 4 |
| fine text 22/16/12/9/7 | 4/4/4/3/3 | 4/4/4/2/1 |
| multi-image | ✅ all Qs + bbox | ✅ all Qs + bbox |
contract_followed |
7/7/7 of 8 | — |
| gen / prefill tok/s | 28 / 434 | 50 / 519 |
Scene IoU differs by 0.002, inside ADR 0012's ±0.01 noise floor. Every exact-match cell is identical. The runtime behaves the same on both backends.
3. But keep it guarded — I found a crash
Under think-on, the 12th consecutive cell (finetext) aborted the runner:
mlx runner aborted: mlx: cudaGraphAddDependencies(graph_, from_nodes_.data(),
to_nodes_.data(), nullptr, from_nodes_.size()) failed: invalid argument
at build/mlx_cuda_v13/_deps/mlx-c-src/mlx/c/transforms.cpp:15
Runner.prefill -> mlx.Eval -> panic
-> (during unwind) cacheSession.close -> mlx.AsyncEval -> panic again
The same test under think-off passes (json_valid=true, eval 263, prompt_eval 1173) at the same num_ctx 16384, so window size is not the trigger. The nested panic in prefix-cache teardown points at cache state interacting with CUDA graph capture — hypothesis, not conclusion; I have not reproduced it in isolation. The other 11 think-on cells all returned valid JSON.
So I'd suggest mlx-cuda land as measured for think-off, with think-on carrying a stated known-defect rather than either unmeasured or a clean bill.
4. What I deliberately am NOT offering
The five-geometry token ladder. I have strong indirect evidence it is flat 1102 exactly as mlx-metal records — a bare probe gave prompt_eval_count 1127 on a 25-token prompt — but ADR 0011 rule 4 exists precisely to stop me pasting the mlx-metal row across, and indirect evidence is not the B8 prefix measurement. It needs measuring properly.
Which is where these all converge: #168 generates the [expect.…] row instead of transcribing it, #171 fixes the docker logs --since window that #168's budget read depends on (zoneless UTC stamp → ten-hour window on this host), and #172 creates the profile to put the row in. I have the server. If you land the rename, I'll run measure_ladder.py against :11495 and post the generated block for the mlx-cuda.gemma4 row.
… premises The profile landed in #172 describing a combination nobody had run. Three of its statements are no longer true, and one of them made the gate unreachable. THE VERSION PATTERN COULD NOT MATCH THE ARTIFACT IT DESCRIBES. It required "-maxusai-", the NATIVE macOS build's stamp. The MLX/CUDA payload ships in a container stamped "-dynres-": server: 0.32.14-rc0-dynres-20-geb0ad43 mlx-cuda (was): ^0\.32\.[0-9]+-maxusai-[0-9a-f]{7,40}$ NO MATCH cuda-dynres-903: ^0\.32\.\d+(-rc\d+)?-dynres(...)$ MATCH So `--platform mlx-cuda` would have failed to resolve a profile for the only server that can exercise it. Now uses the same shape as cuda-dynres-903, the other profile served from a container on this host. THE PACKAGING FAULT IS FIXED. The profile expects the payload "to fail before it measures anything — no ELF RPATH, libquadmath unbundled". Both were repaired before the profile was written and the fix is on main: $ORIGIN for the ELF build/install RPATH in cmake/mlx/CMakeLists.txt, and quadmath appended to MLX_INCLUDE_REGEXES. The shipped payload carries libquadmath.so.0. The upstream-mlx-cuda-payload-unloadable.md reference now says it describes the state BEFORE the fix, and carries the caution that an earlier round of that investigation published "no RPATH" FALSE NEGATIVES because readelf is absent from the runtime image and returned nothing. IT IS NO LONGER UNEXERCISED. gemma4:31b-nvfp4 served the full vision suite on this backend on 2026-08-17, think-off n=3 bit-identical: scene bbox IoU 0.962, boxes/labels/colors 6/6, serial and invoice exact, fine text 4/4/4/3/3, multi-image all Qs + bbox, contract_followed 7/7/7 of 8. Against mlx-metal for the SAME model that is 0.962 vs 0.964 — inside ADR 0012's +/-0.01 noise floor. THE ARCH BLOCKS ARE DELIBERATELY LEFT UNMEASURED. Those assert TOKEN LADDERS, and the evidence above is quality. The B8 prefix measurement has not been run on this host, and ADR 0011 rule 4 forbids copying the mlx-metal ladder across on the strength of quality parity. preflight still exits 4 (NEEDS_BASELINE). A think-on defect is recorded rather than left to be rediscovered: the twelfth consecutive cell aborts the runner with mlx: cudaGraphAddDependencies(...) failed: invalid argument from Runner.prefill -> mlx.Eval, plus a second panic in cacheSession.close during unwind. Same cell passes think-off at the same num_ctx, so window size is not the trigger. Not reproduced in isolation; stated as a hypothesis. verdict gate 34/34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… premises (#175) * fix(preflight): make the mlx-cuda profile resolvable, and correct its premises The profile landed in #172 describing a combination nobody had run. Three of its statements are no longer true, and one of them made the gate unreachable. THE VERSION PATTERN COULD NOT MATCH THE ARTIFACT IT DESCRIBES. It required "-maxusai-", the NATIVE macOS build's stamp. The MLX/CUDA payload ships in a container stamped "-dynres-": server: 0.32.14-rc0-dynres-20-geb0ad43 mlx-cuda (was): ^0\.32\.[0-9]+-maxusai-[0-9a-f]{7,40}$ NO MATCH cuda-dynres-903: ^0\.32\.\d+(-rc\d+)?-dynres(...)$ MATCH So `--platform mlx-cuda` would have failed to resolve a profile for the only server that can exercise it. Now uses the same shape as cuda-dynres-903, the other profile served from a container on this host. THE PACKAGING FAULT IS FIXED. The profile expects the payload "to fail before it measures anything — no ELF RPATH, libquadmath unbundled". Both were repaired before the profile was written and the fix is on main: $ORIGIN for the ELF build/install RPATH in cmake/mlx/CMakeLists.txt, and quadmath appended to MLX_INCLUDE_REGEXES. The shipped payload carries libquadmath.so.0. The upstream-mlx-cuda-payload-unloadable.md reference now says it describes the state BEFORE the fix, and carries the caution that an earlier round of that investigation published "no RPATH" FALSE NEGATIVES because readelf is absent from the runtime image and returned nothing. IT IS NO LONGER UNEXERCISED. gemma4:31b-nvfp4 served the full vision suite on this backend on 2026-08-17, think-off n=3 bit-identical: scene bbox IoU 0.962, boxes/labels/colors 6/6, serial and invoice exact, fine text 4/4/4/3/3, multi-image all Qs + bbox, contract_followed 7/7/7 of 8. Against mlx-metal for the SAME model that is 0.962 vs 0.964 — inside ADR 0012's +/-0.01 noise floor. THE ARCH BLOCKS ARE DELIBERATELY LEFT UNMEASURED. Those assert TOKEN LADDERS, and the evidence above is quality. The B8 prefix measurement has not been run on this host, and ADR 0011 rule 4 forbids copying the mlx-metal ladder across on the strength of quality parity. preflight still exits 4 (NEEDS_BASELINE). A think-on defect is recorded rather than left to be rediscovered: the twelfth consecutive cell aborts the runner with mlx: cudaGraphAddDependencies(...) failed: invalid argument from Runner.prefill -> mlx.Eval, plus a second panic in cacheSession.close during unwind. Same cell passes think-off at the same num_ctx, so window size is not the trigger. Not reproduced in isolation; stated as a hypothesis. verdict gate 34/34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * feat(preflight): measure the mlx-cuda gemma4 token ladder The profile's own text said "do NOT copy the mlx-metal ladder, which describes a different backend". So it was measured instead, with measure_ladder.py (#168) against :11495, server 0.32.14-rc0-dynres-20-geb0ad43: prefix=19 text_only=19 (image_prefix / B8, not text_baseline) 256x144: 1102 2048x1152: 1102 512x288: 1102 3072x1728: 1102 1024x576: 1102 Flat 1102 throughout — the same values mlx-metal records. That is the expected result, because MLX sizing goes through llm.BudgetFillSize, plain Go, identical on both backends. It is now asserted from measurement rather than from that expectation, which is the whole of ADR 0011 rule 4. THE BUDGET FIELDS ARE NOT FROM A LOG, AND COULD NOT BE. The MLX runner does not emit llama.cpp's `load_hparams: image_*_pixels` lines, so measure_ladder.py's --container path finds nothing on an MLX server and leaves every budget field TODO — correctly refusing to invent them. They are read from the fork's own source instead: 70/1120 are the ADR 0008 gemma4 defaults (llm/llama_server_test.go:1575, :1738) and 48 is the align argument BudgetFillSize is called with (llm/llama_server.go:1511). Source is a stronger provenance than a log line, not a weaker one, and the comment says so. qwen35moe and qwen35 stay unmeasured; only gemma4 was run. verdict gate 34/34; image_min_pixels == 70*48^2 and image_max_pixels == 1120*48^2 both check out. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(preflight): correct the budget provenance note; the MLX runner DOES log it An earlier revision of this comment claimed "the MLX runner does not emit llama.cpp's load_hparams: image_*_pixels lines". That is false. It emits them, and preflight's payload_proof passes on this profile precisely by reading them: PASS payload_proof [gemma4]: budget logged as custom on max/min (min 70*48^2=161280, max 1120*48^2=2580480) A check that reads those lines cannot pass without them. The first measure_ladder run returned TODO for every budget field and that was misdiagnosed as a format gap. It was a race between forcing the reload and reading the log; re-running produced the values above, which independently agree with the fork's own constants (ADR 0008's 70/1120, and 48 as the align argument BudgetFillSize is called with). Two independent sources agreeing is a stronger provenance than the one-source story the comment used to tell. A TOOL RETURNING NOTHING IS NOT EVIDENCE THAT NOTHING EXISTS. Same mistake as reading an absent readelf as "no RPATH present", which this fork has already published once and retracted. The comment now says so, because the reasoning error is the part worth carrying forward. Verified by a full run: preflight --platform mlx-cuda --arch gemma4 -> PASS=7 SKIP=3, VERDICT: PASS. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * docs(preflight): replace the stale mlx-cuda qwen reasons with what was measured Both said "As gemma4 above: no CUDA host has served the MLX runtime". gemma4 is now measured and preflight PASSes on it, and both qwen arches have been run, so the text was false on every clause. qwen35moe: attempted, does not complete. Four geometries measured (68/146/578/2306), then the runner aborted on 3072x1728 with cudaMallocAsync out of memory -- with 85 GiB free. MLX's allocator caches, and OLLAMA_MLX_MEMORY_LIMIT was ~82 GiB because client.go derives it from all free VRAM, so the pool could grow before the vision tower asked for a large block. The reason now says to re-measure with the limit pinned rather than to conclude anything about the arch: the first four rungs match qwen35 exactly. qwen35: ladder measured, budgets not. [68, 146, 578, 2306, 5186], prefix == text_only == 13, identical to the mlx-metal row for the same model. The budget read in that same run picked up gemma4's pixel lines -- a preflight run was probing gemma4 on the same server inside the log window -- and dividing 161280/2580480 by qwen's stride 32 produced 157/2520, which does not round-trip (157*32^2 = 160768). measure_ladder.py refuses that now. The reason records the ladder so the re-run only needs a clean budget read. Neither arch is promoted to "measured". A ladder without budgets is not a row, and an arch that aborts mid-ladder is not a baseline. preflight still exits 4. verdict gate 34/34. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
The
platformfield held three naming schemes in four values:cuda/rocmnamed the backend,apple-siliconnamed the hardware, andapple-silicon-mlxfolded the runtime into the same field.That's not cosmetic. It's why a missing CUDA-side MLX profile read as a category error rather than a gap —
apple-silicon-mlxmakes MLX look like an Apple concept, when the build shipsmlx_cuda_v13for Linux and Windows (cmake/local.cmake).Platform is now backend × runtime
The MLX runtime prefixes its backend, matching the payload directories the build produces:
metalmlx-metal—mlx_metal_v4cudamlx-cuda—mlx_cuda_v13rocmcpumlx_cputarget is builtRenames:
apple-silicon-metal→metal,apple-silicon-mlx→mlx-metal,apple-silicon-mlx-0-32-14→mlx-metal-0-32-14,apple-silicon-cpu→cpu. Old--platformvalues are accepted as deprecated aliases and print what they resolved to, so nothing in flight breaks.mlx-cudanow exists — declared, not claimedNever measured, and until now it had no profile at all. Its three arches carry
status = "unmeasured"with reasons naming the known packaging fault: the payload ships with no ELF RPATH andlibquadmathunbundled, so it loads only where the host already provides CUDA, cuDNN, NCCL and libquadmath.preflightexits 4 rather than implying a pass.A bug the rename exposed
--platform'schoiceswas a hardcoded list, so the new names were rejected by argparse beforeresolve_profile— the single place that owns this — ever ran. Choices are now derived from the expectations file plus the aliases, so the two can't drift again.Verification
Historical campaign docs keep the old names — they describe runs that used them.
🤖 Generated with Claude Code