Ctrl K
ComunidadGuías…

internal/autogen/decision.go

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

Archivo original ↗Preguntar a KiBot
package autogen

import (
	"fmt"
	"strings"
)

// Decision models are llama.cpp classifiers served on POST /v1/systemone (the
// TypeSafe-compatible System One API): they answer typed questions (choice,
// score, noul) about a state with probabilities, and never generate text.
// llama-server recognises one from "<arch>.decision.type" and switches itself
// into decision mode, enabling embedding output on its own, so the launch needs
// no mode flag. What it must NOT get is the chat machinery: --embeddings, slot
// saving, spec decoding, chat templates and samplers all do nothing here or,
// worse, change how the prompt is batched.

// llamaDefaultUb is llama-server's -ub when none is passed: the physical batch a
// causal decision model (kev, openjev) runs its compute graph at.
const llamaDefaultUb = 512

// decisionMaxCtx bounds the default window. The state is a ticket, a message or
// a document excerpt, not a conversation, and a decision model with a long
// trained window would otherwise reserve KV nobody uses.
const decisionMaxCtx = 8192

// decisionWholeBatchTypes are the decision types whose whole prompt has to be
// read in ONE batch (llama.cpp tools/server/README.md, /v1/systemone): the
// non-causal encoders (laya, clef, vllm-d3) and lfm2-d1-omni. llama-server's
// default -ub of 512 would reject any prompt longer than that, so these get
// -b/-ub pinned to the window.
var decisionWholeBatchTypes = map[string]bool{
	"laya":         true,
	"clef":         true,
	"vllm-d3":      true,
	"lfm2-d1-omni": true,
}

// IsDecisionModel reports whether a GGUF is a llama.cpp decision model.
// Checked before IsEmbeddingModel: a decision model built on an encoder can
// bake a pooling_type too, and launching it with --embeddings would serve the
// wrong API.
func IsDecisionModel(meta Metadata) bool {
	return strings.TrimSpace(meta.DecisionType) != ""
}

// decisionCtx is the served window: the override, else the trained length
// capped at decisionMaxCtx.
func decisionCtx(meta Metadata, ov *Override) int {
	if ov != nil && ov.Ctx > 0 {
		return ov.Ctx
	}
	ctx := 4096
	if meta.ContextLength > 0 {
		ctx = int(meta.ContextLength)
	}
	if ctx > decisionMaxCtx {
		ctx = decisionMaxCtx
	}
	return ctx
}

// decisionMmproj resolves the vision projector a decision model loads, the
// same way an LLM does (own folder, family sibling, or an explicit
// mmprojFile), honouring `mmproj: none`. openjev, clef, pplx-decider, vllm-d3
// and the lfm2-d1 pair take images on /v1/systemone through it.
func decisionMmproj(row GgufRow, ov *Override) (path string, sizeGB float64) {
	file := ""
	if ov != nil {
		if strings.EqualFold(strings.TrimSpace(ov.Mmproj), NoneSentinel) {
			return "", 0
		}
		file = ov.MmprojFile
	}
	return mmprojFor(row, file)
}

// decisionUb is the physical batch the model runs at: the whole window for the
// types that must read the prompt in one batch, llama-server's default
// otherwise. The launch flag and the VRAM estimate both read it, so they cannot
// disagree about how big the compute graph is.
func decisionUb(meta Metadata, ov *Override) (ub int, wholeBatch bool) {
	if decisionWholeBatchTypes[strings.ToLower(strings.TrimSpace(meta.DecisionType))] {
		return decisionCtx(meta, ov), true
	}
	return llamaDefaultUb, false
}

// decisionAttnScoresGB is the attention-score matrix (n_head x ub x ub, f32) a
// whole-batch model materializes when flash attention does not run. llama.cpp
// picks FA per backend at load (-fa auto) and computeGraphGB assumes it ran, which
// holds for the 512-token batches of a causal model but not for an encoder
// reading 8192 tokens at once: 12 heads at 8192 is 3.2GB. Charged as the peak
// because an under-charge here is a spill into shared memory, and an over-charge
// only keeps a second model from co-residing with a small encoder.
func decisionAttnScoresGB(meta Metadata, ub int) float64 {
	heads := float64(meta.HeadCount)
	if heads <= 0 {
		heads = 12 // ModernBERT-base, the smallest common decision encoder
	}
	return heads * float64(ub) * float64(ub) * 4.0 / gib
}

// decisionPlacement is the -ngl / --n-cpu-moe pair: every layer on the GPU
// unless the user pinned a CPU offload, which goes through the same
// applyForcedOffload an LLM's pin does.
func decisionPlacement(meta Metadata, ov *Override) (ngl, ncpuMoe int) {
	if ov.CpuDevice() {
		return 0, 0
	}
	if ov != nil && ov.CpuOffload > 0 {
		return applyForcedOffload(meta, ov.CpuOffload)
	}
	return 99, 0
}

// decisionGpuShare is the fraction of the weights and KV a dense placement
// keeps in VRAM (a CPU layer holds its own KV). MoE expert offload is left at
// 1: the experts are most of the file, but a decision model is never one.
func decisionGpuShare(meta Metadata, ov *Override) float64 {
	ngl, _ := decisionPlacement(meta, ov)
	if meta.IsMoE || meta.BlockCount <= 0 || int64(ngl) >= meta.BlockCount {
		return 1
	}
	return float64(ngl) / float64(meta.BlockCount)
}

