Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / auto_parallel / sapp_nd / memory_estimation / evaluators / comm.py: 99%

161 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-10-25 05:45 +0800

1# Copyright 2025-2026 Huawei Technologies Co., Ltd 

2# 

3# Licensed under the Apache License, Version 2.0 (the "License"); 

4# you may not use this file except in compliance with the License. 

5# You may obtain a copy of the License at 

6# 

7# http://www.apache.org/licenses/LICENSE-2.0 

8# 

9# Unless required by applicable law or agreed to in writing, software 

10# distributed under the License is distributed on an "AS IS" BASIS, 

11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

12# See the License for the specific language governing permissions and 

13# limitations under the License. 

14# ============================================================================ 

15"""Communication volume submodule""" 

16from __future__ import annotations 

17from typing import TYPE_CHECKING 

18from hyper_parallel.auto_parallel.sapp_nd.nd.common.layer_type import LayerType 

19from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.evaluators.utils import EvalUtils 

20from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.logger import logger 

21from hyper_parallel.auto_parallel.sapp_nd.nd.common.cp_types import ( 

22 CPAlgo, 

23 _resolve_cp_algo, 

24) 

25from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import ( 

26 compute_kv_dim, 

27) 

28 

29if TYPE_CHECKING: 

30 from hyper_parallel.auto_parallel.sapp_nd.nd.common.cost_model_preprocess import CostModelConfig 

31 from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._context import Context 

32 

33 

34class EvalLayerComm: 

35 """Communication volume formulas class""" 

36 

37 @staticmethod 

38 def dp_comm_non_exp(ccfg: CostModelConfig, ctx: Context) -> float: 

39 """DP/OP comm for non-expert parameters""" 

40 non_exp, _, _ = ctx.eval.num_p(ccfg, ctx) 

41 dp_comm_non_exp = 0 

42 # Non expert ZeRO LvL 2 

43 if ccfg.comm_d_non_exp == 2: 

44 dp_comm_non_exp += non_exp / (ccfg.cp * ccfg.t) 

45 dp_comm_non_exp += non_exp / ccfg.t 

46 # Non expert ZeRO LvL 3 

47 if ccfg.comm_d_non_exp == 3: 

48 dp_comm_non_exp += non_exp / ccfg.t 

49 return dp_comm_non_exp 

50 

51 @staticmethod 

52 def dp_comm_exp(ccfg: CostModelConfig, ctx: Context) -> float: 

53 """DP/OP comm for expert parameters""" 

54 _, routed, shared = ctx.eval.num_p(ccfg, ctx) 

55 exp_param_size = routed + shared 

56 if exp_param_size == 0: 

57 return 0 

58 dp_comm_exp = 0 

59 # Expert ZeRO LvL 2 

60 if ccfg.comm_d_exp == 2: 

61 dp_comm_exp += exp_param_size / (ccfg.cp * ccfg.t_exp * ccfg.ep) 

62 dp_comm_exp += exp_param_size / max(ccfg.ep, ccfg.t_exp) 

63 # Expert ZeRO LvL 3 

64 if ccfg.comm_d_exp == 3: 

65 dp_comm_exp += exp_param_size / (ccfg.cp * ccfg.t_exp * ccfg.ep) 

66 return dp_comm_exp 

67 

68 @staticmethod 

69 def dp_comm_layer(ccfg: CostModelConfig, ctx: Context) -> float: 

70 """DP/OP comm sum""" 

71 non_exp = EvalLayerComm.dp_comm_non_exp(ccfg, ctx) 

72 exp = EvalLayerComm.dp_comm_exp(ccfg, ctx) 

73 return non_exp + exp 

74 

75 @staticmethod 

76 def fsdp_layer_parts(ccfg: CostModelConfig, ctx: Context) -> tuple: 

77 """A layer's parameters as FSDP holds them, for :meth:`fsdp_traffic`. 

78 

79 The non-expert parameters, the routed and the shared experts, each as 

80 its count, the ranks the layer computes it over, the ranks that shard 

81 it and the ranks that hold it, a shard or a copy. 

82 """ 

83 non_exp, routed, shared = ctx.eval.num_p(ccfg, ctx) 

84 return ( 

85 (non_exp, ccfg.t, ccfg.shard_p_os_non_exp_partial, ccfg.d * ccfg.cp * ccfg.t), 

86 (routed / ccfg.ep, ccfg.t_exp, ccfg.shard_p_os_exp, max(1, int(ccfg.edp_group))), 

87 (shared, ccfg.t_exp, ccfg.shard_p_os_exp_partial, ccfg.d * ccfg.cp * ccfg.t_exp), 

88 ) 

