DeepSeek V4 源码剖析
第10章 Hyper-Connections:替代残差的 hc_mult 通路
残差被默认成"就是一次加法"。V4 的做法是把它换成一小段自己的线性代数。
V4 把传统残差换成了一个 4 路并行的"小代数"。每层的输入 / 输出都不再是单一 hidden,而是 4 份 hidden 的混合体。
10.1 引子:传统残差走到尽头了吗
ResNet 的 y = x + F(x) 是深度学习近十年最重要的发明之一。它解决了"深网络梯度消失" 的问题,让上百层、上千层的模型成为可能。Transformer 全盘继承了残差——每个 attention block 是 x + Attention(LN(x)),每个 FFN block 是 x + FFN(LN(x))。
但传统残差有两处固定不变的东西:
- 残差流只有 1 条:残差流的宽度被绑死在 hidden size 上,想加宽残差流只能加大整个模型
- 混合方式是全局统一的:所有 token、所有层都按
x + F(x)以固定的 1:1 累加,残差与层输出的比例不能按 token 调整
Hyper-Connections (HC)(Zhu et al.,arXiv:2409.19606)把残差流加宽成 n 条,并用可学的系数决定各条怎么读、怎么写、怎么互相混合。技术报告第 2.2 节对它的评价是:残差流宽度与 hidden size 解耦,提供了一条计算开销很小的扩展维度;但不加约束的 HC 在多层堆叠时经常出现数值不稳定,难以放大规模。
所以 V4 用的不是原版 HC,而是 DeepSeek 自己的约束版 mHC(Manifold-Constrained Hyper-Connections,Xie et al.,arXiv:2512.24880):把每层的 hidden 从 1 条扩展到 4 条,每层都用一组学习的混合系数决定"4 条 hidden 怎么进、attention 输出怎么出、4 条 hidden 怎么再聚合",并把其中的残差混合矩阵约束成双随机矩阵(第 10.4 节)。本章沿用源码的命名,统称 HC,需要区分时再写 mHC。
10.2 HC 的核心思想:hc_mult 维 hidden state
V4 的 Transformer.forward 第二行:
h = h.unsqueeze(2).repeat(1, 1, self.hc_mult, 1)
hc_mult = 4。这一行把 [B, S, D] 变成 [B, S, 4, D]——增加了一个新的"hc 维度",hidden 被复制 4 份。从这一行开始,层与层之间传递的都是 [B, S, 4, D],不再是 [B, S, D](attention / ffn 内部仍在 [B, S, D] 上算,见下文)。
每层 Block 的处理流程:
# inference/model.py:688
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)
x = self.attn_norm(x)
x = self.attn(x, start_pos)
x = self.hc_post(x, residual, post, comb)
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
每个 attention 与 ffn 各被 hc_pre / hc_post 包住——
- hc_pre:4 条 hidden → 1 条 hidden(送进 attention / ffn)
- hc_post:1 条 attention 输出 + 4 条原 residual → 4 条新 hidden
hc_pre 的输入是 [B, S, 4, D],输出是 [B, S, D] —— 它把 4 条 hidden 加权混合成 1 条。
hc_post 的输入是 [B, S, D](attention 输出)+ [B, S, 4, D](原 residual) + post / comb(hc_pre 留下的中间量),输出是 [B, S, 4, D]。
flowchart LR In["h_in: [B,S,4,D]"] --> HCpre["hc_pre"] HCpre --> X1["x: [B,S,D]"] HCpre -.post,comb.-> HCpost["hc_post"] X1 --> Norm["RMSNorm"] --> Attn["Attention"] Attn --> X2["x_out: [B,S,D]"] X2 --> HCpost In --> HCpost HCpost --> Out["h_out: [B,S,4,D]"]
10.3 hc_pre:把 4 路 hidden 混成 1 路
Block.hc_pre 的源码:
# inference/model.py:673
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
x = x.flatten(2).float()
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
mixes = F.linear(x, hc_fn) * rsqrt
pre, post, comb = hc_split_sinkhorn(mixes, hc_scale, hc_base, self.hc_mult, self.hc_sinkhorn_iters, self.hc_eps)
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
return y.to(dtype), post, comb
把这段代码拆成 5 步:
步骤 1:4 路 hidden 拼成 4×D 维向量
x = x.flatten(2).float() # [B, S, hc_mult, D] → [B, S, hc_mult * D]
把 [B, S, 4, D] 压成 [B, S, 4D]——把 4 路当作一个大向量。
步骤 2:RMSNorm
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
对这个 4D 大向量做 RMSNorm 的 rsqrt——保证后续混合矩阵运算的数值稳定。
步骤 3:投影到混合矩阵的"参数空间"
mixes = F.linear(x, hc_fn) * rsqrt
hc_fn: [mix_hc, hc_mult * D],其中 mix_hc = (2 + hc_mult) * hc_mult = 6 * 4 = 24。
输出 mixes: [B, S, 24]——24 个标量参数,决定本层这个 token 的混合方式。
为什么是 24?因为 24 = (2 + 4) × 4 = (pre + post + comb_per_dim) × hc_mult,对应:
- 4 个 pre 权重(4 路 hidden 的混合系数)
- 4 个 post 权重(attention 输出在 4 路上的分布系数)
- 16 个 comb 矩阵元素(4×4 的 4 路 hidden 重组矩阵)
步骤 4:Sinkhorn 归一化得到三组系数
pre, post, comb = hc_split_sinkhorn(mixes, hc_scale, hc_base, self.hc_mult, self.hc_sinkhorn_iters, self.hc_eps)
hc_split_sinkhorn 把 24 维的 mixes 切成三段:pre、post 用 sigmoid 约束成非负有界的系数,comb 用 Sinkhorn 归一化成近双随机矩阵。第 10.4 节展开。
步骤 5:用 pre 加权求和
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
pre: [B, S, 4],x: [B, S, 4, D]。逐 token 把 4 路 hidden 按 pre 系数加权求和——输出 [B, S, D]。(源码注释里写的 y: [b,s,hc,d] 与实际不符,按代码 sum(..., dim=2) 得到的是 [b,s,d]。)
注意 rsqrt 只作用在 mixes 上:它让混合系数不受 x 整体幅度的影响,而加权求和用的仍是未归一化的 x,所以后面还要再过 attn_norm / ffn_norm。
10.4 Sinkhorn 归一化:让混合矩阵"接近双随机"
hc_split_sinkhorn 是 V4 的一个核心 kernel,用 TileLang 写成,不是手写 CUDA:inference/kernel.py:430 的 hc_split_sinkhorn 只负责分配 pre / post / comb 输出,真正的计算在 inference/kernel.py:372 用 @tilelang.jit 装饰的 hc_split_sinkhorn_kernel 里。它做的事:
- 把 mixes [B, S, 24] 切成 pre [B, S, 4]、post [B, S, 4]、comb_raw [B, S, 16]
pre = sigmoid(mix * hc_scale[0] + hc_base) + eps;post = 2 * sigmoid(mix * hc_scale[1] + hc_base)——注意 post 多了个 2 倍系数,也没有加 eps(inference/kernel.py:391-394)。这正对应技术报告第 2.2 节的式 (6)(7):输入映射 A = σ(Ã)、输出映射 C = 2σ(C̃),用 sigmoid 保证非负且有界,避免信号相互抵消- 对 comb_raw 同样先算
mix * hc_scale[2] + hc_base,reshape 成 [B, S, 4, 4],再做行/列交替归一化,共hc_sinkhorn_iters=20轮:第 1 轮的行归一化是一次按行 softmax(数值稳定版:减 row_max 再 exp、行归一化、加 eps),此后 19 轮是普通的除以行和、列和,让它接近双随机矩阵
三组系数的参数化与技术报告式 (3)–(5) 一一对应:hc_fn 是生成动态分量的投影矩阵 W,hc_base 是静态偏置 S,hc_scale 的 3 个标量是报告里"初始化为小值"的门控因子 α(pre、post、comb 各一个)。
Sinkhorn 迭代的伪代码(输入 M 是上面缩放加偏置后的 comb_raw):
def sinkhorn(M, iters=20, eps=1e-6):
"""让 [4, 4] 矩阵 M 接近双随机:每行和为 1, 每列和为 1(真实 kernel 的等价 PyTorch 写法)"""
M = M.softmax(dim=-1) + eps # 第 1 轮的行归一化就是 softmax
M = M / (M.sum(dim=-2, keepdim=True) + eps)
for _ in range(iters - 1):
M = M / (M.sum(dim=-1, keepdim=True) + eps) # 行归一化
M = M / (M.sum(dim=-2, keepdim=True) + eps) # 列归一化
return M
为什么要 Sinkhorn?技术报告第 2.2 节给出的理由是:不加约束的 HC 在多层堆叠时数值不稳定,mHC 把残差映射矩阵约束到双随机矩阵集合(Birkhoff 多面体)上。双随机矩阵每条 hidden 的总输出权重为 1,每条 hidden 接收的混合权重总和也为 1,由此带来两条性质:谱范数不超过 1,残差变换不会放大信号(前向、反向都是);双随机矩阵相乘仍是双随机矩阵,几十层叠起来依然有界。
20 轮是 V4 的工程取值(config.json 里的 hc_sinkhorn_iters=20,技术报告称之为"a practical value")。20 轮之后矩阵只是"近"双随机:最后一步是列归一化,列和精确为 1,行和留有小误差(第 10.15 节有实测)。
另外,hc_pre 里那一路 pre-norm 投影(F.linear(x, hc_fn) * rsqrt)在 DeepGEMM 里有专门的 tensor core kernel:csrc/apis/hyperconnection.hpp:13 的 tf32_hc_prenorm_gemm,SM90 / SM100 各一份实现,见第 13 章。
flowchart LR
Mixes["mixes: [B,S,24]"] --> Split{切分}
Split --> PreRaw["pre_raw: [B,S,4]"]
Split --> PostRaw["post_raw: [B,S,4]"]
Split --> CombRaw["comb_raw: [B,S,16]"]
PreRaw --> Sigmoid1["sigmoid(·×scale+base)+eps"] --> Pre["pre: [B,S,4]"]
PostRaw --> Sigmoid2["2×sigmoid(·×scale+base)"] --> Post["post: [B,S,4]"]
CombRaw --> Reshape["·×scale+base 后 reshape [B,S,4,4]"] --> Sinkhorn["20 轮行/列归一化<br/>(第 1 轮行归一化用 softmax)"] --> Comb["comb: [B,S,4,4]<br/>≈ 双随机"]
10.5 hc_post:1 路 attention 输出 + 4 路 residual → 4 路新 hidden
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 = post.unsqueeze(-1) * x.unsqueeze(-2) + torch.sum(comb.unsqueeze(-1) * residual.unsqueeze(-2), dim=2)
return y.type_as(x)
公式:
这个公式表达:
- 第 h 路新 hidden =
post[h]× attention 输出("派发" attention 输出到 4 路) -
- 对所有原 residual 做加权和(comb 矩阵的第 h 列决定哪几路原 hidden 贡献到新第 h 路)
举个具体例子:如果 post=[1, 0, 0, 0](attention 输出全部进第 0 路),comb=I(identity,每路保持自己):
- 新第 0 路 = 1 × attn_out + residual[0]
- 新第 1 路 = 0 × attn_out + residual[1] = residual[1]
- 新第 2 路 = residual[2]
- 新第 3 路 = residual[3]
这等价于"传统残差"——只有第 0 路被更新,其他路保持不变。
如果 post=[0.25, 0.25, 0.25, 0.25]、comb=I:
- 每路新 hidden = 0.25 × attn_out + residual[i]
这等价于"attention 输出被均分到 4 路"——每路都收到 1/4 的 attention 信号 + 自己的 residual。
如果 comb 是 4×4 的全 1/4 矩阵:
- 每路新 hidden = post[h] × attn_out + (residual[0] + residual[1] + residual[2] + residual[3]) / 4
这等价于"先把 4 路 residual 平均,再加上 post 加权的 attention"——4 路 hidden 互相"渗透"。
V4 让 comb 通过 Sinkhorn 训练成"接近双随机但 token-自适应"——每个 token 自己决定它的 4 路 hidden 怎么互相渗透。
10.6 HC 的几何直觉:4 维 hidden 几何
把 HC 想象成"hidden state 的 4 维并行宇宙"——每个 token 同时在 4 个 hidden 通道上活动,这些通道互相关联但保持独立。
每层 Block 的作用:
- hc_pre:从 4 个并行宇宙中"提炼"出 1 个用于 attention 计算
- attention:在这 1 个宇宙中工作
- hc_post:把 attention 结果"分发"回 4 个宇宙,并允许它们互相渗透
这种"提炼 + 计算 + 分发 + 渗透"的循环让残差流有 4 倍的"信号通道"——梯度反向传播时也多了几条经由 comb 相互耦合的路径(第 10.13 节)。
代价是:层间传递的残差流变成 4 倍尺寸。attention / ffn 内部的计算没有变(仍然在 1 路上做),新增的只有 hc_pre / hc_post 的小投影和逐元素运算,FLOPs 增加很少;主要代价在别处——技术报告第 3.5.2 节说,mHC 增加了激活内存和流水线各阶段之间的通信量,经过融合 kernel、选择性重计算和调整 DualPipe 1F1B 重叠之后,墙钟开销压到了重叠后 1F1B 流水线阶段的 6.7%。
10.7 ParallelHead 的 HC 处理
V4 的最后一层(lm_head)对 HC 有特殊处理:
# inference/model.py:703 的类 + :728-735 的方法(中间 :705-726 的 __init__、
# get_logits、forward 从略——forward 就是先调 hc_head 把 4 路混成 1 路,
# 过 norm 后再 get_logits,最后按 world_size 做 all_gather 拼回完整词表)
class ParallelHead(nn.Module):
def hc_head(self, x: torch.Tensor, hc_fn: torch.Tensor, hc_scale: torch.Tensor, hc_base: torch.Tensor):
shape, dtype = x.size(), x.dtype
x = x.flatten(2).float()
rsqrt = torch.rsqrt(x.square().mean(-1, keepdim=True) + self.norm_eps)
mixes = F.linear(x, hc_fn) * rsqrt
pre = torch.sigmoid(mixes * hc_scale + hc_base) + self.hc_eps
y = torch.sum(pre.unsqueeze(-1) * x.view(shape), dim=2)
return y.to(dtype)
注意 hc_head 与 hc_pre 的差别:
hc_pre除混合结果外还要输出 post / comb,其中 comb 要过 Sinkhornhc_head只输出混合后的 1 路 hidden,系数 pre 只用 sigmoid + scale + bias 算出(hc_head_fn是[4, 4D],hc_head_scale只有 1 个标量)
这是因为 lm_head 的输入是 4 路 hidden,输出是单一 logits——只需要"4 路混成 1 路",不需要"再分发回 4 路"。所以省掉了 post / comb 和 Sinkhorn,只保留 pre 的 sigmoid。
10.8 与 ResNet / DenseNet / NormFormer 的横向对比
把 HC 与历史上的"残差变体"对比:
| 方案 | 残差通道数 | 是否可学 | 信息守恒 | 训练复杂度 |
|---|---|---|---|---|
| ResNet | 1 | 否 | 是 (加法) | 低 |
| Highway Network | 1 | 是 (gate) | 否 | 中 |
| DenseNet | n (累加) | 否 | 否 (越来越大) | 高 |
| NormFormer | 1 (norm 多) | 否 | 是 | 中 |
| ReZero | 1 (× α 标量) | 是 (单标量) | 是 | 低 |
| Hyper-Connections(原版) | n | 是 (n×n 矩阵) | 否 (无约束) | 中 |
| mHC(V4) | hc_mult=4 | 是 (4×4 矩阵) | 是 (Sinkhorn) | 中 |
V4 的 HC 在三个维度上独特:
- 多通道(hc_mult=4):比 ResNet 的 1 通道丰富,比 DenseNet 的"累加所有 prev" 受控
- 可学的 4×4 混合矩阵:比 Highway 的单 gate 表达力强
- Sinkhorn 归一化:把残差映射约束成近双随机矩阵,不放大信号——这是 mHC 相对原版 HC 的改动
这种权衡是 V4 选它的理由:技术报告第 2.2 节认为,mHC 在保留 HC 表达力的同时改善了跨层信号传播的稳定性。
10.9 训练时的 HC 行为
HC 在训练时的几个关键观察(基于公开技术报告 + 设计推断):
观察一:4 路 hidden 会自发"分化"
一种常见的设想是:训练过程中 4 路 hidden 会分化出不同的用途,这种分化是 HC 提升表达力的来源。这只是推测——技术报告和源码都没有给出 4 路各自承担什么的分析,别把它读成"某路管语法、某路管位置"这类具体分工。
观察二:Sinkhorn 迭代次数对收敛影响显著
hc_sinkhorn_iters=20 是 V4 在 config.json 里给出的默认值。迭代太少时 comb 矩阵不够接近双随机,信息守恒被破坏;迭代太多则收益递减而开销线性增长。20 是 V4 团队的经验取值——具体的"太少 / 太多"边界没有公开数据,下面几处提到的阈值都只是定性说明。
观察三:HC 与 RMSNorm 的协同
两处归一化分工不同:hc_pre 里的 rsqrt 只用来算混合系数(对应技术报告里的 ),让系数不受 hidden 幅度影响;hc_pre 混出的 1 路 hidden 本身没有被归一化,要靠随后的 attn_norm / ffn_norm 把它送进 attention / ffn 之前归一化。
观察四:HC 路径用 FP32 计算
源码里 HC 的参数(hc_attn_fn 等)在 set_dtype(torch.float32) 下创建,hc_pre 先 x.float() 再算;hc_post 虽然没有显式 .float(),但 FP32 的 post / comb 与 BF16 的 x 相乘时会按类型提升在 FP32 里算,最后 type_as(x) 转回 BF16。也就是说,推理代码里混合计算在 FP32 中进行,层间传递的 4 路残差流仍存成 BF16。源码没有说明取舍理由,合理的推断是混合系数与 Sinkhorn 迭代对精度敏感。
10.10 动手实验:mini HC
import torch
import torch.nn as nn
import torch.nn.functional as F
def sinkhorn(M, iters=10, eps=1e-6):
# 与 V4 kernel 一样:第 1 轮行归一化用 softmax(先减行最大值再 exp,防溢出)
M = M.softmax(-1) + eps
M = M / (M.sum(-2, keepdim=True) + eps)
for _ in range(iters - 1):
M = M / (M.sum(-1, keepdim=True) + eps)
M = M / (M.sum(-2, keepdim=True) + eps)
return M
class MiniHCBlock(nn.Module):
def __init__(self, dim=128, hc_mult=4):
super().__init__()
self.dim = dim
self.hc_mult = hc_mult
self.attn = nn.Linear(dim, dim) # 占位
self.ffn = nn.Linear(dim, dim) # 占位
# mix_hc = (2 + hc_mult) * hc_mult
# 小值初始化:输入经 rsqrt 归一化后,mixes 的幅度约为 0.01 × sqrt(hc_mult * dim)
self.hc_fn = nn.Parameter(torch.randn((2 + hc_mult) * hc_mult, hc_mult * dim) * 0.01)
def hc_pre(self, x):
# x: [B,S,hc,D]
B, S, hc, D = x.shape
x_flat = x.flatten(2).float()
rsqrt = torch.rsqrt(x_flat.square().mean(-1, keepdim=True) + 1e-6)
mixes = F.linear(x_flat, self.hc_fn) * rsqrt # [B,S,(2+hc)*hc]
# 切分
pre_raw = mixes[..., :hc]
post_raw = mixes[..., hc:2*hc]
comb_raw = mixes[..., 2*hc:].view(B, S, hc, hc)
pre = torch.sigmoid(pre_raw) + 1e-6
post = 2 * torch.sigmoid(post_raw) # 与 V4 一致:2σ,不加 eps
comb = sinkhorn(comb_raw, iters=10)
y = torch.sum(pre.unsqueeze(-1) * x, dim=2)
return y, post, comb
def hc_post(self, x, residual, post, comb):
# x: [B,S,D], residual: [B,S,hc,D], post: [B,S,hc], comb: [B,S,hc,hc]
y = post.unsqueeze(-1) * x.unsqueeze(-2) + torch.sum(comb.unsqueeze(-1) * residual.unsqueeze(-2), dim=2)
return y
def forward(self, h):
# h: [B,S,hc,D]
residual = h
x, post, comb = self.hc_pre(h)
x = self.attn(x)
h = self.hc_post(x, residual, post, comb)
return h
# 测试
block = MiniHCBlock()
h = torch.randn(2, 8, 4, 128)
out = block(h)
print(out.shape) # [2, 8, 4, 128]
这个 mini 版省掉了 V4 的门控因子 hc_scale、静态偏置 hc_base 和 attn_norm,只保留 HC 的骨架。跑通后再回看 V4 源码,会觉得"原来 HC 的核心算法只有几十行"。剩下都是与 attention / FFN / TP 分布的工程粘合。
10.11 HC 训练稳定性的 4 个机制
技术报告第 2.2 节明说,不加约束的 HC 在多层堆叠时经常出现数值不稳定。V4 在 HC 路径上有 4 处与稳定性相关的设计(前两条是报告给出的理由,后两条是源码事实加本专栏的解读):
机制 1:Sinkhorn 双随机约束
20 轮行/列交替归一化让 comb 矩阵接近双随机——每路 hidden 的"总输出权重"和"总接收权重"都接近 1,残差变换的谱范数不超过 1,不会放大信号;双随机矩阵对乘法封闭,几十层连乘也保持有界。
机制 2:sigmoid 约束 pre / post
pre 用 σ、post 用 2σ,二者都非负且有界。报告的说法是这样可以避免信号相互抵消的风险。
机制 3:FP32 混合计算
如第 10.9 节观察四所述,hc_pre / hc_post 的混合在 FP32 中进行,层间残差流存成 BF16。源码没有给出换成 BF16 计算会怎样的数据。
机制 4:两处归一化
hc_pre 里的 rsqrt 让混合系数不随 hidden 幅度变化,attn_norm / ffn_norm 再把混出的 1 路 hidden 归一化后送进 attention / ffn。
至于 hc_sinkhorn_iters=20,报告只说它是"a practical value":轮数不足则 comb 不够接近双随机,轮数过多则开销线性增长而收益递减,具体边界没有公开数据。
要注意比较对象:报告宣称的是 mHC 比不加约束的 HC 更稳定,并没有说它比传统残差更稳定,更没有说它让梯度消失 / 爆炸"免疫"。
10.12 HC 在 fine-tune 与推理时的特殊性
HC 在不同生命周期阶段的行为有差异:
预训练阶段:技术报告只交代了两点:门控因子 α(源码 hc_*_scale)初始化为小值,所以训练初期混合系数主要由静态偏置 S(源码 hc_*_base)决定;优化器上,mHC 的静态偏置和门控因子用 AdamW,投影矩阵 hc_*_fn 与其他大部分模块一样用 Muon(技术报告第 2.4 节)。S 怎么初始化、comb 在训练中怎么演化,报告和推理代码都没有给出。
fine-tune 阶段:Sinkhorn 是每次前向实时做的,不管 hc_* 参数被 fine-tune 成什么样,comb 都会被重新投影成近双随机矩阵——fine-tune 破坏不了双随机性。真正的风险是较大的学习率改变了预训练学到的混合方式。稳妥的做法(本专栏建议,未见官方指引)是给 HC 参数(hc_*)用较小的学习率或先冻结,观察效果再放开。
推理阶段:HC 的所有参数固定,每个 token 通过 hc_pre 算一次 mix → 跑 attention/ffn → hc_post 重组。HC 路径本身只有一次 [4D] → [24] 的投影(M 乘加,每层两次)加一个 4×4 的 Sinkhorn,相对 attention / MoE 那几百 M 的 GEMM 是小量——但它是逐 token、逐层都要跑的固定开销,DeepGEMM 专门为它写了 tf32_hc_prenorm_gemm 就是这个原因。技术报告第 3.3 节也提到,这个输出维度只有 24 的矩阵乘在 batch 很小时只能用 split-k。(推理侧的具体占比没有公开数字;报告里的 6.7% 是训练时相对 1F1B 流水线阶段的墙钟开销,不能挪用到推理上。)
KV cache 与 HC 的关系:HC 让 hidden 是 4 路的,但 KV cache 不是 4 路的——KV cache 在 attention 内部存储,attention 的输入是已经被 hc_pre 混到 1 路的 hidden。所以 KV cache 与单残差 transformer 一样大,HC 不增加 KV 开销。这是 V4 设计的精妙之处——HC 把残差流加宽 4 倍,只在"激活内存"上付代价,不在"持久 KV"上付。
10.13 HC 与传统残差的"梯度通道"对比
把 HC 与传统残差在反向传播时的梯度通道对比:
传统残差:
- 每层残差路径上的雅可比是单位阵 I 加上本层分支的贡献,梯度沿 identity 路径可以不衰减地回传
- 这正是 ResNet 缓解梯度消失的原因;但残差流只有 1 条,宽度等于 hidden size
Hyper-Connections(mHC):
- 每层残差路径上的线性部分换成了 4×4 的 comb:对角元素是"各路保留自己"的份额,非对角元素是 4 路之间的"渗透"
- 跨很多层回传时,梯度要连乘各层的 comb。comb 是近双随机矩阵,连乘结果仍近似双随机、谱范数不超过 1,所以这条路径既不会把梯度放大,也不会像不加约束的 HC 那样在深层堆叠里失控——这是技术报告第 2.2 节给出的稳定性论证
- comb 逐 token 计算,不同 token 的梯度在 4 路之间的分配方式也不同
至于这种多通道结构能否让训练更耐受 MoE 路由、稀疏注意力等带来的扰动,是一个合理的猜想,技术报告没有给出对应的消融数据。
10.14 HC 在小模型 vs 大模型上的表现猜想
HC 在 V4 的 1.6T 规模上是工程胜利,但它在小模型(< 100B)上的表现需要单独评估——大与小可能不同。把这个差异的几个推测列出来:
推测 1:HC 在大模型上的优势更明显
大模型层数多(V4 = 61 层),传统残差的"梯度通道瓶颈"更严重。HC 的 4 路并行扩展了梯度通道,对深网络更有用。小模型(10-20 层)梯度通道压力小,HC 的边际收益可能不如大模型。
推测 2:HC 的 Sinkhorn 开销在小模型上占比更高
20 轮 Sinkhorn 是与模型宽度无关的固定开销:矩阵永远是 4×4,成本只随 token 数增长。大模型的每层 GEMM 量大,这部分被摊薄;小模型的每层 GEMM 本来就小,同样的固定开销占比自然更高。(两边的具体百分比都没有公开测量,这里只给方向。)
推测 3:HC 的混合矩阵在小模型上可能"无聊"
HC 让模型学会 4 路 hidden 的差异化分配。但小模型容量有限——4 路可能学不出有意义的差异,最终 comb 矩阵收敛到接近 identity(即等价于传统残差)。这种情况下 HC 是"白付出代价没换来好处"。不过要注意反面证据:原版 HC 论文的主要实验就在 1B、7B 稠密模型和 OLMoE-1B-7B 上,报告了相对残差的明显提升,所以这条推测至少不能当成普遍规律。
推测 4:HC 与 fine-tune 的协同
如果在 V4 上 fine-tune,HC 的混合矩阵已经训好,fine-tune 可以继承。但如果你想从零训一个小型 V4-like 模型,HC 需要从头训——训练曲线可能比 ResNet 风格的小模型更长。
对工程师的建议:
- 如果你 fine-tune 的模型本身就带 HC(如 V4),HC 参数照常继承;给普通残差模型在 fine-tune 阶段加 HC 等于改架构,没有公开经验可循
- 如果你做的是 1B-10B 从零训练,建议先用普通残差跑 baseline,再尝试 HC / mHC 看是否有 ROI
- 如果你做的是 100B+ 训练,V4 的采用是一个信号,但技术报告没有给出去掉 mHC 的对照实验,是否值得仍要自己验证
这些推测需要社区独立实验验证。(预测)HC 在不同尺度下的表现很可能成为开源研究持续关注的方向。
10.15 Sinkhorn 收敛性与 hc_sinkhorn_iters 的选择
Sinkhorn 迭代是把 4×4 矩阵 M 投影到双随机矩阵的迭代算法。理解它的收敛性,能解释 V4 选 hc_sinkhorn_iters=20 这个数字。
Sinkhorn 收敛速度:
对元素全为正的矩阵,Sinkhorn 迭代是线性(几何级)收敛的——每轮误差大致缩小一个常数因子,这个因子取决于矩阵元素的悬殊程度。本专栏在 CPU 上对 1 万个随机 4×4 输入按上面的伪代码实测:输入元素标准差为 1 时,前 10 轮每轮行和误差中位数约缩小到 0.3 倍;标准差 3 时约 0.66 倍;标准差 10 时约 0.8 倍——元素越悬殊,收敛越慢。
实际收敛:
同一实测里,20 轮后行和误差的中位数:标准差 1 时约 1e-6(已到 eps 量级),标准差 3 时约 2e-4,标准差 10 时约 2e-2。再加轮数收益递减而开销线性增长。V4 选 20 是工程取值——源码里它是 hc_sinkhorn_iters 这个可配置参数,技术报告没有公开收敛曲线;上面的数字只反映随机输入,不代表 V4 训练后 comb 的实际分布。
少于 10 次会怎样:
迭代不足时 comb 矩阵不够接近双随机,谱范数不超过 1 的保证随之变弱,4 路 hidden 的幅度可能在层间漂移。这是按原理的推断,技术报告没有给出轮数消融。
数值稳定的边界:
直接对 M 取 exp,元素一大就会溢出(FP32 下 exp(89) 已超出表示范围)。V4 的 kernel 在第一步 softmax 里先减去行最大值再 exp,从根上避免了溢出;mixes * rsqrt 则让 mixes 不随 hidden 的整体幅度缩放。剩下的风险是元素过于悬殊时收敛变慢(见上面的实测)。
为什么不用 Hungarian / matching 算法:
Hungarian 算法解的是指派问题,给出的是置换矩阵(双随机矩阵集合的顶点),复杂度 O(n³),而且结果是离散的、不可微。mHC 要的是一个可微、能落在 Birkhoff 多面体内部的连续矩阵,所以用 Sinkhorn:每轮只有求和与除法,可微,也适合在 GPU kernel 里做。
hc_sinkhorn_iters 的可调性:
V4 的 config 把它作为参数(hc_sinkhorn_iters=20)——理论上可以调。如果你 fine-tune V4,不要改这个参数——会破坏预训练学到的 comb 分布。如果你从零训练 V4-style 模型,可以从 20 开始,根据训练稳定性调整。
10.16 延伸阅读
- Hyper-Connections 论文(arXiv:2409.19606):原版 HC,没有 Sinkhorn 约束
- mHC 论文(arXiv:2512.24880):V4 实际采用的约束版,技术报告第 3.5.2 节说工程优化细节见此文
- ResNet 论文(arXiv:1512.03385):残差连接的源头
- Sinkhorn-Knopp 算法(1967):双随机矩阵归一化的数学基础
- ReZero 论文(arXiv:2003.04887):可学残差缩放的早期尝试
- 本专栏第 1 章 §1.6:V4 在 Transformer.forward 那 4 行里 HC 入口的全景
- 本专栏第 11 章:MTPBlock 也用 HC——MTP 的 hc_head 处理
10.17 本章小结
- V4 用 Hyper-Connections 的约束版 mHC (hc_mult=4) 替代传统残差——每层有 4 路 hidden state 并行
hc_pre:4 路 → 1 路(用学到的 pre 系数加权求和),同时输出 post / comb 给 hc_post 用hc_post:1 路 attention 输出 + 4 路 residual → 4 路新 hidden(用 post 派发 + comb 矩阵渗透)hc_split_sinkhorn(TileLang kernel)用 20 轮行/列交替归一化(第 1 轮行归一化为 softmax)让 comb 接近双随机——这是 mHC 相对原版 HC 的关键约束,残差映射不放大信号、深层堆叠仍有界- HC 的代价主要是残差流 4 倍带来的激活内存与流水线通信,FLOPs 增加很少;技术报告称优化后墙钟开销为 1F1B 流水线阶段的 6.7%
- 与 ResNet / DenseNet / Highway 等历史方案对比,HC 在多通道、可学混合、信息守恒三个维度独特
第 11 章:MTP 模块——V4 保留的 V3 训练加速器,以及在推理时如何变成投机解码。