Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / callbacks / profiler_callback.py: 90%

161 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"""MindFormers-derived training profiler backed by PyTorch.""" 

16 

17from __future__ import annotations 

18 

19import json 

20import logging 

21from pathlib import Path 

22from typing import TYPE_CHECKING, Any, Optional 

23 

24# Trainer is Torch-owned; match the other callbacks under the legacy backend-import checker. 

25import torch # pylint: disable=forbidden-backend-import 

26 

27from hyper_parallel.trainer.callbacks.base import Callback 

28from hyper_parallel.trainer.config.training import ProfilingAnalysisConfig, ProfilingConfig 

29from hyper_parallel.trainer.runtime.device import get_device_type 

30from hyper_parallel.trainer.state import TrainerState 

31 

32 

33if TYPE_CHECKING: 

34 from hyper_parallel.trainer.base import BaseTrainer 

35 

36 

37logger = logging.getLogger(__name__) 

38 

39 

40class ProfilerCallback(Callback): 

41 """Collect a bounded CPU, CUDA, or Ascend NPU profile. 

42 

43 The profiler schedule, rank selection, output layout, metadata, and NPU 

44 options follow MindFormers ``ProfileMonitor`` semantics. PyTorch profiler 

45 activities replace the corresponding MindSpore profiler backend. 

46 """ 

47 

48 def __init__(self, trainer: "BaseTrainer") -> None: 

49 """Build the profiler selected by ``TrainerConfig.profiler``. 

50 

51 Args: 

52 trainer: Trainer that owns the callback lifecycle. 

53 """ 

54 super().__init__(trainer) 

55 self.config: ProfilingConfig = trainer.config.profiler 

56 self.profiler: Any = None 

57 self.output_path: Optional[Path] = None 

58 self._profiler_module: Any = None 

59 self._device_type: Optional[str] = None 

60 self._is_profiler_started = False 

61 self._metadata_recorded = False 

62 self._training_step = 0 

63 

64 if not self.config.enable: 

65 return 

66 

67 self._validate_types() 

68 self.start_step, self.stop_step = self._normalize_steps() 

69 self.level = self._normalize_level() 

70 

71 rank = trainer.global_rank 

72 if not self._is_profile_required(rank): 

73 return 

74 

75 self._device_type, self._profiler_module = self._resolve_profiler_module() 

76 

77 output_root = Path(self.config.output_path or "./output").expanduser() 

78 self.output_path = output_root / "profile" / f"rank_{rank}" 

79 logger.info("Profile save path: %s", self.output_path) 

80 

81 schedule_config = self._get_schedule() 

82 trace_handler_options = {} 

83 if self._device_type == "npu": 

84 trace_handler_options = { 

85 "analyse_flag": self.config.analysis.enable, 

86 "async_mode": self.config.analysis.async_mode, 

87 } 

88 profile_kwargs = { 

89 "activities": self._get_activities(), 

90 "profile_memory": self.config.memory, 

91 "with_stack": self.config.with_stack, 

92 "schedule": schedule_config, 

93 "on_trace_ready": self._profiler_module.tensorboard_trace_handler( 

94 str(self.output_path), **trace_handler_options 

95 ), 

96 } 

97 if self._device_type == "npu": 

98 profile_kwargs["experimental_config"] = self._get_npu_experimental_config() 

99 self.profiler = self._profiler_module.profile(**profile_kwargs) 

100 

101 def on_step_begin( 

102 self, 

103 state: TrainerState, 

104 micro_batches: Optional[list[dict[str, Any]]] = None, 

105 **kwargs: Any, 

106 ) -> None: 

107 """Start the profiler and align its schedule with the first training step. 

108 

109 Args: 

110 state: Current trainer state. 

111 micro_batches: Optional micro-batches prepared for the current step. 

112 **kwargs: Additional callback context supplied by the trainer. 

113 """ 

114 del state, micro_batches, kwargs 

115 if self.profiler is None: 

116 return 

117 

118 self._training_step += 1 

119 if not self._is_profiler_started: 

120 self._start() 

121 if self._training_step == self.start_step: 

