feat: изоляция памяти ASR-стека (t-261004160531-780666) — ffmpeg и faster-whisper в transient-scope systemd-run --scope c MemoryMax=3G (fallback без systemd), killpg на таймауте; MemoryMax=1G/MemoryHigh=768M на юнит поллера (set-property персистентно + шаблон scripts/simbiot-harness-tg.service); верифицировано: cgroup simbiot-asr-*.scope, реальный whisper через scope 11.5с, тесты 6/6
This commit is contained in:
parent
47352897ce
commit
ef87007472
2 changed files with 91 additions and 13 deletions
|
|
@ -106,8 +106,58 @@ def _download_voice(file_id: str, file_unique_id: str = ""):
|
||||||
return audio, cache
|
return audio, cache
|
||||||
|
|
||||||
|
|
||||||
|
# ─────────── тяжёлые воркеры ВНЕ cgroup поллера (t-261004160531-780666) ─────
|
||||||
|
# faster-whisper large-v3-turbo (int8) живёт в ~2-3G RSS, ffmpeg тоже кусается:
|
||||||
|
# заспавненные поллером как обычные дети они падали в ЕГО cgroup — systemd
|
||||||
|
# Consumption показывал 3.7G peak у simbiot-harness-tg. Теперь тяжёлое уходит
|
||||||
|
# в собственный transient-scope (systemd-run --scope) со своим MemoryMax;
|
||||||
|
# поллер остаётся лёгким (<100M), его юнит держит MemoryMax=1G.
|
||||||
|
# Нет systemd (локалка/тесты) — прозрачный fallback на прямой запуск.
|
||||||
|
_ASR_SCOPE_MEM = os.environ.get("SIMBIOT_ASR_SCOPE_MEM", "3G")
|
||||||
|
_HAS_SYSTEMD = os.path.isdir("/run/systemd/system") and os.path.exists("/usr/bin/systemd-run")
|
||||||
|
|
||||||
|
|
||||||
|
def _heavy_argv(argv):
|
||||||
|
"""argv тяжёлого воркера → запуск в отдельном systemd-scope с MemoryMax."""
|
||||||
|
if not _HAS_SYSTEMD:
|
||||||
|
return list(argv)
|
||||||
|
import uuid
|
||||||
|
unit = f"simbiot-asr-{uuid.uuid4().hex[:10]}"
|
||||||
|
return ["systemd-run", "--scope", "--quiet", "--collect",
|
||||||
|
f"--unit={unit}", f"-p", f"MemoryMax={_ASR_SCOPE_MEM}",
|
||||||
|
"--"] + list(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _heavy_run(argv, input_bytes=None, timeout=600):
|
||||||
|
"""Тяжёлый subprocess: scope-изоляция памяти + killpg на таймауте.
|
||||||
|
|
||||||
|
systemd-run --scope выполняет команду синхронно, поэтому таймаут
|
||||||
|
работает как раньше; start_new_session + killpg (паттерн
|
||||||
|
isolated_runner, e-261003004801) не оставляют осиротевший воркер,
|
||||||
|
если тик умер раньше него.
|
||||||
|
"""
|
||||||
|
import signal
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
_heavy_argv(argv), stdin=subprocess.PIPE, stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE, start_new_session=True)
|
||||||
|
try:
|
||||||
|
out, err = proc.communicate(input=input_bytes, timeout=timeout)
|
||||||
|
return proc.returncode, out or b"", err or b""
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
try:
|
||||||
|
os.killpg(proc.pid, signal.SIGKILL)
|
||||||
|
except (ProcessLookupError, PermissionError, OSError):
|
||||||
|
pass
|
||||||
|
try: # дождаться смерти группы, чтобы зомби не сидели в scope
|
||||||
|
proc.communicate(timeout=10)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return None, b"", b""
|
||||||
|
|
||||||
|
|
||||||
# ────────────── извлечение аудио из видеоконтейнера ─────────────────────────
|
# ────────────── извлечение аудио из видеоконтейнера ─────────────────────────
|
||||||
|
|
||||||
|
|
||||||
def extract_audio_track(container: bytes):
|
def extract_audio_track(container: bytes):
|
||||||
"""Видеоконтейнер (mp4 и т.п.) → ogg/opus аудиодорожка через ffmpeg.
|
"""Видеоконтейнер (mp4 и т.п.) → ogg/opus аудиодорожка через ffmpeg.
|
||||||
|
|
||||||
|
|
@ -120,16 +170,17 @@ def extract_audio_track(container: bytes):
|
||||||
with tempfile.NamedTemporaryFile(prefix="simbiot-v_", delete=False) as tf:
|
with tempfile.NamedTemporaryFile(prefix="simbiot-v_", delete=False) as tf:
|
||||||
tf.write(container)
|
tf.write(container)
|
||||||
src = tf.name
|
src = tf.name
|
||||||
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса
|
# -vn: только звук; моно 16к — формат, который любят оба ASR-яруса.
|
||||||
proc = subprocess.run(
|
# ffmpeg тяжёлый → в свой scope (память поллера не растёт)
|
||||||
|
rc, out, err = _heavy_run(
|
||||||
["ffmpeg", "-v", "error", "-i", src, "-vn", "-ac", "1",
|
["ffmpeg", "-v", "error", "-i", src, "-vn", "-ac", "1",
|
||||||
"-ar", "16000", "-f", "ogg", "-c:a", "libopus", "-"],
|
"-ar", "16000", "-f", "ogg", "-c:a", "libopus", "-"],
|
||||||
capture_output=True, timeout=180)
|
timeout=180)
|
||||||
if proc.returncode != 0 or not proc.stdout:
|
if rc != 0 or not out:
|
||||||
err = (proc.stderr or b"").decode()[:200]
|
err = err.decode()[:200]
|
||||||
print(f"[voice] ffmpeg: дорожка не извлечена ({err})", flush=True)
|
print(f"[voice] ffmpeg: дорожка не извлечена ({err})", flush=True)
|
||||||
return None
|
return None
|
||||||
return proc.stdout
|
return out
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[voice] ffmpeg error: {e}", flush=True)
|
print(f"[voice] ffmpeg error: {e}", flush=True)
|
||||||
return None
|
return None
|
||||||
|
|
@ -198,17 +249,21 @@ def _asr_faster_whisper(audio: bytes, filename: str):
|
||||||
"audio_b64": base64.b64encode(audio).decode(),
|
"audio_b64": base64.b64encode(audio).decode(),
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
}, ensure_ascii=False)
|
}, ensure_ascii=False)
|
||||||
proc = subprocess.run(
|
# faster-whisper large-v3-turbo ~2-3G RSS: только в свой scope,
|
||||||
[_FW_PYTHON, _FW_WORKER], input=req.encode(),
|
# иначе память ложится в cgroup поллера (3.7G peak, t-261004160531)
|
||||||
capture_output=True, timeout=600)
|
rc, out, err = _heavy_run(
|
||||||
out = proc.stdout.decode() if proc.stdout else ""
|
[_FW_PYTHON, _FW_WORKER], input_bytes=req.encode(), timeout=600)
|
||||||
|
if rc is None:
|
||||||
|
print("[voice] faster-whisper: таймаут (группа убита)", flush=True)
|
||||||
|
return None
|
||||||
|
out = out.decode() if out else ""
|
||||||
try:
|
try:
|
||||||
data = json.loads(out.strip().splitlines()[-1]) if out.strip() else {}
|
data = json.loads(out.strip().splitlines()[-1]) if out.strip() else {}
|
||||||
except (ValueError, IndexError):
|
except (ValueError, IndexError):
|
||||||
data = {"error": (proc.stderr or b"").decode()[:300]}
|
data = {"error": err.decode()[:300]}
|
||||||
if data.get("error") or proc.returncode != 0:
|
if data.get("error") or rc != 0:
|
||||||
print(f"[voice] faster-whisper error: "
|
print(f"[voice] faster-whisper error: "
|
||||||
f"{data.get('error') or proc.returncode}", flush=True)
|
f"{data.get('error') or rc}", flush=True)
|
||||||
return None
|
return None
|
||||||
return (data.get("text") or "").strip()
|
return (data.get("text") or "").strip()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
|
||||||
23
scripts/simbiot-harness-tg.service
Normal file
23
scripts/simbiot-harness-tg.service
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
[Unit]
|
||||||
|
Description=Simbiot Harness - Telegram inbox poller
|
||||||
|
After=network-online.target simbiot-harness.service
|
||||||
|
Wants=network-online.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Environment=SIMBIOT_HOME=__DATA_DIR__
|
||||||
|
Environment=SIMBIOT_RAG_DB=/root/rag/rag.db
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=__INSTALL_DIR__
|
||||||
|
ExecStart=__INSTALL_DIR__/venv/bin/python -m harness.tg_inbox
|
||||||
|
Restart=always
|
||||||
|
RestartSec=5
|
||||||
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
# t-261004160531-780666: поллер лёгкий — тяжёлая транскрипция (faster-whisper
|
||||||
|
# large-v3-turbo ~2-3G, ffmpeg) вынесена в отдельные transient-scope'ы
|
||||||
|
# (systemd-run --scope, см. harness/voice.py _heavy_run) с их MemoryMax.
|
||||||
|
# Жёсткий потолок юнита: 3.7G peak больше невозможен физически.
|
||||||
|
MemoryMax=1G
|
||||||
|
MemoryHigh=768M
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Loading…
Reference in a new issue