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>
69 lines
2.1 KiB
Go
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)
|
|
}
|
|
}
|