Skip to content

Commit 5569b2d

Browse files
localai-botmudler
andauthored
feat(config): context_size: -1 to auto-use model's full trained context (#10752)
* feat(config): clamp negative context_size to default in EffectiveContextSize Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * feat(config): resolve context_size=-1 to model trained max with VRAM warn Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * fix(config): treat negative context_size as unset when GGUF is unparseable Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * docs(config): document context_size=-1 auto-max Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Assisted-by: Claude:claude-opus-4-8 [Claude Code] * docs(backend): drop em dashes from EffectiveContextSize comment Signed-off-by: Ettore Di Giacinto <mudler@localai.io> 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 c9f73f4 commit 5569b2d

7 files changed

Lines changed: 123 additions & 9 deletions

File tree

core/backend/options.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -215,9 +215,12 @@ const (
215215
)
216216

217217
// EffectiveContextSize is the context window the backend will run with: the
218-
// configured value, or DefaultContextSize when unset.
218+
// configured value, or DefaultContextSize when unset. A negative value (the
219+
// context_size: -1 auto-max sentinel) that survived config resolution, e.g. on
220+
// a backend that never ran the GGUF resolver, is clamped here so a negative
221+
// n_ctx never reaches a backend.
219222
func EffectiveContextSize(c config.ModelConfig) int {
220-
if c.ContextSize != nil {
223+
if c.ContextSize != nil && *c.ContextSize > 0 {
221224
return *c.ContextSize
222225
}
223226
return DefaultContextSize

core/backend/options_internal_test.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,3 +293,24 @@ var _ = Describe("gRPCPredictOpts chat_template_kwargs metadata", func() {
293293
Expect(opts.Metadata).ToNot(HaveKey("chat_template_kwargs"))
294294
})
295295
})
296+
297+
var _ = Describe("EffectiveContextSize", func() {
298+
Context("EffectiveContextSize", func() {
299+
It("clamps a negative (auto-max sentinel) context size to the default", func() {
300+
neg := -1
301+
cfg := config.ModelConfig{LLMConfig: config.LLMConfig{ContextSize: &neg}}
302+
Expect(EffectiveContextSize(cfg)).To(Equal(DefaultContextSize))
303+
})
304+
305+
It("returns an explicit positive context size unchanged", func() {
306+
ctx := 8192
307+
cfg := config.ModelConfig{LLMConfig: config.LLMConfig{ContextSize: &ctx}}
308+
Expect(EffectiveContextSize(cfg)).To(Equal(8192))
309+
})
310+
311+
It("falls back to the default when context size is unset", func() {
312+
cfg := config.ModelConfig{}
313+
Expect(EffectiveContextSize(cfg)).To(Equal(DefaultContextSize))
314+
})
315+
})
316+
})

core/config/gguf.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,24 @@ func reservedNonChatModel(cfg *ModelConfig) bool {
2727
}
2828

