fix: reserve completion-queue slot at enqueue to restore preserve_ordering - #520
Conversation
…ering b06d69b (fix: Response Cache Memory Leak, #449) moved the completion-queue slot reservation out of DelegateResponse() and into the response delegator. Slots are therefore created when a response completes rather than when the request is received, so completion_queue_ is ordered by completion instead of by arrival. FinalizeResponses() stalls on an empty front slot to hold later responses back; with every slot filled the moment it is created that guard can never trigger, and preserve_ordering silently became a no-op. Observed with two model instances at 100ms and 400ms delay and 12 requests batched 4+4+4: the third batch (fast instance) had its responses sent ~200ms before the second batch (slow instance), even though its requests reached the scheduler later. model_config.proto specifies ordering against requests 'received by the scheduler', so ordering must hold across instances of one dynamic batcher. Reserve the slot in DelegateResponse() again, but only when preserve_ordering_ is set. The leak b06d69b fixed came from reserving unconditionally: with only the response cache enabled the delegator sends directly and never fills the slot, so it is never popped. Guarding the reservation keeps that path allocation-free and restores ordering. Caught by qa/L0_batcher test_multi_batch_preserve_ordering, which has not run in CI since server 838966ae0 (2024-12-16) left TEST_WINDOWS unset on Linux. Reproduced on 10 GPU platforms across RHEL and Ubuntu.
Greptile SummaryThis PR moves preserve-ordering completion-slot reservation from response completion to DelegateResponse. The change restores ordering across concurrently completing model instances, but reservation still occurs too late to preserve scheduler arrival order when cache hits bypass queued misses.
Confidence Score: 4/5This PR should not merge until completion slots are reserved at a scheduler-wide point that preserves arrival order across both cache-hit and queued-request paths. An earlier cache miss can enter the dynamic-batching queue without a slot while a later cache hit reserves one immediately, leaving the completion queue ordered differently from scheduler arrival order. Files Needing Attention: src/dynamic_batch_scheduler.cc Important Files Changed
Sequence DiagramsequenceDiagram
participant R1 as Earlier cache miss
participant E as Enqueue
participant B as BatcherThread
participant Q as completion_queue
participant R2 as Later cache hit
R1->>E: Arrives first
E->>B: Queue request (no slot reserved)
R2->>E: Arrives later and hits cache
E->>Q: Reserve slot 0 for R2
B->>Q: Later reserve slot 1 for R1
Note over Q: Queue order is R2, R1 rather than arrival order R1, R2
Reviews (1): Last reviewed commit: "fix: reserve completion-queue slot at en..." | Re-trigger Greptile |
| if (preserve_ordering_) { | ||
| std::lock_guard<std::mutex> lock(completion_queue_mtx_); | ||
| completion_queue_.emplace_back(); | ||
| queue_slot = &completion_queue_.back(); | ||
| } |
There was a problem hiding this comment.
Slot reservation breaks arrival order
When an earlier cache miss remains in the dynamic-batching queue and a later request hits the cache, the hit reserves its completion slot immediately while the miss reserves only after BatcherThread dequeues it, causing the later response to occupy an earlier slot and violating scheduler-wide preserve_ordering.
|
Thanks — the mechanism described is accurate, and worth recording. Two points on scope. The interleaving gap is pre-existing, not introduced here.
Those same three sites, at the same lines and in the same functions, exist in the So a later cache hit reserving ahead of an earlier queued miss was possible before What each state actually does:
The regression this PR targets is the middle row: with slots created at completion Holding this fix until the cache-hit path is also addressed would mean shipping I will open a separate issue for the cache-hit/queued-miss ordering gap and link it Validation status: unchanged from the description — CI is in flight and this is |
Problem
preserve_orderingin the dynamic batcher does not preserve ordering. Responsesare returned in completion order, not in the order requests reached the
scheduler.
model_config.protois explicit that the guarantee is scheduler-wide:Since one dynamic batcher feeds all instances of a model, ordering must hold
across instances. It currently does not.
Root cause
b06d69b("fix: Response Cache Memory Leak", #449) moved the completion-queueslot reservation out of
DelegateResponse()and into the response delegator:FinalizeResponses()drains with:while (!completion_queue_.empty() && !completion_queue_.front().empty()) {The
!front().empty()test is the ordering mechanism: it stalls the drain whilethe head slot is reserved but unfilled. Once slots are created already-filled,
front()is never empty, the guard becomes dead code, and the drain simplyre-emits in completion order.
Evidence
Two instances of one model (100 ms and 400 ms delay), 12 requests batched 4+4+4,
preserve_ordering: trueconfirmed in the server log. From the trace:Requests 9-12 reached the scheduler after 5-8 but were answered ~200 ms earlier.
INFER_RESPONSE_COMPLETEis captured in the frontend response callback(
server/src/http_server.cc), so it reflects response-send time.qa/L0_batcher/verify_timestamps.py -prequires at least 4 responses after theslow batch and measures 0.
Fix
Reserve the slot in
DelegateResponse()again, but only whenpreserve_ordering_is set.The leak
b06d69bfixed came from reserving unconditionally: with only theresponse cache enabled, the delegator takes the
elsebranch and sendsdirectly, so the slot was never filled and never popped, growing
completion_queue_for the lifetime of the model. Guarding the reservationkeeps that path allocation-free and restores ordering.
std::dequedoes not invalidate references to existing elements on insertion ateither end, and a slot is only
pop_fronted after itsFINALflag is seen,after which the delegator cannot fire again -- the invariant the original code
relied on.
Test coverage
qa/L0_batchertest_multi_batch_preserve_orderingcovers this. It has not runin CI since server
838966ae0(2024-12-16), which gated it onTEST_WINDOWSwithout initialising the variable on Linux, so
[ $TEST_WINDOWS -eq 0 ]erroredand took the else branch. With that gate repaired, the test fails deterministically
on 10 GPU platforms (A100, A30, H100, B200, GB200, GB300, RTX50, DGX-Spark,
AGX-Thor, IGX-Orin) across RHEL and Ubuntu, in plain,
_shmand_cudashmvariants -- 23 CI jobs. It also reproduced on a same-job retry.
Validation status
clang-format(v16.0.5) andcodespellpasssurrounding invariants.
L0_batchertest_multi_batch_preserve_orderinggoing green is the real proof and still needs a CI run.