Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / data / indexed / indexed_split_builder.py: 84%

165 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"""Build train, validation, and test Datasets from indexed data blends.""" 

16 

17from __future__ import annotations 

18 

19import math 

20from collections.abc import Callable, Sequence 

21from enum import Enum 

22from functools import partial 

23from typing import Any, Literal, TypeAlias, cast 

24 

25import numpy as np 

26 

27from hyper_parallel.data.dataset_logging import get_dataset_logger 

28from hyper_parallel.data.indexed.indexed_blended_dataset import BlendedDataset 

29from hyper_parallel.data.indexed.indexed_data_config import GPTDatasetConfig 

30from hyper_parallel.data.indexed.indexed_lazy_dataset import LazyDatasetProxy 

31from hyper_parallel.data.indexed.indexed_pretrain_dataset import GPTDataset, MockGPTDataset 

32from hyper_parallel.data.indexed.indexed_simple_blended_dataset import SimpleBlendedDataset 

33from hyper_parallel.data.parallel import ( 

34 DataLoaderParallelContext, 

35 build_dataset_for_dataloader, 

36) 

37 

38logger = get_dataset_logger(__name__) 

39 

40BlendMode: TypeAlias = Literal["no", "inter", "intra"] 

41DatasetSplits: TypeAlias = tuple[Any | None, Any | None, Any | None] 

42SplitRange: TypeAlias = tuple[float, float] 

43SplitMatrix: TypeAlias = Sequence[SplitRange | None] 

44 

45 

46class _Split(Enum): 

47 """Stable train, validation, and test identifiers used by Dataset caches.""" 

48 

49 TRAIN = 0 

50 VALID = 1 

51 TEST = 2 

52 

53 

54def _get_prefixes_weights_and_sizes_for_blend( 

55 blend: Sequence[str], 

56 target_num_samples_per_split: Sequence[int], 

57 blend_mode: BlendMode, 

58 dataset_margin: float, 

59) -> tuple[list[str], list[float], list[list[int]]]: 

60 """Determine each source's contribution to the blended Dataset splits. 

61 

62 Args: 

63 blend: Alternating ``weight, prefix`` pairs for each data source. 

64 target_num_samples_per_split: Requested ``[train, validation, test]`` sample counts. 

65 blend_mode: ``"no"`` for normalized weighted blending; ``"inter"`` or ``"intra"`` for simple blending. 

66 dataset_margin: Extra sample margin applied only to normalized weighted blending. 

67 

68 Returns: 

69 ``(prefixes, weights, sizes_per_dataset)``, where ``sizes_per_dataset[source][split]`` is the number of 

70 samples requested from one source for the train, validation, or test split. 

71 

72 Example: 

73 Regular: ``["30", "/data/a", "70", "/data/b"]`` with sizes ``[1000, 100, 0]`` and margin ``1.005`` 

74 -> ``(["/data/a", "/data/b"], [0.3, 0.7], [[302, 31, 0], [704, 71, 0]])``. 

75 Simple: ``["1", "/data/a", "1", "/data/b"]`` with sizes ``[1000, 100, 0]`` 

76 -> ``(["/data/a", "/data/b"], [1, 1], [[500, 50, 0], [500, 50, 0]])``. 

77 """ 

78 if len(blend) % 2 != 0: 

79 raise ValueError("Dataset blend must contain alternating weight/prefix pairs") 

80 

81 # standard blend 

82 if blend_mode == "no": 

83 weights, prefixes = zip(*[(float(blend[index]), blend[index + 1].strip()) for index in range(0, len(blend), 2)]) 

84 weight_sum = float(sum(weights)) 

85 weights = [weight / weight_sum for weight in weights] 

86 sizes_per_dataset = [ 

87 [ 

88 int(math.ceil(target_num_samples * weight * dataset_margin)) 

89 for target_num_samples in target_num_samples_per_split 

90 ] 

91 for weight in weights 

92 ] 

93 return prefixes, weights, sizes_per_dataset 

