• Stefy Lanza (nextime / spora )'s avatar
    ocr: bound what a document holds, not just what it reads (v0.3.65) · 2bd7a726
    Stefy Lanza (nextime / spora ) authored
    `ocr.max_concurrency` bounds how many pages are BEING READ. Nothing bounded how
    many were RESIDENT, and those differed by a factor of the document length:
    ocr_document rasterised every page up front and held the list until the response
    was written, so pages queued behind the semaphore were in memory too. The real
    figure was (documents in flight x their page count), which no setting expressed.
    
    A production install was OOM-killed twice inside forty minutes -- 90.7 GB with
    max_concurrency at 1024, then 92.7 GB with it at 128. Eight times smaller, half a
    gigabyte worse, which is the proof the semaphore was never holding the memory.
    
    Two bounds, on the two things that grow:
    
    - `ocr.page_window` (0 = follow max_concurrency, capped at 32) reads a document a
      window of pages at a time -- rendered, read, measured, detected on, dropped,
      then the next window. Residency is O(window) not O(document). Measured on a
      40-page A4 PDF at 250 DPI: 40 pages held = +974 MB RSS, 32 = +768 MB, 8 = +215 MB.
    
    - `ocr.max_resident_mb` (0 = derive it) is a ceiling on the bytes in-flight
      documents may hold as pages, charged before a page exists: page_estimate()
      prices a PDF from its page tree without rendering it. Derived from the smaller
      of the cgroup limit and host RAM -- the container limit first, since a 2 GB
      container sizing itself from a 128 GB host has no ceiling at all.
    
    The estimate carries a measured overhead factor (PAGE_OVERHEAD = 1.6): the RGB
    pixel arithmetic under-predicts real residency by 40-55%, and a budget that
    under-charges admits half again as much as it meant to.
    
    A document larger than the whole budget still runs, clamped and alone -- a gate
    that can refuse forever is a deadlock with a polite error message. Reservations
    are returned on cancellation too, or a disconnected client shrinks the budget
    permanently.
    
    Also: rendering now runs off the event loop (a long PDF used to stall every other
    request in the process), the engine is sticky across windows so a document is not
    read by a different engine in each one, and pages already read keep their results
    when a later window falls back. The comment in config.py that reasoned "64 in
    flight is ~1.2 GB" is corrected in place -- it assumed resident == admitted.
    
    59 tests. Not bounded, and not measured: /v1/ocr/batch still buffers every
    uploaded file; source bytes are ~1% of rasterised pages, so that was not the term
    behind the kills.
    Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01HXRMe9ZPUQCCCtBai9uqt3
    2bd7a726
budget.py 7.35 KB