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:
+18
-1
@@ -5,6 +5,7 @@ import path from 'node:path';
|
||||
import readline from 'node:readline';
|
||||
import * as diar from './diar.js';
|
||||
import * as llm from './llm.js';
|
||||
import * as gate from './gate.js';
|
||||
const pexec = promisify(execFile);
|
||||
|
||||
const WHISPER = '/opt/whisper.cpp/build/bin/whisper-cli';
|
||||
@@ -145,6 +146,16 @@ function startTd() {
|
||||
tdProc.on('exit', cleanup); tdProc.on('error', cleanup);
|
||||
return tdReady;
|
||||
}
|
||||
// Beendet den warmen CT2-Daemon und wartet auf sein Ende. Er bleibt sonst per
|
||||
// Design für immer resident und hält ~0,9 GB — genug, um den rkllm-Load danach
|
||||
// OOM-tötbar zu machen (2026-08-07, #101). Nächster Kurz-Clip startet ihn neu;
|
||||
// der einzige Preis ist der erneute Modell-Load. Siehe gate.freeRam.
|
||||
export function stopTranscribeDaemon() {
|
||||
const p = tdProc;
|
||||
tdProc = null; tdReady = null; tdRl = null;
|
||||
return gate.killAndWait(p);
|
||||
}
|
||||
|
||||
// One request at a time (single warm process), chained like spk.embed.
|
||||
function runDaemonTranscribe(wavPath) {
|
||||
const run = tdChain.then(async () => {
|
||||
@@ -400,7 +411,10 @@ export async function transcribeWav(wavFile, numSpeakers, opts = {}) {
|
||||
}
|
||||
console.log(`cpu-daemon-transcribe: ${words.length} words, RTF ${r.rtf} (${r.transcribe_s}s / ${r.duration}s audio, warm, rkllm kept resident)`);
|
||||
} else if (TRANSCRIBER === 'npu') {
|
||||
await llm.stopServer();
|
||||
// Nicht nur rkllm freigeben: der warme CT2-Daemon und die Embedder halten
|
||||
// zusammen ~2,7 GB und der turbo-rknn-Encoder + CT2-Decoder brauchen den
|
||||
// Platz genauso wie rkllm (siehe gate.freeRam).
|
||||
await gate.freeRam('npu-whisper');
|
||||
await setA76Max(A76_CAP_NPU);
|
||||
// large-v3-turbo, encoder on NPU. CLI already emits word-level timestamps.
|
||||
const r = await runNpuTranscribe(wavPath);
|
||||
@@ -426,6 +440,9 @@ export async function transcribeWav(wavFile, numSpeakers, opts = {}) {
|
||||
}
|
||||
}
|
||||
// Acoustic diarization under the A76 clock cap (both paths). Non-fatal.
|
||||
// Zweiter Haltepunkt: sherpa nagelt alle 4 A76-Kerne fest, das darf nicht
|
||||
// neben einer laufenden Aufnahme passieren (gate.js).
|
||||
await gate.waitIdle('diarize');
|
||||
await setA76Max(A76_CAP);
|
||||
try {
|
||||
const segs = await diar.diarize(wavPath, numSpeakers, { markers: opts.markers || [], voiceprints: opts.voiceprints || [] });
|
||||
|
||||
Reference in New Issue
Block a user