Files
wehub-resource-sync 917eedffcf
Main / Python 3.11 - Docs (push) Has been cancelled
Main / Python 3.11 - Build (push) Has been cancelled
Main / Python 3.11 - Lint (push) Has been cancelled
Main / Python 3.11 - Style (push) Has been cancelled
Main / Python 3.11 - Test (push) Has been cancelled
Main / GPU CI (push) Has been cancelled
Main / Release (push) Has been cancelled
Main / Build and Push Docker Images (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:27:09 +08:00

307 lines
11 KiB
Python

"""
Plot for OCR performance vs cost Pareto frontier figure for NeurIPS paper.
Invocation:
python scripts/pareto_plot.py .
"""
import argparse
import os
from dataclasses import dataclass
from typing import Literal
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
import pandas as pd
from matplotlib import font_manager
# Parse arguments
ap = argparse.ArgumentParser()
ap.add_argument("output_dir", type=str, help="Path to the output directory")
ap.add_argument(
"--font-path",
type=str,
help="Path to the font file",
default=None,
)
args = ap.parse_args()
# Add custom font if provided
if args.font_path:
font_manager.fontManager.addfont(args.font_path)
plt.rcParams["font.family"] = "Manrope"
plt.rcParams["font.weight"] = "medium"
# Ensure output directory exists
os.makedirs(args.output_dir, exist_ok=True)
OUTPUT_PATHS = [f"{args.output_dir}/ocr_pareto.pdf", f"{args.output_dir}/ocr_pareto.png"]
# Define column names
MODEL_COLUMN_NAME = "Model"
CATEGORY_COLUMN_NAME = "Category"
COST_COLUMN_NAME = "Cost_Per_Million"
PERF_COLUMN_NAME = "Performance"
COLOR_COLUMN_NAME = "Color"
OFFSET_COLUMN_NAME = "Label_Offset"
MARKER_COLUMN_NAME = "Marker"
# Define colors
DARK_BLUE = "#093235"
DARK_GREEN = "#255457"
LIGHT_GREEN = "#6FE0BA"
LIGHT_PINK = "#F697C4"
DARK_PINK = "#F0529C"
YELLOW = "#fff500"
ORANGE = "#f65834"
DARK_TEAL = "#0a3235"
OFF_WHITE = "#faf2e9"
TEAL = "#105257"
PURPLE = "#b11be8"
GREEN = "#0fcb8c"
# Dataclass for model data
@dataclass(frozen=True)
class ModelData:
name: str
cost_per_million: float
performance: float
category: str
label_offset: tuple[float, float]
def cost_per_million_by_token(gpu: Literal["a100", "h100", "l40s"], tokens_sec: float, tokens_per_page: float = 750) -> float:
"""
Calculate cost per million pages based on GPU type and token throughput.
Args:
gpu: GPU type ("a100", "h100", or "l40s")
tokens_sec: Number of tokens processed per second
tokens_per_page: Average number of tokens per page (default: 750)
Returns:
Cost per million pages in USD
"""
# GPU hourly costs in USD from https://www.runpod.io/pricing Nov 3 2025
gpu_costs = {
"a100": 1.39,
"h100": 2.69,
"l40s": 0.79,
}
cost_per_hour = gpu_costs[gpu]
# Calculate pages per hour
# tokens per hour = tokens_sec * 3600
# pages per hour = tokens per hour / tokens_per_page
tokens_per_hour = tokens_sec * 3600
pages_per_hour = tokens_per_hour / tokens_per_page
# Calculate cost per million pages
cost_per_million = (cost_per_hour / pages_per_hour) * 1_000_000
return cost_per_million
def cost_per_million_by_page(gpu: Literal["a100", "h100", "l40s"], pages_sec: float) -> float:
"""
Calculate cost per million pages based on GPU type and page throughput.
Args:
gpu: GPU type ("a100", "h100", or "l40s")
pages_sec: Number of pages processed per second
Returns:
Cost per million pages in USD
"""
# GPU hourly costs in USD from https://www.runpod.io/pricing Nov 3 2025
gpu_costs = {
"a100": 1.39,
"h100": 2.69,
"l40s": 0.79,
}
cost_per_hour = gpu_costs[gpu]
# Calculate pages per hour
pages_per_hour = pages_sec * 3600
# Calculate cost per million pages
cost_per_million = (cost_per_hour / pages_per_hour) * 1_000_000
return cost_per_million
# All model data in one place for easy editing
MODEL_DATA = [
# Perf data from olmocr paper
# ModelData(name="GPT-4o", cost_per_million=12480, performance=69.9, category="Commercial VLM", label_offset=(-35, 10)),
ModelData(name="GPT-4o", cost_per_million=7951, performance=69.9, category="Commercial VLM", label_offset=(0, 10)),
# Rescaled gpt-4o prices to gpt-4.1 api rates (3.093315*3+0.833599*12)/1288* 1000000/2
ModelData(name="GPT-4.1", cost_per_million=6112, performance=71.0, category="Commercial VLM", label_offset=(-50, 15)),
ModelData(name="Mistral OCR", cost_per_million=1000, performance=72.0, category="Commercial API Tool", label_offset=(-20, 10)),
ModelData(name="Gemini Flash 2", cost_per_million=342, performance=66.3, category="Commercial VLM", label_offset=(10, -2)),
ModelData(name="Gemini Flash 2.5", cost_per_million=1042, performance=62.1, category="Commercial VLM", label_offset=(-160, 15)),
# Perf data from paper https://arxiv.org/pdf/2509.22186
ModelData(name="MinerU 2.5.4", cost_per_million=cost_per_million_by_page("a100", 2.12), performance=75.2, category="Open VLM", label_offset=(10, -10)),
# Perf data is hard to measure, using previously calculated value, using more generous number from v.1.7.5
ModelData(name="Marker v1.10.1", cost_per_million=1492, performance=76.1, category="Open Source Tool", label_offset=(-25, 10)),
# Using cost per million pages from original olmocr paper
# ModelData(name="Qwen 2 VL", cost_per_million=???, performance=61.3, category="Open VLM", label_offset=(-35, 10)),
ModelData(
name="Qwen 2.5 VL",
cost_per_million=cost_per_million_by_page("h100", 10000 / (36 * 60 + 47)),
performance=64.5,
category="Open VLM",
label_offset=(-35, 10),
),
# Using original olmocr cost, but scaling it by 3100/2100 which is the tokens/second rate difference that we see on H100 inference
ModelData(
name="Qwen 3 VL 8B",
cost_per_million=cost_per_million_by_page("h100", 10000 / (36 * 60 + 47)) * (3100 / 2100),
performance=61.4,
category="Open VLM",
label_offset=(-35, -25),
),
# Perf data from https://arxiv.org/pdf/2509.22186
ModelData(name="Nanonets-OCR2-3B", cost_per_million=cost_per_million_by_page("a100", 0.55), performance=69.5, category="Open VLM", label_offset=(-85, 10)),
# Pricing from this tweet: https://x.com/VikParuchuri/status/1980725223616876704
# You'd get better pricing running locally, but I couldn't get a number
ModelData(name="Chandra OCR API", cost_per_million=4000, performance=83.1, category="Commercial VLM", label_offset=(-85, 10)),
# Going off of 200k pages per day per A100
ModelData(
name="DeepSeek-OCR",
cost_per_million=cost_per_million_by_page("a100", pages_sec=200_000 / (24 * 3600)),
performance=75.7,
category="Open VLM",
label_offset=(-20, 10),
),
# Perf data from paper pg 18 https://arxiv.org/pdf/2510.14528
ModelData(name="PaddleOCR-VL", cost_per_million=cost_per_million_by_page("a100", 1.2241), performance=80.0, category="Open VLM", label_offset=(-35, 10)),
# Perf data is here: https://beaker.allen.ai/orgs/ai2/workspaces/olmocr/work/01K8V42ERGBHAZ2KKDBKXKZHPJ?taskId=01K8V42ERJ9S82C06CSWQT7RR6&jobId=01K8VH0Y9J47ZXMCCWG97J7P54
ModelData(
name="Ours", cost_per_million=cost_per_million_by_page("h100", 10000 / (36 * 60 + 47)), performance=82.3, category="Ours", label_offset=(-20, 10)
),
]
# Create dataframe from the aggregated data
df = pd.DataFrame(
[
{
MODEL_COLUMN_NAME: m.name,
COST_COLUMN_NAME: m.cost_per_million,
PERF_COLUMN_NAME: m.performance,
CATEGORY_COLUMN_NAME: m.category,
OFFSET_COLUMN_NAME: list(m.label_offset),
}
for m in MODEL_DATA
]
)
# Category colors
category_colors = {"Commercial API Tool": DARK_GREEN, "Commercial VLM": DARK_GREEN, "Open Source Tool": PURPLE, "Ours": DARK_PINK, "Open VLM": PURPLE}
df[COLOR_COLUMN_NAME] = df[CATEGORY_COLUMN_NAME].map(category_colors)
# Define marker types
category_markers = {"Commercial API Tool": "o", "Commercial VLM": "^", "Open Source Tool": "o", "Ours": "*", "Open VLM": "^"}
df[MARKER_COLUMN_NAME] = df[CATEGORY_COLUMN_NAME].map(category_markers)
# Define marker sizes - increased sizes
category_marker_sizes = {"Commercial API Tool": 120, "Commercial VLM": 120, "Open Source Tool": 140, "Ours": 300, "Open VLM": 140}
# Define text colors
category_text_colors = {
"Commercial API Tool": DARK_GREEN,
"Commercial VLM": DARK_GREEN,
"Open Source Tool": PURPLE, # darker purple
"Ours": DARK_PINK, # darker pink
"Open VLM": PURPLE, # darker purple
}
# Create the plot
plt.figure(figsize=(10, 8))
# Plot each category
categories = df[CATEGORY_COLUMN_NAME].unique()
for category in categories:
mask = df[CATEGORY_COLUMN_NAME] == category
data = df[mask]
plt.scatter(
data[COST_COLUMN_NAME],
data[PERF_COLUMN_NAME],
label=category,
c=data[COLOR_COLUMN_NAME],
marker=category_markers[category],
alpha=1.0,
s=category_marker_sizes[category],
)
# Add labels for each point with increased font size
FONTSIZE = 22 # Increased from 9
for idx, row in df.iterrows():
plt.annotate(
row[MODEL_COLUMN_NAME],
(row[COST_COLUMN_NAME], row[PERF_COLUMN_NAME]),
xytext=row[OFFSET_COLUMN_NAME],
textcoords="offset points",
fontsize=FONTSIZE,
alpha=1.0,
weight="medium",
color=category_text_colors[row[CATEGORY_COLUMN_NAME]],
)
# Set up axes
plt.ylim(55, 85) # Set y-axis limits from 25 to 85 to include Qwen2VL
plt.xlim(100, 15000)
plt.xscale("log") # Use log scale for cost
plt.grid(True, which="both", ls=":", color=TEAL, alpha=0.2)
# Format y-axis to show percentages without scientific notation
def percent_formatter(y, pos):
return f"{y:.1f}%"
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(percent_formatter))
# Format x-axis to show dollar amounts
def dollar_formatter(x, pos):
return f"${x:,.0f}"
# Set specific x-axis ticks with increased font size
plt.gca().xaxis.set_major_formatter(ticker.FuncFormatter(dollar_formatter))
plt.gca().set_xticks([100, 200, 500, 1000, 2000, 5000, 10000])
plt.xticks(fontsize=16) # Increased tick font size
plt.yticks(fontsize=16) # Increased tick font size
# Add labels and title with increased font size
plt.xlabel("Cost per Million Pages (USD, log scale)", fontsize=FONTSIZE, weight="medium")
plt.ylabel("Overall Performance (Pass Rate %)", fontsize=FONTSIZE, weight="medium")
# plt.title("OCR Engines: Performance vs. Cost", fontsize=12, weight="medium")
# Remove spines
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["right"].set_visible(False)
# Add the legend with custom ordering and increased font size
handles, labels = plt.gca().get_legend_handles_labels()
desired_order = ["Ours", "Open Source Tool", "Open VLM", "Commercial API Tool", "Commercial VLM"]
label_to_handle = dict(zip(labels, handles))
ordered_handles = [label_to_handle[label] for label in desired_order if label in label_to_handle]
ordered_labels = [label for label in desired_order if label in labels]
plt.legend(
ordered_handles, ordered_labels, loc="lower right", fontsize=20, frameon=True, framealpha=0.9, edgecolor=TEAL, facecolor="white" # Increased from 10
)
# Adjust layout
plt.tight_layout()
# Save the figure
for output_path in OUTPUT_PATHS:
plt.savefig(output_path, dpi=300, bbox_inches="tight")
print(f"Plot saved to {', '.join(OUTPUT_PATHS)}")