Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / data / batching / build_dataloader.py: 20%

170 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"""Build DataLoaders and calculate distributed micro-batch sizing.""" 

16 

17from __future__ import annotations 

18 

19import copy 

20from abc import ABC, abstractmethod 

21from collections.abc import Callable, Iterator, Mapping, Sequence 

22from typing import Any 

23 

24import torch 

25from torch.utils.data import IterableDataset 

26from torchdata.stateful_dataloader import StatefulDataLoader 

27 

28from hyper_parallel.data.batching.dynamic_batch import ( 

29 FullSampleCheckpoint, 

30 IndexReplayCheckpoint, 

31 MappingReplayDataset, 

32 _BatchPipeline, 

33 _OmniPackingPipeline, 

34 _TokenBatchPipeline, 

35) 

36from hyper_parallel.data.dataset_logging import get_dataset_logger 

37from hyper_parallel.data.parallel import build_dataset_batch_sampler 

38 

39logger = get_dataset_logger(__name__) 

40 

41 

42def calculate_num_micro_batches( 

43 global_batch_size: int, 

44 micro_batch_size: int, 

45 dp_world_size: int, 

46) -> int: 

47 """Calculate the number of micro-batches in one optimizer step. 

48 

49 Args: 

50 global_batch_size: Number of samples processed by one optimizer step. 

51 micro_batch_size: Number of samples processed by each DP rank and forward pass. 

52 dp_world_size: Number of data-parallel ranks. 

53 

54 Returns: 

55 Number of forward/backward micro-batches executed by each DP rank. 

56 

57 Raises: 

58 ValueError: If the global batch cannot be divided exactly. 

59 """ 

60 distributed_micro_batch_size = micro_batch_size * dp_world_size 

61 if global_batch_size % distributed_micro_batch_size != 0: 

62 raise ValueError( 

63 f"global_batch_size ({global_batch_size}) must be divisible by " 

64 f"micro_batch_size * dp_world_size ({distributed_micro_batch_size})" 

65 ) 

66 

67 num_micro_batches = global_batch_size // distributed_micro_batch_size 

68 logger.debug( 

69 "Resolved micro-batches=%d from global_batch_size=%d, micro_batch_size=%d, dp_world_size=%d", 

70 num_micro_batches, 

71 global_batch_size, 

72 micro_batch_size, 

73 dp_world_size, 

74 ) 

75 return num_micro_batches 

76 

77 

78def _is_iterable_dataset(dataset: Any) -> bool: 

79 """Return whether a Dataset streams samples without mapping-style access.""" 

80 if isinstance(dataset, IterableDataset): 

81 is_iterable = True 

82 else: 

83 has_iterator = callable(getattr(dataset, "__iter__", None)) 

84 has_index_access = callable(getattr(dataset, "__getitem__", None)) 

85 is_iterable = has_iterator and not has_index_access 

86 return is_iterable 

87 

88 

89def build_dataloader( 

90 dataloader_target: Any, 

91 *, 

92 datasets: Sequence[Any | None], 

93 collate_fn: Any, 

94 mesh_context: Any, 

95 training_config: Any, 

96 max_seq_len: int | None = None, 

97 default_seed: int = 1234, 

98) -> tuple[tuple[Any | None, ...], tuple[Any | None, ...]]: 

99 """Build train, validation, and test DataLoaders. 

100 

101 Mapping Datasets receive a distributed batch sampler. Iterable Datasets 

102 control their own order. The configured target decides whether selected 

103 samples use fixed collation or dynamic token batching. 

104 

105 Args: 

106 dataloader_target: DataLoader build target. 

107 datasets: Train, validation, and test datasets. 

108 collate_fn: Collator applied after fixed or dynamic sample selection. 

109 mesh_context: Data-parallel mesh context. 

110 training_config: Batch size and random seed configuration. 

111 max_seq_len: Maximum sample length used to derive dynamic token budget. 

112 default_seed: Seed used when no training seed is configured. 

113 

114 Returns: 

115 DataLoader and batch-sampler tuples for the three Dataset splits. 

116 """ 

117 if len(datasets) != 3: 

