Conversation
|
Something non-obvious here is that certain consumer use-cases for this channel (Like if you wanted Edit: The stated use case (weight transfer metrics) is moot until vllm can serve metrics before/during engine initialization. |
92b6436 to
2a9c07f
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
95b2c23 to
b631219
Compare
b4421a3 to
f8b1bc0
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
a961a56 to
2f70598
Compare
2f70598 to
5a2fadd
Compare
yewentao256
left a comment
There was a problem hiding this comment.
Generally LGTM, thanks for the work!
Also want an approval from @njhill or @BugenZhao
|
✅ @wseaton, CI is now available for this PR.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Team Run ID: 📒 Files selected for processing (12)
🚧 Files skipped from review as they are similar to previous changes (12)
Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review. 📝 SummarySummary by CodeRabbit
WalkthroughAdds tagged custom notification types to the Rust and Python protocols, a bounded worker queue, rank gathering, polling, and frontend delivery. Engine outputs carry optional notifications through in-process and multiprocessing clients. Tests cover serialization, buffering, concurrency, polling, fan-out, and end-to-end delivery. ChangesEngine notification pipeline
Estimated code review effort: 4 (Complex) | ~45 minutes Merge Risk: ⚪ Minimal · up to This change adds a worker-to-frontend notification channel with optional polling disabled by default. No concrete merge-blocking risk is established for the current implementation. Sequence Diagram(s)sequenceDiagram
participant WorkerBase
participant Executor
participant EngineCore
participant EngineCoreProc
participant InprocClient
participant AsyncMPClient
WorkerBase->>Executor: take_notifications
Executor->>EngineCore: collective_rpc("take_notifications")
EngineCore->>EngineCore: gather_worker_notifications
EngineCore->>EngineCore: _flush_notifications
EngineCore->>InprocClient: EngineCoreOutputs(engine_notifications)
EngineCoreProc->>AsyncMPClient: enqueue EngineCoreOutputs(engine_notifications)
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
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 |
1e8a3c5 to
cd67406
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
Worker-side producers publish EngineNotification values that the model runner attaches to ModelRunnerOutput; EngineCore forwards them on EngineCoreOutputs.engine_notifications, broadcasting from EngineCoreProc so every API server sees the same events. EngineCoreOutputs is array_like and omit_defaults does not trim trailing fields, so the appended field lengthens every message. serde_tuple rejects a longer array than it knows, so the Rust wire struct has to move in the same commit. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
Build a local ModelRunnerOutput instead of mutating the shared EMPTY_MODEL_RUNNER_OUTPUT singleton, and add cover for the appended array_like field surviving a msgpack round-trip and for EngineCoreProc fanning one event out to every frontend. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
The fixture mirror lacked the new field, so the cross-language test only proved the tolerated direction: Python encoding 8 elements into a Rust struct that knows 9. Encoding all 9 is the direction this PR depends on. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
…ation take_worker_notifications returns None instead of building a list on every step when no producer is installed. Drop gc=False from CustomNotification, whose payload is plugin-supplied: a cycle through an untracked struct is never collected. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
Draining into ModelRunnerOutput only reached the executor's output_rank,
so producers on other ranks were discarded, and paths that return early
(non-last PP rank via with_kv_conn_output_only) never drained at all,
accumulating in the worker for the life of the process.
Gather with collective_rpc("take_notifications") instead, keeping every
rank's events. The gather is an rpc, so it runs between steps rather than
inside one: once before serving, on in-tree producers' own state changes,
and on VLLM_WORKER_NOTIFICATION_POLL_INTERVAL when set.
Assisted-by: Claude
Signed-off-by: Will Eaton <weaton@redhat.com>
Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
The DP override of run_busy_loop never called the startup gather or the interval poll, so notifications were never collected under data parallelism. Trim the gather docstrings and drop the docstring example's nonexistent StatLoggerBase kwarg while here. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
Pin the DP busy loop's startup gather and per-iteration poll (the wiring that previously shipped missing), and the poll's off-by-default and interval gating. Drop test_empty_drain_allocates_nothing; the roundtrip test already asserts the quiet drain returns None. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
A real engine proc and worker: publish_worker_notification runs in the worker via collective_rpc, the interval poll gathers it, and the frontend client reads it off EngineCoreOutputs alongside generation output. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
The idle input-queue wait now times out at the poll interval and gathers, so an event published while the engine is idle reaches the frontend without waiting for the next request; the knob now bounds notification latency in both states. The interval poll moves to post_step, which both the busy loops and InprocClient reach, closing the in-process gap (InprocClient also gathers load-time events at construction). The gather no longer swallows executor failures: MultiprocExecutor stops dequeuing at the first failed rank, so a swallowed error leaves stale rank replies queued for the next collective. The worker buffer is capped at 1024, dropping the oldest, and the payload contract (string keys, msgpack values) is documented. The e2e now proves idle delivery with no request in flight and bounds every blocking wait. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
InprocClient.get_output now flushes after post_step; step_fn flushes before the gather runs, so a notification gathered on the final step had no later flush and sat in _pending_notifications until a call that a request-less frontend never makes. The worker buffer becomes a deque under a lock: the bounded list's len-check-then-delete raced the drainer's swap, and a publisher could del from the freshly emptied list. deque(maxlen) drops the oldest without a second mutation. The e2e wait helper re-raises drain-thread exceptions so an engine failure surfaces as itself instead of a timeout. Assisted-by: Claude Signed-off-by: Will Eaton <weaton@redhat.com>
Co-authored-by: Wentao Ye <44945378+yewentao256@users.noreply.github.com> Signed-off-by: Will Eaton <weaton@redhat.com>
cd67406 to
90fd7ec
Compare
|
Note GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer. |
|
@njhill @BugenZhao bumping this one, trying to optimistically get it and #54830 in before the dev cut so we can ship some llm-d routing optimizations for LoRA |
First split of #45411. Notification channel only; LoRA events, the Rust frontend consumer, and the scheduler hook will follow as separate stacked PRs.
flowchart LR W0["Worker rank 0<br/>notification buffer"] --> G["EngineCore<br/>gather from every rank"] WN["Worker rank N<br/>notification buffer"] --> G G --> O["EngineCoreOutputs"] O --> F0["API server 0"] O --> FN["API server N"]vllm/v1/notifications.py: tagged msgspec union withCustomNotification(key, payload), and a process-local worker buffer.WorkerBase.take_notifications(): drains that buffer. One default implementation covers every worker type.EngineCore.gather_worker_notifications():collective_rpc("take_notifications"), keeping every rank.EngineCoreOutputs.engine_notifications: appended field, plus the matching RustWireEngineCoreOutputsfield andprotocol/notifications.rs.EngineCoreProcbroadcasts to all API servers so their/metricsagree.Why gather instead of piggybacking on
ModelRunnerOutput: the executor only reads one rank's reply, so anything the other ranks published would just get dropped, and some paths (non-last PP rank viawith_kv_conn_output_only) return early without a full output.The gather itself fires once when the serving loop starts, then only if you opt in with
VLLM_WORKER_NOTIFICATION_POLL_INTERVAL(off by default). This keeps the rpc off of the engine steps path.#54830 adds a doorbell on top of this (the output rank flags a non-empty buffer on
ModelRunnerOutput, and the engine core gathers after that step), so in-tree producers do not depend on polling.cargo test -p vllm-engine-core-client --lib: 101 passed.pytest tests/v1/engine/test_notifications.py: 13 passed.Stack
Each PR is based on
mainand includes the commits of the ones before it; review only the last commits of each.AI assistance disclosure
Developed code and the test harness with AI assistance (Claude Code). All changes were reviewed line-by-line by myself, who ran the tests and reviewed output.