☰
15GB OCR镜像内网离线部署:Docker构建与GPU调优实战
2026/10/1 15:33:27 网站建设 项目流程

用内网离线部署跑一个 15GB 的 Docker 镜像,第一感觉是什么?我当时的反应是:MonkeyOCRv2 这个 OCR 工具看起来只是"识别文字",怎么镜像能撑到 15GB?后来把镜像分层拆开看,才发现这里面一半以上都不是模型权重,而是 PyTorch、CUDA runtime 和各种推理依赖。这篇文章把我从构建镜像、导出压缩、内网导入,到 GPU 驱动对齐、容器 runtime 配置、推理速度调优的完整过程写出来,按可直接复现的顺序展开,给同样被"内网 + 大镜像 + GPU"组合折磨过的同学做个参考。

1. 15GB 的镜像到底装了什么

先别急着写 Dockerfile,搞清楚 15GB 从哪来,后面才知道哪些层能压缩、哪些层动不得。我当时的误区是以为 OCR 模型权重占了绝大头,实际上权重只占三分之一左右。

1.1 模型权重:只是其中一部分

MonkeyOCRv2 属于视觉-语言模型路线,由视觉编码器、多模态投影层和 LLM 主干组成。以常见的 2B 量级 LLM 主干为例,FP16 精度下光权重文件就在 4GB 到 5GB 左右。再加上视觉塔(比如 ViT 系列的 backbone)和投影层,模型目录整体会到 5GB 上下。

为什么不是 FP32?因为镜像里的模型通常以 FP16 保存,推理时也按半精度加载。FP32 存储会让单份权重翻倍,对 OCR 文本识别任务收益几乎看不见,但体积会从 15GB 直接膨胀到 25GB+。

1.2 把 CUDA runtime 和推理框架算进去

如果从裸 CUDA 基础镜像开始构建,torch 2.x 系列安装后自带的 CUDA 依赖库很占空间。以 cu118 版本为例,仅 torch 相关的 CUDA so 文件就在 2GB 上下,再加上 TensorRT 可选组件、cuDNN、OpenCV(这个常常被忽略,但 libopencv 系列能吃掉几百 MB),以及 transformers、accelerate、sentencepiece、jieba、Pillow 这些常用库,环境层很容易堆到 7GB 左右。

所以一个大致的体积分布是:模型权重 4~5GB,Python 环境与 CUDA runtime 7~8GB,系统库与配置文件 1~2GB,加起来刚好在 15GB 这个量级。后面做 Dockerfile 时,我会刻意利用这个构成来优化构建顺序。

1.3 15GB 的耐人寻味之处:分层与缓存体积

Docker 镜像的仓库体积和实际占用是两回事。15GB 是整个镜像的展开体积,但 Docker 是按层存储的,如果你把模型 COPY 放到 Dockerfile 最后一步,模型层改变时,前面环境层可以复用缓存。可 15GB 导入内网之后,docker system df看到的占用会比 15GB 更大,因为镜像加载后还有一层容器可写层。所以我在导出前习惯先清理构建缓存和中间层,否则内网机器可能要准备 30GB 甚至 40GB 的/var/lib/docker分区空间。

2. 在联网机器上"播种":离线镜像的构建链路

内网部署的典型做法,是在一台能联网的构建机上把镜像完整打好,再 export 进去。这台构建机的条件决定了后面所有坑的数量。

2.1 基础镜像怎么选

我建议从nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04起步,而不是pytorch/pytorch镜像。原因是 OCR 推理经常要动态加载一些编译扩展,devel 版本自带 nvcc,后面遇到 flash-attn 这类需要就地编译的库不会卡死。如果你确定完全不需要编译,可以换 runtime 版,体积还能再少 1GB 左右。

CUDA 选 11.8 而不是 12.x,核心考虑是兼容性。离线环境里的宿主机驱动往往不敢随便升级,11.8 对既有驱动的兼容范围比 12.x 宽。这个选择直接影响第四章的 GPU 落地。

