Forráskód Böngészése

fix(paddleocr): 子进程注入 LD_PRELOAD 与 PADDLE_PDX 避免 static TLS 与模型源检查

- paddleocr_fallback: 新增 _get_paddleocr_subprocess_env(),为 PaddleOCR 子进程设置 LD_PRELOAD(libgomp)和 PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK
- call_paddleocr / call_paddleocr_ocr / call_paddleocr_doc_parser_for_text 及 processor/converter、converter 中 create_subprocess_exec 均传入该 env
- 新增 README_STARTUP.md 启动说明与常见问题(含 static TLS 说明)
何文松 2 hete
szülő
commit
0fe830c65a

+ 262 - 0
pdf_converter_v2/README_STARTUP.md

@@ -0,0 +1,262 @@
+# pdf_converter_v2 启动说明
+
+## 使用脚本启动(推荐)
+
+使用 `start_mineru_in_container.sh` 和 `start_api_in_container.sh` 时,**先起 MinerU,再起 pdf_converter_v2**。
+
+### 单卡
+
+**1. 启动 MinerU API(端口 5282)**
+
+在 **Clerk2.5 项目根目录** 执行:
+
+```bash
+# 若项目不在 /root/work/Clerk2.5,可先设置(脚本也会按路径自动识别):
+# export CLERK_ROOT=/你的/Clerk2.5/路径
+
+# NPU:默认 npu;未装 CANN 可先 export MINERU_DEVICE_MODE=cpu
+export MINERU_DEVICE_MODE=npu
+export MINERU_PORT=5282
+
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+或从 `pdf_converter_v2` 目录执行:`sh scripts/start_mineru_in_container.sh`
+
+**2. 启动 pdf_converter_v2 API(端口 4214)**
+
+在同一台机、**Clerk2.5 项目根目录** 执行:
+
+```bash
+export API_URL=http://127.0.0.1:5282
+sh pdf_converter_v2/scripts/start_api_in_container.sh
+```
+
+或从 `pdf_converter_v2` 目录:`sh scripts/start_api_in_container.sh`
+
+脚本会后台运行并写日志,输出类似:
+
+- MinerU:`已后台启动 MinerU API,端口 5282`,日志默认 `$CLERK_ROOT/logs/mineru-api.log`
+- pdf_converter_v2:`已后台启动 pdf_converter_v2 API,端口 4214`,日志默认 `$CLERK_ROOT/logs/pdf-converter-v2-api.log`
+
+**查看日志:** `tail -f $CLERK_ROOT/logs/mineru-api.log` 或 `tail -f $CLERK_ROOT/logs/pdf-converter-v2-api.log`
+
+**多卡模型缓存:** 脚本会设置 `MODELSCOPE_CACHE=$CLERK_ROOT/.cache/modelscope`,多个 MinerU 实例共用同一缓存,避免每卡重复下载。若需自定义缓存目录,可设置 `export MODELSCOPE_CACHE=/你的路径` 后再执行脚本。
+
+---
+
+## 一、单卡(一张 NPU/GPU,不用脚本时)
+
+### 1. 先启动 MinerU API(提供 PDF→Markdown 转换)
+
+在项目根目录(Clerk2.5)执行:
+
+```bash
+# NPU 环境:会尝试加载 CANN;若未装 CANN 可先 export MINERU_DEVICE_MODE=cpu
+export MINERU_DEVICE_MODE=npu    # 或 npu:0
+export MINERU_PORT=5282
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+或使用虚拟环境直接起:
+
+```bash
+cd /path/to/Clerk2.5
+export PYTHONPATH=/path/to/Clerk2.5
+python3 -m uvicorn mineru.cli.fast_api:app --host 0.0.0.0 --port 5282
+```
+
+### 2. 再启动 pdf_converter_v2 API(对外提供 /convert 等接口)
+
+```bash
+cd /path/to/Clerk2.5
+export PYTHONPATH=/path/to/Clerk2.5
+export API_URL=http://127.0.0.1:5282
+python3 pdf_converter_v2/api_server.py --host 0.0.0.0 --port 4214
+```
+
+或使用脚本后台启动:
+
+```bash
+sh pdf_converter_v2/scripts/start_api_in_container.sh
+```
+
+**验证:**
+
+- MinerU:`curl http://127.0.0.1:5282/health` 或访问 `/docs`
+- pdf_converter_v2:`curl http://127.0.0.1:4214/docs`
+
+### 多卡(一键脚本,推荐)
+
+使用 **start_multi_card.sh** 一次启动多张卡的 MinerU + 一个 pdf_converter_v2:
+
+```bash
+cd /path/to/Clerk2.5
+
+# 启动 2 卡(默认):MinerU 端口 5282、5283,再起 pdf_converter_v2 并设 API_URL / PADDLE_OCR_DEVICES
+bash pdf_converter_v2/scripts/start_multi_card.sh
+
+# 启动 3 卡
+bash pdf_converter_v2/scripts/start_multi_card.sh 3
+
+# 或通过环境变量
+export MINERU_NUM_CARDS=2
+bash pdf_converter_v2/scripts/start_multi_card.sh
+```
+
+脚本会依次:起 N 个 MinerU(npu:0→5282, npu:1→5283, …)、等待端口就绪、设 `API_URL` 与 `PADDLE_OCR_DEVICES`、再起 pdf_converter_v2。  
+可选环境变量:`MINERU_NUM_CARDS`(卡数)、`MINERU_BASE_PORT`(起始端口,默认 5282)、`MINERU_WAIT_READY`(等待端口秒数,默认 120)。
+
+### 多卡(手动分终端起)
+
+**1. 每个卡起一个 MinerU(不同端口)**
+
+终端 1:
+
+```bash
+cd /path/to/Clerk2.5
+export MINERU_DEVICE_MODE=npu:0
+export MINERU_PORT=5282
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+终端 2:
+
+```bash
+cd /path/to/Clerk2.5
+export MINERU_DEVICE_MODE=npu:1
+export MINERU_PORT=5283
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+(脚本会按端口自动使用不同日志和 PID 文件:`mineru-api-5282.log` / `mineru-api-5283.log`。)
+
+**2. 起 pdf_converter_v2,指向多 MinerU**
+
+```bash
+cd /path/to/Clerk2.5
+export API_URL=http://127.0.0.1:5282,http://127.0.0.1:5283
+# 若 Paddle 也多卡:export PADDLE_OCR_DEVICES=npu:0,npu:1
+sh pdf_converter_v2/scripts/start_api_in_container.sh
+```
+
+---
+
+## 二、多卡(单任务用满所有卡,不用脚本时)
+
+需要:**多个 MinerU API 实例**(每卡一个进程、不同端口),pdf_converter_v2 用 `API_URL` 逗号分隔;Paddle 多卡用 `PADDLE_OCR_DEVICES`。
+
+### 1. 启动多个 MinerU API(每卡一个)
+
+示例:2 张卡,端口 5282、5283。
+
+**终端 1(卡 0):**
+
+```bash
+cd /path/to/Clerk2.5
+export PYTHONPATH=/path/to/Clerk2.5
+export MINERU_DEVICE_MODE=npu:0
+export MINERU_PORT=5282
+# NPU 需先加载 CANN:source /usr/local/Ascend/ascend-toolkit/set_env.sh
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+**终端 2(卡 1):**
+
+```bash
+cd /path/to/Clerk2.5
+export PYTHONPATH=/path/to/Clerk2.5
+export MINERU_DEVICE_MODE=npu:1
+export MINERU_PORT=5283
+sh pdf_converter_v2/scripts/start_mineru_in_container.sh
+```
+
+(更多卡同理:`npu:2` + `MINERU_PORT=5284` 等。)
+
+### 2. 启动 pdf_converter_v2(多 MinerU 地址 + 可选 Paddle 多卡)
+
+```bash
+cd /path/to/Clerk2.5
+export PYTHONPATH=/path/to/Clerk2.5
+# 多 MinerU:逗号分隔,单次 PDF 会按页拆分并行发往各实例
+export API_URL=http://127.0.0.1:5282,http://127.0.0.1:5283
+# 若用 Paddle 多卡(备用/工况解析):单次 PDF 会按页拆分并行用各卡
+export PADDLE_OCR_DEVICES=npu:0,npu:1
+python3 pdf_converter_v2/api_server.py --host 0.0.0.0 --port 4214
+```
+
+**验证:** 发一个多页 PDF 到 `/convert`,看日志应出现「拆成 N 段并行」之类输出。
+
+---
+
+## 三、用 systemd 管理(生产环境)
+
+### 1. 安装并改配置
+
+```bash
+sudo cp service/pdf-converter-v2.service /etc/systemd/system/
+# 按本机路径修改
+sudo sed -i 's|/mnt/win_d/Clerk2.5|/你的/Clerk2.5/路径|g' /etc/systemd/system/pdf-converter-v2.service
+# 若用虚拟环境,改 ExecStart 为:/path/to/Clerk2.5/.venv_paddleocr/bin/python3 /path/to/Clerk2.5/pdf_converter_v2/api_server.py ...
+```
+
+**多卡时** 在 service 里加环境变量(或建 override):
+
+```ini
+Environment="API_URL=http://127.0.0.1:5282,http://127.0.0.1:5283"
+Environment="PADDLE_OCR_DEVICES=npu:0,npu:1"
+```
+
+### 2. 启停、开机自启、看日志
+
+```bash
+sudo systemctl daemon-reload
+sudo systemctl start pdf-converter-v2
+sudo systemctl enable pdf-converter-v2   # 开机自启
+sudo systemctl status pdf-converter-v2
+sudo journalctl -u pdf-converter-v2 -f   # 看日志
+```
+
+**注意:** MinerU API 需单独起(多个卡就起多个实例,见第二节),systemd 只管理 pdf_converter_v2 服务。
+
+---
+
+## 四、常见问题
+
+### sklearn / paddlex:`cannot allocate memory in static TLS block`
+
+报错类似:
+
+```text
+ImportError: .../scikit_learn.libs/libgomp-d22c30c5.so.1.0.0: cannot allocate memory in static TLS block
+```
+
+原因是 Paddle/sklearn 等库加载顺序导致 libgomp 的 static TLS 空间不足。**用脚本启动 MinerU 时**,`start_mineru_in_container.sh` 会自动设置 `LD_PRELOAD` 预加载 libgomp,一般可避免此问题。
+
+若**未用脚本**(例如直接 `python3 -m uvicorn ...` 或 systemd),需在启动前设置:
+
+```bash
+# 任选其一存在即可(路径按你本机 scikit-learn 安装位置调整)
+export LD_PRELOAD=/usr/local/lib/python3.10/dist-packages/scikit_learn.libs/libgomp-d22c30c5.so.1.0.0
+# 或系统 libgomp
+export LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libgomp.so.1
+python3 -m uvicorn mineru.cli.fast_api:app --host 0.0.0.0 --port 5282
+```
+
+systemd 服务可在 `Environment=` 里加上上述 `LD_PRELOAD`。
+
+---
+
+## 五、环境变量速查
+
+| 作用           | 变量                     | 示例 |
+|----------------|--------------------------|------|
+| MinerU 单地址  | `API_URL`                | `http://127.0.0.1:5282` |
+| MinerU 多卡    | `API_URL`(逗号分隔)    | `http://127.0.0.1:5282,http://127.0.0.1:5283` |
+| Paddle 单卡    | `PADDLE_OCR_DEVICE`      | `npu:0` |
+| Paddle 多卡    | `PADDLE_OCR_DEVICES`     | `npu:0,npu:1` |
+| MinerU 设备    | `MINERU_DEVICE_MODE`     | `npu` / `npu:0` / `npu:1` |
+| 本服务端口     | 命令行 `--port` 或脚本  | 默认 4214 |
+| MinerU 端口    | `MINERU_PORT`            | 默认 5282 |
+
+更多见 [README.md](README.md) 中「环境变量配置」与「多 NPU 配置」小节。

