mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Le moteur mis à jour depuis l'interface (Réglages → Moteur, BIN → /data/engine/server-cuda-b10680/llama-server) ne tenait pas : à la bascule de preset suivante — changer de modèle, tâche planifiée — /app/llama-server revenait, et le modèle qui chargeait cinq minutes plus tôt mourait sur « unknown model architecture: 'qwen4exp' ». Le journal alternait les deux moteurs sans qu'aucun réglage visible ait bougé, et le panneau Moteur affichait « fourni par l'image » juste après un « ✓ moteur mis à jour ». Cause : chaque preset créé depuis l'UI embarque BIN (newPresetSeedKeys), donc le chemin de l'image de l'époque, et applyPresetFile remplace TOUTE la configuration par le preset — BIN n'était pas dans preservedKeys. Le moteur est un réglage de machine : le courant est conservé à la bascule, sauf si le preset désigne un backend personnalisé (compilé pour un modèle précis — là, c'est un vrai choix par modèle, il gagne). Et un nouveau preset ne fige plus le moteur de l'image ni un moteur téléchargé. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01961iDyM6pwn2dE2SW23gYX
163 lines
6.6 KiB
Go
163 lines
6.6 KiB
Go
package loki
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
// setConfig installe une configuration décrite au format des presets.
|
|
func setConfig(t *testing.T, body string) {
|
|
t.Helper()
|
|
if err := WriteConfig(parseEnv(body)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// TestPresetFingerprintIgnoresCosmetics vérifie que la détection du preset actif
|
|
// ne dépend QUE de la config effective (ensemble KEY=VALUE), pas de la mise en
|
|
// forme : ordre des lignes, commentaires (dont `# NAME=`), lignes vides, `export`,
|
|
// espaces, ni des clés « appareil » (preservedKeys) réappliquées par SwitchToPreset.
|
|
// C'est la régression « aucun preset en surbrillance après un reformatage de
|
|
// config.env » (toggle mémoire/internet, réordonnancement…).
|
|
func TestPresetFingerprintIgnoresCosmetics(t *testing.T) {
|
|
base := []byte("# NAME=Mon preset\nMODEL=foo.gguf\nCTX=4096\nNGL=999\n")
|
|
// Même config effective, présentée autrement.
|
|
variant := []byte("export NGL=999\nMEM_MODE=off\n\nCTX=4096\n# un commentaire\nMODEL=foo.gguf\n")
|
|
if presetFingerprint(base) != presetFingerprint(variant) {
|
|
t.Fatal("empreintes différentes alors que la config effective est identique")
|
|
}
|
|
// Un vrai changement de valeur doit, lui, produire une empreinte différente.
|
|
changed := []byte("MODEL=foo.gguf\nCTX=8192\nNGL=999\n")
|
|
if presetFingerprint(base) == presetFingerprint(changed) {
|
|
t.Fatal("empreintes identiques alors que CTX diffère")
|
|
}
|
|
}
|
|
|
|
// TestSwitchToPresetGardeLesReglagesMachine : basculer de preset ne doit pas
|
|
// effacer les réglages qui décrivent la MACHINE et non le modèle. Le cas vécu :
|
|
// `loki gpu 1` écrit CUDA_VISIBLE_DEVICES dans config.env, la bascule de preset
|
|
// suivante l'écrasait, llama.cpp revoyait les deux cartes et réétalait le modèle
|
|
// sur la petite — pendant que `loki gpu` réaffichait « auto ».
|
|
func TestSwitchToPresetGardeLesReglagesMachine(t *testing.T) {
|
|
home := testHome(t)
|
|
write := func(p, body string) {
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
// Configuration courante : un preset + les réglages machine.
|
|
setConfig(t, "MODEL=ancien.gguf\nCTX=4096\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\nMEM_MODE=always\n")
|
|
target := filepath.Join(home, "cible.env")
|
|
write(target, "MODEL=nouveau.gguf\nCTX=8192\n")
|
|
|
|
if err := applyPresetFile(target); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cfg := ReadConfig()
|
|
if cfg["MODEL"] != "nouveau.gguf" || cfg["CTX"] != "8192" {
|
|
t.Fatalf("le preset n'a pas été appliqué : %v", cfg)
|
|
}
|
|
for k, want := range map[string]string{
|
|
"CUDA_VISIBLE_DEVICES": "1", "WEB_ENGINE": "go", "MEM_MODE": "always",
|
|
} {
|
|
if cfg[k] != want {
|
|
t.Errorf("%s = %q après bascule, attendu %q (réglage machine effacé)", k, cfg[k], want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Le moteur suit la machine. Vécu : moteur mis à jour depuis l'interface
|
|
// (BIN → /data/engine/server-cuda-b10680), Qwen3.8-Flash-Next charge ; on
|
|
// change de modèle — le preset, créé avant, porte BIN=/app/llama-server — et
|
|
// le vieux moteur revient : « unknown model architecture: 'qwen4exp' ». Seul
|
|
// un backend personnalisé, choix par modèle, a le droit d'imposer son BIN.
|
|
func TestSwitchToPresetGardeLeMoteur(t *testing.T) {
|
|
home := testHome(t)
|
|
t.Setenv("LOKI_ENGINE_BIN", "/app/llama-server")
|
|
write := func(p, body string) {
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
engine := filepath.Join(home, "engine", "server-cuda-b10680", "llama-server")
|
|
setConfig(t, "MODEL=ancien.gguf\nBIN="+engine+"\n")
|
|
|
|
// Preset d'avant la mise à jour : il porte le moteur de l'image.
|
|
image := filepath.Join(home, "image.env")
|
|
write(image, "MODEL=nouveau.gguf\nBIN=/app/llama-server\n")
|
|
if err := applyPresetFile(image); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ReadConfig()["BIN"]; got != engine {
|
|
t.Errorf("BIN = %q après bascule, attendu le moteur mis à jour %q", got, engine)
|
|
}
|
|
// Preset muet sur le moteur : idem.
|
|
muet := filepath.Join(home, "muet.env")
|
|
write(muet, "MODEL=autre.gguf\n")
|
|
if err := applyPresetFile(muet); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ReadConfig()["BIN"]; got != engine {
|
|
t.Errorf("BIN = %q après preset muet, attendu %q conservé", got, engine)
|
|
}
|
|
// Backend personnalisé : le preset gagne.
|
|
custom := filepath.Join(home, "custom.env")
|
|
write(custom, "MODEL=exotique.gguf\nBIN=/opt/mon-fork/llama-server\n")
|
|
if err := applyPresetFile(custom); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ReadConfig()["BIN"]; got != "/opt/mon-fork/llama-server" {
|
|
t.Errorf("BIN = %q, attendu le backend personnalisé du preset", got)
|
|
}
|
|
// Un nouveau preset ne fige pas ce backend-là… si, justement : c'est un
|
|
// choix par modèle. Mais il ne fige ni l'image ni un moteur téléchargé.
|
|
if seed := newPresetSeed(); seed["BIN"] != "/opt/mon-fork/llama-server" {
|
|
t.Errorf("seed BIN = %q, attendu le backend personnalisé", seed["BIN"])
|
|
}
|
|
setConfig(t, "MODEL=ancien.gguf\nBIN="+engine+"\n")
|
|
if seed := newPresetSeed(); seed["BIN"] != "" {
|
|
t.Errorf("seed BIN = %q, un moteur téléchargé ne doit pas être figé dans un preset", seed["BIN"])
|
|
}
|
|
setConfig(t, "MODEL=ancien.gguf\nBIN=/app/llama-server\n")
|
|
if seed := newPresetSeed(); seed["BIN"] != "" {
|
|
t.Errorf("seed BIN = %q, le moteur de l'image ne doit pas être figé dans un preset", seed["BIN"])
|
|
}
|
|
}
|
|
|
|
// Un preset qui définit LUI-MÊME la sélection de cartes doit gagner sur celle
|
|
// de la machine : « FABLE 2 GPU » impose CUDA_VISIBLE_DEVICES=1,0 pour que son
|
|
// --tensor-split ait deux cartes. Écraser ça par une sélection mono-GPU faisait
|
|
// mourir le chargement sur « cudaMalloc failed: out of memory ».
|
|
func TestSwitchToPresetLaisseLePresetImposerSesGPU(t *testing.T) {
|
|
home := testHome(t)
|
|
write := func(p, body string) {
|
|
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
setConfig(t, "MODEL=ancien.gguf\nCUDA_VISIBLE_DEVICES=1\nWEB_ENGINE=go\n")
|
|
deuxGPU := filepath.Join(home, "deux-gpu.env")
|
|
write(deuxGPU, "MODEL=fable.gguf\nCUDA_VISIBLE_DEVICES=1,0\n")
|
|
if err := applyPresetFile(deuxGPU); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := ReadConfig()["CUDA_VISIBLE_DEVICES"]; got != "1,0" {
|
|
t.Errorf("CUDA_VISIBLE_DEVICES = %q, attendu 1,0 (le preset doit gagner)", got)
|
|
}
|
|
// Un preset MUET sur les GPU laisse, lui, la sélection machine en place.
|
|
muet := filepath.Join(home, "muet.env")
|
|
write(muet, "MODEL=autre.gguf\n")
|
|
if err := applyPresetFile(muet); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cfg := ReadConfig()
|
|
if cfg["CUDA_VISIBLE_DEVICES"] != "1,0" {
|
|
t.Errorf("CUDA_VISIBLE_DEVICES = %q, attendu 1,0 conservé", cfg["CUDA_VISIBLE_DEVICES"])
|
|
}
|
|
if cfg["WEB_ENGINE"] != "go" {
|
|
t.Errorf("WEB_ENGINE = %q, attendu go (réglage machine, jamais dicté par un preset)", cfg["WEB_ENGINE"])
|
|
}
|
|
}
|