DeepSeek V4 源码剖析
第8章 384 专家与 Hash 路由:前 3 层为什么不学
"num_hash_layers": 3—— DeepSeek-V4-Proconfig.json前 3 层 MoE 的路由不学,直接由 token id 查一张
[129280, 6]的整数表决定。这是全 61 层里唯一一处"放弃学习"的地方。
8.1 引子:MoE 路由学习的"早期失败"
V4 的 config.json 里有这么一行:
"num_hash_layers": 3
意思是:前 3 层 MoE 的路由不学,直接由 token id 查表决定。
这个设计在第一眼看时是反直觉的——MoE 的核心价值不就是"学到 token 与 expert 的最优配对"吗?为什么要把前 3 层的"学习"放弃,换成预定的查表?
技术报告第 2.1 节只交代了做法:相比 DeepSeek-V3,把最初几个 Transformer block 里的稠密 FFN 换成了采用 Hash 路由(Roller et al., 2021)的 MoE 层,路由由一个预定义的、以输入 token ID 为自变量的 hash 函数决定。也就是说,这 3 层在 V3 里本来就没有路由可学。DeepSeek 没有公开这个决定的理由。Hash Layers、StableMoE 等工作讨论过学习路由的一个共同问题:同一个输入的目标 expert 会随训练漂移,expert 一边在学、自己负责的 token 集合一边在变。下面几条沿这个方向展开,其中关于"早期层"的部分是本专栏的推断,放在这里作为工程推断而非官方解释:
- 早期层的 hidden state 还非常接近 token embedding——语义层级很低,主要是字面 token 信息
- 早期层的路由器很难"学到差异化"——所有 token 都长得差不多,gate 输出的 score 区分度很小
- 早期层的路由器训练通常会陷入"塌陷"——少数几个 expert 主导,剩下的 expert 闲置、训练不动
- 后期再用 noaux_tc 调 bias 反而打乱已经学到的路由分布——bias 抬高的 expert 突然被大量 token 分给,但这些 expert 之前没怎么训练过,输出质量差
其中第 3 条就是 MoE 文献常说的"路由塌陷"(routing collapse)。按这个推断,V4 的做法是把最容易塌的那几层直接绕过去:前 3 层不学,直接 hash。至于 V4 选 3 而不是 2 或 5,源码与 config 里没有任何线索——num_hash_layers 就是一个可配置的整数。
flowchart TB
subgraph 历史问题["早期层路由的困难(工程推断)"]
direction TB
H1["MoE 模型的早期层"]
H2["所有 token hidden 相似"]
H3["Gate 学不到差异"]
H4["Expert 利用率长尾"]
H1 --> H2 --> H3 --> H4
end
subgraph V4解法
direction TB
V1["前 3 层 hash 路由"]
V2["跳过早期学习困难"]
V3["每个 expert 都有<br/>固定输入分布"]
V4["训练稳定"]
V1 --> V2 --> V3 --> V4
end
H4 -.解决.-> V1
8.2 tid2eid 表:V4 的 hash 路由数据结构
V4 在 hash 层的 Gate 里有这个核心数据结构:
self.tid2eid = nn.Parameter(
torch.empty(args.vocab_size, args.n_activated_experts, dtype=torch.int32),
requires_grad=False
)
tid2eid: [vocab_size=129280, n_activated_experts=6]——每个 token id 对应 6 个 expert id。
注意几个关键点:
点一:dtype=int32:这不是浮点数,是整数索引——每个槽位存一个 0 到 n_routed_experts-1 的 expert 索引。
点二:requires_grad=False:不参与梯度更新——这是个预计算的常量表,不是学习参数。
点三:每层独立:虽然源码里 tid2eid 是 nn.Parameter,但每层的 Gate 都在自己的 __init__ 里新建一份(inference/model.py:559),前 3 层共有 3 份互相独立的 tid2eid 表。三份表的内容是否真的互不相同要下载权重才能确认,源码只能保证它们可以不同。
点四:用 nn.Parameter 包装但 requires_grad=False:这是 PyTorch 的常见模式——让这个张量随模型一起被序列化(保存到 checkpoint),但不更新。
forward 里使用:
if self.hash:
indices = self.tid2eid[input_ids]
注意传进来的 input_ids 已经被 MoE.forward 拍平过——weights, indices = self.gate(x, input_ids.flatten())(inference/model.py:632),所以它是 [B*S] 的一维张量,查表后得到 [B*S, n_activated_experts]。这个二维形状是必须的:紧接着的 original_scores.gather(1, indices) 只吃二维索引。整条路完全跳过了"基于 hidden state 学习路由"的过程。
8.3 tid2eid 表查询的数据流
flowchart LR Token["input_ids: [B, S] int64"] --> Embed["embed lookup"] Token --> TidLookup["tid2eid[input_ids]<br/>查表,无梯度"] TidLookup --> FixIdx["indices: [B*S, 6]<br/>每 token 6 个固定 expert id"] Embed --> Hidden["hidden(经 mHC 与 ffn_norm 后拍平)<br/>[B*S, 7168]"] Hidden --> Score["sqrtsoftplus(linear(x))<br/>仍然要算 score"] Score --> Gather["weights = score.gather(FixIdx)<br/>用固定 indices 取权重"] Gather --> Norm["归一化 + × 2.5"] Norm --> Output["(weights, FixIdx)<br/>送给 MoE.forward"] classDef static fill:#1f2937,stroke:#475569 classDef learn fill:#312e81,stroke:#a78bfa class TidLookup,FixIdx static class Score,Gather,Norm learn
灰色节点是静态部分(tid2eid 查表 → 固定 expert 索引),紫色节点是可学习部分(score 投影 → 权重归一化)。两段式架构在图上一目了然。
8.4 hash 路由的"伪学习"机制
虽然 hash 层不学 routing,但 V4 的 Gate 仍然计算 score——只是 score 只用于 routing weight,不用于 expert 选取:
# Gate.forward,inference/model.py:564–584(简化:省略 softmax / sigmoid 两个
# score_func 分支,只留 V4 用的 sqrtsoftplus;行尾中文注释为本专栏所加)
def forward(self, x: torch.Tensor, input_ids: Optional[torch.Tensor] = None):
scores = linear(x.float(), self.weight.float())
scores = F.softplus(scores).sqrt() # sqrtsoftplus
original_scores = scores
if self.bias is not None:
scores = scores + self.bias # hash 层 bias 是 None,跳过
if self.hash:
indices = self.tid2eid[input_ids] # ← hash 选取
else:
indices = scores.topk(self.topk, dim=-1)[1]
weights = original_scores.gather(1, indices) # ← 仍然用 score 算权重
if self.score_func != "softmax":
weights /= weights.sum(dim=-1, keepdim=True)
weights *= self.route_scale
return weights, indices
读这段代码:
tid2eid决定了"用哪 6 个 expert"——固定的scores(sqrtsoftplus 输出) 决定了"这 6 个 expert 的相对权重"——可学习
意思是:哪些 expert 被激活是固定的,但每个 expert 的"贡献程度"仍然是模型学习的。这种"固定结构 + 学习权重"的混合设计,让 hash 层既避免了"路由学习困难",又保留了"token 自适应权重"的灵活性。
这是 V4 的 hash routing 的精妙之处——它不是完全静态的路由,而是"静态选 expert + 动态选权重" 的两段式。
8.5 tid2eid 表的"生成方法"
V4 的源码里 tid2eid 是 torch.empty(...) 初始化——意味着权重必须从 checkpoint 加载,源码不包含生成逻辑。几种在文献里出现过的生成方法:
方法一:随机生成(最简)
# 给每个 token 随机分配 6 个 expert
import torch
vocab_size = 129280
n_routed = 384
n_activated = 6
tid2eid = torch.zeros(vocab_size, n_activated, dtype=torch.int32)
for tid in range(vocab_size):
tid2eid[tid] = torch.randperm(n_routed)[:n_activated]
简单,但缺点是 expert 的"输入 token 分布" 完全随机——每个 expert 看到的 token 类型分布与 vocab 整体分布一致。
方法二:基于 token 频率均衡
# 先在训练语料上统计 token 频率,按频率从高到低
# 贪心地把每个 token 分给当前负载最轻的 expert
# 让每个 expert 的"总训练 token 数"接近
这就是 Hash Layers 论文里的 Balanced assignment。它让训练更均衡,但需要先在大规模语料上统计 token 频率;论文也指出,由于 Zipf 分布里个别 token 的频率本身就超过理想份额,完美均衡做不到。
方法三:基于聚类
# 用一个小型预训练模型给每个 token 算 embedding
# 对 embedding 聚类成 384 簇,每个 token 取最近的 6 个簇
这种做法让 expert 之间有"语义意义"——簇 0 的 expert 可能专门处理某类语义的 token。不过 Hash Layers 论文的实验里(单 expert、64 个 expert 的设置),按聚类分配的困惑度明显比随机分配差(验证集 23.90 对 23.22),反过来把同一簇内的 token 打散到不同 expert(Dispersed Hash)又能恢复到随机分配的水平。
V4 实际用了哪种没有公开——tid2eid 在 torch.empty(...) 之后只能从 checkpoint 加载,而权重是 LFS 大文件。想知道答案的人可以下载权重后直接统计这张表:算一下每个 expert 被多少个 token id 引用,如果分布近似均匀且与 token 频率无关,那多半是方法一;如果与语料频率相关,是方法二;如果语义相近的 token 常常共享 expert,是方法三。这是一个任何人都能做、但本专栏没有做的实验,结论待补。
8.6 hash 路由与学习路由的协同
V4 是 hash 层(layer_id 0/1/2)+ 学习层(layer_id 3..60,共 58 层)的混合。这种混合架构有两个关键的工程效应(以下为工程推断):
效应一:训练梯度的"温度调节"
前 3 层用 hash 路由后,每个 expert 的"输入 token 分布"是固定的——它在训练全程接收的 token 类别一致。这让前 3 层的 expert 训练非常稳定——每个 expert 学到的"擅长处理什么"是清晰的。
后 58 层的学习路由可以基于这个"已经稳定" 的 hidden state 学到差异化的路由——不会陷入"早期层干扰晚期路由"的循环。
效应二:路由学习的"启动温度"
设想学习路由的训练有一个"warm-up"阶段——前 N 步全部 expert 被均匀使用,然后逐渐让模型自己选 expert。V4 的 hash 层相当于把这个 warm-up 永久化——前 3 层永远按 hash 表固定地使用所有 expert,给后续层提供稳定的 hidden state。
用 token id 固定路由的思路来自 Hash Layers 论文(arXiv:2106.04426)。但要注意几点差别:论文里每个 token 只进 1 个 expert,且没有任何路由参数;论文也没有研究"前几层 hash、后几层学习"这种组合,它的层位置实验(64 个 expert 的 hash 层放在 11 层解码器的不同层)结论是放在较后的层效果更好,与 V4 放在最前 3 层恰好相反。V4 在 1.6T 总参数(技术报告第 4.2.1 节)的规模上把 hash 路由用在了前 3 层,本专栏没有找到直接支撑这个位置选择的公开文献。
8.7 与其他 MoE 模型的预定路由对比
把 V4 的 hash 路由与其他 MoE 模型的"非学习路由"做对比:
| 方案 | 路由依据 | 是否学习 | 应用模型 |
|---|---|---|---|
| Switch Transformer 原始 | softmax(x · W) | 学习 | Switch |
| Hash Layers | 预计算查表 token id → 1 个 expert | 不学习 | 早期 MoE 实验 |
| Random Routing | 随机 | 不学习 | 实验性 |
| Position Hash | 按序列位置 hash | 不学习 | Hash Layers 的对照基线 |
| V4 hash | tid2eid[token_id] | 不学习 | DeepSeek V4 前 3 层 |
| Expert Choice | expert 主动选 token | 学习 | EC-MoE |
| StableMoE | 先学习路由并蒸馏成轻量路由器,再冻结 | 部分学习 | StableMoE |
Hash Layers 论文本身已经试过多种查表方式(随机、按频率均衡、聚类、打散聚类、bigram 等),也比较过每层用同一张表还是各层各用一张表。V4 的 hash 路由与它相比的差别在于:
- 多 expert 路由:每 token 6 个 expert(Hash Layers 是 1 个;它的 MultiHash 变体是把每个 expert 的 FFN 切成 N 段,用 N 个 hash 各取一段拼成一个 FFN,计算量仍是一个 expert)
- 路由权重仍可学:Hash Layers 没有任何路由参数,V4 的 6 个 expert 之间仍由
sqrtsoftplus(linear(x))加权(§8.4) - 与学习路由混合:仅前 3 层固定,后续层仍学
这种"局部 hash + 整体学习"的组合,是 V4 在 MoE 路由上的工程取舍——想同时拿到 hash 的稳定性和学习的灵活性。
8.8 工程师常踩的 hash routing 陷阱
实现 hash routing 时容易踩的几个坑:
陷阱一:tid2eid 在不同 layer 之间不应该共享
# 错误写法
self.tid2eid = shared_tid2eid_global # 所有 hash 层共享
# 正确写法
self.tid2eid = nn.Parameter(torch.empty(vocab_size, n_activated, dtype=torch.int32), requires_grad=False)
每层的 384 个 expert 本来就是各自独立的模块,共享表的问题不在"走同一个 expert",而在于哪些 token 被分到同一组的分组方式在每层都一样——失去了"层级差异化"的好处。Hash Layers 论文做过这个对照(5 个 hash 层、每层 16 个 expert):各层用同一张表验证困惑度 23.74,各层各用一张表 23.21。V4 让每层有独立的 tid2eid,使同一个 token 在不同层可以有不同的分组(三张表是否真的不同见 §8.2 点三)。
陷阱二:tid2eid 的索引不应该有重复
# 错误写法
tid2eid[tid] = [0, 0, 1, 1, 2, 2] # 重复 expert 索引
# 正确写法
tid2eid[tid] = torch.randperm(n_routed)[:n_activated] # 保证唯一
如果一个 token 的 6 个 expert 索引有重复,结果会算错:Gate.forward 归一化时把重复 expert 的 score 算了两次,而 MoE.forward 里 torch.where(indices == i) 会同时匹配多个槽位,得到重复的行号,y[idx] += ... 在行号重复时只写入一次,重复槽位的贡献被丢掉(CPU 实跑:[0, 0, 1, 1, 2, 2] 各配权重 1/6,输出只累加到 0.5 而不是 1)。
陷阱三:tid2eid 的 dtype 必须能容纳 expert id 范围
int32 可以容纳 ~21 亿 expert——足够。但如果你写错成 int8,最大只能表示 127(最多 128 个 expert 编号),384 expert 的 V4 会出现索引溢出。源码用 dtype=torch.int32 是个保守且正确的选择。
陷阱四:gather 的文档写 LongTensor,查表出来的却是 int32
这一条是从 V4 源码本身读出来的,值得单独指出。Gate.forward 里:
indices = self.tid2eid[input_ids] # dtype = torch.int32
...
weights = original_scores.gather(1, indices) # 文档写 index 是 LongTensor
tid2eid 被声明成 dtype=torch.int32,查表结果自然也是 int32,而 torch.gather 的文档(torch 2.11 仍是如此)把参数写成 index (LongTensor)。学习路由那条分支没有这个问题——scores.topk(...)[1] 返回的就是 int64。
不过文档写法不等于实现限制:inference/requirements.txt 要求 torch>=2.10.0,在 CPU 版 torch 2.11.0 上实跑,int32 索引的 gather(含反向)、bincount、indices == i 都能正常工作,所以 pin 版代码的 hash 分支不会因为 dtype 报错(本机无 GPU,CUDA 路径未实测)。另一方面,model.py 末尾的自测入口用的是 args = ModelArgs(n_hash_layers=0),hash 分支在这个自测里根本没被执行到。如果你要在更老的 PyTorch 上复现,或者想让代码与文档口径一致,可以补一个 .long():
indices = self.tid2eid[input_ids].long()
int32 的选择本身是对的——384 个 expert 用 int32 存绰绰有余,而 [129280, 6] 的表用 int32 比 int64 省一半内存(3.1 MB vs 6.2 MB,乘以 3 层)。
陷阱五:nn.Parameter 包整数张量
nn.Parameter(torch.empty(..., dtype=torch.int32), requires_grad=False) 是合法的——PyTorch 只在 requires_grad=True 时要求浮点 dtype。这样写的好处是它进 state_dict()、随 checkpoint 走;register_buffer 也能做到同样的事。
8.9 动手实验:观察 hash routing 的 expert 利用率
import torch
# 模拟一个 hash 层的 expert 利用率分布
vocab_size = 32000
n_routed = 64
n_activated = 6
# 随机生成 tid2eid
torch.manual_seed(0)
tid2eid = torch.zeros(vocab_size, n_activated, dtype=torch.int64)
for tid in range(vocab_size):
tid2eid[tid] = torch.randperm(n_routed)[:n_activated]
# 模拟一段真实 token 频率(zipf 分布)
token_freqs = torch.zeros(vocab_size)
for tid in range(vocab_size):
token_freqs[tid] = 1.0 / (tid + 1)
token_freqs = token_freqs / token_freqs.sum()
# 计算每个 expert 的"接收 token 总数"
expert_load = torch.zeros(n_routed)
for tid in range(vocab_size):
for eid in tid2eid[tid]:
expert_load[eid] += token_freqs[tid]
# 输出 load 分布
print("Min/Max/Std:", expert_load.min().item(), expert_load.max().item(), expert_load.std().item())
print("Top 5 experts:", expert_load.topk(5).values)
print("Bottom 5 experts:", (-expert_load).topk(5).values * -1)
跑这段代码会看到:纯随机 tid2eid 会让 expert load 分布有显著长尾——CPU 实跑(seed 0)最重的 expert 负载约是最轻的 3.8 倍、约是均值的 2.3 倍,主因是 zipf 分布下排名第一的 token 一个就占了约 9% 的频率。Hash Layers 论文用 Random Hash 与 Balanced assignment 的桶频率对比(论文图 4)说明了同一件事。所以如果要均衡,就得用频率感知的生成方法;V4 实际用了哪种仍未公开(§8.5)。
8.10 Hash 路由的"自由度对账"
V4 的前 3 层用 Hash routing——表面上看是"放弃了路由学习"。但深入看会发现 V4 并没有真的损失自由度,只是把自由度从"路由"转移到了"权重"和"expert 内部"。把这个自由度对账算清楚:
自由度 1:哪个 expert 被激活——锁死
Hash 层把这个自由度交给 tid2eid 表——固定的、不学习。损失:模型不能根据上下文动态选 expert。
自由度 2:6 个 expert 的相对权重——保留
Gate 仍然用 sqrtsoftplus 算 score,作为 routing weight。这意味着同一个 token 的 6 个固定 expert 的"贡献程度"仍然是模型学习的——某个 expert 可能"重一点",另一个"轻一点"。这是被保留的自由度。
自由度 3:每个 expert 的 SwiGLU 内部表达——保留并强化
因为 Hash 让每个 expert 的"输入 token 类型分布"固定,每个 expert 可以专注地学 它接收到的 token 类型的处理方式——SwiGLU 的 w1/w2/w3 weight 收敛到这个固定输入分布的最优解。这种"输入稳定 → 学习更深"的强化效应让 expert 内部的表达自由度被更充分利用。
自由度 4:tid2eid 表的设计——一次性自由度
tid2eid 表的具体内容(哪个 token id → 哪 6 个 expert)是 V4 团队在训练前一次性决定的。这是一种"meta-自由度"——不是模型学的,但是设计者的设计选择。聚类、频率均衡等不同生成方法都是不同的自由度选择。
总账:V4 的前 3 层 Hash 路由把"路由动态自由度"换成了"expert 内部深度自由度"。表面看是"少学了",实际是"换了一种学法"——专注式学习而非分散式学习。
这种自由度对账是理解 V4 设计哲学的钥匙——V4 不是不要某些能力,而是把能力放在它最有效的位置。
8.11 Hash routing 的"工程惯性" 风险
Hash routing 在前 3 层带来的稳定性是把双刃剑——它也带来了一种工程惯性风险:tid2eid 表一旦确定,就很难再改。
具体风险:
风险 1:fine-tune 时无法调整 tid2eid
如果你 fine-tune V4 到某个特定领域(比如 V4-Code),新的 token 分布可能让"原 tid2eid 设计的均衡性"不再成立——某些 expert 可能在 fine-tune 数据上严重过载或闲置。但 tid2eid 是 requires_grad=False,不能通过 fine-tune 调整。
风险 2:词表扩展困难
如果未来要扩 V4 的词表(比如加新语言的 token),新 token 的 tid2eid 表条目需要"凭空设计"——没有对应的训练数据指导。一种做法是用聚类把新 token 映射到最相似的旧 token,借用其 tid2eid。
风险 3:合并不同 V4 fine-tune 版本困难
如果两个 fine-tune 版本(比如 V4-Code 和 V4-Math)想合并成一个统一模型——它们的 expert weight 已经分化到不同方向。Hash routing 的固定结构限制了这种合并的灵活性——不像学习路由那样可以"用新 routing 把两个 expert 集合智能整合"。
可以做的:tid2eid 虽然 requires_grad=False,但它是 state_dict 里一个普通的整数张量——没有任何机制阻止你直接改写它。领域微调时,如果统计出某几个 expert 在新数据上严重过载,把它们在高频 token 上的条目换成闲置 expert 是技术上可行的。代价是这些 expert 从没在这类 token 上训练过,改完必须跟一轮微调让它们补课。本专栏没有做过这个实验,只是指出它在结构上是可行的。
不要做的:不要指望通过反向传播调它。整数索引不可微,requires_grad=False 也堵死了这条路。
8.12 Hash routing 会不会伤到某些 token
这一节回答一个读者一定会问、但没有公开数据可以回答的问题:某个 token 被 hash 分到"不合适"的 expert,会不会导致它的表现变差?
先把可以确定的部分说清楚:
前 3 层的输出不是最终输出。 一个 token 在前 3 层被送去哪 6 个 expert 是写死的,但它还要过 58 层学习路由的 MoE 和 61 层注意力。前 3 层的作用更接近"一次固定的特征展开",而不是"决定这个 token 的命运"。
hash 层仍然有可学的权重。 §8.4 讲过,6 个 expert 是固定的,但它们的相对权重来自 sqrtsoftplus(linear(x))——模型可以学会"对这个 token,这 6 个里主要用第 2 个"。也就是说,即使分配不理想,模型仍有一层缓冲。
expert 会向它收到的 token 靠拢,而不是相反。 这是最关键的一点:tid2eid 固定之后,expert 的 SwiGLU 权重是在"我固定收到这批 token"的前提下训练的。所以不存在"token 被分到不擅长它的 expert"——expert 的擅长方向本身就是被这张表定义出来的。这也是为什么表的具体内容(随机 / 频率均衡 / 聚类)影响的是训练效率和负载均衡,而不是"配对是否正确"。
真正会受影响的是两类情况,且都可以从结构上推出来:
- 训练时几乎没出现过的 token——它对应的那 6 个 expert 在这个 token 上没得到过梯度。这与 hash 路由无关,学习路由同样如此。
- 词表扩展后新增的 token——它们的
tid2eid条目是凭空填的,对应的 expert 从未见过它们(见 §8.11 风险 2)。
至于"代码任务受益、小语种受损"这类说法,需要在真实模型上做消融实验才能下结论。本专栏不给没有实验支撑的任务级判断。
8.13 Hash routing 与"模型可解释性"
Hash routing 给 V4 带来一个意外的副作用:部分可解释性。
传统学习路由的 expert 选择是"黑盒"——你只能事后观察"哪个 token 被分到哪个 expert",但不知道"为什么"。Hash routing 是"白盒"——你可以提前知道"这个 token id 会被分到哪 6 个 expert",因为 tid2eid 表是固定的。
这种部分可解释性带来几个具体好处:
好处 1:调试时可以"溯源"
如果模型在某个特定 token 上输出异常,可以查 tid2eid 表看它经过的前 3 层 expert——如果某个 expert 已知"在某些任务上有问题",就能定位问题源。
好处 2:可以"按 expert 修补"
发现某个 expert 输出问题后,可以做"局部 fine-tune"——只更新该 expert 的 SwiGLU 权重,不动其他。Hash routing 让这种局部修补可行——其他 expert 的 token 分配不会因为该 expert 的权重变化而改变。
好处 3:可以做"expert 单元测试"
每个 expert 接收的 token 类别在 Hash 路由层是固定的——可以为每个 expert 写"单元测试" prompt,验证它在自己擅长的领域上输出正确。这种测试粒度细到单个 expert,比"整个模型一起测"细得多。
好处 4:与"模型审计" 的关系
某些合规场景需要解释"模型为什么这样输出"。Hash routing 让前 3 层的解释变成"查表" —— 监管者可以理解。学习路由的解释需要 attribution 算法(如 LIME / SHAP),复杂得多。
这种可解释性不是 V4 设计的初衷——但它是 Hash routing 的"副产品"。在合规重要的行业(金融 / 医疗 / 法律),这个副产品可能比性能更重要。
8.14 延伸阅读
- Hash Layers 论文(arXiv:2106.04426,Roller et al., NeurIPS 2021):按 token id 固定路由的代表性工作,技术报告引用的就是它
- DeepSeekMoE 论文(arXiv:2401.06066):细粒度 + shared expert 的源头
- Expert Choice 论文(arXiv:2202.09368):expert 主动选 token 的反向路由
- 本专栏第 7 章:学习 routing 的 sqrtsoftplus + noaux_tc
- 本专栏第 9 章:Expert 内部如何处理来自 hash 路由的 token
8.15 本章小结
- V4 前 3 层用 hash routing:直接查表,每 token 6 个固定 expert
- 这个设计对应 MoE 文献里长期讨论的"早期层路由塌陷"问题——但 DeepSeek 没有公开说明动机,本专栏把它标为工程推断
tid2eid是预计算的常量表([129280, 6]int32,inference/model.py:559),每层各建一份,requires_grad=False;表的生成方法官方未公开- 源码里有一处值得注意:
tid2eid查表得到 int32,而gather文档写的是 LongTensor——pin 要求的 torch≥2.10 实际接受 int32(CPU 实测),自测入口则用n_hash_layers=0根本没走这条分支(见 §8.8 陷阱四) - hash 层仍然有 sqrtsoftplus score——决定 6 个 expert 的相对权重,但不决定选谁
- "静态选 expert + 动态选权重"的两段式设计是 V4 hash routing 的工程精髓
- 与 Hash Layers 相比,V4 的 hash 是每 token 6 个 expert + 路由权重仍可学 + 与学习层混合的大规模版本;Hash Layers 的实验结论是 hash 层放在较后的层更好,V4 放在最前 3 层的理由官方未公开
第 9 章我们进入 Expert 类本身——SwiGLU + clip + 容量平衡的 384 专家工程。