Files
Ilker ManapandClaude Opus 5 3081c6f69b Place on nodes that can actually hold the model
Placement had no idea how much memory a node has or a model needs. A CPU
profile has no card to be free, so eligibleNodes accepted every ready node:
Qwen2.5-Coder 14B, whose working set is about 24 GB, would have been placed
just as happily on one of the 8 GB nodes, which reports itself perfectly
healthy right up until the OOM killer arrives.

node_selector was yesterday's stopgap and it is the operator restating by
hand something the system can know. Agents now report MemTotal, profiles
declare min_memory_gb, and placement filters on it, naming what was short —
"has 8 GB of memory, needs 24" is actionable where "no candidates were
considered" is not. The label selector stays, because "this node is for
coding models" is a real thing to want to say that memory does not express.

Two deliberate non-exclusions. A profile with no requirement fits anywhere,
which is every profile that exists today. And a node that reports no memory
is still usable: an agent older than the field says nothing, and letting a
control plane upgrade quietly empty half a fleet is a worse failure than the
one this prevents.

ctl nodes grows a MEM column showing "-" rather than 0 for a node that did
not say, because an old agent is not a small machine.

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

40 lines
1.2 KiB
Go

package discovery
import (
"os"
"path/filepath"
"testing"
)
func TestMemTotalIsReadFromMeminfo(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "meminfo")
// The real shape, including the fields before and after, so the parser is
// not tested against something tidier than it will see.
if err := os.WriteFile(p, []byte(
"MemTotal: 32876244 kB\nMemFree: 1234 kB\nMemAvailable: 5678 kB\n"), 0o600); err != nil {
t.Fatal(err)
}
if got, want := memTotalMBFrom(p), 32876244/1024; got != want {
t.Errorf("memTotalMBFrom = %d, want %d", got, want)
}
}
func TestUnreadableMeminfoReportsUnknownRatherThanZeroMemory(t *testing.T) {
// Zero means "did not say" everywhere downstream, and placement treats it
// as no constraint. A node with genuinely no memory is not a case.
if got := memTotalMBFrom(filepath.Join(t.TempDir(), "absent")); got != 0 {
t.Errorf("got %d for a missing file", got)
}
dir := t.TempDir()
p := filepath.Join(dir, "meminfo")
for _, junk := range []string{"", "MemTotal:\n", "MemTotal: not-a-number kB\n", "Other: 1 kB\n"} {
if err := os.WriteFile(p, []byte(junk), 0o600); err != nil {
t.Fatal(err)
}
if got := memTotalMBFrom(p); got != 0 {
t.Errorf("got %d for %q", got, junk)
}
}
}