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

141 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"""memory estimation API""" 

16# pylint: disable=protected-access 

17from __future__ import annotations 

18from typing import TYPE_CHECKING 

19import argparse 

20import os 

21import copy 

22 

23# import pprint 

24import json 

25import hyper_parallel.auto_parallel.sapp_nd.nd.common.hardware as Hard 

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

27 

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

29from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._utils import _Utils 

30from hyper_parallel.auto_parallel.sapp_nd.memory_estimation._hook_manager import _HookManager 

31from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.size import Memory 

32 

33if TYPE_CHECKING: 

34 from typing import Any, Callable, Dict, Optional, Sequence, Union 

35 from hyper_parallel.auto_parallel.sapp_nd.memory_estimation.hook_base import MemEvalHook 

36 

37 

38class EvaluatorV2(_Utils, _HookManager): 

39 """Memory evaluator class""" 

40 

41 def __init__(self, *args, **kwargs): 

42 self.ppb = None 

43 super().__init__(*args, **kwargs) 

44 self._child_cls = self 

45 

46 def reset_config(self) -> None: 

47 """reset current config""" 

48 self.update_config(self.config_path) 

49 

50 def load_hook_cls(self, hook_cls: MemEvalHook) -> None: 

51 """load an instance of MemEvalHook""" 

52 self.hook_cls = hook_cls 

53 self.reset_config() 

54 if not self._ccfg.multimodal: 

55 hook = list(self._ccfg.hooks_dict.values())[0] 

56 hook(self) 

57 

58 def _estimate_on_copy(self, *args) -> Any: 

59 """Run the backbone on a copy of the config, leaving the caller's intact. 

60 

61 The backbone applies each layer's hook in place, one layer after the 

62 next. Run on the caller's config, it would leave the last layer's 

63 state behind for whatever reads that config next, which in a search 

64 is the next candidate. 

65 """ 

66 caller = self._ccfg 

67 self._ccfg = copy.deepcopy(caller) 

68 self._overhead_obj._ccfg = self._ccfg 

69 try: 

70 return self._estimate_backbone(*args) 

71 finally: 

72 self._ccfg = caller 

73 self._overhead_obj._ccfg = caller 

74 

75 def estimate_peak( 

76 self, 

77 stages: list = None, 

78 verbose: bool = False, 

79 spec_stage_id: int = -1, 

80 plot: bool = False, 

81 ) -> float: 

82 """Peak stage memory estimation""" 

83 res = self._estimate_on_copy( 

84 stages, verbose, False, spec_stage_id, plot 

85 ) 

86 insights, _ = res 

87 stage_mems = [i["Static"] + i["Dynamic"] for i in insights] 

88 peak_mem = max(stage_mems) 

89 peak_stage = stage_mems.index(peak_mem) 

90 

91 logger.output( 

92 "model_name: %s, peak memory : \033[1m%s MB\033[0m (stage _%s)", 

93 self._ccfg.model_name, 

94 peak_mem, 

95 peak_stage, 

96 ) 

97 return peak_mem 

98 

99 def estimate_peak_insight(self, stages: list = None) -> list: 

100 """subcomponents' proportion estimation""" 

101 insights, _ = self._estimate_on_copy(stages, False, False, -1, False) 

102 return insights 

103 

104 def estimate_layer_memory( 

105 self, 

106 stages: list = None, 

107 device_type=Hard.Device_A2, 

108 layer_times: Optional[Callable] = None, 

109 ) -> Dict: 

110 """PPB's input 

111 

112 Args: 

113 layer_times: Prices a layer as ``(forward, backward)`` from its 

114 config, kind, layer type and recompute switches, such as 

115 ``perf_estimation.estimate.LayerTimes``. With it, every layer 

116 description carries its times, in units of the first body's 

117 forward time, instead of a placeholder and offers the 

118 selective recompute options worth pricing, and the result is 

119 not cached. 

120 """ 

121 logger.info(device_type) 

122 if layer_times is not None: 

123 with self._ppb_obj.timing(layer_times): 

124 return self._estimate_on_copy(stages, False, True, -1, False)[1] 

125 if self.ppb: 

126 return self.ppb 

