Files
Loki/internal/loki/backend_serve_flags_test.go
T
MichaelandClaude Opus 5.5 1db987844e Chargement du modèle : --mlock seul ne coupe plus le mmap
UN VRAI BUG D'ABORD
normalizeLoadFlags traduisait --mlock seul en « --load-mode mlock ». Or,
pour llama.cpp (common/arg.cpp), « mlock » veut dire PAS de mmap + résident :
le modèle entier montait en RAM au lieu d'être mappé. L'ancien --mlock, lui,
gardait le mmap — son équivalent est « mmap+mlock ». Sur un modèle plus gros
que la RAM (Qwen3.8-Flash-Next, 82 Go pour 64 Go), un preset qui avait
coché « Garder en RAM » tournait donc à l'OOM dès qu'un moteur récent
prenait le relais. Correspondance alignée sur AJEAN 0.16.0 :
  --mlock seul → mmap+mlock · --no-mmap → none · les deux → mlock.

DANS LES DEUX SENS
Un moteur ancien (ou un fork) ne connaît pas --load-mode et sort en erreur
si on le lui passe : downgradeLoadMode le retraduit en anciens drapeaux
(dio, inconnu de ces moteurs, est abandonné avec un avertissement).

UN SÉLECTEUR À LA PLACE DE DEUX INTERRUPTEURS
« Garder en RAM » et « Charger tout en mémoire » ne disaient pas leur
combinaison. L'éditeur de preset propose les six modes de llama.cpp (auto,
mmap, none, mlock, mmap+mlock, dio), avec ce que fait chacun en sous-titre,
et écrit la forme moderne. Un preset aux anciens drapeaux s'affiche sur le
bon mode sans modification.

Au passage, q5_1 porte la mention « lent sur CUDA » dans la liste du cache
KV (voir le commit du port occupé : non accéléré en Flash-Attention).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-02 23:05:34 +02:00

134 lines
4.6 KiB
Go

package loki
import (
"net"
"reflect"
"testing"
"time"
)
func TestHasAnyFlag(t *testing.T) {
args := []string{"--parallel", "4", "--load-mode=mlock", "-fa"}
if !hasAnyFlag(args, "--parallel", "-np") {
t.Fatal("--parallel devrait être détecté")
}
// Forme --clé=valeur : le nom seul compte.
if !hasAnyFlag(args, "--load-mode") {
t.Fatal("--load-mode=… devrait être détecté")
}
if hasAnyFlag(args, "-ngl", "--n-gpu-layers") {
t.Fatal("-ngl absent, ne devrait pas être détecté")
}
// Une VALEUR qui ressemble à un drapeau ne doit pas compter comme tel :
// ici "-ngl" n'est pas posé, seul --parallel l'est.
if hasAnyFlag(nil, "--parallel") {
t.Fatal("liste vide : rien à détecter")
}
}
func TestNormalizeLoadFlags(t *testing.T) {
cases := []struct {
name string
in []string
supports bool
want []string
}{
{"moteur ancien : anciens drapeaux intacts", []string{"--mlock", "--no-mmap"}, false,
[]string{"--mlock", "--no-mmap"}},
// L'ancien --mlock gardait le mmap : « mlock » de llama.cpp le coupe.
{"mlock seul → mmap+mlock", []string{"--mlock", "-fa"}, true,
[]string{"-fa", "--load-mode", "mmap+mlock"}},
{"no-mmap traduit", []string{"--no-mmap"}, true,
[]string{"--load-mode", "none"}},
{"mmap traduit", []string{"--mmap"}, true,
[]string{"--load-mode", "mmap"}},
{"mlock + no-mmap → mlock", []string{"--no-mmap", "--mlock"}, true,
[]string{"--load-mode", "mlock"}},
{"moteur ancien : none retraduit", []string{"-fa", "--load-mode", "none"}, false,
[]string{"-fa", "--no-mmap"}},
{"moteur ancien : mlock retraduit", []string{"--load-mode", "mlock"}, false,
[]string{"--mlock", "--no-mmap"}},
{"moteur ancien : mmap+mlock retraduit", []string{"-lm", "mmap+mlock"}, false,
[]string{"--mlock"}},
{"moteur ancien : dio abandonné", []string{"--load-mode=dio", "-fa"}, false,
[]string{"-fa"}},
{"--load-mode explicite gagne, vieux drapeaux retirés",
[]string{"--mlock", "--load-mode", "dio"}, true,
[]string{"--load-mode", "dio"}},
{"rien à traduire", []string{"-fa", "--n-cpu-moe", "8"}, true,
[]string{"-fa", "--n-cpu-moe", "8"}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got := normalizeLoadFlags(c.in, c.supports)
if !reflect.DeepEqual(got, c.want) {
t.Fatalf("got %q, want %q", got, c.want)
}
})
}
}
func TestNGLArgs(t *testing.T) {
cases := []struct {
name string
ngl string
fitsItself bool
want []string
wantNote bool
}{
// Le cas qui motive tout : defaultConfig() sème NGL=999 sur chaque
// installation neuve. Personne ne l'a choisi, et le moteur récent
// abandonne son calcul de VRAM dès qu'on lui impose un nombre.
{"sentinelle 999, moteur récent", "999", true, []string{"-ngl", "auto"}, true},
{"sentinelle 999, moteur ancien", "999", false, []string{"-ngl", "999"}, false},
{"nombre choisi, jamais touché", "28", true, []string{"-ngl", "28"}, false},
{"nombre choisi, moteur ancien", "28", false, []string{"-ngl", "28"}, false},
{"all reste all", "all", true, []string{"-ngl", "all"}, false},
{"auto : aucun drapeau", "auto", true, nil, false},
{"auto insensible à la casse", "AUTO", true, nil, false},
{"espaces parasites autour de la sentinelle", " 999 ", true, []string{"-ngl", "auto"}, true},
{"clé absente, moteur récent", "", true, []string{"-ngl", "auto"}, false},
{"clé absente, moteur ancien", "", false, []string{"-ngl", "999"}, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, note := nglArgs(c.ngl, c.fitsItself)
if !reflect.DeepEqual(got, c.want) {
t.Fatalf("args = %q, want %q", got, c.want)
}
if (note != "") != c.wantNote {
t.Fatalf("note = %q, en voulait-on une ? %v", note, c.wantNote)
}
})
}
}
func TestArgValueEtPortLibre(t *testing.T) {
if got := argValue([]string{"--port", "8080", "-c", "1", "--port", "9000"}, "--port"); got != "9000" {
t.Fatalf("la dernière occurrence doit gagner, got %q", got)
}
ln, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
_, port, _ := net.SplitHostPort(ln.Addr().String())
if waitPortFree("0.0.0.0", port, 600*time.Millisecond) == nil {
t.Fatal("un port occupé doit être refusé")
}
ln.Close()
if err := waitPortFree("0.0.0.0", port, time.Second); err != nil {
t.Fatalf("un port libre doit passer : %v", err)
}
}
func TestSlowKV(t *testing.T) {
for _, c := range []struct {
k, v string
lent bool
}{{"", "", false}, {"q8_0", "q8_0", false}, {"q4_0", "q4_0", false}, {"q8_0", "q4_0", true}, {"q5_1", "q5_1", true}} {
if slowKV(c.k, c.v) != c.lent {
t.Errorf("%s/%s : lent attendu %v", c.k, c.v, c.lent)
}
}
}