mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Optimiseur : « loki tune » refuse un moteur qui n'est pas celui de son LOKI_HOME
En ligne de commande, « loki tune » et le processus web avec des LOKI_HOME différents (sudo qui retire un LOKI_HOME exporté, variable posée dans un seul shell) ne se voient pas : le verrou tombait dans un dossier que l'interface ne lit pas, l'arrêt du « vrai » moteur visait celui d'une autre configuration, et l'essai chargeait à côté d'un moteur bien vivant — VRAM saturée, mesures fausses, relance possible par l'interface en pleine mesure. - Avant de commencer, la CLI regarde ce qui répond sur le port de son LOKI_HOME : un llama-server (/health 200, 503 ou 401) alors que ce dossier dit son moteur arrêté, un 401 sur /props avec sa clé d'API, ou un model_path qui n'est pas son MODEL (même fichier vérifié par os.SameFile) — refus, avec LOKI_HOME en clair et la marche à suivre (relancer avec le bon LOKI_HOME, ou le bouton « Optimiser… »). - Tout ce qui ne conclut pas laisse passer : rien n'écoute, autre service (404), /props absent d'un moteur ancien, chemin illisible d'ici. Le processus web n'est pas concerné : il fait foi pour son moteur. - Test : faux moteurs httptest pour chaque cas, refus et passages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a325e98dad
commit
ac40a2556a
3 files changed
+128
-1
No files matched your search
@@ -685,7 +685,11 @@ Ajoutées par ce fork :
|
||||
par `kill -9` ne laisse plus le moteur arrêté. **Refus d'entrée** : preset
|
||||
externe, aucun preset actif, génération, tâche, bench ou job `bash_bg` en
|
||||
cours, moteur occupé (`/slots`) — en ligne de commande, seul ce dernier
|
||||
contrôle voit le processus web : préférer le bouton. **Essais** (descente étape par étape depuis
|
||||
contrôle voit le processus web : préférer le bouton. En ligne de commande
|
||||
aussi, un moteur qui ne serait pas celui de ce `LOKI_HOME` (il répond alors
|
||||
que ce dossier le dit arrêté, refuse sa clé d'API ou sert un autre `MODEL` :
|
||||
typiquement `sudo loki tune`, qui perd un `LOKI_HOME` exporté) fait
|
||||
refuser, avec la marche à suivre. **Essais** (descente étape par étape depuis
|
||||
la meilleure configuration du moment, chaque axe seulement si l'aide du
|
||||
moteur et la machine le permettent) : placement — seulement avec « inclure
|
||||
le placement » / `--placement` : `--fit` à la place des experts placés à la
|
||||
|
||||
@@ -197,6 +197,17 @@ func tunePreflight(ctx context.Context, inWeb bool) error {
|
||||
bgJobsRunning())
|
||||
}
|
||||
}
|
||||
// En ligne de commande : le moteur qui répond sur le port de CE LOKI_HOME
|
||||
// est-il bien le sien ? (Le processus web, lui, fait foi pour son moteur.)
|
||||
if !inWeb {
|
||||
cfg := ReadConfig()
|
||||
ours, _ := resolveServeModelPath(strings.TrimSpace(cfg["MODEL"]))
|
||||
auth := tuneAuth()
|
||||
if err := tuneForeignEngine(ctx, fmt.Sprintf("http://localhost:%d", LLMPort()), serviceIsActive(),
|
||||
func(r *http.Request) { auth(r.Header.Set) }, ours); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if serviceIsActive() {
|
||||
e := benchEngine{base: fmt.Sprintf("http://localhost:%d", LLMPort()), auth: tuneAuth(), client: http.DefaultClient}
|
||||
if err := e.idle(ctx); err != nil {
|
||||
@@ -206,6 +217,58 @@ func tunePreflight(ctx context.Context, inWeb bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// tuneForeignEngine : « loki tune » et le processus web avec des LOKI_HOME
|
||||
// différents (sudo qui retire la variable, LOKI_HOME exporté dans un seul
|
||||
// shell) ne se voient pas : le verrou de l'optimisation tombe dans un dossier
|
||||
// que l'interface ne lit pas, et l'arrêt du « vrai » moteur vise celui d'une
|
||||
// autre configuration. L'essai chargeait alors à côté d'un moteur bien vivant
|
||||
// — VRAM saturée, mesures fausses — et l'interface pouvait relancer le sien
|
||||
// en pleine mesure. On regarde donc ce qui répond sur le port de CE
|
||||
// LOKI_HOME :
|
||||
//
|
||||
// - un serveur répond alors que ce LOKI_HOME dit son moteur arrêté : un autre
|
||||
// Loki, ou un llama-server orphelin ;
|
||||
// - le moteur refuse la clé d'API de ce LOKI_HOME (401 sur /props), ou sert
|
||||
// un autre fichier que son MODEL : le moteur d'une autre configuration.
|
||||
//
|
||||
// Tout ce qui ne conclut pas (pas de réponse, /props absent d'un moteur
|
||||
// ancien, chemin illisible d'ici) laisse passer : seul un désaccord constaté
|
||||
// refuse.
|
||||
func tuneForeignEngine(ctx context.Context, base string, active bool, auth func(*http.Request), ourModel string) error {
|
||||
p := tplProber{base: base, auth: auth, client: &http.Client{Timeout: 2 * time.Second}}
|
||||
// /health d'un llama-server : 200, 503 en chargement, 401 derrière une clé
|
||||
// sur certaines versions. Rien n'écoute, ou un autre service (404…) : aucun
|
||||
// moteur à craindre.
|
||||
var he *tplHTTPError
|
||||
if _, err := p.do(ctx, http.MethodGet, "/health", nil); err != nil &&
|
||||
(!errors.As(err, &he) || (he.status != http.StatusServiceUnavailable && he.status != http.StatusUnauthorized)) {
|
||||
return nil
|
||||
}
|
||||
home := LokiHome()
|
||||
foreign := func(why string) error {
|
||||
return fmt.Errorf("%s, alors que LOKI_HOME=%s — un autre Loki tourne sans doute avec un autre LOKI_HOME "+
|
||||
"(sudo retire la variable) : relance avec le sien (LOKI_HOME=… loki tune) ou utilise le bouton "+
|
||||
"« Optimiser… » de l'interface ; un llama-server orphelin, lui, s'arrête à la main", why, home)
|
||||
}
|
||||
if !active {
|
||||
return foreign("un moteur répond sur " + base + " sans que ce dossier le suive")
|
||||
}
|
||||
props, err := p.props(ctx)
|
||||
switch {
|
||||
case errors.As(err, &he) && he.status == http.StatusUnauthorized:
|
||||
return foreign("le moteur de " + base + " refuse la clé d'API de ce dossier")
|
||||
case err != nil || props.ModelPath == "" || ourModel == "":
|
||||
return nil
|
||||
}
|
||||
a, errA := os.Stat(props.ModelPath)
|
||||
b, errB := os.Stat(ourModel)
|
||||
if errA == nil && errB == nil && !os.SameFile(a, b) {
|
||||
return foreign(fmt.Sprintf("le moteur de %s sert %s, pas le MODEL de ce dossier (%s ; config.env modifiée "+
|
||||
"sans redémarrage ? « loki restart »)", base, filepath.Base(props.ModelPath), filepath.Base(ourModel)))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// tunePhysCores : cœurs physiques (Linux ; ailleurs inconnu, l'axe threads est
|
||||
// sauté). Un conteneur à l'étroit (cpuset, quota) donne sa propre limite.
|
||||
func tunePhysCores(probe *tuneProbe) int {
|
||||
|
||||
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -897,3 +899,61 @@ func TestTuneRecoverWatch(t *testing.T) {
|
||||
t.Error("verrou périmé non retiré")
|
||||
}
|
||||
}
|
||||
|
||||
// « loki tune » d'un autre LOKI_HOME que le processus web (sudo) : le moteur
|
||||
// qui répond sur le port de ce dossier n'est pas le sien — refus en clair.
|
||||
// Tout ce qui ne conclut pas laisse passer.
|
||||
func TestTuneForeignEngine(t *testing.T) {
|
||||
home := testHome(t)
|
||||
ours := filepath.Join(home, "ours.gguf")
|
||||
other := filepath.Join(home, "other.gguf")
|
||||
for _, p := range []string{ours, other} {
|
||||
if err := os.WriteFile(p, []byte("GGUF"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
engine := func(health, props int, model string) *httptest.Server {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/health":
|
||||
w.WriteHeader(health)
|
||||
case "/props":
|
||||
w.WriteHeader(props)
|
||||
if props == http.StatusOK {
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"model_path": model})
|
||||
}
|
||||
default:
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}
|
||||
}))
|
||||
t.Cleanup(srv.Close)
|
||||
return srv
|
||||
}
|
||||
closed := httptest.NewServer(http.NotFoundHandler())
|
||||
closed.Close()
|
||||
ctx := context.Background()
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
base string
|
||||
active bool
|
||||
refused bool
|
||||
}{
|
||||
{"rien n'écoute", closed.URL, false, false},
|
||||
{"autre service (404) sur le port, moteur arrêté", engine(404, 404, "").URL, false, false},
|
||||
{"moteur qui répond, ce dossier le dit arrêté", engine(200, 200, ours).URL, false, true},
|
||||
{"moteur en chargement (503), ce dossier le dit arrêté", engine(503, 503, "").URL, false, true},
|
||||
{"notre moteur", engine(200, 200, ours).URL, true, false},
|
||||
{"un autre MODEL", engine(200, 200, other).URL, true, true},
|
||||
{"clé d'API refusée", engine(200, 401, "").URL, true, true},
|
||||
{"/props absent (moteur ancien)", engine(200, 404, "").URL, true, false},
|
||||
{"chemin illisible d'ici", engine(200, 200, filepath.Join(home, "absent.gguf")).URL, true, false},
|
||||
} {
|
||||
err := tuneForeignEngine(ctx, c.base, c.active, nil, ours)
|
||||
if (err != nil) != c.refused {
|
||||
t.Errorf("%s : %v", c.name, err)
|
||||
}
|
||||
if err != nil && !strings.Contains(err.Error(), "LOKI_HOME="+home) {
|
||||
t.Errorf("%s : message sans LOKI_HOME : %v", c.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user