Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions mlxrunner/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -581,11 +581,18 @@ func (c *Client) admit(systemInfo ml.SystemInfo, gpus []ml.DeviceInfo, requireFu
numCtx := int(c.softContextLength.Load())
need, kv, headroom, est := c.needFor(weights, numCtx)
if !est.Known {
// No cache rule for this architecture. Fall back to exactly what
// admission did before the estimator existed rather than guessing: a
// wrong estimate refuses loads that would have served.
slog.Warn("MLX admission is pricing weights only: no KV rule for this architecture",
"architecture", est.Arch, "model", c.modelName, "num_ctx", numCtx)
// No cache rule for this architecture, or a config.json the estimator
// cannot read. Fall back to exactly what admission did before the
// estimator existed rather than guessing: a wrong estimate refuses
// loads that would have served. Say which, since an unreadable config
// has no architecture to name.
if est.Err != nil {
slog.Warn("MLX admission is pricing weights only: config.json does not parse",
"model", c.modelName, "num_ctx", numCtx, "error", est.Err)
} else {
slog.Warn("MLX admission is pricing weights only: no KV rule for this architecture",
"architecture", est.Arch, "model", c.modelName, "num_ctx", numCtx)
}
}

// An automatic rung was never asked for: it is Ollama's VRAM-tier default
Expand Down
15 changes: 13 additions & 2 deletions mlxrunner/kvsize/arch.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ func gemma4Rule(cfg *config, numCtx int) (Estimate, bool) {
if layers <= 0 {
return Estimate{}, false
}
// gemma4.go reads sliding_window_pattern as an int32, so a list fails the
// model's own config parse; without layer_types there is nothing that says
// which layers slide, and the default period of 5 would be a guess.
if cfg.text.SlidingWindowPattern.IsList && len(cfg.text.LayerTypes) == 0 {
return Estimate{}, false
}

// Defaults from gemma4.go:420-441.
headDim := cfg.text.HeadDim
Expand Down Expand Up @@ -151,7 +157,7 @@ func gemma4IsSliding(cfg *config, i int) bool {
if len(cfg.text.LayerTypes) > 0 && i < len(cfg.text.LayerTypes) {
return cfg.text.LayerTypes[i] == "sliding_attention"
}
pattern := cfg.text.SlidingWindowPattern
pattern := cfg.text.SlidingWindowPattern.Period
if pattern <= 0 && len(cfg.text.LayerTypes) == 0 {
pattern = 5
}
Expand Down Expand Up @@ -356,7 +362,12 @@ func cohere2MoeRule(cfg *config, numCtx int) (Estimate, bool) {
if window <= 0 {
window = 4096
}
slidingPattern := cfg.text.SlidingWindowPattern
// cohere2_moe.go reads sliding_window_pattern as an int32 too: a list is a
// config the model cannot load, not one to price with the default of 4.
if cfg.text.SlidingWindowPattern.IsList {
return Estimate{}, false
}
slidingPattern := cfg.text.SlidingWindowPattern.Period
if slidingPattern <= 0 {
slidingPattern = 4
}
Expand Down
41 changes: 35 additions & 6 deletions mlxrunner/kvsize/kvsize.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ package kvsize

import (
"encoding/json"
"fmt"
"strings"
)

Expand Down Expand Up @@ -60,6 +61,9 @@ type Estimate struct {
// Known is false when no rule matched the target architecture. Every byte
// field is then zero and the caller must not refuse a load on it.
Known bool
// Err is why config.json could not be read, when that is why Known is
// false; Arch is then empty, since nothing was read to dispatch on.
Err error
// Arch is the target architecture the estimate dispatched on.
Arch string
// DraftArch is the draft architecture, empty when the model ships no
Expand Down Expand Up @@ -101,7 +105,7 @@ func Model(config, draft []byte, numCtx int) Estimate {

cfg, err := parse(config)
if err != nil {
return Estimate{NumCtx: numCtx}
return Estimate{NumCtx: numCtx, Err: err}
}

rule, arch := lookup(targetRules, cfg)
Expand Down Expand Up @@ -249,11 +253,11 @@ type textConfig struct {
SlidingWindow int `json:"sliding_window"`

// gemma4
GlobalHeadDim int `json:"global_head_dim"`
NumGlobalKeyValueHeads int `json:"num_global_key_value_heads"`
AttentionKEqV bool `json:"attention_k_eq_v"`
NumKVSharedLayers int `json:"num_kv_shared_layers"`
SlidingWindowPattern int `json:"sliding_window_pattern"`
GlobalHeadDim int `json:"global_head_dim"`
NumGlobalKeyValueHeads int `json:"num_global_key_value_heads"`
AttentionKEqV bool `json:"attention_k_eq_v"`
NumKVSharedLayers int `json:"num_kv_shared_layers"`
SlidingWindowPattern slidingWindowPattern `json:"sliding_window_pattern"`

// cohere2_moe
PrefixDenseSlidingWindowPattern int `json:"prefix_dense_sliding_window_pattern"`
Expand Down Expand Up @@ -287,6 +291,31 @@ type textConfig struct {
QKRopeHeadDim int `json:"qk_rope_head_dim"`
}

// slidingWindowPattern is gemma4's sliding_window_pattern, an integer period
// (every Nth layer is global). muse-glimmer writes a per-position list of
// windows instead ([2048,2048,2048,0]) and its model reads layer_types, never
// this field; the list form reads as Period 0 (unset) rather than failing the
// whole config, which priced glimmer as weights only. IsList records it, so a
// rule that would otherwise fall back to a default period can decline instead.
type slidingWindowPattern struct {
Period int
IsList bool
}

func (p *slidingWindowPattern) UnmarshalJSON(data []byte) error {
var n int
if err := json.Unmarshal(data, &n); err == nil {
*p = slidingWindowPattern{Period: n}
return nil
}
var list []json.RawMessage
if err := json.Unmarshal(data, &list); err != nil {
return fmt.Errorf("sliding_window_pattern: want an integer or a list, got %s", data)
}
*p = slidingWindowPattern{IsList: true}
return nil
}

func parse(data []byte) (*config, error) {
var envelope struct {
Architectures []string `json:"architectures"`
Expand Down
105 changes: 105 additions & 0 deletions mlxrunner/kvsize/kvsize_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kvsize

import (
"encoding/json"
"os"
"path/filepath"
"testing"
Expand Down Expand Up @@ -362,3 +363,107 @@ func TestSlotsRoundUpToTheCacheStep(t *testing.T) {
}
}
}

const (
glimmerConfig = "muse-glimmer-30b-config.json"
glimmerDraftConfig = "muse-glimmer-30b-dflash-draft-config.json"
)

// muse-glimmer's config.json gives sliding_window_pattern as a per-position
// list ([2048,2048,2048,0]), not gemma4's integer period. That one field
// failed the whole parse, so MLX admission priced every glimmer load as
// weights only and logged "no KV rule for this architecture" with an empty
// architecture (muse-glimmer:30b-nvfp4, 2026-10-11). The fixture is the
// published config.json without its quantization_config; the draft is the
// -dflash tags' draft/config.json.
func TestGlimmerConfigIsPriced(t *testing.T) {
got := Model(load(t, glimmerConfig), nil, 8192)
if got.Err != nil {
t.Fatalf("parse error: %v", got.Err)
}
if !got.Known || got.Arch != "MuseGlimmerForConditionalGeneration" {
t.Fatalf("known=%v arch=%q, want the glimmer rule", got.Known, got.Arch)
}
// layer_types holds 39 "sliding_attention" and 13 "full_attention" (every
// 4th layer), which is what glimmer.go reads; sliding_window_pattern is
// not consulted.
if want := (LayerCounts{Attention: 13, Sliding: 39}); got.Layers != want {
t.Errorf("layers = %+v, want %+v", got.Layers, want)
}
if got.Total() == 0 {
t.Error("a priced glimmer estimate has no bytes")
}

withDraft := Model(load(t, glimmerConfig), load(t, glimmerDraftConfig), 8192)
if !withDraft.DraftKnown || withDraft.DraftArch != "MuseGlimmerAssistantModel" {
t.Errorf("draft known=%v arch=%q, want the dflash rule", withDraft.DraftKnown, withDraft.DraftArch)
}
if withDraft.Total() <= got.Total() {
t.Errorf("total with the drafter %d, want more than without it %d", withDraft.Total(), got.Total())
}
}

// The list form must price exactly as if the field were absent: layer_types
// decides, as in the model.
func TestSlidingWindowPatternListIsNotAPeriod(t *testing.T) {
var cfg map[string]any
if err := json.Unmarshal(load(t, glimmerConfig), &cfg); err != nil {
t.Fatal(err)
}
delete(cfg, "sliding_window_pattern")
without, err := json.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
for _, n := range ladder {
a, b := Model(load(t, glimmerConfig), nil, n), Model(without, nil, n)
if a.Total() != b.Total() || a.Layers != b.Layers {
t.Errorf("num_ctx %d: list form %d bytes %+v, field absent %d bytes %+v", n, a.Total(), a.Layers, b.Total(), b.Layers)
}
}
}

// A config the parser cannot read says so: the caller's warning must not
// read as an unregistered architecture with an empty name.
func TestUnparsableConfigCarriesItsError(t *testing.T) {
got := Model([]byte(`{"architectures":["MuseGlimmerForConditionalGeneration"],"num_hidden_layers":"52"}`), nil, 8192)
if got.Known {
t.Fatal("an unparsable config must not be priced")
}
if got.Err == nil {
t.Fatal("Err is nil for a config that does not parse")
}
}

// A gemma4-family config whose sliding_window_pattern is a list and that has no
// layer_types cannot be priced: gemma4.go reads the field as an int32, so the
// model itself would fail to parse it, and the period the list implies is not
// the rule's to guess. It must come back unknown (the caller falls back), not
// priced with the default period of 5.
func TestGemma4ListPatternWithoutLayerTypesIsUnknown(t *testing.T) {
var cfg map[string]any
if err := json.Unmarshal(load(t, gemma4Config), &cfg); err != nil {
t.Fatal(err)
}
text := cfg
if tc, ok := cfg["text_config"].(map[string]any); ok {
text = tc
}
delete(text, "layer_types")
text["sliding_window_pattern"] = []int{1024, 1024, 1024, 1024, 1024, 0}
data, err := json.Marshal(cfg)
if err != nil {
t.Fatal(err)
}
if got := Model(data, nil, 8192); got.Known {
t.Errorf("known = true (%d bytes, %+v), want unknown", got.Total(), got.Layers)
}
// The integer form still prices as before.
text["sliding_window_pattern"] = 6
if data, err = json.Marshal(cfg); err != nil {
t.Fatal(err)
}
if got := Model(data, nil, 8192); !got.Known {
t.Error("an integer sliding_window_pattern without layer_types is no longer priced")
}
}
Loading