mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
L'ancien bench ne lisait pas le statut HTTP, inventait un partage 15/85 du temps quand les timings manquaient (et l'enregistrait comme mesuré), tuilait un petit corpus qu'un brouillon recopiait, et ne disait rien du prefill à 30k ni du chemin où le cache est repris. Synchrone, il était coupé par les proxys (60-100 s) pendant que le moteur continuait, et un simple GET suffisait à le lancer. - Tâche de fond : POST /api/bench (202, 405 sur GET, 409 si occupé), /api/bench/status pour la progression, /api/bench/cancel ; chaque requête au moteur porte le contexte annulable. - Verrou de génération tenu pendant toute la mesure : chat, tâches et compaction reçoivent un refus clair ; les clients /v1 un 503 avec Retry-After ; /slots occupé (client externe, CLI) refuse aussi. - Mode rapide par défaut : préchauffage jeté + la ligne courte, sur le chat avec gabarit, raisonnement et échantillonnage du preset, seed fixe. - Mode complet (bouton « bench complet », loki bench --full) : prefill à froid à D = min(CTX/2, 32k, ce qui laisse tenir les tours), 16k si des poids tournent sur CPU, puis 3 tours user → assistant → user de code jamais vu. Reprise du cache lue dans cache_n, jamais supposée ; note pour les hybrides (points de contrôle). Pas d'ignore_eos. - Rien n'est enregistré sans réponses 200 et timings réels, ni pour un résultat partiel (budget de 6 min par requête, contexte plein). - Empreinte enregistrée (configuration + CUDA_VISIBLE_DEVICES + protocole) ; les anciennes mesures retombent sur le nom du modèle. Types de cache KV et build du moteur affichés, KV quantifié signalé (zone grise). - Linux : indication « possible thrash » (read_bytes, VmRSS), jamais enregistrée. - Effacement du slot en fin de bench inchangé (engineSideJob, différé : il tourne aussi sur erreur et annulation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
129 lines
3.9 KiB
Go
129 lines
3.9 KiB
Go
//go:build linux
|
|
|
|
package loki
|
|
|
|
// sys_loadpct_linux.go — pourcentage de CHARGEMENT du modèle, pour la pastille
|
|
// « chargement… » de l'UI qui restait muette de longues minutes sur un gros
|
|
// GGUF (un 27B Q4 met facilement 1 à 3 minutes à monter en VRAM).
|
|
//
|
|
// llama-server n'expose aucun progrès pendant le chargement (/health renvoie
|
|
// juste 503). Mais le chargement, c'est LIRE le fichier .gguf : on mesure donc
|
|
// les octets lus par le process moteur (/proc/<pid>/io, read_bytes — les pages
|
|
// mmap rentrées comptent dedans) et on les rapporte à la taille du fichier.
|
|
// Approximation honnête : monotone, ~0→100, et surtout elle BOUGE — c'est ce
|
|
// qui distingue « ça charge » de « c'est planté ».
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// procVmRSS lit la mémoire résidente (octets) d'un process — /proc/<pid>/status,
|
|
// ligne « VmRSS: N kB ». 0 si illisible.
|
|
func procVmRSS(pid int) int64 {
|
|
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/status")
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
if v, ok := strings.CutPrefix(line, "VmRSS:"); ok {
|
|
v = strings.TrimSpace(strings.TrimSuffix(strings.TrimSpace(v), "kB"))
|
|
if n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64); err == nil {
|
|
return n * 1024
|
|
}
|
|
}
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// engineLoadPct renvoie le pourcentage estimé de chargement du modèle, ou -1
|
|
// quand il n'est pas mesurable (pas de PID, /proc illisible, taille inconnue).
|
|
func engineLoadPct() int {
|
|
pid := readServicePID()
|
|
if pid <= 0 {
|
|
return -1
|
|
}
|
|
cfg := ReadConfig()
|
|
model := cfg["MODEL"]
|
|
if model == "" {
|
|
return -1
|
|
}
|
|
path, err := resolveServeModelPath(model)
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
size := modelTotalSize(path)
|
|
if size <= 0 {
|
|
return -1
|
|
}
|
|
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/io")
|
|
if err != nil {
|
|
return -1
|
|
}
|
|
var read int64 = -1
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
if v, ok := strings.CutPrefix(line, "read_bytes:"); ok {
|
|
read, _ = strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
break
|
|
}
|
|
}
|
|
if read < 0 {
|
|
return -1
|
|
}
|
|
// Modèle déjà dans le cache disque (redémarrage à chaud) : llama-server ne
|
|
// LIT plus rien sur le disque — les pages viennent du cache, read_bytes
|
|
// reste à zéro et le pourcentage restait figé à 0 jusqu'au « prêt ». La
|
|
// mémoire résidente (VmRSS), elle, grandit quoi qu'il arrive : les pages du
|
|
// .gguf touchées via mmap y comptent, cache ou pas. On prend le plus avancé
|
|
// des deux — approximatif (l'offload GPU peut relâcher des pages), mais le
|
|
// compteur BOUGE au lieu de mentir.
|
|
if rss := procVmRSS(pid); rss > read {
|
|
read = rss
|
|
}
|
|
pct := int(read * 100 / size)
|
|
if pct > 99 {
|
|
// 100 % n'existe pas ici : c'est /health qui dit « prêt ». Au-delà de 99
|
|
// (relectures, autres IO du process), on plafonne.
|
|
pct = 99
|
|
}
|
|
return pct
|
|
}
|
|
|
|
// modelTotalSize : taille du modèle, shards compris (model-00001-of-00004.gguf
|
|
// → somme des tranches présentes — c'est tout ça qu'il faut charger).
|
|
func modelTotalSize(path string) int64 {
|
|
if total := shardFamilySize(filepath.Dir(path), filepath.Base(path)); total > 0 {
|
|
return total
|
|
}
|
|
if st, err := os.Stat(path); err == nil {
|
|
return st.Size()
|
|
}
|
|
return 0
|
|
}
|
|
|
|
// engineIOSample : octets lus du disque (read_bytes) et mémoire résidente du
|
|
// moteur, pour l'indication « possible thrash » du benchmark. ok=false si le
|
|
// PID ou /proc est illisible : le bench s'en passe sans rien dire.
|
|
func engineIOSample() (read, rss int64, ok bool) {
|
|
pid := readServicePID()
|
|
if pid <= 0 {
|
|
return 0, 0, false
|
|
}
|
|
b, err := os.ReadFile("/proc/" + strconv.Itoa(pid) + "/io")
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
for _, line := range strings.Split(string(b), "\n") {
|
|
if v, found := strings.CutPrefix(line, "read_bytes:"); found {
|
|
n, err := strconv.ParseInt(strings.TrimSpace(v), 10, 64)
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
return n, procVmRSS(pid), true
|
|
}
|
|
}
|
|
return 0, 0, false
|
|
}
|