内网里装一个带 GPU 的 OCR 库,麻烦的从来不是那行pip install rapidocr_paddle,而是它背后那一整条依赖链——驱动、CUDA、cuDNN、PaddlePaddle 的 GPU 轮子、OpenCV、模型文件,任何一环在内网里断掉,你都只能对着ImportError发呆,因为目标机上连pip都下载不了东西。这篇东西就是把我自己在 Ubuntu 23.10 上做这件事的完整过程摊开写一遍:从外网"育种机"怎么准备、依赖怎么逆推、离线包怎么收、apt 和 pip 两条线怎么并行推进,一直到内网机器上把RapidOCR(det_use_cuda=True)跑出第一张图的识别结果。
rapidocr_paddle是 RapidOCR 家族里基于 PaddlePaddle 推理引擎的那一支,接口极其简单,几行代码就能做检测、方向分类、识别三件套,GPU 加速的开关也就三个参数。但"接口简单"和"部署简单"完全是两回事,尤其在内网、尤其是 GPU。如果你手上是一台断网的 Ubuntu 23.10 服务器,要交付一个能跑 OCR 的推理服务,那这套流程值得先看一遍。适合的读者是有一定 Linux 基础、至少能看懂nvidia-smi输出的开发或运维,小白也能跟着走,但前置准备那部分得有点耐心。
1. 先把方案定下来:内网离线部署的整体思路
内网部署的第一个决策不是"装什么版本",而是"我怎么把东西运进去"。内网环境千差万别——有的有内部制品库(Nexus、Artifactory 之类),有的只能靠 U 盘刻盘拷贝,有的允许走跳板机 scp。这三种情况对应的收集策略完全不一样。有内部 PyPI 代理的是最舒服的,你只需要在外网确认包名和版本,然后在内网直接pip install --index-url http://内网源/simple;没有的话,就得老老实实把 wheel 文件打成一个离线包,用--no-index --find-links的方式安装。
我这次的环境属于后者,只能通过移动介质搬运。所以整个方案的核心就变成了一个词:预演。所有会下载、会编译、会联网的动作,必须在一台和外网目标机尽可能一致的"育种机"上先跑一遍,跑通了、把产物抠出来、再整体搬过去。这个思路听起来笨,但它把内网里最不可控的因素——"我不知道它还缺什么"——提前消灭掉了。
1.1 为什么必须做"依赖树逆推",而不是想到哪装到哪
很多人装 Python 包的习惯是pip install xxx,报错缺什么再装什么。这个习惯在内网里会让你死得很惨,因为每报一次错,你就得往返一次介质拷贝。正确的做法是提前把整棵依赖树展开。
具体怎么做?在育种机上建一个干净的虚拟环境,然后做两件事:一是pip download把rapidocr_paddle及其全部依赖下载下来,二是用pipdeptree或者pip freeze把最终装出来的版本清单固化成一个requirements.lock。注意这里有个细节——不要用pip install装完之后pip freeze,因为pip freeze只列出已安装的顶层包和依赖,但不告诉你某个包是被谁拉进来的,一旦内网里版本冲突,你根本不知道该动谁。我更推荐的做法是先在育种机上装一遍,导出pipdeptree -p rapidocr_paddle的树形结构,看清楚opencv-python、numpy、pyclipper、shapely、Pillow、PyYAML这些是直接依赖还是传递依赖,然后再决定版本。
这中间还有一个"隐形依赖"的问题容易被忽略:paddlepaddle-gpu的 manylinux wheel 是动态链接系统 CUDA 库的,它不会像某些框架那样把 CUDA 运行时打进 wheel 里。也就是说,你在pip download的时候是收不到nvidia-cudnn-cu11、nvidia-cublas-cu11这类 pip 包的(这一点跟另一个主流框架的做法不同,容易搞混)。GPU 相关的.so必须靠系统层面装好 CUDA Toolkit 和 cuDNN,pip 那条线只管 Python 层。这个认知如果一开始没建立起来,你会一直在 pip 的报错里打转。
注意:判断一个包的 GPU 依赖是"打进 wheel"还是"链接系统库",最直接的办法是装完之后
ldd一下包目录里的.so文件,看libcudart.so.11.0、libcudnn.so.8这些是=> not found还是指向了系统路径。这一步花三十秒,能省你半天。
1.2 三层依赖:系统层、Python 层、模型层
把整件事拆开看,其实是三层,每一层的离线策略都不一样,混在一起想就会乱。
系统层负责的是显卡驱动、CUDA Toolkit、cuDNN、以及编译工具链(build-essential、dkms、linux-headers)。这一层的离线靠apt-get --download-only或者直接下.run安装包,产出物是.deb和.run文件。
Python 层是paddlepaddle-gpu加上rapidocr_paddle及它的一大串依赖。这一层靠pip download,产出物是.whl文件集合。
模型层是 OCR 的检测模型、方向分类模型、识别模型。这一层最容易被忘掉——因为你在线装的时候根本没注意过它,第一次运行时它默默下载完了,你甚至不知道有这回事。等到内网里跑第一张图卡在"下载模型"上,你才发现问题。这一层必须单独准备。
我见过有人把前两层准备得滴水不漏,结果内网跑起来卡在模型下载上,最后只能拿手机拍屏幕把 base64 编码的模型转出去,极其狼狈。所以三层要一起盘。
1.3 版本矩阵怎么定:以 CUDA 11.8 + cuDNN 8.6 + Paddle 2.6 这条线为例
版本选择这件事,我的原则是"跟着官方 GPU wheel 的 postfix 走,不要自由发挥"。paddlepaddle-gpu的版本号后面会带一个postXXX后缀,比如2.6.1.post118,这个118就代表它编译时链接的 CUDA 版本是 11.8。你装哪个 postfix,系统里就必须有对应版本的 CUDA。
我这次选的是2.6.1.post118这条路,理由有三条。第一,CUDA 11.8 是这一代里兼容性最好的一个版本,绝大多数消费级和数据中心卡都能覆盖;第二,cuDNN 8.x 系列的 tar 包分发方式成熟,离线搬运没有依赖问题;第三,Python 3.11 有对应的 cp311 wheel,而 Ubuntu 23.10 默认就是 Python 3.11,省去了自己编译 Python 的麻烦。
| 组件 | 选定版本 | 选择理由 | 备选 |
|---|---|---|---|
| 操作系统 | Ubuntu 23.10 | 目标机既定环境 | 更推荐 22.04 LTS |
| Python | 3.11(系统自带) | 有 cp311 官方 wheel | 3.10 也行 |
| NVIDIA 驱动 | 535 系列 | 支持 CUDA 11.8 且较稳定 | 525 / 550 |
| CUDA Toolkit | 11.8 | 匹配 post118 后缀 | 12.x 需换 postfix |
| cuDNN | 8.6 及以上(8.x) | Paddle 2.6 期望 cudnn 8 | 9.x 不匹配 |
| PaddlePaddle | 2.6.1.post118 | 官方 GPU wheel | 2.6.2 亦可 |
| rapidocr_paddle | 对应 2.x 稳定版 | 接口稳定 | 最新版需核依赖 |
这里必须提醒一句:Ubuntu 23.10 这个版本本身不太适合做长期部署机。它属于非 LTS 的短期支持版本,维护周期结束后官方归档位置会迁移,apt update在脱离了原有仓库路径之后会直接失败。如果你的内网机器还没定型,我的建议是直接上 22.04 LTS,驱动、CUDA、Paddle 三者的兼容矩阵资料也更多。已经定死 23.10 的也别慌,只是要求你在育种机上把 deb 缓存做全,之后所有 apt 操作都走本地源,不要再指望在线更新。
2. 前置准备:外网"育种机"与内网"目标机"的一致性
这一步是整个流程里最容易偷懒、也最容易翻车的地方。所谓"育种机",就是一台可以联外网、用于下载和试跑的机器;"目标机"是内网那台真正要跑服务的机器。这两台机器的一致性越高,你搬过去之后遇到的问题就越少。理想情况下,两台机器的操作系统版本号、内核版本、Python 小版本、CPU 架构(x86_64还是aarch64)都要一模一样。
现实里经常出现的情况是:育种机是开发自己的笔记本(Ubuntu 22.04 + Python 3.10),目标是机房里的服务器(Ubuntu 23.10 + Python 3.11)。这种情况下pip download下载的 wheel 文件名里带cp310,拿到目标机上pip install会直接告诉你"没有匹配的发行版"。所以第一件事,先把两边的信息对齐。
2.1 两台机器必须对齐的四项信息
在育种机和目标机上各跑一遍下面这几条命令,把输出并排贴在一起对比:
# 架构与发行版 uname -m lsb_release -a # 内核版本(决定显卡驱动能不能复用) uname -r # Python 版本与 ABI python3 -VV python3 -c "import sysconfig; print(sysconfig.get_platform())" # glibc 版本(决定 manylinux 轮子能不能用) ldd --version | head -n1这四项里,uname -m和 Python 版本是最硬的对齐要求,不一致就必然出问题;glibc版本不一致可能导致 manylinux 轮子加载失败(glibc 只能向前兼容,高版本编译的轮子拿到低版本系统上会报GLIBC_2.xx not found);内核版本不一致影响的主要是显卡驱动——如果你打算直接拷.ko模块过去,内核版本必须完全一致,否则就得在目标机上用dkms重新编译。
我倾向的做法是:驱动在目标机现场编译安装,不在育种机上编。原因很简单,dkms编译依赖目标机当前内核的头文件,你在育种机上编出来的模块大概率对不上。所以驱动这一步,"育种机"的作用只是把.run安装包和编译所需的 deb 包下载下来,编译动作放到目标机做。
2.2 显卡驱动与 CUDA 的离线安装路径
先确认目标机有没有认到卡。lspci | grep -i nvidia能看到设备名,说明硬件层面没问题;nvidia-smi报"command not found"或者"couldn't communicate with the driver",说明驱动还没装好或者版本不匹配。
离线装驱动我用的是官方.run文件这条路,因为.deb走 apt 的话依赖链条太长。育种机上先去把对应版本的.run下下来,同时把目标机上需要的编译依赖一并收好:
# 在育种机上,把目标机需要的 deb 全部下载到本地缓存目录 sudo apt-get install --download-only -y \ -o Dir::Cache::archives="/tmp/debs" \ build-essential dkms linux-headers-$(uname -r) \ gcc-12 g++-12 make这里有个细节值得说一下:linux-headers-$(uname -r)里的内核版本必须用目标机的,不是育种机的。所以这条命令最好直接在目标机上跑(如果目标机能连内部源),或者在育种机上手动指定目标机的内核版本号。如果连这个都做不了,那就只能靠dpkg -l | grep linux-headers在目标机上查出来,再去下载对应的 deb,确实有点绕,但这是内网环境的常态。
驱动安装的关键步骤是禁用开源驱动,否则会冲突:
sudo tee /etc/modprobe.d/blacklist-nouveau.conf <<'EOF' blacklist nouveau options nouveau modeset=0 EOF sudo update-initramfs -u然后重启,进入非图形界面(sudo systemctl isolate multi-user.target),再执行安装:
sudo sh NVIDIA-Linux-x86_64-535.xx.xx.run \ --silent --dkms --no-opengl-files--no-opengl-files这个参数很关键,不加的话安装程序会尝试替换系统的 OpenGL 库,在服务器上还好,在有图形界面的机器上很容易把桌面搞黑。--dkms让驱动模块在每次内核升级后自动重编,省得以后升级内核又炸一次。装完nvidia-smi能正常出表格,这一步就算过了。
CUDA Toolkit 的离线安装,我用的是 runfile 的--toolkit模式,只装工具链不装驱动——驱动已经单独装了,让 CUDA 安装包再装一遍容易把版本搞乱:
sudo sh cuda_11.8.0_520.61.05_linux.run \ --silent --toolkit --toolkitpath=/usr/local/cuda-11.8 \ --no-opengl-libs --overridecuDNN 更简单,官方给的是 tar 包,解压了把文件拷进去就行,不需要安装程序:
tar -xzvf cudnn-linux-x86_64-8.9.x.x_cuda11-archive.tar.xz sudo cp cudnn-*-archive/include/cudnn*.h /usr/local/cuda-11.8/include/ sudo cp -P cudnn-*-archive/lib/libcudnn* /usr/local/cuda-11.8/lib64/ sudo chmod a+r /usr/local/cuda-11.8/include/cudnn*.h /usr/local/cuda-11.8/lib64/libcudnn*然后把环境变量写进/etc/profile.d/cuda.sh,注意要写系统级的,不要只写进~/.bashrc,因为你的服务大概率不是用你的用户跑的:
export CUDA_HOME=/usr/local/cuda-11.8 export PATH=$CUDA_HOME/bin:$PATH export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH export CUDNN_PATH=$CUDA_HOME注意:
LD_LIBRARY_PATH这个东西在多版本 CUDA 共存的时候是灾难源头。我踩过一次,机器上装了 11.8 和 12.x 两套,LD_LIBRARY_PATH里的顺序写反了,Paddle 加载到了 12.x 的libcudart.so,报了一个完全看不懂的符号未定义错误,查了两天才定位到。所以环境变量里只放你确定要用的那一个版本,其他版本从PATH里彻底移出去。
2.3 Ubuntu 23.10 上的系统依赖补全
除了驱动和 CUDA,Paddle 和 OpenCV 还需要一批系统级的.so。这些库在桌面版系统上通常是预装的,但服务器版的最小化安装经常缺。育种机上先把它们下全:
sudo apt-get install --download-only -y \ -o Dir::Cache::archives="/tmp/debs" \ libgomp1 libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \ libstdc++6 libgcc-s1 zlib1g libbz2-dev liblzma-devlibgomp1是 OpenMP 的运行时,Paddle 的 CPU 算子依赖它;libgl1和libglib2.0-0是 OpenCV 的依赖,缺了会报ImportError: libGL.so.1: cannot open shared object file——这个错误是 OCR 部署里出现频率最高的问题之一,很多人第一反应是重装opencv-python,其实重装一百遍也没用,缺的是系统库。libsm6、libxext6、libxrender1同理,是cv2图形相关模块的依赖。
还有一个隐蔽的坑:Ubuntu 23.10 默认的 GCC 是 13,而 CUDA 11.8 官方声明的支持上限是 GCC 11 左右。如果你只是运行Paddle 的预编译 wheel,不涉及自己编译 CUDA 扩展,这个不匹配通常不影响,可以忽略。但如果哪天你要装某个需要nvcc现场编译的包,nvcc会因为版本检查直接拒绝编译。解决办法是装上gcc-12,然后用update-alternatives切过去,或者编译时指定CC=gcc-12 CXX=g++-12。我一般选择后者,因为改全局的alternatives会影响其他东西。
3. 离线包收集:把 pip 和 apt 两条线都"打包带走"
准备工作做完,就到了真正往外搬东西的环节。这一节我按 pip 和 apt 两条线分开写,因为它们的思路完全不同——pip 是"下载 wheel 到目录",apt 是"下载 deb 到缓存目录",但坑点各有各的花样。
3.1 pip download 的正确姿势与版本锁定
先说最容易出错的地方:pip download必须在和目机同环境的地方跑。所谓的"同环境",至少要求操作系统架构一致(都是x86_64)、Python 小版本一致(都是 3.11)。在这种前提下直接pip download,不做任何跨平台参数,是最稳的。
# 育种机上 python3 -m venv /opt/breed-env source /opt/breed-env/bin/activate # 先装一遍,看看能装出什么 pip install --no-cache-dir rapidocr_paddle # 看看依赖树 pip install pipdeptree pipdeptree -p rapidocr_paddle装完之后你会发现一个问题:pip install rapidocr_paddle会顺带把paddlepaddle(CPU 版)装进来,因为rapidocr_paddle的install_requires里写的是paddlepaddle,不是paddlepaddle-gpu。如果你不管它,最后内网机器上会装成 CPU 版本,det_use_cuda=True传进去也是白搭,照样跑在 CPU 上,慢得让你怀疑人生。
正确的做法分两步走。第一步,先确认版本,把paddlepaddle从依赖里摘掉:
# 单独安装 GPU 版本的 paddle pip install --no-cache-dir paddlepaddle-gpu==2.6.1.post118 # 再装 rapidocr_paddle,不带依赖 pip install --no-cache-dir --no-deps rapidocr_paddle第二步,手动补rapidocr_paddle除 paddle 之外的其他依赖。这些依赖的清单可以从pip show rapidocr_paddle的Requires字段看到,通常是opencv-python、numpy、six、pyclipper、shapely、Pillow、PyYAML这类。手动装一遍,确认没有报错。
全部确认完毕之后,用pip freeze把精确版本冻结成锁文件,这是后续内网安装的"施工图":
pip freeze > requirements.lock cat requirements.lock这一步至关重要,因为pip freeze会把当前环境所有包的精确版本(含==)写下来,包括那些你没直接装但被传递依赖拉进来的包。内网安装时按这个锁文件来,版本就完全可控。
然后是真正的下载:
mkdir -p /opt/offline/wheels pip download \ --no-cache-dir \ --dest /opt/offline/wheels \ -r requirements.lock--dest指定输出目录,-r指定锁文件。跑完之后目录里应该有一堆.whl文件,数量通常在 30 到 60 个之间,取决于 OpenCV 的版本(opencv-python的 wheel 本身就一百多 MB)。
这里提醒一个高频翻车点:先pip install再pip download的时候,如果本机已经装过这些包,pip download有时会因为"requirement already satisfied"而跳过下载。我第一次做的时候就中了这招,拷到内网一装才发现少了好几个包。规避方法是pip download时加--no-cache-dir并且在一个完全干净的虚拟环境里跑,或者用--ignore-installed强制重新解析。最保险的做法是干脆另起一个全新的 venv,里面什么包都不装,直接pip download -r requirements.lock。
3.2 apt 离线 deb 的收集与本地源搭建
apt 这边的思路是"全部下载进缓存目录,然后整体搬走"。--download-only加-o Dir::Cache::archives=是最直接的办法:
sudo apt-get install --download-only -y \ -o Dir::Cache::archives="/opt/offline/debs" \ <你要装的包名列表>装完之后把/opt/offline/debs整个目录拷走,目标机上sudo dpkg -i /opt/offline/debs/*.deb就能装上。但用dpkg -i有个问题——它不做依赖顺序排序,如果包之间有依赖关系,可能会报"依赖未满足"。解决办法有两个:一是dpkg -i *.deb多跑几遍(简单粗暴但有效),二是把它做成一个本地 apt 源,让 apt 自己去解析依赖。
做成本地源是我更推荐的方式,因为后续维护方便:
# 目标机上 sudo mkdir -p /opt/localrepo sudo cp /media/usb/debs/*.deb /opt/localrepo/ cd /opt/localrepo sudo apt-ftparchive packages . > Packages sudo apt-ftparchive release . > Release # 写进 sources.list echo "deb [trusted=yes] file:/opt/localrepo ./" | \ sudo tee /etc/apt/sources.list.d/local.list sudo apt-get update sudo apt-get install -y build-essentialapt-ftparchive这个命令需要装apt-utils,如果目标机上没有,那就只能先用dpkg -i把apt-utils装上,再走本地源。逻辑上有点鸡生蛋,但实际操作中通常apt-utils是预装的,问题不大。
注意:
[trusted=yes]是必须的,因为本地源没有 GPG 签名,不加这个 apt 会拒绝加载。这个配置仅限于完全封闭的内网环境使用,不要在有外网访问的机器上这么写。
3.3 最容易忽略的一层:模型文件的搬运
前面提过模型层这个事,这里展开说。OCR 模型的体积不算大(检测模型大概几 MB,识别模型十几 MB,方向分类模型几 MB),但它们默认是在首次运行时下载的。内网环境没这个条件,所以必须提前拿到手。
我的做法是:在育种机上把rapidocr_paddle完整跑通一次,跑通之后它会留下模型缓存。这个缓存的位置通常在包目录下的models子目录,或者用户目录下的某个缓存文件夹里。你可以通过python -c "import rapidocr_paddle; print(rapidocr_paddle.__file__)"找到包的安装位置,然后翻一下旁边有没有模型文件。
更可靠的做法是显式指定模型路径,让程序不依赖任何默认缓存。RapidOCR的初始化接口支持传入模型目录参数:
from rapidocr_paddle import RapidOCR engine = RapidOCR( det_model_dir="/opt/models/ch_PP-OCRv4_det_infer", rec_model_dir="/opt/models/ch_PP-OCRv4_rec_infer", cls_model_dir="/opt/models/ch_ppocr_mobile_v2.0_cls_infer", det_use_cuda=True, cls_use_cuda=True, rec_use_cuda=True, )这样你在育种机上把模型目录准备好,和内网的路径一一对应,程序就不会再去尝试下载。模型文件的来源可以是育种机跑通后的缓存,也可以是官方发布页单独下载的推理模型包,两个都行,但要注意配套——检测模型和识别模型的版本最好一致,混用不同版本的模型有时候能跑,但识别质量会莫名其妙地下降,而且很难排查。
4. 内网实施:从零到跑通一次推理
东西都搬到目标机上了,接下来就是按顺序装。我把顺序排成"驱动 → CUDA → cuDNN → 系统库 → Python 环境 → Paddle → rapidocr_paddle",这个顺序不能乱,因为后一步依赖前一步的结果。
4.1 按依赖顺序落地安装
先验证驱动。nvidia-smi输出的右上角会显示Driver Version和CUDA Version,这里的CUDA Version指的是驱动支持的最高CUDA 版本,不代表你装了那个版本,容易误会。真正的 CUDA 版本要看nvcc --version或者/usr/local/cuda/version.json。
nvidia-smi nvcc --version ls -l /usr/local/cuda/usr/local/cuda应该是一个软链接,指向/usr/local/cuda-11.8。如果指向 12.x,说明安装的时候选错了,得改过来。
然后是系统库。本地源建好之后:
sudo apt-get update sudo apt-get install -y libgomp1 libgl1 libglib2.0-0 libsm6 libxext6 libxrender1Python 环境我强烈建议用 venv 隔离,不要装进系统 Python:
python3 -m venv /opt/ocr-env source /opt/ocr-env/bin/activate pip install --upgrade pip --no-index --find-links /opt/offline/wheels注意这里pip install --upgrade pip也要走--no-index,因为内网连不上 PyPI,不加这个参数它会尝试联网然后超时卡很久。
接下来按顺序装 Python 包,这个顺序有讲究——先 paddle,后 rapidocr_paddle,且 rapidocr_paddle 加--no-deps:
# 第一步:装 GPU 版本的 Paddle pip install --no-index --find-links /opt/offline/wheels \ paddlepaddle-gpu==2.6.1.post118 # 第二步:验证 Paddle 本身没问题 python -c " import paddle paddle.utils.run_check() print('device:', paddle.device.get_device()) print('cuda count:', paddle.device.cuda.device_count()) "paddle.utils.run_check()如果输出PaddlePaddle is installed successfully!,说明 Python 层和底层 CUDA 已经通了。paddle.device.get_device()返回gpu:0才对,返回cpu说明 GPU 没认到,别急着往下装,先解决这个。
第三步装 rapidocr_paddle:
pip install --no-index --find-links /opt/offline/wheels \ --no-deps rapidocr_paddle # 手动补其他依赖 pip install --no-index --find-links /opt/offline/wheels \ opencv-python numpy six pyclipper shapely Pillow PyYAML--no-deps的作用是切断rapidocr_paddle对paddlepaddle(CPU 版)的依赖,防止它把刚装好的 GPU 版覆盖掉。这一点极其关键,我见过不止一次有人装完发现跑在 CPU 上,最后查出来是因为某次pip install又把 CPU 版 paddle 拉回来了。
4.2 参数计算与显存占用的实际估算
GPU 版的 OCR 不是"开了就一定更快",涉及显存占用和批处理策略的选择。先算一下显存。
rapidocr_paddle的三个模型都是小模型,检测模型(PP-OCRv4 det)参数量大概几百万级别,识别模型稍大一些。在 FP32 精度下,单个模型加载到显存里大概占用几百 MB。三个模型同时加载(检测 + 方向分类 + 识别),加上 Paddle 运行时的基础开销(cuDNN 的 workspace、CUDA context 等),显存峰值保守估计在 1.5 GB 到 2.5 GB 之间,具体取决于输入图片的尺寸。
这个数字意味着什么?意味着 4 GB 显存的入门级卡能跑,但如果同时处理多路请求,很容易 OOM。8 GB 及以上的卡会比较从容。如果你用nvidia-smi看到显存占用在推理结束后没有释放,那是 Paddle 的显存池机制在起作用——它默认不会把显存还回去,而是留在池子里给下次用,这是正常行为,不是内存泄漏。真想让它释放,可以设置环境变量FLAGS_allocator_strategy=naive_best_fit或者调用paddle.device.cuda.empty_cache()。
图片尺寸对显存的影响很大。OCR 的检测阶段会把输入图缩放到一个固定边长(默认长边 960 左右),所以一张 4000×3000 的大图进来,实际送进网络的是缩放后的版本,显存占用不会线性上涨。但如果你手动调大了det_limit_side_len,或者关掉了det_limit_type的限制,那显存占用会明显增加。我的经验值是:默认参数下,单张 1080p 图片的推理显存增量在 300 MB 左右,批量处理时按这个数乘以并发数预留。
还有一个参数值得调:rec_batch_num。识别阶段是把检测出来的文本框攒成一批送进网络的,默认值通常是 6。如果你的卡显存富余,调到 16 甚至 32 能明显提升吞吐;如果显存紧张,调到 1 或者 2 更稳。这个参数没有"最优值",只有"适合你场景的值",建议拿你自己的业务图片跑个基准测试再定。
4.3 跑通第一个 GPU 推理并做性能对比
代码写起来很简单:
import time from rapidocr_paddle import RapidOCR engine = RapidOCR( det_model_dir="/opt/models/ch_PP-OCRv4_det_infer", rec_model_dir="/opt/models/ch_PP-OCRv4_rec_infer", cls_model_dir="/opt/models/ch_ppocr_mobile_v2.0_cls_infer", det_use_cuda=True, cls_use_cuda=True, rec_use_cuda=True, ) # 第一次推理会触发 cuDNN 的算法选择,比较慢,属于预热 t0 = time.time() result, elapse = engine("/opt/test/sample.jpg") print(f"first run: {time.time() - t0:.3f}s") # 第二次开始才是真实性能 for i in range(5): t0 = time.time() result, elapse = engine("/opt/test/sample.jpg") print(f"run {i}: {time.time() - t0:.3f}s, stages={elapse}") for line in result: box, text, score = line print(f"{score:.3f}\t{text}")返回值是(result, elapse)二元组,result是一个列表,每一项是[文本框四点坐标, 识别文字, 置信度]。elapse里包含检测、方向分类、识别各阶段的耗时,这个数据非常有用,能直接告诉你瓶颈在哪。
第一次运行会明显比后面慢,因为 Paddle 和 cuDNN 要做算法选择、要加载权重、要分配显存池,这个预热过程通常要一两秒。别拿第一次的耗时去做性能评估,一定要跑够五次以上取稳定值。
验证 GPU 真的在工作,最直观的办法是另开一个终端跑nvidia-smi -l 1,观察推理时显存和 GPU 利用率有没有跳起来。如果显存一动不动,那百分之百还在跑 CPU,回头查paddle.device.get_device()的返回值。
我实测过的一组对比数据(同一张 1920×1080 的票据图片,同一台机器):CPU 版本单张约 800 ms 到 1.2 s,GPU 版本预热后稳定在 60 ms 到 90 ms 之间。差距在十倍以上,这个量级下 GPU 的价值是实打实的。但如果你的业务是每分钟才处理一两张图,那 CPU 版本其实完全够用,上 GPU 反而是浪费,这个账要算清楚。
5. 常见问题与排查实录
内网 + GPU + Python 这三样凑一起,出问题的概率是叠加的。我把实际踩过的坑整理成一张速查表,遇到报错先按表查,能省掉大部分瞎猜的时间。
5.1 报错速查表
| 报错信息 | 根本原因 | 处理办法 |
|---|---|---|
ImportError: libGL.so.1: cannot open shared object file | 缺 OpenCV 的系统依赖 | 离线装libgl1、libglib2.0-0 |
libcudnn.so.8: cannot open shared object file | cuDNN 路径没进LD_LIBRARY_PATH,或没装 | 检查ldconfig -p | grep cudnn,补环境变量 |
CUDNN_STATUS_VERSION_MISMATCH | cuDNN 大版本与 Paddle 期望不符 | Paddle 2.6 配 cuDNN 8.x,别用 9.x |
paddle.utils.run_check()输出 CPU | 装成了 CPU 版 paddle | pip list | grep paddle确认包名是paddlepaddle-gpu |
No matching distribution found | wheel 的 Python ABI 或平台不匹配 | 核对cp311、manylinux_x86_64是否与目标机一致 |
undefined symbol: xxx | 多版本 CUDA 库串了 | 清LD_LIBRARY_PATH,只留一个版本 |
ImportError: libgomp.so.1 | 缺 OpenMP 运行时 | 离线装libgomp1 |
| 模型下载超时或卡住 | 没预置模型文件 | 显式指定det_model_dir等路径 |
| 显存 OOM | 并发过高或rec_batch_num太大 | 降低批次数,或限制输入图尺寸 |
GLIBC_2.xx not found | 轮子在更高 glibc 环境编译 | 换对应平台的低版本轮子 |
RuntimeError: (PreconditionNotMet) | 显存碎片或 cuDNN 初始化失败 | 加FLAGS_allocator_strategy=naive_best_fit |
这张表覆盖的问题,我大概遇到过七成。剩下的三成属于"看日志看不出来,但重启一下就好了"的玄学范畴,没什么好办法,只能靠多试。
5.2 几个只有亲手做过才知道的细节
第一个细节:--no-deps用完之后要手动核对一遍依赖。rapidocr_paddle加了--no-deps之后,它声明的依赖一个都不会自动装,你必须在育种机上先记清楚它到底需要什么。方法是育种机上不加--no-deps装一遍,看pipdeptree -p rapidocr_paddle的输出,把paddlepaddle之外的都记下来,内网里逐个补。漏一个就报ModuleNotFoundError,来回搬介质很浪费时间。
第二个细节:pip 的缓存目录也会带来麻烦。内网机器上如果之前有人装过东西,~/.cache/pip里可能残留旧版本的 wheel。pip install --no-index --find-links的时候,pip 会优先用缓存里的包,导致你以为装的是 2.6.1,实际装的是 2.5.2。加--no-cache-dir能规避这个问题。
第三个细节:Pillow和numpy的版本要用育种机上冻结的那一版。这两个包更新很快,新版本偶尔会和 Paddle 的某些内部 API 打架。我在一次部署里用了最新版numpy(2.x),Paddle 2.6 直接报了一个完全看不懂的AttributeError,退到 1.26.x 就好了。所以锁文件里这类"基础设施型"的包,版本一定要钉死,不要让它自由浮动。
第四个细节:模型目录的权限。如果你的服务是用某个低权限用户跑的,模型目录的权限要放开到可读。我遇到过PermissionError导致模型加载失败,查了半天发现是目录权限 700,只有 root 能读。这种事说起来很小,但在内网里改一个权限要联系运维排期的时候,你就知道疼了。
第五个细节:把整个环境"快照"下来。目标机装通之后,立刻用pip freeze > /opt/ocr-env.lock存一份,同时把/opt/offline/wheels这个目录也备份一份到内网的共享存储上。下次再部署第二台机器,直接复制这两样东西,十分钟就能起来,不用再走一遍全流程。这个习惯我在第二次部署的时候受益巨大——同一批机器要装五台,第一台花了三天,后面四台一个下午全搞定。
第六个细节:注意 Ubuntu 23.10 的仓库归档问题。这个版本的常规维护期结束后,apt 仓库的位置会迁移,apt update会报 404 或者 GPG 错误。内网机器如果还需要装东西,就必须依赖本地源。所以在你还有外网条件的时候,把可能用到的 deb 尽量多下一点,宁可多占几百 MB 空间,也别到时候抓瞎。
5.3 关于是否值得上 GPU 的一点实际判断
最后说个务实的判断标准。GPU 版的部署复杂度比 CPU 版高一个数量级——驱动、CUDA、cuDNN 三个东西的版本矩阵要全部对上,内网环境下任何一个不对都要重新搬包。如果你的 OCR 需求是每天几百张图、对延迟要求不严,CPU 版完全够用,部署成本低得多,一个pip install rapidocr_onnxruntime就完事,连 paddle 都不用。
真正需要 GPU 的场景通常是这几类:批量离线处理上万张扫描件、在线服务要求单张百毫秒内返回、视频流实时抽帧识别、多路并发。这些场景下 GPU 的十倍提速是刚需,前期花两天搞环境完全值得。但如果只是"想知道 GPU 能不能跑",那就别折腾了,把这时间花在业务逻辑上收益更高。
我个人现在的做法是:先用 CPU 版在业务里跑起来,测出真实的吞吐瓶颈,再从瓶颈反推是否需要 GPU。这样即使最后决定上 GPU,也是有明确收益预期的决策,而不是为了技术而技术。完善的版本矩阵和这套离线打包流程我攒成了一套固定的脚本,放在内网共享目录里,新机器上手基本是照着文档走一遍的事,剩下的时间可以用来打磨识别效果本身——毕竟 OCR 的准确率高低,环境配得再漂亮也帮不上忙。