Skip to content

Commit ce2d02f

Browse files
davanstrienclaude
andauthored
ocr: add unlimited-ocr-vllm.py recipe (Baidu Unlimited-OCR, 3.3B — single-image batch OCR) (#63)
* ocr: add unlimited-ocr-vllm.py recipe (Baidu Unlimited-OCR, 3.3B — single-image batch OCR) Offline-vLLM batch recipe for baidu/Unlimited-OCR (3.3B, MIT, a DeepSeek-OCR descendant; vLLM support landed upstream 2026-06-28), mirroring deepseek-ocr-vllm.py. One image per row -> layout-grounded markdown; --strip-grounding for clean text, --grounding-column to keep both clean text and the raw <|det|> bboxes. Runs on Baidu's dedicated vllm/vllm-openai:unlimited-ocr image (arch not in a stable wheel; vLLM/torch come from the image via PYTHONPATH). Validated on HF Jobs: ufo-ColPali (5/5) and encyclopaedia-britannica-1771 (6/6 content pages ~= ground-truth ocr_text length). Multi-page is single-image-only via vLLM (the integration garbles multi-image, and is unproven upstream — PR vllm-project/vllm#46564 benchmarks single-page only). Multi-page lives in the model's SGLang build: serving-unlimited-ocr.md now carries a validated working SGLang command (pinned lmsysorg/sglang:v0.5.10.post1 + a100-large + --attention-backend flashinfer; HF h200 nodes currently fail GPU init with CUDA error 802) alongside the vLLM single-image serving option. Also updates ocr/README.md (table row, serve line, modes table, dedicated-image note) and ocr/CLAUDE.md (dev notes + full investigation). Additive only; superset gate (tools/verify-superset.sh ocr uv-scripts/ocr) passes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ocr: correct unlimited-ocr multi-page framing (vLLM works on clean docs) Control test (same clean synthetic 2-page doc, vLLM server) returned both pages <PAGE>-separated with real text — so the earlier "vLLM multi-page garbles/broken" was an input-difficulty artifact (the hard Britannica/ufo scans), not an engine failure. Corrected across the recipe docstring, serving-unlimited-ocr.md, README, and CLAUDE.md: both engines read clean multi-page docs; SGLang is the *more robust* path (held up on hard/degraded scans where vLLM hallucinated). Batch recipe stays single-image; multi-page is a serving concern. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * ocr: address review on unlimited-ocr-vllm.py (column guard, upload retry, --model) - guard add_column against an existing output/grounding column (fixes the in-place multi-model append crash; parity with deepseek-ocr2-vllm.py) - push_to_hub: 3-attempt retry + HF_HUB_DISABLE_XET fallback + max_shard_size="500MB" - add --model flag (default baidu/Unlimited-OCR) - CLAUDE.md: move the (Production Ready) entry to "Other OCR Scripts"; note the deferred SGLang multi-page-batch follow-up Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 299e612 commit ce2d02f

4 files changed

Lines changed: 724 additions & 13 deletions

File tree

ocr/CLAUDE.md

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,99 @@ hf jobs uv run --flavor l4x1 \
196196

197197
## Other OCR Scripts
198198

199+
### Unlimited-OCR (`unlimited-ocr-vllm.py`)
200+
**Production Ready — single-image** (added + validated 2026-06-28)
201+
202+
Baidu's `baidu/Unlimited-OCR` (3.3B, MIT, DeepSeek-OCR / DeepSeek-OCR-2 descendant). Offline vLLM
203+
batch recipe adapted from `deepseek-ocr-vllm.py``llm.generate()` with PIL images +
204+
`NGramPerReqLogitsProcessor` (imported from `vllm.model_executor.models.unlimited_ocr`), prompt
205+
`<image>document parsing.`, `SamplingParams(temperature=0, skip_special_tokens=False,
206+
extra_args=dict(ngram_size=35, window_size=128))`, `limit_mm_per_prompt={"image": 1}`. One image per
207+
row → one markdown. `--strip-grounding` drops `<|det|>`/`<|ref|>` tags (verified locally on real
208+
output: removes boxes, keeps inner text + LaTeX).
209+
210+
**⚠️ Dedicated image, not the standard one.** The arch is NOT in any stable vLLM pip wheel — must run
211+
on Baidu's `vllm/vllm-openai:unlimited-ocr` (CUDA 13.0; `:unlimited-ocr-cu129` on Hopper). So `vllm`
212+
and `torch` are **omitted from the PEP 723 deps** and come from the image via `PYTHONPATH`. The image
213+
uses the **standard** layout: `--python /usr/bin/python3 -e PYTHONPATH=/usr/local/lib/python3.12/dist-packages`
214+
(vLLM `0.23.1rc1.dev541` lives there; probed 2026-06-28). The `unlimited_ocr` module re-exports
215+
`deepseek_ocr.NGramPerReqLogitsProcessor`. Recipe: https://recipes.vllm.ai/baidu/Unlimited-OCR
216+
217+
**Smoke tests (2026-06-28):**
218+
- **ufo-ColPali** (5, l4x1): 5/5 OK, 2.3 min, ~200 tok/s. Clean layout-grounded markdown — accurate
219+
text, `<|det|>` bboxes (0–1000), multilingual (Spanish), LaTeX. Output `davanstrien/unlimited-ocr-smoke`.
220+
- **encyclopaedia-britannica-1771** (8, l4x1, `--strip-grounding`): 6/6 content pages produced clean
221+
text matching the dataset's own `ocr_text` length almost exactly (e.g. row 1: md 5811 vs ocr_text
222+
5752), period-accurate 1771 OCR (long-ſ, archaic spelling). The 2 "empty" rows are genuinely blank
223+
pages (ground-truth `ocr_text` 3–24 chars). Output `davanstrien/unlimited-ocr-britannica-smoke`.
224+
225+
**Multi-page: BOTH engines work on clean docs; robustness differs on hard scans. (Corrected
226+
2026-06-29 — earlier "vLLM multi-page is broken" was an input-difficulty artifact.)**
227+
- **Control test that overturned the first read:** ran the SAME clean synthetic 2-page doc through the
228+
**vLLM server** that SGLang had aced. vLLM returned **`<PAGE>=2`, both pages, real text** (`Chapter
229+
One The Harbor` + lines / `Chapter Two The Market` + lines), with minor body-OCR slips ("early oakh",
230+
"Guile covered") — i.e. the model *misreading*, not the engine hallucinating. Worked with both 1×
231+
and 2× `<image>` prompt forms + `vllm_xargs.window_size=1024`. So **vLLM multi-page works**.
232+
- **What the earlier garbling actually was:** my first vLLM multi-page tests used **hard** inputs —
233+
`unlimited-ocr-pdf-test` (blank + dense 1771 Britannica) and ufo newspaper clippings. On those, vLLM
234+
multi-page degraded to hallucination (counting garbage "SIGILLUM. 17. 96…", `2017年1月1日` loops,
235+
content in neither input). SGLang read the *same* hard ufo input as real content → **SGLang is more
236+
robust on hard/degraded scans**, but neither engine is "broken."
237+
- **Offline `LLM().generate()`** still needs one `<image>` per image (single placeholder → assertion);
238+
offline multi-page was only tested on the hard Britannica PDF (garbled) — not re-tested on clean, so
239+
the recipe stays single-image (multi-page belongs to serving).
240+
- `images_config`/`image_mode` are **SGLang-only** params (vLLM ignores them); on vLLM use one
241+
`<image>` per page + `window_size=1024` in `vllm_xargs`.
242+
- **Upstream check (vllm-project/vllm#46564, "Support Unlimited OCR", merged 2026-06-28):** confirms
243+
this. Multi-image IS implemented (crop/gundam auto-disabled → base mode; one `<image>` placeholder
244+
per image). R-SWA needs the **FlexAttention** backend (auto on non-FA4 GPUs like L4) or FA4 on
245+
H20/H100 — our run correctly used FlexAttention. BUT: the PR's only benchmark is **single-page
246+
OmniDocBench** (FA4 92.12 / Flex 92.38); there is **no multi-page test, no `examples/`, no canonical
247+
multi-page prompt** in the merged code. PR-author comment: multi-page needs **V1 + NGramPerReq-
248+
LogitsProcessor** (V2 lacks custom logits processors), and their "14-page PDF merge" smoke test only
249+
confirmed "**R-SWA itself works**" (mechanism runs on long seqs) — *not* OCR quality. So nobody
250+
upstream has shown multi-page OCR quality; the tweet's "40+ pages, low edit distance" is ahead of the
251+
merged evidence. (Our own clean-doc control test later showed vLLM multi-page DOES read correctly —
252+
see the corrected block above; the earlier garbling was hard-input degradation, not an engine break.)
253+
- **Conclusion:** the **batch recipe stays single-image** (offline multi-page is finicky and untested
254+
on clean; `--pdf-column` removed). For multi-page, **serve** the model — both engines read clean
255+
multi-page docs; route hard/degraded scans to **SGLang** (more robust; authors' `images_config` path;
256+
serving-unlimited-ocr.md Option B + §3). Image probed: `vllm 0.23.1rc1.dev541` (docs say "0.25.0+").
257+
- **SGLang multi-page — ✅ FIXED + validated working (2026-06-28).** Multi-page is the model's headline
258+
feature and **SGLang delivers it robustly** (vLLM multi-page also works on clean docs but hallucinated
259+
on hard scans — see corrected block above). Two pins were needed:
260+
1. **Image `lmsysorg/sglang:v0.5.10.post1`** (not `:latest`). `:latest` drifted to sglang 0.5.14 /
261+
torch 2.11 / cu130; the wheel (`dev11416`) needs torch 2.9.1 / cuda-python 12.9 / flashinfer 0.6.7 /
262+
xgrammar 0.1.32 / transformers 5.3.0. Found v0.5.10.post1 by bisecting sglang release pyproject
263+
pins — the **last** release before the torch-2.11 bump; matches the wheel exactly.
264+
2. **`a100-large` + `--attention-backend flashinfer`** (not `h200`/`fa3`). `fa3` needs Hopper, but
265+
HF's `h200` nodes **fail GPU init with `CUDA error 802: system not yet initialized`, 3/3** (infra /
266+
Fabric-Manager — *all* working jobs this session were l4x1/a100, never h200). The version pin alone
267+
did NOT fix 802; the 802 is purely the h200 node. a100+flashinfer dodges it.
268+
- **Result:** server up; clean 2-page synthetic doc → **both pages read verbatim, `<PAGE>`-separated**
269+
(`Chapter One: The Harbor…` / `Chapter Two: The Market…`); ufo pages → **real content**
270+
(`OUT OF THIS WORLD / UFO FlyBys…`), *not* vLLM's hallucinated garbage. Client: OpenAI API,
271+
`images_config:{image_mode:base}` + `Multi page parsing.`; no per-request NGram processor (so harder
272+
scans show minor page-merge/OCR slips — fa3 + the custom logit processor would tighten quality; the
273+
mechanism works). Working command lives in `serving-unlimited-ocr.md` Option B; switch back to
274+
`fa3`/`h200` for exact R-SWA once the h200 802 infra issue clears.
275+
276+
**Example usage:**
277+
```bash
278+
hf jobs uv run --flavor l4x1 -s HF_TOKEN \
279+
--image vllm/vllm-openai:unlimited-ocr --python /usr/bin/python3 \
280+
-e PYTHONPATH=/usr/local/lib/python3.12/dist-packages \
281+
./ocr/unlimited-ocr-vllm.py davanstrien/ufo-ColPali output-dataset --max-samples 10 --shuffle
282+
```
283+
284+
**Deferred follow-up (captured, not built):** a *multi-page batch* recipe that drives the **SGLang
285+
server** in-job (server lifecycle + `ThreadPoolExecutor` over multi-page docs, like Baidu's `infer.py`,
286+
→ Hub) — the only way to get robust multi-page at corpus scale, since SGLang offline-Engine is
287+
non-viable (server-only, custom-logit-processor/R-SWA are server-side, `fa3` Hopper-only) and vLLM
288+
offline needs one `<image>` per page and degrades on hard scans. Gate: a real corpus-scale multi-page
289+
need **+** the h200/`fa3` infra fix (for exact R-SWA quality). Single-image vLLM (this recipe) stays
290+
the batch default.
291+
199292
### Nanonets OCR (`nanonets-ocr.py`, `nanonets-ocr2.py`)
200293
✅ Both versions working
201294