+ 2 - 0
pdf_converter_v2/converter.py

@@ -18,6 +18,7 @@ from PIL import Image
 
 from ..utils.logging_config import get_logger
 from ..utils.file_utils import safe_stem
+from ..utils.paddleocr_fallback import _get_paddleocr_subprocess_env
 
 logger = get_logger("pdf_converter_v2.processor")
 PADDLE_CMD = os.getenv("PADDLE_DOC_PARSER_CMD", "paddleocr")
@@ -30,6 +31,7 @@ async def _run_paddle_doc_parser(cmd: Sequence[str]) -> tuple[int, str, str]:
         *cmd,
         stdout=asyncio.subprocess.PIPE,
         stderr=asyncio.subprocess.PIPE,
+        env=_get_paddleocr_subprocess_env(),
     )
     stdout_bytes, stderr_bytes = await process.communicate()
     stdout = stdout_bytes.decode("utf-8", errors="ignore")

+ 2 - 0
pdf_converter_v2/processor/converter.py

@@ -24,6 +24,7 @@ from ..utils.paddleocr_fallback import (
     get_paddle_ocr_devices,
     get_paddle_ocr_device_args_for_index,
     _paddle_ocr_device_args,
+    _get_paddleocr_subprocess_env,
 )
 
 logger = get_logger("pdf_converter_v2.processor")
