Files
wehub-resource-sync 94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

728 lines
25 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Benchmark DeepSeek-OCR-2 (and similar OCR VLMs) on olmOCR-bench via a running sglang server.
Usage:
# 0. Download the dataset (one-time, ~2 GB with PDFs via Git LFS)
hf download --repo-type dataset \\
allenai/olmOCR-bench --local-dir ./olmOCR-bench
# 1. Start the sglang server (matches run.sh)
python -m sglang.launch_server \\
--model-path deepseek-ai/DeepSeek-OCR-2 --host 127.0.0.1 --port 30000
# 2. Run the full benchmark (all 7 splits, ~7,010 tests)
python -m benchmark.ocr.bench_sglang --port 30000 --split all --concurrency 8
# 3. Quick run on a single split
python -m benchmark.ocr.bench_sglang --port 30000 --split arxiv_math --concurrency 16
# 4. Limit pages for a fast smoke-test
python -m benchmark.ocr.bench_sglang --port 30000 --split old_scans --max-samples 10
# 5. Custom dataset location
python -m benchmark.ocr.bench_sglang --bench-dir /data/olmOCR-bench/bench_data
Dataset:
allenai/olmOCR-bench 7 splits, 1,403 PDFs, 7,010 unit tests
Splits: arxiv_math | old_scans_math | table_tests | old_scans |
headers_footers | multi_column | long_tiny_text
PDFs are stored via Git LFS; hf download (step 0) is required.
Reference scores (olmOCR-bench):
DeepSeek-OCR v1 : 75.7 ± 1.0
DeepSeek-OCR-2 : 76.3 (reported on HF model card)
olmOCR v0.4.0 : 82.4 ± 1.1
PaddleOCR-VL : 80.0 ± 1.0
"""
import argparse
import asyncio
import base64
import io
import json
import os
import sys
import time
import traceback
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple
import aiohttp
from tqdm.asyncio import tqdm as atqdm
# ---------------------------------------------------------------------------
# Paths: allow running from the repo root or from benchmark/ocr/
# ---------------------------------------------------------------------------
_SCRIPT_DIR = Path(__file__).resolve().parent
if str(_SCRIPT_DIR) not in sys.path:
sys.path.insert(0, str(_SCRIPT_DIR))
from eval_utils import (
aggregate_results,
evaluate_olmocr_tests,
print_results_table,
)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
OLMOCR_BENCH_SPLITS = [
"arxiv_math",
"old_scans_math",
"table_tests",
"old_scans",
"headers_footers",
"multi_column",
"long_tiny_text",
]
# DeepSeek-OCR-2 prompt formats (https://github.com/deepseek-ai/DeepSeek-OCR-2)
_PROMPT_MARKDOWN = "<|grounding|>Convert the document to markdown."
_PROMPT_FREE_OCR = "Free OCR."
# ---------------------------------------------------------------------------
# Argument dataclass
# ---------------------------------------------------------------------------
@dataclass
class BenchArgs:
port: int = 30000
host: str = "127.0.0.1"
model: str = "deepseek-ai/DeepSeek-OCR-2"
split: str = "all"
concurrency: int = 8
output_dir: str = "./ocr_bench_results"
max_samples: int = -1
prompt_mode: str = "markdown"
bench_dir: str = "./olmOCR-bench/bench_data"
request_timeout: int = 300
save_raw_outputs: bool = False
render_dpi: int = 150
debug: bool = False
debug_accuracy: bool = False
@staticmethod
def add_cli_args(parser: argparse.ArgumentParser) -> None:
parser.add_argument(
"--port", type=int, default=BenchArgs.port, help="sglang server port"
)
parser.add_argument(
"--host", type=str, default=BenchArgs.host, help="sglang server host"
)
parser.add_argument(
"--model",
type=str,
default=BenchArgs.model,
help="Model identifier (must match the running server)",
)
parser.add_argument(
"--split",
type=str,
default=BenchArgs.split,
choices=OLMOCR_BENCH_SPLITS + ["all"],
help="Dataset split to evaluate. Use 'all' for all splits.",
)
parser.add_argument(
"--concurrency",
type=int,
default=BenchArgs.concurrency,
help="Max concurrent requests to the sglang server",
)
parser.add_argument(
"--output-dir",
type=str,
default=BenchArgs.output_dir,
help="Directory for result JSON files",
)
parser.add_argument(
"--max-samples",
type=int,
default=BenchArgs.max_samples,
help="Max samples per split (-1 = all)",
)
parser.add_argument(
"--prompt-mode",
type=str,
default=BenchArgs.prompt_mode,
choices=["markdown", "free_ocr"],
help=(
"Prompt mode for the OCR model: "
"'markdown' → '<|grounding|>Convert the document to markdown.'; "
"'free_ocr' → 'Free OCR.'"
),
)
parser.add_argument(
"--bench-dir",
type=str,
default=BenchArgs.bench_dir,
help=(
"Local directory containing the olmOCR-bench bench_data/ folder "
"(JSONL files + pdfs/ sub-directory). Download first with: "
"hf download --repo-type dataset allenai/olmOCR-bench "
"--local-dir ./olmOCR-bench"
),
)
parser.add_argument(
"--request-timeout",
type=int,
default=BenchArgs.request_timeout,
help="Per-request timeout in seconds",
)
parser.add_argument(
"--save-raw-outputs",
action="store_true",
default=False,
help="Include raw OCR text in result JSON (useful for debugging)",
)
parser.add_argument(
"--render-dpi",
type=int,
default=BenchArgs.render_dpi,
help="DPI for rendering PDF pages to images",
)
parser.add_argument(
"--debug",
action="store_true",
default=False,
help=(
"Enable debug logging: print per-sample errors immediately, "
"show full tracebacks, and abort on the first server connection failure."
),
)
parser.add_argument(
"--debug-accuracy",
action="store_true",
default=False,
help=(
"Print per-sample accuracy details: input PDF path, expected "
"expressions/text, OCR output, and pass/fail per test."
),
)
@classmethod
def from_cli_args(cls, args: argparse.Namespace) -> "BenchArgs":
import dataclasses
attrs = [f.name for f in dataclasses.fields(cls)]
return cls(**{attr: getattr(args, attr) for attr in attrs})
# ---------------------------------------------------------------------------
# Server preflight check
# ---------------------------------------------------------------------------
async def preflight_check(api_url: str, model: str, debug: bool) -> None:
"""
Send a minimal request to the server before the benchmark starts.
Raises SystemExit with a clear message if the server is unreachable or
returns an unexpected error.
"""
print(f" Preflight check → {api_url} … ", end="", flush=True)
payload = {
"model": model,
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 1,
}
try:
async with aiohttp.ClientSession() as session:
async with session.post(
api_url,
json=payload,
timeout=aiohttp.ClientTimeout(total=10),
) as resp:
if resp.status in (200, 400): # 400 = bad request but server is alive
print("OK")
return
body = await resp.text()
print(f"FAILED (HTTP {resp.status})")
raise SystemExit(
f"Server returned HTTP {resp.status}.\nResponse: {body[:400]}\n"
f"Ensure the server is running: python -m sglang.launch_server "
f"--model-path {model} --host 127.0.0.1 --port <PORT>"
)
except (aiohttp.ClientConnectorError, asyncio.TimeoutError) as exc:
print("FAILED")
msg = (
f"Cannot reach sglang server at {api_url}\n"
f"Error: {exc}\n"
"Check:\n"
" 1. Is the server running? (python -m sglang.launch_server ...)\n"
" 2. Is --port correct?\n"
" 3. Are you running inside the same docker container as the server?"
)
if debug:
msg += f"\n\nFull traceback:\n{traceback.format_exc()}"
raise SystemExit(msg)
# ---------------------------------------------------------------------------
# PDF → base64 PNG
# ---------------------------------------------------------------------------
def pdf_page_to_base64_png(pdf_bytes: bytes, page_num: int = 0, dpi: int = 150) -> str:
"""
Render a single PDF page to a base64-encoded PNG string.
Tries PyMuPDF (fitz) first; falls back to pdf2image / poppler.
page_num is 0-indexed.
"""
try:
import fitz # PyMuPDF
doc = fitz.open(stream=pdf_bytes, filetype="pdf")
if page_num >= len(doc):
page_num = len(doc) - 1
page = doc[page_num]
mat = fitz.Matrix(dpi / 72.0, dpi / 72.0)
pix = page.get_pixmap(matrix=mat)
img_bytes = pix.tobytes("png")
doc.close()
return base64.b64encode(img_bytes).decode("utf-8")
except ImportError:
pass # Try pdf2image below
try:
from pdf2image import convert_from_bytes
images = convert_from_bytes(
pdf_bytes, dpi=dpi, first_page=page_num + 1, last_page=page_num + 1
)
if not images:
raise ValueError(f"pdf2image returned no images for page {page_num}")
buf = io.BytesIO()
images[0].save(buf, format="PNG")
return base64.b64encode(buf.getvalue()).decode("utf-8")
except ImportError as exc:
raise ImportError(
"No PDF rendering library found. "
"Install PyMuPDF (pip install pymupdf) or pdf2image (pip install pdf2image)."
) from exc
# ---------------------------------------------------------------------------
# OCR request via sglang OpenAI-compatible API
# ---------------------------------------------------------------------------
async def run_ocr_request(
session: aiohttp.ClientSession,
api_url: str,
model: str,
image_b64: str,
text_prompt: str,
timeout: int,
) -> Tuple[str, float]:
"""
Send an image + text prompt to the sglang /v1/chat/completions endpoint.
Returns (ocr_text, latency_seconds).
On error returns ("ERROR: ...", -1.0).
"""
payload = {
"model": model,
"messages": [
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {"url": f"data:image/png;base64,{image_b64}"},
},
{"type": "text", "text": text_prompt},
],
}
],
"max_tokens": 4096,
"temperature": 0.0,
}
t0 = time.perf_counter()
try:
async with session.post(
api_url,
json=payload,
timeout=aiohttp.ClientTimeout(total=timeout),
) as resp:
latency = time.perf_counter() - t0
if resp.status != 200:
body = await resp.text()
return f"ERROR: HTTP {resp.status} {body[:200]}", -1.0
data = await resp.json()
text = data["choices"][0]["message"].get("content") or ""
return text, latency
except asyncio.TimeoutError:
return "ERROR: request timed out", -1.0
except Exception:
return f"ERROR: {traceback.format_exc(limit=3)}", -1.0
# ---------------------------------------------------------------------------
# Per-sample processing
# ---------------------------------------------------------------------------
# ---------------------------------------------------------------------------
# Local dataset loading (olmOCR-bench flat JSONL)
# ---------------------------------------------------------------------------
def load_jsonl_split(bench_dir: Path, split_name: str) -> List[dict]:
"""
Load test cases from a local olmOCR-bench JSONL file and group them by
(pdf, page) so each unique PDF page becomes one benchmark "sample".
Requires the dataset to have been downloaded first::
hf download --repo-type dataset \\
allenai/olmOCR-bench --local-dir ./olmOCR-bench
"""
jsonl_path = bench_dir / f"{split_name}.jsonl"
if not jsonl_path.exists():
raise FileNotFoundError(
f"JSONL not found: {jsonl_path}\n"
"Download the dataset first:\n"
" hf download --repo-type dataset "
"--resume-download allenai/olmOCR-bench --local-dir ./olmOCR-bench"
)
test_cases: List[dict] = []
with open(jsonl_path, encoding="utf-8") as fh:
for line in fh:
line = line.strip()
if line:
test_cases.append(json.loads(line))
# Group test cases by (pdf_relative_path, page)
pdf_dir = bench_dir / "pdfs"
groups: Dict[Tuple[str, int], dict] = {}
for tc in test_cases:
pdf_rel: str = tc["pdf"]
page: int = tc.get("page", 1)
key = (pdf_rel, page)
if key not in groups:
groups[key] = {
"pdf_path": str(pdf_dir / pdf_rel),
"pdf_rel": pdf_rel,
"page": page,
"tests": [],
}
groups[key]["tests"].append(tc)
return list(groups.values())
def _log_sample_error(label: str, error: str, debug: bool) -> None:
"""Print a sample error. Always shows a one-liner; full detail only in debug mode."""
short = error.splitlines()[0] if error else "unknown error"
print(f" [ERROR] {label}: {short}", file=sys.stderr)
if debug and len(error.splitlines()) > 1:
print(error, file=sys.stderr)
async def process_sample(
semaphore: asyncio.Semaphore,
session: aiohttp.ClientSession,
api_url: str,
args: BenchArgs,
text_prompt: str,
sample: dict,
) -> dict:
"""Process one benchmark sample (a single PDF page) and return its result dict."""
label = f"{sample.get('pdf_rel', '?')} page {sample.get('page', '?')}"
async with semaphore:
pdf_path = Path(sample["pdf_path"])
if not pdf_path.exists():
err = f"PDF not found: {pdf_path}"
_log_sample_error(label, err, args.debug)
return {"error": err, "test_results": [], "passed": 0, "total": 0}
try:
pdf_bytes = pdf_path.read_bytes()
except Exception as exc:
err = f"PDF read error: {exc}"
_log_sample_error(label, err, args.debug)
return {"error": err, "test_results": [], "passed": 0, "total": 0}
page_num = sample["page"] - 1 # convert 1-indexed → 0-indexed
try:
image_b64 = pdf_page_to_base64_png(
pdf_bytes, page_num=page_num, dpi=args.render_dpi
)
except Exception as exc:
err = f"PDF render error: {exc}"
if args.debug:
err += "\n" + traceback.format_exc()
_log_sample_error(label, err, args.debug)
return {"error": err, "test_results": [], "passed": 0, "total": 0}
ocr_text, latency = await run_ocr_request(
session, api_url, args.model, image_b64, text_prompt, args.request_timeout
)
if ocr_text.startswith("ERROR:"):
_log_sample_error(label, ocr_text, args.debug)
return {"error": ocr_text, "test_results": [], "passed": 0, "total": 0}
tests = sample["tests"]
test_results = evaluate_olmocr_tests(tests, ocr_text)
# Accuracy debug: show input expectations and full OCR output
if args.debug_accuracy:
sep = "-" * 72
print(f"\n{sep}", flush=True)
print(f"[INPUT ] PDF : {pdf_path}", flush=True)
print(
f"[INPUT ] Page : {sample['page']} | {len(tests)} test(s)", flush=True
)
for i, t in enumerate(tests):
ttype = t.get("type", "?")
if ttype in ("present", "absent", "text_presence", "text_absence"):
expected = t.get("text", "")
elif ttype in ("math", "math_formula_accuracy"):
expected = t.get("math") or t.get("latex", "")
elif ttype in ("order", "natural_reading_order"):
expected = (
f"before={t.get('before', '')!r} after={t.get('after', '')!r}"
)
else:
expected = str(
{
k: v
for k, v in t.items()
if k not in ("pdf", "page", "id", "type", "url", "checked")
}
)
print(
f"[INPUT ] [{i+1}] type={ttype!r:12s} expected: {expected[:120]}",
flush=True,
)
# Print OCR output (truncate long outputs)
ocr_preview = (
ocr_text
if len(ocr_text) <= 800
else ocr_text[:800] + f"\n... [{len(ocr_text)} chars total, truncated]"
)
print(
f"[OUTPUT] OCR text ({len(ocr_text)} chars, latency={latency:.2f}s):",
flush=True,
)
print(ocr_preview, flush=True)
# Log individual test failures in debug mode
if args.debug or args.debug_accuracy:
for tr in test_results:
status = "PASS" if tr.get("passed") else "FAIL"
detail = tr.get("error", "") if not tr.get("passed") else ""
suffix = f" | {detail}" if detail else ""
print(
f"[RESULT] [{status}] type={tr.get('type')!r}{suffix}",
flush=True,
)
if args.debug_accuracy:
print(sep, flush=True)
result: dict = {
"pdf": sample["pdf_rel"],
"page": sample["page"],
"latency": round(latency, 3),
"test_results": test_results,
"passed": sum(1 for r in test_results if r.get("passed")),
"total": len(test_results),
# Store expected values so the HTML report can render them
"test_inputs": [
{
"type": t.get("type"),
"math": t.get("math") or t.get("latex", ""),
"text": t.get("text", ""),
"before": t.get("before", ""),
"after": t.get("after", ""),
# table-specific fields
"cell": t.get("cell", ""),
"up": t.get("up"),
"down": t.get("down"),
"left": t.get("left"),
"right": t.get("right"),
"top_heading": t.get("top_heading"),
"left_heading": t.get("left_heading"),
}
for t in tests
],
}
if args.save_raw_outputs:
result["ocr_output"] = ocr_text
return result
# ---------------------------------------------------------------------------
# Split-level runner
# ---------------------------------------------------------------------------
async def run_split(
split_name: str,
dataset,
args: BenchArgs,
api_url: str,
text_prompt: str,
) -> dict:
"""Evaluate one olmOCR-bench split; return aggregated results dict."""
samples = list(dataset)
if args.max_samples > 0:
samples = samples[: args.max_samples]
semaphore = asyncio.Semaphore(args.concurrency)
sample_results: List[dict] = []
_first_error: List[str] = [] # capture first error for summary
connector = aiohttp.TCPConnector(limit=args.concurrency + 4)
async with aiohttp.ClientSession(connector=connector) as session:
tasks = [
process_sample(semaphore, session, api_url, args, text_prompt, sample)
for sample in samples
]
for future in atqdm(
asyncio.as_completed(tasks),
total=len(tasks),
desc=f" [{split_name}]",
leave=True,
):
result = await future
if "error" in result and not _first_error:
_first_error.append(result["error"])
sample_results.append(result)
agg = aggregate_results(split_name, sample_results)
agg["samples"] = sample_results # include per-sample data for report generation
# Always surface error summary so silent 0/0 can't happen
error_count = agg.get("error_samples", 0)
if error_count > 0:
first = _first_error[0] if _first_error else "(unknown)"
short = first.splitlines()[0]
print(
f" WARNING: {error_count}/{len(samples)} samples errored and were skipped.",
file=sys.stderr,
)
print(f" First error: {short}", file=sys.stderr)
if not args.debug:
print(
" Re-run with --debug for full per-sample error details.",
file=sys.stderr,
)
return agg
# ---------------------------------------------------------------------------
# Entry point
# ---------------------------------------------------------------------------
def parse_args() -> BenchArgs:
parser = argparse.ArgumentParser(
description="Benchmark OCR VLMs on olmOCR-bench via sglang",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
)
BenchArgs.add_cli_args(parser)
ns = parser.parse_args()
return BenchArgs.from_cli_args(ns)
async def main() -> None:
args = parse_args()
api_url = f"http://{args.host}:{args.port}/v1/chat/completions"
text_prompt = (
_PROMPT_MARKDOWN if args.prompt_mode == "markdown" else _PROMPT_FREE_OCR
)
splits_to_run = OLMOCR_BENCH_SPLITS if args.split == "all" else [args.split]
print("=" * 60)
print(f" OCR Accuracy Benchmark olmOCR-bench")
print("=" * 60)
print(f" Model : {args.model}")
print(f" Server : {api_url}")
print(f" Prompt mode : {args.prompt_mode}")
print(f" Splits : {', '.join(splits_to_run)}")
print(f" Concurrency : {args.concurrency}")
print(f" Bench dir : {args.bench_dir}")
print(f" Output dir : {args.output_dir}")
if args.debug:
print(f" Debug mode : ON (errors)")
if args.debug_accuracy:
print(f" Debug mode : ON (accuracy — input/output per sample)")
print("=" * 60)
await preflight_check(api_url, args.model, args.debug)
bench_dir = Path(args.bench_dir)
if not bench_dir.exists():
raise SystemExit(
f"Benchmark directory not found: {bench_dir}\n"
"Download the dataset first:\n"
" hf download --repo-type dataset "
"allenai/olmOCR-bench --local-dir ./olmOCR-bench"
)
os.makedirs(args.output_dir, exist_ok=True)
all_results: Dict[str, dict] = {}
for split in splits_to_run:
print(f"\nLoading split '{split}' from {bench_dir} …")
try:
samples = load_jsonl_split(bench_dir, split)
except FileNotFoundError as exc:
print(f" WARNING: {exc}")
continue
except Exception as exc:
print(f" WARNING: could not load split '{split}': {exc}")
continue
n = (
len(samples)
if args.max_samples <= 0
else min(len(samples), args.max_samples)
)
print(
f" {n} PDF pages to evaluate ({sum(len(s['tests']) for s in samples[:n])} tests) …"
)
split_result = await run_split(split, samples, args, api_url, text_prompt)
all_results[split] = split_result
# Save per-split JSON
out_path = os.path.join(args.output_dir, f"{split}.json")
with open(out_path, "w", encoding="utf-8") as f:
json.dump(split_result, f, indent=2, ensure_ascii=False)
print(
f" Score: {split_result['overall_score']:.1f}% "
f"({split_result['total_passed']}/{split_result['total_tests']} tests passed)"
)
print(f" Saved → {out_path}")
if not all_results:
print("No results collected exiting.")
return
# Print final table
print_results_table(all_results)
# Save summary
summary_path = os.path.join(args.output_dir, "summary.json")
with open(summary_path, "w", encoding="utf-8") as f:
json.dump(all_results, f, indent=2, ensure_ascii=False)
print(f"\nFull summary saved → {summary_path}")
if __name__ == "__main__":
asyncio.run(main())