Skip to content

Commit fba8c9c

Browse files
localai-botmudler
andauthored
fix(distributed): track in-flight for non-LLM inference methods (VAD, diarize, voice, ...) (#10238)
fix(distributed): track in-flight for non-LLM inference methods InFlightTrackingClient only wrapped a subset of the grpc.Backend inference methods (Predict, Embeddings, TTS, AudioTranscription, Detect, Rerank, ...). Methods like VAD were left as embedded passthrough, so track() never ran for them. In distributed mode every model is loaded with in_flight=1 as a reservation; that reservation is only released by the OnFirstComplete callback, which fires after the first *tracked* inference call completes. A VAD-only model (e.g. silero-vad) never calls a tracked method, so the reservation is never released and in-flight stays pinned at 1 forever - which also blocks the router's idle-eviction logic. Wrap the remaining unary inference methods (VAD, Diarize, Face*, Voice*, TokenClassify, Score, AudioEncode, AudioDecode, AudioTransform) with the same track()/reconcile() pattern. The three bidi-stream constructors (AudioTransformStream, AudioToAudioStream, Forward) are deliberately left as passthrough - their inference spans the stream lifetime, not the constructor call, so track() there would fire onFirstComplete before any data flows. Assisted-by: Claude:claude-opus-4-8 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 6b2badb commit fba8c9c

2 files changed

Lines changed: 178 additions & 0 deletions

File tree

core/services/nodes/inflight.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,3 +157,82 @@ func (c *InFlightTrackingClient) Rerank(ctx context.Context, in *pb.RerankReques
157157
res, err := c.Backend.Rerank(ctx, in, opts...)
158158
return res, c.reconcile(err)
159159
}
160+
161+
func (c *InFlightTrackingClient) VAD(ctx context.Context, in *pb.VADRequest, opts ...ggrpc.CallOption) (*pb.VADResponse, error) {
162+
defer c.track(ctx)()
163+
res, err := c.Backend.VAD(ctx, in, opts...)
164+
return res, c.reconcile(err)
165+
}
166+
167+
func (c *InFlightTrackingClient) Diarize(ctx context.Context, in *pb.DiarizeRequest, opts ...ggrpc.CallOption) (*pb.DiarizeResponse, error) {
168+
defer c.track(ctx)()
169+
res, err := c.Backend.Diarize(ctx, in, opts...)
170+
return res, c.reconcile(err)
171+
}
172+
173+
func (c *InFlightTrackingClient) FaceVerify(ctx context.Context, in *pb.FaceVerifyRequest, opts ...ggrpc.CallOption) (*pb.FaceVerifyResponse, error) {
174+
defer c.track(ctx)()
175+
res, err := c.Backend.FaceVerify(ctx, in, opts...)
176+
return res, c.reconcile(err)
177+
}
178+
179+
func (c *InFlightTrackingClient) FaceAnalyze(ctx context.Context, in *pb.FaceAnalyzeRequest, opts ...ggrpc.CallOption) (*pb.FaceAnalyzeResponse, error) {
180+
defer c.track(ctx)()
181+
res, err := c.Backend.FaceAnalyze(ctx, in, opts...)
182+
return res, c.reconcile(err)
183+
}
184+
185+
func (c *InFlightTrackingClient) VoiceVerify(ctx context.Context, in *pb.VoiceVerifyRequest, opts ...ggrpc.CallOption) (*pb.VoiceVerifyResponse, error) {
186+
defer c.track(ctx)()
187+
res, err := c.Backend.VoiceVerify(ctx, in, opts...)
188+
return res, c.reconcile(err)
189+
}
190+
191+
func (c *InFlightTrackingClient) VoiceAnalyze(ctx context.Context, in *pb.VoiceAnalyzeRequest, opts ...ggrpc.CallOption) (*pb.VoiceAnalyzeResponse, error) {
192+
defer c.track(ctx)()
193+
res, err := c.Backend.VoiceAnalyze(ctx, in, opts...)
194+
return res, c.reconcile(err)
195+
}
196+
197+
func (c *InFlightTrackingClient) VoiceEmbed(ctx context.Context, in *pb.VoiceEmbedRequest, opts ...ggrpc.CallOption) (*pb.VoiceEmbedResponse, error) {
198+
defer c.track(ctx)()
199+
res, err := c.Backend.VoiceEmbed(ctx, in, opts...)
200+
return res, c.reconcile(err)
201+
}
202+
203+
func (c *InFlightTrackingClient) TokenClassify(ctx context.Context, in *pb.TokenClassifyRequest, opts ...ggrpc.CallOption) (*pb.TokenClassifyResponse, error) {
204+
defer c.track(ctx)()
205+
res, err := c.Backend.TokenClassify(ctx, in, opts...)
206+
return res, c.reconcile(err)
207+
}
208+
209+
func (c *InFlightTrackingClient) Score(ctx context.Context, in *pb.ScoreRequest, opts ...ggrpc.CallOption) (*pb.ScoreResponse, error) {
210+
defer c.track(ctx)()
211+
res, err := c.Backend.Score(ctx, in, opts...)
212+
return res, c.reconcile(err)
213+
}
214+
215+
func (c *InFlightTrackingClient) AudioEncode(ctx context.Context, in *pb.AudioEncodeRequest, opts ...ggrpc.CallOption) (*pb.AudioEncodeResult, error) {
216+
defer c.track(ctx)()
217+
res, err := c.Backend.AudioEncode(ctx, in, opts...)
218+
return res, c.reconcile(err)
219+
}
220+
221+
func (c *InFlightTrackingClient) AudioDecode(ctx context.Context, in *pb.AudioDecodeRequest, opts ...ggrpc.CallOption) (*pb.AudioDecodeResult, error) {
222+
defer c.track(ctx)()
223+
res, err := c.Backend.AudioDecode(ctx, in, opts...)
224+
return res, c.reconcile(err)
225+
}
226+
227+
func (c *InFlightTrackingClient) AudioTransform(ctx context.Context, in *pb.AudioTransformRequest, opts ...ggrpc.CallOption) (*pb.AudioTransformResult, error) {
228+
defer c.track(ctx)()
229+
res, err := c.Backend.AudioTransform(ctx, in, opts...)
230+
return res, c.reconcile(err)
231+
}
232+
233+
// AudioTransformStream, AudioToAudioStream and Forward are deliberately left as
234+
// embedded passthrough: they return a stream client and the inference spans the
235+
// stream's lifetime, not the constructor call. Wrapping the constructor with
236+
// track() would increment and immediately decrement (and fire onFirstComplete)
237+
// before any audio flows. Tracking those correctly needs the done() func tied to
238+
// stream close, which the current Backend interface doesn't surface here.

core/services/nodes/inflight_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,105 @@ var _ = Describe("InFlightTrackingClient", func() {
304304
})
305305
})
306306