89 

90 @staticmethod 

91 def fsdp_traffic(ccfg: CostModelConfig, parts: tuple, gathers: float, reduces: float) -> float: 

92 """The bytes a rank moves in one micro-batch for a module FSDP shards. 

93 

94 Each ring collective over a part's n shards moves (n - 1) / n of the 

95 part a rank. FSDP gathers a part's parameters *gathers* times a 

96 micro-batch and reduce-scatters its gradient once, and the ranks 

97 holding copies of a shard all-reduce it *reduces* times a micro-batch. 

98 """ 

99 total = 0.0 

100 for size, computed, kept, group in parts: 

101 whole = size / max(1, computed) 

102 shards = max(1.0, kept / max(1, computed)) 

103 copies = max(1.0, group / max(1, kept)) 

104 total += (shards - 1) / shards * whole * (gathers * ccfg.bytes_p + ccfg.bytes_grad) 

105 total += 2 * (copies - 1) / copies * whole / shards * ccfg.bytes_grad * reduces 

106 return total 

107 

108 @staticmethod 

109 def fsdp_reduce_traffic(ccfg: CostModelConfig, parts: tuple, reduces: float) -> float: 

110 """The all-reduce in :meth:`fsdp_traffic`: the bytes the copies of a shard move reducing it. 

111 

112 HSDP's replicas all-reduce each gradient shard once a step, and a run 

113 whose FSDP shards nothing all-reduces every gradient; the rest of 

114 :meth:`fsdp_traffic` is FSDP's own gathers and reduce-scatters. 

115 """ 

116 total = 0.0 

117 for size, computed, kept, group in parts: 

118 whole = size / max(1, computed) 

119 shards = max(1.0, kept / max(1, computed)) 

120 copies = max(1.0, group / max(1, kept)) 

121 total += 2 * (copies - 1) / copies * whole / shards * ccfg.bytes_grad * reduces 

122 return total 

123 

124 @staticmethod 

125 def tp_comm_non_exp(ccfg: CostModelConfig, ctx: Context, mb: int) -> float: 

126 """TP comm for non-expert parameters""" 

127 rec_layer = ctx.current_node == LayerType.SEL_REC_LAYER 

128 tp_comm_non_exp = 0.25 * ccfg.n_gather 

129 tp_comm_non_exp *= ccfg.s * ccfg.b * ccfg.h * mb 

130 if ccfg.n_exp > 1: 

131 tp_comm_non_exp = ( 

132 0.25 

133 * ccfg.n_gather 

134 * ccfg.h 

135 * ccfg.h 

136 * ccfg.bytes_compute 

137 * ccfg.n_attMM 

138 ) 

139 res = ( 

140 EvalUtils.rec_coeff(rec_layer, EvalUtils.switch(ccfg, ctx, "gather")) 

141 * ccfg.comm_t 

142 * tp_comm_non_exp 

143 / ccfg.cp 

144 ) 

145 return res 

146 

147 @staticmethod 

148 def tp_comm_exp(ccfg: CostModelConfig, ctx: Context, mb: int) -> float: 

149 """TP comm for expert parameters""" 

150 rec_layer = ctx.current_node == LayerType.SEL_REC_LAYER 

151 tp_comm_exp = 0.25 * ccfg.n_gather 

152 tp_comm_exp *= ccfg.s * ccfg.b * ccfg.hff * mb 

153 if ccfg.n_exp > 1: 

154 # Routed and shared experts, each hff_exp wide 

155 routed_comm = ccfg.n_exp / ccfg.ep * ccfg.hff_exp 

156 shared_comm = ccfg.n_shared_exp * ccfg.hff_exp 

157 tp_comm_exp = ( 

158 0.25 

159 * ccfg.n_gather 

160 * ccfg.h 

161 * ccfg.bytes_compute 

162 * ccfg.n_ffMM 

163 * (routed_comm + shared_comm) 

164 ) 

165 res = ( 

166 EvalUtils.rec_coeff(rec_layer, EvalUtils.switch(ccfg, ctx, "gather")) 

167 * ccfg.comm_t 

168 * tp_comm_exp 

169 / ccfg.cp 

170 ) 

171 return res 

172 

173 @staticmethod 