@@ -37,6 +38,7 @@ async def _run_paddle_doc_parser(cmd: Sequence[str]) -> tuple[int, str, str]:
         *cmd,
         stdout=asyncio.subprocess.PIPE,
         stderr=asyncio.subprocess.PIPE,
+        env=_get_paddleocr_subprocess_env(),
     )
     stdout_bytes, stderr_bytes = await process.communicate()
     stdout = stdout_bytes.decode("utf-8", errors="ignore")

+ 55 - 3
pdf_converter_v2/utils/paddleocr_fallback.py

@@ -98,6 +98,55 @@ def get_paddle_ocr_device_args_for_index(device_index: int) -> list:
     return ["--device", device]
 
 
+# 供 PaddleOCR 子进程使用的环境变量(LD_PRELOAD 避免 sklearn libgomp static TLS 报错;PADDLE_PDX 跳过模型源检查)
+_PADDLEOCR_ENV: Optional[Dict[str, str]] = None
+
+
+def _get_paddleocr_subprocess_env() -> Dict[str, str]:
+    """返回调用 paddleocr 子进程时应使用的环境变量(含 LD_PRELOAD 与 PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK)。"""
+    global _PADDLEOCR_ENV
+    if _PADDLEOCR_ENV is not None:
+        return _PADDLEOCR_ENV
+    env = dict(os.environ)
+    # 跳过「Checking connectivity to the model hosters」
+    env.setdefault("PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK", "True")
+    # 子进程若无 LD_PRELOAD,会触发 sklearn/paddlex 的「cannot allocate memory in static TLS block」
+    if not env.get("LD_PRELOAD"):
+        preload_paths: List[str] = []
+        # 系统 libgomp 优先
+        for p in (
+            "/usr/lib/x86_64-linux-gnu/libgomp.so.1",
+            "/usr/lib/aarch64-linux-gnu/libgomp.so.1",
+            "/usr/lib/libgomp.so.1",
+        ):
+            if os.path.isfile(p):
+                preload_paths.append(p)
+                break
+        # scikit_learn.libs 中的 libgomp(不 import sklearn,仅按路径查找)
+        for sp in getattr(sys, "path", []):
+            if not sp or not os.path.isdir(sp):
+                continue
+            for sub in ("scikit_learn.libs", "simsimd.libs"):
+                d = os.path.join(sp, sub)
+                if not os.path.isdir(d):
+                    continue
+                for name in os.listdir(d):
+                    if name.startswith("libgomp") and (name.endswith(".so") or ".so." in name):
+                        preload_paths.append(os.path.join(d, name))
+        # 固定路径(常见容器)
+        for p in (
+            "/usr/local/lib/python3.10/dist-packages/scikit_learn.libs/libgomp-d22c30c5.so.1.0.0",
+            "/usr/local/lib/python3.10/site-packages/scikit_learn.libs/libgomp-d22c30c5.so.1.0.0",
+        ):
+            if os.path.isfile(p) and p not in preload_paths:
+                preload_paths.append(p)
+        if preload_paths:
+            env["LD_PRELOAD"] = ":".join(preload_paths)
+            logger.debug("[PaddleOCR] 子进程 LD_PRELOAD 已设置,避免 static TLS 报错")
+    _PADDLEOCR_ENV = env
+    return env
+
+
 def _paddle_ocr_device_args() -> list:
     """返回 PaddleOCR 命令的 --device 参数列表;多卡时按请求轮询。"""
     devices = _get_paddle_ocr_devices()
