vLLM 源码完全拆解(六):Attention Backend 如何统一 FlashAttention 与 FlashInfer

10500 字
53 分钟
vLLM 源码完全拆解(六):Attention Backend 如何统一 FlashAttention 与 FlashInfer

从公共元数据走进后端接口#

前五篇已经从 vLLM 的整体架构 出发,依次追踪了 V1 引擎的一次请求、Scheduler.schedule() 的预算决策、KVCacheManager 的物理 Block 生命周期 和 GPUModelRunner 的输入批处理。上一站停在 CommonAttentionMetadata:不同长度的请求已经被压成一条连续 token 流,query_start_loc 保存请求边界,seq_lens 保存完整上下文长度,block_table 和 slot_mapping 则把逻辑位置落到分页 KV Cache。

但这些字段还不是任何一个高性能 Kernel 的最终参数。FlashAttention 可以直接消费二维 block table、累计 query 长度和有效 KV 长度;FlashInfer 的原生分页接口则习惯使用 CSR 风格的 paged_kv_indptr、paged_kv_indices、paged_kv_last_page_len,还要在执行前完成一次 plan();Triton、ROCm、MLA、Mamba 等路径又有各自的约束。若 GPUModelRunner 直接认识每一种 Kernel,模型、缓存布局、硬件平台和编译模式的组合分支会迅速淹没主流程。

vLLM 因而在模型层与具体算子之间建立了 Attention Backend 抽象。它不是简单的“把函数名放进字典”,而是同时回答四个问题:当前层能选哪个实现、该实现要求什么 KV 布局、怎样把公共批次语义降成 Kernel 专用 metadata、模型 forward 最终调用哪个实现对象。

本文延续系列既有基线,主要阅读 2026 年 9 月 26 日访问的 vLLM main 分支提交 3b4566c5。vLLM 的后端注册表与选择规则更新很快,本文会区分稳定抽象与该提交中的具体实现,避免把今天的优先级误写成永远不变的规则。FlashAttention 最初由 Tri Dao 等人在 2022 年 NeurIPS 论文中提出;FlashInfer 由华盛顿大学等团队在 2023 年开始开源,它们在 vLLM 中扮演的是可替换执行后端,而不是调度器或 KV 分配器。

本篇只走到接口层和元数据层。PagedAttention Kernel 内部怎样安排线程、如何合并访存、怎样分块归约并保持 softmax 数值稳定,将在下一篇单独展开。

为什么注意力需要独立 Backend#

线性层、RMSNorm 等算子的输入形状通常比较规整;服务场景中的注意力却同时依赖模型结构、动态请求状态和物理缓存布局。对一次普通 decoder attention,可写成:

O=softmax⁡(QKTd+M)VO=\operatorname{softmax}\left(\frac{QK^{\mathsf T}}{\sqrt{d}}+M\right)V

QQ 是本轮新 token 的 query,K,VK,V 包含此前缓存和本轮新增的 Key/Value,dd 是 head dimension,MM 编码因果、滑动窗口或其他可见性规则。这个公式没有说明 K,VK,V 在显存中是否连续,也没有说明一个 batch 里每条请求的 query 和 context 各有多长,更没有说明 FP8 缓存如何反量化。服务框架必须把这些运行时事实一起交给 Kernel。

至少六个维度会改变可用实现:

维度典型取值对后端的影响
硬件NVIDIA Ampere/Hopper/Blackwell、AMD、Intel XPU、CPU指令集、TMA、Tensor Core、可用第三方库不同
数据类型FP16、BF16、FP8、NVFP4 KVKernel 是否支持、scale 位于 host 还是 device、缓存布局不同
模型几何head size、Q/KV 头数、MHA/GQA/MQAtile 形状、XQA 可用性、页大小限制不同
注意力语义causal、non-causal、sliding window、sink、cross-attentionmask 与历史保留规则不同
服务特性prefix cache、DCP/PCP、KV Connector、投机解码metadata、通信和 query 长度语义不同
执行模式eager、piecewise CUDA Graph、full CUDA Graph地址稳定性、launch grid 与 host plan() 是否可捕获不同

因此“哪个 Kernel 更快”不是单一排名。某实现可能在 Hopper 的 BF16 decode 上占优,却不支持当前 head size;某实现支持 FP8 KV,却要求 page size 为 64;某实现能跑普通 causal attention,却无法表达多模态 PrefixLM 的双向图像区间。后端选择首先是可行性过滤,其次才是性能优先级。

分页注意力中 Query Head 遍历物理 KV 页
分页注意力中 Query Head 遍历物理 KV 页

图源:vLLM Triton Attention Backend Deep Dive Figure 3。横向是 query token,纵向是 query head,内层遍历分页 KV Cache。图中省略了因果 mask 和滑动窗口,但已经揭示接口必须提供的两类信息:query 的逻辑边界,以及 KV 页的物理索引。

独立 Backend 带来的并不是“任意后端都能无代价互换”。它提供的是一份明确契约:上游交付统一语义,下游声明能力和布局约束。若契约不满足,Selector 应在启动阶段拒绝配置,而不是运行到某个请求后才让 Kernel 读错内存。

一张源码地图:从模型层到 Kernel#

先把对象和时间线分开。Attention Backend 相关对象并不是在每个 token step 中全部重建,它们分布在模型加载、KV Cache 初始化和逐步执行三个阶段。

模型构造阶段
model definition 创建 Attention layer
-> get_attn_backend(...)
-> AttentionBackend class
-> backend.get_impl_cls()
-> 每层一个 AttentionImpl
-> layer 注册进 static_forward_context
KV Cache 初始化阶段
每层导出 KVCacheSpec
-> Core 形成 KV cache groups
-> Worker.initialize_attn_backend()
-> 按 backend + spec + num_heads_q 建 AttentionGroup
-> backend.get_builder_cls()
-> 每个 group 创建 MetadataBuilder
-> 选择共同 kernel block size 并绑定 KV tensor
每个引擎 step
GPUModelRunner 构造 CommonAttentionMetadata
-> 每个 AttentionGroup 的 builder.build()
-> layer_name -> backend-specific metadata
-> set_forward_context(...)
-> 模型逐层 forward
-> Attention.forward()
-> AttentionImpl.forward()
-> FlashAttention / FlashInfer / Triton 等 Kernel

