Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / runtime / memory_profiler.py: 96%

180 statements  

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

1# Copyright 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"""Process-local allocator memory profiler used by Trainer runtimes.""" 

16 

17from __future__ import annotations 

18 

19import logging 

20import os 

21import stat 

22import sys 

23import time 

24from typing import Any, Optional 

25 

26from hyper_parallel.trainer.config.training import MemoryConfig 

27from hyper_parallel.trainer.runtime.device import get_device_type, get_torch_device 

28 

29 

30logger = logging.getLogger(__name__) 

31 

32 

33class MemoryProfiler: 

34 """Manage one allocator-history profiling session in the current process. 

35 

36 Note: 

37 The module owns one singleton, so only one Trainer profiling session 

38 may be active in a process at a time. 

39 """ 

40 

41 def __init__(self) -> None: 

42 """Initialize an inactive profiler.""" 

43 self.enable = False 

44 self.mem_info = False 

45 self.current_step = 0 

46 self.start_step = 0 

47 self.end_step = 0 

48 self.save_path = "" 

49 self.dump_ranks: list[int] = [] 

50 self.stacks = "all" 

51 self.global_rank = 0 

52 self.tp_rank = 0 

53 self.dp_rank = 0 

54 self._session_active = False 

55 self._history_started = False 

56 self._device_api: Any = None 

57 self._memory_api: Any = None 

58 

59 def reset( 

60 self, 

61 config: Optional[MemoryConfig], 

62 *, 

63 global_rank: int, 

64 tp_rank: int, 

65 dp_rank: int, 

66 ) -> None: 

67 """Replace the process-local session with a Trainer configuration. 

68 

69 Args: 

70 config: Memory profiler configuration, or ``None`` to disable it. 

71 global_rank: Rank in the global process group. 

72 tp_rank: Rank in the tensor-parallel group. 

73 dp_rank: Rank in the data-parallel group. 

74 """ 

75 self._discard_previous_session() 

76 if config is None: 

77 return 

78 

79 self._validate(config) 

80 self.enable = config.enable 

81 self.mem_info = config.mem_info 

82 self.current_step = 0 

83 self.start_step = config.start_step 

84 self.end_step = config.end_step 

85 self.save_path = config.save_path 

86 self.dump_ranks = list(config.dump_ranks) 

87 self.stacks = config.stacks 

88 self.global_rank = global_rank 

89 self.tp_rank = tp_rank 

90 self.dp_rank = dp_rank 

91 self._session_active = self.enable or self.mem_info 

92 

93 if self.enable: 

94 self._resolve_device_apis(require_history=True) 

95 # Match MindSpeed's session-relative control point: step zero runs 

96 # after distributed setup and before model construction. 

97 self.step() 

98 

99 def step(self) -> None: 

100 """Advance the state machine before a Trainer training step starts.""" 

101 if not self._session_active: 

102 return 

103 

104 try: 

105 if self.enable: 

106 if self.current_step == self.start_step: 

107 self._record() 

108 # The control point precedes training, so dump after the inclusive end step. 

109 if self.current_step == self.end_step + 1: 

110 try: 

111 self._dump_and_stop_history() 

112 finally: 

113 self.enable = False 

114 if not self.mem_info and not self._history_started: 

115 self._deactivate_session() 

116 

117 if self.mem_info: 

118 self._log_memory_info() 

119 except BaseException: 

120 self.abort() 

121 raise 

122 finally: 

123 self.current_step += 1 

124 

125 if not self.enable and not self.mem_info: 

126 self._deactivate_session() 

127 

128 def stop(self) -> None: 

129 """Dump an active partial window and end a normal training session.""" 

130 if not self._session_active and not self._history_started: 

131 return 

132 

133 try: 

134 try: 

135 if self._history_started: 

136 self._dump_and_stop_history() 

137 finally: 

138 self.enable = False 

139 self.mem_info = False 

140 if not self._history_started: 

141 self._deactivate_session() 

142 except BaseException: 

143 self.abort() 

144 raise 

145 

146 def abort(self) -> None: 

147 """Stop allocator history without dumping or masking a Trainer error.""" 

148 self.enable = False 

149 self.mem_info = False 

150 try: 

151 self._stop_history() 

152 except Exception: # pylint: disable=broad-exception-caught 

153 logger.exception("Failed to stop allocator memory history while aborting training.") 

154 return 

155 

156 self._deactivate_session() 

157 

158 def _discard_previous_session(self) -> None: 

159 if self._history_started: 

160 self._stop_history() 

161 self._deactivate_session() 

162 

163 def _deactivate_session(self) -> None: 

164 self.enable = False 

165 self.mem_info = False 

166 self._session_active = False 

167 self._device_api = None 

168 self._memory_api = None 

169 

170 @staticmethod 

171 def _validate(config: MemoryConfig) -> None: 

172 MemoryProfiler._validate_bool(config.enable, "enable") 

173 MemoryProfiler._validate_bool(config.mem_info, "mem_info") 

174 MemoryProfiler._validate_steps(config.start_step, config.end_step) 

175 MemoryProfiler._validate_save_path(config.save_path) 

176 MemoryProfiler._validate_dump_ranks(config.dump_ranks) 