127 res = self._estimate_on_copy(stages, False, True, -1, False) 

128 _, ppb = res 

129 self.ppb = ppb 

130 return ppb 

131 

132 def estimate_switch_profiles( 

133 self, 

134 layer_times: Callable, 

135 stages: list = None, 

136 most_in_flight: Optional[int] = None, 

137 each_switch: bool = True, 

138 in_flight: Sequence[int] = (), 

139 ) -> Dict: 

140 """What each recompute switch saves and costs, per model and layer kind. 

141 

142 Walks the stages as :meth:`estimate_layer_memory` does and measures 

143 the first body layer of each kind: plain, with each op the switches 

144 name recomputed alone, and fully recomputed. The config is left as it 

145 was found. 

146 

147 Args: 

148 layer_times: Prices a layer, as for :meth:`estimate_layer_memory`. 

149 stages: The partition; the config's own when omitted. 

150 most_in_flight: The most micro-batches any stage keeps in flight. 

151 A buffer is split into what grows with the micro-batches in 

152 flight and what is kept once by its size at one micro-batch 

153 and at this many, so the split is exact at both. Under 1F1B 

154 without interleaving, the least of the stages and the 

155 micro-batches when omitted. 

156 each_switch: Whether to measure each switch alone. Without, a 

157 profile prices only the plain and the fully recomputed 

158 layer, which is all one mode for every layer needs unless it 

159 is selective. 

160 in_flight: The counts of micro-batches in flight the stages 

161 keep. At each between one and the most, a profile also 

162 states what the split charges beyond the buffers kept, so 

163 that an option's memory is exact at every count. 

164 

165 Returns: 

166 ``{(model name, layer kind): SwitchProfile}``, in model order. 

167 """ 

168 with self._ppb_obj.profiling(layer_times, most_in_flight, each_switch, in_flight) as profiles: 

169 self._estimate_on_copy(stages, False, True, -1, False) 

170 return profiles 

171 

172 # Specific estimation 

173 

174 def static_mem_stage(self, stage_id: int) -> float: 

175 """stage estimation proportions""" 

176 insights = self.estimate_peak_insight() 

177 return insights[stage_id]["Static"] 

178 

179 def dynamic_mem_stage(self, stage_id: int) -> float: 

180 """stage estimation proportions""" 

181 insights = self.estimate_peak_insight() 

182 return insights[stage_id]["Dynamic"] 

183 

184 def logs_mem_stage(self, stage_id: int) -> list: 

185 """stage estimation hook calls trace""" 

186 insights = self.estimate_peak_insight() 

187 return insights[stage_id]["Node Log"] 

188 

189 def static_mem_layer( 

190 self, node: Union[str, LayerType], stage_id: int 

191 ) -> Union[float, list]: 

192 """accounting all hooks for this node""" 

193 logs = self.logs_mem_stage(stage_id) 

194 extracted_log = set() 

195 for k in logs.keys(): 

196 if k[3] == node.name[0]: 

197 extracted_log.add(logs[k]["_param"]) 

198 extracted_log = list(extracted_log) 

199 if len(extracted_log) == 1: 

200 return extracted_log[0] 

201 return extracted_log 

202 

203 def dynamic_mem_layer( 

204 self, node: Union[str, LayerType], stage_id: int 

205 ) -> Union[float, list]: 

206 """accounting all hooks for this node""" 

207 logs = self.logs_mem_stage(stage_id) 

208 extracted_log = set() 

209 for k in logs.keys(): 

210 if k[3] == node.name[0]: 

211 extracted_log.add(logs[k]["_activ"] + logs[k]["_comm"]) 

212 extracted_log = list(extracted_log) 

213 if len(extracted_log) == 1: 

214 return extracted_log[0] 

215 return extracted_log 

216 

217 def mem_fit( 

218 self, mem: float, tolerance: float = 0, margin: float = 0 

219 ) -> bool: 

220 """check if input memory fits in device""" 

221 # Expect arguments to be in MB 

222 memory = Memory.from_mb(mem) 

223 tolerance_mem = Memory.from_mb(tolerance) 

224 margin_mem = Memory.from_mb(margin) 

225 cap = self._ccfg.device_capacity - margin_mem 