94 

95 # inter/intra blend 

96 weights, prefixes = zip(*[(int(blend[index]), blend[index + 1].strip()) for index in range(0, len(blend), 2)]) 

97 sizes_per_dataset = [ 

98 [int(math.ceil(target_num_samples / len(weights))) for target_num_samples in target_num_samples_per_split] 

99 for _ in weights 

100 ] 

101 return prefixes, weights, sizes_per_dataset 

102 

103 

104def _build_low_level_dataset_and_split_indices( 

105 dataset_type: type, 

106 dataset_path: str, 

107 split_matrix: SplitMatrix, 

108 config: GPTDatasetConfig, 

109) -> tuple[Any, list[np.ndarray | None]]: 

110 """Build one low-level Dataset and calculate its requested split indices.""" 

111 low_level_dataset = dataset_type.build_low_level_dataset(dataset_path, config) 

112 num_elements = dataset_type.numel_low_level_dataset(low_level_dataset) 

113 

114 split_indices = [] 

115 for split_range in split_matrix: 

116 if split_range is None: 

117 split_indices.append(None) 

118 continue 

119 

120 begin = int(round(split_range[0] * num_elements)) 

121 end = int(round(split_range[1] * num_elements)) 

122 split_indice = np.arange(start=begin, stop=end, step=1, dtype=np.int32) 

123 split_indices.append(split_indice) 

124 

125 # (low_level_dataset, [train_indices, valid_indices, test_indices]). 

126 dataset_and_split_indices = (low_level_dataset, split_indices) 

127 return dataset_and_split_indices 

128 

129 

130class IndexedDatasetSplitBuilder: 

131 """Build indexed Dataset splits with one shared distributed context.""" 

132 

133 def __init__(self, dataloader_context: DataLoaderParallelContext) -> None: 

134 """Store the DataLoader ownership policy once for all build steps.""" 

135 self.dataloader_context = dataloader_context 

136 

137 def build( 

138 self, 

139 dataset_type: type, 

140 train_valid_test_num_samples: Sequence[int] | None, 

141 config: GPTDatasetConfig, 

142 blend_mode: BlendMode, 

143 ) -> DatasetSplits: 

144 """Build mock, shared-blend, or independent-blend Dataset splits.""" 

145 if train_valid_test_num_samples is None: 

146 raise ValueError("train_valid_test_num_samples must be configured for indexed Datasets") 

147 if len(train_valid_test_num_samples) != len(_Split): 

148 raise ValueError("train_valid_test_num_samples must contain exactly three values") 

149 if any(size < 0 for size in train_valid_test_num_samples): 

150 raise ValueError("train_valid_test_num_samples must be non-negative") 

151 if blend_mode not in {"no", "inter", "intra"}: 

152 raise ValueError(f"Unsupported blend mode: {blend_mode!r}") 

153 if config.mock != (dataset_type is MockGPTDataset): 

154 raise ValueError("mock_data must match the selected MockGPTDataset class") 

155 if not config.mock and (blend_mode == "no") != (dataset_type is GPTDataset): 

156 raise ValueError("simple_blend='no' is only for GPTDataset; other Datasets require inter/intra") 

157 

158 logger.debug( 

159 "Building indexed splits: dataset=%s, num_samples=%s, mock=%s, blend_mode=%s, lazy=%s", 

160 dataset_type.__name__, 

161 tuple(train_valid_test_num_samples), 

162 config.mock, 

163 blend_mode, 

164 config.data_lazy_load, 

165 ) 

166 if config.mock: 

167 return self._build_mock_dataset_splits(dataset_type, train_valid_test_num_samples, config) 

168 

169 if config.blend: 

170 if config.split_matrix is None: 

171 raise ValueError("A shared blend requires blend and split_matrix") 

172 return self._build_splits_from_blend( 

173 dataset_type, config.blend, config.split_matrix, train_valid_test_num_samples, config, blend_mode 

174 ) 

175 

