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
+384 -31

No files matched your search

+6
View File
@@ -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
+99 -6
View File
@@ -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);
}
+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');
+7 -1
View File
@@ -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);
}
+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;
+5 -19
View File
@@ -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)
}
+4
View File
@@ -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)
+171
View File
@@ -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})
}