Files
surya-ocr/benchmarks/concurrency_test.py
T
Fu DaiandClaude Opus 4.8 1a585693be Suya OCR API — vLLM-backed, OpenAI-compatible OCR service
FastAPI service wrapping the Surya-OCR-2 model (datalab-to) served through vLLM:
legacy /v1/api/ai/* endpoints, an OpenAI-compatible /v1/chat/completions endpoint,
a coalescing request batcher, a local OCR CLI, Docker packaging, multilingual
example outputs, and quantization/concurrency benchmarks.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 10:20:02 +04:00

186 lines
6.5 KiB
Python

import argparse
import base64
import json
import statistics
import subprocess
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any, Dict, List
import requests
DEFAULT_OLD_ENDPOINT = "http://127.0.0.1:5002/v1/api/ai/suya_ocr/"
DEFAULT_NEW_ENDPOINT = "http://127.0.0.1:5002/v1/api/ai/suya_ocr_vllm/"
def _encode_image(path: Path) -> str:
return base64.b64encode(path.read_bytes()).decode("utf-8")
def _preflight() -> Dict[str, Any]:
checks: Dict[str, Any] = {}
commands = {
"docker_runtimes": ["docker", "info", "--format", "{{json .Runtimes}}"],
"nvidia_smi": ["nvidia-smi", "-L"],
"nvidia_container_runtime": ["which", "nvidia-container-runtime"],
"nvidia_ctk": ["which", "nvidia-ctk"],
"docker_gpus": [
"docker",
"run",
"--rm",
"--gpus",
"all",
"--entrypoint",
"nvidia-smi",
"vllm/vllm-openai:v0.20.1",
"-L",
],
}
for name, cmd in commands.items():
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=15)
checks[name] = {
"ok": result.returncode == 0,
"stdout": result.stdout.strip(),
"stderr": result.stderr.strip(),
}
except Exception as exc:
checks[name] = {"ok": False, "stdout": "", "stderr": str(exc)}
runtimes = checks.get("docker_runtimes", {}).get("stdout", "")
checks["docker_has_nvidia_runtime"] = '"nvidia"' in runtimes
if not checks["docker_has_nvidia_runtime"] and checks.get("docker_gpus", {}).get("ok"):
checks["recommended_fix"] = (
"Docker GPU passthrough works with --gpus. Use the local "
"scripts/docker_nvidia_runtime_compat.sh wrapper to strip the legacy "
"--runtime nvidia flag from Surya's vLLM spawn command."
)
else:
checks["recommended_fix"] = (
"Run: sudo nvidia-ctk runtime configure --runtime=docker "
"--config=/etc/docker/daemon.json && sudo systemctl restart docker"
)
return checks
def _percentile(values: List[float], percentile: float) -> float:
if not values:
return 0.0
ordered = sorted(values)
index = min(len(ordered) - 1, int(round((percentile / 100) * (len(ordered) - 1))))
return ordered[index]
def _post_once(url: str, payload: Dict[str, Any], timeout: float) -> Dict[str, Any]:
start = time.perf_counter()
try:
response = requests.post(url, json=payload, timeout=timeout)
elapsed = time.perf_counter() - start
body = response.json() if response.content else {}
return {
"ok": response.status_code == 200 and body.get("code") == 200,
"status_code": response.status_code,
"api_code": body.get("code"),
"elapsed_seconds": elapsed,
"error": body.get("message") if body.get("code") != 200 else None,
}
except Exception as exc:
return {
"ok": False,
"status_code": None,
"api_code": None,
"elapsed_seconds": time.perf_counter() - start,
"error": str(exc),
}
def run_endpoint(
name: str,
url: str,
payload: Dict[str, Any],
requests_count: int,
concurrency: int,
timeout: float,
) -> Dict[str, Any]:
start = time.perf_counter()
samples: List[Dict[str, Any]] = []
with ThreadPoolExecutor(max_workers=concurrency) as pool:
futures = [
pool.submit(_post_once, url, payload, timeout)
for _ in range(requests_count)
]
for future in as_completed(futures):
samples.append(future.result())
wall_seconds = time.perf_counter() - start
latencies = [sample["elapsed_seconds"] for sample in samples]
success_count = sum(1 for sample in samples if sample["ok"])
return {
"name": name,
"url": url,
"requests": requests_count,
"concurrency": concurrency,
"success": success_count,
"failed": requests_count - success_count,
"wall_seconds": wall_seconds,
"throughput_rps": requests_count / wall_seconds if wall_seconds else 0.0,
"latency_seconds": {
"min": min(latencies) if latencies else 0.0,
"mean": statistics.mean(latencies) if latencies else 0.0,
"p50": _percentile(latencies, 50),
"p95": _percentile(latencies, 95),
"max": max(latencies) if latencies else 0.0,
},
"errors": [sample for sample in samples if not sample["ok"]][:10],
}
def main() -> None:
parser = argparse.ArgumentParser(description="Compare legacy and vLLM OCR endpoint concurrency.")
parser.add_argument("--image", required=True, help="Path to an input png/jpg image.")
parser.add_argument("--old-url", default=DEFAULT_OLD_ENDPOINT)
parser.add_argument("--new-url", default=DEFAULT_NEW_ENDPOINT)
parser.add_argument("--requests", type=int, default=20)
parser.add_argument("--concurrency", type=int, default=8)
parser.add_argument("--timeout", type=float, default=900)
parser.add_argument("--output", default="concurrency_test_results.json")
parser.add_argument("--skip-old", action="store_true")
parser.add_argument("--skip-new", action="store_true")
args = parser.parse_args()
image_path = Path(args.image)
image_type = image_path.suffix.lstrip(".").lower() or "png"
payload = {
"file": _encode_image(image_path),
"type": "jpg" if image_type == "jpeg" else image_type,
"skip_text_detection": False,
"skip_table_detection": False,
"recognize_math": False,
"ocr_with_boxes": True,
}
results = {
"started_at": time.strftime("%Y-%m-%dT%H:%M:%S%z"),
"image": str(image_path),
"requests": args.requests,
"concurrency": args.concurrency,
"preflight": _preflight(),
"endpoints": [],
}
if not args.skip_old:
results["endpoints"].append(
run_endpoint("legacy", args.old_url, payload, args.requests, args.concurrency, args.timeout)
)
if not args.skip_new:
results["endpoints"].append(
run_endpoint("vllm", args.new_url, payload, args.requests, args.concurrency, args.timeout)
)
output_path = Path(args.output)
output_path.write_text(json.dumps(results, indent=2), encoding="utf-8")
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()