mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
common/fit.cpp abandonne sur LLAMA_SPLIT_MODE_TENSOR comme sur ROW (« not implemented, abort ») : avec -sm tensor, la marge partait quand même et la note annonçait un --fit-target qui ne servait à rien — de quoi mesurer deux fois le même placement. À l'inverse, -ngl -1 est la valeur par défaut du moteur (celle qu'écrit « auto ») : fit tourne, et la clé était refusée à tort. - fitBlocker : row ou tensor, en drapeau ou en LLAMA_ARG_SPLIT_MODE ; -1 traité comme auto. - tests : -sm tensor, LLAMA_ARG_SPLIT_MODE=tensor, -sm layer, -ngl -1. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
219 lines
11 KiB
Go
219 lines
11 KiB
Go
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: "-sm tensor : fit abandonne aussi, rien",
|
|
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "--split-mode tensor"}, si: fit, wantNotes: 1},
|
|
{name: "LLAMA_ARG_SPLIT_MODE=tensor : rien",
|
|
cfg: map[string]string{"FIT_TARGET": "1024"},
|
|
si: serveSysInfo{Help: helpFit, ArgEnv: map[string]string{"LLAMA_ARG_SPLIT_MODE": "tensor"}}, wantNotes: 1},
|
|
{name: "-sm layer : fit tourne",
|
|
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "-sm layer"}, si: fit,
|
|
want: []string{"--fit-target", "1024"}, wantNotes: 1},
|
|
{name: "-ngl -1 (le défaut du moteur) : fit tourne",
|
|
cfg: map[string]string{"FIT_TARGET": "1024", "EXTRA_ARGS": "-ngl -1"}, si: fit,
|
|
want: []string{"--fit-target", "1024"}, 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)
|
|
}
|
|
}
|