2929
func guessGGUFFromFile(cfg *ModelConfig, f *gguf.GGUFFile, defaultCtx int) {
30+
// Explicit opt-in: a negative context_size (canonically -1) means "use the
31+
// model's full trained context (n_ctx_train) from GGUF metadata". Unlike the
32+
// silent unset path below, this overrides an already-present value and warns
33+
// when the resolved window will not fit detected VRAM.
34+
if cfg.ContextSize != nil && *cfg.ContextSize < 0 {
35+
if maxCtx := int(f.Architecture().MaximumContextLength); maxCtx > 0 {
36+
cfg.ContextSize = &maxCtx
37+
warnIfContextExceedsVRAM(f, maxCtx, f.Metadata().Name)
38+
} else {
39+
// No usable trained max in metadata: degrade to the safe default
40+
// rather than leak a negative n_ctx downstream.
41+
d := DefaultContextSize
42+
cfg.ContextSize = &d
43+
xlog.Warn("[gguf] context_size=-1 requested but GGUF exposes no trained max; using default",
44+
"default", d, "model", f.Metadata().Name)
45+
}
46+
}
47+
3048
if defaultCtx == 0 && cfg.ContextSize == nil {
3149
// trainedMax is the model's full trained context window (n_ctx_train).
3250
// Defaulting a model to it unbounded is what OOMs long-context models at
@@ -225,3 +243,35 @@ func applyDetectedThinkingConfig(cfg *ModelConfig, metadata *pb.ModelMetadataRes
225243

226244
xlog.Debug("[gguf] DetectThinkingSupportFromBackend: preserving explicit reasoning config", "supports_thinking", metadata.SupportsThinking, "disable_reasoning", *cfg.ReasoningConfig.DisableReasoning, "disable_reasoning_tag_prefill", *cfg.ReasoningConfig.DisableReasoningTagPrefill)
227245
}
246+
247+
// warnIfContextExceedsVRAM logs a best-effort warning when running the model at
248+
// the given context would not fit detected VRAM. It never blocks load: any
249+
// detection or estimation gap (no GPU, unknown VRAM, estimate failure) silently
250+
// skips the warning. Used by the context_size=-1 auto-max path, where the raw
251+
// trained max can be far larger than a consumer card holds.
252+
func warnIfContextExceedsVRAM(f *gguf.GGUFFile, ctx int, name string) {
253+
defer func() { _ = recover() }() // the run estimate can panic on unusual headers
254+
255+
if !xsysinfo.HasGPU("nvidia") && !xsysinfo.HasGPU("amd") {
256+
return // no VRAM to compare against
257+
}
258+
vram, err := xsysinfo.TotalAvailableVRAM()
259+
if err != nil || vram == 0 {
260+
return
261+
}
262+
263+
sum := f.EstimateLLaMACppRun(gguf.WithLLaMACppContextSize(int32(ctx))).Summarize(true, 0, 0)
264+
if len(sum.Items) == 0 {
265+
return
266+
}
267+
var used uint64
268+
for _, v := range sum.Items[0].VRAMs {
269+
used += uint64(v.NonUMA)
270+
}
271+
if used == 0 || used <= vram {
272+
return
273+
}
274+
xlog.Warn("[gguf] context_size=-1 resolved to the model's trained max; estimated VRAM may exceed available - expect OOM, or set an explicit context_size",
275+
"model", name, "context", ctx,
276+
"estimated_vram_gib", used>>30, "available_vram_gib", vram>>30)
277+
}

core/config/hooks_llamacpp.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,11 @@ func llamaCppDefaults(cfg *ModelConfig, modelPath string) {
3131
}
3232
}()
3333

34-
// Default context size if not set, regardless of whether GGUF parsing succeeds
34+
// Default context size if not set, or if a context_size=-1 auto-max was
35+
// requested but the GGUF could not be parsed, so guessGGUFFromFile never ran
36+
// to resolve it. A negative value must never reach the backend.
3537
defer func() {
36-
if cfg.ContextSize == nil {
38+
if cfg.ContextSize == nil || *cfg.ContextSize < 0 {
3739
ctx := DefaultContextSize
3840
cfg.ContextSize = &ctx
3941
}

core/config/hooks_test.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const (
2626
// array (tokenizer.ggml.tokens). The big array is exactly what SkipLargeMetadata
2727
// + UseMMap are expected to avoid reading element-by-element, so it must survive a
2828
// round-trip through the real hook without corrupting the guessed defaults.
29-
func writeTestGGUF(path, chatTemplate string, vocab int) error {
29+
func writeTestGGUF(path, chatTemplate string, vocab int, ctxTrain uint32) error {
3030
wStr := func(b *bytes.Buffer, s string) {
3131
binary.Write(b, binary.LittleEndian, uint64(len(s)))
3232
b.WriteString(s)
@@ -45,7 +45,7 @@ func writeTestGGUF(path, chatTemplate string, vocab int) error {
4545
var meta bytes.Buffer
4646
kvStr(&meta, "general.architecture", "llama")
4747
kvStr(&meta, "general.name", "ReproModel")
48-
kvU32(&meta, "llama.context_length", 4096)
48+
kvU32(&meta, "llama.context_length", ctxTrain)
4949
kvU32(&meta, "llama.attention.head_count", 32)
5050
kvU32(&meta, "llama.feed_forward_length", 11008)
5151
kvU32(&meta, "llama.block_count", 32)
@@ -211,7 +211,7 @@ var _ = Describe("Backend hooks and parser defaults", func() {
211211
It("guesses defaults from a GGUF whose large vocab is skipped", func() {
212212
dir := GinkgoT().TempDir()
213213
modelFile := "repro.gguf"
214-
Expect(writeTestGGUF(filepath.Join(dir, modelFile), chatTemplate, 50000)).To(Succeed())
214+
Expect(writeTestGGUF(filepath.Join(dir, modelFile), chatTemplate, 50000, 4096)).To(Succeed())
215215

216216
// A pre-set context size short-circuits the GGUF run-estimate, which
217217
// needs full tensor info this header-only fixture deliberately omits;
@@ -236,6 +236,26 @@ var _ = Describe("Backend hooks and parser defaults", func() {
236236
Expect(cfg.KnownUsecaseStrings).To(ContainElement("FLAG_CHAT"))
237237
})
238238

239+
It("resolves context_size=-1 to the model's trained maximum context", func() {
240+
dir := GinkgoT().TempDir()
241+
modelFile := "automax.gguf"
242+
// A distinctive trained max proves we read metadata, not the 4096 default.
243+
Expect(writeTestGGUF(filepath.Join(dir, modelFile), chatTemplate, 100, 131072)).To(Succeed())
244+
245+
neg := -1
246+
cfg := &ModelConfig{
247+
Backend: "llama-cpp",
248+
LLMConfig: LLMConfig{ContextSize: &neg},
249+
PredictionOptions: schema.PredictionOptions{
250+
BasicModelRequest: schema.BasicModelRequest{Model: modelFile},
251+
},
252+
}
253+
cfg.SetDefaults(ModelPath(dir))
254+
255+
Expect(cfg.ContextSize).NotTo(BeNil())
256+
Expect(*cfg.ContextSize).To(Equal(131072))
257+
})
258+
239259
It("falls back to the default context size when the GGUF is unreadable", func() {
240260
dir := GinkgoT().TempDir()
241261
Expect(os.WriteFile(filepath.Join(dir, "bad.gguf"), []byte("not a gguf"), 0o644)).To(Succeed())
@@ -254,6 +274,24 @@ var _ = Describe("Backend hooks and parser defaults", func() {
254274
Expect(cfg.ContextSize).NotTo(BeNil())
255275
Expect(*cfg.ContextSize).To(Equal(DefaultContextSize))
256276
})
277+
278+
It("falls back to the default when context_size=-1 but the GGUF is unreadable", func() {
279+
dir := GinkgoT().TempDir()
280+
Expect(os.WriteFile(filepath.Join(dir, "bad.gguf"), []byte("not a gguf"), 0o644)).To(Succeed())
281+
282+
neg := -1
283+
cfg := &ModelConfig{
284+
Backend: "llama-cpp",
285+
LLMConfig: LLMConfig{ContextSize: &neg},
286+
PredictionOptions: schema.PredictionOptions{
287+
BasicModelRequest: schema.BasicModelRequest{Model: "bad.gguf"},
288+
},
289+
}
290+
cfg.SetDefaults(ModelPath(dir))
291+
292+
Expect(cfg.ContextSize).NotTo(BeNil())
293+
Expect(*cfg.ContextSize).To(Equal(DefaultContextSize))
294+
})
257295
})
258296

259297
Context("PromptCacheAll default", func() {

docs/content/advanced/model-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ These settings apply to most LLM backends (llama.cpp, vLLM, etc.):
145145
| Field | Type | Default | Description |
146146
|-------|------|---------|-------------|
147147
| `threads` | int | `processor count` | Number of threads for parallel computation |
148-
| `context_size` | int | `512` | Maximum context size (number of tokens) |
148+
| `context_size` | int | `512` | Maximum context size in tokens. Set to `-1` to auto-use the model's full trained context from GGUF metadata (raw max, no VRAM capping; a warning is logged if it may not fit detected VRAM). |
149149
| `f16` | bool | `false` | Enable 16-bit floating point precision (GPU acceleration) |
150150
| `gpu_layers` | int | `0` | Number of layers to offload to GPU (0 = CPU only) |
151151

docs/content/reference/cli-reference.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ For more information on VRAM management, see [VRAM and Memory Management]({{%rel
7373
|-----------|---------|-------------|----------------------|
7474
| `--f16` | `false` | Enable GPU acceleration | `$LOCALAI_F16`, `$F16` |
7575
| `-t, --threads` | | Number of threads used for parallel computation. Usage of the number of physical cores in the system is suggested | `$LOCALAI_THREADS`, `$THREADS` |
76-
| `--context-size` | | Default context size for models | `$LOCALAI_CONTEXT_SIZE`, `$CONTEXT_SIZE` |
76+
| `--context-size` | | Default context size for models (`-1` = each model's full trained context from GGUF metadata) | `$LOCALAI_CONTEXT_SIZE`, `$CONTEXT_SIZE` |
7777

7878
## API Flags
7979

0 commit comments

Comments
 (0)