内网离线部署MonkeyOCRv2:Docker与GPU实战指南

发布时间:2026/9/28 13:24:48
内网离线部署MonkeyOCRv2:Docker与GPU实战指南 1. 为什么要在内网离线部署 MonkeyOCRv2内网离线部署这件事做过一次就再也不想碰第二次但偏偏很多场景又绕不开。MonkeyOCRv2 是一套文档解析能力相当强的 OCR 方案能处理表格、公式、多栏排版这些传统 OCR 容易翻车的内容。问题在于它的完整运行环境依赖一大堆东西PyTorch、CUDA 运行时、模型权重、推理引擎还有各种 Python 包。如果目标机器在内网、没有外网出口你就不可能靠pip install和docker pull现场拉取。我这次接到的需求很典型一台内网服务器装了一块 RTX 4060 Laptop GPU对就是笔记本显卡塞进台式机那种要求把 MonkeyOCRv2 跑起来全程离线。最终交付物是一个约 15GB 的 Docker 镜像导入即用。整个过程踩了不少坑从镜像分层、CUDA 版本对齐到 GPU 直通、显存调优每一步都有讲究。这篇文章就把完整过程拆开讲清楚包括那些文档里不会写的细节。先说清楚这套方案适合谁如果你手上有内网 GPU 服务器、需要部署文档解析类模型、又没法联网拉镜像那这篇基本可以照着抄。如果你只是想在自己电脑上玩玩那用官方在线方式更省事没必要折腾离线镜像。另外15GB 这个体积不是随便定的它包含了模型权重、CUDA 基础库和推理依赖后面会详细算这笔账。核心关键词先摆出来MonkeyOCRv2、Docker、GPU、NVIDIA、vLLM。这几个词贯穿全文也是整个部署链条的关键节点。2. 整体方案设计与选型思路2.1 为什么用 Docker 而不是裸机部署裸机部署 MonkeyOCRv2 最直接的问题是环境不可复现。内网机器上可能已经装了别的 Python 项目CUDA 版本、cuDNN 版本、PyTorch 版本互相打架是家常便饭。我见过太多在我机器上能跑的案例最后都死在依赖冲突上。Docker 的价值在这里体现得很明显把整个运行环境连同模型权重一起打包镜像导入后就是一个自包含的黑盒。内网机器只要有 Docker 和 NVIDIA 驱动剩下的都不用管。而且镜像可以版本化管理今天跑通的版本半年后重新导入还是同样的结果。提示内网离线场景下Docker 镜像的自包含特性比在线场景更重要因为在线场景你还能临时补装依赖离线场景一旦缺东西就是死路。2.2 基础镜像怎么选CUDA 版本对齐是第一道坎基础镜像的选择直接决定了后面所有环节的顺畅程度。我的原则是基础镜像的 CUDA 版本必须和宿主机 NVIDIA 驱动支持的 CUDA 版本兼容同时要满足 PyTorch 和 vLLM 的最低要求。这里有个常见的误区很多人以为镜像里的 CUDA 版本要和宿主机完全一致。其实不是。宿主机装的是 NVIDIA 驱动驱动向下兼容多个 CUDA 版本。真正要对齐的是镜像内 CUDA 运行时和 PyTorch 编译时用的 CUDA 版本。我最终选的是nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04作为基础镜像。理由有三点CUDA 12.1 对 RTX 40 系列Ada Lovelace 架构支持完善4060 属于这一代cuDNN 8 是 PyTorch 2.x 的标配runtime 版本比 devel 版本小很多省体积因为我们不需要在镜像里编译 CUDA 代码如果你用的是更老的显卡比如 MI50 那种CUDA 版本可能要降到 11.x这时候 PyTorch 和 vLLM 的版本也要跟着调整链条会复杂不少。2.3 推理引擎vLLM 还是原生 PyTorchMonkeyOCRv2 的推理部分可以用原生 PyTorch 跑也可以接 vLLM 做加速。两者的取舍很明确方案优势劣势适用场景原生 PyTorch依赖少、镜像小、调试直观吞吐低、显存利用差单张图偶尔跑、验证阶段vLLM吞吐高、显存管理好、支持连续批处理依赖多、镜像大、版本敏感批量文档解析、生产环境我这次选了 vLLM因为需求是批量处理文档吞吐量是硬指标。代价就是镜像体积上去了vLLM 本身加上它的依赖xformers、flash-attn 之类能占好几个 GB。注意vLLM 对 CUDA 版本和 PyTorch 版本非常敏感版本组合不对会直接报编译错误或者运行时崩溃。选版本前一定要查 vLLM 官方 release notes 里标注的兼容矩阵。2.4 15GB 体积是怎么来的很多人看到 15GB 第一反应是怎么这么大。拆开看就明白了基础 CUDA 镜像约 2.5GBPyTorch torchvision torchaudio约 3GBvLLM 及其依赖含编译好的算子约 2GBMonkeyOCRv2 模型权重约 5GB其他 Python 包和系统依赖约 1.5GB剩余为镜像层开销和缓存模型权重占大头这个没法省。能优化的是基础镜像和依赖部分比如用 runtime 而非 devel、清理 pip 缓存、合并 RUN 指令减少层数。后面实操部分会讲具体怎么压。3. 构建环境准备与关键细节3.1 构建机与目标机的分工离线部署有个容易被忽略的点构建镜像的机器和目标运行镜像的机器往往不是同一台。构建机需要联网拉基础镜像、装依赖目标机在内网只负责导入和运行。这就带来一个约束构建机上的 GPU 型号最好和目标机一致或者至少架构相同。因为 vLLM 在安装时会根据当前 GPU 架构编译算子如果构建机是 A100 而目标机是 4060编译出来的算子可能不兼容。我的做法是在一台同样装了 4060 的机器上构建确保架构一致。如果实在没有同型号机器可以在构建时指定TORCH_CUDA_ARCH_LIST环境变量手动指定目标架构。4060 对应的计算能力是 8.9所以设置成8.9即可。export TORCH_CUDA_ARCH_LIST8.93.2 NVIDIA 驱动与容器工具链目标机要跑 GPU 容器必须装两样东西NVIDIA 驱动和 NVIDIA Container Toolkit。驱动版本要满足镜像内 CUDA 的最低要求CUDA 12.1 需要驱动版本 530 以上。Container Toolkit 是让 Docker 能识别 GPU 的关键。装好之后docker run时加--gpus all才能把 GPU 透传进容器。验证方法很简单docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi能打印出显卡信息就说明链路通了。这一步不通后面全白搭。提示内网机器装 Container Toolkit 也需要离线包。提前在有网机器上下好 deb 包和依赖一起拷进去。别等到现场才发现装不了。3.3 目录结构规划构建过程涉及不少文件提前规划好目录能省很多事。我的结构是这样的monkeyocr-deploy/ ├── Dockerfile ├── requirements.txt ├── models/ │ └── monkeyocrv2/ # 模型权重 ├── scripts/ │ ├── entrypoint.sh │ └── healthcheck.sh └── build.sh模型权重单独放一个目录构建时 COPY 进去。这样如果模型更新了只需要替换这个目录不用动 Dockerfile。4. Dockerfile 编写与镜像构建实操4.1 Dockerfile 分层策略写 Dockerfile 的核心思路是把变化频率低的内容放前面变化频率高的放后面这样重建时能复用缓存。同时要尽量合并 RUN 指令减少镜像层数。下面是我实际用的 Dockerfile 骨架关键部分都加了注释# 基础镜像CUDA 12.1 cuDNN 8 Ubuntu 22.04 FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 # 设置时区和编码避免中文乱码 ENV TZAsia/Shanghai ENV LANGC.UTF-8 ENV LC_ALLC.UTF-8 # 一次性装完系统依赖并清理缓存减少层数 RUN apt-get update apt-get install -y --no-install-recommends \ python3.10 \ python3-pip \ python3.10-dev \ libgl1-mesa-glx \ libglib2.0-0 \ libsm6 \ libxext6 \ libxrender-dev \ rm -rf /var/lib/apt/lists/* # 升级 pip 并配置国内源构建机联网时加速 RUN python3 -m pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple # 安装 PyTorch指定 CUDA 12.1 版本 RUN pip install --no-cache-dir \ torch2.1.2cu121 \ torchvision0.16.2cu121 \ --index-url https://download.pytorch.org/whl/cu121 # 安装 vLLM 及其他依赖 COPY requirements.txt /tmp/requirements.txt RUN pip install --no-cache-dir -r /tmp/requirements.txt # 拷贝模型权重 COPY models/monkeyocrv2 /app/models/monkeyocrv2 # 拷贝启动脚本 COPY scripts/ /app/scripts/ RUN chmod x /app/scripts/*.sh WORKDIR /app ENTRYPOINT [/app/scripts/entrypoint.sh]这里有几个细节值得展开说。第一--no-install-recommends能省掉大量非必要的系统包镜像能小几百 MB。第二rm -rf /var/lib/apt/lists/*必须在同一个 RUN 里执行否则清理不掉因为分层机制会把上一层的数据保留下来。第三pip 的--no-cache-dir同理避免缓存留在镜像里。4.2 requirements.txt 的版本锁定vLLM 的依赖版本必须锁死否则不同时间构建出来的镜像行为可能不一致。我的 requirements.txt 长这样vllm0.4.2 transformers4.40.0 tokenizers0.19.1 accelerate0.29.3 sentencepiece0.2.0 opencv-python-headless4.9.0.80 pillow10.3.0 numpy1.26.4 pydantic2.7.1 fastapi0.111.0 uvicorn0.29.0版本之间是有依赖关系的。比如 vLLM 0.4.2 要求 transformers 在 4.40 附近tokenizers 要匹配 transformers 的版本。这些约束在 vLLM 的 setup.py 里有声明但 pip 解析时不一定能选到最优组合手动锁定最稳妥。注意numpy 一定要锁在 1.xnumpy 2.x 和很多老库不兼容会引发一堆莫名其妙的报错。这个坑我踩过排查了半天才发现是 numpy 版本问题。4.3 模型权重的处理模型权重有 5GB 左右直接 COPY 进镜像会让构建变慢但这是离线部署必须付出的代价。有个优化技巧把权重单独做成一个层放在 Dockerfile 最后。这样如果只是改脚本重建时不会重新拷贝权重。另外权重文件如果是多个分片比如pytorch_model-00001-of-00002.bin确保全部拷贝完整。少一个分片加载时会报错而且报错信息往往不直观。4.4 构建命令与体积优化构建命令本身很简单docker build -t monkeyocrv2:offline-v1 .但构建完之后可以用docker history看看各层大小找出体积大头docker history monkeyocrv2:offline-v1 --human --no-trunc如果发现某一层特别大回头检查是不是忘了清理缓存。我第一版构建出来 18GB查下来是 pip 缓存和 apt 列表没清干净优化后降到 15GB。导出镜像用docker save monkeyocrv2:offline-v1 -o monkeyocrv2-offline-v1.tar这个 tar 包就是最终要拷进内网的交付物。15GB 的 tar 包用移动硬盘拷比网络传输靠谱。5. GPU 直通与显存调优实战5.1 容器内 GPU 识别验证镜像导入内网机器后第一步是验证 GPU 能不能被容器识别docker load -i monkeyocrv2-offline-v1.tar docker run --rm --gpus all monkeyocrv2:offline-v1 nvidia-smi如果这一步报错could not select device driver说明 Container Toolkit 没装好或者没重启 Docker 服务。如果报错no CUDA-capable device说明驱动有问题。能打印出 nvidia-smi 信息后再进容器验证 PyTorch 能不能看到 GPUdocker run --rm --gpus all monkeyocrv2:offline-v1 python3 -c import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))输出True和NVIDIA GeForce RTX 4060 Laptop GPU就对了。5.2 显存分配策略4060 Laptop 的显存通常是 8GB这个容量跑 MonkeyOCRv2 需要精打细算。vLLM 有个gpu_memory_utilization参数控制它可以使用多少比例的显存默认 0.9。对于 8GB 显存我建议设成 0.85 左右留一点给系统和其他进程。设太高容易 OOM设太低浪费显存。from vllm import LLM llm LLM( model/app/models/monkeyocrv2, gpu_memory_utilization0.85, max_model_len4096, tensor_parallel_size1, )max_model_len也要注意它决定了 KV Cache 的最大长度。设太大显存不够设太小长文档处理不了。4096 是个比较平衡的值具体要看你的文档长度分布。5.3 批处理参数调优vLLM 的吞吐优势来自连续批处理但批大小不是越大越好。显存有限的情况下要找到吞吐和显存的平衡点。我的实测数据4060 8GB处理 A4 扫描件批大小显存占用吞吐页/秒是否稳定14.2GB1.8稳定45.8GB5.2稳定87.1GB7.5稳定16OOM-崩溃从数据看批大小 8 是这张卡的甜点。再往上就爆显存了。这个值因卡而异建议自己压测一遍。提示压测时用真实文档别用空白图。空白图的显存占用和真实文档差很多压测结果没有参考价值。5.4 显存碎片与长时间运行长时间运行后显存碎片会导致原本能跑的批大小突然 OOM。vLLM 内部有显存池管理但也不是万能的。我的应对策略是设置一个略低于理论上限的批大小留出碎片余量。比如理论上能跑 10我就设 8。另外可以开启 vLLM 的swap_space参数把部分 KV Cache 换到内存缓解显存压力。代价是速度会降但稳定性提升明显。llm LLM( model/app/models/monkeyocrv2, gpu_memory_utilization0.85, swap_space4, # 单位 GB )6. 常见问题与排查技巧实录6.1 镜像导入后启动失败现象docker run后容器立刻退出日志显示找不到某个 .so 文件。原因基础镜像的 CUDA 版本和目标机驱动不兼容或者某个系统库没装。排查先看docker logs定位缺失的库。如果是 CUDA 相关检查驱动版本是否满足镜像要求。如果是普通系统库在 Dockerfile 里补装。6.2 GPU 可见但推理报错现象torch.cuda.is_available()返回 True但推理时报CUDA error: no kernel image is available。原因vLLM 编译时的目标架构和实际 GPU 架构不匹配。比如构建机是 A1008.0目标机是 40608.9。解决构建时设置TORCH_CUDA_ARCH_LIST8.9重新编译 vLLM 算子。6.3 显存不足的几种表现显存不足不一定报 OOM有时表现为推理结果异常、进程被 kill、或者速度骤降。常见原因和处理方式表现可能原因处理方式直接 OOM 报错批大小过大降低批大小进程被 kill系统 OOM Killer 介入降低 gpu_memory_utilization速度骤降显存碎片重启容器降低批大小结果异常KV Cache 溢出降低 max_model_len6.4 离线环境的依赖缺失内网机器最怕的就是差一个包。构建镜像时要把所有可能的依赖都装进去宁可多装不可少装。我习惯在构建完后用一个干净的容器跑一遍完整流程确认没有遗漏。另外Python 包有时候会动态加载一些系统库比如 opencv 依赖 libGL这些在 requirements.txt 里看不出来要在 Dockerfile 里显式安装。6.5 镜像体积反复膨胀如果多次构建后发现镜像越来越大多半是缓存没清干净。检查 Dockerfile 里每个 RUN 指令确保 apt 和 pip 的缓存都在同一层清理。另外docker system prune可以清理构建缓存但不会影响已有镜像。7. 部署后的运维与扩展思路镜像跑起来只是开始后面还有运维的事。我一般会加一个健康检查脚本定期验证推理服务是否正常#!/bin/bash # healthcheck.sh curl -f http://localhost:8000/health || exit 1配合 Docker 的--health-cmd使用容器异常时能自动重启。如果后续要处理更大的文档量单张 4060 肯定不够。扩展方向有两个一是加卡做张量并行vLLM 支持tensor_parallel_size参数二是多实例部署用负载均衡分发请求。前者对显存要求高后者更灵活。模型更新时只需要替换模型权重目录重新构建镜像即可。Dockerfile 不用动因为权重是 COPY 进去的改权重不影响前面的层缓存。最后分享一个我在实际部署中总结的小技巧把构建好的镜像 tar 包和对应的 Dockerfile、requirements.txt 一起归档。半年后要复现环境时光有 tar 包不够还得知道它是怎么构建出来的。这三个文件放一起才算一个完整的交付物。