Ctrl K
ComunidadGuías…

internal/autogen/decision_test.go

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

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"path/filepath"
	"strings"
	"testing"
)

// The parser lifts <arch>.decision.type, and a decision model that also bakes
// a pooling_type classifies as a decision model, not an embedder: emitModel
// checks IsDecisionModel first.
func TestReadGgufMetadata_DecisionType(t *testing.T) {
	p := filepath.Join(t.TempDir(), "Kev-4B-Q8_0.gguf")
	writeGgufKVs(t, p, [][2]any{
		{"general.architecture", "qwen3"},
		{"qwen3.decision.type", "kev"},
		{"qwen3.pooling_type", 3},
	})
	meta, err := ReadGgufMetadata(p)
	if err != nil {
		t.Fatal(err)
	}
	if meta.DecisionType != "kev" {
		t.Fatalf("DecisionType = %q, want kev", meta.DecisionType)
	}
	if !IsDecisionModel(meta) {
		t.Error("IsDecisionModel = false, want true")
	}
	if IsDecisionModel(Metadata{Architecture: "qwen3"}) {
		t.Error("a plain chat model must not classify as a decision model")
	}
}

func TestDecisionCmdLines(t *testing.T) {
	s := Settings{ServerExe: "llama-server", Threads: 8}
	row := GgufRow{FullPath: `C:\models\kev\Kev-4B-Q8_0.gguf`}

	causal := strings.Join(decisionCmdLines(s, row, nil, Metadata{Architecture: "qwen3", DecisionType: "kev", ContextLength: 40960}), " ")
	for _, want := range []string{"-m C:/models/kev/Kev-4B-Q8_0.gguf", "-ngl 99", "-c 8192", "-t 8"} {
		if !strings.Contains(causal, want) {
			t.Errorf("decision cmd missing %q in: %s", want, causal)
		}
	}
	// llama-server enables embedding output itself; --embeddings, a pooling
	// mode or the chat machinery would change what it serves.
	for _, bad := range []string{"--embeddings", "--pooling", "--jinja", "-ub", "--slot-save-path"} {
		if strings.Contains(causal, bad) {
			t.Errorf("decision cmd must not carry %q: %s", bad, causal)
		}
	}

	// Whole-prompt types read the state in one ubatch.
	laya := strings.Join(decisionCmdLines(s, row, nil, Metadata{Architecture: "modernbert", DecisionType: "laya", ContextLength: 2048}), " ")
	if !strings.Contains(laya, "-b 2048 -ub 2048") {
		t.Errorf("laya cmd missing -b/-ub = ctx: %s", laya)
	}

	// An explicit ctx override wins over the cap.
	ov := strings.Join(decisionCmdLines(s, row, &Override{Ctx: 16384}, Metadata{DecisionType: "kev", ContextLength: 40960}), " ")
	if !strings.Contains(ov, "-c 16384") {
		t.Errorf("ctx override ignored: %s", ov)
	}

	// A pinned CPU offload reaches -ngl (it used to be dropped for -ngl 99).
	dense := Metadata{DecisionType: "kev", ContextLength: 40960, BlockCount: 32}
	all := strings.Join(decisionCmdLines(s, row, &Override{CpuOffload: 32}, dense), " ")
	if !strings.Contains(all, "-ngl 0 ") {
		t.Errorf("cpuOffload 32/32 should emit -ngl 0: %s", all)
	}
	half := strings.Join(decisionCmdLines(s, row, &Override{CpuOffload: 8}, dense), " ")
	if !strings.Contains(half, "-ngl 24 ") {
		t.Errorf("cpuOffload 8/32 should emit -ngl 24: %s", half)
	}
}

func TestDecisionEstVramGB_CpuOffload(t *testing.T) {
	s := Settings{}
	row := GgufRow{SizeGB: 4}
	meta := Metadata{DecisionType: "kev", ContextLength: 8192, BlockCount: 32}
	gpu := decisionEstVramGB(s, row, nil, meta)
	cpu := decisionEstVramGB(s, row, &Override{CpuOffload: 32}, meta)
	if gpu-cpu < 3.9 {
		t.Errorf("full CPU offload should drop the weights from VRAM: gpu=%.2f cpu=%.2f", gpu, cpu)
	}
}

