Skip to content

[Feature] gRPC Engine-Client Backend PR1: gRPC Engine Service & Standalone Worker Daemon - #250

Open
herotai214 wants to merge 4 commits into
vllm-project:mainfrom
herotai214:feat/grpc-engine-client-pr1
Open

[Feature] gRPC Engine-Client Backend PR1: gRPC Engine Service & Standalone Worker Daemon#250
herotai214 wants to merge 4 commits into
vllm-project:mainfrom
herotai214:feat/grpc-engine-client-pr1

Conversation

@herotai214

@herotai214 herotai214 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

PLEASE FILL IN THE PR DESCRIPTION HERE ENSURING ALL CHECKLIST ITEMS (AT THE BOTTOM) HAVE BEEN CONSIDERED.

[Feature] gRPC Engine-Client Backend PR1: gRPC Engine Service & Standalone Worker Daemon

PR1 of #249

Multi-PR plan and lines of change

Five PRs, each reviewable on its own. This PR is PR 1 only.

Status PR Lines of change What it is
This PR PR 1: Protobuf contract & Python VllmEngineServicer ~830 LOC core / ~1.9k with tests (Python + proto) proto/vllm_engine.proto, in-tree Servicer wrapping AsyncLLMEngine, stubs generated at pip install, mock CPU tests + optional GPU / differential harnesses. No Rust router client.
Next PR 2: Rust tonic client, BackendClient, e2e streaming ~750 LOC (Rust) tonic-build, GrpcEngineBackend, router tokenize + minijinja, OpenAI SSE out. First time POST /v1/chat/completions hits this Servicer.
Planned PR 3: Multi-GPU & DP / prefix-cache routing ~650 LOC (Rust/Python) Several Servicer endpoints, dp_rank routing, cache-aware dispatch.
Planned PR 4: Endpoint parity & cluster admin ~600 LOC (Rust) Live /v1/models, HTTP /reset_prefix_cache, and StartProfile / StopProfile once --profiler-config exists.
Planned PR 5: Differential CI & benches ~650 LOC (Rust/Python) Automated HTTP-vs-gRPC parity in CI, concurrency / long-context benches.

PR 1 measured vs main (ab7e453): +1894 / −6 across 12 files. Breakdown:

File +/- Kind
py_src/vllm_router/vllm_servicer.py +630 Servicer
proto/vllm_engine.proto +109 Contract
setup.py +92 Generate stubs at install
py_src/vllm_router/proto/__init__.py +10 Import generated stubs
pyproject.toml +10 / −1 grpcio, protobuf, vllm-servicer
.gitignore / MANIFEST.in / package __init__.py +31 / −4 Build / ignore generated *_pb2.py
py_test/test_vllm_servicer.py +467 CPU mock tests
py_test/e2e/test_differential_grpc_vs_http.py +384 GPU parity (not CI)
py_test/e2e/test_vllm_servicer_real_gpu.py +170 GPU smoke (not CI)

Generated vllm_engine_pb2.py / vllm_engine_pb2_grpc.py are not in that count (not checked in).


Summary

This PR adds an in-tree gRPC Engine Servicer for vLLM Router: a Python process that wraps vllm.AsyncLLMEngine and speaks Protobuf over HTTP/2.

It is only the engine side. The Rust router still talks HTTP/JSON to vllm serve. Nothing in this PR sends a request from the router to the Servicer, or streams tokens from the Servicer back through the router.

What landed

  • proto/vllm_engine.protopackage vllm.engine.v1, service VllmEngine
  • vllm_router.vllm_servicer.VllmEngineServicer — wraps AsyncLLMEngine (or a self-contained mock)
  • CLI: vllm-servicer / python -m vllm_router.vllm_servicer
  • Stubs (vllm_engine_pb2.py, vllm_engine_pb2_grpc.py) generated at pip install / wheel build, not checked in
  • CPU mock tests + optional GPU e2e / differential harnesses

Structure (what this PR is, and what it is not)

