我第一次做文档解析,是把 PaddleOCR 塞进一台带 GPU 的机器上的,容器跑起来 GPU 利用率常年 3%,CPU 却满载。后来换成一台纯 CPU 的机器,反而快了。
这件事让我重新算了一遍 OCR 流水线里 CPU 和 GPU 的分工,结论跟直觉相反。
先把流水线拆开,看谁在耗什么
一个完整的 OCR 任务大致是这么走的:
| 阶段 | 主要资源 | 能不能并行 |
|---|---|---|
| PDF 解码 / 页面栅格化 | CPU + 内存带宽 | 页面之间完全独立 |
| 预处理(灰度、二值化、纠偏) | CPU | 页面之间完全独立 |
| 版面检测 | GPU 友好,但小模型也够用 | 批处理友好 |
| 文字识别 | GPU 或 CPU 都行 | 批处理友好 |
| 后处理(段落重组、表格对齐) | CPU | 页面之间独立 |
看出规律了吗:真正吃满 CPU 的是前三段,而它们恰好是最不需要 GPU 的部分。
OCR 是个典型的吞吐型任务——一次处理几千页文档,你关心的是每小时能出多少页,不是单页延迟。这种任务对核数的敏感度是接近线性的,一台 16 核机器能干的活,理论上就是两台 8 核机器的量。
反直觉之一:加 GPU 的收益,小于加核数的收益
OCR 里唯一真能吃到 GPU 的是版面检测和识别两个模型。但它们都很小:
- 典型的文本检测模型,输入几百像素高的图,输出框位置。这类模型在 GPU 上的加速比看起来不错,但单次推理时间本来就在毫秒级,瓶颈根本不在那。
- 更大的问题是数据搬运。每一页要先从内存拷到显存、算完再拷回来,PCIe 上这几个 GB 的来回搬运在毫秒级任务面前占比非常难看。
我那台带卡机器上真正的现象是:GPU 大部分时间在等数据,CPU 在忙着把 PDF 拆页、渲染成位图、再喂给框架。
结论是:OCR 这类流水线,核数比显卡更值钱。先按核数往上堆,GPU 放到确实需要的那一段。
反直觉之二:核数越多,内存先崩
我第二次踩坑是给一台 16 核 32G 的机器起了 16 个并行 worker,想榨干 CPU。结果进程被 OOM killer 一个个杀掉,日志里全是"进程退出,没有错误"。
原因是单个 worker 的内存占用非常吓人:
300dpi 的 A4 页面栅格化成 RGB 位图:
2480 × 3508 × 3 字节 ≈ 26MB
这只是一个页面。worker 内部还要同时持有:解码器缓冲区、灰度图、二值化结果、检测模型的输入张量、识别模型的中间特征。实际一个 worker 稳定在 500MB 到 1GB 很常见。
16 个 worker 就是 8 到 16GB,再叠加模型本身,32G 机器根本不够。
所以正确的公式不是"worker 数 = 核数",而是:
worker 数 = min(核数 - 2, 可用内存 / 单 worker 峰值内存)
留两个核给主进程和 IO,两边都别打满。
services:
ocr:
image: my-ocr:latest
deploy:
resources:
limits:
cpus: "14.0"
memory: 28G
environment:
- OMP_NUM_THREADS=1
- OMP_WAIT_POLICY=PASSIVE
- WORKER_COUNT=12
上面那个 OMP_NUM_THREADS=1 是个容易漏的坑。OpenMP 默认每个进程按核数起线程,12 个 worker × 14 核 = 168 个线程互相抢,调度开销能把吞吐打下来一半,而且内存翻倍。解析类任务基本是单页单次计算,让它一个线程老老实实跑反而更快。
反直觉之三:文件类型比参数更影响体验
同样是"解析",我见过的速度差异:
| 输入 | 页/分钟(8 核机器) | 主要耗时 |
|---|---|---|
| 电子版 PDF(纯文本层) | 数千页 | 几乎不耗时,直接抽文本 |
| 扫描 PDF(300dpi 位图) | 几十到一百页 | 栅格化 + 识别 |
| 手机翻拍的照片 | 十几张 | 纠偏 + 去阴影 + 识别 |
| 表格密集的报表 | 二十几页 | 表格结构还原最慢 |
这里最反直觉的是:很多"我们只能做 OCR"的场景,其实电子版文档直接抽文本层就行,快几十倍。
判断方法很简单,扫一遍 PDF 里有没有可提取的文字层:
import fitz
doc = fitz.open(path)
page = doc[0]
text = page.get_text("text").strip()
has_text_layer = len(text) > 50
超过一半的页面没有文字层,才真的需要上 OCR。这个判断在项目早期省掉了我一整轮的无用功——因为需求方给的样本里,本以为全是扫描件,结果八成都有文字层。
按量级选配置
我现在的经验区间:
| 场景 | 建议配置 | 备注 |
|---|---|---|
| 每天几百页 | 4-8 核 / 8-16G | 单进程加线程池就够 |
| 每天几千页 | 16 核 / 32G | 进程池,worker 数按内存倒推 |
| 每天几万页 | 32 核 / 64G + 独立对象存储 | 队列拆任务,分批处理 |
| 强依赖表格结构还原 | 16 核 / 32G 起步 | 这类任务单页耗时最不可控 |
有一点要提前想清楚:文档解析是典型的"批处理 + 可延迟"任务。它不需要和在线接口抢资源,最好是丢进队列由专门的 worker 机器慢慢消化,跟在线服务分开放。这样在线接口的延迟不会因为队列里堆了 2000 页待处理文档而波动。
最后一句,关于 CPU 型号,同代次里单核性能差异不大,真正拉开差距的是物理核数和内存带宽。文档解析对内存带宽的敏感度高于普通后端服务,因为它一直在搬大块位图。核数没跑满但吞吐上不去的时候,往内存带宽方向查比往 CPU 型号方向查更有效。
具体的实例规格和按量计费规则各家都在改,写死数字容易过期,以官方页面为准。