func TestEmitDecisionModel_Capabilities(t *testing.T) {
	var b strings.Builder
	var emitted []string
	s := Settings{ServerExe: "llama-server", Threads: 8, TtlSec: 600}
	row := GgufRow{FullPath: "/models/kev/Kev-4B-Q8_0.gguf", SizeGB: 4.3}
	emitDecisionModel(&b, s, row, nil, "kev-4b-q8_0", Metadata{Architecture: "qwen3", DecisionType: "kev", ContextLength: 4096}, &emitted)
	out := b.String()
	for _, want := range []string{"decision: true", `decisionType: "kev"`, "context: 4096"} {
		if !strings.Contains(out, want) {
			t.Errorf("emitted yaml missing %q:\n%s", want, out)
		}
	}
	if strings.Contains(out, "embedding: true") {
		t.Errorf("decision model must not advertise embedding:\n%s", out)
	}
	if len(emitted) != 1 || emitted[0] != "kev-4b-q8_0" {
		t.Errorf("emitted = %v", emitted)
	}
}

func TestDecisionEstVramGB_ScalesWithWholeBatch(t *testing.T) {
	s := Settings{ComputeBufFactor: 1}
	row := GgufRow{SizeGB: 0.3}
	enc := Metadata{Architecture: "modernbert", DecisionType: "laya", ContextLength: 8192, HeadCount: 12, EmbeddingLength: 768, VocabSize: 50368}
	small := decisionEstVramGB(s, row, &Override{Ctx: 1024}, enc)
	big := decisionEstVramGB(s, row, nil, enc)
	// 12 heads x 8192^2 x 4 bytes is 3GB of scores alone; a flat pad charged 0.5.
	if big-row.SizeGB < 3 {
		t.Errorf("laya at 8192 charged %.2fGB over weights, want >= 3 for the score matrix", big-row.SizeGB)
	}
	if big <= small*2 {
		t.Errorf("estimate should grow with -ub: 1024 -> %.2fGB, 8192 -> %.2fGB", small, big)
	}
	kev := Metadata{Architecture: "qwen3", DecisionType: "kev", ContextLength: 40960, HeadCount: 32, EmbeddingLength: 2560, VocabSize: 151936}
	if ub, whole := decisionUb(kev, nil); whole || ub != llamaDefaultUb {
		t.Errorf("kev ub = %d whole=%v, want %d false", ub, whole, llamaDefaultUb)
	}
}

// A ModernBERT sequence classifier converted without decision.type (von-1.2)
// has no LM head and no mode llama-server serves it in, so it is dropped with
// the reason instead of listed as a chat model. An encoder that bakes a
// pooling_type is still an embedder: that check runs first.
func TestEmitModel_SkipsNonCausalClassifier(t *testing.T) {
	dir := t.TempDir()
	von := filepath.Join(dir, "von-1.2-f16.gguf")
	writeGgufKVs(t, von, [][2]any{
		{"general.architecture", "modern-bert"},
		{"modern-bert.context_length", 2048},
		{"modern-bert.attention.causal", false},
		{"modern-bert.classifier.output_labels", []string{"a", "b", "c"}},
	})
	meta, err := ReadGgufMetadata(von)
	if err != nil {
		t.Fatal(err)
	}
	if !meta.NonCausal || !meta.ClassifierHead {
		t.Fatalf("NonCausal=%v ClassifierHead=%v, want both", meta.NonCausal, meta.ClassifierHead)
	}

	var b strings.Builder
	var emitted []string
	var coexist coexistSets
	row := GgufRow{FullPath: von, FileName: "von-1.2-f16.gguf", SizeGB: 0.8}
	err = emitModel(&b, Settings{ServerExe: "llama-server"}, GenerateFile{}, row, nil, "von-1.2", &emitted, &coexist)
	if err == nil || !strings.Contains(err.Error(), "decision.type") {
		t.Fatalf("err = %v, want a non-causal classifier skip", err)
	}
	if len(emitted) != 0 || b.Len() != 0 {
		t.Errorf("classifier was emitted: %v\n%s", emitted, b.String())
	}

	emb := filepath.Join(dir, "bge-small-f16.gguf")
	writeGgufKVs(t, emb, [][2]any{
		{"general.architecture", "bert"},
		{"bert.attention.causal", false},
		{"bert.pooling_type", 2},
	})
	b.Reset()
	row = GgufRow{FullPath: emb, FileName: "bge-small-f16.gguf", SizeGB: 0.1}
	if err := emitModel(&b, Settings{ServerExe: "llama-server"}, GenerateFile{}, row, nil, "bge-small", &emitted, &coexist); err != nil {
		t.Fatalf("embedder skipped: %v", err)
	}
	if !strings.Contains(b.String(), "--embeddings") {
		t.Errorf("embedder not routed to --embeddings:\n%s", b.String())
	}
}