Files
Loki/internal/loki/backend_presets_test.go
T
MichaelandClaude Opus 5.5 f7f1a30d08 Moteur : files de lancement CUDA élargies (4x) sur deux GPU quand le pipeline entre cartes est possible
Sur plusieurs cartes, llama.cpp fait travailler les GPU en pipeline quand le
modèle y tient en entier ; encore faut-il que le CPU empile assez de
lancements CUDA d'avance. CUDA_SCALE_LAUNCH_QUEUES=4x agrandit cette file du
pilote : mêmes noyaux, même ordre, sortie identique — seul le prompt peut
gagner (+10-25 % en amont sur un 70B, non mesuré ici), le décodage ne bouge pas.
llama.cpp l'a retiré de ses défauts après des blocages sur Jetson : Loki ne le
pose donc que là où le pipeline peut réellement exister.

- launchQueuesEnv (pure) : 4x seulement si ≥ 2 GPU CUDA servis (--device,
  sinon CUDA_VISIBLE_DEVICES, sinon nvidia-smi borné à 3 s) et aucun bloqueur :
  -ot, --cpu-moe, --n-cpu-moe ≠ 0, -nkvo, -sm autre que layer, NGL chiffré
  hors 999 — drapeaux d'EXTRA_ARGS ou LLAMA_ARG_* équivalents
- une variable déjà dans l'environnement n'est jamais touchée ;
  CUDA_LAUNCH_QUEUES=off la coupe, 0.25x/0.5x/2x/4x l'imposent, toute autre
  valeur est ignorée en le disant
- la valeur posée s'affiche sur la ligne « [loki serve] » ; la ligne de
  commande ne change pas, BATCH/UBATCH non plus
- CUDA_LAUNCH_QUEUES survit aux bascules de preset (réglage machine qu'un
  preset peut imposer)
- éditeur : « Experts MoE sur CPU » rappelle que ça coupe le pipeline sur 2 GPU

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 22:43:04 +02:00

214 lines
8.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\nCUDA_LAUNCH_QUEUES=off\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", "CUDA_LAUNCH_QUEUES": "off",
} {
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"])
}
}
// Modifier le preset ACTIF doit changer la configuration courante. Vécu : CTX
// passé à 100000 dans l'éditeur, « enregistré » — et la carte du chat qui
// affiche toujours 32768, parce que seul le fichier du preset avait bougé ;
// config.env, lui, gardait l'ancienne valeur et plus aucun preset n'était
// détecté actif. Un preset qui n'est pas celui en service, ou un preset
// nouveau, ne touche pas à la configuration.
func TestModifierLePresetActifLApplique(t *testing.T) {
testHome(t)
dir := presetsDir()
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
write := func(name, body string) {
if err := os.WriteFile(filepath.Join(dir, name+".env"), []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
write("actif", "# NAME=Actif\nMODEL=foo.gguf\nCTX=32768\n")
write("autre", "# NAME=Autre\nMODEL=bar.gguf\nCTX=4096\n")
setConfig(t, "MODEL=foo.gguf\nCTX=32768\nMEM_MODE=always\n")
// Le preset en service : la config suit, les réglages machine restent.
id, applied, err := SavePresetApplying("actif", "Actif", "# NAME=Actif\nMODEL=foo.gguf\nCTX=100000\n")
if err != nil {
t.Fatal(err)
}
if id != "actif" || !applied {
t.Fatalf("id=%q applied=%v, attendu actif/true", id, applied)
}
cfg := ReadConfig()
if cfg["CTX"] != "100000" || cfg["MEM_MODE"] != "always" {
t.Fatalf("configuration non suivie : %v", cfg)
}
// Un autre preset : le fichier change, la config non.
if _, applied, err = SavePresetApplying("autre", "Autre", "# NAME=Autre\nMODEL=bar.gguf\nCTX=8192\n"); err != nil {
t.Fatal(err)
}
if applied || ReadConfig()["CTX"] != "100000" {
t.Fatalf("un preset inactif a touché la configuration (applied=%v, CTX=%q)", applied, ReadConfig()["CTX"])
}
// Un preset nouveau : jamais appliqué à la volée.
if _, applied, err = SavePresetApplying("", "Neuf", "MODEL=baz.gguf\n"); err != nil {
t.Fatal(err)
}
if applied || ReadConfig()["MODEL"] != "foo.gguf" {
t.Fatalf("un preset nouveau a touché la configuration (applied=%v)", applied)
}
}