Baidu released Unlimited-OCR 2 on October 27, 2026: an MIT-licensed, 3B-parameter document parser that reads a whole PDF, from a few pages to a full book, in one request. The model card claims the best scores on two long-document benchmarks, and the trick is an attention layer that keeps the cost of each step flat as the document grows. If you feed PDFs and scans into a RAG pipeline, parsing is usually the step that breaks first, so this one is worth a close look.
I wrote this on November 3, 2026. I checked every spec and command against the model card and its full revision history, config.json, the Hugging Face API, the GitHub repo and the arXiv paper, and I read the post-processing script the repo ships. I haven’t run the model, so there are no first-hand speed or accuracy claims here: every benchmark below is Baidu’s own.
1. The spec card
Unlimited-OCR 2 keeps v1’s body: a DeepSeek-OCR-style vision encoder feeding a small mixture-of-experts decoder. What changed is how the decoder looks back at the pages. Here it is next to v1, from the cards, the HF API and config.json.
Unlimited-OCR 2 vs Unlimited-OCR (checked November 3, 2026)
Parameter counts come from the safetensors metadata on the HF API. The paper rounds both to “3B-A0.5B”: 3B in total, with about 0.5B active per token.
| Spec | Unlimited-OCR 2 | Unlimited-OCR (v1) |
|---|---|---|
| Released | October 27, 2026 (HF repo created October 26) | June 22, 2026 |
| Parameters | 3.34B, BF16 | 3.34B, BF16 |
| Decoder | 12-layer MoE: 64 routed experts (6 active per token) + 2 shared | Same |
| Vision encoder | DeepEncoder (SAM ViT-B + CLIP-L), frozen during v2 training | Same |
| Decoder attention | SR-SWA: a fixed budget of reference tokens | R-SWA: a sliding window plus every visual token |
| Position range | 131,072 (YaRN ×4 over 32,768) | 32,768 |
| Pages per request | Evaluated up to 300; the card’s SGLang setup uses 10-page windows | About 40, per the v2 paper |
| Input | Page images (JPG, PNG); PDFs rendered at 300 DPI with PyMuPDF | Same |
| Output | Layout blocks with boxes, HTML tables and one <PAGE> marker per page | Layout blocks with <|det|> tags |
| Serving on the card | Transformers, SGLang (custom wheel) | Transformers, vLLM, SGLang |
| License | MIT | MIT |
| Tested on | NVIDIA A800-80GB, Python 3.10 | Python 3.12.3, CUDA 12.9 |
Release facts
- • Hugging Face: baidu/Unlimited-OCR-2. The repo was created on October 26 and the card last changed on November 2. On November 3 the API showed 189,897 downloads and 576 likes.
- • Code: github.com/baidu/Unlimited-OCR, the same repo as v1 (about 30,400 stars, MIT). The card links v1’s code on the UnlimitedOCR1.0 branch.
- • Paper: “Unlimited OCR Works 2”, arXiv 2610.39308, submitted October 29 by 14 authors (the card’s citation credits Baidu’s ERNIE team). v1’s paper is arXiv 2606.23050.
- • Also on ModelScope (PaddlePaddle/Unlimited-OCR-2), Baidu Cloud and a Hugging Face Space demo. The GitHub README adds fine-tuning support in ms-swift as of October 28.
- • Lineage: v1 used DeepSeek-OCR as its baseline, and both cards thank DeepSeek-OCR, DeepSeek-OCR-2 and PaddleOCR. v2 starts from the v1 checkpoint and was post-trained for one epoch.
- • Weights: two safetensors shards, 6.69 GB in total.
It doesn’t output Markdown
It’s easy to assume it writes Markdown, but the repo’s post-processing docs are blunt: “Unlimited-OCR 2 does not emit Markdown.” You get one string of layout blocks, each with a type (title, text, table, image, header…), a bounding box and its content, plus one <PAGE> marker per input page. Tables are HTML and headings carry a level tag. The repo’s process_unlimitedOCR2.py turns that into Markdown using only the standard library, and section 4 builds on it.
2. What’s new in v2: from long to unlimited parsing
v1’s idea was R-SWA, Reference Sliding Window Attention. It kept the KV cache constant while the model wrote, so it could transcribe dozens of pages under a 32K limit. But every new token still attended to every visual token of every page. At a hundred pages, that visual context becomes the bottleneck, and each decoding step gets slower as the document grows.
v2 swaps every decoder attention layer for SR-SWA, Sparse Reference Sliding Window Attention, and adds a new way to run long documents. The paper’s analogy is a person copying a book: eyes on the current page, with a quick glance back when a table runs over. It has three parts.
The three parts, in plain words
Page Gate
Narrows what each token can look at, from the whole document down to the relevant pages, while keeping track of where each page sits in the document.
Token Sieve
Picks a fixed top-k set of remote token blocks to keep long-range links alive, and learns that choice from the normal training loss. The card puts the working set at a fixed size, “e.g., 512 tokens”.
Unlimited-Horizon Decoding
Cuts the document into overlapping page chunks and runs them in order. Adjacent chunks share a boundary page whose KV cache is reused; everything outside the active chunk is freed. In SGLang, this is the page window.
The payoff, per the paper: at the hundred-page scale, each decoding step is 7.0× faster than full attention and 5.6× faster than v1’s R-SWA (details in section 3). On MPDocBench, with documents of up to 30 pages, v2 beats v1 on every metric, most of all where content crosses a page break.
v1 vs v2 on MPDocBench (self-reported, paper Table 1)
↑ higher is better; ↓ lower is better (edit distance). The cross-page rows score text and tables that continue over a page break.
| Metric | Unlimited-OCR (v1) | Unlimited-OCR 2 |
|---|---|---|
| Overall ↑ | 61.89 | 84.59 |
| Text edit distance ↓ | 0.167 | 0.052 |
| Cross-page text edit ↓ | 0.605 | 0.100 |
| Formula CDM ↑ | 73.71 | 89.39 |
| Table TEDS ↑ | 73.51 | 87.78 |
| Cross-page table TEDS ↑ | 60.76 | 89.54 |
| Figure F1 ↑ | 53.19 | 72.91 |
| Reading order edit ↓ | 0.204 | 0.077 |
| Heading TEDS ↑ | 31.58 | 60.10 |
Other changes you’ll notice
- • You no longer write the prompt: the model adds “<PAGE><image>” once per page plus “Multi page parsing.” for you. v1 asked for prompts such as “<image>document parsing.”.
- • A different stack: the v2 card pins Python 3.10, torch 2.13.0 and transformers 4.52.1. v1 was tested with Python 3.12.3, torch 2.10.0 and transformers 4.57.1, so give each its own environment.
- • Training: about 2M multi-page and 10M single-page documents, pseudo-labeled with MinerU2.5-Pro and PaddleOCR-VL-v1.6 plus synthetic pages, on 25 nodes of 8 NVIDIA H800s. Only the language model was trained; the vision encoder stayed frozen.
- • Serving moved to SGLang, with page-window scheduling built in. The v2 card has no vLLM section.
- • A new benchmark, MarathonDocBench: 100 Chinese and English documents of 100 to 300 pages (about 18,000 pages), released by Baidu alongside the model.
The one-sentence version
v1 stopped memory from growing while the model wrote; v2 also stops the reading cost from growing, so Baidu says a 300-page PDF costs about the same per decoding step as a 30-page one.
3. Benchmarks vs other models (self-reported)
Baidu’s leaderboard puts v2 against general-purpose vision-language models on two multi-page benchmarks. Dedicated OCR models such as DeepSeek-OCR 2 only show up in the paper’s single-page table, so I split this into two tables.
Overall score on multi-page benchmarks (self-reported, paper Tables 1 and 2)
MPDocBench: up to 30 pages. MarathonDocBench: 100–300 pages, built by Baidu. Overall is the mean of eight metrics. Selected rows.
| Model | MPDocBench | MarathonDocBench |
|---|---|---|
| Unlimited-OCR 2 | 84.59 | 77.06 |
| Qwen3.8-Max | 78.30 | 22.93 |
| Qwen-3.8-Flash-Next | 77.36 | 27.83 |
| Gemini-3.8-Flash | 76.25 | 28.61 |
| Gemini-3.1-Pro Preview | 71.79 | 26.97 |
| Kimi-K3 | 60.77 | 33.74 |
| Unlimited-OCR (v1) | 61.89 | Not reported (40-page limit) |
On MPDocBench the lead is real but moderate: 6.3 points over Qwen3.8-Max. On MarathonDocBench it’s 43 points over the next model, and the fine print explains a good part of that gap.
Read the MarathonDocBench fine print
The dataset card says the general VLMs got all pages in one request, with images downscaled and retried when a request was too big. Documents over a provider’s image cap were skipped, and failures were left as empty predictions, so the denominator stays at 100 documents. That measures API limits as much as reading skill. It’s still the right test if you want to send a 200-page PDF in one call, but it isn’t an accuracy contest on equal terms.
Single page: OmniDocBench v1.6 (self-reported, paper Table 3)
Overall ↑, text edit distance ↓, table TEDS ↑. Sizes as the paper lists them. Selected rows.
| Model | Size | Overall | Text edit | Table TEDS |
|---|---|---|---|---|
| TeleOCR | 1.2B | 96.91 | 0.027 | 96.82 |
| OvisOCR2 | 0.8B | 96.47 | 0.027 | 94.58 |
| Unlimited-OCR 2 | 3B-A0.5B | 94.02 | 0.038 | 89.60 |
| Unlimited-OCR (v1) | 3B-A0.5B | 93.92 | 0.042 | 90.16 |
| Qianfan-OCR | 4B | 93.90 | 0.040 | 90.53 |
| dots.ocr | 3B | 90.77 | 0.048 | 87.18 |
| DeepSeek-OCR 2 | 3B-A0.5B | 90.25 | 0.050 | 83.89 |
On single pages, v2 is basically v1: 94.02 vs 93.92 overall, with a slightly lower table score (89.60 vs 90.16). It beats DeepSeek-OCR 2 and dots.ocr, but two smaller models, TeleOCR and OvisOCR2, score higher. PaddleOCR-VL isn’t in the table; Baidu used PaddleOCR-VL-v1.6 as a pseudo-labeler instead. For one- or two-page documents, v2 isn’t the obvious pick. Its case is length.
Speed and memory claims (paper, self-reported)
The paper runs its per-step latency tests in gundam mode, with batch size 3 and 1,024 generated tokens per page. Its Unlimited-Horizon Decoding table, run on MarathonDocBench, shows peak KV memory falling from 47.00 GB to 1.77 GB for one sequence and throughput rising from 360 to 1,315 tokens per second at batch size 16. The paper doesn’t name the GPU used for these runs.
4. Quickstart: Transformers, SGLang and a RAG ingestion step
The v2 card documents two paths: Transformers “for quick tests” and SGLang, “recommended for serving”. Every block below is copied from the card, except the last two, which are my own glue and say so. Both paths assume an NVIDIA GPU, and the SGLang setup also assumes Linux. If you’ve read that v2 ships vLLM and Docker Model Runner recipes, they aren’t in any revision of the card; the vLLM recipe is for v1.
Ways to run it (checked November 3, 2026)
From the v2 card and its revision history, the GitHub README, the HF API and the vLLM recipes site.
| Option | Status for v2 | Notes |
|---|---|---|
| Transformers | On the card, “for quick tests” | trust_remote_code; Python 3.10, torch 2.13.0, transformers 4.52.1; tested on an A800-80GB |
| SGLang | On the card, “recommended for serving” | Custom wheel sglang-0.5.20.dev13 in the repo’s wheel/ folder; OpenAI-compatible API on port 10000 |
| vLLM | Not documented for v2 | The recipe and the vllm/vllm-openai:unlimited-ocr images are for v1; recipes.vllm.ai/baidu/Unlimited-OCR-2 returned 404 |
| Docker Model Runner | Not mentioned | No Docker instructions on the v2 card or the GitHub README |
| Hosted demo | Running | Hugging Face Space baidu/Unlimited-OCR-2 on ZeroGPU, plus ModelScope and Baidu Cloud |
| Fine-tuning | ms-swift | Added October 28, per the GitHub README |
Transformers: install (model card)
The card’s pinned versions, tested with Python 3.10 on an NVIDIA A800-80GB. Use a fresh virtual environment.
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.13Transformers: load once, then parse (model card)
infer_multi sends every page in one request, so the model can use cross-page context; infer handles one page with dynamic tiling. With save_results=True, it writes result.md, result_raw.md and one result_with_boxes_N.jpg per page to output_path.
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()
# Multi-page inference: all pages in one request, in page order
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)
# Single-page inference: dynamic tiling (gundam mode) by default
text = model.infer(tokenizer, image_file="your_doc/page_1.jpg",
output_path="output/single", save_results=True)SGLang: environment (model card)
Run it from a clone of the repo, since the wheel path is relative. On GitHub the wheel is a Git LFS file (a 133-byte pointer without LFS); the Hugging Face repo has the full 15.3 MB wheel.
uv venv --python 3.10
source .venv/bin/activate
uv pip install wheel/sglang-0.5.20.dev13+g75b9b06b02-py3-none-any.whl
uv pip install pymupdf==1.27.2.2
uv pip install fastokens==0.3.2SGLang: start the server (model card)
Copy the variable names exactly as written, including SGLANG_UOCR_EANABLE_DOC_LIMIT with its typo. I didn’t check whether the wheel also accepts a corrected spelling.
# Page-window scheduling for very long documents (the KV tail of the previous window is reused).
export SGLANG_UOCR_EANABLE_DOC_LIMIT=1 # 1 = enable, 0 = whole document in one window
export SGLANG_UOCR_DOC_GUNDAM_PAGE_WINDOW=10 # pages per window (gundam mode)
export SGLANG_UOCR_DOC_BASE_PAGE_WINDOW=10 # pages per window (base mode)
export SGLANG_UOCR_MAX_PAGE_TOKENS=20000 # per-page output cap; must be > 0
# Chunk cap for the SAM attention bias. The built-in default (512) allows a single
# ~462 MiB allocation and can OOM; 64 is equivalent with a lower peak.
export SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64
# Pass large crop tensors through /dev/shm instead of pickling them into the
# tokenizer -> scheduler frame.
export SGLANG_UOCR_SHM_WRAP_MODEL_DATA=1
export SGLANG_UOCR_SHM_WRAP_MIN_MB=1
export SGLANG_VLM_CACHE_SIZE_MB=0
# Reduces fragmentation failures on the large contiguous allocations.
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 10000SGLang: OpenAI-compatible client (model card)
Streams the output and returns it as one string. The custom n-gram logit processor curbs repetition loops, and max_tokens is the output budget for the whole document.
import base64
import json
import os
import tempfile
import fitz
import requests
from sglang.srt.sampling.custom_logit_processor import DeepseekOCRNoRepeatNGramLogitProcessor
server_url = "http://127.0.0.1:10000"
NGRAM_PROCESSOR = DeepseekOCRNoRepeatNGramLogitProcessor.to_str()
session = requests.Session()
session.trust_env = False
def pdf_to_images(pdf_path, dpi=300):
doc = fitz.open(pdf_path)
tmp_dir = tempfile.mkdtemp(prefix="pdf_ocr_")
mat = fitz.Matrix(dpi / 72, dpi / 72)
image_paths = []
for i, page in enumerate(doc):
image_path = os.path.join(tmp_dir, f"page_{i + 1:04d}.png")
page.get_pixmap(matrix=mat).save(image_path)
image_paths.append(image_path)
doc.close()
return image_paths
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 build_content(image_paths):
prompt = "<PAGE><image>" * len(image_paths) + "Multi page parsing."
return [encode_image(path) for path in image_paths] + [{"type": "text", "text": prompt}]
def generate(image_paths, crop_mode=True, tokens_per_page=8000):
# A wider n-gram window for multi-page documents, as in our evaluation setup.
window_size = 1024 if len(image_paths) > 1 else 128
payload = {
"model": "Unlimited-OCR-2",
"messages": [{"role": "user", "content": build_content(image_paths)}],
# Must be positive: it is the global output budget for the whole document.
"max_tokens": tokens_per_page * len(image_paths),
"temperature": 0,
"skip_special_tokens": False,
"images_config": {"crop_mode": crop_mode}, # True = gundam (tiles), False = base
"custom_logit_processor": NGRAM_PROCESSOR,
"custom_params": {
"ngram_size": 35,
"window_size": window_size,
"page_token_id": 128827, # <PAGE> marker id (= sparse_attention_config.page_token_id)
"eos_token_id": 1, # = eos_token_id in config.json
"max_page_tokens": 20000, # per-page output cap, <= 0 disables the page guard
},
"stream": True,
}
response = session.post(
f"{server_url}/v1/chat/completions",
headers={"Content-Type": "application/json"},
data=json.dumps(payload),
timeout=6000,
stream=True,
)
response.raise_for_status()
chunks = []
for line in response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
data = line[len("data:"):].strip()
if data == "[DONE]":
break
event = json.loads(data)
choices = event.get("choices") or [] # the final chunk carries usage only
if not choices:
continue
delta = (choices[0].get("delta") or {}).get("content") or ""
if delta:
print(delta, end="", flush=True)
chunks.append(delta)
return "".join(chunks)
# Multi page: all pages of a document, parsed in one request
pages = [f"your_doc/page_{i}.jpg" for i in range(1, 8)]
generate(pages)
# Single page
generate(["your_doc/page_1.jpg"])
# PDF
generate(pdf_to_images("your_doc.pdf", dpi=300))Memory knobs the card calls out
- • --mem-fraction-static 0.7 is stable; 0.75 leaves too little headroom at 32+ concurrent requests.
- • --max-prefill-tokens drives a temporary tensor that grows linearly with it: going from 4096 to 2048 halves that peak. The largest --cuda-graph-bs-prefill bucket must equal it.
- • SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64 avoids a single ~462 MiB allocation that can run out of memory with the default of 512.
- • Smaller page windows lower peak memory but hurt merging for blocks that cross a window boundary.
From PDF to chunks: a RAG ingestion step
Here’s how I’d wire it into ingestion. The card’s client already renders a PDF to 300 DPI page images and returns the raw output. The first block below saves it three ways: raw, as whole-document Markdown for chunking, and as one Markdown file per page for citations. The second is a minimal heading-aware chunker. I ran the chunker on a hand-written Markdown sample; neither block has seen real model output. If you’re choosing a strategy, my RAG chunking guide compares fixed, semantic and late chunking.
Glue (mine, not from the card): save the raw output and Markdown
Needs utils/process_unlimitedOCR2.py from the GitHub repo (standard library only). The DROP_LABELS line is my tweak: by default the converter keeps running headers, footers and page numbers, which repeat on every page and pollute chunks. Per-page files lose cross-page merges, so use them for citations, not as the main index.
# Glue code (mine, not from the model card). Run it after the card's client above.
# Needs utils/process_unlimitedOCR2.py from github.com/baidu/Unlimited-OCR (stdlib only).
import sys
from pathlib import Path
sys.path.append("Unlimited-OCR/utils")
import process_unlimitedOCR2 as uocr
# My tweak: running headers, footers and page numbers repeat on every page
uocr.DROP_LABELS.update({"header", "footer", "page_number"})
def save_for_chunker(doc_id, raw, out_dir="ocr_out"):
out = Path(out_dir)
out.mkdir(parents=True, exist_ok=True)
# 1) Raw layout blocks: one <PAGE> marker per input page, with bounding boxes
(out / f"{doc_id}.raw.txt").write_text(raw, encoding="utf-8")
# 2) Whole-document Markdown: cross-page tables and paragraphs merged
(out / f"{doc_id}.md").write_text(uocr.convert(raw), encoding="utf-8")
# 3) One Markdown file per page, for page-level citations (no cross-page merging)
pages = raw.split("<PAGE>")[1:]
for n, page in enumerate(pages, start=1):
(out / f"{doc_id}.p{n:04d}.md").write_text(uocr.convert(page), encoding="utf-8")
return len(pages)
raw = generate(pdf_to_images("your_doc.pdf", dpi=300))
print(save_for_chunker("your_doc", raw), "pages saved")Glue (mine): a heading-aware chunker
Splits on Markdown headings, then packs blocks up to a size cap and keeps the heading as metadata. A table usually comes out as one HTML block, so a big table becomes its own oversized chunk instead of being cut mid-row.
# Glue code (mine): split on Markdown headings, then pack blocks up to a size cap.
import re
def chunk_markdown(md, doc_id, max_chars=2000):
chunks = []
for section in re.split(r"(?m)^(?=#{1,6} )", md):
section = section.strip()
if not section:
continue
heading = section.splitlines()[0] if section.startswith("#") else ""
buf = ""
# convert() separates blocks with blank lines, so a table usually stays whole
for block in section.split("\n\n"):
if buf.strip() not in ("", heading) and len(buf) + len(block) > max_chars:
chunks.append({"doc": doc_id, "heading": heading, "text": buf.strip()})
buf = ""
buf += block + "\n\n"
if buf.strip() not in ("", heading):
chunks.append({"doc": doc_id, "heading": heading, "text": buf.strip()})
return chunks
md = open("ocr_out/your_doc.md", encoding="utf-8").read()
chunks = chunk_markdown(md, "your_doc")
print(len(chunks), "chunks ready for your embedding model")Where the chunks go next
Embed the chunks with the model you already use and load them into your vector store; my pgvector vs Qdrant guide covers that side. If you’d rather not wire it yourself, OpenRAG bundles ingestion, search and chat, and its Docling pipeline accepts Markdown, so you can upload the .md files instead of the raw PDFs.
5. Honest limits
Plan around these before you send your document archive through it.
Where I’d be careful
- • Every benchmark is Baidu’s, run in Baidu’s harness, and the headline long-document benchmark was built and released by Baidu with the model. Eight days after launch, I found no independent benchmark, only early hands-on posts.
- • Headings are the weak spot: Heading TEDS is 60.10 on MPDocBench and 48.20 on MarathonDocBench. The paper’s own limitations section says the sparse attention “does not preserve global document information”, which limits heading hierarchies. Don’t build a chunker that trusts heading levels.
- • Languages aren’t documented. The card only says “multilingual”, the paper doesn’t list training languages, and MarathonDocBench is Chinese and English. An early hands-on post from MindStudio (October 28, linked below, not verified by me) reports a repetition loop on Urdu text. Test your own languages first.
- • Handwriting isn’t covered: neither the card nor the paper discusses it or reports a handwriting score. The same MindStudio post says handwritten physics equations came out right, which is one sample, not a benchmark.
- • No minimum GPU is stated. The only tested GPU is an 80 GB A800, the weights alone are 6.69 GB in BF16, and MindStudio reports about 8.3 GB of VRAM after loading, without naming the GPU. The 1.77 GB KV figure comes from Unlimited-Horizon Decoding, which the paper says is integrated into SGLang; the card’s Transformers path doesn’t mention page windows, so I’d expect it to need more memory on long documents.
- • No Mac or CPU path: the card’s code calls .cuda(), the repo ships Triton kernels, and the SGLang setup uses a bash virtualenv and /dev/shm.
- • The converter was written to match benchmark ground truth. It forces a single-rooted heading tree and leaves figure placeholders like  that point to crops you’d have to make yourself.
trust_remote_code, the license and the data
Loading with trust_remote_code=True runs Baidu’s Python from the repo on your machine: 10 files, about 480 KB, including the modeling code, a custom sparse-attention module and Triton kernels. The SGLang path installs a 15.3 MB custom SGLang build on top. Both come from Baidu’s official accounts, but read them, run them in a container or VM without access to secrets, and pin what you reviewed: from_pretrained takes a revision argument, and the head commit on November 3 was 0480966938291445591f69f08a13b041c073a96a. The model and code are MIT. MarathonDocBench annotations are Apache-2.0, but its page images are for academic and research use only.
Verdict: should it go into your RAG pipeline?
If your corpus is long PDFs (manuals, filings, reports, books, scanned archives) and you have an NVIDIA GPU, Unlimited-OCR 2 is the most interesting open OCR release this month. It reads a whole document in one request, keeps tables that cross page breaks together and ships under MIT. Those cross-page failures are exactly what wrecks chunking downstream.
It isn’t a universal upgrade. On single pages it’s v1 with a new attention layer, smaller models score higher, and every number is Baidu’s. Treat the benchmarks as a reason to test, not a reason to switch: run 20 of your own documents through it, diff the Markdown against your current parser, and look hardest at tables and headings.
Who should try it
Long PDFs into RAG
Manuals, filings, reports and books of 30+ pages, where tables and paragraphs run across page breaks. This is the case v2 was built for.
Batch jobs on scanned archives
A Linux box with a workstation or data-center GPU, running the SGLang server with page windows.
Already on v1
Upgrade if you hit the ~40-page wall or saw broken cross-page tables. For one-page jobs, v1 scores about the same.
Skip it if…
You need a Mac or CPU path or handwriting, you can’t test languages beyond Chinese and English, or your documents are short single pages where smaller OCR models score higher.
Try-it checklist
- 1. Try the Hugging Face Space with two or three of your hardest PDFs, using documents you’re allowed to upload.
- 2. Create a fresh Python 3.10 environment and install the card’s pinned versions.
- 3. Run infer_multi on a 10–20 page document and open result.md and the result_with_boxes images.
- 4. For long documents, move to the SGLang server with the card’s page-window settings.
- 5. Convert with process_unlimitedOCR2.py, drop headers and footers, chunk, and compare retrieval with your current parser on the same questions.
- 6. Pin the commit you reviewed and keep the remote code in an isolated environment.
Sources for this post
- Hugging Face model card: baidu/Unlimited-OCR-2
- Unlimited-OCR-2 config.json (architecture, positions, sparse attention)
- Hugging Face API: baidu/Unlimited-OCR-2 (dates, license, parameters)
- arXiv 2610.39308: Unlimited OCR Works 2
- arXiv 2606.23050: Unlimited OCR Works (v1)
- GitHub: baidu/Unlimited-OCR
- GitHub: post-processing docs (utils/README.md)
- Hugging Face model card: baidu/Unlimited-OCR (v1)
- MarathonDocBench dataset card (leaderboard, evaluation notes, license)
- Hugging Face Space: Unlimited-OCR-2 demo
- vLLM recipe for Unlimited-OCR (v1)
- AI Weekly: Baidu open-sources Unlimited-OCR-2 (secondary)
- MindStudio: early hands-on with Unlimited-OCR 2 (secondary, Oct 28, 2026)



