mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Moteur : trois réglages d'expert sur demande — marge VRAM par carte (FIT_TARGET), seuil d'offload des experts, branches Q/K/V parallèles
Sur deux cartes inégales (5060 Ti + 3060) ou un MoE aux experts sur CPU, il reste quelques leviers de placement que llama.cpp expose mais que Loki ne savait pas poser. Aucun ne touche au modèle (poids, contexte, cache, échantillonnage) ; tous restent éteints tant que le preset ne les demande pas, et le gain se décide à la mesure. - FIT_TARGET=1024,3072 → --fit-target : marge libre par carte pour --fit. Validée (entiers, 8 valeurs au plus, jamais transmise brute : stoull ferait mourir le moteur en boucle), remontée à 1024 Mio au minimum. Seulement si l'aide la liste, sans -fitt ni LLAMA_ARG_FIT_TARGET déjà posés, avec un contexte chiffré (CTX=0 laisserait fit réduire le contexte) et si --fit tournera vraiment : NGL chiffré, --tensor-split, -ot, --n-cpu-moe, -cmoe, --fit off ou -sm row le coupent, et on le dit au lieu de ne rien faire. - OP_OFFLOAD_MIN_BATCH=N → GGML_OP_OFFLOAD_MIN_BATCH : les lots de moins de N jetons restent sur CPU pour les poids en RAM. Utile pour des brouillons ngram de 32 jetons ou plus ; MTP vérifie déjà sous 32. - CUDA_GRAPH_OPT=on → GGML_CUDA_GRAPH_OPT=1 : expérimental, sortie identique en amont, gain de 0 à quelques % ; refusé si -sm row/tensor ou un -ot sur l'attention pouvait séparer Q, K et V d'une couche. Le lancement rappelle « off puis redémarrer » en cas de GGML_ASSERT. - une variable déjà dans l'environnement n'est jamais touchée ; elles ne vont qu'à llama-server via « loki serve ». - tensorOverride, extrait de pipelineBlocker, sert aussi à la garde de --fit. - --backend-sampling écarté : pas identique au bit près, et inactif sur les requêtes à outils (grammaire) qui font l'essentiel du trafic. - clés documentées dans le squelette de « loki edit » ; tests de table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2de20e0ab2
commit
9aa426a389
5 files changed
+519
-14
No files matched your search
@@ -340,6 +340,7 @@ func cmdServe(args []string) error {
|
||||
// Deux GPU ou plus : de quoi décider des files de lancement CUDA (voir
|
||||
// launchQueuesEnv). Après la sélection GPU, qui fait partie de la réponse.
|
||||
probeServeGPUs(cfg, extra, &si)
|
||||
probeExpertEnv(&si)
|
||||
|
||||
llmArgs, env, notes := buildServeArgs(cfg, extra, bin, si)
|
||||
applyServeEnv(env)
|
||||
@@ -381,7 +382,8 @@ type serveSysInfo struct {
|
||||
|
||||
LaunchQueues string // CUDA_SCALE_LAUNCH_QUEUES déjà dans l'environnement : choix de l'utilisateur, intouché
|
||||
GPUs int // GPU visibles (CUDA_VISIBLE_DEVICES, sinon nvidia-smi) ; 0 = inconnu ou non sondé
|
||||
ArgEnv map[string]string // LLAMA_ARG_* de l'environnement qui décident du pipeline (voir serveArgEnv)
|
||||
ArgEnv map[string]string // LLAMA_ARG_* de l'environnement qui décident du pipeline et de --fit (serveArgEnv, fitArgEnv)
|
||||
UserEnv map[string]string // GGML_* des réglages d'expert déjà posés (voir expertEnvKeys) : intouchés
|
||||
}
|
||||
|
||||
// cudaDeviceEnv : sélection GPU (loki gpu), on filtre les devices visibles par
|
||||
@@ -450,6 +452,17 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
|
||||
env["CUDA_SCALE_LAUNCH_QUEUES"] = q
|
||||
}
|
||||
notes = append(notes, qNotes...)
|
||||
// Réglages d'expert passés par variable (voir backend_serve_expert.go) :
|
||||
// absents du preset, ils ne posent rien.
|
||||
g, gNotes := graphOptEnv(cfg, extra, si)
|
||||
if g != "" {
|
||||
env["GGML_CUDA_GRAPH_OPT"] = g
|
||||
}
|
||||
o, oNotes := opOffloadMinBatchEnv(cfg, si)
|
||||
if o != "" {
|
||||
env["GGML_OP_OFFLOAD_MIN_BATCH"] = o
|
||||
}
|
||||
notes = append(append(notes, gNotes...), oNotes...)
|
||||
|
||||
// Threads : vide ou 0 = AUCUN drapeau, pour que llama.cpp prenne ses cœurs
|
||||
// physiques au lieu de tous les threads logiques (voir threadArgs).
|
||||
@@ -504,6 +517,11 @@ func buildServeArgs(cfg map[string]string, extra []string, bin string, si serveS
|
||||
}
|
||||
args = append(args, ngl...)
|
||||
}
|
||||
// Marge VRAM par carte du placement automatique (FIT_TARGET, voir
|
||||
// fitTargetArgs) : seulement si --fit tournera vraiment.
|
||||
fitt, fitNotes := fitTargetArgs(cfg, extra, si)
|
||||
args = append(args, fitt...)
|
||||
notes = append(notes, fitNotes...)
|
||||
if ktv != "" {
|
||||
args = append(args, "-ctk", ktv)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Réglages d'expert du moteur local, tous éteints tant que le preset ne les
|
||||
// demande pas. Aucun ne touche au modèle : ni poids, ni contexte, ni
|
||||
// échantillonnage — seulement OÙ et COMMENT le même calcul s'exécute.
|
||||
//
|
||||
// FIT_TARGET=1024,3072 → --fit-target : marge VRAM laissée libre par carte
|
||||
// OP_OFFLOAD_MIN_BATCH=N → GGML_OP_OFFLOAD_MIN_BATCH : seuil d'envoi au GPU
|
||||
// des poids restés en RAM
|
||||
// CUDA_GRAPH_OPT=on → GGML_CUDA_GRAPH_OPT=1 : branches Q/K/V en parallèle
|
||||
//
|
||||
// Une variable déjà présente dans l'environnement (gabarit Docker/Unraid) est
|
||||
// un choix de l'utilisateur : jamais touchée. Les variables passent par
|
||||
// l'environnement du seul « loki serve », qui devient llama-server : le reste
|
||||
// du conteneur ne les voit pas.
|
||||
|
||||
// expertEnvKeys : les variables du moteur que ces réglages posent.
|
||||
var expertEnvKeys = []string{"GGML_CUDA_GRAPH_OPT", "GGML_OP_OFFLOAD_MIN_BATCH"}
|
||||
|
||||
// fitArgEnv : les LLAMA_ARG_* qui, sans drapeau, décident de --fit et de sa
|
||||
// marge. llama.cpp les applique quand la ligne de commande se tait.
|
||||
var fitArgEnv = []string{"LLAMA_ARG_FIT", "LLAMA_ARG_FIT_TARGET", "LLAMA_ARG_TENSOR_SPLIT"}
|
||||
|
||||
// probeExpertEnv relève ce que l'environnement a déjà tranché. Appelée après
|
||||
// probeServeGPUs, dont elle complète ArgEnv.
|
||||
func probeExpertEnv(si *serveSysInfo) {
|
||||
si.UserEnv = map[string]string{}
|
||||
for _, k := range expertEnvKeys {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
si.UserEnv[k] = v
|
||||
}
|
||||
}
|
||||
if si.ArgEnv == nil {
|
||||
si.ArgEnv = map[string]string{}
|
||||
}
|
||||
for _, k := range fitArgEnv {
|
||||
if v := os.Getenv(k); v != "" {
|
||||
si.ArgEnv[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fitTargetMin : la marge par défaut de llama.cpp (common.h), en Mio. En
|
||||
// dessous, fit range plus serré que ce qu'il sait estimer — le tampon du modèle
|
||||
// MTP, qu'il ne mesure pas toujours, suffit alors à ramener le « cudaMalloc
|
||||
// failed: out of memory » au premier long prompt. On remonte, en le disant.
|
||||
const fitTargetMin = 1024
|
||||
|
||||
// fitTargetMaxValues : llama.cpp refuse (et le serveur meurt en boucle) une
|
||||
// liste aussi longue que son nombre maximal de cartes ; 8 reste loin du bord.
|
||||
const fitTargetMaxValues = 8
|
||||
|
||||
// parseFitTarget valide FIT_TARGET : des entiers en Mio séparés par « , » ou
|
||||
// « / », une valeur par carte dans l'ordre que voit le moteur (une seule vaut
|
||||
// pour toutes). Jamais transmis brut : llama.cpp lit chaque valeur avec
|
||||
// std::stoull, qui lève sur le moindre caractère parasite — moteur mort au
|
||||
// démarrage, relancé en boucle par systemd ou Docker.
|
||||
func parseFitTarget(v string) (vals []int, raised bool, ok bool) {
|
||||
parts := strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == '/' })
|
||||
if len(parts) == 0 || len(parts) > fitTargetMaxValues ||
|
||||
strings.Count(v, ",")+strings.Count(v, "/") != len(parts)-1 {
|
||||
return nil, false, false
|
||||
}
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p == "" || strings.Trim(p, "0123456789") != "" || len(p) > 7 {
|
||||
return nil, false, false
|
||||
}
|
||||
n, _ := strconv.Atoi(p)
|
||||
if n < fitTargetMin {
|
||||
n, raised = fitTargetMin, true
|
||||
}
|
||||
vals = append(vals, n)
|
||||
}
|
||||
return vals, raised, true
|
||||
}
|
||||
|
||||
// fitTargetArgs traduit FIT_TARGET en --fit-target. Fonction pure.
|
||||
//
|
||||
// La marge règle combien de VRAM --fit laisse libre sur chaque carte ; sur deux
|
||||
// cartes inégales (5060 Ti + 3060), une marge plus large sur la lente pousse
|
||||
// plus de couches vers la rapide. Placement seulement : mêmes poids, mêmes
|
||||
// types de cache, même contexte. Rien n'est posé quand :
|
||||
//
|
||||
// - le moteur ne connaît pas --fit-target (un drapeau inconnu le tue) ;
|
||||
// - EXTRA_ARGS ou LLAMA_ARG_FIT_TARGET l'ont déjà fixée ;
|
||||
// - le contexte n'est pas un nombre positif : avec -c 0, fit a le droit de
|
||||
// RÉDUIRE le contexte pour tenir, et une marge plus large l'y pousserait
|
||||
// sans un mot — de l'information perdue pour le modèle ;
|
||||
// - --fit ne tournera pas (voir fitBlocker) : la clé ne ferait rien, et une
|
||||
// comparaison A/B mesurerait deux fois le même placement.
|
||||
func fitTargetArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, notes []string) {
|
||||
raw := strings.TrimSpace(cfg["FIT_TARGET"])
|
||||
if raw == "" {
|
||||
return nil, nil
|
||||
}
|
||||
ignore := func(why string) (a, n []string) {
|
||||
return nil, []string{"FIT_TARGET=" + raw + " ignoré : " + why}
|
||||
}
|
||||
vals, raised, ok := parseFitTarget(raw)
|
||||
switch {
|
||||
case !ok:
|
||||
return ignore("des Mio par carte, ex. 1024,3072 (8 valeurs au plus)")
|
||||
case !strings.Contains(si.Help, "--fit-target"):
|
||||
return ignore("ce moteur ne connaît pas --fit-target")
|
||||
case hasAnyFlag(extra, "-fitt", "--fit-target"):
|
||||
return ignore("--fit-target déjà dans EXTRA_ARGS")
|
||||
case si.ArgEnv["LLAMA_ARG_FIT_TARGET"] != "":
|
||||
return ignore("LLAMA_ARG_FIT_TARGET déjà posé dans l'environnement")
|
||||
}
|
||||
ctx := flagValue(extra, "-c", "--ctx-size")
|
||||
if ctx == "" {
|
||||
ctx = strings.TrimSpace(cfg["CTX"])
|
||||
if ctx == "" {
|
||||
ctx = "32768"
|
||||
}
|
||||
}
|
||||
if n, err := strconv.Atoi(ctx); err != nil || n <= 0 {
|
||||
return ignore("contexte « " + ctx + " » non fixé — --fit pourrait le réduire pour tenir ; donne un CTX chiffré")
|
||||
}
|
||||
if why := fitBlocker(cfg, extra, si); why != "" {
|
||||
return ignore("--fit inactif (" + why + "), la marge ne servirait à rien")
|
||||
}
|
||||
s := make([]string, len(vals))
|
||||
for i, n := range vals {
|
||||
s[i] = strconv.Itoa(n)
|
||||
}
|
||||
v := strings.Join(s, ",")
|
||||
note := "--fit-target " + v + " (FIT_TARGET, Mio libres par carte, dans l'ordre vu par le moteur) — " +
|
||||
"vérifie « offloaded N/N layers » dans le journal"
|
||||
if raised {
|
||||
note += " ; marges sous 1024 Mio remontées à 1024"
|
||||
}
|
||||
return []string{"--fit-target", v}, []string{note}
|
||||
}
|
||||
|
||||
// fitBlocker dit pourquoi llama.cpp ne lancera PAS --fit (common/fit.cpp :
|
||||
// « already set by user, abort »), ou vide s'il le lancera. Les couches GPU
|
||||
// sont lues comme buildServeArgs les écrira : EXTRA_ARGS d'abord, sinon la
|
||||
// traduction de NGL par nglArgs (999 y devient auto), sinon — NGL=auto, aucun
|
||||
// drapeau — la variable du moteur.
|
||||
func fitBlocker(cfg map[string]string, extra []string, si serveSysInfo) string {
|
||||
fit := flagValue(extra, "-fit", "--fit")
|
||||
if fit == "" {
|
||||
fit = si.ArgEnv["LLAMA_ARG_FIT"]
|
||||
}
|
||||
switch strings.ToLower(strings.TrimSpace(fit)) {
|
||||
case "off", "disabled", "false", "0":
|
||||
return "--fit off"
|
||||
}
|
||||
ngl := flagValue(extra, "-ngl", "--n-gpu-layers", "--gpu-layers")
|
||||
if ngl == "" {
|
||||
if a, _ := nglArgs(cfg["NGL"], helpFitsLayersItself(si.Help)); len(a) == 2 {
|
||||
ngl = a[1]
|
||||
} else {
|
||||
ngl = si.ArgEnv["LLAMA_ARG_N_GPU_LAYERS"]
|
||||
}
|
||||
}
|
||||
if ngl = strings.TrimSpace(ngl); ngl != "" && !strings.EqualFold(ngl, "auto") {
|
||||
return "couches GPU fixées à " + ngl
|
||||
}
|
||||
if hasAnyFlag(extra, "-ts", "--tensor-split") || si.ArgEnv["LLAMA_ARG_TENSOR_SPLIT"] != "" {
|
||||
return "--tensor-split"
|
||||
}
|
||||
if r := tensorOverride(extra, si.ArgEnv); r != "" {
|
||||
return r
|
||||
}
|
||||
sm := flagValue(extra, "-sm", "--split-mode")
|
||||
if sm == "" {
|
||||
sm = si.ArgEnv["LLAMA_ARG_SPLIT_MODE"]
|
||||
}
|
||||
if strings.EqualFold(strings.TrimSpace(sm), "row") {
|
||||
return "-sm row"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// opOffloadMinBatchEnv traduit OP_OFFLOAD_MIN_BATCH en GGML_OP_OFFLOAD_MIN_BATCH.
|
||||
// Fonction pure.
|
||||
//
|
||||
// Quand des poids restent en RAM (experts MoE sur CPU), llama.cpp recopie vers
|
||||
// le GPU ceux dont un lot a besoin dès qu'il compte au moins 32 jetons ; en
|
||||
// dessous, il calcule sur CPU. Monter le seuil garde sur CPU les lots plus
|
||||
// courts : utile seulement si une spéculation ngram propose des brouillons de
|
||||
// 32 jetons ou plus (les lots de vérification MTP, quelques jetons, restent
|
||||
// déjà sous 32). Le prix : un tour d'outil de 32 à N-1 jetons se calcule lui
|
||||
// aussi sur CPU. N se trouve à la mesure, pas d'office. Même distribution ;
|
||||
// les logits ne sont pas identiques au bit près, comme déjà entre décodage CPU
|
||||
// et prefill GPU.
|
||||
func opOffloadMinBatchEnv(cfg map[string]string, si serveSysInfo) (val string, notes []string) {
|
||||
raw := strings.TrimSpace(cfg["OP_OFFLOAD_MIN_BATCH"])
|
||||
if raw == "" || si.UserEnv["GGML_OP_OFFLOAD_MIN_BATCH"] != "" {
|
||||
return "", nil
|
||||
}
|
||||
n, err := strconv.Atoi(raw)
|
||||
if err != nil || n < 1 || n > 1<<20 {
|
||||
return "", []string{"OP_OFFLOAD_MIN_BATCH=" + raw + " ignoré : un nombre de jetons, ex. 128"}
|
||||
}
|
||||
v := strconv.Itoa(n)
|
||||
return v, []string{"GGML_OP_OFFLOAD_MIN_BATCH=" + v + " (OP_OFFLOAD_MIN_BATCH) : lots de moins de " + v +
|
||||
" jetons calculés sur CPU pour les poids restés en RAM"}
|
||||
}
|
||||
|
||||
// graphOptEnv traduit CUDA_GRAPH_OPT=on en GGML_CUDA_GRAPH_OPT=1. Fonction pure.
|
||||
//
|
||||
// Au décodage, llama.cpp lance alors les trois branches Q, K et V de chaque
|
||||
// couche sur des flux CUDA concurrents. Chaque opération garde son noyau et ses
|
||||
// entrées : sortie identique au jeton près en amont (PR #28198, perplexité
|
||||
// inchangée). Gain modeste — quelques % sur un MoE tout GPU, rien sur un dense,
|
||||
// dilué quand les experts tournent sur CPU ou que MTP vérifie par lots. Sans
|
||||
// effet avant #28198 en multi-GPU ou si les graphes CUDA sont coupés.
|
||||
//
|
||||
// Expérimental : sur une architecture qu'il n'a pas vue, le repérage des
|
||||
// branches peut s'arrêter sur un GGML_ASSERT, et le moteur redémarre en boucle.
|
||||
// Une variable ne se détecte pas dans l'aide du moteur : on le dit au
|
||||
// lancement, pour que « CUDA_GRAPH_OPT=off puis redémarrer » vienne à l'esprit.
|
||||
//
|
||||
// Refusé quand Q, K et V d'une même couche pourraient finir sur deux cartes
|
||||
// (-sm row/tensor, -ot visant l'attention) : #28198 en fait une condition
|
||||
// d'exactitude.
|
||||
func graphOptEnv(cfg map[string]string, extra []string, si serveSysInfo) (val string, notes []string) {
|
||||
raw := strings.TrimSpace(cfg["CUDA_GRAPH_OPT"])
|
||||
switch strings.ToLower(raw) {
|
||||
case "", "off":
|
||||
return "", nil
|
||||
case "on":
|
||||
default:
|
||||
return "", []string{"CUDA_GRAPH_OPT=" + raw + " ignoré (on ou off)"}
|
||||
}
|
||||
if si.UserEnv["GGML_CUDA_GRAPH_OPT"] != "" {
|
||||
return "", nil
|
||||
}
|
||||
if why := qkvSplitRisk(extra, si.ArgEnv); why != "" {
|
||||
return "", []string{"CUDA_GRAPH_OPT ignoré : " + why + " peut séparer Q, K et V d'une couche entre cartes"}
|
||||
}
|
||||
return "1", []string{"GGML_CUDA_GRAPH_OPT=1 (CUDA_GRAPH_OPT, expérimental) — si le moteur s'arrête sur un " +
|
||||
"GGML_ASSERT, CUDA_GRAPH_OPT=off puis redémarre"}
|
||||
}
|
||||
|
||||
// qkvSplitRisk : un placement qui pourrait répartir les tenseurs d'attention
|
||||
// d'une même couche sur plusieurs appareils. La découpe par couches (défaut,
|
||||
// --tensor-split compris) garde chaque couche entière : rien à craindre.
|
||||
func qkvSplitRisk(extra []string, argEnv map[string]string) string {
|
||||
sm := flagValue(extra, "-sm", "--split-mode")
|
||||
if sm == "" {
|
||||
sm = argEnv["LLAMA_ARG_SPLIT_MODE"]
|
||||
}
|
||||
if sm = strings.ToLower(strings.TrimSpace(sm)); sm == "row" || sm == "tensor" {
|
||||
return "-sm " + sm
|
||||
}
|
||||
for _, v := range append(flagValues(extra, "-ot", "--override-tensor"), argEnv["LLAMA_ARG_OVERRIDE_TENSOR"]) {
|
||||
if strings.Contains(strings.ToLower(v), "attn") {
|
||||
return "-ot sur l'attention"
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,207 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// helpFit : un moteur récent qui connaît --fit et sa marge.
|
||||
const helpFit = helpRecent + `-fit, --fit [on|off] whether to adjust unset arguments to fit in device memory
|
||||
-fitt, --fit-target MiB0,MiB1,... target margin per device for --fit (default: 1024)
|
||||
`
|
||||
|
||||
// TestFitTargetArgs : --fit-target n'est passé que validé, sur un moteur qui le
|
||||
// connaît, avec un contexte fixé, et quand --fit tournera vraiment.
|
||||
func TestFitTargetArgs(t *testing.T) {
|
||||
fit := serveSysInfo{Help: helpFit}
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg map[string]string
|
||||
si serveSysInfo
|
||||
want []string
|
||||
wantNotes int
|
||||
}{
|
||||
{name: "clé absente : rien, sans un mot",
|
||||
cfg: map[string]string{}, si: fit},
|
||||
{name: "deux marges : passées, et on le dit",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024,3072"}, si: fit,
|
||||
want: []string{"--fit-target", "1024,3072"}, wantNotes: 1},
|
||||
{name: "séparateur / : normalisé en virgules",
|
||||
cfg: map[string]string{"FIT_TARGET": "2048/1536"}, si: fit,
|
||||
want: []string{"--fit-target", "2048,1536"}, wantNotes: 1},
|
||||
{name: "une seule valeur : vaut pour toutes les cartes",
|
||||
cfg: map[string]string{"FIT_TARGET": " 2048 "}, si: fit,
|
||||
want: []string{"--fit-target", "2048"}, wantNotes: 1},
|
||||
{name: "marge sous le défaut : remontée à 1024",
|
||||
cfg: map[string]string{"FIT_TARGET": "256,3072"}, si: fit,
|
||||
want: []string{"--fit-target", "1024,3072"}, wantNotes: 1},
|
||||
{name: "valeur non numérique : jamais transmise (stoull ferait mourir le moteur)",
|
||||
cfg: map[string]string{"FIT_TARGET": "1G,2G"}, si: fit, wantNotes: 1},
|
||||
{name: "valeur négative : refusée",
|
||||
cfg: map[string]string{"FIT_TARGET": "-1024"}, si: fit, wantNotes: 1},
|
||||
{name: "séparateur doublé : refusé",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024,,2048"}, si: fit, wantNotes: 1},
|
||||
{name: "trop de valeurs : refusé",
|
||||
cfg: map[string]string{"FIT_TARGET": "1,2,3,4,5,6,7,8,9"}, si: fit, wantNotes: 1},
|
||||
{name: "moteur sans --fit-target : rien, le drapeau le tuerait",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024,3072"}, si: serveSysInfo{Help: helpRecent}, wantNotes: 1},
|
||||
{name: "-fitt déjà dans EXTRA_ARGS : l'utilisateur a tranché",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "-fitt 2048"}, si: fit, wantNotes: 1},
|
||||
{name: "LLAMA_ARG_FIT_TARGET dans l'environnement : intouché",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024"},
|
||||
si: serveSysInfo{Help: helpFit, ArgEnv: map[string]string{"LLAMA_ARG_FIT_TARGET": "4096"}}, wantNotes: 1},
|
||||
{name: "CTX=0 : fit pourrait réduire le contexte, refusé",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024,3072", "CTX": "0"}, si: fit, wantNotes: 1},
|
||||
{name: "-c 0 dans EXTRA_ARGS l'emporte sur CTX : refusé",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "CTX": "65536", "EXTRA_ARGS": "-c 0"}, si: fit, wantNotes: 1},
|
||||
{name: "CTX absent : Loki passe -c 32768, accepté",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024"}, si: fit,
|
||||
want: []string{"--fit-target", "1024"}, wantNotes: 1},
|
||||
{name: "NGL=999 : devient -ngl auto, fit tourne",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "NGL": "999"}, si: fit,
|
||||
want: []string{"--fit-target", "1024"}, wantNotes: 1},
|
||||
{name: "NGL chiffré : fit abandonne, rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "NGL": "40"}, si: fit, wantNotes: 1},
|
||||
{name: "NGL=all : fit abandonne, rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "NGL": "all"}, si: fit, wantNotes: 1},
|
||||
{name: "-ngl auto dans EXTRA_ARGS : fit tourne",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "NGL": "40", "EXTRA_ARGS": "-ngl auto"}, si: fit,
|
||||
want: []string{"--fit-target", "1024"}, wantNotes: 1},
|
||||
{name: "NGL=auto et LLAMA_ARG_N_GPU_LAYERS=30 : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "NGL": "auto"},
|
||||
si: serveSysInfo{Help: helpFit, ArgEnv: map[string]string{"LLAMA_ARG_N_GPU_LAYERS": "30"}}, wantNotes: 1},
|
||||
{name: "--tensor-split : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "--tensor-split 0.965,0.035"}, si: fit, wantNotes: 1},
|
||||
{name: "preset MoE (-ot, --n-cpu-moe) : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": `-ot "per_layer_token_embd=CPU" --n-cpu-moe 40`},
|
||||
si: fit, wantNotes: 1},
|
||||
{name: "--cpu-moe : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "-cmoe"}, si: fit, wantNotes: 1},
|
||||
{name: "--fit off : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "--fit off"}, si: fit, wantNotes: 1},
|
||||
{name: "LLAMA_ARG_FIT=off : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024"},
|
||||
si: serveSysInfo{Help: helpFit, ArgEnv: map[string]string{"LLAMA_ARG_FIT": "off"}}, wantNotes: 1},
|
||||
{name: "-sm row : rien",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "-sm row"}, si: fit, wantNotes: 1},
|
||||
{name: "--device choisi : le placement reste à fit, accepté",
|
||||
cfg: map[string]string{"FIT_TARGET": "1024,3072", "EXTRA_ARGS": "--device CUDA1,CUDA0"}, si: fit,
|
||||
want: []string{"--fit-target", "1024,3072"}, wantNotes: 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, notes := fitTargetArgs(c.cfg, splitArgs(c.cfg["EXTRA_ARGS"]), c.si)
|
||||
if !reflect.DeepEqual(got, c.want) {
|
||||
t.Fatalf("args = %q, attendu %q", got, c.want)
|
||||
}
|
||||
if len(notes) != c.wantNotes {
|
||||
t.Fatalf("notes = %q, en attendait %d", notes, c.wantNotes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestGraphOptEnv : GGML_CUDA_GRAPH_OPT=1 seulement sur demande, jamais
|
||||
// par-dessus l'environnement, jamais quand Q/K/V pourraient changer de carte.
|
||||
func TestGraphOptEnv(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg map[string]string
|
||||
si serveSysInfo
|
||||
want string
|
||||
wantNotes int
|
||||
}{
|
||||
{name: "clé absente : rien", cfg: map[string]string{}},
|
||||
{name: "off : rien", cfg: map[string]string{"CUDA_GRAPH_OPT": "off"}},
|
||||
{name: "on : posé, et on le dit", cfg: map[string]string{"CUDA_GRAPH_OPT": "on"}, want: "1", wantNotes: 1},
|
||||
{name: "ON (casse libre) : posé", cfg: map[string]string{"CUDA_GRAPH_OPT": "ON"}, want: "1", wantNotes: 1},
|
||||
{name: "valeur inconnue : ignorée, dite", cfg: map[string]string{"CUDA_GRAPH_OPT": "2"}, wantNotes: 1},
|
||||
{name: "variable déjà posée (même à 0) : intouchée",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on"},
|
||||
si: serveSysInfo{UserEnv: map[string]string{"GGML_CUDA_GRAPH_OPT": "0"}}},
|
||||
{name: "découpe par couches + --tensor-split : couches entières, posé",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on", "EXTRA_ARGS": "--device CUDA1,CUDA0 -ts 0.6,0.4"},
|
||||
want: "1", wantNotes: 1},
|
||||
{name: "-sm row : refusé",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on", "EXTRA_ARGS": "-sm row"}, wantNotes: 1},
|
||||
{name: "LLAMA_ARG_SPLIT_MODE=tensor : refusé",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on"},
|
||||
si: serveSysInfo{ArgEnv: map[string]string{"LLAMA_ARG_SPLIT_MODE": "tensor"}}, wantNotes: 1},
|
||||
{name: "-ot sur l'attention : refusé",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on", "EXTRA_ARGS": `-ot "blk\.1\.attn_k.*=CUDA1"`}, wantNotes: 1},
|
||||
{name: "-ot des experts seulement : posé",
|
||||
cfg: map[string]string{"CUDA_GRAPH_OPT": "on", "EXTRA_ARGS": `-ot "ffn_.*_exps=CPU" --n-cpu-moe 40`},
|
||||
want: "1", wantNotes: 1},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, notes := graphOptEnv(c.cfg, splitArgs(c.cfg["EXTRA_ARGS"]), c.si)
|
||||
if got != c.want {
|
||||
t.Fatalf("valeur = %q, attendu %q", got, c.want)
|
||||
}
|
||||
if len(notes) != c.wantNotes {
|
||||
t.Fatalf("notes = %q, en attendait %d", notes, c.wantNotes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestOpOffloadMinBatchEnv : la variable n'existe que si la clé est posée et
|
||||
// valide, et ne remplace jamais celle de l'environnement.
|
||||
func TestOpOffloadMinBatchEnv(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
cfg map[string]string
|
||||
si serveSysInfo
|
||||
want string
|
||||
wantNotes int
|
||||
}{
|
||||
{name: "clé absente : rien", cfg: map[string]string{}},
|
||||
{name: "128 : posé", cfg: map[string]string{"OP_OFFLOAD_MIN_BATCH": "128"}, want: "128", wantNotes: 1},
|
||||
{name: "espaces autour : posé", cfg: map[string]string{"OP_OFFLOAD_MIN_BATCH": " 64 "}, want: "64", wantNotes: 1},
|
||||
{name: "0 : ignoré, dit", cfg: map[string]string{"OP_OFFLOAD_MIN_BATCH": "0"}, wantNotes: 1},
|
||||
{name: "texte : ignoré, dit", cfg: map[string]string{"OP_OFFLOAD_MIN_BATCH": "beaucoup"}, wantNotes: 1},
|
||||
{name: "variable déjà posée : intouchée",
|
||||
cfg: map[string]string{"OP_OFFLOAD_MIN_BATCH": "128"},
|
||||
si: serveSysInfo{UserEnv: map[string]string{"GGML_OP_OFFLOAD_MIN_BATCH": "32"}}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
got, notes := opOffloadMinBatchEnv(c.cfg, c.si)
|
||||
if got != c.want {
|
||||
t.Fatalf("valeur = %q, attendu %q", got, c.want)
|
||||
}
|
||||
if len(notes) != c.wantNotes {
|
||||
t.Fatalf("notes = %q, en attendait %d", notes, c.wantNotes)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Les trois clés absentes, la ligne de commande et l'environnement ne bougent
|
||||
// pas ; posées, --fit-target arrive avant EXTRA_ARGS et les variables vont
|
||||
// dans env, nulle part ailleurs.
|
||||
func TestBuildServeArgsExpertToggles(t *testing.T) {
|
||||
const bin, model = "/opt/llama/llama-server", "/models/Qwen3.6-27B-Q4_K_M.gguf"
|
||||
si := serveSysInfo{Help: helpFit, Model: model}
|
||||
cfg := map[string]string{"CTX": "65536", "EXTRA_ARGS": "--device CUDA0"}
|
||||
base, baseEnv, baseNotes := buildServeArgs(cfg, splitArgs(cfg["EXTRA_ARGS"]), bin, si)
|
||||
if len(baseEnv) != 0 || len(baseNotes) != 0 {
|
||||
t.Fatalf("sans les clés : env = %v, notes = %q", baseEnv, baseNotes)
|
||||
}
|
||||
on := map[string]string{"CTX": "65536", "EXTRA_ARGS": "--device CUDA0",
|
||||
"FIT_TARGET": "1024,3072", "OP_OFFLOAD_MIN_BATCH": "128", "CUDA_GRAPH_OPT": "on"}
|
||||
args, env, notes := buildServeArgs(on, splitArgs(on["EXTRA_ARGS"]), bin, si)
|
||||
n := len(base)
|
||||
want := append(append(append([]string{}, base[:n-2]...), "--fit-target", "1024,3072"), base[n-2:]...)
|
||||
if !reflect.DeepEqual(args, want) {
|
||||
t.Fatalf("args\n got %q\nwant %q", args, want)
|
||||
}
|
||||
wantEnv := map[string]string{"GGML_CUDA_GRAPH_OPT": "1", "GGML_OP_OFFLOAD_MIN_BATCH": "128"}
|
||||
if !reflect.DeepEqual(env, wantEnv) {
|
||||
t.Fatalf("env = %v, want %v", env, wantEnv)
|
||||
}
|
||||
if len(notes) != 3 {
|
||||
t.Fatalf("notes = %q", notes)
|
||||
}
|
||||
}
|
||||
@@ -114,19 +114,8 @@ func servedGPUCount(extra []string, si serveSysInfo) int {
|
||||
// drapeaux, chaque occurrence ajoute les siennes. Un --n-cpu-moe 0 n'annule
|
||||
// donc pas un LLAMA_ARG_N_CPU_MOE=30.
|
||||
func pipelineBlocker(cfg map[string]string, extra []string, argEnv map[string]string) string {
|
||||
if hasAnyFlag(extra, "-ot", "--override-tensor", "--cpu-moe", "-cmoe") ||
|
||||
argEnv["LLAMA_ARG_OVERRIDE_TENSOR"] != "" || envTruthy(argEnv["LLAMA_ARG_CPU_MOE"]) {
|
||||
return "surcharge de tenseurs"
|
||||
}
|
||||
for _, n := range []struct{ env, short, long string }{
|
||||
{"LLAMA_ARG_N_CPU_MOE", "-ncmoe", "--n-cpu-moe"},
|
||||
{"LLAMA_ARG_N_CPU_FFN", "-ncffn", "--n-cpu-ffn"},
|
||||
} {
|
||||
for _, v := range append(flagValues(extra, n.short, n.long), argEnv[n.env]) {
|
||||
if v = strings.TrimSpace(v); v != "" && v != "0" {
|
||||
return "couches " + n.long + " sur CPU"
|
||||
}
|
||||
}
|
||||
if r := tensorOverride(extra, argEnv); r != "" {
|
||||
return r
|
||||
}
|
||||
if !kvOffloaded(extra, argEnv) {
|
||||
return "cache KV sur CPU"
|
||||
@@ -155,6 +144,28 @@ func pipelineBlocker(cfg map[string]string, extra []string, argEnv map[string]st
|
||||
return ""
|
||||
}
|
||||
|
||||
// tensorOverride dit si des tenseurs sont placés à la main (-ot et ses formes
|
||||
// déguisées --cpu-moe, --n-cpu-moe, --n-cpu-ffn), en drapeau ou en variable.
|
||||
// Vide = aucun. Le pipeline entre cartes s'y arrête, et --fit aussi
|
||||
// (« tensor_buft_overrides already set by user, abort »).
|
||||
func tensorOverride(extra []string, argEnv map[string]string) string {
|
||||
if hasAnyFlag(extra, "-ot", "--override-tensor", "--cpu-moe", "-cmoe") ||
|
||||
argEnv["LLAMA_ARG_OVERRIDE_TENSOR"] != "" || envTruthy(argEnv["LLAMA_ARG_CPU_MOE"]) {
|
||||
return "surcharge de tenseurs"
|
||||
}
|
||||
for _, n := range []struct{ env, short, long string }{
|
||||
{"LLAMA_ARG_N_CPU_MOE", "-ncmoe", "--n-cpu-moe"},
|
||||
{"LLAMA_ARG_N_CPU_FFN", "-ncffn", "--n-cpu-ffn"},
|
||||
} {
|
||||
for _, v := range append(flagValues(extra, n.short, n.long), argEnv[n.env]) {
|
||||
if v = strings.TrimSpace(v); v != "" && v != "0" {
|
||||
return "couches " + n.long + " sur CPU"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// kvOffloaded : le cache KV reste-t-il sur GPU ? Le dernier -kvo/-nkvo de la
|
||||
// ligne de commande tranche ; sinon LLAMA_ARG_NO_KV_OFFLOAD, dont la SEULE
|
||||
// présence vaut « non » pour llama.cpp (quelle que soit sa valeur), puis
|
||||
|
||||
@@ -72,6 +72,12 @@ var configTemplate = []struct{ key, help string }{
|
||||
{"UBATCH", "micro-batch (défaut 512)"},
|
||||
{"THREADS", "threads CPU ; vide ou 0 = auto (cœurs physiques ; cœurs P seulement sur Intel hybride Linux)"},
|
||||
{"THREADS_BATCH", "threads CPU du prefill et de la vérification spéculative (MTP) ; vide ou 0 = comme THREADS"},
|
||||
{"FIT_TARGET", "Mio laissés libres par carte par le placement auto (--fit-target), ex. 1024,3072 dans l'ordre des cartes ; " +
|
||||
"vide = 1024, minimum 1024 ; sans effet si NGL chiffré, --tensor-split, -ot ou --n-cpu-moe"},
|
||||
{"OP_OFFLOAD_MIN_BATCH", "experts MoE sur CPU : taille de lot à partir de laquelle ils sont recopiés vers le GPU ; " +
|
||||
"vide = défaut du moteur (32) ; plus haut = lots courts sur CPU, à mesurer"},
|
||||
{"CUDA_GRAPH_OPT", "on = GGML_CUDA_GRAPH_OPT=1, branches Q/K/V en parallèle au décodage (expérimental, " +
|
||||
"sortie identique, gain de 0 à quelques %) ; off puis redémarrage s'il plante"},
|
||||
{"KV_TYPE", "quantization du cache KV (q8_0, q4_0…) ; KV_TYPE_K / KV_TYPE_V pour les séparer"},
|
||||
{"REASONING", "passthrough du mode raisonnement (on/auto/deepseek)"},
|
||||
{"REASONING_BUDGET", "plafond de tokens de réflexion ; -1 = illimité"},
|
||||
|
||||
Reference in new issue
Block a user