MoE混合专家层
## 1. MoE核心思想 ### 1.1 什么是MoE? **混合专家模型**是一种通过多个专门化子网络来处理输入的架构。与传统单一网络不同,MoE将输入路由到不同的"专家"进行处理,然后根据门控机制加权组合输出。 ``` 传统网络: 输入 → [单一大型网络] → 输出 MoE网络: 输入 → [门控] → 选K个专家 → 加权组合 → 输出 ↓ 专家1|专家2|...|专家n ``` ### 1.2 数学公式 MoE层的输出由以下公式定义: $$ y = \sum_{i=1}^{n} G(x)_i \cdot E_i(x) $$ 其中: - $x \in \mathbb{R}^N$ 是输入向量 - $n$ 是专家总数 - $E_i: \mathbb{R}^N \to \mathbb{R}^N$ 是第$i$个专家网络 - $G(x)_i$ 是门控函数分配给专家$i$的权重 ### 1.3 稀疏性的关键 **稀疏MoE**的核心优势:只激活部分专家,大幅降低计算量。 - **稠密MoE**:所有专家都参与计算 - **稀疏MoE**:只计算被选中的top-k个专家 例如:8个专家,k=2,则每个token只经过2个专家,计算量降至1/4! ### 1.4 应用场景 MoE架构广泛应用于: - **GPT-4/ChatGPT**:提升模型规模同时控制计算成本 - **Switch Transformer**:Google的超大规模语言模型 - **Mixtral 8x7B**:开源的MoE大模型
## 2. 门控路由函数 ### 2.1 路由函数定义 门控函数 $G: \mathbb{R}^N \to \mathbb{R}^E$ 将输入映射到每个专家的权重分数。 **核心操作:Top-K选择 + Softmax归一化** $$ G(x) = \text{Softmax}(\text{KeepTopK}(W_g x, k)) $$ 其中: - $W_g \in \mathbb{R}^{N \times E}$ 是学习的门控权重矩阵 - $k$ 是选中的专家数量(通常k=2或k=4) - $\text{KeepTopK}$ 保留top-k个分数,其余设为$-\infty$ ### 2.2 KeepTopK函数详解 $$ \text{KeepTopK}(z, k)_i = \begin{cases} z_i & \text{if } z_i \text{ 在 } z \text{ 的top-k中} \\ -\infty & \text{其他情况} \end{cases} $$ ### 2.3 代码实现 - topk_scores ```python def topk_scores(gate: nn.Linear, k: int, x: torch.Tensor): # 步骤1: 计算原始分数 = gate(x) # W_g: [hidden_size, num_experts] # x: [batch*seq_len, hidden_size] # raw_scores: [batch*seq_len, num_experts] raw_scores = gate(x) # 步骤2: 只保留top-k个分数 scores = keep_topk(raw_scores, k) # 步骤3: Softmax归一化(-inf变成0) scores = torch.softmax(scores, dim=-1, dtype=torch.float32) return scores ``` **逐行解析**: - `gate(x)`: 线性变换,hidden_size维 → num_experts维 - `keep_topk()`: 核心稀疏化操作 - `softmax()`: 将分数转为概率分布 ### 2.4 代码实现 - keep_topk ```python def keep_topk(scores: torch.Tensor, k: int) -> torch.Tensor: # 找到top-k的索引 # scores: [T, E] T=token数, E=专家数 # topk_indices: [T, k] 每个token的top-k专家索引 _, topk_indices = scores.topk(k, dim=-1) # 创建布尔掩码 assignment = torch.zeros_like(scores, dtype=torch.bool) assignment.scatter_(-1, topk_indices, True) # 保留top-k分数,其余设为-inf return torch.where(assignment, scores, float("-inf")) ``` **关键操作**: 1. `topk(k, dim=-1)`: 沿专家维度找最大k个值 2. `scatter_`: 将索引位置标记为True 3. `where`: 根据掩码选择值或-inf **示例**(假设k=2): ``` 输入: [0.3, 0.8, 0.1, 0.9, 0.5] 输出: [0.3, 0.8, -inf, 0.9, -inf] ↑ ↑ 被mask 被mask ``` ### 2.5 为什么是-inf而不是0? Softmax($-\infty$) = 0,这样: 1. 保持数值稳定性 2. 明确区分"被选中"vs"未被选中" 3. 便于后续梯度只流向被选中的专家
## 3. 专家网络实现 ### 3.1 专家定义 每个专家 $E: \mathbb{R}^N \to \mathbb{R}^N$ 学习输入的非线性变换。本实现使用**两层MLP**: $$ E(x) = W_2 \cdot \text{GELU}(W_1 x) $$ ### 3.2 代码 - make_expert ```python def make_expert(hidden_size, ffn_hidden_size = None): if ffn_hidden_size is None: ffn_hidden_size = 4 * hidden_size # 默认4倍扩展 # 两层MLP: 扩展 → 激活 → 压缩 return nn.Sequential( nn.Linear(hidden_size, ffn_hidden_size), # W_1: 扩展层 nn.GELU(), # GELU激活 nn.Linear(ffn_hidden_size, hidden_size), # W_2: 压缩层 ) ``` **结构示例**(hidden=128, ffn_hidden=512): ``` 输入[128] → Linear(128→512) → GELU → Linear(512→128) → 输出[128] ``` ### 3.3 选择性计算 - run_expert **关键优化**:未被选中的专家完全不计算! ```python def run_expert(expert: nn.Module, expert_scores: torch.Tensor, x: torch.Tensor) -> torch.Tensor: # 步骤1: 只选择得分>0的token进行计算 selected_x = x[expert_scores > 0] # 步骤2: 如果没有token选中此专家,直接跳过 if len(selected_x) == 0: return None # 步骤3: 获取对应权重并扩展维度 # weights: [selected_count, 1] weights = expert_scores[expert_scores > 0].unsqueeze(-1) # 步骤4: 专家前向传播并加权 # expert(selected_x): [selected_count, hidden_size] return weights * expert(selected_x) ``` **逐行解析**: 1. **选择过滤** ```python selected_x = x[expert_scores > 0] ``` - 只有得分>0的token才会进入此专家 - 利用稀疏性节省大量计算 2. **空检查** ```python if len(selected_x) == 0: return None ``` - 某些专家可能完全未被选中 - 直接返回None,避免无效计算 3. **权重准备** ```python weights = expert_scores[expert_scores > 0].unsqueeze(-1) ``` - 提取非零得分 - `unsqueeze(-1)`: [T] → [T, 1]以便广播 4. **加权输出** ```python return weights * expert(selected_x) ``` - `expert(selected_x)`: 计算专家输出 - `weights * ...`: 用门控权重加权 ### 3.4 计算复杂度对比 假设:batch=64, seq=128, 专家数=32, k=2 | 方法 | 计算量 | 说明 | |------|--------|------| | 稠密MoE | 64×128×32个专家 | 每个token过所有专家 | | 稀疏MoE | 64×128×2个专家 | 每个token只过k个专家 | | 节省比例 | 1/16 | k=2时 |
## 4. 负载均衡损失 ### 4.1 为什么需要负载均衡? **问题**:没有约束时,门控网络倾向于总是选择相同的少数专家,导致: - 大部分专家"失业"(利用率低) - 少数专家过载(计算瓶颈) - 训练不均衡(性能下降) ### 4.2 GShard/Switch-Transformer的解决方案 引入负载均衡损失,鼓励均匀分配: $$ L_{LB} = n \sum_{i=1}^n f_i \cdot P_i $$ 其中: - $n$ 是专家总数 - $f_i$ 是分配给专家$i$的token比例 - $P_i$ 是专家$i$的平均门控概率 **直观理解**: - $f_i$大、$P_i$小:专家被频繁选中但得分低 → 高损失 - $f_i$小、$P_i$大:专家很少被选中但得分高 → 高损失 - 理想情况:$f_i \approx P_i$,损失最小 ### 4.3 代码实现 ```python def load_balancing_loss( raw_scores: torch.Tensor, # [T, E] 原始分数 token_assignment: torch.Tensor, # [T, E] 布尔分配 ) -> torch.Tensor: _, num_experts = raw_scores.size() # 步骤1: Softmax得到每个专家的概率 raw_scores = torch.softmax(raw_scores, dim=-1, dtype=torch.float32) # 步骤2: 计算每个专家的token比例 f_i # token_fractions: [E] 每个专家平均接收多少比例的token token_fractions = einops.reduce(token_assignment.float(), "t e -> e", "mean") # 步骤3: 计算每个专家的平均权重 P_i # average_weights: [E] 所有token对该专家的平均概率 average_weights = einops.reduce(raw_scores, "t e -> e", "mean") # 步骤4: 计算负载均衡损失 return num_experts ** 2 * (token_fractions * average_weights).mean() ``` **逐行解析**: 1. **Softmax归一化** ```python raw_scores = torch.softmax(raw_scores, dim=-1) ``` - 将原始分数转为概率分布 - 用于计算$P_i$ 2. **Token比例$f_i$** ```python token_fractions = einops.reduce(token_assignment.float(), "t e -> e", "mean") ``` - 对每个专家,统计被多少token选中 - `reduce(..., "mean")`: 求平均得到比例 3. **平均概率$P_i$** ```python average_weights = einops.reduce(raw_scores, "t e -> e", "mean") ``` - 所有token对该专家的平均门控得分 4. **损失计算** ```python return num_experts ** 2 * (token_fractions * average_weights).mean() ``` - `num_experts ** 2`: 归一化系数(论文约定) - 乘积:$f_i \times P_i$鼓励两者匹配 ### 4.4 损失收集机制 ```python _lb_losses = [] # 全局损失列表 def append_lb_loss(loss: torch.Tensor) -> None: global _lb_losses assert (loss >= 0.0).all(), f"Negative loss: {loss.item()}" _lb_losses.append(loss) def get_lb_losses() -> list[torch.Tensor]: global _lb_losses return _lb_losses def clear_lb_losses() -> None: global _lb_losses _lb_losses = [] # 每步后清空 ``` ### 4.5 使用位置 在`keep_topk`中自动收集: ```python def keep_topk(scores: torch.Tensor, k: int) -> torch.Tensor: _, topk_indices = scores.topk(k, dim=-1) assignment = torch.zeros_like(scores, dtype=torch.bool) assignment.scatter_(-1, topk_indices, True) append_lb_loss(load_balancing_loss(scores, assignment)) # 收集损失 return torch.where(assignment, scores, float("-inf")) ``` ### 4.6 训练时的使用 ```python # 在forward函数中 lb_loss = torch.tensor(0.0, device=ce_loss.device) for l in get_lb_losses(): lb_loss += l lb_loss = lb_loss_scale * lb_loss # 可调权重,如0.01 loss = ce_loss + lb_loss # 加到主损失上 clear_lb_losses() # 清空准备下一轮 ``` **实验对比**(来自训练日志): - 无负载均衡:15个专家长期失业 - 有负载均衡:所有专家均匀利用
## 5. 关键代码解析 ### 5.1 为什么要单独封装topk_scores? 你提出了一个很好的问题!让我们对比两种设计方式: **设计A:当前实现(单独封装)** ```python # 定义:独立函数 def topk_scores(gate: nn.Linear, k: int, x: torch.Tensor): raw_scores = gate(x) scores = keep_topk(raw_scores, k) scores = torch.softmax(scores, dim=-1, dtype=torch.float32) return scores # 使用:在MoELayer中调用 class MoELayer(nn.Module): def __init__(self, hidden_size, num_experts, k): self.gate = nn.Linear(hidden_size, num_experts) # 可学习参数 self.k = k def forward(self, x): scores = topk_scores(self.gate, self.k, x) # 调用函数 # ... 后续处理 ``` **设计B:直接内联实现** ```python class MoELayer(nn.Module): def forward(self, x): # 直接在这里实现所有逻辑 raw_scores = self.gate(x) _, topk_indices = raw_scores.topk(self.k, dim=-1) assignment = torch.zeros_like(raw_scores, dtype=torch.bool) assignment.scatter_(-1, topk_indices, True) scores = torch.softmax(torch.where(assignment, raw_scores, float("-inf")), dim=-1) # ... 后续处理 ``` ### 5.2 设计A的优势分析 | 优势 | 说明 | |------|------| | **模块化** | `topk_scores`是独立逻辑单元,职责单一 | | **可测试性** | 可以单独测试路由逻辑,不依赖MoE层 | | **可复用性** | 不同MoE实现可以共享同样的路由函数 | | **可扩展性** | 未来可以轻松添加新的路由策略(如Noisy Top-K) | | **关注点分离** | MoELayer负责整体协调,路由函数负责具体计算 | ### 5.3 设计哲学:什么是模块成员,什么是函数? ```python class MoELayer(nn.Module): def __init__(self, ...): # ✅ 这些必须是模块成员(有可学习参数) self.gate = nn.Linear(...) # 权重矩阵W_g self.experts = nn.ModuleList(...) # 专家网络的权重 # ❌ 这些不需要是成员(无状态操作) # self.topk_func = topk_scores # 不需要保存为成员 def forward(self, x): # ✅ 使用模块成员 raw_scores = self.gate(x) # 使用可学习参数 # ✅ 调用纯函数 scores = topk_scores(self.gate, self.k, x) # 无状态函数 ``` **关键原则**: - **有可学习参数的** → 必须是`self.xxx`模块成员 - **无状态的纯函数** → 可以是独立函数 ### 5.4 self.gate的维度变换 是的!`self.gate = nn.Linear(hidden_size, num_experts)` 正是将输入从hidden维度变换到专家维度: ``` 权重矩阵 W_g: [hidden_size, num_experts] 输入 x_flat: [T, hidden_size] ↓ x_flat @ W_g ↓ 输出 raw_scores: [T, num_experts] ↑ 每一行是该token对所有专家的分数 ``` **示例**(假设hidden_size=128, num_experts=32): ```python # Gate权重矩阵: [128, 32] self.gate = nn.Linear(128, 32) # 单个token: [128] token = torch.randn(128) # 变换后: [32] - 对32个专家的分数 scores = self.gate(token) # [32] # 批量处理: [T, 128] → [T, 32] tokens = torch.randn(100, 128) # 100个token scores = self.gate(tokens) # [100, 32] ``` ### 5.5 专家个数应该如何选择? 这是一个关键的hyperparameter选择问题! #### 经验法则(来自研究论文和实践) | 模型规模 | 专家数范围 | k值 | 典型应用 | |----------|-----------|-----|---------| | 小模型(<1B参数) | 4-8 | 1-2 | 实验验证 | | 中模型(1B-10B) | 8-32 | 2-4 | 生产部署 | | 大模型(10B-100B) | 32-64 | 2-4 | 企业级应用 | | 超大模型(>100B) | 64-128 | 4-8 | 研究项目 | #### 知名模型的配置 ```python # Switch Transformer (Google, 1.6T参数) num_experts = 2048 k = 1 # Mixtral 8x7B (Mistral AI) num_experts = 8 k = 2 # GLaM (Google, 1.2T参数) num_experts = 64 k = 1 # 本教程示例(小型教学模型) num_experts = 32 k = 2 ``` #### 选择专家数的考虑因素 **1. 计算资源** ``` 有效计算量 = (k / num_experts) × 全量计算 示例: - num_experts=32, k=2 → 约6.25%的计算量 - num_experts=8, k=2 → 约25%的计算量 ``` **2. 负载均衡难度** ``` 专家越多 → 负载越难均衡 → 需要更强的lb_loss_scale num_experts=4: 容易均衡 num_experts=32: 需要调参 num_experts=128: 很难均衡,可能需要特殊技巧 ``` **3. 专家专业化程度** ``` 专家越少 → 每个专家需要处理更多模式 专家越多 → 每个专家可以更"专注" 例如: - num_experts=4: 每个专家都是"通才" - num_experts=32: 可以形成"语言专家"、"推理专家"等 ``` #### 推荐的调参策略 ```python def recommend_experts(model_params_billion): """根据模型规模推荐专家数""" if model_params_billion < 1: return {"num_experts": 4, "k": 2, "lb_loss_scale": 0.01} elif model_params_billion < 10: return {"num_experts": 16, "k": 2, "lb_loss_scale": 0.01} elif model_params_billion < 100: return {"num_experts": 32, "k": 2, "lb_loss_scale": 0.02} else: return {"num_experts": 64, "k": 4, "lb_loss_scale": 0.05} # 使用示例 config = recommend_experts(model_params_billion=7) # config = {"num_experts": 16, "k": 2, "lb_loss_scale": 0.01} ``` #### 实验对比 本教程的实验数据显示: ``` 配置A: num_experts=32, k=2, lb_loss_scale=0.01 - 训练2000步后未使用专家: 0 - 负载均衡良好 配置B: num_experts=32, k=2, lb_loss_scale=0.0 - 训练2000步后未使用专家: 13-15 - 负载严重不均衡 ``` **结论**:专家数不是越大越好,需要和负载均衡损失配合调优。 ### 5.6 代码重构建议 如果你觉得`topk_scores`的封装让代码不够直观,可以这样重构: ```python class MoELayer(nn.Module): def __init__(self, hidden_size, num_experts, k): super().__init__() self.gate = nn.Linear(hidden_size, num_experts) self.experts = nn.ModuleList([...]) self.k = k def forward(self, x): x_flat = x.view(-1, x.size(-1)) # 方法1:调用独立函数(当前方式) scores = topk_scores(self.gate, self.k, x_flat) # 方法2:内联实现(更直观但不够模块化) # raw_scores = self.gate(x_flat) # _, topk_indices = raw_scores.topk(self.k, dim=-1) # assignment = torch.zeros_like(raw_scores, dtype=torch.bool) # assignment.scatter_(-1, topk_indices, True) # append_lb_loss(load_balancing_loss(raw_scores, assignment)) # scores = torch.softmax(torch.where(assignment, raw_scores, float("-inf")), dim=-1) # ... 后续处理相同 ``` 两种方式**功能完全相同**,选择取决于你的编码风格偏好! ### 5.7 维度压缩与信息保留:残差连接的关键作用 你提出了一个**非常深刻的观察**!让我们深入分析这个问题。 #### 问题:Gate的降维是否损失信息? ```python self.gate = nn.Linear(hidden_size, num_experts) # hidden_size=128 → num_experts=32 # 确实是降维!128维 → 32维 ``` **直觉上的担忧**: - 128维的信息被压缩到32维 - 如果num_experts=4,那么128→4,损失更大! - 这不是会丢失大量信息吗? #### 答案:残差连接保住了信息! 你的理解完全正确——MoE架构中的**残差连接**是关键! ``` ┌─────────────────────────────────────────────────────────────┐ │ Transformer Block中的信息流 │ └─────────────────────────────────────────────────────────────┘ 输入 x: [batch, seq_len, hidden_size] │ ├─> ┌────────────────────────────────────────────────┐ │ │ Pre-Norm + MultiHeadAttention │ │ │ 输出: attn_output [B, T, H] │ │ └────────────────────────────────────────────────┘ │ ├─> x = x + attn_output ← 残差连接保留原始信息 │ │ │ └─> 此时x包含: 原始x + 注意力提取的信息 │ ├─> ┌────────────────────────────────────────────────┐ │ │ Pre-Norm + MoE Layer (替换传统FFN) │ │ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ │ gate: [H] → [num_experts] 降维路由 │ │ │ │ │ ↑ 这里确实损失了信息! │ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ │ experts: 输出[H]维度(还原维度) │ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ │ 输出: moe_output [B, T, H] │ │ └────────────────────────────────────────────────┘ │ └─> x = x + moe_output ← 残差连接再次保留信息! 最终输出 = 原始x + 注意力信息 + MoE信息 ``` #### 关键洞察:MoE输出仍然是hidden维度 ```python class MoELayer(nn.Module): def forward(self, x): # 输入: [B, T, hidden_size] # Gate降维: [T, hidden] → [T, num_experts] # 这里确实压缩了信息 scores = topk_scores(...) # 但专家输出: [T, hidden] ← 还原回hidden维度 for expert in self.experts: expert_output = expert(selected_x) # [T, hidden] outputs[...] += expert_output # 输出: [B, T, hidden_size] ← 维度完整! return outputs.view_as(x) ``` #### 对比传统FFN ``` ┌─────────────────────────────────────────────────────────────┐ │ 传统FFN vs MoE Layer 对比 │ └─────────────────────────────────────────────────────────────┘ 传统FFN: x: [H] → Linear(H, 4H) → GELU → Linear(4H, H) → [H] ↑ ↑ 保留原信息 扩展到4H维度处理 MoE Layer: x: [H] → Gate(H, E) → TopK → Expert(H, 4H) → [H] ↑ ↑ ↑ 保留 降维路由 稀疏激活但输出仍为H ``` **关键区别**: - 传统FFN:每个token都通过完整的4H维度处理 - MoE Layer:每个token只通过k个专家处理,但输出维度仍为H #### 为什么残差连接如此重要? ```python class Block(nn.Module): def forward(self, x): # 注意力部分 h = self.attn_norm(x) h = self.attn(h) x = x + h # ← 残差1: 保留x + 注意力信息 # MoE部分 h = self.mlp_norm(x) h = self.mlp(h) x = x + h # ← 残差2: 保留(x + 注意力) + MoE信息 return x ``` **双重保障**: 1. **残差1**:即使注意力丢失信息,原始x仍在 2. **残差2**:即使MoE的gate丢失信息,(x + 注意力)仍在 #### 数学视角 没有残差连接: ``` output = MoE(x) = MoE(x_original) 如果gate降维损失信息 → 无法恢复 ``` 有残差连接: ``` output = x + MoE(x) = x_original + MoE(x_original) = x_original + (x_original变换后的信息) 即使gate降维 → x_original仍然存在! ``` #### 实验验证 ```python # 消融实验:测试残差连接的重要性 # 配置A: 有残差连接 (标准) x = x + moe_layer(x) # 配置B: 无残差连接 (gate降维影响大) x = moe_layer(x) # 只有gate压缩后的信息 # 配置C: 传统FFN (基线) x = x + ffn_layer(x) ``` **预期结果**: - 配置A > 配置C > 配置B - MoE需要残差连接来弥补gate降维的信息损失 #### 设计启示 1. **Gate不需要很大** ```python # num_experts=32 就足够了 # 不需要 num_experts=hidden_size # 因为残差连接保住了原始信息 ``` 2. **专家个数可以灵活选择** ```python num_experts = 4 # 降维多,但残差补偿 num_experts = 32 # 降维少,残差增强 num_experts = 128 # 降维很少,残差锦上添花 ``` 3. **MoE的设计哲学** - Gate不是主要的信息处理器 - Gate只是"路由开关" - 真正的信息处理在专家网络中 - 残差连接确保信息不会丢失 #### 总结你的观察 | 你的观察 | 正确性 | 说明 | |----------|--------|------| | Gate降维损失信息 | ✅ 正确 | H → E确实压缩 | | MoE替换FFN | ✅ 正确 | 在Transformer Block中 | | 有残差连接x+MoE(x) | ✅ 正确 | 这是关键设计 | | 信息不会真正损失太多 | ✅ 正确 | 残差连接保住原始信息 | **你的理解完全正确!这正是MoE架构设计的精髓之一。** ### 5.8 总结 | 问题 | 答案 | |------|------| | 为什么要单独封装? | 模块化、可测试、可复用、可扩展 | | self.gate是做什么的? | 将输入从hidden维度变换到专家维度(路由开关) | | Gate降维会损失信息吗? | 会,但残差连接补偿了这个损失 | | 专家数应该选多少? | 小模型4-8,中模型16-32,大模型32-64 | | k应该选多少? | 通常2-4,与专家数成反比 | | 如何调参? | 从小规模开始,逐步增加,监控未使用专家数 | | MoE为什么能work? | 残差连接 + 稀疏激活 + 负载均衡的协同作用 |
## 6. MoE层完整代码 ### 5.1 MoELayer主模块 ```python class MoELayer(nn.Module): def __init__( self, *, hidden_size: int, # 输入维度 num_experts: int, # 专家数量 k: int, # 选中专家数 idx: int, # 层索引(用于统计) ffn_hidden_size=None, # 专家隐藏层大小 ): super().__init__() # 门控网络:将输入映射到专家权重 self.gate = nn.Linear(hidden_size, num_experts) # 创建专家列表 self.experts = nn.ModuleList( [make_expert(hidden_size, ffn_hidden_size) for _ in range(num_experts)] ) self.k = k self.idx = idx def forward(self, x: torch.Tensor): # 步骤1: 展平所有维度(batch, seq_len等) # x: [B, T, H] → x_flat: [B*T, H] x_flat = x.view(-1, x.size(-1)) # 步骤2: 获取每个token对各专家的权重分数 # scores: [B*T, E] 只对k个专家非零 scores = topk_scores(self.gate, self.k, x_flat) # 步骤3: 保存统计信息(熵、未使用专家数) save_statistics(self.idx, scores) # 步骤4: 初始化输出 outputs = torch.zeros_like(x_flat) # 步骤5: 对每个专家分别计算 for i, expert in enumerate(self.experts): # 获取此专家的权重分数 expert_scores = scores[:, i] # 只计算被选中的token,返回加权输出 expert_outputs = run_expert(expert, expert_scores, x_flat) if expert_outputs is None: continue # 此专家未被任何token选中 # 累加到总输出 outputs[expert_scores > 0] += expert_outputs # 步骤6: 恢复原始形状 outputs = outputs.view_as(x) return outputs ``` ### 5.2 统计监控 ```python from contextlib import contextmanager _statistics = {} _statistics_enabled = False @contextmanager def capture_statistics(enable=True): global _statistics_enabled prev_value = _statistics_enabled _statistics_enabled = enable try: yield finally: _statistics_enabled = prev_value def save_statistics(layer_idx: int, topk_scores: torch.Tensor): def compute() -> dict[str, float]: # 计算熵:衡量路由的确定性 def entropy(): entropy_eps = 1e-10 norm_scores = topk_scores / topk_scores.sum(dim=1, keepdim=True) tokens_entropy = -(norm_scores * torch.log2(norm_scores + entropy_eps)).sum(dim=1) return tokens_entropy.mean().item() # 统计未使用的专家数 def unused(): expert_load = einops.reduce(topk_scores > 0, "t e -> e", "sum") return int((expert_load == 0).sum().item()) return {"entropy": entropy(), "unused": unused()} global _statistics, _statistics_enabled if not _statistics_enabled: return assert layer_idx not in _statistics, f"Repeated layer {layer_idx}" _statistics[layer_idx] = compute() def compute_statistics(): global _statistics stats = {"entropy": 0.0, "unused": 0} for layer_statistics in _statistics.values(): stats["entropy"] += layer_statistics["entropy"] stats["unused"] += layer_statistics["unused"] stats["entropy"] /= len(_statistics) return stats def clear_statistics(): global _statistics _statistics = {} ``` ### 5.3 在Transformer中使用 ```python class Block(nn.Module): def __init__(self, config: Config, idx: int): super().__init__() self.attn = MultiHeadAttention(config) self.attn_norm = nn.LayerNorm(config.hidden_size) # 用MoE替换标准FFN self.mlp = MoELayer( hidden_size=config.hidden_size, num_experts=config.n_experts, k=config.k, idx=idx, ffn_hidden_size=config.ffn_hidden_size, ) self.mlp_norm = nn.LayerNorm(config.hidden_size) def forward(self, x: torch.Tensor) -> torch.Tensor: # 注意力 + 残差 h = self.attn_norm(x) h = self.attn(h) x = x + h # MoE + 残差 h = self.mlp_norm(x) h = self.mlp(h) x = x + h return x ``` ### 5.4 配置示例 ```python @dataclasses.dataclass class Config: seq_len: int = 64 n_layers: int = 4 n_heads: int = 4 hidden_size: int = 128 ffn_hidden_size: int = 256 head_size: int = 32 vocab_size: int = len(vocab) # MoE特定参数 k: int = 2 # 每个token选中2个专家 n_experts: int = 32 # 总共32个专家 ``` ### 5.5 完整数据流 ``` 输入 [B, T, H] ↓ 展平 [B*T, H] ↓ gate: Linear(H, E) → [B*T, E] ↓ topk_scores: 只保留k个最大值 ↓ 对每个专家: ├─ 选中token: x[expert_scores > 0] ├─ expert(x): MLP计算 └─ 加权: scores * output ↓ 累加所有专家输出 ↓ 重塑回 [B, T, H] ↓ 输出 ``` ### 5.6 关键要点总结 1. **稀疏性**:只计算被选中的k个专家 2. **可微分**:top-k的softmax保持梯度流 3. **负载均衡**:防止专家利用不均 4. **选择性计算**:`run_expert`中的if判断 5. **统计监控**:熵和未使用专家数
## 7. 训练损失分析 这张图展示了MoE训练过程中的5个关键指标,对比**有负载均衡**(蓝色,Yes)和**无负载均衡**(橙色,No)两种配置。 ### 7.1 实验设计 ```python # 配置A: 有负载均衡 (lb_loss_scale=0.01) lb_train_losses, lb_val_losses, lb_statistics = train_model(lb_loss_scale=0.01) # 配置B: 无负载均衡 (lb_loss_scale=0.0) no_lb_train_losses, no_lb_val_losses, no_lb_statistics = train_model(lb_loss_scale=0.0) ``` **对比维度**: - 训练/验证损失(交叉熵) - 路由熵(专家使用多样性) - 未使用专家数量 - 负载均衡损失值 ### 7.2 5个关键指标解析 #### 指标1:交叉熵训练损失 ``` X轴: 训练步数 (0-2000) Y轴: 交叉熵损失 蓝色: 有负载均衡 橙色: 无负载均衡 ``` **观察**: - **有负载均衡**:损失下降**平滑且稳定**,2000步后达到~1.3-1.4 - **无负载均衡**:损失下降但**始终更高**,2000步后约1.5 **含义**: ``` 有LB: 训练效率更高,损失更低 原因: 所有专家都参与训练,充分利用模型容量 ``` #### 指标2:交叉熵验证损失 ``` X轴: 训练步数 Y轴: 验证集上的交叉熵损失 蓝色: 有负载均衡 橙色: 无负载均衡 ``` **观察**: - **有负载均衡**:验证损失**稳定且较低**,~1.4-1.5 - **无负载均衡**:验证损失**更高且波动**,~1.5-1.6 **含义**: ``` 有LB: 泛化性能更好 原因: 专家利用均衡 → 模型学到更丰富的表征 ``` #### 指标3:路由熵(Router Entropy) ``` X轴: 训练步数 Y轴: 路由熵(衡量专家使用的多样性) 高熵 = 均衡使用 低熵 = 倾向少数专家 蓝色: 有负载均衡 橙色: 无负载均衡 ``` **观察**: - **有负载均衡**:熵从~0.9逐渐降到~0.6,但仍保持较高值 - **无负载均衡**:熵**快速下降**到~0.45,说明过度依赖少数专家 **熵的计算公式**: ```python def entropy(): norm_scores = scores / scores.sum(dim=1, keepdim=True) entropy = -(norm_scores * torch.log2(norm_scores + eps)).sum(dim=1) return entropy.mean() ``` **含义**: ``` 高熵 (~0.6): token均匀分布在多个专家上 低熵 (~0.45): token集中在少数专家上 → 专家塌陷 ``` #### 指标4:未使用专家数 ``` X轴: 训练步数 Y轴: 完全未使用的专家数量 蓝色: 有负载均衡 橙色: 无负载均衡 ``` **观察**: - **有负载均衡**:未使用专家**接近0**,所有专家都参与 - **无负载均衡**:未使用专家**高达10-17个**(总共32个专家!) **含义**: ``` 有LB: 32个专家都在工作 无LB: 只有15-22个专家在工作,10-17个专家"失业" ``` **这是严重的问题**: - 模型容量被浪费(一半专家没用) - 训练不均衡(少数专家过载) - 泛化性能下降(模型退化为小模型) #### 指标5:负载均衡损失 ``` X轴: 训练步数 Y轴: 负载均衡损失值 蓝色: 有负载均衡 橙色: 无负载均衡(无曲线,因为未启用) ``` **观察**: - **有负载均衡**:损失从~0.092降到~0.080,**逐渐稳定** - **无负载均衡**:无曲线(lb_loss_scale=0) **负载均衡损失公式**: $$ L_{LB} = n^2 \sum_{i=1}^n f_i \cdot P_i $$ **含义**: ``` 损失下降 → 专家使用越来越均衡 稳定在低值 → 达到良好均衡状态 ``` ### 7.3 核心发现总结 | 发现 | 有负载均衡 | 无负载均衡 | 影响 | |------|-----------|-----------|------| | **训练损失** | ~1.3 | ~1.5 | 有LB更优 | | **验证损失** | ~1.4 | ~1.6 | 有LB泛化更好 | | **路由熵** | ~0.6 | ~0.45 | 有LB更多样 | | **未使用专家** | ~0 | 10-17 | 有LB充分利用 | | **专家利用率** | 100% | ~50% | 无LB浪费一半容量 | ### 7.4 负载均衡的作用机制 ``` ┌─────────────────────────────────────────────────────────────┐ │ 负载均衡如何防止专家塌陷? │ └─────────────────────────────────────────────────────────────┘ 没有负载均衡: Gate网络发现: 专家1,2,3效果最好 → 倾向只路由到这3个专家 → 其他29个专家得不到梯度更新 → 其他29个专家越来越弱 → 最终只有3个专家在工作(专家塌陷) 有负载均衡: Gate网络想: 只用专家1,2,3 → 但LB_loss惩罚这种不均衡 → 总损失 = CE_loss + LB_loss → 被迫使用其他专家来降低LB_loss → 所有专家都得到训练 → 达到均衡状态 ``` ### 7.5 代码实现(生成这些图) ```python import matplotlib.pyplot as plt import numpy as np # 准备数据 train_steps = list(range(len(lb_train_losses))) val_steps = np.linspace(0, len(train_steps), len(lb_val_losses), dtype=int).tolist() # 提取各种损失 lb_train_ce_loss = [x[1] for x in lb_train_losses] # CE损失 no_lb_train_ce_loss = [x[1] for x in no_lb_train_losses] lb_entropy = [s["entropy"] for s in lb_statistics] # 熵 lb_unused = [s["unused"] for s in lb_statistics] # 未使用专家 lb_train_lb_loss = [x[2] for x in lb_train_losses] # LB损失 # 创建5个子图 fig, axes = plt.subplots(1, 5, figsize=(15, 4)) # 图1: 训练CE损失 axes[0].plot(train_steps, lb_train_ce_loss, label="Yes", color="blue") axes[0].plot(train_steps, no_lb_train_ce_loss, label="No", color="orange") axes[0].set_title("Cross entropy training loss") axes[0].legend(title="Load balancing") # 图2: 验证CE损失 axes[1].plot(val_steps, lb_val_ce_loss, label="Yes", color="blue") axes[1].plot(val_steps, no_lb_val_ce_loss, label="No", color="orange") axes[1].set_title("Cross entropy validation loss") axes[1].legend(title="Load balancing") # 图3: 路由熵 axes[2].plot(train_steps, lb_entropy, label="Yes", color="blue") axes[2].plot(train_steps, no_lb_entropy, label="No", color="orange") axes[2].set_title('Router entropy') axes[2].legend(title="Load balancing") # 图4: 未使用专家数 axes[3].plot(train_steps, lb_unused, label="Yes", color="blue") axes[3].plot(train_steps, no_lb_unused, label="No", color="orange") axes[3].set_title('Unused Experts') axes[3].legend(title="Load balancing") # 图5: 负载均衡损失 axes[4].plot(train_steps, lb_train_lb_loss, label='Yes', color="blue") axes[4].set_title('Load Balancing Loss') axes[4].legend(title="Load balancing") plt.tight_layout() plt.show() ``` ### 7.6 实践建议 **调参指南**: ```python # 场景1: 训练刚开始 lb_loss_scale = 0.01 # 从小值开始 # 场景2: 发现很多专家未使用(>10个) lb_loss_scale = 0.05 # 增大权重 # 场景3: 专家使用均衡但训练损失高 lb_loss_scale = 0.005 # 减小权重,避免过度约束 # 场景4: 专家数很多(>64) lb_loss_scale = 0.1 # 更强的约束 ``` **监控指标**: ```python # 每N步打印统计信息 if step % 100 == 0: stats = compute_statistics() print(f"Step {step}:") print(f" 路由熵: {stats['entropy']:.3f}") print(f" 未使用专家: {stats['unused']}") # 判断标准: if stats['unused'] > 5: print("⚠️ 警告: 专家利用不均,考虑增大lb_loss_scale") elif stats['entropy'] < 0.5: print("⚠️ 警告: 熵过低,可能专家塌陷") else: print("✓ 专家利用均衡") ``` ### 7.7 总结 这张训练损失图清楚地展示了: 1. **负载均衡至关重要**:没有它,一半专家会"失业" 2. **专家塌陷是真实问题**:熵从0.9降到0.45说明严重依赖少数专家 3. **负载均衡有效**:未使用专家从15降到接近0 4. **性能提升显著**:训练和验证损失都更低 5. **需要仔细调参**:lb_loss_scale需要根据专家数和模型规模调整 **MoE训练的核心挑战**: ``` ┌─────────────────────────────────────────┐ │ 让所有专家都工作,而不是让少数专家"卷" │ └─────────────────────────────────────────┘ 没有负载均衡: 少数专家"卷",多数专家"躺平" 有负载均衡: 所有专家"共同进步" ```
MoE全代码示例
## MoE代码流程图 ### 1. 整体架构流程 ``` ┌─────────────────────────────────────────────────────────────────┐ │ MoE混合专家模型架构 │ └─────────────────────────────────────────────────────────────────┘ 输入数据 [batch, seq_len, hidden_size] │ ├─> ┌─────────────────────────────────────────────────────┐ │ │ Token Embedding Layer │ │ └─────────────────────────────────────────────────────┘ │ ↓ ├─> ┌─────────────────────────────────────────────────────┐ │ │ Position Embedding Layer │ │ └─────────────────────────────────────────────────────┘ │ ↓ ├─> ┌─────────────────────────────────────────────────────┐ │ │ Transformer Blocks │ │ │ ┌────────────────────────────────────────────────┐ │ │ │ │ Block 1, Block 2, ..., Block N │ │ │ │ │ ┌────────────────────────────────────────┐ │ │ │ │ │ │ Multi-Head Self-Attention │ │ │ │ │ │ │ + Add & Norm │ │ │ │ │ │ │ ┌────────────────────────────────────┐ │ │ │ │ │ │ │ │ MoE Layer (替换FFN) │ │ │ │ │ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ │ │ │ │ │ Gate (Linear: H → E) │ │ │ │ │ │ │ │ │ │ │ → TopK (选k个专家) │ │ │ │ │ │ │ │ │ │ │ → Softmax归一化 │ │ │ │ │ │ │ │ │ │ └────────────────────────────────┘ │ │ │ │ │ │ │ │ │ ┌────────────────────────────────┐ │ │ │ │ │ │ │ │ │ │ Expert 1: MLP │ │ │ │ │ │ │ │ │ │ │ Expert 2: MLP │ │ │ │ │ │ │ │ │ │ │ ... │ │ │ │ │ │ │ │ │ │ │ Expert E: MLP │ │ │ │ │ │ │ │ │ └────────────────────────────────┘ │ │ │ │ │ │ │ │ + 加权组合输出 │ │ │ │ │ │ │ │ + Add & Norm │ │ │ │ │ │ │ └────────────────────────────────────┘ │ │ │ │ │ └────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────┘ │ ↓ ├─> ┌─────────────────────────────────────────────────────┐ │ │ Final Layer Normalization │ │ └─────────────────────────────────────────────────────┘ │ ↓ └─> ┌─────────────────────────────────────────────────────┐ │ LM Head (Linear: H → vocab_size) │ └─────────────────────────────────────────────────────┘ ↓ 输出Logits [batch, seq_len, vocab_size] ``` ### 2. MoE层详细流程 ``` ┌─────────────────────────────────────────────────────────────────┐ │ MoE Layer 单个Token的处理流程 │ └─────────────────────────────────────────────────────────────────┘ 输入 x: [batch*seq_len, hidden_size] │ ├─> ┌────────────────────────────────────────────────────┐ │ │ 步骤1: Gate门控 (线性变换) │ │ │ gate = nn.Linear(hidden_size, num_experts) │ │ │ raw_scores = x @ W_gate → [T, E] │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 步骤2: TopK选择 (稀疏化) │ │ │ scores.topk(k, dim=-1) │ │ │ → 保留k个最大值,其余设为-inf │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 步骤3: Softmax归一化 │ │ │ softmax(scores, dim=-1) │ │ │ → 选中的专家有正值概率,未选中为0 │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 步骤4: 对每个专家分别计算 │ │ │ for i, expert in enumerate(experts): │ │ │ │ │ │ │ ├─> expert_scores = scores[:, i] │ │ │ │ (获取此专家的权重分数) │ │ │ │ │ │ │ ├─> selected_x = x[expert_scores > 0] │ │ │ │ (只选被选中的token) │ │ │ │ │ │ │ ├─> if selected_x为空: skip │ │ │ │ (节省计算) │ │ │ │ │ │ │ ├─> expert_output = expert(selected_x) │ │ │ │ (MLP前向传播) │ │ │ │ │ │ │ └─> weighted_output = expert_scores * output │ │ │ (加权) │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 步骤5: 累加所有专家输出 │ │ │ outputs[expert_scores > 0] += weighted_output │ │ └────────────────────────────────────────────────────┘ │ ↓ └─> 输出: [batch*seq_len, hidden_size] ``` ### 3. 训练流程 ``` ┌─────────────────────────────────────────────────────────────────┐ │ 训练循环流程 │ └─────────────────────────────────────────────────────────────────┘ for step in range(num_steps): │ ├─> ┌────────────────────────────────────────────────────┐ │ │ 1. 获取数据批次 │ │ │ x, y = next(dataiter) │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 2. 前向传播 │ │ │ logits = model(x) │ │ │ ├─> 穿过所有Transformer Block │ │ │ ├─> 每个Block的MoE层自动收集LB loss │ │ │ └─> _lb_losses累积所有MoE层的负载均衡损失 │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 3. 计算损失 │ │ │ ce_loss = CrossEntropy(logits, y) │ │ │ lb_loss = sum(_lb_losses) * lb_loss_scale │ │ │ total_loss = ce_loss + lb_loss │ │ │ clear_lb_losses() (清空准备下一轮) │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 4. 反向传播 │ │ │ scaler.scale(loss).backward() │ │ └────────────────────────────────────────────────────┘ │ ↓ ├─> ┌────────────────────────────────────────────────────┐ │ │ 5. 优化器更新 │ │ │ scaler.step(optimizer) │ │ │ scaler.update() │ │ │ optimizer.zero_grad() │ │ └────────────────────────────────────────────────────┘ │ ↓ └─> 每 val_every 步: 验证并打印损失 ``` ### 4. 数据流维度变化 ``` 输入 tokens: [batch=64, seq_len=64] │ ├─> Token Embedding: [64, 64, 128] ├─> + Position Embedding: [64, 64, 128] │ ├─> ┌─────────────────────────────────────┐ │ │ Block 1 │ │ │ MultiHeadAttention: [64, 64, 128] │ │ │ MoE Layer: │ │ │ flatten: [4096, 128] │ │ │ gate: [4096, 32] (32个专家) │ │ │ topk(k=2): 只保留2个专家 │ │ │ 专家输出: [4096, 128] │ │ │ reshape: [64, 64, 128] │ │ └─────────────────────────────────────┘ │ ├─> Block 2, 3, 4 (相同流程) │ └─> LM Head: [64, 64, vocab_size=65] ```
## 基础组件代码 ### 1. TopK路由函数 ```python import torch import torch.nn as nn def topk_scores(gate: nn.Linear, k: int, x: torch.Tensor): """ 计算Top-K路由分数 参数: gate: 线性门控层,将hidden_size映射到num_experts k: 选中的专家数量 x: 输入张量 [batch*seq_len, hidden_size] 返回: scores: 稀疏概率分布 [batch*seq_len, num_experts] 只对k个专家有正值,其余为0 """ # 步骤1: 线性变换计算原始分数 # gate权重: [hidden_size, num_experts] # x: [T, hidden_size] T = batch * seq_len # raw_scores: [T, num_experts] raw_scores = gate(x) # 步骤2: 只保留top-k个分数,其余设为-inf # 这里的keep_topk会同时收集负载均衡损失 scores = keep_topk(raw_scores, k) # 步骤3: Softmax归一化 # -inf → 0, 其他值转为概率 # dim=-1: 沿专家维度归一化 scores = torch.softmax(scores, dim=-1, dtype=torch.float32) return scores def keep_topk(scores: torch.Tensor, k: int) -> torch.Tensor: """ 只保留Top-K个分数,其余设为负无穷 参数: scores: 原始分数 [T, num_experts] k: 保留的最大值数量 返回: 处理后的分数,只有top-k位置保持原值,其余为-inf """ # 找到top-k的值和索引 # values: [T, k] top-k的分数值 # topk_indices: [T, k] top-k的专家索引 _, topk_indices = scores.topk(k, dim=-1) # 创建布尔掩码,初始全为False # assignment: [T, num_experts] bool assignment = torch.zeros_like(scores, dtype=torch.bool) # 将top-k位置标记为True # scatter_(-1, indices, True): 在dim=-1上,将indices位置设为True assignment.scatter_(-1, topk_indices, True) # 根据掩码选择:True位置保留原值,False位置设为-inf return torch.where(assignment, scores, float("-inf")) ``` ### 2. 专家网络创建 ```python def make_expert(hidden_size, ffn_hidden_size = None): """ 创建一个专家网络(两层MLP) 参数: hidden_size: 输入/输出的隐藏层维度 ffn_hidden_size: MLP中间层维度,默认为hidden_size的4倍 返回: nn.Sequential: 包含Linear→GELU→Linear的MLP """ if ffn_hidden_size is None: ffn_hidden_size = 4 * hidden_size # 两层MLP结构: # 1. 扩展层: hidden_size → 4*hidden_size # 2. GELU激活函数 # 3. 压缩层: 4*hidden_size → hidden_size return nn.Sequential( nn.Linear(hidden_size, ffn_hidden_size), nn.GELU(), nn.Linear(ffn_hidden_size, hidden_size), ) ``` ### 3. 专家选择性执行 ```python def run_expert(expert: nn.Module, expert_scores: torch.Tensor, x: torch.Tensor) -> torch.Tensor: """ 对专家网络进行选择性执行(只计算被选中的token) 参数: expert: 专家网络(MLP模块) expert_scores: 此专家的路由分数 [T] x: 输入张量 [T, hidden_size] 返回: 加权的专家输出,或None(如果没有token选中此专家) """ # 步骤1: 只选择得分>0的token # 利用稀疏性:大部分token的expert_scores为0 selected_x = x[expert_scores > 0] # 步骤2: 如果没有token选中此专家,跳过计算 # 这是稀疏MoE的核心优化 if len(selected_x) == 0: return None # 步骤3: 获取对应的权重并扩展维度 # expert_scores[expert_scores > 0]: 提取非零分数 # unsqueeze(-1): [N] → [N, 1] 用于广播 weights = expert_scores[expert_scores > 0].unsqueeze(-1) # 步骤4: 专家前向传播并加权 # expert(selected_x): [N, hidden_size] # weights * expert(selected_x): 逐元素加权 return weights * expert(selected_x) ``` ### 4. 配置数据类 ```python import dataclasses @dataclasses.dataclass class Config: """MoE Transformer配置""" # 序列长度 seq_len: int = 64 # Transformer架构参数 n_layers: int = 4 # 层数 n_heads: int = 4 # 注意力头数 hidden_size: int = 128 # 隐藏层维度 ffn_hidden_size: int = 256 # FFN中间层维度 head_size: int = 32 # 每个注意力头的维度 vocab_size: int = 65 # 词汇表大小 # MoE特定参数 k: int = 2 # 每个token选中k个专家 normalize_scores: int = True # 是否归一化分数 n_experts: int = 32 # 专家总数 ```
## 损失函数代码 ### 1. 负载均衡损失 ```python import einops def load_balancing_loss( raw_scores: torch.Tensor, token_assignment: torch.Tensor, ) -> torch.Tensor: """ 计算负载均衡损失(来自GShard/Switch-Transformer论文) 鼓励token均匀分配到所有专家,防止专家利用不均 参数: raw_scores: 门控原始分数 [T, num_experts] token_assignment: 布尔分配矩阵 [T, num_experts] True表示token被分配到此专家 返回: 标量损失值 """ # 获取专家数量 _, num_experts = raw_scores.size() # 步骤1: 将原始分数转为概率分布 # 用于计算P_i(每个专家的平均概率) raw_scores = torch.softmax(raw_scores, dim=-1, dtype=torch.float32) # 步骤2: 计算每个专家接收的token比例 f_i # token_assignment: [T, E] bool # reduce(..., "t e -> e", "mean"): 沿token维度求平均 # token_fractions: [E] 每个专家平均接收多少比例的token token_fractions = einops.reduce(token_assignment.float(), "t e -> e", "mean") # 步骤3: 计算每个专家的平均门控概率 P_i # raw_scores: [T, E] # reduce(..., "t e -> e", "mean"): 沿token维度求平均 # average_weights: [E] 每个专家的平均得分 average_weights = einops.reduce(raw_scores, "t e -> e", "mean") # 步骤4: 计算负载均衡损失 # L_LB = n^2 * mean(f_i * P_i) # num_experts ** 2: 归一化系数(来自论文) # (token_fractions * average_weights): 逐元素相乘 # .mean(): 对所有专家求平均 return num_experts ** 2 * (token_fractions * average_weights).mean() ``` ### 2. 损失收集管理 ```python # 全局损失列表,收集所有MoE层的负载均衡损失 _lb_losses = [] def append_lb_loss(loss: torch.Tensor) -> None: """ 将一个MoE层的负载均衡损失添加到全局列表 参数: loss: 来自单个MoE层的损失值(标量张量) """ global _lb_losses # 断言:损失必须非负 assert (loss >= 0.0).all(), f"Negative loss: {loss.item()}" _lb_losses.append(loss) def get_lb_losses() -> list[torch.Tensor]: """ 获取累积的所有负载均衡损失 返回: 损失张量列表,每个元素来自一个MoE层 """ global _lb_losses return _lb_losses def clear_lb_losses() -> None: """清空损失列表,为下一轮训练做准备""" global _lb_losses _lb_losses = [] ``` ### 3. 统计监控 ```python from contextlib import contextmanager # 全局统计字典 _statistics = {} # 是否启用统计收集 _statistics_enabled = False @contextmanager def capture_statistics(enable=True): """ 上下文管理器,用于启用/禁用统计收集 使用: with capture_statistics(enable=True): output = model(x) stats = compute_statistics() """ global _statistics_enabled prev_value = _statistics_enabled _statistics_enabled = enable try: yield finally: _statistics_enabled = prev_value def save_statistics(layer_idx: int, topk_scores: torch.Tensor): """ 保存单个MoE层的统计信息 参数: layer_idx: MoE层的索引 topk_scores: TopK路由后的分数 [T, num_experts] """ def compute() -> dict[str, float]: """计算统计指标""" def entropy(): """计算路由熵:衡量路由的确定性""" entropy_eps = 1e-10 # 防止log(0) # 归一化分数(每行和为1) norm_scores = topk_scores / topk_scores.sum(dim=1, keepdim=True) # 计算熵: H = -sum(p * log2(p)) # tokens_entropy: [T] 每个token的熵 tokens_entropy = -(norm_scores * torch.log2(norm_scores + entropy_eps) ).sum(dim=1) # 返回平均熵 return tokens_entropy.mean().item() def unused(): """统计未被使用的专家数量""" # 计算每个专家处理了多少token # topk_scores > 0: 布尔矩阵 # reduce(..., "t e -> e", "sum"): 每个专家的token数 expert_load = einops.reduce(topk_scores > 0, "t e -> e", "sum") # 统计负载为0的专家数量 return int((expert_load == 0).sum().item()) return { "entropy": entropy(), # 平均路由熵 "unused": unused(), # 未使用专家数 } global _statistics, _statistics_enabled # 如果未启用统计,直接返回 if not _statistics_enabled: return # 确保同一层只保存一次 assert layer_idx not in _statistics, f"Repeated layer {layer_idx}" # 保存统计信息 _statistics[layer_idx] = compute() def compute_statistics(): """ 汇总所有MoE层的统计信息 返回: dict: {"entropy": 平均熵, "unused": 总未使用专家数} """ global _statistics # 初始化汇总统计 stats = { "entropy": 0.0, "unused": 0, } # 累加每层的统计 for layer_statistics in _statistics.values(): stats["entropy"] += layer_statistics["entropy"] stats["unused"] += layer_statistics["unused"] # 计算平均熵 stats["entropy"] /= len(_statistics) return stats def clear_statistics(): """清空统计信息""" global _statistics _statistics = {} ``` ### 4. 更新的keep_topk(包含损失收集) ```python def keep_topk(scores: torch.Tensor, k: int) -> torch.Tensor: """ 更新版keep_topk,自动收集负载均衡损失 参数和返回值与之前相同,但会自动调用append_lb_loss """ # 找到top-k索引 _, topk_indices = scores.topk(k, dim=-1) # 创建分配掩码 assignment = torch.zeros_like(scores, dtype=torch.bool) assignment.scatter_(-1, topk_indices, True) # 计算并保存负载均衡损失 append_lb_loss(load_balancing_loss(scores, assignment)) # 返回稀疏分数 return torch.where(assignment, scores, float("-inf")) ```
## MoELayer核心代码 ### 1. MoELayer完整实现 ```python class MoELayer(nn.Module): """ 稀疏混合专家层 实现: 1. 门控网络:学习路由决策 2. 专家网络:多个MLP专家 3. Top-K路由:稀疏化激活 4. 负载均衡:防止专家利用不均 """ def __init__( self, *, hidden_size: int, # 输入/输出的隐藏层维度 num_experts: int, # 专家数量 k: int, # 每个token选中的专家数 idx: int, # 层索引(用于统计) ffn_hidden_size=None, # 专家MLP的中间层维度 ): super().__init__() # 门控网络:将输入映射到专家权重 # 输入: [T, hidden_size] # 输出: [T, num_experts] self.gate = nn.Linear(hidden_size, num_experts) # 创建专家列表 # 每个专家是一个MLP (make_expert定义见前) self.experts = nn.ModuleList( [make_expert(hidden_size, ffn_hidden_size) for _ in range(num_experts)] ) self.k = k # 保存top-k参数 self.idx = idx # 保存层索引 def forward(self, x: torch.Tensor) -> torch.Tensor: """ MoE层前向传播 参数: x: 输入张量,形状 [batch, seq_len, hidden_size] 返回: 输出张量,形状 [batch, seq_len, hidden_size] """ # 步骤1: 展平所有非特征维度 # [batch, seq_len, hidden_size] → [batch*seq_len, hidden_size] # 这样可以统一处理batch和seq_len维度 x_flat = x.view(-1, x.size(-1)) # 步骤2: 获取路由分数 # scores: [T, num_experts] # 只有k个专家位置有正值,其余为0 scores = topk_scores(self.gate, self.k, x_flat) # 步骤3: 保存统计信息(熵、未使用专家数) save_statistics(self.idx, scores) # 步骤4: 初始化输出张量 # 与输入形状相同,初始为0 outputs = torch.zeros_like(x_flat) # 步骤5: 对每个专家分别计算 for i, expert in enumerate(self.experts): # 获取此专家的路由分数 # expert_scores: [T] 每个token对专家i的分数 expert_scores = scores[:, i] # 执行专家计算(只处理被选中的token) expert_outputs = run_expert(expert, expert_scores, x_flat) # 如果此专家未被任何token选中,跳过 if expert_outputs is None: continue # 累加到总输出 # 只有被选中的token位置才更新 outputs[expert_scores > 0] += expert_outputs # 步骤6: 恢复原始形状 # [T, hidden_size] → [batch, seq_len, hidden_size] outputs = outputs.view_as(x) return outputs ``` ### 2. MoELayer使用示例 ```python # 创建MoE层 moe_layer = MoELayer( hidden_size=128, # 输入维度 num_experts=32, # 32个专家 k=2, # 每个token选2个专家 idx=0, # 第0层 ffn_hidden_size=512, # 专家中间层维度 ) # 前向传播 batch_size = 4 seq_len = 8 hidden_size = 128 x = torch.randn(batch_size, seq_len, hidden_size) output = moe_layer(x) print(f"输入形状: {x.shape}") # [4, 8, 128] print(f"输出形状: {output.shape}") # [4, 8, 128] ``` ### 3. MoELayer关键特性总结 | 特性 | 实现 | 作用 | |------|------|------| | 稀疏激活 | Top-K路由 | 只计算k个专家,节省计算 | | 可微分 | Softmax | 梯度可流向选中的专家 | | 选择性计算 | `if len==0: skip` | 空专家不计算 | | 负载均衡 | `load_balancing_loss` | 鼓励均匀分配 | | 统计监控 | `save_statistics` | 监控熵和未使用专家 |
## Transformer集成代码 ### 1. 多头注意力层 ```python import einops import torch.nn as nn import torch.nn.functional as F class MultiHeadAttention(nn.Module): """多头自注意力机制""" def __init__(self, config: Config): super().__init__() self.config = config # Q, K, V投影 # 从hidden_size投影到 n_heads * head_size self.queries_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) self.keys_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) self.values_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) # 输出投影 self.out_proj = nn.Linear(config.n_heads * config.head_size, config.hidden_size) def forward(self, x: torch.Tensor, is_causal: bool = True): """ 前向传播 参数: x: [batch, seq_len, hidden_size] is_causal: 是否使用因果掩码(用于自回归生成) 返回: [batch, seq_len, hidden_size] """ _, T, _ = x.size() n_heads, head_dim = self.config.n_heads, self.config.head_size # 步骤1: 计算Q, K, V q, k, v = (self.queries_proj(x), self.keys_proj(x), self.values_proj(x)) # 步骤2: 重塑为多头形式 # [B, T, n_heads * head_dim] → [B, n_heads, T, head_dim] q = einops.rearrange(q, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) k = einops.rearrange(k, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) v = einops.rearrange(v, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) # 步骤3: 缩放点积注意力 # PyTorch内置的高效实现,支持因果掩码 out = F.scaled_dot_product_attention( q, k, v, dropout_p=0.0, is_causal=is_causal ) # 步骤4: 合并多头 # [B, n_heads, T, head_dim] → [B, T, n_heads * head_dim] out = einops.rearrange(out, "b head t proj -> b t (head proj)") # 步骤5: 输出投影 out = self.out_proj(out) return out ``` ### 2. Transformer Block ```python class Block(nn.Module): """ Transformer Block 结构: MultiHeadAttention → Add & Norm → MoE → Add & Norm """ def __init__(self, config: Config, idx: int): super().__init__() # 自注意力层 self.attn = MultiHeadAttention(config) # 注意力后的层归一化 self.attn_norm = nn.LayerNorm(config.hidden_size) # MoE层(替换传统的FFN) self.mlp = MoELayer( hidden_size=config.hidden_size, num_experts=config.n_experts, k=config.k, idx=idx, # 层索引用于统计 ffn_hidden_size=config.ffn_hidden_size, ) # MoE后的层归一化 self.mlp_norm = nn.LayerNorm(config.hidden_size) def forward(self, x: torch.Tensor) -> torch.Tensor: """ Block前向传播 参数: x: [batch, seq_len, hidden_size] 返回: [batch, seq_len, hidden_size] """ # 注意子网络 + 残差连接 h = self.attn_norm(x) # Pre-Norm h = self.attn(h) # 自注意力 x = x + h # 残差连接 # MoE子网络 + 残差连接 h = self.mlp_norm(x) # Pre-Norm h = self.mlp(h) # MoE层 x = x + h # 残差连接 return x ``` ### 3. 完整Transformer模型 ```python class Transformer(nn.Module): """ 带MoE的Transformer语言模型 结构: Token Embedding + Position Embedding + N × Transformer Block (Attn + MoE) + Layer Norm + LM Head """ def __init__(self, config: Config): super().__init__() # Token嵌入层 self.tok_emb = nn.Embedding(config.vocab_size, config.hidden_size) # 位置嵌入层 self.pos_emb = nn.Embedding(config.seq_len, config.hidden_size) # Transformer Block堆叠 self.blocks = nn.ModuleList( Block(config, i) for i in range(config.n_layers) ) # 最终层归一化 self.final_norm = nn.LayerNorm(config.hidden_size) # 语言模型头(投影到词汇表) self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) def forward(self, tokens: torch.Tensor) -> torch.Tensor: """ 前向传播 参数: tokens: [batch, seq_len] 输入token索引 返回: logits: [batch, seq_len, vocab_size] 预测分数 """ # 步骤1: Token嵌入 tok_emb = self.tok_emb(tokens) # [B, T, H] # 步骤2: 位置嵌入 pos_emb = self._get_pos_embeddings(tokens) # [T, H] # 步骤3: 组合嵌入 hidden_states = tok_emb + pos_emb # [B, T, H] # 步骤4: 通过所有Transformer Block for block in self.blocks: hidden_states = block(hidden_states) # 步骤5: 最终归一化 hidden_states = self.final_norm(hidden_states) # 步骤6: 投影到词汇表 logits = self.lm_head(hidden_states) return logits def _get_pos_embeddings(self, tokens: torch.Tensor) -> torch.Tensor: """ 获取位置嵌入 参数: tokens: [batch, seq_len] 返回: [seq_len, hidden_size] """ batch_size, seq_len = tokens.size() # 创建位置索引 [0, 1, 2, ..., seq_len-1] pos_idx = torch.arange(seq_len, device=tokens.device) # 查找位置嵌入 pos_emb = self.pos_emb(pos_idx) return pos_emb ``` ### 4. 模型实例化示例 ```python # 创建配置 config = Config( seq_len=64, n_layers=4, n_heads=4, hidden_size=128, ffn_hidden_size=256, head_size=32, vocab_size=65, k=2, n_experts=32, ) # 创建模型 model = Transformer(config) # 计算参数量 params = sum(p.numel() for p in model.parameters()) print(f"总参数量: {params / 1e6:.2f}M") # 前向传播 batch_size = 4 seq_len = 64 tokens = torch.randint(0, config.vocab_size, (batch_size, seq_len)) logits = model(tokens) print(f"输出形状: {logits.shape}") # [4, 64, 65] ```
## 训练与生成代码 ### 1. 前向传播函数 ```python import torch.nn.functional as F def forward( *, model: Transformer, lb_loss_scale: float, # 负载均衡损失的权重系数 device_type: str, # 设备类型 ("cuda" 或 "cpu") x: torch.Tensor, # 输入token [batch, seq_len] y: torch.Tensor, # 目标token [batch, seq_len] ): """ 模型前向传播并计算损失 返回: (总损失, 交叉熵损失, 负载均衡损失) """ # 使用混合精度训练提高速度 with torch.autocast(device_type=device_type, dtype=torch.float16): # 步骤1: 模型前向传播 logits = model(x) # [B, T, vocab_size] # 步骤2: 重塑为2D用于交叉熵 logits = einops.rearrange(logits, "b s e -> (b s) e") y = einops.rearrange(y, "b s -> (b s)") # 步骤3: 计算交叉熵损失 ce_loss = F.cross_entropy(logits, y) # 步骤4: 累加所有MoE层的负载均衡损失 lb_loss = torch.tensor(0.0, device=ce_loss.device) for l in get_lb_losses(): lb_loss += l # 步骤5: 应用损失权重 lb_loss = lb_loss_scale * lb_loss # 步骤6: 总损失 loss = ce_loss + lb_loss # 步骤7: 清空损失列表 clear_lb_losses() return loss, ce_loss.item(), lb_loss.item() ``` ### 2. 训练步骤函数 ```python def train_step( *, model: Transformer, optimizer: torch.optim.Optimizer, scaler, # GradScaler用于混合精度 lb_loss_scale: float, device_type: str, x: torch.Tensor, y: torch.Tensor, ): """ 执行一个训练步骤 返回: (总损失, 交叉熵损失, 负载均衡损失) """ # 步骤1: 前向传播并计算损失 loss, ce_loss, lb_loss = forward( model=model, lb_loss_scale=lb_loss_scale, device_type=device_type, x=x, y=y ) # 步骤2: 反向传播(混合精度) scaler.scale(loss).backward() # 步骤3: 梯度裁剪和优化器更新 scaler.step(optimizer) # 步骤4: 更新scaler scaler.update() # 步骤5: 清零梯度 optimizer.zero_grad(set_to_none=True) return loss.item(), ce_loss, lb_loss ``` ### 3. 验证函数 ```python @torch.no_grad() def validate( *, model: Transformer, val_dataloader: torch.utils.data.DataLoader, lb_loss_scale: float, device: str, ): """ 验证模型性能 返回: (平均总损失, 平均CE损失, 平均LB损失) """ # 设置为评估模式 model.eval() # 初始化累加器 total_loss = 0.0 total_ce_loss = 0.0 total_lb_loss = 0.0 # 遍历验证集 for x, y in val_dataloader: x, y = x.to(device), y.to(device) # 前向传播 loss, ce_loss, lb_loss = forward( model=model, lb_loss_scale=lb_loss_scale, device_type=device, x=x, y=y ) # 累加损失 total_loss += loss.item() total_ce_loss += ce_loss total_lb_loss += lb_loss # 计算平均 total_loss /= len(val_dataloader) total_ce_loss /= len(val_dataloader) total_lb_loss /= len(val_dataloader) # 清空损失列表 clear_lb_losses() # 恢复训练模式 model.train() return total_loss, total_ce_loss, total_lb_loss ``` ### 4. 文本生成函数 ```python @torch.no_grad() def generate( model: nn.Module, vocab: Vocabulary, input: str, # 输入文本 new_tokens: int, # 要生成的新token数 device, ): """ 自回归生成文本 返回: 生成的完整文本(输入+生成) """ # 设置为评估模式 model.eval() # 步骤1: 编码输入文本 idx = vocab.encode(input) # 步骤2: 转为张量并添加batch维度 idx = torch.tensor(idx, dtype=torch.long, device=device) idx = einops.rearrange(idx, "... -> 1 ...") # [1, T] # 步骤3: 自回归生成 for t in range(new_tokens): # 只使用最近的seq_len个token(上下文窗口) ctx = idx[:, -seq_len:] # 前向传播 with torch.autocast(device_type=device, dtype=torch.float16): logits = model(ctx) # 取最后一个位置的logits next_logits = logits[:, -1, :] # Softmax转为概率 next_probs = torch.softmax(next_logits, dim=-1) # 多项式采样(而不是贪心) next_idx = torch.multinomial(next_probs, num_samples=1) # 拼接到序列 idx = torch.cat((idx, next_idx), dim=1) # 步骤4: 解码为文本 idx = idx.tolist()[0] output = vocab.decode(idx) # 清理 clear_lb_losses() model.train() return output ``` ### 5. 训练主循环 ```python import time import random def set_seed(seed=42): """设置随机种子""" random.seed(seed) torch.manual_seed(seed) torch.set_float32_matmul_precision("high") def train_model( *, config=Config(), lb_loss_scale=0.01, # 负载均衡损失权重 num_steps=2000, # 训练步数 val_every=500, # 验证频率 log_every=50, # 日志频率 ): """完整的训练循环""" set_seed() # 设备选择 device = "cuda" if torch.cuda.is_available() else "cpu" if device == "cpu": print("WARNING: Using cpu device, training will be slow") # 数据加载器 train_dataloader = torch.utils.data.DataLoader( train, batch_size=64, shuffle=True, pin_memory=True, num_workers=2, prefetch_factor=2, ) val_dataloader = torch.utils.data.DataLoader( val, batch_size=64, shuffle=False, pin_memory=True, num_workers=2, prefetch_factor=2, ) # 创建模型 model = Transformer(config) model_params = sum(p.numel() for p in model.parameters()) print(f"Total parameters: {model_params / 1e6:.2f}M") # 优化器 optimizer = torch.optim.AdamW( model.parameters(), lr=2e-3, weight_decay=0.1, betas=(0.9, 0.95), ) # 混合精度训练 scaler = torch.amp.GradScaler() # 移动到设备 model.to(device) # 创建数据迭代器 dataiter = iter(train_dataloader) # 初始化 start_time = time.time() start_step = time.time() train_losses = [] val_losses = [] # 初始验证 val_losses.append(run_validation( model=model, val_dataloader=val_dataloader, lb_loss_scale=lb_loss_scale, device=device, )) statistics = [] # 训练循环 for step in range(1, num_steps + 1): # 获取数据 x, y = next(dataiter) x, y = x.to(device), y.to(device) token_count = x.numel() # 训练步骤(收集统计) with capture_statistics(enable=True): loss, ce_loss, lb_loss = train_step( model=model, optimizer=optimizer, scaler=scaler, lb_loss_scale=lb_loss_scale, device_type=device, x=x, y=y ) step_statistics = compute_statistics() clear_statistics() # 记录损失 train_losses.append((loss, ce_loss, lb_loss)) statistics.append(step_statistics) # 计算步时间 step_time = time.time() - start_step start_step = time.time() # 打印日志 if step % log_every == 0: completed = step / num_steps * 100 elapsed = time.time() - start_time tok_per_sec = token_count / step_time fmt_elapsed = time.strftime("%H:%M:%S", time.gmtime(elapsed)) print( f"Step {step}/{num_steps} ({completed:.0f}%): " f"TrainTime={fmt_elapsed}, " f"Loss={loss:.3f}, " f"Entropy={step_statistics['entropy']:.3}, " f"Unused={step_statistics['unused']}, " f"StepTime={step_time:.2}s" ) # 定期验证 if step % val_every == 0: val_losses.append(run_validation( model=model, val_dataloader=val_dataloader, lb_loss_scale=lb_loss_scale, device=device )) # 最终生成示例 test_input = "\n" test_gen_len = 1000 test_output = generate( model, vocab, test_input, test_gen_len, device ) print("Final generation", "-" * 30) print(test_output) return train_losses, val_losses, statistics ```
## 完整代码汇总 ```python """ MoE (Mixture of Experts) 完整实现 基于GShard和Switch-Transformer的稀疏MoE层 主要组件: 1. TopK路由机制 2. 负载均衡损失 3. 稀疏专家执行 4. Transformer集成 """ import torch import torch.nn as nn import torch.nn.functional as F import einops import dataclasses from contextlib import contextmanager # ============================================================================ # 配置 # ============================================================================ @dataclasses.dataclass class Config: """模型配置""" seq_len: int = 64 n_layers: int = 4 n_heads: int = 4 hidden_size: int = 128 ffn_hidden_size: int = 256 head_size: int = 32 vocab_size: int = 65 # MoE参数 k: int = 2 normalize_scores: int = True n_experts: int = 32 # ============================================================================ # 基础组件 # ============================================================================ def keep_topk(scores: torch.Tensor, k: int) -> torch.Tensor: """只保留top-k个分数,其余设为-inf""" _, topk_indices = scores.topk(k, dim=-1) assignment = torch.zeros_like(scores, dtype=torch.bool) assignment.scatter_(-1, topk_indices, True) append_lb_loss(load_balancing_loss(scores, assignment)) return torch.where(assignment, scores, float("-inf")) def topk_scores(gate: nn.Linear, k: int, x: torch.Tensor): """计算top-k路由分数""" raw_scores = gate(x) scores = keep_topk(raw_scores, k) scores = torch.softmax(scores, dim=-1, dtype=torch.float32) return scores def make_expert(hidden_size, ffn_hidden_size = None): """创建专家网络(两层MLP)""" if ffn_hidden_size is None: ffn_hidden_size = 4 * hidden_size return nn.Sequential( nn.Linear(hidden_size, ffn_hidden_size), nn.GELU(), nn.Linear(ffn_hidden_size, hidden_size), ) def run_expert(expert: nn.Module, expert_scores: torch.Tensor, x: torch.Tensor) -> torch.Tensor: """选择性执行专家计算""" selected_x = x[expert_scores > 0] if len(selected_x) == 0: return None weights = expert_scores[expert_scores > 0].unsqueeze(-1) return weights * expert(selected_x) # ============================================================================ # 损失函数 # ============================================================================ _lb_losses = [] def append_lb_loss(loss: torch.Tensor) -> None: """添加负载均衡损失到全局列表""" global _lb_losses assert (loss >= 0.0).all() _lb_losses.append(loss) def get_lb_losses() -> list[torch.Tensor]: """获取所有负载均衡损失""" global _lb_losses return _lb_losses def clear_lb_losses() -> None: """清空损失列表""" global _lb_losses _lb_losses = [] def load_balancing_loss( raw_scores: torch.Tensor, token_assignment: torch.Tensor, ) -> torch.Tensor: """计算负载均衡损失""" _, num_experts = raw_scores.size() raw_scores = torch.softmax(raw_scores, dim=-1, dtype=torch.float32) token_fractions = einops.reduce(token_assignment.float(), "t e -> e", "mean") average_weights = einops.reduce(raw_scores, "t e -> e", "mean") return num_experts ** 2 * (token_fractions * average_weights).mean() # ============================================================================ # 统计监控 # ============================================================================ _statistics = {} _statistics_enabled = False @contextmanager def capture_statistics(enable=True): """启用统计收集的上下文管理器""" global _statistics_enabled prev_value = _statistics_enabled _statistics_enabled = enable try: yield finally: _statistics_enabled = prev_value def save_statistics(layer_idx: int, topk_scores: torch.Tensor): """保存MoE层的统计信息""" def compute() -> dict[str, float]: entropy_eps = 1e-10 norm_scores = topk_scores / topk_scores.sum(dim=1, keepdim=True) tokens_entropy = -(norm_scores * torch.log2(norm_scores + entropy_eps)).sum(dim=1) expert_load = einops.reduce(topk_scores > 0, "t e -> e", "sum") return { "entropy": tokens_entropy.mean().item(), "unused": int((expert_load == 0).sum().item()), } global _statistics, _statistics_enabled if not _statistics_enabled: return assert layer_idx not in _statistics _statistics[layer_idx] = compute() def compute_statistics(): """汇总所有统计""" global _statistics stats = {"entropy": 0.0, "unused": 0} for layer_statistics in _statistics.values(): stats["entropy"] += layer_statistics["entropy"] stats["unused"] += layer_statistics["unused"] stats["entropy"] /= len(_statistics) return stats def clear_statistics(): """清空统计""" global _statistics _statistics = {} # ============================================================================ # MoE层 # ============================================================================ class MoELayer(nn.Module): """稀疏混合专家层""" def __init__(self, *, hidden_size: int, num_experts: int, k: int, idx: int, ffn_hidden_size=None): super().__init__() self.gate = nn.Linear(hidden_size, num_experts) self.experts = nn.ModuleList( [make_expert(hidden_size, ffn_hidden_size) for _ in range(num_experts)] ) self.k = k self.idx = idx def forward(self, x: torch.Tensor): x_flat = x.view(-1, x.size(-1)) scores = topk_scores(self.gate, self.k, x_flat) save_statistics(self.idx, scores) outputs = torch.zeros_like(x_flat) for i, expert in enumerate(self.experts): expert_scores = scores[:, i] expert_outputs = run_expert(expert, expert_scores, x_flat) if expert_outputs is None: continue outputs[expert_scores > 0] += expert_outputs return outputs.view_as(x) # ============================================================================ # Transformer组件 # ============================================================================ class MultiHeadAttention(nn.Module): """多头自注意力""" def __init__(self, config: Config): super().__init__() self.config = config self.queries_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) self.keys_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) self.values_proj = nn.Linear(config.hidden_size, config.n_heads * config.head_size) self.out_proj = nn.Linear(config.n_heads * config.head_size, config.hidden_size) def forward(self, x: torch.Tensor, is_causal: bool = True): _, T, _ = x.size() n_heads, head_dim = self.config.n_heads, self.config.head_size q, k, v = (self.queries_proj(x), self.keys_proj(x), self.values_proj(x)) q = einops.rearrange(q, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) k = einops.rearrange(k, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) v = einops.rearrange(v, "b t (head proj) -> b head t proj", head=n_heads, proj=head_dim) out = F.scaled_dot_product_attention(q, k, v, dropout_p=0.0, is_causal=is_causal) out = einops.rearrange(out, "b head t proj -> b t (head proj)") return self.out_proj(out) class Block(nn.Module): """Transformer Block (Attention + MoE)""" def __init__(self, config: Config, idx: int): super().__init__() self.attn = MultiHeadAttention(config) self.attn_norm = nn.LayerNorm(config.hidden_size) self.mlp = MoELayer( hidden_size=config.hidden_size, num_experts=config.n_experts, k=config.k, idx=idx, ffn_hidden_size=config.ffn_hidden_size, ) self.mlp_norm = nn.LayerNorm(config.hidden_size) def forward(self, x: torch.Tensor): h = self.attn_norm(x) h = self.attn(h) x = x + h h = self.mlp_norm(x) h = self.mlp(h) x = x + h return x class Transformer(nn.Module): """MoE Transformer语言模型""" def __init__(self, config: Config): super().__init__() self.tok_emb = nn.Embedding(config.vocab_size, config.hidden_size) self.pos_emb = nn.Embedding(config.seq_len, config.hidden_size) self.blocks = nn.ModuleList(Block(config, i) for i in range(config.n_layers)) self.final_norm = nn.LayerNorm(config.hidden_size) self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False) def forward(self, tokens: torch.Tensor): tok_emb = self.tok_emb(tokens) pos_emb = self._get_pos_embeddings(tokens) hidden_states = tok_emb + pos_emb for block in self.blocks: hidden_states = block(hidden_states) hidden_states = self.final_norm(hidden_states) logits = self.lm_head(hidden_states) return logits def _get_pos_embeddings(self, tokens: torch.Tensor): batch_size, seq_len = tokens.size() pos_idx = torch.arange(seq_len, device=tokens.device) return self.pos_emb(pos_idx) ```
技术资料搜索方法
## 技术资料搜索策略总览 ### 1.1 搜索路径图解 当你想了解一个新模型(如Qwen3-VL)时,按照以下路径搜索资料: ``` ┌─────────────────────────────────────────────────────────────────┐ │ 大模型资料搜索路径 │ └─────────────────────────────────────────────────────────────────┘ 第一步:确定模型名称 │ ├─> 官方名称: Qwen3-VL, GPT-4, Claude 3.5, Llama 3 └─> 研究代号: 可能与发布名称不同 │ 第二步:搜索学术论文(ArXiv优先) │ ├─> ArXiv.org → 搜索模型名 + "arxiv" ├─> Google Scholar → 搜索模型名 ├─> Semantic Scholar → 搜索模型名 └─> 获取: PDF技术报告、引用关系 │ 第三步:查找官方代码仓库 │ ├─> GitHub.com → 搜索组织名/模型名 ├─> Hugging Face → 搜索模型卡片 ├─> 官方网站 → 通常有GitHub链接 └─> 获取: 源代码、预训练权重、使用示例 │ 第四步:收集文档与教程 │ ├─> 官方文档(README、API文档) ├─> 技术博客(官方/非官方) ├─> 视频教程(YouTube、Bilibili) └─> 获取: 使用指南、最佳实践 │ 第五步:社区讨论与评测 │ ├─> Reddit (r/MachineLearning) ├─> Hugging Face Forums ├─> Twitter/X(作者动态) └─> 获取: 实战经验、性能评测 ``` ### 1.2 搜索优先级 | 优先级 | 资源类型 | 理由 | |--------|---------|------| | **P0** | ArXiv论文 | 最权威的技术细节 | | **P0** | 官方GitHub | 代码实现和使用示例 | | **P1** | Hugging Face | 预训练权重和模型卡片 | | **P1** | 官方文档 | API说明和使用指南 | | **P2** | 技术博客 | 直观的理解和解读 | | **P2** | 社区讨论 | 实战经验和踩坑记录 | ### 1.3 核心搜索公式 ``` 论文 = "模型名" + "arxiv" + "pdf" 例如: "Qwen3-VL arxiv pdf" 代码 = "模型名" + "github" + "官方组织名" 例如: "Qwen3-VL github QwenLM" 权重 = "模型名" + "huggingface" 例如: "Qwen3-VL huggingface" 文档 = "模型名" + "documentation" + "官方" 例如: "Qwen3-VL documentation" ``` ### 1.4 搜索流程示意图 ``` ┌─────────────┐ │ 输入模型名 │ └──────┬──────┘ │ ├─> ┌──────────────────────────────────────┐ │ │ ArXiv搜索 │ │ │ 搜索: "模型名 arxiv" │ │ │ 获取: 技术报告PDF │ │ └──────────────────────────────────────┘ │ ├─> ┌──────────────────────────────────────┐ │ │ GitHub搜索 │ │ │ 搜索: "模型名 github" │ │ │ 获取: 源码、权重、示例 │ │ └──────────────────────────────────────┘ │ ├─> ┌──────────────────────────────────────┐ │ │ Hugging Face │ │ │ 搜索: "模型名 huggingface" │ │ │ 获取: 模型卡片、在线试用 │ │ └──────────────────────────────────────┘ │ └─> ┌──────────────────────────────────────┐ │ 综合搜索(Google) │ │ 搜索: "模型名 tutorial / 使用指南" │ │ 获取: 博客、视频、社区讨论 │ └──────────────────────────────────────┘ ``` ### 1.5 快速检查清单 当你找到资料后,按以下清单验证: ```markdown ## 资料验证清单 - [ ] 论文: 找到ArXiv链接,PDF可下载 - [ ] 代码: 找到官方GitHub仓库 - [ ] 权重: 找到Hugging Face或官方下载链接 - [ ] 文档: README有基本使用说明 - [ ] 示例: 有运行示例代码 - [ ] 许可证: 确认使用许可(Apache 2.0 / MIT / GPL等) - [ ] 依赖: 明确环境要求(Python版本、依赖包) - [ ] 社区: 有Issue或Discussions可以提问 ```
## 论文搜索方法 ### 2.1 ArXiv搜索技巧 **基础搜索**: ``` 直接访问: https://arxiv.org/ 搜索框输入: "Qwen3-VL" 或 "Qwen VL" ``` **高级搜索语法**: ``` # 按标题搜索 ti:"Qwen VL" # 按作者搜索 au:"Bai" # 按机构搜索 ac:"Alibaba" # 组合搜索 ti:"Qwen" AND cat:"cs.CV" # 计算机视觉分类 # 按时间排序 提交日期排序(最新优先) 相关性排序(最相关优先) ``` **ArXiv分类参考**: - `cs.CV` - 计算机视觉 - `cs.CL` - 计算语言学 - `cs.AI` - 人工智能 - `cs.LG` - 机器学习 - `stat.ML` - 统计机器学习 ### 2.2 Google Scholar搜索 ``` 访问: https://scholar.google.com/ 搜索公式: "模型名" + "arxiv" "模型名" + "paper" "模型名" + "technical report" 例如: "Qwen3-VL arxiv" "Llama 3 technical report" ``` **Scholar高级技巧**: - 点击"引用" → 查看"被引用次数"(评估影响力) - 点击"相关文章" → 发现相关研究 - 设置"邮件提醒" → 新论文自动通知 ### 2.3 Semantic Scholar ``` 访问: https://www.semanticscholar.org/ 特点: - 更好的引用关系可视化 - 自动提取论文关键点 - 显示论文的影响力指标 ``` ### 2.4 论文PDF获取途径 | 来源 | 获取方式 | 说明 | |------|---------|------| | **ArXiv** | 直接下载PDF | 最新预印本,免费 | | **Google Scholar** | 查找[PDF]链接 | 可能有多个版本 | | **OpenReview** | 会议评审版本 | ICLR/NeurIPS等 | | **官方网站** | 研究机构发布 | 最终发表版本 | ### 2.5 论文阅读快速路径 ``` ┌─────────────────────────────────────────────────────────────┐ │ 论文阅读路径(30分钟) │ └─────────────────────────────────────────────────────────────┘ 第1步(5分钟): 读摘要和结论 ├─> 了解: 模型解决了什么问题 ├─> 了解: 主要创新点是什么 └─> 了解: 性能如何 第2步(10分钟): 读架构图 ├─> 关注: 整体架构图(Figure 1) ├─> 理解: 输入输出流程 └─> 标注: 关键模块名称 第3步(10分钟): 读方法部分 ├─> 关注: 关键公式 ├─> 理解: 损失函数 └─> 记录: 超参数值 第4步(5分钟): 读实验部分 ├─> 查看: 基线对比 ├─> 了解: 消融实验 └─> 记录: 最佳配置 ``` ### 2.6 关键信息提取表 阅读论文时填写此表: ```markdown ## 论文摘要表 **模型名称**: Qwen3-VL **论文标题**: Qwen3-VL Technical Report **ArXiv链接**: https://arxiv.org/abs/2511.21631 ### 核心信息 - **发布日期**: 2025-01 - **机构**: Alibaba Cloud - **架构类型**: 视觉-语言多模态 - **参数规模**: 72B ### 关键技术 1. 视觉编码器: [具体实现] 2. 语言模型: [具体实现] 3. 多模态融合: [具体方法] ### 性能指标 - MMBench: [分数] - GQA: [分数] - TextVQA: [分数] ### 代码信息 - GitHub: [链接] - Hugging Face: [链接] - 许可证: [类型] ``` ### 2.7 搜索示例:Qwen3-VL ``` Step 1: ArXiv搜索 访问: arxiv.org 搜索: "Qwen3-VL" 结果: https://arxiv.org/abs/2511.21631 Step 2: 获取PDF 点击: [PDF]链接 下载: qwen3-vl-paper.pdf Step 3: 检查引用 查找: "References"章节 记录: 相关工作 Step 4: 查找作者 记录: 主要作者信息 搜索: 作者GitHub(可能包含代码) ```
## 代码与实现搜索 ### 3.1 GitHub官方仓库 **搜索公式**: ``` # 直接搜索 https://github.com/QwenLM/Qwen3-VL # Google搜索 "Qwen3-VL github" "Qwen3-VL site:github.com" # 组织优先 github.com/QwenLM github.com/meta-llama github.com/mistralai ``` **验证官方性**: - ✅ 有官方验证徽章 - ✅ 组织名与论文作者一致 - ✅ 有大量Stars(>1000) - ✅ 最近有更新 - ⚠️ 谨防第三方复现仓库 ### 3.2 Hugging Face模型 ``` 访问: https://huggingface.co/ 搜索步骤: 1. 搜索框: "Qwen3-VL" 2. 查看: 模型卡片 (Model Card) 3. 确认: 官方组织 (QwenLM) 4. 检查: Downloads数量 ``` **Hugging Face信息检查**: ```markdown ## 模型卡片检查清单 - [ ] 模型大小: 显示参数规模 - [ ] 架构信息: Transformer/MoE等 - [ ] Tokenizer: 词表大小和分词器 - [ ] 使用示例: 有基本推理代码 - [ ] 许可证: 明确使用限制 - [ ] 文件大小: 确认下载需求 ``` ### 3.3 权重文件获取 | 来源 | 格式 | 说明 | |------|------|------| | Hugging Face | `.safetensors` | 安全格式,推荐 | | Hugging Face | `.bin` | PyTorch格式 | | 官方网站 | 原始权重 | 可能需要转换 | | ModelScope | 国内访问 | 阿里镜像站 | **下载命令示例**: ```python # Hugging Face from transformers import AutoModelForCausalLM model = AutoModelForCausalLM.from_pretrained( "Qwen/Qwen3-VL", trust_remote_code=True ) # ModelScope (国内) from modelscope import AutoModel model = AutoModel.from_pretrained( "qwen/Qwen3-VL", device_map="auto" ) ``` ### 3.4 代码结构快速理解 ``` 典型项目结构: Qwen3-VL/ ├── README.md # 使用说明 ├── LICENSE # 许可证 ├── requirements.txt # 依赖包 ├── setup.py # 安装脚本 ├── config/ # 配置文件 ├── src/ # 源代码 │ ├── models/ # 模型定义 │ ├── training/ # 训练脚本 │ └── inference/ # 推理脚本 ├── examples/ # 使用示例 ├── tests/ # 测试代码 └── docs/ # 文档 ``` ### 3.5 快速运行测试 ```bash # 克隆仓库 git clone https://github.com/QwenLM/Qwen3-VL.git cd Qwen3-VL # 查看README cat README.md # 安装依赖 pip install -r requirements.txt # 运行示例 python examples/simple_inference.py ``` ### 3.6 代码搜索技巧 **在仓库内搜索**: ```bash # 搜索特定函数 grep -r "def forward" --include="*.py" # 搜索配置文件 find . -name "*.yaml" -o -name "*.json" # 查找模型定义 grep -r "class.*Model" --include="*.py" ``` **使用GitHub Code Search**: ``` 搜索: "Qwen" language:python 搜索: "attention" repo:QwenLM/Qwen3-VL ``` ### 3.7 第三方实现参考 对于没有官方代码的模型: ``` 1. Hugging Face Transformers集成 搜索: "模型名 transformers" 2. 复现项目 搜索: "模型名 implementation" 搜索: "模型名 pytorch" 3. 社区贡献 Reddit: r/MachineLearning Twitter: #MachineLearning ```
## 文档与教程搜索 ### 4.1 官方文档优先 **常见文档位置**: ``` GitHub README.md ├── 基本介绍 ├── 安装说明 ├── 快速开始 └── API文档链接 项目文档站 ├── ReadTheDocs (python-xxx.readthedocs.io) ├── GitBook (docs.xxx.com) └── 官方网站 (xxx.com/docs) Hugging Face Model Card ├── 模型描述 ├── 训练数据 ├── 使用方法 └── 限制说明 ``` ### 4.2 技术博客搜索 **官方博客**: ``` Google AI Blog: blog.google/technology/ai/ OpenAI Blog: openai.com/blog/ Meta AI: ai.meta.com/blog/ Anthropic: anthropic.com/blog/ 搜索公式: "模型名" + "blog" "模型名" + "technical blog" ``` **非官方解读**: ``` Medium: medium.com/search?q="模型名" Substack: 搜索AI/ML相关作者 知乎: "模型名 解析" 个人博客: Google "模型名 explained" ``` ### 4.3 视频教程 **YouTube**: ``` # 英文 "模型名 tutorial" "模型_name explained" "模型_name paper walkthrough" # 推荐频道 - Andrej Karpathy - Yannic Kilcher - Machine Learning Street Talk ``` **Bilibili**: ``` # 中文 "模型名 解析" "模型_name 论文解读" "模型_name 代码讲解" # UP主推荐 - 李沐 - 同济子豪兄 - 智子发言 ``` ### 4.4 社区资源 **Hugging Face Forums**: ``` forums.huggingface.co/ - 模型使用问题 - 训练技巧分享 - Bug报告和解决 ``` **Reddit**: ``` r/MachineLearning/ r/LocalLLaMA/ r/MachineLearningNews/ 搜索: "模型名 discussion" ``` **Discord/Slack**: ``` - EleutherAI Discord - Hugging Face Discord - PyTorch Slack ``` ### 4.5 技术社交媒体 **Twitter/X**: ``` # 关注作者 @论文作者Twitter账号 # 追踪动态 #MachineLearning #DeepLearning #LLM # 实时讨论 搜索: "模型名" ``` **LinkedIn**: ``` # 关注研究者 # 查看技术分享 ``` ### 4.6 评测与基准 **排行榜**: ``` Hugging Face Open LLM Leaderboard leaderboard.allenai.org/ MMLU基准: 搜索 "模型名 MMLU" MMBench: 搜索 "模型名 MMBench" HELM: stanford.edu/~helm/ ``` **对比工具**: ``` Papers With Code: paperswithcode.com/ - 按任务浏览 - 按指标对比 - 查看SOTA方法 ```
## 实战案例:Qwen3-VL ### 5.1 完整搜索演示 ``` 目标: 了解Qwen3-VL模型 Step 1: 论文搜索 ───────────────────────────────────────── Google: "Qwen3-VL arxiv" 结果: https://arxiv.org/abs/2511.21631 操作: 下载PDF,阅读摘要 关键信息: ├─ 模型类型: 多模态视觉-语言 ├─ 参数规模: 72B ├─ 架构: ViT + LLaMA-style decoder └─ 发布日期: 2025-01 Step 2: 代码搜索 ───────────────────────────────────────── GitHub: github.com/QwenLM/Qwen3-VL 结果: 官方仓库 验证: ├─ Stars: >1000 ✓ ├─ 最近更新: 2025-01 ✓ ├─ 官方徽章: ✓ └─ License: Apache 2.0 Step 3: Hugging Face ───────────────────────────────────────── Hugging Face: huggingface.co/Qwen/Qwen3-VL 结果: 模型卡片 信息: ├─ Model: Qwen/Qwen3-VL-72B ├─ Downloads: 100K+ ├─ Likes: 500+ └─ Tags: vision, multimodal Step 4: 文档收集 ───────────────────────────────────────── README.md: ├─ 安装说明: pip install qwen-vl ├─ 推理示例: Python代码 └─ 模型配置: config.json 官方文档: qwen.readthedocs.io/ ├─ API文档 ├─ 训练指南 └─ 部署教程 Step 5: 社区讨论 ───────────────────────────────────────── Hugging Face Discussions: ├─ 使用问题集锦 ├─ 性能优化讨论 └─ 部署经验分享 Reddit r/MachineLearning: ├─ 技术讨论 ├─ 对比评测 └─ 使用心得 ``` ### 5.2 资料汇总表 | 资源类型 | 链接 | 获取内容 | |---------|------|---------| | **论文** | arxiv.org/abs/2511.21631 | 技术报告PDF | | **代码** | github.com/QwenLM/Qwen3-VL | 源码+示例 | | **权重** | huggingface.co/Qwen/Qwen3-VL | 预训练权重 | | **文档** | qwen.readthedocs.io/ | API文档 | | **博客** | qwenlm.github.io/blog/ | 技术解读 | ### 5.3 快速上手代码 ```python # 安装 pip install qwen-vl transformers # 基础推理 from qwen_vl import QwenVLModel from PIL import Image # 加载模型 model = QwenVLModel.from_pretrained("Qwen/Qwen3-VL-72B") image = Image.open("example.jpg") # 推理 response = model.generate( images=[image], text="Describe this image", max_length=100 ) print(response) ``` ### 5.4 搜索模板 对于任何新模型,使用此模板: ```markdown ## 模型搜索模板 ### 1. 论文 - [ ] ArXiv: _____ - [ ] PDF下载: _____ - [ ] 关键信息: _____ - [ ] 作者信息: _____ ### 2. 代码 - [ ] GitHub: _____ - [ ] 验证官方性: _____ - [ ] 许可证: _____ - [ ] Stars: _____ ### 3. 权重 - [ ] Hugging Face: _____ - [ ] 下载链接: _____ - [ ] 文件大小: _____ ### 4. 文档 - [ ] README: _____ - [ ] API文档: _____ - [ ] 示例代码: _____ ### 5. 社区 - [ ] Discussions: _____ - [ ] Issues: _____ - [ ] 其他资源: _____ ``` ### 5.5 检查清单 完成搜索后,确认以下项目: ```markdown ## 搜索完成检查清单 基础信息 - [ ] 了解模型用途 - [ ] 知道参数规模 - [ ] 理解架构特点 代码资源 - [ ] 获取源码 - [ ] 获取权重 - [ ] 成功运行示例 文档资料 - [ ] 阅读README - [ ] 查看API文档 - [ ] 理解使用方法 社区资源 - [ ] 加入讨论区 - [ ] 关注作者动态 - [ ] 订阅更新通知 实践准备 - [ ] 配置运行环境 - [ ] 准备测试数据 - [ ] 计划实验方案 ``` ### 5.6 高级技巧 **Google搜索操作符**: ``` # 精确匹配 "Qwen3-VL architecture" # 排除词 Qwen3-VL -site:example.com # 文件类型 Qwen3-VL filetype:pdf # 网站限定 Qwen3-VL site:huggingface.co ``` **持续跟踪**: ``` # GitHub Watch 点击: Watch → Custom → Releases + Discussions # Google Alerts 设置: "Qwen3-VL" 邮件提醒 # RSS订阅 ArXiv: arxiv.org/rss/cs.CV Hugging Face: 模型页面RSS ```
参考