fix(vllm): classify disagg decode-engine queued requests correctly in FPM - #8471
Conversation
… FPM `InstrumentedScheduler._compute_queued` only iterated `self.waiting` and classified everything non-PREEMPTED as queued prefill. In disaggregated serving, decode-engine requests awaiting KV transfer from the prefill engine live in `self.skipped_waiting` with status `WAITING_FOR_REMOTE_KVS` (see `Scheduler._enqueue_waiting_request` / `_is_blocked_waiting_status`), so they were silently dropped from the FPM snapshot — the planner saw zero decode pressure on a loaded decode worker and would scale the wrong pool. Iterate `self.skipped_waiting` too: `WAITING_FOR_REMOTE_KVS` requests count as queued decode (their `num_computed_tokens` is the transferred KV length, pre-set in `Scheduler.schedule` at the `load_kv_async` branch). `WAITING_FOR_FSM` / `WAITING_FOR_STREAMING_REQ` have no KV computed yet and remain queued prefill. Adds 10 unit tests in `test_vllm_instrumented_scheduler.py` covering the waiting-queue regression paths and every blocked-waiting status, plus a realistic mixed decode-engine snapshot. Tests invoke the real `_compute_queued` against a stub `self` to catch drift without spinning up full vLLM engine state. Signed-off-by: Hongkuan Zhou <hongkuanz@nvidia.com> Signed-off-by: hongkuanz <hongkuanz@nvidia.com>
WalkthroughThe Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
components/src/dynamo/vllm/instrumented_scheduler.py (1)
486-501: LGTM — fix correctly classifies both queues.The two-loop structure sharing a single Welford accumulator per bucket matches the PR intent:
WAITING_FOR_REMOTE_KVSrequests on the decode engine are now counted as queued decode usingnum_computed_tokens(pre-set byScheduler.scheduleat theload_kv_asyncbranch), whileWAITING_FOR_FSM/WAITING_FOR_STREAMING_REQstay as queued prefill. Variance is correctly aggregated across both queues.One minor defensive consideration: the
elsebranches on lines 489 and 498 fall through to prefill for any unhandled status, so if vLLM adds a new blocked-waiting status in a future version that should count as decode (or something else), it would be silently misclassified. Given the docstring enumerates the known statuses, an explicit allowlist check with alogger.debugfor unknown statuses would make future vLLM upgrades safer to audit — but the current behavior is reasonable and the docstring already notes the rationale. Optional.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@components/src/dynamo/vllm/instrumented_scheduler.py` around lines 486 - 501, The loops iterating over self.waiting and self.skipped_waiting currently treat any non-PREEMPTED / non-WAITING_FOR_REMOTE_KVS status as prefill, which could silently misclassify future statuses; update the logic in instrumented_scheduler.py (the loops that inspect RequestStatus for requests in self.waiting and self.skipped_waiting) to use an explicit allowlist of known statuses (e.g., RequestStatus.PREEMPTED, RequestStatus.WAITING_FOR_REMOTE_KVS, RequestStatus.WAITING_FOR_FSM, RequestStatus.WAITING_FOR_STREAMING_REQ) and handle them as before, and add a logger.debug (or logger.warning) branch that logs unknown/other RequestStatus values so upgrades to vLLM that add new statuses are visible and can be audited.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@components/src/dynamo/vllm/instrumented_scheduler.py`:
- Around line 486-501: The loops iterating over self.waiting and
self.skipped_waiting currently treat any non-PREEMPTED /
non-WAITING_FOR_REMOTE_KVS status as prefill, which could silently misclassify
future statuses; update the logic in instrumented_scheduler.py (the loops that
inspect RequestStatus for requests in self.waiting and self.skipped_waiting) to
use an explicit allowlist of known statuses (e.g., RequestStatus.PREEMPTED,
RequestStatus.WAITING_FOR_REMOTE_KVS, RequestStatus.WAITING_FOR_FSM,
RequestStatus.WAITING_FOR_STREAMING_REQ) and handle them as before, and add a
logger.debug (or logger.warning) branch that logs unknown/other RequestStatus
values so upgrades to vLLM that add new statuses are visible and can be audited.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 4e4f5c71-da0a-4060-b09a-68202c54b594
📒 Files selected for processing (2)
components/src/dynamo/vllm/instrumented_scheduler.pycomponents/src/dynamo/vllm/tests/test_vllm_instrumented_scheduler.py
Summary
InstrumentedScheduler._compute_queuedonly iteratedself.waitingand classified everything non-PREEMPTEDas queued prefill. Under disaggregated serving, decode-engine requests awaiting KV transfer from a prefill engine live inself.skipped_waitingwith statusWAITING_FOR_REMOTE_KVS(see vLLM'sScheduler._enqueue_waiting_request/_is_blocked_waiting_status). They were silently missed from the FPM snapshot, so the planner saw zero decode pressure on a loaded decode worker and would scale the wrong pool.Root cause
Two-queue model in vLLM's scheduler:
self.waiting—WAITING(new) +PREEMPTED(evicted decode).self.skipped_waiting— blocked-waiting states, perScheduler._is_blocked_waiting_status:WAITING_FOR_FSM— structured-output FSM compileWAITING_FOR_REMOTE_KVS— disagg decode-engine KV transferWAITING_FOR_STREAMING_REQ— streaming request handshakePrevious
_compute_queuedonly scannedself.waiting. EveryWAITING_FOR_REMOTE_KVSrequest was invisible to the FPM even though it represents a decode-ready workload.Fix
Iterate
self.skipped_waitingalongsideself.waiting:WAITING_FOR_REMOTE_KVS→ queued decode withnum_computed_tokensas the KV length.Scheduler.schedulepre-setsnum_computed_tokensto the transferred KV count at theload_kv_asyncbranch (schedule()line ~715 in vLLM 0.18.0), so that field is the correct decode-KV-context value.WAITING_FOR_FSM/WAITING_FOR_STREAMING_REQ→ queued prefill; no KV computed yet.Variance accumulation spans both queues (one Welford accumulator per prefill / decode bucket).
Verified against vLLM source
RequestStatusenum (vLLM 0.18.0):WAITING,WAITING_FOR_FSM,WAITING_FOR_REMOTE_KVS,WAITING_FOR_STREAMING_REQ,RUNNING,PREEMPTED,FINISHED_*.Scheduler._is_blocked_waiting_statusconfirms the 3 blocked-waiting states route toself.skipped_waitingvia_enqueue_waiting_request.Scheduler.scheduleatload_kv_async: setsrequest.num_computed_tokens = num_computed_tokensbefore parking inskipped_waiting(comment: "When the transfer is finished, either successfully or not, request.num_computed_tokens will correctly reflect the number of computed tokens.").Tests
Adds
components/src/dynamo/vllm/tests/test_vllm_instrumented_scheduler.py— 10 cases covering:self.waitingregression paths: new prefill, preempted decode.self.skipped_waitingclassification:WAITING_FOR_REMOTE_KVS→ decode,WAITING_FOR_FSM/WAITING_FOR_STREAMING_REQ→ prefill.Tests invoke the real
_compute_queuedagainst a stubself(viaobject.__new__) so any future drift in the production function body surfaces directly, without spinning up full vLLM engine state.Pre-fix: 7/10 fail (every test exercising
self.skipped_waiting).Post-fix: 10/10 pass.
Test plan
pytest components/src/dynamo/vllm/tests/test_vllm_instrumented_scheduler.py -v— 10 passedpre-commit run --files <changed files>— isort / black / ruff / trim-whitespace cleanFollow-ups (out of scope)
executor_request_queueare counted asqueued_num_prefill. Fix will need to inspectdisagg_params/ generation-only markers on the request. Flagged in the PR review thread, not fixed here.🤖 Generated with Claude Code
Summary by CodeRabbit
Bug Fixes
Tests