Target end state (PR 1 + PR 2):

  OpenAI client
       │  HTTP  POST /v1/chat/completions
       ▼
  ┌─────────────────┐     gRPC / HTTP2      ┌──────────────────────┐
  │  vLLM Router    │  ──────────────────►  │  VllmEngineServicer  │
  │  (Rust)         │  ◄──────────────────  │  (Python)            │
  └─────────────────┘     token chunks      └──────────┬───────────┘
       │                                               │
       │  OpenAI SSE                                   ▼
       ▼                                         AsyncLLMEngine
  OpenAI client

This PR implements only the right box. Router → Servicer and Servicer → Router are not done.

  THIS PR (done)                         NOT THIS PR (PR 2)

  test / VllmEngineStub                  OpenAI client
       │  gRPC                                │  HTTP
       ▼                                      ▼
  VllmEngineServicer                     vLLM Router          ← still HTTP
       │                                      │                   to vllm serve
       ▼                                      ✗  no gRPC client
  AsyncLLMEngine                         VllmEngineServicer   ← exists, unused
                                                      │         by the router
                                                      ✗  no path back
                                                         to the router
Direction Status in this PR
Test / grpcurl → Servicer → AsyncLLMEngine Done. Proto + Python Servicer + tests.
Router → Servicer (Rust tonic, tokenize, dispatch) Not done. PR 2.
Servicer → Router (chunks → OpenAI SSE on the public HTTP port) Not done. PR 2.

Public HTTP routes (/v1/chat/completions, …) are unchanged. They still hit stock HTTP backends.


Design

Today the router is an HTTP load balancer in front of vllm serve. Every token pays FastAPI + JSON SSE + HTTP/1.1 on the worker, then JSON parse again in Rust. PR 2 will replace that internal hop with the gRPC link above. PR 1 only proves the Servicer is a correct engine front.

Rules we locked:

  1. Own implementation, in-tree. We do not depend on vllm serve --grpc or smg-grpc-servicer.
  2. Token-first. Production path is prompt_token_idsvllm.TokensPrompt. The Servicer does not run the chat template. prompt_text is a fallback for tests and debugging.
  3. Cancel is HTTP/2 stream cancel, which calls engine.abort(request_id). There is no public OpenAI /v1/abort, so we do not add an Abort RPC in this PR.
  4. Name everything vLLM + Servicer. File vllm_servicer.py, class VllmEngineServicer, CLI vllm-servicer. Not “daemon”, not a generic EngineService.
  5. Only ship RPCs we can actually run. That is why profiler RPCs are not here.

What is in this PR

Piece Role
proto/vllm_engine.proto The contract
py_src/vllm_router/vllm_servicer.py VllmEngineServicer + MockAsyncEngine + CLI
setup.py generate_grpc_stubs() protoc at install; relative imports; grpcio/protobuf floors
vllm-servicer entry point Launch the process
py_test/test_vllm_servicer.py CPU / mock, no GPU
py_test/e2e/test_vllm_servicer_real_gpu.py Live GPU, skipped in CI
py_test/e2e/test_differential_grpc_vs_http.py Parity vs stock HTTP, skipped in CI

Missing vLLM and no --mock-engine → log + SystemExit. The mock is explicit (is_mock), not a silent fake vLLM.

Prefix caching defaults on (--no-enable-prefix-caching to turn it off).


Generated stubs: the two pb2 files

The .proto is the source of truth. Python bindings are not checked in. setup.py compiles them when you install the package.

  proto/vllm_engine.proto
           │
           │  pip install -e .   /   pip install .
           │  setup.py → generate_grpc_stubs()
           │  grpc_tools.protoc  --python_out  --grpc_python_out
           ▼
  py_src/vllm_router/proto/vllm_engine_pb2.py        # messages
  py_src/vllm_router/proto/vllm_engine_pb2_grpc.py   # stub + servicer base

