mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 19:29:16 +02:00
Les llama-server récents ouvrent 4 slots par défaut : des tampons de calcul GPU multipliés par quatre, pour un serveur qui ne sert qu'un utilisateur. Résultat vécu après une mise à jour du moteur : une config 27B qui tournait très bien meurt en « cudaMalloc failed: out of memory » en pleine génération, sans qu'aucun réglage n'ait changé. On repasse explicitement à un slot — le comportement historique — et PARALLEL=n reste disponible dans le preset pour qui veut du parallèle. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
205 lines
9.3 KiB
Go
205 lines
9.3 KiB
Go
package loki
|
||
|
||
import (
|
||
"bytes"
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// binSupportsReasoningFlag dit si ce llama-server accepte « --reasoning ».
|
||
//
|
||
// Le drapeau est récent : les moteurs plus anciens, et certains forks, ne le
|
||
// connaissent pas et REFUSENT de démarrer sur un argument inconnu. Comme on ne
|
||
// l'ajoute que pour interdire le raisonnement, mieux vaut demander au binaire
|
||
// que parier : on lit son aide, une fois, au lancement du moteur.
|
||
//
|
||
// L'aide se lit avec le même chemin de bibliothèques que le vrai lancement
|
||
// (setLibraryPath a déjà été appelé) : sans ça un moteur parfaitement valide
|
||
// échoue à s'exécuter (« libllama-common.so introuvable ») et on conclurait à
|
||
// tort qu'il ne gère pas le drapeau.
|
||
func binSupportsReasoningFlag(bin string) bool {
|
||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||
defer cancel()
|
||
cmd := exec.CommandContext(ctx, bin, "--help")
|
||
var out bytes.Buffer
|
||
cmd.Stdout, cmd.Stderr = &out, &out
|
||
if err := cmd.Run(); err != nil && out.Len() == 0 {
|
||
return false // aide illisible : on ne prend pas le risque
|
||
}
|
||
return strings.Contains(out.String(), "--reasoning ")
|
||
}
|
||
|
||
// cmdServe replaces the historic start.sh: read config.env, build the
|
||
// llama-server invocation, and exec it (replacing this process so systemd
|
||
// supervises llama-server directly).
|
||
func cmdServe(args []string) error {
|
||
cfg := ReadConfig()
|
||
bin := cfg["BIN"]
|
||
if bin == "" {
|
||
return fmt.Errorf("BIN non défini — lance « loki edit »")
|
||
}
|
||
model := cfg["MODEL"]
|
||
if model == "" {
|
||
return fmt.Errorf("MODEL non défini — lance « loki edit »")
|
||
}
|
||
// MODEL vaut soit un simple nom de fichier (le .gguf vit dans LOKI_HOME ou
|
||
// dans un dossier déclaré — disque externe…), soit un chemin absolu. Sous
|
||
// systemd/launchd le WorkingDirectory vaut LOKI_HOME, donc le relatif tombait
|
||
// juste ; lancé depuis une app de bureau, le répertoire courant est « / » et
|
||
// llama-server ne trouvait rien. On résout donc explicitement, quel que soit
|
||
// le contexte de lancement.
|
||
resolved, err := resolveServeModelPath(model)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
model = resolved
|
||
if _, err := os.Stat(model); err != nil {
|
||
return fmt.Errorf("modèle introuvable : %s", model)
|
||
}
|
||
// Modèle découpé en tranches : llama-server ouvre les suivantes tout seul, mais
|
||
// s'il en manque une il démarre puis meurt sur un tenseur introuvable — message
|
||
// incompréhensible, et systemd relance en boucle. On le dit ici, en clair.
|
||
if missing := shardFamilyMissing(filepath.Dir(model), filepath.Base(model)); len(missing) > 0 {
|
||
return fmt.Errorf("modèle incomplet : il manque %s dans %s — ce modèle tient en %d fichiers, télécharge-les tous",
|
||
strings.Join(missing, ", "), filepath.Dir(model), len(shardFamily(filepath.Base(model))))
|
||
}
|
||
if !filepath.IsAbs(bin) {
|
||
bin = filepath.Join(LokiHome(), bin)
|
||
}
|
||
// Le moteur précompilé s'installe dans un dossier versionné : un preset écrit
|
||
// avant une mise à jour pointe sur une release qui n'existe plus. On le fait
|
||
// suivre au moteur courant plutôt que d'échouer en 127.
|
||
bin = prebuiltResolveBin(bin)
|
||
|
||
// Make sure llama-server can find its bundled shared libraries (the .so/.dll
|
||
// neighbours of the binary). This is platform-specific: LD_LIBRARY_PATH on
|
||
// Linux, PATH on Windows — handled inside execServer.
|
||
setLibraryPath(filepath.Dir(bin))
|
||
|
||
// Sélection GPU (loki gpu) : on filtre les devices visibles par llama-server.
|
||
// CUDA_DEVICE_ORDER=PCI_BUS_ID garantit que les index correspondent à ceux
|
||
// affichés par nvidia-smi (sinon CUDA réordonne par "device le plus rapide").
|
||
if v := cfg["CUDA_VISIBLE_DEVICES"]; v != "" {
|
||
_ = os.Setenv("CUDA_VISIBLE_DEVICES", v)
|
||
_ = os.Setenv("CUDA_DEVICE_ORDER", "PCI_BUS_ID")
|
||
}
|
||
|
||
get := func(key, fallback string) string {
|
||
if v, ok := cfg[key]; ok && v != "" {
|
||
return v
|
||
}
|
||
return fallback
|
||
}
|
||
kv := get("KV_TYPE", "")
|
||
ktv := get("KV_TYPE_K", kv)
|
||
vtv := get("KV_TYPE_V", kv)
|
||
|
||
llmArgs := []string{bin,
|
||
"-m", model,
|
||
"-c", get("CTX", "32768"),
|
||
"-t", get("THREADS", "0"),
|
||
"-tb", get("THREADS_BATCH", "0"),
|
||
"-b", get("BATCH", "2048"),
|
||
"-ub", get("UBATCH", "512"),
|
||
"--host", get("HOST", "0.0.0.0"),
|
||
"--port", get("PORT", "8080"),
|
||
// --parallel 1 EXPLICITE : les llama-server récents ouvrent 4 slots par
|
||
// défaut, soit des tampons de calcul GPU ×4 — pour un serveur mono-
|
||
// utilisateur comme Loki, c'est de la VRAM brûlée pour rien. Vécu : une
|
||
// config 27B/60k ctx qui tournait très bien est morte en « cudaMalloc
|
||
// failed: out of memory » après une mise à jour du moteur, uniquement à
|
||
// cause de ce nouveau défaut. PARALLEL=n dans le preset pour qui veut
|
||
// vraiment servir plusieurs requêtes à la fois.
|
||
"--parallel", get("PARALLEL", "1"),
|
||
}
|
||
// NGL=auto : on n'envoie PAS -ngl, et llama.cpp choisit lui-même combien de
|
||
// couches tiennent dans la VRAM libre (common_fit_params). Il s'en abstient
|
||
// dès que la valeur est imposée — « n_gpu_layers already set by user to 999,
|
||
// abort » — d'où des cudaMalloc en échec, un repli partiel sur le CPU et un
|
||
// débit effondré alors que le GPU tourne à 100 %.
|
||
// Le défaut reste 999 (tout sur le GPU) : omettre -ngl sur un llama.cpp
|
||
// antérieur à cet ajustement automatique le ferait tourner 100 % CPU.
|
||
if ngl := get("NGL", "999"); !strings.EqualFold(ngl, "auto") {
|
||
llmArgs = append(llmArgs, "-ngl", ngl)
|
||
}
|
||
if ktv != "" {
|
||
llmArgs = append(llmArgs, "-ctk", ktv)
|
||
}
|
||
if vtv != "" {
|
||
llmArgs = append(llmArgs, "-ctv", vtv)
|
||
}
|
||
// Vision : le projecteur multimodal (mmproj-*.gguf) donne des yeux au modèle.
|
||
// C'est un fichier .gguf À PART du modèle, chargé via --mmproj. On le résout
|
||
// comme le modèle (nom simple cherché dans les dossiers déclarés, ou chemin
|
||
// absolu), pour qu'un preset écrit sous Windows reste lançable ailleurs et que
|
||
// le champ « Vision » de l'interface n'ait qu'à écrire le nom du fichier.
|
||
// Introuvable = on préfère le dire clairement plutôt que laisser llama-server
|
||
// mourir en boucle sur un « failed to load mmproj » cryptique.
|
||
if mm := strings.TrimSpace(cfg["MMPROJ"]); mm != "" {
|
||
mmPath, err := resolveServeModelPath(mm)
|
||
if err != nil {
|
||
return fmt.Errorf("projecteur vision introuvable : %s (%v)", mm, err)
|
||
}
|
||
if _, err := os.Stat(mmPath); err != nil {
|
||
return fmt.Errorf("projecteur vision introuvable : %s", mmPath)
|
||
}
|
||
llmArgs = append(llmArgs, "--mmproj", mmPath)
|
||
}
|
||
// Raisonnement. Trois cas, et la nuance compte :
|
||
//
|
||
// REASONING=on|auto|deepseek → --reasoning <valeur>
|
||
// REASONING=off → --reasoning off (interdiction EXPLICITE)
|
||
// clé absente → aucun drapeau, le moteur fait son défaut
|
||
//
|
||
// L'interface écrivait « off » en EFFAÇANT la ligne, ce qui n'est pas du tout
|
||
// la même chose : sans drapeau, llama-server suit le gabarit du modèle, et un
|
||
// modèle à raisonnement raisonne. L'interrupteur affichait donc « désactivé »
|
||
// pendant que le modèle réfléchissait quand même. Il faut le dire au moteur.
|
||
if r := strings.TrimSpace(cfg["REASONING"]); r != "" {
|
||
if reasoningActive(r) {
|
||
// budget illimité par défaut (-1) : on laisse le modèle réfléchir jusqu'au
|
||
// bout au lieu de le couper à 2048, ce qui tronquait la vraie réponse (la
|
||
// réflexion atteignait le plafond et il ne restait plus de marge pour le
|
||
// contenu). L'anti-boucle côté llm_client.go reste le garde-fou. NE PAS forcer 0 :
|
||
// sur llama.cpp vanilla, 0 = "immediate end" → coupe tout le raisonnement
|
||
// (le fork ik_llama.cpp l'ignore). Configurable via REASONING_BUDGET.
|
||
llmArgs = append(llmArgs, "--reasoning", r, "--reasoning-budget", get("REASONING_BUDGET", "-1"))
|
||
} else if binSupportsReasoningFlag(bin) {
|
||
// Pas de budget ici : « off » suffit, et un budget sur un moteur qui
|
||
// n'attend rien d'autre ne ferait qu'ajouter une occasion d'échouer.
|
||
llmArgs = append(llmArgs, "--reasoning", "off")
|
||
} else {
|
||
// Vieux moteur (ou fork) qui ne connaît pas le drapeau : le lui passer
|
||
// le ferait sortir en erreur au démarrage, donc boucler. On le dit et on
|
||
// continue sans — mieux vaut un modèle qui réfléchit qu'un moteur mort.
|
||
fmt.Fprintf(os.Stderr, "[loki serve] ce moteur ne connaît pas --reasoning : impossible de désactiver le raisonnement\n")
|
||
}
|
||
}
|
||
// API_KEY protège le serveur quand il est exposé sur internet : llama-server
|
||
// exige alors l'en-tête "Authorization: Bearer <clé>". La clé est lue depuis
|
||
// $LOKI_HOME/.api_key en priorité (elle survit ainsi aux changements de preset
|
||
// qui réécrivent config.env), avec config.env comme repli rétro-compatible.
|
||
if k, _ := effectiveAPIKeyErr(); k != "" {
|
||
llmArgs = append(llmArgs, "--api-key", k)
|
||
}
|
||
// EXTRA_ARGS is appended verbatim, split like the shell would — quotes kept
|
||
// together so a path with spaces stays one argument.
|
||
llmArgs = append(llmArgs, splitArgs(cfg["EXTRA_ARGS"])...)
|
||
|
||
// Working dir = LOKI_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj
|
||
// mmproj-F16.gguf) still resolve.
|
||
_ = os.Chdir(LokiHome())
|
||
|
||
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n",
|
||
bin, filepath.Base(model), get("PORT", "8080"))
|
||
|
||
// Hand off to the llama-server process. On Unix this replaces the current
|
||
// process (exec); on Windows it runs as a child and waits. See sys_platform_*.go.
|
||
return execServer(bin, llmArgs)
|
||
}
|