Sparse attention:只看需要的历史
上下文很长,当前 token 一定要翻遍每一页吗?
→ 逐步推进 · ← 回看 · 拖动时间轴观察数据流
Full attention:每个 Q 都看全部可见历史
训练与 prefill 中,attention 的配对数随序列长度近似平方增长。
每个格子是一对 Q 与 K。因果模型只使用下三角:位置 i 能读取 1…i。
从第 1 行扫到第 8 行,读取次数是 1 + 2 + … + 8 = 36。未来位置始终不可见。
稀疏化是在这个可见区域里再选一部分位置。只要 mask 允许,选中的位置照常做点积、softmax 和加权求和。
稀疏 mask 在 softmax 之前生效
第 i 个 query 允许读取的集合记作 Si。先选集合,再在集合内部重新归一化。
aᵢⱼ = exp(logitᵢⱼ) / Σₖ∈Sᵢ exp(logitᵢₖ)
yᵢ = Σⱼ∈Sᵢ aᵢⱼ vⱼ
不能先做完整 softmax,再把没选中的权重置零而不归一化。那会让权重和小于 1,输出幅值也改变。
Sᵢ 必须非空;通常至少保留当前 token 或局部窗口。实现用 masked fill(−∞),再减去行最大值求 softmax。
同样的 V,换集合就会换输出
logits = [0, ln 2, ln 3],V = [2, 4, 8]。完整权重是 [1/6, 2/6, 3/6],输出 17/3 ≈ 5.667。
权重 = [1/4, 3/4]
y = 1/4 × 2 + 3/4 × 8 = 6.5
若把完整权重的中间项直接清零,得到 1/6 × 2 + 3/6 × 8 = 4.333;它并不是这个稀疏 mask 定义的 attention。
kernel 的正确性应与“相同 mask 的 dense 参考”比较;任务质量再与原始 full attention 比较。
滑动窗口:只看最近的 w 个 token
很多局部关系发生在附近,例如短语、语法和刚刚出现的实体。
把窗口宽度设为 3:位置 6 只读取 4、5、6。更早的 token 本层不直接读取。
当新 token 到来,窗口向前滑一格。读取量由全部历史变为固定的 3 份 KV。
堆叠多层后,信息可以逐层传播到更远处;单层直接可见范围仍然由窗口决定。
窗口宽度 3 为演示值;计算成本按每个 query 访问的 key 数量计算。
窗口的精确配对数,不只是 O(nw)
含自己的因果窗口宽 w。前 w−1 行还没填满,后面每行恰好读 w 个位置;假设 n ≥ w。
= nw − w(w−1)/2
n = 4096、w = 256:配对数 = 1,048,576 − 32,640 = 1,015,936。full causal 是 4096 × 4097 / 2 = 8,390,656。
这组算术约少 8.26 倍配对。实际延迟还包括投影、softmax、访存和 kernel 调度。
多层窗口扩大的是间接感受野
最终位置可受影响的范围宽度 ≤ 1 + L(w−1)
L = 4、w = 3:最后一层位置 10 可以间接受到位置 2 的影响,范围宽 9;每层仍只直接读取 3 个位置。
这条路径经过多次压缩与变换,不等同于最后一层直接拿到位置 2 的原始 KV。精确复制远处数字,可能仍需要远程读取。
推导假设每层都是相同因果局部窗口、没有全局 token 或跨层跳接扩大可见性。
局部之外,再留少量远程入口
固定稀疏模式可以组合局部窗口与全局位置。
橙色位置是最近窗口。紫色位置是允许额外访问的全局 token,例如某些结构性标记。
当前 Q 同时读取两类位置:近处保细节,远程入口传递更早的信息。
用于自回归生成时,全局位置也受因果约束:任何一行都不能看见未来。
Longformer 的原始设置包含双向任务;这里画的是保持 causal mask 的自回归示意。
动态稀疏:让内容决定读取哪里
“最近”不等于“相关”。重要答案也许在几千个 token 之前。
查询是“订单号是什么?” 当前 Q 先经过选择器,找出更值得读取的历史位置。
选择器输出位置索引,例如 2、5、7。attention kernel 只对这些位置的完整 K、V 做计算。
选择也有成本:若选择器先扫描所有完整 K,仍会搬大量数据。轻量索引或压缩表示能减少这一步的开销。
分数与选中位置为示意;最终集合还可以加入局部窗口和必要的保留位置。
选择器先要赚回自己的时间
只有 tsparse < tattn(n),attention 部分才更快
假想测量:full attention 2.0 ms;选择 0.35 ms、gather 0.25 ms、稀疏计算 0.50 ms,总计 1.10 ms,attention 加速 1.82×。
若其他网络层还花 6 ms,端到端从 8.0 降到 7.1 ms,只快 1.13×。小上下文中 full 只需 0.4 ms 时,选择开销可能已不划算。
选择器若扫描低维索引,仍然可能随 n 线性增长;“主 attention 只读 k 个”不是整个算法 O(k) 的保证。
top-k 也必须遵守因果约束
valid = key_position <= query_position index_score = where(valid, index_score, -inf) chosen = topk(index_score, k=min(k, valid_count)) chosen = sorted(unique(chosen + local_positions)) y = attention(q, gather(K, chosen), gather(V, chosen))
先 mask 再 top-k,避免未来高分把过去候选挤掉。全局位置与局部窗口取并集时去重,否则同一个 token 会被重复计权。
多 query 的稀疏索引可不同。若为了方便共用一组块,块内每个 query 仍需自己的精确 causal mask。
选择、重归一化、读取内容,顺序不能乱
已缩放 logits 为 [0, ln 2, ln 3],选择位置 {0, 2},对应 V 为 [2, 8]。
所有数值为手算教学示例。
GPU 更喜欢整块读取
零散地省算术,不一定省时间;内存访问和矩阵乘法都需要规律。
逐 token 选择可能产生许多零碎地址。GPU 要反复 gather,计算块也不容易填满。
把相邻 token 组织成 block,再按 block 选择。一次搬一整块,方便连续读取与 tiled kernel。
块越大,调度更规整,也会多读一些不太相关的 token。块大小需要与选择粒度一起权衡。
选了 3 个 token,为什么读了 12 个?
历史长度 16,块宽 4,目标位置 {1, 6, 14}(从 0 编号)。每个目标落在不同块里。
这里区分“选块后块内全部参与”与“块内仍做 token mask”;两者语义不同,读取成本可以相同。
把稀疏模式写成 kernel 能消费的索引
按 query block 存储可见 key blocks。row_ptr 指出每行在 col_idx 中的范围,避免保存巨大布尔矩阵。
row_ptr = [0, 1, 3, 5] # 3 个 query blocks
col_idx = [0, 0, 1, 0, 2] # 依次存每行选中的 key blocks
# query block 1: col_idx[1:3] == [0, 1]
for key_block in col_idx[row_ptr[r]:row_ptr[r+1]]:
load(K[key_block], V[key_block])
update_online_softmax(q_block, k_block, v_block)query / key block 指的是逻辑位置块。分页 KV 还需要块表把逻辑位置映射到物理显存地址。
块可见,不等于块内所有元素都可见
query block 覆盖位置 4…7,key block 也覆盖 4…7。它们处于因果矩阵对角线上。
| query | K4 | K5 | K6 | K7 |
|---|---|---|---|---|
| Q4 | ✓ | 屏蔽 | 屏蔽 | 屏蔽 |
| Q5 | ✓ | ✓ | 屏蔽 | 屏蔽 |
| Q6 | ✓ | ✓ | ✓ | 屏蔽 |
| Q7 | ✓ | ✓ | ✓ | ✓ |
跳过块内 mask 会泄露未来。只在整个 key block 的最大位置不超过最早 query 时,才可以整块免 mask。
少读多少?先看 k 与 n
序列长 n;每个 query 最多读取 k 个位置;head dimension 是 d。
Full causal attention 的 prefill 配对数约 n²/2;decode 的新 Q 读取 n 个位置。
若每个 Q 只读 k 个位置,attention 核心约为 prefill O(nkd)、decode O(kd)。
总时间还包括选择、索引、数据搬运与其他网络层。只有这些成本也划算,稀疏化才会带来端到端收益。
示意 n = 16、k = 4;复杂度忽略层数与头数,且不计选择器。
把复杂度换成 FLOP 与读取字节
单个 query head,n = 32768、k = 1024、d = 128、BF16。点积与 value 加权各约 2kd FLOP。
| 部分 | Full | Sparse 主 attention |
|---|---|---|
| 核心 FLOP | 4nd = 16,777,216 | 4kd = 524,288 |
| K/V 载荷 | 2nd × 2 = 16 MiB | 2kd × 2 = 0.5 MiB |
| 低维选择扫描 | 无 | 另计:例如 n × 16 × 2 = 1 MiB |
如果索引维度是 16 且每步全扫,光索引就要读 1 MiB。还未计索引计算、输出 top-k、非连续访问与多个 head 的共享。
这是一个明确的教学配置。选择器与主 attention 的精度、头共享方式可以不同。
漏选的损失比“命中几个”更有意义
假设完整 attention 在三个关键位置上的权重为 0.80、0.15、0.05。选中两个位置的 recall 都是 2/3,但漏掉谁差别很大。
选中 0.80 + 0.15
保留 attention mass 0.95,漏掉 0.05;重新归一化后主要信息可能仍保留。
选中 0.15 + 0.05
保留 mass 0.20,漏掉最重要位置。虽然索引 recall 相同,输出可能完全改变。
attention mass 仍只是诊断指标;最终要检查模型输出与任务质量。采样部分层和 query 做 full 参考,避免评估本身太贵。
训练阶段还要决定选择器怎么学
固定窗口不需要学习索引;动态 top-k 对“哪些位置被选中”通常没有普通的连续梯度。
被选中的主路径
Q、K、V 的 attention 运算可正常反传。未被选中的位置在该 query 上通常收不到这条 attention 梯度。
索引器的学习信号
可通过辅助目标、蒸馏完整 attention 分布或架构专用训练设计学习。具体目标决定了它学会找什么。
把预训练 full 模型临时套上稀疏 mask,是一种近似推理策略;它与从训练阶段就采用该稀疏结构不是同一件事。
少读历史,与压小历史,是两件事
可以让同一模型同时使用稀疏访问和压缩后的 KV 表示。
左边保留原始位置,但当前 Q 只选择一部分来读。没有被选中的位置仍可能留在 cache。
右边把四个位置变成一个摘要;即使读取这个摘要,也不是逐个读取原来的四份细节。
组合时,先决定“历史怎么存”,再决定“当前读哪些”。是否释放旧 KV,要看未来查询还会不会需要它。
从任务出发选择稀疏模式
局部模式
规则简单、访问连续、kernel 好实现。适合大量依赖近邻的层,也可以和 full attention 层交替使用。
内容选择
能找回远处的相关位置。需要训练或设计选择器,并控制漏选与选择开销。
块级模式
更容易把理论节省变成 GPU 吞吐。要检查块大小、gather 开销与稀疏 kernel 的利用率。
评估真正的难题
长距离检索、跨段推理、多处信息组合;同时记录延迟、吞吐和缓存占用。
稀疏后更慢,先看三段时间
假想 trace:原 attention 1.4 ms;新方案选择 0.7 ms、gather 0.6 ms、稀疏计算 0.4 ms,共 1.7 ms。
| 发现 | 可能原因 | 下一步实验 |
|---|---|---|
| 选择占 0.7 ms | 扫描过宽或 top-k 太贵 | 降低索引维度,测选块粒度 |
| gather 占 0.6 ms | 位置离散、重复搬运 | 排序索引、合并相邻块 |
| 计算仅 0.4 ms | 主矩阵确实变小 | 不要继续只优化这一小段 |
每次只改一个变量,并用相同 mask 对齐正确性。若质量下降,先查 causal、去重、位置编码和归一化,再调选择预算。
长度 8,窗口 3,省掉多少配对?
展开参考解答
Full = 8 × 9 / 2 = 36。窗口 = 1 + 2 + 6 × 3 = 21,也等于 8 × 3 − 3 × 2 / 2。减少 15/36 ≈ 41.67%。
4 层结构的理论上限为 1 + 4 × (3−1) = 9;本题序列仅 8 个 token,实际最多 min(8,9) = 8。多层间接传播不等于一层直接读取。
先写出假设和单位,再代入数字。
块读取能兑现多少理论节省?
展开参考解答
读取 24 × 16 = 384 个 token。目标利用率 = 32 / 384 = 8.33%。
相对 full 流量 = 384 / 1024 = 37.5%,节省 62.5%;不能按只读 32 个 token 声称节省 96.875%。
先写出假设和单位,再代入数字。
未来信息从哪里漏进来了?
展开参考解答
未来高分会占用 top-4 名额,丢掉后候选可能不足,甚至为空;选择结果也依赖未来内容。
先按各 query 的绝对位置屏蔽未来,再做 top-k。测试时修改未来 token,确认较早 query 的输出完全不变;同时覆盖候选数不足与全局 / 局部去重。
先写出假设和单位,再代入数字。
让 sparse gather 对上 dense masked 参考
用 NumPy 在 CPU 上跑一个可检查的参考实现。小尺寸便于把中间张量打印出来。
python3 -m pip install numpy python3 lab.py python3 lab.py --seed 7
masked dense vs gather: PASS future perturbation: PASS window pairs n=8,w=3: 21 3 targets / 12 loaded = 25.0%
把窗口改成局部 + 固定全局位置,先去重;再故意让对角块免 mask,观察未来扰动检查失败。
下载实验代码 lab.py这是正确性与成本的教学实验;耗时不代表 GPU kernel 性能。
参考与延伸阅读
- Longformer:局部窗口与全局 attention
- Big Bird:局部、全局与随机连接
- Native Sparse Attention:硬件友好的可训练稀疏 attention
- DeepSeek-V3.2-Exp:DeepSeek Sparse Attention 与索引器
继续看:KV cache · KV 压缩 · Linear attention