理解 Attention Residuals(AttnRes),需要先回答几个问题:

  1. 残差连接在深度方向上累加了什么?
  2. Attention 的选择对象是 Token,还是历史层输出?
  3. Query 不依赖当前 Hidden State,为什么权重仍然与输入有关?
  4. Full AttnRes 与 Block AttnRes 分别保留哪些状态?
  5. 增加跨层访问后,显存、通信和推理执行需要怎样调整?

下面按照残差连接、深度注意力、分块实现和工程分析的顺序讲解。本文介绍 Moonshot / Kimi Team 的 Attention Residuals;概念与公式依据 原论文第 2–4 节,实现接口可对照 官方仓库。

一、残差连接:既是梯度通路,也是信息聚合

一个 PreNorm 子层可以写成:

\[ h_{l+1}=h_l+f_l(\operatorname{Norm}(h_l)) \]

这里的子层可以是 Self-Attention,也可以是 MLP / MoE。为了简化符号,下面把子层内部的 Norm 包含在 \(f_l\) 中,记子层输出为 \(v_l=f_l(h_l)\),Embedding 为 \(v_0\)。

展开递推,可以得到:

\[ h_l=v_0+\sum_{i=1}^{l-1}v_i \]

这说明每个后续子层读到的是历史输出的累加结果。

观察角度 残差连接在做什么 需要理解什么
梯度传播 提供绕过子层变换的恒等路径 深层网络如何训练
信息传播 累加 Embedding 与历史子层输出 后续子层实际读到什么
聚合权重 对每个历史输出使用单位系数 无法直接按来源选择信息

例如,当前输入是 \(v_0+v_1+v_2\),即使当前子层更需要 \(v_1\),残差合并本身也没有一个单独调整 \(v_1\) 权重的接口。子层仍然可以通过自身变换处理混合结果,但无法直接读取合并前的各个来源。

随着深度增加,累加状态的幅度和新输出的相对占比会变化。这是理解 PreNorm Dilution 的入口;幅度的具体增长速度依赖输出相关性等条件,不能把它理解成所有模型都遵循同一个增长公式。

二、Attention 沿哪个维度发生?

Token 注意力与深度注意力
机制 当前计算选择什么 聚合方向 输出主要解决什么问题
Self-Attention 同一层的不同 Token 表示 序列维度 Token 之间的信息交互
Full AttnRes 同一个 Token 的历史子层输出 深度维度 历史层信息的选择
Block AttnRes 同一个 Token 的历史块摘要 深度维度 降低跨层状态保存成本

AttnRes 的聚合算子本身不会混合不同 Token。不同 Token 可以得到不同的深度权重,但各自只聚合自己位置上的历史表示。

因此,AttnRes 与 MQA、GQA、MLA 等机制处在不同位置:后者改变 Token Attention 的实现,AttnRes 改变子层之间的信息聚合。两类设计可以组合。

还要区分三个容易混淆的 Block:

下面的 \(L\) 统计 Attention / MLP 子层数量,不是 Transformer Block 数量。

三、Full AttnRes:保留来源,再学习如何聚合

Full Attention Residuals 数据流

对于第 \(l\) 个子层,候选来源是:

\[ V_l=\{v_0,v_1,\ldots,v_{l-1}\} \]

每个子层有自己的可学习向量 \(w_l\in\mathbb R^d\)。聚合过程为:

\[ s_{i\to l}=w_l^\top\operatorname{RMSNorm}_l(v_i),\qquad \alpha_{i\to l}=\frac{\exp(s_{i\to l})}{\sum_{j=0}^{l-1}\exp(s_{j\to l})} \]

\[ h_l=\sum_{i=0}^{l-1}\alpha_{i\to l}v_i,\qquad v_l=f_l(h_l) \]

组件 含义 关键区别
Query 子层参数 \(w_l\) 不是从当前 Token 用 \(W_Qh\) 生成
Key 用于打分的归一化历史表示 控制来源幅度对打分的影响
Value 原始历史表示 \(v_i\) 加权聚合时保留原始表示
Softmax 沿历史来源轴归一化 不是沿 Token 轴或 Hidden 轴

这里没有必要机械套用普通 Self-Attention 的 Q/K/V 投影矩阵或 \(1/\sqrt d\) 缩放;应以 AttnRes 自身的参数化为准。

1. 固定 Query 为什么能产生动态权重?

训练结束后,\(w_l\) 是固定参数,但是 Key 来自当前输入产生的历史表示。输入变了,Key 和分数会变化,所以权重仍然是 Input-dependent 的。

