mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Moteur : un refus de LOAD_GUARD n'est plus relancé en boucle par systemd
Le refus d'un chargement résident trop gros pour la RAM faisait sortir « loki serve » en erreur (code 1) ; l'unité loki-engine (Restart=on-failure, RestartSec=3) le relançait toutes les trois secondes, sans fin, pour un échec qui ne dépend que du preset et de la machine. - Le refus sort avec un code dédié, 78 (EX_CONFIG) ; mustExit respecte le code porté par l'erreur, toutes les autres gardent 1. - L'unité écrite par « loki install » porte RestartPreventExitStatus=78 ; les autres échecs restent relancés. - Unité d'une version précédente (« loki update » ne réécrit pas les unités, il tourne sans droits root) : reconnue à coup sûr (parent systemd, INVOCATION_ID, unité et compléments lus sans la directive), le refus y sort sans erreur pour ne pas boucler, avec la marche à suivre au journal. Même chose sous launchd, qui relance toute sortie non nulle. - Le moteur d'essai de l'optimiseur garde toujours le vrai code. Le conteneur ne relance jamais le moteur de lui-même : rien à y changer, la raison reste au journal et l'interface la montre (modelLoadError). - Tests : directive reconnue (et seulement elle), code de sortie du refus, enveloppé ou non, unité générée (Linux). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d50c8a56c6
commit
a4db6d4da1
9 files changed
+194
-3
No files matched your search
@@ -646,6 +646,11 @@ Ajoutées par ce fork :
|
||||
de 80 % (llama.cpp #26110 : swap, décodage de 25 à 7,5 t/s ; mlock : processus
|
||||
tué) ; **refus** au lancement seulement quand la borne basse dépasse 90 %
|
||||
(échec certain), avec la raison en clair, aussi affichée par l'interface.
|
||||
Le refus sort avec le code 78, que l'unité systemd écrite par
|
||||
`loki install` ne relance pas (`RestartPreventExitStatus=78`) ; une unité
|
||||
d'une version précédente (ou launchd) reçoit une sortie sans erreur pour ne
|
||||
pas boucler — `sudo systemctl edit loki-engine` (section `[Service]`,
|
||||
`RestartPreventExitStatus=78`) met l'unité à jour.
|
||||
VRAM inconnue (Mac Intel, AMD, Vulkan, `--device`) ou serveurs `--rpc` :
|
||||
avertissement, jamais de refus. `mmap` et `auto` ne sont jamais concernés ;
|
||||
`LLAMA_ARG_NO_MMAP`/`LLAMA_ARG_MLOCK` ne comptent que sur un moteur ancien
|
||||
|
||||
@@ -410,7 +410,7 @@ func cmdServe(args []string) error {
|
||||
// Chargement résident avec plus de poids en RAM que la machine n'en a :
|
||||
// échec certain (voir loadModeRisk), LOAD_GUARD=off pour passer outre.
|
||||
if _, refuse := loadModeRisk(cfg, llmArgs, si); refuse != "" {
|
||||
return fmt.Errorf("%s", refuse)
|
||||
return loadGuardRefusal(refuse, trialDir != "")
|
||||
}
|
||||
|
||||
// Essai : écoute sur 127.0.0.1 et le port libre choisi par l'optimiseur,
|
||||
|
||||
@@ -242,6 +242,53 @@ func loadGuardOff(cfg map[string]string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// loadGuardExitStatus : le code de sortie de « loki serve » quand LOAD_GUARD
|
||||
// refuse. Le refus est certain et ne dépend que du preset et de la machine :
|
||||
// relancer ne changerait rien. Sous systemd, Restart=on-failure relançait
|
||||
// pourtant toutes les trois secondes, sans fin ; l'unité écrite par
|
||||
// « loki install » porte donc RestartPreventExitStatus pour ce code-là
|
||||
// (EX_CONFIG de sysexits.h : erreur de configuration). Le conteneur, lui, ne
|
||||
// relance jamais le moteur de lui-même.
|
||||
const loadGuardExitStatus = 78
|
||||
|
||||
// unitPreventsRefusalRestart : l'unité systemd (son texte) s'abstient-elle de
|
||||
// relancer sur loadGuardExitStatus ? Une unité écrite par une version
|
||||
// d'avant ne le fait pas.
|
||||
func unitPreventsRefusalRestart(unit string) bool {
|
||||
for _, line := range strings.Split(unit, "\n") {
|
||||
k, v, ok := strings.Cut(strings.TrimSpace(line), "=")
|
||||
if !ok || strings.TrimSpace(k) != "RestartPreventExitStatus" {
|
||||
continue
|
||||
}
|
||||
for _, f := range strings.Fields(v) {
|
||||
if f == strconv.Itoa(loadGuardExitStatus) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// loadGuardRefusal : l'erreur rendue par cmdServe pour un refus, avec son code
|
||||
// de sortie. Un superviseur qui relancerait quand même sur ce code (unité
|
||||
// systemd d'une version d'avant, launchd, qui relance toute sortie non nulle)
|
||||
// reçoit 0 : une sortie « propre » n'est pas relancée, et la raison reste en
|
||||
// clair dans le journal, où l'interface la reconnaît (modelLoadError). Le
|
||||
// moteur d'essai de l'optimiseur garde le vrai code : c'est lui qui dit
|
||||
// l'échec à « loki tune ».
|
||||
func loadGuardRefusal(refuse string, trial bool) error {
|
||||
code := loadGuardExitStatus
|
||||
if !trial {
|
||||
if restarts, hint := supervisorRestartsRefusal(); restarts {
|
||||
code = 0
|
||||
if hint != "" {
|
||||
fmt.Fprintln(os.Stderr, "[loki serve] "+hint)
|
||||
}
|
||||
}
|
||||
}
|
||||
return &exitStatusError{code: code, err: fmt.Errorf("%s", refuse)}
|
||||
}
|
||||
|
||||
// loadModeRisk : un mode de chargement résident avec trop de poids en RAM.
|
||||
// Fonction pure ; args est la ligne finale (ou EXTRA_ARGS normalisé).
|
||||
//
|
||||
@@ -257,7 +304,8 @@ func loadGuardOff(cfg map[string]string) bool {
|
||||
// - warn : l'estimation dépasse 80 % — on prévient, on lance.
|
||||
//
|
||||
// Un refus fait sortir « loki serve » en erreur, comme un modèle introuvable :
|
||||
// mieux vaut la raison en clair qu'un moteur tué en boucle par le noyau.
|
||||
// mieux vaut la raison en clair qu'un moteur tué en boucle par le noyau. Le code
|
||||
// de sortie (loadGuardRefusal) empêche le superviseur de relancer en boucle.
|
||||
func loadModeRisk(cfg map[string]string, args []string, si serveSysInfo) (warn, refuse string) {
|
||||
if isExternalConfig(cfg) {
|
||||
return "", ""
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
@@ -283,3 +285,48 @@ func TestModelLoadErrorLoadGuard(t *testing.T) {
|
||||
t.Errorf("refus ancien encore signalé : %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
// Un refus de LOAD_GUARD ne doit pas être relancé en boucle : l'unité systemd
|
||||
// écrite par « loki install » exclut son code de sortie, une unité d'avant ne
|
||||
// le fait pas.
|
||||
func TestUnitPreventsRefusalRestart(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
unit string
|
||||
want bool
|
||||
}{
|
||||
{"[Service]\nRestart=on-failure\nRestartSec=3\n", false},
|
||||
{"[Service]\nRestart=on-failure\nRestartPreventExitStatus=78\n", true},
|
||||
{"[Service]\n RestartPreventExitStatus = 1 78 SIGKILL\n", true},
|
||||
{"[Service]\nRestartPreventExitStatus=178\n", false},
|
||||
{"[Service]\n# RestartPreventExitStatus=78\n", false},
|
||||
} {
|
||||
if got := unitPreventsRefusalRestart(c.unit); got != c.want {
|
||||
t.Errorf("%q : %v, attendu %v", c.unit, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Le refus sort avec son propre code (pas 1), y compris enveloppé ; une erreur
|
||||
// ordinaire garde 1. Le moteur d'essai garde toujours le vrai code.
|
||||
func TestLoadGuardRefusalExitStatus(t *testing.T) {
|
||||
err := loadGuardRefusal("refus ; "+loadGuardMarker, true)
|
||||
if got := exitStatusOf(err); got != loadGuardExitStatus {
|
||||
t.Errorf("essai : code %d, attendu %d", got, loadGuardExitStatus)
|
||||
}
|
||||
if !strings.Contains(err.Error(), loadGuardMarker) {
|
||||
t.Errorf("raison perdue : %q", err)
|
||||
}
|
||||
if got := exitStatusOf(fmt.Errorf("enveloppé : %w", err)); got != loadGuardExitStatus {
|
||||
t.Errorf("enveloppé : code %d", got)
|
||||
}
|
||||
if got := exitStatusOf(errors.New("modèle introuvable")); got != 1 {
|
||||
t.Errorf("erreur ordinaire : code %d, attendu 1", got)
|
||||
}
|
||||
// Hors superviseur (les tests ne tournent ni sous systemd ni sous launchd) :
|
||||
// le vrai code aussi.
|
||||
if restarts, _ := supervisorRestartsRefusal(); !restarts {
|
||||
if got := exitStatusOf(loadGuardRefusal("refus", false)); got != loadGuardExitStatus {
|
||||
t.Errorf("hors superviseur : code %d", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
+22
-1
@@ -4,6 +4,7 @@
|
||||
package loki
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -202,10 +203,30 @@ Env:
|
||||
func mustExit(err error) {
|
||||
if err != nil {
|
||||
fmt.Fprintln(os.Stderr, "[err]", err)
|
||||
os.Exit(1)
|
||||
os.Exit(exitStatusOf(err))
|
||||
}
|
||||
}
|
||||
|
||||
// exitStatusError : une erreur qui choisit son code de sortie. Seul usage :
|
||||
// le refus de LOAD_GUARD, que le superviseur du moteur ne doit pas relancer
|
||||
// (voir loadGuardExitStatus).
|
||||
type exitStatusError struct {
|
||||
code int
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *exitStatusError) Error() string { return e.err.Error() }
|
||||
func (e *exitStatusError) Unwrap() error { return e.err }
|
||||
|
||||
// exitStatusOf : le code choisi par l'erreur, 1 sinon — comme toujours.
|
||||
func exitStatusOf(err error) int {
|
||||
var es *exitStatusError
|
||||
if errors.As(err, &es) {
|
||||
return es.code
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// LokiHome resolves the Loki data directory.
|
||||
// Précédence : $LOKI_HOME → /etc/default/loki (unix) → défaut plateforme.
|
||||
func LokiHome() string {
|
||||
|
||||
@@ -48,6 +48,15 @@ func unitArgs() map[string]string {
|
||||
return map[string]string{serviceName(): "serve", uiUnitName: "web"}
|
||||
}
|
||||
|
||||
// supervisorRestartsRefusal : sous launchd (le LaunchDaemon du moteur, que
|
||||
// launchd désigne par XPC_SERVICE_NAME), KeepAlive/SuccessfulExit=false relance
|
||||
// TOUTE sortie non nulle — aucune exception par code n'existe. Un refus de
|
||||
// LOAD_GUARD y sort donc sans erreur (voir loadGuardRefusal). L'app de bureau
|
||||
// lance le moteur elle-même et ne le relance pas : code normal.
|
||||
func supervisorRestartsRefusal() (bool, string) {
|
||||
return os.Getenv("XPC_SERVICE_NAME") == "com.loki."+serviceName(), ""
|
||||
}
|
||||
|
||||
func installServices(targetUser, lokiHome string) error {
|
||||
exe := installedExePath()
|
||||
for name, arg := range unitArgs() {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// sys_install_linux.go — la part systemd de l'installation. Le parcours commun
|
||||
@@ -31,6 +32,9 @@ WorkingDirectory=%s
|
||||
ExecStart=%s
|
||||
Restart=on-failure
|
||||
RestartSec=3
|
||||
# Lancement refusé d'avance par LOAD_GUARD (le mode de chargement ne tient pas
|
||||
# en RAM) : relancer n'y changerait rien.
|
||||
RestartPreventExitStatus=78
|
||||
|
||||
# Priorité CPU : on remonte le process pour qu'il ne soit pas dépriorisé face
|
||||
# aux tâches de fond (sampling/orchestration côté CPU pèsent sur le débit même
|
||||
@@ -63,6 +67,40 @@ RestartSec=5
|
||||
WantedBy=multi-user.target
|
||||
`
|
||||
|
||||
// engineUnitPath : l'unité du moteur telle que « loki install » l'écrit.
|
||||
func engineUnitPath() string { return "/etc/systemd/system/" + serviceName() + ".service" }
|
||||
|
||||
// supervisorRestartsRefusal : « loki serve » tourne-t-il sous une unité
|
||||
// systemd qui le relancerait sur loadGuardExitStatus ? C'est le cas d'une unité
|
||||
// écrite avant que Loki ne pose RestartPreventExitStatus : « loki update » ne
|
||||
// réécrit pas les unités (il tourne sans droits root), seul « loki install »
|
||||
// le fait. Reconnu à coup sûr : systemd pour parent, INVOCATION_ID posé par
|
||||
// systemd, et notre unité lisible sans la directive. Dans le doute, non : le
|
||||
// code de sortie reste celui d'une erreur.
|
||||
func supervisorRestartsRefusal() (bool, string) {
|
||||
if !systemdAvailable() || os.Getenv("INVOCATION_ID") == "" || os.Getppid() != 1 {
|
||||
return false, ""
|
||||
}
|
||||
b, err := os.ReadFile(engineUnitPath())
|
||||
if err != nil {
|
||||
return false, ""
|
||||
}
|
||||
// Les compléments (« systemctl edit ») comptent aussi.
|
||||
unit := string(b)
|
||||
drops, _ := filepath.Glob(engineUnitPath() + ".d/*.conf")
|
||||
for _, p := range drops {
|
||||
if d, err := os.ReadFile(p); err == nil {
|
||||
unit += "\n" + string(d)
|
||||
}
|
||||
}
|
||||
if unitPreventsRefusalRestart(unit) {
|
||||
return false, ""
|
||||
}
|
||||
return true, fmt.Sprintf("unité %s d'une version précédente : sortie sans erreur pour que systemd ne relance pas "+
|
||||
"en boucle — « sudo systemctl edit %s », section [Service], RestartPreventExitStatus=%d (ou « sudo loki install ») "+
|
||||
"la met à jour", serviceName(), serviceName(), loadGuardExitStatus)
|
||||
}
|
||||
|
||||
func installServices(targetUser, lokiHome string) error {
|
||||
svc, exe := serviceName(), installedExePath()
|
||||
units := map[string]string{
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
//go:build linux
|
||||
|
||||
package loki
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// L'unité du moteur écrite par « loki install » ne relance pas un refus de
|
||||
// LOAD_GUARD : sinon systemd le relançait toutes les trois secondes, sans fin.
|
||||
func TestEngineUnitPreventsRefusalRestart(t *testing.T) {
|
||||
if !unitPreventsRefusalRestart(engineUnitTemplate) {
|
||||
t.Fatal("RestartPreventExitStatus absent de l'unité du moteur")
|
||||
}
|
||||
if !strings.Contains(engineUnitTemplate, "Restart=on-failure") {
|
||||
t.Fatal("les autres échecs doivent toujours être relancés")
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
// service itself is managed by the PID-file supervisor in sys_service_windows.go
|
||||
// (loki start / stop / status), which needs no admin rights.
|
||||
|
||||
// supervisorRestartsRefusal : aucun superviseur ne relance le moteur sous
|
||||
// Windows (fichier PID, voir sys_service_windows.go) — code de sortie normal.
|
||||
func supervisorRestartsRefusal() (bool, string) { return false, "" }
|
||||
|
||||
func cmdInstall(args []string) error {
|
||||
lokiHome := LokiHome()
|
||||
|
||||
|
||||
Reference in new issue
Block a user