deepinfer-eplb-policy
deepinfer/deepinfer/model_executor/eplb/policy.py · a679bb8 · L12–140 · 固定 commit 上游
这是固定版本源码的只读摘录;可在线阅读,也可通过上游链接查看原文件。
12def _batch_greedy_indices(
13 tokens_per_expert: torch.Tensor,
14 physical_to_logical_map: torch.Tensor,
15 logical_count: torch.Tensor,
16 capacity: int,
17) -> torch.Tensor:
18 """
19 确定 physical expert 如何复制后,根据历史 batch 的信息贪心排布到各 rank 上
20
21 Parameters:
22 tokens_per_expert: [num_steps, num_moe_layers, num_logical_experts]
23 physical_to_logical_map: [num_moe_layers, num_physical_experts]
24 logical_count : [num_moe_layers, num_logical_experts]
25 capacity: 每个 rank 放置的 physical expert 数,即 num_local_physical_experts
26
27 return:
28 indices: [num_moe_layers, num_physical_experts],每层为原 physical expert 编号的一个排列(0-based)
29 每连续 capacity 个元素表示一个 rank 放置的 physical experts
30 """
31 weights = tokens_per_expert.sum(dim=0, dtype=torch.float32) / logical_count
32 # 按每副本历史总负载降序排列,同负载时保留原物理槽位顺序
33 physical_experts = physical_to_logical_map.to(torch.int64)
34 physical_order = weights.gather(-1, physical_experts).argsort(dim=-1, descending=True, stable=True)
35 expert_order = physical_experts.gather(-1, physical_order)
36 vectors = tokens_per_expert.permute(1, 2, 0).to(dtype=torch.float32, memory_format=torch.contiguous_format, copy=True)
37 vectors.div_(logical_count.unsqueeze(-1))
38
39 num_layers, _, num_steps = vectors.shape
40 num_physical = physical_order.size(1)
41 assert num_physical % capacity == 0, f"{num_physical=} {capacity=}"
42 num_ranks = num_physical // capacity
43 # 优化目标是逐 layer 独立的,对固定的 layer 而言,目标是将这层的 physical expert 分到各个 rank 上
44 # 并且使得 sum_{step i} max_{rank j} (sum_{physical expert e} [e 被分到 rank j] e 在 step i 的负载) 尽量小
45 # 具体贪心算法: 每次尝试将要考虑的 expert 放到各个 rank 上,选取当前最小化上述目标的放置方案
46 loads = vectors.new_zeros((num_layers, num_ranks, num_steps))
47 peaks = vectors.new_zeros((num_layers, num_steps))
48 occupancy = torch.zeros((num_layers, num_ranks), dtype=torch.int64, device=vectors.device)
49 candidates = torch.empty_like(loads)
50 scores = vectors.new_empty((num_layers, num_ranks))
51 indices = torch.empty_like(physical_order)
52 layers = torch.arange(num_layers, device=vectors.device)
53 for job_index in range(num_physical):
54 experts = expert_order[:, job_index]
55 vector = vectors[layers, experts]
56 # candidates[layer][j][step] 描述将当前 expert 放到 rank j 上后,各历史 step 在 rank j 上的负载
57 # 因此需和 peaks 取 max 得到当前 expert 放到 rank j 上后,各历史 step 的 rank 最大负载
58 torch.add(loads, vector.unsqueeze(1), out=candidates)
59 torch.maximum(candidates, peaks.unsqueeze(1), out=candidates)
60 torch.sum(candidates, dim=-1, out=scores)
61 # 排除已满的 rank 后选择最低分,argmin 在同分时返回最小 rank ID
62 scores.masked_fill_(occupancy == capacity, float("inf"))
63 rank = scores.argmin(dim=-1)
64 occupied = occupancy[layers, rank]
65 indices[layers, rank * capacity + occupied] = physical_order[:, job_index]
66 occupancy[layers, rank] = occupied + 1
67 updated = loads[layers, rank] + vector
68 loads[layers, rank] = updated
69 torch.maximum(peaks, updated, out=peaks)
70 return indices
71
72
73def rebalance_experts_global(
74 tokens_per_expert: torch.Tensor,
75 num_physical_experts: int,
76 num_local_physical_experts: int,
77) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
78 """
79 全局负载均衡:给热点 logical expert 分配冗余副本,再把所有副本均衡到各 GPU。
80
81 Parameters:
82 tokens_per_expert: [num_steps, num_moe_layers, num_logical_experts]
83
84 return:
85 physical_to_logical_map: [num_moe_layers, num_physical_experts]
86 logical_to_physical_map: [num_moe_layers, num_logical_experts, num_redundant_experts + 1],无效位 -1
87 logical_count: [num_moe_layers, num_logical_experts],各 logical expert 的副本数
88 """
89 _, num_moe_layers, num_logical_experts = tokens_per_expert.shape
90 num_redundant_experts = num_physical_experts - num_logical_experts
91 device = tokens_per_expert.device
92
93 physical_to_logical_map = torch.empty(num_moe_layers, num_physical_experts, dtype=torch.int, device=device)
94 logical_to_physical_map = torch.full(
95 (num_moe_layers, num_logical_experts, num_redundant_experts + 1), -1, dtype=torch.int, device=device
96 )
97 logical_count = torch.ones(num_moe_layers, num_logical_experts, dtype=torch.int, device=device)
98
99 # 初始布局:前 num_logical_experts 个槽位放 logical experts,其余槽位放冗余副本
100 arange_layers = torch.arange(num_moe_layers, dtype=torch.int, device=device)
101 arange_logical = torch.arange(num_logical_experts, dtype=torch.int, device=device)
102 physical_to_logical_map[:, :num_logical_experts] = arange_logical
103 logical_to_physical_map[:, :, 0] = arange_logical
104
105 # 贪心分配冗余副本:每轮选一个 expert 复制。
106 # score[..., j] 是 expert j 当前的每副本负载,score_added 是给 j 加一个副本后的每副本负载;
107 # 把 argmax 位置替换成其 score_added 后,values[..., j] 即“副本给 j”时的最大每副本负载
108 # (j 非 argmax 时最大值不变,j 为 argmax 时是替换后重算的 max),按 step 求和取 argmin 得最优复制对象。
109 tokens_per_expert_all_diff = tokens_per_expert + arange_logical * 1e-4
110 for i in range(num_redundant_experts):
111 score = tokens_per_expert_all_diff / logical_count # NOTE: Values in score must be different from each other
112 score_added = tokens_per_expert / (logical_count + 1)
113 values, indices = score.max(-1, keepdim=True)
114 values = values.expand_as(score).contiguous()
115 score.scatter_(-1, indices, score_added.gather(-1, indices))
116 values.scatter_(-1, indices, score.max(-1, keepdim=True).values)
117 redundancy_indices = values.sum(0).argmin(-1) # [num_moe_layers],本轮被复制的 logical 编号
118
119 physical_index = num_logical_experts + i # 本轮冗余副本占用的 physical 槽位
120 physical_to_logical_map[:, physical_index] = redundancy_indices
121 redundancy_count = logical_count.gather(-1, redundancy_indices.unsqueeze(-1)).squeeze(-1)
122 logical_to_physical_map[arange_layers, redundancy_indices, redundancy_count] = physical_index
123 logical_count[arange_layers, redundancy_indices] += 1
124
125 if num_local_physical_experts > 1:
126 assert num_physical_experts % num_local_physical_experts == 0
127 indices = _batch_greedy_indices(tokens_per_expert, physical_to_logical_map, logical_count, num_local_physical_experts)
128 physical_to_logical_map = physical_to_logical_map.gather(-1, indices)
129 # indices.argsort 给出旧槽位 -> 新槽位的映射,用它重映射 logical_to_physical_map 里的槽位编号(-1 先临时置 0 再还原)
130 mask = logical_to_physical_map == -1
131 logical_to_physical_map[mask] = 0
132 logical_to_physical_map = (
133 indices.argsort(-1)
134 .gather(-1, logical_to_physical_map.view(num_moe_layers, -1).to(torch.int64))
135 .view_as(logical_to_physical_map)
136 .to(torch.int)
137 )
138 logical_to_physical_map[mask] = -1
139
140 return physical_to_logical_map, logical_to_physical_map, logical_count