LLMSelf-HostingAI

Unlimited OCR 2 Self-Hosted: Read a Whole PDF in One Pass (Linux, Windows, Mac)

Before you start

  • 20 min read
  • 19 to run in terminal
  • 3 file contents

Boxes marked Run in terminal are what you type; plain text explains why. Check what you need first

What you get in one minute#

Unlimited OCR 2 is a 3B-parameter OCR model from Baidu that reads every page of a multi-page PDF in one request and keeps its decoding memory nearly flat, so you can turn a long document into text and Markdown on your own GPU.

Unlimited OCR 2 official logo

Logo: Baidu Inc., from the official model card (huggingface.co/baidu/Unlimited-OCR-2)

It is for people with an NVIDIA GPU and a stack of long PDFs. The model card documents Transformers for quick tests and SGLang for serving; the code and weights are MIT-licensed. The catch: the documented paths are Linux and CUDA, and the model does not emit Markdown by itself.

ItemWhat the official sources say
Current versionUnlimited OCR 2, released October 2026 (model card release list, read October 2026)
LicenceMIT
Model size3,343,978,497 parameters (~3.34B); ~6.63 GiB of weights (Hugging Face API)
HardwareNVIDIA CUDA; the model card reports testing on an A800-80GB. No minimum VRAM is published.
Disk~6.63 GiB of weights plus the Python environment; plan ~10 GB free (estimate)
Port10000, the port in the official SGLang command
Time30-60 minutes, mostly downloads (estimate)
Official docsmodel card, repository

At a glance:

  • One request can carry all pages; the output has one <PAGE> per input page.
  • Raw output is layout blocks, not Markdown; the repository's converter turns it into Markdown. Tables stay HTML.
  • Windows and Mac have no official path. This guide says so plainly in every step.
  • The official server command binds 0.0.0.0; a security report in the project's issue tracker (#113) says keep it loopback-only.

How it fits together#

The model never reads the PDF file. You rasterize the pages to images, send them in page order in one request, and post-process the layout blocks into Markdown.

Flow diagram: a PDF is rasterized at 300 DPI, all page images go to Unlimited OCR 2 in one request, and the layout blocks are post-processed into Markdown

The model sees page images, not the PDF. The arrow out of the model carries layout blocks; process_unlimitedOCR2.py turns them into Markdown. Source: official model card and utils/README.md, October 2026.

Before you start#

Unlimited OCR 2 is not a drop-in CPU tool. Check the hardware before you download 6.6 GiB of weights.

  • Linux: 64-bit Linux, an NVIDIA GPU with a CUDA-capable driver, Python 3.10, and enough VRAM for 3.34B BF16 weights plus the KV cache and activations. Baidu's only published hardware line is "on an NVIDIA A800-80GB"; no minimum is published. Do not assume a small card works because the weights are ~6.6 GiB.
  • Windows (PowerShell): no official path. The README uses source .venv/bin/activate, export, and /dev/shm, which are POSIX. The practical route is a Linux machine with the GPU, not a Windows install.
  • Mac (Terminal): no official path. Apple Silicon has no CUDA. Issue #48 says "Currently, the model requires NVIDIA GPUs with CUDA." Community patches exist (#49, #57) but are not merged; do not treat them as official.

Required software:

Run in terminalbash
python3 --version
nvidia-smi
git --version
uv --version

Expected: Python 3.10 for the pinned environment, nvidia-smi prints your GPU and driver, and uv is present for the SGLang path. The model card pins torch==2.13.0, transformers==4.52.1, huggingface-hub==0.36.0, and tokenizers==0.21.4.

Checklist:

  • [ ] NVIDIA GPU with CUDA; no official CPU or Mac path.
  • [ ] Python 3.10.
  • [ ] ~10 GB free disk (estimate).
  • [ ] Port 10000 free if you use SGLang.
  • [ ] You accept trust_remote_code=True, which runs Python from the model repository.
  • [ ] You will keep the server on loopback (see Security and testing).