118 raise ValueError("datasets must contain train, validation, and test entries") 

119 

120 if all(dataset is None for dataset in datasets): 

121 dataloader_splits = (None, None, None) 

122 batch_sampler_splits = (None, None, None) 

123 return dataloader_splits, batch_sampler_splits 

124 

125 if dataloader_target is None: 

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

127 

128 dataloaders: list[Any | None] = [None] * len(datasets) 

129 batch_samplers: list[Any | None] = [None] * len(datasets) 

130 

131 for split_index, (split_name, dataset) in enumerate(zip(("train", "valid", "test"), datasets)): 

132 if dataset is None: 

133 logger.debug("Skipping empty Dataset split=%s", split_name) 

134 continue 

135 

136 batch_sampler = None 

137 if not _is_iterable_dataset(dataset): 

138 batch_sampler = build_dataset_batch_sampler( 

139 total_samples=len(dataset), 

140 micro_batch_size=training_config.micro_batch_size, 

141 global_batch_size=training_config.global_batch_size, 

142 dp_world_size=mesh_context.dp_size, 

143 dp_rank=mesh_context.dp_rank, 

144 drop_last=getattr(dataloader_target, "drop_last", True), 

145 data_rearrange_map=getattr(dataloader_target, "data_rearrange_map", None), 

146 sampler_type=getattr(dataloader_target, "sampler_type", "single"), 

147 data_sharding=getattr(dataloader_target, "data_sharding", False), 

148 seed=training_config.seed if training_config.seed is not None else default_seed, 

149 ) 

150 

151 dataloaders[split_index] = dataloader_target.build( 

152 dataset=dataset, 

153 collate_fn=collate_fn, 

154 batch_sampler=batch_sampler, 

155 batch_size=training_config.micro_batch_size, 

156 sampler_type=getattr(dataloader_target, "sampler_type", "single"), 

157 dp_world_size=mesh_context.dp_size, 

158 max_seq_len=max_seq_len, 

159 seed=training_config.seed if training_config.seed is not None else default_seed, 

160 ) 

161 logger.debug( 

162 "Built DataLoader split=%s, dataset=%s, batch_sampler=%s", 

163 split_name, 

164 type(dataset).__name__, 

165 type(batch_sampler).__name__ if batch_sampler is not None else None, 

166 ) 

167 batch_samplers[split_index] = batch_sampler 

168 

169 logger.debug("Finished building train/valid/test DataLoaders") 

170 return tuple(dataloaders), tuple(batch_samplers) 

171 

172 

173class FixedBatchDataLoader(StatefulDataLoader): 

174 """Build fixed-sample batches while retaining Trainer iterator policy.""" 

175 

176 def __init__( 

177 self, 

178 dataset: Any, 

179 batch_sampler: Any = None, 

180 collate_fn: Callable[[list[Any]], Any] | None = None, 

181 *, 

182 batch_size: int | None = None, 

183 drop_last: bool = True, 

184 sampler_type: str = "single", 

185 num_workers: int = 0, 

186 seed: int = 1234, 

187 pin_memory: bool = False, 

188 prefetch_factor: int | None = None, 

189 ) -> None: 

190 """Initialize the stateful DataLoader.""" 

191 if sampler_type not in ("single", "cyclic"): 

192 raise ValueError("sampler_type must be 'single' or 'cyclic'") 

193 

194 self.drop_last = drop_last 

195 self.sampler_type = sampler_type 

196 generator = torch.Generator().manual_seed(seed) 

197 worker_options = { 

198 "num_workers": num_workers, 

199 "generator": generator, 

200 "pin_memory": pin_memory, 

201 } 

202 if num_workers > 0 and prefetch_factor is not None: 

203 worker_options["prefetch_factor"] = prefetch_factor 

204 if batch_sampler is None: 

205 resolved_batch_size = 1 if batch_size is None else batch_size 

206 super().__init__( 

207 dataset=dataset, 

208 batch_size=resolved_batch_size, 

209 collate_fn=collate_fn, 

210 drop_last=drop_last, 

211 **worker_options, 

212 ) 

213 else: 

