Repository navigation
[Rust Frontend][gRPC] Separate liveness from inference readiness - #63870
Conversation
|
Documentation preview: https://vllm--63870.org.readthedocs.build/en/63870/ |
|
This pull request has merge conflicts that must be resolved before it can be |
|
This pull request has merge conflicts that must be resolved before it can be |
Move `call_utility`'s fan-out unchanged from `EngineCoreClient` to `ClientInner`, which now keeps the engine IDs and the client index; `EngineCoreClient::call_utility` delegates. The next commit runs the fan-out in a task that owns the inner client. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCUEjbFWgsUABtnUGD Signed-off-by: Bugen Zhao <i@bugenzhao.com>
…ncelled Dropping a `call_utility` future unregistered its waiters, but requests already handed to the send tasks on the client runtime were still sent, and the engines executed them. Cancellation only abandoned the bookkeeping, so callers could not tie state to a call's completion. Prepare and encode the requests in the caller, then send them and collect every reply in one task on the client runtime; the caller awaits that task and decodes the replies. The task sends directly instead of through `send_to_engine`, which would spawn again per request. Dropping the future now only stops waiting. This removes `UtilityCallGuard`; the task unregisters the waiters of requests that failed to send. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCUEjbFWgsUABtnUGD Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Local rebase of vllm-project#63870 onto vllm-project#64265: the PR's net change as of 758a88f, without its two workarounds for cancelled control calls. The utility call fan-out on `ClientInner` now comes from vllm-project#64265. Co-authored-by: Bugen Zhao <i@bugenzhao.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCUEjbFWgsUABtnUGD Signed-off-by: Julien Darve <jdarve@NVIDIA.com> Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Pass a control call's `RecheckGuard` into `ClientInner::call_utility`, whose task (vllm-project#64265) holds it until the engines reply. A caller cancelled after sending a pause, for example by a client disconnecting from `/pause`, then keeps readiness withdrawn until the engines have handled it instead of rechecking at once, which could overtake the pause and publish ready after it took effect. The guard now owns an `Arc<Readiness>`, and `start_weight_update` passes its withdrawal into the `collective_rpc` it runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCUEjbFWgsUABtnUGD Signed-off-by: Bugen Zhao <i@bugenzhao.com>
21feaf4 to
c7c67d8
Compare
|
Thanks for taking this on. Having the engine report readiness, and holding the guard until the engines reply, reads well, and clearing the flag only after the draft LM head restore fixes a real bug. On the open question: we'd like to keep both. On shutdown: the DEP requires publishing inference |
On shutdown, the gRPC listener and the health monitor both followed the shutdown token, so the listener could stop, or with `--shutdown-timeout 0` the server could be aborted, before `readiness` and `vllm.Inference` reported `NOT_SERVING`. Watchers then saw only a closed connection. `HealthMonitor::serve` now runs the gRPC server: the signal that stops the listener, and a forced abort, both wait until every service except `liveness` reports `NOT_SERVING`. `lib.rs` and the gRPC test server share this wiring. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCUEjbFWgsUABtnUGD Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Resolve conflicts after vllm-project#64265 was squash-merged to main: - gpu_worker.py: keep main's finish_weight_update (try/finally around the engine finish, _end_weight_update, KV cache loan release, direct reload handling) and clear the incomplete-weight mark only after _end_weight_update(refresh=True) returns, so a failed finish or a failed draft head restore still reports incomplete weights. - Test helpers: initialize both _incomplete_weight_updates and main's KV cache loan fields. - Rust engine-core-client: keep the readiness guard parameter on call_utility and is_inference_ready on top of the squashed vllm-project#64265; take main's test_utils import list. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CJckCCukQgyfESDciCFndy Signed-off-by: Bugen Zhao <i@bugenzhao.com>
|
This pull request has merge conflicts that must be resolved before it can be |
|
/ci run |
|
✅ Triggered Buildkite CI #99166 for commit |
|
/ci run |
|
✅ CI is already running for this commit: https://buildkite.com/vllm/ci/builds/99166 |
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
|
/ci run |
|
✅ Triggered Buildkite CI #99198 for commit |
Update: stacked on #64265, readiness reported by the engines
Stacked on #64265 (
[Rust Frontend] Run utility calls to completion when the caller is cancelled). The base branchbz/rust-uncancellable-utilitymirrors #64265's head; this PR will be retargeted tomainonce #64265 merges. Its three commits:This revision keeps the gRPC service mapping and changes how the frontend learns the engine state. The original revision inferred it in the client from the results of the control calls it sent (
client/availability.rs: per-call facts,resolve, epochs, and a confirm loop askingis_sleepingplus a worker RPC). EngineCore already owns that state, so it now reports it:EngineCore.is_inference_ready()(Python, 8 lines): not paused (including for a checkpoint) or asleep, and no worker reportshas_incomplete_weight_update().client/readiness.rs) withdraws readiness before a call that stops serving is sent (pause, sleep, checkpoint prepare, start weight update), and rechecks after every control call ends by asking the enginesis_inference_ready. An epoch discards answers that predate a call, so readiness becomesSERVINGonly from an engine answer. The publiccollective_rpcalso rechecks, which covers weight updates and the dev-mode/collective_rpcroute.call_utility, whose task (from [Rust Frontend] Run utility calls to completion when the caller is cancelled #64265) holds it until the engines reply. A caller cancelled after sending a pause, for example by a client disconnecting from/pause, therefore cannot recheck before the engines have handled the pause.--checkpoint-prepare-on-startup), and an engine without the utility fails startup with an error naming it.EngineHealthgroups the client's liveness and readiness watches, andHealthMonitorregisters the services before the listener accepts connections and then follows the watches.HealthMonitor::serveruns the gRPC server, and both the signal that stops its listener and the forced abort (immediate with the default--shutdown-timeout 0) wait until every service exceptlivenessreportsNOT_SERVING. AHealth/Watchsubscriber therefore receives the withdrawal before its connection closes. ACheckon a new connection after shutdown starts getsNOT_SERVING, an unknown service, or a refused connection; probes treat all three as not ready.lib.rsand the gRPC test server share this wiring.finish_weight_updateclears the worker's incomplete-update flag only after the draft model's quantized LM heads are restored, so a failed restore keeps readiness withdrawn.The service mapping is the same as in the original revision:
NOT_SERVINGwhenlivenessreadinessandvllm.Inferencevllm.Controland""readinessalways equalsvllm.Inference; both are kept. Kubernetes probes and the Dynamo sidecar uselivenessandreadiness(ai-dynamo/dynamo#14897), whilevllm.Inferenceandvllm.Controlremain the per-service statuses.Validation of this revision:
cargo nextest run -p vllm-server -p vllm-engine-core-client -p vllm-mock-engine: 701 passed.cargo clippy --workspace --tests --examples -- -D warnings,cargo fmt --check, andpre-commitpassed.readinessandvllm.Inference, orderly shutdown closes thelivenesswatch withoutNOT_SERVING, engine failure fails all five services); a worker test where a failed draft finish keeps the weights incomplete;HealthMonitortests where the listener stop and a forced abort both find readiness already withdrawn (300 consecutive runs). Each fails when its fix is reverted.pytest tests/v1/worker/test_gpu_worker_weight_transfer.py: passes; one existing test needs a visible GPU and was not run.livenessreceivedNOT_SERVINGwithin 3 ms and then closed.Live runs of c7c67d8 on one GB200 GPU with
Qwen/Qwen3-0.6B,--enable-sleep-mode --weight-transfer-config '{"backend":"ipc"}', andVLLM_SERVER_DEV_MODE=1. Control steps use thevllm.ControlRPCs, except the HTTP checkpoint and draft routes. Each check opens a new connection, as kubelet does, andHealth/Watchstreams on the services record every transition.readiness,vllm.Inferenceliveness,vllm.Control,""PauseGenerationinabort,wait, andkeepmodesResumeGenerationafter each, then a completionSleep(level=1), thenWakeUp(tags=["weights"])WakeUp, then a completionkeeppause held 180 s, checked every 10 slivenessSERVING throughoutinstance_idPOST /start_draft_weight_updatewithout a draft model (the workers refuse), then a completionStartWeightUpdateFinishWeightUpdate, then a completionStartWeightUpdate, thenUpdateWeightswithupdate_info_jsonset to{}(fails)FinishWeightUpdateafter the failed load (fails)StartWeightUpdateandFinishWeightUpdate, then a completionPOST /checkpoint/prepare, until/checkpoint/statusreturns 200POST /checkpoint/restore, then a completion--checkpoint-prepare-on-startupPOST /checkpoint/restore, then a completionPauseGeneration(wait)during a 3000-token request, with a 0.5 s deadline that cancels the call; checked after the 6 s drainIsPausedtrueResumeGeneration, then a completionPOST /collective_rpcwithstart_weight_updatePOST /collective_rpcwithfinish_weight_update, then a completionlivenesswatch closes without NOT_SERVING; listener closed 4 ms laterkill -9of the EngineCore processlivenesswatch closes without NOT_SERVING;vllm-rsexits with an errorcancelled_pause_rechecks_only_after_the_engine_replies.kill -9: the engine failure path setslivenesslast, and the server aborts the gRPC listener at the same time, so thelivenesswatch closed before receiving NOT_SERVING. This is the race described under the original revision'skill -9row; probes treat a closed connection as a failure.AI assistance (Claude) was used for this revision.
Original description (previous revision)
Overview
The Rust frontend's gRPC health service reports the same status for every service name, so Kubernetes and sidecars cannot tell an engine paused for an RL weight update from one that needs a restart. This adds two health services:
liveness, which fails only when the engine fails, andreadiness, which also fails while the engine is paused (including for a process checkpoint) or asleep, or while a weight update has not finished successfully.vllm.Inferencenow reports the same status asreadiness;vllm.Control,"", and the protobufs are unchanged. Motivation: ai-dynamo/dynamo#14897.Size: about 325 lines of non-test Rust and 8 lines of Python (+336/−34, not counting tests, comments, blank lines, reindentation, or the 39 lines moved unchanged onto
ClientInner). The logic is four pieces:client/availability.rs, about 170 lines:is_sleepingand the workershas_incomplete_weight_update, asks again if another control call made the answer stale, and retries a failed attempt every second.client.rsandclient/imp.rs, about 90 lines:ClientInnerkeeps the engine IDs and client index, so the loop can call the engines;subscribe_availability, the loop's task, and oneis_sleepingcall at connect for sleep-mode engines, which may start prepared for a checkpoint.grpc/health.rsandlib.rs, about 70 lines:liveness, and registerreadinessandvllm.Inferencewith the current availability;readinessandvllm.Inferencefrom the tracker;has_incomplete_weight_update()ingpu_worker.pyandworker_base.py, 8 lines: tracked per target (base or draft model), set once an update starts, and cleared by that target's successful finish. Because of the second target, a successful finish asks the workers rather than assuming the weights are complete.Moving the existing
call_utilityandcollective_rpcfan-out ontoClientInnerchanges no behavior. The rest of the +1119/−162 diff is:docs/deployment/k8s.mdandrust/README.md);Claims
livenessreportsNOT_SERVINGonly after an engine failure. Pause, sleep, checkpoint prepare, weight updates, and orderly shutdown do not affect it until the listener closes.readinessandvllm.InferencereportNOT_SERVINGin these cases:--checkpoint-prepare-on-startupis not yet restored;--shutdown-timeout 0, the gRPC server is aborted without a grace period, so a Watch can see its connection close before it receivesNOT_SERVING; probes treat both as not ready.liveness, readiness usesreadiness, as documented indocs/deployment/k8s.md. A liveness probe that namesvllm.Inferencenow fails while the engine stays paused, so it should move toliveness. Likewise, a client that waits forvllm.Inferenceto be SERVING before it connects now waits while the engine is paused. Dynamo's vLLM sidecar does this at startup (lib/sidecar/vllm/src/engine.rs). To connect to a paused engine, wait onvllm.Control, which reports SERVING while paused in every version; earlier versions do not haveliveness.Validation
Live runs on 1x RTX 6000 Ada with this branch, polling
grpc.health.v1.Health/Checkevery 0.5 s on a new connection, as kubelet does:The weight-update rows add
--weight-transfer-config '{"backend":"ipc"}'andVLLM_SERVER_DEV_MODE=1. The failed load sendsUpdateWeightswithupdate_info_jsonset to{}. The last checkpoint rows add--checkpoint-prepare-on-startup.readiness,vllm.Inferenceliveness,vllm.Control,""PauseGenerationinabort,wait, andkeepmodesResumeGenerationafter each, then a completionSleep(level=1), thenWakeUp(tags=["weights"])WakeUp, then a completionkeeppause held 180 s, checked every 10 slivenessSERVING throughoutinstance_idPOST /start_draft_weight_updatewithout a draft model (the workers refuse), then a completionStartWeightUpdateFinishWeightUpdate, then a completionStartWeightUpdate, then a failedUpdateWeightsFinishWeightUpdateafter the failed load (fails)StartWeightUpdateandFinishWeightUpdate, then a completionPOST /checkpoint/prepare, until/checkpoint/statusreturns 200POST /checkpoint/restore, then a completion--checkpoint-prepare-on-startupPOST /checkpoint/restore, then a completionlivenesswatch closes without NOT_SERVING; listener closed by the next check, under 0.4 s laterkill -9of the EngineCore processvllm-rsexits with an errorProvenance:
kill -9rows ran on 5073411. They also cover the head, d42905b, which changes only tests and docs.readinessNOT_SERVING.kill -9row is timing-dependent on both sides of this change: the watch received NOT_SERVING before the connection dropped in 5 of 8 runs on a build of this head without its last two fixes (the finish confirmation and the repeated-status filter), and in 5 of 7 runs on 8ae979f. When the managed engine exits, the server shuts down with no grace period, so aborting the gRPC server races the health monitor's update.VLLM_USE_FLASHINFER_SAMPLER=0only works around the test machine's CUDA 11.5nvcc.Unit tests:
cargo nextest run -p vllm-server -p vllm-engine-core-client -p vllm-mock-enginepassed (686, with a shortTMPDIRfor the two Unix-socket listener tests), as didcargo clippy --tests -- -D warnings,cargo fmt --check, andpre-commit.pytest tests/v1/worker/test_gpu_worker_weight_transfer.pypassed (17), andpytest tests/v1/worker/test_gpu_draft_lm_head.py -k draft_weight_updatepassed on the GPU (3).Not run:
No model evaluation: generated tokens are unaffected.
Details
The worker reports
has_incomplete_weight_updaterather than whether an update session is active because a failedupdate_weightsends the session without restoring the weights, so generation fails until a later update finishes.Known limitations:
/collective_rpcroute, are not observed.weights, reload, then wakekv_cache) keeps readiness down until the weights are back.readinessstaysNOT_SERVINGandlivenessstaysSERVING, so whatever drives the checkpoint must restart the pod when/checkpoint/statusreports the failure.The frontend infers engine state from the results of the calls it sends and asks the engines when a result is not conclusive. An engine-reported readiness state, as proposed in #60505, could replace these confirmations.
No open or merged PR splits Rust gRPC liveness from readiness (checked 2026-10-26). #60505 and #59179 add Python HTTP readiness endpoints; #56886 (draft) adds a gRPC shutdown grace period.
AI assistance (Claude) was used to write and review this change.
Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.
🤖 Generated with Claude Code