Handbook · 给工程师和产品
Unlimited OCR 2 上手指南 三分钟知道它是什么,半小时把它跑起来,一张能贴在评审会上的「该不该用」判定表。
市面上的 OCR 大多回答「这一页写了什么」。它回答的是另一件事:这一本讲了什么,以及它怎么排的——跨页断开的表格、跨页续写的段落、整本书的标题层级,都在一次请求里给你。代价是它比你想的更难跑通,而且单页精度并不占优。这份文档只讲清楚三件事:它是什么、怎么把它跑起来、以及什么时候你不该用它。
一句话
整篇进,整篇出
一叠页图 → 一份带版面结构的文档,不是逐页拼起来的
单窗口容量
≈ 480 页
131,072 位置上限 ÷ 每页 273 视觉 token(推算)
百页 KV 峰值
1.77 GB
分窗解码后;不分窗是 47 GB(官方实测)
最短验证
1 天
Transformers 直跑 + 抽 6 处跨页结构人工核对
01它是什么
先把它从「又一个 OCR」里择出来:它不是把识别率做得更高的 OCR,而是把「一份文档」当成一次推理单位来处理的文档解析模型。
一句话定义
输入是按页序排好的一组页面图像(或由 PDF 转成的页图),输出是整篇文档的版面块序列:每个块带类型(标题/正文/表格/公式/图注)、标题层级、归一化位置框,以及跨页的延续标记。它返回的不是一段干净正文,而是一棵能还原成 Markdown 的结构。
先记住这一点,能省你半天
它不直接输出 Markdown。模型吐的是 <PAGE><BLOCK>title<LEVEL1>[x1,y1,x2,y2]<FORK>… 这种私有块语法,必须再跑官方脚本 utils/process_unlimitedOCR2.py 才能变成人看的 Markdown。所有「跑出来是乱码」的第一现场,八成是跳过了这一步。
三个数字记住它的工作原理
| 数字 | 含义 | 它回答了什么 |
|---|---|---|
| 273 | 每页压成多少个视觉 token | 为什么百页能塞进 128K 上下文 |
| 640 | 每个解码步最多看多少个 token 4×32 本地 + 16×32 远端 | 为什么页数涨,单步算力不涨 |
| 1.77 GB | 百页文档的 KV 峰值显存 | 为什么单卡能跑千页,而不是必须堆机器 |
这三件事由两个不同机制负责,官方叙述里常被并排讲,容易误当成一个机制:640 是稀疏注意力(SR-SWA)管算力,1.77 GB 是分窗解码管显存。二者独立可拆。深度技术分析见另一份报告,这里不展开。
它的输出长什么样
下面这段是真实格式(取自官方 utils/README.md),一页一个 <PAGE>,块与块之间靠 <BLOCK> 分隔,块头与内容用 <FORK> 断开:
模型原始输出(未后处理)
<PAGE><BLOCK>title<LEVEL1>[120,88,880,142]<FORK>第三章 资产减值
<BLOCK>text[120,160,880,268]<FORK>本公司于每一资产负债表日对长期资产判断是否存在减值迹象……
<BLOCK>cross_table[120,300,880,610]<FORK><table><tr><td>项目</td><td>期末余额</td></tr>……
<PAGE><BLOCK>append_table[120,90,880,420]<FORK><table><tr><td>项目</td><td>期末余额</td></tr>……
跑一次 process_unlimitedOCR2.py 之后:
后处理输出(Markdown)
# 第三章 资产减值
本公司于每一资产负债表日对长期资产判断是否存在减值迹象……
<!-- 两个 cross_table / append_table 片段已合并,重复的表头行已自动去重 -->
<table><tr><td>项目</td><td>期末余额</td></tr>……</table>
注意最后一段:表格在 Markdown 里仍然是 HTML,跨页片段被拼成一张表并干掉了重复表头。这正是它相对「逐页识别再拼接」的核心价值——合并逻辑是模型在原生长上下文里做出的,不是你在后处理里猜的。
后处理只做到「评测对齐」,不等于「生产可用」
那个脚本的默认行为是为了对齐 MarathonDocBench 的标注格式,不是为你的业务。四件事它会「做错」:① 页眉/页脚/页码默认保留进正文;② 公式不做 $$ 包裹,和正文混在一起;③ 标题树被强制压成单根,多篇章合集会丢层级;④ 图片只给一个 pageN_x1_y1_x2_y2.jpg 的占位引用,不会替你切图。这四处都要你自己补。
和你熟悉的三条路的分工
它不是「更强的 OCR」,是一条不同的取舍线。放在一起看最清楚:
| 做法 | 强项 | 弱项 | 什么时候选它 |
|---|---|---|---|
| 传统 OCR PaddleOCR / Tesseract | 字级准确、坐标齐全、快且便宜 | 版面结构弱;跨页完全没有概念 | 只要文字和坐标,不要结构 |
| 页面切块 + 云 OCR API | 运维为零、按页计价 | 每页独立,跨页表格必然断;结构化靠模板 | 票据、单据、模板固定的单页场景 |
| 通用多模态大模型 | 能理解语义,可问答式抽取 | 百页级会崩:要么超上下文,要么分数断崖 | 少页 + 需要「理解」而非「还原」 |
| Unlimited OCR 2 | 整篇一次;跨页表格/段落/标题树天然连续 | 单页精度不如 1B 级专用模型;要自建、要后处理 | 整本要变成一份结构化文档 |
把它放进血统里看得更明白:DeepSeek-OCR 证明了「用视觉 token 压缩页面」这条路可行,Unlimited OCR(一代)把文本侧的长上下文问题解决了一半、卡在 40 页,二代把视觉侧也拆掉,才有了「页数不再是硬上限」。所以二代相对一代的唯一实质提升就是长文档——单页几乎没动(OmniDocBench 93.92 → 94.02)。
五个名词,知道结论就够
| 名词 | 一句话解释 | 你需要关心它吗 |
|---|---|---|
| SR-SWA 稀疏参考滑窗 | 每步只看近处 128 个 token + 远端 16 个「相关块」(512),合计 640 | 只在调优时关心:它决定「20 页以下没用」这条阈值 |
| 分窗解码 page window | 文档切成重叠的页窗顺序处理,边界页的 KV 复用,窗外 KV 释放 | 必须关心:页面窗口大小直接换「显存 ↔ 跨页质量」 |
| base 模式 | 每页压成一张全局视图,约 273 token | 必须关心:多页整篇的默认,也是能塞进百页的前提 |
| gundam 模式 | 单页动态切块,精度更高、token 更多 | 必须关心:单页跑默认用这个;多页文档用它会撑爆上下文 |
| <PAGE> | 页分隔符,同时是页码对齐的唯一锚点 | 必须关心:请求里必须原样保留,否则整篇页码错位 |
02怎么跑
整条链路有四段,每段都会静默失败。先看清楚失败点在哪,再动手。
三条路线,先选一条
不要一上来就部署 SGLang。先按你要验证的问题选路线——80% 的人只需要路线 A 就能做完决策。
| 路线 | 适合谁 | 耗时 | 能回答什么 | 不能回答什么 |
|---|---|---|---|---|
在线 Demohf.co/spaces/baidu/Unlimited-OCR-2 | 产品、想先看效果的人 | 1 分钟 | 输出风格、版面还原度肉眼判断 | 你的文档能不能跑、跑多快 |
| A · Transformers | 做技术验证的工程师 | 15–40 分钟 | 长文档质量、跨页合并效果、显存下限 | 并发吞吐 |
| B · SGLang | 要上服务、要批处理的人 | 1–3 小时 | 吞吐、并发、长时间稳定性 | (都要自己搭) |
路线 A · Transformers 直跑
官方在 Python 3.10 / torch==2.13.0 / transformers==4.52.1 / A800-80GB 上验证过。依赖装法照抄:
pip install torch==2.13.0 torchvision pillow transformers==4.52.1 huggingface-hub==0.36.0 \
tokenizers==0.21.4 addict==2.4.0 einops==0.8.2 easydict==1.13
加载一次,然后直接调两个方法:
import torch
from transformers import AutoModel, AutoTokenizer
model_name = "baidu/Unlimited-OCR-2"
tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True)
model = AutoModel.from_pretrained(model_name, trust_remote_code=True, use_safetensors=True,
torch_dtype=torch.bfloat16).eval().cuda()
# 多页:一次请求吃完整篇,跨页上下文才在
pages = [f"your_doc/page_{i}.jpg" for i in range(1, 8)]
text, num_tokens = model.infer_multi(tokenizer, image_files=pages,
output_path="output/multi", save_results=True)
# 单页:默认走 gundam(动态切块),精度更高
text = model.infer(tokenizer, image_file="your_doc/page_1.jpg",
output_path="output/single", save_results=True)
| 接口 | 内部做了什么 | 你要知道的一件事 |
|---|---|---|
infer_multi | 自动拼 <PAGE><image> × N + "Multi page parsing.",每页一张全局视图(base) | 输出里每个输入页对应一个 <PAGE>,顺序与入参一致——这是你后续做页码映射的依据 |
infer | 单页,默认动态切块(gundam) | 同一张图,gundam 比 base 准,但 token 数高得多 |
两个必须正视的风险
① trust_remote_code=True 意味着加载即执行仓库里的自定义代码(modeling_unlimitedocr.py)。生产环境别直接 from_pretrained,先把这两个文件过一遍。
② 官方只在 A800-80GB 上验证。24GB / 40GB 卡能不能跑属于未验证区——权重本身约 6GB,能吃下,但页图预处理与分窗调度下的峰值取决于页数和并发,别拿“权重装得下”当结论。
路线 B · SGLang 服务化
SGLang 那条路有个隐藏门槛:必须用仓库自带的定制 wheel(sglang-0.5.20.dev10+g8d62f5fb9c),和你现有的环境大概率版本冲突。所以第一步永远是独立虚拟环境:
uv venv --python 3.10
source .venv/bin/activate
uv pip install wheel/sglang-0.5.20.dev10+g8d62f5fb9c-py3-none-any.whl
uv pip install pymupdf==1.27.2.2
uv pip install fastokens==0.3.2
然后用一组环境变量把显存按住——这一段是“不设就 OOM”的部分,不是调优:
# 分窗调度:1 = 开,0 = 整文档塞一个窗口(千页级必炸)
export SGLANG_UOCR_EANABLE_DOC_LIMIT=1
export SGLANG_UOCR_DOC_GUNDAM_PAGE_WINDOW=10 # gundam 每窗页数
export SGLANG_UOCR_DOC_BASE_PAGE_WINDOW=10 # base 每窗页数
export SGLANG_UOCR_MAX_PAGE_TOKENS=20000 # 每页输出上限
# SAM 注意力偏置的分块上限:默认 512 会一次要 ~462 MiB 连续显存并可能 OOM
export SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64
# 大 crop 张量走 /dev/shm,绕开 tokenizer → scheduler 的 pickle
export SGLANG_UOCR_SHM_WRAP_MODEL_DATA=1
export SGLANG_UOCR_SHM_WRAP_MIN_MB=1
export SGLANG_VLM_CACHE_SIZE_MB=0
export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True
启动服务(关键参数已按官方建议调到保守值,尖括号内是场景相关):
python -m sglang.launch_server \
--model-path baidu/Unlimited-OCR-2 \
--served-model-name Unlimited-OCR-2 \
--chat-template unlimited-ocr \
--tokenizer-backend fastokens \
--tokenizer-worker-num 8 \
--mem-fraction-static 0.7 \
--max-running-requests 64 \
--max-prefill-tokens 2048 \
--enable-custom-logit-processor \
--enable-streaming-session \
--allow-auto-truncate \
--disable-radix-cache \
--mm-preprocess-cache-size-mb 0 \
--cuda-graph-backend-decode full --cuda-graph-max-bs-decode 64 \
--cuda-graph-backend-prefill full \
--cuda-graph-max-bs-prefill 16384 \
--cuda-graph-bs-prefill 1024 2048 \
--cuda-graph-config '{"prefill": {"backend": "full", "full_prefill_max_req": 64}}' \
--host 0.0.0.0 --port 10000
四个容易抄错的地方
① 环境变量官方拼写就是 EANABLE(不是 ENABLE),照抄,别顺手改;
② --cuda-graph-bs-prefill 的最大桶必须等于 --max-prefill-tokens:偏小会回退 eager 变慢,偏大纯粹浪费 capture 显存;
③ --mem-fraction-static 0.75 在 32+ 并发下余量不够,官方明确建议 0.7;
④ full_prefill_max_req 默认 8,不改的话 16/32 并发会静默回退到 eager。这条最阴——服务能跑,但慢了你还以为是模型的问题。
请求怎么拼
服务起好后是 OpenAI 兼容接口。下面这段是从官方示例精简出的最小可用客户端,带三处必须写对的地方:
import base64, json, os, tempfile
import fitz, requests
from sglang.srt.sampling.custom_logit_processor import DeepseekOCRNoRepeatNGramLogitProcessor
server_url = "http://127.0.0.1:10000"
NGRAM = DeepseekOCRNoRepeatNGramLogitProcessor.to_str()
session = requests.Session()
session.trust_env = False # ① 绕开系统代理,否则本机 127.0.0.1 也会被代理吞掉
def pdf_to_images(pdf_path, dpi=300):
doc = fitz.open(pdf_path)
tmp = tempfile.mkdtemp(prefix="pdf_ocr_")
mat = fitz.Matrix(dpi / 72, dpi / 72)
out = []
for i, page in enumerate(doc):
p = os.path.join(tmp, f"page_{i + 1:04d}.png")
page.get_pixmap(matrix=mat).save(p)
out.append(p)
return out
def build_content(paths):
prompt = "<PAGE><image>" * len(paths) + "Multi page parsing." # ② 页数必须与图数严格相等
parts = []
for p in paths:
b64 = base64.b64encode(open(p, "rb").read()).decode()
parts.append({"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{b64}"}})
return parts + [{"type": "text", "text": prompt}]
def generate(paths, crop_mode=True, tokens_per_page=8000):
payload = {
"model": "Unlimited-OCR-2",
"messages": [{"role": "user", "content": build_content(paths)}],
"max_tokens": tokens_per_page * len(paths), # ③ 全篇总预算,不是每页
"temperature": 0,
"skip_special_tokens": False, # 关掉就丢 <PAGE>,页码全错
"images_config": {"crop_mode": crop_mode}, # True=gundam False=base
"custom_logit_processor": NGRAM,
"custom_params": {
"ngram_size": 35,
"window_size": 1024 if len(paths) > 1 else 128, # 防复读
"page_token_id": 128827,
"eos_token_id": 1,
"max_page_tokens": 20000,
},
"stream": True,
}
r = session.post(f"{server_url}/v1/chat/completions", json=payload,
timeout=6000, stream=True)
r.raise_for_status()
chunks = []
for line in r.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
ev = json.loads(data)
for c in (ev.get("choices") or []):
chunks.append((c.get("delta") or {}).get("content") or "")
return "".join(chunks)
raw = generate(pdf_to_images("your_doc.pdf", dpi=300))
crop_mode 怎么选,这一条决定成败
多页整篇 → False(base):每页约 273 token,百页才塞得进单窗口,这是官方 infer_multi 的默认。
单页或十几页 → True(gundam):动态切块,精度明显更高,但每页 token 数会涨好几倍。
想把百页文档强行用 gundam 跑,正确做法是同时开分窗(DOC_GUNDAM_PAGE_WINDOW),代价是跨窗的表格合并质量下降。“又长又精”在这套机制里是有物理上限的,别信一键调优。
<PAGE> 流式回传,不能等最后一次性解析。后处理:把它变成能用的 Markdown
两种用法,批量跑还是嵌进代码:
# 批量:把一个目录下的原始输出(.txt/.md)全转成 Markdown
python utils/process_unlimitedOCR2.py --infer-dir raw --out-dir pred
# 嵌进流水线
from process_unlimitedOCR2 import convert
markdown = convert(raw_output)
它认得的块类型一共这些,看懂这张表你就知道能从输出里拿到什么:
| 分组 | 类型 | 转成 Markdown 后 |
|---|---|---|
| 结构 | title 带 <LEVELn> | # × n 级标题 |
| 结构 | text / ref_text / aside_text / algorithm | 正文,直接拼接 |
| 结构 | equation | 裸文本,不会自动包 $$ |
| 表格 | table / cross_table / append_table | 保留 HTML;跨页片段自动合并并去掉重复表头 |
| 图 | image / figure / chart / seal |  占位,不切图 |
| 图 | image_caption / image_footnote | 图注 / 图下注,跟图一起输出 |
| 页边 | header / footer / page_number / page_footnote | 默认保留进正文,要自己清 |
坐标是归一化值,不是像素
块头里的 [x1,y1,x2,y2] 全部落在 0–1000 的归一化坐标系里(脚本里 _scale() 会钳到 0–1000)。要回原图像素,乘上页宽高再除 1000。直接当像素用会得到一堆越界框,这是接前端坐标系统时最常见的错。
参数速查:每一个都写清楚“动了会怎样”
| 参数 | 建议 | 动了会怎样 |
|---|---|---|
--mem-fraction-static | 0.7 | 调到 0.75 在 32+ 并发下余量不足;调太低则 KV 空间不够、并发上不去 |
--max-prefill-tokens | 2048 | 临时张量约 [tokens, context/32],4096 时这块峰值翻倍 |
--cuda-graph-bs-prefill 最大桶 | = max-prefill-tokens | 偏小 → 回退 eager(慢);偏大 → 白占 capture 显存 |
full_prefill_max_req | 64 | 默认 8,16/32 并发静默回退 eager |
SGLANG_UOCR_EANABLE_DOC_LIMIT | 1 | 设 0 = 整文档单窗口:≤480 页更快,千页级必 OOM |
..._BASE_PAGE_WINDOW / ..._GUNDAM_PAGE_WINDOW | 10 | 调小省显存但伤跨页合并(跨窗延续的块丢上下文);调大反之 |
SGLANG_UOCR_MAX_PAGE_TOKENS | 20000 | 每页输出守卫,防单页失控;太小会截断密排页 |
SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB | 64 | 默认 512 会一次要 ~462 MiB 连续显存,启动即 OOM 的元凶 |
images_config.crop_mode | 多页 False 单页 True | True=gundam 切块更准、token 更多;False=base 每页 1 视图 |
max_tokens | 页数 × 8000 起 | 全篇总预算;按页算错会让你在第 30 页被整体截断 |
ngram_size / window_size | 35 / 1024(多页) | 不挂这个 logit processor 会复读;单页窗口 128 就够 |
skip_special_tokens | 必须 False | 设 True 会吃掉 <PAGE>,整篇页码错位且后处理无法恢复 |
dpi(PDF→图) | 300 | 低于 300 直接影响识别质量,尤其小字号与表格线 |
排错:症状 → 原因 → 处置
| 症状 | 最可能的原因 | 处置 |
|---|---|---|
| 服务刚起就 OOM | SAM 偏置默认 512 的一次性大分配 | SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64 |
| 跑到中途 OOM | 没开分窗,或窗口太大 | DOC_LIMIT=1,窗口降到 10 甚至 6 |
| 输出反复念同一段 | 没挂 no-repeat-ngram processor,或窗口过小 | 补 custom_logit_processor;多页把 window 提到 1024 |
| 输出在某页戛然而止 | max_tokens 或 MAX_PAGE_TOKENS 撑到上限 | 按“页数 × 8000”重算总预算 |
| 整篇内容整体偏移一页 | skip_special_tokens=true 把 <PAGE> 吃了 | 改回 False,重跑(不可事后修复) |
| 跨页表格被拆成两张 | 窗口太小,或每页用了 base 却靠后处理硬拼 | 窗口调到 10–15;确认请求里是整篇一次而不是逐页并发 |
| 跑通了但全是乱码块语法 | 跳过了后处理 | 跑 process_unlimitedOCR2.py |
| 正文里混进页码和页眉 | 脚本默认不删页边块 | 后处理后按 type 过滤,或改 DROP_LABELS |
| 所有请求都 500 / 超时 | 客户端走了系统代理,或 timeout 太短 | session.trust_env = False;timeout 给到 6000 秒 |
| 客户端 import 失败 | 客户端也要装同一个 sglang 包 | 在客户端环境里装定制 wheel |
| 单页精度不如预期 | 用了 base(每页一视图) | 单页改 gundam:crop_mode=true |
| 中文小字误识多 | 页图 dpi 不够 | PDF→图改 300 dpi,并确认没有缩放 |
验收清单:一天走完,别省第 3 步
- 1 · 拿你自己最长的一份文档跑通务必 > 100 页。单页一次、多页一次,都要出结果。通过标准:结构肉眼可读,不是“没报错”。
- 2 · 抽 6 处跨页结构逐字核对3 处跨页表格(表头去重、列对齐)+ 3 处跨页段落(有没有被截断/重复)。这是它唯一不可替代的能力,不过关就别往下走。
- 3 · 做「块边界 vs 块内部」的分层对比官方材料没做这一项。按页位置抽边界页与内部页各 10 页,算编辑距离;差距 < 2 个百分点可接受,明显偏大说明窗口要调大。这一步决定你敢不敢上生产。
- 4 · 压一次并发,记录 TPS 与峰值显存B=16 下与官方 1,315 TPS / 1.77GB KV 同量级即可;差一个数量级说明参数没调对,不是模型不行。
- 5 · 跑一遍你的真实后处理页眉页脚、公式包裹、标题树、图片切图——四件事各验一遍。这一步最容易被跳过,也最容易在上线后暴露。
03什么时候用
一句话:页数够多、且文档的结构价值大于文字价值时用它。这两条缺一条,它就不如你现有的方案。
先过三个问题,再谈部署
六个它确实值钱的场景
- 为什么适合
- 资产负债表、现金流量表常年跨页,页眉重复、表头在下一页重印。逐页方案必须写合并逻辑,它原生就给合并结果。
- 怎么配
- 多页整篇一次请求,
crop_mode=False(base),≤480 页不用开分窗。 - 预期
- 跨页表格拼成一张 HTML 表,重复表头自动去掉;标题树能直接给出“第 N 节 → 第 N.M 小节”。
- 主要坑
- 页脚的公司名与页码会进正文;表格转 DataFrame 要用
pandas.read_html()吃那段 HTML,别自己写解析。
- 为什么适合
- 这才是它名字里的“unlimited”。官方演示直接跑完了《哈利·波特》全七部与三体全集——注意这是单次演示,不是基准测试结果。
- 怎么配
- 必开
DOC_LIMIT=1,窗口 10 页;上千页的卷子建议按“卷/册”切成独立请求,别赌一个请求跑到底。 - 预期
- 正文与标题层级连续,跨页断开的句子能接上。
- 主要坑
- 书页的页眉页脚几乎全是噪声,必须清;竖排/繁体/异体字没有公开评测数据,属于未知区,先小样本试。
- 为什么适合
- 公式被页边切断、图表编号跨页续排、章节层级深(到四级标题)——这三件事逐页方案都做不好。
- 怎么配
- 页数不多(20–80)时可试
crop_mode=True加分窗,公式识别会更好;纯速度优先就 base。 - 预期
- 公式以 LaTeX 文本给出,编号与引用关系保留在正文流里。
- 主要坑
- 后处理不会把
equation包成$$…$$,你得自己按块类型补;标题树被强制单根,多篇论文合成一本时要重排层级。
- 为什么适合
- 一份卷宗几十到几百页,资质表、报价表跨页是常态,且要保留“第几页第几张表”的可追溯性。
- 怎么配
- base + 整篇;把
<PAGE>位置映射回原 PDF 页码,作为引用锚点。 - 预期
- 结构与页码可对齐,能做“原文第 37 页”级别的溯源。
- 主要坑
- 手写批注、盖章遮挡、复印件底噪都是弱项——训练数据以印刷文档为主,没有手写评测数据。有手写就老实上“专用手写模型 + 人工复核”。
- 为什么适合
- 输出里表格是标准 HTML,跨页片段已合并。
pandas.read_html()直接吃,比任何自研表格解析都省事。 - 怎么配
- SGLang + 并发;
max_tokens按“页数 × 8000”给足,避免长报表被截断。 - 预期
- 一张跨 3 页的资产负债表 → 一个 DataFrame,行不丢、表头不重。
- 主要坑
- 合并单元格(
rowspan/colspan)在 HTML 里是合法的,但read_html默认不展开;要自己 forward-fill。
- 为什么适合
- RAG 切块最怕“表格被拦腰切开”“标题与正文分家”。它给的是带层级的结构,可以按标题树切而不是按字数切。
- 怎么配
- 整篇 → Markdown → 按 heading 切段,表格整块不切,图片用 bbox 回原图裁。
- 预期
- chunk 自带章节路径(如“第三章 / 3.2 减值测试 / 表 3-1”),召回后能给出处。
- 主要坑
- 后处理默认保留页眉页脚,会污染 embedding;一定要先过滤再用。
五个明确别用的场景
| 场景 | 为什么不该用 | 用什么 |
|---|---|---|
| 单页票据、发票、证件、截图 | OmniDocBench 94.02,落后 0.8B 的 OvisOCR2(96.47)与 1.2B 的 TeleOCR(96.91),还更贵更慢 | 专用小模型 / 云 OCR API |
| 实时高并发小图(每秒几十张) | 3B MoE,每页要生成上千 token,单页延迟在秒级;稀疏化在短输入上不省反贵 | 轻量检测+识别流水线 |
| 需要字符级坐标或置信度的场景 | 它给的是块级 bbox(0–1000 归一化),没有逐字框,也没有置信度分数 | 传统 OCR 引擎 |
| 有文本层的电子版 PDF | 文本已经在文件里,OCR 只会引入误差,还要为它烧 GPU | pdfplumber / PyMuPDF 直接抽 |
| 手写为主、或需要语义问答抽取 | 训练数据以印刷文档为主,手写无公开评测;它是解析模型,不是拿来聊天的 | 手写专用模型 / 通用 VLM + 抽取提示词 |
成本粗算:先算清“每千页”再谈采购
官方给的吞吐是 1,315 TPS @ B=16(A800-80GB)。按每页约 1,000 个输出 token 反推,单卡理论上限约 4,700 页/小时;扣掉 PDF→图(300 dpi 单页约 0.2–0.4 秒 CPU)、prefill、排队与重试,实际按 40–60% 折算更稳。下面全部是算术推算,不是实测。
| 规模 | 页数 | 单卡预计耗时 | 折算 |
|---|---|---|---|
| 临时处理一份年报 | 300 | 8–15 分钟 | 不值得一提,随便一张卡 |
| 小批量归档 | 5,000 | 2–3.5 卡时 | 单卡跑一夜够用 |
| 月度批量(图书馆/律所量级) | 50,000 | 20–35 卡时 | 2–4 卡并行,一天内完成 |
把它换成钱只有一个公式:每千页成本 ≈ (1000 ÷ 单卡小时页数) × 你的卡时单价。按 2,500 页/卡时算,每千页约 0.4 卡时——卡时单价你自己代入,因为租用与自建、包月与按量的差别比模型本身的差别还大。
和按页计价的云 OCR 怎么比
云 OCR 按页收费、零运维,量小时(每月几千页)几乎总是更便宜;自建的优势要到月处理量上万页、且文档长到云 API 按页累计成本吃不住时才显现。另一条常被忽略的成本是后处理与人工抽检——那部分不随模型变,但会占掉项目一半的工期。
04边界与风险
这一节不是唱衰,是把“上线后才会发现”的事提前摆到桌面上。
| 风险 | 具体是什么 | 怎么应对 |
|---|---|---|
| 自建基准的可信度 | “百页级领先 43 分”来自百度自建的 MarathonDocBench,标注由强逐页解析器初始化,训练伪标签也来自 MinerU2.5-Pro / PaddleOCR-VL——同一批“老师”既出题又判卷 | 只看 MPDocBench(第三方,84.59)做横向对比;自己的文档必须自测 |
| 缺一条最该比的基线 | 对比里没有“通用模型 + 分段流水线”,也就是没有回答“我用逐段调用能不能更便宜地达到同样效果” | 内部试点时自己补上这条基线,很可能是它最有力的竞争者 |
| 单页精度不占优 | 94.02 排第三,落后 1B 级模型 2.5–2.9 分;相对一代只 +0.1 | 混合路由:短文档走小模型,长文档才路由给它 |
| 跨页连续性未分层验证 | 官方只给了整篇的 T-TextEdit(0.123),没有“块边界 vs 块内部”的对比,无法判断 1 页重叠够不够 | 这就是验收清单第 3 步,必须自己做 |
trust_remote_code | 加载权重即执行仓库内自定义建模代码,供应链风险实打实 | 把 modeling_unlimitedocr.py 等文件纳入代码审计;生产用固定 commit 的内网镜像 |
| 许可只覆盖代码 | 仓库 LICENSE 是 MIT(商用友好),但模型权重的许可条款需以模型卡为准;本次整理时 HF 模型卡不可达,未完成核验 | 法务按 HF / ModelScope 模型卡原文确认后再商用 |
一句总结给决策者
它的价值不在“识别更准”,而在“第一次让整本文档变成一个可查询的结构”。如果你的业务里存在“跨页的表格或段落”,并且这件事今天靠人工补,那它值得一次试点;如果你的业务是单页、实时、要高并发,它不是更贵的正确答案,而是错误答案。
05速查附录
入口与一手材料
| 用途 | 地址 |
|---|---|
代码仓库(含 infer.py、utils/) | github.com/baidu/Unlimited-OCR |
| 在线试玩(最快看效果) | huggingface.co/spaces/baidu/Unlimited-OCR-2 |
| 权重(HF) | huggingface.co/baidu/Unlimited-OCR-2 |
| 权重(国内镜像) | modelscope.cn/models/PaddlePaddle/Unlimited-OCR-2 |
| 不想自建:云 API | cloud.baidu.com/doc/OCR/s/fmr1p39gb |
| 技术报告 | arxiv.org/abs/2606.23050 |
| 自建基准数据集 | huggingface.co/datasets/baidu/MarathonDocBench |
关键常量(写死在代码里,别猜)
| 常量 | 值 | 出处 / 用途 |
|---|---|---|
page_token_id | 128827 | config.json → 传给 ngram processor 的 <PAGE> id |
image_token_id | 128815 | config.json → 视觉占位符 |
eos_token_id | 1 | 解码终止判断 |
per_page_image_tokens | 273 | 算容量、算成本的基准数 |
max_position_embeddings | 131072 | 单窗口页数上限 ≈ 131072 ÷ 273 ≈ 480 |
sparse_block_size / sparse_local_block / sparse_topk_blocks | 32 / 4 / 16 | 合起来决定每步 640 = (4+16) × 32 |
sliding_window_size | 128 | 本地窗口 |
| 层数 / 专家 / 每 token 激活 | 12 / 64+2 / 6 | MoE 结构,决定激活参数约 0.5B |
| 视觉编码器 | deeplip_b_l | CLIP-L/14 + SAM-ViT-B 双塔,1024×1024 输入 |
最小可行代码骨架
# 1. PDF → 页图(300 dpi) fitz.Matrix(300/72, 300/72)
# 2. 页图 → 原始块串 POST /v1/chat/completions,stream=True
# 3. 块串 → Markdown convert(raw) 或 process_unlimitedOCR2.py
# 4. 你的业务 按 # 标题切块 / pd.read_html 取表 / bbox 裁图
from process_unlimitedOCR2 import convert
md = convert(raw) # 别忘了这一步,否则拿到的是块语法
open("doc.md", "w").write(md)
一句话记忆卡
带走这四句就够
它是把“一整本”当一次推理的文档解析模型,卖点是跨页结构,不是识别率。
怎么跑:页图 → 整篇一次请求 → 跑 process_unlimitedOCR2.py → 才得到 Markdown。
什么时候用:≥20 页,且有跨页表格/段落/整篇标题树。
什么时候别用:单页、实时、要高并发、或 PDF 本来就有文本层。