fix(ocr): keep multi-page OCR from deadlocking on a worker's session lock - #540
sasuketorii wants to merge 3 commits into
Conversation
…lock Recognition spread pages over a shared rayon pool while oar-ocr-core runs rayon work under a worker's ONNX session mutex. Work stealing inside that inner join could start another page for the same worker on the same thread, which then waited forever on the lock its own thread held. Run every page inside its worker's own one-thread pool, dispatched from one scoped thread per worker, so library rayon work under the lock never picks up another page and never depends on the caller's global pool. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Shadow auto-approve: would not auto-approve because issues were found.
Fix all with cubic | Re-trigger cubic
A failure to start a worker's dedicated thread pool aborted engine construction, while the shared pool it replaces fell back to sequential recognition. Keep the workers whose pools started, and fall back to one worker running on the caller's thread when the first pool fails. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
All reported issues were addressed across 2 files (changes from recent commits).
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Shadow auto-approve: would not auto-approve because issues were found.
Fix all with cubic | Re-trigger cubic
Guard the poolless worker with nonblocking, page-scoped admission. Keep uncontended fallback OCR available, but return typed busy errors before concurrent or reentrant callers can enter OAR's session locks. Preserve panic payloads and reject reuse of poisoned fallback sessions. Propagate admission failures through both dispatch paths and add regressions for ordered fallback execution, error recovery, concurrent callers, same-thread reentry, nested global Rayon work, and poisoning. Assisted-by: ChatGPT
There was a problem hiding this comment.
0 issues found across 1 file (changes from recent commits).
Confidence score: 5/5
- Automated review surfaced no issues in the provided summaries.
- No files require special attention.
Heads up: you’re close to your flex budget. Increase your flex budget so reviews don’t pause.
Shadow auto-approve: would require human review. Reworks multi-page OCR dispatch to per-worker single-thread pools to fix a deadlock, adding a nonblocking fallback and two new OarOcrError variants. Needs human sign-off on the concurrency tradeoff and the public API change.
Re-trigger cubic
Summary
Multi-page OCR could hang forever at 0% CPU when the engine runs more than one OCR worker (
pipeline_concurrency()of 2 or 3, which needs 8 or more cores).OarOcrEngine::recognizespread pages over a shared rayon pool and picked each page's worker byrayon::current_thread_index(). oar-ocr-core runs rayon work while it holds a worker's ONNX session mutex:OrtInfer::infer_first_output_f32calls its closure under the lock, and the recognizer's closure decodes withCTCLabelDecode::argmax_predictions(par_chunks_exact). While that innerjoinwaited, work stealing could start another page on the same thread, which maps to the same worker, and that page blocked on the mutex its own thread already held.One pool thread of a hung
pdf2md <100-page scan> --json --ocr auto(Apple M2 Max, 3 workers), sampled withsample:The main thread was waiting in
ThreadPool::installand every other pool thread was asleep. With fewer than 8 cores the engine builds one worker and never takes this branch, so a 4-core machine does not hit it.Changes
OcrWorkerowns a one-thread rayon pool (pdf-inspector-ocr-{k}), and every page runs inside its worker'sinstall. The rayon calls oar-ocr makes under the session lock arejoin-based (par_iter,par_chunks*), so on a one-thread pool they finish from the thread's own deque and never pick up another page.dispatch_pages_on_workerskeeps the old branch: one worker or one page runs in page order onworkers[0]'s pool; otherwisemap_pages_on_workersstarts one scoped dispatch thread per worker, each taking the next page from a shared cursor. Dispatch threads only wait oninstall, so a caller that is itself a rayon worker (for examplefiles.par_iter()aroundprocess_pdf_with_ocr) does not tie up the global pool: all of the library's rayon work stays on the worker pools.Tests
try_locked mutex across a short sleep-based innerpar_iter; no re-entry for one caller or for four concurrent callers sharing one worker set. A 200-page version of this probe re-entered in about 45% of rounds against the previous shared-pool dispatch (release build).current_num_threads().clamp(1, 8)global-pool tasks dispatch pages with inner rayon work under a 30 s timeout. Scoped dispatch threads without per-worker pools time out here, because every global worker waits on its scope while the inner work is queued on the global pool.cargo fmt --all -- --check,cargo clippy -- -D warnings,cargo clippy --features ocr -- -D warnings,cargo testandcargo test --features ocrpass. I don't have access to pdf-evals, so it was not run.Footprint
Whole-document
--json --ocr autoon 100-page scans (2294×1770 RGB page images, Apple M2 Max, 3 workers):🤖 Generated with Claude Code