mirror of
https://github.com/mudler/LocalAI.git
synced 2026-07-30 18:09:05 -04:00
feat: materialize Hugging Face model artifacts (#10825)
* feat(config): add model artifact source contract Assisted-by: Codex:GPT-5 [Codex] * feat(downloader): add authenticated raw-byte progress Assisted-by: Codex:GPT-5 [Codex] * feat(huggingface): resolve immutable snapshot manifests Assisted-by: Codex:GPT-5 [Codex] * feat(models): add artifact storage primitives Assisted-by: Codex:GPT-5 [Codex] * feat(models): materialize pinned Hugging Face snapshots Assisted-by: Codex:GPT-5 [Codex] * feat(models): bind managed snapshots at runtime Assisted-by: Codex:GPT-5 [Codex] * feat(gallery): materialize model artifacts during install Assisted-by: Codex:GPT-5 [Codex] * feat(gallery): declare managed Hugging Face artifacts Assisted-by: Codex:GPT-5 [Codex] * feat(models): preload managed model artifacts Assisted-by: Codex:GPT-5 [Codex] * fix(gallery): retain shared artifact caches on delete Assisted-by: Codex:GPT-5 [Codex] * feat(models): report artifact acquisition progress Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): load managed models from ModelFile Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): load staged speech model snapshots Assisted-by: Codex:GPT-5 [Codex] * refactor(backends): use staged snapshots in engine backends Assisted-by: Codex:GPT-5 [Codex] * test(distributed): cover staged artifact snapshots Assisted-by: Codex:GPT-5 [Codex] * docs: explain managed model artifacts Assisted-by: Codex:GPT-5 [Codex] * docs: add product design context Assisted-by: Codex:GPT-5 [Codex] * feat(ui): show model artifact download progress Assisted-by: Codex:GPT-5 [Codex] * Eagerly materialize Hugging Face artifacts Materialize HF-backed model references as managed GGUF artifacts during load, with lazy download retained only as fallback. Assisted-by: Codex:GPT-5 [shell] * Refactor HF downloads through a shared executor Assisted-by: Codex:GPT-5 [shell] * drop Signed-off-by: Ettore Di Giacinto <mudler@localai.io> --------- Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
This commit is contained in:
@@ -251,7 +251,11 @@ func (ml *ModelLoader) backendLoader(opts ...Option) (client grpc.Backend, err e
|
||||
backend = realBackend
|
||||
}
|
||||
|
||||
model, err := ml.LoadModel(o.modelID, o.model, ml.grpcModel(backend, o))
|
||||
modelFileName := o.modelFile
|
||||
if modelFileName == "" {
|
||||
modelFileName = o.model
|
||||
}
|
||||
model, err := ml.LoadModelWithFile(o.modelID, o.model, modelFileName, ml.grpcModel(backend, o))
|
||||
if err != nil {
|
||||
// Defensive cleanup: the model usually wasn't registered yet (LoadModel
|
||||
// failed before that), so StopGRPC reporting "model not found" is the
|
||||
|
||||
@@ -397,14 +397,21 @@ func (ml *ModelLoader) ListLoadedModels() []*Model {
|
||||
}
|
||||
|
||||
func (ml *ModelLoader) LoadModel(modelID, modelName string, loader func(string, string, string) (*Model, error)) (*Model, error) {
|
||||
return ml.loadModel(modelID, modelName, loader, true)
|
||||
return ml.LoadModelWithFile(modelID, modelName, modelName, loader)
|
||||
}
|
||||
|
||||
// loadModel is the implementation behind LoadModel. checkCooldown gates fresh,
|
||||
func (ml *ModelLoader) LoadModelWithFile(modelID, modelName, modelFileName string, loader func(string, string, string) (*Model, error)) (*Model, error) {
|
||||
if modelFileName == "" {
|
||||
modelFileName = modelName
|
||||
}
|
||||
return ml.loadModel(modelID, modelName, modelFileName, loader, true)
|
||||
}
|
||||
|
||||
// loadModel is the implementation behind LoadModelWithFile. checkCooldown gates fresh,
|
||||
// independent load triggers behind the per-model failure cooldown; it is set to
|
||||
// false for the coalesced retry of an in-flight burst (a follower whose leader
|
||||
// just failed), which is not a new trigger and should still get its one retry.
|
||||
func (ml *ModelLoader) loadModel(modelID, modelName string, loader func(string, string, string) (*Model, error), checkCooldown bool) (*Model, error) {
|
||||
func (ml *ModelLoader) loadModel(modelID, modelName, modelFileName string, loader func(string, string, string) (*Model, error), checkCooldown bool) (*Model, error) {
|
||||
ml.mu.Lock()
|
||||
distributed := ml.modelRouter != nil
|
||||
ml.mu.Unlock()
|
||||
@@ -430,7 +437,7 @@ func (ml *ModelLoader) loadModel(modelID, modelName string, loader func(string,
|
||||
// per inference. Trade-off: cross-frontend in-flight visibility
|
||||
// becomes eventually consistent, acceptable for 1-3 frontend
|
||||
// deployments.
|
||||
modelFile := filepath.Join(ml.ModelPath, modelName)
|
||||
modelFile := filepath.Join(ml.ModelPath, modelFileName)
|
||||
model, err := loader(modelID, modelName, modelFile)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to route model with internal loader: %s", err)
|
||||
@@ -484,7 +491,7 @@ func (ml *ModelLoader) loadModel(modelID, modelName string, loader func(string,
|
||||
}
|
||||
// If still not loaded, the other goroutine failed. Retry once as part of
|
||||
// this burst, bypassing the cooldown gate (we are not a new trigger).
|
||||
return ml.loadModel(modelID, modelName, loader, false)
|
||||
return ml.loadModel(modelID, modelName, modelFileName, loader, false)
|
||||
}
|
||||
|
||||
// Mark this model as loading (create a channel that will be closed when done)
|
||||
@@ -501,7 +508,7 @@ func (ml *ModelLoader) loadModel(modelID, modelName string, loader func(string,
|
||||
}()
|
||||
|
||||
// Load the model (this can take a long time, no lock held)
|
||||
modelFile := filepath.Join(ml.ModelPath, modelName)
|
||||
modelFile := filepath.Join(ml.ModelPath, modelFileName)
|
||||
xlog.Debug("Loading model in memory from file", "file", modelFile)
|
||||
|
||||
model, err := loader(modelID, modelName, modelFile)
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
type Options struct {
|
||||
backendString string
|
||||
model string
|
||||
modelFile string
|
||||
modelID string
|
||||
context context.Context
|
||||
|
||||
@@ -73,6 +74,12 @@ func WithModel(modelFile string) Option {
|
||||
}
|
||||
}
|
||||
|
||||
func WithModelFile(modelFile string) Option {
|
||||
return func(o *Options) {
|
||||
o.modelFile = modelFile
|
||||
}
|
||||
}
|
||||
|
||||
func WithLoadGRPCLoadModelOpts(opts *pb.ModelOptions) Option {
|
||||
return func(o *Options) {
|
||||
o.gRPCOptions = opts
|
||||
|
||||
@@ -73,6 +73,22 @@ var _ = Describe("ModelLoader", func() {
|
||||
})
|
||||
|
||||
Context("LoadModel", func() {
|
||||
It("passes a logical model and managed model file independently", func() {
|
||||
const relative = ".artifacts/huggingface/0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef/snapshot"
|
||||
var receivedName, receivedFile string
|
||||
mockModel = model.NewModel("managed", "test.model", nil)
|
||||
mockModel.MarkHealthy()
|
||||
mockLoader := func(_ string, modelName, modelFile string) (*model.Model, error) {
|
||||
receivedName, receivedFile = modelName, modelFile
|
||||
return mockModel, nil
|
||||
}
|
||||
|
||||
_, err := modelLoader.LoadModelWithFile("managed", "owner/repo", relative, mockLoader)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(receivedName).To(Equal("owner/repo"))
|
||||
Expect(receivedFile).To(Equal(filepath.Join(modelPath, filepath.FromSlash(relative))))
|
||||
})
|
||||
|
||||
It("should load a model and keep it in memory", func() {
|
||||
mockModel = model.NewModel("foo", "test.model", nil)
|
||||
mockModel.MarkHealthy() // skip gRPC health check (no real server)
|
||||
|
||||
Reference in New Issue
Block a user