← 返回首页
推理加速2026-10-10 发布 · 字节 Seed 团队 arXiv 论文(2026-09)解读

DeepSeek V4.1 为何回退 V4 的固定窗口压缩?

一个 token 位移,召回从 50% 掉到 0:分块 KV 压缩的"相位敏感性",以及 V4.1 用 CSA2 + SWA 的解法

一、先看一个例子:多打一个字,模型就找不到密码了

想象一个场景:让模型在一篇长文档里找一串 6 位密码。文档内容没变,问题没变,答案也没变,只是在最前面多打了一个无关紧要的字。结果模型就彻底找不到那串密码了。

具体来说,实验里的两个文档长这样:

文档 A(开头不加东西):

```

The access code for AX31 is 482915.

The access code for BX72 is 193407.

...(中间还有 190 组类似的 KV 对)...

Question: What is the access code for AX31?

Answer:

```

文档 B(开头只多打一个 "Okay"):

```

Okay The access code for AX31 is 482915.

The access code for BX72 is 193407.

...(中间还有 190 组类似的 KV 对)...

Question: What is the access code for AX31?

Answer:

```

两篇文档的正文一模一样,问题一模一样,答案 "482915" 出现的位置也一模一样。唯一的差别,就是文档 B 开头多了 一个 token("Okay")。