177 MemoryProfiler._validate_stacks(config.stacks) 

178 

179 @staticmethod 

180 def _validate_bool(value: bool, name: str) -> None: 

181 if not isinstance(value, bool): 

182 raise TypeError(f"memory.{name} must be a bool") 

183 

184 @staticmethod 

185 def _validate_steps(start_step: int, end_step: int) -> None: 

186 for name, value in (("start_step", start_step), ("end_step", end_step)): 

187 if isinstance(value, bool) or not isinstance(value, int): 

188 raise TypeError(f"memory.{name} must be an int") 

189 if start_step < 0: 

190 raise ValueError("memory.start_step must be non-negative") 

191 if end_step < start_step: 

192 raise ValueError("memory.end_step must be greater than or equal to memory.start_step") 

193 

194 @staticmethod 

195 def _validate_save_path(save_path: str) -> None: 

196 if not isinstance(save_path, str) or not save_path: 

197 raise ValueError("memory.save_path must be a non-empty string") 

198 

199 @staticmethod 

200 def _validate_dump_ranks(dump_ranks: list[int]) -> None: 

201 if not isinstance(dump_ranks, list): 

202 raise ValueError("memory.dump_ranks must be a list of non-negative integers") 

203 if any( 

204 isinstance(rank, bool) or not isinstance(rank, int) or rank < 0 

205 for rank in dump_ranks 

206 ): 

207 raise ValueError("memory.dump_ranks must be a list of non-negative integers") 

208 

209 @staticmethod 

210 def _validate_stacks(stacks: str) -> None: 

211 if stacks not in ("python", "all"): 

212 raise ValueError("memory.stacks must be 'python' or 'all'") 

213 

214 def _record(self) -> None: 

215 """Start allocator memory history recording.""" 

216 self._history_started = True 

217 try: 

218 # Allocator history has no public API in Torch or torch-npu. 

219 self._memory_api._record_memory_history( # pylint: disable=protected-access 

220 stacks=self.stacks, 

221 max_entries=sys.maxsize, 

222 ) 

223 except Exception: 

224 # The private allocator API does not expose whether a failed call 

225 # enabled history, so make a best-effort stop before propagating. 

226 self.abort() 

227 raise 

228 

229 def _dump_and_stop_history(self) -> None: 

230 try: 

231 self._dump() 

232 finally: 

233 self._stop_history() 

234 

235 def _dump(self) -> None: 

236 """Dump the allocator snapshot for a configured rank.""" 

237 if self.global_rank not in self.dump_ranks: 

238 return 

239 os.makedirs(self.save_path, exist_ok=True) 

240 timestamp = time.strftime("%Y-%m-%d-%H-%M") 

241 file_path = os.path.join( 

242 self.save_path, 

243 f"snapshot_{timestamp}_{self.global_rank}.pickle", 

244 ) 

245 # Allocator snapshots have no public API in Torch or torch-npu. 

246 self._memory_api._dump_snapshot(file_path) # pylint: disable=protected-access 

247 # Some torch-npu releases create snapshots with owner-write permission only. 

248 os.chmod(file_path, stat.S_IRUSR | stat.S_IWUSR) 

249 logger.info("Memory snapshot dumped to %s", file_path) 

250 

251 def _stop_history(self) -> None: 

252 if not self._history_started: 

253 return 

254 self._memory_api._record_memory_history(enabled=None) # pylint: disable=protected-access 

255 self._history_started = False 

256 

257 def _log_memory_info(self) -> None: 

258 """Log and reset peak allocator statistics for the current step.""" 

259 self._resolve_device_apis(require_history=False) 

260 logger.info( 

261 "Memory usage step=%s global_rank=%s tp_rank=%s dp_rank=%s " 

262 "max_memory_reserved=%s max_memory_allocated=%s", 

263 self.current_step, 

264 self.global_rank, 

265 self.tp_rank, 

266 self.dp_rank, 

267 self._device_api.max_memory_reserved(), 

268 self._device_api.max_memory_allocated(), 

269 ) 

270 self._device_api.reset_peak_memory_stats() 

271 

272 def _resolve_device_apis(self, *, require_history: bool) -> None: 

273 """Resolve and validate the active device memory APIs.""" 

274 if self._device_api is None: 

275 device_type = get_device_type() 

276 if device_type not in ("cuda", "npu"): 

277 raise RuntimeError( 

278 "memory profiling requires a CUDA or NPU device, " 

279 f"but the active device type is {device_type!r}" 

280 ) 

281 self._device_api = get_torch_device() 

282 

283 if not require_history or self._memory_api is not None: 

284 return 

285 self._memory_api = getattr(self._device_api, "memory", None) 

286 required_methods = ("_record_memory_history", "_dump_snapshot") 

287 missing = [ 

288 method 

289 for method in required_methods 

290 if self._memory_api is None 

291 or not callable(getattr(self._memory_api, method, None)) 

292 ] 

293 if missing: 

294 raise RuntimeError( 

295 "memory profiling backend is missing allocator API(s): " 

296 + ", ".join(missing) 

297 ) 

298 

299 

300memory_profiler = MemoryProfiler() 

301 

302 

303__all__ = ["memory_profiler"]