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:
MichaelandClaude Opus 5.5 committed 2026-10-04 14:29:13 +02:00
1 parent d50c8a56c6
commit a4db6d4da1
9 files changed
+194 -3

No files matched your search

+5
View File
@@ -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
+1 -1
View File
@@ -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,
+49 -1
View File
@@ -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 "", ""
+47
View File
@@ -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
View File
@@ -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 {
+9
View File
@@ -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() {
+38
View File
@@ -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{
+19
View File
@@ -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")
}
}
+4
View File
@@ -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()