fix(config): only inject llama.cpp serving options on the llama.cpp path (#10822)

SetDefaults injected the llama.cpp server options cache_reuse
(ApplyServingDefaults) and parallel (ApplyHardwareDefaults, re-applied
per selected node by the distributed router) onto every model config
regardless of backend. Every other backend ignores options it does not
understand, so this was harmless until longcat-video, which strictly
validates its options and fails LoadModel with
"unknown model option(s): cache_reuse, parallel".

Gate both injections behind a new UsesLlamaCppServingOptions allow-list
(llama-cpp plus the empty/auto-detect case that resolves to llama.cpp
from a GGUF file, mirroring how llamaCppDefaults is registered). This
follows the existing UsesLlamaSamplerDefaults precedent for llama-only
defaults. The typed NBatch field is deliberately left alone: it is a
proto field every backend simply ignores, which is why batch never
triggered the error.

Also harden the longcat-video backend to warn-and-ignore unknown model
options and request params through a testable select_known_options
helper, matching the other LocalAI Python backends, so a future
server-injected option cannot break loading again.


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>
This commit is contained in:
LocalAI [bot]
2026-07-14 17:46:15 +02:00
committed by GitHub
parent 9f14571397
commit b224c96db6
10 changed files with 139 additions and 21 deletions

View File

@@ -32,6 +32,7 @@ from longcat_utils import (
require_bool,
require_float,
require_int,
select_known_options,
validate_dimensions,
)
@@ -126,10 +127,17 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
)
try:
options = parse_options(request.Options)
unknown = sorted(set(options) - LOAD_OPTIONS)
if unknown:
raise ValueError(f"unknown model option(s): {', '.join(unknown)}")
options, ignored = select_known_options(
parse_options(request.Options), LOAD_OPTIONS
)
if ignored:
# The server injects llama.cpp serving defaults (cache_reuse,
# parallel) onto every config; ignore what we don't understand
# rather than fail to load.
print(
f"longcat-video ignoring unknown model option(s): {', '.join(ignored)}",
file=sys.stderr,
)
self._import_torch()
if not self.torch.cuda.is_available():
@@ -240,10 +248,14 @@ class BackendServicer(backend_pb2_grpc.BackendServicer):
self.pipeline._interrupt = True
try:
params = dict(request.params)
unknown = sorted(set(params) - REQUEST_PARAMS)
if unknown:
raise ValueError(f"unknown request param(s): {', '.join(unknown)}")
params, ignored_params = select_known_options(
dict(request.params), REQUEST_PARAMS
)
if ignored_params:
print(
f"longcat-video ignoring unknown request param(s): {', '.join(ignored_params)}",
file=sys.stderr,
)
os.makedirs(os.path.dirname(request.dst) or ".", mode=0o750, exist_ok=True)
if hasattr(context, "add_callback"):

View File

@@ -65,6 +65,23 @@ def parse_options(values):
return options
def select_known_options(options, known):
"""Split parsed options into the subset this backend understands and the
unknown keys to ignore.
LocalAI injects serving defaults (e.g. the llama.cpp cache_reuse / parallel
options) onto every model config regardless of backend. A backend should
tolerate options it does not understand rather than refuse to load, matching
the other LocalAI Python backends; the caller logs the ignored keys.
Returns (kept, ignored) where kept preserves the known entries and ignored is
the sorted list of dropped keys.
"""
ignored = sorted(key for key in options if key not in known)
kept = {key: value for key, value in options.items() if key in known}
return kept, ignored
def require_bool(value, name):
if isinstance(value, bool):
return value

View File

