Skip to content

Commit a906438

Browse files
localai-botmudler
andauthored
fix(config): backend-gate the top_k=40 sampler default (#6632) (#10285)
fix(config): gate top_k=40 default on backend family (#6632) SetDefaults injected top_k=40 (llama.cpp's sampling default) for every model config regardless of backend. That value is wrong for backends whose native default differs: mlx_lm's intended default is top_k=0 (disabled) and mlx does not remap 0->40, so a client that omits top_k silently got 40 shipped to mlx, changing sampling. The mlx backend's own getattr(request,'TopK',0) fallback is dead because proto3 int32 is always present. Gate the injection on backend family via UsesLlamaSamplerDefaults: keep top_k=40 for the llama.cpp family and for the empty/auto backend (the GGUF auto-detect path resolves to llama.cpp, so existing behavior is preserved), but leave TopK nil for the known non-llama backends (mlx, mlx-vlm, mlx-distributed). gRPCPredictOpts now sends 0 when TopK is nil, which is the value mlx actually wants. Only TopK is gated - the confirmed bug. The sibling sampler defaults (top_p, temperature, min_p) are left global to avoid widening scope and introducing nil-deref risk; revisit per-backend if needed. Assisted-by: claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent d28a5b6 commit a906438

4 files changed

Lines changed: 110 additions & 2 deletions

File tree

core/backend/options.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,11 +307,19 @@ func gRPCPredictOpts(c config.ModelConfig, modelPath string) *pb.PredictOptions
307307
}
308308
}
309309

