原文链接:MonkeyOCRv2 内网离线部署:15GB Docker 镜像从构建到 GPU 调优
MonkeyOCRv2 是华中科技大学白翔团队与金山办公联合开源的 0.7B「文档原生视觉编码器」,用 1.13 亿张文档图像做双目标预训练,在 17 语种评测反超 235B 大模型,是低成本落地的文档解析首选。上篇已在本地用 CPU 路径验证过 MonkeyOCRv2 的可行性(此处不展开);本文聚焦完全断网的企业内网生产环境,把验证结果落地为可批量扛量的 GPU 部署,并一次讲透:为什么用容器镜像、镜像怎么构建、怎样验证与部署、如何对接 API、怎样适配 NVIDIA 显卡并调并发性能——一套可照抄的生产教科书。
这篇你能得到什么?
① 容器镜像方案的选型原因与环境一致性价值;
② 镜像构建全记录(跨架构瘦身 23GB→15GB、四处修复怎么烤进去、构建期四坑);
③ 无 GPU 的本地验证与内网服务器三步部署+ API/WebUI/CLI 三形态;
④NVIDIA GPU 适配与全部可配置参数映射、调优建议;
⑤并发模型与性能提升路径,以及直接调 API 的 PDF 处理要点;
⑥ 生产测试与验收清单、故障排查速查。
一、为什么选容器镜像:生产部署的确定性
企业内网生产环境完全断网、配 NVIDIA 显卡,必须走 GPU 路径才能扛批量解析。生产推理分两条线,这是选型核心:
- 解析路径(vLLM):官方实测单页加速 2.2×;bf16 量化仅约 1.5GB 显存;多卡
tensor-parallel吞吐可达 CPU 的 5–20 倍。千页批量直接上 GPU。 - 理解路径(本地 Transformers):文档理解(DocVQA)走本地推理,注意力后端用原生
sdpa,离线无需 flash-attn 源码编译,不依赖 vLLM 服务。
两条线共用同一份权重,按任务切换入口即可。部署形态上,为什么最终从「离线包」演进到「Docker 镜像」?核心矛盾是环境一致性:离线包把上百个轮子 + 可移植 Python 搬进去,仍要在裸机上跑安装脚本离线装一遍,任何一步版本漂移都会暴露;镜像则把"装得上的环境"直接固化为一个制品,导入即运行,零安装、零漂移。
| 维度 | 离线包(裸机方案) | Docker 镜像(本文方案) |
|---|---|---|
| 交付物 | 10.4GB tar.gz(轮子+Python+权重+脚本) | 15.2GB 镜像 tar(环境烤死)+ 权重运行时挂载 |
| 安装 | 安装脚本多步离线装 | docker load一步导入 |
| 环境一致性 | 依赖闭包审计,仍有漂移风险 | 镜像即环境,交付即一致 |
| 启动 | 启动脚本 | docker compose up -d |
| 适用 | 裸机 / 无 Docker 环境 | 有 Docker + nvidia-container-toolkit |
| 典型坑 | 裸机 7 坑(理解教材) | 构建期 4 坑 + 部署期 1 坑 |
离线包并未淘汰:它是无 Docker 裸机环境的兜底,其踩坑经验恰恰是"为什么镜像要把环境烤死"的最好教材。本文主线以镜像为主,构建时把那些崩溃一次性根治进镜像。
镜像设计的另一个关键决定:权重不进镜像。双权重(B-Parsing / B-Und)运行时通过卷-v挂载到/app/MonkeyOCRv2/model_weight,便于权重独立更新、镜像保持精简。最终制品是linux/amd64,可跨 NVIDIA CUDA GPU 与 x86_64 CPU 平台运行。
二、镜像构建全记录:跨架构瘦身 + 修复烤入
镜像在 Mac(arm64)上用docker buildx跨架构构建为linux/amd64,最终交付一个 15.2GB 的 tar。整个构建把"装得上的环境"在构建期一次性焊死。
2.1 瘦身四板斧(23GB → 15GB)
初版用nvidia/cuda:12.8.0-devel-ubuntu22.04作基础,整套 CUDA 开发工具链 ~9GB,镜像飙到 23GB。瘦身四板斧:
- 基础换 runtime:改
nvidia/cuda:12.8.0-runtime,仅补cuda-nvcc-12-8+cuda-cudart-dev-12-8(~几百 MB)——vLLM 的 triton 内核加载时需nvcc+ CUDA 头文件做 JIT 编译,缺一不可,但整套 devel 没必要。 - pip 缓存不进镜像:用 buildx
cache mount(--mount=type=cache,target=/root/.cache/pip)把下载缓存放在宿主机侧,镜像内零 pip 缓存。 - Python 用系统 3.10:弃用 deadsnakes PPA(其 launchpad 源在构建沙箱不稳),改用 Ubuntu 22.04 自带 Python 3.10——torch 2.9.0+cu128 / vLLM 0.11.2 均有 cp310 轮子。
- 权重不进镜像:双权重运行时
-v挂载,镜像只装软件。
2.2 四处修复烤进镜像(部署不再需要文件级补丁)
| 修复 | 内容 | 根治问题 |
|---|---|---|
| serve.py 版本护栏 | == (0,11)→>= (0,11) and < (0,12),兼容 vLLM 0.11.2 上报的(0,11,2) | 版本误判整体退出 |
--enforce-eager | 显式透传,跳过 CUDA graph / torch.compile | 规避 triton/inductor 运行时编译失败 |
| prometheus 补丁 | 覆盖 site-packages,getattr(route,"path",None)兜底 fastapi≥0.141 的_IncludedRouter缺.path崩溃 | API 启动即崩 |
| triton 工具链 | nvcc+cuda/std/atomic头文件 +ptxas随镜像安装 | 原始"全都报错"根因 |
这套修复让早期裸机部署中的若干崩溃在镜像里彻底消失——它们已在构建期被固化进镜像,部署时不再需要额外的文件级补丁。
2.3 构建期四个坑(只有造镜像才会遇到)
坑 A|apt 官方源在构建沙箱 404:官方 Ubuntu 镜像archive.ubuntu.com/security.ubuntu.com对大量具体包返回 404,apt-get update直接失败。解法:Dockerfile 起始sed全量替换为mirrors.aliyun.com。
坑 B|pip 网络 EOF 断流:下载 torch/vllm 时断流;原--no-cache-dir无法续传。解法:pip 缓存改 buildx cache mount +--retries 10 --timeout 120+ 拆成多个独立 RUN,外层加自动重试循环。
坑 C|deadsnakes PPA 不稳:apt-get update卡在NO_PUBKEY、构建机睡眠杀后台,构建卡死。解法:彻底弃用 deadsnakes,改用系统 Python 3.10;构建机用caffeinate -i防睡眠。
坑 D|buildx 导出方式:desktop-linux是 docker driver,只支持--load(导入本地 store),不支持type=docker导出。解法:构建直接对 store 内镜像验证,避免二次 load 占盘。
三、镜像本地验证:无 GPU 也能守住质量门
构建机(Mac)无 GPU,无法真正加载模型跑 OCR,但能在镜像产出后守住四道质量门,避免把坏镜像交付内网:
- 依赖元数据齐备:torch / vllm / transformers / fastapi / starlette / gradio / prometheus / pypdfium2 全部 present →
all key deps present in image。 - triton 工具链在位:
nvcc --version可用、/usr/local/cuda-12.8/include/cuda/std/atomic与cuda_runtime.h存在 →TOOLCHAIN_OK。 - 版本护栏正确:serve.py
>= (0,11) and < (0,12)→VERSION_GUARD_OK。 - tar 回灌校验:
docker load -i回灌成功(Loaded image: monkeyocrv2:latest)。
真正模型加载与端到端解析请在内网 GPU 服务器验证(见第六章、第七章)。这一道本地质量门,是"构建期焊死环境"承诺的实证。
四、内网服务器部署三步跑通(生产实测)
今天生产落地用的就是这条链路。交付目录MonkeyOCRv2容器生产环境部署(内含:镜像 tar、docker-compose.yml、.env.example、deploy_on_server.sh、README)已打包为压缩文件放在网盘,搜索关键词「MonkeyOCRv2」或点击文末「阅读原文」即可获取。
4.1 前置条件
- 宿主机NVIDIA 驱动 ≥ 580(CUDA 12.8 的最低驱动要求)。
- 已安装nvidia-container-toolkit,使 compose 的
gpus: all生效,把 GPU 透传给容器。 - 权重目录已就位:权重不随镜像分发(体积达数 GB),需先在可联网机器从官方仓库下载后拷入内网——
MonkeyOCRv2-B-Parsing(解析,必下)与MonkeyOCRv2-B-Und(理解,按需),官方地址:https://huggingface.co/zenosai/MonkeyOCRv2-B-Parsing 、https://huggingface.co/zenosai/MonkeyOCRv2-B-Und(国内可用 ModelScope:https://modelscope.cn/models/zenosai/MonkeyOCRv2-B-Parsing)。该目录只需可读。
4.2 导入镜像
| docker load -i monkeyocrv2_image.tar # 导入后标签 monkeyocrv2:latest |
4.3 改 .env(关键:权重挂载路径)
| cp .env.example .env vim .env # 必填:HOST_WEIGHTS_DIR=服务器上权重根目录(其下含 MonkeyOCRv2-B-Parsing / -B-Und) # 可选:TP=2(双卡)/ GPU_MEM=0.9 / SERVE_PORT=8888 / WEBUI_PORT=8891 |
权重不进镜像,运行时通过卷挂载到/app/MonkeyOCRv2/model_weight。这是镜像与离线包最大的交付差异,也是部署期唯一典型坑(见 4.6)。
4.4 一键部署
| bash deploy_on_server.sh # 首次运行自动生成 .env 并退出 → 编辑 HOST_WEIGHTS_DIR → 再次运行 # 流程:load → 校验 .env → 校验权重目录 → docker compose config → docker compose up -d |
service 形态常驻:API 监听 8888、WebUI 监听 8891,WebUI 依赖同容器内 API 就绪后自起(entrypoint 最多等 600×5s)。
4.5 三种形态用法实测
API(vLLM OpenAI 兼容,解析入口):
| http://<服务器IP>:8888/v1/chat/completions |
WebUI(人工看效果,Gradio 8891):
| 浏览器开 http://<服务器IP>:8891 |
CLI(批量 / 定时任务,复用同容器 API):
| # 基础批处理:待解析文件放 ./input,结果在 ./output docker compose --profile cli up monkeyocrv2-cli # 带选项(端到端 / 单任务),须显式带 cli 关键字 docker compose --profile cli run --rm monkeyocrv2-cli \ cli -i /data/in -o /data/out -s http://127.0.0.1:8888 --end2end |
4.6 部署期唯一典型坑:权重挂载路径不对
坑 8|权重挂载路径不对 → Engine core initialization failed
- 现象:容器能起、
docker logs报RuntimeError: Engine core initialization failed. Failed core process (3),WebUI/CLI 连 8888 全Connection refused;交互式docker run直报/app/MonkeyOCRv2/model_weight/MonkeyOCRv2-B-Parsing: No such file or directory。 - 根因:权重没挂进容器。镜像设计"权重不进镜像、运行时挂载"——若
docker run漏了-v,或docker compose的.env里HOST_WEIGHTS_DIR还是占位符/path/to/model_weight没改,Docker 会静默建个空目录挂进去,容器内model_weight/永远为空,vLLM 找不到config.json直接崩。 - 解法:确认宿主机权重真实路径,用
docker inspect <容器> --format '{{json .Mounts}}'核对实际挂载源;compose 版必须把HOST_WEIGHTS_DIR改成真实路径再docker compose up -d。挂载正确后 API/WebUI/CLI 三种形态全部正常。
五、NVIDIA GPU 适配与参数配置
这一章把"镜像怎么吃 GPU"和"每个旋钮调什么"一次讲清,全部以entrypoint.sh、serve.py、docker-compose.yml的真实实现为准。
5.1 底层适配:最小可用工具链
- 基础镜像
nvidia/cuda:12.8.0-runtime-ubuntu22.04(CUDA 12.8 runtime)。 - 刻意保留的最小 triton 工具链:
cuda-nvcc-12-8+cuda-cudart-dev-12-8(nvcc +cuda/std/atomic头 + ptxas)——vLLM 的 triton 内核在模型加载时需 JIT 编译,这正是原始"全都报错"根因,由镜像自带消除,宿主机无需再装 CUDA 开发包。 - 宿主机只需NVIDIA 驱动 ≥ 580+nvidia-container-toolkit;compose
gpus: all把 GPU 透传进容器。
5.2 .env 变量如何翻译成 vLLM 实参
entrypoint.sh读取.env变量,拼成serve.py的启动参数。映射关系如下(改.env即生效,无需改代码):
| .env 变量 | 翻译为 serve.py 实参 | 默认值 | 说明 |
|---|---|---|---|
TP | --tensor-parallel-size | 1 | 多卡切分;双卡设 2 |
GPU_MEM | --gpu-memory-utilization | 0.9 | 显存利用率上限(0~1) |
ENFORCE_EAGER | --enforce-eager(on 时加) | on | 跳过 CUDA graph/编译 |
SERVE_PORT | -p | 8888 | API 端口 |
MONKEY_MODEL | 模型目录 | MonkeyOCRv2-B-Parsing | 切 B-Und 改此值 |
WEBUI_PORT | gradio--demo-server-port | 8891 | WebUI 端口 |
注:serve.py还内置--max-num-seqs(默认 128)、--max-num-batched-tokens(默认 16384)、--max-model-len(硬编码 16384),这些当前由 serve.py 默认值提供,未走.env;如需调大并发,可在entrypoint.sh的start_serve中追加对应参数。
5.3 关键参数逐项调优建议
- TP(tensor parallel):单卡 NVIDIA 显卡设
1;双卡设2,vLLM 自带 TP 把模型切到两张卡,吞吐近似线性提升。更多卡以此类推。 - GPU_MEM:默认 0.9;显存紧张(如与其它服务共用 GPU)可调低到 0.7~0.8,给碎片和激活值留余量。
- ENFORCE_EAGER:默认
on,跳过 CUDA graph / torch.compile,规避精简宿主上的运行时编译失败。显存充足且想用 CUDA graph 提速时可改off——但需确保 triton 工具链在位(镜像已带)。 - MONKEY_MODEL:默认
MonkeyOCRv2-B-Parsing(解析);若做文档理解可切MonkeyOCRv2-B-Und。 - MAX_NUM_SEQS / MAX_MODEL_LEN:默认 128 / 16384。大页、长文档或高并发场景可调大
max-num-seqs(受显存与max-model-len约束);max-model-len直接决定单请求最长上下文,调大更吃显存。
5.4 多卡扩容与横向扩容
- 多卡切分:双卡 NVIDIA 显卡设
TP=2即可,模型自动跨卡;单卡TP=1。 - 横向扩容:复制一份 compose(改容器名/端口)指向同一权重目录,起多个 API 实例,前面接负载均衡。
server_max_inflight默认 1024 控制客户端最大在途请求。
5.5 DFlash 投机解码(诚实说明)
serve.py已支持--draft-model(DFlash 投机解码),但本构建使用 vLLM 0.11.2,走 legacy 路径,无 DFlash。DFlash 需 vLLM ≥ 0.25 且 CUDA ≥ 12.9,本期环境不满足。升级 vLLM/CUDA 链路后方可启用,届时用同版本 DFlash 草稿模型即可获得投机解码加速。
六、并发模型与性能提升
理解 MonkeyOCRv2 的并发模型,才能把 GPU 跑满、把吞吐做上去。
6.1 请求模型:一页一请求
API 是标准 vLLM 多模态 OpenAI 兼容服务,但 MonkeyOCRv2 的调用约定是「一页一请求」:每次 POST/chat/completions的 content 仅含 1 个image_url(单页 PNG 的 base64 data URI)+ 1 个文本 prompt。多页 PDF 在 API 侧不被解析,必须由上层拆页。
6.2 两维并发
- 调用方并发
page_max_inflight:同时向 API 提交几页。WebUI 默认 16、CLI 默认 64;直接调 API 时由你自己的客户端控制。 - vLLM 引擎
max-num-seqs:引擎内同时处理的序列数,默认 128;配合max-num-batched-tokens(令牌预算,默认 16384)决定批处理规模。 - 两者相乘决定吞吐上限:调用方把足够多的页并发提交,引擎在
max-num-seqs内自动批处理。
6.3 提升吞吐的旋钮
- 调大
max-num-seqs(如 256)与page_max_inflight,让引擎始终有活可批。 - 多卡
TP切分,线性提升算力。 - 显存充足时
ENFORCE_EAGER=off启用 CUDA graph,降低单步开销。 max-model-len与显存权衡:不必盲目调大,够用即可。
6.4 横向扩容
多实例 + 负载均衡是最稳的扩容方式:每份实例指向同一只读权重目录,改容器名与端口即可水平复制,server_max_inflight控制单实例在途请求上限。
6.5 直接调 API 的 PDF 处理(必看)
- WebUI / CLI:客户端用
pypdfium2把 PDF 逐页渲染成图,再逐页发 API(自动,你不用管)。注意WebUI 默认--max-pages=20,超长 PDF 只取前 20 页;CLI 全量渲染无此截断,长文档走 CLI 更全。 - 直接调 API(curl / OpenAI SDK):必须自己先把 PDF 每页转图(pypdfium2 / pdf2image / PyMuPDF),再每页调一次
/v1/chat/completions,最后按页序合并 markdown。API 本身不解析 PDF。
推荐做法:PDF → 图片(每页) → 循环/v1/chat/completions→ 按页序合并 markdown。
七、生产测试与验收清单
镜像上内网 GPU 服务器后,按下面三张清单验收,才能宣布"生产可用"。
7.1 功能验收
- API:
curl http://<IP>:8888/v1/models返回MonkeyOCRv2模型;发一张图能拿到 markdown。 - WebUI:浏览器开
:8891,上传一页文档,解析出结构化 markdown。 - CLI:
docker compose --profile cli up monkeyocrv2-cli,待解析文件放./input,结果在./output生成 markdown/json。
7.2 性能验收
| 观测项 | 方法 | 目标 |
|---|---|---|
| 单卡吞吐 | 批量页 / 总耗时 | 随并发上升趋于平稳 |
| 并发上限 | 逐步调大 page_max_inflight | 找到 max-num-seqs 拐点 |
| 显存占用 | nvidia-smi | 接近 GPU_MEM 上限但不 OOM |
| 错误率 | 查 /app/logs/serve.log | 无 Engine core 崩溃 |
性能验收时建议先用小批量探并发拐点:把page_max_inflight从 16 往上调,观察吞吐与nvidia-smi显存,到max-num-seqs成为瓶颈后调大该值。
7.3 故障排查速查
| 现象 | 可能原因 | 解法 |
|---|---|---|
| API 起不来 / 端口占用 | SERVE_PORT 冲突、安全组 | 查 docker logs 与 /app/logs/serve.log |
| 复现 "Unsupported vLLM version" | 用了旧 serve.py | 确认镜像来自 MonkeyOCRv2容器生产环境部署 目录包(已内置修复) |
| triton / inductor 编译报错 | 运行时编译失败 | 保持 ENFORCE_EAGER=on,查驱动 ≥ 580 |
| WebUI 打不开 | API 未就绪 | 确认 8888 已通、8891 已映射、等 API 就绪 |
八、结语
内网离线部署大模型的最大成本,往往不是模型本身,而是「看起来齐了、实际跑不通」的反复排查。这轮我们从离线包走到 Docker 镜像,本质是把所有"装得上"的不确定性在构建期一次性焊死,交付一个能跨 NVIDIA CUDA GPU 与 x86_64 CPU 平台的 amd64 制品。
记住三件事:CUDA 运行时别漏(镜像已烤死最小工具链)、锁文件必须一致(镜像已锁定)、权重挂载路径要填对(部署唯一典型坑)。照着本文的选型原因 + 构建记录 + 本地验证 + 三步部署 + GPU 参数 + 并发调优 + 验收清单,你也能把 MonkeyOCRv2 稳稳落进内网生产环境,数据不出域、拷进去就能跑。
你在内网部署大模型时还遇到过哪些「文档里不会写」的坑?欢迎评论区分享,一起补齐这张避坑地图。
点个在看,收藏方便复盘——内网部署资料以后还要翻。
【欢迎访问我的个人博客主页,这里有我的精选文章和AI大模型日报专栏。👇)
参考文献
- [1] MonkeyOCRv2 官方仓库:Yuliang-Liu/MonkeyOCRv2(GitHub)
- [2] vLLM 文档:docs.vllm.ai(OpenAI 兼容服务 / tensor parallel / max-num-seqs)
- [3] NVIDIA CUDA 12.8 容器镜像:nvidia/cuda(runtime vs devel)
- [4] NVIDIA Container Toolkit 文档(nvidia-container-toolkit / gpus: all)
- [5] pypdfium2 文档(PDF 逐页渲染为图片,WebUI/CLI 拆页依赖)