226 diff = abs(memory - cap) 

227 is_close = diff <= tolerance_mem 

228 is_fit = memory <= cap 

229 

230 if Memory.zero() < tolerance_mem and is_close: 

231 logger.info( 

232 "Prediction is CLOSE to memory device (%s of diff)", diff 

233 ) 

234 return True 

235 if is_fit: 

236 logger.output( 

237 "estimation FITS into device memory (%s<=%s-%s)", 

238 mem, 

239 self._ccfg.device_capacity, 

240 margin_mem, 

241 ) 

242 else: 

243 logger.output( 

244 "estimation DOES NOT FIT into device memory (%s>%s-%s)", 

245 mem, 

246 self._ccfg.device_capacity, 

247 margin_mem, 

248 ) 

249 return is_fit 

250 

251 

252def estimate_memory(config: Any) -> bool: 

253 """fast usage for estimation/fit for given input config""" 

254 e = EvaluatorV2(config) 

255 peak = e.estimate_peak() 

256 return e.mem_fit(peak) 

257 

258 

259def main(): 

260 """commandline""" 

261 parser = argparse.ArgumentParser( 

262 description="Command line usage: Estimate peak stage memory" 

263 ) 

264 

265 parser.add_argument( 

266 "model_config_path", 

267 nargs=1, 

268 help="Model config file (MindFormer YAML or MindSpeed JSON)", 

269 ) 

270 

271 parser.add_argument( 

272 "--framework", 

273 default=None, 

274 type=str, 

275 help="Specify a framework name", 

276 ) 

277 

278 parser.add_argument( 

279 "--code-path", 

280 default=None, 

281 type=str, 

282 help="Specify a source code path (Additional parsing)", 

283 ) 

284 

285 parser.add_argument( 

286 "--verbose", action="store_true", help="Show estimation trace" 

287 ) 

288 parser.add_argument("--plot", action="store_true", help="Plot estimation") 

289 parser.add_argument( 

290 "--fit", 

291 action="store_true", 

292 help="Check if estimation fits in device memory", 

293 ) 

294 parser.add_argument( 

295 "--stage", default=-1, type=int, help="Specify pipeline stage ID" 

296 ) 

297 parser.add_argument( 

298 "--hook", 

299 default=None, 

300 type=str, 

301 help="Specify hook class (defined in hooks/)", 

302 ) 

303 parser.add_argument( 

304 "--trace-fun", 

305 default=None, 

306 type=str, 

307 help="Specify a formula function name to get it traced", 

308 ) 

309 parser.add_argument( 

310 "--ppb", 

311 action="store_true", 

312 help="Generate pipeline balancing layers description", 

313 ) 

314 parser.add_argument( 

315 "--ctx", action="store_true", help="Show ctx variables" 

316 ) 

317 parser.add_argument( 

318 "--ccfg", action="store_true", help="Show ccfg variables" 

319 ) 

320 parser.add_argument( 

321 "--warnings", action="store_true", help="Show warnings" 

322 ) 

323 args = parser.parse_args() 

324 

325 path = args.model_config_path[0] 

326 if not os.path.exists(path): 

327 raise argparse.ArgumentTypeError(f"`{path}` was not found") 

328 if not path.endswith((".yaml", ".json",".toml")): 

329 raise argparse.ArgumentTypeError(f"`{path}` has invalid file type") 

330 

331 e = EvaluatorV2( 

332 path, 

333 framework=args.framework, 

334 source_code=args.code_path, 

335 log_level=args.warnings, 

336 hook_cls=args.hook, 

337 trace_fun=args.trace_fun, 

338 ) 

339 if args.ctx: 

340 e.print_ctx() 

341 if args.ccfg: 

342 e.print_ccfg() 

343 if args.ppb: 

344 ppb = e.estimate_layer_memory() 

345 print(json.dumps(ppb, indent=2)) 

346 else: 

347 peak_mem = e.estimate_peak( 

348 verbose=args.verbose, spec_stage_id=args.stage, plot=args.plot 

349 ) 

350 

351 if args.fit: 

352 e.mem_fit(peak_mem) 

353 

354 

355if __name__ == "__main__": 

356 main()