122 self._record_metadata() 

123 

124 def on_step_end( 

125 self, 

126 state: TrainerState, 

127 loss: Optional[float] = None, 

128 loss_dict: Optional[dict[str, float]] = None, 

129 grad_norm: Optional[float] = None, 

130 **kwargs: Any, 

131 ) -> None: 

132 """Advance the profiler schedule after one complete optimizer step. 

133 

134 Args: 

135 state: Current trainer state. 

136 loss: Optional scalar loss for the completed step. 

137 loss_dict: Optional named losses for the completed step. 

138 grad_norm: Optional gradient norm for the completed step. 

139 **kwargs: Additional callback context supplied by the trainer. 

140 """ 

141 del state, loss, loss_dict, grad_norm, kwargs 

142 if self.profiler is None or not self._is_profiler_started: 

143 return 

144 

145 self.profiler.step() 

146 if self._training_step == self.stop_step: 

147 logger.info("End of profiling. Analyze the trace under %s", self.output_path) 

148 self._stop() 

149 

150 def on_train_end(self, state: TrainerState, **kwargs: Any) -> None: 

151 """Flush a partial profile when training ends before the stop step. 

152 

153 Args: 

154 state: Final trainer state. 

155 **kwargs: Additional callback context supplied by the trainer. 

156 """ 

157 del state, kwargs 

158 self.close() 

159 

160 def close(self) -> None: 

161 """Stop collection and release a profiler after completion or failure.""" 

162 self._stop() 

163 

164 def _validate_types(self) -> None: 

165 """Validate profiler fields that dataclass construction cannot constrain.""" 

166 boolean_fields = ( 

167 "enable", 

168 "memory", 

169 "with_stack", 

170 "data_simplification", 

171 ) 

172 for name in boolean_fields: 

173 if not isinstance(getattr(self.config, name), bool): 

174 raise TypeError(f"profiler.{name} must be a bool") 

175 if not isinstance(self.config.analysis, ProfilingAnalysisConfig): 

176 raise TypeError("profiler.analysis must be a ProfilingAnalysisConfig") 

177 for name in ("enable", "async_mode"): 

178 if not isinstance(getattr(self.config.analysis, name), bool): 

179 raise TypeError(f"profiler.analysis.{name} must be a bool") 

180 for name in ("start_step", "stop_step", "warmup_steps"): 

181 value = getattr(self.config, name) 

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

183 raise TypeError(f"profiler.{name} must be an int") 

184 if self.config.output_path is not None and not isinstance(self.config.output_path, str): 

185 raise TypeError("profiler.output_path must be a string or null") 

186 ranks = self.config.rank_ids 

187 if ranks is not None: 

188 if not isinstance(ranks, list): 

189 raise TypeError("profiler.rank_ids must be a list or null") 

190 if any(isinstance(rank, bool) or not isinstance(rank, int) or not 0 <= rank < self.trainer.world_size 

191 for rank in ranks): 

192 raise ValueError(f"profiler.rank_ids must contain only integers in [0, {self.trainer.world_size})") 

193 

194 def _normalize_steps(self) -> tuple[int, int]: 

195 """Normalize the inclusive profiling window to valid positive steps.""" 

196 start_step = self.config.start_step 

197 stop_step = self.config.stop_step 

198 if start_step < 1: 

199 logger.warning("profiler.start_step must be greater than 0; reset it to 1") 

200 start_step = 1 

201 if stop_step < 1: 

202 logger.warning("profiler.stop_step must be greater than 0; reset it to 10") 

203 stop_step = 10 

204 if start_step > stop_step: 

205 logger.warning( 

206 "profiler.stop_step must be greater than or equal to profiler.start_step; reset both to 1 and 10" 

207 ) 

208 start_step, stop_step = 1, 10 

209 if not 0 <= self.config.warmup_steps < start_step: 

210 raise ValueError("profiler.warmup_steps must be non-negative and less than profiler.start_step") 

211 return start_step, stop_step 

212 

213 def _normalize_level(self) -> int: 

214 level = self.config.level 

215 if level is None: 

