mirror of
https://github.com/R0m1k3/Loki.git
synced 2026-10-11 17:26:57 +02:00
VRAM : un bouton pour décharger le modèle et rendre la carte
Le modèle occupe la mémoire vidéo tant que le moteur tourne : une autre
application qui réclame la carte (jeu, encodage, autre serveur d'inférence)
ne trouvait plus rien à prendre. Le geste existait — « arrêter » le service,
au fond des réglages — mais son nom ne disait pas qu'il libérait la VRAM, et
il laissait tourner le serveur de dictée, qui garde la carte lui aussi.
- POST /api/vram/unload : arrête le moteur ET la dictée, attend que le pilote
rende la mémoire (une lecture immédiate rapporte « 0 Mo libérés » après un
déchargement pourtant réussi) et renvoie le bilan chiffré. Refuse pendant une
génération, sauf {force:true} — la couper perdrait la réponse en cours.
- POST /api/vram/reload : relance le moteur, préflight compris (BIN/MODEL
absents = la vraie raison tout de suite, pas un « chargement… » sans fin).
- Bouton sur les jauges du moniteur, là où l'on regarde la VRAM ; il devient
« Recharger le modèle » dès que le moteur est arrêté, d'après /api/status et
non d'un drapeau local (un second onglet afficherait sinon un bouton qui ment).
- Même commande dans Réglages → Moteur → Service du moteur.
- Carte de saisie : « Modèle déchargé — Recharger le modèle » au lieu de
« Le modèle charge », qui promettait un chargement qui ne viendrait jamais.
La lecture nvidia-smi de /api/vram passe dans web_vram.go (gpuStats), partagée
avec le bilan du déchargement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V8n9tDn5sZudAcUT8U6uGg
This commit is contained in:
9 files changed
+384
-31
No files matched your search
@@ -290,6 +290,12 @@ Ajoutées par ce fork :
|
||||
latérale s'escamote pour rendre toute la largeur au fil ; les discussions s'y
|
||||
cherchent au clavier et les jauges **GPU / VRAM / mémoire vive** restent
|
||||
visibles en pied de colonne.
|
||||
- **Libérer la VRAM d'un clic** : sur les jauges du moniteur, un bouton décharge
|
||||
le modèle et arrête le moteur (ainsi que le serveur de dictée, qui occupe la
|
||||
carte lui aussi) pour rendre la mémoire vidéo à une autre application — jeu,
|
||||
encodage, autre serveur d'inférence. Le bilan est annoncé en Gio réellement
|
||||
rendus, et le même bouton devient **Recharger le modèle** pour reprendre la
|
||||
main. Routes : `POST /api/vram/unload` et `POST /api/vram/reload`.
|
||||
- **API OpenAI servie par Loki** : `/v1/*` est exposé **sur le port de
|
||||
l'interface** (8090) et relayé vers llama-server, au lieu d'annoncer l'adresse
|
||||
du moteur. Conséquence directe : l'API est joignable partout où l'interface
|
||||
|
||||
@@ -1720,6 +1720,16 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
/* --- Moniteur machine (pied de barre latérale) ----------------------- */
|
||||
.perf-head{display:flex;align-items:center;gap:8px;margin-bottom:11px;
|
||||
font-size:14px;font-weight:600;color:var(--text);letter-spacing:-.01em}
|
||||
/* Bouton du moniteur (« Libérer la VRAM » / « Recharger le modèle ») : poussé
|
||||
à droite du titre, discret tant qu'on ne le cherche pas. En mode rechargement
|
||||
il prend la couleur d'accent — le moteur est arrêté, c'est l'action qui
|
||||
compte à ce moment-là. */
|
||||
.perf-act{margin-left:auto;flex:none;padding:3px 8px;font-size:11px;line-height:1.4;
|
||||
color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer;
|
||||
white-space:nowrap}
|
||||
.perf-act:hover:not(:disabled){color:var(--text)}
|
||||
.perf-act:disabled{opacity:.55;cursor:progress}
|
||||
.perf-act.reload{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
|
||||
.perf-row{margin:0 0 14px}
|
||||
.perf-l{display:flex;align-items:baseline;gap:8px;font-size:12.5px;color:var(--text);margin-bottom:6px}
|
||||
.perf-l b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
@@ -2231,7 +2241,11 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
fil. Le détail chiffré (Gio, température) vit sous chaque jauge, et le
|
||||
bloc entier se replie d'un clic. -->
|
||||
<div id="side-perf">
|
||||
<div class="perf-head"><span>Performance</span></div>
|
||||
<!-- « Libérer la VRAM » est posé LÀ, sur les jauges : c'est en les regardant
|
||||
qu'on décide de rendre la carte à une autre application. Le libellé
|
||||
bascule en « Recharger le modèle » dès que le moteur est arrêté — voir
|
||||
paintVramBtn (js/05-status.js). -->
|
||||
<div class="perf-head"><span>Performance</span><button id="vram-btn" class="perf-act" onclick="vramBtnClick()" title="décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine">Libérer la VRAM</button></div>
|
||||
<div class="perf-body"><div id="vram">…</div><div id="ram-details"><div id="ram"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -2591,15 +2605,18 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
<pre id="lc-log" hidden></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||
leur place est ici, avec le moteur, et pas dans une section « Service »
|
||||
à part qui ne contenait qu'eux. -->
|
||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
||||
<!-- Ces boutons pilotent le service qui fait tourner llama-server : leur
|
||||
place est ici, avec le moteur, et pas dans une section « Service » à
|
||||
part qui ne contenait qu'eux. Le dernier, « libérer la VRAM », fait
|
||||
plus qu'arrêter : il éteint aussi la dictée et annonce la mémoire
|
||||
vidéo rendue (c'est le même geste que le bouton du moniteur). -->
|
||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).<br><b>Libérer la VRAM</b> arrête le moteur <i>et</i> la dictée, puis annonce la mémoire vidéo rendue : c'est ce qu'il faut quand une autre application réclame la carte. Le même bouton vit sur les jauges, en pied de barre latérale.</span></span></div>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<div class="row" style="margin:0">
|
||||
<button onclick="act('start')">démarrer</button>
|
||||
<button onclick="act('restart')">redémarrer</button>
|
||||
<button onclick="act('stop')">arrêter</button>
|
||||
<button onclick="vramUnload(false)" title="décharge le modèle et rend la mémoire vidéo à la machine">libérer la VRAM</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
@@ -3705,6 +3722,11 @@ async function loadStatus(){
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt;
|
||||
MODEL_READY = !!(s.active && s.health);
|
||||
// Le bouton « Libérer la VRAM » suit l'état RÉEL du moteur (et pas un drapeau
|
||||
// local) : un arrêt venu d'un autre onglet, du terminal ou d'un job doit se
|
||||
// lire ici aussi, sinon le bouton propose de libérer une carte déjà libre.
|
||||
ENGINE_ACTIVE = !!s.active;
|
||||
paintVramBtn();
|
||||
// Le bouton d'envoi suit l'état du moteur : inutile de pouvoir envoyer un
|
||||
// message à un modèle qui n'est pas encore chargé (voir syncSendBtn).
|
||||
STATUS_SEEN = true;
|
||||
@@ -3869,6 +3891,71 @@ async function loadVram(){
|
||||
g.name+' · '+vram));
|
||||
});
|
||||
}
|
||||
// ─── Libérer la mémoire vidéo ───────────────────────────────────────────────
|
||||
// Tant que le moteur tourne, le modèle occupe la VRAM : une autre application
|
||||
// qui réclame la carte (un jeu, un encodage, un autre serveur d'inférence) ne
|
||||
// trouve plus rien à prendre. Ce bouton l'en sort, et le MÊME bouton la
|
||||
// reprend ensuite — le geste existait déjà (« arrêter » le service, au fond des
|
||||
// réglages), mais son nom ne disait pas qu'il rendait la carte.
|
||||
//
|
||||
// ENGINE_ACTIVE vient de /api/status (relu toutes les 5 s), jamais d'un drapeau
|
||||
// posé au clic : sinon un second onglet, ou un arrêt venu du terminal,
|
||||
// afficherait un bouton qui ment.
|
||||
let ENGINE_ACTIVE = true, VRAM_BUSY = false;
|
||||
function paintVramBtn(){
|
||||
const b = document.getElementById('vram-btn');
|
||||
if(!b || VRAM_BUSY) return; // pendant l'opération, le libellé appartient à vramBusy
|
||||
b.disabled = false;
|
||||
b.classList.toggle('reload', !ENGINE_ACTIVE);
|
||||
b.textContent = ENGINE_ACTIVE ? 'Libérer la VRAM' : 'Recharger le modèle';
|
||||
b.title = ENGINE_ACTIVE
|
||||
? 'décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine'
|
||||
: 'relance le moteur : le modèle revient en mémoire vidéo';
|
||||
}
|
||||
function vramBtnClick(){ return ENGINE_ACTIVE ? vramUnload(false) : vramReload(); }
|
||||
function vramBusy(on, label){
|
||||
VRAM_BUSY = on;
|
||||
const b = document.getElementById('vram-btn');
|
||||
if(b){ b.disabled = on; if(on && label) b.textContent = label; }
|
||||
if(!on) paintVramBtn();
|
||||
}
|
||||
// force=true : on a déjà demandé confirmation ET accepté de couper une réponse
|
||||
// en cours d'écriture (le serveur refuse sans ça, voir handleVramUnload).
|
||||
async function vramUnload(force){
|
||||
if(!force){
|
||||
const ok = await askConfirm(
|
||||
'Décharger le modèle ? Le moteur s\'arrête et rend la mémoire vidéo — le chat reste indisponible jusqu\'au rechargement.',
|
||||
{title:'Libérer la VRAM', okText:'Libérer'});
|
||||
if(!ok) return;
|
||||
}
|
||||
vramBusy(true, 'libération…');
|
||||
let r = null;
|
||||
try{ r = await jpost('/api/vram/unload', {force: !!force}); }catch(_){}
|
||||
vramBusy(false);
|
||||
if(r && r.generating){
|
||||
const ok = await askConfirm('Une réponse est en cours d\'écriture : décharger maintenant l\'interrompt. Continuer ?',
|
||||
{title:'Génération en cours', okText:'Décharger quand même'});
|
||||
if(ok) return vramUnload(true);
|
||||
return;
|
||||
}
|
||||
// La mesure d'abord : la dictée peut avoir rendu de la VRAM alors même que le
|
||||
// moteur était déjà arrêté — annoncer « déjà arrêté » nierait ce qui vient
|
||||
// d'être libéré.
|
||||
if(!r || !r.ok) toast('échec : ' + ((r && r.error) || 'réseau'));
|
||||
else if(r.gpu && r.freed_mb > 0) toast('VRAM libérée : ' + (r.freed_mb/1024).toFixed(1) + ' Gio');
|
||||
else if(!r.was_active) toast('le moteur était déjà arrêté');
|
||||
else toast('modèle déchargé');
|
||||
loadAll();
|
||||
}
|
||||
async function vramReload(){
|
||||
vramBusy(true, 'démarrage…');
|
||||
let r = null;
|
||||
try{ r = await jpost('/api/vram/reload', {}); }catch(_){}
|
||||
vramBusy(false);
|
||||
if(!r || !r.ok){ toast('échec : ' + ((r && r.error) || 'réseau')); return; }
|
||||
toast('modèle en cours de chargement…');
|
||||
loadAll();
|
||||
}
|
||||
async function loadRam(){
|
||||
const m=await jget('/api/ram');
|
||||
const box=document.getElementById('ram-details');
|
||||
@@ -6470,10 +6557,16 @@ function syncSendBtn(){
|
||||
// répond pas du tout) — dans le doute on laisse la main.
|
||||
const ready = !STATUS_SEEN || MODEL_READY;
|
||||
sb.disabled = !ready;
|
||||
sb.title = ready ? '' : 'le modèle n\'est pas encore chargé';
|
||||
// Moteur ARRÊTÉ (VRAM libérée à la demande, ou service coupé) : dire « le
|
||||
// modèle charge » serait faux, et on attendrait un chargement qui ne viendra
|
||||
// jamais. On nomme alors le geste qui remet le modèle en mémoire.
|
||||
const unloaded = !ready && !ENGINE_ACTIVE;
|
||||
sb.title = ready ? '' : (unloaded ? 'le modèle est déchargé — recharge-le pour écrire'
|
||||
: 'le modèle n\'est pas encore chargé');
|
||||
const hint=document.getElementById('sendhint');
|
||||
if(hint){
|
||||
hint.textContent = ready ? 'Entrée pour envoyer · Maj+Entrée = nouvelle ligne'
|
||||
: unloaded ? 'Modèle déchargé — « Recharger le modèle » dans le moniteur, en bas de la barre latérale.'
|
||||
: 'Le modèle charge — envoi possible dès qu\'il est prêt.';
|
||||
hint.classList.toggle('waiting', !ready);
|
||||
}
|
||||
|
||||
@@ -90,7 +90,11 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
fil. Le détail chiffré (Gio, température) vit sous chaque jauge, et le
|
||||
bloc entier se replie d'un clic. -->
|
||||
<div id="side-perf">
|
||||
<div class="perf-head"><span>Performance</span></div>
|
||||
<!-- « Libérer la VRAM » est posé LÀ, sur les jauges : c'est en les regardant
|
||||
qu'on décide de rendre la carte à une autre application. Le libellé
|
||||
bascule en « Recharger le modèle » dès que le moteur est arrêté — voir
|
||||
paintVramBtn (js/05-status.js). -->
|
||||
<div class="perf-head"><span>Performance</span><button id="vram-btn" class="perf-act" onclick="vramBtnClick()" title="décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine">Libérer la VRAM</button></div>
|
||||
<div class="perf-body"><div id="vram">…</div><div id="ram-details"><div id="ram"></div></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -450,15 +454,18 @@ document.documentElement.setAttribute('data-side',localStorage.getItem('loki-sid
|
||||
<pre id="lc-log" hidden></pre>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Ces trois boutons pilotent le service qui fait tourner llama-server :
|
||||
leur place est ici, avec le moteur, et pas dans une section « Service »
|
||||
à part qui ne contenait qu'eux. -->
|
||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).</span></span></div>
|
||||
<!-- Ces boutons pilotent le service qui fait tourner llama-server : leur
|
||||
place est ici, avec le moteur, et pas dans une section « Service » à
|
||||
part qui ne contenait qu'eux. Le dernier, « libérer la VRAM », fait
|
||||
plus qu'arrêter : il éteint aussi la dictée et annonce la mémoire
|
||||
vidéo rendue (c'est le même geste que le bouton du moniteur). -->
|
||||
<div class="subhead">Service du moteur<span class="help" tabindex="0" onclick="event.preventDefault();event.stopPropagation();this.classList.toggle('open')">?<span class="tip">Démarre, redémarre ou arrête <b>llama-server</b> — le processus qui charge le modèle et répond. Un redémarrage recharge le modèle du preset actif (quelques secondes à quelques minutes selon sa taille).<br><b>Libérer la VRAM</b> arrête le moteur <i>et</i> la dictée, puis annonce la mémoire vidéo rendue : c'est ce qu'il faut quand une autre application réclame la carte. Le même bouton vit sur les jauges, en pied de barre latérale.</span></span></div>
|
||||
<div class="slist"><div class="rowbody">
|
||||
<div class="row" style="margin:0">
|
||||
<button onclick="act('start')">démarrer</button>
|
||||
<button onclick="act('restart')">redémarrer</button>
|
||||
<button onclick="act('stop')">arrêter</button>
|
||||
<button onclick="vramUnload(false)" title="décharge le modèle et rend la mémoire vidéo à la machine">libérer la VRAM</button>
|
||||
</div>
|
||||
</div></div>
|
||||
</section>
|
||||
|
||||
@@ -18,6 +18,11 @@ async function loadStatus(){
|
||||
el.className='statuspill '+cls;
|
||||
el.innerHTML='<span class="dot"></span>'+txt;
|
||||
MODEL_READY = !!(s.active && s.health);
|
||||
// Le bouton « Libérer la VRAM » suit l'état RÉEL du moteur (et pas un drapeau
|
||||
// local) : un arrêt venu d'un autre onglet, du terminal ou d'un job doit se
|
||||
// lire ici aussi, sinon le bouton propose de libérer une carte déjà libre.
|
||||
ENGINE_ACTIVE = !!s.active;
|
||||
paintVramBtn();
|
||||
// Le bouton d'envoi suit l'état du moteur : inutile de pouvoir envoyer un
|
||||
// message à un modèle qui n'est pas encore chargé (voir syncSendBtn).
|
||||
STATUS_SEEN = true;
|
||||
@@ -182,6 +187,71 @@ async function loadVram(){
|
||||
g.name+' · '+vram));
|
||||
});
|
||||
}
|
||||
// ─── Libérer la mémoire vidéo ───────────────────────────────────────────────
|
||||
// Tant que le moteur tourne, le modèle occupe la VRAM : une autre application
|
||||
// qui réclame la carte (un jeu, un encodage, un autre serveur d'inférence) ne
|
||||
// trouve plus rien à prendre. Ce bouton l'en sort, et le MÊME bouton la
|
||||
// reprend ensuite — le geste existait déjà (« arrêter » le service, au fond des
|
||||
// réglages), mais son nom ne disait pas qu'il rendait la carte.
|
||||
//
|
||||
// ENGINE_ACTIVE vient de /api/status (relu toutes les 5 s), jamais d'un drapeau
|
||||
// posé au clic : sinon un second onglet, ou un arrêt venu du terminal,
|
||||
// afficherait un bouton qui ment.
|
||||
let ENGINE_ACTIVE = true, VRAM_BUSY = false;
|
||||
function paintVramBtn(){
|
||||
const b = document.getElementById('vram-btn');
|
||||
if(!b || VRAM_BUSY) return; // pendant l'opération, le libellé appartient à vramBusy
|
||||
b.disabled = false;
|
||||
b.classList.toggle('reload', !ENGINE_ACTIVE);
|
||||
b.textContent = ENGINE_ACTIVE ? 'Libérer la VRAM' : 'Recharger le modèle';
|
||||
b.title = ENGINE_ACTIVE
|
||||
? 'décharge le modèle et arrête le moteur — la mémoire vidéo repart à la machine'
|
||||
: 'relance le moteur : le modèle revient en mémoire vidéo';
|
||||
}
|
||||
function vramBtnClick(){ return ENGINE_ACTIVE ? vramUnload(false) : vramReload(); }
|
||||
function vramBusy(on, label){
|
||||
VRAM_BUSY = on;
|
||||
const b = document.getElementById('vram-btn');
|
||||
if(b){ b.disabled = on; if(on && label) b.textContent = label; }
|
||||
if(!on) paintVramBtn();
|
||||
}
|
||||
// force=true : on a déjà demandé confirmation ET accepté de couper une réponse
|
||||
// en cours d'écriture (le serveur refuse sans ça, voir handleVramUnload).
|
||||
async function vramUnload(force){
|
||||
if(!force){
|
||||
const ok = await askConfirm(
|
||||
'Décharger le modèle ? Le moteur s\'arrête et rend la mémoire vidéo — le chat reste indisponible jusqu\'au rechargement.',
|
||||
{title:'Libérer la VRAM', okText:'Libérer'});
|
||||
if(!ok) return;
|
||||
}
|
||||
vramBusy(true, 'libération…');
|
||||
let r = null;
|
||||
try{ r = await jpost('/api/vram/unload', {force: !!force}); }catch(_){}
|
||||
vramBusy(false);
|
||||
if(r && r.generating){
|
||||
const ok = await askConfirm('Une réponse est en cours d\'écriture : décharger maintenant l\'interrompt. Continuer ?',
|
||||
{title:'Génération en cours', okText:'Décharger quand même'});
|
||||
if(ok) return vramUnload(true);
|
||||
return;
|
||||
}
|
||||
// La mesure d'abord : la dictée peut avoir rendu de la VRAM alors même que le
|
||||
// moteur était déjà arrêté — annoncer « déjà arrêté » nierait ce qui vient
|
||||
// d'être libéré.
|
||||
if(!r || !r.ok) toast('échec : ' + ((r && r.error) || 'réseau'));
|
||||
else if(r.gpu && r.freed_mb > 0) toast('VRAM libérée : ' + (r.freed_mb/1024).toFixed(1) + ' Gio');
|
||||
else if(!r.was_active) toast('le moteur était déjà arrêté');
|
||||
else toast('modèle déchargé');
|
||||
loadAll();
|
||||
}
|
||||
async function vramReload(){
|
||||
vramBusy(true, 'démarrage…');
|
||||
let r = null;
|
||||
try{ r = await jpost('/api/vram/reload', {}); }catch(_){}
|
||||
vramBusy(false);
|
||||
if(!r || !r.ok){ toast('échec : ' + ((r && r.error) || 'réseau')); return; }
|
||||
toast('modèle en cours de chargement…');
|
||||
loadAll();
|
||||
}
|
||||
async function loadRam(){
|
||||
const m=await jget('/api/ram');
|
||||
const box=document.getElementById('ram-details');
|
||||
|
||||
@@ -277,10 +277,16 @@ function syncSendBtn(){
|
||||
// répond pas du tout) — dans le doute on laisse la main.
|
||||
const ready = !STATUS_SEEN || MODEL_READY;
|
||||
sb.disabled = !ready;
|
||||
sb.title = ready ? '' : 'le modèle n\'est pas encore chargé';
|
||||
// Moteur ARRÊTÉ (VRAM libérée à la demande, ou service coupé) : dire « le
|
||||
// modèle charge » serait faux, et on attendrait un chargement qui ne viendra
|
||||
// jamais. On nomme alors le geste qui remet le modèle en mémoire.
|
||||
const unloaded = !ready && !ENGINE_ACTIVE;
|
||||
sb.title = ready ? '' : (unloaded ? 'le modèle est déchargé — recharge-le pour écrire'
|
||||
: 'le modèle n\'est pas encore chargé');
|
||||
const hint=document.getElementById('sendhint');
|
||||
if(hint){
|
||||
hint.textContent = ready ? 'Entrée pour envoyer · Maj+Entrée = nouvelle ligne'
|
||||
: unloaded ? 'Modèle déchargé — « Recharger le modèle » dans le moniteur, en bas de la barre latérale.'
|
||||
: 'Le modèle charge — envoi possible dès qu\'il est prêt.';
|
||||
hint.classList.toggle('waiting', !ready);
|
||||
}
|
||||
|
||||
@@ -1689,6 +1689,16 @@ html[data-files="1"] #files-btn{color:var(--accent)}
|
||||
/* --- Moniteur machine (pied de barre latérale) ----------------------- */
|
||||
.perf-head{display:flex;align-items:center;gap:8px;margin-bottom:11px;
|
||||
font-size:14px;font-weight:600;color:var(--text);letter-spacing:-.01em}
|
||||
/* Bouton du moniteur (« Libérer la VRAM » / « Recharger le modèle ») : poussé
|
||||
à droite du titre, discret tant qu'on ne le cherche pas. En mode rechargement
|
||||
il prend la couleur d'accent — le moteur est arrêté, c'est l'action qui
|
||||
compte à ce moment-là. */
|
||||
.perf-act{margin-left:auto;flex:none;padding:3px 8px;font-size:11px;line-height:1.4;
|
||||
color:var(--dim);border:1px solid var(--border);border-radius:4px;cursor:pointer;
|
||||
white-space:nowrap}
|
||||
.perf-act:hover:not(:disabled){color:var(--text)}
|
||||
.perf-act:disabled{opacity:.55;cursor:progress}
|
||||
.perf-act.reload{color:var(--accent);border-color:color-mix(in srgb,var(--accent) 45%,transparent)}
|
||||
.perf-row{margin:0 0 14px}
|
||||
.perf-l{display:flex;align-items:baseline;gap:8px;font-size:12.5px;color:var(--text);margin-bottom:6px}
|
||||
.perf-l b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -170,29 +169,16 @@ func handleServiceLog(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, 200, map[string]any{"log": serviceLogTail(n)})
|
||||
}
|
||||
|
||||
// handleVram expose l'état des cartes à l'interface (jauges du moniteur).
|
||||
// La lecture nvidia-smi elle-même vit dans web_vram.go, avec le déchargement
|
||||
// de la VRAM qui s'en sert pour chiffrer la mémoire rendue.
|
||||
func handleVram(w http.ResponseWriter, r *http.Request) {
|
||||
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
||||
"--format=csv,noheader,nounits")).Output()
|
||||
gpus := []map[string]any{}
|
||||
if err == nil {
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) != 5 {
|
||||
continue
|
||||
}
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
used, _ := strconv.Atoi(parts[1])
|
||||
total, _ := strconv.Atoi(parts[2])
|
||||
util, _ := strconv.Atoi(parts[3])
|
||||
temp, _ := strconv.Atoi(parts[4])
|
||||
for _, g := range gpuStats() {
|
||||
gpus = append(gpus, map[string]any{
|
||||
"name": parts[0], "used": used, "total": total, "util": util, "temp": temp,
|
||||
"name": g.Name, "used": g.Used, "total": g.Total, "util": g.Util, "temp": g.Temp,
|
||||
})
|
||||
}
|
||||
}
|
||||
sendJSON(w, 200, gpus)
|
||||
}
|
||||
|
||||
|
||||
@@ -175,6 +175,10 @@ func newWebMux() *http.ServeMux {
|
||||
api("/api/dictate/state", handleDictateState) // serveur allumé, modèle chargé, dernière erreur
|
||||
api("/api/service/log", handleServiceLog) // journal du service pour diagnostiquer un modèle qui ne charge pas
|
||||
api("/api/vram", handleVram)
|
||||
// Rendre la carte à la machine : le modèle occupe la VRAM tant que le moteur
|
||||
// tourne, et une autre application qui en a besoin ne trouve rien.
|
||||
api("/api/vram/unload", handleVramUnload) // arrête moteur + dictée, rapporte la VRAM rendue
|
||||
api("/api/vram/reload", handleVramReload) // relance le moteur (le modèle se recharge)
|
||||
api("/api/ram", handleRam)
|
||||
api("/api/config", handleConfigEnv)
|
||||
api("/api/paths", handlePaths)
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
// web_vram.go — rendre la mémoire vidéo à la machine, à la demande.
|
||||
//
|
||||
// Tant que le moteur tourne, le modèle occupe la VRAM : une autre application
|
||||
// (jeu, encodage, entraînement, un second serveur d'inférence…) qui réclame la
|
||||
// carte ne trouve plus rien à prendre. Le geste existait déjà — arrêter le
|
||||
// service — mais il vivait dans les réglages sous le nom « arrêter », qui ne
|
||||
// dit pas qu'il libère la carte, et il laissait tourner le serveur de dictée
|
||||
// qui, lui aussi, garde de la VRAM quand il est allumé sur GPU.
|
||||
//
|
||||
// POST /api/vram/unload → arrête le moteur ET la dictée, puis rapporte la
|
||||
// mémoire réellement rendue (before/after/freed)
|
||||
// POST /api/vram/reload → relance le moteur (le modèle se recharge)
|
||||
package loki
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// gpuStat : une carte vue par nvidia-smi. Mémoire en Mo (unité de nvidia-smi,
|
||||
// et celle que /api/vram expose déjà à l'interface).
|
||||
type gpuStat struct {
|
||||
Name string
|
||||
Used int
|
||||
Total int
|
||||
Util int
|
||||
Temp int
|
||||
}
|
||||
|
||||
// gpuStats interroge nvidia-smi. Renvoie nil quand il est absent (machine sans
|
||||
// GPU NVIDIA, Mac, CPU seul) : l'absence de mesure n'est pas une erreur, elle
|
||||
// prive juste le déchargement de son bilan chiffré.
|
||||
func gpuStats() []gpuStat {
|
||||
out, err := hideCmd(exec.Command("nvidia-smi",
|
||||
"--query-gpu=name,memory.used,memory.total,utilization.gpu,temperature.gpu",
|
||||
"--format=csv,noheader,nounits")).Output()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var gpus []gpuStat
|
||||
for _, line := range strings.Split(strings.TrimSpace(string(out)), "\n") {
|
||||
parts := strings.Split(line, ",")
|
||||
if len(parts) != 5 {
|
||||
continue
|
||||
}
|
||||
for i := range parts {
|
||||
parts[i] = strings.TrimSpace(parts[i])
|
||||
}
|
||||
g := gpuStat{Name: parts[0]}
|
||||
g.Used, _ = strconv.Atoi(parts[1])
|
||||
g.Total, _ = strconv.Atoi(parts[2])
|
||||
g.Util, _ = strconv.Atoi(parts[3])
|
||||
g.Temp, _ = strconv.Atoi(parts[4])
|
||||
gpus = append(gpus, g)
|
||||
}
|
||||
return gpus
|
||||
}
|
||||
|
||||
// gpuUsedMB : VRAM occupée, toutes cartes confondues.
|
||||
func gpuUsedMB(gpus []gpuStat) int {
|
||||
used := 0
|
||||
for _, g := range gpus {
|
||||
used += g.Used
|
||||
}
|
||||
return used
|
||||
}
|
||||
|
||||
// gpuUsedSettled attend que la VRAM cesse de baisser avant de la relire : le
|
||||
// pilote ne rend pas la mémoire à l'instant où le processus meurt, et une
|
||||
// lecture immédiate rapporterait « 0 Mo libérés » juste après un déchargement
|
||||
// qui a pourtant marché. Au plus ~3 s, et rien du tout sans GPU.
|
||||
func gpuUsedSettled() int {
|
||||
gpus := gpuStats()
|
||||
if gpus == nil {
|
||||
return 0
|
||||
}
|
||||
last := gpuUsedMB(gpus)
|
||||
for i := 0; i < 6; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
cur := gpuUsedMB(gpuStats())
|
||||
if cur >= last {
|
||||
return cur
|
||||
}
|
||||
last = cur
|
||||
}
|
||||
return last
|
||||
}
|
||||
|
||||
// ansiRe / plainErr : les messages de preflightEngine sont écrits pour le
|
||||
// terminal (bold() y glisse des séquences ANSI). Envoyés tels quels à
|
||||
// l'interface, ils y affichent des « [1m » au milieu de la phrase.
|
||||
var ansiRe = regexp.MustCompile("\x1b\\[[0-9;]*m")
|
||||
|
||||
func plainErr(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
return ansiRe.ReplaceAllString(err.Error(), "")
|
||||
}
|
||||
|
||||
// handleVramUnload arrête le moteur (et la dictée) pour rendre la mémoire
|
||||
// vidéo. Refuse pendant une génération — la couper perdrait la réponse en
|
||||
// cours — sauf si l'appelant insiste avec {force:true}.
|
||||
func handleVramUnload(w http.ResponseWriter, r *http.Request) {
|
||||
var req struct {
|
||||
Force bool `json:"force"`
|
||||
}
|
||||
_ = json.NewDecoder(r.Body).Decode(&req)
|
||||
if !req.Force && conv.isGenerating() {
|
||||
sendJSON(w, 409, map[string]any{"ok": false, "generating": true,
|
||||
"error": "une réponse est en cours d'écriture — décharger maintenant l'interromprait"})
|
||||
return
|
||||
}
|
||||
gpus := gpuStats()
|
||||
before := gpuUsedMB(gpus)
|
||||
wasActive := serviceIsActive()
|
||||
|
||||
var errs []string
|
||||
if wasActive {
|
||||
if err := serviceAction("stop"); err != nil {
|
||||
errs = append(errs, "arrêt du moteur : "+plainErr(err))
|
||||
}
|
||||
}
|
||||
// La dictée tourne dans un processus séparé, allumé à la demande et éteint
|
||||
// après dix minutes d'inactivité : sur GPU, elle occupe la carte pendant tout
|
||||
// ce temps. Libérer la VRAM sans elle ne libérerait pas tout.
|
||||
whisperShutdown()
|
||||
|
||||
after := before
|
||||
if gpus != nil {
|
||||
after = gpuUsedSettled()
|
||||
}
|
||||
freed := before - after
|
||||
if freed < 0 {
|
||||
freed = 0
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{
|
||||
"ok": len(errs) == 0,
|
||||
"error": strings.Join(errs, " ; "),
|
||||
"was_active": wasActive,
|
||||
"active": serviceIsActive(),
|
||||
"gpu": gpus != nil,
|
||||
"before_mb": before,
|
||||
"used_mb": after,
|
||||
"freed_mb": freed,
|
||||
})
|
||||
}
|
||||
|
||||
// handleVramReload relance le moteur après un déchargement. Le préflight évite
|
||||
// de démarrer un service condamné à mourir en boucle (BIN ou MODEL absents) :
|
||||
// mieux vaut la vraie raison tout de suite qu'un « chargement… » sans fin.
|
||||
func handleVramReload(w http.ResponseWriter, r *http.Request) {
|
||||
if serviceIsActive() {
|
||||
sendJSON(w, 200, map[string]any{"ok": true, "already": true})
|
||||
return
|
||||
}
|
||||
if err := preflightEngine(); err != nil {
|
||||
sendJSON(w, 200, map[string]any{"ok": false, "error": plainErr(err)})
|
||||
return
|
||||
}
|
||||
if err := serviceAction("start"); err != nil {
|
||||
sendJSON(w, 200, map[string]any{"ok": false, "error": plainErr(err)})
|
||||
return
|
||||
}
|
||||
sendJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
Reference in new issue
Block a user