internal/autogen/cpudevice_test.go
Rama main · réplica de lectura del repositorio original.
package autogen
import (
"path/filepath"
"strings"
"testing"
)
func TestOverride_InCpuPool(t *testing.T) {
cases := []struct {
ov *Override
want bool
}{
{nil, false},
{&Override{}, false},
{&Override{Device: DeviceCPU}, true},
{&Override{Device: DeviceCPU, SwapGroup: "gpu"}, false},
{&Override{Device: DeviceCPU, SwapGroup: "cpu"}, true},
{&Override{Device: DeviceGPU, SwapGroup: "cpu"}, false},
}
for _, c := range cases {
if got := c.ov.InCpuPool(); got != c.want {
t.Errorf("InCpuPool(%+v) = %v, want %v", c.ov, got, c.want)
}
}
if SwapGroupErr("gpu", "cpu") == nil {
t.Error("a GPU-device model in the cpu swap group must be refused")
}
if SwapGroupErr("cpu", "gpu") != nil || SwapGroupErr("", "") != nil {
t.Error("valid device/group pairs must pass")
}
}
// The cpu group is the RAM-budgeted shape: it swaps among its own members and
// is never persistent, and it is exposed on every listener like the other
// coexistence groups.
func TestAutogen_emitGroups_cpuPool(t *testing.T) {
s := Settings{Groups: []GroupSpec{
{Name: "assistant", Listen: "0.0.0.0:1250", Match: []string{"*"}},
}}
out := emitGroupsCoexist(s, []string{"qwen", "kev"}, coexistSets{CPU: []string{"kev"}})
assistant := section(out, " assistant:\n", "\n cpu:")
if strings.Contains(assistant, `- "kev"`) {
t.Fatalf("CPU-pool model must not be in the exclusive group:\n%s", out)
}
cpu := section(out, " cpu:\n", "\nlisteners:")
for _, want := range []string{"swap: true", "exclusive: false", "persistent: false", "pool: ram", `- "kev"`} {
if !strings.Contains(cpu, want) {
t.Fatalf("cpu group missing %q:\n%s", want, out)
}
}
if !strings.Contains(out, "groups: [assistant, cpu]") {
t.Fatalf("cpu group must be exposed on every listener:\n%s", out)
}
}
func TestEmitRamBudget(t *testing.T) {
var b strings.Builder
emitRamBudget(&b, Settings{MaxRamGB: 48})
if !strings.Contains(b.String(), "ramBudgetGB: 48\n") {
t.Fatalf("missing ramBudgetGB: %q", b.String())
}
off := false
b.Reset()
emitRamBudget(&b, Settings{MaxRamGB: 48, MultiResident: &off})
if b.Len() != 0 {
t.Fatalf("multiResident off must withhold ramBudgetGB: %q", b.String())
}
}
// A CPU-device decision model launches with no GPU at all, and its whole
// footprint moves from estVramGB to estRamGB so the RAM pool can admit it.
func TestDecision_CpuDevice(t *testing.T) {
s := Settings{ServerExe: "llama-server", Threads: 8}
row := GgufRow{FullPath: "C:/m/Kev-4B-Q8_0.gguf", SizeGB: 4}
meta := Metadata{Architecture: "qwen3", DecisionType: "kev", ContextLength: 40960, BlockCount: 32}
ov := &Override{Device: DeviceCPU, CpuOffload: 8}
cmd := strings.Join(decisionCmdLines(s, row, ov, meta), " ")
for _, want := range []string{"-ngl 0 ", "--device none"} {
if !strings.Contains(cmd, want) {
t.Errorf("cpu decision cmd missing %q: %s", want, cmd)
}
}
vram, ram := decisionEstGB(s, row, ov, meta)
full, _ := decisionEstGB(s, row, nil, meta)
if vram != 0 || ram != full {
t.Fatalf("cpu device: vram=%g ram=%g, want 0 and %g", vram, ram, full)
}
vram, ram = decisionEstGB(s, row, &Override{CpuOffload: 8}, meta)
if vram <= 0 || ram <= 0 || vram+ram < full-1e-9 || vram+ram > full+1e-9 {
t.Fatalf("partial offload must split the footprint: vram=%g ram=%g full=%g", vram, ram, full)
}
}
func TestEmbedding_CpuDevice(t *testing.T) {
s := Settings{ServerExe: "llama-server", Threads: 8}
row := GgufRow{FullPath: "C:/m/embed.gguf", SizeGB: 0.6}
meta := Metadata{Architecture: "bert", PoolingType: 1, ContextLength: 512}
ov := &Override{Device: DeviceCPU}
cmd := strings.Join(embeddingCmdLines(s, row, ov, meta), " ")
if !strings.Contains(cmd, "-ngl 0 --device none") || strings.Contains(cmd, "-ngl 99") {
t.Fatalf("cpu embedding cmd: %s", cmd)
}
var b strings.Builder
var emitted []string
emitEmbeddingModel(&b, s, row, ov, "embed", meta, &emitted)
if out := b.String(); strings.Contains(out, "estVramGB") || !strings.Contains(out, "estRamGB") {
t.Fatalf("cpu embedder must be priced in RAM only:\n%s", out)
}
}
// End to end: every profile of a CPU-device LLM (solo + ctx tiers) launches
// with no GPU, is priced in RAM only, and joins the cpu group.
func TestGenerate_cpuDeviceLLM(t *testing.T) {
dir := t.TempDir()
name := "Pins-8B-Q4_K_M.gguf"
writeStub(t, dir, name, 4_000_000_000)
seedMetaCache(t, filepath.Join(dir, name), pinsTestMeta())
gf := GenerateFile{
Settings: Settings{ModelsRoot: dir},
Overrides: []Override{{Match: "*", Device: DeviceCPU, CtxVariants: []int{8192}}},
}
gf.Settings.applyDefaults()
out, err := Generate(gf, "T")
if err != nil {
t.Fatalf("generate: %v", err)
}
models := section(out, "models:\n", "\ngroups:")
if n := strings.Count(models, "cmd: >"); n < 2 {
t.Fatalf("test assumption broken, want solo + ctx tier, got %d profiles:\n%s", n, models)
}
if strings.Count(models, "--device none") != strings.Count(models, "cmd: >") {
t.Fatalf("every profile must carry --device none:\n%s", models)
}
if strings.Contains(models, "estVramGB") || !strings.Contains(models, "estRamGB") {
t.Fatalf("CPU profiles must be priced in RAM only:\n%s", models)
}
cpu := section(out, " cpu:\n", "\nlisteners:")
if !strings.Contains(cpu, "pool: ram") || strings.Count(cpu, " - ") != strings.Count(models, "cmd: >") {
t.Fatalf("every profile must be a cpu group member:\n%s", out)
}
}
// A CPU-device plan must report every VRAM component as zero, not just the
// total. The dashboard draws KV / compute buffer / headroom as separate VRAM
// segments, so a 0 GB plan that still carried a 1.3 GB compute buffer painted
// a CPU-only model as holding ~2 GB of VRAM.
func TestEstimatePlan_CpuDeviceZeroesVramComponents(t *testing.T) {
meta := Metadata{
Architecture: "llama", BlockCount: 32,
HeadCountKv: 8, KeyLength: 128, ValueLength: 128,
ContextLength: 131072, EmbeddingLength: 4096,
FileSizeGB: 4,
}
s := Settings{
TargetVramGB: 24, VramOverheadGB: 0.5, MaxRamGB: 64,
DenseCtxLadder: []int{131072, 65536, 32768}, DenseMinCtx: 4096,
}
est, err := EstimatePlan(s, meta, EstimateInput{Ctx: 8192, CpuDevice: true})
if err != nil {
t.Fatal(err)
}
if est.EstVramGB != 0 || est.EstRamGB <= 0 {
t.Fatalf("cpu device: estVram=%v estRam=%v, want 0 / >0", est.EstVramGB, est.EstRamGB)
}
for name, v := range map[string]float64{
"kvReserve": est.KvReserveGB, "checkpoint": est.CheckpointGB, "draft": est.DraftGB,
"computeBuf": est.ComputeBufGB, "mmproj": est.MmprojGB, "overhead": est.OverheadGB, "fixed": est.FixedGB,
} {
if v != 0 {
t.Errorf("cpu device: %s = %v, want 0", name, v)
}
}
}