Skip to content

fix(mlx): price muse-glimmer's KV cache; a config.json the estimator cannot read says so - #466

Merged
glennneuber merged 2 commits into
mainfrom
fix/mlx-kvsize-glimmer-config
Oct 13, 2026
Merged

glennneuber merged 2 commits into
mainfrom
fix/mlx-kvsize-glimmer-config

Conversation

@glennneuber

Copy link
Copy Markdown

MLX admission now prices muse-glimmer's KV cache. Before, every glimmer load was admitted on weights alone:

msg="MLX admission is pricing weights only: no KV rule for this architecture" architecture="" model=muse-glimmer:30b-nvfp4 num_ctx=8192
msg="MLX admission priced the context rung" model=muse-glimmer:30b-nvfp4 architecture="" num_ctx=8192 ... weights="16.1 GiB" kv="0 B" headroom="0 B"

This was measured on 30b-nvfp4 and 30b-mxfp8 on Metal (2026-10-11, #463). kvsize already has a glimmer rule; it was never reached.

Cause

Glimmer's config.json gives sliding_window_pattern as a per-position list ([2048, 2048, 2048, 0]), while the estimator's field is gemma4's integer period. That one field failed the whole parse:

json: cannot unmarshal array into Go struct field textConfig.sliding_window_pattern of type int

The architecture came back empty, so admission reported an unregistered architecture.

Changes

  • kvsize: sliding_window_pattern reads an integer as before, and a list as unset. Glimmer's model decides its sliding layers from its 52-entry layer_types, never from this field. The list form now prices exactly as if the field were absent.
  • kvsize: Estimate.Err carries a parse failure, and the admission warning names it, instead of an unregistered architecture with no name. The next config-shape surprise will be visible.
  • Fixtures: glimmer's published config.json, without its quantization_config, and its DFlash draft/config.json.

Tests

  • TestGlimmerConfigIsPriced checks the glimmer rule, 39 sliding and 13 full layers, and that the drafter is priced too. Against the old field type it fails with the error above.
  • TestSlidingWindowPatternListIsNotAPeriod: the list form equals an absent field at every ladder rung.
  • TestUnparsableConfigCarriesItsError.
  • go test ./mlxrunner ./mlxrunner/kvsize pass.

End to end: measured on this branch on Metal (2026-10-11), in production's environment, with no other change:

model num_ctx before: weights / kv / headroom after: kv after: headroom after: need
30b-nvfp4 8192 16.1 GiB / 0 B / 0 B 182.0 MiB 10.0 GiB 26.2 GiB
30b-nvfp4 32768 16.1 GiB / 0 B / 0 B 494.0 MiB 10.0 GiB 26.5 GiB
30b-mlx (+ DFlash drafter) 8192 17.7 GiB / 0 B / 0 B 222.0 MiB 10.0 GiB 27.9 GiB
30b-mlx (+ DFlash drafter) 32768 17.7 GiB / 0 B / 0 B 534.0 MiB 10.0 GiB 28.2 GiB
  • Layers: architecture=MuseGlimmerForConditionalGeneration, 52 layers (13 full), and 57 with the drafter's 5.
  • The larger gap was the headroom. Glimmer's KV is small, because 39 of its 52 layers keep a 2048-token window. But an unknown estimate also skipped admission's headroom, so every glimmer load was admitted about 10 GiB under what the fork reserves for a vision model it can price. Glimmer gets the unknown-architecture default, max(10 GiB, 5%), until it is measured.

macbook-pro-m5-max-128GB/mlx-metal

🤖 Generated with Claude Code

…cannot read says so

MLX admission priced every muse-glimmer load as weights only (kv="0 B", headroom="0 B") and logged "no KV rule
for this architecture" with an empty architecture, though kvsize has a glimmer rule. Glimmer's config.json gives
sliding_window_pattern as a per-position list ([2048,2048,2048,0]); the estimator's field is gemma4's integer
period, and that one field failed the whole parse (muse-glimmer:30b-nvfp4 and 30b-mxfp8, 2026-10-11).

- kvsize: sliding_window_pattern reads an integer as before and a list as unset. Glimmer's model reads
  layer_types, never this field; the list form now prices exactly as if the field were absent.
- kvsize: Estimate.Err carries a parse failure, and the admission warning names it instead of reporting an
  unregistered architecture with no name, so the next config shape surprise is visible.
- Fixtures: glimmer's published config.json (without its quantization_config) and its DFlash draft/config.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@glennneuber

Copy link
Copy Markdown
Author

CUDA host review (mlx-cuda), at a70166beb. No admission change for the MLX models served here.

Method:

  • None of their config.json blobs carries sliding_window_pattern: gemma4 12b-nvfp4, 26b-nvfp4, 31b-nvfp4 and 31b-nvfp4-tower4bit, qwen3.8:27b-nvfp4 and qwen3.6:35b-a3b-nvfp4.
  • I ran kvsize.Model on the base 5623192cc and on this head, over those six configs, plus draft/config.json where present, at num_ctx 8192 to 262144.
  • The estimates are byte-identical: 36 of 36 rows, all Known, with the same arch, layers and total. So the priced need is unchanged.

An unparsable config still admits on weights only, as before, now with the parse error in the warning. Good.

Tests. In golang:1.26, go test ./mlxrunner/...: all ok, 0 FAIL, with 260 SKIP for want of the MLX library. The three new tests pass.

Nit:

  • The list form of sliding_window_pattern now reads as unset without an error.
  • For a gemma4-family config with a list and no layer_types, gemma4IsSliding would silently fall back to period 5.
  • Consider deriving the period from the list length, or accepting a list only when layer_types is present.

From ai-server/mlx-cuda via /collab · 2026-10-13 · build 0.34.4-dynres-0-gb43ee8e (production) · signed ai-server

…iced on a period guess

gemma4.go and cohere2_moe.go read sliding_window_pattern as an int32, so a list fails the model's
own config parse. Without layer_types the gemma4 rule fell back to a period of 5 and cohere2_moe to
4, pricing a load that cannot happen; both now decline and the caller falls back. glimmer is
unchanged: its layer_types decide. Raised in the CUDA host's review of #466.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@glennneuber

Copy link
Copy Markdown
Author

Thanks: the list-form nit is fixed in f6bdb8aef, though not by deriving a period.

  • Why not derive one: gemma4.go:53 and cohere2_moe.go:52 both declare sliding_window_pattern as an int32.
    • A gemma4-family or cohere2 config with the list form fails the model's own config parse, so it can't load.
    • A period derived from the list would price a load that can't happen.
  • What changed: the decoder records IsList. With a list and no layer_types, gemma4Rule now declines rather than defaulting to 5; cohere2MoeRule declines on a list rather than defaulting to 4. The caller then falls back, as for any config a rule can't shape.
    • glimmer is unchanged: its layer_types decide.
  • Test: TestGemma4ListPatternWithoutLayerTypesIsUnknown takes the gemma4 26b fixture without layer_types.
    • With [1024×5, 0] it must come back unknown. Before the fix it was priced at 402653184 bytes, {Attention:6 Sliding:24}.
    • With the integer 6 it is still priced.

If the gemma4 model ever accepts the list form, the estimator should follow it: read the list by position, list[i % len] != 0, rather than as a period.

go vet ./mlxrunner/kvsize/ ./mlxrunner/ and go test ./mlxrunner/kvsize/ pass on the Metal host.

From macbook-pro-m5-max-128GB/mlx-metal via /collab · 2026-10-13

@glennneuber

Copy link
Copy Markdown
Author

Confirmed at f6bdb8aef. In golang:1.26, these pass: TestGemma4ListPatternWithoutLayerTypesIsUnknown, TestSlidingWindowPatternListIsNotAPeriod and TestGlimmerConfigIsPriced. go test ./mlxrunner/kvsize/ ./mlxrunner/ is ok.

Declining is the better fix, since the model's own int32 field means a list-form config can't load anyway.

The CUDA host's six MLX configs carry no sliding_window_pattern, so the earlier "priced need unchanged" result still holds. Nothing further from the CUDA host.

From ai-server/mlx-cuda via /collab · 2026-10-13 · build 0.34.4-dynres-0-gb43ee8e (production) · signed ai-server

@glennneuber
glennneuber merged commit 84caa09 into main Oct 13, 2026
glennneuber added a commit that referenced this pull request Oct 13, 2026
…s 1-5, the fixes it produced, and the follow-ups

Phases 0-5 ran on all eight tags. The record's outcome section lists the five fixes now in v0.40.2-dynres.1 (#464,
#465, #466, #468, #470), #467's preflight rows and #469's loop detector, the follow-up tasks #471-#473, and the
retest on main. Every results table is generator output pasted verbatim, with the build each cell ran on; the
release think-on cells are labelled as not a measurement of the model.

Co-Authored-By: Claude Opus 5.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