MoE 原理、TC/EC 路由、LSH-MoE 与负载均衡浅析

MoE 的原理是什么?

MoE(Mixture of Experts,混合专家)是一种稀疏激活架构。

它的基本做法是:

把 Transformer 中原本的单一 FFN 替换为多个并行的专家网络(Experts),再引入一个路由器(Router)决定每个 token 应该由哪些专家处理。

每个 token 通常只激活少量专家(Top-1 或 Top-2),其余专家不参与计算。

公式上:

MoE(x)=∑i∈S(x)gi(x)⋅Experti(x) MoE(x) = \sum_{i \in S(x)} g_i(x) \cdot Expert_i(x) MoE(x)=i∈S(x)∑gi(x)⋅Experti(x)

其中:

  • S(x)S(x)S(x) 是路由器为 token xxx 选出的专家集合,大小 k≪Ek \ll Ek≪E;
  • gi(x)g_i(x)gi(x) 是路由权重,通常对选中的 kkk 个专家做 Softmax 归一化;
  • 未选中的专家权重为 0,完全不参与计算。

核心特点:

  • 参数多、计算少 :整个模型有 EEE 个专家,参数量巨大;但每次前向只激活 kkk 个,实际 FLOPs 与稠密模型相当甚至更低。
  • 稀疏激活:通过路由实现条件计算,不同 token 走不同专家。
  • 适用场景:参数规模大、数据和算力充足时优势明显;小规模或资源受限时可能不如稠密模型。

直观比喻:MoE 像一个拥有大量书籍的图书馆,读者(token)不需要翻遍所有书架,而是由管理员(路由器)指引,只去相关书架(专家)找书。

TC 与 EC 路由机制是什么?各有什么缺陷?

TC(Token Choice,词元选择专家)

核心思路:每个 token 主动选择自己最合适的 Top-K 个专家。

路由得分矩阵形状为 B,EB, EB,E(BBB 个 token,EEE 个专家)。TC 对每一行(token)在列维度(专家)上做 Top-K。

优点:

  • 每个 token 都会被分配到至少一个专家,语义完整性高,信息丢失风险小。

缺点:

  • 负载不均衡:少数"热门"专家会处理绝大部分 token,得到充分训练;大量"冷门"专家长期闲置,能力停滞,甚至出现"死亡专家"。
  • 模型容易"偏科":高频领域表现好,低频领域能力不足。

EC(Expert Choice,专家选择词元)

核心思路:每个专家主动从所有 token 中挑选自己最想处理的 Top-K 个 token。

把路由得分矩阵转置为 E,BE, BE,B,EC 对每一行(专家)在列维度(token)上做 Top-K。

优点:

  • 天然负载均衡 :每个专家最多处理 kkk 个 token,处理量有上限,避免热门专家过载。

缺点:

  • token 可能被丢弃:如果某个 token 没有被任何专家选中,它就不会被任何专家处理,导致语义信息缺失,模型可能"断章取义"。

案例:TC 的负载不均衡 vs EC 的 token 丢失

设:

text 复制代码
4 个 token:t0, t1, t2, t3
3 个专家:E0, E1, E2
K = 1(每个 token 选 1 个专家,或每个专家选 1 个 token)

路由得分矩阵 4,34, 34,3:

token E0 E1 E2
t0 0.6 0.3 0.1
t1 0.7 0.2 0.1
t2 0.2 0.5 0.3
t3 0.1 0.3 0.6

TC 模式:每个 token 选 Top-1 专家

  • t0 → E0 (0.6 最大)
  • t1 → E0 (0.7 最大)
  • t2 → E1 (0.5 最大)
  • t3 → E2 (0.6 最大)

专家负载:

专家 处理 token 数 处理的 token
E0 2 t0, t1
E1 1 t2
E2 1 t3

问题: E0 处理了 2 个 token,E1、E2 各 1 个,负载不均衡。如果 token 更多、得分更集中,热门专家可能处理绝大多数 token,冷门专家闲置。

EC 模式:每个专家选 Top-1 token

把得分矩阵转置,得到每个专家对所有 token 的分数:

专家 t0 t1 t2 t3
E0 0.6 0.7 0.2 0.1
E1 0.3 0.2 0.5 0.3
E2 0.1 0.1 0.3 0.6

每个专家选 Top-1:

  • E0 → t1 (0.7 最大)
  • E1 → t2 (0.5 最大)
  • E2 → t3 (0.6 最大)

专家负载:

