Files
llmgw/internal/control/reconciler/spread_test.go
T
Ilker ManapandClaude Opus 5 5c837c531b Pin down where CPU replicas land, and where they deliberately do not
Two tests for behaviour that looked wrong on the cluster and was not. Three
replicas all landed on one node, which is correct when it is the only node
registered: spreading is a preference and a model left unplaced to preserve a
preference would be worse. Over successive ticks with somewhere to go, they
spread.

What neither test asserts, because it is not true: nothing rebalances them
afterwards. A replica that moved on its own would interrupt the traffic it is
serving, and v1 has no autoscaler to decide it is worth it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 09:57:12 +02:00

69 lines
2.1 KiB
Go

package reconciler
import (
"context"
"testing"
"time"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/catalog"
"gitea.mynodes.xyz/ilker/llmgw/internal/control/inventory"
)
func cpuNode(id string) inventory.Node {
return inventory.Node{ID: id, MgmtAddr: "10.0.0." + id, State: inventory.Ready}
}
// TestCPUReplicasSpreadOverSuccessiveTicks checks the placement that has no
// allocation to stop it. Two CPU replicas on one machine compete for the same
// cores, and unlike GPUs nothing in the inventory prevents it.
//
// Spreading is a preference rather than a rule, so this only holds when there
// is somewhere else to go: with one node registered, all three land on it and
// that is correct. Nothing rebalances them afterwards — v1 has no autoscaler,
// and a replica that moved on its own would interrupt the traffic it is serving.
func TestCPUReplicasSpreadOverSuccessiveTicks(t *testing.T) {
m := catalog.Model{
Name: "qwen", Source: "/m.gguf",
Scaling: catalog.Scaling{Min: 3, Max: 3},
Profiles: []catalog.Profile{{Name: "cpu", Engine: "llamacpp"}},
}
h := newHarness(t, m, cpuNode("1"), cpuNode("2"), cpuNode("3"))
ctx := context.Background()
for range 6 {
h.r.Reconcile(ctx)
h.markReady()
h.advance(time.Second)
}
byNode := map[string]int{}
for _, rep := range h.r.Replicas() {
for _, s := range rep.Shards {
byNode[s.NodeID]++
}
}
if len(byNode) != 3 {
t.Errorf("three replicas landed on %d nodes: %v", len(byNode), byNode)
}
}
func TestCPUReplicasStackWhenThereIsNowhereElse(t *testing.T) {
// The other half of "preference, not rule": one node available means one
// node used, rather than a model left unplaced to preserve a preference.
m := catalog.Model{
Name: "qwen", Source: "/m.gguf",
Scaling: catalog.Scaling{Min: 3, Max: 3},
Profiles: []catalog.Profile{{Name: "cpu", Engine: "llamacpp"}},
}
h := newHarness(t, m, cpuNode("1"))
ctx := context.Background()
for range 6 {
h.r.Reconcile(ctx)
h.markReady()
h.advance(time.Second)
}
if n := len(h.r.Replicas()); n != 3 {
t.Errorf("%d replicas placed on the only node available, want 3", n)
}
}