Skip to content

Commit bb54f2d

Browse files
authored
feat(gallery): automatically install missing backends along models (#5736)
Signed-off-by: Ettore Di Giacinto <[email protected]>
1 parent e1cc7ee commit bb54f2d

File tree

15 files changed

+140
-72
lines changed

15 files changed

+140
-72
lines changed

core/application/startup.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ func New(opts ...config.AppOption) (*Application, error) {
5757
}
5858
}
5959

60-
if err := pkgStartup.InstallModels(options.Galleries, options.ModelPath, options.EnforcePredownloadScans, nil, options.ModelsURL...); err != nil {
60+
if err := pkgStartup.InstallModels(options.Galleries, options.BackendGalleries, options.ModelPath, options.BackendsPath, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, nil, options.ModelsURL...); err != nil {
6161
log.Error().Err(err).Msg("error installing models")
6262
}
6363

@@ -86,13 +86,13 @@ func New(opts ...config.AppOption) (*Application, error) {
8686
}
8787

8888
if options.PreloadJSONModels != "" {
89-
if err := services.ApplyGalleryFromString(options.ModelPath, options.PreloadJSONModels, options.EnforcePredownloadScans, options.Galleries); err != nil {
89+
if err := services.ApplyGalleryFromString(options.ModelPath, options.BackendsPath, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadJSONModels); err != nil {
9090
return nil, err
9191
}
9292
}
9393

9494
if options.PreloadModelsFromPath != "" {
95-
if err := services.ApplyGalleryFromFile(options.ModelPath, options.PreloadModelsFromPath, options.EnforcePredownloadScans, options.Galleries); err != nil {
95+
if err := services.ApplyGalleryFromFile(options.ModelPath, options.BackendsPath, options.EnforcePredownloadScans, options.AutoloadBackendGalleries, options.Galleries, options.BackendGalleries, options.PreloadModelsFromPath); err != nil {
9696
return nil, err
9797
}
9898
}

core/backend/llm.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,9 +42,10 @@ func ModelInference(ctx context.Context, s string, messages []schema.Message, im
4242
if _, err := os.Stat(modelFile); os.IsNotExist(err) {
4343
utils.ResetDownloadTimers()
4444
// if we failed to load the model, we try to download it
45-
err := gallery.InstallModelFromGallery(o.Galleries, modelFile, loader.ModelPath, gallery.GalleryModel{}, utils.DisplayDownloadFunction, o.EnforcePredownloadScans)
45+
err := gallery.InstallModelFromGallery(o.Galleries, o.BackendGalleries, modelFile, loader.ModelPath, o.BackendsPath, gallery.GalleryModel{}, utils.DisplayDownloadFunction, o.EnforcePredownloadScans, o.AutoloadBackendGalleries)
4646
if err != nil {
47-
return nil, err
47+
log.Error().Err(err).Msgf("failed to install model %q from gallery", modelFile)
48+
//return nil, err
4849
}
4950
}
5051
}

core/cli/models.go

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ import (
1616
)
1717

1818
type ModelsCMDFlags struct {
19-
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
20-
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
19+
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
20+
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
21+
ModelsPath string `env:"LOCALAI_MODELS_PATH,MODELS_PATH" type:"path" default:"${basepath}/models" help:"Path containing models used for inferencing" group:"storage"`
22+
BackendsPath string `env:"LOCALAI_BACKENDS_PATH,BACKENDS_PATH" type:"path" default:"${basepath}/backends" help:"Path containing backends used for inferencing" group:"storage"`
2123
}
2224

2325
type ModelsList struct {
2426
ModelsCMDFlags `embed:""`
2527
}
2628

2729
type ModelsInstall struct {
28-
DisablePredownloadScan bool `env:"LOCALAI_DISABLE_PREDOWNLOAD_SCAN" help:"If true, disables the best-effort security scanner before downloading any files." group:"hardening" default:"false"`
29-
ModelArgs []string `arg:"" optional:"" name:"models" help:"Model configuration URLs to load"`
30+
DisablePredownloadScan bool `env:"LOCALAI_DISABLE_PREDOWNLOAD_SCAN" help:"If true, disables the best-effort security scanner before downloading any files." group:"hardening" default:"false"`
31+
AutoloadBackendGalleries bool `env:"LOCALAI_AUTOLOAD_BACKEND_GALLERIES" help:"If true, automatically loads backend galleries" group:"backends" default:"true"`
32+
ModelArgs []string `arg:"" optional:"" name:"models" help:"Model configuration URLs to load"`
3033

3134
ModelsCMDFlags `embed:""`
3235
}
@@ -62,6 +65,11 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
6265
log.Error().Err(err).Msg("unable to load galleries")
6366
}
6467

68+
var backendGalleries []config.Gallery
69+
if err := json.Unmarshal([]byte(mi.BackendGalleries), &backendGalleries); err != nil {
70+
log.Error().Err(err).Msg("unable to load backend galleries")
71+
}
72+
6573
for _, modelName := range mi.ModelArgs {
6674

6775
progressBar := progressbar.NewOptions(
@@ -100,7 +108,7 @@ func (mi *ModelsInstall) Run(ctx *cliContext.Context) error {
100108
log.Info().Str("model", modelName).Str("license", model.License).Msg("installing model")
101109
}
102110

103-
err = startup.InstallModels(galleries, mi.ModelsPath, !mi.DisablePredownloadScan, progressCallback, modelName)
111+
err = startup.InstallModels(galleries, backendGalleries, mi.ModelsPath, mi.BackendsPath, !mi.DisablePredownloadScan, mi.AutoloadBackendGalleries, progressCallback, modelName)
104112
if err != nil {
105113
return err
106114
}

core/cli/run.go

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,14 @@ type RunCMD struct {
3030
LocalaiConfigDir string `env:"LOCALAI_CONFIG_DIR" type:"path" default:"${basepath}/configuration" help:"Directory for dynamic loading of certain configuration files (currently api_keys.json and external_backends.json)" group:"storage"`
3131
LocalaiConfigDirPollInterval time.Duration `env:"LOCALAI_CONFIG_DIR_POLL_INTERVAL" help:"Typically the config path picks up changes automatically, but if your system has broken fsnotify events, set this to an interval to poll the LocalAI Config Dir (example: 1m)" group:"storage"`
3232
// The alias on this option is there to preserve functionality with the old `--config-file` parameter
33-
ModelsConfigFile string `env:"LOCALAI_MODELS_CONFIG_FILE,CONFIG_FILE" aliases:"config-file" help:"YAML file containing a list of model backend configs" group:"storage"`
34-
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
35-
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
36-
AutoloadGalleries bool `env:"LOCALAI_AUTOLOAD_GALLERIES,AUTOLOAD_GALLERIES" group:"models"`
37-
PreloadModels string `env:"LOCALAI_PRELOAD_MODELS,PRELOAD_MODELS" help:"A List of models to apply in JSON at start" group:"models"`
38-
Models []string `env:"LOCALAI_MODELS,MODELS" help:"A List of model configuration URLs to load" group:"models"`
39-
PreloadModelsConfig string `env:"LOCALAI_PRELOAD_MODELS_CONFIG,PRELOAD_MODELS_CONFIG" help:"A List of models to apply at startup. Path to a YAML config file" group:"models"`
33+
ModelsConfigFile string `env:"LOCALAI_MODELS_CONFIG_FILE,CONFIG_FILE" aliases:"config-file" help:"YAML file containing a list of model backend configs" group:"storage"`
34+
BackendGalleries string `env:"LOCALAI_BACKEND_GALLERIES,BACKEND_GALLERIES" help:"JSON list of backend galleries" group:"backends" default:"${backends}"`
35+
Galleries string `env:"LOCALAI_GALLERIES,GALLERIES" help:"JSON list of galleries" group:"models" default:"${galleries}"`
36+
AutoloadGalleries bool `env:"LOCALAI_AUTOLOAD_GALLERIES,AUTOLOAD_GALLERIES" group:"models" default:"true"`
37+
AutoloadBackendGalleries bool `env:"LOCALAI_AUTOLOAD_BACKEND_GALLERIES,AUTOLOAD_BACKEND_GALLERIES" group:"backends" default:"true"`
38+
PreloadModels string `env:"LOCALAI_PRELOAD_MODELS,PRELOAD_MODELS" help:"A List of models to apply in JSON at start" group:"models"`
39+
Models []string `env:"LOCALAI_MODELS,MODELS" help:"A List of model configuration URLs to load" group:"models"`
40+
PreloadModelsConfig string `env:"LOCALAI_PRELOAD_MODELS_CONFIG,PRELOAD_MODELS_CONFIG" help:"A List of models to apply at startup. Path to a YAML config file" group:"models"`
4041

4142
F16 bool `name:"f16" env:"LOCALAI_F16,F16" help:"Enable GPU acceleration" group:"performance"`
4243
Threads int `env:"LOCALAI_THREADS,THREADS" short:"t" help:"Number of threads used for parallel computation. Usage of the number of physical cores in the system is suggested" group:"performance"`
@@ -192,6 +193,10 @@ func (r *RunCMD) Run(ctx *cliContext.Context) error {
192193
opts = append(opts, config.EnableGalleriesAutoload)
193194
}
194195

196+
if r.AutoloadBackendGalleries {
197+
opts = append(opts, config.EnableBackendGalleriesAutoload)
198+
}
199+
195200
if r.PreloadBackendOnly {
196201
_, err := application.New(opts...)
197202
return err

core/config/application_config.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ type ApplicationConfig struct {
5555

5656
ExternalGRPCBackends map[string]string
5757

58-
AutoloadGalleries bool
58+
AutoloadGalleries, AutoloadBackendGalleries bool
5959

6060
SingleBackend bool
6161
ParallelBackendRequests bool
@@ -192,6 +192,10 @@ var EnableGalleriesAutoload = func(o *ApplicationConfig) {
192192
o.AutoloadGalleries = true
193193
}
194194

195+
var EnableBackendGalleriesAutoload = func(o *ApplicationConfig) {
196+
o.AutoloadBackendGalleries = true
197+
}
198+
195199
func WithExternalBackend(name string, uri string) AppOption {
196200
return func(o *ApplicationConfig) {
197201
if o.ExternalGRPCBackends == nil {

core/gallery/backends.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,22 @@ func findBestBackendFromMeta(backend *GalleryBackend, systemState *system.System
7171
}
7272

7373
// Installs a model from the gallery
74-
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, name string, basePath string, downloadStatus func(string, string, string, float64)) error {
74+
func InstallBackendFromGallery(galleries []config.Gallery, systemState *system.SystemState, name string, basePath string, downloadStatus func(string, string, string, float64), force bool) error {
75+
if !force {
76+
// check if we already have the backend installed
77+
backends, err := ListSystemBackends(basePath)
78+
if err != nil {
79+
return err
80+
}
81+
if _, ok := backends[name]; ok {
82+
return nil
83+
}
84+
}
85+
86+
if name == "" {
87+
return fmt.Errorf("backend name is empty")
88+
}
89+
7590
log.Debug().Interface("galleries", galleries).Str("name", name).Msg("Installing backend from gallery")
7691

7792
backends, err := AvailableBackends(galleries, basePath)

core/gallery/backends_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,13 +42,13 @@ var _ = Describe("Gallery Backends", func() {
4242

4343
Describe("InstallBackendFromGallery", func() {
4444
It("should return error when backend is not found", func() {
45-
err := InstallBackendFromGallery(galleries, nil, "non-existent", tempDir, nil)
45+
err := InstallBackendFromGallery(galleries, nil, "non-existent", tempDir, nil, true)
4646
Expect(err).To(HaveOccurred())
4747
Expect(err.Error()).To(ContainSubstring("no backend found with name \"non-existent\""))
4848
})
4949

5050
It("should install backend from gallery", func() {
51-
err := InstallBackendFromGallery(galleries, nil, "test-backend", tempDir, nil)
51+
err := InstallBackendFromGallery(galleries, nil, "test-backend", tempDir, nil, true)
5252
Expect(err).ToNot(HaveOccurred())
5353
Expect(filepath.Join(tempDir, "test-backend", "run.sh")).To(BeARegularFile())
5454
})
@@ -181,7 +181,7 @@ var _ = Describe("Gallery Backends", func() {
181181

182182
// Test with NVIDIA system state
183183
nvidiaSystemState := &system.SystemState{GPUVendor: "nvidia"}
184-
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, "meta-backend", tempDir, nil)
184+
err = InstallBackendFromGallery([]config.Gallery{gallery}, nvidiaSystemState, "meta-backend", tempDir, nil, true)
185185
Expect(err).NotTo(HaveOccurred())
186186

187187
metaBackendPath := filepath.Join(tempDir, "meta-backend")

core/gallery/models.go

Lines changed: 38 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"dario.cat/mergo"
1111
"github.com/mudler/LocalAI/core/config"
1212
lconfig "github.com/mudler/LocalAI/core/config"
13+
"github.com/mudler/LocalAI/core/system"
1314
"github.com/mudler/LocalAI/pkg/downloader"
1415
"github.com/mudler/LocalAI/pkg/utils"
1516

@@ -69,7 +70,9 @@ type PromptTemplate struct {
6970
}
7071

7172
// Installs a model from the gallery
72-
func InstallModelFromGallery(galleries []config.Gallery, name string, basePath string, req GalleryModel, downloadStatus func(string, string, string, float64), enforceScan bool) error {
73+
func InstallModelFromGallery(
74+
modelGalleries, backendGalleries []config.Gallery,
75+
name string, basePath, backendBasePath string, req GalleryModel, downloadStatus func(string, string, string, float64), enforceScan, automaticallyInstallBackend bool) error {
7376

7477
applyModel := func(model *GalleryModel) error {
7578
name = strings.ReplaceAll(name, string(os.PathSeparator), "__")
@@ -119,14 +122,26 @@ func InstallModelFromGallery(galleries []config.Gallery, name string, basePath s
119122
return err
120123
}
121124

122-
if err := InstallModel(basePath, installName, &config, model.Overrides, downloadStatus, enforceScan); err != nil {
125+
installedModel, err := InstallModel(basePath, installName, &config, model.Overrides, downloadStatus, enforceScan)
126+
if err != nil {
123127
return err
124128
}
125129

130+
if automaticallyInstallBackend && installedModel.Backend != "" {
131+
systemState, err := system.GetSystemState()
132+
if err != nil {
133+
return err
134+
}
135+
136+
if err := InstallBackendFromGallery(backendGalleries, systemState, installedModel.Backend, backendBasePath, downloadStatus, false); err != nil {
137+
return err
138+
}
139+
}
140+
126141
return nil
127142
}
128143

129-
models, err := AvailableGalleryModels(galleries, basePath)
144+
models, err := AvailableGalleryModels(modelGalleries, basePath)
130145
if err != nil {
131146
return err
132147
}
@@ -139,11 +154,11 @@ func InstallModelFromGallery(galleries []config.Gallery, name string, basePath s
139154
return applyModel(model)
140155
}
141156

142-
func InstallModel(basePath, nameOverride string, config *ModelConfig, configOverrides map[string]interface{}, downloadStatus func(string, string, string, float64), enforceScan bool) error {
157+
func InstallModel(basePath, nameOverride string, config *ModelConfig, configOverrides map[string]interface{}, downloadStatus func(string, string, string, float64), enforceScan bool) (*lconfig.BackendConfig, error) {
143158
// Create base path if it doesn't exist
144159
err := os.MkdirAll(basePath, 0750)
145160
if err != nil {
146-
return fmt.Errorf("failed to create base path: %v", err)
161+
return nil, fmt.Errorf("failed to create base path: %v", err)
147162
}
148163

149164
if len(configOverrides) > 0 {
@@ -155,7 +170,7 @@ func InstallModel(basePath, nameOverride string, config *ModelConfig, configOver
155170
log.Debug().Msgf("Checking %q exists and matches SHA", file.Filename)
156171

157172
if err := utils.VerifyPath(file.Filename, basePath); err != nil {
158-
return err
173+
return nil, err
159174
}
160175

161176
// Create file path
@@ -165,32 +180,32 @@ func InstallModel(basePath, nameOverride string, config *ModelConfig, configOver
165180
scanResults, err := downloader.HuggingFaceScan(downloader.URI(file.URI))
166181
if err != nil && errors.Is(err, downloader.ErrUnsafeFilesFound) {
167182
log.Error().Str("model", config.Name).Strs("clamAV", scanResults.ClamAVInfectedFiles).Strs("pickles", scanResults.DangerousPickles).Msg("Contains unsafe file(s)!")
168-
return err
183+
return nil, err
169184
}
170185
}
171186
uri := downloader.URI(file.URI)
172187
if err := uri.DownloadFile(filePath, file.SHA256, i, len(config.Files), downloadStatus); err != nil {
173-
return err
188+
return nil, err
174189
}
175190
}
176191

177192
// Write prompt template contents to separate files
178193
for _, template := range config.PromptTemplates {
179194
if err := utils.VerifyPath(template.Name+".tmpl", basePath); err != nil {
180-
return err
195+
return nil, err
181196
}
182197
// Create file path
183198
filePath := filepath.Join(basePath, template.Name+".tmpl")
184199

185200
// Create parent directory
186201
err := os.MkdirAll(filepath.Dir(filePath), 0750)
187202
if err != nil {
188-
return fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
203+
return nil, fmt.Errorf("failed to create parent directory for prompt template %q: %v", template.Name, err)
189204
}
190205
// Create and write file content
191206
err = os.WriteFile(filePath, []byte(template.Content), 0600)
192207
if err != nil {
193-
return fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
208+
return nil, fmt.Errorf("failed to write prompt template %q: %v", template.Name, err)
194209
}
195210

196211
log.Debug().Msgf("Prompt template %q written", template.Name)
@@ -202,9 +217,11 @@ func InstallModel(basePath, nameOverride string, config *ModelConfig, configOver
202217
}
203218

204219
if err := utils.VerifyPath(name+".yaml", basePath); err != nil {
205-
return err
220+
return nil, err
206221
}
207222

223+
backendConfig := lconfig.BackendConfig{}
224+
208225
// write config file
209226
if len(configOverrides) != 0 || len(config.ConfigFile) != 0 {
210227
configFilePath := filepath.Join(basePath, name+".yaml")
@@ -213,33 +230,33 @@ func InstallModel(basePath, nameOverride string, config *ModelConfig, configOver
213230
configMap := make(map[string]interface{})
214231
err = yaml.Unmarshal([]byte(config.ConfigFile), &configMap)
215232
if err != nil {
216-
return fmt.Errorf("failed to unmarshal config YAML: %v", err)
233+
return nil, fmt.Errorf("failed to unmarshal config YAML: %v", err)
217234
}
218235

219236
configMap["name"] = name
220237

221238
if err := mergo.Merge(&configMap, configOverrides, mergo.WithOverride); err != nil {
222-
return err
239+
return nil, err
223240
}
224241

225242
// Write updated config file
226243
updatedConfigYAML, err := yaml.Marshal(configMap)
227244
if err != nil {
228-
return fmt.Errorf("failed to marshal updated config YAML: %v", err)
245+
return nil, fmt.Errorf("failed to marshal updated config YAML: %v", err)
229246
}
230247

231-
backendConfig := lconfig.BackendConfig{}
232248
err = yaml.Unmarshal(updatedConfigYAML, &backendConfig)
233249
if err != nil {
234-
return fmt.Errorf("failed to unmarshal updated config YAML: %v", err)
250+
return nil, fmt.Errorf("failed to unmarshal updated config YAML: %v", err)
235251
}
252+
236253
if !backendConfig.Validate() {
237-
return fmt.Errorf("failed to validate updated config YAML")
254+
return nil, fmt.Errorf("failed to validate updated config YAML")
238255
}
239256

240257
err = os.WriteFile(configFilePath, updatedConfigYAML, 0600)
241258
if err != nil {
242-
return fmt.Errorf("failed to write updated config file: %v", err)
259+
return nil, fmt.Errorf("failed to write updated config file: %v", err)
243260
}
244261

245262
log.Debug().Msgf("Written config file %s", configFilePath)
@@ -249,14 +266,12 @@ func InstallModel(basePath, nameOverride string, config *ModelConfig, configOver
249266
modelFile := filepath.Join(basePath, galleryFileName(name))
250267
data, err := yaml.Marshal(config)
251268
if err != nil {
252-
return err
269+
return nil, err
253270
}
254271

255272
log.Debug().Msgf("Written gallery file %s", modelFile)
256273

257-
return os.WriteFile(modelFile, data, 0600)
258-
259-
//return nil
274+
return &backendConfig, os.WriteFile(modelFile, data, 0600)
260275
}
261276

262277
func galleryFileName(name string) string {

0 commit comments

Comments
 (0)