Ctrl K
ComunidadGuías…

internal/autogen/cpudevice_test.go

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

Archivo original ↗Preguntar a KiBot
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)
		}
	}
}