2.2 Dockerfile 的关键顺序与依赖锁定

Dockerfile 的层顺序决定了构建失败时的返工成本。我的写法是:

FROM nvidia/cuda:11.8.0-cudnn8-devel-ubuntu20.04 ENV DEBIAN_FRONTEND=noninteractive \ PIP_CACHE_DIR=/root/.cache/pip \ TZ=Asia/Shanghai RUN sed -i 's@/archive.ubuntu.com@/mirrors.aliyun.com@g' /etc/apt/sources.list \ && apt-get update \ && apt-get install -y --no-install-recommends \ libgl1 libglib2.0-0 libsm6 libxext6 libxrender1 \ fonts-wqy-zenhei \ python3.10 python3.10-distutils curl \ && ln -sf /usr/bin/python3.10 /usr/bin/python RUN curl -sS https://bootstrap.pypa.io/get-pip.py | python3 \ && pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple COPY requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt

顺序上有三个讲究:

  • 先装系统库,再装 python 依赖。因为libgl1这类库是 OpenCV 运行的必要前提,缺了它 OCR 服务的图片解码阶段会直接报错,但很多人构建时根本没注意。
  • requirements.txt单独 COPY,目的就是利用 Docker layer cache。后面代码或模型层改动时,pip install 这层不用重跑。15GB 镜像里这层占了 7GB 左右,能跳过就是几分钟和半小时的差距。
  • 中文字体一定提前装,MonkeyOCR 的输出可视化、PDF 渲染都依赖字体,内网容器里没有字体时中文会糊成一团,这种问题排查起来非常隐蔽。

requirements.txt必须锁死版本,至少锁住 torch、transformers、accelerate 三个关键依赖。我见过不止一次因为 transformers 小版本升级导致 OCR 的 tokenizer 加载行为变化,识别结果开始出现乱码。内网环境没有 pip 可回滚,锁版本比什么都重要。

2.3 构建加速

构建机上即便联网,也建议先把 pip 源切到国内镜像,清华源在多数情况下比默认源稳定。另一个大坑是 flash-attn 这类库,pip 在线安装时经常要现场编译,C++ 编译很吃内存。我后来改成先单独pip download出 wheel 包,再在 Dockerfile 里pip install --no-cache-dir ./flash_attn*.whl,构建时间从半个多小时缩短到几分钟。

构建命令建议这样打标签:

docker build -t monkey-ocr:v2.0 -f Dockerfile . docker image ls

验证完这一步,确保容器能正常启动,再做导出。千万别在没跑过 demo 的情况下直接 save。我在这上面吃过亏,镜像看起来一切正常,load 到内网才发现入口脚本少一个可执行权限。

2.4 构建后的验证

导出一个 15GB 的镜像之前,先在本机跑一次完整流程:

docker run --rm --gpus all \ -e CUDA_VISIBLE_DEVICES=0 \ monkey-ocr:v2.0 \ python -c "import torch; print(torch.cuda.is_available())"

这一步同时验证了 GPU 透传、驱动版本兼容和 torch 的 CUDA 初始化。构建机上这一关过不去,到内网一定会出问题。如果构建机没 GPU,至少要确认torch.cuda.is_available()返回 False 时服务也能走 CPU 兜底,否则离线机器上一个小驱动问题就会让整个服务崩溃。

3. 15GB 怎么进入隔离区:导出、压缩、导入

从联网构建机到内网机器,中间往往隔着一道物理隔离设备。15GB 的镜像怎么安全过去,是部署链路里最容易翻车的一段。

3.1 导出前先做镜像瘦身

镜像导出前能做的瘦身有两类。一类是清理 apt 和 pip 缓存,在 Dockerfile 里用rm -rf清理;另一类是识别那些构建时有用、运行时没用的层,如果服务不需要编译扩展,构建完的镜像里可以把 nvcc 相关文件清掉一部分,但操作风险偏高,我不太推荐新手做。

