NOTE 本文基于 vLLM v0.27.1(tag
6e448d0, 2026-08-11)源码深度剖析。文中所有文件路径、类名和行号均以该版本为准;vLLM 迭代很快,阅读时请以你手上的版本对照。
1. 痛点:为什么推理引擎必须持续适配新模型?
1.1 模型算法同质化与工程实现异构化
LLM 模型在算法上高度同质,都是基于 Transformer模型,围绕以下组件构建:
- Embedding;
- Transformer Block;
- Attention;
- Feed-Forward Network;
- Normalization;
- LM Head。
但模型的工程实现上却是高度异构化:
| 维度 | 差异示例 |
|---|---|
| Attention | MHA / GQA / MQA / MLA / Sliding Window |
| 位置编码 | RoPE / ALiBi / Learned / NTK-RoPE |
| 归一化 | LayerNorm / RMSNorm、Pre-Norm / Post-Norm,位置与数量都可能不同 |
| MLP | Dense / MoE / Switch / Top-K routing |
| 激活函数 | GELU / SiLU / SwiGLU / GeGLU |
| KV Cache 格式 | Full KV / Latent(MLA)/ State(Mamba) |
| 特殊头 | MTP Head / EAGLE Head / Medusa Head |
难点不在”差异多”,而在每一种差异都会往下捅穿好几层:
- 换 Attention 变体 → 影响 Attention Kernel 和 KV Cache 布局
- 换 KV Cache 格式 → 影响显存管理 和 调度策略(Mamba 的 state 根本不是块状的)
- 加特殊头 → 影响采样、调度预算 和 KV 回滚逻辑(见第 7.4.3 节)
- 引入 MoE → 影响路由、Token Dispatch、通信和负载均衡
- 改变权重布局 → 影响加载、量化和张量并行
而新架构的发布频率是周级的。所以真正的问题是:如何在不动 Continuous Batching + PagedAttention 这套通用框架的前提下,容纳每个模型的特殊计算路径。
1.2 新模型差异如何向下游扩散?
一个新模型接入推理引擎时,影响通常可以分为三个层次。
| 层次 | 主要内容 | 典型问题 |
|---|---|---|
| 模型层 | 网络结构、权重、配置 | 如何加载权重?如何表示模型结构? |
| 运行时层 | 调度、KV Cache、并行 | 如何执行动态 Batch?如何管理显存? |
| 算子层 | Attention、MoE、量化 Kernel | 如何获得足够的吞吐和延迟? |
理想情况下,模型层只需要调用运行时提供的通用能力,运行时层则通过标准算子完成执行。但现实中的新模型经常会突破既有假设,导致修改从模型层一路传递到 Kernel 层。
以标准 Attention 为例,其基本路径可以抽象为:
Q、K、V
↓
QKᵀ
↓
Scale 与 Mask
↓
Softmax
↓
与 V 相乘
↓
输出
而在实际推理系统中,还需要同时考虑:
- 当前请求处于 Prefill 还是 Decode;
- Batch 中每条序列的长度是否相同;
- KV Cache 是否分页存储;
- 是否启用张量并行或流水线并行;
- 是否使用量化;
- 是否存在特殊的位置编码;
- 当前硬件适合哪一种 Kernel。
因此,推理引擎适配的难点并不是“把论文公式翻译成代码”,而是:
将模型的计算语义,准确映射到一个动态、异步、分布式且高度优化的执行系统中。
1.3 推理引擎适配的核心目标
一个成熟的模型适配方案通常需要同时满足四个目标:
- 正确性:模型输出应与参考实现一致;
- 性能:不能因为复用抽象而损失关键路径性能;
- 可维护性:模型特化逻辑不能污染整个运行时;
- 可扩展性:新模型能够复用已有的执行原语。
这四个目标之间并不总是一致。
| 方案 | 灵活性 | 性能 | 可维护性 | 适用场景 |
|---|---|---|---|---|
| 完全复用通用实现 | 高 | 中 | 高 | 结构接近已有模型 |
| 模型专用 Python 逻辑 | 高 | 中低 | 中低 | 快速验证和早期接入 |
| Custom Op | 中 | 高 | 中高 | 性能关键算子 |
| 专用 Kernel 与运行时改造 | 低 | 很高 | 中 | 范式级模型创新 |
适配工作的本质,就是在这几个目标之间找到合理的工程折中。
2. vLLM 的核心抽象:从模型接入到运行时执行
在前一节中我们讨论过,模型变化往往会跨层扩散:模型结构会影响状态表示,状态表示会影响执行方式,执行方式又会进一步影响缓存、调度和性能优化。
vLLM 应对这一问题的方式,并不是建立一个包含所有模型逻辑的超大基类,而是根据不同问题的变化边界,建立一组相互衔接但职责相对独立的抽象:
配置语义
→ 模型发现
→ 能力契约
→ 模型实例化与权重装配
→ 运行时调度
→ 执行编排
→ Attention / Kernel 执行
从整体上看,vLLM 的模型接入与运行时执行可以抽象为下面这条链路:
flowchart LR
A[Raw arguments / HF config]
A --> B[ModelConfig]
B --> C[ModelRegistry]
C --> D[Model class]
D --> E[ModelLoader]
E --> F[WeightsMapper]
F --> G[Loaded model]
S[Scheduler] -->|SchedulerOutput| W[Worker]
W --> R[ModelRunner]
G --> R
R --> K[KV cache state]
R --> M[Attention Metadata]
M --> AB[Attention Backend]
AB --> X[Hardware kernels]
B --> T[Tokenizer Registry]
B --> MM[MultiModal Registry]
T --> R
MM --> R
这张图可以分成两个相对独立的部分:
- 模型接入链路:
ModelConfig → ModelRegistry → ModelLoader → Loaded model; - 运行时执行链路:
Scheduler → Worker → ModelRunner → Attention Backend → Kernel。
二者通过加载完成的模型实例、运行时协议和输入状态连接起来。模型代码主要描述“模型如何计算”,运行时组件则负责“在动态请求环境中如何组织这次计算”。
2.1 配置语义:ModelConfig 将外部参数收敛为执行意图
新模型接入的第一步并不是直接编写模型代码,而是先明确:
这个模型是什么,以及系统准备以什么方式运行它?
vLLM 通过 ModelConfig 对外部参数和 Hugging Face 配置进行统一解释。它通常需要处理:
- 模型架构信息,例如
architectures、model_type; - runner 类型,例如 generate、pooling、draft;
- 输出或转换类型,例如 embed、classify、none;
- dtype、量化和并行配置;
- 最大上下文长度;
- tokenizer 和多模态相关配置;
- 模型是否具备某些运行能力或限制。
可以将这一过程表示为:
flowchart TD
A[raw args / model name] --> B[ModelConfig]
B --> C[read HF configuration]
C --> D[resolve runner type]
D --> E[resolve conversion type]
E --> F[inspect capabilities]
F --> G[stable execution intent]
ModelConfig 的价值不在于简单保存参数,而在于完成一次语义收敛:
外部配置、命令行参数、模型元数据
↓
统一的执行意图
后续模块拿到的不是一组零散参数,而是已经具有统一含义的配置对象。这样可以避免 registry、loader、runner 分别重新解释同一组参数,减少重复判断和冲突分支。
需要注意的是,ModelConfig 的职责是描述:
“应该以什么方式运行这个模型。”
它不负责:
- 选择并实例化具体模型对象;
- 读取和装配 checkpoint;
- 组织某一轮请求执行;
- 选择具体 attention kernel。
这些职责分别由后续抽象承担。
2.2 模型发现:ModelRegistry 负责定位实现
当系统已经知道模型的架构语义和运行方式后,下一步是确定:
应该使用哪个模型实现类?
这就是 ModelRegistry 的职责。它将配置中识别出的架构名称映射到具体的模型实现,通常支持以下机制:
- 内置模型映射;
- 运行时注册;
- 外部模块或插件注册;
module:class形式的懒加载;- auto 模式下的回退策略。
其基本数据流可以表示为:
flowchart LR
HF[HF architecture metadata] --> REG[ModelRegistry]
REG --> I[inspect_model_cls]
REG --> R[resolve_model_cls]
R --> CLS[model class]
在工程上,模型检查和模型解析通常可以分成两个阶段:
inspect_model_cls:在模型真正实例化之前,检查模型能力、接口或元信息;resolve_model_cls:真正获取并返回用于实例化的模型类。
这种“两阶段”设计能够避免在仅需要判断模型能力时,就提前导入、初始化或执行大量模型相关逻辑。
这里需要明确一个常见误区:
ModelRegistry负责“找到谁”,而不是负责“怎么跑”。
它通常不负责:
- 创建完整模型对象;
- 读取 checkpoint;
- 将权重写入参数;
- 管理设备侧模型生命周期;
- 处理请求级 batch 和 KV cache。
这些工作分别属于 ModelLoader、Worker 和 ModelRunner。
因此,Registry 更接近一个模型实现路由层,而不是完整的模型生命周期管理器。
2.3 能力契约:nn.Module 加 Protocol,而不是统一继承树
模型类被找到之后,还需要回答另一个问题:
不同模型如何接入统一运行时,同时避免被一棵庞大的继承树束缚?
vLLM 的模型抽象更接近:
PyTorch nn.Module
+
能力协议(Protocol / capability contract)
底层要求模型具备 torch.nn.Module 的基础能力,包括:
- 参数注册;
state_dict;- device 迁移;
- train/eval 状态;
- 与 PyTorch 工具链的兼容性。
在此基础上,上层再通过 Protocol 或类似能力契约描述运行时所需的能力,例如:
- 文本生成;
- pooling;
- embedding;
- classification;
- speculative decoding;
- 多模态输入;
- 特殊 attention;
- 混合架构或状态缓存;
- 量化、LoRA 等扩展能力。
概念上可以表示为:
classDiagram
class nn.Module
class VllmModel~Protocol~
class VllmModelForTextGeneration~Protocol~
class SupportsPooling~Protocol~
class SupportsMultiModal~Protocol~
class SupportsSpeculativeDecoding~Protocol~
nn.Module <|-- ConcreteModel
VllmModel <.. ConcreteModel
VllmModelForTextGeneration <.. ConcreteModel
SupportsPooling <.. ConcreteModel
SupportsMultiModal <.. ConcreteModel
SupportsSpeculativeDecoding <.. ConcreteModel
这种设计带来两个主要好处。
第一,兼容已有实现。
模型不必为了接入 vLLM 而重构成某个复杂的继承体系,只要实现运行时真正需要的能力即可。
第二,能力可以组合。
一个模型可以同时支持生成、多模态和 speculative decoding,也可以只实现 pooling 或 embedding,而不必被迫继承一条包含无关方法的基类链。
因此,这里的统一并不一定意味着所有模型具有完全相同的 Python forward 签名。更准确地说,统一的是:
- 输入和输出的语义;
- 运行时所依赖的能力;
- 模型与执行器之间的契约。
而不是强制所有模型在源码层面使用完全相同的参数列表。
2.4 模型落地:ModelLoader 与 WeightsMapper
找到模型类并不意味着模型已经可以运行。实际接入中最容易出错的部分之一,是 checkpoint 权重如何正确装配到模型参数中。
vLLM 将模型初始化和权重装配集中到 loader 相关抽象中。概念上的加载流程如下:
解析模型类
→ 初始化模型结构
→ 读取 checkpoint
→ 权重名称映射
→ 参数拆分或合并
→ 并行切分
→ 参数加载
→ 量化及加载后处理
→ 设备侧整理
可以表示为:
flowchart TD
A[resolved model class] --> B[initialize model]
B --> C[read checkpoint]
C --> D[WeightsMapper / model-specific mapping]
D --> E[split / merge / shard weights]
E --> F[load parameters]
F --> G[quantization and post-processing]
G --> H[model.eval()]
WeightsMapper 的作用不只是简单重命名。实际加载过程可能同时包含以下几类转换。
1、参数命名差异:checkpoint 中的 key 与模型实现中的参数名可能不同,需要进行重命名或前缀转换。
2、参数结构差异:一个 checkpoint tensor 可能需要:
- 拆分成多个模型参数;
- 与其他 tensor 合并;
- 对 QKV、gate/up projection 等结构进行特殊处理;
- 根据模型实现调整参数排列方式。
3、并行切分差异:在 tensor parallel 或 pipeline parallel 场景下,checkpoint 中的完整权重需要按照运行时并行策略切分到不同设备或不同 rank。
4、量化和设备布局差异:权重加载还可能涉及:
- packed weight;
- 量化权重格式;
- 特殊 dtype;
- 延迟初始化;
- 设备侧布局转换;
- 加载后量化处理。
因此,更准确的表述是:
WeightsMapper及模型特定的权重加载逻辑,负责将外部 checkpoint 表示转换为运行时参数表示。这个过程可能同时包含重命名、拆分合并、并行切分、量化适配和设备布局转换。
这也是为什么许多表面上看起来像“推理错误”的问题,实际上可能源于权重装配错误。将加载过程隔离出来,有助于把问题区分为:
模型结构问题
权重映射问题
运行时执行问题
kernel 或硬件问题
从而降低排障复杂度。
2.5 运行时执行边界:Worker + ModelRunner
前面几节关注的是模型如何被识别、实现和加载。从这里开始,讨论模型如何在动态请求环境中运行。
模型类本身不应该直接感知:
- 请求何时进入或退出;
- 当前 batch 如何变化;
- 每个请求本轮推进多少 token;
- KV cache 如何分配和释放;
- prefill、decode 和 speculative decoding 如何协同;
- 当前设备采用哪种执行策略。
这些运行时复杂度主要由 Worker + ModelRunner 承接。
可以将两者理解为一个整体的执行边界:
Scheduler
↓
Worker
↓
ModelRunner
↓
Model / Attention / Kernel
其中:
2.5.1 Worker
Worker 处在执行器和设备运行环境之间,负责承接上层调度结果,并管理设备侧的执行上下文和生命周期。它通常参与:
- 接收调度器输出;
- 初始化和维护
ModelRunner; - 管理设备执行环境;
- 协调 KV cache 和其他运行时状态;
- 发起一次模型执行;
- 将执行结果返回给上层。
2.5.2 ModelRunner
ModelRunner 更关注单步执行编排,包括:
- 组织输入 token;
- 准备 position;
- 组织 batch 和 token layout;
- 准备 KV cache 引用;
- 构造 attention metadata;
- 调用模型 forward;
- 计算 logits;
- 执行采样、pooling 或其他后处理;
- 封装统一的
ModelRunnerOutput。
因此可以用下面的方式区分模型逻辑和系统逻辑:
模型类:
如何完成一次模型计算
Worker / ModelRunner:
在当前动态系统状态下,如何组织这次计算
需要注意的是,二者的边界不是简单的文件边界。实际工程中,RoPE、mask、position、KV 访问和部分输入预处理,可能由模型层、attention 层、ModelRunner 或 backend 共同承担。更准确的划分方式,是依据数据语义和稳定契约,而不是依据某段代码位于哪个文件。
2.6 执行计划输入:SchedulerOutput
Worker + ModelRunner 的第一个重要输入,是 Scheduler 生成的 SchedulerOutput。
它可以被理解为:
Scheduler 针对当前 step 生成的结构化执行计划。
SchedulerOutput 面向的是调度语义,主要描述:
- 本轮需要处理哪些请求;
- 每个请求需要推进多少 token;
- 请求处于何种执行阶段;
- 哪些请求需要进行 prefill 或 decode;
- 是否包含 speculative decoding 相关 token;
- 是否有 encoder 或多模态输入;
- 哪些请求已经完成;
- 哪些 KV 或 encoder 状态需要创建、更新或释放。
数据流可以表示为:
flowchart LR
S[Scheduler] -->|SchedulerOutput| W[Worker]
W --> R[ModelRunner]
R --> O[ModelRunnerOutput]
O --> W
W --> S
SchedulerOutput 的重要性在于,它将调度决策从具体执行方式中分离出来:
Scheduler 决定:
这一轮处理谁、处理多少
ModelRunner 决定:
如何将这个计划组织成设备侧计算
从运行时数据协议的角度看,SchedulerOutput 可以被称为一种 step-level IR,即每一步的执行计划。但这里的 IR 主要是“运行时结构化协议”的含义,并不等同于编译器意义上的完整中间表示。
它不应该直接描述:
- 某个具体 kernel 的调用方式;
- 某种硬件的线程布局;
- 某个 attention backend 的内部数据结构;
- 模型实现的具体 forward 细节。
这些信息应在执行侧进一步细化。
2.7 从执行计划到算子输入:Attention Metadata
Attention Metadata 与 SchedulerOutput 有关,但二者并不是同一层次的对象。
更准确地说:
SchedulerOutput是 Scheduler 面向执行器产生的高层执行计划;Attention Metadata则是 ModelRunner 根据该计划、当前 KV cache 状态和 backend 需求构造出的 attention 算子输入描述。
其构造过程可以表示为:
flowchart TD
S[Scheduler] -->|SchedulerOutput| R[ModelRunner]
K[KV cache state] --> R
I[Input token layout] --> R
C[Backend capabilities] --> R
R --> B[AttentionMetadataBuilder]
B --> M[Attention Metadata]
M --> A[Attention layer / backend]
Attention metadata 可能包含:
- query 起点;
- 每个序列的长度;
seq_lens;block_table;slot_mapping;- prefill/decode 边界;
- token 到 KV block 的映射;
- 局部 attention、滑动窗口或其他 attention 模式所需的信息;
- 特定 backend 所需的布局和执行参数。
它的作用是把高层运行时状态转换成 attention kernel 可以理解的形式:
请求级调度状态
↓
token 与 KV 的逻辑关系
↓
attention kernel 的输入布局
因此,Attention Metadata 并不是调度器的原始输出,也不是模型结构本身的一部分。它是执行侧的适配层,负责连接:
SchedulerOutput
→ ModelRunner
→ AttentionMetadataBuilder
→ Attention Metadata
→ Attention Backend
三者的职责可以压缩为:
SchedulerOutput 决定这一轮执行什么;
ModelRunner 决定如何组织这次执行;
Attention Metadata 描述 attention 应该看见什么。
这也是 vLLM 能够演进调度策略而不必频繁修改模型定义的重要原因之一。只要调度器仍然通过稳定的执行协议表达计划,执行侧就可以将新的调度策略转换为对应的 metadata 和输入布局。
2.8 模型计算与 Attention Backend:从语义到 Kernel
在 ModelRunner 准备好输入和 attention metadata 后,系统进入模型计算阶段。
模型逻辑层主要负责表达模型本身的计算语义,例如:
- token embedding;
- position embedding 或 RoPE;
- Transformer block;
- attention;
- MLP;
- residual connection;
- logits;
- pooling 或分类表示。
Attention Backend 则负责把 attention 语义映射到具体执行实现。它通常需要处理:
- paged KV cache;
- prefill 和 decode;
- 不同 batch 和 sequence layout;
- MLA 或其他特殊 attention;
- 混合 attention;
- 不同硬件平台;
- 不同 dtype 和 kernel 实现。
可以将这一层表示为:
flowchart LR
R[ModelRunner] --> I[Model inputs]
R --> M[Attention Metadata]
I --> ML[Model logic]
M --> AL[Attention layer]
AL --> AB[Attention Backend]
AB --> K1[Flash / Paged / Triton kernels]
AB --> K2[Hardware-specific kernels]
需要对“算子选择”作一个更精确的描述。
ModelRunner 通常负责准备执行上下文和 metadata;具体 attention 层或 backend 再根据:
- attention 类型;
- prefill/decode 模式;
- metadata 中的布局;
- dtype;
- head dimension;
- 硬件能力;
- backend 支持情况;
选择合适的具体实现。
因此,Attention Backend 不是一个简单的算子实现集合,而是一个包含以下职责的执行适配层:
模型声明 attention 需求
↓
ModelRunner 提供本轮执行 metadata
↓
Backend 判断可用实现
↓
选择并调用具体 kernel
模型代码因此不需要直接绑定某个硬件平台或某个具体 attention kernel。模型接入主要关注“attention 的语义”,而硬件后端负责“如何高效执行这个语义”。
2.9 横切抽象:KV、Tokenizer、多模态与插件
前面的内容构成了主执行链路。但一个模型能否稳定上线,还取决于若干横切抽象。
2.9.1 KV Cache 抽象
KV cache 不应绑定到某个模型实现,而应作为独立的运行时状态管理机制。
相关抽象通常需要统一:
- cache 分组;
- layer 级缓存;
- block 布局;
- dtype;
- 初始化;
- 分配、更新与释放;
- paged cache 访问;
- hybrid attention 的缓存管理。
可以将其理解为:
模型只声明需要什么状态;
KV 系统负责状态如何分配、保存和访问。
不过,这里需要保留一个边界意识:
KV cache 抽象统一的是缓存管理和访问语义,并不意味着所有模型内部状态都能无差别地表示为传统 K/V tensor。
对于 Mamba 类或混合架构,系统可能还需要支持 state cache、循环状态或其他形式的持久化执行状态。因此,KV/cache 抽象应当被理解为更广义的推理状态管理协议。
2.9.2 Tokenizer Registry
Tokenizer Registry 将 tokenizer 的特殊处理从模型执行路径中解耦出来,可以承接:
- tokenizer mode;
- 专有 tokenizer 实现;
- truncation 方向;
- chat template;
- 特殊 token;
- tokenizer 与模型配置之间的差异。
这样,新增 tokenizer 模式时,不必在每个模型实现中复制条件分支。
2.9.3 MultiModal Registry
多模态模型的复杂性往往不只来自模型结构,还来自输入预处理链。MultiModal Registry 可以将模型类与具体 processor 松耦合,承接:
- 图片、视频、音频等输入处理;
- 多模态特征提取;
- 模态特征与文本 token 的对齐;
- encoder 输入组织;
- 不同模型所需的 processor 工厂。
其核心思想是:
模型结构演进
与
多模态数据处理演进
相互解耦
2.9.4 Plugin / Extension 机制
随着模型、硬件和采样策略不断增加,仅依靠核心代码内置实现会导致系统越来越庞大。因此,插件机制可以进一步支持外部注册和扩展,例如:
- 模型实现;
- 自定义 layer;
- attention backend;
- tokenizer;
- 多模态 processor;
- sampler;
- 量化实现;
- 输出处理逻辑。
插件机制的价值在于:
在不修改核心执行路径的前提下,为系统增加新的模型能力或硬件能力。
不过,插件并不是对所有内部对象开放任意修改,而应建立在清晰的注册点和稳定契约之上。否则,插件只会把核心系统中的隐式耦合转移到外部。
2.10 动态运行时与静态执行图:抽象边界的性能代价
运行时抽象提高了灵活性,但也会增加编译优化和图捕获的难度。
动态推理环境通常具有以下特征:
- batch size 持续变化;
- sequence length 持续变化;
- 请求不断加入和退出;
- KV cache 地址不固定;
- prefill、decode 和 speculative decoding 路径不同;
- 不同请求可能具有不同的执行阶段和输入布局。
而编译器和图捕获机制通常更擅长处理:
- 相对稳定的计算图;
- 稳定或可预测的 tensor shape;
- 明确的控制流;
- 固定的内存布局;
- 可重复执行的 kernel 序列。
因此,vLLM 一类的推理引擎需要在动态调度和静态优化之间建立边界:
动态调度、灵活执行
↕
稳定计算片段、编译优化
一个重要的工程思想是:
不是消除动态性,而是将动态性集中在运行时边界内,再把稳定部分下沉给编译器和硬件后端。
例如,系统可以将动态因素保留在:
- Scheduler;
- Worker;
- ModelRunner;
- KV cache 状态;
- Attention Metadata;
- 输入 buffer 和运行时参数。
同时,将相对稳定的计算片段交给:
torch.compile;- CUDA Graph;
- shape bucketing;
- kernel fusion;
- backend-specific graph capture。
可以将这种分工表示为:
动态部分:
请求调度
batch 组织
token 数量
KV block 地址
执行路径选择
静态部分:
稳定的模型子图
固定范围的 shape bucket
可复用的 kernel 序列
硬件相关的融合算子
因此,前文介绍的抽象不仅服务于代码可维护性,也服务于性能优化。SchedulerOutput 和 Attention Metadata 将动态状态显式化,使编译器和 kernel 不必理解完整的请求调度逻辑,而只需要消费结构化的运行时输入。
2.11 新模型接入时的判断路径
这套抽象的工程价值,最终体现在一个具体问题上:
当新模型接入失败时,应该首先修改哪一层?
可以按照变化来源进行判断。
| 变化类型 | 优先检查的层 |
|---|---|
| 模型架构无法识别 | ModelConfig、ModelRegistry |
| runner 或输出类型判断错误 | ModelConfig、能力协议 |
| forward 或模型结构不兼容 | Model class、Protocol、Attention layer |
| checkpoint 参数找不到 | ModelLoader、WeightsMapper |
| QKV 或特殊权重加载错误 | 模型特定加载逻辑、并行切分逻辑 |
| batch 或 token 数量异常 | Scheduler、SchedulerOutput、ModelRunner |
| KV cache 行为异常 | KV cache 配置、状态管理、metadata builder |
| attention 结果异常 | Attention Metadata、Attention layer、Backend |
| tokenizer 输入异常 | Tokenizer Registry |
| 图片、视频等输入异常 | MultiModal Registry |
| 性能不稳定 | ModelRunner、Attention Backend、Graph Capture、编译配置 |
也可以将接入过程概括为下面的判断顺序:
先确认模型是否被正确识别
↓
再确认模型类和能力契约是否匹配
↓
再确认 checkpoint 是否正确装配
↓
再确认运行时输入和 KV 状态是否正确
↓
最后检查 attention backend、编译和硬件性能
这样可以避免在模型尚未正确加载时,就直接从 kernel 或调度器开始排查。
2.12 小结:动态性上浮,静态性下沉
vLLM 的核心设计可以概括为:
配置层收敛执行意图,注册层定位模型实现,契约层表达模型能力,加载层隔离权重差异;Scheduler 生成执行计划,Worker 和 ModelRunner 组织动态执行,Attention Metadata 将运行时状态转换为算子输入,Attention Backend 再将模型语义映射到具体硬件 kernel。
这里需要特别区分几个对象之间的关系:
ModelConfig
表达“准备如何运行”
ModelRegistry
决定“使用哪一个实现”
ModelLoader / WeightsMapper
负责“如何把模型落地”
SchedulerOutput
描述“这一轮执行什么”
Worker / ModelRunner
负责“如何组织这次执行”
Attention Metadata
描述“attention 应该看见什么”
Attention Backend
决定“用什么 kernel 执行”
因此,SchedulerOutput、Attention Metadata 和 ModelRunner 并不是三个平行组件,而是一条逐级细化的数据流:
调度计划
→ 执行编排
→ Attention 输入描述
→ Kernel 执行
这套架构背后的工程哲学可以进一步概括为:
-
动态性上浮
将请求队列、调度策略、batch 变化和执行路径选择集中在 Scheduler、Worker、ModelRunner 及其运行时协议中。 -
静态性下沉
将稳定的模型计算、attention 子图和硬件相关 kernel 下沉给编译器、CUDA Graph 和 Attention Backend。 -
以契约连接两者
通过 Registry、Protocol、WeightsMapper、SchedulerOutput 和 Attention Metadata,在动态系统与静态算子之间建立稳定的数据和能力契约。
最终,支持一个新模型不再意味着对整个推理引擎进行全栈修改,而是先判断变化发生在哪个边界:
配置变化 → ModelConfig
识别变化 → ModelRegistry
能力变化 → Protocol / model class
权重变化 → ModelLoader / WeightsMapper
执行变化 → Scheduler / Worker / ModelRunner
缓存变化 → KV cache / state management
Attention变化 → Metadata / Attention layer / Backend
输入变化 → Tokenizer / MultiModal Registry
性能变化 → Backend / Graph Capture / Compiler
这就是 vLLM 能够在保持高性能的同时快速支持新模型的关键:它并没有试图消除模型差异,而是将不同类型的差异放置到合适的抽象边界中,让模型逻辑、运行时调度、状态管理和硬件执行能够相对独立地演进。
3. 适配机制的演进:从临时补丁到编译化扩展
3.1 早期方式:硬编码与模型专用实现
最直接的适配方式,是为新模型增加一个专用实现。
优点是:
- 开发路径短;
- 便于快速验证;
- 可以直接表达模型特有逻辑。
缺点也很明显:
- 模型代码容易与运行时耦合;
- 重复实现大量已有逻辑;
- 难以复用现有 Kernel;
- 后续维护成本高;
- 不同模型之间容易形成分叉。
这种方式适合模型早期接入,但不适合作为长期架构。
3.2 热插拔适配:Monkey Patch
当模型已有实现,但某些模块暂时无法直接复用时,可以采用热插拔方式替换局部逻辑。
例如:
原始模型实现
↓
替换 Attention 模块
↓
替换 RoPE 或 MLP
↓
复用其余模型结构
这种方法可以降低重复代码,但通常存在以下问题:
- 调用关系不够显式;
- 依赖模块内部实现细节;
- 不同版本之间容易失效;
- 调试和性能分析较困难;
- 不利于长期维护。
因此,Monkey Patch 更适合过渡阶段,而不是稳定扩展接口。
3.3 Custom Op:将性能关键路径下沉为标准算子
当模型结构已经能够通过通用 Python 模块表达,但性能关键路径不足时,Custom Op 是更合理的方案。
Custom Op 通常承担三类职责:
- 为 Python 层提供稳定接口;
- 将计算转发给 CUDA、Triton 或其他后端;
- 隐藏不同硬件后端之间的实现差异。
典型调用路径如下:
Python Module
↓
统一 Op 接口
↓
后端 Dispatch
↓
CUDA / Triton / CPU Kernel
图 7-5 Custom Op 的分层结构
适合下沉为 Custom Op 的部分包括:
- Attention;
- RMSNorm;
- RoPE;
- Fused MLP;
- MoE Routing;
- Quantization;
- Sampling。
需要强调的是,Custom Op 主要解决“算子如何高效执行”,并不自动解决:
- 调度器如何组织请求;
- KV Cache 如何分配;
- 多卡之间如何通信;
- MTP 如何回滚;
- 不同执行阶段如何切换。
当模型创新涉及这些运行时语义时,仅增加 Custom Op 通常是不够的。
3.4 IR:从算子注册走向计算图与后端解耦
IR 可以看作位于模型表达和硬件执行之间的中间层。
它不直接描述某块 GPU 上的具体线程布局,而是描述:
- 计算操作;
- 张量依赖;
- 数据流关系;
- Shape 与布局约束;
- 可融合或可重排的计算;
- 后端执行需求。
IR 驱动的模型执行路径:
模型代码
↓
IR 表示
↓
图变换与优化
↓
后端 Kernel
↓
硬件执行
相较于单纯的算子注册,IR 可以进一步支持:
- 算子融合;
- 内存访问重排;
- 自动选择后端;
- 静态与动态 Shape 的统一表达;
- 不同硬件之间的代码生成;
- 模型结构与 Kernel 实现解耦。
不过,在推理系统中,IR 不能脱离动态运行时单独存在。它仍然需要与以下信息协同:
- 动态 Batch;
- KV Cache Metadata;
- Prefill/Decode 阶段;
- 并行拓扑;
- 显存约束;
- 请求优先级。
因此,更现实的方向不是用静态编译完全替代动态调度,而是:
由运行时决定执行计划,由 IR 和编译器优化计划中的计算部分。
3.5 从“支持模型”到“组合计算原语”
当推理引擎将 Attention、MoE、MTP 等能力抽象为可组合的计算原语后,接入新模型就不再是从头实现整个网络,而是组合已有能力。
标准 Attention 原语
+
标准 MoE 原语
+
标准 RoPE 原语
+
标准 KV Cache 原语
↓
新的模型结构
这意味着模型适配的基本单位正在发生变化:
过去:适配一个完整模型
现在:组合一组标准计算原语
但这种方法的前提是,模型创新仍然能够被现有原语表达。如果模型改变了原语本身的语义,或者改变了多个原语之间的协作方式,就需要扩大抽象边界。
4. 适配的边界:为什么仍然需要特化 Kernel?
4.1 抽象机制解决了什么问题?
通用抽象主要解决以下问题:
- 统一模型加载流程;
- 隔离模型结构与调度器;
- 复用 KV Cache 管理;
- 复用并行和通信机制;
- 减少重复实现;
- 提高新模型接入速度。
对于结构接近已有模型的架构,抽象能够显著降低适配成本。
4.2 抽象机制解决不了什么问题?
当模型改变以下内容时,通用抽象可能会失效:
- 数据表示方式;
- 内存访问模式;
- 算子融合边界;
- 通信与计算的重叠方式;
- Decode 阶段的执行粒度;
- 调度器与模型计算之间的关系。
例如,某个模型使用特殊的低秩 KV 表示。此时,继续将它强行转换为标准 K/V,虽然可以复用已有 Attention 接口,但可能带来:
- 额外的显存读写;
- 额外的矩阵变换;
- 更高的带宽压力;
- 无法发挥模型设计本身的优势。
这时,特化 Kernel 并不是“为了追求极限性能而过度优化”,而是模型语义变化后的必然结果。
4.3 通用算子与特化算子的永恒博弈
| 维度 | 通用实现 | 特化实现 |
|---|---|---|
| 接入速度 | 快 | 慢 |
| 模型复用 | 强 | 弱 |
| 峰值性能 | 中等 | 高 |
| 维护成本 | 低 | 高 |
| 适配范围 | 广 | 窄 |
| 对模型创新的支持 | 有限 | 强 |
可以将适配边界概括为:
当模型只改变“计算结构”时,通用抽象通常足够;当模型改变“数据流、内存流或执行流”时,就需要引入特化实现。
5. 一个新模型接入 vLLM 的完整路径
前文介绍了 vLLM 的核心抽象及其职责边界。这一节我们从工程实施角度说明当一个新模型需要接入 vLLM 时,应该按照什么顺序分析、实现、验证和优化。
从前面vLLM的核心抽象我们知道,新模型接入不是“新增一个模型类”这么简单。一个完整的接入过程,通常需要同时处理以下问题:
模型是否可以复用现有实现
→ 模型结构是否正确
→ checkpoint 权重是否正确装配
→ 单卡最小路径是否可运行
→ KV Cache 和 Attention 是否正确
→ 调度与执行链路是否贯通
→ 并行、量化和高级能力是否可用
→ 性能是否达到预期
→ 正确性和分布式测试是否通过
一个重要原则是:先建立正确、可观察、可复现的最小实现,再逐步引入并行、量化、图捕获和特化算子等优化。
如果一开始就同时修改模型结构、权重加载、KV Cache、并行策略和 kernel,任何错误都会被多个变量掩盖,排障成本会显著上升。
5.1 模型差异分析与复用决策
接入新模型的第一步不是立即编写代码,而是确定新模型与现有实现之间的差异,以及这些差异会影响哪些层级。
首先需要确认模型的基本形态:
- 是 decoder-only、encoder-only 还是 encoder-decoder;
- 是否支持标准文本生成、pooling、embedding 或分类;
- 是否包含 encoder 输出缓存;
- 是否使用标准 Transformer block;
- Attention 是 MHA、MQA、GQA、MLA,还是其他特殊形式;
- MLP 是标准 FFN、SwiGLU、MoE,还是其他结构;
- 是否使用特殊位置编码、attention mask 或 position 计算;
- 是否包含 speculative decoding、MTP 或其他多预测路径;
- 是否支持多模态输入;
- 是否包含 KV Cache 之外的 recurrent state 或其他持久化状态;
- checkpoint 的参数命名、布局和格式是否与现有实现兼容。
可以先建立一张模型差异表:
| 检查项 | 现有实现 | 新模型 | 影响层级 | 是否需要改造 |
|---|---|---|---|---|
| 模型架构 | decoder-only | decoder-only | Config / Model | 是/否 |
| Attention | 标准 MHA/GQA | 特殊 Attention | Model / Backend | 是/否 |
| 状态管理 | 标准 K/V Cache | 压缩 KV 或 recurrent state | Cache / Metadata | 是/否 |
| MLP | Dense FFN | MoE | Model / Parallel | 是/否 |
| 位置编码 | RoPE | 特殊 RoPE | Model / Runner | 是/否 |
| 输入形式 | 纯文本 | 文本加图像 | Tokenizer / Multimodal | 是/否 |
| 输出结构 | 单一 LM Head | 多预测头 | Runner / Sampling | 是/否 |
| 权重命名 | 与实现一致 | 命名不同 | Loader / Mapper | 是/否 |
| 权重布局 | 标准布局 | 融合或打包布局 | Loader / Quantization | 是/否 |
| 并行方式 | Tensor Parallel | 需要 Expert Parallel | Distributed | 是/否 |
差异分析的最终目标不是列出所有不同,而是做出接入决策。通常有三种结果:
A. 直接复用现有模型实现
B. 复用公共模块,仅局部改造模型结构或权重加载
C. 新增模型实现,并扩展运行时状态、执行协议或 backend
可以按照以下顺序判断:
- 新模型的计算图是否与现有模型基本一致;
- 差异是否仅限于配置字段、模型名称或参数命名;
- 差异是否可以由已有的 attention、MLP、MoE 或 position 模块表达;
- checkpoint 是否只需要增加映射规则;
- 现有
ModelRunner是否能够提供模型所需的输入; - 现有 KV Cache 或状态管理机制是否能够表示模型的推理状态;
- 现有 Attention Backend 是否能够执行模型的 attention 语义;
- 新模型是否需要新的并行、通信或输出协议。
其中,应该尽量遵循:
优先复用公共组件,局部适配模型差异;只有当现有抽象无法表达新模型语义时,才扩展运行时协议或底层 backend。
这一阶段的产物应当是一份明确的接入设计,而不是一组尚未验证的代码改动。至少需要确定:
- 复用哪些已有模块;
- 新增哪些模型组件;
- 是否需要新的权重映射;
- 是否需要扩展能力协议;
- 是否需要新的 cache/state 表示;
- 是否需要新的 attention backend;
- 首次接入版本只支持哪些能力;
- 哪些能力留待后续扩展。
5.2 实现模型结构与权重装配
模型实现阶段需要解决两个相互关联但应当分开验证的问题:
模型结构实现:
forward 计算是否正确
权重装配实现:
checkpoint 参数是否正确加载到模型结构
5.2.1 模型结构实现
模型类应首先正确描述模型本身的计算语义,包括:
- embedding;
- position 或 RoPE;
- attention;
- MLP 或 MoE;
- residual connection;
- normalization;
- logits;
- pooling 或其他输出头。
实现时应尽量复用 vLLM 已有的公共组件,例如:
- 线性层和并行线性层;
- attention 层;
- RMSNorm 或其他 normalization;
- RoPE;
- MoE 路由和 expert 模块;
- 量化线性层;
- 输出头;
- 已有的输入和输出协议。
如果新模型只是在层配置、激活函数或参数组织方式上存在差异,通常不应复制一整套已有实现,而应通过组合已有模块完成适配。
模型实现还需要明确自身支持的能力。例如:
- 是否支持文本生成;
- 是否支持 pooling;
- 是否支持多模态输入;
- 是否支持 speculative decoding;
- 是否需要特殊的 cache/state;
- 是否需要特殊 attention metadata。
这些能力应通过已有的能力契约或 Protocol 表达,而不是通过运行时对具体模型类进行大量类型判断。
5.2.2 权重装配
权重加载通常需要处理以下转换:
- 参数名称转换;
- 前缀和模块路径转换;
- QKV 权重合并或拆分;
- gate/up projection 的合并或拆分;
- MoE expert 权重组织;
- Tensor Parallel 切分;
- Pipeline Parallel 所需的层分配;
- 权重转置和重排;
- packed weight 或量化格式转换;
- scale、zero-point 等量化参数加载;
- 加载后的设备布局转换。
因此,WeightsMapper 的职责不只是简单的字符串替换。更准确地说,它负责将外部 checkpoint 表示转换为运行时参数表示,可能同时包含:
参数重命名
→ 参数拆分或合并
→ 并行切分
→ 格式转换
→ 量化适配
→ 设备布局转换
权重加载阶段应建立明确的检查机制,至少包括:
- checkpoint 中的参数是否全部被消费;
- 是否存在未匹配参数;
- 是否存在重复匹配;
- 参数 Shape 是否一致;
- 参数 dtype 是否符合预期;
- QKV、gate/up 和 MoE expert 等融合参数是否按预期处理;
- 不同并行 rank 加载的参数是否互补;
- 量化权重与对应 scale 是否成对加载。
建议将权重装配的验收标准明确为:
无未匹配参数
无重复加载参数
关键参数 Shape 全部一致
参数数量和统计量符合预期
单层输出与参考实现一致
端到端 logits 误差在允许范围内
需要注意的是,权重加载错误不一定会导致程序立即崩溃。参数名称碰巧匹配、Shape 恰好兼容,仍然可能产生数值错误,最终表现为:
- logits 偏差;
- 生成结果异常;
- 某些输入长度下结果错误;
- 多卡结果与单卡不一致;
- 模型质量明显下降但程序运行正常。
因此,不能只通过“模型成功加载”判断权重装配正确。
推荐采用以下验证顺序:
参数匹配检查
→ 关键参数 Shape 检查
→ 参数统计量检查
→ 单层输出对比
→ hidden states 对比
→ logits 对比
→ 端到端生成结果对比
直接比较最终生成文本只能作为最后一层验证,因为采样和离散 token 可能掩盖中间层的数值误差。
5.3 打通单卡最小可运行路径
模型结构和权重装配完成后,不应立即接入所有高级能力。首先应建立一个简单、稳定且容易观察的最小运行路径。
建议按照以下顺序逐步推进:
模型实例化
→ 单卡加载
→ 单请求 Prefill
→ 单请求 Decode
→ 多轮 Decode
→ 基本采样
→ 简单动态 Batch
此阶段暂时不引入或尽量避免:
- Tensor Parallel;
- Pipeline Parallel;
- Expert Parallel;
- 量化;
- CUDA Graph;
- Prefix Cache;
- speculative decoding;
- 自定义特化 kernel;
- 复杂的多模态输入路径。
最小路径的目标不是获得高性能,而是建立一个可以与参考实现进行稳定对比的正确性基线。至少需要确认:
- 模型可以正确实例化;
- 权重可以完整加载;
- 单步 forward 结果正确;
- 单次 Prefill 结果正确;
- Prefill 后可以继续 Decode;
- position 和 attention mask 正确;
- logits 能够正确传递到 sampling;
- EOS 和停止条件正常;
- 请求完成后显存和运行时状态可以释放。
这一阶段尤其需要关注 Prefill 和 Decode 之间的状态衔接。对于同一个序列,可以比较:
一次性 Prefill 得到的 logits
与:
分段 Prefill / Decode 得到的对应 logits
如果二者存在非预期差异,通常应优先检查:
- position 计算;
- attention mask;
- KV Cache 写入;
- KV Cache 读取;
- sequence length;
- token 到 cache block 的映射;
- prefill 和 decode 的路径切换。
只有最小单卡路径稳定后,才适合继续引入动态 Batch 和多卡执行。
5.4 适配运行时状态与 Attention 执行
模型结构正确并不代表已经完成运行时接入。新模型还必须能够与调度、KV Cache、Attention Metadata 和 backend 协同工作。
运行时执行链路可以表示为:
Scheduler
↓
SchedulerOutput
↓
Worker
↓
ModelRunner
↓
Attention Metadata
↓
Model / Attention Backend
↓
KV Cache 访问
其中:
SchedulerOutput描述当前 step 需要处理哪些请求以及每个请求推进多少 token;ModelRunner将调度计划转换为模型输入、batch 布局和运行时状态;Attention Metadata描述当前 attention 需要使用的序列长度、KV block 和 token 映射;- Attention Backend 根据 metadata 和硬件能力选择具体执行实现;
- KV Cache 或其他状态系统负责保存和访问跨 step 的推理状态。
因此,Attention Metadata 并不是调度器直接产生的原始结果,而是由 ModelRunner 根据以下信息构造:
SchedulerOutput
+ 当前输入 token 布局
+ KV Cache / state 状态
+ Attention 类型
+ Backend 能力
↓
Attention Metadata
新模型接入时,应重点确认以下问题。
5.4.1 Attention 路径
- Attention 是否属于现有 backend 支持的类型;
- MHA、MQA、GQA 或 MLA 的 head 数是否正确;
- Q、K、V 的 Shape 和布局是否符合预期;
- prefill 和 decode 是否使用相同的状态语义;
- 是否需要特殊的 mask、position 或 RoPE;
- 是否存在局部 attention、滑动窗口或混合 attention;
- 是否需要新增 attention metadata 字段。
5.4.2 KV Cache 或其他推理状态
- cache 的层数是否正确;
- KV head 数和 head dimension 是否正确;
- cache dtype 是否匹配;
- cache block 的布局是否正确;
- block table 和 slot mapping 是否正确传递;
- cache block 的分配、复用和释放是否正确;
- prefill 写入的状态是否能被 decode 正确读取;
- 模型是否需要 K/V 之外的 recurrent state、压缩状态或其他持久化状态。
对于非标准模型,不应强行将所有推理状态表示为传统 K/V tensor。如果模型使用 recurrent state、压缩 KV 或混合缓存,应首先明确其状态语义,再决定是否复用现有 cache 抽象,还是扩展新的状态管理协议。
5.4.3 并行路径
并行能力建议采用逐级扩展的方式:
单卡
→ Tensor Parallel
→ Pipeline Parallel
→ Expert Parallel
→ 多节点通信
需要分别确认:
- Tensor Parallel 下 Q、K、V、MLP 权重如何切分;
- Pipeline Parallel 下层之间的 hidden state 如何传递;
- MoE 是否需要 Expert Parallel;
- token dispatch 和 combine 是否保持顺序;
- 通信是否会改变 token 对齐;
- 通信是否能够与计算重叠;
- 不同并行规模下输出是否与单卡结果一致。
首次接入时不必一次性支持所有并行方式。应根据模型规模和实际部署需求,先实现最小必要的并行能力。
5.5 注册模型并验证端到端执行
模型结构、权重装配和最小运行时路径稳定后,需要将模型接入模型发现机制,并打通完整执行链路。
模型发现与加载链路为:
ModelConfig
↓
ModelRegistry
↓
Model class
↓
ModelLoader
↓
WeightsMapper
↓
Loaded model
运行时执行链路为:
Scheduler
↓
SchedulerOutput
↓
Worker
↓
ModelRunner
↓
Attention Metadata
↓
Model / Attention Backend
↓
ModelRunnerOutput
这一阶段应分别验证模型发现、模型加载和请求执行,避免将不同问题混在一起。
5.5.1 模型发现与加载验证
重点检查:
- 模型架构名称能否被正确识别;
ModelConfig是否解析出正确的 runner 类型;ModelRegistry是否能够返回正确的模型类;- 模型类是否能够被正常导入和实例化;
- checkpoint 是否能够完整加载;
- dtype、量化和并行配置是否生效;
- 单卡初始化是否成功;
- 是否存在未匹配或重复加载的参数。
5.5.2 单卡端到端验证
重点覆盖:
- 单请求 Prefill;
- 单请求 Decode;
- 多轮 Decode;
- 动态 Batch;
- 流式输出;
- EOS 和停止条件;
- 请求取消;
- 请求完成后的 KV Cache 回收;
- 显存释放;
- 异常请求后的状态恢复。
5.5.3 服务生命周期验证
除了正常生成路径,还应验证:
- 请求在 Prefill 阶段取消;
- 请求在 Decode 阶段取消;
- 多个请求同时完成;
- 长请求被新请求打断或插入;
- OOM 后服务是否能够正确报错;
- 异常请求是否会残留 cache block;
- 多次加载和卸载模型后是否出现显存泄漏。
“模型能够被 Registry 找到”只说明模型发现链路正常,并不代表模型已经能够在动态请求环境中稳定执行。只有上述运行时行为全部贯通,才可以认为模型完成了基础接入。
5.6 扩展并行、量化和其他高级能力
基础单卡路径稳定后,再逐步接入高级能力。建议按照实际需求分阶段推进,而不是将所有能力作为首次接入的前置条件。
5.6.1 并行能力
可以按照以下顺序扩展:
单卡
→ Tensor Parallel
→ Pipeline Parallel
→ Expert Parallel
→ 多节点部署
每增加一种并行方式,都需要重新验证:
- 参数切分是否正确;
- rank 间输出是否一致;
- 通信数据是否正确;
- token 顺序是否保持;
- cache 和状态是否与并行布局匹配;
- 计算和通信是否存在不必要的同步。
5.6.2 量化能力
量化接入不仅是改变 dtype,还可能涉及:
- checkpoint 格式;
- packed weight;
- scale 和 zero-point;
- 权重布局;
- 激活量化;
- kernel 支持;
- 并行切分顺序;
- 量化误差。
因此,量化应在浮点或高精度基线稳定后进行,并比较:
高精度模型
→ 量化模型数值误差
→ 端到端生成质量
→ 性能与显存收益
5.6.3 Prefix Cache、Speculative Decoding 和 MTP
这些能力会改变运行时状态和执行路径,不应仅被视为模型开关。
接入时需要确认:
- Prefix Cache 是否适用于模型的状态语义;
- speculative decoding 是否需要额外的 draft model 或多预测头;
- MTP 的 hidden state 和 logits 如何组织;
- 接受或拒绝 token 后,KV Cache 如何回滚或更新;
- 多预测路径是否与 sampling 和停止条件兼容。
首次接入时,如果这些能力不是模型上线的必要条件,可以先明确标记为暂不支持,而不是在基础实现中加入未经验证的分支。
5.7 性能分析与选择性特化
正确性和稳定性通过后,才进入性能优化阶段。性能优化必须建立在可复现的测试配置和稳定的正确性基线上。
建议先固定以下条件:
- 模型权重和版本;
- GPU 型号;
- dtype 和量化配置;
- 输入长度;
- 输出长度;
- batch size;
- 并行规模;
- sampling 参数;
- cache 配置;
- 软件和驱动版本。
随后使用端到端 profiling 定位关键瓶颈,而不是一开始就重写某个算子。
性能指标应按照场景区分。
| 场景 | 主要指标 |
|---|---|
| 单请求 Prefill | TTFT、Prefill 延迟、Prefill 吞吐 |
| 单请求 Decode | ITL、每秒生成 token 数 |
| 动态 Batch | 总吞吐、P95/P99 延迟、batch 利用率 |
| 长上下文 | KV Cache 占用、显存带宽、OOM 情况 |
| Prefix Cache | 命中率、命中后的 TTFT、cache 管理开销 |
| 多卡推理 | 通信占比、扩展效率、计算通信重叠 |
| MoE 模型 | 路由开销、dispatch/combine 开销、expert 利用率 |
| 量化模型 | 显存占用、量化误差、反量化开销 |
推荐使用以下优化流程:
建立端到端基线
↓
采集算子、访存、通信和调度指标
↓
定位关键瓶颈
↓
判断瓶颈属于计算、访存、通信、调度还是采样
↓
选择性引入优化
↓
重新验证正确性和性能收益
常见瓶颈包括:
- Attention 计算;
- KV Cache 访存;
- MoE 路由、dispatch 和 combine;
- Tensor Parallel 或 Expert Parallel 通信;
- 量化和反量化;
- kernel launch;
- 动态 Shape 导致的图捕获失败;
- Python 或 CPU 侧调度;
- sampling;
- 内存分配和 cache 管理。
因此,Attention 并不总是第一瓶颈。特别是在小 batch Decode 场景中,系统可能受限于 kernel launch、访存、采样或通信,而不是纯粹的矩阵计算。
只有当 profiling 证明现有实现确实是关键路径时,才应考虑:
- 新增特化 kernel;
- 扩展 Attention Backend;
- 增加 shape bucket;
- 引入 CUDA Graph;
- 使用
torch.compile; - 融合量化和计算;
- 优化通信与计算重叠;
- 修改 ModelRunner 的输入布局。
每次优化都必须重新进行数值和端到端验证,避免为了局部吞吐牺牲模型正确性或请求稳定性。
5.8 正确性、性能与分布式验收
模型接入的最终验收至少包括三类测试:
- 正确性测试;
- 性能测试;
- 分布式和故障恢复测试。
5.8.1 正确性测试
正确性测试应覆盖模型结构、权重加载、运行时状态和服务行为。
基础数值测试包括:
- 与参考框架比较 hidden states;
- 与参考框架比较 logits;
- 比较不同输入长度下的结果;
- 比较不同 batch size 下的结果;
- 检查 Prefill 与 Decode 的一致性;
- 检查不同 dtype 下的误差;
- 检查量化模型的误差范围;
- 检查单卡与多卡结果的一致性。
生成行为测试包括:
- greedy decoding;
- temperature、top-k、top-p 等采样参数;
- EOS 行为;
- stop token 和 stop string;
- 最大输出长度;
- 空输入、短输入和超长输入;
- 多轮对话和 chat template;
- 流式输出;
- 请求中途取消。
运行时状态测试包括:
- KV Cache 分配;
- KV Cache 复用;
- Prefix Cache 命中和未命中;
- 请求完成后的 cache 回收;
- 长时间运行后的显存稳定性;
- 动态 Batch 中请求加入和退出;
- OOM 或异常后的状态恢复。
如果模型支持多模态,还应额外测试:
- 图片、视频或音频预处理;
- 多模态 token 与文本 token 的对齐;
- 不同输入尺寸;
- 多模态输入缺失或格式错误;
- 多模态 encoder 状态的缓存和释放。
5.8.2 性能测试
性能测试至少应覆盖以下场景:
- 单请求短上下文;
- 单请求长上下文;
- 短 Prefill、长 Decode;
- 长 Prefill、短 Decode;
- 多请求动态 Batch;
- 高并发 Decode;
- Prefix Cache;
- 不同 batch size;
- 不同输入和输出长度;
- 不同 dtype 和量化配置;
- 不同并行规模。
性能测试不能只观察平均值,还应关注:
- 首 Token 延迟;
- 单 Token 间隔;
- 总吞吐;
- P50、P95、P99 延迟;
- GPU 利用率;
- 显存占用;
- 显存带宽利用率;
- kernel launch 数量;
- 通信占比;
- cache 分配和回收开销。
5.8.3 分布式与故障恢复测试
分布式测试应覆盖:
- 不同 Tensor Parallel size;
- 不同 Pipeline Parallel stage 数;
- Expert Parallel;
- 多节点通信;
- 不同 rank 的参数和输出一致性;
- 通信与计算重叠;
- 请求取消;
- OOM;
- 通信超时;
- 部分 rank 异常;
- 服务重启;
- 模型重复加载和卸载。
建议重点比较:
单卡结果
与
多卡结果
参考框架结果
与
vLLM 结果
未优化实现
与
优化后实现
每增加一种并行方式、量化方式或特化 kernel,都应重新执行相应的正确性测试和性能回归测试。
5.9 推荐的接入顺序与发布门槛
综合上述步骤,一个新模型的推荐接入顺序如下:
1. 分析模型差异
↓
2. 确定复用、局部改造或全新实现
↓
3. 实现模型结构
↓
4. 实现权重映射与装配
↓
5. 通过参数和数值检查
↓
6. 打通单卡、单请求 Prefill
↓
7. 打通单卡 Decode 和多轮执行
↓
8. 接入 KV Cache 和 Attention Metadata
↓
9. 注册 ModelConfig / ModelRegistry
↓
10. 验证动态 Batch 和请求生命周期
↓
11. 扩展 Tensor Parallel 等并行能力
↓
12. 接入量化和其他高级能力
↓
13. Profiling 并进行选择性特化
↓
14. 完成正确性、性能和分布式验收
可以将发布门槛概括为:
模型能够被正确识别
且
权重能够完整装配
且
单卡 Prefill / Decode 数值正确
且
KV Cache 和请求生命周期稳定
且
目标并行配置下输出正确
且
性能达到预期
且
故障和资源回收行为可控
最终,一个新模型是否真正“接入完成”,不能只看模型类是否已经注册,也不能只看单次请求是否能够生成文本。完整接入应当同时满足:
配置可识别
实现可复用或可维护
权重可正确装配
运行时状态可管理
Prefill / Decode 可稳定执行
并行路径可验证
性能瓶颈可解释
异常场景可恢复
从架构角度看,这一过程正好对应前文所描述的抽象链路:
配置语义
→ 模型发现
→ 模型能力
→ 权重装配
→ 调度计划
→ 执行编排
→ Attention Metadata
→ KV Cache / Backend
→ 硬件执行
因此,新模型接入的核心并不是把所有差异都塞进模型类,而是识别差异所在的层级,并将其放置到正确的抽象边界中:
配置差异 → ModelConfig
识别差异 → ModelRegistry
结构差异 → Model class / 公共模块
权重差异 → ModelLoader / WeightsMapper
执行差异 → Worker / ModelRunner
状态差异 → KV Cache / State Management
Attention 差异 → Attention Metadata / Backend
输入差异 → Tokenizer / Multimodal Registry
性能差异 → Compiler / Graph / Kernel Backend
这也是新模型能够在不破坏既有运行时和性能优化的前提下接入 vLLM 的关键。
6. 实际案例分析:DeepSeek架构的工程适配
DeepSeek 系列模型的接入,并不是简单地增加一个模型类、补充若干权重映射,或者为某个算子编写特化 Kernel。它更像是一次对推理引擎既有协议的压力测试:模型在注意力机制、专家路由、推测式生成和缓存管理等方面引入了新的状态组织方式,迫使 vLLM 重新审视原有抽象是否足够表达这些变化。
从工程角度看,DeepSeek 的接入可以被拆解为三类问题:
- 如何表示新的计算状态;
- 如何调度动态变化的计算路径;
- 如何保证这些动态状态在增量执行、并行执行和回滚执行中的一致性。
因此,DeepSeek 的适配并不是单点修改,而是涉及以下多个层次:
模型配置与注册
↓
权重加载与结构映射
↓
注意力与 KV Cache 表达
↓
MoE 路由与分布式执行
↓
MTP / Speculative Decoding 状态管理
↓
调度、验证、回滚与性能优化
这也说明,复杂模型的接入过程,本质上是将模型自身的特殊机制翻译成推理框架能够理解的协议。
6.1 MLA:从注意力优化到 KV Cache 协议扩展
Multi-head Latent Attention,简称 MLA,最直接的目标是降低 KV Cache 的存储与访存开销。传统多头注意力通常需要为每个 Token 保存规模较大的 Key 和 Value 状态,而 MLA 则通过低秩表示压缩历史上下文,使缓存中的信息维度显著降低。
在概念上,传统 KV Cache 可以抽象为:
K_cache: [batch, sequence_length, num_heads, head_dim]
V_cache: [batch, sequence_length, num_heads, head_dim]
而 MLA 更接近于保存某种低秩潜变量:
latent_cache: [batch, sequence_length, latent_dim]
在实际实现中,缓存内容、解码方式以及位置编码相关状态可能比上述形式更加复杂。因此,MLA 的关键变化并不只是“把缓存张量变小”,而是改变了缓存的语义:
KV Cache 不再必然表示已经展开的 Key/Value,而可以表示生成 Key/Value 所需的压缩状态。
这会影响 vLLM 的多个组件。
6.1.1 Cache allocation
缓存分配逻辑不能只根据固定的 num_heads × head_dim 计算容量,而需要能够根据不同注意力后端提供的缓存布局动态确定:
- 每个 Token 需要多少缓存空间;
- 缓存是按完整 KV 保存,还是按 latent 表示保存;
- 不同层是否使用相同的缓存格式;
- 缓存是否包含额外的位置编码或旋转位置状态。
因此,缓存分配接口需要从“分配一块固定形状的 KV 空间”,逐渐演进为“根据注意力协议分配一类可被后端解释的缓存空间”。
6.1.2 Cache write 与 cache copy
在普通注意力中,缓存写入通常可以理解为:
新生成的 K、V
↓
写入对应的 block
而在 MLA 中,写入的数据可能是压缩后的 latent,或者是经过投影、分解和位置编码处理后的中间状态。于是,缓存写入与复制操作必须明确:
- 写入的是展开后的表示还是压缩表示;
- block table 如何解释缓存位置;
- prefix cache 复用时复制的到底是什么;
- 交换、重排、迁移缓存时是否需要额外转换。
这意味着,copy_blocks、cache swap、prefix caching 等底层机制不能假设所有缓存都具有传统 K/V 的形状和语义。
6.1.3 Attention Metadata
MLA 对 Attention Metadata 的要求也更高。Metadata 不仅要描述序列长度、slot 映射和 block table,还可能需要包含:
- latent cache 的布局信息;
- 不同阶段使用的投影参数;
- prefill 与 decode 阶段不同的计算路径;
- 是否启用特定的压缩或恢复逻辑;
- 后端需要的额外 stride、偏移与索引信息。
因此,MLA 可以被视为对注意力后端接口的一次扩展:
传统注意力:
SchedulerOutput
→ seq_lens / block_table
→ 标准 K/V Attention
MLA:
SchedulerOutput
→ cache layout / latent metadata / position metadata
→ MLA-specific Attention Backend
需要注意的是,MLA 并不意味着整个 vLLM 都必须理解 MLA 的数学细节。更合理的设计是:
- 上层负责表达调度结果和缓存状态;
- 模型层负责组织 MLA 所需的输入;
- Attention Backend 负责具体的压缩状态恢复与高效计算;
- Cache Manager 负责提供与该协议兼容的存储和搬运能力。
这体现了“动态性上浮,静态性下沉”的原则:MLA 的特殊性应当被限制在明确的模型和后端边界内,而不是扩散到所有通用调度代码中。
6.2 MoE:从静态执行图到动态专家路由
Mixture-of-Experts,简称 MoE,将单一的前馈网络替换为多个专家网络,并由 Router 为每个 Token 选择少量专家执行。它的核心优势是:
在保持较大参数规模的同时,使每个 Token 实际只激活部分参数。
然而,对推理引擎而言,MoE 带来的并不仅是多个 Linear 层,而是一套动态的 Token-to-Expert 执行机制。
传统 Dense 模型中的执行路径相对稳定:
Token
→ Attention
→ MLP
→ 下一层
MoE 模型则更接近:
Token
→ Router
→ Expert Selection
→ Token Dispatch
→ Expert Compute
→ Token Combine
其中,Expert Selection 和 Token Dispatch 的结果会随着每一批 Token 的内容动态变化。
6.2.1 Token-to-Expert 映射
MoE 执行通常需要构造某种 Token-to-Expert 映射表,用于描述:
- 每个 Token 被分配到哪些专家;
- 每个专家接收多少 Token;
- Token 在重排后位于哪个位置;
- 专家计算完成后如何恢复原始 Token 顺序;
- 跨设备专家如何进行通信。
可以抽象为:
原始 Token 顺序
↓
Router 得分
↓
Top-k Expert Selection
↓
Token-to-Expert Mapping
↓
按专家重排
↓
Expert Computation
↓
按原始顺序还原
这与普通 MLP 的主要差异在于:矩阵乘法的输入排列不再固定,计算负载也不再均匀。
6.2.2 与 Scheduler 的关系
需要谨慎区分两个层次:
Scheduler通常负责请求、序列和 Token 级别的批处理调度;- Router 负责根据当前 Token 的隐藏状态选择专家。
因此,Scheduler 通常无法在真正计算 Router 之前准确知道每个 Token 的专家归属。更准确的说法是:
MoE 要求调度与执行编排层能够容纳动态的专家路由结果,而不是简单地要求 Scheduler 在执行前完全预测专家负载。
在某些优化路径中,系统可以根据历史统计、容量限制或设备拓扑对可能的专家负载进行预估,但这属于调度优化,而不是 Router 语义本身。
这会推动 SchedulerOutput 和 ModelRunner 之间的契约更加丰富。例如,执行计划可能需要支持:
- 当前批次是否包含 MoE 层;
- 是否启用专家并行;
- 专家容量限制;
- 通信与计算的重叠策略;
- 当前执行需要的 dispatch buffer;
- 不同设备上的专家布局。
因此,MoE 的接入点不仅在模型定义中,也在分布式执行和运行时编排中。
6.2.3 专家并行与通信
当专家分布在不同 GPU 上时,Token 需要通过通信操作发送到对应设备。典型过程包括:
本地 Token
→ 本地 Router
→ All-to-All / Dispatch
→ 远程 Expert
→ All-to-All / Combine
→ 本地 Token 顺序恢复
这里的性能瓶颈可能不再是单个 GEMM,而是:
- Token dispatch 的重排;
- All-to-All 通信;
- 专家负载不均衡;
- 小批量专家矩阵乘法;
- 通信与计算之间的同步。
因此,MoE 的验证不能只检查输出数值,还必须检查:
- 专家负载是否严重倾斜;
- 通信量是否符合预期;
- 极端输入下是否出现专家容量溢出;
- 多卡和单卡路径是否具有一致语义;
- Token 重排和还原是否保持顺序正确。
6.3 MTP:从单步生成到可验证的多步执行
Multi-Token Prediction,简称 MTP,改变了传统自回归模型“一次只生成一个 Token、确认后再继续”的执行方式。它允许模型在一次前向过程中提出多个候选 Token,随后通过验证逻辑决定哪些候选可以被接受。
因此,MTP 的基本执行过程可以表示为:
已确认上下文
↓
生成多个候选 Token
↓
验证候选序列
↓
接受部分候选
↓
拒绝或回滚其余候选
↓
提交新的推理状态
这与普通 decode 最大的区别在于:一次计算可能产生多个“暂时状态”,但这些状态并不一定全部成为最终状态。
6.3.1 推理状态不再只有“提交”语义
传统增量推理通常假设:
执行一步
→ 写入 KV Cache
→ 更新 sequence length
→ 进入下一步
MTP 则引入了两种状态:
- 暂存状态:候选 Token 对应的临时计算结果;
- 提交状态:经过验证后正式纳入序列的结果。
因此,KV Cache、序列长度、位置编码状态以及相关元数据都必须能够区分:
tentative state
与
committed state
如果候选 Token 已经直接覆盖了正式缓存,那么验证失败后就必须具备可靠的撤销机制。否则,后续 Token 可能会读取到本不应存在的上下文,导致结果错误。
6.3.2 回滚语义
MTP 的回滚并不是简单地把 Python 列表长度减一。它可能涉及:
- 回退序列长度;
- 释放或重新标记缓存 block;
- 恢复 block table;
- 清理临时 attention metadata;
- 撤销候选 Token 的 logits 状态;
- 恢复位置编码和采样相关状态;
- 修正请求级别的生成进度。
因此,回滚必须是一个具有一致性的事务操作:
开始候选执行
↓
记录旧状态
↓
写入暂存结果
↓
验证候选
↓
提交,或恢复旧状态
从这个意义上说,MTP 将一种过去较少出现在普通推理路径中的语义引入了运行时:
推理状态不仅要支持前进,还要支持可验证、可撤销和部分提交。
这为未来的 Speculative Decoding、树状候选生成以及更复杂的搜索式解码提供了基础。
不过,“MTP 是 vLLM 历史上第一次引入可撤销状态管理”这一表述应当适当收敛。更严谨的说法是:MTP 将显式的候选状态、部分接受和回滚语义带入了更核心的推理状态管理路径,使这一能力从特殊优化机制上升为需要系统级支持的运行时能力。
6.3.3 Scheduler 与 KV Cache 的协同
MTP 要求 Scheduler 不再只处理“下一步要生成哪个 Token”,还需要处理:
- 当前请求允许生成多少候选 Token;
- 哪些候选已经生成;
- 哪些候选通过验证;
- 哪些状态需要提交;
- 哪些状态需要回滚;
- 回滚后下一轮调度从哪个位置继续。
因此,调度输出可能需要携带更丰富的候选和验证信息。ModelRunner 则负责将这些信息编排为实际的模型执行步骤,Attention Backend 最终接收与当前提交状态一致的 metadata。
可以将三者关系概括为:
Scheduler:
决定候选执行与提交/回滚计划
ModelRunner:
将计划编排为若干次模型执行
Attention / KV Backend:
按照当前有效状态读取、写入或恢复缓存
MTP 的关键验证点也不只是“最终文本是否合理”,而是要检查:
- 部分接受后的位置是否连续;
- 回滚后的 KV Cache 是否与基线一致;
- 多轮候选执行后是否发生状态污染;
- 接受 0 个、1 个或全部候选时是否都正确;
- prefill、decode 与 MTP 混合批次是否正确;
- 失败路径是否会造成缓存泄漏或 block 错配。
6.4 DeepSeek 架构接入的边界拆解
下面的决策矩阵可以看出DeepSeek 的每项特性都不能简单归入“模型层”。它们分别触及了缓存、调度、分布式执行、算子后端和状态管理等不同边界。
| 特性 | 主要变化 | 接入动作 | 对应抽象边界 | 关键验证点 |
|---|---|---|---|---|
| MLA | KV 表示从完整 K/V 转向低秩或压缩状态 | 实现低秩状态生成、恢复与缓存读写 | Attention Metadata、KV Cache、Attention Backend | 与标准注意力对比数值误差;检查缓存读写和 prefix 复用 |
| MoE | 每个 Token 动态选择部分专家 | 实现 Router、Token dispatch、专家计算与结果 combine | ModelRunner、Distributed Executor、Expert Parallel | 专家负载均衡、通信吞吐、Token 顺序恢复、容量溢出 |
| MTP | 一次执行产生多个候选并进行验证 | 实现候选状态、部分提交与回滚 | Scheduler、ModelRunner、KV Cache、Sampling | 接受、拒绝和部分接受路径的一致性 |
| 低秩投影 | 权重和激活的形状、计算路径变化 | 增加相应层实现与权重映射 | Model Definition、Weights Mapper | 权重切分、合并、量化格式与精度 |
| 分布式专家 | 专家跨设备分布 | 增加通信与设备映射逻辑 | Parallel State、Distributed Backend | 单卡/多卡一致性、通信死锁、负载倾斜 |
| 动态缓存 | 缓存不再是简单线性追加 | 支持 block 分配、复用、交换与回滚 | KV Cache Manager | 缓存生命周期、碎片率、回滚后状态 |
| 推测式执行 | 计算结果存在暂存与提交两个阶段 | 建立可撤销的执行状态 | Scheduler、ModelRunner、KV Cache | 长序列、多请求并发和异常路径 |
| 特化 Kernel | 通用算子难以覆盖全部性能需求 | 增加硬件与后端特化实现 | Attention Backend、Custom Op | 数值稳定性、边界尺寸、不同硬件兼容性 |
6.5 三层架构:通用层、特化层与验证层
为了控制复杂度,DeepSeek 的接入可以按照三层结构组织。
6.5.1 通用层
通用层负责复用 vLLM 已有的基础能力,包括:
- 请求管理;
- Tokenizer;
- 基础调度;
- block-based KV Cache;
- 权重加载框架;
- 张量并行和流水线并行;
- 采样接口;
- 通用的 ModelRunner 生命周期。
通用层的目标不是理解所有模型细节,而是提供稳定的运行时骨架。
6.5.2 特化层
特化层负责承载 DeepSeek 的模型特性,包括:
- MLA 的注意力与缓存布局;
- MoE Router 和专家执行;
- 专家并行与 Token dispatch;
- MTP 候选生成与验证;
- 特殊的位置编码与投影逻辑;
- 面向特定硬件的优化 Kernel。
特化层应当尽量通过清晰的接口接入通用层,而不应直接修改大量无关的调度和缓存代码。
6.5.3 验证层
验证层负责确保特化逻辑不会破坏通用运行时。它至少应覆盖以下维度:
① 数值正确性
- MLA 与参考实现的输出误差;
- MoE 路由结果与专家计算结果;
- MTP 接受路径与完整自回归路径;
- 不同精度下的误差边界;
- 量化模型与非量化模型的一致性。
② 状态一致性
- KV Cache 写入和读取;
- prefix cache 命中;
- block swap;
- MTP 回滚;
- 部分提交;
- 多轮 decode 后的序列状态。
③ 并发与分布式正确性
- 多请求连续批处理;
- 不同长度请求混合;
- 专家跨设备通信;
- 通信与计算重叠;
- 高并发下的 block 回收。
④ 性能正确性
性能并不是单纯追求吞吐量,还包括:
- 首 Token 延迟;
- 单 Token 延迟;
- KV Cache 占用;
- 专家负载均衡;
- 通信比例;
- 回滚开销;
- 长上下文下的显存增长;
- 不同 batch size 下的退化情况。
对于 DeepSeek 这类复杂模型,验证层的重要性甚至不低于特化层。因为 MLA、MoE 和 MTP 的问题往往并不在正常路径上暴露,而是在以下边界条件中出现:
- 候选全部拒绝;
- 候选只接受一部分;
- 专家负载极度倾斜;
- 缓存 block 发生交换;
- 长序列和短序列混合;
- 多卡通信延迟抖动;
- 特定输入触发数值不稳定。
6.6 小结:DeepSeek 接入的本质
DeepSeek 对 vLLM 的影响,可以概括为三次协议扩展:
-
MLA 扩展了缓存协议
KV Cache 不再只是完整 Key/Value 的存储区域,也可以是压缩潜变量及其恢复所需的运行时状态。 -
MoE 扩展了执行协议
模型执行不再是固定的数据流,而是包含动态路由、Token 重排、专家分发和跨设备通信的运行时过程。 -
MTP 扩展了状态协议
推理状态不再只有“向前追加”,还必须支持候选、验证、部分提交与回滚。
因此,DeepSeek 的接入可以被视为 vLLM 从“支持更多模型”走向“支持更多推理范式”的一个代表性案例。它所揭示的并不是某个单独模型的特殊性,而是现代推理引擎必须面对的共同趋势:
模型结构越来越动态,缓存状态越来越复杂,执行路径越来越依赖运行时决策;而高性能推理框架必须通过稳定的抽象边界,把这些动态变化限制在可控的协议之内。
这也是 vLLM 架构演进的核心方向:让上层能够表达复杂模型,让中层能够编排动态执行,让底层仍然保持高效、确定且可验证的算子执行。
7. 总结:推理引擎适配能力的本质
7.1 模型层、运行时层与算子层的三层适配模型
可以将模型适配归纳为三层:
┌────────────────────────────┐
│ 模型层:结构、权重、配置 │
├────────────────────────────┤
│ 运行时层:调度、Cache、并行 │
├────────────────────────────┤
│ 算子层:Kernel、量化、通信 │
└────────────────────────────┘
不同类型的模型创新,会触及不同层次:
| 模型变化 | 主要影响层 |
|---|---|
| 新增普通层或修改激活函数 | 模型层、算子层 |
| 改变 KV Cache 表示 | 模型层、运行时层、算子层 |
| 引入 MoE | 三层均受影响 |
| 引入 MTP | 模型层、运行时层、采样层 |
| 改变并行方式 | 运行时层、通信层 |
真正成熟的推理引擎,不是让所有模型都使用同一个实现,而是建立清晰的适配边界,让变化能够被隔离在合适的层次中。
7.2 推理引擎的核心竞争力
推理引擎的核心竞争力可以概括为四点:
- 抽象能力:能否识别不同模型之间可复用的共同结构;
- 运行时能力:能否高效处理动态 Batch、KV Cache 和请求调度;
- 特化能力:能否针对关键模型和硬件提供高性能实现;
- 演进能力:能否在新模型出现时快速扩大抽象边界。
如果只有抽象,没有特化,系统可能易于扩展但性能不足;如果只有特化,没有抽象,系统可能性能很高但难以维护。优秀的推理引擎需要在二者之间建立动态平衡。
7.3 从模型适配到模型与引擎共演进
随着模型结构不断创新,模型和推理引擎之间的关系正在发生变化。
过去通常是:
模型先设计
↓
推理引擎被动适配
未来更可能是:
模型结构设计
↔
推理引擎能力
↔
硬件执行特性
模型设计会考虑:
- KV Cache 成本;
- 通信开销;
- Kernel 可实现性;
- 量化友好性;
- 推理阶段的吞吐和延迟。
推理引擎也会通过运行时反馈影响模型设计,例如:
- 哪些结构更适合动态 Batch;
- 哪些 MoE 路由方式更适合多卡;
- 哪些 Attention 形式更节省 Cache;
- 哪些预测机制更适合 Speculative Decoding。
因此,模型适配的最终目标并不是让推理引擎无限承受模型复杂度,而是推动模型、引擎和硬件形成协同演进。
通用抽象负责扩大适配范围,特化 Kernel 负责突破性能边界,运行时与 IR 则负责在动态执行和编译优化之间建立新的连接。
这构成了现代推理引擎适配新模型的基本方法论。
📂 本章源码导航
模型适配
| 想看什么 | 从哪开始 |
|---|---|
| 新模型如何被识别与加载 | vllm/model_executor/models/registry.py |
| 模型实现范例 | vllm/model_executor/models/llama.py、deepseek_v2.py |
| MTP 头 | vllm/model_executor/models/deepseek_mtp.py |
| MTP 方法白名单 | vllm/config/speculative.py |
| MLA 封装 | vllm/model_executor/layers/mla.py |