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

172 statements  

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

1# Copyright 2025 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"""Utility submodule""" 

16from __future__ import annotations 

17from typing import TYPE_CHECKING, Any, Mapping 

18import operator 

19import ast 

20import functools 

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

22 

23if TYPE_CHECKING: 

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

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

26 from typing import Union 

27 

28OPS_MAP = { 

29 ast.Add: operator.add, # x + y 

30 ast.Sub: operator.sub, # x - y 

31 ast.Mult: operator.mul, # x * y 

32 ast.Div: operator.truediv, # x / y 

33 ast.FloorDiv: operator.floordiv, # x // y 

34 ast.Mod: operator.mod, # x % y 

35 ast.Pow: operator.pow, # x ** y 

36 ast.USub: operator.neg, # -x 

37 "min": min, 

38 "max": max, 

39 "abs": abs, 

40 "round": round, 

41} 

42 

43 

44@functools.lru_cache(maxsize=256) 

45def _parsed_expr(expr: str) -> ast.AST: 

46 """The expression a communication formula sums, parsed once rather than once a layer and candidate. 

47 

48 The evaluation only reads the tree, so every call can share it. 

49 """ 

50 return ast.parse(expr).body[0].value 

51 

52 

53class EvalUtils: 

54 """Utility methods class, PP Microbatch factor formulas""" 

55 

56 @staticmethod 

57 def reduced_grads(ccfg: CostModelConfig, parts: tuple) -> tuple: 

58 """What the gradients FSDP reduce-scatters weigh, whole and sharded. 

59 

60 Each part is its parameter count, the ranks the backward computes 

61 its gradient over and the ranks the gradient is kept over. A part 

62 no other rank shards is not reduce-scattered: the gradient the 

63 backward computes is the one it keeps. 

64 """ 

65 reduced = [(size, computed, kept) for size, computed, kept in parts if kept > computed] 

66 whole = sum(size / computed for size, computed, _ in reduced) 

67 sharded = sum(size / kept for size, _, kept in reduced) 

68 return whole * ccfg.bytes_grad, sharded * ccfg.bytes_grad 

69 

70 @staticmethod 

71 def census_bytes(ctx: Context, tokens: float, kept: float, held: float) -> float: 

72 """What a census states for *tokens*: what a layer keeps, *kept* bytes per token, or its working set. 

73 

74 As its backward's working set, the most that backward holds, *held* 

75 per token; less what the layer keeps, and never below it, where the 

76 stage's dynamic memory counts that already, as for a layer that 

77 does not recompute. 

78 """ 

79 if not ctx.working_set: 

80 return tokens * kept 

81 return tokens * (max(0.0, held - kept) if ctx.working_on_saved else held) 

82 

83 @staticmethod 

84 def mb(x: Union[float, dict, tuple]) -> int: 

85 """Convert Byte to MB""" 

86 if isinstance(x, dict): 

87 return dict( 

88 ( 

89 (k, int(sum(v) / 1024 / 1024)) 

90 if isinstance(v, tuple) 

91 else (k, int(v / 1024 / 1024)) 

92 ) 

93 for k, v in x.items() 

94 ) 

95 if isinstance(x, tuple): 

96 return int(sum(x) / 1024 / 1024) 

97 return int(x / 1024 / 1024) 

98 

99 # Layer Blocks 

100 @staticmethod 

101 def rec_coeff(rec_layer: bool, rec_op: bool) -> bool: 

102 """Masking coefficient for select recompute""" 

103 return int(not rec_layer) | rec_op 

104 

105 @staticmethod 

106 def switches(ccfg: CostModelConfig, ctx: Context) -> Any: 

107 """The switches of the layer *ctx* evaluates: the context's where it states them, else its config's.""" 

108 stated = getattr(ctx, "switches", None) 

109 return stated if isinstance(stated, Mapping) else ccfg.rec_op 

110 

111 @staticmethod 

112 def switch(ccfg: CostModelConfig, ctx: Context, name: str) -> Any: 

113 """One recompute switch of the layer *ctx* evaluates, 1 to keep the op's activation and 0 to recompute it.""" 

114 return EvalUtils.state(EvalUtils.switches(ccfg, ctx), name) 

115 

116 @staticmethod 

117 def state(stated: Any, name: str) -> Any: 

118 """Switch *name* in *stated*, a mapping or a config's ``rec_op``: a switch it leaves out keeps its op. 

119 

120 Read from ``vars``, since a ``Config`` answers 0, recompute, for any 

121 attribute it lacks. 

122 """ 

123 values = stated if isinstance(stated, Mapping) else vars(stated) if stated is not None else {} 

124 return values.get(name, 1) 