更稳妥的做法是把不必要的环境和数据集挪到镜像外部,通过 volume 挂载进容器。比如 OCR 服务的根目录里如果有测试图片、训练日志这类非必需内容,就让它留在宿主机,不要 COPY 进镜像。

3.2 压缩和分卷

docker save输出的是未压缩的 tar 流,15GB 的镜像直接 save 出来还是 15GB,传起来很痛苦。我的做法是 save 之后做一层带压缩的传输:

docker save monkey-ocr:v2.0 | gzip -c > monkey-ocr-v2.tar.gz

实测下来,torch 和 CUDA 库这类二进制文件压缩率有限,但能压到 10GB 左右,模型权重部分几乎压不动。如果内网带宽还行,这点收益已经值得。带宽很差的话,改用 zstd 压缩率未必更高,但速度明显更快:

docker save monkey-ocr:v2.0 | zstd -T0 -o monkey-ocr-v2.tar.zst

内网传输大文件时,我强烈建议不要走 scp 裸传,而是用 rsync 配合校验。15GB 的文件传一半断了,scp 只能重头再来,rsync 可以断点续传,对隔离网环境下反而更实用。如果物理介质拷贝,记得拿md5sum或者sha256sum校验,源和目的两边比对一致再继续。

3.3 导入与校验

镜像到内网机器后的导入命令很直接:

docker load -i monkey-ocr-v2.tar.gz

但导入前一定要确认磁盘空间。docker load需要保存镜像每一层的展开内容,实际占用约 15GB,而这个文件本身还在磁盘上。也就是说目标分区最好预留 30GB 以上,否则会看到令人头皮发麻的no space left on device。导入后顺便跑一遍:

docker images docker system df

确认镜像列表里出现monkey-ocr:v2.0。如果标签是空白的,用docker tag补一下名字,内网机器的镜像名最好和构建机上保持一致,减少后面写部署脚本时的心智负担。

4. GPU 落地的三件套:驱动匹配、容器运行时与启动参数

镜像进了内网,能docker load不代表能跑 GPU。这一步最容易被当成"能启动就是成功",实际上 GPU 容器有自己的一整套链路。

4.1 宿主机驱动的"最低消费"

容器内的 CUDA 不是宿主机驱动提供的,而是镜像自带的 CUDA runtime。Docker 只负责把宿主机的 NVIDIA 设备透传给容器,真正驱动 GPU 的是宿主机内核模块。所以宿主机驱动版本必须足够新,才能支持镜像内的 CUDA 库。

判断方式是在宿主机执行:

nvidia-smi

看右上角的 Driver Version 和 CUDA Version。比如镜像用的是 CUDA 11.8,宿主机驱动 525 系列可以覆盖,470 系列就比较麻烦。对应关系大致是:

  • 宿主机驱动 470:最高支持 CUDA 11.4 左右,跑 cu118 镜像大概率会初始化失败
  • 宿主机驱动 520/525:支持 CUDA 12.0/12.1,向下兼容 11.x 没问题
  • 宿主机驱动 535+:基本通吃 11.8 到 12.2

如果内网机器驱动过旧,优先改容器基础镜像到匹配的 CUDA 版本,而不是去升级宿主机驱动。内网升级驱动的代价很高,而且容易碰硬件兼容问题。

4.2 离线安装 nvidia-container-toolkit

Docker 本身不会把 GPU 自动塞进容器,靠的是nvidia-container-toolkit。内网环境装这个工具,得先在联网机器上下载对应系统的安装包:

# 在联网机器上,以 Ubuntu/Debian 为例 wget https://nvidia.github.io/libnvidia-container/gpgkey curl -s -L https://nvidia.github.io/nvidia-container-runtime/gpgkey | apt-key add - apt-get update apt-get download nvidia-container-toolkit-base nvidia-container-toolkit

拿到 deb 包后拷贝到内网安装:

dpkg -i nvidia-container-toolkit*.deb nvidia-ctk runtime configure --runtime=docker systemctl restart docker

runtime configure这步做的事情是修改/etc/docker/daemon.json,把 nvidia runtime 注册进 dockerd。完成后验证:

docker info | grep -i runtime

看到runc和nvidia同时出现,GPU runtime 就算注入了。这一步漏掉,容器启动时--gpus all会直接报could not select device driver "" with capabilities: [[gpu]],非常典型的症状。

4.3 容器启动参数细节

GPU 服务容器的启动命令,除了--gpus all之外还有几个容易被忽视的关键参数。我的完整示例是这样的:

docker run -d --name monkey-ocr-service \ --gpus all \ --shm-size=8g \ -e CUDA_VISIBLE_DEVICES=0 \ -e OMP_NUM_THREADS=8 \ -p 8080:8080 \ -v /data/models:/models \ -v /data/logs:/logs \ monkey-ocr:v2.0

--shm-size=8g是很多 PyTorch 服务翻车的元凶。DataLoader 多进程和某些分布式特性依赖/dev/shm,Docker 默认只给 64MB,OCR 服务一旦加载 batch 数据就容易 OOM。看上去是显存不够,实际上共享内存不够。

CUDA_VISIBLE_DEVICES=0的语义是容器内的 GPU 编号映射,宿主机有多卡时,如果想指定物理卡 2 上跑,这个变量要跟着调整。还有一个容易踩的坑是--gpus all和CUDA_VISIBLE_DEVICES混用时的行为差异,我后来直接固定成--gpus '"device=2"'这种写法,语义更明确。

如果服务想通过 docker compose 管理,对应配置也简单:

services: monkey-ocr: image: monkey-ocr:v2.0 runtime: nvidia shm_size: "8g" environment: - CUDA_VISIBLE_DEVICES=0 ports: - "8080:8080" volumes: - /data/models:/models - /data/logs:/logs

5. GPU 调优三板斧:精度、批量、并发

容器跑起来了,能用和好用是两回事。15GB 镜像里的 OCR 模型在高清扫描件上做推理,如果不调参,单张图可能耗时几秒甚至卡出显存 OOM。我做调优时主要动三个方面。

5.1 精度档位与显存占用

OCR 模型在 GPU 上跑,最大的显存包袱是 KV cache 和中间激活值,模型权重只是固定占用。以 FP16 精度加载时,2B 量级模型权重约 4.5GB,加上 4K 上下文的 KV cache,稳定运行需要 10GB 到 12GB 显存。如果内网机器是 16GB 显存的卡,默认配置差不多能跑,但并发一上来就会紧。

显存确实不够时,可以先降 KV cache 长度,OCR 单图识别通常用不到很长的上下文,把max_seq_len从 8K 收到 2K,显存下降非常明显。还不够就上 4bit 量化,权重降到 2.5GB 左右,但精度会有肉眼可见的损失,长文档和复杂表格场景谨慎使用。

5.2 推理速度的三个调整点

第一个是推理精度设置为 FP16 而不是 FP32。在绝大多数 GPU 上 FP16 的吞吐比 FP32 高接近一倍,OCR 输出质量几乎无差别,这是零成本优化。

第二个是加载模型时固定 batch。OCR 服务如果一次只处理一张图,batch=1 时 GPU 利用率通常只有 20% 到 30%。通过动态 batching 把并发的几张图拼成一个 batch,吞吐能明显改善。前提是注意 KV cache 的显存占用,batch 从 1 涨到 4,显存可能是线性往上走。

第三个是解码策略。OCR 场景默认用 greedy decoding 就好,别上 beam search。beam size 4 的耗时几乎是 greedy 的 2 倍,对正确率的提升约等于零。这个坑很多人会踩,看到模型的论文里有 beam search 就照抄,实际上对 OCR 落地场景收益很小。

5.3 用实际监控数据说话

