音乐
暂未播放
StreamingLLM 完全拆解:attention sink 与大模型的无限流式生成
引子:一场不能重来的长对话#
把大模型部署成聊天助手、实时翻译、代码补全这样的服务时,会话往往没有终点:用户会一直聊下去,系统提示、工具定义和对话历史持续累积。这种场景有一个共同点——模型必须基于一段不断变长的历史继续生成。这类应用在论文里被称为流式应用(Streaming Application),它同时撞上两个问题:
第一个问题是显存与算力随对话线性膨胀。 解码阶段每生成一个 token,模型都要把该 token 在各层的 Key 和 Value 向量写进 KV Cache,供后续所有 token 的注意力读取。以 32 层、32 个注意力头的 Llama-2-7B 为例,每个 token 的 KV 占用为:
MKV=2×Llayer×Hkv×dhead×b=2×32×32×128×2 B=512 KB其中 Llayer 是层数,Hkv 是 KV 头数(Llama-2-7B 是 MHA,KV 头数等于注意力头数),dhead=128 是头维度,b 是每元素字节数(FP16 为 2)。一次 10 万 token 的长对话就要约 50 GB 显存只放 KV——单张 A100/H100 的 80 GB 显存在装下权重后根本撑不住。注意力计算量也随历史长度线性上涨:生成第 T 个 token 时要对 T 个历史 token 逐一遍历,越往后每说一个字越贵。
第二个问题是模型根本不会用超出训练长度的文本。 绝大多数模型在预训练时只在固定窗口(例如 Llama-2 的 4096 token)内见过注意力,把它们放到更长的输入上,困惑度会迅速劣化。换句话说,即使显存无限,“把整段历史一直喂给全注意力”这条路也走不通。
两个问题合起来,指向一个朴素直觉:既然模型用不上太老的内容,干脆只保留最近的一段历史不就行了? 这就是滑窗注意力(Window Attention)——只缓存最近 L 个 token 的 KV,更老的一律丢弃。它来自 Longformer 等早期工作,内存和每 token 计算量都被限制在常数 L 内,看起来是流式推理的天然答案。但 2023 年的研究者们发现了一个反直觉的事实:一旦序列长度超过窗口、第一个 token 的 KV 被驱逐,模型就彻底崩了——Llama-2-13B 的语言建模困惑度从正常的 5.4 直接暴涨到 5158.07,几乎是在随机输出。
这背后是一整个此前无人解释的结构性现象。2023 年 9 月 29 日,MIT(Guangxuan Xiao、Song Han)与 Meta AI(Yuandong Tian、Mike Lewis)等研究者(Beidi Chen 亦参与)将论文 《Efficient Streaming Language Models with Attention Sinks》 提交到 arXiv,并于次年发表于 ICLR 2024。论文给出了一个被后续工作反复引用的核心概念——注意力汇聚点(Attention Sink),以及一个不训练任何参数就能让模型在无限长序列上稳定工作的方案:StreamingLLM。它只做一件事:在滑窗之外,永远保留最开头几个 token 的 KV。
这篇文章把这套机制完整拆开:先看滑窗注意力为什么崩、attention sink 从何而来(这部分是理解一切的关键),再拆 StreamingLLM 的两段式 KV 缓存与位置编码处理,然后是”预训练时种一个专职 sink token”的实验,最后用论文数据讨论它的效果、边界与后续影响。
一张总览图:这个问题的解空间长什么样#
论文 Figure 1 把流式场景下的候选方案全部画在一起。图中场景是:模型在长度 L(如 4096)的窗口内预训练,现在要预测第 T 个 token(T≫L):

