Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / trainer / text_trainer.py: 39%

172 statements  

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

1# Copyright 2025-2026 Bytedance Ltd. and/or its affiliates 

2# Copyright 2026 Huawei Technologies Co., Ltd 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); 

5# you may not use this file except in compliance with the License. 

6# You may obtain a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, 

12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 

13# See the License for the specific language governing permissions and 

14# limitations under the License. 

15"""Text Trainer assembled from the shared BaseTrainer stages.""" 

16 

17__all__ = ["TextTrainer"] 

18 

19from collections import defaultdict 

20from typing import Any, Dict 

21 

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

23 

24from hyper_parallel.data.batching import calculate_num_micro_batches 

25from hyper_parallel.data.text import build_chat_template 

26from hyper_parallel.trainer.runtime.loss_aggregation import count_loss_token 

27from hyper_parallel.trainer.runtime.logging import create_logger 

28from hyper_parallel.trainer.runtime.memory import print_device_mem_info 

29from hyper_parallel.trainer.runtime.memory_profiler import memory_profiler 

30from hyper_parallel.trainer.runtime.device import synchronize 

31from hyper_parallel.trainer.base import BaseTrainer 

32from hyper_parallel.trainer.config import TrainerConfig 

33 

34logger = create_logger(__name__) 

35 

36 

37class TextTrainer: 

38 """Compose the text training runtime from explicit BaseTrainer stages.""" 

39 

40 base: BaseTrainer 

41 

42 def __init__(self, config: TrainerConfig) -> None: 

43 """Build the text Trainer in the same explicit order as VeOmni.""" 

44 self.base = BaseTrainer.__new__(BaseTrainer) 

45 self.base.config = config 

46 

47 self.base._setup() 

48 memory_profiler.reset( 

49 self.base.config.memory, 

50 global_rank=self.base.global_rank, 

51 tp_rank=self.base.mesh.tp_rank, 

52 dp_rank=self.base.mesh.dp_rank, 

53 ) 

54 self.base._build_model() 

55 self.base._build_loss() 

56 

57 # datasets 

58 self._build_model_assets() 

59 self._build_data_transform() 

60 self.base._build_dataset() 

61 

62 # dataloader 

63 self._build_collate_fn() 

64 self.base._build_dataloader() 

65 

66 # get_batch 

67 self._build_get_batch() 

68 self.base.attach_model_integration_data_pipeline() 

69 self.base._compute_train_iters() 

70 

71 self.base._build_optimizer() 

72 self.base.model_integration.attach_optimizer(self.base.optimizer) 

73 self.base._build_lr_scheduler() 

74 self.base._build_training_context() 

75 self.base._build_graph_compiler() 

76 self.base._init_callbacks() 

77 

78 def _build_model_assets(self) -> None: 

79 """Build tokenizer-backed assets for text training.""" 

80 config: TrainerConfig = self.base.config 

81 if config.dataset is None: 

82 raise ValueError("dataset must define a build target") 

83 assets_config = config.dataset.model_assets 

84 tokenizer_target = assets_config.tokenizer 

85 if tokenizer_target is None: 

86 self.base.tokenizer = None 

87 elif getattr(tokenizer_target, "pretrained_model_name_or_path", None): 

88 self.base.tokenizer = tokenizer_target.build() 

89 else: 

90 self.base.tokenizer = tokenizer_target.build( 

91 pretrained_model_name_or_path=config.model.pretrained_model_name_or_path, 

92 ) 

93 

94 self.base.chat_template = None 

95 if assets_config.chat_template is not None: 

96 if self.base.tokenizer is None: 

97 raise ValueError("dataset.model_assets.tokenizer is required for conversation data") 

98 if isinstance(assets_config.chat_template, str): 

99 self.base.chat_template = build_chat_template( 

100 assets_config.chat_template, 

101 self.base.tokenizer, 

102 ) 

103 else: 

104 self.base.chat_template = assets_config.chat_template.build( 

105 tokenizer=self.base.tokenizer, 

106 ) 

107 

108 self.base.model_assets = [self.base.model_config] 

109 if self.base.tokenizer is not None: 

110 self.base.model_assets.append(self.base.tokenizer) 

111 if self.base.chat_template is not None: 

112 self.base.model_assets.append(self.base.chat_template) 

113 

114 def _build_data_transform(self) -> None: 

115 """Build the configured text sample transform.""" 

116 dataset_config = self.base.config.dataset 

117 if dataset_config is None: 

118 raise ValueError("dataset must define a build target") 

119 if dataset_config.data_transform is None: 

120 self.base.data_transform = None 

121 return 