专家 处理 token 数 处理的 token
E0 1 t1
E1 1 t2
E2 1 t3

问题:负载完全均衡,但 t0 没有被任何专家选中,它的信息被完全丢弃。这就是 EC 的 token dropout 风险。

对比总结

模式 负载分布 token 覆盖 主要风险
TC 不均衡 (E0=2, E1=1, E2=1) 所有 token 都被处理 热门专家过载,冷门专家闲置
EC 均衡 (各 1 个) t0 被丢弃 部分 token 语义丢失

LSH-MoE 是什么?有什么缺陷?

LSH-MoE 的核心

LSN(Locality-Sensitive Hashing,局部敏感哈希)路由不使用可学习的路由器 ,而是用固定哈希函数把 token 映射到专家。

典型做法(随机超平面符号哈希):

  1. 生成 nhashesn_{\text{hashes}}nhashes 个随机向量 r1,...,rnhashesr_1, \dots, r_{n_{\text{hashes}}}r1,...,rnhashes;
  2. 对 token 嵌入 xxx,计算投影 x⋅rix \cdot r_ix⋅ri;
  3. 取符号:正数为 1,负数为 0;
  4. 把符号位编码成整数 hash;
  5. 对专家数取模,得到专家 ID:

expert_id=hash(x) mod E \text{expert\_id} = \text{hash}(x) \bmod E expert_id=hash(x)modE

案例

设:

text 复制代码
d_model = 2
n_hashes = 2
num_experts = 3
随机向量:r0 = [1, 0], r1 = [0, 1]
4 个 token 嵌入:t0=[1,1], t1=[-1,1], t2=[1,-1], t3=[-1,-1]

步骤 1:投影

token x⋅r0x \cdot r_0x⋅r0 x⋅r1x \cdot r_1x⋅r1
t0=1,1 1 1
t1=-1,1 -1 1
t2=1,-1 1 -1
t3=-1,-1 -1 -1

步骤 2:取符号(正数为 1,负数为 0)

token sign0 sign1
t0 1 1
t1 0 1
t2 1 0
t3 0 0

步骤 3:编码 hash

权重为 1,21, 21,2(因为 1 << arange(2) = 1, 2):

hash=sign0×1+sign1×2 \text{hash} = \text{sign0} \times 1 + \text{sign1} \times 2 hash=sign0×1+sign1×2

token hash
t0 1×1+1×2=31 \times 1 + 1 \times 2 = 31×1+1×2=3
t1 0×1+1×2=20 \times 1 + 1 \times 2 = 20×1+1×2=2
t2 1×1+0×2=11 \times 1 + 0 \times 2 = 11×1+0×2=1
t3 0×1+0×2=00 \times 1 + 0 \times 2 = 00×1+0×2=0

步骤 4:取模得到专家 ID

expert_id=hash mod 3 \text{expert\_id} = \text{hash} \bmod 3 expert_id=hashmod3

token hash expert_id
t0 3 0
t1 2 2
t2 1 1
t3 0 0

负载统计:

专家 处理 token
E0 t0, t3
E1 t2
E2 t1

E0 处理 2 个,E1、E2 各 1 个。LSH 是概率性均衡,不保证绝对均匀,但比 TC 的极端倾斜好很多。

注意:LSH 的路由结果会随 embedding 训练而变化(因为投影基于 embedding),但哈希函数本身不通过梯度优化。

负载均衡在 MoE 中的作用是什么?

负载均衡的核心目标是:防止少数专家过载、多数专家闲置,避免"死亡专家",让所有专家都能得到充分训练。

常见方法:

策略 核心思路 说明
辅助负载均衡损失 在训练 loss 中加正则项,鼓励专家接收的 token 量、路由概率趋于均匀 Switch Transformer 等采用,最早最经典
容量控制 + overflow 给每个专家设容量上限,超出的 token 走 fallback 或 dropout 防止单个专家过载,也避免忽略冷门专家
动态无辅助损失均衡 根据过去负载统计给每个专家加 bias,动态调整路由分数 Loss-Free Balancing for MoE 提出,不破坏原优化目标
改善路由器、相似度保持路由 让相似语义的 token 分配到相似专家,均匀分布 提升收敛速度和负载均衡
混合共享 + 路由专家池 部分专家设为共享专家,所有 token 都激活;其余为路由专家 DeepSeekMoE 采用,保证语义覆盖 + 专家特长训练