- 图 (a) 稠密注意力(Dense Attention):缓存全部历史 KV。每生成一个 token 的注意力成本是 O(T),总成本 O(T2),缓存随 T 线性增长;且一旦文本长度超过预训练窗口,性能开始下降。两条失败原因它全占了。
- 图 (b) 滑窗注意力(Window Attention):只缓存最近 L 个 token 的 KV,内存与单步成本恒定,高效。但论文用实验证明:一旦文本长度超过缓存大小、开头 token 的 KV 被驱逐,性能断崖式下跌(后文细讲)。
- 图 (c) 滑窗 + 重计算(Sliding Window with Re-computation):不缓存 KV,每生成一个新 token 都把最近 L 个 token 重新前向一遍、现场重算它们的 KV。质量没问题(等价于每次只看一个新鲜滑窗),但单步成本是 O(L2)(窗内二次注意力),整体 O(T⋅L2),慢到无法用于实际流式场景。
- 图 (d) StreamingLLM:保留开头几个 token 作为 attention sink 稳定注意力计算,再叠加最近的 token。单步成本 O(L)、内存恒定,同时困惑度不劣化。
注意一个微妙的对比:图 (b) 与图 (d) 的差别只在于开头那几个 token 的 KV。图 (b) 驱逐它们,图 (d) 保留它们,于是前者崩溃、后者稳定——这就是整篇论文的杠杆点。理解”为什么区区几个 token 的 KV 有这么大威力”,需要先理解滑窗注意力到底是怎么崩的。
观察一:滑窗注意力崩在哪一刻#
论文首先做了一个”定位实验”(Figure 3):在 2 万 token 的文本上做语言建模,逐个位置测量困惑度。测试了 Llama-2、MPT、Falcon、Pythia 等多个模型家族,得到三条一致的规律:

- 稠密注意力在输入长度超过预训练窗口时失效。Llama-2 训练窗口是 4096,困惑度曲线在约 4K token 处开始陡升。这是位置编码外推失败的典型表现——模型没有在那么远的相对距离上训练过。
- 滑窗注意力在输入长度超过缓存大小时崩塌。论文在实验里把缓存大小设成训练窗口的一半(Llama-2 用 2048、其余用 1024,便于把失效点看得更清楚),于是窗口注意力在 2048/1024 之前与稠密注意力表现一致,一到窗口边界——即最老的 token 即将被驱逐的瞬间——困惑度直接冲上天。
- StreamingLLM 全程稳定,困惑度与”滑窗 + 重计算”这个质量上界几乎重合。
第二个发现值得细品:滑窗注意力的崩溃点精确出现在缓存容量耗尽、第一个 token 的 KV 被驱逐的那一刻,而不是”序列太长导致计算退化”。也就是说,崩塌不是渐进的性能衰减,而是一次”删掉一个 token”引发的瞬间雪崩。
论文用 Llama-2-13B 在 PG19 测试集第一本书(6.5 万 token)上量化了这个雪崩(论文 Table 1):
| 缓存配置 | 语言建模困惑度(PPL,越低越好) |
|---|---|
| 0 + 1024(纯滑窗) | 5158.07 |
| 4 + 1020(保留开头 4 个 token) | 5.40 |
| 4 个换行符 “\n” + 1020 | 5.60 |
表格中 “x + y” 记法是全篇的核心记号:保留开头 x 个 token 的 KV,加上最近 y 个 token 的 KV。0+1024 就是纯滑窗;把开头 4 个 token 加回来(4+1020),困惑度从 5158.07 回到 5.40——恢复了整整三个数量级,达到正常水平。注意最后一行:把原来的开头 4 个 token 替换成 4 个毫无语义的换行符 “\n”,效果几乎一样(5.60)。这组对照实验透露了关键信息:模型在意的不是开头 token 写了什么,而是它们出现在开头这个位置。
观察二:模型把注意力”倾泻”给了开头#
为什么删掉一个没有语义价值的 token 会引发崩溃?论文可视化了 Llama-2-7B 的注意力分布(Figure 2:在 256 个长度为 16 的句子上平均的注意力 logits):