125 

126 @classmethod 

127 def eval_expr_insight(cls, **kwargs): 

128 """compute and categorize math expression""" 

129 return cls.__eval_ast_mem(_parsed_expr(kwargs.get("expr")), 0, **kwargs)[0] 

130 

131 @staticmethod 

132 def __log_ast_mem(ctx, cat, mem): 

133 """Save AST memory evaluation result in the current context.""" 

134 ctx.save2log(cat, mem) 

135 ctx.accu_mem_type[cat] += mem 

136 

137 @classmethod 

138 def __eval_ast_name(cls, n: ast.Name, depth: int, wait: bool, **kwargs): 

139 """Evaluate a named memory term.""" 

140 ctx = kwargs.get("ctx") 

141 if n.id not in kwargs.get("mem_val") or n.id not in kwargs.get( 

142 "mem_cat" 

143 ): 

144 raise AttributeError( 

145 f"Unrecognized variable '{n.id}' " 

146 f"from expr: '{kwargs['expr']}' " 

147 f"(recognized: {list(kwargs.get('mem_val').keys())})" 

148 ) 

149 mem = kwargs.get("mem_val")[n.id] 

150 cat = kwargs.get("mem_cat")[n.id] 

151 if depth <= 1 and not wait: 

152 cls.__log_ast_mem(ctx, cat, mem) 

153 return mem, cat 

154 

155 @classmethod 

156 def __eval_ast_unary(cls, n: ast.UnaryOp, depth: int, wait: bool, **kwargs): 

157 """Evaluate a unary expression.""" 

158 ctx = kwargs.get("ctx") 

159 mem, cat = cls.__eval_ast_mem( 

160 n.operand, 

161 depth + int(not isinstance(n.operand, ast.BinOp)), 

162 **kwargs, 

163 ) 

164 mem = OPS_MAP[type(n.op)](mem) 

165 if depth == 0 and not wait: 

166 cls.__log_ast_mem(ctx, cat, mem) 

167 return mem, cat 

168 

169 @classmethod 

170 def __eval_ast_binop(cls, n: ast.BinOp, depth: int, **kwargs): 

171 """Evaluate a binary expression.""" 

172 ctx = kwargs.get("ctx") 

173 l_is_con = isinstance(n.left, ast.Constant) or not isinstance( 

174 n.op, ast.Add 

175 ) 

176 r_is_con = isinstance(n.right, ast.Constant) or not isinstance( 

177 n.op, ast.Add 

178 ) 

179 l_eval = cls.__eval_ast_mem( 

180 n.left, 

181 depth + int(not isinstance(n.left, ast.BinOp)), 

182 wait=r_is_con, 

183 **kwargs, 

184 ) 

185 r_eval = cls.__eval_ast_mem( 

186 n.right, 

187 depth + int(not isinstance(n.right, ast.BinOp)), 

188 wait=l_is_con, 

189 **kwargs, 

190 ) 

191 mem = OPS_MAP[type(n.op)](l_eval[0], r_eval[0]) 

192 cat = l_eval[1] if l_eval[1] else r_eval[1] 

193 if depth == 0 and (r_is_con or l_is_con): 

194 cls.__log_ast_mem(ctx, cat, mem) 

195 return mem, cat 

196 

197 @classmethod 

198 def __eval_ast_call( 

199 cls, n: ast.Call, depth: int, wait: bool, **kwargs 

200 ): 

201 """Evaluate a supported function call.""" 

202 ctx = kwargs.get("ctx") 

203 a_res = [[], []] 

204 for x in n.args: 

205 a_eval = cls.__eval_ast_mem(x, depth + 1, wait=True, **kwargs) 

206 a_res[0] += [a_eval[0]] 

207 a_res[1] += [a_eval[1]] 

208 mem = OPS_MAP[n.func.id](*a_res[0]) 

209 cat = next( 

210 (c for c in a_res[1] if a_res[0][a_res[1].index(c)] == mem), 

211 a_res[1][0], 

212 ) 

213 if depth <= 1 and not wait: 

214 cls.__log_ast_mem(ctx, cat, mem) 

215 return mem, cat 

216 

217 @classmethod 

218 def __eval_ast_mem( 

219 cls, n: ast.AST, depth: int, wait: bool = False, **kwargs 

220 ): 

221 """compute and categorize from AST""" 

222 if isinstance(n, ast.Name): 

223 return cls.__eval_ast_name(n, depth, wait, **kwargs) 

224 if isinstance(n, ast.Constant): 

225 return n.value, None 

226 if isinstance(n, ast.UnaryOp): 

227 return cls.__eval_ast_unary(n, depth, wait, **kwargs) 

