Test that a restart borrows a lower class's room and returns it
CI / test (push) Successful in 3m41s
CI / test (push) Successful in 3m41s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
f288f3c423
commit
cf0178bf05
2 files changed
+69
No files matched your search
@@ -42,6 +42,16 @@ What v0.10.5 made settable from the command line is now on the pages too:
|
||||
critical model's card no longer refuses a replica that preemption would make
|
||||
room for.
|
||||
|
||||
### Restarts borrow a lower class's room
|
||||
|
||||
A restart places the replacement beside the replica it replaces, and where
|
||||
there is no free room it now makes some the way any placement does: by
|
||||
stopping a replica its class outranks, which gets its room back once the old
|
||||
replica has gone. The in-place restart — the model unserved while its
|
||||
replacement loads — is left for when nothing of a lower class is running. On a
|
||||
cluster where every model is standard, that is every full one: mark the models
|
||||
that can wait `opportunistic` (`llmgw ctl model class`, or the model's card).
|
||||
|
||||
## v0.10.5 — 2026-09-30
|
||||
|
||||
Published first as release candidates v0.10.5-rc.1 to rc.14; rc.5 failed its
|
||||
|
||||
@@ -331,3 +331,62 @@ func TestHeadroomCountsWhatAHigherClassCouldTake(t *testing.T) {
|
||||
t.Errorf("a standard model counted room in another's minimum: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestARestartBorrowsALowerClassesRoomAndGivesItBack(t *testing.T) {
|
||||
// chat fills node 1; the only other node holds an opportunistic replica.
|
||||
chat := gpuModel("chat", "", 1)
|
||||
extra := gpuModel("extra", catalog.ClassOpportunistic, 1)
|
||||
h := newFleetHarness(t, []catalog.Model{chat, extra}, gpuNode("1", 8), gpuNode("2", 8))
|
||||
h.settle(t, 3)
|
||||
h.advance(h.r.cfg.MinLifetime + time.Minute)
|
||||
var old string
|
||||
for _, rep := range h.r.Replicas() {
|
||||
if rep.Model == "chat" {
|
||||
old = rep.ID
|
||||
}
|
||||
}
|
||||
if old == "" || replicasOf(h, "extra") != 1 {
|
||||
t.Fatalf("setup: %+v", h.r.Replicas())
|
||||
}
|
||||
|
||||
if err := h.r.Restart(old); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Replacement first, beside the old one, in the room extra gives up;
|
||||
// the old one keeps serving until then.
|
||||
servedThroughout := true
|
||||
for range 8 {
|
||||
h.r.Reconcile(t.Context())
|
||||
h.markReady()
|
||||
ready := 0
|
||||
for _, rep := range h.r.Replicas() {
|
||||
if rep.Model == "chat" && rep.State == Ready {
|
||||
ready++
|
||||
}
|
||||
}
|
||||
if ready == 0 {
|
||||
servedThroughout = false
|
||||
}
|
||||
h.advance(h.r.cfg.PreemptGrace + time.Second)
|
||||
}
|
||||
if !servedThroughout {
|
||||
t.Error("chat had no ready replica during its restart")
|
||||
}
|
||||
var newChat bool
|
||||
for _, rep := range h.r.Replicas() {
|
||||
if rep.Model == "chat" && rep.ID != old && rep.State == Ready {
|
||||
newChat = true
|
||||
}
|
||||
if rep.ID == old {
|
||||
t.Errorf("the old replica is still there: %+v", rep)
|
||||
}
|
||||
}
|
||||
if !newChat {
|
||||
t.Fatalf("no new chat replica: %+v", h.r.Replicas())
|
||||
}
|
||||
// And extra has its replica back, in the room the old chat left.
|
||||
h.settle(t, 4)
|
||||
if replicasOf(h, "extra") != 1 {
|
||||
t.Errorf("the opportunistic model did not get its room back: %+v", h.r.Replicas())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user