可以把 \(w_l\) 理解为该子层学习到的“选择标准”,而历史表示是每次推理中被选择的内容。

2. 加权平均与固定累加的区别

假设某个 Token 的三个来源是 \(v_0,v_1,v_2\),权重为 \((0.1,0.7,0.2)\),则:

\[ h_l=0.1v_0+0.7v_1+0.2v_2 \]

权重对来源不同,但同一来源的所有 Hidden 通道共享这个标量权重。这不是逐通道门控,也不是 Top-k 选择;有限分数下,Softmax 通常仍给每个候选一个非零权重。

由于权重非负、和为 1,给定这些 Value 时,输出是它们的凸组合:

\[ \|h_l\|\leq\sum_i\alpha_{i\to l}\|v_i\|\leq\max_i\|v_i\| \]

这个性质约束的是聚合本身。子层 \(f_l\) 仍然可能放大输出,因此不能由此推导整个网络一定有界或梯度一定稳定。

四、Block AttnRes:用深度分块控制状态数量

Full AttnRes 需要保留各个历史来源。Block AttnRes 将子层分为 \(N\) 组,每组内部累加子层输出,得到一个块摘要。

Block Attention Residuals 状态管理

令 \(B_n\) 为第 \(n\) 个深度块,完成后的摘要为:

\[ b_n=\sum_{j\in B_n}v_j,\qquad b_0=v_0 \]

当前块执行到一半时,还维护已完成子层输出的部分和 \(p\)。每次聚合的候选为:

所在位置 候选来源
网络第一个子层 Embedding
某块第一个子层 Embedding 与所有已完成块摘要
某块后续子层 上述来源,加当前块部分和 \(p\)
最终输出聚合 Embedding 与所有完成块摘要

聚合后的 \(h_l\) 用作子层输入;得到 \(v_l=f_l(h_l)\) 后,更新:

\[ p\leftarrow p+v_l \]

到达块边界时,将 \(p\) 保存为一个完成块摘要,并开始新的块。

需要特别注意:块摘要是该块自身子层输出的和,不能把整个历史混合输入 \(h_l\) 每次加进摘要,否则会重复累加历史信息。

一个具体例子

假设 6 个子层被分成 3 块,每块 2 个子层:

  1. 第一块先读取 \(v_0\),产生 \(v_1\);第二个子层读取 \(\{v_0,v_1\}\),产生 \(v_2\),完成 \(b_1=v_1+v_2\)。
  2. 第二块先读取 \(\{v_0,b_1\}\),产生 \(v_3\);下一个子层读取 \(\{v_0,b_1,v_3\}\),产生 \(v_4\),完成 \(b_2=v_3+v_4\)。
  3. 第三块按相同方式读取历史摘要并维护自己的部分和。

第一块结束后,后续子层可以选择 \(b_1\),但不能再分别选择 \(v_1\) 和 \(v_2\)。这是状态压缩带来的选择粒度变化。

五、一个可读的 PyTorch 聚合示例

下面仅实现候选来源的聚合算子,省略可学习的 RMSNorm Gain、完整 Transformer 与系统优化。sources 可以是 Full 的历史子层输出,也可以是 Block 的摘要与部分和。

import torch


def attention_residual(sources, query, eps=1e-6):
    # sources: 非空列表,每个 Tensor 的 Shape 为 [B, T, D]
    # query: 当前子层的可学习参数,Shape 为 [D]
    values = torch.stack(sources, dim=0)  # [R, B, T, D]
    values_fp32 = values.float()
    rms_inv = torch.rsqrt(values_fp32.square().mean(dim=-1, keepdim=True) + eps)
    keys = values_fp32 * rms_inv
    scores = (keys * query.float()).sum(dim=-1)  # [R, B, T]
    weights = torch.softmax(scores, dim=0)      # 沿来源轴归一化
    mixed = (weights.unsqueeze(-1) * values_fp32).sum(dim=0)
    return mixed.to(values.dtype), weights


sources = [torch.randn(2, 3, 8) for _ in range(4)]
query = torch.nn.Parameter(torch.zeros(8))
mixed, weights = attention_residual(sources, query)
assert mixed.shape == (2, 3, 8)
torch.testing.assert_close(weights.sum(dim=0), torch.ones(2, 3))
torch.testing.assert_close(mixed, torch.stack(sources).mean(dim=0))

\(w_l=0\) 时,分数都是 0,初始权重是均匀分布。这个例子由此验证输出为来源的平均值。均匀平均与标准残差的求和相差来源数量的比例,不能称为数值完全相同。

工程实现还要决定最终输出的聚合、Norm 参数、块边界及初始化方式。用 stack 展开来源适合解释 Shape,但高性能实现应分析它的额外分配和访存成本。

