搞离线部署的朋友应该都体会过这种滋味:外面环境里明明跑得好好的服务,一到内网就变得无比拧巴。依赖拉不下来、模型权重动辄几个 G、Docker 镜像在网络不好的时候传到怀疑人生。MonkeyOCRv2 这个项目当初就卡在这一环——一个内部封装的光学字符识别推理引擎,模型权重加推理框架加 CUDA 底座最终打包出 15GB 的 Docker 镜像,要越过互联网边界完整搬到机房内网里。听起来挺吓人,但把它拆开看,无非就是“构建、导出、传输、加载、调优”五件事,每件事都有对应的绕坑方法。
这篇文章会围绕 MonkeyOCRv2 的实际部署过程,重点讲三块内容:第一,15GB 这么大的镜像到底是怎么一层一层搭起来的,每层该装什么;第二,从有网环境到内网环境的离线搬运方案,包括镜像压缩、拆分、Docker Registry 中转这些土办法;第三,镜像进了内网之后,GPU 显存和推理延迟怎么调,NVIDIA 容器运行时那些参数怎么配。适合正在做模型服务容器化、内网安全区部署,或者只是想把 Docker 镜像折腾得更小更快的人参考。
1. 内容整体设计与思路拆解
1.1 先拆开看:MonkeyOCRv2 这个镜像里到底装了什么
很多人拿到项目就急着写 Dockerfile,结果构建出来镜像跟气球一样,传也传不动,跑起来还各种缺库。其实正确的做法是先盘点内容。MonkeyOCRv2 从功能上拆,包含五部分:
- 模型权重文件:OCR 识别模型、检测模型、方向分类模型,加起来大概 5 到 6GB;
- Python 推理环境:Python 3.10 解释器、PyTorch、ONNX Runtime、OpenCV、NumPy 等科学计算库,装完之后 Python 环境目录能到 4GB 左右;
- CUDA 与 cuDNN 运行时库:这是 GPU 推理的基础,NVIDIA 官方基础镜像本身就占了几个 GB;
- 预处理和后处理代码:图像缩放、裁剪、文本行拼接、坐标转换等脚本,代码量不多但依赖不少;
- 服务入口:FastAPI 或 gRPC 服务、监控接口、日志收集配置。
把这五块加起来,15GB 并不夸张。而且这里面真正“有价值”的其实只有模型权重和你写的服务代码,其余全是环境依赖。理解这一点非常重要,因为后面所有优化动作都围绕一个目标:让环境和权重分开管理,构建的时候环境层只做一次,模型权重层可以单独替换。
1.2 15GB 是怎么凑出来的:权重、依赖和底层库的比例
我习惯用 du 命令把所有目录扫一遍,搞清楚空间都花在哪了。以下是我实测的一个典型比例,虽然不是绝对精确,但足以说明问题:
| 内容 | 安装后大小 | 备注 |
|---|---|---|
| CUDA 12.1 runtime + cuDNN | 约 5.2GB | 使用 nvidia/cuda:12.1-runtime 作为基础镜像 |
| Python 环境与 pip 依赖 | 约 3.8GB | PyTorch 的 CUDA 扩展占了三分之二 |
| 模型权重目录 | 约 5.5GB | 包括检测、识别、方向分类模型 |
| 应用代码及静态资源 | 约 0.8GB | FastAPI、配置、字体文件、自定义算子 |
| 系统基础工具与缓存 | 约 0.5GB | apt 缓存、pip 缓存、临时文件 |
这里有个容易踩的坑:很多人构建完镜像后发现比这个还大,多出来的基本都是缓存。apt 装完的 /var/lib/apt/lists、pip 的 wheel 缓存、PyTorch 的扩展编译临时文件,每一块看着不大,叠加起来轻松超过 1GB。后续构建阶段里把这些清理掉,镜像体积能肉眼可见地缩水。
1.3 为什么选择 Docker 而非裸机部署或 conda 环境
MonkeyOCRv2 一开始在开发机上用的是 conda 环境,团队协作时每个人装的东西还不太一样,这个版本能跑那个版本不能跑,后来干脆切换到 Docker。理由很现实:
第一,模型依赖的 CUDA 版本和宿主机的驱动不一定匹配,Docker 容器里封装好 CUDA 运行库,就可以避免“改一个环境把另一套服务带崩”的问题。第二,内网部署往往有多个业务系统共用 GPU 服务器,裸机安装一个 Python 环境可能和现有服务抢依赖,Docker 的隔离机制对运维更友好。第三,从开发环境到测试环境再到生产环境,镜像的一致性是最容易保证的。虽然 Docker 本身也有平台差异,但比在一堆裸机里手动敲命令要可靠得多。
2. 15GB Docker 镜像的构建顺序与制作细节
2.1 构建机上先做准备:把网络环境一次性搞定
构建 MonkeyOCRv2 镜像之前,我建议先不要急着写 Dockerfile,先去解决网络下载问题。这里说的网络问题不是“网速慢”那么简单,而是 pip、apt 和模型权重下载源各自有各自的脾气。
我当时在构建机上做了三件事:配好 pip 国内镜像源,配好 apt 的国内软件源,再用环境变量把 Hugging Face 的模型下载地址改到本地缓存目录。pip 配置写在 ~/.pip/pip.conf,内容无非就是指向清华或者阿里云的 PyPI 镜像;apt 源写在 /etc/apt/sources.list 里。模型权重这块,如果从 Hugging Face 拉不动,就用镜像站或者直接在能访问外网的时候先把权重下载到本地目录,构建时直接 COPY 进去。
做完这三步,构建过程会顺畅非常多。否则你会在同一个 Dockerfile 里反复遭遇“连接超时”“无法解析域名”“校验和不匹配”,每次失败都会有部分缓存残留,让人很崩溃。
2.2 Dockerfile 分层策略:每一层都有它该待的位置
Docker 镜像的本质是一层层叠加的文件系统,每一层都会占空间。MonkeyOCRv2 的 Dockerfile 我按“底层不变、中间稳定、上层易变”的原则来组织:
# 阶段一:基础运行时层 FROM nvidia/cuda:12.1-runtime-ubuntu22.04 AS runtime ENV DEBIAN_FRONTEND=noninteractive \ TZ=Asia/Shanghai \ LANG=C.UTF-8 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 libgomp1 libgomp-12-dev libsm6 libxext6 libxrender1 \ libgomp1 libtinfo5 libncurses5 \ fonts-dejavu-core \ && rm -rf /var/lib/apt/lists/* # 阶段二:Python 推理环境层 RUN apt-get update && apt-get install -y --no-install-recommends python3.10 python3-pip \ && rm -rf /var/lib/apt/lists/* COPY requirements.txt /tmp/requirements.txt RUN pip3 install --no-cache-dir -r /tmp/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple \ && rm -rf /root/.cache/pip # 阶段三:模型权重与应用代码层 WORKDIR /app COPY ./models /app/models COPY ./monkey_ocr /app/monkey_ocr COPY ./configs /app/configs ENV HF_HUB_OFFLINE=1 \ TRANSFORMERS_OFFLINE=1 \ CUDA_VISIBLE_DEVICES=0 \ MKL_NUM_THREADS=1 \ OMP_NUM_THREADS=4 EXPOSE 8080 CMD ["python3", "-u", "-m", "monkey_ocr.server", "--config", "/app/configs/prod.yaml"]把这个文件拆开看,三层逻辑很清晰:第一层 CUDA 基础镜像基本不会变,构建一次可以长期复用;第二层 Python 依赖除非升级版本否则也不用动;第三层模型权重和服务代码是更新最频繁的。这样分层的好处是日常改代码重新构建镜像时,前两层直接命中缓存,只需要重新打包第三层,构建速度能快很多。
2.3 多阶段构建与清理秘方:把 19GB 压回 15GB
如果你还是觉得 15GB 太大,想进一步瘦身,多阶段构建是必须的。MonkeyOCRv2 里有一类依赖只需要在构建期使用,比如编译一些自定义算子的 gcc、cmake、ninja,这些工具一旦进了最终镜像,就是白白占空间。
多阶段构建的思路是:从基础镜像开始,装好编译工具链,把 C++ 扩展模块编译完,然后把这些编译产物复制到第二阶段干净的 runtime 镜像里。此外清理动作也要做到位,我总结了一个“构建三清”:清 apt 列表、清 pip 缓存、清临时文件。
# 阶段一:编译构建阶段 FROM nvidia/cuda:12.1-devel-ubuntu22.04 AS builder RUN apt-get update && apt-get install -y build-essential cmake ninja-build WORKDIR /build COPY ./custom_ops /build/custom_ops RUN python3 setup.py bdist_wheel # 阶段二:瘦身运行阶段 FROM runtime AS final COPY --from=builder /build/dist/*.whl /tmp/ RUN pip3 install /tmp/*.whl && rm /tmp/*.whl批处理流程跑完之后,再进入镜像里 executedocker image inspect查看历史层记录,你会发现配置文件层、缓存层的体积明显变小。实测从默认裸装 19GB 左右降到 15GB 出头,传输和内网加载时间都能省不少。
2.4 构建时特别要注意的固化依赖清单
MonkeyOCRv2 的 requirements.txt 必须在首次构建时固定好版本。镜像构建时候看到“>=”这种写法就头大,因为它可能会在半年后拉到一个新的小版本,而新版本可能直接改掉 API。我的做法是:
torch==2.2.2+cpu # 仅用于导出,GPU 环境安装时替换为对应 CUDA 版本 torchvision==0.17.2 numpy>=1.24,<2.0 opencv-python-headless==4.9.0.80 onnxruntime-gpu==1.17.1 fastapi==0.110.0 uvicorn==0.29.0这样写的好处是构建的时候不会因为依赖解析冲突而报错。尤其 numpy 这个库,不同版本对 OCR 后处理里一些矩阵运算的结果有细微影响,版本漂移后可能出现同样的输入不同输出的情况。
3. 从构建机到内网:离线传输与镜像仓库中转
3.1 docker save 与 load 的坑:15GB 镜像怎么挪才靠谱
镜像构建完成,接下来就是从外部环境往内网搬运。第一个想到的就是 docker save 加 docker load。命令很简单,但坑不少。
docker save monkeyocr:v2.0 | gzip > monkeyocr_v2.tar.gz15GB 的镜像压缩后一般是 7 到 9GB,但 gzip 压缩需要时间,而且压缩过程中一旦断掉,前面的计算全部白费。更稳妥的做法是直接边压缩边校验:
docker save monkeyocr:v2.0 -o monkeyocr_v2.tar pigz -c monkeyocr_v2.tar > monkeyocr_v2.tar.gz sha256sum monkeyocr_v2.tar.gz > monkeyocr_v2.tar.gz.sha256传输完成后,在内网机器上先检查 sha256,再解压:
sha256sum -c monkeyocr_v2.tar.gz.sha256 gzip -dc monkeyocr_v2.tar.gz | docker load docker images | grep monkeyocr如果单个文件太大不好传,还可以用 split 拆成多块:
split -b 2000m monkeyocr_v2.tar.gz monkeyocr_v2_part_ cat monkeyocr_v2_part_* | gzip -dc | docker load内网环境如果是 Windows 机器加移动硬盘,拆分成 2GB 左右的文件能大幅降低拷贝失败概率。不要为了省事直接往 NTFS 分区拷一个 8GB 的压缩包,很容易遇到“磁盘空间不足但实际还有空间”的假报错。
3.2 内网 Docker Registry 的搭建:比 U 盘搬运更优雅的方案
如果内网有多台 GPU 服务器,U 盘或移动硬盘一个一个拷镜像显然不现实。更科学的做法是在内网找一台服务器搭 Docker Registry,作为所有节点共同拉取镜像的源头。
搭建本身不复杂:
docker run -d -p 5000:5000 --restart=always \ --name registry \ -v /data/registry:/var/lib/registry \ registry:2然后构建机上给镜像打上内网仓库标签推送:
docker tag monkeyocr:v2.0 10.20.30.40:5000/monkeyocr:v2.0 docker push 10.20.30.40:5000/monkeyocr:v2.0内网节点拉取时需要注意 Docker 默认只允许 HTTPS 访问 registry,内网一般是 HTTP 或自签证书,需要在每台节点的 Docker daemon.json 里配置 insecure-registries:
{ "insecure-registries": ["10.20.30.40:5000"] }改完 daemon.json 记得重启 Docker 再拉取。这个方法对外网带宽需求不高,只要能完成一次推送,内网节点之间走交换机,速度飞快。
3.3 不依赖 docker daemon 的镜像库工具:skopeo 与 regctl
有些内网环境甚至不允许跑 Docker daemon,只允许你放一个 registry 二进制和客户端工具。这时候 skopeo 和 regctl 这类镜像管理工具就派上用场了。
skopeo 可以只拷贝镜像分层,不需要本地 Docker 环境:
skopeo copy docker://monkeyocr:v2.0 \ docker://10.20.30.40:5000/monkeyocr:v2.0 \ --dest-tls-verify=falseregctl 更灵活,能直接操作 registry API,比对镜像清单,甚至可以不用下载整个镜像就获取它的元数据。对于构建验收、镜像签名校验这些场景,这两个工具比 docker pull 更轻量。
如果外网构建机内存不够 16GB,用 skopeo 直接跨 registry 推送到内网仓库会比 docker save 再 load 更省空间,因为它是流式复制层,不经过本地磁盘的中转。
3.4 离线加载后的完整性检查清单
docker load 之后,镜像亮出来并不代表万事大吉。我每次都会花五分钟做一套完整性检查,能避免后面调试浪费半天:
- 检查 image 大小和构建时记录是否一致,误差超过 100MB 就要警惕;
- 进入容器跑一次
python -c "import torch; print(torch.__version__)",确认 PyTorch 是否可用; - 跑一次
torch.cuda.is_available(),确认 GPU 驱动通没通; - 加载一个最小测试图片,跑一遍 OCR 推理,对比输出文本是否和预期一致;
- 最后确认服务端口、日志目录、模型路径权限正常。
docker run --rm --gpus all monkeyocr:v2.0 python -c \ "import torch; print('cuda:', torch.cuda.is_available(), 'device:', torch.cuda.get_device_name(0))"输出里能看到 CUDA 设备名字,才说明容器能正常访问 GPU。这一步如果失败,后面所有推理都是空谈。
4. GPU 调优:从能用变成好用
4.1 摸清 GPU 家底:驱动、CUDA 版本和 PyTorch 的三角关系
镜像能跑起来之后,才到了真正花时间的环节:GPU 调优。先说驱动层。MonkeyOCRv2 镜像里的 CUDA 运行库是 12.1,这要求宿主机 GPU 驱动版本足够新。NVIDIA 驱动有一个向下兼容关系:驱动 525 以上才支持 CUDA 12.1。如果机器上驱动停留在 470 甚至更低,容器里的 CUDA 调用会直接报“CUDA driver version is insufficient”。
所以在做任何调优之前,先执行 nvidia-smi 看驱动版本和显卡型号。同时用nvidia-smi -q | grep "Compute Capability"看显卡计算能力。计算能力决定 PyTorch 和 cuDNN 是否能发挥完整算力,比如老一点的 P4 只支持到 6.1,跑现在的 Transformer 类 OCR 模型会比较吃力。
我的建议是:如果是 30 系、40 系显卡,直接上 CUDA 12.1 镜像没问题;如果机房还有更老的 10 系显卡,得换用 CUDA 11.x 或 10.2 的基础镜像,否则就算驱动勉强支持,算子性能和显存利用率也会很差。
4.2 容器启动参数:给 PyTorch 准备好合理的运行环境
GPU 容器启动不是简单加一个--gpus all就完事。MonkeyOCRv2 里用到了多进程数据预处理和 PyTorch 的 DataLoader,有一点必须处理:共享内存。默认 Docker 容器的 /dev/shm 只有 64MB,DataLoader 的 num_workers 稍微设大一点就会报 “Bus error” 或者 “Cannot allocate memory”。
我当时用的启动命令是这样的:
docker run -d \ --name monkeyocr-gpu \ --gpus '"device=0,1"' \ --ipc=host \ --shm-size=8g \ --ulimit memlock=-1 \ --ulimit stack=67108864 \ -e NVIDIA_VISIBLE_DEVICES=0,1 \ -e NVIDIA_DRIVER_CAPABILITIES=compute,utility \ -e CUDA_VISIBLE_DEVICES=0,1 \ -e OMP_NUM_THREADS=8 \ -p 8080:8080 \ -v /data/logs:/app/logs \ -v /data/model_cache:/app/.cache \ monkeyocr:v2.0这里面几个参数值得解释一下。--ipc=host把宿主机的内存进程通信空间共享给容器,PyTorch 多进程在 CPU 端做 tensor 收集时不会因为共享内存不够而挂掉。--shm-size=8g是给容器单独挂一块共享内存,如果不开 host 模式,这个参数要调大。--ulimit memlock=-1允许进程锁住内存,避免某些 CUDA 操作因为内存换页变得奇慢无比。--gpus '"device=0,1"'比--gpus all更精细,可以确保只分配指定的两张卡。
4.3 显存抑制:让 OCR 推理不至于把整张卡占满
很多 OCR 模型默认会抢占大量显存,尤其是 PyTorch 的缓存分配器。默认情况下,PyTorch 会把能拿到的显存全部分给自己的缓存,导致同一张卡上无法再跑第二个服务。MonkeyOCRv2 在经过几个版本迭代后,把显存占用从原来的 9GB 左右压到了 4.5GB 左右,主要靠三个手段:
第一个是环境变量显式限制 PyTorch 缓存。在 Python 代码里设置:
import torch torch.cuda.set_per_process_memory_fraction(0.45)这会限制当前进程最多使用卡上显存的 45%,虽然高并发时可能触发一次缓存整理,但单路 OCR 推理的延迟完全没问题。
第二个是模型精度用 FP16。OCR 检测模型对精度损失不敏感,转换 FP16 之后显存占用直接减半。在推理代码里加载权重后加一行model.half(),同时注意把输入图像也转成.half()。如果你的 GPU 支持 Tensor Core(20 系以上基本都支持),FP16 的推理速度还会明显快于 FP32。
第三个是控制批大小。OCR 服务如果按业务请求单张调用,把 batch_size 设置为 1 最省显存;如果是批量文档识别场景,可以开 batch 4 但要注意监控显存的峰值。
4.4 推理延迟优化:从一次 400ms 到 120ms 的调整记录
我先说一下基准数据:MonkeyOCRv2 在 V100 上处理一张 1080p 的文档图像,全流程包括检测、方向分类、识别,FP32 模式下平均耗时 400ms 左右。后来做了一轮专项调优,把耗时压到了 120ms 上下。
调优动作按收益排序:
- 检测和识别模型都切换到 FP16 推理,耗时降约 30%;
- 固定输入尺寸。之前检测阶段会根据原图宽高比动态缩放,导致每次算子形状不一致,CUDA kernel 每次重新编译。设置最小边长为 960 并保持宽高比不变之后,耗时再降 20%;
- 开启 cuDNN 的 benchmark。PyTorch 里设置
torch.backends.cudnn.benchmark = True,让 cuDNN 根据输入形状选择最优卷积算法,能在连续推理场景省下不少时间; - 将识别模型的 beam search 改成贪心解码。OCR 任务里 beam search 的收益有限,但开销不小,贪心解码延迟低且结果几乎一致;
- 对 CPU 预处理做多线程优化。OpenCV 的 resize 和 normalize 操作默认是单线程,设置
cv2.setNumThreads(4)后,预处理段耗时从 35ms 降到 15ms。
经过这轮调整,V100 单卡可以扛住每秒 8 到 10 次完整的文档识别请求,显存占用不超过 6GB。如果是 A100 或者 4090,还能再往上提一截。
4.5 并发与多卡分配:不要让卡闲着也不要让卡过载
内网环境不可能只有一种业务在跑,GPU 资源往往是多服务共享的。MonkeyOCRv2 的部署我建议把模型加载常数和显存占用做一次完整的压测,然后据此决定并发启动几个容器实例。
一个比较稳妥的分配方案是:每张卡跑一个容器实例,每个容器最多占用该卡 45% 的显存,两个以上实例同时跑时不会互相挤爆显存。如果一台机器有四张卡,那就起四个容器,外部用 Nginx 做负载均衡。
Nginx 里做简单的轮询就够了:
upstream monkey_backend { server 127.0.0.1:8081; server 127.0.0.1:8082; server 127.0.0.1:8083; server 127.0.0.1:8084; }这样好处是单容器崩溃只需要重启一个实例,不影响整体服务。坏处是模型权重会同时加载四份,显存占用乘以实例数。如果显存紧张,可以只起两个实例,每个指向不同卡,权重加载两份,其他资源留作他用。
5. 常见问题与排查技巧实录
5.1 容器启动时的 GPU 报错速查表
内网部署 GPU 容器时,最常见的无非下面这几类报错,我整理了一张速查表,基本上照着排查都能解决:
| 报错信息 | 根本原因 | 解决办法 |
|---|---|---|
| could not select device driver with capabilities: gpu | 宿主机没装 NVIDIA Container Toolkit | apt install nvidia-container-toolkit,重启 docker |
| CUDA driver version is insufficient | 宿主机驱动版本太低,不满足容器内 CUDA 版本 | nvidia-smi查看驱动,升级驱动或降低基础镜像 CUDA 版本 |
| CUDA error: device not yet available | 部分进程占用了 GPU,或 CUDA_VISIBLE_DEVICES 配置了不存在的设备号 | 清理僵尸进程,确认设备编号 |
| Bus error / Cannot allocate memory | 容器的共享内存太小 | 启动命令加--shm-size=8g或--ipc=host |
| ImportError: libcudnn.so.8: cannot open shared object file | cuDNN 缺失或版本不匹配 | 确认基础镜像是否包含 cuDNN,或在最终镜像安装对应的nvidia-cudnn-cu12 |
| NVIDIA-SMI has failed because it couldn't communicate with NVIDIA driver | 宿主机的 nvidia kernel module 未加载 | 执行modprobe nvidia,或重启宿主机 |
5.2 磁盘空间告警:docker 镜像堆积怎么破
15GB 镜像多传几次,磁盘再大也会吃紧。dn层执行 docker images 看镜像列表,会发现很多占空间的残余镜像,有时候同一个服务不同版本堆积了上百 GB。排查思路是先跑docker system df看空间去了哪里。
针对 MonkeyOCRv2 这类大镜像服务,我的做法是建立镜像版本清理规则:线上保留最近两个稳定版本,其余镜像一律删除。容器日志改用 json-file 的 max-size 限制:
{ "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } }内网服务器一旦磁盘写满,Docker 会因为无法写日志而强制停止容器,那个状态非常难受,所以磁盘水位监控一定要提前做好。
5.3 内网推理结果与外网不一致的排查思路
有一类问题很容易被忽视:镜像明明没变,内网跑出来的 OCR 文本却和外部环境不完全一致。这种问题十有八九出在依赖版本差异上,尤其是 NumPy 和 OpenCV。我遇到过一次,内网机器因为基础镜像时间更早,装到了一个旧版 NumPy,mask 处理时坐标矩阵的数据类型变了,导致输出框有 1 个像素的偏移,结果整体识别精度下降。
排查这类问题的思路是:
- 先在容器里锁定依赖版本,重新导出一份新的镜像;
- 对比内外网容器中
pip freeze的输出,找出差异包; - 用同一张测试图分别跑内外网容器,输出 JSON 做 diff,定位是哪一步开始出现偏差;
- 检查浮点精度。如果是 FP16 导致的误差,调整为 FP32 后对比;
- 确认是否开启了 cuDNN benchmark,某些机器上 benchmark 会启用不同的卷积实现,尾数误差导致结果不同。
这个过程中有个实用技巧:把输入图像的前几帧保存下来,输出结果也保存到文件,每次镜像变更以后都跑一遍同样的样本,形成回归记录。内网部署初期不太会有业务压力,这时候建立回归测试最划算。
5.4 构建机没联网时怎么解决 pip 离线安装
有些场景更极端,连构建机都只能短暂联网,或者压根只有一台开发机能上网。这时需要先把所有依赖下载到本地,再通过 pip 的离线安装模式装上。
pip download -r requirements.txt -d /data/pip_wheels \ --only-binary=:all: --platform manylinux2014_x86_64 \ --python-version 3.10 --implementation cp然后内网机器安装时使用:
pip install --no-index --find-links=/data/pip_wheels -r requirements.txt注意--platform和--python-version一定要指定,不能只写简单版。因为有些包在不同 Python 版本里编译产物不同,不指明的话下载出来的 wheel 可能装不上。OpenCV、PyTorch 这类大包的 wheel 文件本身也很大,下载目录建议放在一个单独的磁盘分区。
6. 一点额外的部署心得
MonkeyOCRv2 的这次内网部署让我最深的体会是:大镜像不可怕,可怕的是没有把镜像内容、构建顺序、搬运方式当成一个整体来规划。你单独去优化 Dockerfile,压缩体积却忽略传输环节,结果是镜像瘦了但 load 依旧失败;你把重点放在 GPU 调优,却忘了先验证基础依赖版本,结果是服务怎么调都差口气。
如果你也在搞类似的项目,我建议按这个顺序自查:先把模型的权重路径和依赖固定下来,再做镜像分层,然后设计离线导出和加载流程,最后才去调 GPU 参数。每一步都靠验证往前推,不要凭感觉跳步。镜像构建是项谁都能上手的工作,但把它做扎实、做可复现,才是能真正省下后续大量时间的地方。
最后分享一个小技巧:内网部署验收时,别只盯着“服务起来了”,不妨写一个脚本,连续跑 1000 次最小推理,记录延迟的 P50 和 P99 值,顺便观察显存是否持续增长。显存泄漏这类问题往往不会在第一次调用时暴露,但跑个几百次之后,内存曲线就很诚实了。有了这套基线,后续升级模型版本时,你自己心里就有底了。