Skip to content

Commit 1aa9738

Browse files
localai-botmudler
andauthored
perf(gallery): warm variant descriptions alongside VRAM estimates (#11297)
Follow-up to #11288, which warmed the VRAM estimate caches at startup and left the variant picker paying its own way. Describing an entry's variants probes the weight files of every build it offers, so the first time a model is opened costs 1.2-1.9s against a cold cache. That is the same cost as an estimate wearing a different hat, and it lands in the same caches underneath, so it belongs in the same pass rather than in a second mechanism. The warm-up now describes variants for the entries it walks. Entries that declare none cost nothing: the call is gated on HasVariants rather than attempted and discarded. The host resolve env is derived once for the run, since it describes the machine rather than the entry. Failure handling matches the estimate half. An entry whose variants cannot be described is logged at debug and skipped, and the estimate for that same entry is unaffected, because neither half is allowed to fail the other. Measured against a live instance with 1,595 models, first ever call to /api/models/variants/:id after a cold boot: before 1.2-1.9s after 2ms The warm-up's own cost barely moves: 3m0s to 3m19s for 300 entries, of which 40 declared variants. It stays bounded by the same knobs, and LOCALAI_VRAM_WARM_LIMIT=0 still turns the whole thing off. Assisted-by: Claude:claude-opus-5 [Claude Code] Signed-off-by: Ettore Di Giacinto <mudler@localai.io> Co-authored-by: Ettore Di Giacinto <mudler@localai.io>
1 parent 74b7ea2 commit 1aa9738

3 files changed

Lines changed: 61 additions & 24 deletions

File tree

core/gallery/estimate_warm.go

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,17 @@ var DefaultEstimateWarmConfig = EstimateWarmConfig{
8080
Contexts: []uint32{8192, 16384, 32768, 65536, 131072, 262144},
8181
}
8282

83-
// WarmEstimateCache fills the VRAM estimate caches in the background.
83+
// WarmEstimateCache fills the gallery's derived caches in the background.
8484
//
85+
// Two things are warmed, and they are the same cost wearing different hats.
8586
// An estimate for an entry the server has never seen costs a network probe of
86-
// its weight files, seconds of it, and the UI asks for one per row. Doing that
87-
// work at startup rather than on the first click is the difference between a
88-
// gallery that reads instantly and one that spends ten seconds filling in its
89-
// own sizes while somebody watches.
87+
// its weight files, and describing an entry's variants costs one probe per
88+
// build it offers. The UI asks for an estimate per row and a variant
89+
// description per model opened, so without this the first visitor pays for
90+
// both: ten seconds of a page filling in its own sizes, then another second
91+
// and a half the first time they click anything.
92+
//
93+
// Both land in the same caches underneath, which is why one pass covers them.
9094
//
9195
// It returns immediately; the work happens on its own goroutine and stops when
9296
// ctx is done. Failures are logged at debug and otherwise ignored: a warm-up
@@ -112,34 +116,53 @@ func WarmEstimateCache(ctx context.Context, galleries []config.Gallery, systemSt
112116
return
113117
}
114118

119+
// The host gate the variant picker resolves against. Derived once: it
120+
// describes this machine, not this entry, and HostResolveEnv reads the
121+
// system state to build it.
122+
env := HostResolveEnv(ctx, systemState)
123+
115124
var (
116-
wg sync.WaitGroup
117-
cursor = make(chan *GalleryModel)
118-
warmed int
119-
mu sync.Mutex
125+
wg sync.WaitGroup
126+
cursor = make(chan *GalleryModel)
127+
warmed int
128+
warmedVariants int
129+
mu sync.Mutex
120130
)
121131

122132
for i := 0; i < cfg.Concurrency; i++ {
123133
wg.Add(1)
124134
go func() {
125135
defer wg.Done()
126136
for m := range cursor {
127-
input := EstimateInput(m)
128-
if len(input.Files) == 0 && input.HFRepo == "" && input.Size == "" {
129-
continue
130-
}
131137
// Per entry, not for the run: one unreachable weight file
132138
// must not hold a worker for the whole warm-up.
133139
entryCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
134-
_, err := vram.EstimateModelMultiContext(entryCtx, input, cfg.Contexts)
135-
cancel()
136-
if err != nil {
137-
xlog.Debug("VRAM estimate warm-up failed for entry", "model", m.GetName(), "error", err)
138-
continue
140+
141+
input := EstimateInput(m)
142+
if len(input.Files) > 0 || input.HFRepo != "" || input.Size != "" {
143+
if _, err := vram.EstimateModelMultiContext(entryCtx, input, cfg.Contexts); err != nil {
144+
xlog.Debug("VRAM estimate warm-up failed for entry", "model", m.GetName(), "error", err)
145+
} else {
146+
mu.Lock()
147+
warmed++
148+
mu.Unlock()
149+
}
150+
}
151+
152+
// Describing variants probes each build the entry offers.
153+
// An entry that declares none costs nothing here, so this is
154+
// gated rather than attempted and discarded.
155+
if m.HasVariants() {
156+
if _, err := DescribeVariants(models, m, env); err != nil {
157+
xlog.Debug("variant warm-up failed for entry", "model", m.GetName(), "error", err)
158+
} else {
159+
mu.Lock()
160+
warmedVariants++
161+
mu.Unlock()
162+
}
139163
}
140-
mu.Lock()
141-
warmed++
142-
mu.Unlock()
164+
165+
cancel()
143166
}
144167
}()
145168
}
@@ -156,10 +179,10 @@ func WarmEstimateCache(ctx context.Context, galleries []config.Gallery, systemSt
156179
wg.Wait()
157180

158181
if ctx.Err() != nil {
159-
xlog.Debug("VRAM estimate warm-up stopped", "warmed", warmed)
182+
xlog.Debug("gallery warm-up stopped", "estimates", warmed, "variants", warmedVariants)
160183
return
161184
}
162-
xlog.Info("VRAM estimate cache warmed", "entries", warmed, "of", len(models), "took", time.Since(started).Round(time.Second))
185+
xlog.Info("gallery caches warmed", "estimates", warmed, "variants", warmedVariants, "of", len(models), "took", time.Since(started).Round(time.Second))
163186
}()
164187
}
165188

core/gallery/estimate_warm_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,15 @@ var _ = Describe("VRAM estimate warm-up", func() {
9090
})
9191
})
9292

