第8章 384 专家与 Hash 路由:前 3 层为什么不学
“Sometimes the simpler thing isn’t worse—it’s the right thing. Especially in MoE.” —— 引自一位资深 MoE 训练工程师
Hash 路由不是 V4 偷懒,是 V4 在 V3 的训练曲线里看到了”前几层路由学不动”的真相。
8.1 引子:MoE 路由学习的”早期失败”
V4 的 config.json 里有这么一行:
"num_hash_layers": 3
意思是:前 3 层 MoE 的路由不学,直接由 token id 查表决定。
这个设计在第一眼看时是反直觉的——MoE 的核心价值不就是”学到 token 与 expert 的最优配对”吗?为什么要把前 3 层的”学习”放弃,换成预定的查表?
答案藏在 V2 / V3 的训练曲线里。MoE 训练有一个反复出现的工程教训:
- 早期层的 hidden state 还非常接近 token embedding——语义层级很低,主要是字面 token 信息
- 早期层的路由器很难”学到差异化”——所有 token 都长得差不多,gate 输出的 score 区分度很小
- 早期层的路由器训练通常会陷入”塌陷”——少数几个 expert 主导,剩下的 expert 闲置、训练不动
- 后期再用 noaux_tc 调 bias 反而打乱已经学到的路由分布——bias 抬高的 expert 突然被大量 token 分给,但这些 expert 之前没怎么训练过,输出质量差
这种”早期路由学不动”的现象在 V2 / V3 / V3.2-Exp 训练中反复出现。V4 的解决方案是:前 3 层不学,直接 hash。
flowchart TB
subgraph 历史问题
direction TB
H1["V2/V3 训练早期层"]
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 都有自己的一份——前 3 层共有 3 份不同的 tid2eid 表。这意味着:同一个 token 在第 0 层、第 1 层、第 2 层走的 expert 路径是不同的。
点四:用 nn.Parameter 包装但 requires_grad=False:这是 PyTorch 的常见模式——让这个张量随模型一起被序列化(保存到 checkpoint),但不更新。
forward 里使用:
if self.hash:
indices = self.tid2eid[input_ids]
input_ids: [B, S],索引 tid2eid 后输出 [B, S, n_activated_experts]——直接得到每个 token 的 6 个 expert 索引。完全跳过”基于 hidden state 学习路由”的过程。
8.2·补 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: [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,color:#cbd5e1 classDef learn fill:#312e81,stroke:#a78bfa,color:#ede9fe class TidLookup,FixIdx static class Score,Gather,Norm learn
灰色节点是静态部分(tid2eid 查表 → 固定 expert 索引),紫色节点是可学习部分(score 投影 → 权重归一化)。两段式架构在图上一目了然。
8.3 hash 路由的”伪学习”机制
虽然 hash 层不学 routing,但 V4 的 Gate 仍然计算 score——只是 score 只用于 routing weight,不用于 expert 选取:
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.4 tid2eid 表的”生成方法”
V4 的源码里 tid2eid 是 torch.empty(...) 初始化——意味着权重必须从 checkpoint 加载,源码不包含生成逻辑。但从 DeepSeek 公开技术报告可以推测出几种可能的生成方法:
方法一:随机生成(最简)
# 给每个 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 分散到更多 expert,低频 token 集中
# 让每个 expert 的"总训练 token 数"接近
这种做法让训练更均衡,但实现复杂——需要先在大规模语料上统计 token 频率。
方法三:基于聚类
# 用一个小型预训练模型给每个 token 算 embedding
# 对 embedding 聚类成 384 簇,每个 token 取最近的 6 个簇
这种做法让 expert 之间有”语义意义”——簇 0 的 expert 可能专门处理某类语义的 token。
V4 实际用了哪种生成方法没有公开。但从 V4 的训练稳定性可以推测:至少不是纯随机。
8.5 hash 路由与学习路由的协同
V4 是 hash 层(前 3 层)+ 学习层(后 58 层)的混合。这种混合架构有两个关键的工程效应:
效应一:训练梯度的”温度调节”
前 3 层用 hash 路由后,每个 expert 的”输入 token 分布”是固定的——它在训练全程接收的 token 类别一致。这让前 3 层的 expert 训练非常稳定——每个 expert 学到的”擅长处理什么”是清晰的。
后 58 层的学习路由可以基于这个”已经稳定” 的 hidden state 学到差异化的路由——不会陷入”早期层干扰晚期路由”的循环。
效应二:路由学习的”启动温度”
learning routing 的训练通常需要一个”warm-up” 阶段——前 N 步全部 expert 被均匀使用,然后逐渐让模型自己选 expert。V4 的 hash 层相当于把这个 warm-up 永久化——前 3 层永远均匀(按 hash 表)使用所有 expert,给后续层提供稳定的 hidden state。
这种”前几层固定路由 + 后几层学习路由”的设计在 MoE 领域有先例——Hash Layers 论文(arXiv:2106.04426)最早提出了这套思路。V4 把它工业化到 384 expert + 1.6T 规模。
8.6 与其他 MoE 模型的预定路由对比
把 V4 的 hash 路由与其他 MoE 模型的”非学习路由”做对比:
| 方案 | 路由依据 | 是否学习 | 应用模型 |
|---|---|---|---|
| Switch Transformer 原始 | softmax(x · W) | 学习 | Switch |
| Hash Layers | hash(token id) % E | 不学习 | 早期 MoE 实验 |
| Random Routing | 随机 | 不学习 | 实验性 |
| Position Hash | hash(position) % E | 不学习 | 部分变种 |
| V4 hash | tid2eid[token_id] | 不学习 | DeepSeek V4 前 3 层 |
| Expert Choice | expert 主动选 token | 学习 | EC-MoE |
| StableMoE | 学习 + 滑动窗口约束 | 部分学习 | StableMoE |
V4 的 hash 路由相比”hash(token id) % E”的简单方案有几个改进:
- 多 expert 路由:每 token 6 个 expert(而非 1 个)
- 可定制表:tid2eid 不是简单的 mod 运算,可以基于聚类或频率定制
- 与学习路由混合:仅前 3 层固定,后续层仍学
这种”局部 hash + 整体学习”的组合,是 V4 在 MoE 路由领域的工程创新——既拿到 hash 的稳定性,又保留学习的灵活性。
8.7 工程师常踩的 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)
如果所有 hash 层共享一个 tid2eid,每个 token 在每层都走相同的 expert——失去了”层级差异化”的好处。V4 让每层有独立的 tid2eid,让同一个 token 在不同层走不同的 expert。
陷阱二:tid2eid 的索引不应该有重复
# 错误写法
tid2eid[tid] = [0, 0, 1, 1, 2, 2] # 重复 expert 索引
# 正确写法
tid2eid[tid] = torch.randperm(n_routed)[:n_activated] # 保证唯一
如果一个 token 的 6 个 expert 索引有重复,gate 输出会算错——indices == i 的 mask 会同时匹配多个槽位,导致权重重复加。
陷阱三:tid2eid 的 dtype 必须能容纳 expert id 范围
int32 可以容纳 ~21 亿 expert——足够。但如果你写错成 int8,最多容纳 127 个 expert,384 expert 的 V4 会出现索引溢出。源码用 dtype=torch.int32 是个保守且正确的选择。
陷阱四:tid2eid 必须随 checkpoint 一起保存
requires_grad=False 但仍是 nn.Parameter——这意味着 PyTorch 会把它包含在 state_dict() 里。如果你自己实现时写成 register_buffer,PyTorch 会把它当作 buffer,behavior 略有不同(buffer 默认会保存,但有些 distillation 库会跳过 buffer 复制)。V4 的写法是最稳妥的。
8.8 动手实验:观察 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 分布有显著长尾——某些 expert 接收的 token 量是其他的 2-3 倍。这就是为什么 V4 实际生成 tid2eid 时不会用纯随机,而是用某种均衡化算法。
8.8·补 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.8·补·补 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 集合智能整合”。
应对:V4 团队大概率在 V5 引入”可微 Hash”——把 tid2eid 表设计成可以软更新的形式(比如基于 token embedding 的最近邻查询),既保留 Hash 路由的稳定性,又解决”惯性”风险。
理解这个风险让你在使用 V4 时更清醒——知道它在某些场景下不灵活,从而做对部署决策。
8.9 延伸阅读
- Hash Layers 论文(arXiv:2106.04426):hash routing 的最早期理论
- DeepSeekMoE 论文(arXiv:2401.06066):细粒度 + shared expert 的源头
- Expert Choice 论文(arXiv:2202.09368):expert 主动选 token 的反向路由
- 本书第 7 章:学习 routing 的 sqrtsoftplus + noaux_tc
- 本书第 9 章:Expert 内部如何处理来自 hash 路由的 token
8.9·补 Hash routing 在不同任务上的表现差异
V4 的前 3 层 Hash routing 对模型的整体性能有正面贡献,但在某些特定任务上有差异化的影响。把这些差异列出来:
任务 A:通用聊天
Hash routing 对通用聊天几乎透明 —— 用户感知不到差异。原因:聊天任务的 token 多样性高,Hash 表的”按 token id 分配 expert” 与”理想路由”几乎一致。
任务 B:代码生成
Hash routing 对代码任务略有正面影响。原因:代码 token(关键字、操作符等)通常 token id 集中,Hash 分配让”代码相关 expert” 在前 3 层固定接收代码 token——为后续学习路由层提供更稳定的代码 hidden state。
任务 C:数学推理
Hash routing 对数学中性。原因:数学 token 多样性中等,Hash 表的随机分配既不特别有利、也不特别有害。
任务 D:稀有语言(小语种)
Hash routing 对小语种可能略有负面影响。原因:小语种的 token 在 Hash 表上可能被分到”擅长其他语言”的 expert——前 3 层的路径不利于小语种的 hidden 表达。但后 58 层的学习路由可以补偿。
任务 E:领域 fine-tune
Hash routing 对 fine-tune 有约束作用。如果你 fine-tune 到极特殊领域(如医学 / 法律),前 3 层的固定路由可能让 fine-tune 受限——某些”医学专属 expert” 在前 3 层不被激活。fine-tune 时这部分的能力提升受限。
总结:Hash routing 对通用任务有利、对特殊领域 fine-tune 略有损失。这是 V4 设计的工程权衡——选择对大多数用户有利的路径。
8.9·补·补 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,验证它在自己擅长的领域上输出正确。这种测试粒度比”整个模型一起测” 细 384 倍。
好处 4:与”模型审计” 的关系
某些合规场景需要解释”模型为什么这样输出”。Hash routing 让前 3 层的解释变成”查表” —— 监管者可以理解。学习路由的解释需要 attribution 算法(如 LIME / SHAP),复杂得多。
这种可解释性不是 V4 设计的初衷——但它是 Hash routing 的”副产品”。在合规重要的行业(金融 / 医疗 / 法律),这个副产品可能比性能更重要。
8.10 本章小结
- V4 前 3 层用 hash routing:直接查表,每 token 6 个固定 expert
- 这个设计回应了 V2 / V3 训练里”早期层路由学不动”的工程教训
tid2eid是预计算的常量表,每层独立,不参与梯度更新- hash 层仍然有 sqrtsoftplus score——决定 6 个 expert 的相对权重,但不决定选谁
- “静态选 expert + 动态选权重”的两段式设计是 V4 hash routing 的工程精髓
- 与 Hash Layers 等先前工作相比,V4 的 hash 是 multi-expert + per-layer 独立 + 与学习层混合的工业化版本
第 9 章我们进入 Expert 类本身——SwiGLU + clip + 容量平衡的 384 专家工程。
评论 0
还没有评论,来说两句吧。
评论加载失败,刷新重试。