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

201 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-11-15 07:18 +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 itertools import count 

21from typing import Any, Dict 

22 

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

24 

25from hyper_parallel.data.batching import calculate_num_micro_batches 

26from hyper_parallel.data.text import build_chat_template 

27from hyper_parallel.trainer.runtime.loss_aggregation import count_loss_token 

28from hyper_parallel.trainer.runtime.logging import create_logger 

29from hyper_parallel.trainer.runtime.memory import print_device_mem_info 

30from hyper_parallel.trainer.runtime.memory_profiler import memory_profiler 

31from hyper_parallel.trainer.runtime.device import synchronize 

32from hyper_parallel.trainer.base import BaseTrainer 

33from hyper_parallel.distributed_data import DistributedDataLoader 

34from hyper_parallel.trainer.config import TrainerConfig 

35 

36logger = create_logger(__name__) 

37 

38 

39class TextTrainer: 

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

41 

42 base: BaseTrainer 

43 

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

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

46 self.base = BaseTrainer.__new__(BaseTrainer) 

47 self.base.config = config 

48 

49 self.base._setup() 

50 memory_profiler.reset( 

51 self.base.config.memory, 

52 global_rank=self.base.global_rank, 

53 tp_rank=self.base.mesh.tp_rank, 

54 dp_rank=self.base.mesh.dp_rank, 

55 ) 

56 self.base._build_model() 

57 self.base._build_loss() 

58 

59 # datasets 

60 self._build_model_assets() 

61 self._build_data_transform() 

62 self.base._build_dataset() 

63 

64 # dataloader 

65 self._build_collate_fn() 

66 self.base._build_dataloader() 

67 

68 # get_batch 

69 self._build_get_batch() 

70 self.base.attach_model_integration_data_pipeline() 

71 self.base._compute_train_iters() 

72 

73 self.base._build_optimizer() 

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

75 self.base._build_lr_scheduler() 

76 self.base._build_training_context() 

77 self.base._build_graph_compiler() 

78 self.base._init_callbacks() 

79 

80 def _build_model_assets(self) -> None: 

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

82 config: TrainerConfig = self.base.config 

83 if config.dataset is None: 

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

85 assets_config = config.dataset.model_assets 

86 tokenizer_target = assets_config.tokenizer 

87 if tokenizer_target is None: 

88 self.base.tokenizer = None 

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

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

91 else: 

92 self.base.tokenizer = tokenizer_target.build( 

93 pretrained_model_name_or_path=config.model.pretrained_model_name_or_path, 

94 ) 

95 

96 self.base.chat_template = None 

97 if assets_config.chat_template is not None: 

98 if self.base.tokenizer is None: 

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

100 if isinstance(assets_config.chat_template, str): 

101 self.base.chat_template = build_chat_template( 

102 assets_config.chat_template, 

103 self.base.tokenizer, 

104 chat_template_kwargs=assets_config.chat_template_kwargs, 

105 log_first_rendered_template=config.debug.log_first_rendered_template, 

106 ) 

107 else: 

108 if assets_config.chat_template_kwargs or config.debug.log_first_rendered_template: 

109 raise ValueError( 

110 "dataset.model_assets chat-template options require chat_template='tokenizer'" 

111 ) 

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

113 tokenizer=self.base.tokenizer, 

114 ) 

115 

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

117 if self.base.tokenizer is not None: 

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

119 if self.base.chat_template is not None: 

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

121 

122 def _build_data_transform(self) -> None: 

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

124 dataset_config = self.base.config.dataset 

125 if dataset_config is None: 

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

127 if dataset_config.data_transform is None: 

128 self.base.data_transform = None 

129 return 

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

131 tokenizer=self.base.tokenizer, 

132 chat_template=self.base.chat_template, 

133 ) 

134 

135 def _build_collate_fn(self) -> None: 

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

137 dataloader_config = self.base.config.dataloader 

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

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

140 training_config = self.base.config.training 

141 self.base.num_micro_batches = calculate_num_micro_batches( 

142 global_batch_size=training_config.global_batch_size, 

143 micro_batch_size=training_config.micro_batch_size, 

144 dp_world_size=self.base.mesh.dp_size, 

145 ) 

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

147 mesh_context=self.base.mesh, 

148 ) 

149 

150 def _build_get_batch(self) -> None: 

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

152 config = self.base.config 

153 if config.dataloader.get_batch is None: 

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

155 get_batch = config.dataloader.get_batch.build( 

156 mesh_context=self.base.mesh, 

157 dataloader_context=self.base.dataloader_context, 

158 device=self.base.device, 

159 tokenizer=self.base.tokenizer, 

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

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

162 ) 

163 self.base.get_batch = get_batch 

164 

165 @property 

166 def distributed_setup(self) -> Any: 

167 """Return the shared distributed setup.""" 

168 return self.base.distributed_setup 

169 

170 @property 

171 def mesh(self) -> Any: 

172 """Return the shared mesh context.""" 

173 return self.base.mesh 

174 

175 @property 

176 def dp_cp_mesh(self) -> Any: 

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

178 return self.base.dp_cp_mesh 

179 

180 def on_train_begin(self) -> None: 

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

182 self.base.on_train_begin() 

183 

184 def on_train_end(self) -> None: 

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

186 self.base.on_train_end() 

187 

188 def on_epoch_begin(self) -> None: 

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

190 self.base.on_epoch_begin() 

191 

192 def on_epoch_end(self) -> None: 

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

194 self.base.on_epoch_end() 

195 

196 def on_step_begin(self) -> None: 

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

198 self.base.on_step_begin() 

199 

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

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

202 

203 Args: 

204 micro_batch: Prepared inputs and lightweight metric metadata. 

