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:
+36
-4
@@ -12,8 +12,26 @@ import * as diar from './diar.js';
|
||||
import * as spk from './spk.js';
|
||||
import * as search from './search.js';
|
||||
import * as emb from './emb.js';
|
||||
import * as gate from './gate.js';
|
||||
const pexec = promisify(execFile);
|
||||
|
||||
// Der Recorder ist die einzige Wahrheit darueber, ob gerade aufgenommen
|
||||
// wird. gate.js fragt sie hier ab, statt recorder.js zu importieren (das
|
||||
// waere ein Zyklus, weil recorder.js llm.js importiert).
|
||||
gate.setProbe(() => rec.isRecording());
|
||||
|
||||
// Wer sich wie freigeben lässt, wenn ein großes Modell RAM braucht (gate.freeRam).
|
||||
// Hier verdrahtet, damit gate.js keines der Module importieren muss (Zyklen) —
|
||||
// dasselbe Muster wie setProbe oben. Jeder Helfer startet faul neu.
|
||||
gate.registerReleaser('rkllm', async () => {
|
||||
if (!llm.status().running) return false;
|
||||
await llm.stopServer();
|
||||
return true;
|
||||
});
|
||||
gate.registerReleaser('cpu-transcriber', () => rec.stopTranscribeDaemon());
|
||||
gate.registerReleaser('text-embedder', () => emb.stop());
|
||||
gate.registerReleaser('spk-embedder', () => spk.stop());
|
||||
|
||||
// Glossary → { soundalike(lowercased): correctTerm }. Only entries WITH
|
||||
// soundalikes get a deterministic swap; term-only entries are left to the LLM hint.
|
||||
function glossaryMap(glossary) {
|
||||
@@ -254,7 +272,7 @@ async function analyzeRecording(id) {
|
||||
// spk-embed helper OOM-dies next to it ("spk beendet" per cluster, #71 got
|
||||
// 48 raw speakers). llm.analyze() below respawns it (~96s load, noise
|
||||
// against a 15-min analysis).
|
||||
llm.stopServer();
|
||||
await llm.stopServer();
|
||||
await pexec('sudo', ['-n', '/opt/callrec/aime-cpucap.sh', '1608000']).catch(() => {});
|
||||
try {
|
||||
const rec = await db.getRecording(id);
|
||||
@@ -312,6 +330,13 @@ async function analyzeRecording(id) {
|
||||
a.participants = (a.participants || []).map(p => applyGlossaryText(p, gmap));
|
||||
a.todos = (a.todos || []).map(t => applyGlossaryText(t, gmap));
|
||||
await db.updateAnalysis(id, { ...a, status: 'done' });
|
||||
// rkllm ist ab hier fertig und wird freigegeben, BEVOR der Suchindex unten
|
||||
// das e5-Modell (1,15 GB) startet: am 2026-08-07 traf genau diese Reihenfolge
|
||||
// den OOM-Killer, der den residenten rkllm erschlug (13:27:09, dmesg). Vorher
|
||||
// blieb rkllm nach einer Analyse für immer resident — das war der Zustand
|
||||
// "nur noch 100 MB frei", in dem jeder weitere Helferstart russisches
|
||||
// Roulette war. Preis: die nächste Analyse lädt das Modell neu (~96 s).
|
||||
await llm.stopServer();
|
||||
// A speaker can be named WHILE the analysis runs (it takes ~15 min, and the
|
||||
// marker/voiceprint passes themselves rename clusters). The finished text
|
||||
// still carries the label from analysis start ("Sprecher 1"), so patch it
|
||||
@@ -373,6 +398,10 @@ function withNpuLock(label, fn) {
|
||||
const waiting = npuDepth - 1;
|
||||
const run = npuChain.then(async () => {
|
||||
if (waiting > 0) console.log(`npu-queue: ${label} start (${waiting} ahead done)`);
|
||||
// Nie neben einer laufenden Aufnahme: rkllm haelt ~6,7 GB von 7,74 GB,
|
||||
// eine zweite schwere Last traf am 2026-08-06 den globalen OOM-Killer
|
||||
// und nahm den Dienst mit. Siehe gate.js.
|
||||
await gate.waitIdle(label);
|
||||
try { return await fn(); }
|
||||
finally { npuDepth--; }
|
||||
});
|
||||
@@ -411,7 +440,7 @@ async function transcribeAndAnalyze(id, wavFile, numSpeakers) {
|
||||
await db.updateAnalysis(id, { title: null, summary: null, todos: null, status: 'error' }).catch(() => {});
|
||||
return; // no transcript -> skip LLM analysis
|
||||
}
|
||||
analyzeRecording(id);
|
||||
await analyzeRecording(id);
|
||||
}
|
||||
|
||||
app.post('/api/record/stop', wrap(async (req, res) => {
|
||||
@@ -593,7 +622,7 @@ app.post('/api/search/answer', wrap(async (req, res) => {
|
||||
try {
|
||||
res.json(await withNpuLock('search-answer', () => search.answer(q, hits)));
|
||||
} finally {
|
||||
llm.stopServer();
|
||||
await llm.stopServer();
|
||||
}
|
||||
}));
|
||||
app.get('/api/search/status', wrap(async (req, res) => {
|
||||
@@ -666,7 +695,10 @@ async function rediarize(id, numSpeakers) {
|
||||
// Free the resident rkllm first — diar's consolidation embeds every cluster
|
||||
// via TitaNet, which OOM-dies next to a resident 5.9GB LLM (see
|
||||
// analyzeRecording; on #71 that silently yielded 48 raw speakers).
|
||||
llm.stopServer();
|
||||
// Abwarten: ein nicht abgewartetes stopServer() laesst proc/ready gesetzt,
|
||||
// bis das exit-Event eintrifft — der naechste ask() schreibt dann in die
|
||||
// tote Pipe (EPIPE, riss am 2026-08-06 den Dienst mit).
|
||||
await llm.stopServer();
|
||||
// Diarization runs on the A76 cores; the board is fanless, so cap the clock
|
||||
// for the whole run (transcribeWav does this for the record path — rediarize
|
||||
// is the other entry point and must do it too, or a long re-diar overheats).
|
||||
|
||||
Reference in New Issue
Block a user