Home / Blog / AI Models

Unlimited-OCR, hands-on: long-horizon document parsing on a single RTX 4090

Unlimited-OCR is Baidu's long-horizon document-parsing model — a VLM built on DeepSeek-OCR that turns a page image into structured output: layout boxes, reading order, and HTML tables. I served it with SGLang on one 24 GB 4090 and ran it on both clean English papers and 39 scanned 1890s Arabic pages. The split is the story: near-perfect on modern English — including exact table extraction — and much harder on historical Arabic.

A scanned Arabic page on the left, an arrow, and the model's structured layout-tagged output on the right
One page image in, structured layout-tagged Markdown out — header, body text, table and page-number regions detected

TL;DR

Unlimited-OCR is a layout-and-structure engine that also reads text well when the text is clean. On the model's own academic paper — crisp modern English — it was excellent: near-perfect prose, correct section/heading detection, and two data tables reconstructed to HTML with every digit matching the source. That's the case to reach for it in production.

Point it at hard historical Arabic (1890s Naskh, tashkeel, faded paper) and the same model still nails layout and reading order, but character-level text accuracy drops to roughly 60–80%, numbers become unreliable, 10/39 pages show stray CJK glyphs, and two pages fall into a degenerate generation loop that ran to ~31,000 tokens. The loop has a one-flag fix: image_mode=base (one page dropped 31,237 → 2,845 tokens).

Everything below was measured on a single RTX 4090, served with SGLang at ~845 tokens/s with eight concurrent requests — install steps, full-page → Markdown comparisons for both English and Arabic, live table extraction, and the failure mode (and fix) in detail.

What Unlimited-OCR actually is

Most OCR you've used returns a flat wall of text. Unlimited-OCR is part of a newer family — seeded by DeepSeek-OCR — that treats a page as a document, not a string. Its output is interleaved layout tags and content: each region comes wrapped in a <|det|>TYPE [x1,y1,x2,y2]<|/det|> marker giving the region type (header, title, text, table, page_number, footer, image) and its bounding box, followed by the recognized text — or, for tables, an HTML <table> grid.

Conceptual pipeline: a stack of document pages flows through a glowing vision encoder into a structured document of outlined regions and a table grid
The mental model: a page image is encoded by a vision backbone, then a decoder emits region tags + text — structured output, not a flat string

Those tags are the model's raw output. You rarely want to keep them — a consumer strips the coordinates and region labels and renders a clean document. Throughout this article I show the rendered Markdown, not the tag soup. The transform is mechanical:

# raw model output
<|det|>title [114,377,339,396]<|/det|>6. Efficiency Analysis
<|det|>text [113,408,884,522]<|/det|>As presented in Table 4, we compare …
<|det|>table [149,286,851,354]<|/det|><table>…</table>

# → rendered Markdown
## 6. Efficiency Analysis
As presented in Table 4, we compare …
| Model\TPS | 256 | 512 | … |

title## heading, text → paragraph, table → a rendered table, and the running header / page-number tags are dropped. (A ~30-line det_to_markdown.py does this; the same converter feeds the demo at the end.)

"Long-horizon" is the pitch: the model is tuned to parse dense, multi-region pages in one shot inside a 32k-token context, rather than being fed pre-cropped lines. Two knobs dominate behaviour. image_mode is either gundam (tiles the page into 640 px crops — the single-image default) or base (one 1024 px view, no crop). And a custom no-repeat-ngram logit processor (ngram_size=35) is wired into the sampler to fight the repetition that long autoregressive OCR is prone to — hold that thought, it matters later.

The repo is thin on purpose. What ships is a pinned SGLang wheel (carrying the custom logit processor), a single infer.py batch driver, and a README. The ~6 GB of weights live on Hugging Face at baidu/Unlimited-OCR and download on first server start.

Install & run it on a single 4090

The serving path is an SGLang server exposing an OpenAI-compatible streaming endpoint; infer.py (or my little demo) just POSTs images to /v1/chat/completions. Setup on the 4090 (Pop!_OS, CUDA 12.9) was a clean uv virtualenv — the pinned wheel brings its own CUDA torch, so there's no separate torch index to wrangle:

# 1. venv (asdf python 3.12) + model cache off the system drive
uv venv .venv --python "$(asdf which python)" && source .venv/bin/activate
export HF_HOME=/d/hugging_face_cache

# 2. install the pinned SGLang wheel (pulls torch 2.9.1+cu128) + helpers
uv pip install wheel/sglang-0.0.0.dev11416+g92e8bb79e-py3-none-any.whl
uv pip install kernels==0.11.7 pymupdf==1.27.2.2 requests

# 3. verify the GPU
python -c "import torch; print(torch.__version__, torch.cuda.is_available(), torch.cuda.get_device_name(0))"
# -> 2.9.1+cu128 True NVIDIA GeForce RTX 4090

Then launch the server with the model's expected flags (these are load-bearing, not decorative):

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
# wait for: "The server is fired up and ready to roll!"

And run inference over a folder of images or a PDF (each PDF page is rasterized at 300 DPI and sent as one request):

# a directory of page images
python infer.py --image_dir ./arabic --output_dir ./out \
    --concurrency 8 --image_mode gundam

# or a PDF (e.g. the paper itself)
python infer.py --pdf ./Unlimited-OCR.pdf --output_dir ./paper_out \
    --concurrency 6 --image_mode base
A pleasant surprise: --attention-backend fa3 (FlashAttention-3) is usually flagged as Hopper-oriented, but it initialized and served fine on the Ada 4090 — weights loaded, CUDA graphs captured, requests flowed. The server holds ~20 GB of VRAM (model + ~11.8 GB KV cache). --enable-custom-logit-processor is what activates the anti-repetition sampler — don't drop it.

Clean English: a full page next to its Markdown

Start with the easy, real-world case: crisp modern English. The most honest test document was the model's own 14-page paper — so here is page 11, a page that mixes two data tables with three prose sections, beside the rendered Markdown Unlimited-OCR produced from it (det tags stripped). The OCR ran in base mode in ~5 s.

The source English page 11 with two data tables and three prose sections
SOURCE — page 11 (image in)

rendered Markdown (out)

Table 3 | Performance of long-horizon OCR. We test the distinct-n and edit distance under different page numbers.

Metric\Pages2510152040+
Distinct-20 ↑99.76%99.78%97.49%99.92%98.73%96.08%
Distinct-35 ↑99.87%99.98%99.83%99.99%99.89%96.90%
Edit Distance ↓0.03620.04520.05260.07870.05720.1069

6. Efficiency Analysis

As presented in Table 4, we compare the output tokens per second (TPS) of Unlimited OCR and DeepSeek OCR under ideal concurrency conditions … at 6,000 tokens, it lags behind Unlimited OCR—which incorporates R-SWA—by 35%.

7. Limitation and Future Work

Our model cannot achieve truly unlimited parsing under a finite context length (e.g., 32K), as it is also constrained by the prefill length …

8. Conclusion

In this technical report, we propose the Unlimited OCR model and present the R-SWA algorithm to support its capability for long-horizon parsing …

— page 11 —

This is the result that recalibrates everything else in the article: on clean print the text recognition is essentially flawless. The prose comes through verbatim (em-dashes, R-SWA, (e.g., 32K) and all), the three numbered headings become ## Markdown headings, and the two tables render with the numbers intact. Whatever struggles we see later on Arabic are about the input, not a weak model.

Table extraction, verbatim

Tables are where structured OCR earns its name. The model emits each table as an HTML <table>, which renders straight into Markdown. Below are the actual reconstructed tables from page 11 — shown as rendered Markdown, checked cell-by-cell against the source image:

Table 3 — Performance of long-horizon OCR (rendered from the model's output)
Metric\Pages2510152040+
Distinct-20 ↑99.76%99.78%97.49%99.92%98.73%96.08%
Distinct-35 ↑99.87%99.98%99.83%99.99%99.89%96.90%
Edit Distance ↓0.03620.04520.05260.07870.05720.1069
Table 4 — Theoretical TPS ceiling (rendered from the model's output)
Model\TPS25651210242048307240966144
Deepseek OCR7229.327468.277422.507166.856790.726430.215822.87
Unlimited OCR7229.527714.787840.947881.117881.937905.187847.71

No cell drift, no transposed digits, header row preserved. For digitizing reports, statements or papers, this is genuinely production-grade — and it stands in sharp contrast to the Arabic tables in the next sections, where the grid survives but the numeric cells do not.

The dividing line

Across the whole paper (14 pages), three pages contained tables and all three were recovered cleanly, with no runaways and no hallucinations. The model's table machinery is solid; what breaks it is ambiguous input — faded scans and unfamiliar scripts — not table structure per se.

Hard mode: 39 pages of 1890s Arabic

Now the stress test. These 39 images are screenshots of a scanned, late-19th-century Arabic history book (visible page numbers ١٨٠–٢١٨). About as unfriendly as OCR input gets: Naskh metal type on yellowed paper, partial tashkeel (vowel diacritics), tight leading, footnote catchwords, and dense statistical tables of Arabic-Indic numerals.

A scanned page of dense classical Arabic prose with a dashed list at the bottom
Page ١٨٠ — dense classical prose + a dashed list
A scanned page containing a multi-column Arabic financial table
Page ٢١٨ — a multi-column financial table with Arabic-Indic numerals

I ran all 39 through infer.py in gundam mode at concurrency 8, then re-ran the problem pages in base. There's no ground-truth transcription, so the Arabic text-accuracy figures are a reading-based estimate by an Arabic reader, not a measured CER/WER — the structural and token metrics, by contrast, are exact.

Terminal output of the 39-page batch run showing per-page token counts and throughput
The full 39-page batch: 133,960 tokens, 158 s wall, 845 tokens/s — note two outliers at ~31k tokens

What survives on Arabic: layout & reading order

The good news is that the headline feature is script-robust. Across the 39 pages the model emitted 950 layout regions (~24 per page) and segmented the furniture cleanly: 33 headers, 12 page-numbers, 13 footers and 889 body blocks, plus table regions. Reading order came out right-to-left, top-to-bottom, with the running header and bottom catchword correctly separated. Here is page ١٨٠ rendered to Markdown (det tags stripped, RTL preserved):

rendered Markdown — page ١٨٠ (verbatim, errors and all)

الکرناطيق فاناحربمع الفرنساو يمتصرفياج بنا ابنداه سنة ١٦٦١ بعدالملادحى

ان الالكتيز تغلوا على فونشرى وهمدوا أسوارالمدينه وسوها بالدرض واتقطعت مشروعات الفرنساو …

وفأنتاء ما كانت الفقرمتؤيا الجيوش الالكيزة في كافة جهاات … جويح الثالث … في ٢٥ اكتوبر ١٧٦٠ …

— ١٨٠ —

Read it as an Arabic speaker and the structure and gist are right — the running header, the body flow, the date ١٧٦٠ — but words like فاناحربمع (should be فإن الحرب مع) show the character-level errors this historical type produces. That's the honest split: layout right, fine text shaky.

Why this matters

If your goal is to digitize structure — make a scanned archive searchable, split pages into regions, find every table — Unlimited-OCR earns its keep even on bad scans where individual characters are wrong. The layout layer degraded gracefully; the text layer did not.

Where Arabic text recognition struggles

Flip to the content of those boxes and it's more sober. On the cleaner prose lines the subject matter survives — George III, the French Revolution, Paris, the cotton trade — and Arabic-transliterated proper nouns are often recovered. But at the character level this historical typeface with its diacritics yields frequent letter/dot errors, broken ligatures and noisy tashkeel. Honest estimate: ~60–80% word accuracy on the good pages, lower on faded text — fine for search and gist, not for republication without a human pass.

Numbers are the weak point

Long Arabic-Indic numerals are the worst category. In the table cells they collapse into runs of repeated digits — the same numeric ambiguity that, on the English paper, the model handled perfectly. Here it's the seed of the runaway in the next section. Treat any digit in the Arabic output as unverified.

CJK hallucinations

On 10 of 39 pages the output contained stray Chinese/Japanese glyphs — 年 月 日, 薄荷 ("mint"), 侧面, . The tell of a CJK-heavy base model meeting an ambiguous Arabic shape and decoding a visually-near token from the wrong script. No inference-time cure short of fine-tuning; a one-line CJK strip is a cheap post-filter for an Arabic-only corpus.

A scorecard of the Arabic OCR run: token counts, layout regions, table and CJK statistics
The measured scorecard across all 39 Arabic pages
MetricClean English (paper, 14 pp)Historical Arabic (39 pp)
Text accuracy (reading estimate)near-perfect~60–80% (worse on faded)
Tables recovered cleanly3 / 3structure yes, cells no
Runaway loops02 / 39
CJK hallucinations010 / 39
Tokens / page~958 avgmedian 1,865 (max 31,237)

The failure that matters: a degenerate generation loop

Two of the 39 Arabic pages took ~129 seconds and produced ~31,000 tokens each — vs the ~13 s and ~1,800 tokens typical — almost touching the 32k context ceiling. They weren't slow because they were dense. They were stuck in a loop.

On page ٢١٠ — a pure prose page, not even a table — one detected region degenerated into an auto-incrementing LaTeX superscript counter: \( ^{1} \) \( ^{2} \) \( ^{3} \) … all the way to \( ^{351} \), a single line of 67,167 characters. The other runaway fell into an Arabic-Indic digit loop, ٢٥٢٥٢٥….

Why the anti-repetition guard doesn't save you. The no-repeat-ngram processor blocks repeated n-grams — but each ^{1}, ^{2}, ^{3} chunk is unique because the counter increments. No n-gram ever repeats, so the guard never fires. It's a counting loop, not a verbatim-repetition loop, and it walks straight to the context limit.
Diagram of the superscript-counter runaway and the image_mode=base fix
The runaway, and the fix: switching to image_mode=base
PagegundambaseOutcome
٢١٠ (prose)31,237 tok · 129 s2,845 tok · 10 sloop eliminated
digit-loop page31,074 tok · 128 s7,950 tok · 25 smuch reduced
٢١٨ (table)2,906 tok984 tokmore contained

gundam vs base: which to use

The fix above is mechanical. gundam tiles the page into 640 px crops; base sees the whole page at 1024 px. One of gundam's tiles produced the tiny ambiguous sub-region that seeded the loop — base never creates it.

A whole page on the left versus the same page split into a grid of small tiles on the right
base reads the whole page as one view; gundam tiles it into 640 px crops for fine detail — at the cost of occasionally seeding a runaway

Practical guidance from this run: for clean modern documents either mode is fine and base is fast and safe; for messy historical batches, prefer base and add a guard regardless — cap max_tokens per request, or post-filter any page whose longest output line exceeds a few thousand characters and re-run it. The trade-off is that base can lose fine detail on very dense small text, but a complete, sane page beats a perfect-then-runaway one.

Long-horizon, in one image

The model's headline trick is reading a long sequence of pages while keeping a moving memory window (the paper calls it R-SWA). It's why a 14-page PDF parses in one pipeline — and also why a finite 32k context is the hard ceiling a runaway slams into.

A long ribbon of document pages flowing through a translucent sliding window
Long-horizon parsing: many pages streamed through a moving attention window

A tiny demo to see it live

The repo ships no UI, so I wrote a ~90-line Gradio front-end (demo_app.py) that talks to the same SGLang endpoint and pipes the result through the same det_to_markdown.py converter: upload a page, pick gundam/base, and watch the rendered Markdown stream in — headings, paragraphs and tables, not raw tags.

The Gradio demo showing the English paper page 11 on the left and its parsed output rendered as Markdown — with both tables reconstructed — on the right
The demo parsing English page 11 — source on the left, output rendered as Markdown (both tables reconstructed) on the right

Verdict

Unlimited-OCR is a strong structured-OCR model with a clear comfort zone. On clean, modern documents it's genuinely production-grade: near-perfect text, correct section structure, and exact table extraction — at ~845 tokens/s on a single 24 GB card. If you're digitizing reports, papers or statements, reach for it.

On hard historical material — and especially non-Latin scripts like 1890s Arabic — recalibrate. You still get excellent layout (segmentation, reading order, table scaffolds, searchable gist), but budget a human correction pass, scrub the CJK glyphs and numbers, run in image_mode=base with a token cap to dodge the runaway. The model is strong; the skill is knowing which half of it to lean on for your documents.

Reproduce it: install the SGLang wheel, launch the server, then python infer.py --image_dir ./your_docs --output_dir ./out --concurrency 8 --image_mode base. Full onboarding notes (install, config, issues, the complete scorecard) live alongside the repo.

References