Baidu lanzó Unlimited-OCR 2 el 27 de octubre de 2026: un parser de documentos de 3B de parámetros, con licencia MIT, que lee un PDF entero, desde unas pocas páginas hasta un libro completo, en un solo request. La model card dice que tiene los mejores puntajes en dos benchmarks de documentos largos, y el truco es una capa de atención que mantiene plano el costo de cada paso a medida que el documento crece. Si cargás PDFs y escaneos en un pipeline RAG, el parsing suele ser lo primero que se rompe, así que vale la pena mirarlo de cerca.
Escribí esto el 3 de noviembre de 2026. Revisé cada spec y cada comando contra la model card y todo su historial de revisiones, el config.json, la API de Hugging Face, el repo de GitHub y el paper de arXiv, y leí el script de post-procesamiento que trae el repo. No corrí el modelo, así que acá no hay afirmaciones propias de velocidad ni de precisión: todos los benchmarks de abajo son de Baidu.
1. La ficha técnica
Unlimited-OCR 2 mantiene el cuerpo de v1: un encoder de visión al estilo DeepSeek-OCR que alimenta un decoder chico de mixture-of-experts. Lo que cambia es cómo el decoder vuelve a mirar las páginas. Acá lo tenés al lado de v1, según las cards, la API de HF y el config.json.
Unlimited-OCR 2 vs Unlimited-OCR (revisado el 3 de noviembre de 2026)
Los parámetros salen de la metadata de safetensors en la API de HF. El paper redondea ambos a “3B-A0.5B”: 3B en total, con unos 0.5B activos por token.
| Spec | Unlimited-OCR 2 | Unlimited-OCR (v1) |
|---|---|---|
| Lanzamiento | 27 de octubre de 2026 (repo de HF creado el 26 de octubre) | 22 de junio de 2026 |
| Parámetros | 3.34B, BF16 | 3.34B, BF16 |
| Decoder | MoE de 12 capas: 64 expertos ruteados (6 activos por token) + 2 compartidos | Igual |
| Encoder de visión | DeepEncoder (SAM ViT-B + CLIP-L), congelado durante el entrenamiento de v2 | Igual |
| Atención del decoder | SR-SWA: un presupuesto fijo de tokens de referencia | R-SWA: una ventana deslizante más todos los tokens visuales |
| Rango de posiciones | 131.072 (YaRN ×4 sobre 32.768) | 32.768 |
| Páginas por request | Evaluado hasta 300; el setup de SGLang de la card usa ventanas de 10 páginas | Unas 40, según el paper de v2 |
| Entrada | Imágenes de página (JPG, PNG); PDFs renderizados a 300 DPI con PyMuPDF | Igual |
| Salida | Bloques de layout con cajas, tablas en HTML y un marcador <PAGE> por página | Bloques de layout con tags <|det|> |
| Serving en la card | Transformers, SGLang (wheel propio) | Transformers, vLLM, SGLang |
| Licencia | MIT | MIT |
| Probado en | NVIDIA A800-80GB, Python 3.10 | Python 3.12.3, CUDA 12.9 |
Datos del lanzamiento
- • Hugging Face: baidu/Unlimited-OCR-2. El repo se creó el 26 de octubre y la card cambió por última vez el 2 de noviembre. El 3 de noviembre la API mostraba 189.897 descargas y 576 likes.
- • Código: github.com/baidu/Unlimited-OCR, el mismo repo que v1 (unas 30.400 estrellas, MIT). La card enlaza el código de v1 en la rama UnlimitedOCR1.0.
- • Paper: “Unlimited OCR Works 2”, arXiv 2610.39308, enviado el 29 de octubre por 14 autores (la cita de la card se lo atribuye al equipo ERNIE de Baidu). El paper de v1 es arXiv 2606.23050.
- • También está en ModelScope (PaddlePaddle/Unlimited-OCR-2), Baidu Cloud y una demo en un Space de Hugging Face. El README de GitHub suma soporte de fine-tuning en ms-swift desde el 28 de octubre.
- • Linaje: v1 tomó DeepSeek-OCR como base, y las dos cards le agradecen a DeepSeek-OCR, DeepSeek-OCR-2 y PaddleOCR. v2 arranca del checkpoint de v1 y tuvo un post-entrenamiento de una época.
- • Pesos: dos shards de safetensors, 6,69 GB en total.
No devuelve Markdown
Es fácil suponer que devuelve Markdown, pero la documentación de post-procesamiento del repo es tajante: “Unlimited-OCR 2 does not emit Markdown.” Lo que recibís es un string de bloques de layout, cada uno con un tipo (title, text, table, image, header…), una bounding box y su contenido, más un marcador <PAGE> por cada página de entrada. Las tablas vienen en HTML y los títulos llevan un tag de nivel. El process_unlimitedOCR2.py del repo convierte eso a Markdown usando solo la biblioteca estándar, y la sección 4 se apoya en él.
2. Qué trae v2: de parsing largo a parsing ilimitado
La idea de v1 era R-SWA, Reference Sliding Window Attention. Mantenía constante la caché KV mientras el modelo escribía, así que podía transcribir decenas de páginas con un límite de 32K. Pero cada token nuevo seguía atendiendo a todos los tokens visuales de todas las páginas. Con cien páginas, ese contexto visual se vuelve el cuello de botella, y cada paso de decodificación se hace más lento a medida que el documento crece.
v2 reemplaza todas las capas de atención del decoder por SR-SWA, Sparse Reference Sliding Window Attention, y suma una forma nueva de correr documentos largos. La analogía del paper es alguien copiando un libro: la vista en la página actual y un vistazo rápido hacia atrás cuando una tabla sigue en la otra hoja. Tiene tres partes.
Las tres partes, en criollo
Page Gate
Achica lo que cada token puede mirar, del documento entero a las páginas relevantes, sin perder la cuenta de dónde cae cada página en el documento.
Token Sieve
Elige un conjunto fijo top-k de bloques de tokens lejanos para mantener vivas las relaciones de largo alcance, y aprende esa elección con la loss normal de entrenamiento. La card fija el conjunto de trabajo en un tamaño fijo, “e.g., 512 tokens”.
Unlimited-Horizon Decoding
Corta el documento en tramos de páginas solapados y los procesa en orden. Los tramos vecinos comparten una página de borde cuya caché KV se reutiliza; todo lo que queda fuera del tramo activo se libera. En SGLang, esto es la ventana de páginas.
El resultado, según el paper: a escala de cien páginas, cada paso de decodificación es 7,0× más rápido que con atención completa y 5,6× más rápido que con el R-SWA de v1 (detalles en la sección 3). En MPDocBench, con documentos de hasta 30 páginas, v2 le gana a v1 en todas las métricas, sobre todo donde el contenido cruza un salto de página.
v1 vs v2 en MPDocBench (autorreportado, Tabla 1 del paper)
↑ más alto es mejor; ↓ más bajo es mejor (distancia de edición). Las filas entre páginas miden texto y tablas que siguen después de un salto de página.
| Métrica | Unlimited-OCR (v1) | Unlimited-OCR 2 |
|---|---|---|
| Overall ↑ | 61.89 | 84.59 |
| Distancia de edición de texto ↓ | 0.167 | 0.052 |
| Edición de texto entre páginas ↓ | 0.605 | 0.100 |
| Fórmulas CDM ↑ | 73.71 | 89.39 |
| Tablas TEDS ↑ | 73.51 | 87.78 |
| Tablas entre páginas TEDS ↑ | 60.76 | 89.54 |
| Figuras F1 ↑ | 53.19 | 72.91 |
| Edición del orden de lectura ↓ | 0.204 | 0.077 |
| Títulos TEDS ↑ | 31.58 | 60.10 |
Otros cambios que vas a notar
- • Ya no escribís el prompt: el modelo agrega “<PAGE><image>” una vez por página más “Multi page parsing.” por su cuenta. v1 pedía prompts como “<image>document parsing.”.
- • Otro stack: la card de v2 fija Python 3.10, torch 2.13.0 y transformers 4.52.1. v1 se probó con Python 3.12.3, torch 2.10.0 y transformers 4.57.1, así que dale a cada uno su propio entorno.
- • Entrenamiento: unos 2M de documentos multipágina y 10M de una sola página, con pseudo-etiquetas de MinerU2.5-Pro y PaddleOCR-VL-v1.6 más páginas sintéticas, en 25 nodos de 8 NVIDIA H800. Solo se entrenó el modelo de lenguaje; el encoder de visión quedó congelado.
- • El serving pasó a SGLang, con el scheduling por ventanas de páginas ya integrado. La card de v2 no tiene sección de vLLM.
- • Un benchmark nuevo, MarathonDocBench: 100 documentos en chino e inglés de 100 a 300 páginas (unas 18.000 páginas), publicado por Baidu junto con el modelo.
La versión en una frase
v1 frenó el crecimiento de la memoria mientras el modelo escribía; v2 además frena el crecimiento del costo de lectura, así que Baidu dice que un PDF de 300 páginas cuesta más o menos lo mismo por paso de decodificación que uno de 30.
3. Benchmarks contra otros modelos (autorreportados)
El leaderboard de Baidu enfrenta a v2 con modelos de visión y lenguaje de uso general en dos benchmarks multipágina. Los modelos de OCR dedicados como DeepSeek-OCR 2 solo aparecen en la tabla de una página del paper, así que lo separé en dos tablas.
Puntaje overall en benchmarks multipágina (autorreportado, Tablas 1 y 2 del paper)
MPDocBench: hasta 30 páginas. MarathonDocBench: 100–300 páginas, armado por Baidu. Overall es el promedio de ocho métricas. Filas seleccionadas.
| Modelo | 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 | Sin dato (límite de 40 páginas) |
En MPDocBench la ventaja es real pero moderada: 6,3 puntos sobre Qwen3.8-Max. En MarathonDocBench son 43 puntos sobre el siguiente modelo, y la letra chica explica buena parte de esa diferencia.
Leé la letra chica de MarathonDocBench
La card del dataset dice que los VLMs generales recibieron todas las páginas en un solo request, con las imágenes achicadas y reintentos cuando el request era demasiado grande. Los documentos que superaban el tope de imágenes de un proveedor se salteaban, y las fallas quedaban como predicciones vacías, para que el denominador siga siendo 100 documentos. Eso mide límites de API tanto como capacidad de lectura. Sigue siendo la prueba correcta si querés mandar un PDF de 200 páginas en una sola llamada, pero no es una comparación de precisión en igualdad de condiciones.
Una página: OmniDocBench v1.6 (autorreportado, Tabla 3 del paper)
Overall ↑, distancia de edición de texto ↓, tablas TEDS ↑. Tamaños tal como los lista el paper. Filas seleccionadas.
| Modelo | Tamaño | Overall | Edición de texto | Tablas 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 |
En una sola página, v2 es básicamente v1: 94.02 vs 93.92 de overall, con un puntaje de tablas un poco más bajo (89.60 vs 90.16). Le gana a DeepSeek-OCR 2 y a dots.ocr, pero dos modelos más chicos, TeleOCR y OvisOCR2, sacan más. PaddleOCR-VL no está en la tabla: Baidu usó PaddleOCR-VL-v1.6 para generar pseudo-etiquetas. Para documentos de una o dos páginas, v2 no es la elección obvia. Su fuerte es el largo.
Lo que dicen de velocidad y memoria (paper, autorreportado)
El paper corre sus pruebas de latencia por paso en modo gundam, con batch de 3 y 1.024 tokens generados por página. Su tabla de Unlimited-Horizon Decoding, corrida sobre MarathonDocBench, muestra que la memoria KV pico baja de 47,00 GB a 1,77 GB para una secuencia y que el throughput sube de 360 a 1.315 tokens por segundo con batch de 16. El paper no dice qué GPU usó para esas corridas.
4. Quickstart: Transformers, SGLang y un paso de ingesta para RAG
La card de v2 documenta dos caminos: Transformers “for quick tests” y SGLang, “recommended for serving”. Todos los bloques de abajo están copiados de la card, salvo los dos últimos, que son glue mío y lo aclaran. Los dos caminos asumen una GPU NVIDIA, y el setup de SGLang además asume Linux. Si leíste que v2 trae recetas para vLLM y Docker Model Runner, no están en ninguna revisión de la card; la receta de vLLM es para v1.
Formas de correrlo (revisado el 3 de noviembre de 2026)
Según la card de v2 y su historial de revisiones, el README de GitHub, la API de HF y el sitio de recetas de vLLM.
| Opción | Estado para v2 | Notas |
|---|---|---|
| Transformers | En la card, “for quick tests” | trust_remote_code; Python 3.10, torch 2.13.0, transformers 4.52.1; probado en una A800-80GB |
| SGLang | En la card, “recommended for serving” | Wheel propio sglang-0.5.20.dev13 en la carpeta wheel/ del repo; API compatible con OpenAI en el puerto 10000 |
| vLLM | Sin documentar para v2 | La receta y las imágenes vllm/vllm-openai:unlimited-ocr son para v1; recipes.vllm.ai/baidu/Unlimited-OCR-2 devolvió 404 |
| Docker Model Runner | Sin mención | No hay instrucciones de Docker en la card de v2 ni en el README de GitHub |
| Demo hosteada | Funcionando | Space de Hugging Face baidu/Unlimited-OCR-2 sobre ZeroGPU, más ModelScope y Baidu Cloud |
| Fine-tuning | ms-swift | Agregado el 28 de octubre, según el README de GitHub |
Transformers: instalación (model card)
Las versiones fijadas de la card, probadas con Python 3.10 en una NVIDIA A800-80GB. Usá un entorno virtual nuevo.
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: cargá una vez y parseá (model card)
infer_multi manda todas las páginas en un solo request, así el modelo puede usar contexto entre páginas; infer procesa una página con tiling dinámico. Con save_results=True, escribe result.md, result_raw.md y un result_with_boxes_N.jpg por página en 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: entorno (model card)
Correlo desde un clon del repo, porque la ruta del wheel es relativa. En GitHub el wheel es un archivo de Git LFS (un puntero de 133 bytes si no tenés LFS); el repo de Hugging Face tiene el wheel completo de 15,3 MB.
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: levantá el servidor (model card)
Copiá los nombres de las variables tal cual están, incluido SGLANG_UOCR_EANABLE_DOC_LIMIT con su typo. No revisé si el wheel también acepta la ortografía corregida.
# 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: cliente compatible con OpenAI (model card)
Hace streaming de la salida y la devuelve como un solo string. El logit processor de n-gramas evita los loops de repetición, y max_tokens es el presupuesto de salida para todo el documento.
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))Las perillas de memoria que marca la card
- • --mem-fraction-static 0.7 es estable; 0.75 deja muy poco margen con 32 o más requests concurrentes.
- • --max-prefill-tokens define un tensor temporal que crece linealmente con él: pasar de 4096 a 2048 reduce ese pico a la mitad. El bucket más grande de --cuda-graph-bs-prefill tiene que ser igual a ese valor.
- • SGLANG_OPT_UOCR_SAM_REL_POS_BIAS_MB=64 evita una asignación única de ~462 MiB que puede quedarse sin memoria con el default de 512.
- • Las ventanas de páginas más chicas bajan la memoria pico, pero empeoran la unión de bloques que cruzan el borde de una ventana.
Del PDF a los chunks: un paso de ingesta para RAG
Así lo conectaría yo a la ingesta. El cliente de la card ya renderiza el PDF a imágenes de 300 DPI y devuelve la salida cruda. El primer bloque de abajo la guarda de tres formas: cruda, como Markdown del documento completo para chunking, y como un Markdown por página para las citas. El segundo es un chunker mínimo que respeta los títulos. Probé el chunker con un Markdown de ejemplo escrito a mano; ninguno de los dos bloques vio salida real del modelo. Si estás eligiendo estrategia, mi guía de chunking para RAG compara chunking fijo, semántico y tardío.
Glue (mío, no de la card): guardá la salida cruda y el Markdown
Necesita utils/process_unlimitedOCR2.py del repo de GitHub (solo biblioteca estándar). La línea de DROP_LABELS es un ajuste mío: por default el conversor conserva encabezados, pies y números de página, que se repiten en cada hoja y ensucian los chunks. Los archivos por página pierden las uniones entre páginas, así que usalos para las citas, no como índice principal.
# 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 (mío): un chunker que respeta los títulos
Corta en los títulos de Markdown, después agrupa bloques hasta un tope de tamaño y guarda el título como metadata. Una tabla suele salir como un solo bloque HTML, así que una tabla grande queda como su propio chunk sobredimensionado en lugar de cortarse a mitad de fila.
# 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")A dónde van los chunks después
Generá los embeddings con el modelo que ya usás y cargalos en tu base vectorial; mi guía de pgvector vs Qdrant cubre esa parte. Si preferís no armarlo vos, OpenRAG junta ingesta, búsqueda y chat, y su pipeline de Docling acepta Markdown, así que podés subir los .md en lugar de los PDFs crudos.
5. Límites honestos
Tenelos en cuenta antes de pasar tu archivo de documentos por este modelo.
Dónde tendría cuidado
- • Todos los benchmarks son de Baidu, corridos con el harness de Baidu, y el benchmark estrella de documentos largos lo armó y lo publicó Baidu junto con el modelo. Ocho días después del lanzamiento no encontré ningún benchmark independiente, solo los primeros posts de pruebas.
- • Los títulos son el punto débil: Heading TEDS da 60.10 en MPDocBench y 48.20 en MarathonDocBench. La propia sección de limitaciones del paper dice que la atención dispersa “does not preserve global document information”, lo que limita las jerarquías de títulos. No armes un chunker que confíe en los niveles de los títulos.
- • Los idiomas no están documentados. La card solo dice “multilingual”, el paper no lista los idiomas de entrenamiento y MarathonDocBench es chino e inglés. Un post temprano de pruebas de MindStudio (28 de octubre, enlazado abajo, no lo verifiqué) cuenta un loop de repetición con texto en urdu. Probalo primero con documentos en español o en los idiomas que uses.
- • La escritura a mano no está cubierta: ni la card ni el paper la mencionan ni reportan un puntaje de manuscritos. El mismo post de MindStudio dice que unas ecuaciones de física escritas a mano salieron bien, que es una muestra, no un benchmark.
- • No hay una GPU mínima declarada. La única GPU probada es una A800 de 80 GB, solo los pesos ocupan 6,69 GB en BF16, y MindStudio reporta unos 8,3 GB de VRAM después de cargarlo, sin decir qué GPU usó. La cifra de 1,77 GB de KV sale del Unlimited-Horizon Decoding, que según el paper está integrado en SGLang; el camino de Transformers de la card no menciona ventanas de páginas, así que espero que necesite más memoria con documentos largos.
- • No hay camino para Mac ni CPU: el código de la card llama a .cuda(), el repo trae kernels de Triton y el setup de SGLang usa un virtualenv de bash y /dev/shm.
- • El conversor se escribió para coincidir con el ground truth del benchmark. Fuerza un árbol de títulos con una sola raíz y deja placeholders de figuras como  que apuntan a recortes que tendrías que generar vos.
trust_remote_code, la licencia y los datos
Cargar con trust_remote_code=True ejecuta en tu máquina el Python de Baidu que está en el repo: 10 archivos, unos 480 KB, entre ellos el código del modelo, un módulo propio de atención dispersa y kernels de Triton. El camino de SGLang encima instala un build propio de SGLang de 15,3 MB. Los dos vienen de las cuentas oficiales de Baidu, pero leelos, correlos en un contenedor o una VM sin acceso a secretos, y fijá lo que revisaste: from_pretrained acepta un argumento revision, y el commit de cabecera del 3 de noviembre era 0480966938291445591f69f08a13b041c073a96a. El modelo y el código son MIT. Las anotaciones de MarathonDocBench son Apache-2.0, pero sus imágenes de página son solo para uso académico y de investigación.
Veredicto: ¿va a tu pipeline RAG?
Si tu corpus son PDFs largos (manuales, presentaciones regulatorias, informes, libros, archivos escaneados) y tenés una GPU NVIDIA, Unlimited-OCR 2 es el lanzamiento de OCR abierto más interesante del mes. Lee un documento entero en un solo request, mantiene juntas las tablas que cruzan saltos de página y sale con licencia MIT. Esas fallas entre páginas son justamente las que arruinan el chunking más adelante.
No es una mejora universal. En una sola página es v1 con una capa de atención nueva, hay modelos más chicos que sacan más, y todos los números son de Baidu. Tomá los benchmarks como un motivo para probar, no para migrar: pasá 20 documentos tuyos, compará el Markdown con el de tu parser actual y mirá con lupa las tablas y los títulos.
Quién debería probarlo
PDFs largos para RAG
Manuales, presentaciones regulatorias, informes y libros de más de 30 páginas, donde las tablas y los párrafos cruzan saltos de página. Es el caso para el que se hizo v2.
Procesos batch sobre archivos escaneados
Una máquina Linux con una GPU de workstation o de data center, corriendo el servidor de SGLang con ventanas de páginas.
Si ya usás v1
Actualizá si chocaste con el techo de ~40 páginas o viste tablas rotas entre páginas. Para trabajos de una página, v1 saca más o menos lo mismo.
Saltealo si…
Necesitás correrlo en Mac o CPU o leer escritura a mano, no podés probar idiomas más allá del chino y el inglés, o tus documentos son páginas sueltas cortas donde modelos de OCR más chicos sacan más.
Checklist para probarlo
- 1. Probá el Space de Hugging Face con dos o tres de tus PDFs más difíciles, usando documentos que puedas subir sin problema.
- 2. Creá un entorno nuevo de Python 3.10 e instalá las versiones fijadas de la card.
- 3. Corré infer_multi sobre un documento de 10–20 páginas y abrí result.md y las imágenes result_with_boxes.
- 4. Para documentos largos, pasate al servidor de SGLang con la configuración de ventanas de la card.
- 5. Convertí con process_unlimitedOCR2.py, sacá encabezados y pies, hacé el chunking y compará el retrieval con tu parser actual sobre las mismas preguntas.
- 6. Fijá el commit que revisaste y mantené el código remoto en un entorno aislado.
Fuentes de este post
- Model card en Hugging Face: baidu/Unlimited-OCR-2
- config.json de Unlimited-OCR-2 (arquitectura, posiciones, atención dispersa)
- API de Hugging Face: baidu/Unlimited-OCR-2 (fechas, licencia, parámetros)
- arXiv 2610.39308: Unlimited OCR Works 2
- arXiv 2606.23050: Unlimited OCR Works (v1)
- GitHub: baidu/Unlimited-OCR
- GitHub: documentación de post-procesamiento (utils/README.md)
- Model card en Hugging Face: baidu/Unlimited-OCR (v1)
- Card del dataset MarathonDocBench (leaderboard, notas de evaluación, licencia)
- Space de Hugging Face: demo de Unlimited-OCR-2
- Receta de vLLM para Unlimited-OCR (v1)
- AI Weekly: Baidu libera Unlimited-OCR-2 (fuente secundaria)
- MindStudio: primeras pruebas de Unlimited-OCR 2 (fuente secundaria, 28 de octubre de 2026)