调优之后,别凭感觉判断效果。我会用nvidia-smi dmon持续监控 GPU 利用率和显存变化:

nvidia-smi dmon -s pucm -d 5

重点看 sm(流式多处理器利用率)和 mem 两列。sm 始终在 10% 以下,说明瓶颈在数据加载或 CPU,要从--num-workers和图像解码侧找问题。显存接近上限但 sm 只有 30%,说明批次太大,反而拖低了并发能力。

性能对比我一般会做成一张表:

配置单图耗时显存占用GPU 利用率效果
FP32 + batch=11.8s11.2GB18%正常
FP16 + batch=10.9s6.8GB32%正常
FP16 + batch=40.4s/张11.5GB68%正常
INT4 量化 + batch=40.3s/张4.2GB75%细节损失

内网机器上如果持续 OOM,我会第一时间看两件事:NVIDIA 驱动是否真的把显存透传给了容器,以及/dev/shm是不是又满到 100%。前者是环境问题,后者是参数问题,排查方向完全不同。

6. 这次踩坑后的完整排查链路

最后聊一个典型故障场景,也是整个部署过程中最容易让运维和算法互相甩锅的一环:容器启动报 GPU 相关错误时,怎么一步步确认问题出在哪。

6.1 现象:--gpus all 直接报错

当时内网机器的现象是:

docker run --rm --gpus all monkey-ocr:v2.0 nvidia-smi

输出:

docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]].

这个报错的意思是 dockerd 在创建容器时,找不到能提供 GPU 设备的 runtime。它不一定是驱动问题,更可能是 nvidia-container-toolkit 没有被注册到 Docker runtime 里。先别急着重装驱动,按下面的链路逐级看。

6.2 从驱动到 dockerd 的逐级确认

第一步,确认宿主机能看到 GPU:

nvidia-smi

如果这个命令正常显示显卡信息,说明驱动层没问题,问题在容器运行时。如果nvidia-smi也失败,那先看驱动加载状态,dkms status和dmesg | grep -i nvidia是首选排查命令。

第二步,确认 nvidia-container-toolkit 是否安装:

dpkg -l | grep nvidia-container-toolkit

第三步,确认 runtime 配置有没有写进 Docker 配置:

cat /etc/docker/daemon.json

正确的结果应该是default-runtime和runtimes里都有 nvidia 条目。如果没有,重新执行:

nvidia-ctk runtime configure --runtime=docker systemctl restart docker

第四步,回到 Docker 本身。如果 Docker 服务都起不来,systemctl status docker和journalctl -u docker -n 50是关键。有时候是权限问题,比如当前用户不在 docker 组,报错是:

permission denied while trying to connect to the docker api at unix:///var/run/docker.sock

这个不是 GPU 问题,但很常见。把用户加入 docker 组或者用 root 执行,问题就消失了。

到这一步,99% 的--gpus all启动失败都能定位到具体层。如果都正常但容器启动仍然报 CUDA init 失败,再用容器内的nvidia-smi对照容器的 CUDA 版本与驱动支持范围差异。

6.3 内网离线场景的其他附加坑

  • 缺中文字体导致 OCR 输出乱码,排查半天才发现是容器里没有fonts-wqy-zenhei。字体文件在构建阶段就要 COPY 进镜像,别指望内网 apt 源。
  • 内网部署要跑不同的模型版本时,镜像里的requirements.txt和校验和文件最好单独保留一份,不然半年后镜像更新,没人记得当时用的 torch 是哪个版本。
  • 下载基础镜像时如果构建机也是国内网络,Docker Hub 的拉取经常会很慢或者直接失败。提前配置 registry mirror 或者用代理拉取到本地再 save,是最稳妥的方案。

我自己经历过一次整套排查下来,最后发现只是 daemon.json 没生效,Docker 没重启。所以现在写部署文档的时候,都会把"修改配置后必须重启 dockerd"单独标红。内网离线环境试错成本高,每一步验证清楚,比赶进度重要得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询