mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-24 15:38:12 +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 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 id="eng-latest" style="margin-top:6px"></div>
|
||||||
<div class="row" style="margin:8px 0 0">
|
<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 onclick="engUpdate()">mettre à jour le moteur</button>
|
||||||
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
||||||
</div>
|
</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-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-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 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>
|
||||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||||
<div class="pe-row">
|
<div class="pe-row">
|
||||||
@@ -3267,7 +3269,12 @@ async function loadStatus(){
|
|||||||
let cls='err', txt='arrêté';
|
let cls='err', txt='arrêté';
|
||||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||||
else if(s.load_error){ cls='err'; txt='erreur'; }
|
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.className='statuspill '+cls;
|
||||||
el.innerHTML='<span class="dot"></span>'+txt;
|
el.innerHTML='<span class="dot"></span>'+txt;
|
||||||
MODEL_READY = !!(s.active && s.health);
|
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-image').style.display = beImagePath ? '' : 'none';
|
||||||
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
||||||
document.getElementById('be-lab-opt').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' : '';
|
document.getElementById('be-image-note').textContent = beImagePath ? 'llama.cpp officiel' : '';
|
||||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
// 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).
|
// le dossier backends de loki (l'utilisateur peut y déposer son propre build).
|
||||||
@@ -4354,8 +4364,11 @@ async function populateBackend(){
|
|||||||
const cur = currentBinInTextarea();
|
const cur = currentBinInTextarea();
|
||||||
let mode = 'custom';
|
let mode = 'custom';
|
||||||
// Un preset sans BIN hérite du moteur global : en conteneur c'est celui de
|
// 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';
|
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, beFastPath) || underDir(cur, beFastDir)) mode = 'fast';
|
||||||
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
||||||
const radio = document.querySelector('input[name=m-be][value='+mode+']');
|
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 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 id="eng-latest" style="margin-top:6px"></div>
|
||||||
<div class="row" style="margin:8px 0 0">
|
<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 onclick="engUpdate()">mettre à jour le moteur</button>
|
||||||
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
<button id="eng-revert" style="display:none" onclick="engUse('image')">revenir au moteur de l'image</button>
|
||||||
</div>
|
</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-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-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 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>
|
||||||
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
<div id="m-backend-custom" class="pe-list" style="display:none;margin-top:8px">
|
||||||
<div class="pe-row">
|
<div class="pe-row">
|
||||||
|
|||||||
@@ -9,7 +9,12 @@ async function loadStatus(){
|
|||||||
let cls='err', txt='arrêté';
|
let cls='err', txt='arrêté';
|
||||||
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
if(s.active && s.health){ cls='ok'; txt='prêt'; }
|
||||||
else if(s.load_error){ cls='err'; txt='erreur'; }
|
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.className='statuspill '+cls;
|
||||||
el.innerHTML='<span class="dot"></span>'+txt;
|
el.innerHTML='<span class="dot"></span>'+txt;
|
||||||
MODEL_READY = !!(s.active && s.health);
|
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-image').style.display = beImagePath ? '' : 'none';
|
||||||
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
document.getElementById('be-lab-fast').style.display = beImagePath ? 'none' : '';
|
||||||
document.getElementById('be-lab-opt').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' : '';
|
document.getElementById('be-image-note').textContent = beImagePath ? 'llama.cpp officiel' : '';
|
||||||
// Menu « backend détecté » du mode personnalisé : tout ce qu'on trouve dans
|
// 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).
|
// le dossier backends de loki (l'utilisateur peut y déposer son propre build).
|
||||||
@@ -413,8 +416,11 @@ async function populateBackend(){
|
|||||||
const cur = currentBinInTextarea();
|
const cur = currentBinInTextarea();
|
||||||
let mode = 'custom';
|
let mode = 'custom';
|
||||||
// Un preset sans BIN hérite du moteur global : en conteneur c'est celui de
|
// 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';
|
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, beFastPath) || underDir(cur, beFastDir)) mode = 'fast';
|
||||||
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
else if(sameBinPath(cur, beOptPath)) mode = 'opt';
|
||||||
const radio = document.querySelector('input[name=m-be][value='+mode+']');
|
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… »
|
// doit être signalée clairement plutôt que de laisser un « chargement… »
|
||||||
// perpétuel / un crash-loop muet.
|
// perpétuel / un crash-loop muet.
|
||||||
loadErr := ""
|
loadErr := ""
|
||||||
|
loadPct := -1
|
||||||
if !health {
|
if !health {
|
||||||
loadErr = modelLoadError()
|
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 à
|
// Alerte de persistance : /data non monté en conteneur = données perdues à
|
||||||
// la prochaine recréation. Même bandeau que les autres avertissements.
|
// 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,
|
"version": Version,
|
||||||
"warn": warn, // App Translocation macOS, /data non monté… — vide si tout va bien
|
"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_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