205 """ 

206 self.base.on_micro_step_begin(micro_batch) 

207 

208 def on_micro_step_end(self, loss_dict: dict[str, torch.Tensor]) -> None: 

209 """Dispatch the micro-step-end lifecycle hook. 

210 

211 Args: 

212 loss_dict: Named losses and metrics from the completed forward-backward step. 

213 """ 

214 self.base.on_micro_step_end(loss_dict) 

215 

216 def on_step_end( 

217 self, 

218 loss: Any = None, 

219 loss_dict: Any = None, 

220 grad_norm: Any = None, 

221 ) -> None: 

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

223 

224 Args: 

225 loss: Aggregated loss for the optimizer step. 

226 loss_dict: Named loss values for the optimizer step. 

227 grad_norm: Gradient norm measured before the optimizer update. 

228 """ 

229 self.base.on_step_end( 

230 loss=loss, 

231 loss_dict=loss_dict, 

232 grad_norm=grad_norm, 

233 ) 

234 

235 def forward_backward_step( 

236 self, 

237 data_iterator: Any, 

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

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

240 

241 Args: 

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

243 

244 Returns: 

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

246 """ 

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

248 self.base.current_token_counts = count_loss_token(loss_inputs) 

249 metric_inputs = { 

250 **model_inputs, 

251 **loss_inputs, 

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

253 } 

254 self.on_micro_step_begin(metric_inputs) 

255 del metric_inputs 

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

257 self.on_micro_step_end(loss_dict) 

258 

259 return loss, loss_dict 

260 

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

262 """Execute one text training step. 

263 

264 Args: 

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

266 

267 Returns: 

268 Aggregated loss and gradient norm for the completed step. 

269 """ 

270 memory_profiler.step() 

271 num_micro_steps = self.base.num_micro_batches 

272 self.base.begin_loss_step() 

273 

274 self.on_step_begin() 

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

276 synchronize() 

277 

278 total_loss = 0.0 

279 total_loss_dict = defaultdict(int) 

280 

281 for micro_step in range(num_micro_steps): 

282 self.base.model_reshard(micro_step, num_micro_steps) 

283 self.base.configure_fsdp_gradient_sync( 

284 micro_step, 

285 num_micro_steps, 

286 ) 

287 self.base.begin_fsdp_runtime_diagnostics(micro_step) 

288 loss, loss_dict = self.forward_backward_step( 

289 data_iterator, 

290 ) 

291 

292 total_loss += loss.item() 

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

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

295 

296 total_loss, grad_norm = self.base.prepare_optimizer_step(total_loss, total_loss_dict) 

297 self.base.step_optimizers_and_schedulers() 

298 

299 # Checkpoint and logging callbacks observe the number of completed 

300 # optimizer updates. 

301 self.base.state.global_step += 1 

302 grad_norm_value = float(grad_norm) 

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

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

305 ) 

306 self.on_step_end( 

307 loss=total_loss, 

308 loss_dict=total_loss_dict, 

309 grad_norm=grad_norm_value, 

310 ) 

311 

312 return { 

313 "loss": total_loss, 

314 "grad_norm": grad_norm_value, 

315 } 

316 

317 def _train_epoch(self, epoch: int, balanced_loader: bool) -> bool: 

318 """Consume complete optimizer steps and report whether the source epoch ended.""" 

319 train_dataloader = self.base.train_dataloader 

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

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

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

323 if balanced_loader: 

324 start_step = 0 

325 train_steps = self.base.train_iters - self.base.state.global_step 

326 for _ in range(start_step, train_steps): 

327 try: 

328 self.train_step(data_iterator) 

329 except StopIteration: 

330 if balanced_loader: 

331 optimizers = self.base.optimizer 

332 for optimizer in optimizers if isinstance(optimizers, list) else [optimizers]: 

333 optimizer.zero_grad() 

334 logger.info( 

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

336 ) 

337 return True 

338 return False 

339 

340 def train(self) -> None: 

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

342 try: 

343 self.on_train_begin() 

344 logger.info( 

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

346 self.base.local_rank, 

347 self.base.state.global_step, 

348 self.base.train_iters, 

349 self.base.state.epoch, 

350 self.base.train_epochs, 

351 ) 

352 

353 train_dataloader = self.base.train_dataloader 

354 balanced_loader = isinstance(train_dataloader, DistributedDataLoader) 

355 start_epoch = self.base.state.epoch 

356 epochs = count(start_epoch) if balanced_loader else range(start_epoch, self.base.train_epochs) 

357 for epoch in epochs: 

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

359 break 

360 # The first collective iterator may hold a restored checkpoint cursor. 

361 if hasattr(train_dataloader, "set_epoch") and (not balanced_loader or epoch != start_epoch): 

362 train_dataloader.set_epoch(epoch) 

363 

364 self.base.state.epoch = epoch 

365 self.on_epoch_begin() 

366 epoch_start_step = self.base.state.global_step 

367 epoch_exhausted = self._train_epoch(epoch, balanced_loader) 

368 

369 if balanced_loader and epoch != start_epoch and self.base.state.global_step == epoch_start_step: 

370 raise ValueError("Indexed source epoch cannot provide one complete optimizer step") 

371 

372 self.on_epoch_end() 

373 self.base.state.epoch = epoch + 1 if not balanced_loader or epoch_exhausted else epoch 

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

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

376 break 

377 

378 memory_profiler.stop() 

379 self.on_train_end() 

380 

381 except BaseException: 

382 try: 

383 memory_profiler.stop() 

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

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

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

387 finally: 

388 self.base.abort_profilers() 

389 raise 

390 

391 if balanced_loader: 

392 try: 

393 train_dataloader.wait_for_prefetch() 

394 finally: 

395 train_dataloader.close() 

396 

397 synchronize() 

398 self.base.destroy_distributed()