CI / test (push) Successful in 1m15s
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)
|
|
}
|
|
}
|