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.
88 lines
4.2 KiB
JavaScript
88 lines
4.2 KiB
JavaScript
// 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;
|
|
}
|