Files
surya-ocr/IMPLEMENTATION_PLAN_AND_TEST_RESULTS.md
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

183 lines
8.2 KiB
Markdown
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.
# vLLM Backend Plan and Test Results
Date: 2026-06-09
## Plan
1. Keep the legacy OCR endpoint at `/v1/api/ai/suya_ocr/` for baseline comparison.
2. Add `/v1/api/ai/suya_ocr_vllm/`, backed by Surya2 `SuryaInferenceManager(method="vllm")`.
3. Use the Surya vLLM backend settings from the referenced Surya checkout:
- `SURYA_INFERENCE_BACKEND=vllm`
- `SURYA_INFERENCE_KEEP_ALIVE=true`
- T4 default: `VLLM_GPU_TYPE=t4`, `VLLM_DTYPE=float16`, `SURYA_INFERENCE_PARALLEL=16`
- optional tuning through `VLLM_GPUS`, `VLLM_GPU_MEMORY_UTILIZATION`, and `SURYA_INFERENCE_URL`
4. Add a concurrency benchmark that sends the same payload to the legacy and vLLM endpoints and writes latency/throughput JSON.
5. Verify syntax locally, then run the benchmark on a host with the Surya2 package, Docker, and NVIDIA GPU access.
## Implemented Files
- `vllm_tools.py`: vLLM-backed Surya2 predictor setup and OCR response formatting.
- `api.py`: new `/v1/api/ai/suya_ocr_vllm/` endpoint and health endpoint.
- `concurrency_test.py`: repeatable old-vs-new endpoint concurrency benchmark.
- `concurrency_test_results.json`: placeholder result file that the benchmark overwrites with measured results.
- `requirements.txt`: dependency alignment for Surya2/vLLM OpenAI client support.
- `Dockerfile`: T4 vLLM defaults. The referenced backend maps T4 16 GB VRAM to `max_num_seqs=16` and `max_num_batched_tokens=4096`; `float16` is required because T4 does not support `bfloat16`.
## Single-Container Kubernetes Deployment
The deployment now uses `vllm/vllm-openai:v0.20.1` as the base image. The container starts the vLLM OpenAI-compatible server in the background and runs `api.py` in the foreground through `scripts/start_single_container.sh`.
The API no longer depends on nested Docker. It attaches to:
```text
SURYA_INFERENCE_URL=http://127.0.0.1:8000/v1
```
The Surya source needed by the API is vendored into this repo under `surya/`, so the container does not depend on `/path/to/surya` at runtime.
Serving defaults for T4:
```text
SUYA_OCR_MODE=block
SUYA_MAX_BATCH_SIZE=8
SUYA_BATCH_WAIT_MS=25
SURYA_INFERENCE_PARALLEL=8
SURYA_INFERENCE_MAX_RETRIES=1
SURYA_MAX_BLOCKS_PER_PAGE=80
VLLM_DTYPE=float16
VLLM_MAX_NUM_SEQS=16
VLLM_MAX_BATCHED_TOKENS=4096
```
Speed-related changes:
- `/v1/api/ai/suya_ocr_vllm/` uses `vllm_batcher.py` to coalesce concurrent HTTP requests into batch Surya calls.
- `vllm_tools.py` batches layout and recognition across images.
- Default OCR mode is block mode, avoiding long full-page generations and hidden fallback bursts.
- Vendored Surya caps block OCR per page with `SURYA_MAX_BLOCKS_PER_PAGE`.
- Retry amplification is limited with `SURYA_INFERENCE_MAX_RETRIES=1`.
- The endpoint skips drawing annotated images because the API response only returns JSON/text.
- Base64 input is decoded once per request and the same PIL image object is reused for low/high-res OCR paths unless a caller explicitly asks for a copy.
- vLLM image payload encoding defaults to high-quality JPEG (`SUYA_VLLM_IMAGE_FORMAT=JPEG`, `SUYA_VLLM_JPEG_QUALITY=92`) to reduce CPU serialization and local vLLM transfer size versus PNG. Set `SUYA_VLLM_IMAGE_FORMAT=PNG` if lossless transport is required.
## Benchmark Command
```bash
python concurrency_test.py \
--image /path/to/surya/ttt.png \
--requests 20 \
--concurrency 8 \
--output concurrency_test_results.json
```
## Current Test Results
Import verification passed with `PYTHONPATH=/path/to/surya`.
Concurrency benchmark run:
```bash
/opt/conda/bin/python concurrency_test.py \
--image temp_image_c3c56948-282e-453b-8fac-6c482243d1e5.jpg \
--requests 16 \
--concurrency 16 \
--timeout 900 \
--output concurrency_test_results.json
```
Run started at `2026-06-11T14:35:17+0400`. Docker NVIDIA runtime and GPU checks passed. The local Python environment does not include the `vllm` package, so this validation attached the updated API to the already-warm local `vllm/vllm-openai:v0.20.1` server on port `43401` with `SURYA_INFERENCE_URL=http://127.0.0.1:43401/v1`. The API path still used the single-container attach mode (`SURYA_INFERENCE_AUTOSTART=false`) and did not start nested Docker.
| Endpoint | Requests | Concurrency | Success | Failed | Wall seconds | Throughput rps | Mean latency | P50 latency | P95 latency | Max latency |
| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
| `/v1/api/ai/suya_ocr/` | 16 | 16 | 16 | 0 | 43.62 | 0.3668 | 40.81 | 41.84 | 43.39 | 43.62 |
| `/v1/api/ai/suya_ocr_vllm/` | 16 | 16 | 16 | 0 | 83.59 | 0.1914 | 59.08 | 60.41 | 83.52 | 83.54 |
Latency values are per request in seconds. Mean latency is the average request elapsed time, not the total benchmark wall time.
## Approach A Outcome (2026-06-12)
Diagnosis + work-reduction tuning (see `docs/diagnosis_baseline.md` and
`docs/tier2_results.md`). Measured on the A100 dev host against the warm
`surya-vllm-43401` server — same host/server as the table above, so comparable.
**Production is T4; the fix below is hardware-independent, the absolute numbers
are not.**
**Root cause:** the request batcher coalesced requests then dispatched them only
`SURYA_INFERENCE_PARALLEL=8` wide, so only 8 of vLLM's 16 sequence slots were
used (poller `peak_running=8`). The GPU sat half-idle, which is why the vLLM
endpoint was *slower* than the flooding legacy path.
**Fix (Tier 1):** scale the worker pool to the in-flight block count, capped by a
new `SURYA_INFERENCE_MAX_INFLIGHT` (default 16, = `VLLM_MAX_NUM_SEQS`). This fills
all 16 slots (`peak_running=16`).
| vLLM endpoint, 16/16 | mean | p50 | p95 | peak_running |
| --- | ---: | ---: | ---: | ---: |
| MI=8 (before) | 50.22 | 49.48 | 72.69 | 8 |
| MI=16 (fix) | 33.75 | 36.11 | 49.15 | 16 |
- Mean 33%, p95 32%; single request 6.16 s → ~5.3 s. CER divergence 0.0009
(lossless; gate ≤ 0.005). The vLLM endpoint now beats legacy on mean and median.
- MI=24 overcommits (worse). Batch-size / batch-wait are within noise.
**Tier 2 (token ceiling, recognition DPI, block cap):** no latency benefit on
this sparse page — decode dominates and the page is light — so all Tier-2 knobs
are left at defaults. They remain relevant for dense / pathological pages.
**Locked change:** `SURYA_INFERENCE_MAX_INFLIGHT=16` added to `Dockerfile` and
`scripts/start_single_container.sh`. Re-confirm absolute latencies on a T4 before
treating them as production SLAs; the regression fix itself applies unchanged.
## FP8 Experiment
I attempted to start the latest vLLM image with multimodal encoder FP8 enabled:
```bash
--dtype bfloat16 \
--mm-encoder-attn-dtype fp8
```
The first attempt on `cuda:0` failed before serving traffic because GPU 0 did not have enough free memory for the default `--gpu-memory-utilization 0.85` target.
The second attempt on `cuda:1` booted farther, but vLLM aborted during model initialization with:
```text
ValueError: mm_encoder_attn_dtype='fp8' requires the FlashInfer cuDNN backend with cuDNN >= 9.17.1 on a GPU with native FP8 support.
```
That means:
- no FP8 latency number was produced
- the current A100 hardware is not a valid target for this exact FP8 path
- the failure is architectural, not a tuning issue
To measure a real FP8 speed change, the next engineer should use a GPU with native FP8 support and a vLLM build that satisfies the FlashInfer cuDNN requirement for multimodal encoder FP8.
## NVIDIA Docker Fix
Docker currently has no registered `nvidia` runtime. The expected daemon config is:
```json
{
"data-root": "/data/docker_cache",
"runtimes": {
"nvidia": {
"args": [],
"path": "nvidia-container-runtime"
}
}
}
```
This user cannot write `/etc/docker/daemon.json` or restart Docker because `sudo` requires a password. The host does support Docker GPU passthrough through modern `--gpus all`, so the API now patches Surya's vLLM spawn path to use `scripts/docker_nvidia_runtime_compat.sh`. That wrapper strips the legacy `--runtime nvidia` pair and preserves `--gpus device=...`.
An admin can still register the named runtime permanently with:
```bash
sudo nvidia-ctk runtime configure --runtime=docker --config=/etc/docker/daemon.json
sudo systemctl restart docker
docker run --rm --runtime nvidia --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi
```
The repo includes `scripts/configure_nvidia_docker.sh` with those commands.