[Bugfix]: Fix health checker to preserve worker load during checks - #216
[Bugfix]: Fix health checker to preserve worker load during checks#216wuhang2014 wants to merge 4 commits into
Conversation
…llm-project#197) The registry health checker unconditionally reset every worker's load counter every 10 health-check cycles, even with requests in flight. Since the cache-aware prefix tree was not reset, the policy kept routing by prefix affinity to saturated workers, creating hot spots. - Remove the periodic load reset from WorkerRegistry::start_health_checker (health checks now run in parallel) - Reset load only when a worker transitions unhealthy -> healthy, where remaining load is drift from the down period (no new requests are routed to unhealthy workers) - Delete the unused standalone start_health_checker with its conditional low-load reset (zero callers) - Redesign WorkerLoadGuard as an owning RAII guard with share()/release() and adopt it in the OpenAI router proxy path, fixing a double-decrement on retryable responses and guaranteeing paired accounting on every path - Clamp decrement at zero with a warning and wire the vllm_router_worker_load gauge on increment/decrement/reset Adds unit tests for guard semantics and registry tests proving the health checker preserves in-flight load and resets only on recovery. Signed-off-by: WU Hang <whlbx@hotmail.com>
There was a problem hiding this comment.
Pull request overview
Fixes a load-accounting bug where periodic health checks reset worker load counters while requests were in flight, which could mislead cache-aware routing and create hot-spot feedback loops. The PR decouples health checking from load accounting and hardens load tracking via an owning RAII guard.
Changes:
- Removes periodic load resets from the registry health checker; runs health checks in parallel and only resets load on unhealthy → healthy recovery.
- Redesigns
WorkerLoadGuardinto an owning, shareable RAII guard to ensure exactly-once decrement across success/failure/retry/stream paths, and clamps load decrements at zero with metrics updates. - Updates the HTTP router streaming/non-streaming request paths to use
WorkerLoadGuardinstead of manual increment/decrement logic.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/routers/http/router.rs | Switches cache-aware load accounting to WorkerLoadGuard, including streaming lifecycle handling. |
| src/core/worker.rs | Implements new owning WorkerLoadGuard, clamps decrements at zero, and updates load metrics wiring. |
| src/core/worker_registry.rs | Removes periodic load resets; parallelizes health checks; resets load only on recovery; adds regression tests. |
| src/core/mod.rs | Removes re-export of deleted standalone start_health_checker and updates public exports accordingly. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7314e6214b
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
…ssion test Address review findings on the vllm-project#197 fix: P1: resetting the load counter on an unhealthy->healthy transition can erase real in-flight load. A failed /health probe does not cancel requests routed before the failure, and between set_healthy(true) and the reset a newly routed request's increment can be wiped, or an older guard's decrement can consume a newer request's count. The health checker now never mutates load accounting; WorkerLoadGuard keeps increments and decrements paired on every request path. P2: the regression test only observed ~3 health-check cycles and could not catch the previous 10-cycle periodic reset. It now runs under paused Tokio time with a no-I/O health check and advances through 12 cycles; verified to fail against the pre-fix implementation (load reset to 0) and pass with the fix. Adds a recovery test asserting load is preserved across an unhealthy->healthy transition. Adds tokio's test-util feature as a dev-dependency for paused-time testing. Signed-off-by: WU Hang <whlbx@hotmail.com>
The only usage is the fully-qualified futures::future::join_all, which resolves via the edition extern prelude, so the use statement is unnecessary. Signed-off-by: WU Hang <whlbx@hotmail.com>
Address follow-up review findings on the vllm-project#197 fix: P1: the P/D router tracked prefill and decode load with manual increments and decrements. Cancelling the request task (e.g. client disconnect dropping the handler future) between the increment and an explicit decrement branch leaked the load permanently, since health checks no longer mask it. Both phases now use WorkerLoadGuard: the prefill guard is released at the transition to decode, the decode guard at phase completion, and cancellation releases either guard via Drop. Adds cancellation tests that run the real two-stage request flow against mock prefill/decode servers, abort the request while the upstream response is pending, and assert load returns to zero for both phases. Verified these tests fail against the previous manual accounting (load left at 1) and pass with the fix. P3: reset_load is no longer called by the runtime; update the trait and BasicWorker docs to describe it as an administrative operation that is only safe when the caller has established that no requests are in flight. Signed-off-by: WU Hang <whlbx@hotmail.com>
|
@ywang96 @zhuohan123 PTAL |
Purpose
Fixes #197: the runtime health checker unconditionally reset every worker's load counter to zero every 10 health-check cycles, even while requests were in flight. The cache-aware prefix tree was never reset, so the policy saw all workers as balanced and kept routing by prefix affinity to already-saturated workers, creating a positive feedback loop of hot spots.
Changes:
WorkerRegistry::start_health_checker(); health checks now run in parallel and never mutate load accounting. Load counters are neither reset periodically nor reset on health transitions: a failed/healthprobe does not cancel in-flight requests, so the load a worker reports at recovery time can still be real work.start_health_checker()with its conditional low-load reset (zero callers).WorkerLoadGuardas an owning RAII guard withshare()/release()and adopt it in the OpenAI router proxy path. This also fixes a pre-existing double-decrement on retryable responses and guarantees paired increment/decrement on every path (success, failure, retry, stream abort, client disconnect).WorkerLoadGuardfor both phases of the P/D two-stage flow (process_vllm_two_stage_request), fixing a permanent load leak when a P/D request task is cancelled (e.g. client disconnect) between the manual increment and a decrement branch. Cancellation now releases the prefill/decode load via guard drop; covered by cancellation tests for both phases.vllm_router_worker_loadgauge on increment/decrement/reset.Changes since first review
reset_load()call that the first review round flagged: it could erase real in-flight load (a failed/healthprobe does not cancel requests) and raced with new routing. The health checker now never mutates load accounting;reset_loadis documented as an administrative operation that is only safe when the caller has established that no requests are in flight.left: 0, right: 5).left: 1, right: 0) and pass with the guard-based fix.Test Plan
New tests:
src/core/worker.rs:WorkerLoadGuardunit tests (drop decrements, release idempotence, shared handle decrements once, multi-worker, decrement-at-zero clamps).src/core/worker_registry.rs:test_health_checker_preserves_inflight_load(regression test for [Bug]: Periodic health checks reset active worker load counters and cause cache-aware hot spots #197; runs under paused Tokio time with a no-I/O health check and advances through 12 cycles, so it fails against the pre-fix 10-cycle reset) andtest_health_checker_preserves_load_across_recovery.src/routers/http/vllm_pd_router.rs:test_pd_request_cancellation_releases_prefill_loadandtest_pd_request_cancellation_releases_decode_load— run the real two-stage flow against mock prefill/decode servers, abort the request while the upstream response is pending, and assert load returns to zero (verified to fail against the previous manual accounting).Test Result
All Rust unit/integration tests pass:
cargo test --lib --bins(494 passed),cargo test --test '*'(all suites passed),cargo fmt --checkand CI-grade clippy clean, all pre-commit hooks pass.End-to-end verification with 3 mock workers (requests held in flight, 1s health-check interval) comparing router-reported load vs actual in-flight requests:
Resetting worker loads (cycle 10); router viewmax_load=0, min_load=0max_load=4, min_load=3(≈ actual in-flight)max_load=0..1,is_imbalanced=false-> prefix-affinity routing continuesmax_load=4..5tracking actual in-flight; balancing decisions see true loadMutation probes: restoring the old 10-cycle reset makes the regression test fail (
left: 0, right: 5); restoring the manual P/D accounting makes both cancellation tests fail (left: 1, right: 0).Essential Elements of an Effective PR Description Checklist