图 1:文档 A 与文档 B 的对比
flowchart TB subgraph A["文档 A:开头无额外 token"] direction TB A1["The access code for AX31 is 482915."] A2["Question: What is the access code for AX31?"] A1 --> A2 A2 --> A3["模型回答:482915
✓ 正确"] end subgraph B["文档 B:开头多打一个 'Okay'"] direction TB B0["Okay"] B1["The access code for AX31 is 482915."] B2["Question: What is the access code for AX31?"] B0 --> B1 --> B2 B2 --> B3["模型回答:???
✗ 错误"] end A3 --> C["召回率 50%"] B3 --> D["召回率 0%"] style A fill:#e6f3ff,stroke:#333,color:#000000 style B fill:#ffe6e6,stroke:#333,color:#000000 style A3 fill:#ccffcc,color:#000000 style B3 fill:#ffcccc,color:#000000 style C fill:#ccffcc,color:#000000 style D fill:#ffcccc,color:#000000,stroke:#cc0000

先解释两个词:召回率和 gap

召回率:我们给模型 16 个不同密码的问题,它答对几个,召回率就是几除以 16。答对 8 个,召回率就是 0.500;答对 0 个,召回率就是 0.000。

gap:我们把文档开头分别插入 0 个、1 个、2 个……一直到 7 个 token,得到 8 个召回率。这 8 个数里,最高分减去最低分,就是 gap。

- gap = 0:不管文档怎么挪,召回率都一样,模型完全不挑位置。

- gap = 0.500:最好情况下能答对一半,最差情况下全错,模型非常挑位置。

现在再看 Full Attention(普通注意力)和 8:1 压缩的对比:

图 2:Full Attention 与 8:1 固定窗口压缩的 gap 对比
flowchart LR A["同一篇文档
AX31 = 482915"] --> B["开头插入 0~7 个 token
得到 8 个召回率"] B --> C["Full Attention
最高 0.969,最低 0.938
gap = 0.031
只差 3.1 个百分点,几乎不波动"] B --> D["8:1 固定窗口压缩
最高 0.500,最低 0.000
gap = 0.500
差 50 个百分点,断崖"] style C fill:#ccffcc,color:#000000 style D fill:#ffcccc,color:#000000,stroke:#cc0000

也就是说,Full Attention 的 gap 只有 0.031,意思是:最好情况召回率约 96.9%,最差约 93.8%,只差 3.1 个百分点,几乎是一条平线。而 8:1 压缩的 gap 是 0.500,意思是:最好能答对一半,最差全错,差了 50 个百分点。前者是后者的 16 倍。

再往深走一步:为什么 128 个一组、组与组不重叠,就会丢信息

前面玩具例子里 W=8,真正的 HCA 是每 128 个 token 压成 1 条。机制拿一个生活里的词就能看明白——「阿莫西林胶囊」,分词后正好 6 个 token(阿 / 莫 / 西 / 林 / 胶 / 囊,下图每个黄格 = 1 个 token):

切分完整(写文章时见过的排布):6 个 token 恰好整段落在同一块里。均值池化给这 128 个 token 写一句"块摘要",药名在这 128 个位置里占 6 席——摘要里还带着完整的药名轮廓,查询「阿莫西林胶囊」能对上。

边界 0块边界 128边界 256块 1(128 token)块 2(128 token)阿莫西林胶囊(6 token,整段在块内)
全貌 · 切分完整:药名 6 token 整段落在块 1 内部(黄格),离块边界很远
…… 阿莫西林胶囊 ……
↓ 均值池化(这一块的 128 个 token → 1 条摘要)
块摘要:带着完整的「阿莫西林胶囊」✓  ·  查询「阿莫西林胶囊」→ 匹配成功

被切割(开头多打几个字):token 整体后移,块边界纹丝不动。总有一个相位会让药名正好骑在块边界上——黄格被边界切成 3 + 3:阿 / 莫 / 西 留在前一块的最尾巴,林 / 胶 / 囊 落到后一块的最开头。两块的均值摘要里各自只有半截药名(各占 3/128 ≈ 2.3%)——查询「阿莫西林胶囊」哪条都匹配不上,召回归零。

边界 0块边界 128边界 256块 1块 2阿莫西(3 格)林胶囊(3 格)药名被边界切成 3 + 3
全貌 · 被切割:药名骑在块边界上,被切成 3 + 3(黄格跨过中间红色块边界)
…… 阿莫西 林胶囊 ……
↓ 各自均值池化
块 1 摘要:「……阿莫西」(半截药名)✗ 块 2 摘要:「林胶囊……」(半截药名)✗ 查询「阿莫西林胶囊」→ 两条摘要都拼不出完整药名 → 召回 0

对照着看:同样的内容,唯一变的是它和 128 边界的相对位置——整段在块内就是完整语义,骑上边界语义就被拆散。delta 扫过 0–127,总有一些相位正好骑在边界上,召回率因此周期波动。(实验里的随机密码「482915」同理:被切成「482 / 915」两半,机制完全一样。)

这和 DeepSeek V4 的 HCA 有什么关系?

这要从 DeepSeek V4 的注意力设计说起。V4 采用了一套混合注意力架构,由两个模块交替使用:

- CSA(Compressed Sparse Attention,压缩稀疏注意力):每 4 个 token 的 KV 缓存压成 1 个条目,再用 Top-K 索引器挑出最相关的压缩块做注意力;

- HCA(Heavily Compressed Attention,重度压缩注意力):直接把每 128 个 token 压成 1 个条目,但保持稠密注意力,也就是不挑选、每个查询都看到所有压缩条目。

HCA 的设计目标很明确:用极高的压缩率(128:1)换取超长上下文的全局视野覆盖。它像是一个“速记员”——把 128 个 token 浓缩成一条记录,虽然信息粒度粗了,但胜在覆盖范围广。

图 3:DeepSeek V4 的 CSA + HCA 混合架构
flowchart TD A0["DeepSeek V4 混合注意力"] --> A1["CSA
4:1 压缩 + Top-K 稀疏"] A0 --> A2["HCA
128:1 压缩 + 稠密注意力"] A2 --> A3["固定窗口、非重叠
块边界固定在 0, 128, 256..."] A3 --> A4["文档平移 1 token
→ 相位敏感性
→ 检索准确率波动 40pp"] style A0 fill:#e6f3ff,color:#000000 style A1 fill:#e6f3ff,color:#000000 style A2 fill:#ffcccc,color:#000000 style A3 fill:#ffe6e6,color:#000000 style A4 fill:#ffcccc,color:#000000,stroke:#cc0000

但问题在于:HCA 采用的是固定窗口、非重叠压缩。每 128 个 token 一块,块边界固定在位置 0、128、256、384……这些整数倍位置。一旦文档整体平移 1 个 token,所有关键信息在压缩块内的位置全部偏移,块内组合完全改变。这就是“相位敏感性”的根源——研究团队发现,同一信息仅因物理位置不同,DeepSeek-V4 系列的检索准确率最高能拉开 40 个百分点。

V4.1 的做法很直接:弃用 HCA,用 CSA2 取代 CSA 与 HCA 的混合架构。CSA2 沿三个维度同时压缩缓存——条目大小、序列维度和层维度——并通过跨层 KV 共享和 Top-K 索引复用,在不需要 HCA 那种极端压缩率的情况下,同样把 KV Cache 压到了 V4-Flash 的 1/4。同时,V4.1 前两层使用滑动窗口注意力(SWA),保留最近 128 个 token 的未压缩原始 KV。用滑动窗口保住“近处看得清”,用 CSA2 的跨层共享和 FP4 量化保住“远处压得狠”,两条腿走路,不再依赖单一极端压缩率。

图 4:DeepSeek V4.1 的 CSA2 + SWA 架构
flowchart TD B0["DeepSeek V4.1 注意力"] --> B1["CSA2
跨层 KV 共享 + Top-K 索引复用"] B0 --> B2["SWA
前两层保留 128 token 原始 KV"] B1 --> B3["条目大小 / 序列维度 / 层维度
三维同时压缩 + FP4 量化"] B2 --> B3 B3 --> B4["不再依赖单一极端压缩率
KV Cache 降至 V4 的 1/4"] style B0 fill:#e6f3ff,color:#000000 style B1 fill:#ccffcc,color:#000000 style B2 fill:#ccffcc,color:#000000 style B3 fill:#e6ffe6,color:#000000 style B4 fill:#ccffcc,color:#000000

这恰恰解释了 DeepSeek V4 被大量用户长期诟病的“抽风”问题——同一个问题,有时精准找到答案,有时死活检索不到关键内容,仿佛换了个大脑。字节 Seed 团队今年 9 月底在 arXiv 提交的论文,直接点明了这一现象的根源:分块 KV 缓存压缩带来的相位敏感性。多数用户曾将这种不稳定归因于服务器压力或网络波动,但技术研究揭示,模型架构本身的设计选择才是核心原因。在 128K 长上下文检索测试中,同一信息仅因物理位置不同,DeepSeek-V4 系列的检索准确率最高能拉开 40.2 个百分点。这正是 V4.1 要回退 V4 激进固定窗口压缩设计的核心原因之一:省显存的代价,是把“位置相位”变成了系统性风险。

图 5:用户反馈、社区归因与论文揭示的根源
flowchart TD A["DeepSeek V4 用户反馈
同一问题有时精准有时失效"] --> B["社区归因:
服务器压力 / 网络波动"] A --> C["字节 Seed 论文揭示:
分块 KV 缓存压缩的相位敏感性"] C --> D["128K 检索准确率
不同相位相差 40.2 个百分点"] D --> E["V4.1 回退激进压缩设计"] style A fill:#e6f3ff,color:#000000 style B fill:#ffe6e6,color:#000000 style C fill:#ffcccc,color:#000000,stroke:#cc0000 style D fill:#ffcccc,color:#000000,stroke:#cc0000 style E fill:#ccffcc,color:#000000

二、实验数据:翻译一下,这是什么意思

刚才已经看到了那个“多打一个字就找不到密码”的例子。现在来看完整的实验数据——但这次我们换个方式说。

先看一张“体检报告”

我们把模型对 16 个不同密码的检索能力,在 8 种“文档开头插入几个字”的情况下各测了一遍。结果像一张体检报告:

条件通俗解释gap(最差和最好的差距)
Full Attention不压缩,原汁原味。挪不挪位置都一样,最高 96.9%,最低 93.8%0.031(只差 3.1 个百分点,几乎没波动)
8:1 压缩 + 随机相位训练压缩了,但训练时见过各种偏移位置。每次考 50 分,稳定但平庸≈0(不波动,但也不高)
8:1 压缩 + 弱训练(60 步)压缩了,但没学好。几乎全考 0 分≈0(全崩)
8:1 压缩 + 仅 delta=0 对齐训练压缩了,只见过“开头不加字”的情况。不加字考 50 分,加一个字直接 0 分0.500(差 50 个百分点,断崖)

最刺眼的是最后一行:同一个密码,同样的文档,开头不加字能答对一半,加一个字就全错。 波动周期严格等于压缩窗口 W=8,跨数据种子完全确定。

图 6:召回率随 delta 的变化
xychart-beta title "召回率随 delta 的变化" x-axis "delta (token)" [0, 1, 2, 3, 4, 5, 6, 7] y-axis "召回率" 0 --> 1 line "Full Attention" [0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97, 0.97] line "压缩+对齐训练" [0.50, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00, 0.00] line "压缩+随机相位训练" [0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50, 0.50]

为什么?用一个“翻译”来理解

把这件事想象成把中文翻译成英文:

- 压缩窗口 = 一个翻译员。给翻译员 8 个中文词,他翻成一个英文短语。

- 均值池化 = 他只能翻出“大概意思”,不能逐字对应。

- 关键 token = 密码 “482915” 这串数字。

- 文档平移 = 把整篇文章往后挪一个字。

现在翻译员的工作规则是:每 8 个字一组,固定分组。 他每组只翻一个“概括”。

- delta=0 时:“482915” 这组里可能还有 “is” 和句号,翻译员翻出来的概括里“482915”占的比重还行,模型能认出来。

- delta=1 时:整篇文章往后挪了一个字。“482915” 被挤到了另一组,和更多无关的字混在一起。翻译员翻出来的概括里,“482915” 被稀释了,模型就认不出来了。

问题不在于密码本身变了,而在于分组的方式变了。 密码没动,是它旁边的“邻居”换了。

图 7:密码在块中央与块边缘的对比
flowchart LR subgraph delta0["delta=0:密码在块中央"] direction LR A1["...is"] --> A2["482915"] --> A3["."] --> A4["The"] A2 --> A5["压缩条:482915 信号强 ✓"] end subgraph delta1["delta=1:密码被挤到块边缘"] direction LR B1["Okay"] --> B2["...is"] --> B3["482915"] --> B4["."] B3 --> B5["压缩条:被无关内容稀释 ✗"] end style delta0 fill:#e6f3ff,color:#000000 style delta1 fill:#ffe6e6,color:#000000 style A5 fill:#ccffcc,color:#000000 style B5 fill:#ffcccc,color:#000000

训练相位覆盖度:决定“坑”有多深

压缩器是训练出来的。它学的是“块内内容分布”长什么样。

- 只训练 delta=0 → 只见过“密码在块中央”的分组方式。一旦文档平移,分组方式超出训练分布,解码能力断崖,gap=0.500。

- 随机相位训练 → 见过所有偏移情况。周期坑被填平了,但平均性能变成恒 0.500 的“中庸”——什么位置都能应付一点,但都没有特别好。

- 训练不足 → 压缩器没学会恢复,直接全崩。

这解释了一条完整光谱:训练相位覆盖越窄,周期坑越深;覆盖越全,坑越平,但平均分未必更高。

对应到 DeepSeek V4 系列的实际数字:V4-Flash-Base 的 gap 达到 40.2 个百分点,经过后训练之后 V4-Flash-0731 缩小到 19.1,但后训练可以缓解、不能消除固定窗口切割本身的信息瓶颈。所以 V4.1 选择弃用 HCA,用 CSA2 沿三个维度同时压缩缓存(条目大小、序列维度、层维度),并通过跨层 KV 共享和 Top-K 索引复用,在不需要极端压缩率的情况下同样实现了高效的 KV Cache 压缩——这不是“退步”,而是在为周期性最差 case 买单。

图 8:训练相位覆盖度决定 gap 大小
flowchart LR A["训练相位覆盖度"] --> B["仅 delta=0"] A --> C["随机相位全覆盖"] A --> D["训练不足"] B --> E["gap = 0.500
周期坑最深"] C --> F["gap ≈ 0
周期坑填平
平均恒 0.500"] D --> G["全崩
召回 0.000"] E --> H["V4-Flash-Base:gap 40.2pp"] F --> I["V4.1:弃用 HCA
改用 CSA2 + SWA"] G --> J["不可用"] style A fill:#e6f3ff,color:#000000 style E fill:#ffcccc,color:#000000 style F fill:#ccffcc,color:#000000 style H fill:#ffcccc,color:#000000 style I fill:#ccffcc,color:#000000

三、原理:固定窗口压缩如何制造周期性弱点

1. 窗口边界是固定的

固定窗口分块压缩的规则很简单:每 W 个 token 一块,块内 KV 做均值池化,压成一条压缩 KV。窗口边界固定在位置 0、8、16、24……这些整数倍位置。问题在于:文本不会永远按训练时的相位排列。

图 9:窗口边界固定在整数倍位置,文档平移后块内容改变
flowchart LR subgraph 文档A["文档 A(delta=0)"] direction LR A1["块0
token 0-7"] --> A2["块1
token 8-15"] --> A3["块2
token 16-23"] end subgraph 文档B["文档 B(delta=1)"] direction LR B0["填充 token"] --> B1["块0
token 1-8"] --> B2["块1
token 9-16"] --> B3["块2
token 17-24"] end A1 -.->|"窗口边界固定"| B1 style 文档A fill:#e6f3ff,color:#000000 style 文档B fill:#ffe6e6,color:#000000 style A1 fill:#cce6ff,color:#000000 style B1 fill:#ffcccc,color:#000000

2. 文档平移改变的是“相位”,不是内容

当文档整体平移 delta 个 token 时,每个 token 的绝对位置都加了 delta,但压缩窗口边界没有动。于是,关键 token 在块内的位置变了,和它同块的邻居也变了。召回率因此随 delta mod W 周期波动。

图 10:文档平移如何改变关键 token 的相位
flowchart TD A["文档整体平移 delta"] --> B["每个 token 绝对位置 +delta"] B --> C["压缩窗口边界不变
仍固定在 0, W, 2W, ..."] C --> D["关键 token 在块内的位置改变"] D --> E["同块邻居改变"] E --> F["块内均值池化结果改变"] F --> G["压缩 KV 与 query 的匹配变差或变好"] G --> H["召回率随 delta mod W 周期波动"] style A fill:#e6f3ff,color:#000000 style H fill:#ffcccc,color:#000000

3. 均值池化会稀释关键信号

块内均值池化把 8 个 token 的 K/V 平均成 1 条。如果关键 token 和大量无关填充混在同一块,它的信号会被稀释。压缩条与 query 的匹配变差,数字就抄不出来。

图 11:纯净块与混合块的均值池化对比
flowchart LR subgraph 纯净块["纯净块(关键 token 信号强)"] P1["关键 token
访问码 482915"] --> P2["均值池化"] P3["无关填充"] --> P2 P2 --> P4["压缩 KV 清晰
匹配成功"] end subgraph 混合块["混合块(关键 token 被稀释)"] M1["关键 token
访问码 482915"] --> M2["均值池化"] M3["大量无关填充"] --> M2 M4["更多无关填充"] --> M2 M2 --> M5["压缩 KV 模糊
匹配失败"] end style 纯净块 fill:#e6ffe6,color:#000000 style 混合块 fill:#ffe6e6,color:#000000 style P4 fill:#ccffcc,color:#000000 style M5 fill:#ffcccc,color:#000000

4. RoPE 让压缩必须“先去旋转,再平均”

K 向量带着 RoPE 位置旋转。直接对烘焙后的 K 做块均值,位置语义会混乱,实验会全崩。正确做法是:逐 token 把 K 旋转到 0 参考位,去掉 RoPE;在去 RoPE 域做块内均值;再把均值烘焙到块中心位置。这一步不做对,压缩条件会直接 0% 召回。

图 12:RoPE 域压缩的正确流程与错误做法
flowchart TD A["原始 K 向量
带 RoPE 旋转"] --> B["逐 token 旋转到 0 参考位
去掉 RoPE"] B --> C["在去 RoPE 域做块内均值"] C --> D["得到块均值向量"] D --> E["烘焙到块中心位置
重新加 RoPE"] E --> F["压缩 KV"] G["错误做法:直接平均烘焙后的 K"] --> H["位置语义混乱"] H --> I["实验全崩,0% 召回"] style A fill:#e6f3ff,color:#000000 style F fill:#ccffcc,color:#000000 style G fill:#ffcccc,color:#000000 style I fill:#ffcccc,color:#000000

5. 训练相位覆盖度决定 gap 大小

图 13:训练相位覆盖度与 gap 的关系
flowchart LR A["训练相位覆盖度"] --> B["仅 delta=0"] A --> C["随机相位全覆盖"] A --> D["训练不足"] B --> E["gap = 0.500
周期坑最深"] C --> F["gap ≈ 0
周期坑填平
平均恒 0.500"] D --> G["全崩
召回 0.000"] style A fill:#e6f3ff,color:#000000 style E fill:#ffcccc,color:#000000 style F fill:#ccffcc,color:#000000

四、拿同一个例子,看 V4.1 为什么能解决

还是「阿莫西林胶囊」这 6 个 token、还是开头多打几个字把边界推到词中间。V4.1 的方案拆开是三招,每一招都在这个例子上起作用:

第一招:前两层 SWA(滑动窗口)——窗口跟着内容一起滑

固定窗口的病根是"边界钉死在 0、128、256,内容动了边界不动"。SWA 反过来:窗口边界跟着文档一起平移。文档 B 比文档 A 多 1 个 token,滑动窗口的起点也整体后移 1 位——窗口内每个 token 的"相对位置"一个都没变。于是最近 128 个 token 的原始 KV 原封不动、零压缩地保住了:只要密码落在这个范围内,无论开头多打几个字,它的 K/V 和训练时一模一样,怎么挪都查得到。本例里药名就在文档前部,SWA 直接覆盖。

第二招:CSA2 把 128:1 改成 4:1 + Top-K——就算被稀释也稀释得轻,还能挑

远处的块仍要压缩,但压缩比从 128:1 退到 4:1:一条压缩记录只代表 4 个 token。就算被边界拆开,药名 token 在一条压缩记录里的占比也有 1/2(4 条一拆,最差 2/4),而 HCA 骑边界时只剩 3/128 ≈ 2.3%——信号密度是原来的 20 多倍。更关键的是 Top-K 索引器:注意力不再平等地看所有压缩条,而是只挑和查询最相关的那几条。无关内容的"糊照"根本不参与注意力,被稀释的影响被隔离在挑中的少数条目里。

第三招:跨层 KV 共享 + FP4 量化——省显存的活儿换个方式干

HCA 的存在理由是"压得狠、省显存"。V4.1 把省显存的任务拆给了跨层 KV 共享(多个层复用同一份 KV)和 FP4 量化(每个数字存得更省),不再依赖单一维度的极端压缩。压缩温和了,相位的坑自然就浅了。

同一个例子下的对照:

维度V4 · HCA(128:1 固定窗口)V4.1 · CSA2 + SWA
压缩比128:1(均值池化)4:1(CSA2)+ 最近 128 原始 KV(SWA)
块边界钉死在 0 / 128 / 256SWA 窗口随内容滑动;CSA2 压缩比温和
delta=1 时密码被切成两半,各占 1.6% / 3.1%,两块都对不上近处原始 KV 完整保留;远处每条记录占比约 1/4,Top-K 只挑相关的
召回表现gap 40.2pp,最差归零不依赖相位,坑被填平

一句话:V4.1 不是放弃压缩,而是把"固定边界的极端压缩"换成"跟着内容滑的窗口 + 温和的压缩 + 挑重点的索引"。同一个 1 token 位移,再也踩不出那个 50 个百分点的坑。

【示意 C】SWA:窗口跟着内容一起滑,药名的"相对位置"永远不变:

flowchart TD subgraph SA["delta=0"] direction LR w0["SWA 窗口起点 a"]:::n --> s0["… 前文 …"]:::n --> k0["药名(相对窗口起点恒为第 k 格)"]:::p --> e0["… 后文 …"]:::n end subgraph SB["delta=1:整个文档 +1,窗口起点也 +1"] direction LR w1["SWA 窗口起点 a+1"]:::n --> s1["… 前文 …"]:::n --> k1["药名(相对窗口起点仍是第 k 格)"]:::p --> e1["… 后文 …"]:::n end SB --> R["相对位置不变 → 原始 KV 零压缩保留 → 怎么挪都查得到 ✓"]:::ok style SA fill:#e6f3ff,color:#000000 style SB fill:#e6ffe6,color:#000000 style R fill:#ccffcc,stroke:#1a7f4b,color:#155c38 classDef n fill:#f6f8fa,stroke:#8b949e,color:#1f2937 classDef p fill:#fff3d6,stroke:#b45309,color:#7c3e08,stroke-width:2px classDef ok fill:#ccffcc,stroke:#1a7f4b,color:#155c38

【示意 D】CSA2 4:1:就算药名骑在边界被拆开,每条压缩里也占一半以上,Top-K 两条都挑(对照 HCA:只剩 3/128 ≈ 2.3%):

flowchart LR subgraph E1["压缩条 E1(只代表 4 个 token)"] direction LR x0["阿"]:::p --> x1["莫"]:::p --> x2["西"]:::p --> x3["无"]:::n end subgraph E2["压缩条 E2(只代表 4 个 token)"] direction LR y0["林"]:::p --> y1["胶"]:::p --> y2["囊"]:::p --> y3["无"]:::n end E1 --> T["Top-K 索引:两条都相关 → 都进注意力
药名在每条里占比 ≥ 1/2(对照 HCA 的 3/128 ≈ 2.3%)✓"]:::ok E2 --> T style T fill:#ccffcc,stroke:#1a7f4b,color:#155c38 classDef n fill:#f6f8fa,stroke:#8b949e,color:#1f2937 classDef p fill:#fff3d6,stroke:#b45309,color:#7c3e08,stroke-width:2px classDef ok fill:#ccffcc,stroke:#1a7f4b,color:#155c38
flowchart TD Q["同一个考题:开头多打一个字,找 482915"]:::io Q --> A["近处(最近 128 token)
SWA 保留原始 KV、窗口随内容滑动
相位不变 → 密码原样可查"]:::good Q --> B["远处(更早上下文)
CSA2 4:1 温和压缩
密码信号占比 ≈1/4,稀释轻"]:::good B --> C["Top-K 索引只挑相关压缩条
无关糊照不参与注意力"]:::good A --> D["找得到 ✓
1 token 位移不再触发断崖"]:::io C --> D style Q fill:#e6f3ff,color:#000000 style A fill:#ccffcc,color:#000000 style B fill:#ccffcc,color:#000000 style C fill:#ccffcc,color:#000000 style D fill:#ccffcc,color:#000000

五、为什么普通 benchmark 测不出来

普通 benchmark 通常固定一种数据布局。如果只测 delta=0,相位对齐的压缩条件能拿到 0.500 的“及格分”,看起来能用。但它完全暴露不了:把文档平移 1 个 token,召回就归零。

平均分掩盖了最差 case。长上下文压缩模型的评测,如果只测固定布局,就像只测一条相位曲线上的一个点,然后宣称整条曲线都安全。

图 14:固定布局评测(只测 delta=0)
flowchart TD A1["文档 + 提问"] --> A2["召回 0.500"] A2 --> A3["结论:可用"] style A1 fill:#e6f3ff,color:#000000 style A2 fill:#ccffcc,color:#000000 style A3 fill:#ccffcc,color:#000000

必须做相位扫描:同一篇文档,前缀插入 1 到 W-1 个无关 token,分别测召回。只有逐 delta 扫描,才能看到周期坑。

图 15:相位扫描评测(delta=0..7)
flowchart TD B1["delta=0"] --> B2["召回 0.500"] B3["delta=1"] --> B4["召回 0.000"] B5["delta=2..7"] --> B6["召回 0.000"] B2 --> B7["发现周期坑"] B4 --> B7 B6 --> B7 style B1 fill:#e6f3ff,color:#000000 style B2 fill:#ccffcc,color:#000000 style B3 fill:#ffe6e6,color:#000000 style B4 fill:#ffcccc,color:#000000 style B5 fill:#ffe6e6,color:#000000 style B6 fill:#ffcccc,color:#000000 style B7 fill:#ffcccc,color:#000000,stroke:#cc0000

六、对长上下文系统的启示

1. 评测必须做相位扫描:固定布局的平均分没有意义。同文档 + 前缀插入 1..W-1 个无关 token,是最小压测集。

2. 训练必须覆盖压缩窗口相位:随机平移增广可以填平周期坑。但代价是平均性能可能变得平庸——恒 0.5 的条件就是证明。

3. 生产压测要包含最差 case:不是测“平均文档”,而是测“同文档 + 一个 token 位移”的变体。一个 token 就足以触发断崖。

4. 固定窗口压缩不是免费的:它省显存、省算力,但把位置相位变成了系统性风险。V4.1 弃用 HCA、改用 CSA2(跨层 KV 共享 + Top-K 索引复用)并保留前两层 SWA 原始 KV,正是在不牺牲稳定性的前提下继续推进 KV Cache 压缩。

图 16:固定窗口压缩的代价与 V4.1 的应对
flowchart LR A["固定窗口压缩
CSA + HCA"] --> B["省显存、省算力"] A --> C["引入位置相位风险"] C --> D["周期坑:gap 巨大"] D --> E["评测:必须做相位扫描"] D --> F["训练:必须覆盖全相位"] D --> G["生产:必须压测最差 case"] E --> H["V4.1 弃用 HCA
改用 CSA2 + SWA"] F --> H G --> H style A fill:#e6f3ff,color:#000000 style B fill:#ccffcc,color:#000000 style C fill:#ffcccc,color:#000000 style D fill:#ffcccc,color:#000000 style H fill:#ccffcc,color:#000000

结语

固定窗口分块 KV 压缩的周期性弱点,不是 DeepSeek 独有,也不是某个实现 bug。只要窗口边界固定,只要文档可能整体平移,只要压缩器训练相位覆盖不全,周期坑就会出现。

一个 token 的位移,足以让 50% 变 0。这就是 V4.1 弃用 HCA、改用 CSA2 的根本原因之一:省下来的显存,可能要用最差 case 的稳定性来还。

图 17:从 V4 的 HCA 到 V4.1 的 CSA2 + SWA
flowchart TD A["V4:CSA + HCA 混合"] --> B["HCA 128:1 固定窗口压缩"] B --> C["文档平移改变相位"] C --> D["均值池化稀释关键信号"] D --> E["召回率周期波动"] E --> F["gap 远大于 Full Attention"] F --> G["V4.1:弃用 HCA
改用 CSA2 + SWA"] G --> H["用稳定性换显存"] style A fill:#e6f3ff,color:#000000 style B fill:#ffe6e6,color:#000000 style C fill:#ffcccc,color:#000000 style D fill:#ffcccc,color:#000000 style E fill:#ffcccc,color:#000000 style F fill:#ffcccc,color:#000000 style G fill:#ccffcc,color:#000000 style H fill:#ccffcc,color:#000000
华杉的技术博客 · 苏ICP备2026063680号-1
首页