214 super().__init__( 

215 dataset=dataset, 

216 batch_sampler=batch_sampler, 

217 collate_fn=collate_fn, 

218 **worker_options, 

219 ) 

220 

221 def set_epoch(self, epoch: int) -> None: 

222 """Forward epoch state to the Dataset and configured batch sampler.""" 

223 if self.batch_sampler is not None and hasattr(self.batch_sampler, "set_epoch"): 

224 self.batch_sampler.set_epoch(epoch) 

225 

226 epoch_setter = getattr(self.dataset, "set_epoch", None) 

227 if callable(epoch_setter): 

228 dataset_epoch = epoch if self.sampler_type == "cyclic" else 0 

229 epoch_setter(dataset_epoch) 

230 

231 

232class _DynamicBatchLoader(ABC): 

233 """Own common dynamic source iteration, buffering, and checkpoint state. 

234 

235 Subclasses define how candidates are selected and finalized before the 

236 configured collator builds a batch. 

237 

238 Args: 

239 dataset: Online mapping or iterable Dataset producing ModelSamples. 

240 batch_sampler: Optional mapping-style batch sampler. 

241 collate_fn: Final batch collator receiving the selected ModelSamples. 

242 batch_size: Configured micro batch size used to derive the token budget. 

243 dp_world_size: Number of data-parallel source lanes. 

244 save_by_idx: Whether to checkpoint buffer entries as output indices. 

245 Mapping Datasets default to true and iterable Datasets default to false. 

246 max_seq_len: Maximum sample length used to derive the token budget. 

247 token_budget: Optional candidate-selection budget. Values smaller than 

248 one sample still emit that sample alone instead of truncating it. 

249 min_buffered_samples: Minimum candidate samples buffered before batching. 

250 drop_last: Compatibility option retained from fixed DataLoader configuration. 

251 Dynamic buffers are always drained when the source is exhausted. 

252 sampler_type: Whether each Dataset epoch reuses one order or advances 

253 the deterministic cyclic order. 

254 num_workers: Number of source DataLoader workers. 

255 seed: Source DataLoader random seed. 

256 pin_memory: Whether source samples use pinned host memory. 

257 prefetch_factor: Number of source batches prefetched by each worker. 

258 """ 

259 

260 def __init__( 

261 self, 

262 dataset: Any, 

263 batch_sampler: Any = None, 

264 collate_fn: Callable[[Sequence[Mapping[str, Any]]], Mapping[str, Any]] | None = None, 

265 *, 

266 batch_size: int, 

267 dp_world_size: int | None = None, 

268 save_by_idx: bool | None = None, 

269 max_seq_len: int | None, 

270 token_budget: int | None = None, 

271 min_buffered_samples: int = 200, 

272 drop_last: bool = True, 

273 sampler_type: str = "single", 

274 num_workers: int = 0, 

275 seed: int = 1234, 

276 pin_memory: bool = False, 

277 prefetch_factor: int | None = None, 

278 ) -> None: 

279 """Initialize source reading, token selection, and Online packing.""" 

280 # pylint: disable=too-many-locals 

281 if collate_fn is None: 

282 raise ValueError(f"{type(self).__name__} requires a collate_fn") 

283 

284 if max_seq_len is None: 

285 raise ValueError(f"{type(self).__name__} requires data_transform.max_seq_len") 

286 if token_budget is not None and token_budget <= 0: 

287 raise ValueError("token_budget must be positive") 

288 

289 resolved_dp_world_size = dp_world_size 

290 if resolved_dp_world_size <= 0: 

291 raise ValueError("dp_world_size must be positive") 

292 

293 sampler_dp_world_size = getattr(batch_sampler, "dp_world_size", resolved_dp_world_size) 

294 if sampler_dp_world_size != resolved_dp_world_size: 

295 raise ValueError("batch_sampler.dp_world_size must match dp_world_size") 

296 

297 self.drop_last = drop_last 

298 self.batch_collate_fn = collate_fn 

299 self.dp_world_size = resolved_dp_world_size 

300 

301 # build_candidate_buffer 

302 self._batch_pipeline = self._build_batch_pipeline(dataset) 

303 resolved_token_budget = batch_size * max_seq_len if token_budget is None else token_budget 