六、显存与计算成本如何估算?

设当前处理的 Tensor 大小为 \([B,T,d]\),存储每个元素需要 \(e\) 字节。以下估算针对前向聚合使用的来源状态,不包含模型权重、Token Attention KV Cache 或完整反向激活。

形式 历史来源状态规模 直接实现的深度聚合计算量
标准残差 \(O(BTd)\) \(O(BTLd)\) 次加法
Full AttnRes \(O(BTLd)\) \(O(BTL^2d)\)
Block AttnRes \(O(BTNd)\),另有当前部分和 \(O(BTLNd)\)

Block 的计算估算来自“\(L\) 个子层分别访问最多约 \(N\) 个来源”,不应仅因为状态数降到 \(N\),就把整个网络直接实现的计算量写成与 \(L\) 无关。

例如,假设 \(B=1,T=8192,d=4096,N=8,e=2\),只按 8 个摘要 Tensor 估算:

\[ M=BTNd e=536{,}870{,}912\text{ bytes}=512\text{ MiB} \]

这不包含 Embedding、当前部分和及临时 Buffer。即使块数不多,长上下文 Prefill 的临时状态仍可能很大。

训练中,如果某些激活本来就为反向传播保留,来源状态与其可能重叠;采用重计算或流水线并行时,应重新分析状态生命周期和跨 Stage 传输,不能直接把上表当作额外显存的精确值。

七、为什么可以采用两阶段计算?

对于当前块,已经完成的历史摘要不再变化,而块内所有子层的 Query 都是已知参数。因此,可以先批量计算这些 Query 对历史摘要的打分和聚合,再随子层执行依次处理当前部分和。

但 Self-Attention 和 MLP 本身仍依赖前面的计算,不能因为 Query 已知,就把整个 Transformer 的子层并行执行。

对两组候选,分别保存稳定 Softmax 的三个统计量:

\[ m=\max_i s_i,\quad z=\sum_i e^{s_i-m},\quad u=\sum_i e^{s_i-m}v_i \]

合并两组统计量时,令 \(m=\max(m_a,m_b)\):

\[ z=e^{m_a-m}z_a+e^{m_b-m}z_b,\qquad u=e^{m_a-m}u_a+e^{m_b-m}u_b,\qquad h=u/z \]

这说明两组候选可以分别处理后再合并,保持同一个 Softmax 的数学结果;浮点实现仍可能有舍入差异。不能把两组各自归一化后的输出直接相加,因为它们的归一化分母不同。

对照 GPU Arch,这类优化主要减少重复 HBM 读取并改变 Kernel 调度,而不是改变子层要执行的数学变换。

八、与其他残差机制如何比较?

机制 用于理解的主要问题 保存的状态与选择方式
标准残差 新输出如何加入当前状态? 单个累加状态
缩放 / 门控残差 当前更新占多大比例? 对递推中的状态和输出调整系数
mHC 多条残差流如何读、写与混合? 多流状态与受约束的混合
Full AttnRes 当前子层需要哪些历史来源? 保存历史输出并进行深度 Softmax
Block AttnRes 怎样减少历史来源的保存成本? 保存块摘要与当前部分和

这个对比描述设计接口,不代表模型效果排序。比较实际模型需要控制参数量、训练数据、训练预算和执行配置。

九、工程实践中需要检查什么?

场景 检查重点
模型结构 每个 Attention / MLP 是否有独立 Query,最终输出如何聚合
Block 状态 Embedding 是否重复加入、摘要是否只包含本块输出
数值实现 RMSNorm 作用于 Key,Softmax 作用于来源轴
长上下文 Prefill 摘要规模、分块执行、Sequence Sharding
Decode 临时深度来源与长期 Token KV Cache 的生命周期
Pipeline Parallel 接收方需要哪些历史摘要、是否存在重复传输
性能测量 聚合访存、临时分配、Kernel Launch 与端到端耗时

AttnRes 来源状态服务于当前 Token 沿深度的计算;Token KV Cache 服务于未来 Token 的序列 Attention。不要把两个维度的缓存混为一谈,也不要默认所有深度摘要都需要跨生成步永久保留。

这是模型结构的改变。把已训练模型中的残差加法直接替换成加权平均,会改变各层输入分布;要得到有效模型,需要相应训练或经过验证的适配过程。

学习时可以先手工展开三个子层的残差,再实现来源轴上的 Softmax,最后加入 Block 摘要。每一步都检查:保存了哪些来源、哪些来源还可被单独选择,以及为了读取它们付出了多少存储与访存成本。