grpcio-tools>=1.60.0 is in [build-system] requires, so a PEP 517 install gets a compiler. After protoc, setup.py patches the output:

  • make the _grpc import package-relative (from . import vllm_engine_pb2)
  • pin GRPC_GENERATED_VERSION = "1.60.0" to the runtime floor in pyproject.toml
  • soften the protobuf runtime version check so a slightly newer/older protobuf does not hard-crash import
  • drop an unused warnings import

Both files are in .gitignore. A fresh clone does not contain them.

What each file is for

File What it is Who uses it
vllm_engine_pb2.py Message classes: GenerateRequest, SamplingParams, GenerateStreamResponse, … Servicer and tests, to build/parse payloads
vllm_engine_pb2_grpc.py gRPC wiring for service VllmEngine See the three classes below

vllm_engine_pb2_grpc.py defines three classes. Only two matter:

Class Role We use it?
VllmEngineStub Stable client. channel.unary_stream(...) / unary_unary(...). Yes. Tests (and later the idea of a client). PR 2’s Rust side will use tonic, not this Python stub.
VllmEngineServicer Empty server base. Every method is UNIMPLEMENTED + NotImplementedError. Compiler output, not a TODO list. Yes, as a base. Our vllm_router.vllm_servicer.VllmEngineServicer subclasses it and overrides the five RPCs. The dummy methods never run.
VllmEngine Experimental static client. Marked in the generated file: This class is part of an EXPERIMENTAL API. Each method is a @staticmethod that calls grpc.experimental.unary_stream / unary_unary with a target string, instead of a Channel. No. Unused. Do not treat it as a PR 2 TODO. Do not call it.

add_VllmEngineServicer_to_server(...) is the registration helper. We call that in serve().

Import path after install:

from vllm_router.proto import vllm_engine_pb2, vllm_engine_pb2_grpc
# client:
stub = vllm_engine_pb2_grpc.VllmEngineStub(channel)
# server:
class VllmEngineServicer(vllm_engine_pb2_grpc.VllmEngineServicer): ...

If the two files are missing, vllm_router/proto/__init__.py raises a clear ImportError telling you to re-install.

When to install again

Re-run pip install -e . from the router repo root when:

  1. First checkout / new venv — stubs are not in git.
  2. You edited proto/vllm_engine.proto — old pb2 files will not match the new RPC/field numbers.
  3. ImportError: gRPC stubs are missing — generate step did not run (install skipped setup.py, or grpcio-tools was absent).
  4. You deleted the generated files (git clean, wiped py_src/vllm_router/proto/vllm_engine_pb2*.py).

You do not need to reinstall after editing vllm_servicer.py or tests. Those import the already-generated modules.

Supported command:

pip install -e .

pip install -e . is the path reviewers and CI should use. A one-off from setup import generate_grpc_stubs also writes the files (as a side effect of importing setup.py) but is not the supported workflow.


Intentionally not in this PR

Left out Why Comes back
Rust tonic client, BackendClient, grpc:// workers This PR has no router dispatch PR 2
POST /v1/chat/completions → gRPC Public HTTP already exists; mapping is the next milestone PR 2
Rust tokenizer + minijinja chat template Router finishes template/MM, then sends token IDs PR 2
GetTokenizer Only needed for air-gap tokenizer fetch. HF id + local cache is enough for PR 2 later, if needed
Multi-GPU / dp_rank routing / prefix-cache aware dispatch Fields exist; out-of-range dp_rank is rejected PR 3
P/D ExecutionMode (PREFILL_ONLY / DECODE_ONLY) Reserved; rejected with UNIMPLEMENTED later P/D work
Multimodal payloads Reserved; rejected with UNIMPLEMENTED later
StartProfile / StopProfile vLLM profiler is created only with --profiler-config, which this CLI does not expose. Shipping the RPCs now would be success=False on a default launch PR 4 (with HTTP /start_profile /stop_profile broadcast + a real trace test)
HTTP /v1/models live probe, cluster admin fan-out Router HTTP surface PR 4
Abort RPC Stream cancel already calls engine.abort only if we need admin kill-by-id
Embeddings, KV-event subscribe, SHM/RDMA MM, KV-transfer params SMG production surface, not our milestone not planned for PR 2

