vLLM系统拆解-03-入口分层:LLM、LLMEngine、AsyncLLMEngine与vllm serve
vLLM系统拆解-03-入口分层:LLM、LLMEngine、AsyncLLMEngine与vllm serve
刚接触 vLLM 的人通常会有一个困惑:一个推理框架,为什么有 LLM、LLMEngine、AsyncLLMEngine 三个类,还有一个命令行工具 vllm serve?
这不是"东西太多",而是三个不同层次的抽象,分别针对不同的使用场景:如何让用户用得简单、如何管理推理生命周期、如何把引擎接进 HTTP 服务。三层各自解决一类问题,共享同一套底层执行核心。
这篇文章先建立这套分层的整体视图,再逐层解释每层的设计逻辑,最后用完整时序图走一遍在线请求的调用链。
三层入口的整体结构
flowchart TD
PyUser["Python 用户\n离线脚本 / 测试 / Benchmark"]
HTTPClient["HTTP 客户端\n应用服务 / OpenAI SDK"]
subgraph L1["第一层:用户接口层"]
LLM["LLM\nvllm/entrypoints/llm.py\n高层封装,屏蔽底层复杂度"]
end
subgraph L3["第三层:服务入口层"]
Serve["vllm serve\nvllm/entrypoints/openai/\nOpenAI-compatible 协议 + HTTP 服务"]
end
subgraph L2["第二层:引擎层"]
LLMEng["LLMEngine\nvllm/engine/llm_engine.py\n同步引擎,输入处理/调度/执行/输出"]
AsyncEng["AsyncLLMEngine\nvllm/v1/engine/async_llm.py\n异步引擎,per-request stream + output handler"]
end
subgraph Core["Engine Core(执行核心)"]
EngCore["Engine Core\nvllm/v1/engine/core.py\nbusy loop 调度 + KV Cache 管理"]
Workers["GPU Workers\nvllm/v1/worker/gpu_worker.py\n模型 forward + 显存管理"]
EngCore --> Workers
end
PyUser --> LLM
HTTPClient --> Serve
LLM --> LLMEng
Serve --> AsyncEng
LLMEng --> EngCore
AsyncEng --> EngCore
这张图有几个关键点:
LLM和vllm serve是两条不同的入口路径,面向不同使用场景- 两条路径最终都收敛到同一个 Engine Core
LLMEngine(同步)和AsyncLLMEngine(异步)都是引擎抽象,针对不同的调用模型- Engine Core 下面才是真正执行推理的 GPU workers
为什么需要三层,而不是一个统一入口
把所有东西放进一个大类在理论上可行,但工程上代价很高——因为三类使用者关注的问题差别很大:
离线脚本用户关心的是:给我一个 prompt,返回结果,代码尽量短。
服务系统开发者关心的是:请求如何排队、如何异步返回、如何不中断 event loop、如何同时处理大量连接。
框架维护者关心的是:输入处理在哪层做、scheduler 放在哪里、worker 怎么起、engine core 怎么通信。
把这三类关注点混在一个类里,会导致接口臃肿、同步/异步模型混杂、生命周期管理混乱、难以单独演进某一层。
更合理的结构是:给普通用户一个简单入口(LLM),给系统内部一个控制核心(LLMEngine/AsyncLLMEngine),给服务场景一个协议适配入口(vllm serve)。每层职责单一,底层可复用。
第一层:LLM 类
LLM 是面向普通 Python 用户的高层封装,典型用法:
1 | from vllm import LLM, SamplingParams |
三行代码,不需要关心 request object、async stream、engine core、scheduler step、output handler、detokenizer queue——这些复杂度全部被 LLM 封装掉了。
LLM 的本质是对 engine 的高层封装,不是另一套推理系统。 LLM.generate() 虽然看起来像一个普通函数调用,但底层仍然走 engine 的执行链路,完整的 batch 组织、cache 管理、调度优化都在里面。这也是离线使用 vLLM 时仍然比直接调 HuggingFace 快的原因。
第二层:LLMEngine——推理核心不是 model,而是 engine
官方 Architecture Overview 对 LLMEngine 的描述很直接:核心组件,负责接收请求并生成输出,其中包括输入处理、调度、模型执行和输出处理。
为什么推理系统的核心必须是 engine,而不是 model?
因为在线推理最难的问题不是"某一层 Transformer 怎么实现",而是:
- 这个请求该在什么时候进 batch
- 这个请求现在是 Prefill 还是 Decode
- 这个请求的 KV Cache 够不够用
- 该不该抢占某个低优先级请求
- 输出如何增量回传
- 多张 GPU 如何协同
- 多进程如何通信
这些都是 engine 层的问题,model 完全不管。
从设计目标上说:model 是算子与参数的承载体,engine 是请求生命周期的控制器。
LLMEngine 负责的四件事:
| 阶段 | 做什么 |
|---|---|
| 输入处理 | 标准化 prompt/token,应用 chat template,准备请求对象 |
| 调度 | 决定这一轮哪些请求进入 batch,分配 token budget 和 KV Cache block |
| 执行 | 把 batch 交给执行层(GPU workers)跑 forward |
| 输出处理 | 把 token ID 转成文本,整理输出格式,回传结果 |
第二层:AsyncLLMEngine——不只是"加了 async/await"
这是最容易被误解的一个类。很多人把 AsyncLLMEngine 理解为"LLMEngine 的异步版本",这个说法没错但太表层。
真正的区别来自 server 场景的特殊需求。
同步接口在 server 场景的问题
同步思路是:发起调用 → 程序阻塞 → 等完整结果 → 继续。单请求脚本里完全够用,但 server 同时面对大量连接——有的请求刚进来,有的正在等 token,有的在流式输出,有的刚结束要清理资源。用同步阻塞包裹这一切,吞吐和响应性都会很差。
AsyncLLMEngine 解决的核心问题
官方文档对 AsyncLLMEngine.generate() 的流程描述如下:
- 为该请求建立独立的
AsyncStream - 处理输入(tokenization 等)
- 把请求加入
Detokenizer - 把请求送入独立进程中的
EngineCore - 后台有一个独立的
output_handler循环,持续从 EngineCore 拉取输出,放回对应请求的AsyncStream - 调用方迭代这个异步生成器,拿到增量输出
两个关键词值得注意:per-request stream(每个请求有独立的输出流)和 separate process(EngineCore 在独立进程里运行)。这两点说明 vLLM 在架构上已经把"服务连接"和"核心执行"明确解耦了。
这个设计带来的好处:
- 前端连接层不需要直接盯着 worker 的执行状态
- EngineCore 可以按自己的调度节奏推进,不受前端影响
- 每个请求独立消费自己的 AsyncStream,互不阻塞
- 天然适合多连接并发场景
第三层:vllm serve——协议层,不是引擎
vllm serve 做的事情更接近"把 vLLM engine 封装成可被外部访问的服务",而不是重新实现一套推理逻辑。
官方说明:vLLM 提供兼容 OpenAI Completions API 和 Chat API 的 HTTP server,通过 vllm serve 启动,默认可通过 http://localhost:8000/v1 访问。
它实际做的事情:
- 接受 HTTP 请求
- 解析 OpenAI 风格的请求 payload
- 调用
AsyncLLMEngine - 把结果重新包装成 OpenAI 风格响应
- 处理流式输出(SSE)、API key 校验、中间件、request ID 等服务层问题
它不做的事情:调度、KV Cache 管理、GPU forward——这些全在 engine 里。
为什么 OpenAI 兼容很重要
应用层通常不想绑定到某个具体的推理后端。如果 vLLM 使用 OpenAI 兼容协议,应用代码可以统一 SDK、统一消息格式、统一 streaming 方式,切换推理后端时上层代码几乎不用改。
vllm serve 解决的是接入与服务化问题,engine 解决的是推理生命周期控制问题。分工清晰。
四个角色的定位对比
| 角色 | 面向谁 | 解决什么问题 | 不负责什么 |
|---|---|---|---|
LLM |
Python 用户 | 屏蔽复杂度,简化调用 | 推理控制逻辑 |
LLMEngine |
系统内部 | 请求生命周期全流程管理 | 网络协议、服务层 |
AsyncLLMEngine |
服务系统 | 异步/流式/多连接并发 | 具体执行(交给 EngineCore) |
vllm serve |
工程部署 | HTTP 协议适配、OpenAI 兼容 | 调度、KV Cache、GPU 执行 |
这四者的关系是逐层包装和适配,不是互相竞争。
一次在线请求的完整调用链
把上面的结构转成时序图:
sequenceDiagram
participant C as HTTP 客户端
participant A as API Server (vllm serve)
participant AE as AsyncLLMEngine
participant EC as EngineCore (独立进程)
participant W as GPU Worker
participant OH as output_handler (后台循环)
participant AS as AsyncStream (per-request)
C->>A: POST /v1/chat/completions
A->>A: 鉴权 / 协议解析 / 参数校验<br/>OpenAI messages → 内部格式
A->>AE: 提交请求
AE->>AS: 创建 AsyncStream(与请求绑定)
AE->>AE: 输入处理(tokenization 等)
AE->>EC: 送入 EngineCore(独立进程)
loop 调度循环(每轮 Decode)
EC->>EC: 调度决策 / 分配 KV Cache
EC->>W: 下发 batch
W->>W: 执行 forward / 更新 KV Cache
W->>EC: 返回 logits / tokens
EC->>OH: 输出 token
end
OH->>AS: 将 token 放入对应请求的 AsyncStream
AE->>A: 迭代 AsyncStream,获取增量输出
A->>C: 流式返回(SSE / streaming response)
EC->>EC: 检测结束条件(EOS / max_tokens)
EC->>EC: 释放 KV Cache / 清理请求状态
A->>C: 最终响应结束
这条链路的核心特征:
- 请求进来 → API server 做协议转换,不做推理
- 转到 AsyncLLMEngine → 建立独立 AsyncStream,送入 EngineCore
- EngineCore 独立运行调度循环,不受前端连接状态影响
- output_handler 是一个后台异步循环,把 EngineCore 的输出分发回各请求的 stream
- 每个请求通过自己的 AsyncStream 独立消费输出,互不干扰
这和"请求进来 → 直接调用 model → 返回字符串"完全是两种设计思路。
为什么 API server 和 engine core 必须拆开
这里有个关键的设计决策值得单独说清楚。
如果不拆,会出现什么
如果 API server 直接持有完整推理执行逻辑,它要同时处理:网络收包、参数校验、tokenization、调度、GPU 执行控制、输出流式返回、连接管理。这会带来几个典型问题:
- 职责耦合:前端协议层一旦改动,容易触发执行层的变动
- 异常传播范围大:某个请求处理失败可能直接影响整个服务进程
- 扩展困难:无法单独替换前端协议或单独扩展执行层
- 资源争用:前端网络事件和核心调度事件竞争 CPU,互相拖累
- 可观测性差:难以区分是前端问题、调度问题还是 worker 问题
拆开之后的收益
前端更像网关/协议层,core 更像计算与调度中枢,两边通过明确的通信机制交互。
官方文档也给出了一个具体的典型部署配置:单机 4 卡、TP=4 时,是 1 个 API server + 1 个 engine core + 4 个 GPU worker。这个组织方式本身说明 vLLM 把"服务接入"和"执行控制"视为不同层级的职责。
控制平面与数据平面分离、接入层与核心层分离,是成熟 infra 系统的通用做法,vLLM 的设计完全符合这个思路。
engine 管策略,worker 管执行
还有一个分工值得说明:engine core 为什么不直接做 GPU 计算,而是再委托给 worker?
engine core 的职责是"决定做什么":谁该算、这轮 batch 是什么、资源怎么分、结果怎么回。
GPU worker 的职责是"把它做出来":加载模型权重、管理本 GPU 显存、执行 forward、维护 KV Cache、参与分布式并行。
把 GPU 执行的重职责(权重常驻显存、KV Cache 显存维护、attention backend 选择、批处理执行、可能的 TP/PP 通信)全塞进 engine,会让 engine 的边界越来越模糊,调试和扩展都更困难。"engine 管策略,worker 管执行"是一个清晰的职责边界。
源码路径速查
| 层次 | 源码路径 | 职责 |
|---|---|---|
| 用户接口层 | vllm/entrypoints/llm.py |
LLM 类,高层封装 |
| 同步引擎 | vllm/engine/llm_engine.py |
LLMEngine,核心控制 |
| 异步引擎 | vllm/v1/engine/async_llm.py |
AsyncLLMEngine,per-request stream / output handler |
| 服务前端 | vllm/entrypoints/openai/ |
OpenAI-compatible 协议层,路由,请求解析,响应包装 |
| Engine Core | vllm/v1/engine/core.py |
busy loop 调度,资源管理,worker 协调 |
| 多进程执行器 | vllm/v1/executor/multiproc_executor.py |
启动 worker,连接 engine core 和实际 worker |
| GPU Worker | vllm/v1/worker/gpu_worker.py |
GPU 执行,权重,显存,forward |
这些文件沿着"入口 → 引擎 → 核心执行 → worker"这条链组织,不是平铺的。
几个常见误区
把 LLM 当成真正的核心:LLM 是高层入口,真正的系统核心是 engine。LLM.generate() 的执行能力来自底层的 engine,不是 LLM 类本身。
把 AsyncLLMEngine 理解成"只是多了 async/await":它的核心在于 per-request stream 和 output_handler 的设计,解决的是服务场景下异步流式请求处理的问题,不只是语法层面的改变。
把 vllm serve 理解成重新实现了一套推理逻辑:它主要做协议适配和服务封装,调度、KV Cache、GPU 执行全部委托给 engine。
把前后端拆开看作只是代码风格问题:这是为了职责边界清晰、故障隔离、扩展性和并发服务能力,不是风格偏好。
分层设计的综合收益
| 收益维度 | 具体表现 |
|---|---|
| 对用户 | LLM + SamplingParams + generate() 三行代码即可使用 |
| 对服务系统 | AsyncLLMEngine 对接异步请求模型,天然支持多连接并发和流式输出 |
| 对框架本身 | 离线、在线、未来其他接入方式,底层都复用同一套 engine 能力 |
| 对系统演进 | 前端协议层和后端引擎层可以独立演进,互不强耦合 |
| 对稳定性 | 边界清晰后,异常隔离、资源规划、多进程扩展都更容易做 |
这套分层设计是后续理解 scheduler、KV Cache Manager、PagedAttention 的基础——那些机制都运行在 engine core 这一层及其周边,而请求的进入方式、调用链的组织方式,就是由这篇文章描述的这套入口结构决定的。
