fix(ram): nur ein grosses Modell resident (Analyse starb im OOM-Killer)
rkllm_server braucht 6,5 GB von 7,74 GB. Blieb daneben ein anderer Dauerhelfer resident, wurde rkllm beim Laden OOM-getoetet -- bei #101 in allen 5 Startversuchen, weil das Retry nichts freigab. Ausloeser war der warme CT2-Whisper-Daemon, den die 2-Sekunden-Aufnahme davor gestartet hatte. Zweiter Fall: rkllm blieb nach der Analyse fuer immer resident, und der Suchindex startete e5-small daneben. gate.js bekommt eine Freigabe-Registry (registerReleaser/freeRam/ killAndWait, verdrahtet in server.js wie setProbe). Vor jedem rkllm-Versuch und vor dem NPU-Whisper-Lauf werden die anderen Helfer beendet und ihr Prozessende abgewartet; analyzeRecording gibt rkllm vor dem Suchindex frei. Alle Helfer starten faul neu. Verifiziert mit der Abfolge, die scheiterte: 12-s-Clip (CT2-Daemon resident) -> Analyse #101 laeuft durch (221 s, done), rkllm lud in 22 s, kein OOM, danach 5,3 GB frei statt 100 MB.
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
// Sperre für schwere Hintergrundarbeit, solange eine Aufnahme läuft.
|
||||
//
|
||||
// Warum: whisper, die sherpa-Diarisierung und rkllm dürfen nicht neben einer
|
||||
// laufenden Aufnahme arbeiten. Auf diesem lüfterlosen 8-GB-Board hält rkllm
|
||||
// allein ~6,7 GB (RssShmem 4,79 GB + RssAnon 1,93 GB von 7,74 GB MemTotal), es
|
||||
// bleibt also ~1 GB Rest. Am 2026-08-06 lief die Analyse von #96 gleichzeitig
|
||||
// mit der Transkription von #97 an — der globale OOM-Killer erschlug
|
||||
// rkllm_server, systemd nahm den ganzen Dienst mit ('oom-kill'), und die GUI
|
||||
// zeigte "failed to fetch". Dazu treibt die Zusatzlast die A76-Kerne auf ~80 °C,
|
||||
// während das Mikrofon offen ist, und kostet Audioqualität.
|
||||
//
|
||||
// Der Recorder ist die einzige Wahrheit über den Aufnahmezustand; server.js
|
||||
// hängt die Abfrage beim Start ein. Eigenes Modul, weil recorder.js bereits
|
||||
// llm.js importiert — llm.js darf recorder.js deshalb nicht zurückimportieren
|
||||
// (Zyklus).
|
||||
let probe = () => false;
|
||||
|
||||
// Beim Start aus server.js: gate.setProbe(() => rec.isRecording()).
|
||||
export function setProbe(fn) { probe = typeof fn === 'function' ? fn : (() => false); }
|
||||
|
||||
export function busy() {
|
||||
try { return !!probe(); } catch { return false; }
|
||||
}
|
||||
|
||||
// Parken, bis der Recorder wieder still ist. Ein- und Austritt werden geloggt,
|
||||
// damit eine lange Pause im Journal als Pause erkennbar ist und nicht wie ein
|
||||
// hängender Job aussieht.
|
||||
export async function waitIdle(label) {
|
||||
if (!busy()) return 0;
|
||||
const t0 = Date.now();
|
||||
console.log(`gate: ${label} pausiert — Aufnahme läuft`);
|
||||
while (busy()) await new Promise(r => setTimeout(r, 2000));
|
||||
const s = Math.round((Date.now() - t0) / 1000);
|
||||
console.log(`gate: ${label} fortgesetzt nach ${s} s Pause`);
|
||||
return s;
|
||||
}
|
||||
|
||||
// --- Speicher freiräumen, bevor ein großes Modell lädt -----------------------
|
||||
//
|
||||
// Auf diesem 7,74-GB-Board darf nur EIN großes Modell resident sein. rkllm_server
|
||||
// braucht allein ~6,5 GB (davon 4,79 GB unevictable NPU-Shmem), und daneben halten
|
||||
// die faul gestarteten Dauerhelfer in der Spitze (gemessen, VmHWM):
|
||||
// e5-small (embed_text.py) 1,15 GB
|
||||
// CT2-Whisper (transcribe_daemon) 0,90 GB
|
||||
// TitaNet (spk_embed) 0,70 GB
|
||||
// Am 2026-08-07 kostete das die Analyse von #101: die 2-Sekunden-Aufnahme #100
|
||||
// davor hatte den warmen CT2-Daemon gestartet (Kurz-Clip-Pfad, bleibt per Design
|
||||
// resident), danach wurde rkllm in ALLEN 5 Startversuchen OOM-getötet → GUI
|
||||
// "KI-Analyse fehlgeschlagen". zram hilft hier NICHT: der komprimierte Swap liegt
|
||||
// selbst im RAM, ausgelagerte 0,9 GB kosten immer noch ~0,3-0,45 GB physisch.
|
||||
//
|
||||
// Jeder Helfer registriert deshalb, wie er sich beenden lässt; wer ein großes
|
||||
// Modell laden will, ruft vorher freeRam(<eigener Name>). Alle Helfer starten
|
||||
// beim nächsten Aufruf faul neu — es geht nur Ladezeit verloren, kein Zustand.
|
||||
const releasers = new Map();
|
||||
export function registerReleaser(name, stop) { releasers.set(name, stop); }
|
||||
|
||||
// Beendet einen Kindprozess und wartet, bis er wirklich weg ist (sonst hat der
|
||||
// Kernel die Seiten beim Laden des nächsten Modells noch nicht zurück).
|
||||
// Rückgabe: true, wenn etwas beendet wurde.
|
||||
export function killAndWait(proc, timeoutMs = 5000) {
|
||||
if (!proc || proc.exitCode !== null || proc.signalCode) return Promise.resolve(false);
|
||||
return new Promise(resolve => {
|
||||
let settled = false;
|
||||
const done = () => { if (!settled) { settled = true; resolve(true); } };
|
||||
proc.once('exit', done);
|
||||
try { proc.kill('SIGTERM'); } catch { done(); return; }
|
||||
setTimeout(() => { try { proc.kill('SIGKILL'); } catch {} setTimeout(done, 500); }, timeoutMs);
|
||||
});
|
||||
}
|
||||
|
||||
// Alle registrierten Helfer außer `keep` beenden. Wird vor jedem Ladeversuch
|
||||
// eines großen Modells aufgerufen — auch vor einem Retry, denn ein Retry ohne
|
||||
// freien Speicher wird genauso OOM-getötet wie der erste Versuch.
|
||||
export async function freeRam(keep) {
|
||||
const freed = [];
|
||||
for (const [name, stop] of releasers) {
|
||||
if (name === keep) continue;
|
||||
try { if (await stop()) freed.push(name); }
|
||||
catch (e) { console.warn(`ram: ${name} nicht freigegeben — ${e.message}`); }
|
||||
}
|
||||
if (freed.length) {
|
||||
console.log(`ram: für ${keep} freigegeben — ${freed.join(', ')}`);
|
||||
await new Promise(r => setTimeout(r, 1500)); // Kernel-Reclaim abwarten
|
||||
}
|
||||
return freed;
|
||||
}
|
||||
Reference in New Issue
Block a user