Timeouts, leases, and compute tiers¶
Voxint's pipeline distinguishes a slow healthy run from a hung run with a chain of nested time budgets. Slower compute tiers (CPU, and to a lesser degree ROCm) stretch inference wall-time by an order of magnitude, so the chain has to be sized carefully: get it wrong and a healthy 4-hour CPU transcription is indistinguishable from a dead worker. Recovery reclaims the stage, a second worker re-executes it, and the run pays twice.
The inequality chain¶
Every budget must fit strictly inside the next one out:
per-call HTTP timeout gpu_http_timeout_seconds
+ persistence margin GPU_CALL_PERSISTENCE_MARGIN_SECONDS (600 s)
< stage lease stage_lease_seconds / diarize_embed_lease_seconds
< sum of all six stage leases (acquire + prepare + transcribe + diarize_embed
+ enhance_match + finalize)
≤ Celery visibility horizon celery_visibility_timeout_seconds
with the recovery sweep (recovery_sweep_seconds, 5 min) running much more
often than any lease so expiry is noticed promptly.
Why each link exists:
- HTTP timeout < stage lease. A stage that calls a model service must survive the call and still persist its results inside its lease. If the timeout could reach the lease, a slow-but-successful call would return to a stage that no longer owns the run: recovery has reclaimed it, a second worker is executing the same stage, and both race to commit, which is duplicate execution. The 600 s margin covers result persistence + the stage commit.
- diarize_embed's dedicated lease. That stage makes one diarization call
plus N sequential embedding batches under a single lease. The static
validator can only enforce the single-call floor; sizing the lease for the
batch sum is operational: budget roughly
diarization_call + ceil(windows / 512) × embed_callfor your longest media, and raiseDIARIZE_EMBED_LEASE_SECONDSif you routinely process very long recordings on a slow tier. - Sum of leases ≤ visibility horizon. The pipeline tasks are acks-late
Celery tasks. If Redis redelivered one while a worker legitimately held a
late stage, a second worker would start the same run from its current stage.
The horizon must therefore outlast every lease a single task can hold back to
back (enforced by
_celery_visibility_covers_all_leases). Since the two-lane split, each task holds at most its own lane's leases (voxint.run_pipeline: acquire through diarize_embed;voxint.finish_pipeline: enhance_match and finalize), so the enforced all-six-stage floor is deliberately conservative: it stays valid for both lanes and for any future re-partition without new configuration. - acquire is download-bound, not inference-bound. Its chain
(
acquire_timeout_seconds+ kill/hash/publish tail <acquire_lease_seconds) is validated separately and is not scaled by compute tier, because network speed doesn't change with the inference backend.
All four links are enforced at startup by Settings validators
(src/voxint/config.py); a violating combination refuses to boot rather than
open a duplicate-execution window in production.
Compute-tier profiles¶
COMPUTE_TIER selects a named timing profile (default gpu):
| Tier | Meaning | Timing |
|---|---|---|
gpu |
CUDA services (compose.gpu.yaml) |
Baseline defaults (4 h call / 6 h lease / 12 h diarize_embed / 48 h visibility) |
rocm |
AMD-accelerated services | GPU-class timing (same defaults) |
metal |
Bare-metal Apple Silicon services (compose.metal.yaml + native launcher) |
GPU-class timing (same defaults). Measured decision (Gate M, M1 Pro): no metal factor, because CPU whisper runs 0.38–0.45× RT and MPS diarization ~0.105× RT, so even a 6 h recording transcribes in ~2.7 h against the 4 h per-call budget (docs/gpu-contracts.md, metal verdict). |
cpu |
CPU-only services (compose.cpu.yaml) |
Baseline × CPU_TIER_TIMEOUT_FACTOR (4×): 16 h call / 24 h lease / 48 h diarize_embed / 192 h visibility |
Design decision: per-tier static profile, not per-request scaling. The alternative (scaling each request's timeout by media duration) was rejected: clients are process-cached with one static timeout, per-request scaling still needs a static lease to fit inside (leases are claimed before media duration is known), and it turns every timeout bug into a per-request heisenbug. A static profile keeps the whole chain inspectable at startup.
Rules:
- The profile scales only
gpu_http_timeout_seconds,stage_lease_seconds,diarize_embed_lease_seconds, andcelery_visibility_timeout_seconds(TIER_SCALED_TIMING_FIELDS), and only when the field is at its default. - An explicitly-set env value always wins: the profile never overrides an operator decision. If your explicit value breaks the chain against the other (scaled) values, startup fails with the exact inequality named; set the related fields explicitly too.
- The factor is deliberately coarse. It does not try to predict your CPU; it keeps the ratios of the chain intact while giving slow tiers real headroom. Operators with unusually slow (or fast) hardware should set explicit values for all four fields.
What "too slow" looks like operationally¶
If a stage genuinely exceeds its lease on a slow tier, the run is reclaimed
and retried (stage_max_attempts budget), and the log line from
apply_run_preferences / recovery names the lease that expired. The fix is
configuration (raise the lease / pick the right tier), not code. The CPU tier's
release gate includes a duplicate-execution test with an artificially slow
service precisely to keep this failure mode loud in CI rather than silent in
production.