Tested in October 2026: Linux = not run by DIY Lab Hub (Baidu reports an A800-80GB); Windows = not supported officially; Mac = not supported officially.

Step 1: Install the Python environment#

The Transformers path is the official quick-test route. The model card pins exact versions; install them in a Python 3.10 environment.

Linux#

Run in terminalbash
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

Run this inside a Python 3.10 virtual environment. The command is copied from the model card.

Windows (PowerShell)#

There are no official Windows instructions. The package list above is Python, but the model code ends in .cuda() and the tested platform is Linux. Do not follow a Windows tutorial that claims otherwise. If you have a Windows machine with an NVIDIA GPU, run the model on a Linux host and call its API from Windows.

Mac (Terminal)#

There are no official Mac instructions. There is no CUDA on Apple Silicon, and the model card's load code calls .cuda(). Do not install this on a Mac expecting the official path to work.

Check it worked

Run in terminalbash
python -c "import torch, transformers; print(torch.__version__, transformers.__version__)"

Expected: the two pinned versions, 2.13.0 and 4.52.1, as in the model card. On Windows and Mac there is no official install to check.

Potential issue: if pip resolves a different torch build, you are not on the tested stack. The model card's versions are the only ones it reports.

Step 2: Load the model and read page images#

The official quick-test path loads the model once, then calls infer_multi with a list of page images in page order. All pages go in one request.

Linux#

Run in terminalpython
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()
File contentpython
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)

The model card's example uses seven pages. Replace the list with your own page images, in order. infer_multi builds the prompt "<PAGE><image>" * num_pages + "Multi page parsing." and encodes each page as one global view (base mode). The output has one <PAGE> per input page.

For a single page, the official call is:

File contentpython
text = model.infer(tokenizer, image_file="your_doc/page_1.jpg",
                   output_path="output/single", save_results=True)

Single pages use dynamic tiling (gundam mode) by default.

Windows (PowerShell)#

Not supported. The load code calls .cuda() and there is no CPU or DirectML path in the model card. A Windows machine can send HTTP requests to a Linux server, but it cannot run this step as documented.

Mac (Terminal)#

Not supported. There is no CUDA and no official MPS path. Issue #81 reports that MPS crashes mid-generation even after community patches.

Check it worked

Codepython
print(num_tokens)
print(text[:500])

Expected: a token count and the first characters of the layout-block output. The docs do not print a fixed sample, so do not expect a particular string.

Potential issue: the Transformers path is for quick tests. The model card recommends SGLang for serving.

Step 3: Serve it with SGLang#

SGLang is the recommended serving path. It needs the custom wheel that ships inside the repository, not a stock sglang install.

Linux#

Run in terminalbash
git clone https://github.com/baidu/Unlimited-OCR.git
cd Unlimited-OCR
Run in terminalbash
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.2

Then set the environment variables from the README. They control page windows and memory:

Run in terminalbash
export SGLANG_UOCR_EANABLE_DOC_LIMIT=1
export SGLANG_UOCR_DOC_GUNDAM_PAGE_WINDOW=10
export SGLANG_UOCR_DOC_BASE_PAGE_WINDOW=10
export SGLANG_UOCR_MAX_PAGE_TOKENS=20000
export SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64
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

Start the server:

Run in terminalbash
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

The README notes that --mem-fraction-static 0.75 leaves too little headroom at 32+ concurrent requests and that 0.7 is stable. A smaller page window lowers peak memory but hurts cross-page merging.

Before you start it, read the security section below: the official command binds 0.0.0.0 and enables a custom logit processor. On a single-user machine, use --host 127.0.0.1 instead.

Windows (PowerShell)#