122 self.base.data_transform = dataset_config.data_transform.build( 

123 tokenizer=self.base.tokenizer, 

124 chat_template=self.base.chat_template, 

125 ) 

126 

127 def _build_collate_fn(self) -> None: 

128 """Build the text collator and gradient-accumulation batch count.""" 

129 dataloader_config = self.base.config.dataloader 

130 if dataloader_config is None or dataloader_config.collate_fn is None: 

131 raise ValueError("dataloader.collate_fn must define a build target") 

132 training_config = self.base.config.training 

133 self.base.num_micro_batches = calculate_num_micro_batches( 

134 global_batch_size=training_config.global_batch_size, 

135 micro_batch_size=training_config.micro_batch_size, 

136 dp_world_size=self.base.mesh.dp_size, 

137 ) 

138 self.base.collate_fn = dataloader_config.collate_fn.build( 

139 mesh_context=self.base.mesh, 

140 ) 

141 

142 def _build_get_batch(self) -> None: 

143 """Build the DataLoader-to-LLM batch runtime.""" 

144 config = self.base.config 

145 if config.dataloader.get_batch is None: 

146 raise ValueError("dataloader.get_batch must define a batching runtime target") 

147 get_batch = config.dataloader.get_batch.build( 

148 mesh_context=self.base.mesh, 

149 device=self.base.device, 

150 tokenizer=self.base.tokenizer, 

151 data_config=getattr(config.dataset, "data_config", {}), 

152 pp_shared_data=bool(getattr(config.dataloader, "pp_shared_data", False)), 

153 ) 

154 self.base.get_batch = get_batch 

155 

156 @property 

157 def distributed_setup(self) -> Any: 

158 """Return the shared distributed setup.""" 

159 return self.base.distributed_setup 

160 

161 @property 

162 def mesh(self) -> Any: 

163 """Return the shared mesh context.""" 

164 return self.base.mesh 

165 

166 @property 

167 def dp_cp_mesh(self) -> Any: 

168 """Return the shared data/context-parallel mesh.""" 

169 return self.base.dp_cp_mesh 

170 

171 def on_train_begin(self) -> None: 

172 """Dispatch the training-begin lifecycle hook.""" 

173 self.base.on_train_begin() 

174 

175 def on_train_end(self) -> None: 

176 """Dispatch the training-end lifecycle hook.""" 

177 self.base.on_train_end() 

178 

179 def on_epoch_begin(self) -> None: 

180 """Dispatch the epoch-begin lifecycle hook.""" 

181 self.base.on_epoch_begin() 

182 

183 def on_epoch_end(self) -> None: 

184 """Dispatch the epoch-end lifecycle hook.""" 

185 self.base.on_epoch_end() 

186 

187 def on_step_begin(self) -> None: 

188 """Dispatch the step-begin lifecycle hook.""" 

189 self.base.on_step_begin() 

190 

191 def on_micro_step_begin(self, micro_batch: dict[str, Any]) -> None: 

192 """Dispatch the micro-step-begin lifecycle hook. 

193 

194 Args: 

195 micro_batch: Prepared inputs and lightweight metric metadata. 

196 """ 

197 self.base.on_micro_step_begin(micro_batch) 

198 

199 def on_step_end( 

200 self, 

201 loss: Any = None, 

202 loss_dict: Any = None, 

203 grad_norm: Any = None, 

204 ) -> None: 

205 """Dispatch the step-end lifecycle hook. 

206 

207 Args: 

208 loss: Aggregated loss for the optimizer step. 

209 loss_dict: Named loss values for the optimizer step. 

210 grad_norm: Gradient norm measured before the optimizer update. 

211 """ 

212 self.base.on_step_end( 

213 loss=loss, 

214 loss_dict=loss_dict, 

215 grad_norm=grad_norm, 

216 ) 

217 

218 def forward_backward_step( 

219 self, 

220 data_iterator: Any, 

221 num_micro_steps: int, 

222 ) -> tuple[torch.Tensor, Dict[str, torch.Tensor]]: 

223 """Fetch and execute one forward-backward micro-step. 

224 

225 Args: 

226 data_iterator: Iterator providing one raw FB batch per call. 

227 num_micro_steps: Number of FB steps in the current optimizer step. 

228 

229 Returns: 

230 Loss tensor and named loss tensors for this FB step. 

231 """ 

232 model_inputs, loss_inputs = self.base.get_batch(data_iterator) 

233 self.base.current_token_counts = count_loss_token(loss_inputs) 

234 self.base.step_token_counts = { 

235 name: token_count * num_micro_steps 

236 for name, token_count in self.base.current_token_counts.items() 

237 } 

