Skip to content

Commit 5f39199

Browse files
committed
fix(gallery): write installed inference defaults under parameters so the loader reads them back (#11230)
ModelConfig embeds schema.PredictionOptions with `yaml:"parameters"`, so the loader only reads temperature/top_p/top_k/min_p/repeat_penalty/ presence_penalty from the parameters: submap. The gallery installer wrote those family defaults at the top level of the model YAML, where nothing reads them back, leaving every persisted default inert on reload. Merge them into the parameters: submap instead (preserving any values the config already sets there), and add a regression test that installs a qwen3.5 model and asserts the defaults round-trip through the typed loader rather than landing as inert top-level keys. Signed-off-by: Anai-Guo <antai12232931@outlook.com>
1 parent a6cf67c commit 5f39199

2 files changed

Lines changed: 104 additions & 13 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package gallery_test
2+
3+
import (
4+
"context"
5+
"os"
6+
"path/filepath"
7+
8+
. "github.com/onsi/ginkgo/v2"
9+
. "github.com/onsi/gomega"
10+
"gopkg.in/yaml.v3"
11+
12+
"github.com/mudler/LocalAI/core/config"
13+
"github.com/mudler/LocalAI/core/gallery"
14+
"github.com/mudler/LocalAI/pkg/modelartifacts"
15+
"github.com/mudler/LocalAI/pkg/system"
16+
)
17+
18+
var _ = Describe("gallery inference-default persistence", func() {
19+
It("persists inference defaults under parameters so the loader reads them back", func() {
20+
modelsPath := GinkgoT().TempDir()
21+
state, err := system.GetSystemState(system.WithModelPath(modelsPath))
22+
Expect(err).NotTo(HaveOccurred())
23+
resolved := modelartifacts.Spec{
24+
Name: "model", Target: "model",
25+
Source: modelartifacts.Source{Type: "huggingface", Repo: "owner/repo", Revision: "main"},
26+
Resolved: &modelartifacts.Resolved{
27+
Endpoint: "https://huggingface.co",
28+
Revision: "0123456789abcdef0123456789abcdef01234567",
29+
CacheKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
30+
},
31+
}
32+
fake := &fakeArtifactMaterializer{result: modelartifacts.Result{
33+
Spec: resolved,
34+
RelativePath: ".artifacts/huggingface/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/snapshot",
35+
}}
36+
definition := &gallery.ModelConfig{Name: "qwen3.5-managed", ConfigFile: `
37+
backend: transformers
38+
artifacts:
39+
- name: model
40+
target: model
41+
source:
42+
type: huggingface
43+
repo: owner/repo
44+
parameters:
45+
model: owner/repo
46+
`}
47+
48+
// A qwen3.5 name makes ApplyInferenceDefaults fill in the recommended
49+
// sampling parameters (repeat_penalty=1, presence_penalty=1.5, min_p=0).
50+
// Those belong under the parameters: key — ModelConfig embeds
51+
// schema.PredictionOptions with `yaml:"parameters"`, so the loader only
52+
// reads them from that submap (#11230).
53+
_, err = gallery.InstallModel(context.Background(), state, "qwen3.5-managed", definition, nil, nil, false,
54+
gallery.WithArtifactMaterializer(fake))
55+
Expect(err).NotTo(HaveOccurred())
56+
57+
data, err := os.ReadFile(filepath.Join(modelsPath, "qwen3.5-managed.yaml"))
58+
Expect(err).NotTo(HaveOccurred())
59+
60+
// The defaults must survive a round-trip through the typed loader.
61+
var reloaded config.ModelConfig
62+
Expect(yaml.Unmarshal(data, &reloaded)).To(Succeed())
63+
Expect(reloaded.PresencePenalty).To(BeNumerically("==", 1.5))
64+
Expect(reloaded.RepeatPenalty).To(BeNumerically("==", 1))
65+
Expect(reloaded.MinP).NotTo(BeNil())
66+
Expect(reloaded.Temperature).NotTo(BeNil())
67+
68+
// They must live under parameters:, never at the top level, or they are
69+
// silently dropped on reload.
70+
var raw map[string]any
71+
Expect(yaml.Unmarshal(data, &raw)).To(Succeed())
72+
Expect(raw).NotTo(HaveKey("presence_penalty"))
73+
Expect(raw).NotTo(HaveKey("repeat_penalty"))
74+
Expect(raw).NotTo(HaveKey("min_p"))
75+
parameters := raw["parameters"].(map[string]any)
76+
Expect(parameters).To(HaveKey("presence_penalty"))
77+
Expect(parameters).To(HaveKey("repeat_penalty"))
78+
Expect(parameters).To(HaveKey("min_p"))
79+
})
80+
})

core/gallery/models.go

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -270,36 +270,47 @@ func InstallModel(ctx context.Context, systemState *system.SystemState, nameOver
270270
lconfig.ApplyInferenceDefaults(&modelConfig, name, modelConfig.Model)
271271

272272
// Merge inference defaults into configMap so they are persisted without losing unknown fields.
273+
// These sampling parameters live under the `parameters:` key on disk: ModelConfig
274+
// embeds schema.PredictionOptions with `yaml:"parameters"`, so the loader only reads
275+
// them from that submap. Writing them at the top level produced keys the loader never
276+
// read back, leaving the persisted defaults inert on reload (#11230).
277+
params, ok := configMap["parameters"].(map[string]any)
278+
if !ok {
279+
params = make(map[string]any)
280+
}
273281
if modelConfig.Temperature != nil {
274-
if _, exists := configMap["temperature"]; !exists {
275-
configMap["temperature"] = *modelConfig.Temperature
282+
if _, exists := params["temperature"]; !exists {
283+
params["temperature"] = *modelConfig.Temperature
276284
}
277285
}
278286
if modelConfig.TopP != nil {
279-
if _, exists := configMap["top_p"]; !exists {
280-
configMap["top_p"] = *modelConfig.TopP
287+
if _, exists := params["top_p"]; !exists {
288+
params["top_p"] = *modelConfig.TopP
281289
}
282290
}
283291
if modelConfig.TopK != nil {
284-
if _, exists := configMap["top_k"]; !exists {
285-
configMap["top_k"] = *modelConfig.TopK
292+
if _, exists := params["top_k"]; !exists {
293+
params["top_k"] = *modelConfig.TopK
286294
}
287295
}
288296
if modelConfig.MinP != nil {
289-
if _, exists := configMap["min_p"]; !exists {
290-
configMap["min_p"] = *modelConfig.MinP
297+
if _, exists := params["min_p"]; !exists {
298+
params["min_p"] = *modelConfig.MinP
291299
}
292300
}
293301
if modelConfig.RepeatPenalty != 0 {
294-
if _, exists := configMap["repeat_penalty"]; !exists {
295-
configMap["repeat_penalty"] = modelConfig.RepeatPenalty
302+
if _, exists := params["repeat_penalty"]; !exists {
303+
params["repeat_penalty"] = modelConfig.RepeatPenalty
296304
}
297305
}
298306
if modelConfig.PresencePenalty != 0 {
299-
if _, exists := configMap["presence_penalty"]; !exists {
300-
configMap["presence_penalty"] = modelConfig.PresencePenalty
307+
if _, exists := params["presence_penalty"]; !exists {
308+
params["presence_penalty"] = modelConfig.PresencePenalty
301309
}
302310
}
311+
if len(params) > 0 {
312+
configMap["parameters"] = params
313+
}
303314

304315
// Re-marshal from configMap to preserve unknown fields
305316
updatedConfigYAML, err = yaml.Marshal(configMap)
@@ -494,4 +505,4 @@ func SafetyScanGalleryModel(galleryModel *GalleryModel) error {
494505
}
495506
}
496507
return nil
497-
}
508+
}

0 commit comments

Comments
 (0)