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

118 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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"""Terminal progress callback for Trainer.""" 

16 

17from __future__ import annotations 

18 

19import logging 

20import sys 

21import time 

22from typing import Any 

23 

24from .base import Callback, TrainerState 

25 

26 

27logger = logging.getLogger(__name__) 

28 

29 

30class _LogProgressBar: 

31 """Write one finalized tqdm-formatted snapshot per step to a non-TTY stream.""" 

32 

33 def __init__( 

34 self, 

35 tqdm_type: Any, 

36 total: int, 

37 initial: int, 

38 stream: Any, 

39 ) -> None: 

40 """Initialize non-interactive progress state without emitting a line.""" 

41 self._tqdm_type = tqdm_type 

42 self._initial = initial 

43 self._start_time = time.monotonic() 

44 self._postfix: dict[str, str] = {} 

45 self._pending_message: str | None = None 

46 self.total = total 

47 self.n = initial 

48 self.fp = stream 

49 

50 def set_postfix(self, postfix: dict[str, str], refresh: bool = True) -> None: 

51 """Store the final metrics for the next completed-step snapshot. 

52 

53 Args: 

54 postfix: Formatted metric names and values. 

55 refresh: Accepted for compatibility with the tqdm API. 

56 """ 

57 del refresh 

58 self._postfix = dict(postfix) 

59 

60 def update(self, amount: int) -> None: 

61 """Advance progress and write exactly one newline-terminated snapshot. 

62 

63 Args: 

64 amount: Number of newly completed steps. 

65 """ 

66 self.n += amount 

67 elapsed = max(time.monotonic() - self._start_time, 0.0) 

68 completed = self.n - self._initial 

69 rate = completed / elapsed if elapsed > 0 else None 

70 postfix = ", ".join(f"{name}={value}" for name, value in self._postfix.items()) 

71 progress_line = self._tqdm_type.format_meter( 

72 n=self.n, 

73 total=self.total, 

74 elapsed=elapsed, 

75 prefix="Training", 

76 unit="step", 

77 rate=rate, 

78 postfix=postfix or None, 

79 ) 

80 if self._pending_message: 

81 progress_line = f"{progress_line} | {self._pending_message}" 

82 self.fp.write(f"{progress_line}\n") 

83 self.fp.flush() 

84 self._pending_message = None 

85 

86 def write(self, message: str, file: Any = None) -> None: 

87 """Attach one structured message to the next progress snapshot. 

88 

89 Args: 

90 message: Structured log message for the current step. 

91 file: Accepted for compatibility with the tqdm API. 

92 """ 

93 del file 

94 self._pending_message = message 

95 

96 def close(self) -> None: 

97 """Close without writing a duplicate final progress line.""" 

98 

99 

100class TqdmCallback(Callback): 

101 """Display global training progress and shared metrics on global rank zero.""" 

102 

103 _PRIMARY_METRICS = ( 

104 ("training/total_loss", "loss"), 

105 ("training/grad_norm", "grad_norm"), 

106 ("training/lr", "lr"), 

107 ) 

108 _ENV_METRICS = ( 

109 ("performance/tokens_per_second", "tokens/s"), 

110 ("performance/step_time", "step_time"), 

111 ("performance/tflops_per_device", "TFLOP/s"), 

112 ) 

113 

114 def __init__(self, trainer: Any) -> None: 

115 """Initialize progress state without producing terminal output. 

116 

117 Args: 

118 trainer: Trainer that owns the callback lifecycle. 

119 """ 

120 super().__init__(trainer) 

121 self._progress_bar = None 

122 self._last_updated_step = 0 

123 self._missing_dependency_warned = False 

124 

125 @staticmethod 

126 def _to_float(value: Any) -> float | None: 

127 """Convert a scalar or scalar tensor-like value to a float.""" 

128 if value is None: 

129 return None 

130 item = getattr(value, "item", None) 

131 if callable(item): 

132 value = item() 

133 return float(value) 

134 

135 @classmethod 

136 def _format_metric(cls, name: str, value: Any) -> str: 

137 """Format a shared metric using a unit-appropriate representation.""" 

138 scalar = cls._to_float(value) 

139 if scalar is None: 

140 return "n/a" 

141 if name == "training/lr": 

142 return f"{scalar:.3e}" 

143 if name == "performance/tokens_per_second": 

144 return f"{scalar:.3f}" 

145 if name == "performance/step_time": 

