mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
La relecture de 3a6ad6f a trouvé quatre trous, aucun ne touche la sortie du
modèle ; tous font que le garde-fou se trompe de verdict.
- Couches en RAM : lastOffload repartait de la dernière ligne contenant
« load_model » — or llama-server écrit « load_model: initializing… » APRÈS
le chargement, et « loading model tensors » aussi pour le brouillon. Le
contrôle ne trouvait jamais rien, ou ne regardait que le brouillon. Repère
désormais la ligne « loading model '<chemin>' » du serveur, et la pire des
lignes offloaded compte (modèle ou brouillon).
- Jeton de tentative : il était consommé (et l'échec inscrit) AVANT le test du
port. Un second « loki serve » refusé pendant que le premier chargeait
marquait la configuration comme ratée. Lecture d'abord (specAutoPeek), puis
rangement une fois le port libre (specAutoSettle).
- Process web : il effaçait le jeton sans le relire sous verrou, donc parfois
celui d'un lancement plus récent. specAttemptClear ne touche qu'au sien.
- MODEL_DRAFT vers une tête EAGLE-3 ou dFlash : Loki imposait draft-simple, que
le moteur ne peut pas charger. La note renvoie maintenant à -md et
--spec-type dans EXTRA_ARGS.
- Une tête MTP choisie comme MODEL_DRAFT compte comme modèle utilisé (« utilisé
par », garde à la suppression).
- Éditeur : choisir un brouillon sur un ancien preset dont le MTP est en
--spec-type dans EXTRA_ARGS fait passer le preset à SPEC=mtp. Sans ça, le
brouillon était ignoré.
- Tests : un journal réel (initializing après chargement, brouillon chargé à
part), un second lancement qui ne fait que lire, le jeton d'un autre
lancement gardé, les têtes eagle3/dflash, et MODEL_DRAFT dans les références.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
535 lines
19 KiB
Go
535 lines
19 KiB
Go
package loki
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"os"
|
||
"regexp"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
bolt "go.etcd.io/bbolt"
|
||
)
|
||
|
||
// Décodage spéculatif géré par Loki : une tête MTP (prédiction de plusieurs
|
||
// jetons) intégrée au modèle ou publiée à part, ou un petit modèle brouillon.
|
||
//
|
||
// SPEC=off (défaut) → rien, même avec MODEL_DRAFT
|
||
// SPEC=auto → seulement si tout est réuni (voir specAutoBlocker)
|
||
// SPEC=mtp → imposé, sans les garde-fous d'auto, avec un avertissement
|
||
// MODEL_DRAFT=<nom> → la tête ou le brouillon, résolu comme MMPROJ
|
||
// SPEC_N_MAX=<n> → --spec-draft-n-max (défaut du moteur : 3)
|
||
// SPEC_SAMPLING=probabilistic → seulement avec SPEC=mtp ; greedy sinon
|
||
//
|
||
// Rien n'y touche au modèle : chaque jeton émis est tiré par l'échantillonneur
|
||
// du modèle cible, un jeton du brouillon n'est gardé que s'il coïncide
|
||
// (common_sampler_sample_and_accept_n). Le prix est ailleurs : 1 à 2 Go de VRAM
|
||
// en plus, un prefill parfois plus lent, et une fonction très récente qui peut
|
||
// empêcher le moteur de démarrer. D'où l'opt-in, les garde-fous d'auto et le
|
||
// jeton de tentative (specAutoVerdict) : un lancement automatique qui n'a
|
||
// jamais répondu ne se retente pas avec la même configuration.
|
||
|
||
const (
|
||
// specAutoMinBuild : premier build officiel où Loki ose l'auto (MTP serveur,
|
||
// PR #22673 et suites). Build inconnu = non : un compilé maison ou un fork
|
||
// peut connaître le drapeau sans le tenir.
|
||
specAutoMinBuild = 11009
|
||
specNMaxLimit = 64
|
||
)
|
||
|
||
// specArgEnv : les variables qui règlent la spéculation sans drapeau. Posées,
|
||
// elles valent un réglage d'EXTRA_ARGS : Loki se tait.
|
||
var specArgEnv = []string{"LLAMA_ARG_SPEC_TYPE", "LLAMA_ARG_SPEC_DRAFT_MODEL", "LLAMA_ARG_SPEC_DRAFT_HF_REPO",
|
||
"LLAMA_ARG_SPEC_DRAFT_N_MAX", "LLAMA_ARG_SPEC_DRAFT_SAMPLING"}
|
||
|
||
// specUserFlags : la spéculation réglée à la main. Les ajouter EN PLUS ferait
|
||
// cumuler les types (--spec-type s'additionne) ou charger deux brouillons.
|
||
var specUserFlags = []string{"--spec-type", "-md", "--model-draft", "--spec-draft-model",
|
||
"-hfd", "-hfrd", "--spec-draft-hf", "--hf-repo-draft", "--spec-default"}
|
||
|
||
// probeSpec : variables du moteur, puis MODEL_DRAFT résolu comme MMPROJ (nom
|
||
// simple cherché dans les dossiers déclarés, ou chemin absolu). Introuvable ne
|
||
// bloque PAS le lancement, contrairement au projecteur : le brouillon n'est
|
||
// qu'une accélération, le moteur démarre sans et la note dit pourquoi.
|
||
func probeSpec(cfg map[string]string, si *serveSysInfo) {
|
||
if si.ArgEnv == nil {
|
||
si.ArgEnv = map[string]string{}
|
||
}
|
||
for _, k := range specArgEnv {
|
||
if v, ok := os.LookupEnv(k); ok && strings.TrimSpace(v) != "" {
|
||
si.ArgEnv[k] = v
|
||
}
|
||
}
|
||
d := strings.TrimSpace(cfg["MODEL_DRAFT"])
|
||
if d == "" {
|
||
return
|
||
}
|
||
p, err := resolveServeModelPath(d)
|
||
if err == nil {
|
||
_, err = os.Stat(p)
|
||
}
|
||
if err != nil {
|
||
si.DraftErr = err.Error()
|
||
return
|
||
}
|
||
si.Draft = p
|
||
if g, err := ggufMeta(p); err == nil {
|
||
si.DraftGGUF = &g
|
||
}
|
||
}
|
||
|
||
// specSidecarArch : têtes de spéculation qui ne sont pas des modèles autonomes
|
||
// (llama.cpp : draft-eagle3, draft-dflash/dspark). Un -md vers elles avec
|
||
// draft-simple ne démarrerait pas : leur type se règle à la main.
|
||
func specSidecarArch(arch string) bool {
|
||
switch strings.ToLower(strings.TrimSpace(arch)) {
|
||
case "eagle3", "dflash":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// specMode lit SPEC : "off", "auto", "mtp", ou "" si illisible.
|
||
func specMode(cfg map[string]string) string {
|
||
switch v := strings.ToLower(strings.TrimSpace(cfg["SPEC"])); v {
|
||
case "", "off", "non", "no", "0":
|
||
return "off"
|
||
case "auto", "mtp":
|
||
return v
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// specUserSet : EXTRA_ARGS ou l'environnement règlent déjà la spéculation.
|
||
func specUserSet(extra []string, argEnv map[string]string) bool {
|
||
if hasAnyFlag(extra, specUserFlags...) {
|
||
return true
|
||
}
|
||
for _, k := range []string{"LLAMA_ARG_SPEC_TYPE", "LLAMA_ARG_SPEC_DRAFT_MODEL", "LLAMA_ARG_SPEC_DRAFT_HF_REPO"} {
|
||
if argEnv[k] != "" {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
// helpSupportsMTP : le moteur connaît le type draft-mtp ET le drapeau actuel
|
||
// du nombre de jetons — un moteur qui n'a que l'ancien --draft-max est d'une
|
||
// autre génération, où un MTP n'existe pas.
|
||
func helpSupportsMTP(help string) bool {
|
||
return strings.Contains(help, "draft-mtp") && strings.Contains(help, "--spec-draft-n-max")
|
||
}
|
||
|
||
// ctxFixed : le contexte que verra le moteur (EXTRA_ARGS, sinon CTX, sinon le
|
||
// défaut de buildServeArgs), et s'il est chiffré. Avec 0 ou une valeur
|
||
// illisible, --fit a le droit de RÉDUIRE le contexte pour tenir.
|
||
func ctxFixed(cfg map[string]string, extra []string) (string, bool) {
|
||
c := flagValue(extra, "-c", "--ctx-size")
|
||
if c == "" {
|
||
c = strings.TrimSpace(cfg["CTX"])
|
||
}
|
||
if c == "" {
|
||
c = "32768"
|
||
}
|
||
n, err := strconv.Atoi(c)
|
||
return c, err == nil && n > 0
|
||
}
|
||
|
||
// statefulSampler : un échantillonneur dont l'état avance à chaque tirage
|
||
// (mirostat, adaptive-p). Le rejet probabiliste fait d'abord tirer le modèle
|
||
// cible, puis peut émettre un autre jeton : ces échantillonneurs avanceraient
|
||
// sur un jeton que la réponse ne contient pas. Le mode greedy, lui, reste exact.
|
||
func statefulSampler(extra []string) string {
|
||
for _, v := range flagValues(extra, "--mirostat") {
|
||
if v = strings.TrimSpace(v); v != "" && v != "0" {
|
||
return "--mirostat " + v
|
||
}
|
||
}
|
||
for _, v := range flagValues(extra, "--samplers") {
|
||
if strings.Contains(strings.ToLower(strings.ReplaceAll(v, "-", "_")), "adaptive_p") {
|
||
return "--samplers adaptive-p"
|
||
}
|
||
}
|
||
for _, v := range flagValues(extra, "--sampling-seq", "--sampler-seq") {
|
||
if strings.Contains(v, "a") {
|
||
return "--sampling-seq avec adaptive-p"
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// specAutoBlocker dit pourquoi SPEC=auto ne s'active pas, ou vide. Chaque
|
||
// garde répond à une panne précise :
|
||
//
|
||
// - qwen4exp : MTP fusionné il y a quelques jours, OOM signalés en multi-GPU ;
|
||
// - placement manuel (couches fixées, -ts, -ot, experts sur CPU, -sm row,
|
||
// --fit off, -dev) : seul --fit compte la VRAM de la tête MTP ;
|
||
// - contexte non chiffré : fit aurait le droit de le RÉDUIRE pour faire de
|
||
// la place — de l'information perdue pour le modèle ;
|
||
// - projecteur vision : une VRAM déjà serrée par un second modèle ;
|
||
// - build officiel inconnu ou trop ancien ;
|
||
// - un essai précédent avec la même configuration n'a jamais répondu.
|
||
func specAutoBlocker(cfg map[string]string, extra []string, si serveSysInfo) string {
|
||
if si.GGUF != nil && strings.EqualFold(si.GGUF.Arch, "qwen4exp") {
|
||
return "qwen4exp : MTP trop récent pour ce moteur, SPEC=mtp pour l'imposer"
|
||
}
|
||
if why := fitBlocker(cfg, extra, si); why != "" {
|
||
return "placement manuel (" + why + ") — --fit ne compte pas la VRAM du brouillon"
|
||
}
|
||
if hasAnyFlag(extra, "-dev", "--device") || si.ArgEnv["LLAMA_ARG_DEVICE"] != "" {
|
||
return "placement manuel (--device) — --fit ne compte pas la VRAM du brouillon"
|
||
}
|
||
if _, ok := ctxFixed(cfg, extra); !ok {
|
||
return "contexte non chiffré — fit pourrait le réduire pour loger le brouillon"
|
||
}
|
||
if si.MMProj != "" || hasAnyFlag(extra, "--mmproj", "-mm") {
|
||
return "projecteur vision chargé"
|
||
}
|
||
if si.EngineBuild < specAutoMinBuild {
|
||
if si.EngineBuild <= 0 {
|
||
return "build du moteur inconnu (compilé ou fork)"
|
||
}
|
||
return fmt.Sprintf("moteur b%d, b%d au moins", si.EngineBuild, specAutoMinBuild)
|
||
}
|
||
return si.SpecAutoBlocked
|
||
}
|
||
|
||
// specArgs : drapeaux de décodage spéculatif que Loki ajoute, ce qu'il en dit,
|
||
// et s'ils viennent de SPEC=auto (cmdServe pose alors le jeton de tentative).
|
||
// Fonction pure ; cmdServe a déjà résolu MODEL_DRAFT et lu les GGUF.
|
||
func specArgs(cfg map[string]string, extra []string, si serveSysInfo) (args, notes []string, auto bool) {
|
||
mode := specMode(cfg)
|
||
draftKey := strings.TrimSpace(cfg["MODEL_DRAFT"])
|
||
switch mode {
|
||
case "":
|
||
return nil, []string{"SPEC=" + cfg["SPEC"] + " illisible (off, auto ou mtp) : sans décodage spéculatif"}, false
|
||
case "off":
|
||
if draftKey != "" {
|
||
return nil, []string{"MODEL_DRAFT ignoré : SPEC=off (auto ou mtp pour s'en servir)"}, false
|
||
}
|
||
return nil, nil, false
|
||
}
|
||
if specUserSet(extra, si.ArgEnv) {
|
||
return nil, []string{"décodage spéculatif déjà réglé dans EXTRA_ARGS ou LLAMA_ARG_* : SPEC ignoré"}, false
|
||
}
|
||
skip := func(why string) ([]string, []string, bool) {
|
||
return nil, []string{"SPEC=" + mode + " : " + why + " — sans décodage spéculatif"}, false
|
||
}
|
||
|
||
// La source : MODEL_DRAFT s'il est posé, sinon la tête MTP du modèle. Dans
|
||
// les deux cas, on regarde les TENSEURS, comme llama.cpp : une clé
|
||
// nextn_predict_layers seule ne prouve pas que la tête est dans le fichier.
|
||
mtp := false
|
||
switch {
|
||
case draftKey != "":
|
||
switch {
|
||
case si.Draft == "":
|
||
why := "MODEL_DRAFT=" + draftKey + " introuvable"
|
||
if si.DraftErr != "" {
|
||
why += " (" + si.DraftErr + ")"
|
||
}
|
||
return skip(why)
|
||
case si.DraftGGUF == nil:
|
||
return skip("MODEL_DRAFT illisible (GGUF incomplet ou en cours de téléchargement ?)")
|
||
case si.DraftGGUF.HasNextNTensor:
|
||
if !helpSupportsMTP(si.Help) {
|
||
return skip("ce moteur ne connaît pas draft-mtp")
|
||
}
|
||
// Type explicite : llama.cpp ne le devine que sur la première tranche.
|
||
args, mtp = []string{"-md", si.Draft, "--spec-type", "draft-mtp"}, true
|
||
case specSidecarArch(si.DraftGGUF.Arch):
|
||
return skip("MODEL_DRAFT est une tête " + si.DraftGGUF.Arch +
|
||
", pas un modèle brouillon : règle -md et --spec-type dans EXTRA_ARGS")
|
||
default:
|
||
if !strings.Contains(si.Help, "--spec-type") || !strings.Contains(si.Help, "draft-simple") {
|
||
return skip("ce moteur ne connaît pas --spec-type draft-simple")
|
||
}
|
||
// Sans type, un brouillon qui n'est pas une tête MTP serait chargé en
|
||
// VRAM puis jamais utilisé.
|
||
args = []string{"-md", si.Draft, "--spec-type", "draft-simple"}
|
||
}
|
||
case si.GGUF == nil:
|
||
return skip("métadonnées du modèle illisibles")
|
||
case !si.GGUF.HasNextNTensor:
|
||
if mode == "mtp" || si.GGUF.NextN > 0 {
|
||
return skip("pas de tête MTP dans ce fichier (publiée à part ? MODEL_DRAFT=mtp-….gguf)")
|
||
}
|
||
return nil, nil, false // auto sur un modèle sans MTP : rien à dire
|
||
case !helpSupportsMTP(si.Help):
|
||
return skip("ce moteur ne connaît pas draft-mtp")
|
||
default:
|
||
args, mtp = []string{"--spec-type", "draft-mtp"}, true
|
||
}
|
||
|
||
label := "brouillon " + baseName(si.Draft)
|
||
if mtp {
|
||
label = "tête MTP"
|
||
}
|
||
if mode == "auto" {
|
||
if why := specAutoBlocker(cfg, extra, si); why != "" {
|
||
return skip(why)
|
||
}
|
||
auto = true
|
||
notes = append(notes, "SPEC=auto → "+label+" : sortie inchangée (chaque jeton est vérifié par le modèle), "+
|
||
"~1-2 Go de VRAM en plus. Mesure le prefill ; SPEC=off pour couper.")
|
||
} else {
|
||
note := "SPEC=mtp → " + label + " imposé : sortie inchangée, ~1-2 Go de VRAM en plus"
|
||
if why := fitBlocker(cfg, extra, si); why != "" || hasAnyFlag(extra, "-dev", "--device") {
|
||
if why == "" {
|
||
why = "--device"
|
||
}
|
||
note += " ; placement manuel (" + why + ") : --fit ne compte pas le brouillon, " +
|
||
"place-le avec -devd / -ngld si la VRAM manque"
|
||
}
|
||
if _, ok := ctxFixed(cfg, extra); !ok {
|
||
note += " ; contexte non chiffré : fit pourrait le réduire pour loger le brouillon"
|
||
}
|
||
if si.GGUF != nil && strings.EqualFold(si.GGUF.Arch, "qwen4exp") {
|
||
note += " ; MTP qwen4exp très récent"
|
||
}
|
||
notes = append(notes, note)
|
||
}
|
||
|
||
// Nombre de jetons anticipés : défaut du moteur (3) si la clé est vide.
|
||
if v := strings.TrimSpace(cfg["SPEC_N_MAX"]); v != "" {
|
||
n, err := strconv.Atoi(v)
|
||
switch {
|
||
case err != nil || n < 1 || n > specNMaxLimit:
|
||
notes = append(notes, "SPEC_N_MAX="+v+" illisible (1 à 64 jetons) : défaut du moteur")
|
||
case !strings.Contains(si.Help, "--spec-draft-n-max"):
|
||
notes = append(notes, "ce moteur ne connaît pas --spec-draft-n-max : SPEC_N_MAX ignoré")
|
||
case hasAnyFlag(extra, "--spec-draft-n-max") || si.ArgEnv["LLAMA_ARG_SPEC_DRAFT_N_MAX"] != "":
|
||
default:
|
||
args = append(args, "--spec-draft-n-max", strconv.Itoa(n))
|
||
}
|
||
}
|
||
|
||
// Tirage du brouillon. greedy est exact quelle que soit la chaîne
|
||
// d'échantillonnage : on le fixe dès que le moteur connaît le drapeau, pour
|
||
// qu'un changement de défaut en amont ne change rien ici.
|
||
sampling := "greedy"
|
||
switch v := strings.ToLower(strings.TrimSpace(cfg["SPEC_SAMPLING"])); v {
|
||
case "", "greedy":
|
||
case "probabilistic":
|
||
switch {
|
||
case mode != "mtp":
|
||
notes = append(notes, "SPEC_SAMPLING=probabilistic ignoré avec SPEC=auto (SPEC=mtp pour le choisir) : greedy")
|
||
case statefulSampler(extra) != "":
|
||
notes = append(notes, "SPEC_SAMPLING=probabilistic refusé avec "+statefulSampler(extra)+
|
||
" (cet échantillonneur avancerait sur des jetons rejetés) : greedy")
|
||
case !strings.Contains(si.Help, "probabilistic"):
|
||
notes = append(notes, "ce moteur ne connaît pas --spec-draft-sampling probabilistic : greedy")
|
||
default:
|
||
sampling = v
|
||
}
|
||
default:
|
||
notes = append(notes, "SPEC_SAMPLING="+v+" illisible (greedy ou probabilistic) : greedy")
|
||
}
|
||
if strings.Contains(si.Help, "--spec-draft-sampling") && !hasAnyFlag(extra, "--spec-draft-sampling") &&
|
||
si.ArgEnv["LLAMA_ARG_SPEC_DRAFT_SAMPLING"] == "" {
|
||
args = append(args, "--spec-draft-sampling", sampling)
|
||
}
|
||
return args, notes, auto
|
||
}
|
||
|
||
// --- Jeton de tentative ------------------------------------------------------
|
||
//
|
||
// Un essai automatique qui fait tomber le moteur ne doit pas se rejouer : sous
|
||
// systemd il redémarrerait en boucle en rechargeant 15 Go à chaque fois. cmdServe
|
||
// ne peut pas constater l'échec lui-même (sous Unix il DEVIENT llama-server).
|
||
// Il pose donc un jeton avant de lancer ; le process web l'efface dès que le
|
||
// moteur répond. Au lancement suivant, un jeton encore là pour la même
|
||
// configuration, le même moteur et le même build veut dire « jamais répondu » :
|
||
// l'auto est coupé pour cette combinaison, et on le dit. Une mise à jour du
|
||
// moteur ou un preset modifié retente. Sans process web (CLI seule), le jeton
|
||
// n'est jamais effacé : on retombe sur off, le côté sûr.
|
||
|
||
const (
|
||
specAttemptKey = "spec_auto_attempt"
|
||
specFailedKey = "spec_auto_failed"
|
||
specFailedMax = 32
|
||
)
|
||
|
||
// specAutoMark identifie une combinaison preset × moteur.
|
||
type specAutoMark struct {
|
||
FP string `json:"fp"`
|
||
Bin string `json:"bin"`
|
||
Build int `json:"build"`
|
||
}
|
||
|
||
func (m specAutoMark) key() string { return fmt.Sprintf("%s|%s|%d", m.FP, m.Bin, m.Build) }
|
||
|
||
// specAutoVerdict : pure. Renvoie la raison de couper l'auto (vide = permis)
|
||
// et s'il faut inscrire cur parmi les échecs.
|
||
func specAutoVerdict(cur specAutoMark, attempt *specAutoMark, failed map[string]string) (why string, record bool) {
|
||
if w := failed[cur.key()]; w != "" {
|
||
return w, false
|
||
}
|
||
if attempt != nil && attempt.key() == cur.key() {
|
||
return "le dernier lancement avec cette configuration n'a jamais répondu", true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
// specAutoPeek lit l'état sans rien changer : la raison de couper l'auto (vide
|
||
// = permis) et s'il faudra inscrire l'échec. Rien n'est consommé avant que le
|
||
// port soit libre (specAutoSettle) : un second « loki serve », refusé parce que
|
||
// le premier charge encore, ne doit pas prendre le jeton de celui-ci pour un
|
||
// échec.
|
||
func specAutoPeek(cur specAutoMark) (why string, record bool) {
|
||
_ = view(bkState, func(b *bolt.Bucket) error {
|
||
attempt, failed := readSpecAuto(b)
|
||
why, record = specAutoVerdict(cur, attempt, failed)
|
||
return nil
|
||
})
|
||
return why, record
|
||
}
|
||
|
||
// specAutoSettle range, port libre, juste avant de lancer le moteur : l'ancien
|
||
// jeton est consommé, l'échec inscrit s'il y a lieu (record, why), et un
|
||
// nouveau jeton posé si ce lancement ajoute des drapeaux automatiques.
|
||
func specAutoSettle(cur specAutoMark, why string, record, attempt bool) {
|
||
_ = update(bkState, func(b *bolt.Bucket) error {
|
||
_, failed := readSpecAuto(b)
|
||
_ = b.Delete([]byte(specAttemptKey))
|
||
if record {
|
||
if err := putFailed(b, failed, cur.key(), why); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if !attempt {
|
||
return nil
|
||
}
|
||
raw, err := json.Marshal(cur)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.Put([]byte(specAttemptKey), raw)
|
||
})
|
||
}
|
||
|
||
// readSpecAuto : le jeton en cours (nil = aucun) et les échecs inscrits.
|
||
func readSpecAuto(b *bolt.Bucket) (attempt *specAutoMark, failed map[string]string) {
|
||
if raw := b.Get([]byte(specAttemptKey)); raw != nil {
|
||
var a specAutoMark
|
||
if json.Unmarshal(raw, &a) == nil {
|
||
attempt = &a
|
||
}
|
||
}
|
||
failed = map[string]string{}
|
||
if raw := b.Get([]byte(specFailedKey)); raw != nil {
|
||
_ = json.Unmarshal(raw, &failed)
|
||
}
|
||
return attempt, failed
|
||
}
|
||
|
||
func putFailed(b *bolt.Bucket, failed map[string]string, key, why string) error {
|
||
if len(failed) >= specFailedMax {
|
||
failed = map[string]string{}
|
||
}
|
||
failed[key] = why
|
||
raw, err := json.Marshal(failed)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return b.Put([]byte(specFailedKey), raw)
|
||
}
|
||
|
||
// --- Côté process web --------------------------------------------------------
|
||
|
||
var (
|
||
specWatchOnce sync.Once
|
||
offloadedRe = regexp.MustCompile(`offloaded (\d+)/(\d+) layers to GPU`)
|
||
)
|
||
|
||
// startSpecAttemptWatch : une goroutine qui efface le jeton dès que le moteur
|
||
// répond, sans attendre qu'une page interroge /api/status.
|
||
func startSpecAttemptWatch() {
|
||
specWatchOnce.Do(func() {
|
||
go func() {
|
||
t := time.NewTicker(5 * time.Second)
|
||
defer t.Stop()
|
||
for range t.C {
|
||
specAttemptTick()
|
||
}
|
||
}()
|
||
})
|
||
}
|
||
|
||
func specAttemptTick() {
|
||
var a specAutoMark
|
||
if !getJSON(bkState, specAttemptKey, &a) {
|
||
return
|
||
}
|
||
if externalActive() {
|
||
specAttemptClear(a, "") // plus de moteur local à attendre
|
||
return
|
||
}
|
||
if !healthCheck() {
|
||
return
|
||
}
|
||
// Répondu. Mais avec -c fixé, fit ne réduit pas le contexte : s'il manque
|
||
// la place du brouillon, il descend des couches en RAM — le moteur tourne,
|
||
// plus lentement que sans MTP. Ça compte aussi comme un échec de l'auto.
|
||
why := ""
|
||
if n, m := lastOffload(serviceLogTail(600)); m > 0 && n < m {
|
||
why = fmt.Sprintf("seulement %d/%d couches sur GPU avec le brouillon : fit a déplacé des couches en RAM", n, m)
|
||
fmt.Println("[loki] SPEC=auto : " + why + " — coupé au prochain démarrage (SPEC=mtp pour l'imposer)")
|
||
}
|
||
specAttemptClear(a, why)
|
||
}
|
||
|
||
// specAttemptClear efface le jeton a — et lui seul : relu sous verrou, car un
|
||
// « loki serve » a pu entre-temps poser celui d'un AUTRE lancement, qui n'a pas
|
||
// encore répondu. why non vide inscrit l'échec de a.
|
||
func specAttemptClear(a specAutoMark, why string) {
|
||
_ = update(bkState, func(b *bolt.Bucket) error {
|
||
cur, failed := readSpecAuto(b)
|
||
if cur == nil || cur.key() != a.key() {
|
||
return nil
|
||
}
|
||
_ = b.Delete([]byte(specAttemptKey))
|
||
if why == "" {
|
||
return nil
|
||
}
|
||
return putFailed(b, failed, a.key(), why)
|
||
})
|
||
}
|
||
|
||
// lastOffload lit les lignes « offloaded N/M layers to GPU » du DERNIER
|
||
// chargement du journal, repéré à la ligne du serveur « loading model
|
||
// '<chemin>' ». Pas à « load_model » (le serveur écrit encore « load_model:
|
||
// initializing… » APRÈS le chargement) ni à « loading model tensors » (écrit
|
||
// aussi pour le brouillon, qui masquerait le modèle). Un brouillon chargé à
|
||
// part a sa propre ligne : la pire des deux compte. 0, 0 = introuvable : on ne
|
||
// conclut rien.
|
||
func lastOffload(log string) (n, m int) {
|
||
lines := strings.Split(log, "\n")
|
||
start := -1
|
||
for i, l := range lines {
|
||
if strings.Contains(l, "loading model '") {
|
||
start = i
|
||
}
|
||
}
|
||
if start < 0 {
|
||
return 0, 0
|
||
}
|
||
for _, l := range lines[start:] {
|
||
s := offloadedRe.FindStringSubmatch(l)
|
||
if s == nil {
|
||
continue
|
||
}
|
||
a, _ := strconv.Atoi(s[1])
|
||
b, _ := strconv.Atoi(s[2])
|
||
if m == 0 || a < b {
|
||
n, m = a, b
|
||
}
|
||
if a < b {
|
||
break
|
||
}
|
||
}
|
||
return n, m
|
||
}
|