|
9 | 9 | "fmt" |
10 | 10 | "strings" |
11 | 11 | "sync" |
| 12 | + "time" |
12 | 13 |
|
13 | 14 | "github.com/mudler/LocalAI/core/application" |
14 | 15 | "github.com/mudler/LocalAI/core/backend" |
@@ -57,6 +58,9 @@ type wrappedModel struct { |
57 | 58 | classifier *router.ScoreClassifier |
58 | 59 | classifierKey string |
59 | 60 | classifierWarn sync.Once |
| 61 | + // prewarmKey remembers the option set whose scoring prompt was last |
| 62 | + // prewarmed, so re-pushing an unchanged config doesn't re-score. |
| 63 | + prewarmKey string |
60 | 64 |
|
61 | 65 | // Routing — populated by newModel when the application wires routing |
62 | 66 | // deps in. nil-safe: with classifierRegistry == nil the per-turn |
@@ -114,6 +118,9 @@ func (m *transcriptOnlyModel) FillToolArguments(ctx context.Context, messages sc |
114 | 118 | return "", nil, fmt.Errorf("classifier mode not supported in transcript-only mode") |
115 | 119 | } |
116 | 120 |
|
| 121 | +func (m *transcriptOnlyModel) PrewarmClassifier(ctx context.Context, options []types.ClassifierOption, normalization string) { |
| 122 | +} |
| 123 | + |
117 | 124 | func (m *transcriptOnlyModel) TTS(ctx context.Context, text, voice, language string) (string, *proto.Result, error) { |
118 | 125 | return "", nil, fmt.Errorf("TTS not supported in transcript-only mode") |
119 | 126 | } |
@@ -488,6 +495,44 @@ func (m *wrappedModel) classifierFor(options []types.ClassifierOption, normaliza |
488 | 495 | return m.classifier, nil |
489 | 496 | } |
490 | 497 |
|
| 498 | +// PrewarmClassifier primes the scoring backend's prompt cache for a newly |
| 499 | +// registered option list so the first real turns don't pay the prefill. |
| 500 | +// Two throwaway scores with distinct probes run back to back: the first |
| 501 | +// prefills the new option-list prompt, and the second — diverging exactly |
| 502 | +// where the per-turn probe text starts — leaves the backend a rewind point |
| 503 | +// (a KV checkpoint on hybrid/recurrent models, which cannot rewind |
| 504 | +// arbitrarily) at the stable-prefix boundary every subsequent turn reuses. |
| 505 | +// Best-effort: errors are logged, never surfaced. |
| 506 | +func (m *wrappedModel) PrewarmClassifier(ctx context.Context, options []types.ClassifierOption, normalization string) { |
| 507 | + classifier, err := m.classifierFor(options, normalization) |
| 508 | + if err != nil { |
| 509 | + xlog.Debug("realtime classifier: prewarm skipped", "error", err) |
| 510 | + return |
| 511 | + } |
| 512 | + m.classifierMu.Lock() |
| 513 | + key := m.classifierKey |
| 514 | + done := m.prewarmKey == key |
| 515 | + m.classifierMu.Unlock() |
| 516 | + if done { |
| 517 | + return |
| 518 | + } |
| 519 | + start := time.Now() |
| 520 | + for _, probe := range []string{"warmup", "standing by"} { |
| 521 | + if ctx.Err() != nil { |
| 522 | + return |
| 523 | + } |
| 524 | + if _, err := classifier.Classify(ctx, router.Probe{Prompt: probe, Messages: []string{probe}}); err != nil { |
| 525 | + xlog.Warn("realtime classifier: prewarm scoring failed", "error", err) |
| 526 | + return |
| 527 | + } |
| 528 | + } |
| 529 | + m.classifierMu.Lock() |
| 530 | + m.prewarmKey = key |
| 531 | + m.classifierMu.Unlock() |
| 532 | + xlog.Debug("realtime classifier: prewarmed scoring prompt cache", |
| 533 | + "options", len(options), "latency_ms", time.Since(start).Milliseconds()) |
| 534 | +} |
| 535 | + |
491 | 536 | func (m *wrappedModel) ClassifyTurn(ctx context.Context, messages schema.Messages, options []types.ClassifierOption, normalization string) ([]router.LabelScore, error) { |
492 | 537 | classifier, err := m.classifierFor(options, normalization) |
493 | 538 | if err != nil { |
|
0 commit comments