307+
Describe("non-LLM inference methods track in-flight", func() {
308+
// silero-vad and friends only ever expose a single non-Predict method.
309+
// If that method isn't wrapped, the load-time reservation released by
310+
// onFirstComplete never fires and in-flight is stuck at 1 forever.
311+
assertTracked := func(call func() error) {
312+
var firstFired int
313+
client.OnFirstComplete(func() { firstFired++ })
314+
err := call()
315+
Expect(err).ToNot(HaveOccurred())
316+
Expect(tracker.increments).To(Equal(1), "method must increment in-flight")
317+
Expect(tracker.decrements).To(Equal(1), "method must decrement in-flight")
318+
Expect(firstFired).To(Equal(1), "method must release the load-time reservation")
319+
}
320+
321+
It("VAD", func() {
322+
assertTracked(func() error {
323+
_, err := client.VAD(context.Background(), &pb.VADRequest{})
324+
return err
325+
})
326+
})
327+
328+
It("Diarize", func() {
329+
assertTracked(func() error {
330+
_, err := client.Diarize(context.Background(), &pb.DiarizeRequest{})
331+
return err
332+
})
333+
})
334+
335+
It("VoiceVerify", func() {
336+
assertTracked(func() error {
337+
_, err := client.VoiceVerify(context.Background(), &pb.VoiceVerifyRequest{})
338+
return err
339+
})
340+
})
341+
342+
It("VoiceAnalyze", func() {
343+
assertTracked(func() error {
344+
_, err := client.VoiceAnalyze(context.Background(), &pb.VoiceAnalyzeRequest{})
345+
return err
346+
})
347+
})
348+
349+
It("VoiceEmbed", func() {
350+
assertTracked(func() error {
351+
_, err := client.VoiceEmbed(context.Background(), &pb.VoiceEmbedRequest{})
352+
return err
353+
})
354+
})
355+
356+
It("FaceVerify", func() {
357+
assertTracked(func() error {
358+
_, err := client.FaceVerify(context.Background(), &pb.FaceVerifyRequest{})
359+
return err
360+
})
361+
})
362+
363+
It("FaceAnalyze", func() {
364+
assertTracked(func() error {
365+
_, err := client.FaceAnalyze(context.Background(), &pb.FaceAnalyzeRequest{})
366+
return err
367+
})
368+
})
369+
370+
It("TokenClassify", func() {
371+
assertTracked(func() error {
372+
_, err := client.TokenClassify(context.Background(), &pb.TokenClassifyRequest{})
373+
return err
374+
})
375+
})
376+
377+
It("Score", func() {
378+
assertTracked(func() error {
379+
_, err := client.Score(context.Background(), &pb.ScoreRequest{})
380+
return err
381+
})
382+
})
383+
384+
It("AudioEncode", func() {
385+
assertTracked(func() error {
386+
_, err := client.AudioEncode(context.Background(), &pb.AudioEncodeRequest{})
387+
return err
388+
})
389+
})
390+
391+
It("AudioDecode", func() {
392+
assertTracked(func() error {
393+
_, err := client.AudioDecode(context.Background(), &pb.AudioDecodeRequest{})
394+
return err
395+
})
396+
})
397+
398+
It("AudioTransform", func() {
399+
assertTracked(func() error {
400+
_, err := client.AudioTransform(context.Background(), &pb.AudioTransformRequest{})
401+
return err
402+
})
403+
})
404+
})
405+
307406
Describe("stale model reload (self-heal)", func() {
308407
It("removes the replica when the backend reports the model is not loaded", func() {
309408
backend.predictErr = fmt.Errorf("parakeet-cpp: model not loaded")

0 commit comments

Comments
 (0)