174 def tp_comm_layer(ccfg: CostModelConfig, ctx: Context, mb: int) -> float: 

175 """TP comm sum""" 

176 non_exp = EvalLayerComm.tp_comm_non_exp(ccfg, ctx, mb) 

177 exp = EvalLayerComm.tp_comm_exp(ccfg, ctx, mb) 

178 return non_exp + exp 

179 

180 @staticmethod 

181 def cp_comm_non_exp(ccfg: CostModelConfig, ctx: Context) -> float: 

182 """CP comm for non-expert parameters""" 

183 rec_layer = ctx.current_node == LayerType.SEL_REC_LAYER 

184 rec_factor = EvalUtils.rec_coeff(rec_layer, EvalUtils.switch(ccfg, ctx, "gather")) * int( 

185 ccfg.p == 1 

186 ) # [HYPOTHESIS] 

187 # hybird_cp is a known typo for hybrid_cp kept for backward compat 

188 if ccfg.cp_algo in ["colossalai_cp", "hybrid_cp", "hybird_cp"]: 

189 # FW Ring P2P + BW Ring P2P 

190 # KV transfers, can be recomputed 

191 return ( 

192 ccfg.comm_cp 

193 * 2 

194 * ccfg.s 

195 * ccfg.b 

196 * ((2 * 0.5 * rec_factor + 0.5) * ccfg.n_attMM * ccfg.h) 

197 / (ccfg.t) 

198 ) 

199 if ccfg.cp_algo == "ulysses_cp": 

200 return ( 

201 ccfg.comm_cp 

202 * 2 

203 * ccfg.s 

204 * ccfg.b 

205 * ((0.5 * rec_factor + 0.5) * ccfg.n_attMM * ccfg.h) 

206 / (ccfg.t) 

207 ) 

208 return 0 

209 

210 @staticmethod 

211 def cp_comm_exp(ccfg: CostModelConfig, _: Context) -> float: 

212 """CP comm for expert parameters""" 

213 # hybird_cp is a known typo for hybrid_cp kept for backward compat 

214 if ccfg.cp_algo in ["colossalai_cp", "hybrid_cp", "hybird_cp", "ulysses_cp"]: 

215 # FW Ring P2P + BW Ring P2P 

216 # or FW + BW All2Alls 

217 res = ccfg.comm_cp * 2 * ccfg.s * ccfg.b * ccfg.n_ffMM * ccfg.hff 

218 return res / ccfg.t 

219 return 0 

220 

221 @staticmethod 

222 def cp_comm_layer(ccfg: CostModelConfig, ctx: Context) -> float: 

223 """CP comm sum""" 

224 non_exp = EvalLayerComm.cp_comm_non_exp(ccfg, ctx) 

225 exp = EvalLayerComm.cp_comm_exp(ccfg, ctx) 

226 return non_exp + exp 

227 

228 @staticmethod 

229 def ep_comm_layer_balanced( 

230 ccfg: CostModelConfig, ctx: Context, mb: int # pylint: disable=unused-argument 

231 ) -> float: 

232 """EP comm for balanced token distribution (byte volume). 

233 

234 Uses (ep-1)/ep correction: only (ep-1)/ep fraction of local tokens 

235 actually cross rank boundaries in an all-to-all dispatch/combine pair. 

236 Result is in bytes (like TP activation comm), unlike CP/DP which are 

237 in element counts (parameter comm). 

238 """ 

239 del ctx 

240 if ccfg.ep <= 1 or ccfg.comm_ep == 0: 

241 return 0 

242 t_local = mb * ccfg.n_chosen_exp * ccfg.s * ccfg.b / ccfg.cp 

243 t_cross = t_local * (ccfg.ep - 1) / ccfg.ep 

244 return t_cross * ccfg.h * ccfg.bytes_compute * 2 * ccfg.comm_ep 

245 

246 @staticmethod 

247 def ep_comm_layer_imbalanced( 

248 ccfg: CostModelConfig, ctx: Context, mb: int 

249 ) -> float: 

250 """EP comm for imbalanced (skewed) token distribution (byte volume). 

251 

252 Uses max(rank_tokens) to bound communication volume. 

253 Normalized with (ep-1)/ep cross-rank factor and mb scaling, 

254 so it reduces to balanced when token distribution is uniform. 

255 Falls back to balanced when tokens_per_expert is empty 

256 or n_exp not divisible by ep. 

257 

258 tokens_per_expert: global per-expert token count per microbatch 

259 (all EP ranks combined, before all-to-all dispatch; None = balanced). 

260 Under uniform distribution, each rank's share equals 

261 n_chosen_exp * s * b / (cp * t), matching t_local in the balanced formula. 

262 

263 Result is in bytes (like TP activation comm), unlike CP/DP which are 

264 in element counts (parameter comm). 

265 """ 

