Skip to content

Commit 4ae460e

Browse files
mudlerlocalai-bot
authored andcommitted
feat: inferencing default, automatic tool parsing fallback and wire min_p (mudler#9092)
* feat: wire min_p Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * feat: inferencing defaults Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore(refactor): re-use iterative parser Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: generate automatically inference defaults from unsloth Instead of trying to re-invent the wheel and maintain here the inference defaults, prefer to consume unsloth ones, and contribute there as necessary. Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: apply defaults also to models installed via gallery Signed-off-by: Ettore Di Giacinto <mudler@localai.io> * chore: be consistent and apply fallback to all endpoint Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io>
1 parent fdd214a commit 4ae460e

26 files changed

Lines changed: 1154 additions & 22 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: Bump inference defaults
2+
3+
on:
4+
schedule:
5+
# Run daily at 06:00 UTC
6+
- cron: '0 6 * * *'
7+
workflow_dispatch: # Allow manual trigger
8+
9+
permissions:
10+
contents: write
11+
pull-requests: write
12+
13+
jobs:
14+
bump:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- uses: actions/setup-go@v5
20+
with:
21+
go-version-file: go.mod
22+
23+
- name: Re-fetch inference defaults
24+
run: make generate-force
25+
26+
- name: Check for changes
27+
id: diff
28+
run: |
29+
if git diff --quiet core/config/inference_defaults.json; then
30+
echo "changed=false" >> "$GITHUB_OUTPUT"
31+
else
32+
echo "changed=true" >> "$GITHUB_OUTPUT"
33+
fi
34+
35+
- name: Create Pull Request
36+
if: steps.diff.outputs.changed == 'true'
37+
uses: peter-evans/create-pull-request@v7
38+
with:
39+
commit-message: "chore: bump inference defaults from unsloth"
40+
title: "chore: bump inference defaults from unsloth"
41+
body: |
42+
Auto-generated update of `core/config/inference_defaults.json` from
43+
[unsloth's inference_defaults.json](https://github.com/unslothai/unsloth/blob/main/studio/backend/assets/configs/inference_defaults.json).
44+
45+
This PR was created automatically by the `bump-inference-defaults` workflow.
46+
branch: chore/bump-inference-defaults
47+
delete-branch: true
48+
labels: automated

Makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ core/http/react-ui/dist: react-ui
107107

108108
## Build:
109109

110-
build: protogen-go install-go-tools core/http/react-ui/dist ## Build the project
110+
build: protogen-go generate install-go-tools core/http/react-ui/dist ## Build the project
111111
$(info ${GREEN}I local-ai build info:${RESET})
112112
$(info ${GREEN}I BUILD_TYPE: ${YELLOW}$(BUILD_TYPE)${RESET})
113113
$(info ${GREEN}I GO_TAGS: ${YELLOW}$(GO_TAGS)${RESET})
@@ -398,6 +398,16 @@ protogen-go: protoc install-go-tools
398398
./protoc --experimental_allow_proto3_optional -Ibackend/ --go_out=pkg/grpc/proto/ --go_opt=paths=source_relative --go-grpc_out=pkg/grpc/proto/ --go-grpc_opt=paths=source_relative \
399399
backend/backend.proto
400400

401+
core/config/inference_defaults.json: ## Fetch inference defaults from unsloth (only if missing)
402+
$(GOCMD) generate ./core/config/...
403+
404+
.PHONY: generate
405+
generate: core/config/inference_defaults.json ## Ensure inference defaults exist
406+
407+
.PHONY: generate-force
408+
generate-force: ## Re-fetch inference defaults from unsloth (always)
409+
$(GOCMD) generate ./core/config/...
410+
401411
.PHONY: protogen-go-clean
402412
protogen-go-clean:
403413
$(RM) pkg/grpc/proto/backend.pb.go pkg/grpc/proto/backend_grpc.pb.go

backend/backend.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ message PredictOptions {
178178
int32 Logprobs = 50; // Number of top logprobs to return (maps to OpenAI logprobs parameter)
179179
int32 TopLogprobs = 51; // Number of top logprobs to return per token (maps to OpenAI top_logprobs parameter)
180180
map<string, string> Metadata = 52; // Generic per-request metadata (e.g., enable_thinking)
181+
float MinP = 53; // Minimum probability sampling threshold (0.0 = disabled)
181182
}
182183

183184
// ToolCallDelta represents an incremental tool call update from the C++ parser.

backend/cpp/llama-cpp/grpc-server.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,7 @@ json parse_options(bool streaming, const backend::PredictOptions* predict, const
136136
data["mirostat_eta"] = predict->mirostateta();
137137
data["n_keep"] = predict->nkeep();
138138
data["seed"] = predict->seed();
139+
data["min_p"] = predict->minp();
139140

140141

141142
std::string grammar_str = predict->grammar();

core/backend/options.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ func gRPCPredictOpts(c config.ModelConfig, modelPath string) *pb.PredictOptions
252252
TopP: float32(*c.TopP),
253253
NDraft: c.NDraft,
254254
TopK: int32(*c.TopK),
255+
MinP: float32(*c.MinP),
255256
Tokens: int32(*c.Maxtokens),
256257
Threads: int32(*c.Threads),
257258
PromptCacheAll: c.PromptCacheAll,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# gen_inference_defaults
2+
3+
This tool fetches per-model-family inference parameter defaults from [unsloth's inference_defaults.json](https://github.com/unslothai/unsloth/blob/main/studio/backend/assets/configs/inference_defaults.json), validates the data, remaps field names to LocalAI conventions, and writes `core/config/inference_defaults.json`.
4+
5+
## What it does
6+
7+
1. Fetches the latest `inference_defaults.json` from unsloth's repo
8+
2. Validates that every entry has required fields (`temperature`, `top_p`, `top_k`)
9+
3. Validates that every pattern references an existing family
10+
4. Warns if pattern ordering would cause shorter prefixes to shadow longer ones
11+
5. Remaps `repetition_penalty``repeat_penalty` (LocalAI naming)
12+
6. Filters to allowed fields only: `temperature`, `top_p`, `top_k`, `min_p`, `repeat_penalty`, `presence_penalty`
13+
7. Writes the validated JSON to `core/config/inference_defaults.json`
14+
15+
## Usage
16+
17+
```bash
18+
# Only regenerate if the file is missing (runs during make build)
19+
make generate
20+
21+
# Force re-fetch from unsloth
22+
make generate-force
23+
24+
# Or directly via go generate
25+
go generate ./core/config/...
26+
```
27+
28+
## Automation
29+
30+
The GitHub Actions workflow `.github/workflows/bump-inference-defaults.yml` runs `make generate-force` daily and opens a PR if the upstream data changed.
Lines changed: 222 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
// gen_inference_defaults fetches unsloth's inference_defaults.json,
2+
// validates its structure, remaps field names to LocalAI conventions,
3+
// and writes the result to core/config/inference_defaults.json.
4+
//
5+
// Run via: go generate ./core/config/
6+
package main
7+
8+
import (
9+
"encoding/json"
10+
"fmt"
11+
"io"
12+
"net/http"
13+
"os"
14+
"sort"
15+
"strings"
16+
)
17+
18+
const (
19+
unslothURL = "https://raw.githubusercontent.com/unslothai/unsloth/main/studio/backend/assets/configs/inference_defaults.json"
20+
outputFile = "inference_defaults.json"
21+
)
22+
23+
// unslothDefaults mirrors the upstream JSON structure
24+
type unslothDefaults struct {
25+
Comment string `json:"_comment"`
26+
Families map[string]map[string]float64 `json:"families"`
27+
Patterns []string `json:"patterns"`
28+
}
29+
30+
// localAIDefaults is our output structure
31+
type localAIDefaults struct {
32+
Comment string `json:"_comment"`
33+
Families map[string]map[string]float64 `json:"families"`
34+
Patterns []string `json:"patterns"`
35+
}
36+
37+
// requiredFields are the fields every family entry must have
38+
var requiredFields = []string{"temperature", "top_p", "top_k"}
39+
40+
// fieldRemap maps unsloth field names to LocalAI field names
41+
var fieldRemap = map[string]string{
42+
"repetition_penalty": "repeat_penalty",
43+
}
44+
45+
// allowedFields are the only fields we keep (after remapping)
46+
var allowedFields = map[string]bool{
47+
"temperature": true,
48+
"top_p": true,
49+
"top_k": true,
50+
"min_p": true,
51+
"repeat_penalty": true,
52+
"presence_penalty": true,
53+
}
54+
55+
func main() {
56+
fmt.Fprintf(os.Stderr, "Fetching %s ...\n", unslothURL)
57+
58+
resp, err := http.Get(unslothURL)
59+
if err != nil {
60+
fatal("fetch failed: %v", err)
61+
}
62+
defer resp.Body.Close()
63+
64+
if resp.StatusCode != 200 {
65+
fatal("fetch returned HTTP %d", resp.StatusCode)
66+
}
67+
68+
body, err := io.ReadAll(resp.Body)
69+
if err != nil {
70+
fatal("read body: %v", err)
71+
}
72+
73+
var upstream unslothDefaults
74+
if err := json.Unmarshal(body, &upstream); err != nil {
75+
fatal("parse upstream JSON: %v", err)
76+
}
77+
78+
// Validate structure
79+
if len(upstream.Families) == 0 {
80+
fatal("upstream has no families")
81+
}
82+
if len(upstream.Patterns) == 0 {
83+
fatal("upstream has no patterns")
84+
}
85+
86+
// Validate every pattern references a family
87+
for _, p := range upstream.Patterns {
88+
if _, ok := upstream.Families[p]; !ok {
89+
fatal("pattern %q has no corresponding family entry", p)
90+
}
91+
}
92+
93+
// Validate every family has required fields and remap field names
94+
output := localAIDefaults{
95+
Comment: "Auto-generated from unsloth inference_defaults.json. DO NOT EDIT. Run go generate ./core/config/ to update.",
96+
Families: make(map[string]map[string]float64, len(upstream.Families)),
97+
Patterns: upstream.Patterns,
98+
}
99+
100+
// Sort family names for deterministic output
101+
familyNames := make([]string, 0, len(upstream.Families))
102+
for name := range upstream.Families {
103+
familyNames = append(familyNames, name)
104+
}
105+
sort.Strings(familyNames)
106+
107+
for _, name := range familyNames {
108+
params := upstream.Families[name]
109+
110+
// Check required fields
111+
for _, req := range requiredFields {
112+
found := false
113+
for k := range params {
114+
mapped := k
115+
if m, ok := fieldRemap[k]; ok {
116+
mapped = m
117+
}
118+
if mapped == req || k == req {
119+
found = true
120+
break
121+
}
122+
}
123+
if !found {
124+
fatal("family %q missing required field %q", name, req)
125+
}
126+
}
127+
128+
// Remap and filter fields
129+
remapped := make(map[string]float64)
130+
for k, v := range params {
131+
if newName, ok := fieldRemap[k]; ok {
132+
k = newName
133+
}
134+
if allowedFields[k] {
135+
remapped[k] = v
136+
}
137+
}
138+
output.Families[name] = remapped
139+
}
140+
141+
// Validate patterns are ordered longest-match-first within same prefix groups
142+
validatePatternOrder(output.Patterns)
143+
144+
// Marshal with ordered keys for readability
145+
data, err := marshalOrdered(output)
146+
if err != nil {
147+
fatal("marshal output: %v", err)
148+
}
149+
150+
if err := os.WriteFile(outputFile, data, 0644); err != nil {
151+
fatal("write %s: %v", outputFile, err)
152+
}
153+
154+
fmt.Fprintf(os.Stderr, "Written %s (%d families, %d patterns)\n",
155+
outputFile, len(output.Families), len(output.Patterns))
156+
}
157+
158+
// validatePatternOrder warns if a shorter pattern appears before a longer one
159+
// that it's a prefix of (e.g., "qwen3" before "qwen3.5")
160+
func validatePatternOrder(patterns []string) {
161+
for i, p := range patterns {
162+
for j := i + 1; j < len(patterns); j++ {
163+
if strings.HasPrefix(patterns[j], p) {
164+
fmt.Fprintf(os.Stderr, "WARNING: pattern %q at index %d is a prefix of %q at index %d — longer match should come first\n",
165+
p, i, patterns[j], j)
166+
}
167+
}
168+
}
169+
}
170+
171+
// marshalOrdered produces JSON with families in pattern order for readability
172+
func marshalOrdered(d localAIDefaults) ([]byte, error) {
173+
var sb strings.Builder
174+
sb.WriteString("{\n")
175+
sb.WriteString(fmt.Sprintf(" %q: %q,\n", "_comment", d.Comment))
176+
sb.WriteString(" \"families\": {\n")
177+
178+
// Write families in pattern order, then any remaining not in patterns
179+
written := make(map[string]bool)
180+
allFamilies := make([]string, 0, len(d.Families))
181+
for _, p := range d.Patterns {
182+
if _, ok := d.Families[p]; ok && !written[p] {
183+
allFamilies = append(allFamilies, p)
184+
written[p] = true
185+
}
186+
}
187+
for name := range d.Families {
188+
if !written[name] {
189+
allFamilies = append(allFamilies, name)
190+
}
191+
}
192+
193+
for i, name := range allFamilies {
194+
params := d.Families[name]
195+
paramJSON, err := json.Marshal(params)
196+
if err != nil {
197+
return nil, err
198+
}
199+
comma := ","
200+
if i == len(allFamilies)-1 {
201+
comma = ""
202+
}
203+
sb.WriteString(fmt.Sprintf(" %q: %s%s\n", name, paramJSON, comma))
204+
}
205+
206+
sb.WriteString(" },\n")
207+
208+
// Patterns array
209+
patternsJSON, err := json.Marshal(d.Patterns)
210+
if err != nil {
211+
return nil, err
212+
}
213+
sb.WriteString(fmt.Sprintf(" \"patterns\": %s\n", patternsJSON))
214+
sb.WriteString("}\n")
215+
216+
return []byte(sb.String()), nil
217+
}
218+
219+
func fatal(format string, args ...any) {
220+
fmt.Fprintf(os.Stderr, "gen_inference_defaults: "+format+"\n", args...)
221+
os.Exit(1)
222+
}

core/config/gguf.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,8 @@ func guessGGUFFromFile(cfg *ModelConfig, f *gguf.GGUFFile, defaultCtx int) {
7676
cfg.Options = append(cfg.Options, "use_jinja:true")
7777
cfg.KnownUsecaseStrings = append(cfg.KnownUsecaseStrings, "FLAG_CHAT")
7878

79+
// Apply per-model-family inference parameter defaults (temperature, top_p, etc.)
80+
ApplyInferenceDefaults(cfg, f.Metadata().Name)
7981
}
8082

8183
// DetectThinkingSupportFromBackend calls the ModelMetadata gRPC method to detect

0 commit comments

Comments
 (0)