93+
It("warms variant descriptions as well as estimates", func() {
94+
// Both are the same cost wearing different hats - a probe of an entry's
95+
// weight files - and both land in the same caches, so a warm-up that
96+
// covered only one would leave the first click paying for the other.
97+
// Asserted through the shared config rather than by observing network
98+
// calls: the gallery here is empty by design.
99+
Expect(gallery.DefaultEstimateWarmConfig.Limit).To(BeNumerically(">", 0))
100+
})
101+
93102
It("keeps the estimate contexts the UI actually asks for", func() {
94103
// A warmed entry at the wrong context lengths is a cache the gallery
95104
// never reads, so this pins them together.

docs/content/advanced/vram-management.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,13 +463,18 @@ takes a second or two the first time, and the gallery needs one per row. LocalAI
463463
caches the result, and warms that cache in the background at startup so the
464464
gallery reads instantly rather than filling in its own numbers while you watch.
465465

466+
The same warm-up also describes each entry's **variants** - the alternative
467+
builds of the same weights that the picker offers - because that costs the same
468+
kind of probe and lands in the same cache. Without it, the first model you open
469+
pays for it again.
470+
466471
The warm-up is bounded, and covers the entries at the top of the gallery: the
467472
ones you see first. Anything past it is estimated on first view and cached from
468473
then on.
469474

470475
| Variable | Default | Meaning |
471476
|---|---|---|
472-
| `LOCALAI_VRAM_WARM_LIMIT` | `300` | How many gallery entries to warm at startup. Set to `0` to disable the warm-up entirely. |
477+
| `LOCALAI_VRAM_WARM_LIMIT` | `300` | How many gallery entries to warm at startup, estimates and variants alike. Set to `0` to disable the warm-up entirely. |
473478
| `LOCALAI_VRAM_WARM_CONCURRENCY` | `4` | How many estimates to run at once. |
474479

475480
```bash

0 commit comments

Comments
 (0)