Files
Loki/internal/loki/sys_service_linux.go
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

142 lines
4.4 KiB
Go

//go:build linux
package loki
import (
"fmt"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
// serviceActionOS wraps `systemctl <action> <svc>` with passwordless sudo where
// it makes sense, and prints a follow-up status check after start/restart.
// Sans systemd (conteneur Docker, LOKI_CONTAINER=1), on bascule sur la
// supervision par fichier PID (sys_service_container.go).
func serviceActionOS(action string) error {
if !systemdAvailable() {
return userSvcAction(action)
}
svc := serviceName()
needsRoot := action == "start" || action == "stop" || action == "restart" || action == "enable" || action == "disable"
args := []string{}
bin := "systemctl"
if needsRoot && os.Geteuid() != 0 {
bin = "sudo"
args = append(args, "-n", "systemctl")
}
args = append(args, action, svc)
cmd := exec.Command(bin, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return err
}
switch action {
case "start", "restart":
return checkStarted(svc)
case "stop":
fmt.Println(green("[ok]") + " arrêté")
case "enable":
fmt.Println(green("[ok]") + " démarrage auto activé")
case "disable":
fmt.Println(green("[ok]") + " démarrage auto désactivé")
}
return nil
}
// unitState renvoie ActiveState et SubState de l'unité (« activating » /
// « auto-restart » par exemple).
func unitState(svc string) (active, sub string) {
out, _ := exec.Command("systemctl", "show", svc, "-p", "ActiveState", "-p", "SubState").Output()
for _, line := range strings.Split(string(out), "\n") {
k, v, ok := strings.Cut(strings.TrimSpace(line), "=")
if !ok {
continue
}
switch k {
case "ActiveState":
active = v
case "SubState":
sub = v
}
}
return
}
// checkStarted attend que l'unité se stabilise, puis dit la vérité.
//
// ⚠️ « activating » n'est PAS un succès : avec Restart=on-failure, un moteur qui
// meurt au démarrage repasse en boucle par activating (SubState auto-restart).
// L'ancien test l'acceptait, et `loki start` répondait « [ok] activating » sur
// un service qui ne démarrerait jamais — d'où des rapports « loki start dit ok
// mais loki test ne répond pas ». On distingue donc le SubState, et on laisse
// au moteur le temps de charger un gros modèle (il reste en activating, mais
// PAS en auto-restart).
func checkStarted(svc string) error {
var state, sub string
for i := 0; i < 5; i++ {
time.Sleep(2 * time.Second)
state, sub = unitState(svc)
if state == "active" {
fmt.Printf("%s %s: actif\n", green("[ok]"), svc)
return nil
}
if state == "failed" || sub == "auto-restart" {
break // inutile d'attendre : il boucle sur un échec
}
}
if state == "activating" && sub != "auto-restart" {
// Chargement en cours (un gros .gguf prend des minutes) : légitime.
fmt.Printf("%s %s: démarrage en cours (chargement du modèle) — %s pour suivre\n",
green("[ok]"), svc, bold("loki logs"))
return nil
}
what := state
if sub == "auto-restart" {
what = "redémarre en boucle (le moteur meurt au lancement)"
}
fmt.Printf("%s %s: %s — derniers logs :\n", red("[ERREUR]"), svc, what)
fmt.Println("------------------------------------------------")
logs, _ := exec.Command("journalctl", "-u", svc, "-n", "20", "--no-pager").Output()
fmt.Print(string(logs))
fmt.Println("------------------------------------------------")
fmt.Printf("→ loki logs pour plus de détails\n→ loki edit pour corriger la configuration (BIN, MODEL…)\n")
return fmt.Errorf("service %s non démarré", svc)
}
func serviceLogs() error {
if !systemdAvailable() {
return userServiceLogs()
}
cmd := exec.Command("journalctl", "-u", serviceName(), "-n", "80", "-f")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
// serviceIsActive reports whether the systemd unit is currently running.
func serviceIsActive() bool {
if !systemdAvailable() {
pid := readServicePID()
return pid > 0 && processAlive(pid)
}
out, _ := exec.Command("systemctl", "is-active", serviceName()).Output()
return strings.TrimSpace(string(out)) == "active"
}
// serviceLogTail renvoie les n dernières lignes du journal du service (pour
// l'UI web). Linux : journalctl.
func serviceLogTail(n int) string {
if !systemdAvailable() {
return tailFile(logFilePath(), n)
}
out, err := exec.Command("journalctl", "-u", serviceName(), "-n", strconv.Itoa(n), "--no-pager").CombinedOutput()
if err != nil && len(out) == 0 {
return "journalctl indisponible : " + err.Error()
}
return string(out)
}