mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
Deux reprises d'AJEAN 0.15.9, côté `loki serve`. PORT OCCUPÉ Un llama-server orphelin (un stop qui n'a pas tué, un relancement trop rapide) peut encore tenir le port quand le suivant démarre. Deux moteurs sur le même port se partagent alors les requêtes au hasard : VRAM saturée, réponses du mauvais modèle. waitPortFree tente une connexion TCP (fiable même en SO_REUSEADDR, là où un Listen de test réussirait), laisse 5 s à un moteur qu'on vient d'arrêter pour libérer, puis refuse avec un message qui dit quoi faire. argValue lit la DERNIÈRE occurrence de --port/--host, comme llama-server, puisque EXTRA_ARGS peut les surcharger. CACHE KV LENT llama.cpp CUDA, compilé avec ses options par défaut (FA_ALL_QUANTS=OFF), n'accélère en Flash-Attention que f16, bf16, q8_0 et q4_0 — et seulement à l'identique pour K et V. C'est le cas du moteur de l'image server-cuda et de ceux installés par OCI. q5_1 ou un couple mixte q8_0/q4_0 marchent mais sont reconvertis en f16 à chaque pas. L'amont ne prévient que pour son binaire précompilé ; chez nous tous les moteurs sont concernés, donc l'avertissement part toujours dans loki-engine.log. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
125 lines
4.1 KiB
Go
125 lines
4.1 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 : rien ne bouge", []string{"--mlock", "--no-mmap"}, false,
|
|
[]string{"--mlock", "--no-mmap"}},
|
|
{"mlock traduit", []string{"--mlock", "-fa"}, true,
|
|
[]string{"-fa", "--load-mode", "mlock"}},
|
|
{"no-mmap traduit", []string{"--no-mmap"}, true,
|
|
[]string{"--load-mode", "none"}},
|
|
{"mmap traduit", []string{"--mmap"}, true,
|
|
[]string{"--load-mode", "mmap"}},
|
|
{"mlock l'emporte sur no-mmap", []string{"--no-mmap", "--mlock"}, true,
|
|
[]string{"--load-mode", "mlock"}},
|
|
{"--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)
|
|
}
|
|
}
|
|
}
|