Ctrl K
ComunidadGuías…

internal/autogen/vram_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"testing"

	"github.com/quartermaster-labs/quartermaster/internal/perf"
)

func TestAutogen_freeVramGBFromStats(t *testing.T) {
	cases := []struct {
		name   string
		stats  []perf.GpuStat
		wantGB float64
		wantOK bool
	}{
		{"empty", nil, 0, false},
		{"no total", []perf.GpuStat{{MemTotalMB: 0, MemUsedMB: 0}}, 0, false},
		{"single", []perf.GpuStat{{MemTotalMB: 8192, MemUsedMB: 1024}}, 7.0, true},
		{"used exceeds total clamps to 0", []perf.GpuStat{{MemTotalMB: 8192, MemUsedMB: 9000}}, 0, true},
		{
			// An iGPU reports a slice of system RAM as dedicated VRAM. Pooling
			// it would invent budget no card has, so it is dropped below the
			// inference floor and only the dGPU counts.
			"drops the iGPU below the floor",
			[]perf.GpuStat{
				{ID: 0, MemTotalMB: 2048, MemUsedMB: 0},    // iGPU, 2GB free
				{ID: 1, MemTotalMB: 8192, MemUsedMB: 2048}, // dGPU, 6GB free
			},
			6.0, true,
		},
		{
			// Two real cards POOL. This is the rule that changed for issue #4:
			// the old sizer reported the largest adapter alone (6.0 here) and
			// left the second card's VRAM unused.
			"pools two eligible cards",
			[]perf.GpuStat{
				{ID: 0, MemTotalMB: 12288, MemUsedMB: 1024},
				{ID: 1, MemTotalMB: 16384, MemUsedMB: 4096},
			},
			23.0, true,
		},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			gb, ok := freeVramGBFromStats(tc.stats, GpuPolicy{})
			if ok != tc.wantOK {
				t.Fatalf("ok = %v, want %v", ok, tc.wantOK)
			}
			if ok && gb != tc.wantGB {
				t.Fatalf("gb = %v, want %v", gb, tc.wantGB)
			}
		})
	}
}

func TestAutogen_noteFreeVramGB_keepsIdleHighWaterMark(t *testing.T) {
	// The mark stands in for "the card with none of our models resident". A later,
	// lower reading is a model holding VRAM — sizing the next plan against it
	// plans into the scraps — so only upward moves count.
	ResetIdleFreeVramGB()
	t.Cleanup(ResetIdleFreeVramGB)

	if got := noteFreeVramGB(21.9); got != 21.9 {
		t.Fatalf("first reading = %v, want 21.9", got)
	}
	if got := noteFreeVramGB(2.6); got != 21.9 {
		t.Fatalf("reading taken with a model resident = %v, want the idle 21.9", got)
	}
	// VRAM freed elsewhere legitimately raises the mark.
	if got := noteFreeVramGB(23.4); got != 23.4 {
		t.Fatalf("higher reading = %v, want 23.4", got)
	}
}