Not supported. The setup uses source .venv/bin/activate, export, and /dev/shm. There is no official PowerShell version, and WSL2 is not documented by Baidu. A community PR (#49) tested a CPU-only WSL2 path for v1, not v2.

Mac (Terminal)#

Not supported. SGLang is CUDA, and the README's shared-memory option uses /dev/shm. Do not follow this step on a Mac.

Check it worked

Run in terminalbash
ss -ltnp | grep 10000
curl -i http://127.0.0.1:10000/health

Expected: a process listening on port 10000 and an HTTP 200 from /health. Issue #105 shows that infer.py treats HTTP 200 from /health as ready. If you changed the host to 127.0.0.1, the socket should show the loopback address, not 0.0.0.0.

Step 4: Turn a PDF into Markdown#

The official SGLang client rasterizes a PDF with PyMuPDF at 300 DPI, sends the page images in one request, and streams the raw output. The converter then turns the layout blocks into Markdown.

Linux#

Run in terminalpython
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)


# PDF
raw_output = generate(pdf_to_images("your_doc.pdf", dpi=300))

Then convert the raw string:

Run in terminalpython
from process_unlimitedOCR2 import convert
markdown = convert(raw_output)

process_unlimitedOCR2.py uses only the standard library. It splits on <PAGE>, turns title<LEVELn> into headings, keeps tables as HTML, and turns images into ![](page<N>_x1_y1_x2_y2.jpg) references. The repository's own single-document demo uses the CLI:

Run in terminalbash
python infer.py --doc demo --raw-dir raw_demo
python utils/process_unlimitedOCR2.py --infer-dir raw_demo --out-dir pred_demo

The --infer-dir folder must contain .txt or .md files. Important: the model does not emit Markdown. The converter is a separate step, and tables stay HTML by design.

Windows (PowerShell)#

Not supported as a documented path. The client code is Python, but it imports DeepseekOCRNoRepeatNGramLogitProcessor from the custom SGLang wheel, and Baidu documents the wheel and server on Linux only. If the server runs on a Linux host, you can reach its HTTP API from Windows, but you still need a client that can send the custom logit processor. There is no official Windows client.

Mac (Terminal)#

Not supported. The same custom wheel and CUDA server are not documented for macOS. The converter itself uses only the standard library and can run anywhere, but it needs raw output from a server you can run.

Check it worked

Run in terminalbash
ls -lh pred_demo

Expected: one .md file per input document. Open it and check that the number of page sections matches your PDF; the converter keeps blank pages so page numbers do not shift.

Put it behind HTTPS with Traefik#

