mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +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>
215 lines
5.8 KiB
Go
215 lines
5.8 KiB
Go
package loki
|
|
|
|
// code_jobs.go — commandes shell d'ARRIÈRE-PLAN pour le mode code : bash_bg
|
|
// lance un process détaché (serveur de dev, watcher, build long) et rend la
|
|
// main tout de suite ; bash_tail relit sa sortie et son état. Sans ça, le
|
|
// modèle lance `./serveur &` dans bash et le tour reste suspendu aux tubes
|
|
// (voir le commentaire WaitDelay de runShell).
|
|
//
|
|
// La sortie va dans un fichier du dossier de la discussion
|
|
// (.loki/jobs/<id>.log) : elle survit au tour, et le panneau Fichiers la montre.
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
type bgJob struct {
|
|
ID string
|
|
Command string
|
|
LogPath string
|
|
Cmd *exec.Cmd
|
|
Started time.Time
|
|
|
|
mu sync.Mutex
|
|
done bool
|
|
exit int
|
|
}
|
|
|
|
var (
|
|
jobsMu sync.Mutex
|
|
jobs = map[string]*bgJob{}
|
|
jobSeq int
|
|
)
|
|
|
|
func bashBgTool() Tool {
|
|
return Tool{
|
|
Type: "function",
|
|
Function: ToolFunction{
|
|
Name: "bash_bg",
|
|
Description: "Start a shell command in the BACKGROUND (dev server, watcher, long build) and return immediately with a job id. Output goes to a log file; check it with bash_tail. Use this instead of appending '&' in bash.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"command": map[string]any{"type": "string", "description": "The command"},
|
|
},
|
|
"required": []string{"command"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
func bashTailTool() Tool {
|
|
return Tool{
|
|
Type: "function",
|
|
Function: ToolFunction{
|
|
Name: "bash_tail",
|
|
Description: "Read the last lines of a background job's output and its status (running/exited). kill:true stops the job.",
|
|
Parameters: map[string]any{
|
|
"type": "object",
|
|
"properties": map[string]any{
|
|
"id": map[string]any{"type": "string", "description": "Job id from bash_bg"},
|
|
"lines": map[string]any{"type": "integer", "description": "Lines to show (default 40, max 200)"},
|
|
"kill": map[string]any{"type": "boolean", "description": "Stop the job"},
|
|
},
|
|
"required": []string{"id"},
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// toolBashBg lance la commande détachée. Même politique que bash (commandes
|
|
// dangereuses refusées), même dossier de départ (discussion courante).
|
|
func toolBashBg(args map[string]any) string {
|
|
command, _ := args["command"].(string)
|
|
if strings.TrimSpace(command) == "" {
|
|
return "[erreur] commande vide"
|
|
}
|
|
if reason := dangerousCommand(command); reason != "" {
|
|
return refusedCommandResult(reason)
|
|
}
|
|
// Un serveur de dev ou un build lancé pendant une optimisation fausserait
|
|
// ses mesures (CPU, disque) — et un tour de modèle n'a de toute façon pas pu
|
|
// démarrer : ceci ne vise qu'un tour lancé juste avant.
|
|
if err := tuneGuard(); err != nil {
|
|
return "[erreur] " + err.Error()
|
|
}
|
|
ws := agentCwd()
|
|
logDir := filepath.Join(ws, ".loki", "jobs")
|
|
if err := os.MkdirAll(logDir, 0o755); err != nil {
|
|
return "[erreur] " + err.Error()
|
|
}
|
|
jobsMu.Lock()
|
|
jobSeq++
|
|
id := fmt.Sprintf("job%d", jobSeq)
|
|
jobsMu.Unlock()
|
|
logPath := filepath.Join(logDir, id+".log")
|
|
logFile, err := os.Create(logPath)
|
|
if err != nil {
|
|
return "[erreur] " + err.Error()
|
|
}
|
|
// Contexte de FOND, pas celui du tour : le job doit survivre à la fin du
|
|
// tour — c'est sa raison d'être. Il s'arrête par bash_tail kill:true, ou
|
|
// avec le conteneur.
|
|
cmd := newShellCmdDetached(command)
|
|
cmd.Dir = ws
|
|
cmd.Stdout = logFile
|
|
cmd.Stderr = logFile
|
|
if err := cmd.Start(); err != nil {
|
|
logFile.Close()
|
|
return "[erreur] " + err.Error()
|
|
}
|
|
j := &bgJob{ID: id, Command: command, LogPath: logPath, Cmd: cmd, Started: time.Now()}
|
|
jobsMu.Lock()
|
|
jobs[id] = j
|
|
jobsMu.Unlock()
|
|
go func() {
|
|
err := cmd.Wait()
|
|
logFile.Close()
|
|
j.mu.Lock()
|
|
j.done = true
|
|
if ee, ok := err.(*exec.ExitError); ok {
|
|
j.exit = ee.ExitCode()
|
|
}
|
|
j.mu.Unlock()
|
|
}()
|
|
return fmt.Sprintf("[ok] job %s lancé (pid %d) — sortie dans .loki/jobs/%s.log, suis-la avec bash_tail", id, cmd.Process.Pid, id)
|
|
}
|
|
|
|
// toolBashTail relit la fin du journal d'un job et son état.
|
|
func toolBashTail(args map[string]any) string {
|
|
id, _ := args["id"].(string)
|
|
jobsMu.Lock()
|
|
j := jobs[id]
|
|
jobsMu.Unlock()
|
|
if j == nil {
|
|
return "[erreur] job inconnu : " + id
|
|
}
|
|
if kill, _ := args["kill"].(bool); kill {
|
|
j.mu.Lock()
|
|
running := !j.done
|
|
j.mu.Unlock()
|
|
if running && j.Cmd.Process != nil {
|
|
_ = j.Cmd.Process.Kill()
|
|
}
|
|
}
|
|
lines := 40
|
|
if v, ok := args["lines"].(float64); ok && int(v) > 0 {
|
|
lines = int(v)
|
|
}
|
|
if lines > 200 {
|
|
lines = 200
|
|
}
|
|
b, err := os.ReadFile(j.LogPath)
|
|
if err != nil {
|
|
return "[erreur] " + err.Error()
|
|
}
|
|
all := strings.Split(strings.TrimRight(string(b), "\n"), "\n")
|
|
if len(all) > lines {
|
|
all = all[len(all)-lines:]
|
|
}
|
|
j.mu.Lock()
|
|
status := "en cours"
|
|
if j.done {
|
|
status = fmt.Sprintf("terminé (exit %d)", j.exit)
|
|
}
|
|
j.mu.Unlock()
|
|
out := fmt.Sprintf("job %s — %s — depuis %s\ncommande : %s\n", j.ID, status, time.Since(j.Started).Round(time.Second), j.Command)
|
|
tail := strings.Join(all, "\n")
|
|
if strings.TrimSpace(tail) == "" {
|
|
tail = "(aucune sortie pour l'instant)"
|
|
}
|
|
return out + "\n" + tailRunes(tail, toolMaxOutput)
|
|
}
|
|
|
|
// bgJobsRunning : jobs d'arrière-plan encore en vie. L'optimiseur refuse de
|
|
// mesurer à côté d'eux : un build ou un serveur de dev prend le CPU et le
|
|
// disque que mesurent ses essais.
|
|
func bgJobsRunning() int {
|
|
jobsMu.Lock()
|
|
defer jobsMu.Unlock()
|
|
n := 0
|
|
for _, j := range jobs {
|
|
j.mu.Lock()
|
|
if !j.done {
|
|
n++
|
|
}
|
|
j.mu.Unlock()
|
|
}
|
|
return n
|
|
}
|
|
|
|
// stopConvJobs tue les jobs d'une discussion (suppression / vidage de la
|
|
// discussion : ses fichiers partent, ses process aussi).
|
|
func stopConvJobs(convID string) {
|
|
prefix := filepath.Join(agentWorkspace(), convFilesRoot, safeConvID(convID))
|
|
jobsMu.Lock()
|
|
defer jobsMu.Unlock()
|
|
for id, j := range jobs {
|
|
if strings.HasPrefix(j.LogPath, prefix) {
|
|
j.mu.Lock()
|
|
running := !j.done
|
|
j.mu.Unlock()
|
|
if running && j.Cmd.Process != nil {
|
|
_ = j.Cmd.Process.Kill()
|
|
}
|
|
delete(jobs, id)
|
|
}
|
|
}
|
|
}
|