mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
La liste des points de reprise de llama-server est PAR SLOT : la garde de RAM de l'espacement d'office ne comptait qu'une liste, et sous-estimait d'autant le pire cas avec PARALLEL > 1. Et « loki serve » lit désormais le build du moteur avant de le lancer (hybrides seulement) : sans délai, un binaire coincé à l'initialisation de ses backends bloquait le lancement, là où binHelp, lui, était déjà borné. - ckptSlots : --parallel d'EXTRA_ARGS, sinon PARALLEL, sinon 1 ; « auto » ou illisible compté pour 4. Le pire cas devient points × slots × poids. - engineBuildOf borné à 15 s (WaitDelay pour un descendant qui garde la sortie) : passé le délai, build inconnu, ni avis ni drapeau. Profite aussi au panneau Moteur. - Tests : PARALLEL=2 garde l'espacement, PARALLEL=4 et --parallel 4 le retirent ; table de ckptSlots. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
248 lines
11 KiB
Go
248 lines
11 KiB
Go
package loki
|
||
|
||
import (
|
||
"os"
|
||
"path/filepath"
|
||
"reflect"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
const helpCkpt = helpRecent + `-ctxcp, --ctx-checkpoints, --swa-checkpoints N
|
||
max number of context checkpoints to create per slot (default: 32)
|
||
-cms, --checkpoint-min-step N minimum spacing between context checkpoints in tokens (default: 8192, 0 = no minimum)
|
||
--reasoning-preserve, --no-reasoning-preserve
|
||
preserve reasoning trace in the full history, not just the last assistant message (default: enabled)
|
||
`
|
||
|
||
// hybridOld : un Qwen3.6 27B hybride de 17 Go sur un moteur officiel b10678,
|
||
// machine de 64 Go. État récurrent : 48 couches × (3 × 10240 + 128 × 6144) × 4
|
||
// octets ≈ 150 Mio par point de reprise.
|
||
func hybridOld() serveSysInfo {
|
||
return serveSysInfo{
|
||
Help: helpCkpt,
|
||
Model: "/models/Qwen3.6-27B.gguf",
|
||
GGUF: &GGUFInfo{Arch: "qwen35", Hybrid: true, BlockCount: 64, FullAttnInterval: 4, HeadCountKV: 4,
|
||
KeyLen: 256, ValLen: 256, SSMConv: 4, SSMInner: 6144, SSMState: 128, SSMGroups: 16},
|
||
ModelBytes: 17 << 30,
|
||
RAMMiB: 65536,
|
||
RAMAvailMiB: 50000,
|
||
ArgEnv: map[string]string{},
|
||
EngineBuild: 10678,
|
||
}
|
||
}
|
||
|
||
func TestCkptArgs(t *testing.T) {
|
||
cms := []string{"--checkpoint-min-step", "2048"}
|
||
cases := []struct {
|
||
name string
|
||
cfg map[string]string
|
||
extra []string
|
||
si func(*serveSysInfo)
|
||
want []string
|
||
notes int
|
||
}{
|
||
{name: "hybride, moteur b10678 : espacement resserré d'office", want: cms, notes: 1},
|
||
{name: "hybride, moteur b11351 : rien", si: func(s *serveSysInfo) { s.EngineBuild = 11351 }},
|
||
{name: "hybride, build pile au correctif : rien", si: func(s *serveSysInfo) { s.EngineBuild = engineMinRecommended }},
|
||
{name: "build inconnu (compilé, clone superficiel, fork) : ni avis ni drapeau",
|
||
si: func(s *serveSysInfo) { s.EngineBuild = 0 }},
|
||
{name: "modèle dense sur moteur ancien : rien",
|
||
si: func(s *serveSysInfo) { s.GGUF = &GGUFInfo{Arch: "qwen3", BlockCount: 64} }},
|
||
{name: "métadonnées illisibles : jamais deviné au nom du fichier",
|
||
si: func(s *serveSysInfo) { s.GGUF = nil }},
|
||
{name: "arch hybride connue sans clé ssm.*", want: cms, notes: 1,
|
||
si: func(s *serveSysInfo) { s.GGUF = &GGUFInfo{Arch: "qwen3next"} }},
|
||
{name: "CKPT_MIN_STEP=4096 passé tel quel, quel que soit le build",
|
||
cfg: map[string]string{"CKPT_MIN_STEP": "4096"}, want: []string{"--checkpoint-min-step", "4096"},
|
||
si: func(s *serveSysInfo) { s.EngineBuild = 0 }},
|
||
{name: "CKPT_MIN_STEP=0 refusé, défaut du moteur, et on le dit",
|
||
cfg: map[string]string{"CKPT_MIN_STEP": "0"}, notes: 1},
|
||
{name: "-cms d'EXTRA_ARGS : Loki se tait sur l'espacement",
|
||
extra: []string{"-cms", "1024"}, notes: 1},
|
||
{name: "--checkpoint-min-step=1024 d'EXTRA_ARGS", cfg: map[string]string{"CKPT_MIN_STEP": "4096"},
|
||
extra: []string{"--checkpoint-min-step=1024"}},
|
||
{name: "LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT posée : pas d'espacement d'office", notes: 1,
|
||
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_CHECKPOINT_MIN_SPACING_NT"] = "4096" }},
|
||
{name: "--ctx-checkpoints d'EXTRA_ARGS ne coupe pas l'espacement", extra: []string{"--ctx-checkpoints", "8"},
|
||
want: cms, notes: 1},
|
||
{name: "aide sans -cms : pas de drapeau, l'avis reste", notes: 1,
|
||
si: func(s *serveSysInfo) { s.Help = helpRecent }},
|
||
{name: "points de reprise coupés : l'espacement ne sert à rien", cfg: map[string]string{"CTX_CHECKPOINTS": "0"},
|
||
want: []string{"--ctx-checkpoints", "0"}, notes: 1},
|
||
{name: "MoE mmappé de 82 Go sur 64 Go : conseil, pas d'espacement d'office", notes: 2,
|
||
si: func(s *serveSysInfo) { s.ModelBytes = 82 << 30 }},
|
||
{name: "RAM inconnue : pas d'espacement d'office", notes: 1,
|
||
si: func(s *serveSysInfo) { s.RAMMiB = 0 }},
|
||
{name: "RAM libre trop juste pour 32 points", notes: 1,
|
||
si: func(s *serveSysInfo) { s.RAMAvailMiB = 8000 }},
|
||
// Une liste par slot : 2 × 32 × ~150 Mio tient dans le quart de 50 Go, 4 × non.
|
||
{name: "PARALLEL=2 : deux listes, encore dans le quart de la RAM libre", cfg: map[string]string{"PARALLEL": "2"},
|
||
want: cms, notes: 1},
|
||
{name: "PARALLEL=4 : quatre listes, trop pour la RAM libre", cfg: map[string]string{"PARALLEL": "4"}, notes: 1},
|
||
{name: "--parallel 4 d'EXTRA_ARGS l'emporte sur PARALLEL", cfg: map[string]string{"PARALLEL": "1"},
|
||
extra: []string{"--parallel", "4"}, notes: 1},
|
||
{name: "CTX_CHECKPOINTS=16 passé", cfg: map[string]string{"CTX_CHECKPOINTS": "16"},
|
||
want: []string{"--ctx-checkpoints", "16", "--checkpoint-min-step", "2048"}, notes: 1},
|
||
{name: "CTX_CHECKPOINTS illisible", cfg: map[string]string{"CTX_CHECKPOINTS": "beaucoup"}, want: cms, notes: 2},
|
||
{name: "CTX_CHECKPOINTS et LLAMA_ARG_CTX_CHECKPOINTS", cfg: map[string]string{"CTX_CHECKPOINTS": "16"},
|
||
want: cms, notes: 2,
|
||
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_CTX_CHECKPOINTS"] = "8" }},
|
||
{name: "CTX_CHECKPOINTS et -ctxcp d'EXTRA_ARGS", cfg: map[string]string{"CTX_CHECKPOINTS": "16"},
|
||
extra: []string{"-ctxcp", "8"}, want: cms, notes: 1},
|
||
{name: "moteur sans --ctx-checkpoints : CTX_CHECKPOINTS ignoré, dit", cfg: map[string]string{"CTX_CHECKPOINTS": "16"},
|
||
notes: 2, si: func(s *serveSysInfo) { s.Help = helpRecent }},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
si := hybridOld()
|
||
if c.si != nil {
|
||
c.si(&si)
|
||
}
|
||
got, notes := ckptArgs(c.cfg, c.extra, si)
|
||
if !reflect.DeepEqual(got, c.want) {
|
||
t.Fatalf("ckptArgs = %v, attendu %v", got, c.want)
|
||
}
|
||
if len(notes) != c.notes {
|
||
t.Fatalf("notes = %q, attendu %d", notes, c.notes)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// Place dans la ligne complète : avant EXTRA_ARGS, qui garde le dernier mot.
|
||
func TestBuildServeArgsCkpt(t *testing.T) {
|
||
args, _, notes := buildServeArgs(map[string]string{"NGL": "999"}, []string{"--jinja"}, "/bin/llama-server", hybridOld())
|
||
if got := strings.Join(args, " "); !strings.HasSuffix(got, "--checkpoint-min-step 2048 --jinja") {
|
||
t.Fatalf("ligne = %s", got)
|
||
}
|
||
found := false
|
||
for _, n := range notes {
|
||
found = found || strings.Contains(n, "b10678")
|
||
}
|
||
if !found {
|
||
t.Fatalf("l'avis du moteur ancien manque : %q", notes)
|
||
}
|
||
}
|
||
|
||
func TestEngineBuildTrust(t *testing.T) {
|
||
cases := []struct {
|
||
src string
|
||
build int
|
||
want int
|
||
}{
|
||
{"downloaded", 10678, 10678},
|
||
{"image", 11351, 11351},
|
||
{"prebuilt", 10864, 10864},
|
||
{"custom", 10678, 0}, // compilé ou fork : numérotation inconnue
|
||
{"downloaded", 0, 0}, // --version illisible
|
||
{"image", 1, 0}, // clone superficiel
|
||
{"prebuilt", 4999, 0}, // sous le plancher
|
||
{"downloaded", 5000, 5000},
|
||
}
|
||
for _, c := range cases {
|
||
if got := engineBuildTrust(c.src, c.build); got != c.want {
|
||
t.Errorf("engineBuildTrust(%s, %d) = %d, attendu %d", c.src, c.build, got, c.want)
|
||
}
|
||
}
|
||
if engineBuildNotice(10678) == "" || engineBuildNotice(engineMinRecommended) != "" || engineBuildNotice(0) != "" {
|
||
t.Fatal("avis : seulement pour un build connu et antérieur au correctif")
|
||
}
|
||
}
|
||
|
||
func TestReasoningPreserveArgs(t *testing.T) {
|
||
cases := []struct {
|
||
name string
|
||
cfg map[string]string
|
||
extra []string
|
||
si func(*serveSysInfo)
|
||
want []string
|
||
notes int
|
||
}{
|
||
{name: "clé absente : rien, même sur un moteur qui connaît le drapeau"},
|
||
{name: "on", cfg: map[string]string{"REASONING_PRESERVE": "on"}, want: []string{"--reasoning-preserve"}},
|
||
{name: "OFF", cfg: map[string]string{"REASONING_PRESERVE": "OFF"}, want: []string{"--no-reasoning-preserve"}},
|
||
{name: "illisible", cfg: map[string]string{"REASONING_PRESERVE": "peut-être"}, notes: 1},
|
||
{name: "moteur sans le drapeau", cfg: map[string]string{"REASONING_PRESERVE": "off"}, notes: 1,
|
||
si: func(s *serveSysInfo) { s.Help = helpRecent }},
|
||
{name: "EXTRA_ARGS tranche déjà", cfg: map[string]string{"REASONING_PRESERVE": "off"},
|
||
extra: []string{"--reasoning-preserve"}},
|
||
{name: "--chat-template-kwargs le fixe", cfg: map[string]string{"REASONING_PRESERVE": "off"},
|
||
extra: []string{"--chat-template-kwargs", `{"preserve_reasoning":true}`}, notes: 1},
|
||
{name: "LLAMA_ARG_REASONING_PRESERVE posée", cfg: map[string]string{"REASONING_PRESERVE": "off"}, notes: 1,
|
||
si: func(s *serveSysInfo) { s.ArgEnv["LLAMA_ARG_REASONING_PRESERVE"] = "1" }},
|
||
{name: "LLAMA_ARG_CHAT_TEMPLATE_KWARGS le fixe", cfg: map[string]string{"REASONING_PRESERVE": "on"}, notes: 1,
|
||
si: func(s *serveSysInfo) {
|
||
s.ArgEnv["LLAMA_ARG_CHAT_TEMPLATE_KWARGS"] = `{"preserve_reasoning":false}`
|
||
}},
|
||
}
|
||
for _, c := range cases {
|
||
t.Run(c.name, func(t *testing.T) {
|
||
si := serveSysInfo{Help: helpCkpt, ArgEnv: map[string]string{}}
|
||
if c.si != nil {
|
||
c.si(&si)
|
||
}
|
||
got, notes := reasoningPreserveArgs(c.cfg, c.extra, si)
|
||
if !reflect.DeepEqual(got, c.want) {
|
||
t.Fatalf("reasoningPreserveArgs = %v, attendu %v", got, c.want)
|
||
}
|
||
if len(notes) != c.notes {
|
||
t.Fatalf("notes = %q, attendu %d", notes, c.notes)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// Après une mise à jour, la version téléchargée qui tournait reste installée :
|
||
// c'est le retour arrière sans réseau. Le moteur de l'image, lui, n'a rien à
|
||
// garder sous engine/.
|
||
func TestEngineKeepAfterUpdate(t *testing.T) {
|
||
testHome(t)
|
||
for _, tag := range []string{"server-cuda-b10500", "server-cuda-b10678", "server-cuda-b10900"} {
|
||
d := filepath.Join(engineDir(), tag)
|
||
if err := os.MkdirAll(d, 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if err := os.WriteFile(filepath.Join(d, "llama-server"), []byte("x"), 0o755); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
}
|
||
nouveau := filepath.Join(engineDir(), "server-cuda-b10900", "llama-server")
|
||
ancien := filepath.Join(engineDir(), "server-cuda-b10678", "llama-server")
|
||
keep := engineKeepAfterUpdate(nouveau, ancien, nil)
|
||
if !reflect.DeepEqual(keep, map[string]bool{"server-cuda-b10900": true, "server-cuda-b10678": true}) {
|
||
t.Fatalf("gardés = %v", keep)
|
||
}
|
||
enginePrune(keep, nil)
|
||
if got := engineInstalled(); !reflect.DeepEqual(got, []string{"server-cuda-b10678", "server-cuda-b10900"}) {
|
||
t.Fatalf("après ménage : %v", got)
|
||
}
|
||
if keep := engineKeepAfterUpdate(nouveau, "/app/llama-server", nil); len(keep) != 1 {
|
||
t.Fatalf("moteur de l'image : %v", keep)
|
||
}
|
||
if keep := engineKeepAfterUpdate(nouveau, nouveau, nil); len(keep) != 1 {
|
||
t.Fatalf("même version : %v", keep)
|
||
}
|
||
}
|
||
|
||
// Nombre de slots : chacun a sa liste de points de reprise.
|
||
func TestCkptSlots(t *testing.T) {
|
||
cases := []struct {
|
||
cfg map[string]string
|
||
extra []string
|
||
want int
|
||
}{
|
||
{want: 1},
|
||
{cfg: map[string]string{"PARALLEL": "3"}, want: 3},
|
||
{cfg: map[string]string{"PARALLEL": "3"}, extra: []string{"-np", "2"}, want: 2},
|
||
{extra: []string{"--parallel=6"}, want: 6},
|
||
{extra: []string{"--parallel", "-1"}, want: 4},
|
||
{cfg: map[string]string{"PARALLEL": "auto"}, want: 4},
|
||
}
|
||
for _, c := range cases {
|
||
if got := ckptSlots(c.cfg, c.extra); got != c.want {
|
||
t.Errorf("ckptSlots(%v, %v) = %d, attendu %d", c.cfg, c.extra, got, c.want)
|
||
}
|
||
}
|
||
}
|