Skip to content

Commit b88a65d

Browse files
committed
test(diffusion): add MinerU-Diffusion benchmark harness
1 parent edf4556 commit b88a65d

15 files changed

Lines changed: 4753 additions & 0 deletions

benchmarks/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Benchmark helpers for MinerU development."""
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
# MinerU-Diffusion Benchmark Harness
2+
3+
This directory contains a small benchmark harness for MinerU-Diffusion style
4+
OpenAI-compatible OCR endpoints. It is intentionally separated from model
5+
runtime code so it can be used with vLLM, HF remote-code servers, or other
6+
compatible services.
7+
8+
The harness covers four use cases:
9+
10+
- single-image text/table/formula/layout requests;
11+
- end-to-end two-step page parsing, layout first and content extraction second;
12+
- PDF-page suite rendering and batch throughput measurement;
13+
- output quality comparison, including markdown similarity and layout box F1.
14+
15+
## Single-Image Benchmark
16+
17+
Start a compatible server, then run the default four tasks:
18+
19+
```bash
20+
python -m benchmarks.mineru_diffusion.bench_hf_server \
21+
--endpoint http://127.0.0.1:18083/v1/chat/completions \
22+
--image file:///path/to/page.png \
23+
--output-dir benchmark_results/mineru_diffusion/single_image \
24+
--timeout 600
25+
```
26+
27+
To cap individual task budgets:
28+
29+
```bash
30+
python -m benchmarks.mineru_diffusion.bench_hf_server \
31+
--endpoint http://127.0.0.1:18083/v1/chat/completions \
32+
--image file:///path/to/page.png \
33+
--output-dir benchmark_results/mineru_diffusion/single_image_budgeted \
34+
--task-max-tokens text=1024,table=1024,formula=768,layout=128
35+
```
36+
37+
Results are written as JSONL plus `latest_summary.json` under the selected
38+
output directory.
39+
40+
## Native HF Remote-Code Helper
41+
42+
For local HF remote-code smoke tests, the helper below starts the bundled
43+
compatibility server, waits for `/health`, runs the client, and shuts it down:
44+
45+
```bash
46+
python -m benchmarks.mineru_diffusion.run_native_benchmark \
47+
--cuda-visible-devices 0 \
48+
--model-path /path/to/MinerU-Diffusion-V1-0320-2.5B \
49+
--image file:///path/to/page.png \
50+
--output-dir benchmark_results/mineru_diffusion/native_direct
51+
```
52+
53+
The lower-level server can also be started manually:
54+
55+
```bash
56+
CUDA_VISIBLE_DEVICES=0 python -m benchmarks.mineru_diffusion.native_server \
57+
--model-path /path/to/MinerU-Diffusion-V1-0320-2.5B \
58+
--host 127.0.0.1 \
59+
--port 18083 \
60+
--device cuda:0 \
61+
--dtype bfloat16
62+
```
63+
64+
`hf_server.py` provides the same OpenAI-compatible surface for an HF
65+
remote-code reference path.
66+
67+
## PDF-Page Suite
68+
69+
Render a manifest from local PDFs:
70+
71+
```bash
72+
python -m benchmarks.mineru_diffusion.end2end_suite \
73+
render \
74+
--output-dir benchmark_results/mineru_diffusion/pdf_suite_coverage
75+
```
76+
77+
Run a two-step parse against an OpenAI-compatible endpoint:
78+
79+
```bash
80+
python -m benchmarks.mineru_diffusion.end2end_suite \
81+
run \
82+
--manifest benchmark_results/mineru_diffusion/pdf_suite_coverage/manifest.json \
83+
--endpoint http://127.0.0.1:18083/v1/chat/completions \
84+
--output-dir benchmark_results/mineru_diffusion/pdf_suite_dllm \
85+
--layout-concurrency 4 \
86+
--content-concurrency 4 \
87+
--dynamic-threshold 0.90
88+
```
89+
90+
The suite records page-level latency, layout output, extracted blocks, markdown,
91+
and aggregate summary metrics.
92+
93+
## Quality Comparison
94+
95+
Compare a candidate result file against a baseline:
96+
97+
```bash
98+
python -m benchmarks.mineru_diffusion.compare_results \
99+
--baseline benchmark_results/mineru_diffusion/baseline/latest_results.jsonl \
100+
--candidate benchmark_results/mineru_diffusion/candidate/latest_results.jsonl \
101+
--output benchmark_results/mineru_diffusion/compare.json \
102+
--similarity-cases text,table \
103+
--min-similarity 0.95 \
104+
--max-control-repeat 8
105+
```
106+
107+
The comparison report includes character-level similarity, output length ratio,
108+
control-token ratio, longest repeated control-token run, and layout box
109+
precision/recall/F1 for layout cases.
110+
111+
## Layout Sampling Experiment
112+
113+
`layout_sampling_experiment.py` compares default sampling against MinerU-style
114+
deterministic layout sampling for the layout stage:
115+
116+
```bash
117+
python -m benchmarks.mineru_diffusion.layout_sampling_experiment \
118+
--manifest benchmark_results/mineru_diffusion/pdf_suite_coverage/manifest.json \
119+
--endpoint http://127.0.0.1:18083/v1/chat/completions \
120+
--baseline-results benchmark_results/mineru_diffusion/baseline/latest_results.jsonl \
121+
--output-dir benchmark_results/mineru_diffusion/layout_sampling_experiment \
122+
--layout-concurrency 4 \
123+
--dynamic-threshold 0.90
124+
```
125+
126+
The experiment writes per-variant JSONL files and a markdown summary.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""MinerU-Diffusion benchmark helpers."""
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
from __future__ import annotations
2+
3+
import argparse
4+
import json
5+
import time
6+
from pathlib import Path
7+
from typing import Any
8+
9+
import requests
10+
11+
from benchmarks.mineru_diffusion.harness import (
12+
BenchmarkResult,
13+
STOP_STRINGS,
14+
read_cases,
15+
summarize_results,
16+
write_default_cases,
17+
)
18+
19+
20+
def _extract_output(payload: dict[str, Any]) -> str:
21+
return payload["choices"][0]["message"]["content"]
22+
23+
24+
def create_session() -> requests.Session:
25+
session = requests.Session()
26+
session.trust_env = False
27+
return session
28+
29+
30+
def build_request_payload(case: dict[str, Any]) -> dict[str, Any]:
31+
block_size = case.get("block_size", 32)
32+
dynamic_threshold = case.get("dynamic_threshold", 0.95)
33+
vllm_xargs = {
34+
"block_size": block_size,
35+
"dynamic_threshold": dynamic_threshold,
36+
}
37+
payload = {
38+
"model": case.get("model", "mineru-diffusion"),
39+
"messages": case["messages"],
40+
"max_tokens": case.get("max_tokens", 1024),
41+
"temperature": case.get("temperature", 1.0),
42+
"stop": case.get("stop", list(STOP_STRINGS)),
43+
"block_size": block_size,
44+
"dynamic_threshold": dynamic_threshold,
45+
"vllm_xargs": vllm_xargs,
46+
}
47+
if "max_denoising_steps" in case:
48+
max_denoising_steps = int(case["max_denoising_steps"])
49+
payload["max_denoising_steps"] = max_denoising_steps
50+
vllm_xargs["max_denoising_steps"] = max_denoising_steps
51+
return payload
52+
53+
54+
def parse_task_max_tokens(value: str) -> dict[str, int]:
55+
if not value:
56+
return {}
57+
parsed: dict[str, int] = {}
58+
for part in value.split(","):
59+
if not part:
60+
continue
61+
task, sep, raw_tokens = part.partition("=")
62+
if not sep or not task:
63+
raise ValueError(
64+
"--task-max-tokens entries must use task=tokens format"
65+
)
66+
parsed[task] = int(raw_tokens)
67+
return parsed
68+
69+
70+
def run_case(endpoint: str, case: dict[str, Any], timeout: float) -> BenchmarkResult:
71+
case_id = str(case.get("case_id", "unknown"))
72+
request_payload = build_request_payload(case)
73+
started = time.perf_counter()
74+
try:
75+
response = create_session().post(endpoint, json=request_payload, timeout=timeout)
76+
latency = time.perf_counter() - started
77+
response.raise_for_status()
78+
return BenchmarkResult(
79+
case_id=case_id,
80+
ok=True,
81+
latency_s=latency,
82+
output_text=_extract_output(response.json()),
83+
error=None,
84+
)
85+
except Exception as exc:
86+
return BenchmarkResult(
87+
case_id=case_id,
88+
ok=False,
89+
latency_s=time.perf_counter() - started,
90+
output_text="",
91+
error=str(exc),
92+
)
93+
94+
95+
def parse_args() -> argparse.Namespace:
96+
parser = argparse.ArgumentParser()
97+
parser.add_argument("--endpoint", default="http://127.0.0.1:18082/v1/chat/completions")
98+
parser.add_argument("--cases-jsonl", type=Path, default=None)
99+
parser.add_argument("--write-default-cases", type=Path, default=None)
100+
parser.add_argument(
101+
"--image",
102+
default=None,
103+
help="image URL used when generating default text/table/formula/layout cases",
104+
)
105+
parser.add_argument("--output-dir", type=Path, default=Path("benchmark_results/mineru_diffusion"))
106+
parser.add_argument("--timeout", type=float, default=600.0)
107+
parser.add_argument(
108+
"--task-max-tokens",
109+
default="",
110+
help="comma-separated task=tokens overrides for generated default cases",
111+
)
112+
return parser.parse_args()
113+
114+
115+
def main() -> None:
116+
args = parse_args()
117+
if (
118+
args.cases_jsonl is None
119+
or args.write_default_cases is not None
120+
) and args.image is None:
121+
raise SystemExit("--image is required when generating default cases")
122+
123+
if args.write_default_cases is not None:
124+
write_default_cases(
125+
args.write_default_cases,
126+
args.image,
127+
task_max_tokens=parse_task_max_tokens(args.task_max_tokens),
128+
)
129+
print(f"wrote {args.write_default_cases}")
130+
return
131+
132+
cases_path = args.cases_jsonl
133+
if cases_path is None:
134+
cases_path = args.output_dir / "default_cases.jsonl"
135+
write_default_cases(
136+
cases_path,
137+
args.image,
138+
task_max_tokens=parse_task_max_tokens(args.task_max_tokens),
139+
)
140+
141+
args.output_dir.mkdir(parents=True, exist_ok=True)
142+
cases = read_cases(cases_path)
143+
results = [run_case(args.endpoint, case, args.timeout) for case in cases]
144+
summary = summarize_results(results)
145+
146+
results_path = args.output_dir / f"results_{int(time.time())}.jsonl"
147+
summary_path = args.output_dir / "latest_summary.json"
148+
results_path.write_text(
149+
"".join(result.to_json() + "\n" for result in results),
150+
encoding="utf-8",
151+
)
152+
summary_path.write_text(
153+
json.dumps(summary, indent=2, ensure_ascii=False) + "\n",
154+
encoding="utf-8",
155+
)
156+
print(json.dumps(summary, indent=2, ensure_ascii=False))
157+
print(f"results: {results_path}")
158+
159+
160+
if __name__ == "__main__":
161+
main()

0 commit comments

Comments
 (0)