MoE 模型的分布式推理调度:Expert 放置策略、All-to-All 通信与负载均衡

📅 2026/7/17 15:28:13
MoE 模型的分布式推理调度:Expert 放置策略、All-to-All 通信与负载均衡
MoE 模型的分布式推理调度Expert 放置策略、All-to-All 通信与负载均衡一、Mixtral 的 8 个 Expert 如何将单 GPU 推理变成多 GPU 通信噩梦Mixture of ExpertsMoE模型的核心是稀疏激活每个 token 只激活 8 个 Expert 中的 Top-2。这降低了计算量8 倍参数、2 倍计算但引入了 Expert 间通信。当 Expert 分布在多 GPU 上时token 的路由决策选择哪个 Expert触发了跨设备的 All-to-All 通信。以 Mixtral-8x7B46.7B 总参数在 4×A100 上推理为例。每个 GPU 托管 2 个 Expert。一个 batch 的 1024 个 token 经过 Gating Network 后被分派到不同 GPU 上的 Expert。最坏情况下所有 token 选择了 GPU 3 上的 ExpertGPU 0~2 空闲GPU 3 过载——这是 MoE 的负载均衡问题。All-to-All 通信的开销更为显著。256 个 token 的路由结果需要通过 NCCLAllToAll跨 4 个 GPU 交换。每个 token 的 hidden state 为 4096 维 fp168KB。256 token × 8KB 2MB 的总通信量。NCCL AllToAll 带宽约 200 GB/sNVLink2MB 传输耗时约 10μs。但 NCCL 的 kernel launch 和同步开销约 50μs——总通信延迟约 60μs与单次 Expert 计算延迟约 80μs相当。二、Expert 并行的通信拓扑与调度策略All-to-All 通信分为两个阶段Dispatch每个 GPU 将属于其他 GPU Expert 的 token 发送出去Combine每个 GPU 接收计算结果并汇总Expert 的放置策略有三种均匀放置每个 GPU 托管相同数量的 Expert如 8 Expert / 4 GPU 2容量感知放置将热门 Expert历史上被选择最多的分布到不同 GPU数据感知放置将处理相似 token 的 Expert 放在同一 GPU减少通信容量感知放置需要追踪 Expert 被选择的频率使用指数加权移动平均更新热度。但需要防止所有热门 Expert 恰好分散在不同 GPU——这会导致每个 GPU 的负载不均。三、Rust 中 MoE 调度的核心实现use std::collections::{HashMap, HashSet}; use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; /// Expert 的容量配置 #[derive(Debug, Clone)] struct ExpertCapacity { expert_id: usize, /// 最大并发 token 数超过此数则溢出到下一个 Expert max_tokens: usize, } /// Token 到 Expert 的路由记录 #[derive(Debug, Clone)] struct TokenRoute { token_idx: usize, /// Top-2 Expert 选择 expert_ids: [usize; 2], /// Gate 权重用于加权 Combine gate_weights: [f32; 2], } /// Expert 热度追踪器用于负载均衡 struct ExpertHeatMap { /// Expert 近期被选中的次数 heat: VecAtomicU64, /// 衰减因子每次路由后乘以 decay decay: f64, } impl ExpertHeatMap { fn new(num_experts: usize, decay: f64) - Self { ExpertHeatMap { heat: (0..num_experts).map(|_| AtomicU64::new(0)).collect(), decay, } } /// 记录 Expert 被选中 fn record_selection(self, expert_id: usize) { self.heat[expert_id].fetch_add(1, Ordering::Relaxed); } /// 衰减并获取当前热度排名 fn decay_and_rank(self) - Vec(usize, u64) { let mut ranked: Vec(usize, u64) self.heat.iter() .enumerate() .map(|(id, cnt)| (id, cnt.load(Ordering::Relaxed))) .collect(); // 衰减 for (_, cnt) in mut ranked { *cnt (*cnt as f64 * self.decay) as u64; } // 按热度降序排列 ranked.sort_by(|a, b| b.1.cmp(a.1)); ranked } } /// Expert 放置策略 #[derive(Debug, Clone)] enum ExpertPlacement { /// 均匀分布轮询 Uniform, /// 基于容量给每个 Expert 分配最大并发数 CapacityBased, /// 基于热度热门分散、冷门集中 HeatAware, } /// MoE 层调度器 struct MoeScheduler { /// Expert 总数 num_experts: usize, /// GPU 数量 num_gpus: usize, /// Expert → GPU 映射 expert_to_gpu: Vecusize, /// GPU → Expert 列表映射 gpu_to_experts: VecVecusize, /// Expert 热度追踪 heat_map: ExpertHeatMap, /// 放置策略 placement: ExpertPlacement, } impl MoeScheduler { fn new(num_experts: usize, num_gpus: usize, placement: ExpertPlacement) - Self { let heat_map ExpertHeatMap::new(num_experts, 0.9); // 初始均匀放置 let mut expert_to_gpu vec![0usize; num_experts]; let mut gpu_to_experts vec![Vec::new(); num_gpus]; for eid in 0..num_experts { let gid eid % num_gpus; expert_to_gpu[eid] gid; gpu_to_experts[gid].push(eid); } MoeScheduler { num_experts, num_gpus, expert_to_gpu, gpu_to_experts, heat_map, placement, } } /// 处理 token 路由为每个 token 分配 Expert 和 GPU fn route_tokens(self, routes: [TokenRoute]) - HashMapusize, VecTokenRoute { let mut gpu_tokens: HashMapusize, VecTokenRoute HashMap::new(); for route in routes { for (i, expert_id) in route.expert_ids.iter().enumerate() { let gpu_id self.expert_to_gpu[expert_id]; gpu_tokens.entry(gpu_id) .or_default() .push(TokenRoute { token_idx: route.token_idx, expert_ids: [expert_id, expert_id], // 简化 gate_weights: [route.gate_weights[i], 0.0], }); } // 更新热度 for expert_id in route.expert_ids { self.heat_map.record_selection(expert_id); } } gpu_tokens } /// 检查负载是否均衡如果不均衡则触发 rebalance fn check_load_balance(self, gpu_tokens: HashMapusize, VecTokenRoute) - bool { if gpu_tokens.is_empty() { return true; } let loads: Vecusize (0..self.num_gpus) .map(|g| gpu_tokens.get(g).map_or(0, |t| t.len())) .collect(); let avg loads.iter().sum::usize() as f64 / loads.len() as f64; let max_load *loads.iter().max().unwrap_or(0) as f64; // 最忙 GPU 的负载不超过平均值的 1.5 倍认为均衡 max_load avg * 1.5 } /// 动态重新放置 Expert基于热度 fn rebalance_experts(mut self) { let ranked self.heat_map.decay_and_rank(); match self.placement { ExpertPlacement::HeatAware { // 策略将热度最高的 Expert 分布到不同 GPU // 将热度最低的 Expert 集中在同一 GPU let mut new_mapping vec![0usize; self.num_experts]; for (rank, (expert_id, _heat)) in ranked.iter().enumerate() { if rank self.num_gpus { // Top-K 热门 Expert各占一个 GPU new_mapping[*expert_id] rank; } else { // 剩余 Expert均匀分布 new_mapping[*expert_id] rank % self.num_gpus; } } // 更新映射 self.expert_to_gpu new_mapping; self.update_gpu_to_experts(); } _ { // Uniform 和 CapacityBased 策略保持稳定不动态调整 } } } fn update_gpu_to_experts(mut self) { self.gpu_to_experts vec![Vec::new(); self.num_gpus]; for (eid, gid) in self.expert_to_gpu.iter().enumerate() { self.gpu_to_experts[gid].push(eid); } } /// 估算 All-to-All 通信时间 fn estimate_communication_time( self, tokens_per_gpu: HashMapusize, usize, hidden_dim: usize, nvlink_bandwidth_gbps: f64, ) - f64 { let total_tokens: usize tokens_per_gpu.values().sum(); let bytes_per_token hidden_dim * 2; // fp16 2 字节 // All-to-All 通信量每个 GPU 发送 (total/G) 个 token 到其他 GPU let comm_per_gpu (total_tokens as f64 / self.num_gpus as f64) * bytes_per_token as f64; // 传输时间ms 数据量 / 带宽 // 注意All-to-All 的实际带宽取决于拓扑 let effective_bandwidth nvlink_bandwidth_gbps * 1e9 / 8.0; // bytes/s comm_per_gpu / effective_bandwidth * 1000.0 // ms } } /// 简化的 MoE 前向传播模拟 struct MoeForward { scheduler: MoeScheduler, /// 每个 GPU 上的 Expert 实现 expert_compute_time_us: f64, } impl MoeForward { fn forward(mut self, routes: [TokenRoute], batch_size: usize) - f64 { // Step 1Token 路由 let gpu_tokens self.scheduler.route_tokens(routes); // Step 2负载检查 if !self.scheduler.check_load_balance(gpu_tokens) { self.scheduler.rebalance_experts(); } // Step 3All-to-All 通信时间估算 let token_counts: HashMapusize, usize gpu_tokens.iter() .map(|(gid, tokens)| (*gid, tokens.len())) .collect(); let comm_time self.scheduler.estimate_communication_time( token_counts, 4096, 900.0, // 900 GB/s NVSwitch ); // Step 4Expert 计算时间每个 Expert 处理分配到的 token let max_gpu_tokens token_counts.values().max().copied().unwrap_or(0); let compute_time max_gpu_tokens as f64 * self.expert_compute_time_us / 1000.0; // 总延迟假设 Compute 和 Communication 重叠有限 comm_time compute_time } } fn main() { let scheduler MoeScheduler::new(8, 4, ExpertPlacement::HeatAware); // 模拟 256 个 token 的路由 let routes: VecTokenRoute (0..256) .map(|i| TokenRoute { token_idx: i, expert_ids: [i % 8, (i 1) % 8], gate_weights: [0.6, 0.4], }) .collect(); let gpu_tokens scheduler.route_tokens(routes); println!( MoE Token Distribution ); for gid in 0..4 { let count gpu_tokens.get(gid).map_or(0, |t| t.len()); println!(GPU {}: {} tokens, gid, count); } println!(\n Communication Estimate ); let token_counts: HashMapusize, usize gpu_tokens.iter() .map(|(gid, tokens)| (*gid, tokens.len())) .collect(); let comm scheduler.estimate_communication_time(token_counts, 4096, 900.0); println!(All-to-All time: {:.3} ms, comm); }ExpertPlacement::HeatAware策略的核心理念将最热门的 Expert 分散到不同 GPU 上并行处理将冷门 Expert 集中到同一 GPU 减少通信。这借鉴了缓存的分区分治思想。estimate_communication_time使用简化的带宽模型。在生产环境中All-to-All 的实际带宽受 PCIe 拓扑NVLink vs NVSwitch vs PCIe Switch影响NCCL 的ncclAllToAll会根据拓扑选择 Ring 或 Tree 算法。Ring 算法对均衡负载最优Tree 算法对不均衡负载更鲁棒。四、MoE 推理的规模化挑战Expert 容量溢出当一个 Expert 收到的 token 超过容量上限多余的 token 必须丢弃或路由到其他 Expert负载均衡损失函数Load Balancing Loss在训练时强制执行均衡推理时通过容量限制做保险内存碎片化每个 Expert 的 KV Cache 分配独立8 个 Expert × 每层 大量小块分配合并分配为所有 Expert 在所在 GPU 上一次性分配共享空间批大小敏感Batch Size 16MoE 的 All-to-All 开销占比 50%不如 Dense 模型Batch Size 256通信开销被摊销MoE 的优势显现五、总结MoE 推理的核心开销来自 All-to-All 通信Expert 间 token 交换在 NVLink 下约 60μs与单 Expert 计算时间80μs相当。Expert 放置策略分为三种均匀简单但可能不均衡、容量感知限制单 Expert 负载、热度感知冷热分离热度感知在负载不均时效果最好。负载均衡检查阈值 1.5x 是工程经验值平衡了 rebalance 的频率频繁调整有开销和效益不均衡导致 GPU 空闲。MoE 的通信-计算比对 Batch Size 敏感小 Batch16通信占比 50% 无优势大 Batch256计算占主导。Expert 容量溢出需要 Load Balancing Loss 在训练时保障推理时通过容量上限做兜底。