176 if config.blend_per_split: 

177 return self._build_splits_from_independent_blends( 

178 dataset_type, train_valid_test_num_samples, config, blend_mode 

179 ) 

180 raise ValueError("One of blend or blend_per_split must be configured") 

181 

182 def _build_mock_dataset_splits( 

183 self, 

184 dataset_type: type, 

185 sizes: Sequence[int], 

186 config: GPTDatasetConfig, 

187 ) -> DatasetSplits: 

188 """Build generated train, validation, and test Datasets.""" 

189 datasets = [] 

190 for split, size in zip(_Split, sizes): 

191 if size == 0: 

192 datasets.append(None) 

193 continue 

194 

195 dataset_factory = partial(dataset_type, None, None, None, int(size), split, config) 

196 mock_dataset = self._build_dataset_for_dataloader(dataset_factory, barrier_needed=False) 

197 datasets.append(mock_dataset) 

198 return cast(DatasetSplits, tuple(datasets)) 

199 

200 def _build_splits_from_blend( 

201 self, 

202 dataset_type: type, 

203 blend: Sequence[str], 

204 split_matrix: SplitMatrix, 

205 sizes: Sequence[int], 

206 config: GPTDatasetConfig, 

207 blend_mode: BlendMode, 

208 ) -> DatasetSplits: 

209 """ 

210 Parse one blend and dispatch it to the single- or multiple-source path. 

211 Low level: Index -> Mid level: GPTDataset -> Final: Blend. 

212 """ 

213 if len(blend) == 1: 

214 return self._build_mid_level_dataset_splits(dataset_type, blend[0], split_matrix, sizes, config) 

215 

216 prefixes, weights, sizes_per_dataset = _get_prefixes_weights_and_sizes_for_blend( 

217 blend, sizes, blend_mode, config.dataset_margin 

218 ) 

219 logger.debug( 

220 "Resolved blend: mode=%s, sources=%d, first_weight=%s, first_source_sizes=%s", 

221 blend_mode, 

222 len(prefixes), 

223 weights[0], 

224 sizes_per_dataset[0], 

225 ) 

226 dataset_splits = self._build_multiple_source_splits( 

227 dataset_type, prefixes, weights, sizes_per_dataset, split_matrix, config, blend_mode 

228 ) 

229 return dataset_splits 

230 

231 def _build_splits_from_independent_blends( 

232 self, 

233 dataset_type: type, 

234 sizes: Sequence[int], 

235 config: GPTDatasetConfig, 

236 blend_mode: BlendMode, 

237 ) -> DatasetSplits: 

238 """Build each split from its own data-source distribution.""" 

239 if config.blend_per_split is None or len(config.blend_per_split) != len(_Split): 

240 raise ValueError("Independent blends require one blend per split") 

241 

242 datasets = [] 

243 for split_index, blend in enumerate(config.blend_per_split): 

244 if not blend: 

245 datasets.append(None) 

246 continue 

247 

248 split_matrix: list[SplitRange | None] = [None] * len(_Split) 

249 split_matrix[split_index] = (0.0, 1.0) 

250 split_sizes = [0] * len(_Split) 

251 split_sizes[split_index] = int(sizes[split_index]) 

252 split_datasets = self._build_splits_from_blend( 

253 dataset_type, blend, split_matrix, split_sizes, config, blend_mode 

254 ) 

255 datasets.append(split_datasets[split_index]) 

256 return cast(DatasetSplits, tuple(datasets)) 

257 

258 def _build_multiple_source_splits( 

259 self, 

260 dataset_type: type, 

261 prefixes: Sequence[str], 

262 weights: Sequence[float], 

263 sizes_per_dataset: Sequence[Sequence[int]], 

264 split_matrix: SplitMatrix, 

265 config: GPTDatasetConfig, 

266 blend_mode: BlendMode, 

267 ) -> DatasetSplits: 

