213 lines
7.0 KiB
Go
213 lines
7.0 KiB
Go
package nodes
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"sync"
|
|
|
|
. "github.com/onsi/ginkgo/v2"
|
|
. "github.com/onsi/gomega"
|
|
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
)
|
|
|
|
// --- fakeModelRouterForSmartRouter implements ModelRouter ---
|
|
|
|
type fakeModelRouterForSmartRouter struct {
|
|
mu sync.Mutex
|
|
node *BackendNode
|
|
nodeModel *NodeModel
|
|
findErr error
|
|
decrementCalled map[string]int // "nodeID:model" -> count
|
|
}
|
|
|
|
func newFakeModelRouterForSmartRouter() *fakeModelRouterForSmartRouter {
|
|
return &fakeModelRouterForSmartRouter{
|
|
decrementCalled: make(map[string]int),
|
|
}
|
|
}
|
|
|
|
func (f *fakeModelRouterForSmartRouter) FindAndLockNodeWithModel(_ context.Context, _ string, _ []string, _ *RoutePreference) (*BackendNode, *NodeModel, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
return f.node, f.nodeModel, f.findErr
|
|
}
|
|
|
|
func (f *fakeModelRouterForSmartRouter) DecrementInFlight(_ context.Context, nodeID, modelName string, _ int) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.decrementCalled[nodeID+":"+modelName]++
|
|
return nil
|
|
}
|
|
|
|
func (f *fakeModelRouterForSmartRouter) IncrementInFlight(_ context.Context, _, _ string, _ int) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) RemoveNodeModel(_ context.Context, _, _ string, _ int) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) RemoveAllNodeModelReplicas(_ context.Context, _, _ string) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) TouchNodeModel(_ context.Context, _, _ string, _ int) {}
|
|
func (f *fakeModelRouterForSmartRouter) SetNodeModel(_ context.Context, _, _ string, _ int, _, _ string, _ int) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) SetNodeModelLoadInfo(_ context.Context, _, _ string, _ int, _ string, _ []byte) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) UpsertModelLoadInfo(_ context.Context, _, _ string, _ []byte) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) GetModelLoadInfo(_ context.Context, _ string) (string, []byte, error) {
|
|
return "", nil, fmt.Errorf("not found")
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) NextFreeReplicaIndex(_ context.Context, _, _ string, _ int) (int, error) {
|
|
return 0, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) CountReplicasOnNode(_ context.Context, _, _ string) (int, error) {
|
|
return 0, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindNodeWithVRAM(_ context.Context, _ uint64) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindIdleNode(_ context.Context) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindLeastLoadedNode(_ context.Context) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindGlobalLRUModelWithZeroInFlight(_ context.Context) (*NodeModel, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindLRUModel(_ context.Context, _ string) (*NodeModel, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) Get(_ context.Context, nodeID string) (*BackendNode, error) {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
if f.node != nil && f.node.ID == nodeID {
|
|
return f.node, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) GetModelScheduling(_ context.Context, _ string) (*ModelSchedulingConfig, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindNodesBySelector(_ context.Context, _ map[string]string) ([]BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindNodesWithFreeSlot(_ context.Context, _ string, _ []string) ([]BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) ReserveVRAM(_ context.Context, _ string, _ uint64) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) ReleaseVRAM(_ context.Context, _ string, _ uint64) error {
|
|
return nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindNodeWithVRAMFromSet(_ context.Context, _ uint64, _ []string) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindIdleNodeFromSet(_ context.Context, _ []string) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindLeastLoadedNodeFromSet(_ context.Context, _ []string) (*BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) GetNodeLabels(_ context.Context, _ string) ([]NodeLabel, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) FindNodesWithModel(_ context.Context, _ string) ([]BackendNode, error) {
|
|
return nil, nil
|
|
}
|
|
func (f *fakeModelRouterForSmartRouter) LoadedReplicaStats(_ context.Context, _ string, _ []string) ([]ReplicaCandidate, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
// Compile-time check
|
|
var _ ModelRouter = (*fakeModelRouterForSmartRouter)(nil)
|
|
|
|
var _ = Describe("ModelRouterAdapter", func() {
|
|
Describe("ReleaseModel", func() {
|
|
It("calls stored release function", func() {
|
|
adapter := &ModelRouterAdapter{
|
|
release: make(map[string]func()),
|
|
}
|
|
|
|
released := false
|
|
adapter.release["my-model"] = func() { released = true }
|
|
|
|
adapter.ReleaseModel("my-model")
|
|
|
|
Expect(released).To(BeTrue())
|
|
// Release function should be removed after calling
|
|
Expect(adapter.release).NotTo(HaveKey("my-model"))
|
|
})
|
|
|
|
It("is no-op for unknown model", func() {
|
|
adapter := &ModelRouterAdapter{
|
|
release: make(map[string]func()),
|
|
}
|
|
|
|
// Should not panic
|
|
adapter.ReleaseModel("nonexistent-model")
|
|
Expect(adapter.release).To(BeEmpty())
|
|
})
|
|
})
|
|
|
|
Describe("Route", func() {
|
|
It("delegates to SmartRouter and stores release func", func() {
|
|
fakeNode := &BackendNode{
|
|
ID: "node-1",
|
|
Name: "test-node",
|
|
Address: "10.0.0.1:50051",
|
|
}
|
|
fakeNM := &NodeModel{
|
|
NodeID: "node-1",
|
|
ModelName: "test-model",
|
|
}
|
|
|
|
fakeReg := newFakeModelRouterForSmartRouter()
|
|
fakeReg.node = fakeNode
|
|
fakeReg.nodeModel = fakeNM
|
|
|
|
// The fake gRPC client that SmartRouter will use for health check
|
|
factory := newFakeBackendClientFactory()
|
|
factory.setClient("10.0.0.1:50051", &fakeBackendClient{healthy: true})
|
|
|
|
sr := NewSmartRouter(fakeReg, SmartRouterOptions{
|
|
ClientFactory: factory,
|
|
})
|
|
adapter := NewModelRouterAdapter(sr)
|
|
|
|
opts := &pb.ModelOptions{Model: "test-model"}
|
|
m, err := adapter.Route(context.Background(), "llama-cpp", "test-model", "test-model", "model.gguf", opts, false)
|
|
|
|
Expect(err).NotTo(HaveOccurred())
|
|
Expect(m).NotTo(BeNil())
|
|
Expect(m.ID).To(Equal("test-model"))
|
|
|
|
// Verify release function was stored
|
|
adapter.mu.Lock()
|
|
_, hasRelease := adapter.release["test-model"]
|
|
adapter.mu.Unlock()
|
|
Expect(hasRelease).To(BeTrue())
|
|
|
|
// The initial in-flight reservation is released via OnFirstComplete after
|
|
// the first inference call, not during ReleaseModel. ReleaseModel only
|
|
// closes the client.
|
|
fakeReg.mu.Lock()
|
|
countBeforeRelease := fakeReg.decrementCalled["node-1:test-model"]
|
|
fakeReg.mu.Unlock()
|
|
Expect(countBeforeRelease).To(Equal(0))
|
|
|
|
adapter.ReleaseModel("test-model")
|
|
fakeReg.mu.Lock()
|
|
countAfterRelease := fakeReg.decrementCalled["node-1:test-model"]
|
|
fakeReg.mu.Unlock()
|
|
Expect(countAfterRelease).To(Equal(0))
|
|
})
|
|
})
|
|
})
|