为什么负载均衡重要?

  • 如果某些专家长期不被激活,它们的参数得不到梯度更新,能力停滞,甚至"死亡";
  • 热门专家过载,训练效率下降,且模型过度依赖少数专家,泛化能力受限;
  • 负载均衡让所有专家都参与训练,提升模型整体容量利用率。

MoE 代码

TC_MoE 代码

python 复制代码
class TC_MoE(nn.Module):
    def __init__(self, dim, num_experts, k):
        super().__init__()
        # 设置专家数量
        self.num_experts = num_experts
        # 设置每个token选用的专家数量
        self.k = k
        # 路由器:将输入映射到专家特征空间
        self.router = nn.Linear(dim, num_experts)
        # 创建专家模块列表(每个专家是独立的)
        self.experts = nn.ModuleList([Expert(dim) for _ in range(num_experts)])
    def forward(self, x, tokens=None, verbose=False):
        # 获取批量大小和特征维度
        B, D = x.shape
        # 计算每个专家对每个token的分数,使用softmax得到概率分布
        gate_scores = F.softmax(self.router(x), dim=-1)  # gate_scores: [B, E]

        # token选取分数最高的k个专家及其分数
        # topk_scores: [B, k](对应被选中的专家概率值)
        # topk_idx:    [B, k](对应被选中的专家索引)
        topk_scores, topk_idx = gate_scores.topk(self.k, dim=-1)
 
        # 初始化输出张量与输入同形状
        out = torch.zeros_like(x)

        # 每个token对应的每一个top-k位置单独处理(同一个token可能被不同专家处理)
        for i in range(self.k):
            # B表示处理的token总数
            # expert_ids表示每个token在第i个Top-K选择的专家编号,形状:[B]
            expert_ids = topk_idx[:, i]
            # expert_weight表示第每个token在第i个Top-K选择上专家所占的权重,形状:[B]
            expert_weight = topk_scores[:, i]

            # 用于累加当前第i个选择位置上所有专家的输出
            expert_output = torch.zeros_like(x)

            # 遍历所有专家,让对应专家处理被分配给它的token
            # e_id表示对应Top-K专家处理的token索引值
            for e_id, expert in enumerate(self.experts):
                # 创建掩码当token在第i个选择位置的专家索引等于当前专家e_id时为1,否则为0
                # mask形状为[B, 1],用于在计算专家网络前把不属于该专家的token置0
                mask = (expert_ids == e_id).float().unsqueeze(1)

                # mask.sum()表示属于该专家的token数量;若为0表示该专家在本轮没有任务
                if mask.sum() == 0:
                    continue

                # 只把属于该专家的token送入该专家的前馈网络
                # 注意:这里采用x * mask的方式,能保持张量形状一致并保留反向传播路径
                expert_output += expert(x * mask)

            # 将第i个选择位置上专家的输出按对应权重加权并累加到最终out
            # expert_weight.unsqueeze(1)变为[B, 1]以便广播乘到[B, D]
            out += expert_output * expert_weight.unsqueeze(1)

        # out:每个token在Top-K专家上的加权聚合的向量表示
        return out

功能说明:

  1. router(x) 把每个 token 映射为 EEE 维得分,Softmax 归一化;
  2. topk 为每个 token 选出 kkk 个专家;
  3. 外层循环遍历 kkk 个选择位置;
  4. 内层循环遍历所有专家,用 mask 筛选出"当前 token 的第 iii 个选择正好是这个专家"的 token;
  5. 只把这些 token 送入对应专家,输出乘以路由权重累加。

对应数值例子(TC): 如果 t0、t1 都选 E0,那么 E0 会处理 2 个 token,E1、E2 各处理 1 个,负载不均。

EC_MoE : 专家选择模式实现步骤

