Skip to content

refactor(preflight): platform names a (backend, runtime) pair, and mlx-cuda exists - #172

Merged
glennneuber merged 1 commit into
mainfrom
refactor/platform-taxonomy
Aug 17, 2026
Merged

glennneuber merged 1 commit into
mainfrom
refactor/platform-taxonomy

Conversation

@glennneuber

Copy link
Copy Markdown

The platform field held three naming schemes in four values: cuda/rocm named the backend, apple-silicon named the hardware, and apple-silicon-mlx folded 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-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 × runtime

The MLX runtime prefixes its backend, matching 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. Old --platform values are accepted as deprecated aliases and print what they resolved to, so nothing in flight breaks.

mlx-cuda now exists — declared, not claimed

Never 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 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.

A bug the rename exposed

--platform's choices was 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 can't drift again.

Verification

verdict gate            34/34  (it caught a missing `reason` on the new
                                unmeasured blocks and was right to)
--platform mlx-metal    resolves, 10 PASS / 0 FAIL on this host
--platform apple-silicon-mlx  -> "deprecated; using 'mlx-metal'", resolves to
                                 mlx-metal-0-32-14, 10 PASS
--platform mlx-cuda     exit 4, status = unmeasured ×3

Historical campaign docs keep the old names — they describe runs that used them.

🤖 Generated with Claude Code

…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>
@glennneuber
glennneuber merged commit bc11eab into main Aug 17, 2026
8 of 11 checks passed
@glennneuber
glennneuber deleted the refactor/platform-taxonomy branch August 17, 2026 06:28
@glennneuber

Copy link
Copy Markdown
Author

Reviewing as consolidator. The taxonomy is right and worth landing — platform genuinely held three naming schemes in four values, and deriving --platform's choices from the expectations file plus aliases (rather than a hardcoded list that shadowed resolve_profile) is the correct fix for a bug that could otherwise recur on the next rename. Deprecated aliases that print what they resolved to means nothing in flight breaks.

Two corrections, both in your favour, plus data.

1. The unmeasured reason for mlx-cuda is out of date

The blocks cite "no ELF RPATH and libquadmath unbundled, so it loads only where the host already provides CUDA, cuDNN, NCCL and libquadmath". That was fixed and is on main:

cmake/mlx/CMakeLists.txt:38   set(CMAKE_BUILD_RPATH "$ORIGIN")      # ELF spelling
cmake/mlx/CMakeLists.txt:39   set(CMAKE_INSTALL_RPATH "$ORIGIN")
cmake/mlx/CMakeLists.txt:109  MLX_INCLUDE_REGEXES ... gfortran quadmath

And the shipped payload carries it — maxusai/ollama:0.32.14-rc0-dynres-mlxfix:

/usr/lib/ollama/mlx_cuda_v13/libquadmath.so.0
/usr/lib/ollama/mlx_cuda_v13/libquadmath.so.0.0.0

Worth correcting in place, because that text is the thing a future reader will use to decide whether the platform is worth measuring, and it currently says "don't bother".

(A caution from my own record on this: an earlier round of that investigation published "no RPATH" findings that were false negatives — readelf is absent from the runtime image and returned nothing, which I read as "no RPATH present". Anything asserting RPATH absence should say what tool produced the evidence.)

2. mlx-cuda is no longer unmeasured — I measured it today

Full vision suite against maxusai/ollama:0.32.14-rc0-dynres-mlxfix on :11495, gemma4:31b-nvfp4, through run_engine_compare.sh with a cold restart per cell.

Think-off, n=3 — bit-identical across all three repeats:

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.

glennneuber added a commit that referenced this pull request Aug 17, 2026
… 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>
glennneuber added a commit that referenced this pull request Aug 17, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant