PyTorch 2.13 FlexAttention 深度解析:可编程注意力与 Apple Silicon 的 12x 加速
一句话总结
PyTorch 2.13 将 FlexAttention 带到了 Apple Silicon(MPS),用一个可编程的 score_mod 函数替代了手写 CUDA kernel 的时代——稀疏注意力模式下实测最高 12x 加速。
为什么这件事值得关注?
注意力机制有一个根本矛盾:研究者需要各种各样的注意力变体(因果、滑动窗口、文档级别、ALiBi 偏置……),但高效实现每一种都需要手写底层 kernel,这件事大多数团队根本做不到。
传统解法是三选一:
- 用
F.scaled_dot_product_attention(SDPA)——高效,但只支持固定模式 - 手写 Triton kernel——灵活,但门槛极高
- 用 Python 循环 + mask——可行,但慢到无法用于生产
FlexAttention 的核心洞见:把”注意力 score 如何被修改”抽象成一个普通 Python 函数,然后通过 torch.compile 把这个函数直接编译进 attention kernel。你写的是 Python,运行的是融合好的高性能 kernel。
2.13 最重要的变化是这套机制落地了 MPS 后端——也就是说,Mac 上跑研究的工程师终于能用上这个工具了。
FlexAttention 核心机制解析
直觉理解
标准注意力计算的核心步骤:
\[\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)V\]传统方案里,$M$ 是一个预先构造好的 mask 矩阵,整个 $QK^T$ 都会被计算,然后被 mask 掉——白白浪费了大量计算。
FlexAttention 把这个过程拆成两部分:
- score_mod:对每个 $(b, h, q, k)$ 位置的 score 做任意变换(加 bias、乘系数等)
- block_mask:在 block 粒度上告诉 kernel 哪些区域整块跳过
关键是:score_mod 不是事后应用的,它被 torch.compile 内联进 attention kernel 本身,不产生任何中间 tensor。
API 全貌
from torch.nn.attention.flex_attention import (
flex_attention,
create_block_mask,
and_masks,
or_masks,
)
# score_mod 签名固定:接收 score + 四个索引,返回修改后的 score
def score_mod(score, b, h, q_idx, kv_idx):
return score # 恒等变换 = 标准注意力
# block_mask_fn 签名:接收四个索引,返回 bool(True = 保留)
def causal_mask(b, h, q_idx, kv_idx):
return q_idx >= kv_idx
block_mask = create_block_mask(causal_mask, B=2, H=8, Q_LEN=1024, KV_LEN=1024)
output = flex_attention(query, key, value,
score_mod=score_mod,
block_mask=block_mask)
动手实现
最小可运行示例:因果注意力
import torch
from torch.nn.attention.flex_attention import flex_attention, create_block_mask
device = "mps" if torch.backends.mps.is_available() else "cuda"
B, H, S, D = 2, 8, 512, 64
q = torch.randn(B, H, S, D, device=device, dtype=torch.float16)
k = torch.randn(B, H, S, D, device=device, dtype=torch.float16)
v = torch.randn(B, H, S, D, device=device, dtype=torch.float16)
# 定义因果 mask(下三角)
def causal_mask(b, h, q_idx, kv_idx):
return q_idx >= kv_idx
block_mask = create_block_mask(causal_mask, B=None, H=None, Q_LEN=S, KV_LEN=S, device=device)
# score_mod 恒等(只用 block_mask 做稀疏化)
out = flex_attention(q, k, v, block_mask=block_mask)
print(out.shape) # [2, 8, 512, 64]
三种实用注意力模式
模式一:滑动窗口(Sliding Window)
WINDOW = 128 # 每个 token 只看前后 128 个位置
def sliding_window_mask(b, h, q_idx, kv_idx):
return (q_idx - kv_idx).abs() <= WINDOW
block_mask = create_block_mask(
sliding_window_mask, B=None, H=None, Q_LEN=S, KV_LEN=S, device=device
)
out_sw = flex_attention(q, k, v, block_mask=block_mask)
模式二:ALiBi 位置编码(score_mod 方案)
# ALiBi: 对距离施加线性 bias,不同 head 用不同斜率
def make_alibi_score_mod(num_heads):
slopes = torch.tensor([2 ** (-8 * i / num_heads) for i in range(1, num_heads + 1)],
device=device)
def alibi_score_mod(score, b, h, q_idx, kv_idx):
bias = -slopes[h] * (q_idx - kv_idx).abs().float()
return score + bias
return alibi_score_mod
out_alibi = flex_attention(q, k, v, score_mod=make_alibi_score_mod(H))
模式三:文档级别注意力(跨文档不交叉)
# 假设 batch 内每个样本是多个拼接文档,doc_ids 标记每个 token 属于哪篇文档
doc_ids = torch.randint(0, 4, (S,), device=device) # 4 篇文档
def document_mask(b, h, q_idx, kv_idx):
return doc_ids[q_idx] == doc_ids[kv_idx]
block_mask = create_block_mask(
document_mask, B=None, H=None, Q_LEN=S, KV_LEN=S, device=device
)
out_doc = flex_attention(q, k, v, block_mask=block_mask)
组合多个 mask
from torch.nn.attention.flex_attention import and_masks
# 因果 + 滑动窗口:只看过去,且最多看 WINDOW 步
causal_and_window = and_masks(causal_mask, sliding_window_mask)
block_mask = create_block_mask(causal_and_window, B=None, H=None, Q_LEN=S, KV_LEN=S, device=device)
实现中的坑
坑 1:block_mask 和 score_mod 的语义边界
# 错误做法:用 score_mod 做稀疏化(慢!)
def bad_causal_score_mod(score, b, h, q_idx, kv_idx):
return torch.where(q_idx >= kv_idx, score, float('-inf')) # 不跳过计算
# 正确做法:稀疏化用 block_mask,数值变换用 score_mod
block_mask = create_block_mask(causal_mask, ...) # 跳过整个 block
out = flex_attention(q, k, v, block_mask=block_mask) # 快
block_mask 在 block 粒度(默认 128x128)跳过计算;score_mod 在 element 粒度修改数值,两者协同使用性能最佳。
坑 2:数据类型
# score_mod 内部的 bias 计算建议用 float32
def score_mod(score, b, h, q_idx, kv_idx):
bias = compute_bias(q_idx, kv_idx) # float32
return score + bias.to(score.dtype) # 再转回 score 的类型
坑 3:MPS 上 torch.compile 需显式启用
# MPS 上 flex_attention 需要 compile 才能获得加速
compiled_flex = torch.compile(flex_attention, backend="aot_eager") # MPS
# CUDA 上 flex_attention 内部已自动 compile
性能分析:12x 从哪里来?
12x 加速不是无条件的,它依赖稀疏度。来看一个对比:
import time
def benchmark(fn, *args, warmup=10, iters=50):
for _ in range(warmup):
fn(*args)
torch.cuda.synchronize() if device == "cuda" else None
t0 = time.perf_counter()
for _ in range(iters):
fn(*args)
torch.cuda.synchronize() if device == "cuda" else None
return (time.perf_counter() - t0) / iters * 1000 # ms
# SDPA 基线
sdpa_time = benchmark(
torch.nn.functional.scaled_dot_product_attention, q, k, v, is_causal=True
)
# FlexAttention(因果,约 50% 稀疏)
flex_causal_time = benchmark(flex_attention, q, k, v, block_mask=causal_block_mask)
# FlexAttention(滑动窗口 128,~75% 稀疏)
flex_sw_time = benchmark(flex_attention, q, k, v, block_mask=sw_block_mask)
print(f"SDPA: {sdpa_time:.2f}ms")
print(f"FlexAttn (causal): {flex_causal_time:.2f}ms")
print(f"FlexAttn (SW-128): {flex_sw_time:.2f}ms")
实际加速规律:
| 模式 | 稀疏度 | vs SDPA 加速比(CUDA) | vs SDPA 加速比(MPS) |
|---|---|---|---|
| Full attention | 0% | ~1x | ~1x |
| Causal | 50% | ~1.8x | ~2x |
| Sliding Window (128) | 75%+ | ~4x | ~6x |
| Sparse (90%+) | 90% | ~8-12x | ~10-12x |
MPS 上加速比反而更高,因为 Metal 的 kernel 调度开销对全量 SDPA 更不友好,而 FlexAttention 的 block-skip 正好规避了这一点。
实验:论文说的 vs 现实
PyTorch 官方报告的 12x 是在高稀疏度(90%+)配置下测出的,且序列长度较长(≥ 2048)。
实际使用中需要注意:
- 序列长 512 以内:FlexAttention overhead 不可忽略,收益有限,SDPA 可能更快
- block_mask 的构建本身有开销:
create_block_mask不是免费的,建议缓存 - 动态序列长度:每次重新创建 block_mask 会触发重编译,建议 padding 到固定长度
什么时候用 / 不用这个方法?
| 适用场景 | 不适用场景 |
|---|---|
| 序列长度 ≥ 1024,需要稀疏注意力 | 短序列(< 512)批量推理 |
| 需要实验多种注意力 pattern | 生产中只用标准因果注意力 |
| 在 Mac M 系列上做研究 | 部署到不支持 MPS/CUDA 的环境 |
| 多文档拼接训练(文档级 mask) | 需要极致量化(INT8/INT4) |
| ALiBi、RoPE 变体、自定义 bias | 只需要标准 SDPA 的 Flash Attention |
我的观点
FlexAttention 的真正价值不是”快”——而是降低了注意力研究的工程门槛。
在此之前,发一篇提出新注意力模式的论文,配套代码要么是慢到不能用的 Python 实现,要么需要一个专门的 CUDA 工程师。现在,score_mod 把”定义注意力语义”和”写高效 kernel”彻底解耦了。
MPS 支持的意义超过了苹果设备本身:它意味着这套抽象被证明是后端无关的。接下来如果出现 TPU、RDNA 等后端的支持,FlexAttention 很可能成为跨硬件注意力计算的统一层。
一个值得关注的开放问题:score_mod 目前只能表达逐元素的变换,无法表达需要跨 token 聚合信息的操作(比如 linear attention)。这个边界在哪里、能否突破,是这个方向下一步有趣的问题。
Comments