// decisionCmdLines builds the llama-server argv (exe first). Shared by
// emitDecisionModel and RenderSoloCmdLayers so the editor preview matches a
// save. The exe is the class "llm" pick, with the same vllm exception
// embedders make: there is no vllm decision emitter.
func decisionCmdLines(s Settings, row GgufRow, ov *Override, meta Metadata) []string {
	threads := s.Threads
	if ov != nil && ov.Threads > 0 {
		threads = ov.Threads
	}
	ctx := decisionCtx(meta, ov)
	ngl, ncpuMoe := decisionPlacement(meta, ov)
	lines := []string{
		embeddingExe(s, ov),
		fmt.Sprintf("-m %s", strings.ReplaceAll(row.FullPath, "\\", "/")),
		"--port ${PORT}",
		"--host 127.0.0.1",
		corsOriginsFlag,
		fmt.Sprintf("-ngl %d", ngl),
	}
	if ncpuMoe > 0 {
		lines = append(lines, fmt.Sprintf("--n-cpu-moe %d", ncpuMoe))
	}
	if ov.CpuDevice() {
		lines = append(lines, "--device none")
	}
	lines = append(lines, fmt.Sprintf("-c %d", ctx))
	if ub, whole := decisionUb(meta, ov); whole {
		lines = append(lines, fmt.Sprintf("-b %d -ub %d", ub, ub))
	}
	if p, _ := decisionMmproj(row, ov); p != "" {
		lines = append(lines, fmt.Sprintf("--mmproj %s", strings.ReplaceAll(p, "\\", "/")))
	}
	lines = append(lines,
		fmt.Sprintf("-t %d", threads),
		"--no-warmup --no-ui --metrics --props",
	)
	if ov != nil {
		if extra := strings.TrimSpace(ov.ExtraArgs); extra != "" {
			lines = append(lines, extra)
		}
	}
	return lines
}

// decisionEstVramGB is weights + the window's KV at f16 + the compute buffer at
// the model's real -ub, plus the projector when one loads. Unlike an embedder, a
// causal decision model (kev, openjev) keeps a real KV cache over the whole
// state. The compute buffer is the same model every llama profile is charged
// with (computeBufferGB), because a whole-batch type runs at -ub = ctx and a flat
// pad cannot follow that; those also pay decisionAttnScoresGB.
func decisionEstVramGB(s Settings, row GgufRow, ov *Override, meta Metadata) float64 {
	ub, whole := decisionUb(meta, ov)
	share := decisionGpuShare(meta, ov)
	est := row.SizeGB*share + computeBufferGB(meta, ub, s.ComputeBufFactor)
	if whole {
		est += decisionAttnScoresGB(meta, ub)
	}
	if kv := GetKvCostModel(meta, "f16", "f16"); kv.OK {
		est += (kv.SlopeGB*float64(decisionCtx(meta, ov)) + kv.ConstGB) * share
	}
	if p, gb := decisionMmproj(row, ov); p != "" {
		est += MmprojVramGB(p, gb, s)
	}
	return est
}

// decisionEstGB splits the footprint between VRAM and RAM. A CPU-device pin
// moves all of it (compute buffer included) to RAM; a partial offload leaves
// the offloaded share of the weights and KV there.
func decisionEstGB(s Settings, row GgufRow, ov *Override, meta Metadata) (vram, ram float64) {
	if ov == nil || (!ov.CpuDevice() && ov.CpuOffload <= 0) {
		return decisionEstVramGB(s, row, ov, meta), 0
	}
	gpu := *ov
	gpu.Device, gpu.CpuOffload = "", 0
	full := decisionEstVramGB(s, row, &gpu, meta)
	if ov.CpuDevice() {
		return 0, full
	}
	vram = decisionEstVramGB(s, row, ov, meta)
	return vram, full - vram
}

// emitDecisionModel writes the YAML entry. capabilities.decision is what
// /v1/models turns into output_modalities ["decisions"] (llama-server's own
// marker), which puts the model in the UI's Decision category and keeps it out
// of every chat picker.
func emitDecisionModel(b *strings.Builder, s Settings, row GgufRow, ov *Override, name string, meta Metadata, emitted *[]string) {
	fmt.Fprintf(b, "\n  # arch=%s size=%gGB (decision model, llama-server /v1/systemone, decision.type=%s)\n", meta.Architecture, row.SizeGB, meta.DecisionType)
	fmt.Fprintf(b, "  %q:\n", name)
	b.WriteString("    cmd: >\n")
	for _, line := range decisionCmdLines(s, row, ov, meta) {
		fmt.Fprintf(b, "      %s\n", line)
	}
	fmt.Fprintf(b, "    ttl: %d\n", s.TtlSec)
	writeSingleDeviceEnv(b, s, embeddingExe(s, ov))
	vram, ram := decisionEstGB(s, row, ov, meta)
	writeEstVram(b, vram)
	writeEstRam(b, ram)
	if ov != nil && ov.Unlisted {
		b.WriteString("    unlisted: true\n")
	}
	b.WriteString("    capabilities:\n")
	if p, _ := decisionMmproj(row, ov); p != "" {
		b.WriteString("      in: [text, image]\n")
	}
	b.WriteString("      decision: true\n")
	fmt.Fprintf(b, "      decisionType: %q\n", meta.DecisionType)
	fmt.Fprintf(b, "      context: %d\n", decisionCtx(meta, ov))
	writeDisplayName(b, s, name)
	*emitted = append(*emitted, name)
}