package placement import ( "strings" "testing" "gitea.mynodes.xyz/ilker/llmgw/internal/control/inventory" ) // cpuProfile is what the catalog produces for a llama.cpp model: no GPU class, // because there is no card to ask for. func cpuProfile(name string) Profile { return Profile{Name: name, Engine: "llamacpp"} } func TestCPUProfilePlacesOnANodeWithNoGPUs(t *testing.T) { // The whole point: a machine with no cards is useless to every other engine // and perfectly fine for this one. s := New(store(node("n1", "", 0))) res := s.Place([]Profile{cpuProfile("cpu")}, nil) if res.Placement == nil { t.Fatalf("no placement:\n%s", res.Explain()) } if len(res.Placement.Nodes) != 1 || res.Placement.Nodes[0].NodeID != "n1" { t.Fatalf("placed on %+v", res.Placement.Nodes) } if len(res.Placement.Nodes[0].GPUIndices) != 0 { t.Errorf("a CPU replica was given GPUs: %v", res.Placement.Nodes[0].GPUIndices) } if res.Placement.Pool != "none/cpu" { t.Errorf("pool %q", res.Placement.Pool) } } func TestCPUReplicasSpreadRatherThanStack(t *testing.T) { // There is no allocation to stop two CPU replicas sharing a machine's // cores, so placement has to do it. Bin packing, which is right for GPUs, // would put every replica on the same node. st := store(node("n1", "", 0), node("n2", "", 0), node("n3", "", 0)) s := New(st) seen := map[string]int{} for i := range 3 { res := s.Place([]Profile{cpuProfile("cpu")}, nil) if res.Placement == nil { t.Fatalf("iteration %d:\n%s", i, res.Explain()) } id := res.Placement.Nodes[0].NodeID seen[id]++ // Record it the way the reconciler would, so the next decision sees it. st.Allocate(id, inventory.Allocation{ShardID: id + "-sh", ReplicaID: id + "-rep"}) } if len(seen) != 3 { t.Errorf("three replicas landed on %d nodes: %v", len(seen), seen) } } func TestCPUProfileIgnoresGPUsItDoesNotNeed(t *testing.T) { // A GPU node is still a valid host, and taking it must not consume cards // that a GPU profile would need. st := store(node("gpu", "h100-80g", 8)) s := New(st) res := s.Place([]Profile{cpuProfile("cpu")}, nil) if res.Placement == nil { t.Fatalf("no placement:\n%s", res.Explain()) } if n := len(res.Placement.Nodes[0].GPUIndices); n != 0 { t.Errorf("a CPU replica claimed %d GPUs", n) } if free := st.FreeGPUs("gpu"); len(free) != 8 { t.Errorf("GPUs were consumed: %d free", len(free)) } } func TestGPUProfileStillNeedsItsClass(t *testing.T) { // The CPU path must not become a way for a GPU profile to be placed on a // machine that cannot run it. s := New(store(node("n1", "", 0))) res := s.Place([]Profile{{Name: "tp1", GPUClass: "h100-80g", GPUsPerNode: 1}}, nil) if res.Placement != nil { t.Fatal("a GPU profile was placed on a node with no GPUs") } if len(res.Rejections) == 0 { t.Error("the rejection should say why") } } func TestCPUProfileRespectsAvoid(t *testing.T) { // Spreading replicas of one model across failure domains matters the same // way it does for GPU replicas. s := New(store(node("n1", "", 0), node("n2", "", 0))) res := s.Place([]Profile{cpuProfile("cpu")}, []string{"n1"}) if res.Placement == nil { t.Fatalf("no placement:\n%s", res.Explain()) } if got := res.Placement.Nodes[0].NodeID; got != "n2" { t.Errorf("placed on %q despite being told to avoid it", got) } } func TestCPUCoresAreReservedAsMemoryIs(t *testing.T) { n := node("n1", "", 0) n.CPUCount = 8 st := store(n) s := New(st) p := cpuProfile("cpu") p.CPUCores = 3 for i := range 2 { res := s.Place([]Profile{p}, nil) if res.Placement == nil { t.Fatalf("replica %d did not fit in 8 cores:\n%s", i+1, res.Explain()) } st.Allocate("n1", inventory.Allocation{ShardID: string(rune('a' + i)), CPUCores: 3}) } // Six of eight given away: a third would need three. res := s.Place([]Profile{p}, nil) if res.Placement != nil { t.Fatal("a third replica was given cores the node does not have") } if got := res.Explain(); !strings.Contains(got, "has 8 CPUs with 2 free, needs 3") { t.Errorf("the refusal should say how many cores are left:\n%s", got) } // Unknown counts constrain nothing: an older agent, or a profile that // reserves no cores. old := node("n2", "", 0) if res := New(store(old)).Place([]Profile{p}, nil); res.Placement == nil { t.Error("a node that does not report its CPUs was excluded") } if res := s.Place([]Profile{cpuProfile("cpu")}, nil); res.Placement == nil { t.Error("a profile reserving no cores was refused") } }