@@ -371,13 +420,14 @@ def call_paddleocr(image_path: str) -> Optional[Dict[str, Any]]:
         
         logger.info(f"[PaddleOCR] 执行命令: {' '.join(cmd)}")
         
-        # 执行命令
+        # 执行命令(env 含 LD_PRELOAD 与 PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK,避免 static TLS / 模型源检查)
         result = subprocess.run(
             cmd,
             capture_output=True,
             text=True,
             timeout=300,  # 5分钟超时
             check=False,
+            env=_get_paddleocr_subprocess_env(),
         )
         
         if result.returncode != 0:
@@ -693,13 +743,14 @@ def call_paddleocr_ocr(image_path: str, save_path: str) -> tuple[Optional[List[s
 
         logger.info(f"[PaddleOCR OCR] 执行命令: {' '.join(cmd)}")
 
-        # 执行命令
+        # 执行命令(env 含 LD_PRELOAD 与 PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK)
         result = subprocess.run(
             cmd,
             capture_output=True,
             text=True,
             timeout=300,  # 5分钟超时
             check=False,
+            env=_get_paddleocr_subprocess_env(),
         )
 
         if result.returncode != 0:
@@ -791,13 +842,14 @@ def call_paddleocr_doc_parser_for_text(image_path: str, save_path: str) -> tuple
         
         logger.info(f"[PaddleOCR DocParser] 执行命令: {' '.join(cmd)}")
         
-        # 执行命令
+        # 执行命令(env 含 LD_PRELOAD 与 PADDLE_PDX_DISABLE_MODEL_SOURCE_CHECK)
         result = subprocess.run(
             cmd,
             capture_output=True,
             text=True,
             timeout=300,  # 5分钟超时
             check=False,
+            env=_get_paddleocr_subprocess_env(),
         )
         
         if result.returncode != 0: