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:
+23
-1
@@ -4,6 +4,7 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import readline from 'node:readline';
|
||||
import * as gate from './gate.js';
|
||||
|
||||
const SERVER = '/opt/callrec/rkllm_server';
|
||||
const MODEL = process.env.RKLLM_MODEL || '/opt/rkllm-models/qwen2.5-3b.rkllm';
|
||||
@@ -30,6 +31,14 @@ function spawnServer() {
|
||||
let done = false;
|
||||
proc = spawn('taskset', ['-c', '4-7', SERVER, MODEL], { stdio: ['pipe', 'pipe', 'pipe'] });
|
||||
proc.stderr.on('data', d => process.stderr.write('[rkllm] ' + d));
|
||||
// Ein write() in die stdin eines toten Kindes loest 'error' (EPIPE) auf
|
||||
// diesem Socket aus. Ohne Listener behandelt node das als unhandled
|
||||
// 'error' event und beendet den GANZEN Dienst — genau so starb callrec am
|
||||
// 2026-08-06 nach #98. Der Fehler gehoert an die offene Anfrage, nicht an
|
||||
// den Prozess.
|
||||
proc.stdin.on('error', e => {
|
||||
if (pending) { const p = pending; pending = null; p.reject(new Error('rkllm stdin: ' + e.message)); }
|
||||
});
|
||||
ready = new Promise((resolve, reject) => {
|
||||
settleReject = reject;
|
||||
// 180s: Qwen3-4B (5.3GB) needs ~96s to load on the capped clock — the old
|
||||
@@ -61,6 +70,11 @@ async function startServer() {
|
||||
// loads succeed. Up to 5 attempts, ~4s apart.
|
||||
let lastErr;
|
||||
for (let attempt = 1; attempt <= 5; attempt++) {
|
||||
// Vor JEDEM Versuch die anderen großen Helfer beenden. rkllm braucht ~6,5 GB
|
||||
// von 7,74 GB; blieb z. B. der warme CT2-Daemon der vorherigen Kurz-Aufnahme
|
||||
// resident, wird rkllm OOM-getötet — und ein Retry ohne freien Speicher
|
||||
// stirbt genauso (2026-08-07: 5/5 Versuche, "KI-Analyse fehlgeschlagen").
|
||||
await gate.freeRam('rkllm');
|
||||
try { spawnServer(); await ready; return; }
|
||||
catch (e) {
|
||||
lastErr = e;
|
||||
@@ -78,10 +92,18 @@ async function startServer() {
|
||||
// next one (otherwise one bad chunk fails all following chunks instantly).
|
||||
function ask(prompt) {
|
||||
const run = chain.then(async () => {
|
||||
// Haltepunkt zwischen den Chunks: startet mittendrin eine Aufnahme,
|
||||
// pausiert die Analyse hier, statt neben dem offenen Mikrofon zu rechnen.
|
||||
await gate.waitIdle('llm');
|
||||
await startServer();
|
||||
return await new Promise((resolve, reject) => {
|
||||
// Zweites Netz: der Prozess kann zwischen startServer() und hier
|
||||
// gestorben sein (exit-Event noch nicht verarbeitet).
|
||||
if (!proc || !proc.stdin || proc.stdin.destroyed)
|
||||
return reject(new Error('rkllm nicht bereit (Prozess weg)'));
|
||||
pending = { resolve, reject };
|
||||
proc.stdin.write(b64e(prompt) + '\n');
|
||||
try { proc.stdin.write(b64e(prompt) + '\n'); }
|
||||
catch (e) { pending = null; reject(new Error('rkllm stdin: ' + e.message)); }
|
||||
});
|
||||
});
|
||||
chain = run.catch(() => {}); // next request starts from a clean, resolved chain
|
||||
|
||||
Reference in New Issue
Block a user