310+
// TopK may be nil after SetDefaults for backends that don't use llama.cpp's
311+
// top_k=40 default (issue #6632, e.g. mlx). proto3 int32 can't be unset, so
312+
// send 0 — the value mlx actually wants (top-k disabled).
313+
var topK int32
314+
if c.TopK != nil {
315+
topK = int32(*c.TopK)
316+
}
317+
310318
pbOpts := &pb.PredictOptions{
311319
Temperature: float32(*c.Temperature),
312320
TopP: float32(*c.TopP),
313321
NDraft: c.NDraft,
314-
TopK: int32(*c.TopK),
322+
TopK: topK,
315323
MinP: float32(*c.MinP),
316324
Tokens: int32(*c.Maxtokens),
317325
Threads: int32(*c.Threads),

core/config/backend_capabilities.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -517,6 +517,33 @@ func NormalizeBackendName(backend string) string {
517517
return strings.ReplaceAll(backend, ".", "-")
518518
}
519519

520+
// nonLlamaSamplerBackends lists backends whose native sampler defaults differ
521+
// from llama.cpp's, so LocalAI must NOT inject llama.cpp's top_k=40 default for
522+
// them (issue #6632). mlx_lm's intended default is top_k=0 (disabled) and mlx
523+
// does not remap 0->40, so shipping 40 silently changes sampling for clients
524+
// that omit top_k. Leaving TopK nil lets the wire value default to 0.
525+
//
526+
// This is intentionally a small allow-list of KNOWN non-llama backends: empty
527+
// and unknown backends fall through to the llama.cpp default to preserve the
528+
// GGUF auto-detect path's behavior.
529+
var nonLlamaSamplerBackends = map[string]struct{}{
530+
"mlx": {},
531+
"mlx-vlm": {},
532+
"mlx-distributed": {},
533+
}
534+
535+
// UsesLlamaSamplerDefaults reports whether a backend should receive llama.cpp's
536+
// sampler defaults (e.g. top_k=40). Empty/unknown backends return true so the
537+
// GGUF auto-detect path (which resolves to llama.cpp) keeps today's behavior;
538+
// only the known non-llama backends in nonLlamaSamplerBackends return false.
539+
func UsesLlamaSamplerDefaults(backend string) bool {
540+
if backend == "" {
541+
return true
542+
}
543+
_, isNonLlama := nonLlamaSamplerBackends[NormalizeBackendName(backend)]
544+
return !isNonLlama
545+
}
546+
520547
// GetBackendCapability returns the capability info for a backend, or nil if unknown.
521548
// Handles backend name normalization.
522549
func GetBackendCapability(backend string) *BackendCapability {

core/config/model_config.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -867,7 +867,12 @@ func (cfg *ModelConfig) SetDefaults(opts ...ConfigLoaderOption) {
867867
cfg.Seed = &defaultSeed
868868
}
869869

870-
if cfg.TopK == nil {
870+
// top_k=40 is llama.cpp's sampling default and is wrong for backends whose
871+
// native default differs (issue #6632). Only inject it for the llama.cpp
872+
// family and the empty/auto backend; leave TopK nil for known non-llama
873+
// backends (e.g. mlx, whose intended default is top_k=0) so the wire value
874+
// is 0 rather than a silently-changed 40.
875+
if cfg.TopK == nil && UsesLlamaSamplerDefaults(cfg.Backend) {
871876
cfg.TopK = &defaultTopK
872877
}
873878

core/config/model_config_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,4 +529,72 @@ concurrency_groups:
529529
"models that template in Go still rely on the Go-generated grammar")
530530
})
531531
})
532+
533+
// The default top_k=40 is llama.cpp's sampling default and is WRONG for
534+
// backends whose native default differs. mlx_lm's intended default is
535+
// top_k=0 (disabled) and mlx does not remap 0->40, so injecting 40 silently
536+
// changes sampling for mlx clients that omit top_k (issue #6632). Gate the
537+
// injection on backend family: keep 40 for the llama.cpp family and for the
538+
// empty/auto backend (the GGUF auto-detect path resolves to llama.cpp), but
539+
// leave TopK nil for the mlx family so the wire value is 0.
540+
Context("TopK default is backend-gated (issue #6632)", func() {
541+
It("injects top_k=40 for the llama.cpp backend", func() {
542+
cfg := &ModelConfig{}
543+
cfg.Backend = "llama-cpp"
544+
545+
cfg.SetDefaults()
546+
547+
Expect(cfg.TopK).NotTo(BeNil(), "llama.cpp must keep its top_k=40 default")
548+
Expect(*cfg.TopK).To(Equal(40))
549+
})
550+
551+
It("injects top_k=40 for the empty/auto backend (GGUF auto-detect)", func() {
552+
cfg := &ModelConfig{}
553+
554+
cfg.SetDefaults()
555+
556+
Expect(cfg.TopK).NotTo(BeNil(), "empty backend resolves to llama.cpp; default unchanged")
557+
Expect(*cfg.TopK).To(Equal(40))
558+
})
559+
560+
It("leaves TopK nil for the mlx backend", func() {
561+
cfg := &ModelConfig{}
562+
cfg.Backend = "mlx"
563+
564+
cfg.SetDefaults()
565+
566+
Expect(cfg.TopK).To(BeNil(),
567+
"mlx_lm's intended default is top_k=0 (disabled); LocalAI must not inject 40")
568+
})
569+
570+
It("leaves TopK nil for the mlx-vlm backend", func() {
571+
cfg := &ModelConfig{}
572+
cfg.Backend = "mlx-vlm"
573+
574+
cfg.SetDefaults()
575+
576+
Expect(cfg.TopK).To(BeNil())
577+
})
578+
579+
It("leaves TopK nil for the mlx-distributed backend", func() {
580+
cfg := &ModelConfig{}
581+
cfg.Backend = "mlx-distributed"
582+
583+
cfg.SetDefaults()
584+
585+
Expect(cfg.TopK).To(BeNil())
586+
})
587+
588+
It("respects an explicit top_k even for the mlx backend", func() {
589+
explicit := 7
590+
cfg := &ModelConfig{}
591+
cfg.Backend = "mlx"
592+
cfg.TopK = &explicit
593+
594+
cfg.SetDefaults()
595+
596+
Expect(cfg.TopK).NotTo(BeNil())
597+
Expect(*cfg.TopK).To(Equal(7))
598+
})
599+
})
532600
})

0 commit comments

Comments
 (0)