mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
Chargement du modèle : pourcentage visible ; interface allégée
La pastille « chargement… » restait identique de longues minutes sur un gros GGUF — indiscernable d'un plantage. llama-server n'expose aucun progrès, mais charger c'est LIRE le fichier : on rapporte les octets lus par le process (/proc/<pid>/io, shards compris) à la taille du modèle et la pastille affiche « chargement 43 % » (rafraîchie toutes les 5 s, plafonnée à 99 — c'est /health qui dit prêt). Carte Moteur : bouton « vérifier la version » retiré — « mettre à jour » fait sa propre vérification et dit s'il n'y a rien de neuf. Éditeur de preset : en conteneur, l'option de moteur « Personnalisé » disparaît (rien à compiler dans l'image, la mise à jour passe par la carte Moteur) ; un BIN non reconnu retombe sur le moteur de l'image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
34218fae52
commit
7ef2c956c5
7 files changed
+125
-8
No files matched your search
@@ -0,0 +1,76 @@
|
||||
//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"
|
||||
)
|
||||
|
||||
// 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
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
//go:build !linux
|
||||
|
||||
package loki
|
||||
|
||||
// Hors Linux, pas de /proc/<pid>/io : le pourcentage de chargement n'est pas
|
||||
// mesurable, l'UI garde « chargement… » sans chiffre.
|
||||
func engineLoadPct() int { return -1 }
|
||||
@@ -2199,7 +2199,9 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
||||
<div id="eng-latest" style="margin-top:6px"></div>
|
||||
<div class="row" style="margin:8px 0 0">
|
||||
<button onclick="engCheck()">vérifier la version</button>
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<button onclick="engUpdate()">mettre à jour le moteur</button>
|
||||
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
||||
</div>
|
||||
@@ -2752,7 +2754,7 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<label id="be-lab-image" style="display:none"><input type="radio" name="m-be" value="image" onchange="onBackendMode('image')"><span>Image</span><span class="be-note" id="be-image-note"></span></label>
|
||||
<label id="be-lab-fast"><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Précompilé</span><span class="be-note" id="be-fast-note"></span></label>
|
||||
<label id="be-lab-opt"><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Compilé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||
<label id="be-lab-custom"><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||
</div>
|
||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||
<div class="pe-row">
|
||||
@@ -3267,7 +3269,12 @@ async function loadStatus(){
|
||||
let cls='err', txt='arrêté';
|
||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||
else if(s.load_error){ cls='err'; txt='erreur'; }
|
||||
else if(s.active){ cls='loading'; txt='chargement…'; }
|
||||
else if(s.active){
|
||||
cls='loading'; txt='chargement…';
|
||||
// Pourcentage estimé (octets du modèle déjà lus) : voir « ça avance »
|
||||
// plutôt qu'un « chargement… » figé qui ressemble à un plantage.
|
||||
if(typeof s.load_pct==='number' && s.load_pct>=0) txt='chargement '+s.load_pct+' %';
|
||||
}
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt;
|
||||
MODEL_READY = !!(s.active && s.health);
|
||||
@@ -4340,6 +4347,9 @@ async function populateBackend(){
|
||||
document.getElementById('be-lab-image').style.display = beImagePath ? '' : 'none';
|
||||
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
||||
document.getElementById('be-lab-opt').style.display = beImagePath ? 'none' : '';
|
||||
// En conteneur, « Personnalisé » ne mène nulle part (rien à compiler, et le
|
||||
// moteur se met à jour par la carte Moteur des réglages) : on le retire.
|
||||
document.getElementById('be-lab-custom').style.display = beImagePath ? 'none' : '';
|
||||
document.getElementById('be-image-note').textContent = beImagePath ? 'llama.cpp officiel' : '';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de loki (l'utilisateur peut y déposer son propre build).
|
||||
@@ -4354,8 +4364,11 @@ async function populateBackend(){
|
||||
const cur = currentBinInTextarea();
|
||||
let mode = 'custom';
|
||||
// Un preset sans BIN hérite du moteur global : en conteneur c'est celui de
|
||||
// l'image, qu'on affiche comme tel plutôt qu'en « personnalisé ».
|
||||
// l'image, qu'on affiche comme tel plutôt qu'en « personnalisé ». Et comme
|
||||
// l'option Personnalisé n'y est plus proposée, TOUT BIN non reconnu retombe
|
||||
// aussi sur l'image (un vieux preset pointant un chemin mort redevient sain).
|
||||
if(beImagePath && (sameBinPath(cur, beImagePath) || !cur)) mode = 'image';
|
||||
else if(beImagePath) mode = 'image';
|
||||
else if(sameBinPath(cur, beFastPath) || underDir(cur, beFastDir)) mode = 'fast';
|
||||
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
||||
const radio = document.querySelector('input[name=m-be][value='+mode+']');
|
||||
|
||||
@@ -203,7 +203,9 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<div class="muted" style="margin-top:4px"><code id="lc-provided-bin" style="word-break:break-all"></code></div>
|
||||
<div id="eng-latest" style="margin-top:6px"></div>
|
||||
<div class="row" style="margin:8px 0 0">
|
||||
<button onclick="engCheck()">vérifier la version</button>
|
||||
<!-- Pas de bouton « vérifier la version » : la mise à jour fait sa
|
||||
propre vérification et dit s'il n'y a rien de neuf. Un bouton de
|
||||
moins pour la même information. -->
|
||||
<button onclick="engUpdate()">mettre à jour le moteur</button>
|
||||
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
||||
</div>
|
||||
@@ -756,7 +758,7 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<label id="be-lab-image" style="display:none"><input type="radio" name="m-be" value="image" onchange="onBackendMode('image')"><span>Image</span><span class="be-note" id="be-image-note"></span></label>
|
||||
<label id="be-lab-fast"><input type="radio" name="m-be" value="fast" onchange="onBackendMode('fast')"><span>Précompilé</span><span class="be-note" id="be-fast-note"></span></label>
|
||||
<label id="be-lab-opt"><input type="radio" name="m-be" value="opt" onchange="onBackendMode('opt')"><span>Compilé</span><span class="be-note" id="be-opt-note"></span></label>
|
||||
<label><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||
<label id="be-lab-custom"><input type="radio" name="m-be" value="custom" onchange="onBackendMode('custom')"><span>Personnalisé</span></label>
|
||||
</div>
|
||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||
<div class="pe-row">
|
||||
|
||||
@@ -9,7 +9,12 @@ async function loadStatus(){
|
||||
let cls='err', txt='arrêté';
|
||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||
else if(s.load_error){ cls='err'; txt='erreur'; }
|
||||
else if(s.active){ cls='loading'; txt='chargement…'; }
|
||||
else if(s.active){
|
||||
cls='loading'; txt='chargement…';
|
||||
// Pourcentage estimé (octets du modèle déjà lus) : voir « ça avance »
|
||||
// plutôt qu'un « chargement… » figé qui ressemble à un plantage.
|
||||
if(typeof s.load_pct==='number' && s.load_pct>=0) txt='chargement '+s.load_pct+' %';
|
||||
}
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt;
|
||||
MODEL_READY = !!(s.active && s.health);
|
||||
|
||||
@@ -399,6 +399,9 @@ async function populateBackend(){
|
||||
document.getElementById('be-lab-image').style.display = beImagePath ? '' : 'none';
|
||||
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
||||
document.getElementById('be-lab-opt').style.display = beImagePath ? 'none' : '';
|
||||
// En conteneur, « Personnalisé » ne mène nulle part (rien à compiler, et le
|
||||
// moteur se met à jour par la carte Moteur des réglages) : on le retire.
|
||||
document.getElementById('be-lab-custom').style.display = beImagePath ? 'none' : '';
|
||||
document.getElementById('be-image-note').textContent = beImagePath ? 'llama.cpp officiel' : '';
|
||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
||||
// le dossier backends de loki (l'utilisateur peut y déposer son propre build).
|
||||
@@ -413,8 +416,11 @@ async function populateBackend(){
|
||||
const cur = currentBinInTextarea();
|
||||
let mode = 'custom';
|
||||
// Un preset sans BIN hérite du moteur global : en conteneur c'est celui de
|
||||
// l'image, qu'on affiche comme tel plutôt qu'en « personnalisé ».
|
||||
// l'image, qu'on affiche comme tel plutôt qu'en « personnalisé ». Et comme
|
||||
// l'option Personnalisé n'y est plus proposée, TOUT BIN non reconnu retombe
|
||||
// aussi sur l'image (un vieux preset pointant un chemin mort redevient sain).
|
||||
if(beImagePath && (sameBinPath(cur, beImagePath) || !cur)) mode = 'image';
|
||||
else if(beImagePath) mode = 'image';
|
||||
else if(sameBinPath(cur, beFastPath) || underDir(cur, beFastDir)) mode = 'fast';
|
||||
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
||||
const radio = document.querySelector('input[name=m-be][value='+mode+']');
|
||||
|
||||
@@ -41,8 +41,15 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
// doit être signalée clairement plutôt que de laisser un « chargement… »
|
||||
// perpétuel / un crash-loop muet.
|
||||
loadErr := ""
|
||||
loadPct := -1
|
||||
if !health {
|
||||
loadErr = modelLoadError()
|
||||
// Modèle en cours de chargement (service actif, pas d'échec relevé) :
|
||||
// pourcentage estimé par les octets lus du .gguf (sys_loadpct_linux.go),
|
||||
// pour que « chargement… » bouge au lieu de laisser croire à un plantage.
|
||||
if active && loadErr == "" {
|
||||
loadPct = engineLoadPct()
|
||||
}
|
||||
}
|
||||
// Alerte de persistance : /data non monté en conteneur = données perdues à
|
||||
// la prochaine recréation. Même bandeau que les autres avertissements.
|
||||
@@ -63,6 +70,7 @@ func handleStatus(w http.ResponseWriter, r *http.Request) {
|
||||
"version": Version,
|
||||
"warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien
|
||||
"load_error": loadErr, // modèle qui ne charge pas (incompat moteur…) — vide sinon
|
||||
"load_pct": loadPct, // % estimé du chargement en cours (-1 = non mesurable)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user