mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-15 03:08:56 +02:00
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>
249 lines
7.9 KiB
Go
249 lines
7.9 KiB
Go
package loki
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Le benchmark tourne en TÂCHE DE FOND. Le mode complet dure de une à cinq
|
|
// minutes : une requête HTTP synchrone était coupée bien avant par un reverse
|
|
// proxy (nginx d'Unraid à 60 s, Cloudflare à 100 s) ou par le délai de
|
|
// l'interface (30 s), pendant que le moteur, lui, continuait — et un second
|
|
// clic empilait un deuxième bench derrière le premier. Désormais :
|
|
//
|
|
// POST /api/bench lance (202 + id), 409 si le moteur est occupé
|
|
// GET /api/bench/status phase en cours, puis résultat ou erreur
|
|
// POST /api/bench/cancel annule (chaque requête au moteur porte le contexte)
|
|
// GET /api/bench/last dernier résultat enregistré
|
|
//
|
|
// Pendant toute la mesure, le bench tient le verrou de génération de la
|
|
// conversation : un message, une tâche planifiée ou une compaction reçoivent
|
|
// un refus clair au lieu d'attendre des minutes dans la file du moteur. Les
|
|
// clients /v1 reçoivent un 503 avec Retry-After (llm_oai.go). Restent hors du
|
|
// verrou les appels annexes déclenchés par la fin d'un tour (titre, mémoire) :
|
|
// le verrou les précède de toute façon.
|
|
|
|
var errBenchBusy = errors.New("un benchmark occupe le moteur — attends sa fin ou annule-le")
|
|
|
|
// benchLease prend le verrou de génération pour un benchmark. cancel est
|
|
// branché sur le bouton stop du chat, comme pour une tâche planifiée. La
|
|
// fonction rendue le libère tant que benching dit que le verrou est encore au
|
|
// bench : un Reset l'a peut-être déjà rendu (benching remis à faux), et le tour
|
|
// qui a pu démarrer depuis ne doit pas être déclaré libre. L'epoch ne suffit
|
|
// pas : changer de discussion le bumpe SANS arrêter le bench (loadFrom).
|
|
func (c *Conversation) benchLease(cancel context.CancelFunc) (func(), error) {
|
|
return c.measureLease(cancel, errBenchBusy)
|
|
}
|
|
|
|
// measureLease : le verrou de génération pour une mesure du moteur — bench,
|
|
// ou optimiseur (backend_tune_job.go) — why étant le refus que reçoivent les
|
|
// autres pendant ce temps.
|
|
func (c *Conversation) measureLease(cancel context.CancelFunc, why error) (func(), error) {
|
|
c.mu.Lock()
|
|
defer c.mu.Unlock()
|
|
if c.Generating {
|
|
return nil, ErrBusy
|
|
}
|
|
c.Generating, c.benching, c.cancel, c.leaseWhy = true, true, cancel, why
|
|
var once sync.Once
|
|
return func() {
|
|
once.Do(func() {
|
|
c.mu.Lock()
|
|
if c.benching {
|
|
c.Generating, c.benching, c.cancel, c.leaseWhy = false, false, nil, nil
|
|
}
|
|
c.mu.Unlock()
|
|
})
|
|
}, nil
|
|
}
|
|
|
|
// benchJob : le benchmark en cours ou le dernier terminé (un seul à la fois).
|
|
var benchJob struct {
|
|
mu sync.Mutex
|
|
seq int
|
|
running bool
|
|
id string
|
|
mode string
|
|
phase string
|
|
step int
|
|
steps int
|
|
started time.Time
|
|
finished time.Time
|
|
cancel context.CancelFunc
|
|
result *benchResult
|
|
err string
|
|
canceled bool
|
|
}
|
|
|
|
// benchRunning : un benchmark de ce processus est-il en cours ?
|
|
func benchRunning() bool {
|
|
benchJob.mu.Lock()
|
|
defer benchJob.mu.Unlock()
|
|
return benchJob.running
|
|
}
|
|
|
|
// benchRunner est runBench, remplaçable dans les tests.
|
|
var benchRunner = runBench
|
|
|
|
// benchStart lance un benchmark en arrière-plan. code : le statut HTTP à
|
|
// renvoyer en cas de refus.
|
|
func benchStart(opts benchOpts) (id string, code int, err error) {
|
|
// Hors verrou : healthCheck peut attendre le moteur quelques secondes, le
|
|
// suivi de progression ne doit pas rester bloqué derrière.
|
|
if externalActive() {
|
|
return "", http.StatusBadRequest, errors.New("benchmark indisponible : le preset actif est une API externe")
|
|
}
|
|
if err := tuneGuard(); err != nil {
|
|
return "", http.StatusConflict, err
|
|
}
|
|
if !healthCheck() {
|
|
return "", http.StatusServiceUnavailable, errModelLoading
|
|
}
|
|
benchJob.mu.Lock()
|
|
defer benchJob.mu.Unlock()
|
|
if benchJob.running {
|
|
return "", http.StatusConflict, errors.New("un benchmark tourne déjà")
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
release, err := conv.benchLease(cancel)
|
|
if err != nil {
|
|
cancel()
|
|
return "", http.StatusConflict, conv.busyReason()
|
|
}
|
|
benchJob.seq++
|
|
id = fmt.Sprintf("b%d-%d", time.Now().Unix(), benchJob.seq)
|
|
benchJob.running, benchJob.id, benchJob.mode = true, id, opts.Mode
|
|
benchJob.phase, benchJob.step, benchJob.steps = "démarrage", 0, 0
|
|
benchJob.started, benchJob.finished = time.Now(), time.Time{}
|
|
benchJob.cancel, benchJob.result, benchJob.err, benchJob.canceled = cancel, nil, "", false
|
|
go func() {
|
|
defer cancel()
|
|
res, err := benchRunner(ctx, opts, func(phase string, step, steps int) {
|
|
benchJob.mu.Lock()
|
|
if benchJob.id == id {
|
|
benchJob.phase, benchJob.step, benchJob.steps = phase, step, steps
|
|
}
|
|
benchJob.mu.Unlock()
|
|
})
|
|
canceled := ctx.Err() != nil
|
|
// Le verrou du chat est rendu AVANT d'annoncer la fin : l'interface qui
|
|
// lit « terminé » et envoie aussitôt un message ne doit pas se voir
|
|
// refuser « benchmark en cours ».
|
|
release()
|
|
benchJob.mu.Lock()
|
|
defer benchJob.mu.Unlock()
|
|
if benchJob.id != id {
|
|
return
|
|
}
|
|
benchJob.running, benchJob.finished, benchJob.cancel = false, time.Now(), nil
|
|
benchJob.result = res
|
|
if err != nil {
|
|
benchJob.canceled = canceled
|
|
benchJob.err = err.Error()
|
|
if canceled {
|
|
benchJob.err = "benchmark annulé"
|
|
}
|
|
}
|
|
}()
|
|
return id, http.StatusAccepted, nil
|
|
}
|
|
|
|
// benchCancel annule le benchmark en cours. false : aucun ne tournait.
|
|
func benchCancel() bool {
|
|
benchJob.mu.Lock()
|
|
defer benchJob.mu.Unlock()
|
|
if !benchJob.running || benchJob.cancel == nil {
|
|
return false
|
|
}
|
|
benchJob.cancel()
|
|
return true
|
|
}
|
|
|
|
// benchStatus : l'état à renvoyer à l'interface.
|
|
func benchStatus() map[string]any {
|
|
benchJob.mu.Lock()
|
|
defer benchJob.mu.Unlock()
|
|
st := map[string]any{"ok": true, "running": benchJob.running, "id": benchJob.id, "mode": benchJob.mode}
|
|
if benchJob.id == "" {
|
|
return st
|
|
}
|
|
st["phase"], st["step"], st["steps"] = benchJob.phase, benchJob.step, benchJob.steps
|
|
end := benchJob.finished
|
|
if benchJob.running {
|
|
end = time.Now()
|
|
}
|
|
st["elapsed_sec"] = end.Sub(benchJob.started).Seconds()
|
|
if !benchJob.running {
|
|
if benchJob.result != nil {
|
|
st["result"] = benchJob.result
|
|
st["saved"] = benchSavable(benchJob.result)
|
|
}
|
|
if benchJob.err != "" {
|
|
st["error"] = benchJob.err
|
|
st["canceled"] = benchJob.canceled
|
|
}
|
|
}
|
|
return st
|
|
}
|
|
|
|
// handleBench lance un benchmark : POST seulement — il occupe le moteur des
|
|
// minutes durant, une balise <img> sur une page tierce ne doit pas y suffire.
|
|
// Corps : {"mode":"quick"|"full", "prompt":N, "n":N}, tous facultatifs.
|
|
func handleBench(w http.ResponseWriter, r *http.Request) {
|
|
if !postOnly(w, r) {
|
|
return
|
|
}
|
|
var body struct {
|
|
Mode string `json:"mode"`
|
|
Prompt int `json:"prompt"`
|
|
N int `json:"n"`
|
|
}
|
|
_ = json.NewDecoder(r.Body).Decode(&body)
|
|
opts := benchOpts{Mode: benchModeQuick, Prompt: 2000, Predict: 300}
|
|
if strings.EqualFold(strings.TrimSpace(body.Mode), benchModeFull) {
|
|
opts.Mode = benchModeFull
|
|
}
|
|
if body.Prompt > 0 {
|
|
opts.Prompt = min(body.Prompt, 32768)
|
|
}
|
|
if body.N > 0 {
|
|
opts.Predict = min(body.N, 4096)
|
|
}
|
|
id, code, err := benchStart(opts)
|
|
if err != nil {
|
|
sendJSON(w, code, map[string]any{"ok": false, "error": err.Error()})
|
|
return
|
|
}
|
|
sendJSON(w, code, map[string]any{"ok": true, "id": id})
|
|
}
|
|
|
|
// handleBenchStatus : progression du benchmark en cours, ou issue du dernier.
|
|
func handleBenchStatus(w http.ResponseWriter, r *http.Request) {
|
|
sendJSON(w, 200, benchStatus())
|
|
}
|
|
|
|
// handleBenchCancel annule le benchmark en cours.
|
|
func handleBenchCancel(w http.ResponseWriter, r *http.Request) {
|
|
if !postOnly(w, r) {
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": benchCancel()})
|
|
}
|
|
|
|
// handleBenchLast returns the most recent persisted benchmark, or {ok:false}
|
|
// when none has been run yet.
|
|
func handleBenchLast(w http.ResponseWriter, r *http.Request) {
|
|
sb := loadLastBench()
|
|
if sb == nil {
|
|
sendJSON(w, 200, map[string]any{"ok": false})
|
|
return
|
|
}
|
|
sendJSON(w, 200, map[string]any{"ok": true, "result": sb.Result, "model": sb.Model, "at": sb.At})
|
|
}
|