• Stefy Lanza (nextime / spora )'s avatar
    a lease may not wait in a thread (v0.4.19) · 72bbed69
    Stefy Lanza (nextime / spora ) authored
    Third run, third failure, and this one was mine. A 5,000-document run
    dispatched ZERO pages to 50 healthy pods in 41 minutes: not one
    `POST /v1/ocr` in any pod's log, every GPU at `KV 0.0%`, 811 pages waiting,
    and the scaler reading that unservable queue as demand and renting to 73
    pods at $43/hr. `/healthz` answered in 1.6 ms the whole time, so the loop
    fixes from 0.4.16/0.4.17 were holding and nothing looked wrong from outside.
    
    0.4.18 bounded a lease attempt at 20 s and ran it on a 256-thread pool. That
    made the LEASE STAGE the ceiling on everything: 256 threads at one attempt
    per 20 s passes ~13 pages a second however many pods are up, and a thread
    that drew the growth branch sat through an entire ~9-minute boot. Against
    811 pages each retrying every couple of seconds, nothing got through.
    
    So `acquire(grow_inline=False)`: answer now, or start growth in the
    background and raise `NoPodRightNow`. The caller waits on its event loop,
    where waiting is free, and `POD_LEASE_ATTEMPT_S` drops 20 s -> 2 s for the
    pools whose acquire predates the flag. Threaded through
    `placement.choose(grow_inline=)` by signature inspection, so a candidate
    that cannot take it keeps working.
    
    Demand had to survive that. `_waiting` only ever counted THREADS parked in
    acquire, and the non-blocking path has none, so the pool would have stopped
    growing. It now counts DENIALS over a window equal to the caller's retry
    interval, which makes the count distinct waiting PAGES rather than the
    attempts they have made between them -- a page retrying every 2 s appears
    once in any 2-second window. A longer window multiplies demand by the retry
    rate, which is how the same run read its own retries as new work.
    
    Two more things found on the way:
    
      * `pod_weight`/`pod_capacity`/`pod_threshold` depend only on a card's NAME,
        but were recomputed per POD per acquire -- `_pick` once per free pod,
        `_pods_for_demand` once per serving pod, `_preferred_vram_gb` walking
        every pod inside each -- all under the pool lock. Memoised per card:
        measured 286us -> 69us per acquire at 70 pods, 528us -> 102us at 128. The
        memo self-initialises via getattr, because a pool built without __init__
        must not have an optimisation turn into an AttributeError.
    
      * `_pick` returned a bare None for four different causes -- no pods,
        none healthy, all draining, all at their in-flight cap -- which need
        opposite responses, and the caller could only report the queue it could
        not drain. It now says which, once per 15 s, with every count in the line.
    
    I also mis-read `futex_do_wait` on 324 of 352 threads as lock contention. A
    condition-variable wait IS a futex wait, so that is what pages legitimately
    waiting for a pod look like. The pod logs settled it instead: zero POSTs
    means the dispatch never happened, not that it was slow.
    Co-Authored-By: 's avatarClaude Opus 5 <noreply@anthropic.com>
    Claude-Session: https://claude.ai/code/session_01KnfkxBpcZ1v6ypRFFyhrVM
    72bbed69
manager.py 118 KB