Files
Loki/internal/loki/web_engine.go
T
MichaelandClaude Opus 5.5 9bffff78c2 Optimiseur : loki tune et bouton « Optimiser », essais sans perte sur un moteur privé
Chercher à la main le bon micro-lot, les threads ou la marge --fit d'un preset
demandait de dupliquer, basculer et comparer des bench complets un par un.
L'optimiseur le fait sur un moteur d'essai, sans jamais toucher à config.env ni
à ce que calcule le modèle, et n'écrit rien sans un clic.

- Isolation : moteur principal arrêté puis relancé ; chaque essai est une copie
  de la configuration (LOKI_HOME/tune/run), lancée par « loki serve » sur
  127.0.0.1 et un port libre, groupe de processus tué en sortie
- Verrou exclusif inter-processus (LOKI_HOME/tune.lock, PID + démarrage +
  binaire) vérifié par serviceAction start/restart et par le chat, les tâches,
  bash_bg, /v1, le bench, la bascule et l'enregistrement de preset, GPU, clé
  d'API, moteur ; essai orphelin arrêté avant tout démarrage du moteur
- Essais : lots, threads, délestage, marges --fit, files CUDA ; placement --fit
  et SPEC/CUDA_GRAPH_OPT/--backend-sampling seulement sur demande
- EXTRA_ARGS jeton par jeton, liste blanche seulement ; ligne de commande de
  chaque essai composée à blanc et comparée à la référence (« dénature »),
  contexte, slots et cache KV recontrôlés une fois chargé
- Mesure : bench complet du lot 1 à profondeur fixe ; score = durée d'un tour
  type (médianes de la télémétrie) ; gain > max(3 %, écart entre passages)
- Application sur clic : copie du preset, ou preset actuel sauvegardé, réécrit
  clé par clé, vérifié par une sonde et rétabli en cas d'échec

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

268 lines
9.1 KiB
Go