Skip this section if you run the model on your own machine. There is no official Docker image (issue #27 asks for one), so the Traefik labels from our Docker guides do not apply. A security report in the project's issue tracker (#113) says to keep the server loopback-only. Publishing port 10000 on a public host is a bad idea, even behind TLS.

With Traefik#

Traefik can proxy a non-Docker backend, but Baidu documents no Traefik configuration. We will not invent labels for it. If you already run Traefik, point a router at http://127.0.0.1:10000 with TLS and authentication; that is your configuration, not an official one. For most readers the honest answer is: do not expose this server.

Without Traefik: Caddy#

If you need a local HTTPS name on a trusted network, Caddy can reverse-proxy the loopback server. This is not an official configuration either, and it does not fix the custom logit processor risk.

File contenttext
ocr.example.com {
    tls internal
    reverse_proxy 127.0.0.1:10000
}

Run Caddy on the same machine, with the Caddyfile in the current folder:

Run in terminalbash
caddy run

tls internal uses Caddy's local certificate authority; trust its root certificate on the devices that need it. Check with:

Run in terminalbash
curl -I https://ocr.example.com

Expected: an HTTP response over TLS. If you only use the machine locally, skip Caddy and keep the server on 127.0.0.1.

Security and testing#

Three things matter more than the benchmark: the server's bind address, the custom logit processor, and trust_remote_code.

  • Bind to loopback. The README's command uses --host 0.0.0.0. Issue #113 reports that the custom logit processor path reaches dill.loads() and that the README enables it on 0.0.0.0 without an API key. The issue's advice: "Keep this loopback-only with trusted callers." Use --host 127.0.0.1 on a single-user machine, and an SSH tunnel or VPN for remote access. An API key alone does not make the processor safe for untrusted clients.
  • trust_remote_code=True. The official load code passes it twice. config.json maps AutoConfig and AutoModel to modeling_unlimitedocr.*, so the flag downloads and runs the repository's Python model code. Read that code or pin the revision if that matters to you. The first load downloads ~6.63 GiB of weights.
  • Network. Once the weights and packages are local, the client posts to http://127.0.0.1:10000. The README client sets session.trust_env = False, so proxy variables do not route loopback traffic. No telemetry is documented. We did not audit every line of the custom model code.

Checks:

Run in terminalbash
ss -ltnp | grep 10000
curl -i http://127.0.0.1:10000/health
nvidia-smi

Expected: the socket shows 127.0.0.1:10000 if you changed the host, /health returns HTTP 200 (the check infer.py uses, per issue #105), and nvidia-smi shows the GPU memory in use.

What the numbers mean#

The v2 model card claims state-of-the-art results on MPDocBench and MarathonDocBench, but the numbers are in assets/leaderboard.png, an image. We do not turn an image into numbers. The text table we can quote is from the v1 paper (June 2026), and we label it as v1.

ModelSizeOmniDocBench v1.5 overall (higher is better)
Unlimited-OCR (v1 paper)3B-A0.5B93.23
DeepSeek-OCR 23B-A0.5B89.17
dots.ocr3B88.41
Gemini-2.5 Pro-88.03
Qwen2.5-VL72B87.02
DeepSeek-OCR3B-A0.5B87.01
MinerU2-VLM0.9B85.56

On v1.6, Unlimited-OCR scores 93.92, ahead of Qianfan-OCR 4B at 93.90 and Logics-Parsing-v2 4B at 93.33. These are the paper's own benchmark, not a DIY Lab Hub test, and not v2 numbers.

Long-horizon results from the same paper:

PagesDistinct-35 (higher is better)Edit distance (lower is better)
299.87%0.0362
599.98%0.0452
1099.83%0.0526
1599.99%0.0787
2099.89%0.0572
40+96.90%0.1069

At 40+ pages the edit distance is 0.1069 and Distinct-35 is 96.90%. The paper says most repeated errors come from small text in Base mode, not from the attention mechanism losing direction.

On speed, the paper reports 5580 TPS at 512 concurrency vs 4951 for DeepSeek OCR (+12.7%), and at 6,000 output tokens DeepSeek OCR lags by 35% in Table 4. These are v1 paper numbers.

Read the tables as the authors' evidence, not as a promise for your PDFs. The paper's limitation section says the model cannot achieve truly unlimited parsing under a finite 32K context; prefill length grows with pages.

Compared with Docling, PaddleOCR and Tesseract#

This is the part where a lot of AI-written pages invent a benchmark. The official sources do not give one.

  • Docling: not mentioned in the model card, the repository README, or the paper's Table 1. No official number compares Unlimited OCR 2 with Docling.
  • Tesseract: same: no row, no number, no mention in the sources we read.
  • PaddleOCR: the paper uses PaddleOCR to annotate its single-page training data (section 4.1) and thanks it in the acknowledgement. It is not a row in Table 1. The table's caption says all models are end-to-end VLM-based architectures, so a pipeline OCR tool is outside that comparison.

Here is what the official table does compare: other end-to-end VLM OCR models. Unlimited-OCR v1 scores 93.23 on OmniDocBench v1.5 vs DeepSeek-OCR 87.01, dots.ocr 3B 88.41, Gemini-2.5 Pro 88.03, and Qwen2.5-VL 72B 87.02. That is a real, sourced comparison, but it is not a comparison with Docling, PaddleOCR or Tesseract.

The honest summary: if you need CPU-only OCR, a pipeline with layout detection, or a tool that runs on Windows or Mac, the official numbers do not tell you that Unlimited OCR 2 is better. They only tell you it is strong among end-to-end VLM OCR models on the authors' benchmark, on an A800-80GB, in v1.

Common mistakes#

MistakeWhy it happensDo this instead
pip install sglang instead of the repo wheelThe official instructions use the custom wheel from the repository; issue #27 calls a plain install a silent failure pointInstall wheel/sglang-0.5.20.dev13+g75b9b06b02-py3-none-any.whl from the repository
Expecting Windows or Mac supportThe README is POSIX and the model code calls .cuda(); #48 says NVIDIA CUDA is requiredRun it on Linux with an NVIDIA GPU; use Windows or Mac only as a client
Binding 0.0.0.0 without authThe README's command uses --host 0.0.0.0 and enables the custom logit processor; #113 reports a dill.loads() riskUse --host 127.0.0.1 and an SSH tunnel or VPN
Expecting Markdown from the modelThe model outputs layout blocks; utils/README.md says soRun process_unlimitedOCR2.py or convert() after inference
Omitting trust_remote_code=TrueThe model uses custom code; config.json has an auto_mapPass the flag to both from_pretrained calls, and understand it runs repo code
Passing a PDF path to the modelThe model takes page images; the official client rasterizes with PyMuPDF at 300 DPICall pdf_to_images("your_doc.pdf", dpi=300) first
AI assistants inventing a Docker image or a vLLM command for v2vLLM support was announced for v1; v2 documents Transformers and SGLang; #27 asks for a Docker imageUse the official wheel and server command; do not run an image you cannot trace to the repository
Expecting the n-gram guard to catch every loop#55 reports loops with no_repeat_ngram_size=35; 0.24% gundam and 1.2% base pages looped in v1Check the output; the guard reduces but does not remove loops

Tip: Start with the Transformers path on one page. It is the official quick-test route and avoids the custom SGLang wheel.

Tip: Rasterize at 300 DPI, as the official client does. The model reads images, not the PDF's text layer.

Tip: Count the <PAGE> markers before you convert. Issue #107 reports missing separators in v1 multi-page output.

Tip: Keep port 10000 on loopback. Issue #113 says the custom logit processor is unsafe for untrusted clients.

Tip: For 40+ pages, watch prefill memory. The paper's limitation says the 32K context is finite, and a smaller page window hurts cross-page merging.

Troubleshooting#

ProblemSolution
ImportError: [sgl_kernel] CRITICAL: Could not load any common_ops library!Issue #32: SGLang failed on a DGX Spark GB10 (v1). Use the Transformers path, or a supported GPU.
ValueError: Either --image_dir or --pdf is requiredIssue #104: infer.py validated input after launching the server in the reported version. Pass exactly one input; expect the wait.
SGLang output is bbox coordinate loops on GB10/SM121Issue #114: v2 bug in the eager-mode path; the maintainer says a fix is coming. Use Transformers (clean on the same page, ~22 s) or the full CUDA Graph workaround in the issue.
Output repeats text on some pagesIssue #55: the n-gram guard misses short repeats. Inspect the raw output; the reported looping pages took 100-738 s.
Loopback calls fail when a proxy is setIssue #105: infer.py honors proxy env vars. The README client sets session.trust_env = False; use that client.
<PAGE> separators missingIssue #107: v1 report, both vLLM and Transformers, more often with vLLM. Check the count before converting.
Mac MPS output is emptyIssues #18 and #81: MPS masked_scatter_ bug. There is no official Mac path; do not use MPS.

Known bugs#

  • #114 (v2, open, October 2026): SGLang on GB10/SM121 degenerates into bbox loops; Transformers is clean. The maintainer says the eager-mode implementation will be fixed.
  • #113 (open, October 2026): security report; custom logit processors reach dill.loads(); the README enables them on 0.0.0.0 without an API key. Keep loopback-only.
  • #107 (open, v1): inconsistent <PAGE> separators in multi-page inference.
  • #55 (open, v1): repetition loops on some pages.
  • #27 (open, v1): no Docker image; the custom wheel is a silent failure point.

Diagnostic commands:

Run in terminalbash
nvidia-smi
ss -ltnp | grep 10000
curl -i http://127.0.0.1:10000/health

There is no docker logs here because there is no official Docker image.

Updates and backups#

Pin what you install. The repository has no GitHub releases, and the Hugging Face model revision changes: the API showed lastModified in late October 2026 and sha c14071b549f4fcab10909f3c4acc6a2edb366947 when we read it. Write that sha in your notes.

Update: re-read the model card, then re-install the pinned versions in a fresh environment. Do not run pip install -U across a working setup; the model card's versions are the tested ones.

Back up your outputs and scripts, not the weights. The weights are ~6.63 GiB and can be downloaded again.

Linux and Mac:

Run in terminalbash
mkdir -p backups
tar -czf "backups/unlimited-ocr-$(date +%F).tar.gz" output your_client.py

Windows (PowerShell):

Codepowershell
New-Item -ItemType Directory -Force backups | Out-Null
Compress-Archive -Path output, your_client.py -DestinationPath "backups\unlimited-ocr-$(Get-Date -Format yyyy-MM-dd).zip"

Schedule: Linux and Mac cron at 03:00; Windows Task Scheduler at 03:00. Restore test: extract the archive to a temporary folder and run process_unlimitedOCR2.py on one raw file.

Performance and resources#

Official: 3.34B parameters, ~6.63 GiB of weights, tested on an A800-80GB. No minimum VRAM is published. The README's memory notes: --mem-fraction-static 0.7 is stable at 32+ concurrent requests; 0.75 leaves too little headroom; --max-prefill-tokens drives a temporary tensor of roughly [tokens, context/32]; a smaller page window lowers peak memory but hurts cross-page merging.

Community v1 numbers, not v2 requirements: issue #32 reports a ~6.3 GB VRAM load; #99 reports 5.441 GiB resident VRAM for one page with an Intel Arc/Vulkan GGUF recipe. These are v1 and community results, not a hardware spec.

CPU-only: no official path. Issue #81 reports ~550 s for one dense page on an M1 Pro 16GB after patching ~17 .cuda() calls; #49 reports 42-68 s per page on Mac CPU/MPS. Do not plan a CPU deployment from these numbers.

Monitor:

Run in terminalbash
nvidia-smi -l 2

On Windows, nvidia-smi works the same way. There is no docker stats because there is no official Docker image.

FAQ#

Does it really read a whole PDF in one request? infer_multi takes a list of page images and builds one prompt; the output has one <PAGE> per input page. The paper says dozens of pages under a 32K context. The limitation section says it is not truly unlimited: prefill grows with pages, and the authors plan 128K context.

Can I run it on Windows or a Mac? No official path. The README is POSIX and the model code calls .cuda(). Issue #48 says NVIDIA CUDA is required. Community Mac patches exist but are unmerged. Use a Linux NVIDIA host.

Do I need an 80 GB GPU? The only official hardware line is "on an NVIDIA A800-80GB". No minimum VRAM is published. The weights are ~6.63 GiB, but the KV cache and activations add more. Test on your hardware; do not assume a small card works.

Does it output Markdown? Not directly. It outputs layout blocks. process_unlimitedOCR2.py converts them to Markdown; tables stay HTML, and images become ![](page<N>_x1_y1_x2_y2.jpg) references.

Does it call the internet? The first load downloads the weights (~6.63 GiB) and the packages. trust_remote_code=True downloads and runs the repository's Python model code. After that, the client posts to 127.0.0.1:10000. No telemetry is documented in the model card or README.

Sources#

Checked against the official docs on 2026-10-30.