228 if isinstance(n, ast.BinOp): 

229 return cls.__eval_ast_binop(n, depth, **kwargs) 

230 if isinstance(n, ast.Call): 

231 return cls.__eval_ast_call(n, depth, wait, **kwargs) 

232 return 0, None 

233 

234 # PP MICRO FACTOR 

235 

236 @staticmethod 

237 def pp_1f1b_micro_factor(ccfg: CostModelConfig, ctx: Context) -> int: 

238 """1F1B Warm-up microbatches count""" 

239 stage_id, chunk_id = ctx.current_stage_id, ctx.current_chunk_id 

240 # Warm_up micros num compute 

241 micro_factor = 1 

242 extra = 0 

243 base_micro = min(ccfg.p, ccfg.m) 

244 if ccfg.vp == 1: 

245 micro_factor = base_micro - stage_id 

246 else: # VPP 

247 if 0 < chunk_id < ccfg.vp - 1: # Middle chunk 

248 micro_factor = base_micro 

249 else: # First/Last chunk 

250 if not ctx.vpp_less_mem: # Big memory 

251 # Balance micros between last chunk and next first chunk 

252 extra = base_micro - stage_id - 1 

253 last_chunk_micros = base_micro - stage_id 

254 if last_chunk_micros < base_micro: 

255 last_chunk_micros += min(1, extra) 

256 extra = max(0, extra - 1) 

257 if chunk_id == 0: 

258 if stage_id == 0: 

259 extra -= 1 

260 micro_factor = base_micro + extra 

261 else: 

262 micro_factor = last_chunk_micros 

263 else: # Less memory 

264 if chunk_id == 0: 

265 micro_factor = base_micro 

266 else: 

267 micro_factor = base_micro - stage_id 

268 return micro_factor 

269 

270 @staticmethod 

271 def pp_seq1f1b_micro_factor(ccfg: CostModelConfig, ctx: Context) -> int: 

272 """Seq1F1B Warm-up microbatches count, in sequence chunks. 

273 

274 Each chunk holds ``n_s_split``'th of a micro-batch's sequence: the 

275 count goes to ``ctx.seq_chunks``, and the backbone evaluates the node 

276 at that length. 

277 """ 

278 stage_id, chunk_id = ctx.current_stage_id, ctx.current_chunk_id 

279 # Warm_up micros num compute 

280 micro_factor = 1 

281 ctx.seq_chunks = max(1, ccfg.n_s_split) 

282 base_micro = min(ccfg.p, ccfg.m) 

283 if ccfg.vp == 1: 

284 micro_factor = base_micro - stage_id + ccfg.n_s_split - 1 

285 else: # VPP 

286 if 0 < chunk_id < ccfg.vp - 1: # Middle chunk 

287 micro_factor = base_micro 

288 else: # First/Last chunk 

289 if not ctx.vpp_less_mem: # Big memory 

290 # Balance micros between last chunk and next first chunk 

291 extra = base_micro - stage_id - 1 

292 last_chunk_micros = base_micro - stage_id 

293 last_chunk_micros += ccfg.n_s_split - 1 

294 if last_chunk_micros > base_micro: 

295 last_chunk_micros -= 1 

296 extra += 1 

297 elif last_chunk_micros < base_micro: 

298 last_chunk_micros += min(1, extra) 

299 extra = max(0, extra - 1) 

300 if chunk_id == 0: 

301 micro_factor = base_micro + extra 

302 else: 

303 micro_factor = last_chunk_micros 

304 else: # Less memory 

305 if chunk_id == 0: 

306 micro_factor = base_micro 

307 else: 

308 micro_factor = base_micro - stage_id 

309 micro_factor += ccfg.n_s_split - 1 

310 return micro_factor 

311 

312 @staticmethod 

313 def pp_dualpipe_v_micro_factor(ccfg: CostModelConfig, ctx: Context) -> int: 

314 """DualPipeV/ZeroBubbleV Warm-up microbatches count""" 

315 stage_id, chunk_id = ctx.current_stage_id, ctx.current_chunk_id 

316 # First half layer from stage 0->PP then second half from stage PP->0 

317 if ccfg.vp > 2: 

318 logger.warning("DualPipeV with VPP>2 not handled") 

319 return 0 

320 if chunk_id == 0: 

321 return min(ccfg.p, ccfg.m) * 2 - 1 - stage_id 

322 return stage_id 

323 

324 @staticmethod 

325 def pp_gpipe_micro_factor(ccfg: CostModelConfig, ctx: Context) -> int: 

326 """GPipe warm-up microbatches count.""" 

327 _ = ctx 

328 return ccfg.p # Minimum value