mirror of
https://github.com/mudler/LocalAI.git
synced 2026-10-30 18:19:33 -04:00
* fix(vulkan): preserve host ICD discovery for packaged backends Add bundled Mesa manifests through VK_ADD_DRIVER_FILES instead of replacing the system driver list. Merge inherited and model-specific additive paths while preserving explicit operator overrides, with regression coverage. Assisted-by: Codex:gpt-5 golangci-lint Assisted-by: Codex:gpt-5.6-sol Signed-off-by: Richard Palethorpe <io@richiejp.com> * feat(3d): add Kimodo CPU and Vulkan animation backend Introduce a distinct animation capability and model-described 3D operations, with a typed /3d/animate API, RPC transport, distributed media staging, permissions, and tracing. Add a persistent kimodo.cpp adapter, skeleton GLB export, CPU/Vulkan packages, model and backend galleries, importer support, CI builds, and documentation. Adapt Studio inputs to each model and provide real-time skeleton playback, seeking, and history. Cover backend validation, packaging, API behavior, importer inventories, distributed staging, and Studio workflows. Validate real-model CPU/Vulkan generation and deploy the integration to the local QA instance. Assisted-by: Codex:gpt-5 golangci-lint Assisted-by: Codex:gpt-5.6-sol Signed-off-by: Richard Palethorpe <io@richiejp.com> * feat(kimodocpp): adopt monolithic encoders and resident inference Update upstream for resident weights, packed execution paths, and cached motion graphs. Default to all 32 text layers while retaining configurable streaming and legacy bundle support. Use monolithic Q8_0 encoders by default and offer all six published quantizations through the gallery and importer. Refresh pinned hashes, tests, and documentation; remove the obsolete thread patch and ensure cached source checkouts follow the upstream pin. Validated CPU and Vulkan generation, lower-bit streaming, gallery/importer suites, packaging, lint, and cold/warm Studio generation on localai-dev. Assisted-by: Codex:gpt-5 golangci-lint Assisted-by: Codex:gpt-5.6-sol Signed-off-by: Richard Palethorpe <io@richiejp.com> --------- Signed-off-by: Richard Palethorpe <io@richiejp.com>
297 lines
13 KiB
Go
297 lines
13 KiB
Go
package nodes
|
|
|
|
import (
|
|
"context"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/mudler/LocalAI/pkg/grpc"
|
|
"github.com/mudler/LocalAI/pkg/grpc/grpcerrors"
|
|
pb "github.com/mudler/LocalAI/pkg/grpc/proto"
|
|
"github.com/mudler/xlog"
|
|
ggrpc "google.golang.org/grpc"
|
|
)
|
|
|
|
// InFlightTrackingClient wraps a grpc.Backend and tracks active inference requests
|
|
// in the NodeRegistry. This allows the router's eviction logic to know which models
|
|
// are actively serving and should not be unloaded.
|
|
//
|
|
// Per-replica: a single tracker instance is bound to (nodeID, modelName, replicaIndex).
|
|
// The router constructs one tracker per Route() result, so each in-flight tick lands
|
|
// on the correct row even when multiple replicas of the same model live on the same node.
|
|
//
|
|
// Embedding only grpc.ControlBackend (not the whole grpc.Backend) is what makes
|
|
// the in-flight accounting safe by construction: the control-plane methods pass
|
|
// through untracked, while every grpc.InferenceBackend method must be declared
|
|
// explicitly below to satisfy grpc.Backend. Adding an inference method to the
|
|
// interface therefore breaks this file's build (see the var assertion below)
|
|
// until it is wrapped with track() - so a new inference path can't be added
|
|
// without an in-flight accounting decision.
|
|
type InFlightTrackingClient struct {
|
|
grpc.ControlBackend // passthrough for control-plane / streaming-constructor methods
|
|
inner grpc.InferenceBackend // tracked inference methods delegate here
|
|
registry InFlightTracker
|
|
nodeID string
|
|
modelName string
|
|
replicaIndex int
|
|
|
|
firstOnce sync.Once // guards onFirstComplete
|
|
onFirstComplete func() // called once after the first tracked inference call completes
|
|
}
|
|
|
|
// Compile-time contract: *InFlightTrackingClient must implement the FULL backend
|
|
// surface. Because it embeds only ControlBackend, this fails to compile if any
|
|
// InferenceBackend method is left unwrapped.
|
|
var _ grpc.Backend = (*InFlightTrackingClient)(nil)
|
|
|
|
// NewInFlightTrackingClient wraps a gRPC backend client with in-flight tracking.
|
|
func NewInFlightTrackingClient(inner grpc.Backend, registry InFlightTracker, nodeID, modelName string, replicaIndex int) *InFlightTrackingClient {
|
|
return &InFlightTrackingClient{
|
|
ControlBackend: inner,
|
|
inner: inner,
|
|
registry: registry,
|
|
nodeID: nodeID,
|
|
modelName: modelName,
|
|
replicaIndex: replicaIndex,
|
|
}
|
|
}
|
|
|
|
// OnFirstComplete registers a callback that fires once after the first tracked
|
|
// inference call completes. This is used to release the initial in-flight
|
|
// reservation (set during model load) after the triggering request finishes,
|
|
// so that in-flight returns to 0 when the model is idle.
|
|
func (c *InFlightTrackingClient) OnFirstComplete(fn func()) {
|
|
c.onFirstComplete = fn
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) track(ctx context.Context) func() {
|
|
if err := c.registry.IncrementInFlight(ctx, c.nodeID, c.modelName, c.replicaIndex); err != nil {
|
|
xlog.Warn("Failed to increment in-flight counter", "node", c.nodeID, "model", c.modelName, "replica", c.replicaIndex, "error", err)
|
|
return func() {}
|
|
}
|
|
return func() {
|
|
decCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
c.registry.DecrementInFlight(decCtx, c.nodeID, c.modelName, c.replicaIndex)
|
|
// Release the initial reservation after the first inference call completes
|
|
if c.onFirstComplete != nil {
|
|
c.firstOnce.Do(c.onFirstComplete)
|
|
}
|
|
}
|
|
}
|
|
|
|
// reconcile self-heals stale routing: when a backend reports that the model is
|
|
// no longer loaded (the process survived but the model was evicted, while the
|
|
// registry still lists it as loaded), it drops the replica row so the next
|
|
// request triggers a fresh load instead of routing back here. Without this the
|
|
// model stays unreachable until the controller restarts. The original error is
|
|
// returned unchanged.
|
|
//
|
|
// It also drops the row when the backend reports that it has a DIFFERENT model
|
|
// loaded. That happens when a worker recycles a stopped backend's gRPC port for
|
|
// another model's backend: the cached row still names that address, and a
|
|
// liveness-only probe cannot tell the stale row from a valid one, so the wrong
|
|
// backend answers (#10952). The two conditions differ in cause but not in cure.
|
|
func (c *InFlightTrackingClient) reconcile(err error) error {
|
|
mismatch := grpcerrors.IsModelMismatch(err)
|
|
if !grpcerrors.IsModelNotLoaded(err) && !mismatch {
|
|
return err
|
|
}
|
|
reason := "model not loaded"
|
|
if mismatch {
|
|
reason = "wrong model loaded"
|
|
}
|
|
rmCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
if rmErr := c.registry.RemoveNodeModel(rmCtx, c.nodeID, c.modelName, c.replicaIndex); rmErr != nil {
|
|
xlog.Warn("Failed to drop stale replica",
|
|
"node", c.nodeID, "model", c.modelName, "replica", c.replicaIndex, "reason", reason, "error", rmErr)
|
|
} else {
|
|
xlog.Warn("Dropped stale replica so the next request reloads",
|
|
"node", c.nodeID, "model", c.modelName, "replica", c.replicaIndex, "reason", reason)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// --- Tracked inference methods ---
|
|
|
|
func (c *InFlightTrackingClient) Predict(ctx context.Context, in *pb.PredictOptions, opts ...ggrpc.CallOption) (*pb.Reply, error) {
|
|
defer c.track(ctx)()
|
|
reply, err := c.inner.Predict(ctx, in, opts...)
|
|
return reply, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) PredictStream(ctx context.Context, in *pb.PredictOptions, f func(reply *pb.Reply), opts ...ggrpc.CallOption) error {
|
|
defer c.track(ctx)()
|
|
return c.reconcile(c.inner.PredictStream(ctx, in, f, opts...))
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Embeddings(ctx context.Context, in *pb.PredictOptions, opts ...ggrpc.CallOption) (*pb.EmbeddingResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Embeddings(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) GenerateImage(ctx context.Context, in *pb.GenerateImageRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.GenerateImage(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) UpscaleImage(ctx context.Context, in *pb.UpscaleImageRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.UpscaleImage(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) GenerateVideo(ctx context.Context, in *pb.GenerateVideoRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.GenerateVideo(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Animate3D(ctx context.Context, in *pb.Animate3DRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Animate3D(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Generate3D(ctx context.Context, in *pb.Generate3DRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Generate3D(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) TTS(ctx context.Context, in *pb.TTSRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.TTS(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) TTSStream(ctx context.Context, in *pb.TTSRequest, f func(reply *pb.Reply), opts ...ggrpc.CallOption) error {
|
|
defer c.track(ctx)()
|
|
return c.reconcile(c.inner.TTSStream(ctx, in, f, opts...))
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) SoundGeneration(ctx context.Context, in *pb.SoundGenerationRequest, opts ...ggrpc.CallOption) (*pb.Result, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.SoundGeneration(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) AudioTranscription(ctx context.Context, in *pb.TranscriptRequest, opts ...ggrpc.CallOption) (*pb.TranscriptResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.AudioTranscription(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) AudioTranscriptionStream(ctx context.Context, in *pb.TranscriptRequest, f func(chunk *pb.TranscriptStreamResponse), opts ...ggrpc.CallOption) error {
|
|
defer c.track(ctx)()
|
|
return c.reconcile(c.inner.AudioTranscriptionStream(ctx, in, f, opts...))
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Detect(ctx context.Context, in *pb.DetectOptions, opts ...ggrpc.CallOption) (*pb.DetectResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Detect(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Depth(ctx context.Context, in *pb.DepthRequest, opts ...ggrpc.CallOption) (*pb.DepthResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Depth(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Rerank(ctx context.Context, in *pb.RerankRequest, opts ...ggrpc.CallOption) (*pb.RerankResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Rerank(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) VAD(ctx context.Context, in *pb.VADRequest, opts ...ggrpc.CallOption) (*pb.VADResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.VAD(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Diarize(ctx context.Context, in *pb.DiarizeRequest, opts ...ggrpc.CallOption) (*pb.DiarizeResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Diarize(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) FaceVerify(ctx context.Context, in *pb.FaceVerifyRequest, opts ...ggrpc.CallOption) (*pb.FaceVerifyResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.FaceVerify(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) FaceAnalyze(ctx context.Context, in *pb.FaceAnalyzeRequest, opts ...ggrpc.CallOption) (*pb.FaceAnalyzeResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.FaceAnalyze(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) VoiceVerify(ctx context.Context, in *pb.VoiceVerifyRequest, opts ...ggrpc.CallOption) (*pb.VoiceVerifyResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.VoiceVerify(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) VoiceAnalyze(ctx context.Context, in *pb.VoiceAnalyzeRequest, opts ...ggrpc.CallOption) (*pb.VoiceAnalyzeResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.VoiceAnalyze(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) VoiceEmbed(ctx context.Context, in *pb.VoiceEmbedRequest, opts ...ggrpc.CallOption) (*pb.VoiceEmbedResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.VoiceEmbed(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) TokenClassify(ctx context.Context, in *pb.TokenClassifyRequest, opts ...ggrpc.CallOption) (*pb.TokenClassifyResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.TokenClassify(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) Score(ctx context.Context, in *pb.ScoreRequest, opts ...ggrpc.CallOption) (*pb.ScoreResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.Score(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) SoundDetection(ctx context.Context, in *pb.SoundDetectionRequest, opts ...ggrpc.CallOption) (*pb.SoundDetectionResponse, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.SoundDetection(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) AudioEncode(ctx context.Context, in *pb.AudioEncodeRequest, opts ...ggrpc.CallOption) (*pb.AudioEncodeResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.AudioEncode(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) AudioDecode(ctx context.Context, in *pb.AudioDecodeRequest, opts ...ggrpc.CallOption) (*pb.AudioDecodeResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.AudioDecode(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
func (c *InFlightTrackingClient) AudioTransform(ctx context.Context, in *pb.AudioTransformRequest, opts ...ggrpc.CallOption) (*pb.AudioTransformResult, error) {
|
|
defer c.track(ctx)()
|
|
res, err := c.inner.AudioTransform(ctx, in, opts...)
|
|
return res, c.reconcile(err)
|
|
}
|
|
|
|
// AudioTransformStream, AudioToAudioStream and Forward live in grpc.ControlBackend
|
|
// and are passed through via the embedded field, NOT tracked: they return a stream
|
|
// client and the inference spans the stream's lifetime, not the constructor call.
|
|
// Wrapping the constructor with track() would increment and immediately decrement
|
|
// (and fire onFirstComplete) before any audio flows. Tracking those correctly needs
|
|
// the done() func tied to stream close, which the Backend interface doesn't surface
|
|
// here. If they ever need tracking, move them to grpc.InferenceBackend - the build
|
|
// will then force an explicit wrapper here.
|