PR 2 milestone: client → router HTTP → Rust gRPC → this Servicer → tokens back as OpenAI SSE.


The proto: what is in it, and why

package vllm.engine.v1;

service VllmEngine {
  rpc GenerateStream (GenerateRequest) returns (stream GenerateStreamResponse);
  rpc Generate (GenerateRequest) returns (GenerateResponse);
  rpc GetModelInfo (ModelInfoRequest) returns (ModelInfoResponse);
  rpc HealthCheck (HealthCheckRequest) returns (HealthCheckResponse);
  rpc ResetPrefixCache (EmptyRequest) returns (AdminResponse);
}

Wire path: /vllm.engine.v1.VllmEngine/GenerateStream.

package vllm.engine.v1 is a protobuf namespace, not a Python import of vLLM.

Five RPCs

GenerateStream (core). Token-first generate. Each chunk can carry optional uint32 token_id, text_delta, is_finished, finish_reason, and WorkerMetrics (running / waiting / KV %). optional matters: token id 0 is a real token, not “unset”.

Generate (unary). Same request, one complete response (output_token_ids, output_text, finish_reason). Useful for tests and non-streaming callers. Same request_id space as the stream.

GetModelInfo. Model name, max_model_len, dp_size, block_size. PR 2 uses this for discovery. stop_tokens is reserved; we do not invent a stop list here.

HealthCheck. SERVING / NOT_SERVING. Also register grpc.health.v1 when grpcio-health-checking is installed.

ResetPrefixCache. Forwards to engine.reset_prefix_cache(). Prefix caching is on by default, and a live GPU run already returned success=True. This is a real admin call, not a stub.

Request / response choices

Field Why it looks like this
prompt_token_ids Primary path. Router (PR 2) finishes template + tokenize, Servicer does not.
prompt_text Fallback. Engine tokenizes. Not the same as SMG TokenizedInput.original_text (IDs plus a string for logging/renderer).
SamplingParams Mapped onto vllm.SamplingParams (temperature, top_p, top_k, max_tokens, stops, penalties, seed, ignore_eos).
optional seed Presence vs “seed 0”.
WorkerMetrics on every chunk Avoid a second GetLoads RPC on the hot path. Best-effort; counters if the engine has no stats.
optional token_id Multi-token steps emit one chunk per new id so speculative/multi-step does not drop tokens.
dp_rank Accepted only if < dp_size. PR 1 does not implement DP routing.
ExecutionMode / MultimodalItem In the schema so the number space is stable. PR 1 rejects non-NORMAL and any MM payload.

What the proto is not

It is not a clone of SMG’s proto. It is not vllm serve’s HTTP API. It is the smallest contract that lets a router send tokens in and get tokens + finish + light load signals out.


Diff vs SMG (smg-grpc-servicer / vllm serve --grpc)

SMG already runs this kind of architecture in production. We are not wrapping their package. We share the idea (Rust gateway, Python engine, Protobuf) and a few names (VllmEngine, VllmEngineServicer, vllm_engine.proto). The contract and the process are ours.

This PR (vllm-project/router) SMG (smg-grpc-servicer)
Lives where In-tree in the router repo Separate PyPI package; vllm serve --grpc is a convenience flag
Proto package vllm.engine.v1 vllm.grpc.engine
How you start it vllm-servicer / python -m vllm_router.vllm_servicer vllm serve --grpc or their servicer
Generate shape Two RPCs: GenerateStream + unary Generate One Generate + stream bool; response is oneof { chunk, complete }
Input prompt_token_ids + optional prompt_text oneof input { TokenizedInput tokenized; string text }
Extra generate fields Reserved MM / execution mode (rejected) KV-transfer params, richer MM, logprobs, cached-token counts
Admin / extra RPCs ResetPrefixCache only Embed, Abort, GetServerInfo, GetLoads, GetTokenizer, SubscribeKvEvents
Load signals Piggybacked on stream chunks Separate GetLoads
Cancel HTTP/2 cancel → engine.abort Explicit Abort RPC (list of ids)
Tokenizer fetch Not in proto; PR 2 uses HF id + cache GetTokenizer streams a zip of tokenizer files
Profiler Omitted until --profiler-config exists Not part of their current core 7 either (Generate / Embed / Health / Abort / GetModelInfo / GetServerInfo / GetTokenizer)

