mirror of
https://github.com/mudler/LocalAI.git
synced 2026-11-06 04:23:15 -05:00
refactor(addons): pick engine job backends by gallery tag
The finetune.start and quantize.start operations defaulted to the backend names "trl" and "llama-cpp-quantization", and the backend lister had one method per engine family. Both were special cases that a new engine could not use. The lister now answers for a gallery tag. A start call that names no backend gets the first installed backend of the tag in gallery order, and a named backend must carry the tag. With no engine installed the call fails with a 400 that says so. The shipped gallery order keeps trl and llama-cpp-quantization as the defaults, and a test pins it. The change is additive for add-ons: no manifest field or operation changes. Assisted-by: Claude:claude-sonnet-5-5 go gofmt
This commit is contained in:
6 files changed
+147
-29
No files matched your search
@@ -1663,3 +1663,29 @@ var _ = Describe("installed development backend preference root guards", func()
|
||||
Expect(found).To(HaveKey("cpu-example-engine-development"))
|
||||
})
|
||||
})
|
||||
|
||||
var _ = Describe("the shipped backend index", func() {
|
||||
// An add-on job that names no backend gets the first backend of its tag in
|
||||
// gallery order, so the order of these entries is the default.
|
||||
It("lists the default engine of each family first", func() {
|
||||
raw, err := os.ReadFile("../../backend/index.yaml")
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var all []GalleryBackend
|
||||
Expect(yaml.Unmarshal(raw, &all)).To(Succeed())
|
||||
first := func(tag string) string {
|
||||
for _, b := range all {
|
||||
for _, t := range b.Tags {
|
||||
if t == tag {
|
||||
if b.Alias != "" {
|
||||
return b.Alias
|
||||
}
|
||||
return b.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Expect(first("fine-tuning")).To(Equal("trl"))
|
||||
Expect(first("quantization")).To(Equal("llama-cpp-quantization"))
|
||||
})
|
||||
})
|
||||
@@ -71,11 +71,17 @@ type EngineBackends struct {
|
||||
|
||||
var _ hostops.BackendLister = EngineBackends{}
|
||||
|
||||
func (e EngineBackends) FineTune() []string { return e.names("fine-tuning") }
|
||||
// Backends lists the installed backends that carry tag, in gallery order.
|
||||
func (e EngineBackends) Backends(tag string) []string {
|
||||
infos := e.lookup(tag)
|
||||
out := make([]string, 0, len(infos))
|
||||
for _, i := range infos {
|
||||
out = append(out, i.Name)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (e EngineBackends) Quantization() []string { return e.names("quantization") }
|
||||
|
||||
func (e EngineBackends) names(tag string) []string {
|
||||
func (e EngineBackends) lookup(tag string) []engineBackendInfo {
|
||||
// The interface has no request context, so the lookup gets its own bound.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
@@ -83,11 +89,7 @@ func (e EngineBackends) names(tag string) []string {
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
out := make([]string, 0, len(infos))
|
||||
for _, i := range infos {
|
||||
out = append(out, i.Name)
|
||||
}
|
||||
return out
|
||||
return infos
|
||||
}
|
||||
|
||||
// FineTuneJobs adapts the fine-tuning service to the add-on host operations.
|
||||
|
||||
@@ -42,11 +42,32 @@ type Quantizer interface {
|
||||
List(userID string) []*schema.QuantizationJob
|
||||
}
|
||||
|
||||
// BackendLister names the backends of each engine feature that the gallery
|
||||
// offers. A start operation accepts only these.
|
||||
// Gallery tags that name an engine family. A backend carries the tag in its
|
||||
// gallery entry; the host reads the engines of a family from there, so a new
|
||||
// engine needs a gallery entry and no code.
|
||||
const (
|
||||
TagFineTuning = "fine-tuning"
|
||||
TagQuantization = "quantization"
|
||||
)
|
||||
|
||||
// BackendLister names the installed backends that carry a gallery tag, in
|
||||
// gallery order. A start operation accepts only these.
|
||||
type BackendLister interface {
|
||||
FineTune() []string
|
||||
Quantization() []string
|
||||
Backends(tag string) []string
|
||||
}
|
||||
|
||||
// pickEngine answers the engine for a start call. A named engine is kept for
|
||||
// requireEngine to check. Without a name the default is the first installed
|
||||
// engine of the tag in gallery order, so the gallery picks the default by the
|
||||
// order of its entries; "" means none is installed.
|
||||
func pickEngine(l BackendLister, tag, named string) string {
|
||||
if named != "" {
|
||||
return named
|
||||
}
|
||||
if all := l.Backends(tag); len(all) > 0 {
|
||||
return all[0]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// Checkpoint is what the operations need of a saved checkpoint. The path is
|
||||
@@ -115,11 +136,8 @@ func fineTuneSpecs(b *Broker, ft FineTuner, backends BackendLister) []Spec {
|
||||
if a.LearningRate < 0 || a.NumEpochs < 0 || a.BatchSize < 0 || a.MaxSteps < 0 || a.AdapterRank < 0 || a.AdapterAlpha < 0 {
|
||||
return nil, fmt.Errorf("%w: numeric arguments must not be negative", ErrBadRequest)
|
||||
}
|
||||
backend := a.Backend
|
||||
if backend == "" {
|
||||
backend = "trl"
|
||||
}
|
||||
if err := requireEngine("fine-tuning", backend, backends.FineTune()); err != nil {
|
||||
backend := pickEngine(backends, TagFineTuning, a.Backend)
|
||||
if err := requireEngine("fine-tuning", backend, backends.Backends(TagFineTuning)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := ft.Start(ctx, userKey(c), schema.FineTuneJobRequest{
|
||||
@@ -198,11 +216,8 @@ func quantizeSpecs(q Quantizer, backends BackendLister) []Spec {
|
||||
if a.QuantizationType != "" && !quantType.MatchString(a.QuantizationType) {
|
||||
return nil, fmt.Errorf("%w: quantizationType is not a quantization name", ErrBadRequest)
|
||||
}
|
||||
backend := a.Backend
|
||||
if backend == "" {
|
||||
backend = "llama-cpp-quantization"
|
||||
}
|
||||
if err := requireEngine("quantization", backend, backends.Quantization()); err != nil {
|
||||
backend := pickEngine(backends, TagQuantization, a.Backend)
|
||||
if err := requireEngine("quantization", backend, backends.Backends(TagQuantization)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp, err := q.Start(ctx, userKey(c), schema.QuantizationJobRequest{
|
||||
@@ -319,6 +334,9 @@ var engineName = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]{0,63}$`)
|
||||
// the engine, in a short fixed sentence that the add-on can pass on to the
|
||||
// person, so a missing engine is not mistaken for a bad argument.
|
||||
func requireEngine(kind, backend string, installed []string) error {
|
||||
if backend == "" {
|
||||
return fmt.Errorf("%w: no %s engine is installed", ErrBadRequest, kind)
|
||||
}
|
||||
if !engineName.MatchString(backend) {
|
||||
return refusedField("backend")
|
||||
}
|
||||
|
||||
@@ -74,8 +74,15 @@ func (q *fakeQ) List(user string) []*schema.QuantizationJob {
|
||||
|
||||
type backends struct{}
|
||||
|
||||
func (backends) FineTune() []string { return []string{"trl"} }
|
||||
func (backends) Quantization() []string { return []string{"llama-cpp-quantization"} }
|
||||
func (backends) Backends(tag string) []string {
|
||||
switch tag {
|
||||
case TagFineTuning:
|
||||
return []string{"trl"}
|
||||
case TagQuantization:
|
||||
return []string{"llama-cpp-quantization"}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func call(spec Spec, mode, user, args string) (json.RawMessage, error) {
|
||||
return spec.Handler(context.Background(), Call{AddonID: "fine-tuning", OperationID: "op1", Actor: contract.Actor{UserID: user, Mode: mode}, Arguments: json.RawMessage(args)})
|
||||
@@ -87,6 +94,13 @@ func register(ft FineTuner, q Quantizer) map[string]Spec {
|
||||
return b.specs
|
||||
}
|
||||
|
||||
// registerWith is register over a given backend lister.
|
||||
func registerWith(ft FineTuner, q Quantizer, l BackendLister) map[string]Spec {
|
||||
b := New(NewInFlight(), func(string, string) bool { return true }, contract.DefaultLimits())
|
||||
Expect(RegisterEngineJobs(b, ft, q, l)).To(Succeed())
|
||||
return b.specs
|
||||
}
|
||||
|
||||
// registerWithout is register with a host that has no engine installed.
|
||||
func registerWithout(ft FineTuner, q Quantizer) map[string]Spec {
|
||||
b := New(NewInFlight(), func(string, string) bool { return true }, contract.DefaultLimits())
|
||||
@@ -96,8 +110,12 @@ func registerWithout(ft FineTuner, q Quantizer) map[string]Spec {
|
||||
|
||||
type noBackends struct{}
|
||||
|
||||
func (noBackends) FineTune() []string { return nil }
|
||||
func (noBackends) Quantization() []string { return nil }
|
||||
func (noBackends) Backends(string) []string { return nil }
|
||||
|
||||
// tagged is a lister over a fixed map of engines per tag.
|
||||
type tagged struct{ engines map[string][]string }
|
||||
|
||||
func (t tagged) Backends(tag string) []string { return t.engines[tag] }
|
||||
|
||||
func specs(ft FineTuner) map[string]Spec {
|
||||
return register(ft, nil)
|
||||
@@ -162,7 +180,7 @@ var _ = Describe("Fine-tuning and quantization job operations", func() {
|
||||
s := registerWithout(&fakeFT{}, nil)
|
||||
_, err := call(s["finetune.start"], "named-user", "u1", `{"model":"org/base","datasetSource":"org/data"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
Expect(err.Error()).To(ContainSubstring("fine-tuning engine trl is not installed"))
|
||||
Expect(err.Error()).To(ContainSubstring("no fine-tuning engine is installed"))
|
||||
_, err = call(s["finetune.start"], "named-user", "u1", `{"model":"org/base","datasetSource":"org/data","backend":"../x"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
Expect(err.Error()).NotTo(ContainSubstring(".."))
|
||||
@@ -261,7 +279,7 @@ var _ = Describe("Fine-tuning and quantization job operations", func() {
|
||||
s := registerWithout(nil, &fakeQ{})
|
||||
_, err := call(s["quantize.start"], "named-user", "u1", `{"model":"org/base"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
Expect(err.Error()).To(ContainSubstring("quantization engine llama-cpp-quantization is not installed"))
|
||||
Expect(err.Error()).To(ContainSubstring("no quantization engine is installed"))
|
||||
})
|
||||
|
||||
It("start refuses a field that is not in the tool schema", func() {
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
package hostops
|
||||
|
||||
import (
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// The engine of a job comes from the gallery tag, not from a name in the host.
|
||||
var _ = Describe("engine selection by gallery tag", func() {
|
||||
start := func(l BackendLister, ft *fakeFT, q *fakeQ, op, args string) error {
|
||||
b := registerWith(ft, q, l)
|
||||
_, err := call(b[op], "named-user", "u1", args)
|
||||
return err
|
||||
}
|
||||
|
||||
It("uses the first engine of the tag in gallery order when no backend is named", func() {
|
||||
ft, q := &fakeFT{}, &fakeQ{}
|
||||
l := tagged{engines: map[string][]string{TagFineTuning: {"beta", "alpha"}, TagQuantization: {"delta", "gamma"}}}
|
||||
Expect(start(l, ft, q, "finetune.start", `{"model":"org/base","datasetSource":"org/data"}`)).To(Succeed())
|
||||
Expect(start(l, ft, q, "quantize.start", `{"model":"org/base"}`)).To(Succeed())
|
||||
Expect(ft.started[0].Backend).To(Equal("beta"))
|
||||
Expect(q.started[0].Backend).To(Equal("delta"))
|
||||
})
|
||||
|
||||
It("accepts a named engine that carries the tag, and refuses one that does not", func() {
|
||||
ft := &fakeFT{}
|
||||
l := tagged{engines: map[string][]string{TagFineTuning: {"alpha", "beta"}}}
|
||||
Expect(start(l, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data","backend":"beta"}`)).To(Succeed())
|
||||
Expect(ft.started[0].Backend).To(Equal("beta"))
|
||||
err := start(l, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data","backend":"trl"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
Expect(err.Error()).To(ContainSubstring("fine-tuning engine trl is not installed"))
|
||||
})
|
||||
|
||||
It("refuses a start with no backend when none is installed", func() {
|
||||
ft := &fakeFT{}
|
||||
err := start(tagged{}, ft, &fakeQ{}, "finetune.start", `{"model":"org/base","datasetSource":"org/data"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
Expect(err.Error()).To(ContainSubstring("no fine-tuning engine is installed"))
|
||||
Expect(ft.started).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("does not mix the families", func() {
|
||||
l := tagged{engines: map[string][]string{TagFineTuning: {"alpha"}, TagQuantization: {"gamma"}}}
|
||||
err := start(l, &fakeFT{}, &fakeQ{}, "quantize.start", `{"model":"org/base","backend":"alpha"}`)
|
||||
Expect(err).To(MatchError(ErrBadRequest))
|
||||
})
|
||||
})
|
||||
@@ -432,6 +432,11 @@ The generic binary selects no policy automatically.
|
||||
|
||||
Fine-tuning and Quantization include operation names passed through mutation and undo wrappers.
|
||||
They do not request the host's unused progress operations.
|
||||
|
||||
The `finetune.start` and `quantize.start` operations take an optional `backend`.
|
||||
The host accepts only an installed backend whose gallery entry carries the tag of the operation (`fine-tuning` or `quantization`).
|
||||
When the call names no backend, the host uses the first installed backend of that tag in gallery order.
|
||||
A new engine needs only a gallery entry with the tag. With no engine installed, the call fails with a `400` that says so.
|
||||
Telegram's historical `connector.` deployment grant does not imply a hostclient call or manufacture a manifest request.
|
||||
|
||||
Sandbox's proxy uses the optional egress operations when configured connections need policy decisions.
|
||||
|
||||
Reference in new issue
Block a user