ocr/README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This will:
3333

3434
## Serve a model as a live endpoint
3535

36-
The recipes here run as batch jobs. To call a model interactively, from an agent, or with concurrent ad-hoc requests, you can instead run it as a temporary endpoint: [HF Jobs serving](https://huggingface.co/docs/hub/jobs-serving) exposes a port on a GPU Job, giving an OpenAI-compatible endpoint that runs until the job is cancelled or its `--timeout` is reached. See [serving-unlimited-ocr.md](serving-unlimited-ocr.md) for a worked example serving Baidu's [Unlimited-OCR](https://huggingface.co/baidu/Unlimited-OCR) with SGLang.
36+
The recipes here run as batch jobs. To call a model interactively, from an agent, or with concurrent ad-hoc requests, you can instead run it as a temporary endpoint: [HF Jobs serving](https://huggingface.co/docs/hub/jobs-serving) exposes a port on a GPU Job, giving an OpenAI-compatible endpoint that runs until the job is cancelled or its `--timeout` is reached. See [serving-unlimited-ocr.md](serving-unlimited-ocr.md) for a worked example serving Baidu's [Unlimited-OCR](https://huggingface.co/baidu/Unlimited-OCR) with vLLM (official image) or SGLang. To OCR a whole corpus of single-page images instead, the batch recipe `unlimited-ocr-vllm.py` is the better fit (it's single-image only). **Multi-page** documents need a server: both vLLM and SGLang read clean multi-page docs, but **SGLang is the more robust** — on hard/degraded scans vLLM multi-page hallucinated in our tests while SGLang held up.
3737

3838
## Models at a glance
3939

@@ -69,6 +69,7 @@ _Sorted by model size:_
6969
| `deepseek-ocr-vllm.py` | [DeepSeek-OCR](https://huggingface.co/deepseek-ai/DeepSeek-OCR) | 4B | vLLM | 5 resolution + 5 prompt modes |
7070
| `deepseek-ocr.py` | [DeepSeek-OCR](https://huggingface.co/deepseek-ai/DeepSeek-OCR) | 4B | Transformers | Same model, Transformers backend |
7171
| `deepseek-ocr2-vllm.py` | [DeepSeek-OCR-2](https://huggingface.co/deepseek-ai/DeepSeek-OCR-2) | 3B | vLLM | Newer; needs nightly vLLM **+ the `vllm/vllm-openai` image** ([why](#if-a-vllm-script-crashes-at-startup-the-nvcc--nvrtc-error)) |
72+
| `unlimited-ocr-vllm.py` | [Unlimited-OCR](https://huggingface.co/baidu/Unlimited-OCR) | 3.3B | vLLM | DeepSeek-OCR-based; layout-grounded markdown (`--strip-grounding` for clean text). Single-image batch — needs Baidu's **dedicated `vllm/vllm-openai:unlimited-ocr` image** (`-cu129` on Hopper). Multi-page "long-horizon" parsing → serve it ([doc](serving-unlimited-ocr.md)); both engines do clean docs, **SGLang more robust** on hard scans. MIT |
7273
| `nuextract3.py` | [NuExtract3](https://huggingface.co/numind/NuExtract3) | 4B | vLLM | Markdown OCR **+ schema-guided JSON extraction** (template/Pydantic). Needs `vllm/vllm-openai` image |
7374
| `qianfan-ocr.py` | [Qianfan-OCR](https://huggingface.co/baidu/Qianfan-OCR) | 4.7B | vLLM | #1 OmniDocBench v1.5 (93.12), Layout-as-Thought, 192 languages |
7475
| `olmocr2-vllm.py` | [olmOCR-2-7B](https://huggingface.co/allenai/olmOCR-2-7B-1025-FP8) | 7B | vLLM | 82.4% olmOCR-Bench |
@@ -213,10 +214,11 @@ Beyond the shared flags, some models add their own. Run `--help` on any script f
213214
| `dots-ocr.py` | `--prompt-mode ocr\|layout-all\|layout-only` |
214215
| `dots-mocr.py` | `--prompt-mode` (8: ocr, layout-all, layout-only, web-parsing, scene-spotting, grounding-ocr, svg, general); SVG: `--model rednote-hilab/dots.mocr-svg --prompt-mode svg` |
215216
| `qianfan-ocr.py` | `--prompt-mode ocr\|table\|formula\|chart\|scene\|kie`, `--think` (Layout-as-Thought); `kie` needs `--custom-prompt` |
217+
| `unlimited-ocr-vllm.py` | `--strip-grounding` (drop `<\|det\|>`/`<\|ref\|>` grounding tags); needs the **`vllm/vllm-openai:unlimited-ocr`** image |
216218
| `numarkdown-ocr.py` | `--include-thinking` (store the reasoning trace) |
217219
| `nuextract3.py` | `--template` / `--schema` / `--enable-thinking` — see the NuExtract3 section above |
218220

219-
**Image-mode models**`abot-ocr.py` and `nuextract3.py` (Qwen3.5 architecture) need the `vllm/vllm-openai` image because the default uv-script image lacks `nvcc`. Add `--image vllm/vllm-openai:latest --python /usr/bin/python3 -e PYTHONPATH=/usr/local/lib/python3.12/dist-packages` (see the NuExtract3 example above for the full command).
221+
**Image-mode models**`abot-ocr.py` and `nuextract3.py` (Qwen3.5 architecture) need the `vllm/vllm-openai` image because the default uv-script image lacks `nvcc`. Add `--image vllm/vllm-openai:latest --python /usr/bin/python3 -e PYTHONPATH=/usr/local/lib/python3.12/dist-packages` (see the NuExtract3 example above for the full command). `unlimited-ocr-vllm.py` is a special case — its architecture isn't in any stable vLLM wheel, so it needs Baidu's **dedicated** `vllm/vllm-openai:unlimited-ocr` image (tag `:unlimited-ocr-cu129` on Hopper), e.g. `--image vllm/vllm-openai:unlimited-ocr --python /usr/bin/python3 -e PYTHONPATH=/usr/local/lib/python3.12/dist-packages` (its docstring has the full command).
220222

221223
## Output & features
222224

0 commit comments

Comments
 (0)