python 复制代码
class EC_MoE(nn.Module):
    def __init__(self, dim, num_experts, k):
        super().__init__()
        # 专家的总数量
        self.num_experts = num_experts
        # 每个专家最多选多少个token  
        self.k = k
        # 用于给每个token输出E个专家得分的路由器                
        self.router = nn.Linear(dim, num_experts)  
        self.experts = nn.ModuleList([Expert(dim) for _ in range(num_experts)])  
    def forward(self, x, tokens=None, verbose=False):
        # 获取输入token的数量B_total和维度D
        # B_total表示所有token的总数(批次 × token数)
        B_total, D = x.shape

        # 路由器计算每个token、每个专家的匹配得分输出维度:[B_total, num_experts]
        # softmax确保所有专家得分加起来为1
        gate_scores = F.softmax(self.router(x), dim=-1)

        # EC模式: "专家挑token"
        # 转置后变成[num_experts, B_total]
        # scores_T[e][t] = 第e个专家对第t个token的评分
        scores_T = gate_scores.transpose(0, 1)

        # 每个专家从所有token中挑选top-k个最相关的token
        # topk_idx: 每个专家选中Top-K token的索引
        # topk_scores: 对应的路由得分
        # 维度:[num_experts, k]
        topk_scores, topk_idx = scores_T.topk(min(self.k, B_total), dim=-1)

        # dispatch_weights大小:[B_total, num_experts]
        # 初始化dispatch_weights
        dispatch_weights = x.new_zeros((B_total, self.num_experts))

        # 对每个专家e,把top-k token的得分写入对应位置
        for e in range(self.num_experts):
            # topk_idx[e]是一个Top-K token索引列表
            # topk_scores[e]是Top-K token的得分
            # 填写dispatch_weights:每个专家对各个token评分归一化处理
            for t_idx, s in zip(topk_idx[e].tolist(), topk_scores[e].tolist()):
                dispatch_weights[t_idx, e] = s

        # 初始化输出out,与输入x大小相同
        out = torch.zeros_like(x)

        # 对每个专家进行前向计算
        for e_id, expert in enumerate(self.experts):

            # mask: 这个专家是否选择了该token
            # mask[t] == 1 → token t被这个专家选中
            # 维度:[B_total, 1]
            mask = (dispatch_weights[:, e_id] > 0).float().unsqueeze(1)

            # 如果专家没有选择任何token,则跳过计算
            if mask.sum() == 0:
                continue

            # 确保每个专家只处理其选中的Top-K token
            # mask会把不属于该专家的token设置为0,不同专家可能会处理相同的token
            expert_out = expert(x * mask)

            # 将专家输出按其权重加回到最终输出中
            # dispatch_weights[:, e_id]是每一组Top-K token对这个专家的权重
            out += expert_out * dispatch_weights[:, e_id].unsqueeze(1)
        return out

功能说明:

  1. router(x) 得到 B,EB, EB,E 得分;
  2. 转置为 E,BE, BE,B,每个专家对每个 token 的分数;
  3. topk 为每个专家选出它最想要的 kkk 个 token;
  4. dispatch_weights 记录每个 token 被哪些专家选中、权重多少;
  5. 对每个专家,mask 筛选出它选中的 token,送入专家,输出乘以权重累加。

对应数值例子(EC): E0 选 t1,E1 选 t2,E2 选 t3,t0 没有被任何专家选中,信息丢失。

基于LSH路由机制的简易MoE实现:

python 复制代码
import torch
import torch.nn as nn
# 简单字符级 tokenizer
class CharTokenizer:
    def __init__(self):
        self.vocab = {}
        self.inv_vocab = {}
    def build_vocab(self, texts):
        chars = set("".join(texts))
        self.vocab = {c:i for i,c in enumerate(sorted(chars))}
        self.inv_vocab = {i:c for c,i in self.vocab.items()}
    def encode(self, text):
        return [self.vocab[c] for c in text]
    def decode(self, ids):
        return "".join([self.inv_vocab[i] for i in ids])

# Expert FFN
class Expert(nn.Module):
    def __init__(self, dim):
        super().__init__()
        self.ffn = nn.Sequential(
            nn.Linear(dim, dim*4),
            nn.ReLU(),
            nn.Linear(dim*4, dim)
        )
    def forward(self, x):
        return self.ffn(x)

# LSH Router
class LSHRouter(nn.Module):
    def __init__(self, d_model, num_experts, n_hashes=8):
        super().__init__()
        self.num_experts = num_experts
        self.n_hashes = n_hashes
        self.register_buffer(
            "random_vectors",
            torch.randn(n_hashes, d_model)
        )
    def forward(self, x):
        projections = x @ self.random_vectors.T
        signs = (projections > 0).long()
        hashes = signs @ (1 << torch.arange(self.n_hashes, device=x.device))
        expert_ids = hashes % self.num_experts
        return hashes, expert_ids