146 return f"{scalar:.3f}s" 

147 if name == "performance/tflops_per_device": 

148 return f"{scalar:.3f}" 

149 return f"{scalar:.6g}" 

150 

151 def _create_progress_bar(self, total: int, initial: int) -> Any: 

152 """Create tqdm lazily because it is an optional dependency.""" 

153 try: 

154 from tqdm import tqdm # pylint: disable=C0415 

155 except ImportError: 

156 if not self._missing_dependency_warned: 

157 logger.warning("TqdmCallback: 'tqdm' is not installed; progress bar disabled") 

158 self._missing_dependency_warned = True 

159 return None 

160 stream = sys.stderr 

161 isatty = getattr(stream, "isatty", None) 

162 if not callable(isatty) or not isatty(): 

163 return _LogProgressBar( 

164 tqdm_type=tqdm, 

165 total=total, 

166 initial=initial, 

167 stream=stream, 

168 ) 

169 return tqdm( 

170 total=total, 

171 initial=initial, 

172 desc="Training", 

173 unit="step", 

174 dynamic_ncols=True, 

175 file=stream, 

176 ) 

177 

178 def _postfix(self) -> dict[str, str]: 

179 """Build the progress postfix exclusively from shared metric dictionaries.""" 

180 train_metrics = getattr(self.trainer, "step_train_metrics", {}) 

181 env_metrics = getattr(self.trainer, "step_env_metrics", {}) 

182 postfix = { 

183 label: self._format_metric(name, train_metrics[name]) 

184 for name, label in self._PRIMARY_METRICS 

185 if name in train_metrics 

186 } 

187 

188 primary_names = {name for name, _ in self._PRIMARY_METRICS} 

189 for name, value in sorted(train_metrics.items()): 

190 if name in primary_names: 

191 continue 

192 label = name.split("/", 1)[-1] 

193 postfix[label] = self._format_metric(name, value) 

194 

195 for name, label in self._ENV_METRICS: 

196 if name in env_metrics: 

197 postfix[label] = self._format_metric(name, env_metrics[name]) 

198 return postfix 

199 

200 def on_train_begin(self, state: TrainerState, **kwargs: Any) -> None: 

201 """Create a single full-training progress bar on global rank zero. 

202 

203 Args: 

204 state: Current training state. 

205 **kwargs: Additional callback arguments. 

206 """ 

207 del kwargs 

208 self._last_updated_step = state.global_step 

209 if getattr(self.trainer, "global_rank", 0) != 0: 

210 return 

211 self._progress_bar = self._create_progress_bar( 

212 total=self.trainer.train_iters, 

213 initial=state.global_step, 

214 ) 

215 

216 def on_step_end( 

217 self, 

218 state: TrainerState, 

219 loss: float | None = None, 

220 loss_dict: dict[str, float] | None = None, 

221 grad_norm: float | None = None, 

222 **kwargs: Any, 

223 ) -> None: 

224 """Publish shared metrics and advance once per completed global step. 

225 

226 Args: 

227 state: Current training state. 

228 loss: Aggregated loss for the completed step. 

229 loss_dict: Named loss values for the completed step. 

230 grad_norm: Gradient norm measured before the optimizer update. 

231 **kwargs: Additional callback arguments. 

232 """ 

233 del loss, loss_dict, grad_norm, kwargs 

234 if self._progress_bar is None or state.global_step <= self._last_updated_step: 

235 return 

236 

237 postfix = self._postfix() 

238 if postfix: 

239 self._progress_bar.set_postfix(postfix, refresh=False) 

240 self._progress_bar.update(state.global_step - self._last_updated_step) 

241 self._last_updated_step = state.global_step 

242 

243 def write(self, message: str) -> bool: 

244 """Write above the active progress bar and redraw it. 

245 

246 Args: 

247 message: Complete terminal line to display. 

248 

249 Returns: 

250 Whether an active progress bar handled the message. 

251 """ 

252 if self._progress_bar is None: 

253 return False 

254 self._progress_bar.write(message, file=getattr(self._progress_bar, "fp", None)) 

255 return True 

256 

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

258 """Close the global progress bar idempotently. 

259 

260 Args: 

261 state: Final training state. 

262 **kwargs: Additional callback arguments. 

263 """ 

264 del state, kwargs 

265 if self._progress_bar is not None: 

266 self._progress_bar.close() 

267 self._progress_bar = None