// web_engine.go — panneau « Moteur de l'image » : voir la version de llama.cpp
// qui tourne, savoir s'il en existe une plus récente, l'installer, et revenir
// en arrière si elle ne convient pas.
//
// GET /api/engine → version courante, versions installées, variante
// POST /api/engine/check → dernier build publié vs celui qui tourne
// POST /api/engine/update → job : téléchargement + test à blanc + bascule
// POST /api/engine/use → bascule vers une version déjà installée
// POST /api/engine/remove → supprime une version installée
//
// Jusqu'ici, mettre à jour llama.cpp imposait de reconstruire l'image de Loki —
// donc un rebuild complet et un redéploiement pour un composant qui publie
// plusieurs versions par jour. Le panneau ne le disait même pas : il annonçait
// « rien à mettre à jour ici ». Ces quatre routes suppriment ce détour.
package loki
import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
)
// engineUseImage est la valeur de `tag` qui désigne le moteur d'origine, celui
// que l'image Docker fournit dans /app. C'est le retour arrière garanti : il ne
// dépend d'aucun téléchargement et ne peut pas manquer.
const engineUseImage = "image"
// handleEngineStatus décrit le moteur courant et ce qui est installé à côté.
func handleEngineStatus(w http.ResponseWriter, r *http.Request) {
bin := currentEngineBin()
build, commit := engineBuildOf(bin)
imageBin := providedEngineBin()
source := engineSource(bin)
var installed []map[string]any
for _, tag := range engineInstalled() {
p := engineBinFor(tag)
installed = append(installed, map[string]any{
"tag": tag,
"bin": p,
"build": engineTagBuild(tag),
"in_use": samePath(p, bin),
})
}
sendJSON(w, 200, map[string]any{
"supported": engineOCISupported(),
"variant": engineVariant(),
"repo": ociHost() + "/" + ociEngineRepo,
"bin": bin,
"build": build,
"commit": commit,
"source": source,
"image_bin": imageBin,
"dir": engineDir(),
"installed": installed,
"job": lcJobSnapshot(0, false),
// Avis « moteur trop ancien » : seulement pour un build digne de foi
// (voir engineBuildTrust), jamais pour un compilé ou un fork.
"notice": engineBuildNotice(engineBuildTrust(source, build)),
})
}
// handleEngineCheck interroge le registre : quel est le dernier build publié
// pour cette variante, et sommes-nous en retard ? Synchrone, quelques secondes.
func handleEngineCheck(w http.ResponseWriter, r *http.Request) {
if !engineOCISupported() {
sendJSON(w, 200, map[string]any{"ok": false, "error": engineUnsupportedWhy()})
return
}
variant := engineVariant()
build, tag, err := engineOCILatest(variant)
if err != nil {
sendJSON(w, 200, map[string]any{"ok": false, "error": err.Error()})
return
}
cur, _ := engineBuildOf(currentEngineBin())
sendJSON(w, 200, map[string]any{
"ok": true,
"variant": variant,
"latest": build,
"tag": tag,
"current": cur,
// Un build courant inconnu (0) ne prouve pas qu'on est à jour : on
// propose alors la mise à jour plutôt que d'affirmer le contraire.
"update": build == 0 || cur == 0 || build > cur,
})
}
// handleEngineUpdate lance le job de mise à jour. {tag} permet d'épingler une
// version précise ; sans lui, on prend la dernière publiée pour la variante.
func handleEngineUpdate(w http.ResponseWriter, r *http.Request) {
if !engineOCISupported() {
sendJSON(w, 400, map[string]any{"ok": false, "error": engineUnsupportedWhy()})
return
}
var req struct {
Tag string `json:"tag"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
tag := strings.TrimSpace(req.Tag)
if err := startLcJob("engine", func() { engineRunUpdate(tag) }); err != nil {
sendJSON(w, 409, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true})
}
func engineUnsupportedWhy() string {
return "les images officielles llama.cpp n'existent que pour Linux x86-64 et arm64 ; " +
"sur cette machine, utilise « llama.cpp précompilé » ou « llama.cpp compilé »."
}
// engineRunUpdate : corps du job. Télécharger, ESSAYER, puis seulement basculer.
// L'ordre compte — le seul vrai risque ici est un moteur plus récent que le
// runtime CUDA de l'image, et on le découvre au test à blanc, moteur courant
// encore intact.
func engineRunUpdate(tag string) {
variant := engineVariant()
ancien := currentEngineBin()
if tag == "" {
lcPhase("recherche de la dernière version publiée…")
build, latest, err := engineOCILatest(variant)
if err != nil {
lcFail(err)
return
}
tag = latest
if build > 0 {
if cur, _ := engineBuildOf(currentEngineBin()); cur == build {
lcDone(fmt.Sprintf("déjà à jour (build %d)", build))
return
}
}
}
lcAppend("variante : " + variant + " — version visée : " + tag)
bin, err := engineOCIInstall(tag, lcAppend, lcPhase)
if err != nil {
lcFail(err)
return
}
lcPhase("essai du nouveau moteur…")
if err := engineSmokeTest(bin, ancien); err != nil {
// On ne garde pas un moteur inutilisable : il encombrerait la liste et
// inviterait à le sélectionner.
_ = os.RemoveAll(filepath.Dir(bin))
lcFail(fmt.Errorf("%w\n\nLe moteur courant n'a PAS été touché. Cette version demande sans doute un runtime CUDA plus récent que celui de l'image : reconstruis l'image avec un LLAMACPP_IMAGE récent, ou épingle un build antérieur", err))
return
}
if build, commit := engineBuildOf(bin); build > 0 {
lcAppend(fmt.Sprintf("moteur testé : build %d (%s)", build, commit))
}
if err := engineSwitchTo(bin); err != nil {
lcFail(err)
return
}
enginePrune(engineKeepAfterUpdate(bin, ancien, lcAppend), lcAppend)
lcDone("moteur mis à jour (" + tag + ")")
}
// engineKeepAfterUpdate : les versions téléchargées que le ménage épargne après
// une bascule. La nouvelle, et celle qui tournait juste avant si c'en était une
// : le moteur de l'image n'est pas forcément l'ancien build (il dépend de
// LLAMACPP_IMAGE), et revenir sur la version d'hier ne doit demander ni réseau
// ni que le tag soit encore publié. Les plus anciennes partent (~450 Mo chacune).
func engineKeepAfterUpdate(bin, ancien string, logf func(string)) map[string]bool {
keep := map[string]bool{filepath.Base(filepath.Dir(bin)): true}
if prev := engineTagOf(ancien); prev != "" && !keep[prev] {
keep[prev] = true
if logf != nil {
logf("version précédente gardée pour revenir en arrière : " + prev)
}
}
return keep
}
// engineSwitchTo pointe BIN sur un moteur et redémarre le service s'il tournait.
func engineSwitchTo(bin string) error {
if err := tuneGuard(); err != nil {
return err
}
wasUp := serviceIsActive()
if wasUp {
lcPhase("arrêt du moteur…")
if err := serviceAction("stop"); err != nil {
lcAppend("[warn] arrêt du moteur : " + err.Error())
}
}
if err := SetConfigKey("BIN", bin); err != nil {
if wasUp {
_ = serviceAction("start")
}
return fmt.Errorf("moteur installé mais échec d'écriture de BIN : %w", err)
}
lcAppend("BIN → " + bin)
if wasUp {
lcPhase("redémarrage du moteur…")
if err := serviceAction("start"); err != nil {
lcAppend("[warn] redémarrage du moteur : " + err.Error())
}
}
return nil
}
// handleEngineUse bascule vers une version DÉJÀ installée, ou revient au moteur
// de l'image ({tag:"image"}). Aucun réseau : c'est la sortie de secours quand une
// mise à jour s'avère décevante.
func handleEngineUse(w http.ResponseWriter, r *http.Request) {
if tuneDenyHTTP(w) {
return
}
var req struct {
Tag string `json:"tag"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
tag := strings.TrimSpace(req.Tag)
var bin string
if tag == engineUseImage {
if bin = providedEngineBin(); bin == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "cette installation n'a pas de moteur d'image (LOKI_ENGINE_BIN)"})
return
}
} else if bin = engineBinFor(tag); bin == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "version non installée : " + tag})
return
}
if err := SetConfigKey("BIN", bin); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
if serviceIsActive() {
_ = serviceAction("restart")
}
sendJSON(w, 200, map[string]any{"ok": true, "bin": bin})
}
// handleEngineRemove supprime une version téléchargée. On refuse de retirer
// celle qui tourne : ça laisserait une configuration pointant dans le vide, et
// le moteur ne redémarrerait plus.
func handleEngineRemove(w http.ResponseWriter, r *http.Request) {
var req struct {
Tag string `json:"tag"`
}
_ = json.NewDecoder(r.Body).Decode(&req)
tag := filepath.Base(strings.TrimSpace(req.Tag))
bin := engineBinFor(tag)
if bin == "" {
sendJSON(w, 400, map[string]any{"ok": false, "error": "version non installée : " + tag})
return
}
if samePath(bin, currentEngineBin()) {
sendJSON(w, 409, map[string]any{"ok": false, "error": "c'est le moteur utilisé — bascule d'abord sur une autre version"})
return
}
if err := os.RemoveAll(filepath.Join(engineDir(), tag)); err != nil {
sendJSON(w, 500, map[string]any{"ok": false, "error": err.Error()})
return
}
sendJSON(w, 200, map[string]any{"ok": true})
}