216 return 0 

217 if isinstance(level, bool) or not isinstance(level, int): 

218 raise TypeError("profiler.level must be an int or null") 

219 if level not in (0, 1, 2): 

220 logger.warning("Invalid profiler.level %s; reset it to 0", level) 

221 return 0 

222 return level 

223 

224 def _get_schedule(self) -> Any: 

225 return self._profiler_module.schedule( 

226 wait=0, 

227 warmup=self.config.warmup_steps, 

228 active=self.stop_step - self.start_step + 1, 

229 repeat=1, 

230 skip_first=self.start_step - self.config.warmup_steps, 

231 ) 

232 

233 def _is_profile_required(self, rank: int) -> bool: 

234 """Return whether the current rank is selected for profiling.""" 

235 ranks = self.config.rank_ids or [] 

236 return not ranks or rank in ranks 

237 

238 def _resolve_profiler_module(self) -> tuple[str, Any]: 

239 """Resolve the profiler backend for the active device type.""" 

240 device_type = get_device_type() 

241 if device_type == "npu": 

242 # torch-npu is optional and must not become a trainer import-time dependency. 

243 import torch_npu # pylint: disable=import-outside-toplevel 

244 

245 return device_type, torch_npu.profiler 

246 if device_type in ("cpu", "cuda"): 

247 return device_type, torch.profiler 

248 raise RuntimeError(f"profiler does not support device type {device_type!r}") 

249 

250 def _get_activities(self) -> list[Any]: 

251 activities = [self._profiler_module.ProfilerActivity.CPU] 

252 if self._device_type == "cuda": 

253 activities.append(self._profiler_module.ProfilerActivity.CUDA) 

254 elif self._device_type == "npu": 

255 activities.append(self._profiler_module.ProfilerActivity.NPU) 

256 return activities 

257 

258 def _get_npu_experimental_config(self) -> Any: 

259 """Build the torch-npu experimental profiler configuration.""" 

260 profiler_level = getattr(self._profiler_module.ProfilerLevel, f"Level{self.level}") 

261 options = { 

262 "profiler_level": profiler_level, 

263 "data_simplification": self.config.data_simplification, 

264 "mstx": False, 

265 } 

266 export_type = getattr(getattr(self._profiler_module, "ExportType", None), "Text", None) 

267 if export_type is not None: 

268 options["export_type"] = [export_type] 

269 # torch-npu exposes this version-dependent profiler entry point only under its private name. 

270 experimental_config = getattr(self._profiler_module, "_ExperimentalConfig") 

271 return experimental_config(**options) 

272 

273 def _start(self) -> None: 

274 self.profiler.start() 

275 self.profiler.step() 

276 self._is_profiler_started = True 

277 

278 def _record_metadata(self) -> None: 

279 """Record distributed topology metadata once per profiling session.""" 

280 if self._metadata_recorded: 

281 return 

282 parallel = self.trainer.config.accelerator 

283 metadata = { 

284 "tensor_model_parallel_size": parallel.tp_size, 

285 "pipeline_model_parallel_size": parallel.pp_size, 

286 "data_parallel_size": getattr(self.trainer.mesh, "dp_size", 1), 

287 "expert_model_parallel_size": parallel.ep_size, 

288 "sequence_parallel": parallel.sequence_parallel, 

289 "parallel_mode": "distributed" if self.trainer.world_size > 1 else "stand_alone", 

290 "world_size": self.trainer.world_size, 

291 } 

292 try: 

293 self.profiler.add_metadata_json("distributed_args", json.dumps(metadata)) 

294 self._metadata_recorded = True 

295 except AttributeError as error: 

296 logger.warning("Profiler failed to record distributed args: %s", error) 

297 

298 def _stop(self) -> None: 

299 """Stop the profiler session and release its resources.""" 

300 profiler = self.profiler 

301 self.profiler = None 

302 try: 

303 if profiler is not None and self._is_profiler_started: 

304 profiler.stop() 

305 finally: 

306 self._is_profiler_started = False 

307 

308 

309__all__ = ["ProfilerCallback"]