304 self.candidate_buffer = self._batch_pipeline.build_candidate_buffer( 

305 token_budget=resolved_token_budget, 

306 min_buffered_samples=min_buffered_samples, 

307 ) 

308 if batch_sampler is not None: 

309 enable_source_resume = getattr(batch_sampler, "enable_source_batch_resume", None) 

310 if callable(enable_source_resume): 

311 enable_source_resume() 

312 

313 # Resuming training from a checkpoint 

314 self._buffer_checkpoint = self._build_buffer_checkpoint(dataset, save_by_idx) 

315 

316 self.source_dataloader = FixedBatchDataLoader( 

317 dataset=self._buffer_checkpoint.source_dataset, 

318 batch_sampler=batch_sampler, 

319 # Keep source samples separate until the candidate buffer selects them. 

320 collate_fn=list, 

321 batch_size=1 if batch_sampler is None else batch_size, 

322 drop_last=False, 

323 sampler_type=sampler_type, 

324 num_workers=num_workers, 

325 seed=seed, 

326 pin_memory=pin_memory, 

327 prefetch_factor=prefetch_factor, 

328 ) 

329 self.resume_pending = False 

330 

331 @staticmethod 

332 def _build_buffer_checkpoint( 

333 dataset: Any, 

334 save_by_idx: bool | None, 

335 ) -> IndexReplayCheckpoint | FullSampleCheckpoint: 

336 """Build candidate-buffer checkpoint storage for the Dataset access mode.""" 

337 is_iterable = _is_iterable_dataset(dataset) 

338 resolved_save_by_idx = not is_iterable if save_by_idx is None else save_by_idx 

339 

340 # Mapping Datasets have stable indices. Iterable Datasets must explicitly 

341 # provide replay access before their buffer can be checkpointed by index. 

342 replay_dataset = dataset if is_iterable else MappingReplayDataset(dataset) 

343 supports_replay = not is_iterable or IndexReplayCheckpoint.supports(dataset) 

344 if resolved_save_by_idx and not supports_replay: 

345 raise ValueError("save_by_idx=True requires get_item() and output_index_for_resume") 

346 

347 if is_iterable and hasattr(dataset, "output_index_for_resume"): 

348 dataset.output_index_for_resume = resolved_save_by_idx 

349 

350 if resolved_save_by_idx: 

351 buffer_checkpoint = IndexReplayCheckpoint(replay_dataset) 

352 else: 

353 # Keep replay access only for loading an earlier index-buffer checkpoint. 

354 checkpoint_replay_dataset = replay_dataset if supports_replay else None 

355 buffer_checkpoint = FullSampleCheckpoint(dataset, checkpoint_replay_dataset) 

356 

357 return buffer_checkpoint 

358 

359 def __iter__(self) -> Iterator[Mapping[str, Any]]: 

360 """Start source iteration while retaining restored buffer state.""" 

361 if not self.resume_pending: 

362 self.candidate_buffer.buffer.clear() 

363 self.candidate_buffer.buffer_output_indices.clear() 

364 self.candidate_buffer.buffer_token_count = 0 

365 

366 source_iterator = iter(self.source_dataloader) 

367 batch_iterator = self._batch_data_generator(source_iterator) 

368 self.resume_pending = False 

369 

370 return batch_iterator 

371 

372 def _batch_data_generator( 

373 self, 

374 source_iterator: Iterator[list[Any]], 

375 ) -> Iterator[Mapping[str, Any]]: 

376 """Lazily yield micro-batches as source samples fill the candidate buffer.""" 

377 # Stage 1: Use ready checkpointed candidates before advancing the source. 

378 while self.candidate_buffer.is_ready_for_micro_batch(): 

379 yield self._build_micro_batch() 

380 

381 # Stage 2: Retain unselected candidates while incrementally reading source batches. 

382 for source_samples in source_iterator: 

383 for source_sample in source_samples: 

384 self._buffer_checkpoint.put_source_item(source_sample, self.candidate_buffer) 

385 

386 while self.candidate_buffer.is_ready_for_micro_batch(): 

387 yield self._build_micro_batch() 

388 