这条链上有四种关键身份:

  • AttentionBackend 是类级能力描述与工厂,回答“支持什么、应创建什么”。它本身不保存某一批请求的状态。
  • AttentionImpl 是层级执行对象,保存 head 数、scale、sliding window、量化模式等层参数,并在每次 forward 时调用 Kernel。
  • AttentionMetadataBuilder 是 group 级适配器,持有 workspace 和可复用 buffer,把一批公共 metadata 编译为专用执行计划。
  • AttentionMetadata 是 step 级产物,描述这一次批次的 query、分页 KV、prefill/decode 路径和图捕获所需状态。

AttentionGroup 把后端、若干 layer name、一个 KVCacheSpec 和对应 builder 绑在一起。模型的层数可能是 32 或 80,但若它们共享后端、缓存规格和 Q 头数,一次 metadata build 的结果可以被多层复用。这样,按请求和页表进行的 Python/CPU 准备成本不会机械乘以层数。

代码入口可按下列顺序定位:

目标固定提交中的文件
选择后端vllm/v1/attention/selector.py
抽象接口和公共 metadatavllm/v1/attention/backend.py
建立 AttentionGroupvllm/v1/worker/gpu_model_runner.py
group 与 block size 工具vllm/v1/worker/utils.py
模型层与 Impl 调用vllm/model_executor/layers/attention/attention.py
FlashAttention 适配vllm/v1/attention/backends/flash_attn.py
FlashInfer 适配vllm/v1/attention/backends/flashinfer.py

第一次读源码时,不应从两个两千行以上的具体后端开头顺序下读。先沿这张图建立对象生命周期,再分别进入 builder 和 impl,才能判断某段代码发生在初始化、step 前处理还是层内热路径。

Backend Selector:不是按名字做一次分支#

每个 Attention 层在构造时调用 get_attn_backend()。传入的不只是用户选择的名字,还包括该层与本次运行的能力向量:

self.attn_backend = get_attn_backend(
head_size=head_size,
dtype=dtype,
kv_cache_dtype=kv_cache_dtype,
use_mla=False,
has_sink=self.has_sink,
use_mm_prefix=self.use_mm_prefix,
use_per_head_quant_scales=use_per_head_quant_scales,
attn_type=attn_type,
has_sliding_window=sliding_window is not None,
)

Selector 又从全局 VllmConfig 补入 block size、KV Connector、DCP、PCP、自适应投机验证、非因果模式和 batch invariance 等信息,打包成 AttentionSelectorConfig。随后才把用户显式指定的 backend 或自动模式交给当前 platform:

Attention layer 的局部约束
+
VllmConfig 的运行级约束
↓
AttentionSelectorConfig
↓
current_platform.get_attn_backend_cls(...)
↓
完全限定类名
↓
lazy import -> type[AttentionBackend]

把平台判断放到 current_platform 而不是 Selector 主文件,原因是 CUDA、ROCm、XPU 与 CPU 的候选集和优先级根本不同。公共层只负责收集事实;平台层负责判断某个实现能否在当前设备上成立。

自动选择与手工指定语义不同#

自动模式会按平台注册的优先顺序检查候选后端,选取第一个通过配置验证的实现。手工指定 --attention-backend FLASH_ATTN 或 --attention-config.backend FLASH_ATTN 时,框架仍然会检查 dtype、head size、compute capability、block size 和特性组合;不兼容应直接报错,而不是静默换成另一个实现。官方的 Attention Backend Feature Support 页面由注册表和 validate_configuration() 检查自动生成,适合核对具体版本的支持矩阵。

这一区别对性能实验很重要。若用户以为自己固定了 FlashInfer,而框架悄悄退回 FlashAttention,实验结论会失去意义;反过来,自动模式若在每项新特性出现后都要求人工选后端,也无法成为可靠默认值。显式配置采用“验证失败即停止”,自动配置采用“过滤后选最优可用者”,两者分别服务可复现性和易用性。

为什么选择结果要缓存#

_cached_get_attn_backend() 使用函数缓存。同一组选择参数在数十层模型中反复出现时,无需重复探测平台和导入类。缓存键必须包含所有会改变结果的运行级因素;固定提交甚至显式把 _run_kv_cache_dtype 放入键中,因为某些平台选择器从当前配置读取它。若某个影响布局的参数没有进入键,不同层或 draft/target 模型就可能错误共享选择结果。

Backend 还会反过来约束 KV Cache#

选择并不是“缓存已经建好后再挑一个 Kernel”。AttentionBackend 暴露 get_supported_kernel_block_sizes()、supported_kv_cache_layouts() 和 customize_spec()。例如某些 FlashAttention 路径要求页大小是 16 的倍数,某些特化 Kernel 固定要求 64 或 128 token 页;FlashInfer 不同硬件路径支持的 page size 集合也不同。模型层先根据后端定制 KVCacheSpec,Worker 再在同一 KV cache group 的所有后端之间求共同可用的 kernel block size。

因此选择错误不只是慢一点。最严重的后果是生产者按一种布局写 KV,消费者按另一种布局解释相同字节。vLLM 试图在启动阶段通过能力检查、共同 block size 和全模型布局协商消除这种歧义。

AttentionGroup:层、后端与 KV Cache Group 怎样绑定#

上一篇已经出现 kv_cache_group,本篇又出现 AttentionGroup。两者不是同一个概念。

KV cache group 解决存储与生命周期问题:哪些层的 cache spec 可以由同一组 block table、相同页几何和同一种回收语义管理。Attention group 解决执行适配问题:在一个 KV cache group 内,哪些层可以共享同一种 backend metadata。

固定提交中的分组键可以概括为:

Gattn=(Cbackend, Skv, Hqrank)G_{attn}=\left(C_{backend},\ S_{kv},\ H_q^{rank}\right)

CbackendC_{backend} 是后端类的完整类名,SkvS_{kv} 是该层的 KVCacheSpec,HqrankH_q^{rank} 是当前并行 rank 持有的 query head 数。三个量全部相同的层才进入同一个 AttentionGroup。

为什么还要把 HqrankH_q^{rank} 放进键?Builder 可能按 Q 头数预分配 scratch 或 workspace。投机解码 draft head 与 target model 即使使用同一后端和缓存规格,也可能拥有不同 Q 头数;强行共享 builder 会让内部 buffer 尺寸和 Kernel 参数错误。这个字段看似重复,实际是在防止形状合法但语义错误的内存访问。

两层索引结构#

Runner 保存的结构近似:

attn_groups[kv_cache_group_id][attention_group_id]

外层选择哪套 block table 和 slot mapping,内层选择哪种 backend、哪些 layer name 共享 builder。一个普通 Llama 模型可能只有一个 KV cache group,内部也只有一个 AttentionGroup;混合模型则可能出现:

