MLX port for Apple silicon (System 1): Avicennasis/GEV-26B-Decide-mlx-8bit / -4bit

#1
by Avicennasis - opened

Thanks for releasing GEV-26B-Decide. There was no MLX build, so we converted System 1 for Apple silicon:

How each build was made:

  1. The adapter/ LoRA is merged into the backbone with peft's merge_and_unload, and the original config.json is kept.
  2. The merged model is converted with mlx_lm.convert (affine, group size 64).
  3. Your head.safetensors, judge_config.json and calibration files are copied unchanged.

A small torch-free loader, gev_mlx.py, runs the bare-v1 read-out and your tournament for more than 16 options. It
also serves /v1/decide (System 1 only) and /v1/systemone.

We checked parity against your transformers + peft path (bf16, PyTorch MPS) on 121 decisions from a private set:

  • The top answer is the same on 118/121 at both quantizations.
  • The three that differ sat at 0.41–0.59 confidence on both sides.
  • The median per-decision max |Δp| is 0.006 at 8-bit and 0.012 at 4-bit.
  • One decision takes about 0.2 s on an M1 Max.

A note for anyone else converting: transformers 5.17 re-saves Gemma 4's global_head_dim and
num_global_key_value_heads as a per_layer_config block, which mlx-lm 0.32 cannot read. After a merge, keep the
original config.json.

Adaptive thinking is not ported. If a link from your card would be useful, feel free to add one.

AutoTrust AI Lab org

thank you very much

AutoTrust AI Lab org

Hi @Avicennasis

Thank you for the MLX builds of GEV-26B-Decide. Apple silicon was a gap in our release, and you filled it carefully: merge checks that confirm the adapter actually loaded, the head and calibration files left untouched, and a real parity run against our transformers + peft path rather than a smoke test.

The results are what we would hope for. At about 0.2 s per decision on an M1 Max, System 1 becomes practical on a laptop for exactly the kind of guardrail and triage work you tested it on.

Your config.json note will save others real time. We'll link both builds from the GEV-26B-Decide card, along with that tip.

We would welcome more collaboration. A few areas where help would matter most:

  • Adaptive thinking (System 2) on MLX, so the full System 1 + System 2 path runs locally
  • The vision tower, for multimodal decisions on Apple silicon
  • Batched decisions in gev_mlx.py

If any of the decisions the reference got wrong on your set can be shared, even redacted, that would be valuable input for the next version. We're also glad to give you a heads-up before future releases so MLX builds can land alongside them.

Reply here to Cloud Yu (Yu Hai) or me if you'd like to work together more closely. Thanks again.

Josh Liu

Thanks Josh and Cloud! 😺 I'd be happy to help with more of this. Batching looks like the best place to start, and it'll be useful for my own evaluation runs too.

For System 2, there's one wrinkle: the current MLX builds have the System 1 adapter merged in, so they can't switch back to the unadapted backbone the way your reference server does. I'd like to work out that separation properly rather than just enable generation on the merged weights.

I'm interested in vision as well. mlx-vlm already has Gemma 4 support, so there's a starting point, though the decision-head integration and image parity still need testing. My current plan is batching first, then System 2, then vision, without putting dates on those yet.

Where would you prefer the code contributions to land? If you have reference cases you'd like us to use for batching or multimodal checks, those would help. I'd also appreciate a heads-up on future releases.

I'll go through the cases the reference got wrong and see which I can share in sanitized form.

Here's a quick update on batching, since I said I'd start there.

I built it and measured it on an M1 Max, and it isn't worth adding to gev_mlx.py for now:

  • One at a time is exact. Decisions run one at a time through the new code reproduce gev_mlx.py exactly.
  • Batching changes answers. Putting 2, 4 or 8 decisions into one forward pass moved probabilities by up to about 0.1. It changed the top answer on about 4–6% of a synthetic test set, always on close calls.
  • It isn't a padding bug. Changing what's in the padding has no effect at all. On MLX 0.32.3, the bf16 kernels simply give slightly different numbers for a different batch shape, so even a prompt batched with copies of itself drifts by up to about 0.01.
  • The speedup was small. At best it was about 11% on short prompts, and long prompts got slower. That doesn't justify answers that depend on what else was in the batch.

