fix: prevent dynamic-batcher starvation from waiting_consumer_count drift - #509
Conversation
5806511 to
d003a48
Compare
…erges RateLimiter::EnqueuePayload decrements the per-queue consumer count once per payload, but a single DequeuePayload call that merges k payloads increments it only once, leaving a permanent -(k-1) drift. Over time waiting_consumer_count_ goes non-positive; with prefetching disabled the batcher then stops being woken by Enqueue and WaitForPayloadSlotAvailable keeps failing, so dispatch falls back to a slow 500ms poll and throughput degrades (well below offered load, though not to zero) while idle instances are starved. A model reload resets the counter and restores throughput; pausing traffic does not. Credit back one consumer count per merged payload in InstanceQueue::Dequeue, where the merge happens. The credit lands on the same InstanceQueue that was dequeued from, which is exactly the queue whose count gates dispatch for that batcher mode (per-model reads queue_, device-blocking reads specific_queues_[instance]), so the accounting is balanced in both modes. Adds a gtest (src/test/instance_queue_test.cc) asserting the count returns to the idle-instance count after a merge-heavy sequence; without the fix it ends deeply negative. Signed-off-by: jimmystique <jimmy.ho@hotmail.fr>
d003a48 to
c9512ce
Compare
|
Reviewed and validated. Root cause looks correct: enqueue decrements waiting_consumer_count_ once per payload, dequeue increments once, so merges can leak credits and starve the dynamic batcher. The fix in InstanceQueue::Dequeue (credit one per merged payload) is in the right place. Unit test fails without the fix and passes with it (verified locally). Related dynamic-batching / infer paths also look good from my side. Follow-up: the new test is built/installed in core, but isn’t wired into an L0 runner yet , I’ll open a small follow-up so CI actually runs it after merge. LGTM from my side. |
yinggeh
left a comment
There was a problem hiding this comment.
Verdict
Approve. Root cause and fix look correct for the reported starvation path (server#8870).
Why the fix is right
RateLimiter::EnqueuePayload decrements waiting_consumer_count_ once per payload; DequeuePayload increments once per dequeue call. InstanceQueue::Dequeue can merge additional queued payloads into that single dequeue, so each merge of k payloads permanently leaks -(k-1).
With prefetching disabled (default_queue_policy.default_timeout_microseconds != 0), PayloadSlotAvailable / WaitForPayloadSlotAvailable gate on WaitingConsumerCount() > 0, so the drift progressively stalls dispatch until model reload.
Crediting one count per entry in merged_payloads inside InstanceQueue::Dequeue is the right place: the merge is local, and the credit lands on the same queue that was dequeued from (per-model queue_ or device-blocking specific_queues_[instance]), which is exactly what gates that batcher mode.
Locking looks fine: waiting_consumer_mu_ is never held across a payload_queue->mu_ acquisition, so doing IncrementConsumerCount() under the payload-queue lock during Dequeue does not invert the existing order.
Test
ConsumerCountStableAcrossMerges models the RateLimiter contract correctly (idle +N, per-enqueue -1, post-work re-park +1, with merge credits supplied by Dequeue). Empty payloads + non-zero max_queue_delay_ns reliably force merges. The no-merge case is a good guard against over-crediting. Stubbing Payload to avoid pulling scheduler/backend symbols matches the existing lightweight unit-test style (repo_agent_test, etc.).
Noted that L0 wiring is handled separately in server#8905 — good follow-up so CI actually runs the binary.
Nits / follow-ups (non-blocking)
-
Copyright year on modified
src/instance_queue.ccis still2021-2022; please bump to include2026before merge (Triton copyright check). -
Residual
queue_drift on instance-bound payloads (optional follow-up):EnqueuePayloadalways decrementsqueue_and, whenpayload->GetInstance() != nullptr, also decrements the specific queue. This PR only credits the queueDequeueruns on. For per-instance batchers the dispatch gate reads the specific count, so the reported bug is fixed;queue_can still drift under that path. Not needed for #8870, but worth a follow-up if we want the dual-queue counters fully balanced (e.g. also creditqueue_fromRateLimiter::DequeuePayloadwhen merges come from a specific queue).
There was a problem hiding this comment.
Please fix pre-commit pre-commit / pre-commit (pull_request)
Greptile SummaryThis PR restores waiting-consumer accounting when
Confidence Score: 5/5The PR appears safe to merge. No blocking failure remains. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart LR
A[Enqueue each payload] --> B[Decrement consumer count per payload]
B --> C[Dequeue primary payload]
C --> D[Merge additional payloads]
D --> E[Caller restores one dequeue credit]
D --> F[InstanceQueue restores one credit per merged payload]
E --> G[Consumer count returns to idle-instance count]
F --> G
Reviews (3): Last reviewed commit: "style: apply clang-format" | Re-trigger Greptile |
|
Updated copyright years ( |
Relates to triton-inference-server/server#8870
Summary
InstanceQueue::Dequeuecan merge multiple queued payloads into one payload. Each payload was already counted as claiming a waiting consumer when it was enqueued, butRateLimiter::DequeuePayloadonly increments the waiting-consumer count once for the single dequeue call. This leaves a permanent-(k-1)drift wheneverkextra payloads are merged.With prefetching disabled (
default_queue_policy.default_timeout_microseconds != 0), the dynamic batcher gates dispatch onWaitingConsumerCount() > 0, so the drift can progressively degrade dispatch and eventually block it until model reload.This change credits back one waiting-consumer count per merged payload inside
InstanceQueue::Dequeue, where the merge happens, so the credit lands on the same queue that was dequeued from.Why This Location
Doing the rebalance inside
InstanceQueue::Dequeuekeeps the accounting local to the merge and works for both paths:Dequeueruns onqueue_, whose count gates dispatch;Dequeueruns onspecific_queues_[instance], whose count gates dispatch.Testing
Added
src/test/instance_queue_test.cc, which reproduces the accounting drift across repeated merge-heavy dequeues and verifies thatWaitingConsumerCount()returns to the idle-instance count. Without the fix, the test ends with a negative count.