fix(credit): wait for worker registration instead of raising in StickyCreditRouter - #23
Conversation
…yCreditRouter
Phase-start credit dispatch can outrun worker registration, and when it does
the whole run hangs forever with no request ever reaching the server.
Sequence:
1. Workers announce readiness asynchronously over ZMQ. Measured skew between
the first credit dispatch and the first `WorkerReady` is 0.4-0.9s, widening
with concurrency (more workers to register).
2. `AgenticReplayTiming._execute_warmup` dispatches the phase-start burst with
sequential `await`s.
3. `CreditIssuer._issue_credit_internal` calls `increment_sent()` *before*
`send_credit()`, so a failed send is still counted as sent.
4. `StickyCreditRouter.send_credit` raises
`RuntimeError("No workers available for routing")` when `_workers` is empty,
which aborts the sequential loop — the remaining credits are never attempted.
5. The exception is swallowed at the task boundary and in-flight credits have no
timeout, so the run stalls permanently.
Observed symptom: `sent=1 returned=0 in_flight=1 errors=0` with zero server-side
evidence (gateway/router counters 0, engine running 0, a fresh curl to the same
endpoint returns 200). With the default 6h `--request-timeout-seconds` this is
effectively a deadlock.
Fix: when no worker has registered yet, wait for registration (60s cap, 50ms
poll) instead of raising. The raise is kept as the timeout path.
Verified on a 1P1D vLLM PD-disaggregated stack (MiniMax-M2.5, 145K context,
`--scenario inferencex-agentx-mvp`, `--public-dataset
semianalysis_cc_traces_weka_062126_256k`):
| concurrency | stack | before | after |
|-------------|-------------|-------------------------------|--------------------------------|
| 8 | warm | hang, `sent=1 returned=0` 2/2 | pass, 82 warmup reqs, 0 errors |
| 16 | warm | hang, `sent=1/13` | 19/19 credits routed |
| 16 | fresh stack | - | pass, 110 routed, 90 warmup |
| 8 | fresh stack | hang | pass |
The race triggers on every fresh stack (logged wait 0.55-0.85s), so it is
structural rather than incidental.
Try out this PRQuick install: pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@ad434d116e3d3435de28afd1c10303c308be94feRecommended with virtual environment (using uv): uv venv --python 3.12 && source .venv/bin/activate
uv pip install --upgrade --force-reinstall git+https://github.com/ai-dynamo/aiperf.git@ad434d116e3d3435de28afd1c10303c308be94feLast updated for commit: |
|
The failing From the job log: The title itself validates — Adding this to the jobs:
conventional-commits:
permissions:
pull-requests: writeSeparately: only two checks have run on this PR ( For context on the change itself: we hit this in a multi-turn agentic replay where the phase-start credit burst outran worker registration (0.4–0.9s skew). |
Phase-start credit dispatch can outrun worker registration. When it does, the run hangs forever and no request ever reaches the server.
The race
WorkerReadyis 0.4–0.9s, widening with concurrency (more workers to register).AgenticReplayTiming._execute_warmupdispatches the phase-start burst with sequentialawaits.CreditIssuer._issue_credit_internalcallsincrement_sent()beforesend_credit(), so a failed send is still counted as sent.StickyCreditRouter.send_creditraisesRuntimeError("No workers available for routing")when_workersis empty. That aborts the sequential loop — the remaining credits are never attempted.Instrumented evidence:
Symptom
sent=1 returned=0 in_flight=1 errors=0, with zero server-side evidence — gateway/router counters 0, enginenum_requests_running0, KV-transfer counters 0, and a freshcurlto the same endpoint returns 200 while the run is stuck. Because the request never left the client, there is nothing to find on the server, which makes this very hard to diagnose. With the default 6-hour--request-timeout-secondsit is effectively a deadlock.Fix
When no worker has registered yet, wait for registration (60s cap, 50ms poll) instead of raising. The raise is kept as the timeout path, so a genuinely worker-less run still fails loudly.
Verification
1P1D vLLM PD-disaggregated stack, MiniMax-M2.5, 145K context,
--scenario inferencex-agentx-mvp --public-dataset semianalysis_cc_traces_weka_062126_256k, on208125ac:sent=1 returned=0(2/2 runs)sent=1/13in_flight=11The race triggers on every fresh stack (logged wait 0.55–0.85s) and the patch absorbs it each time, so it is structural rather than incidental.
Two related defects found while debugging (not fixed here)
Happy to send separate PRs if you want them addressed:
ReplayBarrierCoordinator._releases_pausedis never reset toFalseoutside__init__.pause_releases()is called from_finish_accelerated_warmup, andcomplete()early-returns while paused — so deferred releases can be stranded once the barrier is active.ROUTER_MANDATORYis never set on the credit ROUTER socket, so unroutable messages are dropped silently instead of surfacing an error. Setting it would have turned this hang into an immediate, obvious failure.