NOTE 本文基于 vLLM v0.27.1(tag
6e448d0, 2026-08-11)源码深度剖析。文中所有文件路径、类名和行号均以该版本为准;vLLM 迭代很快,阅读时请以你手上的版本对照。
上一章讨论了模型适配:面对不断变化的模型结构,Serving 框架如何通过统一接口、模型注册和模块化执行路径,降低新模型接入成本。
但模型只是变化来源之一。
在真实部署环境中,硬件同样在快速变化。GPU 不再是唯一选择,AMD ROCm、华为昇腾 Ascend、Intel XPU、Google TPU 以及各种专用加速器,都在参与大模型推理基础设施的竞争。
这就带来一个更棘手的问题:
如何让同一套 Serving 逻辑运行在不同芯片上,同时避免芯片差异渗透到 Scheduler、KV Cache 和请求生命周期管理之中?
vLLM 的答案不是在核心代码里堆积更多硬件分支,而是建立一套平台抽象、后端选择和插件扩展机制,把硬件差异尽可能隔离在系统边界之外。
1. 一条设计原则:硬件适配不能污染 Serving 核心
先看一个反例。
假设 Scheduler 需要根据硬件特性决定是否允许某种调度策略,于是代码变成:
if is_cuda():
...
elif is_ascend():
...
elif is_rocm():
...
KV Cache 管理器也出现类似判断:
if is_cuda():
allocate_cuda_blocks()
elif is_ascend():
allocate_npu_blocks()
Attention、通信、量化和模型执行路径中也不断加入类似分支。
一开始,这种方式看起来很直接。但随着硬件类型增加,问题会迅速暴露:
- 调度逻辑开始依赖具体设备名称;
- KV Cache 管理器需要理解不同设备的内存模型;
- 模型执行器被迫维护多套硬件分支;
- 每加入一种芯片,都要修改多个核心模块;
- 不同硬件分支之间逐渐产生行为差异;
- 测试矩阵从“功能 × 模型”膨胀为“功能 × 模型 × 芯片”。
最终,Serving 核心不再是一个与硬件无关的调度系统,而变成了各种硬件特殊情况的集合。
因此,硬件适配首先是一条架构原则:
硬件适配不能污染上层 Serving 逻辑。
把这句话放回前面几章的语境里,会更容易理解。
第四章的 Scheduler 关心的是:
- 当前请求需要执行多少 token;
- 当前 batch 还有多少计算预算;
- 哪些请求应该继续 decode;
- 哪些请求应该被抢占或延迟。
第五章的 KV Cache Manager 关心的是:
- KV Cache 被划分成多少个 block;
- 哪些 block 已经分配;
- 哪些 block 可以复用;
- prefix cache 是否命中;
- 显存不足时如何回收或换出。
这些模块应该依赖的是抽象能力,而不是具体芯片:
Scheduler
└── 只关心 token 预算、请求状态和执行资源
KVCacheManager
└── 只关心 block、容量、分配和回收
Model Executor
└── 只关心模型层如何执行
硬件平台层
└── 负责设备、算子、通信、数据类型和内存实现
理想的依赖关系如下:
graph TD
CORE["vLLM Serving Core<br/>Scheduler · KV Cache Manager · EngineCore<br/><br/>只依赖抽象能力,不包含芯片分支"]
CORE --> PLATFORM
PLATFORM["Platform 抽象层<br/>设备能力 · 数据类型 · 通信 · 算子导入<br/>Attention Backend 选择"]
PLATFORM --> CUDA
PLATFORM --> ROCM
PLATFORM --> ASCEND
PLATFORM --> XPU
CUDA["CudaPlatform<br/>CUDA / cuBLAS / FlashAttention<br/>FlashInfer / Triton"]
ROCM["RocmPlatform<br/>ROCm / HIP / AITER"]
ASCEND["AscendPlatform(OOT)<br/>CANN / ACL / NPU Kernel"]
XPU["XPU Platform<br/>oneAPI / XPU Kernel"]
style CORE fill:#e8f3ff,stroke:#4a90e2
style PLATFORM fill:#fff4d6,stroke:#d99a00
上层只需要提出类似的问题:
当前设备是什么类型?
支持哪些数据类型?
支持 FP8 或其他量化格式吗?
使用哪个 Attention Backend?
集合通信由哪个实现负责?
需要加载哪些平台扩展?
它不应该关心:
这是 NVIDIA GPU、AMD GPU,还是昇腾 NPU?
底层使用 CUDA、HIP 还是 CANN?
具体 Kernel 是哪个动态库?
通信实现是 NCCL、RCCL 还是 HCCL?
这些问题应该由平台层回答。
2. Platform:硬件能力的统一来源
在 vLLM 中,Platform 可以理解为硬件适配的“能力中心”。
它并不是简单的设备名称包装,而是向上层提供一组相对稳定的能力查询接口,例如:
- 设备类型和设备名称;
- 当前设备的计算能力;
- 支持的数据类型;
- 支持的量化格式;
- 默认通信后端;
- Attention Backend 选择;
- 底层扩展导入;
- Device Communicator;
- 平台级配置检查;
- 平台相关的 Worker 或执行组件。
从抽象上看,上层代码依赖的是这样的接口:
class Platform:
device_name: str
device_type: str
@classmethod
def get_attn_backend_cls(cls, ...):
...
@classmethod
def import_kernels(cls):
...
@classmethod
def get_device_communicator_cls(cls):
...
@classmethod
def get_punica_wrapper(cls):
...
@classmethod
def check_and_update_config(cls, config):
...
不同平台提供不同实现,但上层不需要知道这些实现的细节。
可以把它理解为:
上层代码提出能力问题
↓
Platform 提供平台事实
↓
具体 Backend 或 Kernel 被选择
这里的关键不是“所有硬件都实现完全相同的代码”,而是:
所有硬件都通过相对稳定的抽象边界向 Serving 核心提供能力。
2.1 current_platform 是如何出现的?
程序启动时,vLLM 需要先确定当前运行平台。这个过程通常涉及:
- 读取运行环境和设备配置;
- 检测可用设备;
- 加载内置平台或外部平台插件;
- 选择与当前环境匹配的平台实现;
- 将平台对象暴露为全局使用的
current_platform。
因此,后续代码不需要到处重新判断设备类型,而是统一读取:
from vllm.platforms import current_platform
然后通过它获取设备能力。
需要注意的是,实际初始化路径会随着 vLLM 版本、插件机制和部署方式变化。对于博客来说,更准确的表述是:
current_platform是运行时平台选择机制的统一出口。它背后可能来自内置平台,也可能来自通过插件机制注册的 Out-of-Tree 平台。
3. Platform、Attention Backend 与 Kernel Backend 的真实关系
很多人第一次阅读 vLLM 硬件适配代码时,会自然地形成一种“三层调用栈”:
Platform Backend
↓
Attention Backend
↓
Kernel Backend
这种理解在 Attention 局部路径上有一定合理性,但如果把它当成整个硬件适配体系的真实结构,就会产生误解。
更准确的关系是:
Platform 是设备能力和运行时事实的来源;Attention Backend 是其中一个重要的动态选择器;大量其他 Kernel、通信组件和平台扩展则可以由 Platform 直接提供。
整体关系更接近下面这样:
graph TD
START["vLLM 启动"] --> DETECT["平台检测与注册"]
DETECT --> CP["current_platform"]
CP --> ATTENTION["Attention 选择路径"]
CP --> KERNEL["平台 Kernel 路径"]
CP --> COMM["通信路径"]
CP --> CONFIG["平台配置检查"]
CP --> WORKER["Worker / Device 执行路径"]
ATTENTION --> SELECTOR["get_attn_backend()"]
SELECTOR --> CONFIG2["AttentionSelectorConfig"]
CONFIG2 --> PLATFORM_SELECT["current_platform.get_attn_backend_cls()"]
PLATFORM_SELECT --> BACKEND["FlashAttention<br/>FlashInfer<br/>Triton<br/>AITER<br/>CANN Attention"]
KERNEL --> IMPORT["import_kernels()"]
IMPORT --> EXT["CUDA / HIP / CANN / XPU 扩展"]
COMM --> COMM_IMPL["CudaCommunicator<br/>RCCL / HCCL / XCCL 等"]
CONFIG --> CHECK["dtype、quantization、并行配置检查"]
WORKER --> DEVICE["设备初始化、内存管理、执行上下文"]
3.1 路径一:Attention Backend 选择
Attention 是 vLLM 中最重要的动态后端选择场景之一。
典型入口可以抽象为:
def get_attn_backend(
head_size,
dtype,
kv_cache_dtype,
use_mla,
sliding_window,
...
):
selector_config = AttentionSelectorConfig(
head_size=head_size,
dtype=dtype,
kv_cache_dtype=kv_cache_dtype,
use_mla=use_mla,
sliding_window=sliding_window,
...
)
return current_platform.get_attn_backend_cls(
selector_config
)
平台在选择 Attention Backend 时,可能需要综合判断:
- 设备类型;
- GPU Compute Capability 或 NPU 能力;
- head size;
- query 和 KV 的数据类型;
- KV Cache 的数据类型;
- 是否使用 MLA;
- 是否启用 sliding window;
- 是否支持 prefix caching;
- 是否支持 paged attention;
- prefill 和 decode 的执行模式;
- 当前硬件是否存在对应 Kernel。
最终得到的可能是:
FlashAttentionBackend
FlashInferBackend
TritonAttentionBackend
AITER Attention Backend
Ascend / CANN Attention Backend
可以用一个简化后的伪代码表示:
class CudaPlatform(Platform):
@classmethod
def get_attn_backend_cls(cls, config):
if supports_flashinfer(config):
return FlashInferBackend
if supports_flash_attention(config):
return FlashAttentionBackend
return TritonAttentionBackend
这并不意味着所有平台都必须把选择逻辑写成同样的形式。平台可以根据自己的能力返回合适的实现。
3.2 路径二:直接导入平台 Kernel
并不是所有底层算子都需要经过 Attention Backend。
平台可能直接加载自己的 C++、CUDA、HIP 或 CANN 扩展:
class CudaPlatform(Platform):
@classmethod
def import_kernels(cls) -> None:
import vllm._C_stable_libtorch
import vllm._moe_C_stable_libtorch
with contextlib.suppress(ImportError):
import vllm._qutlass_C
这些扩展可能包含:
- MoE 相关算子;
- Quantization 相关算子;
- RMSNorm、RoPE 等基础算子;
- GEMM 或矩阵乘法优化;
- 自定义通信算子;
- 平台专用的运行时扩展。
它们的加载不需要经过 Attention Backend。
3.3 路径三:平台直接提供通信和其他组件
集合通信同样可能由平台直接决定:
class CudaPlatform(Platform):
@classmethod
def get_device_communicator_cls(cls) -> str:
return (
"vllm.distributed.device_communicators."
"cuda_communicator.CudaCommunicator"
)
不同硬件平台可能分别对接:
NVIDIA GPU → NCCL
AMD GPU → RCCL
昇腾 NPU → HCCL
Intel XPU → XCCL 或对应通信实现
LoRA、量化、内存管理以及平台特有的执行组件,也可能走类似的直接派发路径。
因此,系统级的真实关系不是:
Platform → Attention Backend → 所有 Kernel
而是:
┌─ Attention Backend Selector
│
current_platform ────────┼─ Kernel Import
│
├─ Device Communicator
│
├─ Worker / Device Runtime
│
└─ Platform Configuration
3.4 为什么 Attention 要单独做 Selector?
Attention 之所以被单独抽象出来,不只是因为它名字特殊,而是因为它同时具备三个特点:
- 计算量大:Attention 是推理性能的关键组成部分;
- 硬件敏感:不同设备对矩阵乘法、稀疏访问、KV Cache 读取的优化方式不同;
- 场景复杂:prefill、decode、paged KV Cache、MLA、不同 head size 和不同数据类型,都可能影响最优实现。
因此,Attention 往往需要根据运行配置动态选择后端。
例如,同一块 GPU 上:
某种 head size + FP16 + prefill
→ FlashAttention
某种 head size + FP8 KV Cache + decode
→ FlashInfer
特殊模型结构或不满足优化条件
→ Triton 或通用实现
而 RMSNorm、RoPE 等算子,很多时候可以通过平台扩展直接绑定。它们也可能存在多个实现,但通常不需要像 Attention 一样根据大量运行时条件进行复杂选择。
所以,Attention Selector 可以看作:
性能关键路径上的动态决策机制。
而 import_kernels() 更像是:
平台级实现的加载和绑定机制。
两者都属于硬件适配,但解决的问题不同。
4. Out-of-Tree插件架构:把新硬件放到主仓库之外
如果每接入一种硬件,都必须修改 vLLM 主仓库,那么硬件生态很容易受到两个问题限制:
- 主仓库需要长期维护大量平台代码;
- 硬件厂商无法独立发布适配版本。
因此,vLLM 支持 Out-of-Tree,也就是 OOT 适配。
OOT 的核心思想是:
主仓库提供稳定的扩展接口,第三方通过独立包实现具体平台。
以昇腾为例,适配包可以独立维护在 vllm-ascend 中,而不是把所有 CANN 相关代码直接放入 vLLM 主仓库。
从架构上看:
graph LR
VLLM["vLLM 主仓库<br/>Serving Core + Platform Interface"]
PLUGIN["vllm-ascend<br/>AscendPlatform + Worker + Backend"]
CANN["CANN / ACL / HCCL<br/>昇腾运行时与算子库"]
USER["用户部署环境"]
USER --> VLLM
USER --> PLUGIN
PLUGIN --> VLLM
PLUGIN --> CANN
style VLLM fill:#e8f3ff,stroke:#4a90e2
style PLUGIN fill:#fff4d6,stroke:#d99a00
style CANN fill:#f3e8ff,stroke:#8a5cc7
主仓库和插件包之间大致是这样的分工:
| 能力 | vLLM 主仓库提供 | Ascend 插件实现 |
|---|---|---|
| 平台抽象 | Platform 接口和平台注册机制 |
AscendPlatform |
| 模型接口 | 标准模型执行接口 | 尽量复用标准模型实现 |
| Attention | AttentionBackend 抽象和选择入口 |
CANN 或 Ascend 专用 Attention |
| Worker | Worker 基类和执行生命周期 | NPU 设备初始化、执行和内存管理 |
| Kernel | 通用算子接口、扩展加载约定 | CANN Custom Ops、Ascend Kernel |
| 通信 | 分布式和 Device Communicator 抽象 | HCCL 等昇腾通信实现 |
| 数据类型 | 配置和能力查询接口 | NPU 支持的数据类型与限制 |
| 量化 | 量化配置和模型接口 | 昇腾量化 Kernel 与转换逻辑 |
| 内存 | KV Cache 抽象和缓存管理流程 | NPU 内存分配、显存/内存池适配 |
| 配置检查 | 通用配置校验入口 | NPU 特有约束和兼容性检查 |
因此,“接入昇腾”绝不是简单地把:
if device == "npu":
...
添加到几个文件里。
一个真正可用的昇腾后端,通常需要完成以下工作。
5. 昇腾适配需要解决哪些问题?
5.1 平台识别与注册
首先,vLLM 必须能够识别当前设备,并将其映射到 Ascend 平台实现。
平台对象需要提供基础信息,例如:
class AscendPlatform(Platform):
device_name = "npu"
device_type = "npu"
但平台识别并不等于适配完成。它还需要解决:
- 如何检测 NPU 是否可用;
- 如何读取设备数量;
- 如何设置当前设备;
- 如何初始化 CANN 运行时;
- 如何让分布式进程看到正确的设备;
- 如何让 vLLM 在启动时加载正确的插件。
启动阶段的逻辑可以抽象为:
sequenceDiagram
participant User as 用户启动 vLLM
participant Runtime as vLLM Runtime
participant Registry as Platform Registry
participant Plugin as vllm-ascend
participant CANN as CANN Runtime
User->>Runtime: 启动推理服务
Runtime->>Registry: 查找可用平台
Registry->>Plugin: 加载 AscendPlatform
Plugin->>CANN: 初始化 NPU Runtime
CANN-->>Plugin: 返回设备能力
Plugin-->>Registry: 注册平台能力
Registry-->>Runtime: current_platform = AscendPlatform
Runtime->>Runtime: 创建 Worker、Engine 和 Backend
这里有一个重要的工程事实:
平台注册只解决“让系统看见这个硬件”,不代表底层算子、通信和模型执行都已经可用。
5.2 Worker 与设备生命周期
Serving 核心通常不会直接操作每一种硬件的底层运行时,而是通过 Worker 负责:
- 设备初始化;
- 设备上下文建立;
- 模型加载;
- 权重搬运;
- 内存统计;
- 执行请求;
- 设备同步;
- 进程退出时资源释放。
昇腾 Worker 需要将这些流程映射到 NPU 和 CANN 的运行时模型中。
例如:
通用 Worker 生命周期
↓
NPU 设备选择
↓
CANN Runtime 初始化
↓
模型权重加载到 NPU
↓
创建 NPU 内存池
↓
加载 CANN / Custom Ops
↓
执行模型
↓
同步与错误处理
这里最容易被低估的是“错误处理”和“同步语义”。
不同设备的执行可能是异步的,算子错误也可能延迟到同步点才暴露。因此,Worker 不能只完成基本的 forward() 调用,还需要适配:
- 设备同步方式;
- 异步执行异常;
- 内存不足错误;
- 设备复位或上下文失效;
- 多进程下的设备隔离;
- 进程退出时的资源清理。
5.3 Attention Backend
Attention 通常是昇腾适配中最关键的部分之一。
一个 Ascend Attention Backend 至少需要回答:
- 使用哪一种 CANN Attention 算子;
- 输入张量布局是什么;
- Q、K、V 的数据类型是什么;
- KV Cache 的布局如何组织;
- 如何支持 paged KV Cache;
- prefill 和 decode 是否使用不同 Kernel;
- 是否支持 sliding window;
- 是否支持 MLA 或其他特殊 Attention 结构;
- 不同 head size 是否都能运行;
- 不支持的配置如何回退。
抽象来看,调用路径可以是:
Attention Layer
↓
AttentionBackend.forward()
↓
Ascend Attention Backend
↓
CANN / Custom Attention Operator
↓
NPU Kernel
一个简化的后端结构可能如下:
class AscendAttentionBackend(AttentionBackend):
@staticmethod
def get_impl_cls():
return AscendAttentionImpl
class AscendAttentionImpl:
def forward(
self,
query,
key,
value,
kv_cache,
attn_metadata,
):
# 根据 prefill/decode、KV Cache 布局等条件
# 调用对应的 CANN 或自定义算子
return ascend_attention_op(
query=query,
key=key,
value=value,
kv_cache=kv_cache,
metadata=attn_metadata,
)
真正的实现通常还需要处理张量布局转换、元数据构造和不同执行阶段的分派。
尤其要注意,Attention Backend 并不是只实现一个数学公式。它必须适配 vLLM 的运行时语义:
请求调度结果
↓
Attention Metadata
↓
Block Table / KV Cache 位置
↓
Prefill 或 Decode
↓
设备专用 Attention Kernel
如果平台只实现了一个能够计算 Attention 的算子,但不能正确理解 vLLM 的 KV Cache block 布局,那么它仍然不能作为完整的 vLLM Attention Backend 使用。
5.4 KV Cache 与内存管理
vLLM 的 KV Cache 管理器通常应该保持平台无关。它负责的是逻辑 block:
逻辑层:
block 0、block 1、block 2……
但这些 block 最终如何落到 NPU 内存上,则需要平台和 Worker 共同完成。
昇腾适配需要处理:
- NPU 内存容量查询;
- KV Cache block 大小计算;
- Cache Tensor 的创建;
- Cache Tensor 的数据类型;
- Cache Tensor 的布局;
- block table 到设备 Tensor 的映射;
- 内存池或缓存分配器;
- 多卡场景下的内存隔离。
理想的分层是:
graph TD
MANAGER["KV Cache Manager<br/>逻辑 Block 分配、回收、复用"]
WORKER["Ascend Worker<br/>把逻辑容量映射为 NPU 内存"]
ALLOC["NPU Memory Allocator<br/>内存池、设备 Tensor、生命周期"]
KERNEL["Ascend Attention Kernel<br/>按照 block table 读取 KV Cache"]
MANAGER --> WORKER
WORKER --> ALLOC
ALLOC --> KERNEL
关键点在于:
KV Cache Manager 不应该知道 Ascend 的内存 API;Ascend Worker 也不应该重新实现一套 KV Cache 调度逻辑。
如果这两个层次混在一起,未来接入另一种 NPU 时,就会再次出现核心逻辑复制。
5.5 基础算子与自定义 Kernel
完整的模型执行不仅包含 Attention,还包括大量基础算子:
- RMSNorm;
- LayerNorm;
- RoPE;
- SiLU、GELU 等激活函数;
- Linear / GEMM;
- Quantization;
- MoE Router;
- Top-k;
- Expert 执行;
- Logits 计算;
- Sampling 前后的数据处理。
这些算子可能来自:
- CANN 已有算子;
- PyTorch NPU 算子;
- vLLM 通用实现;
- Ascend Custom Operator;
- 针对特定模型或数据类型优化的专用 Kernel。
适配时不能只追求“能运行”,还需要确认:
- 算子是否支持目标数据类型;
- 输入输出布局是否一致;
- 是否产生隐式数据类型转换;
- 是否存在 CPU 回退;
- 是否引入不必要的设备同步;
- 是否支持动态 shape;
- 是否适合 decode 阶段的小 batch 场景。
例如,一个算子即使功能正确,但每次执行都触发设备同步,也可能严重拖慢 token 生成速度。
因此,算子适配需要同时验证:
数值正确性
+
形状正确性
+
设备放置正确性
+
异步执行正确性
+
性能可接受
5.6 量化支持
量化是硬件适配中非常容易产生差异的部分。
同一种量化名称,在不同硬件平台上可能对应不同实现:
权重存储格式
激活量化格式
缩放因子布局
反量化位置
矩阵乘法 Kernel
KV Cache 数据类型
因此,昇腾平台需要明确支持哪些量化方式,以及每一种量化方式对应什么实现。
平台层可以提供能力查询:
@classmethod
def get_supported_quantization(cls):
return {
"w8a8",
"w8a16",
# 具体能力取决于硬件、CANN 和插件版本
}
然后在模型加载或配置检查阶段提前拒绝不支持的组合,而不是等到运行中才失败:
模型要求:某种量化格式
↓
平台能力检查
↓
支持 → 选择对应 Kernel
不支持 → 明确报错或选择兼容路径
这也是硬件抽象的重要价值:
把硬件限制变成可查询、可验证的能力,而不是隐藏在深层 Kernel 错误中。
5.7 分布式通信与并行策略
单卡推理只是硬件适配的一部分。大模型部署经常需要:
- Tensor Parallel;
- Pipeline Parallel;
- Data Parallel;
- Expert Parallel;
- 多进程多卡执行;
- 节点间通信。
在 NVIDIA 环境中,常见通信组件是 NCCL;在昇腾环境中,则需要对接 HCCL 或相应的通信实现。
昇腾插件需要处理:
- 通信后端初始化;
- rank 和 world size;
- NPU 卡号与进程绑定;
- 集合通信算子;
- 通信拓扑;
- 多机环境变量;
- 通信异常和超时;
- Tensor Parallel 所需的 AllReduce、AllGather、ReduceScatter 等操作。
逻辑上,Serving 核心只需要表达:
communicator.all_reduce(tensor)
而具体实现由平台提供:
CudaPlatform → CudaCommunicator → NCCL
AscendPlatform → AscendCommunicator → HCCL
如果通信层没有正确适配,模型可能单卡正常、多卡却出现:
- 初始化失败;
- rank 卡死;
- 输出不一致;
- 通信性能异常;
- 进程无法正常退出。
所以,硬件适配的验证范围必须覆盖单卡和多卡。
6. OOT 适配的边界:不是“主仓库完全不用改”
“Out-of-Tree”经常被简化成一句话:
安装插件就可以支持新硬件,主仓库一行代码都不用改。
这句话表达了 OOT 的目标,但从工程角度看需要更谨慎。
OOT 能否做到真正独立,取决于主仓库是否已经提供足够稳定的扩展点,包括:
- Platform 接口;
- 平台注册机制;
- Attention Backend 接口;
- Worker 基类;
- Device Communicator 接口;
- Kernel 导入约定;
- 配置检查入口;
- 模型执行接口;
- KV Cache 和内存管理边界。
如果某项能力还没有抽象出来,插件就可能需要:
- 提交主仓库补丁;
- 扩展新的注册点;
- 临时使用兼容层;
- 等待上游版本提供接口;
- 针对不同 vLLM 版本维护不同分支。
因此,更准确的表述是:
OOT 把硬件实现从主仓库中隔离出来,但它仍然依赖主仓库提供稳定的扩展契约。
这也是为什么平台适配不只是“写一个 AscendPlatform 类”。它还需要持续跟踪:
- vLLM 内部接口变化;
- Attention Metadata 变化;
- Worker 生命周期变化;
- KV Cache 布局变化;
- 分布式接口变化;
- PyTorch 和 CANN 版本兼容性;
- 不同硬件型号的能力差异。
一个成熟的 OOT 插件,实际上是一个独立的适配层和发行生态。
7. 一次请求在异构硬件上的执行路径
把前面的模块组合起来,可以得到一个更完整的请求执行路径:
sequenceDiagram
participant API as OpenAI API / Client
participant Engine as vLLM Engine
participant Scheduler as Scheduler
participant Cache as KV Cache Manager
participant Worker as Platform Worker
participant Attn as Attention Backend
participant Device as GPU / NPU
API->>Engine: 发送请求
Engine->>Scheduler: 加入请求队列
Scheduler->>Cache: 分配或查找 KV Cache Block
Scheduler->>Worker: 下发本轮执行计划
Worker->>Attn: 执行 Attention
Attn->>Device: 调用平台专用 Attention Kernel
Device-->>Attn: 返回 Attention 结果
Worker->>Device: 执行其他模型算子
Device-->>Worker: 返回 logits
Worker-->>Engine: 返回执行结果
Engine-->>API: 返回 token
Engine->>Cache: 更新 KV Cache 状态
在这条路径中:
- API 层不关心设备;
- Engine 不关心设备;
- Scheduler 不关心设备;
- KV Cache Manager 不关心设备;
- Worker 负责设备生命周期;
- Attention Backend 负责关键 Attention 实现;
- Platform 负责能力查询、扩展加载和平台组件选择。
这就是硬件解耦真正想要达到的效果:
上层流程保持稳定,底层实现可以替换。
8. 如何判断硬件适配是否真正做到了解耦?
可以用下面几个问题进行检查。
8.1 检查一:Serving 核心是否出现设备判断?
重点搜索:
is_cuda()
is_ascend()
is_rocm()
device.type == ...
如果这些判断大量出现在 Scheduler、请求状态机和 KV Cache 逻辑中,说明硬件边界可能已经被突破。
8.2 检查二:平台能力是否可以被查询?
例如:
current_platform.get_attn_backend_cls(...)
current_platform.get_device_communicator_cls()
current_platform.import_kernels()
current_platform.check_and_update_config(...)
如果上层必须自己判断“这个芯片是否支持某算子”,说明能力抽象还不够完整。
8.3 检查三:不支持的配置是否能提前失败?
理想情况是:
启动阶段发现不支持
↓
给出明确错误信息
而不是:
服务启动成功
↓
请求执行到某个深层 Kernel 时崩溃
8.4 检查四:插件是否能独立演进?
一个好的 OOT 适配应该能够:
- 独立发布;
- 独立测试;
- 独立适配硬件驱动和算子库版本;
- 尽量减少对主仓库的侵入;
- 在主仓库升级时有清晰的兼容边界。
8.5 检查五:是否只完成了“能跑”,还是同时完成了“跑得好”?
硬件适配至少要验证:
功能正确
数值正确
数据类型正确
KV Cache 正确
多卡通信正确
性能达到预期
异常处理可用
能在 NPU 上返回结果,只能说明适配链路打通了;能在真实模型、真实 batch 和真实上下文长度下稳定达到目标吞吐,才算完成了工程适配。
9. 小结:Platform 是边界,不是万能胶
这一章最重要的结论可以概括为三句话。
第一:
Serving 核心应该依赖抽象能力,而不是依赖具体芯片。
第二:
Platform 是硬件能力中心,但不是所有底层组件的唯一调用父类。
Attention Backend、Kernel、通信组件、Worker 和平台扩展,都可能从 Platform 获取能力或被 Platform 直接派发。
第三:
Out-of-Tree 让硬件适配可以独立演进,但前提是主仓库提供稳定的扩展契约。
因此,vLLM 的硬件解耦并不是简单地增加几个平台类,而是建立了多层边界:
graph TD
CORE["Serving Core<br/>请求、调度、KV Cache、Engine"]
ABSTRACT["抽象契约<br/>Platform · Backend · Worker · Communicator"]
DEVICE["平台实现<br/>CUDA · ROCm · Ascend · XPU"]
RUNTIME["硬件运行时<br/>CUDA · HIP · CANN · oneAPI"]
KERNEL["高性能 Kernel<br/>Attention · GEMM · MoE · Quantization"]
CORE --> ABSTRACT
ABSTRACT --> DEVICE
DEVICE --> RUNTIME
RUNTIME --> KERNEL
style CORE fill:#e8f3ff,stroke:#4a90e2
style ABSTRACT fill:#fff4d6,stroke:#d99a00
style DEVICE fill:#e8f8ee,stroke:#4a9c68
最终,硬件差异应该停留在最底层:
芯片差异
↓
运行时差异
↓
平台实现差异
↓
Backend / Worker / Kernel 差异
↓
Serving 核心保持稳定
这正是一个高性能推理框架面对异构硬件时最重要的架构能力:
让底层硬件快速变化,让上层 Serving 逻辑尽量不变。
📂 本章源码导航
硬件平台
| 想看什么 | 从哪开始 |
|---|---|
| 平台抽象接口(本章核心) | vllm/platforms/interface.py |
| CUDA 平台实现 | vllm/platforms/cuda.py(注意 import_kernels() 与 get_attn_backend_cls() 是两条并行路径) |
| 其他平台 | vllm/platforms/(rocm.py、xpu.py、cpu.py) |
| Attention 分派点 | vllm/v1/attention/selector.py |