# LSH-MoE
class LSH_MoE_Text(nn.Module):
    def __init__(self, dim, num_experts, n_hashes=8, vocab_size=None):
        super().__init__()
        self.embedding = nn.Embedding(vocab_size, dim)  # embedding 层
        self.dim = dim
        self.num_experts = num_experts
        self.router = LSHRouter(dim, num_experts, n_hashes)
        self.experts = nn.ModuleList([Expert(dim) for _ in range(num_experts)])

    def forward(self, token_lists, verbose=True):
        """
        token_lists: list of LongTensor,每条tensor是一条文本token ID
        """
        lengths = [t.size(0) for t in token_lists]
        total_tokens = sum(lengths)
        x_flat = torch.cat(token_lists, dim=0)  # [total_tokens]
        x_flat = self.embedding(x_flat)         # [total_tokens, D]

        hashes, expert_ids = self.router(x_flat)
        out_flat = torch.zeros_like(x_flat)

        expert_load = torch.zeros(self.num_experts, dtype=torch.long, device=x_flat.device)
        for e_id, expert in enumerate(self.experts):
            mask = (expert_ids == e_id).float().unsqueeze(1)
            n_tokens = int(mask.sum().item())
            expert_load[e_id] = n_tokens
            if n_tokens > 0:
                out_flat += expert(x_flat * mask) * mask

        # 拆回原句子
        outputs = []
        start = 0
        for l in lengths:
            outputs.append(out_flat[start:start+l])
            start += l
        if verbose:
            print("\n========== LSH-MoE Token 哈希映射 ==========")
            start = 0
            for idx, l in enumerate(lengths):
                for j in range(l):
                    token_idx = start + j
                    print(f"Sentence {idx}, Char {j}: Hash={hashes[token_idx].item()} -> Expert {expert_ids[token_idx].item()}")
                start += l
            print("\n========== LSH-MoE 专家负载统计 ==========")
            for e in range(self.num_experts):
                print(f"Expert {e}: {expert_load[e].item()} tokens")
            print("------------------------------------------------\n")

        return outputs

# 测试
if __name__ == "__main__":
    sentences = ["你好世界", "今天天气很好"]
    tokenizer = CharTokenizer()
    tokenizer.build_vocab(sentences)
    token_lists = [torch.tensor(tokenizer.encode(s), dtype=torch.long) for s in sentences]
    dim, num_experts = 16, 5    # 每个 token 的 embedding 维度,专家数量
    moe_text = LSH_MoE_Text(dim=dim, num_experts=num_experts, vocab_size=len(tokenizer.vocab))
    outputs = moe_text(token_lists)
    for i, out in enumerate(outputs):
        print(f"Sentence {i} 输出shape: {out.shape}")

总结

问题 回答
MoE 原理 多个专家 + 路由器,每个 token 只激活 Top-K 专家,参数多但计算稀疏
TC 每个 token 选 Top-K 专家;优点:token 全覆盖;缺点:负载不均衡,热门专家过载、冷门专家闲置
EC 每个专家选 Top-K token;优点:负载均衡;缺点:部分 token 可能被丢弃,语义丢失
LSH-MoE 固定哈希函数把 token 映射到专家;优点:无需训练、负载概率均衡、高效;缺点:语义表达弱、无法动态调整
负载均衡 防止专家过载/闲置,避免死亡专家,让所有专家充分训练;方法包括辅助损失、容量控制、动态偏置、共享专家等
数值例子核心 TC:E0 处理 2 个 token,E1/E2 各 1 个;EC:各 1 个但 t0 被丢弃;LSH:E0 处理 2 个,E1/E2 各 1 个,概率均衡
代码对应 TC_MoE 按 token 选专家;EC_MoE 按专家选 token;LSH_MoE_Text 用随机投影哈希路由
相关推荐
Sarvartha1 小时前
栈的四则运算、栈的递归应用
数据结构·算法
码农小白AI1 小时前
无损检测 NDT 批量报告审核效率如何提升?IACheck AI 报告文档审核 Agent 优化批量报告审核
大数据·人工智能
hrrrrxeeeee1 小时前
不会编程适合考什么证书?AI应用认证与技术型认证的门槛区别
人工智能
stolentime1 小时前
CF2277E Tree Guessing题解
c++·算法·贪心算法·交互·ad-hoc
shxjnpl1 小时前
粤语客服录音如何实现自动质检?从 ASR 语音识别到大模型分析
人工智能·语音识别·实时音视频
2601_955662461 小时前
短视频配音怎么做?从文案到成品音频的完整流程
人工智能·音视频·语音识别·媒体·视频
加贝哥|usun1 小时前
谈谈多Agent协作下的 AI 记忆和知识治理
人工智能·ai
2603_954708311 小时前
微能网协调控制箱的架构优势对能源管理的影响?
大数据·人工智能·分布式·安全·架构
边缘计算社区2 小时前
从终端智能到业务落地,睿思智联打造 AI 走向现场的算力底座
人工智能