状态:实测记录(2026-10-28 至 10-30)。 症状和命令取自测量日志和执行脚本。对于日志中未留下、无法准确还原的错误信息,文中均已说明。截至 2026-10-30,这三个 PR 均处于开启状态(未合并),结果不作绝对定论。
目录
- 1. 一目了然
- 2. 模型下载在 2.88GB 处停止
- 3. 在一个镜像中放入两个框架时
- 4. OpenCV、libGL 以及损坏的 cv2 文件夹
- 5. 漫长的测量中断了两次
- 6. 一个评分陷阱
- 7. 安全提示:用 eval() 运行模型输出的代码
- 8. 如果重来一次,一开始该做的事
- 参考
第 1 篇只展示了结果。在此之后,安装过程中遇到了不少麻烦。为了让走同一条路的人少走弯路,特此记录症状、原因以及修复方法。测试环境是一台配有一张 24GB GPU 的 Linux 服务器,测量是在使用 GPU 的一次性容器内进行的。
1. 一目了然
| 卡住的地方 | 症状 | 修复方法 |
|---|---|---|
| 获取模型 | Hugging Face 下载在 2.88GB 处停止 | HF_HUB_DISABLE_XET=1 禁用 Xet 后下载 |
| 两个框架 | 在 PyTorch 镜像中放入 Paddle 导致 torch 崩溃(NCCL 冲突) | 将 Paddle 放入独立的虚拟环境中,将 Unlimited-OCR 依赖放入单独的文件夹中 |
| OpenCV 与 libGL | ImportError: libGL.so.1 |
系统中 libgl1·libglib2.0-0 安装 |
| headless OpenCV | 触发 PaddleX 依赖检查,反复切换导致 cv2 损坏 |
卸载所有四个版本,仅重新安装一个版本 --force-reinstall --no-deps |
| 漫长的测量 | 远程连接断开,整个容器随之消失 | 分批运行,每部分结果在日志中打印一行 |
| 评分 | 由于坐标标记,CER 被放大至 16% | 添加坐标正则表达式(第 1 篇第 3 节) |
| 安全 | 远程代码通过 eval() |
save_results=False 测量模型输出,向主干仓库提交 ast.literal_eval PR |
2. 模型下载在 2.88GB 处停止
首次下载 Unlimited-OCR-2 的权重时,下载进度在 2.88GB 处停滞不前。既没有报错,进度条也完全卡住。Hugging Face Hub 会使用一种名为 Xet 的新存储方式(将文件切片以实现无重复地传输)来分发大文件,而 huggingface_hub 如果安装了
hf_xet ,就会走这条路径。
设置环境变量 HF_HUB_DISABLE_XET=1 后,即可回退到传统的 HTTP 下载方式。修改后,两个权重文件在 1 分 50 秒内下载完毕,加载模型耗时 114.7 秒。我们没有单独去重现为什么会卡住(网络路径、 版本、服务器端情况)(未确认)。我们在启动测量容器的脚本中默认加入了这个变量。 hf_xet 另外值得一提的是,PaddleOCR 的模型也是从 Hugging Face 下载的。通过 PaddleX 的
指定来源,并将缓存文件夹( PADDLE_PDX_MODEL_SOURCE=huggingface )放在持久化卷上,这样每次启动新容器时就不需要重新下载了。PADDLE_PDX_CACHE_HOME3. 在一个镜像中放入两个框架时
起初,我们试图将 PaddlePaddle 放入包含 PyTorch 的同一个容器镜像中,以便在一个地方运行所有三个引擎。这里遇到了两个问题。
PyPI 上没有 Paddle wheel 所需的 NCCL 版本。
要求 NVIDIA 的集合通信库 NCCL 版本为 paddlepaddle-gpu==3.4.0 ,但默认索引中最高只有 2.27.3,导致安装失败。解决方法是优先读取 Paddle 维护的针对 CUDA 12.9 的索引,然后再附加 PyPI 进行安装。
nvidia-nccl-cu12==2.28.3 放入相同环境会导致 torch 崩溃。
pip install paddlepaddle-gpu==3.4.0 \
-i https://www.paddlepaddle.org.cn/packages/stable/cu129/ \
--extra-index-url https://pypi.org/simple
PyTorch 和 Paddle 各自会拉取针对自身构建的 CUDA 库依赖包。将 Paddle 放入相同的 nvidia-* 中会导致两边的 NCCL 冲突,从而在加载 torch 阶段引发崩溃(日志中未记录究竟哪个版本获胜以及具体的错误信息)。由于不会在同一个进程中同时使用这两个框架,因此 site-packages 拆分环境 才是正解。 通过
python -m venv --without-pip /models/ocr/venv-paddle
P=/models/ocr/venv-paddle/bin/python
pip --python $P install paddlepaddle-gpu==3.4.0 -i …/cu129/ --extra-index-url https://pypi.org/simple
pip --python $P install "paddleocr[doc-parser]==3.7.0"
--without-pip 创建一个空的虚拟环境,并使用外部 pip 的 --python 在其中进行安装,可以节省为每个虚拟环境重新安装 pip 的时间。Unlimited-OCR 部分也没有去修改镜像中的软件包。模型卡测试所需的 Transformers 4.52.1 及其对应版本的 tokenizers·huggingface-hub
等被放入 pip install --target 的独立文件夹中,并仅通过 PYTHONPATH 调用。这种方式保持了镜像自带的 torch 不变,只加载模型代码所需的版本。
4. OpenCV、libGL 以及损坏的 cv2 文件夹
构建 Paddle 环境并 import cv2 后便卡住了。巡查发现:
ImportError: libGL.so.1: cannot open shared object file: No such file or directory
PaddleOCR 所引入的 OpenCV(opencv-contrib-python)附带了屏幕输出功能,因此会检查系统的 libGL 。而服务器容器镜像中并不包含该库。
常见的解决方法是将其替换为剔除屏幕功能的 headless 版(opencv-contrib-python-headless)。在此过程中,我们又遇到了两次阻碍。
- PaddleX 的依赖检查会检查发行版名称。 PaddleX 并不是通过检查所需软件包是否已安装、而是通过已安装的发行版名称来判断是否
import。虽然 headless 版提供相同的功能,但由于名称不同,它被判定为“缺少所需软件包”而中止。cv2在不同版本间反复切换时, - 文件夹损坏了。
cv2OpenCV 的四个版本( )都会向同一个opencv-python,-headless,-contrib,-contrib-headless文件夹中写入文件。卸载一个版本会连同其他版本写入的文件一并删掉,而在此基础上安装新版本则会留下一个残缺的文件夹。pip 无法阻止这种情况。site-packages/cv2/最终确定的步骤如下:保留原版本不动,补齐缺失的系统库。
必须将这四种版本
V=$(pip --python $P show opencv-contrib-python | sed -n 's/^Version: //p')
pip --python $P uninstall -y opencv-contrib-python opencv-contrib-python-headless \
opencv-python opencv-python-headless
pip --python $P install --force-reinstall --no-deps opencv-contrib-python==$V
apt-get install -y libgl1 libglib2.0-0
$P -c "import cv2; print(cv2.__version__)"
全部 卸载,然后 仅重新安装一种版本 --force-reinstall --no-deps ,才能保持文件夹干净。版本号沿用 PaddleOCR 最初选择的值。如果每次启动新容器时运行 apt-get 的开销太大,最好单独制作一个包含 libgl1 的镜像。
5. 长时间测试两次中断
在同时运行 PaddleOCR 两个引擎的测试中,远程执行连接中途断开,测试容器随之被删除。日志末尾仅留下 TLS 错误和“等待条件超时”。Unlimited-OCR-2 的 40 页组合包也在会话重启过程中于输出时中断(因此第 1 篇中只有第 10 页和第 20 页)。
两次能够恢复,完全是因为测试脚本在 每完成一页时 都会保存结果。
- 输出原文会直接逐页写入文件。
- 时间、内存和错误信息会以单行 JSON 的形式按页打印到标准输出中。由于汇总文件只在最后写入一次,如果中途中断则不会生成,但通过收集日志中的 JSON 行,可以完整恢复汇总数据。第 1 篇中的表格就是这样从日志中恢复出来的数值。
此后,我们改为以文本包(普通正文、模糊复印件、表格、长文档)为参数, 分批运行,并为每个文本包单独记录日志。在容易断开连接的环境中,避免单次运行持续数小时是最划算的保险。此外,每次运行的第一页作为预热被丢弃了。Unlimited-OCR-2 的第一页耗时 43.9 秒,比后续页面(约 30 秒左右)更长。
6. 评分陷阱
正如第 1 篇第 3 节所述,如果不删除 Unlimited-OCR-2 输出中的区块坐标 text[71, 96, 900, 160] ,普通正文的 CER 将显示为 16.3%,而删除后则为 0.80%。如果引擎输出格式发生变化,评分器会默默给出错误的数值。在接入新引擎时,我们必须先用肉眼阅读几页输出原文,将规范化后的字符与标准答案并排比对后,方可相信得分。
7. 安全备忘:曾对模型输出执行 eval() 的代码
Unlimited-OCR-2 使用 trust_remote_code=True 从代码仓库拉取并运行模型代码。在阅读这些代码时,我们在结果保存路径中发现了这样一行(modeling_unlimitedocr.py,修订版本 5632329a).
cor_list = eval(ref_text[2])
infer(..., save_results=True) 调用后,远程代码会通过正则表达式在模型输出中查找 <|det|>이름[…]<|/det|> 格式的标记,并为了将方括号内的字符读取为坐标而传递给 eval() 。只要方括号内不包含 ] ,正则表达式就会放行任何内容。而模型输出是 随输入文档而变的字符。文档人人可造,因此从原理上讲,文档中的字符存在通过模型输出并以 Python 表达式方式执行的途径。
我们将已确认的事实分类整理如下。
- 已确认: 仅当存在上述
eval()行且满足save_results=True时,该函数才会被调用(代码中已读)。 - 已确认: 在本次测试的默认提示词下,Unlimited-OCR-2 输出的格式为
<BLOCK>text[…]<FORK>,该格式不会触发上述正则表达式。 - 未实测: 未测试是否能通过文档内容引导模型输出
<|det|>标记及任意表达式。这并非证明存在攻击可能,而是指出代码中确实存在使不受信任的输入到达eval()的路径。
我们在测试中使用了 save_results=False 并仅获取输出字符。如果需要绘制图像或裁剪保存方框,坐标可以通过
ast.literal_eval 读取。由于该方法仅接收数字、列表、字符串等字面量并通过 ValueError 拒绝表达式,因此对于正常的坐标,结果是相同的。
由于该函数延续自 DeepSeek-OCR 系列的示例代码,相同的代码行存在于多个代码仓库中。我们已向三处提交了包含相同修改的 PR。我们在 PR 正文中说明了以下验证内容:使用相同输入运行原函数和修改后的函数,确保正常坐标的结果一致,且包含表达式的输入不会被执行而是进入原有的错误路径。同时我们还附上了声明,表明该修改是在 AI 辅助下准备的。
| 代码仓库 | PR | 状态(2026-10-30) |
|---|---|---|
| deepseek-ai/DeepSeek-OCR-2 (vLLM 示例脚本) | #61 | 开启 |
| openvinotoolkit/openvino_notebooks (deepseek-ocr, deepseek-vl2 notebook) | #3820 | 开启 |
| basetenlabs/truss-examples (deepseek-ocr 示例) | #594 | 打开 |
trust_remote_code=True 我们在使用模型时遵循的原则也一同记录在此。固定 revision(提交哈希)以确保获取的代码不发生变化,通读一遍固定版本的代码,并关闭未使用的存储与可视化路径。启动用于评测的 GPU 容器时不带集群 API 令牌(关闭自动挂载服务账户令牌)。
8. 如果重新来过,一开始就该做的事
- 在启动评测容器的脚本中加入
HF_HUB_DISABLE_XET=1和模型缓存卷作为默认项。 - 按框架划分环境。在 PyTorch 镜像之上为 Paddle 单独配置虚拟环境,远程代码模型的依赖放在
--target文件夹。 libgl1·libglib2.0-0使用固定的镜像。OpenCV 版本只用 PaddleOCR 选定的一款,若需更改则将四个版本全部清除后重来。- 评测脚本在每一页输出一个文件和一行 JSON,分批运行,第一页作为预热丢弃。
- 评分前先阅读输出原文,将归一化后的文字与标准答案并排对照。
trust_remote_code对于模型,固定 revision,并在该版本的代码中首先查找eval·exec·pickle·subprocess。
TT 的一句话。 OCR 模型读取的文字完全按文档呈现。因此,模型的输出也应像顾客递过来的便条一样对待。没人会直接执行便条上写的内容吧?
参考
- Hugging Face Hub 环境变量(
HF_HUB_DISABLE_XET): https://huggingface.co/docs/huggingface_hub/package_reference/environment_variables - Unlimited-OCR-2 模型卡片(Transformers 测试版,推荐 SGLang): https://huggingface.co/baidu/Unlimited-OCR-2
- PaddleOCR 安装指南: https://www.paddleocr.ai/
- Python
ast.literal_eval: https://docs.python.org/3/library/ast.html#ast.literal_eval - 上游 PR: DeepSeek-OCR-2#61, openvino_notebooks#3820, truss-examples#594