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:
MichaelandClaude Opus 5 committed 2026-08-26 20:18:09 +00:00
1 parent 895aa67366
commit 009b585e75
9 files changed
+388 -35

No files matched your search

+12 -5
View File
@@ -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>
+70
View File
@@ -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');
+8 -2
View File
@@ -277,11 +277,17 @@ 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'
: 'Le modèle charge — envoi possible dès qu\'il est prêt.';
: 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);
}
}
+10
View File
@@ -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;