389 # Stage 3: At EOF, bypass readiness thresholds so no pulled candidate is lost. 

390 while not self.candidate_buffer.empty(): 

391 yield self._build_micro_batch() 

392 

393 @abstractmethod 

394 def _build_batch_pipeline(self, dataset: Any) -> _BatchPipeline: 

395 """Build the batching pipeline selected by the concrete Loader.""" 

396 raise NotImplementedError 

397 

398 def _build_micro_batch(self) -> Mapping[str, Any]: 

399 """Select, finalize, collate, and encode one micro-batch.""" 

400 selected_samples = self.candidate_buffer.get_micro_batch() 

401 finalized_samples = self._batch_pipeline.finalize_selected_samples(selected_samples) 

402 

403 # build batch data 

404 batch = self.batch_collate_fn(finalized_samples) 

405 encoded_batch = self._batch_pipeline.encode_batch(batch) 

406 return encoded_batch 

407 

408 def state_dict(self) -> dict[str, Any]: 

409 """Capture the future source cursor and unconsumed dynamic buffer.""" 

410 state = { 

411 "dp_world_size": self.dp_world_size, 

412 "source_dataloader": self.source_dataloader.state_dict(), 

413 "save_by_idx": self._buffer_checkpoint.save_by_idx, 

414 "buffer": self._buffer_checkpoint.get_buffer_state(self.candidate_buffer), 

415 "buffer_token_count": self.candidate_buffer.buffer_token_count, 

416 } 

417 checkpoint_state = copy.deepcopy(state) 

418 

419 return checkpoint_state 

420 

421 def load_state_dict(self, state_dict: Mapping[str, Any]) -> None: 

422 """Restore the future source cursor and unconsumed dynamic buffer.""" 

423 checkpoint_state = copy.deepcopy(dict(state_dict)) 

424 saved_dp_world_size = checkpoint_state.get("dp_world_size", self.dp_world_size) 

425 if saved_dp_world_size != self.dp_world_size: 

426 raise ValueError( 

427 "Online dataloader resume does not support DP world-size changes: " 

428 f"saved_dp_world_size={saved_dp_world_size}, current_dp_world_size={self.dp_world_size}" 

429 ) 

430 

431 previous_save_by_idx = bool(checkpoint_state.get("save_by_idx", False)) 

432 saved_buffer = checkpoint_state["buffer"] 

433 restored_buffer, restored_indices = self._buffer_checkpoint.restore_buffer( 

434 saved_buffer, 

435 previous_save_by_idx, 

436 self.candidate_buffer, 

437 ) 

438 

439 restored_token_count = sum(sample_length for _, sample_length in restored_buffer) 

440 if restored_token_count != checkpoint_state["buffer_token_count"]: 

441 raise ValueError("buffer_token_count does not match the restored dynamic buffer") 

442 

443 self.source_dataloader.load_state_dict(checkpoint_state["source_dataloader"]) 

444 self.candidate_buffer.buffer = restored_buffer 

445 self.candidate_buffer.buffer_output_indices = restored_indices 

446 self.candidate_buffer.buffer_token_count = restored_token_count 

447 self.resume_pending = True 

448 

449 def set_epoch(self, epoch: int) -> None: 

450 """Forward epoch state to the stateful source DataLoader.""" 

451 self.source_dataloader.set_epoch(epoch) 

452 

453 

454class TokenBatchLoader(_DynamicBatchLoader): 

455 """Select fully encoded samples within one token budget.""" 

456 

457 def _build_batch_pipeline(self, _dataset: Any) -> _BatchPipeline: 

458 """Build token-budget batching for fully encoded Dataset samples.""" 

459 batch_pipeline = _TokenBatchPipeline() 

460 return batch_pipeline 

461 

462 

463class OmniPackingLoader(_DynamicBatchLoader): 

464 """Pack selected samples after applying the Dataset encoding lifecycle.""" 

465 

466 def _build_batch_pipeline(self, dataset: Any) -> _BatchPipeline: 

467 """Build packing around the normalized Dataset encoding lifecycle.""" 

468 batch_pipeline = _OmniPackingPipeline.from_dataset(dataset) 

469 return batch_pipeline