Files
Loki/internal/loki/tasks_sched.go
T
MichaelandClaude Opus 5.5 9bffff78c2 Optimiseur : loki tune et bouton « Optimiser », essais sans perte sur un moteur privé
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>
2026-10-04 11:56:11 +02:00

74 lines
2.4 KiB
Go

package loki
// tasks_sched.go — la boucle de planification. Une seule goroutine, un tick par
// minute, dans le process qui possède la conversation et l'UI (ajean-ui /
// `ajean link`). Voir StartTaskScheduler, câblée dans newWebMux (web_server.go).
import (
"sync"
"time"
)
var taskSchedOnce sync.Once
// StartTaskScheduler démarre la boucle de planification (idempotent : sûr à
// appeler à chaque construction du mux, ne lance qu'une goroutine).
func StartTaskScheduler() {
taskSchedOnce.Do(func() { go taskSchedLoop() })
}
// taskSchedLoop réveille les tâches dues, une par tick (elles sont de toute façon
// sérialisées par le gate de génération). Le tick d'une minute suffit : c'est la
// résolution de la planification (voir tasks_cron.go).
func taskSchedLoop() {
// Premier passage immédiat pour amorcer les NextRun manquants (après un
// redémarrage du process, ou pour une tâche fraîchement créée sans NextRun).
primeNextRuns()
t := time.NewTicker(time.Minute)
defer t.Stop()
for range t.C {
tickTasks(time.Now())
}
}
// primeNextRuns renseigne NextRun de toute tâche activée qui n'en a pas (0),
// relativement à maintenant — sinon une tâche à NextRun=0 serait considérée « due »
// en permanence, ou jamais planifiée selon le sens de la comparaison.
func primeNextRuns() {
now := time.Now()
for _, t := range listTasks() {
if t.Enabled && t.NextRun == 0 {
t.NextRun = computeNextRun(t.Schedule, t.TZ, now)
_ = saveTask(t)
}
}
}
// tickTasks lance la première tâche due (si l'interrupteur maître n'est pas en
// pause). Une seule par tick : lancer plusieurs inférences en parallèle est de
// toute façon impossible (gate de génération), et étaler leur départ évite qu'une
// rafale de tâches monopolise le modèle d'un coup.
func tickTasks(now time.Time) {
if tasksPaused() || tuneGuard() != nil {
// Optimisation en cours : la tâche due attend sa fin, elle n'échoue pas.
return
}
nowMs := now.UnixMilli()
for _, t := range listTasks() {
if !t.Enabled {
continue
}
if t.NextRun == 0 {
// Priming raté (schedule invalide) ou tâche neuve : on tente de le poser,
// sans la déclencher ce tour-ci.
t.NextRun = computeNextRun(t.Schedule, t.TZ, now)
_ = saveTask(t)
continue
}
if t.NextRun <= nowMs {
runTask(t) // met à jour LastRun/NextRun/… et persiste
return
}
}
}