CH 12 / SOURCE EXAMPLE
moe_ranking.py
ch12 精排模型/code/ch04_attention/moe_ranking.py
这个示例讲什么
│ └── moe_ranking.py MoE 精排:Top-K 稀疏路由 + 负载均衡损失
MoE 精排模型(第12章 §12.4.3.7、§12.4.3.8) 用稀疏专家混合替换 Transformer 中的前馈网络(FFN): MoE(x) = Σ_i g_i(x) · E_i(x) 1. 稠密 MoE(所有专家都参与): g_i(x) = softmax(W_r x)_i 计算量随专家数线性增长,论文指出这不是稀疏化的初衷。 2. Top-K 稀疏路由(§12.4.3.7): r = W_r x p = softmax(r) 𝒯_K = TopK(p, K) # 只激活 K 个专家 MoE(x) = Σ_{i∈𝒯_K} g̃_i · E_i(x), g̃_i = p_i / Σ_{j∈𝒯_K} p_j 计算量只与
上面来自章节 README 或源码自身的说明,不是自动验证的运行结果,也不能代替 PDF 的书中论断。讲解时请结合实际代码、输入输出与原书页码。
源码结构与阅读入口
_auc· 第 49 行TopKRouter· 第 60 行Top-K 稀疏路由:r = W_r x → softmax → 取 Top-K → 在 K 个专家内重新归一化。MoEFFN· 第 84 行Top-K 路由专家 + 共享专家。MoERanker· 第 177 行演示用的极简结构:嵌入 → 投影到 d_model → MoE 层 → 输出 logit。make_data· 第 227 行特征交互型标签:让不同样本落在特征空间的不同区域,
1"""2MoE 精排模型(第12章 §12.4.3.7、§12.4.3.8)34用稀疏专家混合替换 Transformer 中的前馈网络(FFN):56 MoE(x) = Σ_i g_i(x) · E_i(x)78 1. 稠密 MoE(所有专家都参与):9 g_i(x) = softmax(W_r x)_i10 计算量随专家数线性增长,论文指出这不是稀疏化的初衷。1112 2. Top-K 稀疏路由(§12.4.3.7):13 r = W_r x14 p = softmax(r)15 𝒯_K = TopK(p, K) # 只激活 K 个专家16 MoE(x) = Σ_{i∈𝒯_K} g̃_i · E_i(x), g̃_i = p_i / Σ_{j∈𝒯_K} p_j17 计算量只与 K 有关,与专家总数解耦。1819 3. 共享专家(§12.4.3.8,DeepSeekMoE):20 MoE(x) = Σ_{i=1}^{K_s} E_i^shared(x) + Σ_{j∈ℛ(x)} g_j · E_j^routed(x)21 共享专家对所有 token 都激活,负责捕获通用知识;22 路由专家负责捕获差异化知识,减少专家间的参数冗余。2324 4. 负载均衡损失(§12.4.3.8):25 专家级:L_ExpBal = α₁ · N · Σ_i f_i · P_i26 f_i = 分配到专家 i 的 token 占比,P_i = 专家 i 的平均路由概率27 设备级:L_DevBal = α₂ · D · Σ_d f'_d · P'_d28 (按设备对专家分组,避免单个设备成为瓶颈)2930 5. 工程约束(§12.4.3.7 正文):31 MoE 引入的通信开销与负载不均,往往是「稀疏化省下的算力」32 被抵消的主要原因,因此负载均衡损失不是可有可无的正则项。3334本文件的对照实验直接检验上述两点:35 - 去掉负载均衡损失,观察专家利用率是否塌缩到少数专家;36 - 加上专家级 / 设备级均衡损失,观察利用率与设备分配是否变均匀。3738本书依据:《第十二章-精排模型.pdf》§12.4.3.7–§12.4.3.8。39依赖:PyTorch。运行:python3 moe_ranking.py40"""4142import random43import torch44import torch.nn as nn45import torch.nn.functional as F46import torch.optim as optim474849def _auc(scores, labels) -> float:50 pos = [s for s, y in zip(scores, labels) if y == 1]51 neg = [s for s, y in zip(scores, labels) if y == 0]52 if not pos or not neg:53 return 0.554 return sum(1.0 if p > n else 0.5 if p == n else 0.055 for p in pos for n in neg) / (len(pos) * len(neg))565758# ── 路由层 ───────────────────────────────────────────────────────────────────5960class TopKRouter(nn.Module):61 """62 Top-K 稀疏路由:r = W_r x → softmax → 取 Top-K → 在 K 个专家内重新归一化。6364 返回:65 topk_idx : (B, K) 被选中的专家下标66 gates : (B, K) 归一化后的门控权重67 probs : (B, N) 完整路由分布(用于计算负载均衡的 P_i)68 """6970 def __init__(self, d_model: int, n_experts: int, top_k: int):71 super().__init__()72 self.top_k = top_k73 self.gate = nn.Linear(d_model, n_experts, bias=False)7475 def forward(self, x: torch.Tensor):76 probs = F.softmax(self.gate(x), dim=-1) # (B, N)77 topk_p, topk_idx = torch.topk(probs, self.top_k, dim=-1)78 gates = topk_p / topk_p.sum(dim=-1, keepdim=True) # 重新归一化79 return topk_idx, gates, probs808182# ── MoE 前馈层 ───────────────────────────────────────────────────────────────8384class MoEFFN(nn.Module):85 """86 Top-K 路由专家 + 共享专家。8788 参数:89 d_model : 输入/输出维度90 d_ff : 每个专家的隐藏维度91 n_routed : 路由专家数(细粒度切分时取较大值,单个专家较窄)92 top_k : 每个 token 激活的路由专家数93 n_shared : 共享专家数(DeepSeekMoE 做法,对所有 token 激活)94 n_device : 设备数,用于设备级负载均衡损失95 """9697 def __init__(self, d_model: int, d_ff: int, n_routed: int = 8,98 top_k: int = 2, n_shared: int = 1, n_device: int = 2):99 super().__init__()100 self.n_routed = n_routed101 self.n_shared = n_shared102 self.n_device = n_device103104 self.router = TopKRouter(d_model, n_routed, top_k)105 self.routed_experts = nn.ModuleList([106 nn.Sequential(nn.Linear(d_model, d_ff), nn.GELU(),107 nn.Linear(d_ff, d_model))108 for _ in range(n_routed)])109 self.shared_experts = nn.ModuleList([110 nn.Sequential(nn.Linear(d_model, d_ff), nn.GELU(),111 nn.Linear(d_ff, d_model))112 for _ in range(n_shared)])113 # 残差投影:与 Transformer FFN 的位置对齐114 self.out_proj = nn.Linear(d_model, d_model)115116 def _device_of(self, expert_idx: torch.Tensor) -> torch.Tensor:117 """把专家下标平均分到 n_device 个设备上。"""118 return expert_idx * self.n_device // self.n_routed119120 def forward(self, x: torch.Tensor):121 """122 x : (B, d_model)123 返回: y (B, d_model), stats(dict) 供计算负载均衡损失与诊断124 """125 B, _ = x.shape126 topk_idx, gates, probs = self.router(x) # (B,K), (B,K), (B,N)127128 # 路由专家:逐专家处理被分配给它的 token(实现上是掩码 + 累加,129 # 与真实系统中按 token 重排(permutation)后分组计算等价)130 y = torch.zeros_like(x)131 for k in range(topk_idx.size(1)):132 idx_k = topk_idx[:, k] # (B,)133 gate_k = gates[:, k].unsqueeze(-1) # (B,1)134 for e in range(self.n_routed):135 mask = (idx_k == e).unsqueeze(-1) # (B,1)136 if not mask.any():137 continue138 out_e = self.routed_experts[e](x) # (B, d_model)139 y = y + torch.where(mask, gate_k * out_e,140 torch.zeros_like(out_e))141142 # 共享专家:所有 token 都激活143 for e in self.shared_experts:144 y = y + e(x)145146 y = self.out_proj(y)147148 # ── 负载均衡统计 ─────────────────────────────────────────────────────149 # f_i:分配到专家 i 的 token 占比150 one_hot = F.one_hot(topk_idx, self.n_routed).float().sum(dim=1) # (B,N)151 f = one_hot.mean(dim=0) # (N,)152 # P_i:专家 i 的平均路由概率153 P = probs.mean(dim=0) # (N,)154 # 设备级:把专家按设备分组聚合155 dev = self._device_of(torch.arange(self.n_routed))156 f_dev = torch.zeros(self.n_device, device=x.device)157 P_dev = torch.zeros(self.n_device, device=x.device)158 f_dev.index_add_(0, dev, f)159 P_dev.index_add_(0, dev, P)160161 stats = dict(f=f, P=P, f_dev=f_dev, P_dev=P_dev,162 top1_share=one_hot[:, topk_idx[:, 0]].mean().item())163 return y, stats164165 # ── 负载均衡损失 ─────────────────────────────────────────────────────────166 def balance_loss(self, stats, alpha_exp: float = 0.01,167 alpha_dev: float = 0.01) -> torch.Tensor:168 N = self.n_routed169 L_exp = N * (stats["f"] * stats["P"]).sum()170 D = self.n_device171 L_dev = D * (stats["f_dev"] * stats["P_dev"]).sum()172 return alpha_exp * L_exp + alpha_dev * L_dev173174175# ── 单个 MoE 层上的精排模型 ──────────────────────────────────────────────────176177class MoERanker(nn.Module):178 """179 演示用的极简结构:嵌入 → 投影到 d_model → MoE 层 → 输出 logit。180181 只用一层 MoE,是为了让路由行为成为唯一的观察对象;182 真实系统中的 MoE 位于 Transformer 的每个 FFN 位置。183 """184185 def __init__(self, vocab: int, n_fields: int, d_model: int = 16,186 d_ff: int = 16, n_routed: int = 8, top_k: int = 2,187 n_shared: int = 1, n_device: int = 2):188 super().__init__()189 self.emb = nn.Embedding(vocab, d_model)190 self.n_fields = n_fields191 self.proj = nn.Linear(d_model * n_fields, d_model)192 self.moe = MoEFFN(d_model, d_ff, n_routed, top_k, n_shared, n_device)193 self.norm = nn.LayerNorm(d_model)194 self.head = nn.Linear(d_model, 1)195196 def forward(self, feat_ids: torch.Tensor):197 e = torch.cat([self.emb(feat_ids[:, i])198 for i in range(self.n_fields)], dim=1)199 h = F.gelu(self.proj(e)) # (B, d_model)200 y, stats = self.moe(h)201 h = self.norm(h + y) # 残差 + LayerNorm202 return self.head(h).squeeze(-1), stats203204 def fit(self, feat_ids, y, use_balance: bool, epochs: int = 40,205 lr: float = 5e-3):206 opt = optim.Adam(self.parameters(), lr=lr, weight_decay=1e-4)207 for ep in range(epochs):208 opt.zero_grad()209 logit, stats = self.forward(feat_ids)210 loss = F.binary_cross_entropy_with_logits(logit, y.float())211 if use_balance:212 loss = loss + self.moe.balance_loss(stats)213 loss.backward()214 opt.step()215 if (ep + 1) % 20 == 0:216 print(f" epoch {ep+1:3d} loss={loss.item():.4f}")217 return loss.item()218219 def predict_proba(self, feat_ids):220 with torch.no_grad():221 logit, _ = self.forward(feat_ids)222 return torch.sigmoid(logit)223224225# ── 合成数据 ─────────────────────────────────────────────────────────────────226227def make_data(n: int = 3000, vocab: int = 16, n_fields: int = 6,228 seed: int = 42):229 """230 特征交互型标签:让不同样本落在特征空间的不同区域,231 从而为「不同专家负责不同区域」提供存在的理由。232233 注意:真实系统中专家分工是被数据自然诱导出来的;234 本文件不人为构造「专家分工」,只观察路由是否出现塌缩。235 """236 torch.manual_seed(seed)237 random.seed(seed)238239 feat = torch.randint(0, vocab, (n, n_fields)).float()240 logit = (0.35 * feat[:, 0] + 0.30 * feat[:, 1]241 - 0.25 * feat[:, 2]242 + 0.20 * (feat[:, 3] * feat[:, 4]).sin()243 + 0.15 * (feat[:, 5] > 8).float()244 - 1.2)245 y = (torch.sigmoid(logit / 3.0 + 0.4 * torch.randn(n)) > 0.5).long()246 return dict(feat=feat.long(), y=y)247248249# ── 主演示 ────────────────────────────────────────────────────────────────────250251if __name__ == "__main__":252 random.seed(42)253 torch.manual_seed(42)254255 VOCAB, N_FIELDS = 16, 6256 data = make_data(3000, vocab=VOCAB, n_fields=N_FIELDS)257 split = 2400258 tr = {k: v[:split] for k, v in data.items()}259 te = {k: v[split:] for k, v in data.items()}260261 def build():262 torch.manual_seed(42)263 return MoERanker(VOCAB, N_FIELDS, d_model=16, d_ff=16,264 n_routed=8, top_k=2, n_shared=1, n_device=2)265266 def evaluate(model):267 p = model.predict_proba(te["feat"])268 return _auc(p.tolist(), te["y"].tolist())269270 def show_util(model, tag):271 with torch.no_grad():272 _, stats = model.forward(te["feat"])273 f = stats["f"].tolist()274 fd = stats["f_dev"].tolist()275 print(f" {tag}")276 print(" 专家利用率 f_i: " +277 " ".join(f"E{i}={v:.3f}" for i, v in enumerate(f)))278 print(f" 最大/最小利用率比 = {max(f) / max(min(f), 1e-6):.2f}"279 f" 分布标准差 = {torch.tensor(f).std().item():.4f}")280 print(" 设备 token 占比 f'_d: " +281 " ".join(f"Dev{d}={v:.3f}" for d, v in enumerate(fd)))282283 # ── 对照组:无负载均衡损失 ───────────────────────────────────────────────284 print("=== 对照组:只优化主任务(无负载均衡损失)===")285 m_free = build()286 m_free.fit(tr["feat"], tr["y"], use_balance=False, epochs=40)287 auc_free = evaluate(m_free)288 show_util(m_free, "路由利用情况:")289290 # ── 实验组:加入专家级 + 设备级负载均衡损失 ───────────────────────────────291 print("\n=== MoE:主任务 + 专家级 / 设备级负载均衡损失 ===")292 m_bal = build()293 m_bal.fit(tr["feat"], tr["y"], use_balance=True, epochs=40)294 auc_bal = evaluate(m_bal)295 show_util(m_bal, "路由利用情况:")296297 # ── 对比结论 ─────────────────────────────────────────────────────────────298 print(f"\n对比(正文 §12.4.3.8):")299 print(f" 无均衡损失 AUC={auc_free:.4f}")300 print(f" 有均衡损失 AUC={auc_bal:.4f}")301 print(" 正文要点:Top-K 路由天然倾向于把 token 集中到少数「强者」专家,")302 print(" 这会让其余专家参数闲置,并在分布式训练中造成设备负载倾斜;")303 print(" 负载均衡损失是让稀疏化真正兑现「算力与专家数解耦」的必要条件。")304305 # ── 共享专家的作用 ───────────────────────────────────────────────────────306 print("\n共享专家 vs 路由专家(正文 §12.4.3.8):")307 print(" DeepSeekMoE 让共享专家对所有 token 激活,捕获通用知识;")308 print(" 路由专家只对部分 token 激活,捕获差异化知识。")309 n_shared_p = sum(p.numel() for p in m_bal.moe.shared_experts.parameters())310 n_routed_p = sum(p.numel() for p in m_bal.moe.routed_experts.parameters())311 print(f" 共享专家参数={n_shared_p} 路由专家总参数={n_routed_p}"312 f" 每个 token 实际激活的路由专家={m_bal.moe.router.top_k} / "313 f"{m_bal.moe.n_routed}")314315 # ── 精排推理演示 ─────────────────────────────────────────────────────────316 p = m_bal.predict_proba(te["feat"][:20]).tolist()317 top5 = sorted(enumerate(p), key=lambda t: -t[1])[:5]318 print("\n精排 Top-5(候选集 20 条):")319 for rank_i, (idx, s) in enumerate(top5, 1):320 print(f" #{rank_i} item={idx:2d} p_click={s:.4f} "321 f"label={te['y'][idx].item()}")如何用于学习或编写新代码
- 先看章节 README 的依赖和运行成本,定位这个例子的输入、输出及关键函数。
- 在原书 PDF 中找到对应概念的具体页;代码演示与书中论断分开标注。
- 只在依赖和耗时可控时运行单个示例,修改一个参数并记录真实输出与失败情形。
- 参考实现新代码时说明复用、改动、测试与尚未完成的生产依赖;教学示例并非生产系统。