Skip to content

ADR 0037: keep the MLX #3912 fix and promote 0.34 on mlx-metal — campaign, OCRBench, SPEC H15–H18, results - #324

Merged
glennneuber merged 4 commits into
mainfrom
docs/vision-campaign-8a7ba949
Sep 19, 2026
Merged

glennneuber merged 4 commits into
mainfrom
docs/vision-campaign-8a7ba949

Conversation

@glennneuber

@glennneuber glennneuber commented Sep 18, 2026 •

Copy link
Copy Markdown

Closes the 0.34 fold's Metal kernel question and promotes the build on the mlx-metal surface.

Decision — ADR 0037

Keep the MLX pin at d9add9d1 with ml-explore/mlx#3912. No revert, no compat patch restoring the old fp_qmm_t loop bound. Decided on correctness, not accuracy:

0.34.0-maxusai-8a7ba949 (MLX d9add9d1, fixed)    835/1000 = 0.8350
0.33.2-maxusai-2b95b4a5 (MLX c793734e, defect)   833/1000 = 0.8330
both right 826 | both wrong 158 | fixed only 9 | defect only 7
discordant 16   exact McNemar two-tailed p = 0.8036

OCRBench cannot tell the kernels apart; the golden delta can (fixed = MLX-CUDA's 0.0898 exactly). ADR 0036 already accepted the same 4→3 move on 31b's 9px cell as the cost of the faithful path on GGUF — two unrelated correctness fixes, two engines, same cell, same direction.

SPEC — vision-harness-reuse H15–H18

rule enforced?
H15 one capture of a quality cell is one draw — measure a rate no
H16 both finetext arms count; the renderer shows only the suite arm no — the renderer works against it
H17 a checkpoint is its manifest digest, not its tag or config digest partly (quant_dims.py)
H18 cross-build accuracy needs a paired many-item test; a tier move needs the bf16 arm no

Promotion

0.34.0-maxusai-8a7ba949 has served :11435 since 2026-09-18. README deploy line + the release matrix's mlx-metal row, generated by release_matrix.py from the build's preflight run (PASS 19 / SKIP 12) and spliced beside the CUDA row — whose run was never committed, so regenerating the whole matrix here would have erased it.

Two things found while promoting, recorded rather than fixed:

  • Stamps disagree with ADR 0032. Metal builds stamp 0.34.0-maxusai-<sha>, the fold 0.34.1-dynres-0-g<sha>, same commit.
  • The Metal launchd env does not set OLLAMA_MLX_DRAFT_UNDER_GRAMMAR=0, unlike the CUDA container (ADR 0033, the fold's unbounded-retention finding). Changing it is a persistent config change plus a service restart, left to the operator.

Committed results

Assembled from gitignored captures by script (ADR 0012 rule 8), one record per line:

  • bench-runs/ocrbench-v1-1000-gemma4-31b-nvfp4-0340-vs-0332.json — 1000 paired items
  • bench-runs/finetext-9px-31b-quant-reps-2026-09-18.json — N=10 × 3 quants, both-arm history
  • bench-runs/vision-campaign-2026-09-18-mlx8a7ba949.json — every campaign cell, both arms
  • preflight/runs/preflight-mlx-metal-0340-8a7ba949.json — force-added like the 0.33.0 fold's; the invalid --container native run is left out

Retractions (all mine, corrected in the docs they reached)

  1. "Twelve runs split cleanly" counted one of two finetext arms — the cell is bimodal.
  2. "31b-mxfp8 carries a bf16 vision tower" generalised from one tensor; 162 of 356 vision layers are 8-bit.
  3. The 200-item OCRBench slice's 4–0 was noise (5/7 over the rest) and its 87.5% unrepresentative (83.5% over 1000).

🤖 Generated with Claude Code

glennneuber and others added 3 commits September 18, 2026 23:49
…ncoder

Two corrections to claims made earlier today.

1. "Twelve runs split cleanly by build across both think modes" counted only
   one of the suite's TWO finetext arms. vision_suite.py writes
   scores_<tag>.json and finetext_probe.py writes ft_<tag>.json; they are
   independent generations. The scores_ arm holds a 0.34.0 capture scoring 4
   with byte-identical provenance to an ft_ capture scoring 3 — same
   prompt_sha, images_sha, window, eval_count and answer_chars, texts differing
   by one character, RNK against RMK, ground truth RNK. Replaced with N=10 per
   quantization:

     31b-mlx-bf16  10/10 at 4      31b-mxfp8  0/10      31b-nvfp4  2/10

   A single capture of this cell reports the minority mode about one time in
   eight.

2. "31b-mxfp8 carries a bf16 vision tower" was wrong. Its vision down_proj
   blob IS byte-identical to bf16's (9,916,560 B, digest 4b80a6f9), and I
   generalised from that one tensor to all 356. In fact 162 of its vision
   layers are 8-bit; only down_proj was left at bf16. So mxfp8 is not a
   bf16-encoder arm, and the inference drawn from it — that the language model
   alone flips the glyph — is withdrawn. Surfaced by the MLX-CUDA session's
   registry audit, which found our local 31b-nvfp4 and the library's current
   tag share a config digest but differ in 194 vision layers.

What survives is stronger than what it replaces: the cell does not order by
numerical precision. 31b-mxfp8 has a bf16 down_proj — the exact tensor ollama#3912
corrupts — an 8-bit tower and an 8-bit LM, and scores 0/10, below the all-4-bit
31b-nvfp4's 2/10. A metric on which more precision scores worse is not
measuring quality and cannot be read as a verdict on the encoder.

Adds the 2026-09-18 campaign: five nvfp4 models think-off, two think-on, on
0.34.0-maxusai-8a7ba949, both phases rc=0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ified tags

Our local gemma4:31b-nvfp4 and the registry's current tag share a config digest
while differing in 194 vision layers: ours has a 4-bit vision tower, the
registry now ships that tag with a bf16 one. A pull would silently replace the
checkpoint this campaign measured, under the same name, with no version change.

Copies the three 31b MLX checkpoints to tower-qualified tags — manifests only,
so the blobs are shared and the copies cost no disk:

  gemma4:31b-nvfp4-tower-nvfp4      4-bit tower,  4-bit LM
  gemma4:31b-mxfp8-tower-mxfp8      bf16 down_proj, 8-bit tower, 8-bit LM
  gemma4:31b-mlx-bf16-tower-bf16    bf16 tower,   bf16 LM

The -tower-mxfp8 name is deliberately lossy; the table in the doc is the
authority, since that tower is 8-bit in 162 of 356 vision layers with only
down_proj at bf16 — the reason it never meets the ollama#3912 defect even pre-fix.

12b needs no such tag: it has no vision_tower.encoder at all, only a
vision_embedder, which is why it was structurally immune throughout.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…table end to end

All 1000 OCRBench v1 items, gemma4:31b-nvfp4 think-off, same local checkpoint
under both archived binaries so only the build differs:

  0.34.0-maxusai-8a7ba949  (MLX d9add9d1, FIXED)   835/1000 = 0.8350
  0.33.2-maxusai-2b95b4a5  (MLX c793734e, BROKEN)  833/1000 = 0.8330

  both right 826 | both wrong 158 | FIXED only 9 | BROKEN only 7
  discordant 16, exact McNemar two-tailed p = 0.8036, churn 1.6%

Statistically indistinguishable. A kernel that corrupts 232722 of 294912
elements at K=4304 in isolation moves two items in a thousand end to end.

Two corrections this run forced:

- The earlier 200-item slice gave 4-0 to the fixed build and was reported as
  directionally unanimous but underpowered. The direction was noise: across the
  remaining 800 items the split is 5 fixed / 7 broken.
- That slice was also unrepresentative. Per-chunk accuracy runs 0.875, 0.845,
  0.945, 0.845, 0.665, so reading the first 200 as 87.5% and calling it a match
  for the tech report's 88.3 was wrong. The full-benchmark figure for this
  checkpoint is 83.5%, and the gap is attributed to nvfp4 quantization by
  plausibility, not measurement — 31b-mlx-bf16 has not been run over the 1000.

The decision stands on correctness, not accuracy: do not revert the kernel,
because the fixed one is arithmetically right and matches MLX-CUDA's golden
delta exactly at 0.0898 — not because reverting would cost OCR accuracy, which
at this precision it would not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…tal; SPEC H15-H18; commit the results

ADR 0037 records the decision: the MLX pin stays at d9add9d1 with
ml-explore/mlx#3912, no revert and no compat patch restoring the old
fp_qmm_t loop bound. It rests on correctness, not accuracy — the fixed
kernel matches MLX-CUDA's golden delta exactly at 0.0898, and on all 1000
OCRBench v1 items the two kernels are indistinguishable (835 vs 833,
exact McNemar p = 0.80). ADR 0036 already accepted the same 4 -> 3 move on
31b's 9px cell as the cost of the faithful computation on the GGUF path;
two unrelated correctness fixes on two engines move that cell the same way.

SPEC vision-harness-reuse gains four rules this investigation forced,
each with an honest conformance row (three are not enforced):
  H15  one capture of a quality cell is one draw — measure a rate
  H16  both finetext arms count; the renderer shows only the suite arm
       (summarize_engine_compare.py falls back to ft_ only when absent)
  H17  a checkpoint is its MANIFEST digest — not its tag, and not its
       config digest, which local 31b-nvfp4 and 31b-mlx-bf16 share
  H18  cross-build accuracy needs a paired test on many items; a tier
       move needs the unquantized arm on the same binary

Promotes the mlx-metal surface: README deploy line and the release
matrix's mlx-metal row, generated by release_matrix.py from the build's
own preflight run and spliced beside the CUDA row, whose run was never
committed — regenerating the whole matrix here would have erased it. The
matrix comment records why the two rows come from two hosts and two
stamps (0.34.0-maxusai vs 0.34.1-dynres, flagged against ADR 0032). The
deploy line also records that the Metal launchd environment does not set
OLLAMA_MLX_DRAFT_UNDER_GRAMMAR=0, unlike the CUDA container.

Committed results, assembled from the gitignored run captures by script
(ADR 0012 rule 8), serialized one record per line:
  bench-runs/ocrbench-v1-1000-gemma4-31b-nvfp4-0340-vs-0332.json
  bench-runs/finetext-9px-31b-quant-reps-2026-09-18.json
  bench-runs/vision-campaign-2026-09-18-mlx8a7ba949.json
  preflight/runs/preflight-mlx-metal-0340-8a7ba949.json  (force-added,
    as the 0.33.0 fold's closing Metal run was; the invalid
    --container native run of the same build is deliberately left out)

BINARIES.md marks 8a7ba94 promoted and warns that the 0.33.2 rollback
target reinstates the defect. The campaign doc records the manifest
digest of every checkpoint it measured, per H17.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@glennneuber glennneuber changed the title docs(vision): the 2026-09-18 campaign, and OCRBench settles the kernel question ADR 0037: keep the MLX #3912 fix and promote 0.34 on mlx-metal — campaign, OCRBench, SPEC H15–H18, results Sep 19, 2026
@glennneuber
glennneuber merged commit 6bd8634 into main Sep 19, 2026
1 check passed
@glennneuber
glennneuber deleted the docs/vision-campaign-8a7ba949 branch September 19, 2026 07:41
@glennneuber

Copy link
Copy Markdown
Author

Consolidated into #327, which merges this PR with the other two OCRBench streams (#323 CUDA, #324 mlx-metal, #325 ROCm/GGUF). It contains every commit from all three, so merging #327 closes them together and there is no three-way conflict to resolve later.

What moved, so nothing is lost:

All three runs agree: no quantisation difference resolves on any host, and OCRBench cannot separate the kernels where the goldens can.

🤖 Generated with Claude Code

glennneuber pushed a commit that referenced this pull request Sep 19, 2026
main took 0037 for "keep the MLX ollama#3912 kernel fix" when #324 merged, so
this branch's 0037 and main's are different decisions under one number.
The merge shows CLEAN because the filenames differ -- git has no reason
to object, and the collision would only surface when someone cites
"ADR 0037" and gets the wrong document.

Mechanical: two git mv plus the six references in README.md,
ocrbench-quantisation-ladder.md and spec/vision-harness-reuse.md.
Patterns are anchored to "ADR 003x" and the full filenames, because a
bare 0037 also matches the 1.003776 JPEG constant in
synthetic-triggers/README.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
glennneuber pushed a commit that referenced this pull request Sep 19, 2026
…he mlx-metal promotion

#324 added the Apple Silicon deployment to the same block this branch was
updating with the ADR 0036 rebuild stamp. Both are true and neither replaces
the other: the CUDA host runs 0.34.1-dynres-16-g16649e8 with the drafting knob,
the Apple Silicon host runs 0.34.0-maxusai-8a7ba949 without it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
glennneuber added a commit that referenced this pull request Sep 19, 2026
Consolidates #323, #324 and #325 into one OCRBench test, renderer and format. #325 was closed unmerged, so this carries the only copy of the ROCm ladder and its summarizer.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
glennneuber added a commit that referenced this pull request Sep 19, 2026
Closes the ADR 0036 record: the deployed stamp on the CUDA host, the production verification, and the retirement-register row. Main merged in twice to keep both deployment lines after #324 and #328 edited the same block.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
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