源码精读:Attention 后端
谁该读这一篇? 想理解 SGLang 是如何把"逻辑请求 batch"转成"GPU 上一次 attention kernel"的工程师;做 perf tuning 的同学。 前置阅读:
02-core-concepts/05-token-attention.md、04-model-runner.md。 耗时: 40 分钟 学完能: 1. 列出 SGLang 支持的主要 attention backend; 2. 解释 FlashInfer 的 wrapper / page table / cascade 三个核心概念; 3. 在源码里找到 backend 注册和选择的位置; 4. 看到 attention 慢能判断是 backend 选错、kernel 不匹配还是数据不连续; 5. 知道什么时候该切换 backend。
1. Backend 概览
srt/layers/attention/ 下有 20+ 个 backend 文件,但常用的就几个:
| Backend | 文件 | 默认 | 强在 |
|---|---|---|---|
| FlashInfer | flashinfer_backend.py |
是(H100 类) | Cascade / page=1 / 多种 attention 变体 |
| FlashAttention | flashattention_backend.py |
部分场景 | 经典实现,稳定 |
| FlashInfer MLA | flashinfer_mla_backend.py |
DeepSeek 系 | MLA 模型专用 |
| FlashMLA | flashmla_backend.py |
DeepSeek alt | MLA 模型另一实现 |
| Triton | triton_*.py 散见 |
备选 | 兼容老 GPU |
| Torch SDPA | (fallback) | 不推荐生产 | 调试用 |
| AITER | aiter_backend.py |
AMD 路径 | AMD GPU |
| Cutlass MLA | cutlass_mla_backend.py |
实验 | Cutlass 实现 |
| Hybrid | hybrid_attn_backend.py |
Mamba 类 | 混合架构(attention + ssm) |
| NSA / DSA | nsa_backend.py / dsa_backend.py |
DeepSeek-V3.x | Native / Differentiable Sparse Attention |
切换:
python -m sglang.launch_server --attention-backend flashinfer
# 或 triton, flashinfer-mla, flashmla, torch, ...
2. 抽象基类
每个 backend 都实现:
class AttentionBackend(ABC):
def init_forward_metadata(self, forward_batch):
"""在 forward 前预算 page table、index、wrapper 等。"""
def forward(self, q, k, v, layer, forward_batch):
"""单层 attention 计算。"""
def init_cuda_graph_state(self, max_bs):
"""CUDA Graph 录制时分配持久化张量。"""
def init_forward_metadata_capture_cuda_graph(self, ...):
"""CUDA Graph 录制时调用。"""
def init_forward_metadata_replay_cuda_graph(self, ...):
"""CUDA Graph 回放时调用。"""
注册表:attention_registry.py。
ModelRunner 启动时按 --attention-backend 字符串查注册表实例化。
3. FlashInfer Backend(114+)
源码:flashinfer_backend.py:114(class FlashInferAttnBackend)。
3.1 Wrapper 概念
FlashInfer 的设计是把"一次 attention 调用"包装成一个 wrapper 对象:
self.decode_wrapper = BatchDecodeWithPagedKVCacheWrapper(workspace_buffer, ...)
self.prefill_wrapper = BatchPrefillWithPagedKVCacheWrapper(workspace_buffer, ...)
每个 wrapper 内部持有:
workspace_buffer:一块预分配显存(cuBLAS workspace)。page_table缓存。- 当前 batch 的元数据。
调用 wrapper.plan(...) 配置好元数据,再 wrapper.run(q, k, v) 跑 kernel。
3.2 Page Table
FlashInfer 期望传入 "page table":
page_indices: [page_id_0, page_id_1, ..., page_id_K-1]
page_indptr: [start_0, start_1, ..., start_N] # 每请求的 page 起止
last_page_len: [len_0, len_1, ..., len_N-1] # 每请求最后一 page 实际占多少 token
这个布局源自 vLLM 的 PagedAttention,FlashInfer 把它做成 first-class API。
SGLang 的 RadixCache 在 init_forward_metadata 时把"每请求当前命中的 token slot 列表"翻译成上面三个张量。
3.3 Cascade Inference
最酷的特性:当多个请求共享一段 prefix 时,FlashInfer 可以一次性算"shared prefix part",再算每请求"unique tail part",最后合并 attention logsumexp。
数学等价但运算量大幅减少:
其中 $K_s$ / $V_s$ 是 shared prefix,$K_t$ / $V_t$ 是 unique tail。 共享部分只被读一次(K/V 命中 L2 cache),不像传统做法把整段 K/V 复制 N 份。
SGLang 用 MultiLevelCascadeAttentionWrapper 直接吃 RadixTree 的多层结构。
3.4 决定权所在
# WrapperDispatch:
class WrapperDispatch(Enum):
SLIDING_WINDOW = "sliding_window"
CROSS_ATTENTION = "cross_attention"
SHARED_PREFIX = "shared_prefix" # Cascade
ENCODER_DECODER = "encoder_decoder"
...
init_forward_metadata 根据当前 batch 形态选 wrapper(flashinfer_backend.py:953 的 FlashInferIndicesUpdaterDecode)。
4. FlashInfer MLA Backend
MLA(Multi-head Latent Attention)是 DeepSeek-V2/V3 的核心创新: 把 KV "压缩"到一个低维 latent 空间,attention kernel 直接吃压缩 KV。
源码:flashinfer_mla_backend.py。
和普通 FlashInfer 的差异主要在:
- KV 张量的形状(latent_dim 而不是 head_dim × num_kv_heads)。
- 用
BatchPrefillWithRaggedKVCacheWrapper而不是 Paged 版本(MLA 内部不分 page)。 - 与
MLATokenToKVPool配合。
SGLang 在 DeepSeek 系上对 vLLM 有性能优势,主要靠这条路径。
5. 怎么选 backend
启动时自动选(server_args.py 默认值)的逻辑大致:
if model_is_mla:
backend = "flashinfer_mla"
elif gpu_compute_capability >= 9.0: # Hopper +
backend = "flashinfer"
elif gpu_compute_capability >= 8.0: # Ampere
backend = "flashinfer"
else:
backend = "triton" # 老 GPU
可以强制:
--attention-backend flashinfer # 通用
--attention-backend flashinfer-mla # DeepSeek
--attention-backend flashmla # 另一 MLA 实现
--attention-backend triton # 老卡
6. 一次 forward 里 attention 的具体路径
sequenceDiagram
participant MR as ModelRunner.forward
participant AB as AttentionBackend
participant FI as FlashInfer wrapper
participant K as GPU kernel
MR->>AB: init_forward_metadata(forward_batch)
AB->>AB: 算 page_indices / page_indptr / last_page_len
AB->>FI: wrapper.plan(...)
Note over FI: 预算 workspace, kv layout
loop 每层 layer
MR->>MR: linear Q/K/V projection
MR->>AB: backend.forward(q, k, v, layer, forward_batch)
AB->>FI: wrapper.run(q, k, v)
FI->>K: launch attention kernel
K-->>FI: output
FI-->>AB: output
AB-->>MR: output
end
注意:
init_forward_metadata一次性算好整个 batch 所有层的 metadata(不是每层算)。- 每层
forward调用复用同一 wrapper,只是输入 q/k/v 不同。 - Decode 模式下 wrapper 是
decode_wrapper,prefill 是prefill_wrapper,混合 batch 时按 token 切分。
7. Workspace buffer
FlashInfer wrapper 需要一块"草稿纸"——workspace_buffer:
self.workspace_buffer = torch.empty(
workspace_size_bytes, # 通常几百 MB 到几 GB
dtype=torch.uint8,
device=device,
)
这块显存预分配,运行时复用;它是 KV pool 之外另一块固定显存开销。 启动日志 "Memory pool end. avail mem=XX GB" 里已经扣掉这块。
碎片化导致 workspace 不够时会 OOM,这是诊断 "起 server 时 OOM" 的常见原因。
8. Triton Backend
flashattention_backend.py 和 triton_*.py 系列。
特点:
- Pure Python + Triton,调试容易、易改。
- 性能比 FlashInfer 略差(5-15%)。
- 适合:开发调试、老 GPU、FlashInfer 不支持的特殊形状。
9. 自定义 attention backend
from sglang.srt.layers.attention.base_attn_backend import AttentionBackend
class MyBackend(AttentionBackend):
def init_forward_metadata(self, fb): ...
def forward(self, q, k, v, layer, fb): ...
def init_cuda_graph_state(self, max_bs): ...
...
然后在 attention_registry.py 注册:
register_attention_backend("my_backend", MyBackend)
启动用 --attention-backend my_backend。
用途:实验性 kernel、特定硬件(如 TPU/NPU)、新 attention 算子(DSA、NSA)。
10. 性能调优 cheatsheet
| 现象 | 调什么 |
|---|---|
| forward 时延高、profiler 显示 attention 占 70%+ | 确认用了 FlashInfer 而不是 Triton |
| 起 server OOM | workspace 太大;降 max_running_requests |
| Cascade 不生效(看 wrapper dispatch log) | 检查请求是否真的共享 prefix;page_size 是否影响 |
| DeepSeek 跑慢 | 切到 flashinfer-mla 或 flashmla |
| AMD GPU 跑不快 | 用 aiter_backend |
| 想跑新 attention 变体 | 看 NSA / DSA backend 是不是已经支持 |
11. 关键源码索引
| 内容 | 文件 |
|---|---|
| 抽象基类 | base_attn_backend.py |
| 注册表 | attention_registry.py |
| FlashInfer 主体 | flashinfer_backend.py |
| FlashInfer Updater(per-batch metadata) | flashinfer_backend.py:953 |
| Cascade wrapper dispatcher | flashinfer_backend.py:58(WrapperDispatch) |
| FlashInfer MLA | flashinfer_mla_backend.py |
| FlashMLA | flashmla_backend.py |
| FlashAttention | flashattention_backend.py |
| AMD AITER | aiter_backend.py |
12. 小结
- SGLang 支持多个 attention backend;默认 FlashInfer(DeepSeek 用 MLA 变体)。
- FlashInfer = Wrapper + Page Table + Cascade,吃 SGLang 的 token-level KV 无压力。
- Cascade Inference 是 SGLang 在多分支 / 长共享 prefix 上的性能秘密武器。
- Workspace buffer 是固定显存开销;不够会启动 OOM。
- 想自定义只需实现
AttentionBackend接口 + 注册。
13. 自检
- FlashInfer 和 FlashAttention 的关系?SGLang 为什么选前者?
答案
FlashAttention 是**算法**(tiled / online softmax / IO-aware),有自己的 kernel 实现(Dao Lab 维护)。 FlashInfer 是**LLM serving attention engine**,集成了 FlashAttention 系列算法,外加 Page Table API、Cascade Inference、JIT 模板生成、多种 attention 变体支持(MLA / sliding window / softcap 等)。 SGLang 选 FlashInfer 是因为:(a) token-level paged KV 必须有 page table 原生支持;(b) RadixAttention 的多分支共享 prefix 必须靠 Cascade Inference;(c) JIT 让新 attention 变体可以快速集成。 FlashAttention v3 也支持 paged,但 cascade 不如 FlashInfer 完善。- Page table 的三个张量分别是什么?
答案
`page_indices: [int32]` —— flat 数组,依次列出所有请求用到的 page id。 `page_indptr: [int32, batch_size+1]` —— CSR-style 行起止指针,`page_indices[page_indptr[r]:page_indptr[r+1]]` 是请求 r 的 page 列表。 `last_page_len: [int32, batch_size]` —— 每请求最后一 page 实际占用多少 token(不是满 page 时尾部 padding 信息)。 FlashInfer kernel 通过这三个张量 + 全局 KV pool 张量做间接访问。SGLang 在 `FlashInferIndicesUpdaterDecode.update_metadata`([`flashinfer_backend.py:953`](../sglang/python/sglang/srt/layers/attention/flashinfer_backend.py))从 RadixCache 翻译出来。- Cascade Inference 的数学等价怎么写?
答案
设 K, V 拆成共享 prefix $K_s, V_s$ 和 per-request tail $K_t, V_t$:- MLA 模型为什么不能直接用普通 FlashInfer wrapper?
答案
MLA(Multi-head Latent Attention,DeepSeek-V2/V3)把 K 和 V 压缩到共享 **latent dim**,物理存储不是 `[num_kv_heads, head_dim]` 而是 `[latent_dim]`;attention 时还要做 latent → head_dim 的 absorb projection。 普通 paged wrapper 假设 K/V 张量按 `[num_kv_heads, head_dim]` 布局,shape 和 access pattern 都不对。 FlashInfer MLA wrapper([`flashinfer_mla_backend.py`](../sglang/python/sglang/srt/layers/attention/flashinfer_mla_backend.py))专门处理 latent 压缩 / 解压、用 `BatchPrefillWithRaggedKVCacheWrapper`(MLA 内部不分 page);还要配套 `MLATokenToKVPool`。- workspace buffer 太大会怎么样?太小会怎么样?
答案
**太大**:占固定显存(几百 MB 到几 GB),挤压 KV pool 容量 → max_running_requests 降、cache 容量小 → 吞吐降。 **太小**:`wrapper.plan(...)` 报 "workspace insufficient",启动时直接失败;运行时大 batch + 长 seq + 多层 cascade 突然超限时 forward 报错。 合理估算:基础 128 MB;每多 64 并发 +50 MB;MLA / cascade 多层多加 200 MB;启动时观察 `cuda graph capture` 阶段的峰值显存。 实战:用 `--flashinfer-workspace-size` 调,遇到 OOM 先看是 KV pool OOM 还是 workspace plan 失败两类不同的 case。14. 下一步
04-optimizations/01-flashinfer.md— 深入 Cascade。04-model-runner.md— 上层调用方。02-core-concepts/05-token-attention.md— 设计动机。- 源码:
sglang/python/sglang/srt/layers/attention/。