268 """Build each source's mid-level splits and assemble top-level blends. 

269 

270 Example with two sources:: 

271 

272 prefixes = ["/data/books", "/data/code"] 

273 sizes_per_dataset = [[80, 10, 0], [20, 5, 0]] 

274 

275 Offline preparation, tokenize and optionally pre-pack raw records: 

276 books.jsonl -> tokenizer -> token IDs -> pack to 2049-token records 

277 -> /data/books.idx + /data/books.bin 

278 code.jsonl -> tokenizer -> token IDs -> pack to 2049-token records 

279 -> /data/code.idx + /data/code.bin 

280 

281 Low-level, open one IndexedDataReader per prefix and read one sequence (uint16 example): 

282 books.idx: sequence_lengths[1] = 2049, sequence_pointers[1] = 4098 

283 books_reader[1] -> books.bin[offset=4098, count=2049] -> text token IDs 

284 

285 Mid-level, organize tokens into training records (input_ids, labels, and masks), 

286 with one GPTDataset/GPTFromMRDataset per source and split: 

287 books_train[0] -> split indices[0] = 1 -> books_reader[1] 

288 -> input_ids = text[:-1], labels = text[1:] 

289 datasets_per_source = [ 

290 (books_train[80], books_valid[10], None), 

291 (code_train[20], code_valid[5], None), 

292 ] 

293 

294 Transpose source-major tuples into split-major components: 

295 train components = [books_train, code_train], total size = 100 

296 valid components = [books_valid, code_valid], total size = 15 

297 test components = [], size = 0 

298 

299 Blend-level, select the source and source-local sample for each train index, for example: 

300 dataset_index = [0, 1, ...] 

301 dataset_sample_index = [0, 0, ...] 

302 global sample 0 -> (dataset_id=0, sample_id=0) -> books_train[0] 

303 global sample 1 -> (dataset_id=1, sample_id=0) -> code_train[0] 

304 

305 ``BlendedDataset[i]`` reads ``dataset_index[i]`` and ``dataset_sample_index[i]``; 

306 ``SimpleBlendedDataset[i]`` reads ``_locations[i] = (dataset_id, sample_id)``. 

307 Both return ``datasets[dataset_id][sample_id]``. 

308 

309 final result = (blended_train[100], blended_valid[15], None) 

310 """ 

311 # pylint: disable=too-many-locals 

312 datasets_per_source = [] 

313 logger.debug("Building %d indexed Dataset sources", len(prefixes)) 

314 # TODO: Add a separate cache-prebuild phase that assigns source_index % world_size to each rank, 

315 # synchronizes failures, and atomically writes a completion manifest before Dataset construction. 

316 

317 # books -> (books_train, books_valid, None), then code -> (code_train, code_valid, None). 

318 for prefix, sizes_for_source in zip(prefixes, sizes_per_dataset): 

319 source_datasets = self._build_mid_level_dataset_splits( 

320 dataset_type, prefix, split_matrix, sizes_for_source, config 

321 ) 

322 datasets_per_source.append(source_datasets) 

323 

324 blended_sizes = [sum(split_sizes) for split_sizes in zip(*sizes_per_dataset)] 

325 blended_splits = [] 

326 for split_index, (split_range, size) in enumerate(zip(split_matrix, blended_sizes)): 

327 if split_range is None or size == 0: 

328 blended_splits.append(None) 

329 continue 

330 

331 component_datasets = [datasets[split_index] for datasets in datasets_per_source] 

332 missing_datasets = [dataset is None for dataset in component_datasets] 

333 if any(missing_datasets) and not all(missing_datasets): 

334 raise ValueError("Enabled sources must either all build or all remain None on a rank") 

335 

336 component_datasets = [dataset for dataset in component_datasets if dataset is not None] 

337 if blend_mode == "no": 

338 dataset_factory = partial(BlendedDataset, component_datasets, weights, int(size), config) 

339 else: 

340 dataset_factory = partial(SimpleBlendedDataset, component_datasets, int(size), blend_mode) 

