v1/engine/core.py: EngineCore and the engine core process
Code version: vLLM @ 3e44c0d · 2026-10-22
Purpose#
vllm/v1/engine/core.py is the inner loop of vLLM. It owns the scheduler and the executor, runs the engine steps, and, in vllm serve, wraps all of this in a process that talks ZMQ with the front end.
| File | vllm/v1/engine/core.py, 2,847 lines of Python. Seven classes: three engine classes, one enum, three Ray actor classes. |
|---|---|
| Main classes | EngineCore (vllm/v1/engine/core.py:118), EngineCoreProc (vllm/v1/engine/core.py:1153), DPEngineCoreProc (vllm/v1/engine/core.py:2269). |
| Runs in | The EngineCore process, one per data parallel rank: the main thread (busy loop), the input thread and the output thread. With the uni executor, the worker and the model runner also run in the main thread. |
| Called by | The engine core client, which starts the process with run_engine_core; with multiprocessing off, InprocClient makes an EngineCore in the caller's process (vllm/v1/engine/core_client.py:384-389). |
| Calls | The scheduler (schedule, update_from_output, get_grammar_bitmask), the executor (execute_model, sample_tokens, start-up RPCs), the KV cache planner, StructuredOutputManager, msgpack and ZMQ. |
| Not in this file | The scheduling algorithm (v1/core/sched/scheduler.py), the client side of the sockets (core_client.py), the process launcher and the handshake on the front-end side (v1/engine/utils.py), the workers (v1/executor/, v1/worker/). |
The page The engine core loop: threads, queues and the busy loop explains the ideas. This page reads the file method by method.
Where it sits#
Who calls it#
- Process launch. The engine core client starts one process per local DP rank with the target
EngineCoreProc.run_engine_core(vllm/v1/engine/utils.py:279-288). - Ray data parallel backend.
EngineCoreActororDPMoEEngineCoreActorrun the same classes inside Ray actors (vllm/v1/engine/utils.py:516-522). - In-process engine.
InprocClientcallsEngineCore.step_fndirectly, with no threads and no sockets (vllm/v1/engine/core_client.py:396-399).vllm servenever uses it:AsyncLLMalways makes a multiprocess client (vllm/v1/engine/async_llm.py:193). - The front end at run time. ADD, ABORT and UTILITY messages over ZMQ; the utility methods are the public methods of
EngineCoreandEngineCoreProc, looked up by name (vllm/v1/engine/core.py:1756-1761).
What it calls#
load_general_plugins, thenexecutor_class(vllm_config)(vllm/v1/engine/core.py:130-147).- Executor start-up RPCs and
get_kv_cache_configsin_initialize_kv_caches(vllm/v1/engine/core.py:253-379). get_scheduler_cls()and the scheduler constructor (vllm/v1/engine/core.py:164-185).- Per step:
schedule,execute_model,get_grammar_bitmask,sample_tokens,update_from_output(vllm/v1/engine/core.py:724-785). Request.from_engine_core_requestin the input thread (vllm/v1/engine/core.py:1128).
Key classes and functions#
| Name | Line | What it does |
|---|---|---|
EngineCore.__init__ | vllm/v1/engine/core.py:121-243 | Plugins, executor, KV caches, scheduler, batch queue, block hasher, step_fn. |
_initialize_kv_caches | vllm/v1/engine/core.py:245-406 | Profile memory, plan the KV cache, allocate it on the workers, warm up. |
add_request / abort_requests | vllm/v1/engine/core.py:508-559 | Check a request and give it to the scheduler; finish requests as aborted. |
step | vllm/v1/engine/core.py:657-687 | One synchronous step (no batch queue). |
step_with_batch_queue | vllm/v1/engine/core.py:698-812 | The default step: start N+1, then wait for N. |
_process_aborts_queue | vllm/v1/engine/core.py:814-822 | Apply early aborts before an update. |
sleep / wake_up / pause_scheduler | vllm/v1/engine/core.py:943-1044 | Pause scheduling and free or restore GPU memory. |
collective_rpc | vllm/v1/engine/core.py:1098-1105 | Forward an RPC to all workers. |
preprocess_add_request | vllm/v1/engine/core.py:1114-1136 | Build the Request (runs in the input thread). |
EngineShutdownState | vllm/v1/engine/core.py:1147-1150 | RUNNING, REQUESTED, SHUTTING_DOWN. |
EngineCoreProc.__init__ | vllm/v1/engine/core.py:1160-1273 | Queues, handshake, EngineCore.__init__, I/O threads. |
_perform_handshakes / startup_handshake | vllm/v1/engine/core.py:1275-1447 | HELLO, init message with addresses, READY. |
run_engine_core | vllm/v1/engine/core.py:1449-1591 | Process entry: title, signals, class choice, busy loop, fatal errors. |
run_busy_loop | vllm/v1/engine/core.py:1608-1620 | The main loop. |
_process_input_queue / _process_engine_step | vllm/v1/engine/core.py:1635-1684 | Drain the input queue (block when idle); run one step. |
_handle_client_request | vllm/v1/engine/core.py:1739-1771 | Dispatch ADD, ABORT, UTILITY, EXECUTOR_FAILED, WAKEUP. |
_invoke_utility_method | vllm/v1/engine/core.py:1822-1839 | Run a utility call; defer the reply for a Future. |
_send_engine_dead | vllm/v1/engine/core.py:1858-1869 | Send ENGINE_CORE_DEAD and wait 5 s for the output thread. |
_make_ready_response | vllm/v1/engine/core.py:1871-1919 | The values that the front end copies into its config. |
process_input_sockets | vllm/v1/engine/core.py:1921-2028 | The input thread. |
process_output_sockets | vllm/v1/engine/core.py:2030-2096 | The output thread. |
DPEngineCoreProc | vllm/v1/engine/core.py:2269-2670 | MoE DP: waves, dummy batches, all-reduce of "has work". |
EngineCoreActorMixin and two actor classes | vllm/v1/engine/core.py:2673-2847 | The same engines as Ray actors. |
Walk through the main methods#
EngineCore.__init__: build the engine#
# Setup Model.
self.model_executor = executor_class(vllm_config)
self._pooler_config_logged = False
if executor_fail_callback is not None:
self.model_executor.register_failure_callback(executor_fail_callback)
self.available_gpu_memory_for_kv_cache = -1
if envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
self._eep_scale_up_before_kv_init()
measure_encoder_output_widths(vllm_config, self.collective_rpc)
# Setup KV Caches and update CacheConfig after profiling.
kv_cache_config = self._initialize_kv_caches(vllm_config)
self.structured_output_manager = StructuredOutputManager(vllm_config)
# Setup scheduler.
Scheduler = vllm_config.scheduler_config.get_scheduler_cls()
if len(kv_cache_config.kv_cache_groups) == 0: # noqa: SIM102
# Encoder models without KV cache don't support
# chunked prefill. But do SSM models?
if vllm_config.scheduler_config.enable_chunked_prefill:
logger.warning("Disabling chunked prefill for model without KVCache")
vllm_config.scheduler_config.enable_chunked_prefill = False
scheduler_block_size, hash_block_size = resolve_kv_cache_block_sizes(
kv_cache_config, vllm_config
)
self.scheduler: SchedulerInterface = Scheduler(
vllm_config=vllm_config,
kv_cache_config=kv_cache_config,
structured_output_manager=self.structured_output_manager,
include_finished_set=include_finished_set,
log_stats=self.log_stats,
block_size=scheduler_block_size,
hash_block_size=hash_block_size,
)- Line 147. The executor constructor starts the workers and loads the model. This is the slowest line of start-up.
- Line 150. The failure callback (from
EngineCoreProc) lets a dead worker stop the busy loop. - Line 160. The KV cache set-up needs the loaded model (next method).
- Line 164.
get_scheduler_cls()returns the class inscheduler_clswhen it is set. Otherwise it returnsAsyncSchedulerwhen async scheduling is on, orScheduler(vllm/config/scheduler.py:231-239). - Lines 166-171. A model with no KV cache groups (an encoder-only model) gets chunked prefill turned off.
- Lines 177-185. The scheduler gets the final KV cache config, so its block pool has the real number of blocks.
# Setup batch queue for pipeline parallelism.
# Batch queue for scheduled batches. This enables us to asynchronously
# schedule and execute batches, and is required by pipeline parallelism
# to eliminate pipeline bubbles.
self.batch_queue_size = vllm_config.max_concurrent_batches
self.batch_queue: (
deque[tuple[Future[ModelRunnerOutput], SchedulerOutput, Future[Any]]] | None
) = None
if self.batch_queue_size > 1:
logger.debug("Batch queue is enabled with size %d", self.batch_queue_size)
self.batch_queue = deque(maxlen=self.batch_queue_size)
self.is_mm_encoder_only = vllm_config.is_mm_encoder_only
self.is_pooling_model = vllm_config.model_config.runner_type == "pooling"
self.request_block_hasher: Callable[[Request], list[BlockHash]] | None = None
if vllm_config.cache_config.enable_prefix_caching or kv_connector is not None:
caching_hash_fn = get_hash_fn_by_name(
vllm_config.cache_config.prefix_caching_hash_algo
)
init_none_hash(caching_hash_fn)
self.request_block_hasher = get_request_block_hasher(
hash_block_size, caching_hash_fn
)
self.step_fn = (
self.step if self.batch_queue is None else self.step_with_batch_queue
)
self.async_scheduling = vllm_config.scheduler_config.async_scheduling
self.aborts_queue = queue.Queue[list[str]]()
self._idle_state_callbacks: list[Callable] = []
# Mark the startup heap as static so that it's ignored by GC.
# Reduces pause times of oldest generation collections.
freeze_gc_heap()
# If enable, attach GC debugger after static variable freeze.
maybe_attach_gc_debug_callback()
# Enable environment variable cache (e.g. assume no more
# environment variable overrides after this point)
enable_envs_cache()- Lines 205-211. The batch queue is a
dequewithmaxlen = max_concurrent_batches, only when that value is more than 1. - Lines 217-225. The block hasher exists when prefix caching is on or a KV connector is set. The input thread uses it.
- Lines 227-229. The step function:
stepwithout a batch queue,step_with_batch_queuewith one. - Line 232. The abort queue for early aborts.
- Line 238.
freeze_gc_heap()marks all start-up objects as static, so the garbage collector does not scan them again (comment at lines 236-237). - Line 243. From here on, environment variables are cached.
_initialize_kv_caches: memory and KV cache#
has_kv_cache = any(kv_cache_spec for kv_cache_spec in kv_cache_specs)
if has_kv_cache:
if envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
# NOTE(yongji): should already be set
# during _eep_scale_up_before_kv_init
assert self.available_gpu_memory_for_kv_cache > 0
available_gpu_memory = [self.available_gpu_memory_for_kv_cache] * len(
kv_cache_specs
)
else:
# Profiles the peak memory usage of the model to determine how
# much memory can be allocated for kv cache.
available_gpu_memory = self.model_executor.determine_available_memory()
self.available_gpu_memory_for_kv_cache = available_gpu_memory[0]
else:
# Attention free models don't need memory for kv cache
available_gpu_memory = [0] * len(kv_cache_specs)
assert len(kv_cache_specs) == len(available_gpu_memory)
# Track max_model_len before KV cache config to detect auto-fit changes
max_model_len_before = vllm_config.model_config.max_model_len
kv_cache_configs = get_kv_cache_configs(
vllm_config, kv_cache_specs, available_gpu_memory
)
for kv_cache_config in kv_cache_configs:
kv_cache_config.kv_cache_layout = vllm_config.cache_config.kv_cache_layout
# If auto-fit reduced max_model_len, sync the new value to workers.
# This is needed because workers were spawned before memory profiling
# and have the original (larger) max_model_len cached.
max_model_len_after = vllm_config.model_config.max_model_len
if max_model_len_after != max_model_len_before:
self.collective_rpc("update_max_model_len", args=(max_model_len_after,))
scheduler_kv_cache_config = generate_scheduler_kv_cache_config(kv_cache_configs)
vllm_config.cache_config.num_gpu_blocks = scheduler_kv_cache_config.num_blocks- Line 300. When the model has a KV cache (line 289), each worker runs a profile run and returns the bytes left for the KV cache. The page v1/worker/gpu_worker.py: the GPU Worker has the formula and a worked example.
- Lines 311-313.
get_kv_cache_configsturns bytes into blocks, one config per worker. The page The KV cache manager: blocks, the block pool and memory sizing explains it. - Lines 321-322. If the planner changed
max_model_len(for example, auto-fit lowered it to fit the memory), the workers learn the new value by RPC. - Lines 324-325. The scheduler's view of the cache, and the final
num_gpu_blocks. The ready message sends this number to the front end (vllm/v1/engine/core.py:1876).
for kv_cache_config in (*kv_cache_configs, scheduler_kv_cache_config):
kv_cache_config.hash_block_size = hash_block_size
kv_cache_config.cache_hit_alignment_tokens = cache_hit_alignment_tokens
self.model_executor.initialize_from_config(kv_cache_configs)
kv_transfer_config = vllm_config.kv_transfer_config
if (
kv_transfer_config is not None
and kv_transfer_config.has_connector("SimpleCPUOffloadConnector")
and vllm_config.cache_config.enable_prefix_caching
):
aligned_num_cpu_blocks = (
self.model_executor.get_simple_cpu_offload_num_blocks()
)
assert aligned_num_cpu_blocks is not None, (
"SimpleCPUOffloadConnector workers must report their block count"
)
vllm_config.cache_config.simple_cpu_offload_num_blocks = (
aligned_num_cpu_blocks
)
if not envs.VLLM_ELASTIC_EP_SCALE_UP_LAUNCH:
self.model_executor.compile_or_warm_up_model()
- Line 362. The workers allocate the KV cache tensors.
- Line 379. Compile, warm up and capture CUDA graphs. Only after this line can the engine serve requests.
step: the synchronous step#
def step(self) -> tuple[dict[int, EngineCoreOutputs], bool]:
"""Schedule, execute, and make output.
Returns tuple of outputs and a flag indicating whether the model
was executed.
"""
# Check for any requests remaining in the scheduler - unfinished,
# or finished and not yet removed from the batch.
if not self.scheduler.has_requests():
return {}, False
scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
future = self.model_executor.execute_model(scheduler_output, non_block=True)
grammar_output = self.scheduler.get_grammar_bitmask(scheduler_output)
with (
self.capture_iteration_details(scheduler_output) as iteration_details,
self.log_error_detail(scheduler_output),
):
model_output = future.result()
if model_output is None:
model_output = self.model_executor.sample_tokens(grammar_output)
# Before processing the model output, process any aborts that happened
# during the model execution.
self._process_aborts_queue()
engine_core_outputs = self.scheduler.update_from_output(
scheduler_output, model_output
)
self._attach_iteration_details(engine_core_outputs, iteration_details)
self._attach_weight_version(engine_core_outputs)
return engine_core_outputs, scheduler_output.total_num_scheduled_tokens > 0- Lines 665-666. No requests: no step.
- Lines 668-669. The forward pass starts (non-blocking), and the grammar bitmask runs on the CPU meanwhile.
- Lines 674-676. Wait for
execute_model. ANoneresult means "now callsample_tokens", which here is blocking. - Line 687. The second return value tells
post_stepwhether the model ran.
step_with_batch_queue: the default step#
model_executed = False
deferred_scheduler_output = None
if self.scheduler.has_requests():
scheduler_output = self.scheduler.schedule(self._should_throttle_prefills())
with self.log_error_detail(scheduler_output):
exec_future = self.model_executor.execute_model(
scheduler_output, non_block=True
)
if not self.is_mm_encoder_only:
model_executed = scheduler_output.total_num_scheduled_tokens > 0
if self.is_pooling_model or not model_executed:
# No sampling required (no requests scheduled).
future = cast(Future[ModelRunnerOutput], exec_future)
else:
if not scheduler_output.pending_structured_output_tokens:
# We aren't waiting for any tokens, get any grammar output
# and sample immediately.
grammar_output = self.scheduler.get_grammar_bitmask(
scheduler_output
)
future = self.model_executor.sample_tokens(
grammar_output, non_block=True
)
else:
# We need to defer sampling until we have processed the model output
# from the prior step.
deferred_scheduler_output = scheduler_output
if not deferred_scheduler_output:
# Add this step's future to the queue.
batch_queue.appendleft((future, scheduler_output, exec_future))
if len(batch_queue) < self.batch_queue_size and (
model_executed or self.scheduler.has_requests()
):
# Don't block on next worker response unless the queue is full
# or there are no more requests to schedule.
return None, model_executed # Block until the next result is available.
future, scheduler_output, exec_model_fut = batch_queue.pop()
with (
self.capture_iteration_details(scheduler_output) as iteration_details,
self.log_error_detail(scheduler_output),
):
model_output = future.result()
if model_output is None:
# None from sample_tokens() implies that the original execute_model()
# call failed - raise that exception.
exec_model_fut.result()
raise RuntimeError("unexpected error")
# Before processing the model output, process any aborts that happened
# during the model execution.
self._process_aborts_queue()
engine_core_outputs = self.scheduler.update_from_output(
scheduler_output, model_output
)
self._attach_iteration_details(engine_core_outputs, iteration_details)
self._attach_weight_version(engine_core_outputs)The page The engine core loop: threads, queues and the busy loop reads this method line by line in a deep dive, and Async scheduling: overlap the CPU and the GPU explains the deferred-sampling branch (line 749). The short version: start step N+1 (lines 725-753); if the queue is full, wait for step N (lines 768-778) and update the scheduler (line 783).
run_busy_loop#
@fault_tolerant_wrapper
def run_busy_loop(self):
"""Core busy loop of the EngineCore."""
while self._handle_shutdown():
# 1) Poll the input queue until there is work to do.
self._process_input_queue()
# Publish request counts before and after GPU step to ensure freshness.
self._maybe_publish_request_counts()
# 2) Step the engine core and return the outputs.
self._process_engine_step()
self._maybe_publish_request_counts()
raise SystemExit- Line 1608.
fault_tolerant_wrapperre-raises at once unless fault tolerance is on (vllm/v1/fault_tolerance/engine_core_sentinel.py:175-183). - Line 1611.
_handle_shutdown()is true while running, and also while a requested shutdown drains work. - Line 1613. Blocks when there is no work (
vllm/v1/engine/core.py:1638-1656). - Line 1620. The loop ends only with
SystemExit.run_engine_corecatches it and cleans up (vllm/v1/engine/core.py:1560-1583).
process_input_sockets: the input thread#
# Msgpack serialization decoding with optional tensor IPC receiver.
add_request_decoder = MsgpackDecoder(
EngineCoreRequest, oob_tensor_provider=self.tensor_ipc_receiver
)
generic_decoder = MsgpackDecoder(oob_tensor_provider=self.tensor_ipc_receiver)
with ExitStack() as stack, zmq.Context() as ctx:
input_sockets = [
stack.enter_context(
make_zmq_socket(
ctx, input_address, zmq.DEALER, identity=identity, bind=False
)
)
for input_address in input_addresses
]
if coord_input_address is None:
coord_socket = None
else:
coord_socket = stack.enter_context(
make_zmq_socket(
ctx,
coord_input_address,
zmq.XSUB,
identity=identity,
bind=False,
)
)
# Send subscription message to coordinator.
coord_socket.send(b"\x01")
# Register sockets with poller.
poller = zmq.Poller()
ready_response = self._make_ready_response()
ready_payload = msgspec.msgpack.encode(ready_response)
for input_socket in input_sockets:
# Send initial message to each input socket - this is required
# before the front-end ROUTER socket can send input messages
# back to us.
input_socket.send(ready_payload)
poller.register(input_socket, zmq.POLLIN)
if coord_socket is not None:
# Wait for ready message from coordinator.
assert coord_socket.recv() == b"READY"
poller.register(coord_socket, zmq.POLLIN)
ready_event.set()- Lines 1930-1933. One typed decoder for ADD messages, one generic decoder for the rest. Both can receive tensors through the tensor IPC queue of multimodal inputs.
- Lines 1936-1943. One DEALER socket per front end (per API server), with this engine's identity. The front end owns the ROUTER side.
- Lines 1961-1968. The first message on each socket is the ready response.
- Line 1972. With a DP coordinator, the thread first waits for its
READY. - Line 1975. Only now can
EngineCoreProc.__init__finish (vllm/v1/engine/core.py:1267-1273).
The receive loop that follows (lines 1977-2028) is read on The engine core loop: threads, queues and the busy loop, Step 3.
process_output_sockets: the output thread#
"""Output socket IO thread."""
# Msgpack serialization encoding.
encoder = MsgpackEncoder()
# Send buffers to reuse.
reuse_buffers: list[bytearray] = []
# Payload buffers that can't be reused yet because zmq may still be
# sending them.
# Buffers of the zero-copy tensor/ndarray frames don't need tracking
# here: zmq itself holds a reference to each until it's done with it.
pending = deque[tuple[zmq.MessageTracker, bytearray]]()
# We must set linger to ensure the ENGINE_CORE_DEAD
# message is sent prior to closing the socket.
with ExitStack() as stack, zmq.Context() as ctx:
sockets = [
stack.enter_context(
make_zmq_socket(ctx, output_path, zmq.PUSH, linger=4000)
)
for output_path in output_paths
]
coord_socket = (
stack.enter_context(
make_zmq_socket(
ctx, coord_output_path, zmq.PUSH, bind=False, linger=4000
)
)
if coord_output_path is not None
else None
)
max_reuse_bufs = len(sockets) + 1- Line 2035. One encoder for the life of the thread.
- Lines 2037 and 2042. Send buffers are reused. A buffer that ZMQ still sends waits in
pendingwith its tracker. - Line 2049. One PUSH socket per front end.
linger=4000gives theENGINE_CORE_DEADmessage 4 seconds to leave (comment at lines 2044-2045). - Line 2062. At most one spare buffer per socket, plus one, is kept.
The send loop (lines 2064-2096) is read on The engine core loop: threads, queues and the busy loop, Step 7.
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 diveData flow: types in and out
| Value | Type | Where |
|---|---|---|
| ADD message | frames [b"\x00", msgpack(EngineCoreRequest), tensor frames...] | vllm/v1/engine/core.py:1980-1991 |
| after preprocessing | (Request, wave: int) | vllm/v1/engine/core.py:1114-1136 |
| UTILITY message | (client_idx: int, call_id: int, method: str, args: tuple) | vllm/v1/engine/core.py:2010-2011 |
| batch queue entry | (Future[ModelRunnerOutput], SchedulerOutput, Future[Any]) | vllm/v1/engine/core.py:206-208 |
| step result | dict[int, EngineCoreOutputs] (client index → outputs), or None | vllm/v1/engine/core.py:698-700 |
| output queue item | (client_index, EngineCoreOutputs) or b"ENGINE_CORE_DEAD" | vllm/v1/engine/core.py:1173 |
| ready response | EngineCoreReadyResponse (msgpack) | vllm/v1/engine/core.py:1871-1919 |
The fields of these types are on Data contracts between the parts of vLLM.
Deep diveProcesses and threads
| Thread | Methods of this file | Blocks in |
|---|---|---|
| main | run_engine_core, run_busy_loop, every EngineCore method, every utility method | input_queue.get() when idle; future.result() when the batch queue is full |
| input | process_input_sockets, preprocess_add_request, the error-output helpers | poller.poll() |
| output | process_output_sockets | output_queue.get() |
The input thread calls preprocess_add_request, which touches mm_receiver_cache and structured_output_manager. Its comments explain why there is no race: only the input thread uses the cache after start-up, and grammar compilation is asynchronous and per request (vllm/v1/engine/core.py:1120-1135).
Deep diveWorked example: what one engine core sends in four steps
One request, async scheduling, queue size 2, a 3-token answer that ends with EOS. Each line is one call of _process_engine_step (vllm/v1/engine/core.py:1666-1684).
- Call 1: schedule step 1 (the prompt) and return
None. Nothing goes intooutput_queue. - Call 2: schedule step 2, wait for step 1, update.
output_queuegets(0, EngineCoreOutputs(outputs=[token 1])). - Call 3: schedule step 3, wait for step 2. Output: token 2.
- Call 4: schedule step 4, wait for step 3. Output: token 3 = EOS,
finish_reason=STOP. - Call 5:
has_requests()is still true for one call, because the finished request id must reach the workers in the nextSchedulerOutput(vllm/v1/core/sched/scheduler.py:2913-2914,vllm/v1/core/sched/scheduler.py:1597-1601). It schedules an empty step 5, then waits for step 4, whose result is skipped.schedule()clears the finished ids (vllm/v1/core/sched/scheduler.py:1731-1734), so a sixth call only pops the empty step 5.
So 4 GPU steps produce 3 tokens. (Derived from vllm/v1/engine/core.py:724-785 and vllm/v1/core/sched/scheduler.py:2184-2192.)
Deep diveError paths: what raises, what only logs
| Situation | Result | Where |
|---|---|---|
request_id is not a string | TypeError in add_request | vllm/v1/engine/core.py:515-518 |
| pooling task not supported | ValueError | vllm/v1/engine/core.py:520-529 |
kv_transfer_params without a KV connector | warning; transfer off for the request | vllm/v1/engine/core.py:531-537 |
| preprocessing fails in the input thread | logged; error output for that request only | vllm/v1/engine/core.py:1999-2008, vllm/v1/engine/core.py:2149-2167 |
| a utility method raises | logged; failure_message in the reply | vllm/v1/engine/core.py:1836-1839 |
| unknown message type | logged at ERROR; ignored | vllm/v1/engine/core.py:1768-1771 |
| a worker died | RuntimeError("Executor failed.") | vllm/v1/engine/core.py:1766-1767 |
| exception in a step | engine state dumped, exception re-raised | vllm/v1/engine/core.py:561-575 |
| exception before the busy loop | "EngineCore failed to start." | vllm/v1/engine/core.py:1571-1572 |
| exception in the busy loop | "EngineCore encountered a fatal error."; ENGINE_CORE_DEAD | vllm/v1/engine/core.py:1573-1576 |
| no init message in 5 minutes | RuntimeError | vllm/v1/engine/core.py:1408-1413 |
| input thread dies at start-up | RuntimeError("Input socket thread died during startup") | vllm/v1/engine/core.py:1269-1271 |
Deep diveEnvironment variables and config fields
| Name | Default | Read at | Effect |
|---|---|---|---|
VLLM_ELASTIC_EP_SCALE_UP_LAUNCH | off | vllm/v1/engine/core.py:154, vllm/v1/engine/core.py:290, vllm/v1/engine/core.py:378 | Elastic EP only: skip profiling and warm-up for a new rank. |
max_concurrent_batches (derived) | 2 on one GPU | vllm/v1/engine/core.py:205 | Batch queue size. |
prefix_caching_hash_algo | sha256 | vllm/v1/engine/core.py:218-220 | Hash of the block hasher. |
shutdown_timeout | 0 | vllm/v1/engine/core.py:1697 | Abort or drain at shutdown. |
enable_logging_iteration_details | off | vllm/v1/engine/core.py:581-586 | Per-step details in the scheduler stats (needs log_stats). |
Related concepts#
- The engine core loop: threads, queues and the busy loop: the threads, the busy loop, utility calls, aborts and errors.
- Async scheduling: overlap the CPU and the GPU: why
step_with_batch_queueis the default. - Executors and workers: from the engine core to the GPUs: what
model_executordoes with each call. - Data parallel serving: several engine cores behind one endpoint:
DPEngineCoreProc, waves and the coordinator. - v1/engine/core_client.py: the engine core clients: the other end of the sockets.
- Tour: One engine step: one call of
step_with_batch_queuewith real numbers.
Check yourself#
- 1Which method does the engine core use aseasy
step_fnwith the default settings on one GPU?Show answer
step_with_batch_queue, because the batch queue exists whenmax_concurrent_batchesis more than 1 (vllm/v1/engine/core.py:209-211,vllm/v1/engine/core.py:227-229). With async scheduling and Model Runner V2 it ispp_size + 1 = 2(vllm/config/vllm.py:591-593). - 2In which order doesmedium
EngineCore.__init__build the executor, the KV cache and the scheduler, and why this order?Show answer
Executor (line 147), KV caches (line 160), scheduler (lines 177-185) (
vllm/v1/engine/core.py:146-185). The KV cache size depends on a profile run of the loaded model, and the scheduler needs the final KV cache config. - 3Which thread runsmedium
preprocess_add_request, and what does that buy?Show answer
The input thread (
vllm/v1/engine/core.py:1993). TheRequestobject and its block hashes are built while the main thread runs the model, as the docstring says (vllm/v1/engine/core.py:1115-1119). - 4The front end sends a utility call tohard
sleepwhile requests still run, with the pause mode"wait". When does the reply go out?Show answer
sleep()pauses the scheduler and gets aFutureback, because the engine is not idle yet. It returns a secondFuturethat puts the executor to sleep when the pause completes (vllm/v1/engine/core.py:994-1017)._invoke_utility_methodsees aFutureand adds a callback, so the reply is sent only when that future is done (vllm/v1/engine/core.py:1828-1834). The busy loop meanwhile keeps stepping the in-flight requests.