mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-16 19:59:56 +02:00
Le verrou de l'optimiseur ne doit jamais bloquer quand aucune optimisation ne tourne, et ne jamais passer pour périmé quand elle tourne. Plusieurs identités de processus ne tenaient pas ces deux promesses hors Windows. - Linux : l'instant de démarrage est relatif au boot. Après une coupure, un service relancé au même moment du démarrage retrouvait PID et top : le verrou de l'ancien passait pour vivant et tout était refusé jusqu'à son retrait à la main. L'identifiant du boot le préfixe ; un verrou qui porte notre PID sans être l'un des nôtres est périmé. - Linux : un binaire remplacé par une mise à jour (« (deleted) ») faisait passer le propriétaire vivant pour mort. La mise à jour est de toute façon refusée pendant une optimisation (interface et loki update). - macOS : ps rend lstart dans la langue et le fuseau de l'appelant ; le web (launchd) et un terminal en français ne lisaient pas la même identité. LC_ALL=C et TZ=UTC. - Un verrou illisible tout juste créé (vide entre création et écriture) n'est plus retiré : seulement après 10 s. - loki tune : terminal fermé ou kill (SIGTERM, SIGHUP) annulent proprement, comme Ctrl-C ; Ctrl-C retrouve son sens aux questions de fin. - Application : la sauvegarde est notée dans le verrou ; un Loki tué avant la vérification remet l'ancienne version au redémarrage. Deux clics rapprochés ne lancent plus deux applications. Un échec de relance du vrai moteur se voit dans le résultat, plus seulement au journal ; un essai qui survit à son arrêt reste noté dans le verrou. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1041 lines
35 KiB
Go
1041 lines
35 KiB
Go
package loki
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"net"
|
||
"net/http"
|
||
"os"
|
||
"os/exec"
|
||
"path/filepath"
|
||
"strconv"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
// Optimiseur sans perte — le déroulé (la partie pure est dans backend_tune.go).
|
||
//
|
||
// 1. Refus d'entrée : preset externe, aucun preset actif, génération, tâche,
|
||
// bench ou job en cours, moteur occupé par un client.
|
||
// 2. Verrou exclusif (backend_tune_lock.go), vrai moteur arrêté comme le fait
|
||
// « décharger la VRAM ».
|
||
// 3. Chaque essai : une COPIE de la configuration avec ses surcharges, posée
|
||
// dans LOKI_HOME/tune/run/<essai>/config.env — jamais config.env — puis
|
||
// « loki serve » à blanc (la ligne de commande, comparée à la référence
|
||
// avant tout chargement), puis pour de vrai sur 127.0.0.1 et un port libre.
|
||
// Mesure : le bench complet du lot 1 (llm_bench.go) à une profondeur fixe.
|
||
// Le groupe de processus de l'essai est tué en sortie, quoi qu'il arrive.
|
||
// 4. Fin : le vrai moteur est relancé (comme handleVramReload), le verrou
|
||
// rendu. Le résultat est montré ; RIEN n'est écrit dans un preset sans un
|
||
// geste de l'utilisateur (tuneApply).
|
||
|
||
// tuneOpts : ce que l'utilisateur a demandé.
|
||
type tuneOpts struct {
|
||
Placement bool // « inclure le placement » (réécrit EXTRA_ARGS pour --fit)
|
||
OptIn bool // « inclure les options opt-in » (SPEC, CUDA_GRAPH_OPT, --backend-sampling)
|
||
Budget time.Duration // 0 = tuneDefaultBudget
|
||
Via string // « web » ou « cli »
|
||
// Stages : étapes de base à faire (marges, lots, délestage, threads, files) ;
|
||
// vide = toutes. Placement et opt-in ont leur propre case.
|
||
Stages []string
|
||
}
|
||
|
||
const (
|
||
tuneDefaultBudget = 30 * time.Minute
|
||
tuneMaxBudget = 6 * time.Hour
|
||
)
|
||
|
||
// tuneLoadTimeout : chargement d'un essai. Un MoE de 80 Go relu depuis un
|
||
// disque lent prend plusieurs minutes ; au-delà, l'essai est déclaré en échec.
|
||
var tuneLoadTimeout = 20 * time.Minute
|
||
|
||
// tuneTrial : un essai, tel que montré à l'utilisateur.
|
||
type tuneTrial struct {
|
||
Stage string `json:"stage"`
|
||
Label string `json:"label"`
|
||
Status string `json:"status"` // en cours, mesuré, retenu, écarté, échec, doublon
|
||
Why string `json:"why,omitempty"`
|
||
Runs []tuneRun `json:"runs,omitempty"`
|
||
TurnSec float64 `json:"turn_sec,omitempty"` // moyenne des passages
|
||
Gain float64 `json:"gain"` // contre la meilleure configuration du moment
|
||
Headroom int `json:"headroom_mib"` // VRAM libre minimale après le bench ; -1 = non mesurée
|
||
Offloaded string `json:"offloaded,omitempty"`
|
||
Changes []string `json:"changes,omitempty"` // contre la référence
|
||
Sec float64 `json:"sec"`
|
||
}
|
||
|
||
// tuneResult : l'issue d'une optimisation.
|
||
type tuneResult struct {
|
||
PresetID string `json:"preset_id"`
|
||
PresetName string `json:"preset_name"`
|
||
BaseFP string `json:"base_fp"` // configFingerprint de la configuration mesurée
|
||
PresetFP string `json:"preset_fp"` // presetFingerprint du preset au départ
|
||
Engine string `json:"engine"` // build_info du moteur
|
||
EngineBuild int `json:"engine_build"`
|
||
Weights tuneWeights `json:"weights"`
|
||
Depth int `json:"depth"`
|
||
NCtx int `json:"n_ctx"`
|
||
Slots int `json:"slots"`
|
||
Baseline tuneTrial `json:"baseline"`
|
||
Trials []tuneTrial `json:"trials"`
|
||
Best string `json:"best,omitempty"` // libellé de l'essai retenu ; vide = la référence reste la meilleure
|
||
Gain float64 `json:"gain"` // contre la référence
|
||
Set map[string]string `json:"set,omitempty"` // clés changées et leur nouvelle valeur ("" = retirée)
|
||
Old map[string]string `json:"old,omitempty"` // leur valeur mesurée en référence
|
||
Changes []string `json:"changes,omitempty"`
|
||
Placement bool `json:"placement"` // EXTRA_ARGS réécrit : confirmation à part
|
||
Partial string `json:"partial,omitempty"`
|
||
Skipped []string `json:"skipped,omitempty"`
|
||
Notes []string `json:"notes,omitempty"`
|
||
OptIn bool `json:"opt_in"`
|
||
At int64 `json:"at"`
|
||
}
|
||
|
||
// tuneTracker : la progression, lue par l'interface ou affichée par la CLI.
|
||
type tuneTracker struct {
|
||
mu sync.Mutex
|
||
phase string
|
||
started time.Time
|
||
ended time.Time // fin de la mesure ; zéro = en cours
|
||
trials []tuneTrial
|
||
avgSec float64 // durée moyenne d'un essai mesuré
|
||
left int // essais encore prévus (estimation)
|
||
budget time.Duration
|
||
print bool
|
||
}
|
||
|
||
func (t *tuneTracker) setPhase(format string, a ...any) {
|
||
t.mu.Lock()
|
||
t.phase = fmt.Sprintf(format, a...)
|
||
p := t.print
|
||
t.mu.Unlock()
|
||
if p {
|
||
fmt.Printf(" %s %s\n", dim(time.Now().Format("15:04:05")), fmt.Sprintf(format, a...))
|
||
}
|
||
}
|
||
|
||
func (t *tuneTracker) put(i int, tr tuneTrial) int {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
if i < 0 || i >= len(t.trials) {
|
||
t.trials = append(t.trials, tr)
|
||
return len(t.trials) - 1
|
||
}
|
||
t.trials[i] = tr
|
||
return i
|
||
}
|
||
|
||
func (t *tuneTracker) snapshot() map[string]any {
|
||
t.mu.Lock()
|
||
defer t.mu.Unlock()
|
||
end := t.ended
|
||
if end.IsZero() {
|
||
end = time.Now()
|
||
}
|
||
el := end.Sub(t.started)
|
||
eta := -1.0
|
||
if t.avgSec > 0 {
|
||
eta = min(t.avgSec*float64(t.left), (t.budget - el).Seconds())
|
||
eta = max(eta, 0)
|
||
}
|
||
return map[string]any{"phase": t.phase, "elapsed_sec": el.Seconds(), "eta_sec": eta,
|
||
"trials": append([]tuneTrial(nil), t.trials...)}
|
||
}
|
||
|
||
// tuneActivePreset : le preset dont l'empreinte est celle de la configuration
|
||
// active — c'est lui que l'optimisation mesure, et lui seul qu'elle peut
|
||
// réécrire. Aucun : la mesure n'aurait nulle part où aller.
|
||
func tuneActivePreset() (Preset, string, error) {
|
||
list, err := ListPresets()
|
||
if err != nil {
|
||
return Preset{}, "", err
|
||
}
|
||
for _, p := range list {
|
||
if p.Active {
|
||
content, err := ReadPreset(p.ID)
|
||
return p, content, err
|
||
}
|
||
}
|
||
return Preset{}, "", errors.New("aucun preset ne correspond à la configuration active : enregistre-la comme preset " +
|
||
"(ou bascule sur l'un d'eux) avant d'optimiser")
|
||
}
|
||
|
||
// tuneAuth : l'en-tête du moteur protégé par clé (essai comme vrai moteur).
|
||
func tuneAuth() func(set func(k, v string)) {
|
||
key, _ := effectiveAPIKeyErr()
|
||
return func(set func(k, v string)) {
|
||
if key != "" {
|
||
set("Authorization", "Bearer "+key)
|
||
}
|
||
}
|
||
}
|
||
|
||
// tunePreflight : tout ce qui interdit de commencer, dans CE processus. Le
|
||
// processus web voit la discussion, les tâches et les jobs ; la CLI ne voit que
|
||
// le moteur (/slots).
|
||
func tunePreflight(ctx context.Context, inWeb bool) error {
|
||
if externalActive() {
|
||
return errors.New("optimisation indisponible : le preset actif est une API externe")
|
||
}
|
||
if err := tuneGuard(); err != nil {
|
||
return err
|
||
}
|
||
if _, _, err := tuneActivePreset(); err != nil {
|
||
return err
|
||
}
|
||
if inWeb {
|
||
switch {
|
||
case benchRunning():
|
||
return errBenchBusy
|
||
case conv.isGenerating():
|
||
return conv.busyReason()
|
||
case bgJobsRunning() > 0:
|
||
return fmt.Errorf("%d job(s) d'arrière-plan du mode code tournent encore (bash_bg) : ils fausseraient les mesures — arrête-les d'abord",
|
||
bgJobsRunning())
|
||
}
|
||
}
|
||
if serviceIsActive() {
|
||
e := benchEngine{base: fmt.Sprintf("http://localhost:%d", LLMPort()), auth: tuneAuth(), client: http.DefaultClient}
|
||
if err := e.idle(ctx); err != nil {
|
||
return errors.New("le moteur traite une requête (discussion, tâche ou client /v1) : réessaie une fois libre")
|
||
}
|
||
}
|
||
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 {
|
||
if probe != nil && probe.CPUThreads > 0 {
|
||
return probe.CPUThreads
|
||
}
|
||
fsys := os.DirFS("/")
|
||
online, err := readCPUList(fsys, "sys/devices/system/cpu/online")
|
||
if err != nil || len(online) == 0 {
|
||
return 0
|
||
}
|
||
atom, _ := readCPUList(fsys, "sys/devices/cpu_atom/cpus")
|
||
n, err := physicalCores(fsys, online, atom)
|
||
if err != nil {
|
||
return 0
|
||
}
|
||
return n
|
||
}
|
||
|
||
func tuneFreePort() (int, error) {
|
||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||
if err != nil {
|
||
return 0, err
|
||
}
|
||
defer l.Close()
|
||
return l.Addr().(*net.TCPAddr).Port, nil
|
||
}
|
||
|
||
// tuneTrialer lance les essais d'une optimisation.
|
||
type tuneTrialer struct {
|
||
dir string
|
||
self string
|
||
lock *tuneLock
|
||
seq int
|
||
optIn bool
|
||
}
|
||
|
||
// prepare pose la configuration d'un essai (copie + adresse privée) dans un
|
||
// dossier neuf.
|
||
func (t *tuneTrialer) prepare(cfg map[string]string) (dir string, port int, err error) {
|
||
t.seq++
|
||
dir = filepath.Join(t.dir, fmt.Sprintf("e%02d", t.seq))
|
||
if err := os.MkdirAll(dir, 0o700); err != nil {
|
||
return "", 0, err
|
||
}
|
||
if port, err = tuneFreePort(); err != nil {
|
||
return "", 0, err
|
||
}
|
||
c := copyCfg(cfg)
|
||
c["HOST"], c["PORT"] = "127.0.0.1", strconv.Itoa(port)
|
||
return dir, port, writeTuneTrialConfig(dir, c)
|
||
}
|
||
|
||
// dry : « loki serve » à blanc — la ligne de commande, rien de chargé.
|
||
func (t *tuneTrialer) dry(ctx context.Context, dir string) (tuneLaunch, error) {
|
||
ctx, cancel := context.WithTimeout(ctx, 2*time.Minute)
|
||
defer cancel()
|
||
cmd := hideCmd(exec.CommandContext(ctx, t.self, "serve"))
|
||
cmd.Dir = LokiHome()
|
||
cmd.Env = append(os.Environ(), tuneTrialEnv+"="+dir, tuneDryRunEnv+"=1")
|
||
out, err := cmd.CombinedOutput()
|
||
if err != nil {
|
||
msg := strings.TrimSpace(ansiRe.ReplaceAllString(string(out), ""))
|
||
if i := strings.LastIndex(msg, "\n"); i >= 0 {
|
||
msg = msg[i+1:]
|
||
}
|
||
if msg == "" {
|
||
msg = err.Error()
|
||
}
|
||
return tuneLaunch{}, errors.New(msg)
|
||
}
|
||
return readTuneLaunch(dir)
|
||
}
|
||
|
||
// tuneLoaded : un essai chargé et prêt.
|
||
type tuneLoaded struct {
|
||
eng benchEngine
|
||
nCtx int
|
||
slots int
|
||
build string
|
||
offloaded int
|
||
layers int
|
||
kv string
|
||
log string
|
||
}
|
||
|
||
// tuneProps lit n_ctx, total_slots et build_info du moteur.
|
||
func tuneProps(ctx context.Context, e benchEngine) (nCtx, slots int, build string, err error) {
|
||
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||
defer cancel()
|
||
data, err := e.do(ctx, http.MethodGet, "/props", nil)
|
||
if err != nil {
|
||
return 0, 0, "", err
|
||
}
|
||
var p struct {
|
||
BuildInfo string `json:"build_info"`
|
||
TotalSlots int `json:"total_slots"`
|
||
NCtx int `json:"n_ctx"`
|
||
Default struct {
|
||
NCtx int `json:"n_ctx"`
|
||
} `json:"default_generation_settings"`
|
||
}
|
||
if err := json.Unmarshal(data, &p); err != nil {
|
||
return 0, 0, "", err
|
||
}
|
||
nCtx = p.Default.NCtx
|
||
if nCtx <= 0 {
|
||
nCtx = p.NCtx
|
||
}
|
||
return nCtx, p.TotalSlots, p.BuildInfo, nil
|
||
}
|
||
|
||
// load lance l'essai pour de vrai et attend qu'il réponde. La fonction rendue
|
||
// l'arrête (groupe entier), efface sa trace du verrou et attend que la VRAM
|
||
// soit rendue : à appeler en defer, quoi qu'il arrive.
|
||
func (t *tuneTrialer) load(ctx context.Context, dir string, port int, launch tuneLaunch) (*tuneLoaded, func(), error) {
|
||
logPath := filepath.Join(dir, "engine.log")
|
||
logf, err := os.Create(logPath)
|
||
if err != nil {
|
||
return nil, func() {}, err
|
||
}
|
||
cmd := exec.Command(t.self, "serve")
|
||
cmd.Dir = LokiHome()
|
||
cmd.Env = append(os.Environ(), tuneTrialEnv+"="+dir)
|
||
cmd.Stdout, cmd.Stderr = logf, logf
|
||
tuneTrialAttr(cmd)
|
||
if err := cmd.Start(); err != nil {
|
||
logf.Close()
|
||
return nil, func() {}, err
|
||
}
|
||
pid := cmd.Process.Pid
|
||
done := make(chan struct{})
|
||
go func() {
|
||
_ = cmd.Wait()
|
||
logf.Close()
|
||
close(done)
|
||
}()
|
||
proc := tuneProc{PID: pid, Exes: []string{t.self, launch.Bin}}
|
||
if start, _, ok := procIdentity(pid); ok {
|
||
proc.Start = start
|
||
}
|
||
t.lock.setTrial(&proc)
|
||
stop := func() {
|
||
before := gpuUsedMB(gpuStats())
|
||
tuneKillTree(pid, done)
|
||
// Noté jusqu'à sa fin constatée : un essai qui survit à l'arrêt reste
|
||
// désigné dans le verrou, qu'un Loki redémarré pourra encore arrêter.
|
||
select {
|
||
case <-done:
|
||
t.lock.setTrial(nil)
|
||
default:
|
||
fmt.Fprintf(os.Stderr, "[loki tune] l'essai (PID %d) ne s'est pas arrêté dans les temps\n", pid)
|
||
}
|
||
if before > 0 {
|
||
gpuUsedSettled(before)
|
||
} else {
|
||
time.Sleep(2 * time.Second)
|
||
}
|
||
}
|
||
readLog := func() string {
|
||
b, _ := os.ReadFile(logPath)
|
||
return string(b)
|
||
}
|
||
e := benchEngine{base: fmt.Sprintf("http://127.0.0.1:%d", port), auth: tuneAuth(), client: http.DefaultClient}
|
||
deadline := time.Now().Add(tuneLoadTimeout)
|
||
hc := &http.Client{Timeout: 3 * time.Second}
|
||
for {
|
||
select {
|
||
case <-ctx.Done():
|
||
return nil, stop, ctx.Err()
|
||
case <-done:
|
||
return nil, stop, errors.New(tuneLoadFailure(readLog()))
|
||
case <-time.After(time.Second):
|
||
}
|
||
if resp, err := hc.Get(e.base + "/health"); err == nil {
|
||
resp.Body.Close()
|
||
if resp.StatusCode == http.StatusOK {
|
||
break
|
||
}
|
||
}
|
||
if time.Now().After(deadline) {
|
||
return nil, stop, fmt.Errorf("pas prêt après %s", tuneLoadTimeout)
|
||
}
|
||
}
|
||
nCtx, slots, build, err := tuneProps(ctx, e)
|
||
if err != nil {
|
||
return nil, stop, fmt.Errorf("/props illisible : %w", err)
|
||
}
|
||
log := readLog()
|
||
off, layers, kv := tuneLogFacts(log)
|
||
return &tuneLoaded{eng: e, nCtx: nCtx, slots: slots, build: build, offloaded: off, layers: layers, kv: kv, log: log}, stop, nil
|
||
}
|
||
|
||
// tuneBenchSetup : le bench du preset de l'essai — son échantillonnage et son
|
||
// raisonnement réels (une spéculation se juge sur le tirage du preset, pas en
|
||
// glouton), profondeur plafonnée comme celle de la référence.
|
||
func tuneBenchSetup(cfg map[string]string, argEnv map[string]string, cpuPlaced bool) benchSetup {
|
||
s := benchSetupFrom(cfg, argEnv)
|
||
s.learn = func(want, got string) { effortRemember(want, got) }
|
||
s.cpuPlaced = cpuPlaced
|
||
return s
|
||
}
|
||
|
||
// runTune déroule une optimisation. L'appelant fournit le suivi ; le verrou,
|
||
// l'arrêt et la relance du vrai moteur sont faits ici.
|
||
func runTune(ctx context.Context, opts tuneOpts, tr *tuneTracker) (out *tuneResult, retErr error) {
|
||
if opts.Budget <= 0 {
|
||
opts.Budget = tuneDefaultBudget
|
||
}
|
||
opts.Budget = min(opts.Budget, tuneMaxBudget)
|
||
tr.mu.Lock()
|
||
tr.started, tr.budget = time.Now(), opts.Budget
|
||
tr.mu.Unlock()
|
||
cfg := ReadConfig()
|
||
preset, content, err := tuneActivePreset()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
self, err := os.Executable()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
mainActive := engineNeedsStop()
|
||
lock, err := tuneLockAcquire(opts.Via, mainActive)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
defer lock.release()
|
||
// Le vrai moteur revient AVANT que le verrou ne soit rendu (les defer
|
||
// passent dans l'ordre inverse) : personne ne le relance par-dessus un essai.
|
||
defer func() {
|
||
if !mainActive {
|
||
return
|
||
}
|
||
lock.setPhase("relance")
|
||
tr.setPhase("relance du moteur principal")
|
||
// Un échec de relance doit se voir là où l'on regarde (résultat de
|
||
// l'interface, sortie de « loki tune »), pas seulement au journal.
|
||
failed := func(msg string) {
|
||
fmt.Fprintf(os.Stderr, "[loki tune] %s\n", msg)
|
||
switch {
|
||
case out != nil:
|
||
out.Notes = append(out.Notes, "⚠️ "+msg)
|
||
case retErr != nil:
|
||
retErr = fmt.Errorf("%w — %s", retErr, msg)
|
||
}
|
||
}
|
||
if err := preflightEngine(); err != nil {
|
||
failed("relance du moteur principal impossible : " + plainErr(err))
|
||
return
|
||
}
|
||
if err := serviceActionOS("start"); err != nil {
|
||
failed("relance du moteur principal : " + plainErr(err))
|
||
}
|
||
gpuMonitor.invalidate()
|
||
}()
|
||
|
||
runDir := filepath.Join(LokiHome(), "tune", "run")
|
||
_ = os.RemoveAll(runDir)
|
||
if err := os.MkdirAll(runDir, 0o700); err != nil {
|
||
return nil, err
|
||
}
|
||
t := &tuneTrialer{dir: runDir, self: self, lock: lock, optIn: opts.OptIn}
|
||
|
||
if mainActive {
|
||
tr.setPhase("arrêt du moteur principal")
|
||
gpus := gpuStats()
|
||
before := gpuUsedMB(gpus)
|
||
if err := serviceActionOS("stop"); err != nil {
|
||
return nil, fmt.Errorf("arrêt du moteur : %s", plainErr(err))
|
||
}
|
||
asrShutdownVite()
|
||
if gpus != nil {
|
||
gpuUsedSettled(before)
|
||
}
|
||
gpuMonitor.invalidate()
|
||
}
|
||
|
||
res := &tuneResult{PresetID: preset.ID, PresetName: preset.Name, BaseFP: configFingerprint(cfg),
|
||
PresetFP: presetFingerprint([]byte(content)), OptIn: opts.OptIn, At: time.Now().Unix(),
|
||
Weights: tuneWeightsFrom(perfLog.snapshot())}
|
||
|
||
// Référence : la ligne composée telle quelle, la sonde de la machine.
|
||
tr.setPhase("référence : composition de la ligne de commande")
|
||
baseDir, basePort, err := t.prepare(cfg)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
baseLaunch, err := t.dry(ctx, baseDir)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("la configuration active ne se lance pas : %s", err)
|
||
}
|
||
probe := baseLaunch.Probe
|
||
if probe == nil {
|
||
probe = &tuneProbe{}
|
||
}
|
||
smi := gpuStats()
|
||
env := tuneEnv{Help: probe.Help, GPUs: probe.GPUs, VRAMMiB: probe.VRAMMiB, ModelBytes: probe.ModelBytes,
|
||
MoE: probe.MoE, PhysCores: tunePhysCores(probe), SMI: smi != nil, ArgEnv: probe.ArgEnv,
|
||
UserEnv: probe.UserEnv, QueuesEnv: probe.QueuesEnv, Placement: opts.Placement, OptIn: opts.OptIn}
|
||
visible := tuneVisibleGPUs(cfg)
|
||
if env.VRAMMiB == 0 && smi != nil {
|
||
for i, g := range smi {
|
||
if len(visible) == 0 || containsInt(visible, i) {
|
||
env.VRAMMiB += int64(g.Total)
|
||
}
|
||
}
|
||
}
|
||
if env.GPUs == 0 && smi != nil {
|
||
env.GPUs = len(smi)
|
||
if len(visible) > 0 {
|
||
env.GPUs = len(visible)
|
||
}
|
||
}
|
||
extra := splitArgs(cfg["EXTRA_ARGS"])
|
||
kt, vt, _ := effectiveKVTypes(cfg, extra, env.ArgEnv)
|
||
if label, quant := benchKVLabel(kt, vt); quant {
|
||
res.Notes = append(res.Notes, "cache KV "+label+" quantifié : zone grise, conservé tel quel (jamais modifié par l'optimiseur)")
|
||
}
|
||
if !env.SMI {
|
||
res.Notes = append(res.Notes, "VRAM libre non mesurable (nvidia-smi absent) : ni micro-lot plus grand ni marges --fit essayés")
|
||
}
|
||
if _, total := ramUsageMB(); total > 0 && env.ModelBytes > int64(total)<<20 {
|
||
res.Notes = append(res.Notes, "modèle plus gros que la RAM : chaque essai le relit depuis le disque — prévoir un budget "+
|
||
"plus long, ou ne garder que quelques étapes")
|
||
}
|
||
reserve := tuneTightMiB
|
||
if strings.TrimSpace(cfg["MMPROJ"]) != "" {
|
||
reserve += tuneVisionReserveMiB
|
||
}
|
||
cpuPlaced := tensorOverride(extra, env.ArgEnv) != ""
|
||
|
||
// La référence se mesure deux fois : son écart entre passages est le bruit
|
||
// que tout gain doit dépasser.
|
||
tr.setPhase("référence : chargement")
|
||
bi := tr.put(-1, tuneTrial{Stage: "référence", Label: "configuration actuelle", Status: "en cours", Headroom: -1})
|
||
t0 := time.Now()
|
||
base, stop, err := t.load(ctx, baseDir, basePort, baseLaunch)
|
||
if err != nil {
|
||
stop()
|
||
return nil, fmt.Errorf("la référence ne charge pas : %s", err)
|
||
}
|
||
res.Engine, res.NCtx, res.Slots = base.build, base.nCtx, base.slots
|
||
if m := reBuildInfo.FindStringSubmatch(base.build); m != nil {
|
||
res.EngineBuild, _ = strconv.Atoi(m[1])
|
||
}
|
||
// Une profondeur pour tous : celle que la référence peut tenir avec le plus
|
||
// gros préchauffage essayé (2 × 4096 jetons, au plus un quart du contexte).
|
||
warmMax := max(min(2*4096, base.nCtx/4), 64) + benchWarmGen
|
||
res.Depth, _ = benchDepthFor(base.nCtx, cpuPlaced, warmMax)
|
||
bopts := benchOpts{Mode: benchModeFull, Prompt: 2000, Predict: 300, Depth: res.Depth}
|
||
corp := defaultBenchCorpora()
|
||
measure := func(cfg map[string]string, l *tuneLoaded, n int, phase string) ([]tuneRun, error) {
|
||
var runs []tuneRun
|
||
for i := 0; i < n; i++ {
|
||
tr.setPhase("%s : mesure %d/%d", phase, i+1, n)
|
||
// SIDE_SLOT : deux slots ; les tours en profondeur restent sur le même
|
||
// (le second, comme le bench ordinaire), sans quoi la reprise du cache
|
||
// dépendrait du slot choisi par le moteur.
|
||
s := tuneBenchSetup(cfg, env.ArgEnv, cpuPlaced)
|
||
s.sideSlot = sideSlotOn(cfg) && l.slots >= 2
|
||
r, err := benchRun(ctx, l.eng, bopts, s, corp, nil)
|
||
if err != nil {
|
||
return runs, err
|
||
}
|
||
run, err := tuneRunFrom(r, res.Weights)
|
||
if err != nil {
|
||
return runs, err
|
||
}
|
||
runs = append(runs, run)
|
||
}
|
||
return runs, nil
|
||
}
|
||
headroom := func() (int, bool) {
|
||
if !env.SMI {
|
||
return -1, false
|
||
}
|
||
free, ok := tuneHeadroom(gpuStats(), visible)
|
||
if !ok {
|
||
return -1, false
|
||
}
|
||
return free, free < reserve
|
||
}
|
||
baseRuns, err := measure(cfg, base, 2, "référence")
|
||
hr, _ := headroom()
|
||
stop()
|
||
if err != nil {
|
||
return nil, fmt.Errorf("mesure de la référence : %s", err)
|
||
}
|
||
bt := tuneTrial{Stage: "référence", Label: "configuration actuelle", Status: "mesuré", Runs: baseRuns,
|
||
TurnSec: turnMean(baseRuns), Headroom: hr, Sec: time.Since(t0).Seconds()}
|
||
if base.offloaded >= 0 {
|
||
bt.Offloaded = fmt.Sprintf("%d/%d", base.offloaded, base.layers)
|
||
}
|
||
res.Baseline = bt
|
||
tr.put(bi, bt)
|
||
if hr >= 0 && hr < reserve {
|
||
res.Notes = append(res.Notes, fmt.Sprintf("la configuration actuelle laisse déjà moins de %d Mio libres sur une carte", reserve))
|
||
}
|
||
|
||
best, bestRuns, bestLabel := cfg, baseRuns, ""
|
||
durations := []float64{bt.Sec}
|
||
measured := map[string]string{tuneLaunchKey(baseLaunch): bt.Label}
|
||
deadline := tr.started.Add(opts.Budget)
|
||
|
||
stages:
|
||
for si, st := range tuneStages {
|
||
if !tuneStageWanted(st.Name, opts.Stages) {
|
||
res.Skipped = append(res.Skipped, st.Name+" : non demandé")
|
||
continue
|
||
}
|
||
cands, why := st.Gen(best, env)
|
||
if why != "" {
|
||
res.Skipped = append(res.Skipped, st.Name+" : "+why)
|
||
continue
|
||
}
|
||
for ci, c := range cands {
|
||
// Ce qui reste à faire, pour l'estimation : les essais restants de
|
||
// l'étape, plus ceux des étapes suivantes vus depuis la meilleure
|
||
// configuration du moment.
|
||
left := len(cands) - ci
|
||
for _, nx := range tuneStages[si+1:] {
|
||
if !tuneStageWanted(nx.Name, opts.Stages) {
|
||
continue
|
||
}
|
||
more, _ := nx.Gen(best, env)
|
||
left += len(more)
|
||
}
|
||
avg := tuneMean(durations)
|
||
tr.mu.Lock()
|
||
tr.avgSec, tr.left = avg, left
|
||
tr.mu.Unlock()
|
||
if ctx.Err() != nil {
|
||
res.Partial = "annulé"
|
||
break stages
|
||
}
|
||
if time.Until(deadline).Seconds() < avg {
|
||
res.Partial = fmt.Sprintf("budget de %s atteint : %d essai(s) non faits", opts.Budget.Round(time.Minute), left)
|
||
break stages
|
||
}
|
||
trial := tuneTrial{Stage: c.Stage, Label: c.Label, Status: "en cours", Headroom: -1,
|
||
Changes: tuneDiff(cfg, c.Cfg)}
|
||
idx := tr.put(-1, trial)
|
||
finish := func(status, why string) {
|
||
trial.Status, trial.Why = status, why
|
||
tr.put(idx, trial)
|
||
res.Trials = append(res.Trials, trial)
|
||
}
|
||
if err := tuneCfgCheck(cfg, c.Cfg, opts.OptIn); err != nil {
|
||
finish("écarté", "dénature : "+err.Error())
|
||
continue
|
||
}
|
||
tr.setPhase("%s — %s : composition", st.Name, c.Label)
|
||
dir, port, err := t.prepare(c.Cfg)
|
||
if err != nil {
|
||
finish("échec", err.Error())
|
||
continue
|
||
}
|
||
launch, err := t.dry(ctx, dir)
|
||
if err != nil {
|
||
if ctx.Err() != nil {
|
||
finish("échec", "annulé")
|
||
res.Partial = "annulé"
|
||
break stages
|
||
}
|
||
finish("écarté", "refusé au lancement : "+err.Error())
|
||
continue
|
||
}
|
||
if why := tuneDenature(baseLaunch, launch, opts.OptIn); why != "" {
|
||
finish("écarté", "dénature : "+why)
|
||
continue
|
||
}
|
||
key := tuneLaunchKey(launch)
|
||
if prev, ok := measured[key]; ok {
|
||
finish("doublon", "même ligne de commande que « "+prev+" »")
|
||
continue
|
||
}
|
||
measured[key] = c.Label
|
||
t1 := time.Now()
|
||
tr.setPhase("%s — %s : chargement", st.Name, c.Label)
|
||
l, stop, err := t.load(ctx, dir, port, launch)
|
||
if err != nil {
|
||
stop()
|
||
if ctx.Err() != nil {
|
||
finish("échec", "annulé")
|
||
res.Partial = "annulé"
|
||
break stages
|
||
}
|
||
finish("échec", err.Error())
|
||
durations = append(durations, time.Since(t1).Seconds())
|
||
continue
|
||
}
|
||
trial.Sec = time.Since(t1).Seconds()
|
||
if l.offloaded >= 0 {
|
||
trial.Offloaded = fmt.Sprintf("%d/%d", l.offloaded, l.layers)
|
||
}
|
||
// Contre-vérification à l'exécution : même contexte, mêmes slots,
|
||
// mêmes types de cache que la référence.
|
||
switch {
|
||
case l.nCtx != base.nCtx || l.slots != base.slots:
|
||
stop()
|
||
finish("écarté", fmt.Sprintf("dénature : contexte %d × %d slot(s) au lieu de %d × %d", l.nCtx, l.slots, base.nCtx, base.slots))
|
||
continue
|
||
case base.kv != "" && l.kv != "" && l.kv != base.kv:
|
||
stop()
|
||
finish("écarté", "dénature : cache KV "+l.kv+" au lieu de "+base.kv)
|
||
continue
|
||
}
|
||
fitRegressed := base.offloaded >= 0 && l.offloaded >= 0 && l.offloaded < base.offloaded
|
||
runs, err := measure(c.Cfg, l, 1, st.Name+" — "+c.Label)
|
||
if err == nil && len(runs) == 1 && tuneWorthRepeat(turnSecs(bestRuns), runs[0].TurnSec) && !fitRegressed {
|
||
var more []tuneRun
|
||
more, err = measure(c.Cfg, l, 1, st.Name+" — "+c.Label+" (confirmation)")
|
||
runs = append(runs, more...)
|
||
}
|
||
hr, tight := headroom()
|
||
stop()
|
||
trial.Sec = time.Since(t1).Seconds()
|
||
durations = append(durations, trial.Sec)
|
||
trial.Runs, trial.Headroom = runs, hr
|
||
if err != nil {
|
||
if ctx.Err() != nil {
|
||
finish("échec", "annulé")
|
||
res.Partial = "annulé"
|
||
break stages
|
||
}
|
||
finish("échec", "mesure : "+err.Error())
|
||
continue
|
||
}
|
||
trial.TurnSec = turnMean(runs)
|
||
gain, need, wins := tuneWins(turnSecs(bestRuns), turnSecs(runs))
|
||
trial.Gain = gain
|
||
proseBest, proseCand := proseMean(bestRuns), proseMean(runs)
|
||
switch {
|
||
case fitRegressed:
|
||
finish("écarté", fmt.Sprintf("fit en recul : %d couches déportées au lieu de %d (performance)", l.offloaded, base.offloaded))
|
||
case tight:
|
||
finish("écarté", fmt.Sprintf("trop juste : %d Mio libres sur une carte (< %d)", hr, reserve))
|
||
case proseBest > 0 && proseCand < 0.95*proseBest:
|
||
finish("mesuré", fmt.Sprintf("decode en prose plus lent de %.0f %%", (1-proseCand/proseBest)*100))
|
||
case wins:
|
||
best, bestRuns, bestLabel = c.Cfg, runs, c.Label
|
||
if c.Stage == "placement" {
|
||
res.Placement = true
|
||
}
|
||
finish("retenu", fmt.Sprintf("−%.1f %% par tour (seuil %.1f %%)", gain*100, need*100))
|
||
case len(runs) < 2 && gain > 0:
|
||
finish("mesuré", fmt.Sprintf("−%.1f %%, sous le seuil de %.0f %%", gain*100, tuneMinGain*100))
|
||
default:
|
||
delta := fmt.Sprintf("%+.1f %%", -gain*100)
|
||
if gain*1000 > -0.5 && gain*1000 < 0.5 {
|
||
delta = "≈ 0 %"
|
||
}
|
||
finish("mesuré", delta+fmt.Sprintf(" (seuil %.1f %%)", need*100))
|
||
}
|
||
}
|
||
}
|
||
tr.mu.Lock()
|
||
tr.left = 0
|
||
tr.mu.Unlock()
|
||
|
||
res.Best = bestLabel
|
||
if bestLabel != "" {
|
||
res.Gain = (turnMean(baseRuns) - turnMean(bestRuns)) / turnMean(baseRuns)
|
||
res.Changes = tuneDiff(cfg, best)
|
||
res.Set, res.Old = map[string]string{}, map[string]string{}
|
||
for k := range keysOf(cfg, best) {
|
||
if cfg[k] != best[k] {
|
||
res.Set[k], res.Old[k] = best[k], cfg[k]
|
||
}
|
||
}
|
||
}
|
||
if ctx.Err() != nil && res.Partial == "" {
|
||
res.Partial = "annulé"
|
||
}
|
||
saveTuneResult(res)
|
||
return res, nil
|
||
}
|
||
|
||
func turnSecs(r []tuneRun) []float64 {
|
||
out := make([]float64, len(r))
|
||
for i, x := range r {
|
||
out[i] = x.TurnSec
|
||
}
|
||
return out
|
||
}
|
||
|
||
func turnMean(r []tuneRun) float64 { return tuneMean(turnSecs(r)) }
|
||
|
||
func proseMean(r []tuneRun) float64 {
|
||
v := make([]float64, len(r))
|
||
for i, x := range r {
|
||
v[i] = x.ProseTG
|
||
}
|
||
return tuneMean(v)
|
||
}
|
||
|
||
func keysOf(ms ...map[string]string) map[string]bool {
|
||
out := map[string]bool{}
|
||
for _, m := range ms {
|
||
for k := range m {
|
||
out[k] = true
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// --- résultats enregistrés -------------------------------------------------------
|
||
|
||
// saveTuneResult garde le dernier résultat par preset, avec l'empreinte et le
|
||
// build du moteur : après une mise à jour du moteur, l'interface propose de
|
||
// relancer l'optimisation.
|
||
func saveTuneResult(res *tuneResult) {
|
||
m := map[string]tuneResult{}
|
||
getJSON(bkState, "tune_results", &m)
|
||
if m == nil {
|
||
m = map[string]tuneResult{}
|
||
}
|
||
m[res.PresetID] = *res
|
||
_ = putJSON(bkState, "tune_results", m)
|
||
}
|
||
|
||
func loadTuneResult(id string) (*tuneResult, bool) {
|
||
m := map[string]tuneResult{}
|
||
if !getJSON(bkState, "tune_results", &m) {
|
||
return nil, false
|
||
}
|
||
r, ok := m[id]
|
||
if !ok {
|
||
return nil, false
|
||
}
|
||
return &r, true
|
||
}
|
||
|
||
// --- application ------------------------------------------------------------------
|
||
|
||
// tuneApplyTarget : où écrire le résultat.
|
||
const (
|
||
tuneApplyCopy = "copy" // une copie du preset, non activée
|
||
tuneApplyPreset = "preset" // le preset mesuré lui-même, réappliqué et vérifié
|
||
)
|
||
|
||
// tunePlanApply prépare la réécriture sans rien écrire : le contenu du preset
|
||
// réglé et le diff des clés. Refusé si le preset a changé depuis la mesure.
|
||
func tunePlanApply(res *tuneResult) (content, patched string, err error) {
|
||
if res == nil || len(res.Set) == 0 {
|
||
return "", "", errors.New("rien à appliquer : la configuration actuelle reste la meilleure")
|
||
}
|
||
content, err = ReadPreset(res.PresetID)
|
||
if err != nil {
|
||
return "", "", fmt.Errorf("preset « %s » introuvable : %w", res.PresetName, err)
|
||
}
|
||
if presetFingerprint([]byte(content)) != res.PresetFP {
|
||
return "", "", errors.New("le preset a changé depuis la mesure : relance l'optimisation")
|
||
}
|
||
base := parseEnv(content)
|
||
win := copyCfg(base)
|
||
for k, v := range res.Set {
|
||
if v == "" {
|
||
delete(win, k)
|
||
} else {
|
||
win[k] = v
|
||
}
|
||
}
|
||
if err := tuneCfgCheck(base, win, res.OptIn); err != nil {
|
||
return "", "", err
|
||
}
|
||
patched, err = tunePatchPreset(content, base, win)
|
||
return content, patched, err
|
||
}
|
||
|
||
// tuneApply écrit le résultat : dans une COPIE du preset (rien d'autre ne
|
||
// bouge), ou dans le preset lui-même — il doit être encore actif. Dans ce cas
|
||
// le moteur redémarre sur la nouvelle version, qui doit répondre, garder son
|
||
// contexte et ses slots, et passer une sonde (un prompt à la profondeur mesurée
|
||
// puis un decode) ; sinon l'ancienne version est rétablie. L'appelant a la
|
||
// confirmation de l'utilisateur.
|
||
func tuneApply(ctx context.Context, res *tuneResult, target string, say func(string)) (string, error) {
|
||
content, patched, err := tunePlanApply(res)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
switch target {
|
||
case tuneApplyCopy:
|
||
name := res.PresetName + " (optimisé)"
|
||
id, err := SavePreset("", name, patched)
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
return fmt.Sprintf("copie « %s » créée (%s) — bascule dessus pour l'utiliser", name, id), nil
|
||
case tuneApplyPreset:
|
||
default:
|
||
return "", fmt.Errorf("cible inconnue : %s", target)
|
||
}
|
||
if externalActive() {
|
||
return "", errors.New("le preset actif est une API externe")
|
||
}
|
||
cur := ReadConfig()
|
||
if presetFingerprint([]byte(content)) != configFingerprint(cur) {
|
||
return "", errors.New("ce preset n'est plus celui en service : bascule dessus, ou enregistre le résultat dans une copie")
|
||
}
|
||
if configFingerprint(cur) != res.BaseFP {
|
||
return "", errors.New("la configuration active a changé depuis la mesure : relance l'optimisation")
|
||
}
|
||
lock, err := tuneLockAcquire("apply", serviceIsActive())
|
||
if err != nil {
|
||
return "", err
|
||
}
|
||
defer lock.release()
|
||
lock.setPhase("application")
|
||
backup := filepath.Join(LokiHome(), "tune", "backup", fmt.Sprintf("%s-%d.env", res.PresetID, time.Now().Unix()))
|
||
_ = os.MkdirAll(filepath.Dir(backup), 0o700)
|
||
if err := os.WriteFile(backup, []byte(content), 0o600); err != nil {
|
||
return "", fmt.Errorf("sauvegarde du preset : %w", err)
|
||
}
|
||
// Noté dans le verrou AVANT d'écrire : un Loki tué pendant l'application
|
||
// la défait au redémarrage (tuneUndoApply).
|
||
lock.setBackup(backup, res.PresetID, res.PresetName)
|
||
revert := func(why string) (string, error) {
|
||
say("échec (" + why + ") : retour à l'ancienne version")
|
||
if _, _, err := SavePresetApplying(res.PresetID, res.PresetName, content); err != nil {
|
||
return "", fmt.Errorf("%s ; RETOUR IMPOSSIBLE (%v) — l'ancienne version est dans %s", why, err, backup)
|
||
}
|
||
if err := tuneSvc("restart"); err != nil {
|
||
return "", fmt.Errorf("%s ; ancienne version remise, redémarrage : %s", why, plainErr(err))
|
||
}
|
||
return "", fmt.Errorf("%s : ancienne version rétablie", why)
|
||
}
|
||
say("écriture du preset (sauvegarde : " + backup + ")")
|
||
_, applied, err := SavePresetApplying(res.PresetID, res.PresetName, patched)
|
||
if err != nil {
|
||
return revert("écriture : " + err.Error())
|
||
}
|
||
if !applied {
|
||
return revert("le preset n'a pas été appliqué à la configuration active")
|
||
}
|
||
// Même configuration que l'essai retenu, à l'empreinte près : même
|
||
// composition de la ligne de commande (buildServeArgs) que mesurée.
|
||
if why := tuneAppliedMismatch(ReadConfig(), res); why != "" {
|
||
return revert(why)
|
||
}
|
||
say("redémarrage du moteur")
|
||
if err := tuneSvc("restart"); err != nil {
|
||
return revert("redémarrage : " + plainErr(err))
|
||
}
|
||
if err := tuneProbeFn(ctx, res, say); err != nil {
|
||
return revert(err.Error())
|
||
}
|
||
return "preset « " + res.PresetName + " » optimisé et vérifié", nil
|
||
}
|
||
|
||
// tuneSvc et tuneProbeFn : serviceActionOS et tunePostApplyProbe, remplaçables
|
||
// dans les tests (application puis retour arrière sans vrai moteur).
|
||
var (
|
||
tuneSvc = serviceActionOS
|
||
tuneProbeFn = tunePostApplyProbe
|
||
tuneHealthFn = healthCheck
|
||
)
|
||
|
||
// tuneAppliedMismatch : la configuration active après application doit être
|
||
// EXACTEMENT celle de l'essai retenu — la référence mesurée plus les clés
|
||
// réglées. Remises à leur valeur de référence, on doit retrouver l'empreinte
|
||
// mesurée. Même configuration, même composition (buildServeArgs) : le moteur
|
||
// relancé est bien celui qui a gagné. "" = conforme.
|
||
func tuneAppliedMismatch(now map[string]string, res *tuneResult) string {
|
||
back := copyCfg(now)
|
||
for k, v := range res.Set {
|
||
if now[k] != v {
|
||
return "la configuration appliquée diffère de l'essai retenu sur " + k
|
||
}
|
||
if old := res.Old[k]; old == "" {
|
||
delete(back, k)
|
||
} else {
|
||
back[k] = old
|
||
}
|
||
}
|
||
if configFingerprint(back) != res.BaseFP {
|
||
return "la configuration a changé ailleurs que sur les clés réglées depuis la mesure"
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// tunePostApplyProbe : le moteur redémarré répond, garde le contexte et les
|
||
// slots mesurés, et tient un prompt à la profondeur de la mesure suivi d'un
|
||
// decode — ce qu'un /health seul ne montre pas (un OOM au premier long prompt).
|
||
func tunePostApplyProbe(ctx context.Context, res *tuneResult, say func(string)) error {
|
||
say("attente du moteur")
|
||
e := benchEngine{base: fmt.Sprintf("http://localhost:%d", LLMPort()), auth: tuneAuth(), client: http.DefaultClient}
|
||
deadline := time.Now().Add(tuneLoadTimeout)
|
||
// Un moteur mort au chargement (OOM) se voit vite : plus de service du tout,
|
||
// trois fois de suite après les premières secondes. Inutile d'attendre le
|
||
// délai entier pour revenir à l'ancienne version.
|
||
gone, t0 := 0, time.Now()
|
||
for !tuneHealthFn() {
|
||
if time.Now().After(deadline) {
|
||
return fmt.Errorf("le moteur ne répond pas après %s", tuneLoadTimeout)
|
||
}
|
||
if time.Since(t0) > 10*time.Second && !engineNeedsStop() {
|
||
if gone++; gone >= 3 {
|
||
return errors.New("le moteur s'est arrêté au chargement")
|
||
}
|
||
} else {
|
||
gone = 0
|
||
}
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
case <-time.After(2 * time.Second):
|
||
}
|
||
}
|
||
nCtx, slots, _, err := tuneProps(ctx, e)
|
||
if err != nil {
|
||
return fmt.Errorf("/props illisible : %w", err)
|
||
}
|
||
if nCtx != res.NCtx || slots != res.Slots {
|
||
return fmt.Errorf("contexte %d × %d slot(s) au lieu de %d × %d", nCtx, slots, res.NCtx, res.Slots)
|
||
}
|
||
if res.Depth <= 0 {
|
||
return nil
|
||
}
|
||
say(fmt.Sprintf("sonde : prompt de %d jetons puis decode", res.Depth))
|
||
cfg := ReadConfig()
|
||
s := tuneBenchSetup(cfg, nil, false)
|
||
code := defaultBenchCorpora().code
|
||
sample, _ := benchSlice(code, 0, benchSampleBytes)
|
||
text, _ := benchSlice(code, 0, int(float64(res.Depth)*e.bytesPerToken(ctx, sample)))
|
||
defer engineSideJob()()
|
||
if _, err := s.ask(ctx, e, []Message{{Role: "user", Content: text + "\n\n" + benchAskFirst}}, 64, false); err != nil {
|
||
return fmt.Errorf("sonde : %w", err)
|
||
}
|
||
return nil
|
||
}
|