A model put first is not evicted, idle, for a lower class
CI / test (push) Failing after 7m59s

The VLM is to come first on the one card. Its class makes it take the card
from flux, triposr or sf3d when asked for, but idle eviction ignored class:
a 3D job two minutes into a lull would have taken the card from it. Idle
eviction offers only replicas of no higher class than the model wanting
room, and the wait such a model is told runs to the holder's idle timeout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilker ManapandClaude Opus 5.5 committed 2026-10-13 10:15:14 +02:00
1 parent 967df77e53
commit fa6e68c8e8
4 files changed
+69 -7

No files matched your search

+5
View File
@@ -86,6 +86,11 @@ APIs change between minors without a deprecation period.
- A picture in a prompt was counted as its base64 by the context check, so
one 1280x720 PNG was "138 thousand tokens" and a vision model refused it;
a picture or a sound now counts as about a thousand.
- An on-demand model of a higher class was evicted, idle, for a lower-class
one two minutes into a lull; it keeps the card through its idle timeout,
and a model waiting on it is told so.
- A model removed from the catalog stayed wanted, so added again it loaded
unasked.
## v0.14.3 — 2026-10-10
+11
View File
@@ -90,6 +90,11 @@ func (r *Reconciler) evictable(wantedBy string) []evictionCandidate {
cat := r.cat.Load()
now := r.now()
wantRank := -1
if w, ok := cat.Resolve(wantedBy); ok {
wantRank = catalog.ClassRank(w.EffectiveClass())
}
r.mu.Lock()
defer r.mu.Unlock()
@@ -102,6 +107,12 @@ func (r *Reconciler) evictable(wantedBy string) []evictionCandidate {
if !ok || !other.Scaling.OnDemand() {
continue
}
// Not for a model of a lower class: the one put first keeps the
// card through its idle timeout, rather than losing it two minutes
// into a lull to whatever was asked for next.
if catalog.ClassRank(other.EffectiveClass()) > wantRank {
continue
}
// Unknown is not idle. A replica the router has said nothing about —
// after a restart, that is all of them — must not be evicted on the
// strength of a fact nobody has reported.
+11 -7
View File
@@ -201,13 +201,17 @@ func (r *Reconciler) WaitFor(model string) (heldBy string, eta time.Duration, ok
}
h := holder{v: victim{ID: rep.ID, Model: rep.Model}}
if !rep.LastUsedAt.IsZero() {
evict := rep.LastUsedAt.Add(r.cfg.EvictionGrace)
if !rep.ReadyAt.IsZero() && rep.ReadyAt.Add(r.cfg.MinLifetime).After(evict) {
evict = rep.ReadyAt.Add(r.cfg.MinLifetime)
}
h.release = evict
if idle := rep.LastUsedAt.Add(r.idleTimeout(other)); idle.Before(evict) {
h.release = idle
h.release = rep.LastUsedAt.Add(r.idleTimeout(other))
// Evicted sooner, when it is no higher a class than the model
// waiting; one put first is waited for to its idle timeout.
if catalog.ClassRank(other.EffectiveClass()) <= catalog.ClassRank(m.EffectiveClass()) {
evict := rep.LastUsedAt.Add(r.cfg.EvictionGrace)
if !rep.ReadyAt.IsZero() && rep.ReadyAt.Add(r.cfg.MinLifetime).After(evict) {
evict = rep.ReadyAt.Add(r.cfg.MinLifetime)
}
if evict.Before(h.release) {
h.release = evict
}
}
}
holders = append(holders, h)
@@ -1337,6 +1337,48 @@ func TestAModelRemovedWhileRunningIsStopped(t *testing.T) {
}
}
func TestAModelPutFirstIsNotEvictedForALowerClassOne(t *testing.T) {
// The VLM is put first: a picture or 3D model asked for in a lull must
// wait for its idle timeout, not take the card two minutes in.
h := newHarness(t, testModel(1), gpuNode("1", 4))
cat := twoOnDemandModels(t)
ms := cat.Models()
for i := range ms {
if ms[i].Name == "llama" {
ms[i].Class = catalog.ClassStandard
} else {
ms[i].Class = catalog.ClassOpportunistic
}
}
built, err := catalog.New(ms)
if err != nil {
t.Fatal(err)
}
h.r.cat.Store(built)
h.r.Want("llama")
h.r.Reconcile(t.Context())
h.markReady()
reps := h.r.Replicas()
h.r.RecordUsage(map[string]time.Time{reps[0].ID: h.clock.now()})
h.advance(DefaultConfig().MinLifetime + DefaultConfig().EvictionGrace + time.Minute)
if got := h.r.evictable("second"); len(got) != 0 {
t.Errorf("a standard model was offered to an opportunistic one: %+v", got)
}
h.r.Want("second")
h.r.Reconcile(t.Context())
// It waits for the idle timeout (an hour here), and is told so.
_, eta, ok := h.r.WaitFor("second")
want := time.Hour - (DefaultConfig().MinLifetime + DefaultConfig().EvictionGrace + time.Minute)
if !ok || eta != want {
t.Errorf("WaitFor: %s %v, want %s", eta, ok, want)
}
// The other way, it is offered.
if got := h.r.evictable("llama"); len(got) != 0 {
t.Errorf("nothing of second runs, yet %+v was offered", got)
}
}
func TestAnOnDemandModelRemovedAndAddedAgainIsNotLoadedUnasked(t *testing.T) {
// The VLM, removed and added again to give it a licence, came back
// loading on its own: still on the wanted list from before its removal.