Skip to content

[Bugfix]: Fix health checker to preserve worker load during checks - #216

Open
wuhang2014 wants to merge 4 commits into
vllm-project:mainfrom
wuhang2014:fix/197-health-check-load-reset
Open

[Bugfix]: Fix health checker to preserve worker load during checks#216
wuhang2014 wants to merge 4 commits into
vllm-project:mainfrom
wuhang2014:fix/197-health-check-load-reset

Conversation

@wuhang2014

@wuhang2014 wuhang2014 commented Aug 16, 2026

Copy link
Copy Markdown

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:

  • Remove the periodic load reset from 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 /health probe does not cancel in-flight requests, so the load a worker reports at recovery time can still be real work.
  • 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. 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).
  • Adopt WorkerLoadGuard for 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.
  • Clamp decrement at zero with a warning and wire the vllm_router_worker_load gauge on increment/decrement/reset.

Changes since first review

  • Removed the unhealthy-to-healthy reset_load() call that the first review round flagged: it could erase real in-flight load (a failed /health probe does not cancel requests) and raced with new routing. The health checker now never mutates load accounting; reset_load is documented as an administrative operation that is only safe when the caller has established that no requests are in flight.
  • Strengthened the [Bug]: Periodic health checks reset active worker load counters and cause cache-aware hot spots #197 regression test: it now runs under paused Tokio time with a no-I/O health check and advances through 12 health-check cycles, so it fails against the pre-fix 10-cycle reset (verified by mutation probe: left: 0, right: 5).
  • Added P/D cancellation tests for both phases (see Test Plan); verified they fail against the previous manual accounting (left: 1, right: 0) and pass with the guard-based fix.

Test Plan

cargo fmt --check
cargo clippy --all-targets --all-features -- -D warnings
cargo test --lib --bins
cargo test --test '*'
pre-commit run --files src/core/mod.rs src/core/worker.rs src/core/worker_registry.rs \
  src/routers/http/router.rs src/routers/http/vllm_pd_router.rs Cargo.toml

New tests:

  • src/core/worker.rs: WorkerLoadGuard unit 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) and test_health_checker_preserves_load_across_recovery.
  • src/routers/http/vllm_pd_router.rs: test_pd_request_cancellation_releases_prefill_load and test_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 --check and 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:

Event Before After
10 requests in flight, 10 health-check cycles elapsed Resetting worker loads (cycle 10); router view max_load=0, min_load=0 No reset lines; max_load=4, min_load=3 (≈ actual in-flight)
Probes while 13-15 requests in flight Router view max_load=0..1, is_imbalanced=false -> prefix-affinity routing continues max_load=4..5 tracking actual in-flight; balancing decisions see true load

Mutation 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
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results

…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>
Copilot AI lite review requested due to automatic review settings August 16, 2026 08:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 WorkerLoadGuard into 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 WorkerLoadGuard instead 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.

Comment thread src/core/worker_registry.rs

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread src/core/worker_registry.rs Outdated
…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>
@hsliuustc0106

Copy link
Copy Markdown

@ywang96 @zhuohan123 PTAL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Periodic health checks reset active worker load counters and cause cache-aware hot spots

3 participants