238 metric_inputs = { 

239 **model_inputs, 

240 **loss_inputs, 

241 "token_count": self.base.current_token_counts["foundation_tokens"], 

242 } 

243 self.on_micro_step_begin(metric_inputs) 

244 del metric_inputs 

245 loss, loss_dict = self.base.forward_backward_step(model_inputs, loss_inputs) 

246 

247 return loss, loss_dict 

248 

249 def train_step(self, data_iterator: Any) -> Dict[str, float]: 

250 """Execute one text training step. 

251 

252 Args: 

253 data_iterator: Iterator providing the step's micro-batches. 

254 

255 Returns: 

256 Aggregated loss and gradient norm for the completed step. 

257 """ 

258 memory_profiler.step() 

259 num_micro_steps = self.base.num_micro_batches 

260 

261 self.on_step_begin() 

262 self.base.model_integration.begin_step(self.base.state.global_step + 1) 

263 synchronize() 

264 

265 total_loss = 0.0 

266 total_loss_dict = defaultdict(int) 

267 

268 for micro_step in range(num_micro_steps): 

269 self.base.model_reshard(micro_step, num_micro_steps) 

270 self.base.configure_fsdp_gradient_sync( 

271 micro_step, 

272 num_micro_steps, 

273 ) 

274 self.base.begin_fsdp_runtime_diagnostics(micro_step) 

275 loss, loss_dict = self.forward_backward_step( 

276 data_iterator, 

277 num_micro_steps, 

278 ) 

279 

280 total_loss += loss.item() 

281 for loss_name, loss_value in loss_dict.items(): 

282 total_loss_dict[loss_name] += loss_value.item() 

283 

284 grad_norm = self.base.prepare_optimizer_step() 

285 self.base.step_optimizers_and_schedulers() 

286 

287 # Checkpoint and logging callbacks observe the number of completed 

288 # optimizer updates. 

289 self.base.state.global_step += 1 

290 grad_norm_value = float(grad_norm) 

291 self.base._end_model_integration_step( # pylint: disable=protected-access 

292 {"loss": total_loss, "grad_norm": grad_norm_value} 

293 ) 

294 self.on_step_end( 

295 loss=total_loss, 

296 loss_dict=total_loss_dict, 

297 grad_norm=grad_norm_value, 

298 ) 

299 

300 return { 

301 "loss": total_loss, 

302 "grad_norm": grad_norm_value, 

303 } 

304 

305 def train(self) -> None: 

306 """Run the configured global optimizer steps by Dataset epoch.""" 

307 try: 

308 config = self.base.config 

309 self.on_train_begin() 

310 logger.info( 

311 "Rank%s Start training. Global step: %s. Train iters: %s. Start epoch: %s. Train epochs: %s.", 

312 self.base.local_rank, 

313 self.base.state.global_step, 

314 self.base.train_iters, 

315 self.base.state.epoch, 

316 self.base.train_epochs, 

317 ) 

318 

319 # Checkpoint resume restores state.global_step, state.epoch, and the DataLoader cursor. 

320 for epoch in range(self.base.state.epoch, self.base.train_epochs): 

321 train_dataloader = self.base.train_dataloader 

322 if hasattr(train_dataloader, "set_epoch"): 

323 train_dataloader.set_epoch(epoch) 

324 

325 self.base.state.epoch = epoch 

326 self.on_epoch_begin() 

327 data_iterator = iter(train_dataloader) if train_dataloader is not None else None 

328 start_step = self.base.state.global_step - epoch * self.base.train_steps 

329 train_steps = min(self.base.train_steps, self.base.train_iters - epoch * self.base.train_steps) 

330 for _ in range(start_step, train_steps): 

331 try: 

332 self.train_step(data_iterator) 

333 except StopIteration: 

334 logger.info( 

335 "epoch:%s Dataloader finished with drop_last %s", epoch, config.dataloader.drop_last 

336 ) 

337 break 

338 

339 self.on_epoch_end() 

340 self.base.state.epoch = epoch + 1 

341 print_device_mem_info(f"VRAM usage after epoch {epoch + 1}") 

342 if self.base.state.global_step >= self.base.train_iters: 

343 break 

344 

345 memory_profiler.stop() 

346 self.on_train_end() 

347 

348 except BaseException: 

349 try: 

350 memory_profiler.stop() 

351 # Snapshot export must preserve the training error even if interrupted. 

352 except BaseException: # pylint: disable=broad-exception-caught 

353 logger.exception("Failed to save memory snapshot after training error.") 

354 finally: 

355 self.base.abort_profilers() 

356 raise 

357 

358 synchronize() 

359 self.base.destroy_distributed()