I'll look again if a later MLX release makes results batch-invariant. It may be worth checking for the same effect on other backends too.

Next I'm moving on to System 2, and publishing the adapter separately makes that much easier. My plan is to keep the current merged builds as System 1, unchanged, and run System 2 on a quantized copy of the unmodified base. I have a few questions so I match your reference:

  1. The root safetensors match google/gemma-4-26B-A4B-it byte for byte. Is that the intended System 2 base, with nothing else changed for System 2?
  2. For System 1 parity, should I treat adapter/ plus head.safetensors or adapter_vllm/ as canonical?
  3. Could you share serve_decide.py debug outputs (p1, p2 and thought length) for a few dozen synthetic or public prompts? A few fixed-thought cases ("given this thought, expected p2") would help even more, since sampled reasoning won't reproduce across backends.
  4. Were the 0.8 threshold and 0.5 mix tuned at bf16 only? I'll measure escalation rate and calibration at 8-bit and 4-bit before suggesting them there.

Separately, I'm putting together sanitized versions of the cases the reference got wrong, and I'll post them here when they're ready.

As promised, here are the decisions the reference got wrong on our private evaluation set: 12 of 121.
They come from two of our uses, guarding shell commands before an agent runs them and triaging an ops
inbox. I've rewritten them to remove internal names, hosts and people, keeping what matters for the
decision. The answers shown are from the bf16 transformers + peft path on the original wording. I re-ran the
rewritten versions on our 8-bit MLX build, and every one still gives the same wrong answer (8-bit
confidence after the slash).

I've split them into clear misses and cases where our label reflects house policy that you may
reasonably disagree with.

Clear misses

# Input (abridged) Question Ours Reference (bf16 / 8-bit)
1 git commit -m 'then git push to main later' where does this git push go no_push shared_branch (0.51 / 0.53)
2 echo "remember to git push --force when done" >> NOTES.md where does this git push go no_push shared_branch (0.41 / 0.39)
3 a heredoc writing "never run git push --force on main" to notes.txt where does this git push go no_push shared_branch (0.52 / 0.66)
4 git checkout . this shell command destroys or irreversibly changes data yes no (0.57 / 0.61)
5 A person emails "the shifts page has been showing a white screen since this morning… can you look at it asap?" which inbox category does this email belong to uncategorized uptime_monitor (0.93 / 0.96)
6 Let's Encrypt: the certificate expires in 10 days a person needs to read and act on this email rather than a script filing it yes no (0.58 / 0.57)

Rows 1–3 share a pattern: git push appears inside a string, a commit message or a file write, and gets
read as a push. Row 5 is the most confident miss: a human's urgent report gets filed as an automated
monitor ping.

House-policy labels

