尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

内网离线部署MonkeyOCRv2:Docker镜像构建与vLLM GPU调优实战

内网离线部署MonkeyOCRv2:Docker镜像构建与vLLM GPU调优实战 1. 为什么要在内网离线环境折腾 MonkeyOCRv2把 MonkeyOCRv2 部署到内网离线环境这件事听起来像是把大象装进冰箱但真正动手之后你会发现难点从来不是装而是装完之后它能不能跑起来、跑得稳不稳、GPU 有没有真的在干活。我前后在三个不同的内网环境里部署过这套东西踩过的坑从 Docker 镜像构建失败、NVIDIA 驱动版本对不上到 vLLM 引擎启动后显存莫名其妙被吃满几乎每一环都有一段故事。先说清楚 MonkeyOCRv2 是什么定位。它是一套面向文档解析的 OCR 推理服务核心能力是把图片、扫描件、PDF 里的文字和版面结构提取出来底层通常依赖视觉编码器加语言解码器的组合推理侧常见做法是用 vLLM 这类高性能推理引擎来托管模型权重。之所以要内网离线部署是因为很多实际场景里机器根本连不上外网——比如生产车间的质检终端、档案室的扫描工作站、或者某些对数据出域有硬性要求的业务系统。这些地方你没法pip install一下就从云端拉包所有依赖必须提前打包好用 U 盘或者内部镜像仓库搬进去。15GB 的 Docker 镜像这个数字不是随便写的。它大致对应的是基础 CUDA 运行时镜像约 3-4GB Python 依赖和系统库约 2-3GB 模型权重视量化方式不同FP16 下可能 6-8GB vLLM 及其编译产物约 1-2GB。这个体积意味着你不能指望用docker save之后随手拷来拷去得考虑分层、压缩、以及目标机器的磁盘余量。我见过有人镜像构建完 15GB结果目标机器根分区只剩 12GB直接卡在docker load那一步白忙活一整天。这篇文章适合谁看如果你正在做内网 AI 服务的落地手上有 NVIDIA 显卡比如 RTX 4060 Laptop、L20、甚至 MI50 这类需要把 OCR 或大模型推理服务塞进一个不能上网的环境那这篇内容基本就是为你写的。我会从镜像构建的取舍讲起一路说到 GPU 调优和 vLLM 引擎参数怎么调中间穿插我实际踩过的坑和验证过的参数。不会只给你一堆命令让你抄而是把为什么这么选讲透这样你换个模型、换个显卡也能自己推。2. 镜像构建15GB 是怎么来的又该怎么瘦身2.1 基础镜像选型别一上来就用 latest构建 MonkeyOCRv2 镜像的第一步是选基础镜像。很多人习惯性写FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04这没错但版本号一定要锁死。我吃过亏某次用了带latest标签的镜像构建时拉到的 CUDA 版本和宿主机驱动不匹配容器起来后nvidia-smi能看到卡但 PyTorch 一调用 CUDA 就报CUDA error: no kernel image is available for execution on the device。这个错误的本质是编译时的 CUDA 架构sm_XX和实际显卡的计算能力对不上。选 runtime 还是 devel如果你只是跑推理runtime 镜像足够体积能小 2-3GB。devel 镜像带编译工具链只有在你要在容器里现场编译 vLLM 的 CUDA kernel 时才需要。我的建议是如果 vLLM 用预编译 wheel 安装就选 runtime如果要用源码编译比如为了适配特定显卡架构那 devel 省不掉。FROM nvidia/cuda:12.1.0-cudnn8-runtime-ubuntu22.04 ENV DEBIAN_FRONTENDnoninteractive ENV PYTHONUNBUFFERED1 ENV TZAsia/Shanghai RUN apt-get update apt-get install -y --no-install-recommends \ python3.10 python3-pip python3.10-dev \ libgl1 libglib2.0-0 libsm6 libxext6 libxrender-dev \ rm -rf /var/lib/apt/lists/*这里libgl1和libglib2.0-0是 OpenCV 的运行时依赖OCR 项目几乎必装。很多人本地构建时因为宿主机有这些库所以没报错一到干净的内网机器上就ImportError: libGL.so.1: cannot open shared object file。提前装好省得后面排查。2.2 模型权重怎么进镜像三种方案的成本对比模型权重是 15GB 里的大头。把权重打进镜像有三种常见做法各有取舍方案做法优点缺点打进镜像层COPY model_weights /app/weights一次构建分发简单镜像体积暴涨更新权重需重建挂载卷权重放宿主机-v挂载镜像小权重可独立更新分发时要额外拷贝权重目录构建时下载RUN huggingface-cli download ...镜像自包含内网构建时根本下不动内网离线场景下我强烈推荐挂载卷方案。原因很直接权重文件动辄几个 GB如果打进镜像每次微调模型或者换量化版本你都得重新构建、重新docker save、重新搬运。而挂载卷的话镜像本身可能只有 5-6GB权重单独用一个移动硬盘拷过去就行。启动命令大概长这样docker run -d --gpus all \ -v /data/models/monkeyocrv2:/app/weights:ro \ -v /data/cache:/root/.cache \ -p 8000:8000 \ --name monkeyocr \ monkeyocrv2:offline注意:ro只读挂载防止容器内进程意外改写权重。/root/.cache也挂出来是因为 vLLM 和 HuggingFace 库会在里面写编译缓存和 tokenizer 缓存不挂的话每次重启容器都要重新编译慢得让人抓狂。2.3 依赖安装的离线化处理内网构建最大的痛点是 pip 装不了包。标准做法是提前在有网机器上把 wheel 包全部下载下来pip download -r requirements.txt -d ./wheels \ --platform manylinux2014_x86_64 \ --python-version 310 \ --only-binary:all:这里--platform和--python-version必须和目标环境一致否则下下来的 wheel 装不上。我遇到过一次在有网机器上用 Python 3.11 下载目标容器是 3.10结果一堆包报is not a supported wheel on this platform。后来老老实实按目标版本重新下。vLLM 的安装要特别注意。它依赖torch、xformers、flash-attn这些带 CUDA 扩展的包版本必须严格对齐。我的经验是直接锁定 vLLM 官方推荐的组合比如 vLLM 0.4.x 配 torch 2.1.2 cu121。版本错配的典型症状是启动时报undefined symbol或者推理时 kernel 崩溃。COPY wheels /tmp/wheels RUN pip3 install --no-index --find-links/tmp/wheels \ torch2.1.2cu121 \ vllm0.4.2 \ -r /tmp/wheels/requirements.txt \ rm -rf /tmp/wheels--no-index强制 pip 不去网上找--find-links指向本地 wheel 目录。这样构建过程完全离线构建出来的镜像也干净。2.4 分层缓存与镜像瘦身Docker 构建的层缓存机制要用好。把不常变的部分系统依赖、Python 基础包放前面常变的部分业务代码、配置放后面。这样改代码时不用重装依赖构建时间能从十几分钟降到一两分钟。瘦身方面几个实用手段rm -rf /root/.cache/pip清掉 pip 缓存能省 1-2GBapt-get clean清掉 apt 缓存如果用了 devel 镜像编译编译完可以把工具链删掉。但要注意删的时候用串在同一条 RUN 里否则删掉的文件还在上一层里占着空间。RUN pip3 install --no-index --find-links/tmp/wheels -r requirements.txt \ rm -rf /root/.cache/pip /tmp/wheels \ apt-get clean \ rm -rf /var/lib/apt/lists/*最终镜像大小控制在 15GB 左右是合理的。如果超过 20GB大概率是权重打进去了或者缓存没清干净。3. 内网搬运与加载镜像和权重的实际流转3.1 docker save 与 load 的正确姿势镜像构建完下一步是搬到内网机器。docker save出来的 tar 包体积和镜像一样大15GB 的镜像就是 15GB 的 tar。传输前建议先压缩docker save monkeyocrv2:offline | gzip -1 monkeyocrv2.tar.gz这里用-1而不是默认压缩级别是因为镜像里大部分是已经压缩过的二进制wheel、模型权重高压缩级别收益很小但耗时翻倍。实测-1能把 15GB 压到 13GB 左右传输时间省一点是一点。加载的时候gunzip -c monkeyocrv2.tar.gz | docker load注意别先解压再 load那样磁盘上会同时存在 tar 和解压后的内容空间不够的机器直接爆盘。管道方式边解压边加载省一半空间。提示docker load过程中如果中断可能会留下不完整的镜像层。重新 load 前先docker images确认必要时docker rmi清掉残留。3.2 权重文件的校验与放置权重搬运最怕的是文件损坏。大文件在 U 盘或网络传输中出错是常事而且往往到推理时才暴露报个莫名其妙的unexpected EOF或者权重 shape 不匹配。我的做法是搬运前生成校验和find /data/models/monkeyocrv2 -type f -exec sha256sum {} \; checksums.txt到内网后sha256sum -c checksums.txt验证一遍。多花几分钟能省掉后面几小时的排查。权重目录结构也要注意。vLLM 加载模型时对目录布局有要求通常是config.json、tokenizer.json、*.safetensors这些文件平铺在一个目录下。如果是从 HuggingFace 下载的默认就是这个结构直接拷过去就行。但如果你自己转换过格式要确认config.json里的architectures字段和 vLLM 支持的模型类型对得上否则启动时报Model architecture not supported。3.3 内网镜像仓库的搭建可选但推荐如果内网机器不止一台每次都docker save/load太累。可以在内网搭一个轻量镜像仓库docker run -d -p 5000:5000 --name registry \ -v /data/registry:/var/lib/registry \ registry:2然后把镜像tag成内网IP:5000/monkeyocrv2:offline再push。其他机器直接pull就行。这个方案的前提是内网机器之间网络通且能访问仓库端口。注意 Docker 默认要求仓库走 HTTPS内网用 HTTP 的话需要在每台机器的/etc/docker/daemon.json里加insecure-registries配置然后重启 Docker。4. GPU 环境打通驱动、容器运行时与设备可见性4.1 宿主机 NVIDIA 驱动的版本匹配逻辑容器里能不能用 GPU取决于宿主机驱动和容器内 CUDA 运行时的配合。这里有个关键认知容器里不需要装完整的 NVIDIA 驱动只需要 CUDA 运行时驱动由宿主机提供。NVIDIA Container Toolkit 会把宿主机的驱动库和设备节点映射进容器。版本匹配的规则是宿主机驱动版本要 容器内 CUDA 版本要求的最低驱动。比如 CUDA 12.1 要求驱动 530CUDA 11.8 要求 520。查法很简单nvidia-smi右上角显示的CUDA Version是驱动支持的最高 CUDA 版本容器里的 CUDA 不能超过这个。我遇到过最坑的情况是宿主机驱动是 470 系列容器里装了 CUDA 12.1 的 PyTorch结果torch.cuda.is_available()返回 False但nvidia-smi在容器里又能正常显示。原因就是驱动太老不支持 CUDA 12。解决办法要么升级宿主机驱动要么把容器 CUDA 降到 11.8。# 宿主机查看驱动版本 nvidia-smi --query-gpudriver_version --formatcsv,noheader # 容器内验证 CUDA 可用性 python3 -c import torch; print(torch.cuda.is_available(), torch.version.cuda)4.2 nvidia-container-toolkit 的安装与验证宿主机要装 NVIDIA Container ToolkitDocker 才能识别--gpus参数。安装步骤以 Ubuntu 为例distribution$(. /etc/os-release; echo $ID$VERSION_ID) curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | apt-key add - curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \ tee /etc/apt/sources.list.d/nvidia-container-toolkit.list apt-get update apt-get install -y nvidia-container-toolkit nvidia-ctk runtime configure --runtimedocker systemctl restart docker装完验证docker run --rm --gpus all nvidia/cuda:12.1.0-base-ubuntu22.04 nvidia-smi如果这条命令能打印出显卡信息说明链路通了。如果报could not select device driver with capabilities: [[gpu]]说明 toolkit 没装好或者 Docker 没重启。注意内网离线环境下上面这些 apt 源和 curl 都下不动。需要提前在有网机器上把 deb 包下好或者直接用 NVIDIA 提供的离线安装包。这一步是内网部署 GPU 环境最容易卡住的地方务必提前准备。4.3 多卡与显存可见性控制如果机器上有多张卡或者你想限制容器只用某几张用--gpus参数控制# 只用第 0 和第 1 张卡 docker run --gpus device0,1 ... # 用全部卡 docker run --gpus all ...也可以用环境变量NVIDIA_VISIBLE_DEVICES控制。这个在共享 GPU 的机器上很有用避免你的容器把别人的卡也占了。显存方面vLLM 有个--gpu-memory-utilization参数默认 0.9意思是占用 90% 显存。这个值设太高容易 OOM设太低又浪费。我的经验是如果机器上只跑这一个服务0.85-0.9 比较合适如果还要留显存给其他进程降到 0.6-0.7。RTX 4060 Laptop 只有 8GB 显存跑 MonkeyOCRv2 这种模型要特别小心可能需要用量化版本或者限制并发。5. vLLM 引擎调优让 OCR 推理真正跑满 GPU5.1 vLLM 在 OCR 场景下的角色定位MonkeyOCRv2 的推理侧用 vLLM 托管核心原因是 vLLM 的 PagedAttention 机制能大幅提升显存利用率和吞吐。传统推理框架把每个请求的 KV Cache 连续存放显存碎片严重vLLM 把 KV Cache 分页管理像操作系统管理内存一样碎片率大幅降低。对于 OCR 这种输入长度差异很大的场景有的图片只有几个字有的整页文档几千字这个优势特别明显。vLLM 的架构里EngineCore负责调度Scheduler决定哪些请求进 batchExecutor实际执行模型前向。理解这个流程对调优有帮助当并发请求多的时候Scheduler 会把请求打包成 batchbatch 越大 GPU 利用率越高但显存占用也越大。调优的本质就是在吞吐和显存之间找平衡点。5.2 关键启动参数与实测取值启动 vLLM 服务时几个参数对性能影响最大python3 -m vllm.entrypoints.openai.api_server \ --model /app/weights \ --served-model-name monkeyocrv2 \ --gpu-memory-utilization 0.85 \ --max-model-len 4096 \ --max-num-seqs 16 \ --tensor-parallel-size 1 \ --dtype float16 \ --port 8000逐个说--gpu-memory-utilization 0.85留 15% 显存给 CUDA 上下文和其他开销。设 0.95 经常在压力测试时 OOM。--max-model-len 4096OCR 场景单页文档的 token 数一般不超过 4096。设太大浪费显存设太小长文档会被截断。--max-num-seqs 16同时处理的最大请求数。这个值和显存强相关8GB 显存的卡建议设 8-1624GB 的卡可以设 32-64。--tensor-parallel-size 1单卡推理设 1。多卡才需要调大但 MonkeyOCRv2 这个量级的模型单卡足够。--dtype float16半精度推理显存减半精度损失可接受。如果显卡支持 bfloat16Ampere 及以上可以用bfloat16数值稳定性更好。实测在 RTX 4060 Laptop8GB上max-num-seqs8、gpu-memory-utilization0.8是比较稳的组合单张 A4 扫描件的推理延迟在 1-2 秒。在 L2048GB上max-num-seqs64、gpu-memory-utilization0.9吞吐能到每秒十几张。5.3 显存不够时的降级策略8GB 显存跑 OCR 模型确实紧张。几个降级方向第一用量化版本。GPTQ 或 AWQ 量化能把权重压到 4bit显存占用降到 FP16 的四分之一左右。代价是精度略降但 OCR 任务对精度没那么敏感实测识别准确率下降在 1-2 个百分点以内。第二限制max-model-len。如果实际文档都不长把 4096 降到 2048KV Cache 显存直接减半。第三减小max-num-seqs。并发降下来显存峰值也降。代价是吞吐下降但如果你的场景是低频调用影响不大。第四开启--enable-chunked-prefill。这个选项把长 prompt 的 prefill 阶段分块处理降低显存峰值。对长文档场景特别有用。--enable-chunked-prefill \ --max-num-batched-tokens 20485.4 推理性能的观测与瓶颈定位服务跑起来后怎么知道 GPU 有没有真的在干活几个观测手段nvidia-smi dmon能实时看 GPU 利用率和显存占用。如果利用率长期低于 30%说明 batch 太小或者请求间隔太长GPU 在等活干。如果利用率 100% 但吞吐上不去可能是显存带宽瓶颈或者 kernel 效率问题。vLLM 自身会打印吞吐指标包括prompt throughput和generation throughput。OCR 场景主要看 generation throughput因为它反映的是实际生成文字的速度。# 实时监控 watch -n 1 nvidia-smi --query-gpuutilization.gpu,memory.used,memory.total --formatcsv如果发现显存占用持续增长不释放可能是 KV Cache 没回收检查是不是有请求卡住没结束。vLLM 有--disable-log-requests选项生产环境建议关掉请求日志减少 IO 开销。6. 踩坑实录那些让我加班到凌晨的问题6.1 容器内 nvidia-smi 正常但 PyTorch 用不了 GPU这个问题的排查链路我走过完整一遍。现象是docker run --gpus all进去后nvidia-smi能显示卡但torch.cuda.is_available()返回 False。第一步查驱动版本和 CUDA 版本匹配。nvidia-smi显示的 CUDA Version 是驱动支持的最高版本如果容器内 PyTorch 编译的 CUDA 版本高于这个就用不了。第二步查LD_LIBRARY_PATH。容器内需要能找到libcuda.so这个由 nvidia-container-toolkit 挂载。如果手动改过环境变量可能把它覆盖了。第三步查 PyTorch 是不是 CPU 版本。pip install torch默认可能装 CPU 版要装torch2.1.2cu121这种带 CUDA 标识的。验证方法python3 -c import torch; print(torch.version.cuda)如果打印 None就是 CPU 版。我那次最终原因是驱动太老470容器 CUDA 是 12.1。升级驱动到 535 后解决。6.2 vLLM 启动时报 undefined symbol这个错误通常是 vLLM 和 torch 版本不匹配或者 CUDA 扩展编译时用的架构和运行时不一致。排查方法python3 -c import vllm; print(vllm.__version__) python3 -c import torch; print(torch.__version__, torch.version.cuda)对照 vLLM 官方文档的版本兼容表。如果版本对得上还报错可能是 wheel 包本身有问题重新下载或者换一个版本。还有一种情况是flash-attn没装好。vLLM 依赖 flash-attn 做注意力加速如果它编译时用的 CUDA 架构和实际显卡不匹配运行时会报 kernel 相关错误。解决办法是设置TORCH_CUDA_ARCH_LIST环境变量指定实际显卡的架构比如 RTX 4060 是 sm_89export TORCH_CUDA_ARCH_LIST8.96.3 镜像加载后磁盘空间不足docker load需要临时空间解压镜像层。如果镜像 15GB加载过程中可能临时占用 30GB。目标机器根分区不够的话可以改 Docker 的数据目录到有大空间的分区# /etc/docker/daemon.json { data-root: /data/docker }改完重启 Docker。注意迁移已有镜像的话要把原/var/lib/docker的内容拷过去。6.4 推理结果乱码或截断OCR 输出乱码常见原因有几个tokenizer 和模型不匹配用了错误的 tokenizer 文件max-model-len设太小导致长文档被截断采样参数不对OCR 场景应该用贪心解码temperature0不要用随机采样。--temperature 0 \ --top-p 1.0如果输出里有重复文字可能是repetition_penalty没设。OCR 场景建议设 1.1 左右抑制重复生成。7. 上线前的自检清单与长期维护7.1 部署完成后的验证步骤服务起来后别急着接业务先跑一遍验证健康检查curl http://localhost:8000/health返回 200 说明服务活着。模型列表curl http://localhost:8000/v1/models确认模型名对得上。单张图片推理用一张标准测试图确认输出文字正确。并发测试用ab或wrk压一下看吞吐和延迟是否符合预期。显存监控压测时nvidia-smi看显存峰值确认没超过安全线。curl -X POST http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: monkeyocrv2, messages: [{role: user, content: 识别这张图片的文字}], temperature: 0 }7.2 日志、监控与故障恢复生产环境一定要配日志轮转否则 vLLM 的日志能把磁盘写满。Docker 层面可以配docker run --log-opt max-size100m --log-opt max-file3 ...监控方面vLLM 暴露了 Prometheus 指标可以接 Grafana 看吞吐、延迟、显存。如果不想搞那么复杂至少写个脚本定时检查服务健康状态挂了自动重启。# 简单的健康检查脚本 if ! curl -sf http://localhost:8000/health /dev/null; then docker restart monkeyocr fi7.3 模型更新与镜像迭代的流程内网环境更新模型是个麻烦事。我的建议是把模型权重和镜像解耦权重走挂载卷更新时只换权重目录容器重启即可。镜像只在依赖或代码变更时才重建。更新流程新权重拷到宿主机新目录 - 校验 sha256 - 停旧容器 - 改挂载路径 - 起新容器 - 验证。整个过程不用重新搬运 15GB 镜像几分钟搞定。如果非要更新镜像记得保留旧版本 tag出问题能快速回滚。docker tag和docker rmi配合使用别把旧镜像直接删了。8. 一些关于 GPU 选型和成本的实际体会最后聊点实际的。MonkeyOCRv2 这种 OCR 服务对显卡的要求其实没有大语言模型那么夸张。RTX 4060 Laptop 8GB 能跑但并发上不去适合个人或小团队自用。L20 48GB 是企业级选择能扛几十并发但价格不便宜。如果预算有限二手的 3090 24GB 性价比很高显存够大架构也支持 bfloat16。MI50 这类卡我试过vLLM 对它的支持不如 NVIDIA 完善需要额外编译 ROCm 版本的依赖坑比较多。除非你已经有现成的 MI50 环境否则不建议为了省钱走这条路。GPU 租用也是个选项但内网离线部署的场景通常对数据出域有要求租用云 GPU 可能不符合合规。这个要结合具体业务判断。显存永远是稀缺资源。我的经验是宁可显存留 20% 余量也不要压到 95% 去追求那点吞吐。OOM 一次带来的排查成本和业务中断远比多买一张卡贵。调优的时候先用小 batch 跑通再逐步加大并发观察显存曲线找到稳定点就停别贪。这套东西我在三个环境里部署过从最初的磕磕绊绊到现在基本半天能搞定核心就是把版本匹配、离线依赖、显存预算这三件事提前想清楚。剩下的就是耐心排查GPU 相关的问题看着吓人但排查思路和普通软件问题没本质区别——看日志、对版本、做隔离测试。
返回列表