We stayed smaller on purpose: one repo, one process, five RPCs we can test, no PyPI pin on their servicer, no requirement to change vLLM.


Tests: what, why, expected behavior

CI runs pytest py_test/ -v --ignore=py_test/e2e. GPU tests never run on CPU.

A. py_test/test_vllm_servicer.py (CPU, mock)

In-process grpc.aio server + MockAsyncEngine. No GPU, no real weights.

Test Why Expected
test_health_check Readiness status == SERVING
test_get_model_info Discovery name / max_model_len / dp_size / block_size match fixture
test_generate_stream_prompt_token_ids Token-first path chunks with text (+ token ids when present), then is_finished
test_generate_stream_text_fallback prompt_text still works same stream shape
test_generate_unary Unary RPC full text + output token ids + finish
test_reset_prefix_cache Admin we actually support success == True
test_stream_cancellation Disconnect == abort cancel does not crash the server
test_unsupported_execution_mode_rejected Honest reserved fields PREFILL_ONLY / DECODE_ONLYUNIMPLEMENTED
test_unsupported_multimodal_rejected same any multimodal_dataUNIMPLEMENTED
test_invalid_dp_rank_rejected same dp_rank >= dp_sizeINVALID_ARGUMENT
test_multi_token_step_emission spec/multi-step every new token id is emitted, none dropped
test_stream_token_zero_preserved optional token_id token 0 is present, not treated as missing
test_prefix_caching_default_enabled CLI default on
test_prefix_caching_can_be_disabled CLI --no-enable-prefix-caching works
test_serve_without_vllm_exits_unless_mock no silent fake engine SystemExit without vLLM unless --mock-engine; skipped if vLLM is installed

Last run (env with vLLM installed): 14 passed, 1 skipped.

B. py_test/e2e/test_vllm_servicer_real_gpu.py (1 GPU, not CI)

@pytest.mark.e2e + @pytest.mark.skipif(not HAS_VLLM).

Spawns python -m vllm_router.vllm_servicer on a real model (default Qwen/Qwen3.5-4B). Client uses HuggingFace AutoTokenizer, sends prompt_token_ids, not text.

Expected: HealthCheck SERVING, GetModelInfo matches the model, GenerateStream produces a completion that contains "Paris" for the France prompt, ResetPrefixCache returns success=True.

This does not run chat-template / Rust tokenize. That is PR 2. It proves the Servicer’s token-id ingress into a live engine.

C. py_test/e2e/test_differential_grpc_vs_http.py (2 GPUs, not CI)

Same model, same sampling (temperature=0, fixed seed):

  • GPU 1: stock python -m vllm.entrypoints.openai.api_server (POST /v1/completions, text)
  • GPU 0: this Servicer (GenerateStream, pre-tokenized ids)

Three prompts (factual, arithmetic, code). Compare generated text, finish_reason, and token sequence.

Expected: byte-for-byte identical text, identical tokens, identical finish reason. That is the claim that the gRPC path is the same engine, not a different sampler.

Already observed on live H800s for those three cases.


How to run

From the router repo root, after pip install -e . (stubs are generated by setup.py).

CPU / CI (no GPU):

pytest py_test/test_vllm_servicer.py -v
# same ignore CI uses:
pytest py_test/ -v --ignore=py_test/e2e

Lint:

ruff check py_src/ py_test/
black --check py_src/vllm_router/vllm_servicer.py py_test/test_vllm_servicer.py py_test/e2e/

Regenerate stubs only (if you edited the proto without reinstalling):

VLLM_ROUTER_BUILD_NO_RUST=1 python -c "from setup import generate_grpc_stubs; generate_grpc_stubs()"