KV group 0: full attention
AttentionGroup 0: FlashAttention, layers [0, 2, 4, ...]
KV group 1: sliding-window attention
AttentionGroup 0: FlashAttention, layers [1, 3, 5, ...]
KV group 2: recurrent state
AttentionGroup 0: Mamba backend, layers [...]

若同一存储组中部分层因特性差异选到不同 backend,还会继续拆成多个 AttentionGroup。分组不是为了给文章画出漂亮层次,而是为了精确确定“一份 metadata 可以安全给谁用”。

Builder 为什么属于 Group,不属于 Layer#

假设 32 层 decoder 的每层都使用相同 FlashAttention 配置。query_start_loc、seq_lens、block table 和 batch 的 prefill/decode 划分对所有层相同;差别只在每层自己的 Q/K/V、KV cache tensor 和 scale。若每层各自把同一批次 metadata 构造 32 次,CPU 热路径会重复完成相同工作。

因此 AttentionGroup.create_metadata_builders() 只创建 group 级 builder,构建完成后把同一个 metadata 对象登记给组内所有 layer_name:

for layer_name in attn_group.layer_names:
attn_metadata_dict[layer_name] = attn_metadata_i

当混合 KV group 只有 block table 与 slot mapping 不同时,Runner 还会按 (KVCacheSpec, builder type) 缓存第一次构造结果。若 builder 声明 supports_update_block_table=True,后续 group 可以复用控制计划,只替换页表与逐 token 写入地址。这个优化把“批次结构相同”和“物理 KV 页面不同”拆开,减少重复 CPU 工作。

KV tensor 怎样在模型层出现#

Engine Core 只管理 block ID,不持有真正 K/V。Worker 分配 KV tensor 后,bind_kv_cache() 按 layer name 把对应 tensor view 绑定到 Attention 层。模型 forward 到某层时,get_attention_context(layer_name) 从 forward context 取出三样东西:该层的 backend metadata、绑定的 KV tensor、对应 slot mapping。层内实现因而不需要向 Scheduler 查询任何状态,也不需要知道其他层属于哪个 group。

三层接口:Backend、Impl 与 MetadataBuilder#

三层接口最容易被误读成同一对象的不同名字。实际上它们分别处于静态能力、层内执行和批次编译三个时间尺度。

AttentionBackend:能力声明与类工厂#

抽象类的最小核心近似为:

class AttentionBackend(ABC):
@staticmethod
def get_name() -> str: ...
@staticmethod
def get_impl_cls() -> type[AttentionImplBase]: ...
@staticmethod
def get_builder_cls() -> type[AttentionMetadataBuilder]: ...

除此之外,它还声明支持的 dtype、KV dtype、head size、block size、attention type、sliding window、sink、多模态 prefix、context parallel 和 KV cache layout。Selector 使用这些声明判定可行性,KV Cache 初始化使用布局与 block size 声明确定物理存储,Runner 使用两个工厂方法创建 Impl 与 Builder。

Backend 通常是类而不是实例,因为这些属性不属于某一层或某一批请求。FlashAttentionBackend.get_name() 返回 FLASH_ATTN,get_impl_cls() 返回 FlashAttentionImpl,get_builder_cls() 返回 FlashAttentionMetadataBuilder。这种关系使注册表只需保存一个后端类路径,就能把整套实现接进框架。

AttentionImpl:每层持有的执行对象#

Attention 层拿到 backend 后创建 Impl,并把本层的 num_heads、num_kv_heads、head size、softmax scale、sliding window、KV dtype、ALiBi 或 sink 等参数传入。它的核心签名是:

def forward(
layer,
query,
key,
value,
kv_cache,
attn_metadata,
output,
...,
): ...

其中 Q/K/V 和输出随层变化,kv_cache 是该层绑定的物理缓存视图,attn_metadata 则可能由同组多层共享。Impl 把通用层参数和当前批次 metadata 拼成真正的 Kernel 调用。

FlashAttention 与 FlashInfer 在固定提交中都把 forward_includes_kv_cache_update 设为 False。这意味着层 forward 先通过统一的 KV update 自定义算子,把本轮 K/V 按 slot_mapping 写入缓存,再调用 attention 实现读取完整缓存。分开写入和读取可以显式建立编译器可见的数据依赖,也允许不同 attention Kernel 共享缓存写入路径。若某个 backend 把写入融合进 attention,它可以把该标志设为 True,由实现自行维护顺序。

AttentionMetadataBuilder:把批次语义编译成执行计划#

Builder 的公共入口是:

def build(
common_prefix_len,
common_attn_metadata,
fast_build=False,
) -> BackendSpecificMetadata:
...

它不是纯函数。Builder 可以持有长期 workspace、pinned CPU buffer、CUDA Graph 稳定地址、不同 batch size 的 wrapper,以及已探测的 Kernel 能力。逐 step 的 build() 只改写这些预分配容器的有效区域,并产出轻量 metadata 引用。

抽象类还公开几项影响 Runner 行为的能力:

  • reorder_batch_threshold:是否要把短 query 的 decode 请求移动到 batch 前部;
  • supports_update_block_table:相同结构的其他 cache group 是否只需替换页表;
  • get_cudagraph_support():支持任意混合 batch、仅统一 query 长度、仅单 token decode,还是完全不支持;
  • build_for_cudagraph_capture():图捕获时是否需要专门构造稳定 metadata;
  • build_for_drafting():投机 draft 的短生命周期计划能否走更快构造路径。

这解释了为何 metadata 构造不是若干 tensor 字段的机械复制。Builder 实际在做一次小型编译:识别批次形态、选择执行分支、准备索引和 workspace,并保证这些决定与 CUDA Graph 及具体 Kernel ABI 一致。

公共元数据怎样变成后端专用计划#

CommonAttentionMetadata 保存的是数学与地址意义上的公共事实。固定提交中的主干字段可以分成四组:

组别字段含义
query 布局query_start_loc、CPU 副本、num_actual_tokens、max_query_len扁平 query 中每条请求的边界
context 布局seq_lens、max_seq_len本轮完成后每条请求可见的完整序列长度
KV 地址block_table_tensor、slot_mapping历史 KV 页和本轮 KV 写入槽位
语义与扩展causal、positions、is_prefilling、encoder/DCP/MM prefix 字段mask、阶段与高级功能

对请求 ii,query 长度和本轮开始前的 context 长度分别为:

qi=si+1−siq_i=s_{i+1}-s_ici=li−qic_i=l_i-q_i

