两篇文档的正文一模一样,问题一模一样,答案 "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
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
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
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
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