Data contracts between the parts of vLLM
Code version: vLLM @ 3e44c0d · 2026-10-22
In one sentence
Each boundary in vLLM has a fixed message type: msgspec structs over ZMQ, dataclasses between scheduler and worker, pydantic models over HTTP.
The idea#
A request crosses two process boundaries in vllm serve, and more with several GPUs. At each boundary the sender turns a Python object into bytes. The receiver turns the bytes back into an object of a known type. So vLLM defines one type for each hop. We call these types the data contracts. If you know them, you can read any log line, debug any hop, and write a plugin that touches a hop.
There are three families:
- msgspec structs cross the ZMQ boundary between the API server and the engine core: EngineCoreRequest one way and EngineCoreOutputs the other way. They are encoded as msgpack.
- dataclasses stay inside the engine core, or go to the workers: SchedulerOutput and
ModelRunnerOutput. With one GPU they are passed by a plain call; with worker processes they are pickled into shared memory. - pydantic models describe the HTTP JSON: the OpenAI request and response objects.
Two more types never cross a boundary: the engine core's own Request, and the front end's RequestOutput. The page The process and thread architecture of vllm serve shows the processes and channels.
An everyday comparison. Think of the forms in international shipping. The shop fills in an order form, customs needs a fixed declaration form, and the receiver gets a delivery note. Each form has fixed fields in a fixed order, so every office can read it. The comparison stops here: in vLLM, the "declaration" (EngineCoreRequest) is written as a list without field names. So both sides must agree on the field order, not only on the names.
The picture#
Read the panels from top to bottom. Panel 2 reads from right to left. The values come from Tour: Life of a chat request: a 61-token prompt, 48 tokens from the prefix cache, and the first output token 785 ("The").
In the code#
Step 1: EngineCoreRequest, the message into the engine core#
class EngineCoreRequest(
msgspec.Struct,
array_like=True, # type: ignore[call-arg]
omit_defaults=True, # type: ignore[call-arg]
gc=False,
): # type: ignore[call-arg]
request_id: str
prompt_token_ids: list[int] | None
mm_features: list[MultiModalFeatureSpec] | None
sampling_params: SamplingParams | None
pooling_params: PoolingParams | None
arrival_time: float
lora_request: LoRARequest | None
cache_salt: str | None
data_parallel_rank: int | None
prompt_embeds: torch.Tensor | None = None
# Per-position mask for mixed-mode inputs (e.g chat completion with
# prompt_embeds content parts). `True` means the position is a real
# token ID; `False` means the position uses a pre-computed entry from
# `prompt_embeds`. `None` for pure-tokens and pure-embeds requests.
prompt_is_token_ids: list[bool] | None = None
# Index of the client, used to ensure outputs are sent back to the same
# client for this request when scaling out the front-end.
client_index: int = 0array_like=True: msgpack writes the fields as an array in this order, without names. So a new field must go at the end, as comments inEngineCoreOutputsay (vllm/v1/engine/__init__.py:231-235).omit_defaults=Truehas no effect on anarray_likestruct: msgspec still writes every field. The decoder accepts a shorter array and fills the missing last fields with their defaults.gc=False: the garbage collector does not track the object. (This is msgspec behaviour, not vLLM code; we checked it with msgspec 0.21.1 and 0.22.0.)- The input processor makes it (
vllm/v1/engine/input_processor.py:538-555). The client setsclient_indexjust before it sends (vllm/v1/engine/core_client.py:1333-1335).
The rest of the fields are optional: current_wave and priority, trace_headers, resumable, external_req_id, the reasoning fields, abort_immediately, session_id and kv_hints (vllm/v1/engine/__init__.py:143-165). SamplingParams and LoRARequest inside it are msgspec structs too (vllm/sampling_params.py:271-277, vllm/lora/request.py:8-11).
Step 2: Request, the engine core's own object#
The input thread of the engine core turns the struct into a Request (vllm/v1/engine/core.py:1128, vllm/v1/request.py:243-270). This object never leaves the engine core process. It holds the counters that the scheduler uses:
# Used in async scheduling.
self.num_output_placeholders = 0
# Tokens of output in flight when the request was preempted: delivered
# on return, but must not mutate the reset counters.
self.num_stale_output_tokens = 0
# Drop the stale output instead, for same-step preempt + resume
# (reset_prefix_cache).
self.drop_stale_output = False
# Tokens of steps whose output is not yet processed (async scheduling
# and PP run ahead of the GPU); `num_computed_tokens` counts them
# optimistically.
self.num_in_flight_tokens = 0
# V2+PP+async: Enforces `pp_size` cadence between same-request decode steps
# so the worker's broadcast slot ring stays consistent.
self.next_decode_eligible_step = 0
# Seq of the most recent step this request was scheduled in; fences
# deferred block freeing (see Scheduler._free_request_blocks).
self.last_sched_seq = 0
self.spec_token_ids: list[int] = []
self.num_computed_tokens = 0Three properties read the token lists: num_tokens (prompt plus output), num_tokens_with_spec (plus draft tokens) and num_output_tokens (vllm/v1/request.py:294-304). The status is an integer enum. Every status after PREEMPTED counts as finished:
class RequestStatus(enum.IntEnum):
"""Status of a request."""
WAITING = enum.auto()
WAITING_FOR_STRUCTURED_OUTPUT_GRAMMAR = enum.auto()
WAITING_FOR_REMOTE_KVS = enum.auto()
WAITING_FOR_STREAMING_REQ = enum.auto()
RUNNING = enum.auto()
PREEMPTED = enum.auto()
# Note: anything after PREEMPTED will be considered
# as a finished status.
FINISHED_STOPPED = enum.auto()
FINISHED_LENGTH_CAPPED = enum.auto()
FINISHED_ABORTED = enum.auto()
FINISHED_IGNORED = enum.auto()
FINISHED_ERROR = enum.auto()
FINISHED_REPETITION = enum.auto()
def __str__(self) -> str:
return self.name
@staticmethod
def is_finished(status: "RequestStatus") -> bool:
return status > RequestStatus.PREEMPTED
@staticmethod
def get_finished_reason(status: "RequestStatus") -> FinishReason | None:
return _FINISHED_REASON_MAP.get(status)A finished status maps to a FinishReason. FINISHED_IGNORED (a prompt that is too long) maps to LENGTH, like in the OpenAI API (vllm/v1/request.py:400-412).
Step 3: SchedulerOutput, the plan of one step#
The scheduler makes one SchedulerOutput per step (vllm/v1/core/sched/scheduler.py:1588-1609). The comments give the design: a new request travels in full once, and later steps send only the difference.
@dataclass
class SchedulerOutput:
# list of the requests that are scheduled for the first time.
# We cache the request's data in each worker process, so that we don't
# need to re-send it every scheduling step.
scheduled_new_reqs: list[NewRequestData]
# list of the requests that have been scheduled before.
# Since the request's data is already cached in the worker processes,
# we only send the diff to minimize the communication cost.
scheduled_cached_reqs: CachedRequestData
# req_id -> num_scheduled_tokens
# Number of tokens scheduled for each request.
num_scheduled_tokens: dict[str, int]
# Total number of tokens scheduled for all requests.
# Equal to sum(num_scheduled_tokens.values())
total_num_scheduled_tokens: int
# req_id -> spec_token_ids
# If a request does not have any spec decode tokens, it will not be
# included in the dictionary.
scheduled_spec_decode_tokens: dict[str, list[int]]
# req_id -> encoder input indices that need processing.
# E.g., if a request has [0, 1], it could mean the vision encoder needs
# to process that the request's 0-th and 1-th images in the current step.
scheduled_encoder_inputs: dict[str, list[int]]
# Request IDs that are finished in between the previous and the current
# steps. This is used to notify the workers about the finished requests
# so that they can free the cached states for those requests.
finished_req_ids: set[str]
# list of mm_hash strings associated with the encoder outputs to be
# freed from the encoder cache.
free_encoder_mm_hashes: list[str]NewRequestData carries the prompt, the sampling parameters, the block ids and the computed-token count (vllm/v1/core/sched/output.py:40-56). With Model Runner V2 it also carries prefill_token_ids: all token ids of the request (vllm/v1/core/sched/scheduler.py:1505-1516). CachedRequestData is column-wise: one list per field, indexed like req_ids (vllm/v1/core/sched/output.py:137-152). Its new_token_ids is used only with pipeline parallelism (line 144).
Step 4: ModelRunnerOutput, the result of one step#
# ModelRunnerOutput is serialized and sent to the scheduler process.
# This is expensive for torch.Tensor so prefer to use list instead.
@dataclass
class ModelRunnerOutput:
# [num_reqs]
req_ids: list[str]
# req_id -> index
req_id_to_index: dict[str, int]
# num_reqs x num_generated_tokens
# num_generated_tokens is the number of tokens
# generated in the current step. It can be different for
# each request due to speculative/jump decoding.
sampled_token_ids: list[list[int]] = field(default_factory=list)
# [num_reqs, max_num_logprobs + 1]
# [num_reqs, max_num_logprobs + 1]
# [num_reqs]
logprobs: LogprobsLists | None = None
# req_id -> (token_ids, logprobs, ranks)
# [prompt_len, num_prompt_logprobs]
# [prompt_len, num_prompt_logprobs]
# [prompt_len]
prompt_logprobs_dict: dict[str, LogprobsTensors | None] = field(
default_factory=dict
)
# req_id -> [num_scored_rows, num_token_ids] prompt_logprob_token_ids scores.
prompt_token_id_logprobs_dict: dict[str, torch.Tensor] = field(default_factory=dict)
# [num_reqs, hidden_size]
pooler_output: list[torch.Tensor | None] | None = NoneThe comment at the top says why the fields are lists: the object is sent to the scheduler process, and tensors are expensive to send. sampled_token_ids has one list per request, because spec decoding can accept several tokens in one step. Model Runner V2 does not return it directly. On the last pipeline stage, sample_tokens wraps it in an AsyncOutput, a subclass of AsyncModelRunnerOutput (vllm/v1/worker/gpu/model_runner.py:2209-2218, vllm/v1/worker/gpu/async_utils.py:115). Its get_output() waits for the GPU copy and then gives the ModelRunnerOutput (vllm/v1/outputs.py:353-363). Model Runner V2 fills req_id_to_index only for compatibility; the comment says that it does not use it (vllm/v1/worker/gpu/model_runner.py:2197-2199).
Step 5: EngineCoreOutput and EngineCoreOutputs, the message back#
class EngineCoreOutput(
msgspec.Struct,
array_like=True, # type: ignore[call-arg]
omit_defaults=True, # type: ignore[call-arg]
gc=False,
): # type: ignore[call-arg]
request_id: str
new_token_ids: list[int]
new_logprobs: LogprobsLists | None = None
new_prompt_logprobs_tensors: LogprobsTensors | None = None
pooling_output: torch.Tensor | None = None
finish_reason: FinishReason | None = None
stop_reason: int | str | None = None
events: list[EngineCoreEvent] | None = None
kv_transfer_params: dict[str, Any] | None = None
ec_transfer_params: dict[str, Any] | None = None
The output carries token ids, never text. The scheduler adds an EngineCoreOutput only when a request has new tokens, a pooling output, or has stopped (vllm/v1/core/sched/scheduler.py:2293-2295, vllm/v1/core/sched/scheduler.py:2356-2380). The outputs of one step are grouped by API server:
class EngineCoreOutputs(
msgspec.Struct,
array_like=True, # type: ignore[call-arg]
omit_defaults=True, # type: ignore[call-arg]
gc=False,
): # type: ignore[call-arg]
# NOTE(Nick): We could consider ways to make this more compact,
# e.g. columnwise layout
engine_index: int = 0
# [num_reqs]
outputs: list[EngineCoreOutput] = []
scheduler_stats: SchedulerStats | None = None
timestamp: float = 0.0
utility_output: UtilityOutput | None = None
finished_requests: set[str] | None = None
# In DP case, used to signal that the current wave of requests
# has finished and the engines are paused.
wave_complete: int | None = None
# In DP case, used to signal that a request was received for an
# "old" wave, so the next wave needs to be started in other engines.
start_wave: int | None = None
# Weight version used for this step.
weight_version: str | None = None
def __post_init__(self):
if self.timestamp == 0.0:
self.timestamp = time.monotonic()outputsholds one entry per request with news. The scheduler builds oneEngineCoreOutputsperclient_index(vllm/v1/core/sched/scheduler.py:2486-2491).scheduler_statsgoes to only one API server per step (vllm/v1/core/sched/scheduler.py:2516-2521).timestampistime.monotonic()of the engine core process, set when the object is made (lines 295-297).utility_outputcarries the reply to a utility call. Such a message has no request outputs.
Events and finish reasons are small integer enums, so the message stays short. EngineCoreEvent has a type (QUEUED, SCHEDULED, PREEMPTED) and a monotonic timestamp (vllm/v1/engine/__init__.py:176-200). FinishReason is STOP = 0 to REPETITION = 4, and str() gives the OpenAI string (vllm/v1/engine/__init__.py:37, vllm/v1/engine/__init__.py:49-70).
Step 6: RequestOutput and CompletionOutput, the result in the front end#
The output processor makes a RequestOutput per request and step (vllm/v1/engine/output_processor.py:459-473). It holds the external request id, the prompt, and one CompletionOutput per sequence:
index: int
text: str
token_ids: GenericSequence[int]
cumulative_logprob: float | None
logprobs: SampleLogprobs | None
routed_experts: np.ndarray | None = None # [seq_len,layer_num,topk]
finish_reason: str | None = None
stop_reason: int | str | None = None
lora_request: LoRARequest | None = None
sampling_mask: SamplingMask | None = None
spec_decode_metrics: RequestSpecDecodeMetrics | None = None
weight_versions: list[WeightVersionSpan] | None = Nonefinish_reason is now a string such as "stop" (vllm/v1/engine/output_processor.py:525). The other fields of RequestOutput are the prompt, the prompt token ids, the prompt logprobs, finished, metrics, num_cached_tokens and the KV transfer parameters (vllm/outputs.py:169-209).
Step 7: the OpenAI response objects#
The chat handler turns each RequestOutput into pydantic objects. For a stream, the chunk is a ChatCompletionStreamResponse with one ChatCompletionResponseStreamChoice per sequence; each choice has a DeltaMessage (vllm/entrypoints/openai/chat_completion/protocol.py:162-193, vllm/entrypoints/generate/base/protocol.py:356-360). Without a stream, the handler builds one ChatCompletionResponse with a ChatMessage per choice and a UsageInfo (vllm/entrypoints/openai/chat_completion/protocol.py:137-145, vllm/entrypoints/serve/engine/protocol.py:135-140). The fields stop_reason and token_ids are vLLM additions that are not in the OpenAI spec (vllm/entrypoints/openai/chat_completion/protocol.py:118-122).
Step 8: who makes what, and how it travels#
| Type | Kind | Made by (process) | Read by (process) | Travels as |
|---|---|---|---|---|
ChatCompletionRequest | pydantic | FastAPI, from the JSON body (API server) | OpenAIServingChat (API server) | HTTP JSON |
engine input (TokensInput) | TypedDict | renderer (API server) | InputProcessor (API server) | in-process |
EngineCoreRequest | msgspec struct | InputProcessor (API server) | input thread (engine core) | msgpack over ZMQ ROUTER → DEALER |
Request | class | input thread (engine core) | scheduler (engine core) | in-process, never sent |
SchedulerOutput | dataclass | scheduler (engine core) | model runner (worker) | plain call with the uni executor; pickle in shared memory with worker processes |
ModelRunnerOutput | dataclass | model runner (worker) | Scheduler.update_from_output (engine core) | plain call; or pickle through the response queue of one worker |
EngineCoreOutputs | msgspec struct | scheduler (engine core) | engine core client (API server) | msgpack over ZMQ PUSH → PULL |
RequestOutput | class | OutputProcessor (API server) | generate() and the chat handler (API server) | in-process, through the collector |
ChatCompletionStreamResponse | pydantic | chat handler (API server) | the HTTP client | SSE line data: {JSON} |
Sources for the transports: msgpack encoder and decoder of the client (vllm/v1/engine/core_client.py:731-732) and of the engine core (vllm/v1/engine/core.py:1930-1933, vllm/v1/engine/core.py:2035); the uni executor's direct call (vllm/v1/executor/uniproc_executor.py:102-117); the shared-memory queue's pickler (vllm/distributed/device_communicators/shm_broadcast.py:934-940).
Deep dive#
The sections below give the full detail. Open them one at a time, or use Open all deep dives at the top of the page.
Deep diveWorked example: the field values of one request at each stage
The example is the request of Tour 1. It has four chat messages, a ChatML template and 61 prompt tokens. The block size is 16, and 3 blocks (48 tokens) are already in the prefix cache. All values are illustrative.
| # | Stage | Field values |
|---|---|---|
| 1 | ChatCompletionRequest | messages: 4 entries; stream=True; max_tokens=None; n=1; request_id="3f9c...e1" from random_uuid (vllm/entrypoints/openai/chat_completion/protocol.py:403-410) |
| 2 | TokensInput | type="token"; prompt_token_ids: 61 ids; prompt: the template text; arrival_time=1761140000.12 (vllm/inputs/engine.py:18-40) |
| 3 | EngineCoreRequest | request_id="chatcmpl-3f9c...e1-a1b2c3d4"; external_req_id="chatcmpl-3f9c...e1"; prompt_token_ids: 61 ids; sampling_params.max_tokens=32707 (32768 − 61, vllm/v1/engine/input_processor.py:476-480); sampling_params.output_kind=DELTA; client_index=0; data_parallel_rank=None; priority=0 |
| 4 | Request, after add_request | status=WAITING; num_prompt_tokens=61; num_tokens=61; num_computed_tokens=0; num_output_placeholders=0; max_tokens=32707; block_hashes: 3 (61 // 16 full blocks); events=[QUEUED] |
| 5 | SchedulerOutput, step 1 | scheduled_new_reqs=[NewRequestData(num_computed_tokens=48, block_ids=([7, 3, 12, 40],), ...)]; scheduled_cached_reqs: empty; num_scheduled_tokens={id: 13}; total_num_scheduled_tokens=13; scheduled_spec_decode_tokens={}; finished_req_ids=set() |
| 6 | Request, after schedule() | status=RUNNING (vllm/v1/core/sched/scheduler.py:1438-1439); num_computed_tokens=61 (vllm/v1/core/sched/scheduler.py:1714-1716); is_prefill_chunk=False; num_output_placeholders=1 (the async scheduler adds one for the token in flight, vllm/v1/core/sched/async_scheduler.py:32-47); events=[QUEUED, SCHEDULED] |
| 7 | ModelRunnerOutput | req_ids=["chatcmpl-...-a1b2c3d4"]; req_id_to_index={id: 0}; sampled_token_ids=[[785]]; logprobs=None |
| 8 | Request, after update_from_output | output_token_ids=[785]; num_tokens=62; num_output_placeholders=0 (vllm/v1/core/sched/async_scheduler.py:71-73) |
| 9 | EngineCoreOutputs | engine_index=0; timestamp: monotonic; outputs=[EngineCoreOutput(new_token_ids=[785], finish_reason=None, events=[QUEUED, SCHEDULED], prefill_stats=PrefillStats(num_prompt_tokens=61, num_computed_tokens=13, num_cached_tokens=48, ...))]; scheduler_stats: set |
| 10 | RequestOutput | request_id="chatcmpl-3f9c...e1" (the external id); outputs=[CompletionOutput(index=0, text="The", token_ids=[785], finish_reason=None)]; finished=False; num_cached_tokens=48 |
| 11 | last EngineCoreOutput | new_token_ids=[<eos>]; finish_reason=FinishReason.STOP (0 on the wire); stop_reason=None for EOS |
| 12 | last chunk | finish_reason="stop"; delta.content="", because the detokenizer skips the EOS token (vllm/v1/engine/detokenizer.py:107-111) |
The PrefillStats in row 9 come from set() at the first schedule: 61 prompt tokens, 48 local cached, 0 external, so 13 computed (vllm/v1/core/sched/scheduler.py:1136-1143, vllm/v1/metrics/stats.py:327-340). The front end takes num_cached_tokens from them for RequestOutput (vllm/v1/engine/output_processor.py:790-800).
Deep diveThe three request ids
| Id | Example | Made at | Seen by |
|---|---|---|---|
| body or header id | 3f9c...e1 | the X-Request-Id header if it is present, else ChatCompletionRequest.request_id (vllm/entrypoints/serve/engine/serving.py:133-142) | the serving layer |
| external id | chatcmpl-3f9c...e1 | the chat handler adds the prefix (vllm/entrypoints/openai/chat_completion/serving.py:320-322) | the client (chunk id), RequestOutput.request_id, abort() |
| internal id | chatcmpl-3f9c...e1-a1b2c3d4 | assign_request_id adds 8 random characters (vllm/v1/engine/input_processor.py:363-371) | the engine core, the scheduler, the workers, the logs of the engine core |
The output processor keeps a map from external id to internal ids, so abort() works with both (vllm/v1/engine/output_processor.py:599-630). With n > 1, the children have the ids 0_<internal id>, 1_<internal id> and so on (vllm/v1/engine/parallel_sampling.py:94).
Deep diveHow msgspec and msgpack encode the structs
- The client encoder is
MsgpackEncoder.encode()returns a list of buffers: the msgpack message first, then the large arrays as extra frames (vllm/v1/serial_utils.py:165-175). - A NumPy array or a tensor smaller than
VLLM_MSGPACK_ZERO_COPY_THRESHOLD(256 bytes by default) goes inline. A larger one goes as its own frame, with no copy (vllm/v1/serial_utils.py:236-254,vllm/envs.py:1704-1706). - Any other type that msgspec does not know raises
TypeError. Only withVLLM_ALLOW_INSECURE_SERIALIZATION=1does the encoder fall back to pickle or cloudpickle (vllm/v1/serial_utils.py:220-234). - On the engine side, the input thread uses a typed decoder for ADD messages,
MsgpackDecoder(EngineCoreRequest), and a generic decoder for the rest (vllm/v1/engine/core.py:1930-1933). - The output thread encodes into reused
bytearraybuffers, to avoid a new allocation per step (vllm/v1/engine/core.py:2081-2096).
The message frames are: the engine identity (added by the ROUTER), one request-type byte, then the encoded buffers (vllm/v1/engine/core_client.py:1284-1294). The type bytes are ADD = b"\x00", ABORT = b"\x01", START_DP_WAVE = b"\x02" and UTILITY = b"\x03" (vllm/v1/engine/__init__.py:300-312).
Deep diveSchedulerOutput: the less common fields
| Field | Meaning | Line |
|---|---|---|
scheduled_spec_decode_tokens | draft token ids per request; requests without drafts are not in the dict | vllm/v1/core/sched/output.py:248-251 |
scheduled_encoder_inputs | which multimodal items the encoder must run this step | vllm/v1/core/sched/output.py:252-255 |
finished_req_ids | requests that finished since the last step, so the workers can free their state | vllm/v1/core/sched/output.py:257-260 |
preempted_req_ids | preempted in this step; only used by Model Runner V2 | vllm/v1/core/sched/output.py:267-269 |
has_structured_output_requests, pending_structured_output_tokens | flags for the grammar bitmask with async scheduling | vllm/v1/core/sched/output.py:271-277 |
kv_connector_metadata | instructions for a KV connector (disaggregated prefill) | vllm/v1/core/sched/output.py:282-283 |
new_block_ids_to_zero | fresh blocks that the worker must zero before use | vllm/v1/core/sched/output.py:296-299 |
num_common_prefix_blocks | deprecated; always empty, because cascade attention was removed | vllm/v1/core/sched/output.py:311-313 |
Model Runner V2 sends a resumed (preempted) request again as a new request, with all its token ids. The scheduler moves it into scheduled_new_reqs (vllm/v1/core/sched/scheduler.py:1505-1507).
What can go wrong#
- A plugin adds an object that msgpack cannot encode. The encoder raises
TypeErrorand namesVLLM_ALLOW_INSECURE_SERIALIZATION(vllm/v1/serial_utils.py:220-225). Do not turn the flag on in production: it lets the peer send pickled objects. - Field order of an
array_likestruct. A field in the middle ofEngineCoreRequestorEngineCoreOutputshifts all later fields. Front end and engine core must run the same vLLM version. The comments ask developers to append new fields last (vllm/v1/engine/__init__.py:231-235). - The wrong request id. The engine core logs the internal id with the random suffix, but the client knows only the external id. Search the logs by the prefix.
VLLM_DISABLE_REQUEST_ID_RANDOMIZATIONremoves the suffix, but then duplicate ids can break the engine (vllm/v1/engine/input_processor.py:364-369). - Engine timestamps compared with wall-clock time. Event and output timestamps are monotonic values of the engine core process. The docstring says that they must not be compared with timestamps of other processes (
vllm/v1/engine/__init__.py:184-190).
Why it matters. The contracts are the seams of vLLM. A custom scheduler reads Request and writes SchedulerOutput. A KV connector rides in SchedulerOutput and ModelRunnerOutput. A stat logger reads what EngineCoreOutputs carries. Each extension starts with the right type.
Check yourself#
- 1Which type crosses the ZMQ boundary from the API server to the engine core, and how is it encoded?easy
Show answer
EngineCoreRequest, amsgspec.Structwitharray_like=True, encoded as msgpack (vllm/v1/engine/__init__.py:113-118,vllm/v1/engine/core_client.py:731). - 2Doeseasy
EngineCoreOutputcontain the generated text?Show answer
No. It carries
new_token_ids(vllm/v1/engine/__init__.py:209-210). The text appears only in the front end, where the detokenizer of the output processor makes it (vllm/v1/engine/output_processor.py:823-827). - 3In step 2 of our example, the request is a decode. Is it inmedium
scheduled_new_reqsor inscheduled_cached_reqs, and what does the worker receive for it?Show answer
In
scheduled_cached_reqs. The worker already has the request's data from step 1. So it gets only the difference: new block ids,num_computed_tokensandnum_output_tokens(vllm/v1/core/sched/output.py:137-152, comments atvllm/v1/core/sched/output.py:233-240). - 4Right afterhard
schedule()in step 1,num_computed_tokensis 61, but no token has come back yet. What tells the nextschedule()that one token is in flight, and when does that counter go back to 0?Show answer
num_output_placeholders. The async scheduler adds 1 after a step that samples a token (vllm/v1/core/sched/async_scheduler.py:32-47).update_from_outputremoves it when the token arrives (vllm/v1/core/sched/async_scheduler.py:71-73). The running pass adds the placeholders to the token count (vllm/v1/core/sched/scheduler.py:759-763). - 5A request finishes with an EOS token. Which finish values do the engine core, the front end and the HTTP client see?hard
Show answer
The engine core sets status
FINISHED_STOPPED, which maps toFinishReason.STOP(vllm/v1/request.py:404-405). On the wire this is the integer 0 (vllm/v1/engine/__init__.py:63). The output processor writesstr(finish_reason), which is"stop", intoCompletionOutput(vllm/v1/engine/output_processor.py:525,vllm/v1/engine/__init__.py:69-70). The client sees"finish_reason": "stop"andstop_reasonnull.