2. 序列维度:滑动窗口与稀疏检索
2.1 全注意力在 1M 上下文下的两笔账
bllm 第 3 章和第 4 章里的注意力是全注意力:位置 \(t\) 对 \(1..t\) 全部位置算分数。数学基础第 7.6 节算过每层约 \(4HT^2d\) FLOP。先把"如果 40 层都做全注意力"的账算出来,\(T=10^6\)、\(H=64,d=512\):
\[ 4\times 64\times 512\times (10^6)^2 =1.31\times 10^{17}, \]一层,单位是 FLOP。40 层就是 \(5.2\times10^{18}\);按单卡 400 TFLOP/s 的有效算力,光这一项就是 3.6 小时。这是 prefill 的账。
decode 的账不同:每生成一个 token,要读全部历史 KV。按 1.4 节的 1024 B/token/层、40 层:
\[ 1024\times 40\times 10^6=40.96\ \text{GB}, \]每生成一个 token 都要把这个数从显存搬到计算单元。按 3 TB/s 带宽,0.0137 秒/token,也就是 73 token/s 的上限——模型算力再大也没用。这是解码的账:它不随参数变小,只随上下文长度线性增长。
长上下文的第一瓶颈是"每步搬多少字节",不是"算多少 FLOP"。但要把两个量分开:890 B/token 是全局缓存的每 token 增量,乘 1M 得到约 0.89 GB 的常驻容量,比上面那份 40.96 GB 小 46 倍。每步真正搬多少,还要按层、按分支另算(2.5 节)。容量小 46 倍不等于带宽省 46 倍。
2.2 滑动窗口:只用最近 128 个位置
SWA(滑动窗口注意力,sliding window attention)把位置 \(t\) 的可见集合从 \(\{1,\dots,t\}\) 换成
\[ \mathcal W_t=\{\max(0,t-W+1),\dots,t\}, \] \[ W=128 . \]每个位置只和之前 127 个位置算分数,于是每层的 KV 只需要保留最近 128 条:是一个常数,不随 \(T\) 增长。读的字节数从 \(O(T)\) 变成 \(O(W)\)。
代价是感受野被截断。算一下它到底能看多远。设第 \(l\) 层输出的位置 \(t\) 依赖第 \(l-1\) 层的一个区间 \(I_{l-1}(t)\),而第 \(l-1\) 层每个位置又依赖它自己前 \(W-1\) 个位置,于是区间每次向前扩 \(W-1\):
\[ \begin{aligned} |I_l(t)|&=|I_{l-1}(t)|+(W-1),\\ |I_0(t)|&=1 \end{aligned} \] \[ \Longrightarrow\ R_D=1+D\,(W-1). \]代入 \(D=20,W=128\):\(R_{20}=1+20\times127=2541\)。报告的估算式是 \(R\approx DW=2560\),差别 19 个位置,可以忽略——但推导过程要记住,因为第 3 章的 bounded replay 用的就是这条式子。
两层的时候是 \(1+2\times127=255\)。这个数字在第 4 章会再出现一次:CED 在 Encoder 最前面放两层 SWA,就是为了让第三层开始做压缩时,输入已经带着 255 个位置的局部上下文。
2.3 稀疏检索:给全部历史打分,只读最好的 512 条
滑动窗口的问题是它只能看局部。稀疏注意力换一条路:仍然保留全部历史的条目,但用一个小得多的注意力给它们打分,只把分数最高的 \(K=512\) 条交给主注意力。这个打分的模块叫 Indexer。
Indexer 自己有一套 Q/K,维度和主注意力不同:32 个头、每头 128 维(主注意力是 64 头 × 512 维)。对查询位置 \(t\) 与历史位置 \(j\):
\[ s_{t,j}=\sum_{h=1}^{32}\gamma_{t,h}\, \operatorname{ReLU}\!\big(\langle q^{I}_{t,h},\,k^{I}_{j}\rangle\big), \] \[ S_t=\operatorname{TopK}_{j\le t}\,s_{t,j}, \qquad |S_t|\le\min(512,\,t) . \]三个细节值得停一下(先说一句:报告只给了"32 头 × 128 维、top-512"这几个数,没有给出打分函数的具体形式;下面的 ReLU 与逐头权重沿用上一代稀疏注意力的写法,当作示意,不要引用成报告设定):
- 不需要 softmax。主注意力要的是归一化的权重,Indexer 要的是排序。排序对单调变换不变,所以省掉 exp 和归一化,只要分量之间的相对大小对。
- ReLU 而不是 exp。负分数直接归零,等于"这一头认为不相关"。只要 \(\gamma_{t,h}\ge0\),多头求和后仍非负,且 ReLU 比 exp 便宜、不会溢出;即使 \(\gamma\) 可为负,Top-K 也只关心排序,结论不变。
- \(\gamma_{t,h}\) 随查询变。32 个头的重要程度由当前位置的隐状态决定,是一个逐 token 的加权,而不是固定权重。
还有一条边界条件容易被忽略:候选只能是 \(j\le t\)(因果可见),压缩条目还得是已经完成的块。所以短上下文时 \(|S_t|\) 达不到 512——2.4 节的 640 与 2.5 节的 144 KiB 都是上界。
算一下 Indexer 自己的成本:每个查询要对 \(N\) 个位置各做 32 次 128 维点积,
\[ N\times 32\times 128=10^6\times 4096=4.1\times10^{9} \]次乘加。比较对象要说清楚:如果主注意力也做全历史打分,一次是 \(64\times512=32{,}768\) 乘加,Indexer 是 4096,省 8 倍;把主注意力的 \(AV\) 读出也算进去,总量约省 16 倍——远不是三个数量级。而且实际主注意力只读 512 条稀疏条目,Indexer 却要扫全部 \(N\),所以稀疏检索并没有解决"每步扫全部历史"的问题,只是把每次扫描变便宜了。第 4.7 节的 HSI 才是针对这一点的。
2.4 为什么两个都要:"看得远"和"看得准"是两件事
把两者放在一起,位置 \(t\) 的可见集合是
\[ A_t=\underbrace{\{c_j: j\in S_t\}}_{\text{全局 512 条}} \;\cup\; \underbrace{\mathrm{KV}^{\text{swa}}_t}_{\text{局部 128 条}}, \] \[ |A_t|\le 640 . \]两者互补:SWA 是精确的(没有选错的风险),但只有 2541 个位置的视野;稀疏检索覆盖全部历史,但选错会在局部付出代价——第 5.6 节会证明,在沿用同一份选择集的复用段内,若目标条目既不在 \(S_t\) 也不在局部窗口里,仅靠改变主查询够不到它。后续 Reindex / Full 仍可能重新召回,所以这不是"永久损失",而是"这一段内补不回来"。局部窗口保精度,稀疏检索保召回。
这也解释了两个看起来奇怪的设计:为什么窗口只有 128(而不是 2048)——因为远处的信息由稀疏检索负责;为什么稀疏的 \(K=512\) 也不算大——因为最近 128 个位置已经由窗口精确覆盖,稀疏部分只需要补"远处的关键条目"。
2.5 每步读多少字节:把三本账分开
这里最容易出错的一步,是把"缓存有多大"当成"每步搬多少"。它们不是同一个量:前者是常驻容量,要乘上下文长度;后者是单次读出的流量,与上下文长度基本无关,但按层重复。至少有三笔账要分开:
- 存储账:全局 KV 每 token 890 B(4.4 节),乘上下文长度得到容量。
- 检索账:Indexer 每步要扫全部可见历史,成本 \(O(N)\)、每 token 约 4096 次乘加(2.3 节),读的是 Indexer K。
- 读出账:每个做全局稀疏注意力的层、每个查询位置,读一次选中的 512 条主 KV:\(512\times288\ \text{B}=147{,}456\ \text{B}\approx144\ \text{KiB}\)(288 B 见 4.4 节)。
再加一项最常被漏掉的:本地窗口 128 条。它是每层自己的 FP8 缓存,与上下文无关,但每层都要读。
按这个口径粗算:做全局分支的层有 38 层(40 层里去掉最前两层纯 SWA),单 token 的全局主 KV 读出约 \(38\times144\ \text{KiB}\approx5.3\ \text{MiB}\);再加上窗口与 Indexer 扫描,才是每步的真实流量。所以
- "每步读取的条目数与上下文无关"成立;
- "178 KiB 是每 token 的总读取量"不成立——178 KiB 只是"512 条主 KV + 512 条 Indexer K"两笔载荷之和,既没算窗口,也没算跨层重复;
- "只有 4 层读全局 KV"更不成立——4 是生产全局缓存的层数。
"\(O(1)\)"指的是稀疏读出的条目数,不是总成本。Indexer 打分仍是 \(O(N)\),prefill 仍要过一遍全部位置;跨层复用省的是"缓存有几份",不是"有几层不做注意力"。真正的常数化只发生在两处:读到的主 KV 条数(本节)和索引搜索域(第 4.7 节 HSI)。
2.6 和两本书的对应
这一章没有一个新的数学工具:窗口就是一个带状因果掩码,稀疏就是在 softmax 之前把一部分分数设成 \(-\infty\)——正是 bllm 3.5 节因果掩码的推广。区别只在掩码的形状:因果掩码允许的是下三角,窗口是下三角里的一条带,稀疏是在下三角里"按打分结果动态选出的格子"。
因果 窗口(W=3) 稀疏(示意) 1 0 0 0 1 0 0 0 1 0 0 0 1 1 0 0 1 1 0 0 1 1 0 0 1 1 1 0 1 1 1 0 0 1 1 0 1 1 1 1 0 1 1 1 0 0 1 1
(玩具例子:4 个位置、窗口 \(W=3\);实际是 \(W=128\)。稀疏块里 1 的位置由 Indexer 打分动态选出,但永远只能落在下三角——不能读未来。)
三者可以叠加:V4.1 Flash 除最前两层是纯 SWA 外,其余各层同时有窗口和稀疏两块,拼成上面 2.4 节的 \(A_t\)。
2.7 容易算错的地方
- 把窗口当感受野。\(W=128\) 是一层的可见宽度,不是"模型只能看 128 个 token";\(D\) 层叠起来是 \(1+D(W-1)\)。
- 用 \(DW\) 算精确值。\(DW=2560\) 是上界估算,精确式是 2541;差别小,但第 3 章用它做开销分析时要一致。
- 以为 Indexer 用了 softmax。它输出的是可比较的分数,不是概率。Top-K 只依赖排序,而 softmax 是保序的,加了也不改变选中结果——所以推理时直接省掉。要不要归一化,只与辅助损失怎么定义有关。
- 把 640 当成"每层都读 640 条"。640 是一次全局稀疏读出的候选条目上界(512 条全局 + 128 条窗口);40 层里只有 4 层负责生产全局缓存,但其余做全局分支的层仍各自读出。