百度 Unlimited-OCR 深度解析:R-SWA 常量KV缓存如何让OCR一口气吃下几十页文档——从单图解析到多页PDF的完整实战指南
前言:OCR的「长文档噩梦」
2026年,以DeepSeek OCR为代表的端到端OCR模型再次将文档识别推上了风口浪尖。大语言模型作为解码器,确实能够利用语言先验分布大幅提升OCR准确率——但代价同样明显:随着输出序列变长,累积的KV缓存急剧膨胀内存占用,生成速度持续下降。
这个问题有多严重?假设你要OCR一份50页的合同。传统LLM-based OCR在处理到第20页时,KV缓存可能已经吃掉几十GB显存,推理速度下降到最初的几分之一。这与人类形成鲜明对比——我们抄写文档时,无论抄多少页,效率基本恒定。
2026年6月22日,百度开源了 Unlimited OCR Works(GitHub 13K+ Stars),提出了一个优雅的解决方案:Reference Sliding Window Attention(R-SWA),让模型像人类一样拥有「恒定的工作记忆」,在标准32K上下文长度下,单次前向传播就能转录几十页文档。
本文将从架构设计、核心算法、推理部署、性能基准四个维度深度解析这个项目,并提供完整的代码实战指南。
一、项目概览:为什么叫「Unlimited」
1.1 项目定位
Unlimited-OCR 的核心主张可以用一句话概括:Welcome the Era of One-shot Long-horizon Parsing(欢迎一次性长程解析时代)。
它不是从零构建的OCR系统,而是站在DeepSeek OCR的肩膀上,解决了一个关键瓶颈:解码器的KV缓存线性增长问题。
| 特性 | DeepSeek OCR | Unlimited-OCR |
|---|---|---|
| 编码器 | 高压缩率视觉编码器 | 沿用DeepSeek OCR编码器 |
| 解码器注意力 | 标准Multi-Head Attention | R-SWA(Reference Sliding Window Attention) |
| KV缓存行为 | 线性增长 | 常量恒定 |
| 单次处理页数 | 受限于显存 | 几十页(32K上下文内) |
| 适用场景 | 单页/少量页文档 | 长文档、书籍、多页PDF |
1.2 技术栈
- 语言:Python
- 框架:PyTorch + HuggingFace Transformers
- 推理引擎:Transformers / vLLM / SGLang
- 模型架构:Vision Encoder + R-SWA Decoder
- 许可证:MIT
- 论文:arXiv:2606.23050
1.3 核心创新点
- R-SWA注意力机制:用滑动窗口+参考token的方式,将解码器的KV缓存从O(n)降到O(1)
- Gundam/Base双模式:针对单图和多页文档提供不同的图像处理策略
- 自定义N-gram去重:通过
no_repeat_ngram_size和ngram_window防止OCR输出中的重复文本 - 通用性:R-SWA不仅适用于OCR,还可用于ASR、翻译等任何需要长序列解码的任务
二、核心架构:从人类工作记忆到R-SWA
2.1 问题本质:为什么KV缓存会爆炸
在标准Transformer解码器中,每生成一个token,都需要与之前所有token的Key和Value做注意力计算。对于OCR任务,输出序列可能长达数万token(几十页文档的文本),这导致:
KV缓存大小 = 2 × num_layers × num_heads × seq_len × head_dim × dtype_bytes
以一个典型的7B模型为例:
- 32层,32个头,head_dim=128,bf16
- seq_len=32768时:KV缓存 ≈ 2 × 32 × 32 × 32768 × 128 × 2 bytes ≈ 16GB
这仅仅是KV缓存,还没算模型权重和激活值。当文档更长时,显存直接溢出。
2.2 R-SWA的设计理念
人类在抄写文档时,并不需要记住之前抄过的每一个字。我们的「工作记忆」是恒定的——只需要关注当前正在抄写的部分,加上少量「参考信息」(比如当前段落的上下文)。
R-SWA正是模拟了这种机制:
传统Attention: Q @ [K_1, K_2, ..., K_n]^T → 与所有历史token交互
R-SWA: Q @ [K_ref, K_{n-w}, ..., K_n]^T → 只与参考token + 滑动窗口交互
其中:
- Reference tokens:编码器输出的视觉特征token,作为全局参考
- Sliding Window:最近w个解码token,保持局部连贯性
- 窗口大小w:远小于总序列长度n
2.3 R-SWA的数学形式
R-SWA的注意力计算可以形式化为:
$$\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{Q \cdot [K_{\text{ref}}; K_{\text{window}}]^T}{\sqrt{d_k}}\right) \cdot [V_{\text{ref}}; V_{\text{window}}]$$
关键特性:
- KV缓存大小 =
len(reference_tokens) + window_size,这是一个常数 - 计算复杂度 = O(n × (R + w)),其中R是参考token数,w是窗口大小
- 与总序列长度n无关的缓存占用
2.4 与标准Sliding Window Attention的区别
你可能会问:Mistral等模型不也用了Sliding Window Attention吗?区别在于:
| 特性 | 标准SWA(Mistral等) | R-SWA(Unlimited-OCR) |
|---|---|---|
| 窗口类型 | 固定大小滑动窗口 | 滑动窗口 + 参考token |
| 全局信息 | 通过多层传递间接获得 | 直接通过reference tokens获得 |
| KV缓存 | 仍随层数线性增长 | 完全常量 |
| 适用场景 | 通用长文本生成 | 长程解析(OCR/ASR/翻译) |
参考token的设计是R-SWA的核心创新。在OCR场景中,编码器输出的视觉特征天然就是「全局参考」——它们包含了整个页面的布局、字体、表格等信息。将这些token作为永久参考,模型就能在不缓存所有历史解码token的情况下,始终保持对原始文档的「视觉锚定」。
三、双模式图像处理:Gundam vs Base
3.1 为什么需要两种模式
不同场景对图像处理的需求不同:
- 单页文档:需要高分辨率细节(小字、表格线),但不需要处理多页
- 多页PDF:需要统一分辨率、支持批量处理,但单页分辨率可以略低
Unlimited-OCR提供了两种配置:
# Gundam模式:单图高精度
# base_size=1024, image_size=640, crop_mode=True
# 适合:单页文档、需要精细识别的场景
# Base模式:多页统一处理
# base_size=1024, image_size=1024, crop_mode=False
# 适合:多页PDF、批量文档处理
3.2 Gundam模式的工作原理
Gundam模式的核心是crop_mode=True,它会:
- 将高分辨率图像(base_size=1024)裁剪为多个640×640的子图
- 每个子图独立编码,保留局部细节
- 编码器输出的token序列更长,但信息密度更高
这种策略类似于人类阅读时的「逐行扫描」——我们不会同时看清整页所有文字,而是通过眼球运动逐区域聚焦。
3.3 Base模式的统一处理
Base模式不做裁剪,直接将图像resize到1024×1024。对于多页文档,每页独立处理后拼接token序列,由R-SWA解码器统一生成文本。
# 多页处理的核心流程
images = [page1, page2, page3, ...] # 每页一个图像
# 编码器对每页独立编码
# 所有页面的视觉token拼接为reference tokens
# R-SWA解码器使用常量KV缓存生成全部文本
四、推理部署完整实战
4.1 环境准备
# Python 3.12 + CUDA 12.9
pip install torch==2.10.0 torchvision==0.25.0
pip install transformers==4.57.1
pip install Pillow==12.1.1 matplotlib==3.10.8
pip install einops==0.8.2 addict==2.4.0 easydict==1.13
pip install pymupdf==1.27.2.2 psutil==7.2.2
4.2 Transformers推理:单图识别
import os
import torch
from transformers import AutoModel, AutoTokenizer
# 加载模型
model_name = 'baidu/Unlimited-OCR'
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,
)
model = model.eval().cuda()
# 单图OCR - Gundam模式(高精度)
result = model.infer(
tokenizer,
prompt='<image>document parsing.',
image_file='contract_page1.jpg',
output_path='./output',
base_size=1024,
image_size=640,
crop_mode=True, # Gundam模式
max_length=32768,
no_repeat_ngram_size=35, # N-gram去重
ngram_window=128, # 短窗口(单图)
save_results=True,
)
print(result)
4.3 多页PDF解析
这是Unlimited-OCR最亮眼的场景——一口气解析整个PDF:
import tempfile
import fitz # PyMuPDF
def pdf_to_images(pdf_path, dpi=300):
"""将PDF转换为图像列表"""
doc = fitz.open(pdf_path)
tmp_dir = tempfile.mkdtemp(prefix='pdf_ocr_')
mat = fitz.Matrix(dpi / 72, dpi / 72)
paths = []
for i, page in enumerate(doc):
out = os.path.join(tmp_dir, f'page_{i+1:04d}.png')
page.get_pixmap(matrix=mat).save(out)
paths.append(out)
doc.close()
return paths
# 将50页PDF转为图像
image_files = pdf_to_images('long_contract.pdf', dpi=300)
# 一次性解析所有页面!
result = model.infer_multi(
tokenizer,
prompt='<image>Multi page parsing.',
image_files=image_files,
output_path='./output',
image_size=1024, # Base模式
max_length=32768,
no_repeat_ngram_size=35,
ngram_window=1024, # 长窗口(多页)
save_results=True,
)
关键参数说明:
no_repeat_ngram_size=35:防止35-gram重复,避免OCR输出中出现大段重复文本ngram_window:单图用128,多页用1024。窗口越大,去重范围越广
4.4 vLLM部署(生产级)
vLLM是目前最流行的大模型推理引擎,Unlimited-OCR已获得官方支持:
# 拉取官方Docker镜像
# CUDA 13.0(默认)
docker pull vllm/vllm-openai:unlimited-ocr
# Hopper GPU (CUDA 12.9)
docker pull vllm/vllm-openai:unlimited-ocr-cu129
# 启动服务
docker run --gpus all -p 8000:8000 \
vllm/vllm-openai:unlimited-ocr \
--model baidu/Unlimited-OCR \
--served-model-name unlimited-ocr \
--max-model-len 32768
详细部署参见官方Recipe:https://recipes.vllm.ai/baidu/Unlimited-OCR
4.5 SGLang部署(高性能流式)
SGLang提供了更灵活的流式推理支持,特别是自定义logit处理器:
# 环境准备
uv venv --python 3.12
source .venv/bin/activate
uv pip install wheel/sglang-0.0.0.dev11416+g92e8bb79e-py3-none-any.whl
uv pip install kernels==0.11.7
uv pip install pymupdf==1.27.2.2
# 启动SGLang服务器
python -m sglang.launch_server \
--model baidu/Unlimited-OCR \
--served-model-name Unlimited-OCR \
--attention-backend fa3 \
--page-size 1 \
--mem-fraction-static 0.8 \
--context-length 32768 \
--enable-custom-logit-processor \
--disable-overlap-schedule \
--skip-server-warmup \
--host 0.0.0.0 \
--port 10000
流式调用示例:
import base64
import json
import requests
from sglang.srt.sampling.custom_logit_processor import DeepseekOCRNoRepeatNGramLogitProcessor
server_url = "http://127.0.0.1:10000"
def encode_image(image_path):
ext = os.path.splitext(image_path)[1].lower()
mime = "image/jpeg" if ext in (".jpg", ".jpeg") else f"image/{ext.lstrip('.')}"
with open(image_path, "rb") as f:
data = base64.b64encode(f.read()).decode("utf-8")
return {"type": "image_url", "image_url": {"url": f"data:{mime};base64,{data}"}}
def stream_ocr(image_paths, image_mode="gundam", ngram_window=128):
content = [{"type": "text", "text": "document parsing."}] + \
[encode_image(p) for p in image_paths]
payload = {
"model": "Unlimited-OCR",
"messages": [{"role": "user", "content": content}],
"temperature": 0,
"skip_special_tokens": False,
"images_config": {"image_mode": image_mode},
"custom_logit_processor": DeepseekOCRNoRepeatNGramLogitProcessor.to_str(),
"custom_params": {
"ngram_size": 35,
"window_size": ngram_window,
},
"stream": True,
}
response = requests.post(
f"{server_url}/v1/chat/completions",
json=payload,
timeout=1200,
stream=True,
)
for line in response.iter_lines(chunk_size=1, decode_unicode=True):
if not line or not line.startswith("data: "):
continue
data = line[len("data: "):]
if data == "[DONE]":
break
delta = json.loads(data)["choices"][0].get("delta", {}).get("content", "")
if delta:
print(delta, end="", flush=True)
# 使用示例
stream_ocr(["document.jpg"], image_mode="gundam")
4.6 批量推理脚本
项目自带的infer.py支持批量处理:
# 批量处理图片目录
python infer.py \
--image_dir ./examples/images \
--output_dir ./outputs \
--concurrency 8 \
--image_mode gundam
# 批量处理PDF
python infer.py \
--pdf ./examples/document.pdf \
--output_dir ./outputs \
--concurrency 8 \
--image_mode gundam
# 使用本地模型路径
python infer.py \
--model_dir /path/to/local/model \
--gpu 0 \
--image_dir ./scanned_books \
--output_dir ./ocr_results \
--concurrency 4
五、性能分析与对比
5.1 KV缓存对比
让我们量化R-SWA的优势。假设处理一份50页文档(每页约500个视觉token + 1000个输出token):
标准Attention(DeepSeek OCR风格):
- 总输出token:50 × 1000 = 50,000
- KV缓存:随序列长度线性增长到50,000 token
- 7B模型峰值KV缓存:约 24GB
R-SWA(Unlimited-OCR):
- 参考token:50 × 500 = 25,000(视觉编码器输出,只缓存一次)
- 滑动窗口:假设w=1024
- 有效KV缓存:25,000 + 1024 = 26,024 token(恒定不变)
- 7B模型KV缓存:约 12GB,且不随生成进度增长
节省约50%显存,且最重要的是:生成速度不会随页数增加而下降。
5.2 推理速度基准
在单张A100-80GB GPU上的典型性能:
| 场景 | 标准OCR | Unlimited-OCR | 提升 |
|---|---|---|---|
| 1页文档 | ~2秒 | ~2秒 | 1x |
| 10页文档 | ~25秒 | ~12秒 | 2.1x |
| 30页文档 | ~120秒(可能OOM) | ~35秒 | 3.4x |
| 50页文档 | OOM | ~55秒 | ∞ |
关键洞察:Unlimited-OCR的速度优势随文档长度增加而愈发明显。在短文档上差异不大,但在长文档场景下,它可能是唯一能跑通的方案。
5.3 准确度表现
根据论文报告,在标准OCR基准上:
- 单页文档准确率与DeepSeek OCR持平或略优(得益于更好的长程依赖建模)
- 多页文档准确率显著优于所有基线(因为其他模型无法在合理资源下处理完整长文档)
六、R-SWA的通用性:不止于OCR
论文明确指出,R-SWA是一个通用的解析注意力机制,适用于任何「输入序列远长于输出序列」的场景:
6.1 语音识别(ASR)
# 语音编码器输出的声学特征token作为reference
# 滑动窗口保持解码时的局部语言连贯性
# 一次前向传播转录60分钟会议录音
微软的VibeVoice已经展示了超长语音处理的可能性,R-SWA提供了一种互补的架构思路。
6.2 机器翻译
长文档翻译同样面临KV缓存爆炸问题。R-SWA可以让翻译模型一次性处理整本书,而不是分段翻译后拼接(分段翻译会丢失跨段落的语义连贯性)。
6.3 文档理解+生成
结合RAG系统,R-SWA解码器可以作为「长文档生成器」,一次性产出几十页的分析报告、摘要或翻译。
七、与同类项目对比
7.1 OCR领域对比
| 项目 | 类型 | 长文档支持 | KV缓存 | 开源 |
|---|---|---|---|---|
| PaddleOCR | 传统Pipeline | 需分页处理 | N/A | ✅ |
| DeepSeek OCR | 端到端LLM | 受限于显存 | 线性增长 | ✅ |
| GOT-OCR | 端到端VLM | 受限于上下文 | 线性增长 | ✅ |
| Unlimited-OCR | 端到端+R-SWA | 几十页一次性 | 常量 | ✅ |
7.2 与文档理解系统的互补
Unlimited-OCR专注于「从图像到文本」的转录,而RAGFlow等系统专注于「从文本到知识」的检索增强生成。两者是天然互补的:
扫描文档 → [Unlimited-OCR] → 结构化文本 → [RAGFlow] → 知识库/问答
八、生产部署最佳实践
8.1 显存规划
| 模型规模 | 最小显存 | 推荐显存 | 说明 |
|---|---|---|---|
| 3B | 12GB | 16GB | 单页/少量页 |
| 7B | 24GB | 40GB | 多页文档 |
| 7B(vLLM) | 32GB | 80GB | 生产级高并发 |
8.2 PDF预处理优化
# DPI选择指南
# 150 DPI: 快速预览,适合纯文字大字号文档
# 200 DPI: 平衡模式,适合大多数文档
# 300 DPI: 高精度,适合小字、表格、复杂排版(推荐)
# 批量PDF处理的内存优化
import gc
def process_large_pdf(pdf_path, batch_size=10):
"""分批处理大型PDF,避免内存溢出"""
doc = fitz.open(pdf_path)
total_pages = len(doc)
results = []
for start in range(0, total_pages, batch_size):
end = min(start + batch_size, total_pages)
batch_images = []
for i in range(start, end):
page = doc[i]
mat = fitz.Matrix(300 / 72, 300 / 72)
pix = page.get_pixmap(matrix=mat)
# 直接从内存加载,不写磁盘
img = Image.frombytes("RGB", [pix.width, pix.height], pix.samples)
batch_images.append(img)
batch_result = model.infer_multi(
tokenizer,
prompt='<image>Multi page parsing.',
image_files=batch_images,
output_path=f'./output/batch_{start}',
image_size=1024,
max_length=32768,
)
results.extend(batch_result)
# 及时释放内存
del batch_images
gc.collect()
torch.cuda.empty_cache()
doc.close()
return results
8.3 并发服务架构
# 使用vLLM的并发处理能力
# 推荐配置:
# - max_num_seqs: 8-16(根据GPU显存调整)
# - max_model_len: 32768
# - gpu_memory_utilization: 0.85
# 生产环境Docker Compose示例
"""
services:
unlimited-ocr:
image: vllm/vllm-openai:unlimited-ocr
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
- "8000:8000"
command: >
--model baidu/Unlimited-OCR
--served-model-name unlimited-ocr
--max-model-len 32768
--max-num-seqs 12
--gpu-memory-utilization 0.85
"""
九、局限性与注意事项
9.1 当前局限
- 模型大小:目前主要是7B规模,对边缘设备不友好
- 语言支持:虽然支持多语言,但中文和英文表现最佳,其他语言可能需要微调
- 结构化输出:对于复杂表格、表单的结构化提取,仍需后处理
- 实时性:单页2秒的延迟不适合实时视频OCR场景
9.2 适用场景建议
| 场景 | 推荐度 | 说明 |
|---|---|---|
| 扫描文档数字化 | ⭐⭐⭐⭐⭐ | 核心场景 |
| 多页PDF解析 | ⭐⭐⭐⭐⭐ | 最大优势 |
| 书籍电子化 | ⭐⭐⭐⭐⭐ | 长文档一次性处理 |
| 实时视频OCR | ⭐⭐ | 延迟偏高 |
| 手写体识别 | ⭐⭐⭐ | 依赖训练数据覆盖 |
| 表格/表单提取 | ⭐⭐⭐ | 需要后处理 |
十、总结与展望
10.1 核心价值
Unlimited-OCR通过R-SWA机制,优雅地解决了端到端OCR模型在长文档场景下的KV缓存瓶颈。这不是简单的工程优化,而是一种架构层面的范式转换——从「记住一切」到「只记该记的」。
10.2 技术启示
- 人类认知启发的架构设计:R-SWA的设计灵感来自人类工作记忆的恒定特性,这种跨学科的思路值得借鉴
- 通用性优先:R-SWA不仅限于OCR,任何长序列解码任务都可以受益
- 站在巨人肩膀上:Unlimited-OCR没有从零开始,而是在DeepSeek OCR基础上精准解决了一个关键问题
10.3 未来展望
- 更小的模型:3B甚至1B的轻量版本,适配边缘设备
- 更多模态:结合语音、视频的多模态长程解析
- 与Agent集成:作为AI Agent的文档理解组件,支持自动化文档处理流水线
- R-SWA的标准化:如果R-SWA被更多模型采用,可能成为长序列解析的标准组件
对于需要处理大量扫描文档、PDF文件的开发者来说,Unlimited-OCR是目前最值得关注的开源项目之一。它的常量KV缓存设计,让「一口气解析几十页文档」从不可能变成了日常。