基于昇腾的 DeepSeek-V4 流形约束超残差连接学习笔记
作者:昇腾实战派
知识地图:https://blog.csdn.net/Lumos_Lovegood/article/details/161601003
1. 背景概述
随着大语言模型向百万级上下文窗口演进,标准 Transformer 架构在训练稳定性、长程依赖建模和推理效率方面面临严峻挑战。DeepSeek-V4 针对这些痛点,提出了多项架构级创新,包括流形约束的超残差连接(mHC)、压缩稀疏注意力(CSA)与重度压缩注意力(HCA)混合机制等。本文将从标准残差连接的局限性出发,逐步解析这些技术的设计动机与实现原理。
2. 标准残差连接的局限性
标准残差连接作为 Transformer 的核心组件,在实际应用中存在以下问题:
- 缺乏显式的层间加权与尺度控制:各层更新直接累加,可能导致残差流的方差或范数随深度增长,进而引发激活尺度膨胀甚至数值不稳定。
- 无法主动协调不同层的贡献尺度:Attention 分支和 FFN 分支的贡献尺度无法被主动调节,若不同层的范数相差较大,可能导致部分层主导、部分层被淹没。
- 梯度稳定性不足:残差连接虽缓解了梯度消失,但不能保证激活和梯度始终稳定。
- 遗忘机制缺失:标准残差连接天然偏向信息保留,虽可通过抵消实现遗忘,但缺少显式、灵活的遗忘控制。
核心问题:标准残差连接本质上是无条件、无加权地累加历史状态和新更新,缺乏对“更新多少、保留多少、遗忘多少”的显式控制。
3. 标准 Hyper-Connections 的局限性
Hyper-Connections 通过维护多条残差流,并引入可学习的读出、写入和流间混合权重,在子层前将多流聚合为单流,在子层后再将输出写回多流,从而增强了残差连接的表达能力。
然而,跨层残差传播会形成多个无约束流间混合矩阵的连乘。若这些矩阵的奇异值缺少控制,不同表示方向可能被连续放大或衰减,导致残差流尺度失衡、激活离群值以及梯度不稳定。其数学形式如下:
x L = ( ∏ i = 1 L H L − i r e s ) x 0 + ∑ i = 0 L − 1 ( ∏ j = 1 L − 1 − i H L − j r e s ) H i p o s t F ( H i p r e x i , W i ) x_L = \left( \prod_{i=1}^{L}\mathcal{H}^{res}_{L-i} \right)x_0 + \sum_{i=0}^{L-1} \left( \prod_{j=1}^{L-1-i}\mathcal{H}^{res}_{L-j} \right) \mathcal{H}^{post}_{i} \mathcal{F} \left( \mathcal{H}^{pre}_{i}x_i,\mathcal{W}_i \right) xL=(i=1∏LHL−ires)x0+i=0∑L−1(j=1∏L−1−iHL−jres)HipostF(Hiprexi,Wi)
4. 流形约束超残差连接(mHC)
4.1 设计动机
mHC 的核心思想是将 Hyper-Connections 中无约束的残差流混合矩阵投影到双随机矩阵构成的 Birkhoff 多面体上。双随机约束使每次流间混合都表现为非负、守恒的凸组合;同时,有限个双随机矩阵的乘积仍为双随机矩阵,因此该稳定约束可以跨层保持,避免无约束矩阵连乘导致的残差流放大、衰减和失衡,从而恢复广义恒等映射并显著改善深层训练稳定性。
4.2 数学约束
残差流聚合矩阵 H l r e s \mathcal{H}^{res}_l Hlres 满足约束 MZ:
H l r e s ∈ M : = { M ∈ R n × n ∣ M 1 n = 1 n , 1 n T M = 1 n T , M ≥ 0 } \mathcal{H}^{res}_l \in \mathcal{M} := \left\{ M\in\mathbb{R}^{n\times n} \mid M\mathbf{1}_n=\mathbf{1}_n, \mathbf{1}_n^T M=\mathbf{1}_n^T, M\geq0 \right\} Hlres∈M:={M∈Rn×n∣M1n=1n,1nTM=1nT,M≥0}
降流矩阵及升流矩阵利用 sigmoid 保证数值在 0 和 1 之间。
4.3 参数生成方式
mHC 的参数生成采用可学习的线性投影与偏置:
$$
\tilde{A}_l
\alpha_l^{pre}\cdot
(X_lW_l^{pre})
+
S_l^{pre}
$$
$$
\tilde{B}_l
\alpha_l^{res}\cdot
\operatorname{Mat}(X_lW_l^{res})
+
S_l^{res}
$$
$$
\tilde{C}_l
\alpha_l^{post}\cdot
(\hat{X}_lW_l^{post})
+
S_l^{post}
$$
其中 W W W、 α \alpha α 和 S S S 均为可学习参数。得到 A ~ \tilde{A} A~、 B ~ \tilde{B} B~、 C ~ \tilde{C} C~ 后:
A l = σ ( A ~ l ) A_l=\sigma(\tilde{A}_l) Al=σ(A~l)
C l = 2 σ ( C ~ l ) C_l=2\sigma(\tilde{C}_l) Cl=2σ(C~l)
升降流矩阵只和当前层相关,因此做 Sigmoid 稳定数值即可。升流矩阵前的系数 2 用于校正 sigmoid 在零点输出 0.5 的尺度,使升流权重以 1 为中心,从而在初始化附近保持标准残差连接的单位写回增益,同时允许模型在 0 到 2 之间自适应调节各条残差流的更新强度。
4.4 双随机投影实现
对于残差映射 B B B,采用 Sinkhorn-Knopp 算法进行双随机投影:
$$
M^{(t)}
\mathcal{T}_r
\left(
\mathcal{T}_c
\left(
M^{(t-1)}
\right)
\right)
$$
先对输入 M M M 矩阵取指数以保证非负性,然后每轮迭代交替做行归一化和列归一化,多轮迭代后近似收敛为双随机矩阵。
4.5 代码实现
以下代码展示了 mHC 在 Transformer Block 中的具体实现(参考 DeepSeek-V4 推理代码):
class Block(nn.Module):
"""Transformer block with Hyper-Connections (HC) mixing.
Instead of a simple residual, HC maintains `hc_mult` copies of the hidden state.
hc_pre: reduces hc copies -> 1 via learned weighted sum (pre-weights from Sinkhorn).
hc_post: expands 1 -> hc copies via learned post-weights + combination matrix."""
def __init__(self, layer_id: int, args: ModelArgs):
super().__init__()
self.layer_id = layer_id
self.norm_eps = args.norm_eps
self.attn = Attention(layer_id, args)
self.ffn = MoE(layer_id, args)
self.attn_norm = RMSNorm(args.dim, self.norm_eps)
self.ffn_norm = RMSNorm(args.dim, self.norm_eps)
# 残差流的数量
self.hc_mult = hc_mult = args.hc_mult
# 行列归一化迭代次数
self.hc_sinkhorn_iters = args.hc_sinkhorn_iters
# 残差流有关算子用到的eps,防止除零.打破 Sinkhorn 迭代中的对称性,辅助收敛
self.hc_eps = args.hc_eps
# 降流投影 hc_mult* 1, 升流投影 1 * hc_mult , 残差聚合 hc_mult * hc_mult
mix_hc = (2 + hc_mult) * hc_mult
# 将hc_mult维度和hidden_dim维度合并
hc_dim = hc_mult * args.dim
with set_dtype(torch.float32):
# 以下参数用于生成A_hat,B_hat,C_hat矩阵, fn 既Wl, base既Sl,scale既α
self.hc_attn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
self.hc_ffn_fn = nn.Parameter(torch.empty(mix_hc, hc_dim))
# 这里的6个张量都把三类映射参数放在了一起存
self.hc_attn_base = nn.Parameter(torch.empty(mix_hc))
self.hc_ffn_base = nn.Parameter(torch.empty(mix_hc))
self.hc_attn_scale = nn.Parameter(torch.empty(3))
self.hc_ffn_scale = nn.Parameter(torch.empty(3))
# 完成attn或ffn前投影
def hc_pre(self, x: torch.Tensor, hc_fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor):
# x: [b,s,hc,d], hc_fn: [mix_hc,hc*d], hc_scale: [3], hc_base: [mix_hc], y: [b,s,hc,d]
shape, dtype = x.size(), x.dtype
# 从第2维开始, 展平后面所有维 x->[b,s,hc*d]
x = x.flatten(2).float()
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
# 等价于所有流输入展平做RMSNorm后,再同时完成三个矩阵的投影
# mixes=x[b,s,hc*d]@hc_fn.T[hc*d,mix_hc]->[b,s,mix_hc]
# 在 mix_hc 维拼接了 [hc_mult,1],[1,hc_mult],[hc_mult,hc_mult]
mixes = F.linear(x, hc_fn) * rsqrt
# 融合算子,直接返回A、B、C矩阵 pre: [b,s,hc_mult], post: [b,s,hc_mult],comb:[b,s,hc_mult,hc_mult]
pre, post, comb = hc_split_sinkhorn(mixes, hc_scale, hc_base, self.hc_mult, self.hc_sinkhorn_iters, self.hc_eps)
# pre.unsqueeze(-1) -> [b,s,hc_mult,1] * x.view[b,s,hc,d] -> [b,s,hc,d]
# 这条代码完成: 对x的每个流(既hc维度)加权求和
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
# 感觉也可以直接 x.view(shape).transpose(-1,-2) @ pre.unsqueeze(-1)-> [b,s,d,1]
# 再把最后一维 squeeze 掉
# y -> [b,s,d]
# 直接返回降流后的x, 和升流矩阵、聚合矩阵
return y.to(dtype), post, comb
# 完成hc升流投影
def hc_post(self, x: torch.Tensor, residual: torch.Tensor, post: torch.Tensor, comb: torch.Tensor):
# x: [b,s,d], residual: [b,s,hc,d], post: [b,s,hc], comb: [b,s,hc,hc], y: [b,s,hc,d]
# post[b,s,hc,1]*x[b,s,1,d] -> y[b,s,hc,d]
# post完成 FN(x) 对不同流的不同比例混合
# comb[b,s,hc,hc,1] * residual[b,s,hc,1,d] -> [b,s,hc,hc,d]
# torch.sum(...,2)对输入流做加权聚合
# 等价post.unsqueeze(-1) @ x.unsqueeze(-2) + comb @ residual
y = post.unsqueeze(-1) * x.unsqueeze(-2) + torch.sum(comb.unsqueeze(-1) * residual.unsqueeze(-2), dim=2)
return y.type_as(x)
def forward(self, x: torch.Tensor, start_pos: int, input_ids: Optional[torch.Tensor]) -> torch.Tensor:
# 原输入
residual = x
# 获取降流后的输入,以及另外两个矩阵
x, post, comb = self.hc_pre(x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base)
# RMSNorm及attention
x = self.attn_norm(x)
x = self.attn(x, start_pos)
# 同时完成 comb@resudual + post@x
x = self.hc_post(x, residual, post, comb)
# 同上, 操作由attention变为ffn
residual = x
x, post, comb = self.hc_pre(x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base)
x = self.ffn_norm(x)
x = self.ffn(x, input_ids)
x = self.hc_post(x, residual, post, comb)
return x
核心算子:
@tilelang.jit(pass_configs=pass_configs)
def hc_split_sinkhorn_kernel(hc: int, sinkhorn_iters: int, eps: float):
n = T.symbolic("n")
mix_hc = (2 + hc) * hc
threads = 64
@T.prim_func
def hc_split_sinkhorn_kernel_(
mixes: T.Tensor[(n, mix_hc), FP32],
hc_scale: T.Tensor[(3,), FP32],
hc_base: T.Tensor[(mix_hc,), FP32],
pre: T.Tensor[(n, hc), FP32],
post: T.Tensor[(n, hc), FP32],
comb: T.Tensor[(n, hc, hc), FP32],
):
# 算子框架为tileLang, 指明线程块为n,块内theads线程
with T.Kernel(n, threads=threads) as i:
mixes_shared = T.alloc_shared(mix_hc, FP32)
comb_frag = T.alloc_fragment((hc, hc), FP32)
T.copy(mixes[i, :], mixes_shared)
# 每线程块处理一个token
for j in T.Parallel(hc):
# 映射出升流矩阵
pre[i, j] = T.sigmoid(mixes_shared[j] * hc_scale[0] + hc_base[j]) + eps
for j in T.Parallel(hc):
# 映射出降流矩阵
post[i, j] = 2 * T.sigmoid(mixes_shared[j + hc] * hc_scale[1] + hc_base[j + hc])
# 不涉及矩阵乘法, 皆为向量操作, 因此与形状排列无关
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = mixes_shared[j * hc + k + hc * 2] * hc_scale[2] + hc_base[j * hc + k + hc * 2]
row_sum = T.alloc_fragment(hc, FP32)
col_sum = T.alloc_fragment(hc, FP32)
# comb = comb.softmax(-1) + eps
row_max = T.alloc_fragment(hc, FP32)
T.reduce_max(comb_frag, row_max, dim=1)
# 数值稳定的exp
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = T.exp(comb_frag[j, k] - row_max[j])
T.reduce_sum(comb_frag, row_sum, dim=1)
# eps的作用可能是防止低精度时的下溢,以及缓解迭代算法的收敛僵局?
# 亦或者,参考下面T.serial(sinkhorm_iters - 1)循环体内容,可能写错了忘在分母上加括号?
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / row_sum[j] + eps
# comb = comb / (comb.sum(-2) + eps)
T.reduce_sum(comb_frag, col_sum, dim=0)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
# 感觉上面两个循环完全可以写在下面的循环体里, 代码都是一样的。不知道为什么要单独提出去
for _ in T.serial(sinkhorn_iters - 1):
# comb = comb / (comb.sum(-1) + eps)
T.reduce_sum(comb_frag, row_sum, dim=1)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (row_sum[j] + eps)
# comb = comb / (comb.sum(-2) + eps)
T.reduce_sum(comb_frag, col_sum, dim=0)
for j, k in T.Parallel(hc, hc):
comb_frag[j, k] = comb_frag[j, k] / (col_sum[k] + eps)
T.copy(comb_frag, comb[i, :, :])
return hc_split_sinkhorn_kernel_
# 返回 sigmoid(hc_scale*mixes + hc_base), 2*sigomid(hc_scale*mixes+hc_base), M
def hc_split_sinkhorn(mixes: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor, hc_mult: int = 4, sinkhorn_iters: int = 20, eps: float = 1e-6):
b, s, _ = mixes.size()
pre = mixes.new_empty(b, s, hc_mult)
post = mixes.new_empty(b, s, hc_mult)
comb = mixes.new_empty(b, s, hc_mult, hc_mult)
kernel = hc_split_sinkhorn_kernel(hc_mult, sinkhorn_iters, eps)
kernel(mixes.view(-1, (2 + hc_mult) * hc_mult), hc_scale, hc_base,
pre.view(-1, hc_mult), post.view(-1, hc_mult), comb.view(-1, hc_mult, hc_mult))
return pre, post, comb
核心算子 hc_split_sinkhorn 实现了 Sinkhorn-Knopp 迭代,将混合矩阵投影到双随机矩阵空间,确保残差流的稳定传播。
5. 总结
DeepSeek-V4 通过流形约束超残差连接(mHC)解决了深层 Transformer 训练不稳定的问题,通过混合注意力机制(CSA+HCA)实现了百万级上下文的高效处理。这些技术创新共同构成了 DeepSeek-V4 长上下文基础设施的核心,为下一代大语言模型的发展提供了重要参考。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐
所有评论(0)