EngineCore:v1 引擎真正核心
职责
LLMEngine 在 v1 架构里只是一个薄壳:它拿着 InputProcessor 把外部进来的 prompt 加工成 EngineCoreRequest,拿着 OutputProcessor 把 EngineCoreOutputs 翻译回 RequestOutput,真正干活的全丢给 EngineCore。LLMEngine.__init__ 里那行 self.engine_core = EngineCoreClient.make_client(...)(llm_engine.py:105-111),才是引擎核心 (EngineCore) 真正入场的地方。
引擎核心 (EngineCore) 自己把三件事拼到一起:model_executor(执行器,即 executor_class 实例,负责跨 worker 跑前向)、scheduler(调度器,负责 waiting/running 队列、连续批处理 (continuous batching)、抢占)、KVCacheManager(KV 缓存管理器,通过 _initialize_kv_caches 在 profiling 之后确定每张卡能给 KV cache 多少显存)。构造函数里先把这三件套初始化好(core.py:122-158),然后把 step 函数指针定成 self.step 或 self.step_with_batch_queue(core.py:221-223),后面循环里就反复调它。
step() 干一件事:调度、跑前向、回收输出(core.py:479-508)。具体是 scheduler.schedule() 拿到 SchedulerOutput,model_executor.execute_model(..., non_block=True) 立刻返回 future,期间调度器顺手把 grammar bitmask 算好,future 一 result() 出来就交给 scheduler.update_from_output() 把 token 写回 request 并产出 EngineCoreOutputs。这就是 v1 的一个迭代,所有上层 API(LLM.generate、AsyncLLMEngine.generate)最后都落在这几十行上。
设计动机
为什么要把 LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient 拆成四层?
- 进程隔离:
EngineCoreProc是EngineCore的子类(core.py:896-897),把它扔到一个独立的后台进程里跑,前端的 Python 崩了不会拖垮已经在跑前向的 worker,worker OOM 也不会把 API 进程一起带走。 - ZMQ 解耦:前后端用 ZMQ socket + 两个
queue.Queue桥接(core.py:915-916),ZMQ 在 socket IO 时释放 GIL,可以和 GPU forward 真正重叠;输入和输出各有独立线程(core.py:980-1001),序列化/反序列化也能藏到前向背后。 - 同进程兜底:不开多进程时
InprocClient直接self.engine_core = EngineCore(...)(core_client.py:286-287),get_output()就地调step_fn(),保留 v0 那种LLMEngine.add_request()/.step()的同步用法。 - 异步入口:
AsyncMPClient走多进程 + asyncio,服务器场景下AsyncLLM通过它把 EngineCore 包成可await的对象,前端不阻塞。 - DP 扩展:
run_engine_core在多 DP rank 时给每个 rank 起一个EngineCoreProc(core.py:1192-1200),MoE 走DPEngineCoreProc,非 MoE 当作独立 DP=1,前端再由DPLBAsyncMPClient做负载均衡。
关键文件
EngineCore class:96-97—class EngineCore:定义,docstring 写明Inner loop of vLLM's Engine.EngineCore.__init__:99-237— 装配 model_executor、_initialize_kv_caches、Scheduler、batch_queue、prefix caching hasher。_initialize_kv_caches:240-295— 拿get_kv_cache_specs、profile 显存、组装KVCacheConfig。EngineCore.step:479-508— 调度 + execute_model + update_from_output 的核心 30 行。step_with_batch_queue:519-535— PP 场景下用 batch queue 异步交叠多批,优先填满队列。EngineCoreProc class:896-897—ZMQ-wrapper for running EngineCore in background process.run_engine_core:1154-1224— 进程入口,起 EngineCoreProc、注册 SIGTERM/SIGINT、调run_busy_loop。run_busy_loop:1259-1267—while self._handle_shutdown(): _process_input_queue(); _process_engine_step()。_process_engine_step:1300-1317— 调step_fn()、把 outputs 塞进output_queue、调post_step。LLMEngine.make_client:105-111— LLMEngine 通过EngineCoreClient.make_client选 Inproc/MP/AsyncMP。EngineCoreClient.make_client:83-105— 同步多进程 / 异步多进程 / 同进程三选一。
数据流
一条请求从客户端进来,先在 LLMEngine 里被 InputProcessor 加工成 EngineCoreRequest,然后 engine_core.add_request(...) 丢下去。在多进程模式下,这个调用最终被 MPClient 序列化经 ZMQ 发到 EngineCoreProc 的 input socket,process_input_sockets 线程解码后 put_nowait 进 input_queue(core.py:915-916)。主循环 run_busy_loop 醒来后从队列里取出来,交给 _handle_client_request 分发(core.py:1372-1405),ADD 类型最终落到 self.scheduler.add_request(request)(core.py:372-403)。
# vllm/v1/engine/core.py L1259-L1267
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()
# 2) Step the engine core and return the outputs.
self._process_engine_step()
raise SystemExit_process_engine_step(core.py:1300-1317)拿到 step_fn() 返回的 outputs,逐条 output_queue.put_nowait(output)。output 线程再把它们 ZMQ 回送前端,MPClient.get_output() 反序列化交给 LLMEngine.step() 的 engine_core.get_output() 调用(llm_engine.py:302-314),最后 OutputProcessor.process_outputs 把 token 流拼回 RequestOutput。
step 函数本体就是这么三段:
# vllm/v1/engine/core.py L488-L508
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.log_error_detail(scheduler_output),
self.log_iteration_details(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
)non_block=True 让 execute_model 立刻返回 future,主线程在这个空档里算 grammar bitmask——这是 v1 把调度和 worker 真正并发起来的关键动作。
边界与失败
- EngineCoreProc 进程崩溃:
run_engine_core在except Exception里调engine_core._send_engine_dead()(core.py:1229-1235),往output_queue塞一个ENGINE_CORE_DEAD字节串(core.py:1470-1474),前端MPClient收到后抛错,不会无限等下去。 - Executor 失败回调:构造时把
executor_fail_callback注册到model_executor(core.py:124-125),worker 内部报错时回调往input_queue塞一条EXECUTOR_FAILED,主循环_handle_client_request拿到就raise RuntimeError("Executor failed.")(core.py:1400-1401)。 - Shutdown 模式:
shutdown_timeout == 0是 abort 模式,立刻把所有在飞请求标成FINISHED_ABORTED;非 0 是 drain 模式,等所有在飞请求跑完(core.py:1329-1360)。shutdown 中新进来的 ADD 和 UTILITY 会被显式拒绝(core.py:1407-1432)。 - 非因果注意力层:有些 attention 层(如 Prefix LM)标了
non_causal=True,在_initialize_kv_caches里检测到后强制关掉 chunked prefill 和 prefix caching(core.py:255-269),否则 KV cache 会被写脏。 - batch_queue 让出 CPU:某个 step 没跑前向但 scheduler 还有未完成请求时(比如等远端 KV transfer),
time.sleep(0.001)主动让 GIL(core.py:1311-1315),免得后台传输线程饿死。 - in-process 模式无 busy loop:
InprocClient.get_output直接同步调step_fn(core_client.py:289-292),不跑run_busy_loop,这意味着同进程模式下没有 input/output 线程,add_request 是同步进 scheduler。
小结
引擎核心 (EngineCore) 是 v1 的发动机:它把调度器 (scheduler)、执行器 (executor)、KV 缓存 (KV cache) 三件套装配到一起,用 step() 这一招反复调度 + 前向 + 回收。子类 EngineCoreProc 把它扔进独立进程,用 ZMQ + 两个 queue 桥接前后端,让 GPU 前向和网络 IO 真正重叠。前端用什么客户端(InprocClient、MPClient、AsyncMPClient)决定了它是同步跑还是异步跑,但引擎核心这一层对所有前端都是同一份代码。继续往里看可以去 /engine/llm-engine 看 LLMEngine 这层薄壳到底包了什么,或者去 /engine/engine-core-proc 深入 ZMQ 那一层,客户端三种实现见 /client/inproc-mp,调度器内部见 /scheduler/scheduler。