Files
msgbot/easyocr_worker.py
T
youpaishidifu a3b0e35581 fix: EasyOCR内存不足时强制CPU模式,避免CUDA加载导致系统冻结
- monitor.py: 内存阈值从2GB提高到3GB,CUDA加载torch需要大量内存
- easyocr_worker.py: 新增物理内存预检(3GB阈值),不足时强制CPU模式
- easyocr_worker.py: _init_reader()再次检查物理内存,双重保障
- 改进错误日志:记录内存详细信息(总量、使用率)
- 解决:内存不足时仍加载CUDA导致电脑冻结的问题
2026-08-04 12:18:54 +08:00

274 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""EasyOCR 子进程 worker。
通过 stdin 读 base64 图片、stdout 写 JSON 结果。GPU crash 只杀此进程,不冻主进程。
协议:
stdin: {"img":"<base64_png_bytes>"}
stdout: {"ok":true,"text":"识别结果"}
stdout: {"ok":false,"error":"错误描述"}
初始化慢(首次 3-10s),初始化完成后常驻处理。
"""
import json
import sys
import base64
import io
import os
import traceback
from datetime import datetime
# ── 崩溃日志文件:写入源码目录根(easyocr_crash.log),统一管理、便于父进程读取 ──
def _get_virtual_memory_info() -> dict:
"""获取虚拟内存(页面文件)状态,用于诊断 WinError 1455。"""
try:
import ctypes
class MEMORYSTATUSEX(ctypes.Structure):
_fields_ = [
("dwLength", ctypes.c_ulong),
("dwMemoryLoad", ctypes.c_ulong),
("ullTotalPhys", ctypes.c_ulonglong),
("ullAvailPhys", ctypes.c_ulonglong),
("ullTotalPageFile", ctypes.c_ulonglong),
("ullAvailPageFile", ctypes.c_ulonglong),
("ullTotalVirtual", ctypes.c_ulonglong),
("ullAvailVirtual", ctypes.c_ulonglong),
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
]
mem = MEMORYSTATUSEX()
mem.dwLength = ctypes.sizeof(MEMORYSTATUSEX)
ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(mem))
return {
"total_phys_gb": round(mem.ullTotalPhys / (1024**3), 1),
"avail_phys_gb": round(mem.ullAvailPhys / (1024**3), 1),
"total_pagefile_gb": round(mem.ullTotalPageFile / (1024**3), 1),
"avail_pagefile_gb": round(mem.ullAvailPageFile / (1024**3), 1),
"commit_pct": round((1 - mem.ullAvailPageFile / max(mem.ullTotalPageFile, 1)) * 100, 1),
}
except Exception:
return {}
def _get_crash_log_path() -> str:
try:
base = os.path.dirname(os.path.abspath(__file__))
except Exception:
base = "."
return os.path.join(base, "easyocr_crash.log")
CRASH_LOG = _get_crash_log_path()
def _write_crash_log(msg: str):
try:
with open(CRASH_LOG, "a", encoding="utf-8") as f:
f.write(msg + "\n")
f.flush()
except Exception:
pass
# ── 顶层异常捕获:确保崩溃原因写入 stderr + 落盘(否则父进程看到空 stderr) ──
def _flush_exit(msg, code=1):
full = f"[easyocr_worker] FATAL: {msg}"
sys.stderr.write(full + "\n")
sys.stderr.flush()
_write_crash_log(f"{datetime.now()} {full}")
sys.exit(code)
# ── 全局未捕获异常钩子:任何漏网的 Python 异常都落盘 ──
def _global_excepthook(exc_type, exc_val, exc_tb):
msg = "".join(traceback.format_exception(exc_type, exc_val, exc_tb))
sys.stderr.write(f"[easyocr_worker] 未捕获异常:\n{msg}")
sys.stderr.flush()
_write_crash_log(f"{datetime.now()} [easyocr_worker] 未捕获异常:\n{msg}")
sys.excepthook = _global_excepthook
# ── faulthandler:捕获原生层崩溃(segfault / CUDA 初始化竞态),Python try/except 抓不到 ──
# 必须在重型 import 之前启用,连 import 阶段的原生崩溃也能 dump 出来。
try:
import faulthandler
_fh = open(CRASH_LOG, "a", encoding="utf-8")
faulthandler.enable(file=_fh, all_threads=True)
except Exception:
pass
try:
# ── 线程/进程限制:避免 MKL/OpenMP 在子进程多线程冲突 ──
os.environ.setdefault("OMP_NUM_THREADS", "1")
os.environ.setdefault("MKL_NUM_THREADS", "1")
# ── 预检:物理内存+虚拟内存是否足够加载 CUDA 库 ──
_vm = _get_virtual_memory_info()
# 检查物理内存:CUDA加载需要足够物理内存,否则会使用页面文件导致冻结
try:
import psutil
_mem = psutil.virtual_memory()
_avail_phys_gb = _mem.available / (1024**3)
if _avail_phys_gb < 3.0:
_msg = (
f"物理内存不足: 剩余{_avail_phys_gb:.1f}GB"
f"(总计{_mem.total / (1024**3):.1f}GB,使用{_mem.percent}%)"
f",CUDA加载需要≥3GB物理内存"
)
_write_crash_log(f"{datetime.now()} [easyocr_worker] 物理内存预检失败: {_msg}")
# 设置环境变量强制CPU模式,而不是直接退出
os.environ["EASYOCR_FORCE_CPU"] = "1"
sys.stderr.write(f"[easyocr_worker] ⚠️ {_msg},降级CPU模式\n")
sys.stderr.flush()
except Exception:
pass # psutil不可用时不阻断
# 检查页面文件:虚拟内存不足时直接退出(无法运行)
if _vm and _vm.get("avail_pagefile_gb", 99) < 2.0:
_msg = (
f"页面文件不足: 剩余{_vm.get('avail_pagefile_gb', '?')}GB"
f"(提交{_vm.get('commit_pct', '?')}%)"
f",CUDA库需要≥2GB连续虚拟地址空间"
)
_write_crash_log(f"{datetime.now()} [easyocr_worker] 页面文件预检失败: {_msg}")
_flush_exit(f"页面文件不足: {_msg}", code=2)
from PIL import Image
import numpy as np
import easyocr
except Exception as e:
_vm = _get_virtual_memory_info()
_vm_hint = ""
if _vm:
_vm_hint = (
f"\n[virtual memory] 物理内存: {_vm.get('avail_phys_gb',0)}/{_vm.get('total_phys_gb',0)} GB"
f" | 页面文件: {_vm.get('avail_pagefile_gb',0)}/{_vm.get('total_pagefile_gb',0)} GB"
f" | 提交使用率: {_vm.get('commit_pct',0)}%"
)
# WinError 1455 = 页面文件太小 → 标记为永久失败,不重试
_is_pagefile_error = "1455" in str(e) or "页面文件" in str(e)
_exit_code = 2 if _is_pagefile_error else 1 # exit(2) = 虚拟内存不足
_write_crash_log(
f"{datetime.now()} [easyocr_worker] import {_exit_code}: "
f"{'页面文件不足' if _is_pagefile_error else 'import失败'}: {e}{_vm_hint}"
)
_flush_exit(f"import 失败{'(页面文件不足)' if _is_pagefile_error else ''}: {e}{_vm_hint}", code=_exit_code)
def _init_reader():
"""初始化 EasyOCR reader(GPU 优先,父进程可强制 CPU)。"""
# 先检查环境变量是否强制CPU
_force_cpu = os.environ.get("EASYOCR_FORCE_CPU") == "1"
# 再次检查物理内存:加载torch+CUDA需要大量内存,内存不足时强制CPU
if not _force_cpu:
try:
import psutil
_mem = psutil.virtual_memory()
_avail_phys_gb = _mem.available / (1024**3)
if _avail_phys_gb < 3.0:
_force_cpu = True
sys.stderr.write(
f"[easyocr_worker] ⚠️ 物理内存不足(剩余{_avail_phys_gb:.1f}GB),"
f"强制CPU模式(避免CUDA加载导致冻结)\n"
)
sys.stderr.flush()
except Exception:
pass # psutil不可用时不阻断
try:
import torch
# 父进程 EASYOCR_FORCE_CPU=1 → 强制 CPU(显存反复 OOM 时降级,防冻结)
use_gpu = (not _force_cpu) and torch.cuda.is_available()
if use_gpu:
# CUDA 健康检查:尝试创建一个小张量,避免驱动/显存问题导致后续崩溃
try:
_test_tensor = torch.zeros(1, device="cuda")
del _test_tensor
torch.cuda.empty_cache()
sys.stderr.write(f"[easyocr_worker] CUDA 健康检查通过 (设备: {torch.cuda.get_device_name(0)})\n")
sys.stderr.flush()
except Exception as cuda_err:
sys.stderr.write(f"[easyocr_worker] CUDA 健康检查失败({cuda_err}),降级 CPU\n")
sys.stderr.flush()
use_gpu = False
except Exception:
use_gpu = False
try:
reader = easyocr.Reader(["ch_sim", "en"], gpu=use_gpu)
mode = "GPU" if use_gpu else "CPU"
sys.stderr.write(f"[easyocr_worker] Reader 就绪({mode}模式)\n")
sys.stderr.flush()
return reader
except Exception as e:
# GPU 不可用 → CPU 兜底
sys.stderr.write(f"[easyocr_worker] GPU 初始化失败({e}),尝试 CPU\n")
sys.stderr.flush()
reader = easyocr.Reader(["ch_sim", "en"], gpu=False)
sys.stderr.write("[easyocr_worker] Reader 就绪(CPU 模式)\n")
sys.stderr.flush()
return reader
def _process_image(img_b64: str, reader) -> str:
"""解码 base64 图片 → EasyOCR 识别 → 返回空格拼接文本。"""
if reader is None:
return ""
raw = base64.b64decode(img_b64)
img = Image.open(io.BytesIO(raw))
# 保持与 monitor.py 一致:RGBA → numpy
if img.mode != "RGBA":
img = img.convert("RGBA")
img_np = np.array(img)
results = reader.readtext(img_np)
return "\n".join([r[1] for r in results]).strip()
def _main():
_write_crash_log(f"{datetime.now()} [easyocr_worker] 启动 (pid={os.getpid()})")
reader = None
try:
reader = _init_reader()
except Exception as e:
sys.stderr.write(f"[easyocr_worker] Reader 初始化失败: {e}\n")
sys.stderr.flush()
_write_crash_log(f"{datetime.now()} [easyocr_worker] Reader 初始化失败: {e}")
# ⭐ 就绪握手:即使 Reader 失败也要发 ready,否则父进程永远等握手
sys.stdout.write(json.dumps({"ready": True}, ensure_ascii=False) + "\n")
sys.stdout.flush()
sys.stderr.write("[easyocr_worker] 就绪,等待 stdin 输入\n")
sys.stderr.flush()
for line in sys.stdin:
line = line.strip()
if not line:
continue
try:
payload = json.loads(line)
img_b64 = payload.get("img", "")
if not img_b64:
raise ValueError("缺少 img 字段")
sys.stderr.write(f"[easyocr_worker] 收到请求,处理中... (img长度={len(img_b64)})\n")
sys.stderr.flush()
text = _process_image(img_b64, reader)
sys.stderr.write(f"[easyocr_worker] 处理完成,结果长度={len(text)}\n")
sys.stderr.flush()
out = json.dumps({"ok": True, "text": text}, ensure_ascii=False)
sys.stdout.write(out + "\n")
sys.stdout.flush()
except Exception as e:
estr = str(e)
# ⭐ CUDA OOM / kernel error:GPU context 已损坏,继续跑只会反复卡死占显存
# → 立即退出进程释放显存,父进程检测到 stdout 关闭后自动重启/降级 CPU
if "cuda" in estr.lower() or "out of memory" in estr.lower():
_write_crash_log(f"{datetime.now()} [easyocr_worker] CUDA 错误,主动退出释放显存: {estr[:200]}")
_flush_exit(f"CUDA 错误,主动退出释放显存: {estr[:200]}")
out = json.dumps({"ok": False, "error": str(e)}, ensure_ascii=False)
sys.stdout.write(out + "\n")
sys.stdout.flush()
sys.stderr.write(f"[easyocr_worker] 处理失败: {e}\n")
sys.stderr.flush()
if __name__ == "__main__":
try:
_main()
except Exception as e:
_flush_exit(f"_main 崩溃: {e}\n{traceback.format_exc()}")