Skip to content

EngineCore:v1 引擎真正核心

源码版本v0.25.1

职责

LLMEngine 在 v1 架构里只是一个薄壳:它拿着 InputProcessor 把外部进来的 prompt 加工成 EngineCoreRequest,拿着 OutputProcessorEngineCoreOutputs 翻译回 RequestOutput,真正干活的全丢给 EngineCoreLLMEngine.__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.stepself.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.generateAsyncLLMEngine.generate)最后都落在这几十行上。

设计动机

为什么要把 LLMEngine / EngineCore / EngineCoreProc / EngineCoreClient 拆成四层?

  • 进程隔离:EngineCoreProcEngineCore 的子类(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 做负载均衡。

关键文件

数据流

一条请求从客户端进来,先在 LLMEngine 里被 InputProcessor 加工成 EngineCoreRequest,然后 engine_core.add_request(...) 丢下去。在多进程模式下,这个调用最终被 MPClient 序列化经 ZMQ 发到 EngineCoreProc 的 input socket,process_input_sockets 线程解码后 put_nowaitinput_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)。

python
# 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 函数本体就是这么三段:

python
# 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=Trueexecute_model 立刻返回 future,主线程在这个空档里算 grammar bitmask——这是 v1 把调度和 worker 真正并发起来的关键动作。

边界与失败

  • EngineCoreProc 进程崩溃:run_engine_coreexcept 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

对照官方资料:vLLM 文档 · README