nowordsxiaomu's picture
Initial release: DeepSeek-Flash-Mini nano (15M MoE, MLA+MTP)
5e6d9f5 verified
Raw
History Blame Contribute Delete
6.71 kB
"""DeepSeekMoE:细粒度专家 + 共享专家 + 无辅助损失负载均衡。
三个和普通 MoE 不一样的地方:
1. 细粒度专家(fine-grained):把专家切得更小更多,top-k 也调大。
同样的激活参数量,组合数从 C(8,2) 变成 C(64,8),专家分工能更专一。
2. 共享专家(shared expert):抽出 1 个专家让所有 token 都过。
通用知识(语法、常识)由它承担,路由专家就不必人手一份重复学。
3. 无辅助损失负载均衡(aux-loss-free,V3 的关键改进):
传统做法是加一个负载均衡损失,但它和语言建模目标是打架的,会损伤效果。
V3 改成给每个专家挂一个**不参与梯度**的偏置 b_i,只在 top-k 选择时加上:
选择依据 = s_i + b_i, 聚合权重仍用原始 s_i
每步训练后,谁超载就把谁的 b_i 调小,谁欠载就调大:
b_i += γ * sign(平均负载 - 该专家负载)
于是负载被掰平了,而梯度完全没被污染。
"""
from typing import Tuple
import torch
import torch.nn as nn
import torch.nn.functional as F
from .layers import SwiGLU
class Gate(nn.Module):
"""路由门控:决定每个 token 该交给哪几个专家。"""
def __init__(self, cfg):
super().__init__()
self.dim = cfg.dim
self.n_experts = cfg.n_routed_experts
self.topk = cfg.n_activated_experts
self.n_groups = cfg.n_expert_groups
self.topk_groups = cfg.n_limited_groups
self.score_func = cfg.score_func
self.route_scale = cfg.route_scale
self.aux_alpha = cfg.aux_loss_alpha
self.weight = nn.Parameter(torch.empty(self.n_experts, self.dim))
nn.init.normal_(self.weight, std=0.02)
# 负载均衡偏置:只影响"选谁",不参与反向传播
self.register_buffer("expert_bias", torch.zeros(self.n_experts))
# 统计窗口内每个专家接了多少 token,供 update_bias 使用
self.register_buffer("load_count", torch.zeros(self.n_experts), persistent=False)
self.last_imbalance = 1.0
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""x: (N, dim) -> (weights (N,topk), indices (N,topk), aux_loss 标量)"""
logits = F.linear(x.float(), self.weight.float())
if self.score_func == "sigmoid":
scores = logits.sigmoid()
else:
scores = logits.softmax(dim=-1)
original = scores
# ---- 选择用的分数 = 原始分数 + 负载偏置 ----
sel = scores + self.expert_bias
# ---- group-limited routing:先选组再选专家,限制跨设备通信量 ----
if self.n_groups > 1:
g = sel.view(-1, self.n_groups, self.n_experts // self.n_groups)
per_group = g.size(-1)
if per_group > 1:
group_score = g.topk(min(2, per_group), dim=-1)[0].sum(dim=-1)
else:
group_score = g.squeeze(-1)
keep = group_score.topk(self.topk_groups, dim=-1)[1] # (N, topk_groups)
gmask = torch.zeros_like(group_score, dtype=torch.bool).scatter_(1, keep, True)
sel = sel.masked_fill(~gmask.unsqueeze(-1).expand_as(g).reshape_as(sel),
torch.finfo(sel.dtype).min)
indices = sel.topk(self.topk, dim=-1)[1] # (N, topk)
weights = original.gather(1, indices)
if self.score_func == "sigmoid":
weights = weights / weights.sum(dim=-1, keepdim=True).clamp_min(1e-9)
weights = weights * self.route_scale
# ---- 统计负载 + 兜底的序列级辅助损失(权重很小)----
onehot = F.one_hot(indices, self.n_experts).sum(dim=1).float() # (N, E)
counts = onehot.sum(dim=0)
with torch.no_grad():
self.load_count += counts.detach()
if self.training and self.aux_alpha > 0:
f = counts / counts.sum().clamp_min(1.0) # 实际负载占比
p = original.mean(dim=0) # 平均路由概率
aux_loss = self.aux_alpha * self.n_experts * (f * p).sum()
else:
aux_loss = x.new_zeros(())
return weights.type_as(x), indices, aux_loss
@torch.no_grad()
def update_bias(self, speed: float):
"""训练循环每步调一次:把超载专家的偏置压低、欠载的抬高。"""
total = self.load_count.sum()
if total <= 0:
return
self.last_imbalance = (self.load_count.max() / total * self.n_experts).item()
target = total / self.n_experts
err = target - self.load_count
self.expert_bias += speed * torch.sign(err)
self.load_count.zero_()
@torch.no_grad()
def load_stats(self) -> dict:
"""负载不均衡度 = 最大负载 / 平均负载。1.0 完全均衡,topk/E 的倒数为最差。"""
total = self.load_count.sum()
if total <= 0:
return {"imbalance": self.last_imbalance}
frac = self.load_count / total
return {"imbalance": (frac.max() * self.n_experts).item()}
class MoE(nn.Module):
def __init__(self, cfg):
super().__init__()
self.dim = cfg.dim
self.n_experts = cfg.n_routed_experts
self.gate = Gate(cfg)
self.experts = nn.ModuleList([SwiGLU(cfg.dim, cfg.moe_inter_dim)
for _ in range(cfg.n_routed_experts)])
self.shared = (SwiGLU(cfg.dim, cfg.moe_inter_dim * cfg.n_shared_experts)
if cfg.n_shared_experts > 0 else None)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
shape = x.shape
x = x.view(-1, self.dim)
weights, indices, aux = self.gate(x)
y = torch.zeros_like(x)
for i, expert in enumerate(self.experts):
token_idx, slot = torch.where(indices == i)
if token_idx.numel() == 0:
continue
y.index_add_(0, token_idx,
expert(x[token_idx]) * weights[token_idx, slot].unsqueeze(-1))
if self.shared is not None:
y = y + self.shared(x)
return y.view(shape), aux
class DenseFFN(nn.Module):
"""前几层用的普通 FFN。V3 的做法:靠前的层先稠密,训练更稳。"""
def __init__(self, cfg):
super().__init__()
self.ffn = SwiGLU(cfg.dim, cfg.dense_inter_dim)
def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
return self.ffn(x), x.new_zeros(())