func TestAutogen_resolveAutoVram_postconditions(t *testing.T) {
	// Hardware-agnostic: with a GPU present resolveAutoVram caps the target at the
	// live free reading; without one it leaves the static value. Either way the
	// target must stay strictly positive and never exceed the free reading.
	ResetIdleFreeVramGB()
	t.Cleanup(ResetIdleFreeVramGB)
	const static = 7.0
	s := &Settings{TargetVramGB: static, VramOverheadGB: 1.0, AutoVram: true}
	resolveAutoVram(s, nil)
	if s.TargetVramGB <= 0 {
		t.Fatalf("TargetVramGB = %v, want > 0", s.TargetVramGB)
	}
	free, haveGPU := SampleFreeVramGB(autoVramSampleTimeout, GpuPolicy{})
	if !haveGPU {
		if s.TargetVramGB != static {
			t.Fatalf("no GPU reading but target changed to %v (want static %v)", s.TargetVramGB, static)
		}
		return
	}
	// Tolerance, not a hard bound: resolveAutoVram budgets against the idle
	// high-water mark, which legitimately sits at or above a sample taken later.
	if s.TargetVramGB > free+0.5 {
		t.Fatalf("live target %v exceeds free reading %v", s.TargetVramGB, free)
	}
	// A static ceiling tighter than free is a deliberate limit and must survive.
	if free > static && s.TargetVramGB != static {
		t.Fatalf("tighter static ceiling %v was raised to %v (free %v)", static, s.TargetVramGB, free)
	}
	// A static ceiling ABOVE what the card has left gets clamped to the free
	// reading in full — NOT free minus vramOverheadGB. The overhead is already
	// charged inside EstVramGB, so subtracting it here spends it twice and costs
	// a layer of offload on a tight fit.
	// Tolerance, not equality: resolveAutoVram takes its own sample and live VRAM
	// drifts between the two. 0.5 still separates "the free reading" from "free
	// minus the 1.0 overhead".
	hi := &Settings{TargetVramGB: free + 4, VramOverheadGB: 1.0, AutoVram: true}
	resolveAutoVram(hi, nil)
	if hi.TargetVramGB <= free-0.5 {
		t.Fatalf("target above free clamped to %v; want ~%v, not free minus the %v overhead", hi.TargetVramGB, free, hi.VramOverheadGB)
	}
}

// TestClassifyBackendRuntime pins the path classifier that decides between the
// 0.4 GB Vulkan and 0.8 GB ROCm per-process constants. Getting this wrong is
// silent: it just mis-sizes every model on the box by 0.4 GB, in whichever
// direction, so the "says nothing" case must stay UNKNOWN rather than defaulting.
func TestClassifyBackendRuntime(t *testing.T) {
	cases := []struct {
		name       string
		exe        string
		rocm, know bool
	}{
		{"installer rocm layout", `E:\bin\custom-lemonade-sdk-llamacpp-rocm\b1328-llama-windows-rocm-gfx110x\llama-server.exe`, true, true},
		{"hand-built hip", "/opt/llama.cpp/build-hip/bin/llama-server", true, true},
		{"hipblas in path", "C:/llama/hipblas-b1300/llama-server.exe", true, true},
		{"vulkan build", `E:\bin\llamacpp\b10405-llama-windows-vulkan\llama-server.exe`, false, true},
		{"cuda build", `E:\bin\llamacpp\b10405-llama-windows-cuda-12.4\llama-server.exe`, false, true},
		{"bare exe says nothing", "llama-server.exe", false, false},
		{"empty", "  ", false, false},
		// "hip" must not match inside an ordinary word, or every model on the box
		// is charged 0.4 GB it never allocates.
		{"hip inside a word", "D:/shipsets/llama/llama-server.exe", false, false},
		{"chipset dir", `D:\chipset-tools\llama-server.exe`, false, false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			rocm, known := classifyBackendRuntime(tc.exe)
			if rocm != tc.rocm || known != tc.know {
				t.Fatalf("classifyBackendRuntime(%q) = (rocm=%v, known=%v), want (%v, %v)", tc.exe, rocm, known, tc.rocm, tc.know)
			}
		})
	}
}

// TestNoteBackendRuntimeLatch covers the idempotence rule: an unreadable path
// must leave the previous verdict standing (a bare exe is not evidence of
// Vulkan), but a path that DOES name a runtime must be able to clear the flag
// again when the UI repoints the registry at a different build.
func TestNoteBackendRuntimeLatch(t *testing.T) {
	t.Cleanup(func() { rocmBackend.Store(false) })

	NoteBackendRuntime(`E:\bin\llamacpp-rocm\b1328-llama-windows-rocm-gfx110x\llama-server.exe`)
	if !usingRocmBackend() {
		t.Fatal("a rocm path should set the rocm backend flag")
	}
	NoteBackendRuntime("llama-server.exe")
	if !usingRocmBackend() {
		t.Fatal("an unrecognised path must leave the previous verdict alone, not clear it")
	}
	NoteBackendRuntime(`E:\bin\llamacpp\b10405-llama-windows-vulkan\llama-server.exe`)
	if usingRocmBackend() {
		t.Fatal("repointing at a vulkan build should clear the rocm flag")
	}
}