vLLM系统拆解-03-入口分层:LLM、LLMEngine、AsyncLLMEngine与vllm serve

刚接触 vLLM 的人通常会有一个困惑:一个推理框架,为什么有 LLMLLMEngineAsyncLLMEngine 三个类,还有一个命令行工具 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

这张图有几个关键点:

  • LLMvllm 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
2
3
4
5
from vllm import LLM, SamplingParams

llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct")
sampling_params = SamplingParams(temperature=0.7, top_p=0.9, max_tokens=128)
outputs = llm.generate(["解释一下什么是 KV Cache"], sampling_params)

三行代码,不需要关心 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() 的流程描述如下:

  1. 为该请求建立独立的 AsyncStream
  2. 处理输入(tokenization 等)
  3. 把请求加入 Detokenizer
  4. 把请求送入独立进程中的 EngineCore
  5. 后台有一个独立的 output_handler 循环,持续从 EngineCore 拉取输出,放回对应请求的 AsyncStream
  6. 调用方迭代这个异步生成器,拿到增量输出

两个关键词值得注意: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 访问。

它实际做的事情

  1. 接受 HTTP 请求
  2. 解析 OpenAI 风格的请求 payload
  3. 调用 AsyncLLMEngine
  4. 把结果重新包装成 OpenAI 风格响应
  5. 处理流式输出(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 这一层及其周边,而请求的进入方式、调用链的组织方式,就是由这篇文章描述的这套入口结构决定的。


系列导航