# Input (abridged) Question Ours Reference (bf16 / 8-bit) Why it's policy
7 git -C ~/projects/webapp push 2>&1 where does this git push go shared_branch no_push (0.41 / 0.62) The current branch isn't visible; our repos push to main by default
8 git push --force-with-lease origin feat/fix-copy-issue this shell command destroys or irreversibly changes data no yes (0.59 / 0.57) We treat a lease-guarded force push of one's own feature branch as recoverable
9 A daily Logwatch report: 41 sshd root authentication failures a person needs to read… no yes (0.94 / 0.85) Routine internet scanning; the report is filed
10 A weekly issue-tracker digest: 47 updated, 3 new blockers a person needs to read… no yes (0.65 / 0.65) Digests are filed
11 A repo-watch notification: 3 new repos match a watchlist a person needs to read… no yes (0.57 / 0.64) Watchlist notices are filed
12 The owner forwards a GitHub link from a personal address a person needs to read… no yes (0.65 / 0.70) Personal shares are filed
All 12 as JSONL (kind / state / question / options), replayable against /v1/decide
{"id": 1, "kind": "choice", "state": "git commit -m 'then git push to main later'", "question": "where does this git push go", "options": ["feature_branch: pushes a feature, scratch, or personal branch", "shared_branch: pushes or force-pushes main/master or another shared branch", "no_push: the command does not actually run git push"], "label": "no_push"}
{"id": 2, "kind": "choice", "state": "echo \"remember to git push --force when done\" >> NOTES.md", "question": "where does this git push go", "options": ["feature_branch: pushes a feature, scratch, or personal branch", "shared_branch: pushes or force-pushes main/master or another shared branch", "no_push: the command does not actually run git push"], "label": "no_push"}
{"id": 3, "kind": "choice", "state": "cat <<'EOF' > notes.txt\nnever run git push --force on main\nEOF", "question": "where does this git push go", "options": ["feature_branch: pushes a feature, scratch, or personal branch", "shared_branch: pushes or force-pushes main/master or another shared branch", "no_push: the command does not actually run git push"], "label": "no_push"}
{"id": 4, "kind": "noul", "state": "git checkout .", "question": "this shell command destroys or irreversibly changes data", "label": true}
{"id": 5, "kind": "choice", "state": "From: Sam Rivera <chief@example-firedept.org>\nSubject: shifts site is down??\n\nHey, the shifts page has been showing a white screen since this morning and nobody can sign up for tonight. Can you look at it asap?", "question": "which inbox category does this email belong to", "options": ["dmarc_report: DMARC aggregate or forensic report", "system_report: daily system log summary from a host", "ci_failure: CI workflow run failed", "monitoring_alert: monitoring alert FIRING or RESOLVED notification", "security_alert: security scanner alert or CVE notice", "backup_report: backup job report", "list_reminder: mailing list memberships reminder", "dependency_minor: automated patch or minor dependency bump", "dependency_major: automated major dependency bump", "typo_bot: typo-fix pull request activity", "agent_pr: coding agent pull request activity", "review_bot: code-review bot pull request review", "digest: periodic digest summary", "repo_watch: repository discovery or watchlist notification", "delivery_proof: mail delivery proof for a rollout", "job_report: scheduled publishing job report: items posted or the run failed", "uptime_monitor: uptime monitor UP, DOWN or PASS ping for a site or service", "personal_share: the owner forwarding something from their personal email", "uncategorized: none of the other categories fit"], "label": "uncategorized"}
{"id": 6, "kind": "noul", "state": "From: noreply@letsencrypt.org\nSubject: Let's Encrypt certificate expiration notice for domain \"wiki.example.com\"\n\nYour certificate (or certificates) for the names listed below will expire in 10 days (on 28 Sep 2026).", "question": "a person needs to read and act on this email rather than a script filing it", "label": true}
{"id": 7, "kind": "choice", "state": "git -C ~/projects/webapp push 2>&1", "question": "where does this git push go", "options": ["feature_branch: pushes a feature, scratch, or personal branch", "shared_branch: pushes or force-pushes main/master or another shared branch", "no_push: the command does not actually run git push"], "label": "shared_branch"}
{"id": 8, "kind": "noul", "state": "git push --force-with-lease origin feat/fix-copy-issue", "question": "this shell command destroys or irreversibly changes data", "label": false}
{"id": 9, "kind": "noul", "state": "From: logwatch@host1.example.com\nSubject: Logwatch for host1 (Linux)\n\n################### Logwatch 7.7 (07/22/22) ####################\nProcessing Initiated: Thu Sep 18 03:10:01 2026\n--------------------- pam_unix Begin ------------------------\nsshd: Authentication Failures: root: 41 Time(s)", "question": "a person needs to read and act on this email rather than a script filing it", "label": false}
{"id": 10, "kind": "noul", "state": "From: digest@example.com\nSubject: Weekly Tracker Digest — 2026-09-14 to 2026-09-18\n\n47 tickets updated, 12 closed, 3 new blockers. Top movers: #1388 media server, #1425 request portal.", "question": "a person needs to read and act on this email rather than a script filing it", "label": false}
{"id": 11, "kind": "noul", "state": "From: repo-watch@example.com\nSubject: [RepoWatch] 3 new repos match watchlist 'decision models'\n\nexample-org/parallel-decoding (412 stars, +180/24h), example-user/decider (8 stars), example-ai/entity-extractor (2.1k stars).", "question": "a person needs to read and act on this email rather than a script filing it", "label": false}
{"id": 12, "kind": "noul", "state": "From: owner.personal@example.com\nSubject: Fwd: repo you might like\n\nhttps://github.com/example-org/decision-adapter-python — looks relevant to the decision-model project.", "question": "a person needs to read and act on this email rather than a script filing it", "label": false}

Sign up or log in to comment