处理扫描版合同、整本年报这种长 PDF,最烦的从来不是单页识别得准不准,而是跨页:一张利润表从第 37 页排到第 38 页,切页识别完你得手动把两截表拼回去,段落被页码拦腰截断也是同样的事。百度 10 月 27 日开源的 Unlimited OCR 2 把「一次请求读任意页」当成了核心卖点,官方演示里整套《哈利·波特》7 部 3600 多页一口气读完,不用切段拼接。
发布次日下午我把它的仓库、权重和官方 Demo 都摸了一遍:GitHub 仓库 baidu/Unlimited-OCR 我调 API 看的时候是 29,526 star,MIT 许可;权重 BF16 大约 6.7GB。更重要的是我在自己的 16GB MacBook(M4)上把它跑通了——官方测试环境写的是 A800-80GB,Mac 用户第一反应都是「这跟我没关系」,实际上只差两行代码。跑通之后我还撞上了一个官方没提的坑:多页解析默认放弃高分辨率切图,表格数字会看错。下面按顺序讲。
它到底解决什么问题
先说架构,因为它决定了后面所有行为。Unlimited OCR 2 是个 3.34B 的 MoE 模型(每 token 只激活 500M,12 层,64 个路由专家加 2 个共享专家,每 token 选 6 个),视觉侧沿用 DeepEncoder,血统来自 DeepSeek-OCR。我扒了它的 config.json,几个关键数字都能对上:per_page_image_tokens: 273,即每页固定消耗 273 个视觉 token;sparse_topk_blocks: 16 乘 sparse_block_size: 32 等于 512,加上 _ring_window: 128 的局部窗口,正好是技术报告里说的「每一步解码只看 128 个局部 token 加 512 个从相关页面挑出来的远端 token」。
这套稀疏参考滑动窗口注意力(SR-SWA)的意义在于把成本和页数解耦:不管文档是 10 页还是 300 页,每步解码的工作集都是固定大小。官方给的对比是百页文档单序列峰值 KV 缓存从 47GB 降到 1.77GB,16 路并发吞吐从 360 提到 1315 token/s;基准方面 MPDocBench 84.6 分、专测百页级文档的 MarathonDocBench 77.1 分,第二名只有 33.74。这些是官方自报数,我没法复现 A800 上的吞吐,但 KV 缓存那个量级跟架构是对得上的——工作集固定,缓存自然不随页数涨。
顺便修正一个容易被带偏的预期:它不是单页精度的王者。百度自己另有 PaddleOCR-VL 走单页精细识别的路线,Unlimited OCR 2 的战场是多页连贯性和显存可行性,拿一张发票去比单页准确率是选错了工具。
官方 Space 实测:单页很好用,匿名额度只够单页
最快的上手路径是官方 Hugging Face Space,它是标准 Gradio 应用,用 gradio_client 探测到三个 API 端点:/parse_page(单页)、/parse_document(多页)、/explode_pdf(PDF 拆页)。
我先造了一份 4 页的压测文档(300dpi 扫描件式图片):段落被分页切断、同一张表横跨两页、一个带减号的公式、中英混排加一堆容易看错的数字(8,000 和 0,000 这种)。单页调用第 2 页,19.2 秒返回:
<PAGE><BLOCK>table[78, 55, 784, 133]<FORK><table><tr><td>个人订阅</td><td>2,610</td>
<td>2,088.0</td><td>8,000</td></tr><tr><td>渠道代理</td><td>622</td><td>5,138.4</td>
<td>82,600</td></tr><tr><td>合计</td><td>4,182</td><td>11,845.6</td>
<td>28,325</td></tr></table>
<BLOCK>text[76, 140, 916, 170]<FORK>其中企业版年付渠道占比 $41.3\%$ ,较上季度的 $33.7\%$ ……
<BLOCK>title[78, 180, 250, 198]<FORK>三、单位经济模型每个版面块带归一化坐标和类型(table / text / title),数字全部正确,百分比还自动转成了 LaTeX。输出不是纯文本,是带结构的标记语言,自己写个解析器就能转 Markdown 或 HTML。
然后我在多页这里撞墙了。4 页的 /parse_document 直接被拒:
AppError: The requested GPU duration (900s) is larger than the maximum allowed
Subscribe to Hugging Face PRO to allow GPU tasks up to 40 min降到 2 页,请求时长变成 270s,还是被拒;把 max_page_tokens 从 20000 降到 8000、4000 也没用——估时按页数固定,跟输出预算无关。也就是说匿名额度刚好够单页,想通过 Space 测多页合并,要么订阅 PRO,要么本地部署。这个坑我在 10 月 22 日那篇 小米 Xiaomi-Lens-0 的实测 里踩过一次类似的(ZeroGPU 按 max_new_tokens 估时),这次是按页数估,思路一样:Space 匿名跑大模型,先拿最小用例探额度,别把最想验证的用例放第一个。当然这次我第一个就放了最想要的,学费照交。
16GB Mac 实测:两行补丁,MPS 就能跑
官方模型的 Transformers 用法只给了 CUDA 示例,.eval().cuda() 一路到底。但翻源码会发现稀疏注意力的 Triton 内核只在 q.is_cuda 时启用,非 CUDA 设备会回退到纯 torch 的 block-mask 路径——也就是说 Mac 理论上能跑,真正挡路的是 modeling_unlimitedocr.py 里两处硬编码:
# modeling_unlimitedocr.py 第 785 / 789 行,vision embedding 的 scatter 写死了 .cuda()
# 原代码:
inputs_embeds[idx].masked_scatter_(images_seq_mask[idx].unsqueeze(-1).cuda(), images_in_this_batch)
# 改成跟随输入设备(第 789 行同理):
inputs_embeds[idx].masked_scatter_(images_seq_mask[idx].unsqueeze(-1).to(inputs_embeds.device), images_in_this_batch)除了这两行,其他文件不用动。我从 hf-mirror 拉了两个分片权重(4,998,430,312 + 1,689,875,264 字节,共 6.69GB),环境是 torch 2.15.0 + transformers 4.52.1(官方要求 4.52.1,别用别的版本猜),MPS 加载 36.2 秒。三组用例的结果:
| 用例 | 模式 | 耗时 | 输出 token | 结果 |
|---|---|---|---|---|
| 单页(公式页) | gundam 切图 | 18.8s | 300 | 版面块、正文全对,公式块有瑕疵(见下) |
| 两页(跨页表格) | base 整页 | 27.0s | 627 | 跨页关系认出来了,但表格没合并,数字错了一片 |
| 单页(对照) | gundam 切图 | 14.4s | 271 | 同一页数字全部正确 |
本地跑的核心代码就这么多:
# 环境:macOS 26.5.2 / Apple M4 / 16GB,Python 3.13,torch 2.15.0 + transformers 4.52.1
# 已打上面两行 .cuda() 补丁
import torch
from transformers import AutoModel, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("./model", trust_remote_code=True)
model = AutoModel.from_pretrained("./model", trust_remote_code=True,
use_safetensors=True,
torch_dtype=torch.bfloat16).eval().to("mps")
# 单页:gundam 模式(动态切图)
r = model.infer(tokenizer, image_file="page_3.jpg", crop_mode=True,
max_new_tokens=4000, return_dict=True)
print(r["text"])
# 两页:base 模式(整页视图)
r = model.infer(tokenizer, image_file=["page_1.jpg", "page_2.jpg"],
crop_mode=False, max_new_tokens=8000, return_dict=True)
print(r["text"])16GB 内存装 6.7GB 的 BF16 权重没有压力,激活只有 500M 的 MoE 在 MPS 上每页十几秒,这个速度做批处理完全可用。想省显存可以等社区出 GGUF 量化版,不过稀疏注意力那套逻辑能不能在 llama.cpp 里复刻是另一回事,短期别指望。
两个官方没写的坑
坑一:多页默认走 base 模式,数字会看错。 这是这次实测最值钱的发现。看 infer 的文档字符串:crop_mode: None -> vLLM auto (gundam tiles for one page, base for several)——单页默认动态切图(把整页切成小图分别编码,等效分辨率高),多页默认整页视图(省算力,但 273 个 token 装一页,细节分辨率低)。后果直接摆在上面的对照表里:同一页第 2 页,gundam 模式下 82,600、11,845.6、28,325 全部正确;base 模式下变成 52,600、11,045.6、20,325,两位数字看错多处。财务数据差一位就是事故。
解法是显式传 crop_mode=True 强制多页也走切图,代价是 prefill 更慢、显存更高。我的建议:长文档里只要有表格或数字,就强制 gundam;纯文字书稿用 base 才划算。SGLang 部署的话对应 images_config: {"crop_mode": true}。
坑二:跨页表格「合并」没有发生,至少在我这条路径上没有。 官方演示和报道都强调跨页表格自动合并成一张表,但我本地两页实测的输出里,第 2 页的表格续行被标成了一个独立的 append_table 块,没有并进第 1 页那张表——模型认出了「这是上一张表的延续」(块类型本身就是证据),但没有输出合并后的单表。Space 的多页路径因为额度墙没能验证,所以这事我给个诚实结论:合并能力存在与否、在哪个部署路径下生效,未实测,需人工确认;拿到 append_table 块自己按表格结构拼一下并不难,别指望开箱即合并。
顺带一提公式:第 3 页的公式被识别成 equation 块并输出了 LaTeX,但减号丢了,R_total − R_onetime 变成 R\_{t}otal\quad R\_{o}netime——减号被吃成空距。正文文本里的 R_total 倒是完好。公式场景建议识别完人工过一遍。
什么场景我建议你别用它
- 单张票据、发票、单页表单。这类活 PaddleOCR 或任何传统 OCR 流水线更快更准,Unlimited OCR 2 的多页机制在这里纯属负担,光加载就 36 秒。
- 把输出直接进财务流程。上面已经演示了数字看错的样子,任何数字进账目前都必须有校验环节(比如重跑一遍比对,或按 bbox 回原图裁剪复核)。
- 没有 GPU 又想跑生产服务。MPS 能跑通是「能用」级别,官方推荐的服务方案是带定制 wheel 的 SGLang,那份环境变量清单里的页窗口、显存配比都是按 A800 调的,Mac 上没有对应的生产路径。
- 需要 100% 保真公式和复杂化学式、乐谱的场景。公式块会丢符号,这类内容当前版本过不了关。
上手路径建议
最省事的验证顺序:先拿官方 Space 的 /parse_page 用单页试你的真实文档(免费、19 秒出结果),确认识别质量达标;要上多页,本地部署走 Transformers + 两行补丁(Mac)或照官方 Transformers/SGLang 示例(NVIDIA 卡),多页带表格的文档记得强制 crop_mode=True。文档切完要入检索库的话,接着用嵌入模型建向量索引,可以看我之前那篇 EmbeddingGemma 2 的本地实测,两件事正好是一条 RAG 管线的上下游;模型选型上如果纠结「要不要这么大」,LFM3 那篇六款 2B 模型的实测 和 讯飞 Spark-X2.6-4B 的端侧实测 可以对照着看——OCR 这个任务和通用对话不一样,3.3B 总参、500M 激活的 MoE 是专才,别拿通用模型的小体积直接类比。
最后交个底:文中所有第一人称数据都来自我这台 M4 / 16GB MacBook 和官方 Space 的真实调用,Space 的多页路径、SGLang 部署和官方跑分(3600 页《哈利·波特》、884 页《指环王》42 分 56 秒,见 新华网报道)我没有复现,前者卡在匿名额度,后两者需要 A800 级别的卡。模型同时上了 ModelScope 和百度智能云,国内下载权重建议直接走镜像或 ModelScope,huggingface.co 直连在多数国内网络下不通。