(import setup also runs setup(); the stubs are written as a side effect. pip install -e . is the supported path.)

Mock Servicer (no GPU):

vllm-servicer --model mock-model --mock-engine --host 127.0.0.1 --port 50051
# or:
python -m vllm_router.vllm_servicer --model mock-model --mock-engine --host 127.0.0.1 --port 50051

Real GPU Servicer:

vllm-servicer \
  --model Qwen/Qwen3.5-4B \
  --host 0.0.0.0 \
  --port 50055 \
  --gpu-memory-utilization 0.85

GPU e2e:

pytest py_test/e2e/test_vllm_servicer_real_gpu.py -v
# or:
python py_test/e2e/test_vllm_servicer_real_gpu.py

Differential (2 GPUs):

pytest py_test/e2e/test_differential_grpc_vs_http.py -v
# or:
python py_test/e2e/test_differential_grpc_vs_http.py

MODEL_PATH overrides the default checkpoint for the e2e tests.


Test plan

  • pytest py_test/test_vllm_servicer.py -v — 14 passed, 1 skipped when vLLM is installed
  • Real-GPU Servicer stream + ResetPrefixCache (prior H800 run; re-run after this proto trim if you want a fresh log)
  • Differential HTTP vs gRPC text/token/finish parity (prior H800 run)
  • Reviewers: CPU tests only; do not require GPU

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

herotai214 and others added 2 commits September 8, 2026 16:34
…Python worker daemon (PR 1)

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: herotai214 <herotai214@gmail.com>
…s gRPC worker daemon

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: herotai214 <herotai214@gmail.com>

@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: 95b7bef0a5