266 if ccfg.ep <= 1 or ccfg.comm_ep == 0: 

267 return 0 

268 tokens = ccfg.tokens_per_expert 

269 if not tokens: 

270 return EvalLayerComm.ep_comm_layer_balanced(ccfg, ctx, mb) 

271 if ccfg.n_exp % ccfg.ep != 0: 

272 logger.warning( 

273 "n_exp=%d not divisible by ep=%d, falling back to balanced", 

274 ccfg.n_exp, 

275 ccfg.ep, 

276 ) 

277 return EvalLayerComm.ep_comm_layer_balanced(ccfg, ctx, mb) 

278 experts_per_rank = ccfg.n_exp // ccfg.ep 

279 rank_tokens = [] 

280 for r in range(ccfg.ep): 

281 rank_sum = sum( 

282 tokens[r * experts_per_rank + i] for i in range(experts_per_rank) 

283 ) 

284 rank_tokens.append(rank_sum) 

285 max_inbound = max(rank_tokens) 

286 # max_inbound: per-rank inbound tokens for one microbatch 

287 # multiply by mb for the full pipeline stage, by (ep-1)/ep for cross-rank fraction 

288 t_cross = max_inbound * mb * (ccfg.ep - 1) / ccfg.ep 

289 return t_cross * ccfg.h * ccfg.bytes_compute * 2 * ccfg.comm_ep 

290 

291 @staticmethod 

292 def ep_comm_layer(ccfg: CostModelConfig, ctx: Context, mb: int) -> float: 

293 """EP comm dispatcher: balanced or imbalanced based on tokens_per_expert.""" 

294 if ccfg.ep <= 1 or ccfg.comm_ep == 0: 

295 return 0 

296 if ccfg.tokens_per_expert is not None: 

297 return EvalLayerComm.ep_comm_layer_imbalanced(ccfg, ctx, mb) 

298 return EvalLayerComm.ep_comm_layer_balanced(ccfg, ctx, mb) 

299 

300 @staticmethod 

301 def cp_comm_buffer(ccfg: CostModelConfig, ctx: Context) -> float: 

302 """Estimate CP communication buffer memory per layer. 

303 

304 Ring CP (colossalai_cp / hybrid_cp): 

305 Intra-node: all-gather among device_per_node ranks → buffer for 

306 (intra_ranks - 1) extra KV chunks. 

307 Cross-node: intra-node all-gather result stays resident, plus 1 

308 full-node KV chunk as ring receive buffer → peak is 

309 (2 * intra_ranks - 1) extra chunks. 

310 

311 Ulysses CP: 

312 All2All rearranges heads across CP ranks. Peak buffer is 

313 1 send chunk + 1 receive chunk = 2 chunks, where each chunk 

314 is the per-rank activation slice being exchanged. 

315 """ 

316 del ctx 

317 if ccfg.cp <= 1: 

318 return 0.0 

319 

320 s, b, cp = ccfg.s, ccfg.b, ccfg.cp 

321 fp16_bytes = 2 

322 kv_bytes = fp16_bytes * 2 

323 

324 cp_algo = _resolve_cp_algo(ccfg) 

325 

326 if cp_algo == CPAlgo.ULYSSES_CP: 

327 kv_dim = compute_kv_dim(ccfg) 

328 chunk = s * b * (kv_dim / cp) * kv_bytes 

329 intra_ranks = min(int(cp), int(ccfg.device_per_node)) 

330 if cp <= ccfg.device_per_node: 

331 extra_chunks = intra_ranks - 1 

332 else: 

333 extra_chunks = 2 * intra_ranks - 1 

334 return extra_chunks * chunk 

335 

336 kv_dim = compute_kv_dim(ccfg) 

337 chunk = (s / cp) * b * kv_dim * kv_bytes 

338 

339 intra_ranks = min(int(cp), int(ccfg.device_per_node)) 

340 

341 if cp <= ccfg.device_per_node: 

342 extra_chunks = intra_ranks - 1 

343 else: 

344 extra_chunks = 2 * intra_ranks - 1 

345 

346 return extra_chunks * chunk