mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-12 01:37:06 +02:00
llama.cpp publie plusieurs versions par jour ; l'image de Loki ne se reconstruit qu'à une mise à jour de Loki. Le moteur y était donc figé à la date du dernier build, et le rattraper imposait un rebuild complet de 2,6 Go pour un composant qui en pèse 170 Mo. Le panneau « Moteur » ne le disait même pas : il annonçait « rien à mettre à jour ici ». Réglages → Moteur affiche maintenant la version qui tourne (bXXXXX et son commit, lus dans la bannière du binaire — jusqu'ici invisibles ailleurs que dans le journal du moteur) et la met à jour en un clic. Où le moteur est pris. Pas dans les releases GitHub de llama.cpp : elles ne contiennent AUCUN binaire CUDA pour Linux, et le mode « précompilé » retomberait sur Vulkan, donc sur une régression pour une carte NVIDIA. La seule distribution CUDA/Linux officielle et précompilée est l'image de conteneur — celle-là même dont l'image de Loki hérite. On lit son manifeste OCI et on ne télécharge que les couches qui portent /app, en descendant du sommet : le runtime CUDA (2 Go) et la base système sont déjà là. S'arrêter au binaire ne suffit pas — llama.cpp le range dans une couche et ses .so dans la précédente — d'où une règle d'arrêt sur « binaire + libggml-base + libllama », et un garde-fou de taille qui interdit de descendre jusqu'au runtime. Le moteur atterrit dans /data/engine/<version>/, donc sur le volume de données : il survit à un docker compose pull. La variante (CUDA, Vulkan, SYCL, MUSA, CPU) est déduite des backends ggml posés à côté du moteur courant — le conteneur ne sait pas de quelle image il vient, et faire retenir « server-cuda » à l'utilisateur serait un piège. Le risque, et ce qui le couvre. La mise à jour apporte llama.cpp, pas le runtime CUDA, qui reste celui de l'image : un llama.cpp compilé pour un CUDA plus récent ne chargerait pas son backend GPU. Le symptôme serait silencieux — tout marche, mais sur le processeur. Le nouveau moteur est donc lancé à blanc avant toute bascule ; il est refusé s'il ne démarre pas, ET s'il ne voit plus aucune carte alors que le moteur courant en voyait. Dans les deux cas le moteur courant n'est pas touché, et celui de l'image reste intact : « revenir au moteur de l'image » y ramène en un clic, sans réseau. Vérifié de bout en bout contre le vrai ghcr.io (166 Mo, 7 s, toutes les bibliothèques et leurs liens de version présents) et, pour les chemins d'échec, contre un faux registre. LOKI_OCI_REGISTRY permet de viser un miroir quand ghcr.io n'est pas joignable. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LPyFxVHNAN9u5pVzSYMwjd
254 lines
8.4 KiB
Go
254 lines
8.4 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 := "custom"
|
|
switch {
|
|
case imageBin != "" && samePath(bin, imageBin):
|
|
source = "image"
|
|
case engineOwns(bin):
|
|
source = "downloaded"
|
|
case prebuiltOwns(bin):
|
|
source = "prebuilt"
|
|
}
|
|
|
|
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),
|
|
})
|
|
}
|
|
|
|
// 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
|
|
}
|
|
// On garde la version qu'on vient de poser ; les précédentes ne servent plus
|
|
// (le retour arrière se fait sur le moteur de l'image, qui, lui, est intact).
|
|
enginePrune(map[string]bool{filepath.Base(filepath.Dir(bin)): true}, lcAppend)
|
|
lcDone("moteur mis à jour (" + tag + ")")
|
|
}
|
|
|
|
// engineSwitchTo pointe BIN sur un moteur et redémarre le service s'il tournait.
|
|
func engineSwitchTo(bin string) error {
|
|
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) {
|
|
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})
|
|
}
|