从一次超长文档问答说起
你打开一个 400K token 的年度财报 PDF,想让模型回答“去年第三季度现金流为什么下降”。模型上下文窗口只有 4K,直接截断会丢掉关键信息;用 RAG 检索又可能漏掉跨章节的因果链条。更麻烦的是,如果这是一场多轮对话,你希望模型记住前面所有提问和回答,而不是每轮都重新读一遍全文。
传统做法是把整篇文档塞进上下文窗口,但 KV Cache 会随 token 数线性增长,注意力计算量随序列长度平方增长。4K 窗口的模型处理 400K 输入,要么截断,要么用 StreamingLLM 这类滑动窗口方案——后者只保留最近 token 和注意力沉没,历史信息几乎被丢弃。Activation Beacon 提供了第三条路:把历史 token 的激活值压缩成紧凑形式,让模型在有限窗口内“感知”无限长的过去。
基线方案为何失效
截断与滑动窗口的代价
最简单的办法是只取最后 4K token。对于财报问答,如果问题涉及文档开头的数据,答案必然缺失。滑动窗口保留最近 token,但注意力沉没(attention sink)现象会让模型过度关注初始 token,导致历史信息被“冲淡”。StreamingLLM 正是利用这一点,固定保留前几个 token 作为注意力沉没,再滑动窗口,但它并不压缩历史,只是丢弃,因此无法回答需要早期信息的问题。
软提示压缩的瓶颈
另一类方法是用软提示(soft prompt)传递压缩信息,例如将长文本编码成少量可训练向量。但软提示的容量有限,难以封装长文本中复杂的依赖关系。论文《Long Context Compression with Activation Beacon》指出,直接压缩每一层的 Key 和 Value 激活值,比软提示能保留更多信息,因为激活值本身携带了模型内部的丰富表示。
Activation Beacon 的核心机制
激活值压缩:把历史“浓缩”进信标 token
Activation Beacon 引入一种特殊 token——信标 token(beacon token),所有信标共享同一个 embedding。处理长文本时,将输入分割成等长 chunk,每个 chunk 再划分成更小的单元(unit),在每个单元末尾插入一个信标 token。模型编码这些 chunk 时,信标 token 的激活值会“吸收”该单元的信息。编码完成后,原始 token 的激活值被丢弃,只保留信标 token 的激活值,作为后续 chunk 编码时的上下文。
例如,压缩比 α=L/k,其中 L 是单元长度,k 是信标数量。若 α=4,则每 4 个 token 对应 1 个信标,KV 缓存大小降为原来的 1/4。论文实验显示,在 128K 长上下文任务上,Activation Beacon 相比未压缩基线,推理速度提升 2 倍,KV 缓存内存减少 8 倍。
渐进式压缩流程
压缩不是一次性完成,而是逐步进行的。每个 chunk 编码时,模型会看到之前所有 chunk 累积的信标激活值,以及当前 chunk 的原始 token。信标 token 的注意力范围不同:越早的信标覆盖越长的历史,越晚的信标覆盖越近的上下文。这种渐进式设计让压缩更精细,也便于训练时梯度连续传播。
具体流程如下:
- 将长文本分割为 chunk:
[X1, X2, ..., Xn]。 - 对每个 chunk,插入信标 token,形成
X'i = [xi1,...,xiα, ⟨b⟩i1, ..., xiw-α+1,...,xiw, ⟨b⟩ik]。 - 编码当前 chunk 时,输入包含之前累积的信标激活值和当前 chunk 的 token。
- 注意力计算时,查询向量同时作用于历史信标的 Key/Value 和当前 chunk 的 Key/Value。
- 更新信标激活值:将当前 chunk 中信标 token 的 Key/Value 追加到历史信标激活值中。
基于压缩的自回归训练
训练目标是优化基于压缩上下文的生成质量。损失函数只计算除第一个 chunk 外的所有 token 的交叉熵,即模型需要学会利用信标激活值来预测后续 token。训练时随机采样压缩比,让模型适应多种压缩配置。论文使用 1B 纯文本和 30K 指令数据,最大训练长度 20K,在 8 张 A800 GPU 上训练不到 9 小时(10K 步)。
与 StreamingLLM 的对比
StreamingLLM 是另一种流式推理方案,它利用注意力沉没现象,固定保留初始 token 的 KV,再滑动窗口保留最近 token。它不压缩历史,只是丢弃,因此无法回答需要早期信息的问题。Activation Beacon 则通过压缩保留历史信息,两者在信息保留和内存占用上有本质区别。
| 方案 | 信息保留 | KV 缓存大小 | 推理速度 | 适用场景 |
|---|---|---|---|---|
| StreamingLLM | 仅最近 token + 注意力沉没 | 与窗口大小线性 | 快 | 对历史信息要求低的流式对话 |
| Activation Beacon | 压缩的历史激活值 | 与压缩后信标数线性 | 中等 | 需要长期记忆的超长文档问答 |
从表格可见,StreamingLLM 牺牲历史信息换取极低内存,而 Activation Beacon 用更多计算换取信息保留。在超长文档问答中,后者能回答“第三季度现金流为何下降”这类需要跨章节推理的问题。
工程实现:组件与数据流
模型结构改动
Activation Beacon 作为插件模块,不修改原始 LLM 参数。它新增两组投影矩阵:一组用于原始 token 的 Q/K/V(沿用 LLM 原有参数),另一组用于信标 token 的 Q/K/V(新增参数)。注意力计算时,信标 token 的查询会同时作用于历史信标和当前 chunk 的 Key/Value。
推理时的数据流
下图展示了推理时的数据流:
flowchart TD
A[输入长文本] --> B[分割为 chunk]
B --> C[每个 chunk 插入信标 token]
C --> D[编码当前 chunk]
D --> E[注意力计算:查询信标和历史 KV]
E --> F[生成当前 chunk 的输出]
F --> G[丢弃原始 token 激活值]
G --> H[保留并累积信标激活值]
H --> I[继续下一个 chunk]
I --> D
关键转折点在于第 6 步:丢弃原始 token 激活值后,信标激活值成为唯一的历史信息载体。如果压缩比设置不当,信息可能在此处丢失,导致后续生成质量下降。
伪代码示例
以下伪代码展示核心推理循环(非真实 API):
def encode_long_context(model, text, chunk_size=2048, beacon_interval=512):
chunks = split(text, chunk_size)
beacon_kv = None # 累积的信标 KV
for chunk in chunks:
units = split_into_units(chunk, beacon_interval)
interleaved = insert_beacons(units) # 每个 unit 末尾插入 beacon
# 输入包含历史 beacon 激活值和当前 interleaved 序列
output, beacon_kv_new = model.encode(interleaved, past_kv=beacon_kv)
# 只保留 beacon 对应的 KV
beacon_kv = extract_beacon_kv(beacon_kv_new)
return beacon_kv # 用于后续生成
这里 extract_beacon_kv 是关键操作:它从全部 KV 中筛选出信标 token 的 KV,丢弃原始 token 的 KV。如果实现错误,内存节省将大打折扣。
压缩率、信息保留与推理速度的权衡
压缩率的影响
压缩比 α 决定 KV 缓存大小和计算量。α 越大,内存越小,但每个信标需要编码更多信息,可能导致信息丢失。论文实验显示,在 4K 到 32K 上下文内,模型性能随上下文长度增加而提升,说明压缩并未显著损害信息保留。但超过训练长度(20K)后,性能仍能保持,得益于训练时的随机压缩比。
计算开销的构成
虽然 KV 缓存减少,但插入信标 token 增加了额外计算。每个 chunk 编码时,信标 token 的注意力计算量不可忽略。论文指出,推理时间加速 2 倍,主要来自 KV 缓存减少带来的内存带宽节省,而非计算量本身下降。在长上下文场景,内存带宽往往是瓶颈,因此收益明显。
与全注意力微调的对比
全注意力微调(如 LongChat-32K)将上下文窗口扩展到 32K,但训练和推理成本随长度平方增长。Activation Beacon 使用短滑动窗口,成本与窗口大小线性相关,因此扩展到 400K 时仍能保持可接受的内存和时间。论文对比了 Position Interpolation、NTK-Aware Scaled RoPE、LongLlama 等方法,Activation Beacon 在长上下文生成质量和运行效率上均占优。
失败模式与适用边界
压缩比过高导致信息丢失
如果压缩比设置过大,信标 token 无法承载足够信息,模型可能遗忘早期细节。例如,在 400K 文档中,如果压缩比达到 100:1,每个信标需要编码 100 个 token 的信息,这很可能超出激活值的容量。论文实验显示,在 128K 任务上性能仍接近未压缩基线,但未测试更高压缩比下的极限。
分布偏移与长尾输入
Activation Beacon 在训练时使用 20K 以内的文本,但推理时可能遇到 400K 的输入。虽然实验表明能泛化,但若输入分布与训练数据差异较大(如代码、多语言、特殊格式),压缩质量可能下降。此外,长尾输入(如罕见实体、复杂逻辑)可能难以被压缩激活值完整保留。
多轮对话中的累积误差
在多轮对话中,每轮都会新增信标激活值,累积的历史信息可能逐渐失真。论文提到,渐进式压缩允许在多轮对话中逐步更新压缩结果,但未给出长期累积的实验数据。工程上,可能需要定期重置或重新压缩历史。
可观测性与调试
生产环境中,需要监控以下信号:
- 压缩后生成困惑度(perplexity)是否异常升高。
- 信标激活值的范数是否稳定,若波动过大可能表示压缩不稳定。
- 回答中是否出现与历史事实矛盾的内容,这可能是信息丢失的信号。
替代方案比较与选型建议
| 方案 | 内存成本 | 信息保留 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 全注意力扩展 | 高(平方增长) | 完整 | 低(需微调) | 中等长度(≤32K) |
| StreamingLLM | 低(线性) | 仅最近 | 低 | 实时对话,历史不重要 |
| Activation Beacon | 中(线性,压缩后) | 压缩历史 | 中(需训练插件) | 超长文档,需要长期记忆 |
选型时,如果任务要求精确回忆早期信息,且上下文长度超过 100K,Activation Beacon 是更合适的选择。如果只是流式对话,StreamingLLM 足够。全注意力扩展适合长度在 32K 以内且预算充足的场景。
未解决的问题
Activation Beacon 仍有一些开放问题:
- 压缩比的上限在哪里?是否存在信息论意义上的极限?
- 如何评估压缩激活值的质量?目前主要依赖下游任务性能,缺乏直接度量。
- 能否与其他技术(如检索、RAG)结合,进一步提升长上下文理解能力?
论文作者已发布数据和代码(FlagEmbedding 仓库),感兴趣的读者可以复现实验并探索这些方向。