sis_i 来自 query_start_loc,lil_i 来自 seq_lens。Backend 可以由同一组公共字段恢复“本轮新算多少”和“此前已有多少”,无需上游硬编码 PREFILL、DECODE 两套对象。

FlashInfer 的 Ragged Query/KV 布局
FlashInfer 的 Ragged Query/KV 布局

图源:FlashInfer KV-Layout 教程。不同请求的 Q、K、V 按有效 token 连续存放,indptr 的相邻差值恢复各请求长度。vLLM 的 query_start_loc 正是同类前缀和表示,不需要把每条序列 padding 到 batch 中的最长长度。

Runner 怎样为多个 group 构造 metadata#

GPUModelRunner._build_attention_metadata() 先以第一个 cache group 的 block table 和 slot mapping 构造一份公共模板。随后遍历 KV cache group:

  1. 浅拷贝公共模板;
  2. 替换本 group 的 encoder length、block table 和 slot mapping;
  3. 遍历该 group 内的 AttentionGroup;
  4. 取出对应 builder,调用 build();
  5. 把返回对象登记到组内每个 layer name。

简化伪代码如下:

for kv_gid, kv_group in enumerate(kv_cache_groups):
common = copy(common_base)
common.block_table_tensor = block_tables[kv_gid]
common.slot_mapping = slot_mappings[kv_gid]
for attn_group in attn_groups[kv_gid]:
builder = attn_group.get_metadata_builder()
metadata = builder.build(common_prefix_len, common)
for layer_name in attn_group.layer_names:
per_layer_metadata[layer_name] = metadata

模型 forward 期间,set_forward_context() 把这个 layer_name -> metadata 映射挂到线程或编译上下文。Attention.forward() 的 Python 签名仍然只有 Q/K/V;进入统一 attention 自定义算子后,get_attention_context(layer_name) 才取回 metadata 和 KV tensor。这样,模型定义不必把十几个服务字段沿每层函数签名传递,torch.compile 也可以把注意力视为一个具有明确副作用的边界。

为什么保留 CPU 和 GPU 两份长度#

某些 builder 需要在 CPU 上决定分流、调用第三方 plan() 或计算最大长度,Kernel 又需要设备 tensor。每轮把 GPU seq_lens 同步回 CPU 会在热路径上形成硬同步,因此 Runner 尽量同时维护 CPU 上界和 GPU 真值。

异步投机验证会让 CPU 侧长度暂时是“草稿全部接受”的乐观上界,而设备端 query 边界才是实际接受结果。Backend 必须声明自己是否支持这种 device/CPU query length 不一致;不能把上界当精确长度交给需要严格边界的 Kernel。公共 metadata 同时保留两份视图,正是为了让 Builder 明确选择,而不是在内部偷偷触发 D2H 同步。

公共层为何不生成巨大 mask#

若总 query 数为 QQ、最长 context 为 LL,显式布尔 mask 的规模接近 Q×LQ\times L。分页长上下文下,这既浪费显存,也会额外消耗写入和读取带宽。公共 metadata 只保存 O(R)O(R) 的请求边界、O(Q)O(Q) 的逐 token 地址和 O(P)O(P) 的页表,其中 RR 是请求数、PP 是实际页表项数。具体 Kernel 根据因果标志、窗口和边界在片上生成访问范围,把结构信息保留到最接近计算的位置。

FlashAttention 路径:变长序列与分页 KV#

FlashAttention Builder 的特点是:vLLM 的公共表示与底层接口已经比较接近。FlashAttentionMetadata 直接保留 query_start_loc、seq_lens、二维 block_table、slot_mapping、最大 query/context 长度和实际 token 数。对普通非级联路径,Impl 取出:

cu_seqlens_q = attn_metadata.query_start_loc
seqused_k = attn_metadata.seq_lens
max_seqlen_q = attn_metadata.max_query_len
max_seqlen_k = attn_metadata.max_seq_len
block_table = attn_metadata.block_table

cu_seqlens_q 告诉变长 attention 每条 query 在连续 Q tensor 中的边界;seqused_k 给出每条请求实际使用的 KV 长度;block_table 则让 Kernel 按逻辑页读取不连续物理缓存。与上一节公式对应,Kernel 可以为请求 ii 处理 qiq_i 个 query,并限制其 Key 范围到 lil_i。

Builder 主要在决定什么#

普通字段复制只是开始。Builder 还会处理:

  • 是否启用 cascade attention,把公共前缀与请求私有后缀分开计算;
  • DCP 下各 rank 的本地 context 长度,以及 prefill/decode 分段;
  • FlashAttention 版本与 ahead-of-time scheduler metadata;
  • CUDA Graph 捕获时固定的 split 上限和持久 buffer;
  • 多模态 PrefixLM 的双向区间与 R-SWA prefix 长度;
  • 动态 causal、sliding window、sink 和量化 KV 所需的附加字段。

因此 FlashAttention metadata 看起来接近公共结构,不代表 Builder 可被删除。公共结构描述“批次是什么”,Builder 还要决定“这一版 FlashAttention 用什么控制路径执行”。

KV Cache 视图必须匹配 Kernel ABI#

固定提交中 FlashAttention 的 KV cache 注释形状为:

[num_blocks, num_kv_heads, block_size, 2 * head_size]

Impl 在 forward 中通过 transpose 和 split 得到 Key/Value view,并修正 size-1 维度的退化 stride。这个细节对使用 TMA 的 FA3/FA4 很关键:张量元素完全相同,不代表 stride 一定满足至少 16 字节的对齐要求。Backend 适配层必须把框架分配的通用 buffer 解释成 Kernel 接受的视图,必要时只改 metadata stride,避免真正复制整块 KV。

若 KV 使用 FP8,缓存底层可能以字节容器保存,Impl 再 view 成平台 FP8 dtype,并把 Q/K/V scale 按请求与 KV head 展开。量化支持因设备和 FlashAttention 版本而异,所以它既出现在 Selector 的能力检查里,也出现在 Impl 的运行时准备里:前者防止不可能的组合启动,后者提供真实 Kernel 参数。

为什么 KV 写入在 Attention 之前#

FlashAttention Backend 声明 forward 不包含 KV update。模型层先执行:

unified_kv_cache_update(K, V, slot_mapping)
↓ 数据依赖
unified_attention_with_output(Q, kv_cache, metadata)