341 blended_dataset = self._build_dataset_for_dataloader(dataset_factory, barrier_needed=True) 

342 blended_splits.append(blended_dataset) 

343 

344 dataset_splits = cast(DatasetSplits, tuple(blended_splits)) 

345 split_types = tuple(type(split).__name__ if split is not None else None for split in dataset_splits) 

346 logger.debug("Built blended splits=%s", split_types) 

347 return dataset_splits 

348 

349 def _build_mid_level_dataset_splits( 

350 self, 

351 dataset_type: type, 

352 dataset_path: str, 

353 split_matrix: SplitMatrix, 

354 sizes_for_source: Sequence[int], 

355 config: GPTDatasetConfig, 

356 ) -> DatasetSplits: 

357 """Build each mid-level split from one low-level indexed Dataset.""" 

358 # pylint: disable=too-many-locals 

359 if ( 

360 config.data_lazy_load 

361 and self.dataloader_context.distributed_enabled 

362 and not ( 

363 self.dataloader_context.data_index_cache 

364 or self.dataloader_context.build_on_rank() 

365 ) 

366 ): 

367 return cast(DatasetSplits, (None, None, None)) 

368 

369 low_level_dataset = None 

370 split_indices: list[np.ndarray | None] = [None] * len(split_matrix) 

371 if not config.data_lazy_load: 

372 low_level_dataset, split_indices = _build_low_level_dataset_and_split_indices( 

373 dataset_type, dataset_path, split_matrix, config 

374 ) 

375 

376 datasets = [] 

377 for split, split_range, indices, size in zip(_Split, split_matrix, split_indices, sizes_for_source): 

378 if split_range is None or size == 0: 

379 datasets.append(None) 

380 continue 

381 

382 size = int(size) 

383 if config.data_lazy_load: 

384 lazy_builder = self._build_lazy_mid_level_dataset_split 

385 dataset_factory = partial(lazy_builder, dataset_type, dataset_path, split, split_range, size, config) 

386 unique_identifiers = dataset_type.build_unique_identifiers(dataset_path, size, split, config) 

387 mid_level_dataset = LazyDatasetProxy(dataset_factory, unique_identifiers=unique_identifiers) 

388 else: 

389 dataset_factory = partial(dataset_type, low_level_dataset, dataset_path, indices, size, split, config) 

390 mid_level_dataset = self._build_dataset_for_dataloader( 

391 dataset_factory, barrier_needed=dataset_type is GPTDataset 

392 ) 

393 datasets.append(mid_level_dataset) 

394 return cast(DatasetSplits, tuple(datasets)) 

395 

396 def _build_lazy_mid_level_dataset_split( 

397 self, 

398 dataset_type: type, 

399 dataset_path: str, 

400 split: _Split, 

401 split_range: SplitRange, 

402 size: int, 

403 config: GPTDatasetConfig, 

404 ) -> Any | None: 

405 """Apply distributed construction when a lazy split is first accessed.""" 

406 

407 def _dataset_factory() -> Any: 

408 """Build the low-level Dataset and one lazy split on first access.""" 

409 low_level_dataset, split_indices = _build_low_level_dataset_and_split_indices( 

410 dataset_type, dataset_path, [split_range], config 

411 ) 

412 mid_level_dataset = dataset_type(low_level_dataset, dataset_path, split_indices[0], size, split, config) 

413 return mid_level_dataset 

414 

415 local_dataset = self._build_dataset_for_dataloader(_dataset_factory, barrier_needed=False) 

416 return local_dataset 

417 

418 def _build_dataset_for_dataloader( 

419 self, 

420 dataset_factory: Callable[[], Any], 

421 *, 

422 barrier_needed: bool, 

423 ) -> Any | None: 

424 """Build one Dataset with the shared DataLoader context.""" 

425 local_dataset = build_dataset_for_dataloader( 

426 dataset_factory, 

427 self.dataloader_context, 

428 barrier_needed=barrier_needed, 

429 ) 

430 return local_dataset