ℹ️ 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 py_src/vllm_router/worker_daemon.py Outdated
Stream generated tokens for incoming GenerateRequest over gRPC HTTP/2.
Supports pre-tokenized prompt_token_ids and cancellation propagation.
"""
from vllm import TokensPrompt

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove vLLM imports from the mock path

In the checked .buildkite/pipeline.yml Python Tests environment, the Rust image installs only .[dev], which does not include vLLM, but every mock generation imports vllm.TokensPrompt here before invoking MockAsyncEngine; _build_sampling_params and the mock generator also import vLLM classes. Consequently the newly added mock generation tests fail with ModuleNotFoundError and --mock-engine cannot provide the advertised GPU-free, vLLM-free mode. Use lightweight mock-native objects or otherwise avoid all vLLM imports when the mock engine is selected.

Useful? React with 👍 / 👎.

Comment thread pyproject.toml
Comment on lines +26 to +27
"grpcio>=1.60.0",
"protobuf>=5.0.0",

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Raise runtime dependency floors to match generated code

These constraints permit environments that the checked-in generated modules explicitly reject: engine_client_pb2_grpc.py:10-29 requires grpcio 1.83.1 or newer, while engine_client_pb2.py:13-15 validates against protobuf 7.35.1. An installation constrained to allowed grpcio 1.60–1.82 or an older protobuf runtime succeeds dependency resolution but then fails while importing vllm_router.proto, making the worker entry point unusable. Regenerate with the declared minimum toolchain or require the generated versions.

Useful? React with 👍 / 👎.

Comment on lines +440 to +444
engine_args_kwargs = {
"model": args.model,
"tensor_parallel_size": args.tensor_parallel_size,
"gpu_memory_utilization": args.gpu_memory_utilization,
"trust_remote_code": args.trust_remote_code,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Configure the engine with the requested DP size

When the daemon is launched with --dp-size N for N > 1, that value is only copied into the discovery response; it is absent from the AsyncEngineArgs configuration, so the engine retains its default data-parallel size while GetModelInfo advertises N replicas. Multi-DP deployments therefore start the wrong topology and expose misleading routing metadata.

Useful? React with 👍 / 👎.

Comment on lines +133 to +137
generator = self.engine.generate(
prompt=prompt,
sampling_params=sampling_params,
request_id=request_id,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject or implement unsupported generation controls

For requests with a nonzero dp_rank, PREFILL_ONLY/DECODE_ONLY execution mode, or multimodal_data, this call forwards only the text/tokens, sampling parameters, and request ID, silently treating the request as an ordinary text generation. That can send work to the wrong DP rank, perform a full decode for a prefill-only request, or return an answer that ignores image/audio input; these fields should be translated into engine inputs or rejected instead of being accepted and discarded.

Useful? React with 👍 / 👎.

Comment thread py_src/vllm_router/worker_daemon.py Outdated
Comment on lines +159 to +162
delta_token_id = 0
if len(current_token_ids) > prev_token_count:
delta_token_id = current_token_ids[-1]
prev_token_count = len(current_token_ids)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Emit every newly produced token ID

If a RequestOutput adds more than one token since the previous yield, as can occur with batched, multi-step, or speculative output, this code emits only current_token_ids[-1] while text_delta contains the text for all newly accepted tokens. The streaming token sequence and unary output_token_ids then lose tokens despite the output text being complete; process the entire slice beginning at prev_token_count rather than retaining only its last element.

Useful? React with 👍 / 👎.

Comment on lines +58 to +59
self._running_requests: int = 0
self._waiting_requests: int = 0

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Populate waiting-request metrics from the engine

_waiting_requests is initialized to zero and never updated anywhere in the daemon, so every streamed WorkerMetrics claims the queue is empty even while requests are waiting inside vLLM. A router using this newly exposed field for load balancing will continue directing traffic toward an overloaded worker; derive it from actual engine statistics or omit the unsupported metric.

Useful? React with 👍 / 👎.

Comment on lines +323 to +324
text_match = http_text == grpc_text
finish_match = http_finish == grpc_finish

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Actually compare token sequences in the differential test

The differential harness advertises token-by-token parity, but its verdict checks only decoded text and finish reason; grpc_tokens is never compared with any HTTP-derived token sequence. A regression that drops or changes gRPC token IDs—while preserving the decoded text—therefore still reports complete parity, so the test should obtain or reconstruct the HTTP token IDs and assert equality.

Useful? React with 👍 / 👎.

…ize, and validations

- Decouple mock path from vLLM imports so CPU unit tests run in zero-GPU / zero-vLLM CI environments
- Emit all token IDs across multi-token steps (multi-step / speculative decoding) to prevent dropped tokens
- Forward requested dp_size to AsyncEngineArgs if supported, or log operational guidance
- Validate dp_rank, execution_mode, and multimodal_data in GenerateStream with proper gRPC status codes
- Dynamically track waiting requests and query engine statistics for worker metrics
- Verify token sequence equivalence against tokenizer in differential parity test harness
- Relax generated protobuf/grpcio runtime version check to match pyproject.toml constraints

Co-authored-by: Cursor <cursoragent@cursor.com>
Signed-off-by: herotai214 <herotai214@gmail.com>

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.

🟡 Changes recommended

There are correctness-breaking issues (generated stub/runtime version constraints vs declared dependencies, unary request_id mismatch, and an API contract ambiguity around token_id==0) that can cause runtime failures or incorrect semantics.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

This PR introduces a new Python-side gRPC “worker daemon” backend for vllm-router, along with the protobuf service contract, generated Python stubs, and verification harnesses (mock-unit, real-GPU, and HTTP-vs-gRPC differential parity).

Changes:

  • Add EngineService protobuf contract (proto/engine_client.proto) plus generated Python gRPC/protobuf stubs under py_src/vllm_router/proto/.
  • Add a standalone grpc.aio worker daemon (py_src/vllm_router/worker_daemon.py) that wraps vllm.AsyncLLMEngine (or a mock engine).
  • Add test harnesses and wiring: unit tests, real-GPU integration script, differential parity script, and new Python deps/script entrypoint.
File summaries
File Description
scripts/test_worker_daemon_real_gpu.py Real-GPU end-to-end smoke test for the worker daemon.
scripts/differential_test_grpc_vs_http.py Differential parity harness comparing stock HTTP server vs gRPC daemon.
pyproject.toml Adds gRPC/protobuf deps and a vllm-worker-daemon entrypoint.
py_test/test_worker_daemon_grpc.py Pytest suite for gRPC RPCs using the mock engine + in-process server.
py_src/vllm_router/worker_daemon.py Implements the gRPC service, mock engine, CLI, and server lifecycle.
py_src/vllm_router/proto/engine_client_pb2.py Generated protobuf message classes.
py_src/vllm_router/proto/engine_client_pb2_grpc.py Generated gRPC stub/servicer scaffolding with runtime version check.
py_src/vllm_router/proto/init.py Proto package init and exports.
py_src/vllm_router/init.py Makes Router export conditional when Rust extension isn’t available.
proto/engine_client.proto Defines the gRPC EngineService API and message schemas.
examples/simulate_consistent_hash.rs Minor ring iteration tweak in the example.
Review details

Files not reviewed (1)

  • py_src/vllm_router/proto/engine_client_pb2.py: Generated file

Suppressed comments (2)

py_src/vllm_router/worker_daemon.py:210

  • Generate() generates its own request_id when the incoming request omits one, but GenerateStream() separately generates a (different) ID. This can cause the unary response request_id to disagree with the ID actually used for engine generation and streamed chunks.
        """Unary generation returning full output in a single response."""
        accumulated_text = []
        accumulated_token_ids = []
        finish_reason = ""
        request_id = request.request_id or f"req-{uuid.uuid4().hex[:12]}"

        async for chunk in self.GenerateStream(request, context):
            if chunk.text_delta:
                accumulated_text.append(chunk.text_delta)
            if chunk.token_id > 0:
                accumulated_token_ids.append(chunk.token_id)
            if chunk.is_finished:
                finish_reason = chunk.finish_reason

        return engine_client_pb2.GenerateResponse(
            request_id=request_id,

py_src/vllm_router/worker_daemon.py:350

  • --enable-prefix-caching is declared with action="store_true" and default=True, which makes it impossible to disable (the value is always True regardless of whether the flag is passed). If the intent is to make prefix caching configurable, this needs a --disable-prefix-caching (store_true) or a paired enable/disable flag setup.
    parser.add_argument(
        "--enable-prefix-caching",
        action="store_true",
        default=True,
        help="Enable automatic prefix caching",
    )
  • Files reviewed: 10/11 changed files
  • Comments generated: 5
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pyproject.toml
Comment on lines 25 to 28
"requests>=2.25.0",
"grpcio>=1.60.0",
"protobuf>=5.0.0",
]
Comment thread py_src/vllm_router/proto/__init__.py Outdated
Comment on lines +1 to +11
import os
import sys

_proto_dir = os.path.dirname(os.path.abspath(__file__))
if _proto_dir not in sys.path:
sys.path.insert(0, _proto_dir)

from . import engine_client_pb2 as engine_client_pb2 # noqa: E402
from . import engine_client_pb2_grpc as engine_client_pb2_grpc # noqa: E402

__all__ = ["engine_client_pb2", "engine_client_pb2_grpc"]
Comment thread py_src/vllm_router/worker_daemon.py Outdated
Comment on lines +155 to +163
# Compute deltas
text_delta = current_text[len(prev_text) :]
prev_text = current_text

delta_token_id = 0
if len(current_token_ids) > prev_token_count:
delta_token_id = current_token_ids[-1]
prev_token_count = len(current_token_ids)

Comment on lines +160 to +165
async for chunk in stream:
if chunk.text_delta:
text_chunks.append(chunk.text_delta)
if chunk.token_id > 0:
token_ids.append(chunk.token_id)
if chunk.is_finished:
Comment thread pyproject.toml
Comment on lines 31 to 36
dev = [
"pytest>=7.0.0",
"pytest-asyncio>=0.21.0",
"pytest-cov>=4.0.0",
"grpcio-tools>=1.60.0",
]
Signed-off-by: herotai214 <herotai214@gmail.com>
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.

2 participants