本轮 query 在因果 attention 中需要看到本轮已经产生的 K/V,例如 prefill chunk 内第 jj 个 query 要看到同一 chunk 中不晚于自己的位置。先按 slot_mapping 把整个 chunk 写入分页 cache,再由 Kernel 用 causal 边界限制可见范围,可以统一 prefill、extend 与 decode。自定义算子返回的 dummy dependency 并不承载数据,它用于阻止编译器错误地把缓存读取重排到写入之前。

版本不是 Backend 名称的一部分#

用户通常选择 FLASH_ATTN,Impl 再根据 compute capability、head size、KV dtype、sink、soft cap 等条件解析 FA2、FA3 或 FA4。这样,上层 Backend 名称保持稳定,版本差异留在实现内部;代价是性能复现实验必须记录实际日志中的 FlashAttention 版本,不能只写“使用 FLASH_ATTN”。

不同版本对 CUDA Graph 的支持也不同。固定提交把 FA3 Builder 视为可支持更一般图模式,而 FA2 在混合 prefill/decode 和特殊 GQA 路径上限制更多。Runner 会综合所有 AttentionGroup 的 AttentionCGSupport,若任一后端不能支持完整图,就降到 piecewise 或 eager,而不是让某一层在重放时使用错误形状。

FlashInfer 路径:Plan/Run、Workspace 与分页布局#

FlashInfer 对分页 KV 的接口更像稀疏矩阵库:页表被压成 CSR 风格的三个数组,prefill 和 decode 由不同 wrapper 执行,wrapper 在 run() 前先调用 plan() 准备调度。vLLM 的 Builder 因而承担更多结构转换。

FlashInfer 的分页 KV Cache 索引布局
FlashInfer 的分页 KV Cache 索引布局

图源:FlashInfer KV-Layout 教程。每个请求拥有一串物理页号,所有请求的页号拼成 kv_page_indices;kv_page_indptr 给出每条请求的页号区间;kv_last_page_len 记录最后一页的有效 token 数。FlashInfer 只负责按照这些索引计算 attention,不负责分配或回收页面。

从二维 Block Table 到 CSR 页表#

设请求 ii 的有效序列长度为 lil_i,page size 为 BB,其有效页数为:

ni=⌈liB⌉n_i=\left\lceil\frac{l_i}{B}\right\rceil

所有请求页数的前缀和构成:

p0=0,pi+1=pi+nip_0=0,\qquad p_{i+1}=p_i+n_i

这就是 paged_kv_indptr。二维 vLLM block table 中每行前 nin_i 个有效物理 ID 被拼入一维 paged_kv_indices。最后一页有效长度为:

ri={B,li>0 且 li mod B=0li mod B,其他情况r_i=\begin{cases} B, & l_i>0\ \text{且}\ l_i\bmod B=0\\ l_i\bmod B, & \text{其他情况} \end{cases}

对应 paged_kv_last_page_len。不能把余数为 0 直接写成 0,因为这表示最后一页是满页,而不是没有有效 token。

固定提交用 NumPy 在 CPU 侧计算页数和 indptr,再用 Triton Kernel 从 GPU 二维 block table 抽取有效页号。原生 FlashInfer wrapper 的 plan() 还会消费 CPU 侧 query/page 指针和长度,因此 Builder 使用预分配 buffer,并在必要时将短小控制数组放到 pinned memory,保持 non-blocking copy 的可能性。

为什么先把 Decode 移到前面#

FlashInferMetadataBuilder 的 reorder_batch_threshold 默认为 1。Runner 在构造输入前可把 query 较短的请求移到 batch 前部;Builder 再将批次拆成:

[decode requests / tokens][prefill or extend requests / tokens]

普通 decode 的 query 长度为 1;某些支持 spec-as-decode 的路径会把阈值提高到“1 + 最大 draft token 数”,让短投机验证仍走 decode Kernel。不能简单用请求状态标签分流,因为 chunked prefill、短 extend 和投机验证可能拥有相近 query 长度,但对 mask 与 Kernel 能力的要求不同。Builder 综合 query length、causal 模式和后端能力进行划分。

FlashInferMetadata 因而不只是一张页表,它显式记录 num_decodes、num_decode_tokens、num_prefills、num_prefill_tokens,并分别持有 prefill 与 decode 对象。模型层拿到的 Q tensor仍是一条连续流,Impl 只需按计数切出前后两个 view。

plan() 与 run() 分别做什么#

以原生分页 prefill wrapper 为例,Builder 先执行近似调用:

prefill_wrapper.plan(
qo_indptr=query_offsets,
paged_kv_indptr=page_offsets,
paged_kv_indices=page_ids,
paged_kv_last_page_len=last_page_len,
num_qo_heads=num_q_heads,
num_kv_heads=num_kv_heads,
head_dim_qk=head_dim,
page_size=page_size,
causal=causal,
...,
)

模型执行到每一层时,Impl 再调用:

prefill_wrapper.run(query, kv_cache_tuple, out=output)

plan() 根据当前 batch 的变长结构选择调度并准备索引或 workspace;run() 只接收本层 Q 和 KV cache view,复用该计划。若同一个 AttentionGroup 覆盖 32 层,plan 成本只支付一次,run 执行 32 次。这正是 Builder 与 Impl 分开的直接收益。

decode 原生路径同样有 BatchDecodeWithPagedKVCacheWrapper。为支持 full CUDA Graph,Builder 可以按捕获 batch size 保存不同 wrapper,使 workspace 地址和内部 buffer 在重放间稳定。固定提交还可能在 Hopper/Blackwell 上把 decode 分发到 FlashInfer 暴露的 XQA 或 TensorRT-LLM generation API;这些路径直接使用 GPU block table 和 seq_lens,不一定需要原生 CSR 页号。也就是说,FLASHINFER 本身还是一个二级调度器,内部会根据硬件、页大小、GQA 结构、dtype 和 batch 阶段选择 native、XQA 或 TRT-LLM 路径。

FlashAttention 与 FlashInfer 的本质差异#

方面FlashAttention BackendFlashInfer Backend
公共页表降级多数路径保留二维 block table原生路径转换成 CSR 页表三元组
混合 batch单套 varlen metadata 可覆盖多种请求通常先重排,再拆 prefill/decode wrapper
预执行准备Builder 准备 scheduler/cascade 等 metadataBuilder 显式 plan() 并管理 workspace
层内调用直接调用对应 FA varlen/paged 路径对前后两段分别 run(),或调用 XQA/TRT-LLM API
图捕获难点版本、split、动态网格与 metadata 稳定性wrapper/plan buffer、batch size 和地址稳定性

“统一 Backend 接口”并没有把两者强行做成相同内部结构。它只统一上游交付和下游调用边界,允许每个实现保留最适合自身 Kernel 的数据表示。

同一批混合 Prefill 与 Decode 的完整推演#

用一个小批次把两条后端路径放在同一坐标系中。设 page size 为 16,本轮有三个请求,Runner 已把普通 decode 放到最前:

请求本轮含义已计算 token本轮 query完成后 seq_len物理 block IDs
A普通 decode1271128[7,2,9,4,11,6,3,10]
B首段 prefill044[5]
C后续 extend32335[1,8,12]

Runner 先构造公共事实#

本轮扁平 query 长度为 1+4+3=81+4+3=8:

query_start_loc = [0, 1, 5, 8]
seq_lens = [128, 4, 35]
max_query_len = 4
max_seq_len = 128
num_actual_tokens = 8

block_table_tensor 是三行二维表,不足最大宽度的尾部使用无效或 null 值填充。slot_mapping 则已经逐 token 算好写入地址。A 的新位置是 127,逻辑页号为 7、页内偏移为 15,对应物理 block 10,因此:

slotA=10×16+15=175slot_A=10\times16+15=175

B 的位置 0–3 写到物理 block 5 的槽位 80–83;C 的位置 32–34 位于逻辑页 2,写到物理 block 12 的槽位 192–194。最终:

slot_mapping = [175, 80, 81, 82, 83, 192, 193, 194]

这份公共 metadata 不包含“必须调用 FlashAttention 还是 FlashInfer”的答案。它只承诺请求边界、有效长度和地址正确。

FlashAttention Builder 的结果#

在普通非级联路径中,Builder 基本保留:

cu_seqlens_q = [0, 1, 5, 8]
seqused_k = [128, 4, 35]
block_table = 三行二维页表
max_q = 4, max_k = 128

Impl 可以让一个 varlen/paged 调用处理全部八个 query。Kernel 通过相邻 cu_seqlens_q 的差知道 A、B、C 分别有 1、4、3 个 query,通过 seqused_k 知道其 Key 长度为 128、4、35,并沿各自行的物理 block ID 取历史 K/V。B 和 C 的多 token query 在各自区间内应用因果 mask,不会跨请求互相注意。

FlashInfer Builder 的结果#

Builder 识别 A 为 decode,B、C 为 prefill/extend:

num_decodes = 1
num_decode_tokens = 1
num_prefills = 2
num_prefill_tokens = 7

三条请求的有效页数为 [8,1,3],于是完整 CSR 页表为:

paged_kv_indptr = [0, 8, 9, 12]
paged_kv_indices = [7,2,9,4,11,6,3,10, 5, 1,8,12]
paged_kv_last_page_len = [16, 4, 3]

decode wrapper 只消费 A 对应的前缀切片;prefill wrapper 的 qo_indptr 从原数组切出并减去起点,变成 [0,4,7],对应 B 与 C 的七个 query。Builder 分别 plan 两条路径,Impl forward 再把 Q 和 output 按 token 数切开:

Q[:1] -> decode run -> output[:1]
Q[1:8] -> prefill run -> output[1:8]

两条 run 写回同一个预分配输出 tensor,所以模型后续层仍看到与扁平输入完全相同的 token 顺序。后端内部拆成两个 Kernel 并不改变上游 batch 语义。

这个例子说明了什么#

同一份公共批次在 FlashAttention 中更接近“一次变长调用”,在 FlashInfer 中更接近“一个 decode 计划加一个 prefill 计划”。如果把 FlashInfer 的 CSR 转换塞进 GPUModelRunner,Runner 就必须认识它的 wrapper 生命周期;如果让 FlashAttention 也伪装成相同 CSR/plan 接口,又会增加无意义转换。Backend 层让共同事实保持共同、专用计划保持专用。

CUDA Graph、级联注意力与高级路径#

Backend 抽象的真正压力不在普通单卡 causal attention,而在动态批次与静态执行图、共享前缀和并行分片同时出现时。这里选三个最能暴露接口价值的分支。

CUDA Graph 约束的是控制路径,不只是 Tensor 形状#

CUDA Graph 重放要求设备地址和被捕获的操作图稳定。Runner 可以把真实 token 数 QQ padding 到捕获形状 Q′Q',并复用预分配 input/output buffer,但这还不够:attention backend 内部若根据当前长度创建新 tensor、改变 launch 数量或重新分配 workspace,图仍无法安全重放。

因此 Builder 用 AttentionCGSupport 声明四级能力:

级别可安全捕获的批次
ALWAYS包括混合 prefill/decode 在内的一般批次
UNIFORM_BATCH所有请求 query 长度一致,例如固定宽度投机验证
UNIFORM_SINGLE_TOKEN_DECODE仅每请求一个 token 的统一 decode
NEVER不支持该图模式

Runner 必须取所有活跃 AttentionGroup 的共同下界。一个模型有 30 层支持 full graph、2 层只支持单 token decode,整次模型执行就不能把混合 prefill/decode 当作 full graph 捕获。图模式是全执行链契约,不能由“多数层”投票。

FlashAttention Builder 会为图捕获预分配 scheduler metadata,并固定允许的 split 上限;FlashInfer Builder 会保留 workspace,并可能按捕获 batch size 创建 decode wrapper。两者都把动态内容写入稳定 buffer,而不是每轮替换对象地址。对这套机制的 CUDA 原理,可参考 CUDA Graph 完全拆解。

Cascade Attention 把共享前缀只算一次#

若一个 batch 的请求共享长度为 LpL_p 的前缀,可以把每个 query 对公共前缀和私有后缀的 attention 分开:

Op,LSE⁡p=Attn⁡(Q,Kp,Vp)O_p,\operatorname{LSE}_p=\operatorname{Attn}(Q,K_p,V_p)Os,LSE⁡s=Attn⁡(Q,Ks,Vs)O_s,\operatorname{LSE}_s=\operatorname{Attn}(Q,K_s,V_s)

再用两部分的 log-sum-exp 权重合并输出。这里 LSE 保存 softmax 分母的对数统计,不能简单把 OpO_p 与 OsO_s 相加。若两段的最大值和归一化因子不同,直接相加会得到错误概率。

Builder 的 use_cascade_attention() 决定当前批次是否值得这样做,common_prefix_len 再进入专用 metadata。收益取决于共享前缀长度、batch size 和额外合并成本;前缀很短时,拆成两次 attention 反而增加 launch 和归约开销。

在本文固定提交里,FlashAttention 路径保留了实际 cascade metadata 与执行分支;FlashInfer 虽包含 MultiLevelCascadeAttentionWrapper 相关代码,但其 use_cascade_attention() 明确返回 False 并标注暂时禁用。读源码时应以最终选择函数为准,不能因为文件里出现 wrapper 就断言功能已经在生产路径启用。

混合 KV 与虚拟 Kernel Block#

KV Cache Manager 的 block size 和 Kernel page size不必完全相同。假设 Manager 为统一多个 cache group 选择 64-token 物理管理块,而某后端 Kernel 只接受 16-token 页;Worker 可以把一个管理块映射成四个虚拟 Kernel block。select_common_block_size() 要求选出的 Kernel size 能被 manager block size整除,并同时满足该 group 内所有 backend。

这层转换不能放到 Engine Core:Core 关心分配和引用计数,不应知道 FlashAttention 或 FlashInfer 的页 ABI。也不能交给每层临时猜测:同一物理 page 的子块展开必须在 block table 和 KV tensor view 中一致。AttentionGroup 初始化阶段先协商共同 Kernel block size,Builder 和缓存 view 随后都使用这一结果。

Context Parallel 与投机解码改变的是 metadata 真值#

DCP 将一条长上下文的 KV 分给多个 rank。每个 rank 的本地 Key 长度可能不同,最后还要结合各 rank 的 attention 输出与 LSE;Backend 必须声明是否能返回正确 LSE、是否支持变长投机 query,以及怎样建立本地长度。PCP 又会把 query/prefill chunk 分片,进一步改变请求行与本地 token 的关系。

投机解码的 draft 数可能在设备上才得到最终接受长度。此时 CPU 的 seq_lens 只是上界,设备 query_start_loc 才是真值。某些 Kernel 能直接读取设备边界,某些 plan() 路径必须依赖 CPU 精确长度。Selector 和 Builder 需要共同阻止不支持的组合,否则错误不会表现为简单 shape mismatch,而会表现为某些草稿 token 读到越界或未来 KV。

如何选择后端,以及怎样定位不兼容与性能问题#

大多数部署应先使用自动选择。只有在做可复现实验、绕开已知 bug、验证特定 dtype/layout,或比较 Kernel 时才固定 backend。当前官方接口支持:

Terminal window
vllm serve MODEL_NAME --attention-backend FLASH_ATTN

也可以使用结构化配置:

Terminal window
vllm serve MODEL_NAME \
--attention-config.backend FLASHINFER

不同 vLLM 版本的参数和后端枚举会变化,运行前应以对应版本的 --help 与官方支持矩阵为准。不要把其他版本文档中的 backend 名称直接套到固定提交。

不要只问“FlashAttention 还是 FlashInfer 更快”#

可靠比较至少要固定:

  • GPU 型号与 compute capability;
  • vLLM、FlashAttention、FlashInfer 和 CUDA 版本;
  • 模型的 Q/KV 头数、head size 与 sliding-window 结构;
  • 权重 dtype、激活 dtype 与 KV cache dtype;
  • block/page size 与 KV cache layout;
  • 输入长度、输出长度、并发数和 prefill/decode 比例;
  • eager、piecewise graph 或 full graph;
  • prefix cache、投机解码、DCP/PCP 等开关。

后端性能依赖 workload。长 prefill 与大 decode batch 的算术强度、并行度和页遍历方式不同;某个端到端吞吐数字还混合了调度、H2D、LM head 和采样成本。正确方法是先用端到端指标确认业务目标,再用 profiler 分开 metadata build、KV update、prefill attention 和 decode attention。分析工具链可参考 GPU 推理性能分析完全指南。

启动时报“不支持”时怎样定位#

按下面顺序核对,通常比立即换后端更快找到根因:

  1. 记录 Selector 收到的 AttentionSelectorConfig,确认 head size、dtype、KV dtype 和显式 block size;
  2. 查看候选 backend 给出的 invalid reasons,区分硬件、单项能力与组合能力失败;
  3. 检查模型是否混有 full/sliding/MLA/Mamba 等不同 spec;
  4. 检查 KV cache layout 与所有后端支持布局的交集;
  5. 检查共同 Kernel block size 是否存在;
  6. 最后再判断是取消某项高级特性,还是换 backend。

例如“FlashInfer 支持 FP8 KV”和“FlashInfer 支持当前 FP8 KV 配置”不是同一句话。后者还取决于 head size、设备代际、page size、是否使用 sink、DCP 和具体内部 decode 路径。只查 dtype 支持表无法证明整个组合可行。

运行正确但性能差时怎样分层#

现象先检查可能原因
forward 前有明显 CPU 空洞Builder build/plan、H2D、小 tensor 分配每轮同步取长度、workspace 重建、pinned copy 未重叠
prefill 快、decode 慢decode Kernel、页表遍历、batch size路径未进入 XQA/TRT-LLM、并行度不足、页布局不合适
eager 快、full graph 反而慢捕获形状、padding、固定 launch grid捕获 batch 过大、有效工作少、重放了过多空工作
混合 batch 比分开运行差重排、两条 Kernel launch、资源互补是否成立prefill/decode 分流成本超过混批收益
metadata 时间随层数增长AttentionGroup 分组与共享情况spec 或 Q 头数差异导致无法共享,或错误地逐层 build
只在长上下文抖动split 数、workspace、page 数与 DCP动态调度变化、额外归约或 CPU plan 成本

Profiler 中看到 plan() 很长,不等于 FlashInfer Kernel 本身很慢;看到 attention Kernel 很短,也不等于后端端到端更好。Backend 的开销账本至少包括一次 step 的 Builder 成本和每层的 Impl 成本:

Tbackend=Tbuild+Lg⋅Trun+TupdateT_{backend}=T_{build}+L_g\cdot T_{run}+T_{update}

LgL_g 是该 AttentionGroup 中实际执行的层数,TbuildT_{build} 是共享元数据构造,TrunT_{run} 是每层 attention,TupdateT_{update} 是独立 KV 写入等公共操作。Builder 优化主要降低第一项,Kernel 优化主要降低第二项;模型层数越多,第二项占比通常越高,但小 batch decode 中第一项仍可能成为可见的 CPU 间隙。

正确性不变量与源码阅读顺序#

Attention Backend 的错误常常不会立刻抛出异常。错误页号、错误 request row 或错误有效长度都可能保持 tensor 形状合法,却让模型静默读到另一个请求的 KV。沿不变量检查比从某个具体 Kernel 逆向猜测更可靠。

九条必须成立的不变量#

  1. 每个 layer name 必须映射到与该层 backend 和 KVCacheSpec 一致的 metadata。
  2. 同一 AttentionGroup 内共享 metadata 的层必须拥有相同后端、cache spec 和每 rank 的 Q 头数。
  3. query_start_loc 单调不减,首项为 0,最后一个有效值等于真实 query token 数。
  4. 对每个请求,seq_len - query_len 必须等于本轮开始前可读取的 context 长度。
  5. block_table 中所有有效物理 ID 必须仍归该请求所有,且与当前 KV tensor layout 使用同一 page 几何。
  6. 每个非 padding slot_mapping 必须落在该请求拥有的可写 page 中。
  7. 后端重排 batch 时,Q/K/V、output、请求行、采样状态和页表必须使用同一个置换。
  8. CUDA Graph 重放引用的 workspace、metadata tensor 和 wrapper 内部 buffer 地址必须保持稳定。
  9. 若 CPU 长度只是乐观上界,依赖精确长度的后端必须使用设备真值或拒绝该配置。

一个最小调试批次#

先关闭投机解码、多模态、KV Connector、DCP/PCP 和量化,只保留一个单 token decode 与一个多 token prefill。每轮记录:

backend=FLASHINFER
req_ids=[A,B]
query_start=[0,1,5]
seq_lens=[128,4]
block_rows={A:[...], B:[...]}
slot_mapping=[...]
split={decode_reqs:1, prefill_reqs:1}
page_indptr=[0,...]
last_page_len=[...]

手算一个跨 page 边界的位置,确认 block table 到 slot 的翻译;再检查 Builder 输出的 decode/prefill 切片是否覆盖全部 token 且不重叠。基础路径正确后,每次只打开一项高级能力。若一次同时启用 FP8 KV、prefix cache、spec decode 和 full graph,布局、长度和图重放错误会呈现非常相似的随机输出异常。

推荐源码阅读顺序#

  1. Attention.__init__():看局部模型参数怎样进入 Selector,Impl 怎样创建;
  2. AttentionBackend 与 AttentionMetadataBuilder:只读抽象字段和能力声明;
  3. GPUModelRunner.initialize_attn_backend():看 KV group 内怎样按三元键拆 AttentionGroup;
  4. _build_attention_metadata():看公共模板怎样替换各 group 的页表并映射到 layer name;
  5. FlashAttentionMetadataBuilder.build():先追普通 non-cascade、non-DCP 路径;
  6. FlashAttentionImpl.forward():看 metadata 怎样变成 varlen/paged 调用;
  7. FlashInferMetadataBuilder.build():追 batch 重排、CSR 页表、prefill/decode plan;
  8. FlashInferImpl.forward():核对 Q/output 切片与 wrapper run;
  9. 最后再打开 CUDA Graph、cascade、DCP、量化和多模态分支。

每读到一个判断,先标注它属于哪一层:Selector 的“能不能用”、Builder 的“这批怎样执行”、Impl 的“这一层怎样调用”。把三类判断混在一起,是阅读这部分源码最常见的失焦方式。

系列进度与下一站#

至此,vLLM V1 的主数据流已经从外部请求走到具体 attention 实现:

篇次已完成主题关键源码坐标
一整体架构与模块全景Entrypoint、Engine、Worker、Kernel
二请求穿过 V1 引擎add_request()、EngineCore.step()、输出提交
三Scheduler 的批次决策token budget、waiting/running、抢占
四KV Cache Managerprefix lookup、block pool、分配与回收
五GPUModelRunner 与 InputBatch扁平 token、block table、slot mapping、公共 metadata
六Attention Backendselector、AttentionGroup、builder、Impl 与后端专用 metadata

本篇最应保留的源码关系有六条:

  1. Selector 输入的是完整能力向量,不是单个 backend 名称;手工指定也必须通过兼容性验证。
  2. KV cache group 管存储生命周期,AttentionGroup 管 metadata 共享边界,两者不能混为一层。
  3. Backend 声明能力并创建类,Builder 按 batch 构造计划,Impl 按层执行 Kernel。
  4. CommonAttentionMetadata 保存后端无关真值;FlashAttention 可以较直接消费,FlashInfer 原生路径会降成 CSR 页表并执行 plan/run。
  5. group 级 metadata 共享把批次准备成本从逐层重复中移出,层内仍保留各自 Q/K/V、scale 与 KV tensor。
  6. CUDA Graph、混合 KV、DCP 与投机解码不是旁支补丁,它们决定长度、布局和地址是否仍满足 Backend 契约。

下一篇继续沿数据面向下进入 PagedAttention Kernel。重点将是 block table 如何变成 GPU 地址、一个 query head 怎样由线程组协作读取非连续 KV page、分块 softmax 如何合并局部最大值与归一化因子,以及 GQA、向量化加载和不同 block size 怎样影响寄存器、共享内存与带宽。接口层已经解释过的 Selector 与 Builder 不再重复,只从 Kernel 实际拿到的参数开始。

参考资料#

  1. vLLM Attention Backend 抽象源码(固定提交)
  2. vLLM Attention Backend Selector 源码(固定提交)
  3. vLLM GPUModelRunner:AttentionGroup 初始化与 metadata 构造(固定提交)
  4. vLLM Attention 模型层与统一自定义算子(固定提交)
  5. vLLM FlashAttention Backend 源码(固定提交)
  6. vLLM FlashInfer Backend 源码(固定提交)
  7. vLLM Attention Backend Feature Support
  8. vLLM Triton Attention Backend Deep Dive
  9. FlashInfer KV-Layout 教程
  10. FlashInfer:Efficient and Customizable Attention Engine for LLM Inference Serving
  11. FlashAttention:Fast and Memory-Efficient Exact Attention with IO-Awareness
  12. FlashAttention-3:Fast and Accurate Attention with Asynchrony and Low-precision

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

vLLM 源码完全拆解(六):Attention Backend 如何统一 FlashAttention 与 FlashInfer
https://pinghaoyang.com.cn/aigc/posts/vllm-attention-backend/
作者
平昊阳
发布于
2026-10-07
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
平昊阳
乘长风,破巨浪, 展鸿图于未央!
--
总访问量
--
访客数
公告
欢迎来到我的个人博客!欢迎关注交流吖!
更多相关公告,见
「社交-留言」。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
站点统计
文章
212
分类
26
标签
291
总字数
2,407,536
运行时长
0 天
最后活动
0 天前

文章目录