DeepSeek V4 源码剖析

第8章 384 专家与 Hash 路由:前 3 层为什么不学

作者 杨艺韬 · 6,224 字 · 发布于 · 更新于

"num_hash_layers": 3 —— DeepSeek-V4-Pro config.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 集合一边在变。下面几条沿这个方向展开,其中关于"早期层"的部分是本专栏的推断,放在这里作为工程推断而非官方解释:

  1. 早期层的 hidden state 还非常接近 token embedding——语义层级很低,主要是字面 token 信息
  2. 早期层的路由器很难"学到差异化"——所有 token 都长得差不多,gate 输出的 score 区分度很小
  3. 早期层的路由器训练通常会陷入"塌陷"——少数几个 expert 主导,剩下的 expert 闲置、训练不动
  4. 后期再用 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 的擅长方向本身就是被这张表定义出来的。这也是为什么表的具体内容(随机 / 频率均衡 / 聚类)影响的是训练效率和负载均衡,而不是"配对是否正确"。

真正会受影响的是两类情况,且都可以从结构上推出来:

  1. 训练时几乎没出现过的 token——它对应的那 6 个 expert 在这个 token 上没得到过梯度。这与 hash 路由无关,学习路由同样如此。
  2. 词表扩展后新增的 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 专家工程。