图里能看到两条清晰规律:
- 最底下的两层(layer 0、1)呈现”局部注意力”模式:最近的一两个 token 获得最多关注,符合”底层在抄近邻词”的直觉。
- 从第 3 层往上,几乎所有层、几乎所有注意力头都把最大注意力给了第一个 token:在热力图上,位置 0(第一个 token)那一列在绝大多数子图里明显亮于其他列。
这还不是短序列的偶然现象。论文附录 F 做了定量测量:在 4096 token 的长序列上,统计第 4096 个 token 对第一个 token 的注意力分数——多数层里这个分数超过总注意力的一半(论文 Figure 12),且头与头之间的方差很小。也就是说”看开头”不是个别头的怪癖,而是模型的系统性行为。
论文把这类承受了巨额注意力、但自身不携带多少语义价值的 token 命名为 attention sink(后文简称 sink)。sink 的特征是:无论它是什么内容、离当前位置多远,模型都会稳定地把大量注意力质量堆在它上面。
为什么 sink 必然存在:softmax 的”质量守恒”#
要理解 sink 为什么必然出现,得回到注意力最基础的公式。第 i 个 query 对所有历史 token 的注意力权重是:
softmax(x)i=∑j=1Nexjexi,其中 xj=dkqi⋅kjxj 是 query 与第 j 个 key 的点积(经过缩放),softmax 把它们归一化成和为 1 的概率分布。关键约束在这里:所有注意力权重之和必须等于 1。这不是”约定俗成”,而是 softmax 分母归一化的数学强制。
于是出现了一个结构性的矛盾:解码时,大部分 query 其实在历史里”谁都不特别像”——语义已经由前几层充分编码,注意力只是形式上必须把质量分配出去。当 x1≫xj(对所有 j=1)时,公式里的 ex1 一项就吞掉了分母的绝大部分:
softmax(x)i=ex1+∑j=2Nexjexi,x1≫xj,j∈{2,…,N}论文对这种现象的机制解释很直白:softmax 不允许所有被注意的 token 都拿零权重,即便当前 token 的表示已经”自给自足”,模型也必须把多余的注意力质量倒到某些 token 上。正如量化社区观察到激活离群值总是集中在少数固定位置(LLM.int8() 与 SmoothQuant 两篇文章的核心现象),注意力离群值也倾向于集中在少数固定 token——softmax 的归一化约束决定了垃圾必须有个去处。这甚至催生了 Softmax-off-by-One 这样的”治本”提议(见后文),直接把分母的约束从”和为 1”改成”和为 1 再多 1”,给多余质量开一个不占 token 的出口。
为什么偏偏是开头的 token:因果掩码的可见性#
接下来是第二个问题:垃圾场为什么总设在第一个 token?
论文给出的解释依赖自回归(Autoregressive)模型的结构特征——因果掩码。第 j 个 token 只能被第 j 个及之后的 query 看到;因此:
- 第一个 token 对所有后续 token 都可见(永远是注意力候选),无论 query 在第 1 位还是第 100 万位,都可以把质量倒给它;
- 越靠后的 token 只能被越少的 query 看到,作为”全局垃圾场”的候选资格就越差。
预训练时,梯度会反复强化”把无处安放的注意力质量交给可见范围最广的 token”这一模式。第一个 token 是所有训练样本里都存在的、对所有位置都可用的唯一候选,于是它被训练成了最可靠的 sink。这个”可见性”论证还能解释两个后续观察:
- 滑窗为什么崩:窗内 token 被驱逐后,softmax 的分母被砍掉了最大的一项。分母突变 → 剩余 token 的权重全部被重新归一化放大 → 注意力分布与训练时见过的形态严重偏离 → 输出概率分布错乱。论文指出,删掉开头 token 的 KV 相当于删掉了 softmax 分母的很大一块(长序列里甚至超过一半),这直接解释了 5158 的困惑度雪崩。
- MPT、Falcon、Pythia 全部中招:论文在四种主流模型家族上都观察到了同样的现象。有意思的是 MPT 用的是 ALiBi、其他几个用 RoPE,说明 sink 现象与位置编码的具体形式无关,是自回归 Transformer 的共性。
附录里论文还把镜头拉到编码器模型:BERT-base 同样表现出 attention sink——不过它的 sink 是几乎每个句子都有的 [SEP] 分隔符(对 BERT 来说,[SEP] 是双向注意力下少数的”全局可见”位置之一)。同期达姆施塔特理工的 Darcet 等人在视觉 Transformer(ViT)里发现随机背景 patch 会充当”寄存器”(registers)吸收全局信息,论文认为这与 attention sink 是同一现象。凡是”注意力质量必须有去处”的架构,就必然长出 sink。
位置 vs 语义:换掉开头 token 也能用#
回到 Table 1 的第三个对照:把开头 4 个 token 换成 4 个 “\n”,困惑度几乎不受伤。这说明模型依赖的并非开头 token 的语义,而是”最开头”这个位置本身承载的 sink 角色。这个结论是后续一切设计的地基:既然 sink 是”位置角色”而非”内容角色”,那么保留任意几个开头的 token 就足以稳定注意力——不需要语义内容,不需要微调,甚至不需要它们是真实文本。
StreamingLLM:4 个 sink + 滚动 KV 缓存#
基于上述观察,StreamingLLM 的方案简单到令人惊讶:在滑窗的基础上,把最开头的几个 token(默认 4 个)的 KV 永远留在缓存里。不需要任何训练或微调,直接套用到任何使用相对位置编码(RoPE 或 ALiBi)的自回归模型上。
论文 Figure 4 画出了它的 KV 缓存结构:

缓存概念上分两段:
- Attention Sinks:最开头 x 个 token(论文默认 x=4)的 KV,永远驻留、永不驱逐。它们的作用不是提供信息,而是稳定注意力计算——让 softmax 的分母始终包含模型习惯的那块”压舱石”。
- Rolling KV Cache:最近 y 个 token 的 KV,随生成滚动更新:每生成一个新 token,把它追加进队尾,同时把最老的 token 逐出队头。
用 “x + y” 记法描述就是 4 + L(论文实验中 Llama-2 用 4+2044 左右、Falcon/MPT/Pythia 用 4+1020 左右——缓存总大小取训练窗口的一半,纯粹为了在图上把三种方案的失效点分开)。于是整个系统在任何时刻的 KV 总量恒定为 x+y,与已经生成了多少 token 完全无关:每生成一个新 token,注意力成本恒为 O(y)、内存恒为 O(x+y),这就是”无限流式”的含义——生成第 1 万个 token 与第 4 百万个 token 的开销完全相同。
位置编码:最容易被忽略、也最致命的一个细节#
缓存里存了 token,还得给它们分配位置编码。StreamingLLM 有一个贯穿全文的关键设计决定:位置的编号永远基于”token 在缓存里的序号”,而不是它在原文里的序号。
论文给的例子:假设当前缓存是 [0, 1, 2, 3, 6, 7, 8](token 4、5 已被驱逐),正在解码第 9 个 token。StreamingLLM 给这 8 个 token 分配的位置是 [0, 1, 2, 3, 4, 5, 6, 7]——它们就像一篇新写的、连续的文章;而不是原文位置 [0, 1, 2, 3, 6, 7, 8, 9]。
为什么要这样?两类主流相对位置编码都依赖”连续的距离”:
- RoPE 把位置信息编码成旋转角。如果沿用原文位置,窗口内会出现”跳号”:token 6 与 7 在原文里相邻,但在驱逐了 4、5 之后,它们与 sink 区之间的相对距离变得参差不齐,query 与某个 key 的相对距离可能被推到一个巨大的外推值——而 RoPE 的频率随距离增长会迅速振荡到模型从未训练过的形态。StreamingLLM 的处理是:缓存里保存旋转前的原始 Key,每个解码步前向时,按当步的缓存内 position_ids 对 Key 重新施加旋转变换。由于每一步 query 的位置都在整体前移,缓存里任意相邻 slot 的相对距离始终为 1,query 与各 key 的相对距离每步均匀递增 1——模型见到的所有相对距离始终落在训练范围内。若反过来缓存已旋转好的 Key,也只需每步把所有 Key 的旋转角整体推进一档(RoPE 的相对位置只取决于角度差),两种记账方式等价;论文选择前者,因为它不需要在缓存层增量维护旋转状态,代价是注意力 kernel 要支持”按给定 position_ids 旋转”,这恰好与 FlashAttention 这类融合 kernel 的接口兼容。
- ALiBi 更直接:它在注意力分数上减去一个随距离线性增长的惩罚项。沿用原文位置会让惩罚项”跳跃”(相邻 token 的距离一会儿是 1、一会儿是 2),而按缓存内编号分配时惩罚项是连续递增的线性序列。
这个决定为什么生死攸关:它把”长度外推”问题整个消解掉了。模型在训练时见过的最大相对距离是 L(窗口内),而缓存内任意两个 token 的相对距离永远不会超过 x+y≤L——所有注意力都在模型熟悉的相对距离范围内发生,位置编码从头到尾没有遇到任何”陌生”的输入。对比之下,朴素实现滑窗时如果不处理位置编号(很多早期的滑窗实现就是这样),即使保留了窗口也会因为位置跳号而崩,或者把崩溃点推迟到超出训练长度之外。
更新流程伪代码#
把上面的逻辑浓缩成缓存维护的伪代码(省略了逐层存储的细节):
1class StreamingLLMCache:2 def __init__(self, num_sinks: int = 4, window_size: int = 2048):3 self.num_sinks = num_sinks # 永远驻留的 sink 数量4 self.window_size = window_size # rolling 区容量5 self.rolling: deque = deque() # rolling 区,队头最老、队尾最新6
7 def append(self, token_kv):8 """新 token 的 KV(RoPE 模型存旋转前的原始 K)入队"""9 if len(self.rolling) == self.window_size:10 self.rolling.popleft() # 驱逐最老的 rolling token11 self.rolling.append(token_kv) # 新 token 追加到队尾12
13 def position_ids(self):14 """每次解码时使用的统一位置编号(论文的核心细节)"""15 return list(range(self.num_sinks)) + \16 list(range(self.num_sinks, self.num_sinks + len(self.rolling)))生成第 T 个 token 时,把 position_ids 交给注意力 kernel(RoPE 在其内部完成旋转),sink 区永远占用位置 0 到 x−1,rolling 区从 x 开始连续编号。驱逐发生时,队头 token 的 KV 被释放,下一次解码会按最新的缓存内容重新生成 position_ids——由于队尾每步追加一个、队头每步弹出一个,缓存内任意相邻 slot 的相对距离恒为 1,编号永远连续、从不跳号。这正是上一节强调的”缓存内编号”设计的工程落点。
为什么是 4 个 sink,而不是 1 个#
一个自然的问题:既然 sink 是”位置角色”,为什么默认保留 4 个开头 token 而不是 1 个?论文做了系统的消融(Table 2),在 40 万 token 的 PG19 拼接测试集上改变 sink 数量:
| 模型(缓存总大小) | 0 sink | 1 sink | 2 sink | 4 sink | 8 sink |
|---|---|---|---|---|---|
| Falcon-7B(0+2048) | 17.90 | 12.12 | 12.12 | 12.12 | 12.12 |
| MPT-7B(0+2048) | 460.29 | 14.99 | 15.00 | 14.99 | 14.98 |
| Pythia-12B(0+2048) | 21.62 | 11.95 | 12.09 | 12.09 | 12.02 |
| Llama-2-7B(0+4096) | 3359.95 | 11.88 | 10.51 | 9.59 | 9.54 |
三条清晰的规律:
- 0 个 sink(纯滑窗)全军覆没,困惑度在 17.9 到 3359.95 之间,模型基本在随机说话。
- Falcon、MPT、Pythia 放 1 个 sink 就基本恢复;Llama-2-7B 不行——1 个 sink 只有 11.88,要到 4 个才收敛到 9.59。
- 超过 4 个后收益递减(Llama-2 8 个 sink 只从 9.59 降到 9.54),所以默认取 4。
模型之间的差异从何而来?论文给出的解释是预训练数据的开头格式不同:Llama-2 虽然给每个段落加 <s> 起始符,但这个符号加在文本切块(chunking)之前——切块后落在位置 0 的往往是随机 token,不同训练样本的”开头”并不一致。模型学不到”把第一个 token 当专用 sink”,只好把 sink 职责分摊到开头好几个 token 上;而 Falcon 等模型的开头格式更一致,模型就可以把职责集中到一个 token。这解释了为什么 Llama-2 需要更多 sink——sink 的个数取决于预训练时开头 token 的统计一致性。
这个解释同时给出一个可验证的预测:如果预训练时人为保证每个样本开头都有同一个固定 token,模型就应该只需要 1 个 sink。论文在下一节验证了它。
进阶:预训练时”种”一个专职 sink token#
推理期保留 4 个真 token 虽然不花钱,但毕竟挤占了缓存容量(在 4+2044 配置里占 4/2048 ≈ 0.2%,放大窗口时几乎可忽略,但对 4+252 这种小窗口就是 1.6%)。更重要的是,如果模型天生就有一个”专职” sink,流式部署的稳定性会更好。论文提出一个预训练侧的处方:在每个训练样本的开头加一个可学习的专用占位 token(sink token),让模型把垃圾都倒给它。
论文顺带分析了另一条”治本”路线——来自 Softmax-off-by-One 的思想:既然问题出在”softmax 要求权重和为 1”,那把分母多留 1 个单位,让多余质量有个不占 token 的出口:
softmax1(x)i=1+∑j=1Nexjexi这个变体等价于在注意力计算里前置一个 Key 和 Value 全为零的 token,论文把它叫做 Zero Sink,与可学习的 sink token 一起做了对照实验。
实验设置:用 Pythia-160M 的训练配方从零训练三个 1.6 亿参数模型——标准注意力(Vanilla)、Zero Sink、开头加可学习 sink token(Sink Token),在去重后的 Pile 数据集上各训 14.3 万步(8 张 A6000)。推理时在流式设置下测困惑度(Table 3,PG19 第一个样本,缓存 0+1024 到 4+1020):
| 训练方式 | 0+1024(纯滑窗) | 1+1023 | 2+1022 | 4+1020 |
|---|---|---|---|---|
| Vanilla | 27.87 | 18.49 | 18.05 | 18.05 |
| Zero Sink | 29214 | 19.90 | 18.27 | 18.01 |
| Learnable Sink Token | 1235 | 18.01 | 18.01 | 18.02 |
三个结论:
- Vanilla 模型需要 4 个开头 token 才能稳住(18.05),与上一节的消融一致。
- Zero Sink 没能兑现承诺:纯滑窗(0+1024)下困惑度 29214,比 Vanilla 的 27.87 还差得多——“分母加 1”只是多提供了一个质量出口,并没有阻止模型在训练中继续习得”把大量注意力堆到开头的真 token 上”的习惯,一旦驱逐照样雪崩。
- Learnable Sink Token 完美生效:只保留 sink token 一个(1+1023)就达到 18.01,且加更多开头 token 不再有任何增益(18.01 → 18.01 → 18.02)。sink token 完全接管了”收集多余注意力”的职责,其他 token 不需要再兼职。
加 sink token 会不会伤模型?论文在两个维度做了检查:收敛曲线与 Vanilla 几乎重合;在 ARC、HellaSwag、LAMBADA、OpenbookQA、PIQA、Winogrande 等 7 个零样本基准上不仅不掉点,反而每一项都略高(例如 ARC-Challenge 从 18.6 到 19.6,HellaSwag 从 29.4 到 29.8)。注意力可视化(论文 Figure 7)显示,加了 sink token 的模型各层各头都显著地把注意力指向它,而给其他开头 token 的注意力明显减少——它真的在当垃圾桶。
附录 I 还追问了一个问题:两个 sink token 会不会更好?答案是不会:第二个 sink token 不带来任何增益(推理时模型会”平均地”依赖两个 sink,反而在只保留 1 个时需要 2 个才稳)。这与视觉 Transformer 里多个 “register” 有用的结论相反——对语言模型,一个专职 sink 就够了。
效果:四百万 token 不崩、流式问答不掉点#
4M token 语言建模#
论文把 PG19 测试集 100 本书拼接成超长流(跨书处有主题切换),对 Llama-2-[7,13,70]B、Falcon-[7,40]B、Pythia-[2.8,6.9,12]B、MPT-[7,30]B 共 9 个模型跑语言建模:

每条曲线的困惑度在 400 万 token 内保持平稳,只在书与书的交界处有可解释的小幅波动。作为对比,稠密注意力在 4K 处就失效了,而滑窗注意力在缓存边界处崩溃——两种失败模式都被绕开之后,剩下的只有”稳定”。
流式问答:模拟真实的多轮对话#
论文用两套评测检验真实场景。第一套直接把 ARC-Easy/Challenge 的全部问答对拼接成一条流,喂给 Llama-2-[7,13,70]B-Chat,在每道题的回答位置按精确匹配打分(Table 5,缓存 1024):
| 模型 / 设置 | ARC-Easy | ARC-Challenge |
|---|---|---|
| Llama-2-7B-Chat One-shot(逐个样本独立回答,上界参考) | 71.25 | 53.16 |
| Llama-2-7B-Chat Dense(全注意力) | OOM | OOM |
| Llama-2-7B-Chat Window(纯滑窗) | 3.58 | 1.39 |
| Llama-2-7B-Chat StreamingLLM | 71.34 | 55.03 |
稠密注意力直接 OOM;纯滑窗掉到 3.58/1.39(几乎零分——不是”变笨”,是模型崩了在随机输出);StreamingLLM 拿到 71.34/55.03,与逐个样本独立回答的上界相当,甚至略高(13B 和 70B 模型上同样如此,70B 的 ARC-Challenge 达到 80.20 vs one-shot 的 78.50)。
第二套是论文自建的 StreamEval 数据集,模拟”问题总是关于最近信息”的真实流式场景。它的结构(Figure 8)是:模型读 10 行新内容,被问一个问题,答案藏在 20 行之前——每个 query 的答案永远在窗口内,但要模型不被更早的无关内容干扰:

结果(Figure 9):StreamingLLM 在输入长达 12 万 token 时仍保持合理精度;稠密注意力在预训练长度处失效,滑窗注意力在缓存边界处失效:

论文还用两个上下文扩展模型(LongChat-7B-v1.5-32K、Llama-2-7B-32K-Instruct)验证了与上下文扩展技术正交:StreamingLLM 语境下的”上下文扩展”就是把 rolling 缓存开大,让模型能利用更长的近期上下文。32K 长上下文模型经过长文本微调、位置编码插值后,仍然在超出窗口处失效——任何方案都绕不开”不要超过训练过的距离”,而 StreamingLLM 用”永远不超”回应了它。
效率:对重计算基线的 22.2 倍加速#
“滑窗 + 重计算”是唯一质量达标的基线,但它每生成一个 token 都要在窗口内重算一次二次注意力。论文在单张 A6000 上用 HuggingFace Transformers 实现对比(Llama-2-7B/13B,Figure 10):
- StreamingLLM 的每 token 延迟随缓存大小线性增长(注意力扫一遍缓存,成本 O(L));
- 重计算基线的延迟二次方增长(每步重算 O(L2) 的窗内注意力);
- 在论文测试的最大缓存下,StreamingLLM 达到最高 22.2 倍的每 token 加速,内存占用与重计算基线持平(都只有 O(L) 而非稠密的 O(T))。
边界:它不是长上下文,是”永远新鲜”的记忆#
把 StreamingLLM 讲透之后,必须强调它的边界——它不扩展模型的上下文窗口,也不增强模型对长文的利用。它只保证一件事:模型能无限稳定地”看着缓存里最近的内容”继续生成。老内容被驱逐后就真的消失了,模型既看不到、也”记不得”。
论文用 LongBench(长文本基准)上的实验把这个边界量化得很清楚(Table 8)。对 Llama-2-7B-Chat,LongBench 的默认做法是把超长输入截断成”开头 1750 + 结尾 1750”;论文对比了三种配置:
| 配置(NarrativeQA 为例) | 分数(越高越好) |
|---|---|
| StreamingLLM 4+3496(只留 4 个 sink + 最近 3496) | 11.6 |
| 官方截断基线 1750+1750 | 18.7 |
| StreamingLLM 1750+1750(1750 个”开头 token”+ 最近 1750) | 18.2 |
4+3496 远低于截断基线,因为 NarrativeQA 的答案藏在文档开头,被 rolling 缓存驱逐了;而把 sink 数量从 4 扩到 1750(相当于”开头全保留”),分数立刻追平截断基线(18.2 vs 18.7)。结论:StreamingLLM 的可用性完全取决于目标信息是否在缓存内——信息在缓存里,效果与截断基线相当;信息不在,模型就无从作答。
所以它的正确适用场景是:多轮对话、实时字幕与翻译、代码持续补全、语音助手、agent 的运行日志流这类”模型只需要最近一段上下文”的应用。论文在摘要里也明确不把上下文窗口扩展算作贡献。长文档问答应该用摘要、RAG 或专门的上下文管理,而不是指望无限流式方案。
影响:一次现象级发现的下游#
attention sink 的发现之所以重要,是因为它解释了一个此前被归咎于”玄学”的系统性现象,并给出可操作的应对。它的影响沿着几条线扩散:
工程线——StreamingLLM 发布后迅速进入主流推理栈:NVIDIA TensorRT-LLM 在 Llama 示例里提供了 StreamingLLM 支持,HuggingFace Transformers 通过 PR #26681 集成,Intel Extension for Transformers、HPC-AI Tech 的 SwiftInfer 相继跟进,CMU/UW/OctoAI 合作把它跑上了 iPhone。对 KV Cache 管理来说,它贡献了一条至今被默认遵守的规则:无论用什么驱逐或量化策略,开头几个 token 必须特殊对待。
学术线——“模型把注意力质量倾倒在特定 token 上”的视角启发了大量后续研究:长上下文 KV 压缩工作普遍把”开头 token + 最近 token”当作压缩后的保留集(如 SnapKV 等),前缀缓存(如 SGLang 的 RadixAttention)与”前缀 + 滑窗”类方案在机制上与它同源;BERT 的 [SEP]、ViT 的 register 等跨架构的对应物则证明这是 softmax 归一化约束的普遍后果。它还给量化社区的现象(离群值集中在固定位置)提供了一个注意力侧的镜像:结构性冗余是深度网络习得的稳定习惯,不是噪声。
一条仍然开放的路——sink 的存在本质上是 softmax “和为 1” 约束的代价。Softmax-off-by-One 式的改造可以在数学上去掉 sink 的成因,但会改变注意力语义、需要重新预训练;如何在不动模型的前提下让 sink 尽量少占用缓存(比如把 4 个真 token 压成更少的专用 token),至今仍是一个活跃方向。
小结#
StreamingLLM 这篇论文的核心贡献可以压缩成三句话:
- 一个观察:自回归语言模型会把大量注意力质量倾泻到开头的几个 token 上——无论它们有没有语义价值。这是 softmax 归一化(质量必须守恒)与因果掩码(开头 token 对所有人可见)共同塑造的结构性结果,论文称之为 attention sink。
- 一个方法:在滑窗注意力之外永远保留开头 4 个 token 的 KV(“4 + 窗口”),并把位置编号统一按缓存内顺序计算,就能在零微调、零参数增加的前提下,让 Llama-2/MPT/Falcon/Pythia 在 400 万 token 的文本上稳定建模,流式问答精度与单发回答相当,比唯一可行的重计算基线快最高 22.2 倍。
- 一个处方:预训练时在每个样本开头放一个可学习的专职 sink token,模型就只需要这一个 token 来稳定流式推理,且不损失任何下游能力。
理解 attention sink,也就理解了一个贯穿推理系统的底层事实:KV Cache 里装的不仅是信息,还有模型的结构性习惯。后续读到 H2O 这类驱逐策略为何对开头 token 格外小心、各种稀疏注意力为何都保留滑窗与”常驻 token”、长上下文量化为何单独处理首部——都能在本文的机制里找到根源。
参考资料#
- Efficient Streaming Language Models with Attention Sinks(论文,arXiv:2309.17453)
- StreamingLLM 官方代码仓库(mit-han-lab/streaming-llm)
- MIT Han Lab StreamingLLM 项目主页
- NVIDIA TensorRT-LLM:Run LLaMa with StreamingLLM 官方示例(v0.8.0 版本文档)
- HuggingFace Transformers 集成 StreamingLLM 的 PR #26681
- Attention Is Off By One(Evan Miller,softmax-off-by-one 原博客)
- RoFormer:RoPE 旋转位置编码原始论文
- Train Short, Test Long:ALiBi 线性偏置注意力论文
- Longformer:滑窗注意力的经典来源
- PG19:长篇语言建模测试集
- LongBench:长文本基准(论文用其量化 StreamingLLM 的边界)
- SnapKV:受 attention sink 观察启发的 KV 压缩后续工作
- Vision Transformers Need Registers:ViT 中与 attention sink 同源的寄存器现象
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时
评论区
分享你的想法,与大家交流讨论
音乐
暂未播放



