mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
UN VRAI BUG D'ABORD normalizeLoadFlags traduisait --mlock seul en « --load-mode mlock ». Or, pour llama.cpp (common/arg.cpp), « mlock » veut dire PAS de mmap + résident : le modèle entier montait en RAM au lieu d'être mappé. L'ancien --mlock, lui, gardait le mmap — son équivalent est « mmap+mlock ». Sur un modèle plus gros que la RAM (Qwen3.8-Flash-Next, 82 Go pour 64 Go), un preset qui avait coché « Garder en RAM » tournait donc à l'OOM dès qu'un moteur récent prenait le relais. Correspondance alignée sur AJEAN 0.16.0 : --mlock seul → mmap+mlock · --no-mmap → none · les deux → mlock. DANS LES DEUX SENS Un moteur ancien (ou un fork) ne connaît pas --load-mode et sort en erreur si on le lui passe : downgradeLoadMode le retraduit en anciens drapeaux (dio, inconnu de ces moteurs, est abandonné avec un avertissement). UN SÉLECTEUR À LA PLACE DE DEUX INTERRUPTEURS « Garder en RAM » et « Charger tout en mémoire » ne disaient pas leur combinaison. L'éditeur de preset propose les six modes de llama.cpp (auto, mmap, none, mlock, mmap+mlock, dio), avec ce que fait chacun en sous-titre, et écrit la forme moderne. Un preset aux anciens drapeaux s'affiche sur le bon mode sans modification. Au passage, q5_1 porte la mention « lent sur CUDA » dans la liste du cache KV (voir le commit du port occupé : non accéléré en Flash-Attention). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
515 lines
20 KiB
Go
515 lines
20 KiB
Go
package loki
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"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 {
|
|
return strings.Contains(binHelp(bin), "--reasoning ")
|
|
}
|
|
|
|
// binHelp lit « <bin> --help » UNE fois par binaire et garde le texte. Plusieurs
|
|
// capacités se déduisent de cette aide (--reasoning, --load-mode, « -ngl auto »)
|
|
// et relancer le moteur à chaque question coûterait quelques secondes de plus au
|
|
// démarrage. Aide illisible = chaîne vide : tous les tests de capacité répondent
|
|
// « non », c'est-à-dire « ne prends aucun risque, garde la forme historique ».
|
|
var (
|
|
helpMu sync.Mutex
|
|
helpCache = map[string]string{}
|
|
)
|
|
|
|
func binHelp(bin string) string {
|
|
helpMu.Lock()
|
|
defer helpMu.Unlock()
|
|
if h, ok := helpCache[bin]; ok {
|
|
return h
|
|
}
|
|
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
|
|
h := ""
|
|
if err := cmd.Run(); err == nil || out.Len() > 0 {
|
|
h = out.String()
|
|
}
|
|
helpCache[bin] = h
|
|
return h
|
|
}
|
|
|
|
// binSupportsLoadMode : les moteurs récents ont fusionné --mlock, --mmap et
|
|
// --no-mmap dans un seul --load-mode ; les anciens ne connaissent que les trois
|
|
// drapeaux d'origine. On traduit donc à l'exécution, sans réécrire le preset :
|
|
// le même EXTRA_ARGS reste lançable sur les deux générations de moteur.
|
|
func binSupportsLoadMode(bin string) bool {
|
|
return strings.Contains(binHelp(bin), "--load-mode")
|
|
}
|
|
|
|
// binSupportsNGLAuto : « -ngl auto » laisse llama.cpp mesurer la VRAM libre et
|
|
// choisir le nombre de couches. Imposer un nombre (999 compris) désarme ce
|
|
// calcul — « n_gpu_layers already set by user to 999, abort » — et le moteur
|
|
// tente alors de tout mettre sur le GPU, quitte à échouer en cudaMalloc.
|
|
func binSupportsNGLAuto(bin string) bool {
|
|
h := binHelp(bin)
|
|
i := strings.Index(h, "--n-gpu-layers")
|
|
if i < 0 {
|
|
i = strings.Index(h, "-ngl")
|
|
}
|
|
if i < 0 {
|
|
return false
|
|
}
|
|
end := i + 400
|
|
if end > len(h) {
|
|
end = len(h)
|
|
}
|
|
return strings.Contains(h[i:end], "'auto'")
|
|
}
|
|
|
|
// binFitsLayersItself : ce moteur sait-il répartir les couches tout seul ?
|
|
//
|
|
// La lecture fine de l'aide (binSupportsNGLAuto) reste la source de vérité, mais
|
|
// elle dépend de la mise en page d'un texte d'aide — une description reformulée
|
|
// ou une colonne plus large, et on conclut « non » sur un moteur parfaitement
|
|
// capable, donc on lui réimpose 999 et l'abandon revient. --load-mode est arrivé
|
|
// dans la même vague que « -ngl auto » : sa présence sert de second témoin, plus
|
|
// grossier mais insensible à la mise en page.
|
|
func binFitsLayersItself(bin string) bool {
|
|
return binSupportsNGLAuto(bin) || binSupportsLoadMode(bin)
|
|
}
|
|
|
|
// nglArgs traduit la valeur NGL du preset en arguments, et renvoie au passage la
|
|
// note à afficher quand Loki a substitué quelque chose. Fonction pure : la seule
|
|
// question qui demande le moteur (« sait-il répartir les couches tout seul ? »)
|
|
// arrive déjà tranchée, ce qui la rend testable sans lancer llama-server.
|
|
func nglArgs(ngl string, fitsItself bool) ([]string, string) {
|
|
ngl = strings.TrimSpace(ngl)
|
|
switch {
|
|
case strings.EqualFold(ngl, "auto"):
|
|
return nil, "" // rien du tout : le moteur décide seul
|
|
case ngl == "999" && fitsItself:
|
|
return []string{"-ngl", "auto"},
|
|
"NGL=999 (sentinelle « toutes les couches ») → -ngl auto : ce moteur mesure lui-même la " +
|
|
"VRAM libre. NGL=all pour forcer l'ancien comportement."
|
|
case ngl != "":
|
|
return []string{"-ngl", ngl}, ""
|
|
case fitsItself:
|
|
return []string{"-ngl", "auto"}, ""
|
|
default:
|
|
// Moteur ancien : omettre -ngl le ferait tourner 100 % CPU.
|
|
return []string{"-ngl", "999"}, ""
|
|
}
|
|
}
|
|
|
|
// hasAnyFlag dit si l'utilisateur a déjà posé l'un de ces drapeaux dans
|
|
// EXTRA_ARGS. Loki ajoute plusieurs valeurs par défaut (--parallel, -ngl) : les
|
|
// ajouter EN PLUS de celles de l'utilisateur fait râler le moteur (« argument
|
|
// specified multiple times ») et, pire, c'est la DERNIÈRE occurrence qui gagne
|
|
// — le réglage explicite du preset serait donc silencieusement écrasé par le
|
|
// défaut de Loki. Quand l'utilisateur a tranché, on se tait.
|
|
func hasAnyFlag(args []string, flags ...string) bool {
|
|
for _, a := range args {
|
|
name := a
|
|
if i := strings.IndexByte(name, '='); i > 0 {
|
|
name = name[:i]
|
|
}
|
|
for _, f := range flags {
|
|
if name == f {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// normalizeLoadFlags choisit, au lancement, la forme des drapeaux de chargement
|
|
// que CE binaire comprend. llama.cpp récent a remplacé --mlock / --no-mmap par
|
|
// --load-mode (et refuse les anciens) ; un moteur ancien ou un fork ne connaît
|
|
// que les anciens. L'éditeur écrit --load-mode, un preset d'avant peut porter
|
|
// les anciens : on traduit dans le sens qu'il faut (repris d'AJEAN 0.16.0).
|
|
//
|
|
// Vers --load-mode (moteur récent). Deux interrupteurs → une seule valeur, au
|
|
// sens où llama.cpp l'entend (« mlock » = PAS de mmap + résident) :
|
|
//
|
|
// --mlock seul → mmap+mlock (l'ancien sens de --mlock : mmap conservé)
|
|
// --no-mmap seul → none
|
|
// --mlock + --no-mmap → mlock
|
|
// --mmap → mmap
|
|
//
|
|
// ⚠️ Jusqu'ici --mlock seul devenait « mlock », qui COUPE le mmap : le modèle
|
|
// entier montait en RAM au lieu d'être mappé — un OOM assuré sur un modèle plus
|
|
// gros que la mémoire. Un --load-mode déjà écrit gagne sur tout : on retire
|
|
// alors simplement les vieux drapeaux.
|
|
//
|
|
// Vers les anciens drapeaux (moteur qui ne connaît pas --load-mode) :
|
|
//
|
|
// none → --no-mmap · mlock → --mlock --no-mmap · mmap+mlock → --mlock
|
|
// auto, mmap, dio → rien (défaut de l'ancien moteur)
|
|
func normalizeLoadFlags(args []string, supportsLoadMode bool) []string {
|
|
if !supportsLoadMode {
|
|
return downgradeLoadMode(args)
|
|
}
|
|
explicit := hasAnyFlag(args, "--load-mode", "-lm")
|
|
mlock, nommap, mmap := false, false, false
|
|
out := make([]string, 0, len(args)+2)
|
|
for _, a := range args {
|
|
switch a {
|
|
case "--mlock":
|
|
mlock = true
|
|
case "--no-mmap":
|
|
nommap = true
|
|
case "--mmap":
|
|
mmap = true
|
|
default:
|
|
out = append(out, a)
|
|
}
|
|
}
|
|
if explicit || !(mlock || nommap || mmap) {
|
|
return out
|
|
}
|
|
mode := "mmap"
|
|
switch {
|
|
case mlock && nommap:
|
|
mode = "mlock"
|
|
case mlock:
|
|
mode = "mmap+mlock"
|
|
case nommap:
|
|
mode = "none"
|
|
}
|
|
return append(out, "--load-mode", mode)
|
|
}
|
|
|
|
// downgradeLoadMode retraduit un --load-mode pour un moteur qui ne le connaît
|
|
// pas : le lui passer tel quel le ferait sortir en erreur au démarrage.
|
|
func downgradeLoadMode(args []string) []string {
|
|
mode, found := "", false
|
|
kept := make([]string, 0, len(args))
|
|
for i := 0; i < len(args); i++ {
|
|
a := args[i]
|
|
if a == "--load-mode" || a == "-lm" {
|
|
found = true
|
|
if i+1 < len(args) {
|
|
mode = strings.ToLower(args[i+1])
|
|
i++
|
|
}
|
|
continue
|
|
}
|
|
if v, ok := strings.CutPrefix(a, "--load-mode="); ok {
|
|
found, mode = true, strings.ToLower(v)
|
|
continue
|
|
}
|
|
kept = append(kept, a)
|
|
}
|
|
if !found {
|
|
return args
|
|
}
|
|
add := func(flag string) {
|
|
for _, a := range kept {
|
|
if a == flag {
|
|
return
|
|
}
|
|
}
|
|
kept = append(kept, flag)
|
|
}
|
|
switch mode {
|
|
case "none":
|
|
add("--no-mmap")
|
|
case "mlock":
|
|
add("--mlock")
|
|
add("--no-mmap")
|
|
case "mmap+mlock":
|
|
add("--mlock")
|
|
case "dio":
|
|
fmt.Fprintf(os.Stderr, "[loki serve] ce moteur ne connaît pas --load-mode dio (DirectIO) : chargement par défaut\n")
|
|
}
|
|
return kept
|
|
}
|
|
|
|
// 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)
|
|
|
|
// EXTRA_ARGS est lu ICI, avant de composer la ligne de commande : ce que
|
|
// l'utilisateur a écrit à la main décide des défauts que Loki a le droit
|
|
// d'ajouter (voir hasAnyFlag). Il est aussi traduit vers les drapeaux de
|
|
// chargement actuels quand le moteur les attend (voir normalizeLoadFlags).
|
|
extra := normalizeLoadFlags(splitArgs(cfg["EXTRA_ARGS"]), binSupportsLoadMode(bin))
|
|
|
|
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 plusieurs slots
|
|
// par défaut, soit des tampons de calcul GPU multipliés d'autant — 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 — ou --parallel dans
|
|
// EXTRA_ARGS, auquel cas on ne redouble pas le drapeau.
|
|
if !hasAnyFlag(extra, "--parallel", "-np") {
|
|
llmArgs = append(llmArgs, "--parallel", get("PARALLEL", "1"))
|
|
}
|
|
// Couches GPU. Quatre cas, dans cet ordre :
|
|
//
|
|
// NGL=auto → rien du tout (le moteur décide seul)
|
|
// NGL=999 → -ngl auto sur un moteur récent (voir plus bas)
|
|
// NGL=<nombre> → -ngl <nombre> (l'utilisateur tranche pour de vrai)
|
|
// clé absente → -ngl auto si le moteur le propose, sinon -ngl 999
|
|
//
|
|
// Imposer un NOMBRE désarme common_fit_params, qui mesure la VRAM libre et
|
|
// choisit combien de couches y tiennent : « n_gpu_layers already set by user
|
|
// to 999, abort ». Le moteur pousse alors tout sur le GPU, échoue en
|
|
// cudaMalloc ou se replie à moitié sur le CPU — débit effondré, GPU à 100 %.
|
|
//
|
|
// 999 est traité à part parce que ce n'a JAMAIS été un nombre de couches :
|
|
// c'est la sentinelle historique « toutes », semée dans config.env par
|
|
// defaultConfig() sur chaque installation neuve. La quasi-totalité des
|
|
// configurations la portent donc sans que personne ne l'ait choisie — la
|
|
// traiter comme un choix délibéré, c'est condamner tout le monde à l'abandon
|
|
// ci-dessus. Sur un moteur qui connaît « auto », 999 devient donc auto, et on
|
|
// le DIT sur stderr plutôt que de le faire en douce. Qui veut réellement
|
|
// forcer tout sur le GPU écrit NGL=all (ou un nombre qui n'est pas 999).
|
|
if !hasAnyFlag(extra, "-ngl", "--n-gpu-layers", "--gpu-layers") {
|
|
args, note := nglArgs(get("NGL", ""), binFitsLayersItself(bin))
|
|
if note != "" {
|
|
fmt.Fprintln(os.Stderr, "[loki serve] "+note)
|
|
}
|
|
llmArgs = append(llmArgs, args...)
|
|
}
|
|
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 (déjà découpé plus haut comme le ferait le shell — les guillemets
|
|
// gardent ensemble un chemin qui contient des espaces) ferme la marche.
|
|
llmArgs = append(llmArgs, extra...)
|
|
|
|
// Working dir = LOKI_HOME so relative paths in EXTRA_ARGS (e.g. --mmproj
|
|
// mmproj-F16.gguf) still resolve.
|
|
_ = os.Chdir(LokiHome())
|
|
|
|
// Port déjà pris (souvent un llama-server orphelin qu'un stop n'a pas pu
|
|
// tuer) : deux moteurs sur un même port se partagent les requêtes au hasard,
|
|
// VRAM saturée et réponses du mauvais modèle. On refuse en clair.
|
|
host, port := argValue(llmArgs, "--host"), argValue(llmArgs, "--port")
|
|
if err := waitPortFree(host, port, 5*time.Second); err != nil {
|
|
return err
|
|
}
|
|
warnSlowKV(ktv, vtv)
|
|
|
|
fmt.Fprintf(os.Stderr, "[loki serve] %s model=%s port=%s\n",
|
|
bin, filepath.Base(model), port)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// argValue renvoie la valeur de la DERNIÈRE occurrence de flag (llama-server
|
|
// retient la dernière : EXTRA_ARGS peut surcharger --port).
|
|
func argValue(args []string, flag string) string {
|
|
v := ""
|
|
for i := 0; i+1 < len(args); i++ {
|
|
if args[i] == flag {
|
|
v = args[i+1]
|
|
}
|
|
}
|
|
return v
|
|
}
|
|
|
|
// waitPortFree vérifie que personne n'écoute déjà sur host:port. On tente une
|
|
// connexion TCP (fiable même si l'occupant a bindé en SO_REUSEADDR, où un
|
|
// Listen de test pourrait réussir). Un moteur qu'on vient d'arrêter peut tenir
|
|
// le port quelques instants : on réessaie jusqu'à wait avant de conclure.
|
|
// Repris d'AJEAN 0.15.9.
|
|
func waitPortFree(host, port string, wait time.Duration) error {
|
|
if port == "" {
|
|
return nil
|
|
}
|
|
h := strings.Trim(host, "[]")
|
|
if h == "" || h == "0.0.0.0" || h == "::" {
|
|
h = "127.0.0.1"
|
|
}
|
|
addr := net.JoinHostPort(h, port)
|
|
deadline := time.Now().Add(wait)
|
|
for {
|
|
c, err := net.DialTimeout("tcp", addr, 500*time.Millisecond)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
c.Close()
|
|
if time.Now().After(deadline) {
|
|
return fmt.Errorf("le port %s est déjà utilisé (sans doute un ancien llama-server encore actif) : "+
|
|
"« loki stop », docker restart loki, ou change PORT", port)
|
|
}
|
|
time.Sleep(500 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// fastKV : combinaisons K/V que llama.cpp CUDA accélère en Flash-Attention
|
|
// avec ses options de compilation par défaut (GGML_CUDA_FA_ALL_QUANTS=OFF) —
|
|
// c'est le cas du moteur de l'image server-cuda comme de ceux tirés par OCI.
|
|
// Les autres (q5_*, q4_1, combinaisons mixtes) marchent mais sont reconverties
|
|
// en f16 à chaque pas : la génération ralentit nettement (AJEAN 0.15.9).
|
|
var fastKV = map[string]bool{"f16|f16": true, "q8_0|q8_0": true, "q4_0|q4_0": true, "bf16|bf16": true}
|
|
|
|
func slowKV(k, v string) bool {
|
|
if k == "" {
|
|
k = "f16"
|
|
}
|
|
if v == "" {
|
|
v = "f16"
|
|
}
|
|
return !fastKV[k+"|"+v]
|
|
}
|
|
|
|
func warnSlowKV(k, v string) {
|
|
if slowKV(k, v) {
|
|
fmt.Fprintf(os.Stderr, "[loki serve] avertissement : cache KV %s/%s non accéléré par llama.cpp CUDA "+
|
|
"(converti en f16 à chaque pas, lent). Préfère q8_0/q8_0, q4_0/q4_0 ou f16.\n", k, v)
|
|
}
|
|
}
|