Repository navigation
fix(sidecar): serve long-form audio in its own lane, with per-model batch cost caps - #628
ShaiShmuel wants to merge 12 commits into
Conversation
|
Heads-up on a sidecar change I'm about to open nearby. No action needed here; it is meant to compose with this PR. Problem. On the sidecar path, SGLang OCR extract (LightOnOCR-3-4B, GLM-OCR) costs up to twice as much as on the worker's direct path. The scheduler packs up to 12 pages into one Planned change:
Overlap with this PR. Only After both land. The split could move onto your runs-alone lane: an extract item of an engine-batched model would report |
The sidecar batch former sorts pending work by cost, shortest first, so a long recording kept being skipped while shorter requests arrived, and clips packed with long-form audio waited for the whole batch to finish. Prepared audio longer than 30 s (Whisper's feature window) now runs alone: it never shares a batch, and such items are served one per batch in arrival order. While other items are pending too, the two lanes take turns: a cost-sorted batch of the other items, then the oldest long recording. Before any batch has been taken, the first one comes from the lane holding the oldest pending item. This is the rule the Python batcher adopts in #601. Batch formation is unchanged when nothing pending runs alone. Cost (audio duration in ms), the cost, request and hard audio caps, flush reasons and timer re-anchoring are unchanged. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The Python batcher serves long-form audio with the same runs-alone lane since #601. What gets flagged differs: Python flags audio only in the Whisper adapter, at its 30 s feature window, while the sidecar flags every prepared audio item longer than 30 s, whatever the model, Parakeet included. State this in the batch former's port-contract header, next to the 30 s threshold in audio preparation, and in the architecture guide's scheduling section. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add a debug "scheduler batch formed" line where the batch former drains a batch: flush reason, lane (alone or batched), batch size and cost, and the pending, pending-alone and remaining counts. This is the sidecar counterpart of the lane field in the Python batcher's "Batch formed" debug log, so batch composition and lane turns can be read from the scheduler's own log. No behaviour change. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The sidecar built every model's scheduler from one global batch config, so every model ran a 16384 cost cap and the adaptive cost range 4096 to 65536 derived from it. The Python worker sizes both from the model's own max_batch_tokens. Speech models count cost in audio milliseconds and declare 720000, so the shared cap held a clip batch to one or two clips; with the runs-alone lane alternating clip batches and long recordings, clips starved under load. EnsureModelReadyResponse gains an optional max_batch_tokens. The scheduler registry creates each model's scheduler from it: the static cost cap starts at the budget, and the adaptive controller derives its range with Python's formula (floor min(max(256, budget / 4), budget), ceiling 4 * budget, start at the budget). One clip turn now takes every pending clip up to the request count cap. Precedence: SIE_BATCHER_MAX_BATCH_COST, when set, still pins the cap and range for every model; otherwise the reported budget applies; otherwise 16384. Backends that do not report a budget, and models whose budget is 16384, behave as before. SIE_ADAPTIVE_BATCH_*_COST overrides still apply on top. When a model later reports a different budget, its scheduler re-bases every batcher and rebuilds its controller; record_completion now steps under the controller lock so the two cannot interleave. Other profiles change to match the Python path, for example 4096 or 8192 for some vision and cross-encoder models and 1 for extractors that take one document per batch. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
EnsureModelReadyResponse gains an optional max_batch_tokens: the budget from the model's profile, in the model's cost units (tokens for text, milliseconds for audio). It is filled on the ready path only, from the worker's static WorkerConfig.max_batch_tokens rather than the batch config cost that the direct-path adaptive controller rewrites as it steps. The worker-sidecar sizes the model's batch cost cap and adaptive cost range from it, as ModelWorker does for its own batcher. Sidecars that do not know the field ignore it. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With long-form audio pending, the batch former alternated one batch of batchable items with one long recording. The sidecar's request count cap is 12, against Python's 64, so once each model sizes its own cost cap the count cap bounds a clip turn instead. When more clips were pending than one batch takes, the cost sort left the longest clips behind every turn, and with clients that resubmit at once they could wait indefinitely: the starvation from #585, now limited to the costliest clips. A batchable turn now serves the batchable items that were pending when it began, cheapest first, up to the cost cap it began with. One Python batch takes the same items. The request count cap and drain budgets split the turn into batches. Items that arrive during the turn wait for the next one. A turn begins after each long recording runs, even before the next recording is pending, so the clips that waited behind it go first. Batch formation is unchanged while no long recording is pending and no turn is under way. Python's turn stays one batch; the batch former documents the difference. A closed-loop model in the tests serves 16-48 clip clients and 1-4 long-form clients one batch at a time for 30 min, with drain budgets cutting some extracts short. The longest wait for a clip drops from 893-1810 s to 17-28 s. Recordings wait a little longer, since a clip turn can now take several batches: at most 15-16 s instead of 13 s with one long-form client, and 58-80 s instead of 52 s with four. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…atch A scheduler's static cost cap started at the model's budget even when SIE_ADAPTIVE_BATCH_MAX_COST or SIE_ADAPTIVE_BATCH_INITIAL_COST clamped the adaptive controller below it. Until the first controller step, after creation and again after every re-base, batches could pack up to the full budget: 720000 ms of audio for a speech model, where an operator may have set 65536. The static cap now starts at the controller's starting cost, which is the budget unless one of those overrides clamps it, so nothing changes without overrides. The registry, the batch config and the controller now read their variables through an injectable lookup (SchedulerRegistry::from_lookup; from_env passes the process environment). Tests can then cover the precedence without touching the process environment: SIE_BATCHER_MAX_BATCH_COST set to an unsigned integer wins over a reported budget; unset or unparseable, the reported budget applies, and 16384 when none is reported. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…delReady The Candle worker's EnsureModelReadyResponse gains the optional max_batch_tokens the Python server already sends. On the ready path it carries the effective max_batch_tokens of the profile the model id routes to, following extends. A worker-sidecar in front of a Candle pool then sizes each model's batch cost cap and adaptive range from the model's own budget instead of the 16384 default. For example, prithivida/Splade_PP_en_v2:candle declares 8192. Other states leave the field unset, and readers that do not know it ignore it. Refs #585 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
80c76fa to
f94b2d8
Compare
A batchable turn of the runs-alone lane served the clips pending when it began, cheapest first, up to the cost cap it began with. The clips it left were sorted again by cost in the next turn, behind the cheaper clips that had arrived meanwhile. At a speech model's adaptive floor (180000 ms), with 18 resubmitting 10 s clients filling every turn, a 30 s clip was never served. A turn now owes the clips earlier turns left over: the oldest, whatever its cost, and more of them, oldest first, while they fit the cost cap together. Its budget is the cost cap plus the oldest one's cost. It takes its other clips cheapest first while what is left of the budget still covers what it owes, and the owed clips join the batch at which the next cheapest clip would leave too little for them. Every turn therefore serves the oldest left-over clip, so a clip is served within n + 1 turns of its arrival, where n counts it and the older clips still pending when its first turn ended; later arrivals never add to n. Batches stay in cost order and stop only at the cost cap, the request count cap, a drain budget or to keep room for owed clips, so a costly left-over clip neither cuts a batch short nor joins the cheapest clips ahead of their turn. A left-over clip above the cost cap (possible at 16384 ms and below, for backends that report no budget) runs alone and, thanks to the extra budget, no longer takes the whole turn from the cheaper clips. Until a turn has taken a clip it does not owe, it takes its cheapest one even above what is left, as a turn's first batch always did. A turn takes at most its cost cap plus two clips, and recordings still alternate with turns. Packing outside turns is unchanged. Closed-loop simulation (16, 32 and 48 clip clients, 4 recording clients, 30 minutes, 3 seeds), against cost-first turns: - 180000 ms: worst clip wait 30-61 s instead of up to 1791 s; median 14-31 s instead of 14-15 s; up to 10% more clips and up to 3% fewer recordings. - 65536 ms: worst clip wait 53-146 s instead of the whole run; median 25-68 s instead of 13 s. - 16384 ms: worst clip wait at most 533 s instead of the whole run; median 13-36 s instead of 12 s; 1.8-2.4 times as many clips. - 4096 ms: worst clip wait at most 559 s instead of the whole run and 70-84% more clips, but the median rises from 12-164 s to 97-231 s. - 720000 ms: unchanged. Tests: the 180000 ms boundary case; deterministic cases for cost order around owed clips, an owed clip above the cost cap, and the turn after the last recording; a randomized audit of the wait bound, cost order, cheapest-first order, room for owed clips, full batches and per-batch caps under replenishment, moving caps and drain budgets; closed-loop latency at 180000, 16384 and 4096 ms. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
huronat
left a comment
There was a problem hiding this comment.
Reviewed 469a4cb. No blocking issue found.
The carry-over reservation addresses the previously reported starvation case at the 180000 audio-cost floor: over 300 extracted batches, the 30-second clip is now served 50 times, with short clips and recordings continuing to make progress. Independently ran the sidecar library suite: 721 passed, 2 ignored. Required CI is green.
I support per-model batch cost budgets by default, retaining explicit operator overrides and the existing safety caps. Keep configurable per-model/profile pipeline depth as a separate follow-up; this PR leaves depth 2 unchanged, including Helm's explicit setting. When composing this with #708, preserve the clip-turn reservation and validate the combined scheduler behavior.
Non-blocking documentation correction: the low-cap overload trade-off can also be reached through SIE_ADAPTIVE_BATCH_MAX_COST, even when the backend reports a 720000 budget. GPU performance results were not independently rerun in this review.
Codex review, posted at Attila's request.
Brings the branch up to main at 8ec90e1, including #708 (aged promotion) and #714 (serde-saphyr). #708's aged promotion moves into order_for_next_batch and applies only outside batchable turns with no runs-alone request pending, which is where packing is point 3's. Inside a turn, what the turn owes already bounds every clip's wait, and a promoted recording or late clip would break the lanes. The lane test harness keeps aging out of reach, because its arrival times are simulated while aging reads the real clock. #708's new test backend gains max_batch_tokens: None. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…anes Point 3's aged promotion applies only outside batchable turns with nothing flagged pending. Lanes::aged makes every request count as aged, which is deterministic, unlike the real-clock wait window. - A recording that has waited longest still waits for the clip turn under way. - The turn after the last recording still serves its clips cheapest first. Once it ends, the oldest clip goes first again. - The cost-sorted packing reference models the promotion, and runs with and without aging. - Both lane property tests run with and without aging. While a recording stays pending, the aged run must form the same batches as the unaged one. Dropping the turn check fails the turn-after-the-last-recording test and the first property test. Dropping the whole guard fails both new tests and both property tests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Point 3, the Python comparison and try_drain_up_to's doc now say that nothing is promoted during a batchable turn or while a runs-alone item is pending. "Caps and flush triggers" is restored as #708 had it. The overload note names SIE_ADAPTIVE_BATCH_MAX_COST as another way to a low cap, and notes that point 3 makes the same trade between turns. The architecture guide no longer says batches outside turns are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95e06bd
|
@huronat #708 landed first, so this branch now merges main at
We also ran a closed-loop simulation of the combined behaviour, which is not committed:
|
Summary
max_batch_tokensinstead, which is 720000 ms of audio for Whisper large-v3-turbo and Parakeet-TDT.max_batch_tokenswhen the model is ready. The sidecar sizes that model's cost cap and adaptive range with the Python worker's formula.Net effect:
Results
These runs used one L4, with the sidecar in front of
sie-server, comparing maind680237with this branch. Both sides predate #708, and the runs were not repeated after the merge. Whisper large-v3-turbo unless noted.d680237reached 1.3 s only by never serving the recordings.SIE_BATCHER_MAX_BATCH_COST=65536, which sets every model's budget.SIE_ADAPTIVE_BATCH_MAX_COSTbelow 180000, which holds the cap there from the first batch.SIE_ADAPTIVE_BATCH_MIN_COSTat or below 65536, which lets the cap adapt down under load.Changes
scheduler/batch_former.rs,item.rs,audio_prep.rs):n + 1turns after it arrives. Herencounts it and the older clips still pending when the first of those turns ended. Later arrivals never add ton.order_for_next_batch, and applies only outside clip turns while no long recording is pending.EnsureModelReadyResponsegains an optionalmax_batch_tokens. The Python server fills it from the worker config, and the Candle worker from the routed profile. Mixed versions keep today's behaviour.b: floormin(max(256, b/4), b), ceiling4b, startb.SIE_BATCHER_MAX_BATCH_COSTwhen set, then the model's budget, then 16384.SIE_ADAPTIVE_BATCH_*still override every model's range, now from its first batch. The count cap of 12 and the 720000 ms audio cap are unchanged.docs/architecture-guide.mdand the batch former's module docs.8ec90e1, which includes perf(sidecar): send engine-batched extract items one per RunBatch, as wide as the adapter dispatches #685 and fix(sidecar): bound costly request waiting #708.max_batch_tokens: None.Tests
95e06bd: fmt and clippy (-D warnings) are clean.469a4cb. Nothing they cover changed since.For review
Refs #585, #601, #708.
🤖 Generated with Claude Code