@@ -21,6 +21,7 @@ from longcat_utils import ( # noqa: E402
normalize_model_source,
normalize_num_frames,
parse_options,
select_known_options,
validate_dimensions,
)
@@ -61,6 +62,26 @@ class LongCatUtilsTest(unittest.TestCase):
self.assertEqual(options["source"], "https://example.com/model")
self.assertEqual(options["flag"], True)
def test_select_known_options_keeps_known_and_reports_unknown(self):
# The server injects llama.cpp serving defaults (cache_reuse, parallel)
# onto every model config. longcat-video must ignore what it does not
# understand rather than refuse to load - matching every other backend.
options = {
"use_distill": True,
"max_segments": 4,
"cache_reuse": 256,
"parallel": 1,
}
kept, ignored = select_known_options(options, {"use_distill", "max_segments"})
self.assertEqual(kept, {"use_distill": True, "max_segments": 4})
self.assertEqual(ignored, ["cache_reuse", "parallel"])
def test_select_known_options_reports_nothing_when_all_known(self):
kept, ignored = select_known_options({"use_distill": True}, {"use_distill"})
self.assertEqual(kept, {"use_distill": True})
self.assertEqual(ignored, [])
def test_classify_model_accepts_only_supported_longcat_models(self):
cases = {
"meituan-longcat/LongCat-Video": MODEL_KIND_BASE,

View File

@@ -648,6 +648,27 @@ func UsesLlamaSamplerDefaults(backend string) bool {
return !isNonLlama
}
// UsesLlamaCppServingOptions reports whether a backend understands llama.cpp's
// serving-tuning model options - the free-form option strings cache_reuse /
// n_cache_reuse (cross-request KV-prefix reuse) and parallel / n_parallel
// (concurrent slots). These are llama.cpp server flags; LocalAI injects them as
// defaults, but a backend that strictly validates its options (e.g.
// longcat-video) rejects an unknown one with "unknown model option(s)" at
// LoadModel. Only the llama.cpp backend - and the empty/auto-detect case, which
// resolves to llama.cpp from a GGUF file, mirroring how llamaCppDefaults is
// registered - should receive them.
//
// This is an allow-list on purpose (unlike UsesLlamaSamplerDefaults's
// deny-list): these options are meaningful to no other backend, so a new
// backend defaults to NOT getting them rather than breaking the same way.
func UsesLlamaCppServingOptions(backend string) bool {
switch NormalizeBackendName(backend) {
case "", "llama-cpp":
return true
}
return false
}
// GetBackendCapability returns the capability info for a backend, or nil if unknown.
// Handles backend name normalization.
func GetBackendCapability(backend string) *BackendCapability {

View File

@@ -336,7 +336,12 @@ func ApplyHardwareDefaults(cfg *ModelConfig, gpu GPU) {
// context budget, but a context large enough to fill a single device leaves
// no room for the per-slot scratch, so the slot count is gated on per-device
// headroom too (issue #10485). Explicit parallel/n_parallel always wins.
if before := len(cfg.Options); true {
//
// parallel is a llama.cpp option string; skip it for backends that would
// reject an unknown option (e.g. longcat-video). The typed batch above is a
// proto field every backend simply ignores, so it needs no such gate.
if UsesLlamaCppServingOptions(cfg.Backend) {
before := len(cfg.Options)
cfg.Options = EnsureParallelOptionForContext(cfg.Options, gpu, ctx)
if len(cfg.Options) > before {
xlog.Debug("[hardware_defaults] defaulting parallel slots for concurrent serving",

View File

@@ -169,5 +169,14 @@ var _ = Describe("Hardware-driven config defaults", func() {
ApplyHardwareDefaults(cfg, GPU{VRAM: 119 * gib})
Expect(cfg.Options).To(Equal([]string{"parallel:2"}))
})
It("adds no parallel option for a non-llama backend", func() {
// parallel is a llama.cpp server option (n_parallel); a backend that
// strictly validates options (e.g. longcat-video) rejects it. Only the
// llama.cpp path should receive it, even on a capable GPU.
cfg := &ModelConfig{}
cfg.Backend = "longcat-video"
ApplyHardwareDefaults(cfg, GPU{ComputeCapability: "12.1", VRAM: 119 * gib})
Expect(cfg.Options).ToNot(ContainElement(ContainSubstring("parallel")))
})
})
})

View File

@@ -29,6 +29,11 @@ func ApplyServingDefaults(cfg *ModelConfig) {
if cfg == nil {
return
}
// cache_reuse is a llama.cpp server option; a backend that strictly
// validates its options rejects it. Only inject it on the llama.cpp path.
if !UsesLlamaCppServingOptions(cfg.Backend) {
return
}
if !backendOptionSet(cfg.Options, "cache_reuse", "n_cache_reuse") {
cfg.Options = append(cfg.Options, fmt.Sprintf("cache_reuse:%d", DefaultCacheReuse))
xlog.Debug("[serving_defaults] enabling cross-request prefix cache",

View File

@@ -26,5 +26,20 @@ var _ = Describe("Serving-policy config defaults", func() {
It("no-ops on nil", func() {
Expect(func() { ApplyServingDefaults(nil) }).ToNot(Panic())
})
It("does not enable cache_reuse for a non-llama backend", func() {
// cache_reuse is a llama.cpp server option (n_cache_reuse). Backends
// that strictly validate their options (e.g. longcat-video) reject it
// with "unknown model option(s)". Only the llama.cpp path gets it.
cfg := &ModelConfig{}
cfg.Backend = "longcat-video"
ApplyServingDefaults(cfg)
Expect(cfg.Options).ToNot(ContainElement(ContainSubstring("cache_reuse")))
})
It("still enables cache_reuse for an explicit llama-cpp backend", func() {
cfg := &ModelConfig{}
cfg.Backend = "llama-cpp"
ApplyServingDefaults(cfg)
Expect(cfg.Options).To(ContainElement("cache_reuse:256"))
})
})
})

View File

@@ -177,7 +177,7 @@ type scheduleLoadResult struct {
// an upstream host (the GPU-less frontend in distributed mode) guessed higher.
// Only values the heuristics themselves manage are touched, so an explicit user
// batch (e.g. 1024) is never overridden.
func applyNodeHardwareDefaults(opts *pb.ModelOptions, node *BackendNode) {
func applyNodeHardwareDefaults(opts *pb.ModelOptions, node *BackendNode, backend string) {
if opts == nil || node == nil || config.HardwareDefaultsDisabled() {
return
}
@@ -196,8 +196,12 @@ func applyNodeHardwareDefaults(opts *pb.ModelOptions, node *BackendNode) {
// the options may have no GPU). Gated on the node's per-device VRAM at this
// model's context, so a large context that already fills the device can't
// tip it into OOM by adding slot scratch (issue #10485). Only adds when no
// parallel option is set.
opts.Options = config.EnsureParallelOptionForContext(opts.Options, gpu, int(opts.ContextSize))
// parallel option is set. parallel is a llama.cpp option string, so it is
// also gated by backend: a strict backend (e.g. longcat-video) rejects an
// unknown option at LoadModel. The typed NBatch above needs no such gate.
if config.UsesLlamaCppServingOptions(backend) {
opts.Options = config.EnsureParallelOptionForContext(opts.Options, gpu, int(opts.ContextSize))
}
}
// scheduleAndLoad is the shared core for loading a model on a new node.
@@ -218,7 +222,7 @@ func (r *SmartRouter) scheduleAndLoad(ctx context.Context, backendType, tracking
// Tune node-agnostic options to the SELECTED node's GPU. Only now do we know
// which node (and its compute capability) will run the model — the frontend
// that built modelOpts may have no GPU at all in distributed mode.
applyNodeHardwareDefaults(modelOpts, node)
applyNodeHardwareDefaults(modelOpts, node, backendType)
// Pre-stage model files via FileStager before loading
loadOpts := modelOpts

View File

@@ -10,34 +10,34 @@ import (
var _ = Describe("applyNodeHardwareDefaults", func() {
It("raises a managed default batch on a Blackwell node with headroom", func() {
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch, ContextSize: 8192}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30}, "llama-cpp")
Expect(opts.NBatch).To(BeEquivalentTo(config.BlackwellPhysicalBatch))
})
It("keeps the default batch when a large context would overflow the node", func() {
// Regression guard for issue #10485 on the distributed path.
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch, ContextSize: 204800}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.0", TotalVRAM: 16 << 30})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.0", TotalVRAM: 16 << 30}, "llama-cpp")
Expect(opts.NBatch).To(BeEquivalentTo(config.DefaultPhysicalBatch))
})
It("resets a Blackwell guess on a non-Blackwell node", func() {
// frontend (Blackwell) guessed high, but the selected node is not Blackwell
opts := &pb.ModelOptions{NBatch: config.BlackwellPhysicalBatch}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "9.0"})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "9.0"}, "llama-cpp")
Expect(opts.NBatch).To(BeEquivalentTo(config.DefaultPhysicalBatch))
})
It("never overrides an explicit (non-managed) batch", func() {
opts := &pb.ModelOptions{NBatch: 1024}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1"})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1"}, "llama-cpp")
Expect(opts.NBatch).To(BeEquivalentTo(int32(1024)))
})
It("adds a VRAM-scaled parallel option for the selected node", func() {
// frontend may have had no GPU (no parallel option); the node has a big GPU
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30}, "llama-cpp")
Expect(opts.Options).To(ContainElement("parallel:8"))
})
@@ -45,17 +45,26 @@ var _ = Describe("applyNodeHardwareDefaults", func() {
// Regression guard for issue #10485: a 16 GiB node with a ~200k context
// is a tight single-model fit — the slot scratch would tip it into OOM.
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch, ContextSize: 204800}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.0", TotalVRAM: 16 << 30})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.0", TotalVRAM: 16 << 30}, "llama-cpp")
Expect(opts.Options).ToNot(ContainElement(ContainSubstring("parallel")))
})
It("never overrides an explicit parallel option on the node path", func() {
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch, Options: []string{"parallel:2"}}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30})
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30}, "llama-cpp")
Expect(opts.Options).To(Equal([]string{"parallel:2"}))
})
It("adds no parallel option for a non-llama backend on the node path", func() {
// parallel is a llama.cpp option string; a backend that strictly validates
// options (e.g. longcat-video) rejects it. The node-tuning path must gate
// it by backend just like the single-host config path does.
opts := &pb.ModelOptions{NBatch: config.DefaultPhysicalBatch}
applyNodeHardwareDefaults(opts, &BackendNode{GPUComputeCapability: "12.1", TotalVRAM: 119 << 30}, "longcat-video")
Expect(opts.Options).ToNot(ContainElement(ContainSubstring("parallel")))
})
It("no-ops on nil inputs", func() {
Expect(func() { applyNodeHardwareDefaults(nil, nil) }).ToNot(Panic())
Expect(func() { applyNodeHardwareDefaults(nil, nil, "llama-cpp") }).ToNot(Panic())
})
})