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

162 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 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_simple_blended_dataset import SimpleBlendedDataset 

32from hyper_parallel.data.parallel import ( 

33 DataLoaderParallelContext, 

34 build_dataset_for_dataloader, 

35) 

36 

37logger = get_dataset_logger(__name__) 

38 

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

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

41SplitRange: TypeAlias = tuple[float, float] 

42SplitMatrix: TypeAlias = Sequence[SplitRange | None] 

43 

44 

45class _Split(Enum): 

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

47 

48 TRAIN = 0 

49 VALID = 1 

50 TEST = 2 

51 

52 

53def _get_prefixes_weights_and_sizes_for_blend( 

54 blend: Sequence[str], 

55 target_num_samples_per_split: Sequence[int], 

56 blend_mode: BlendMode, 

57 dataset_margin: float, 

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

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

60 

61 Args: 

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

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

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

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

66 

67 Returns: 

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

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

70 

71 Example: 

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

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

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

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

76 """ 

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

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

79 

80 # standard blend 

81 if blend_mode == "no": 

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

83 weight_sum = float(sum(weights)) 

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

85 sizes_per_dataset = [ 

86 [ 

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

88 for target_num_samples in target_num_samples_per_split 

89 ] 

90 for weight in weights 

91 ] 

92 return prefixes, weights, sizes_per_dataset 

93 

94 # inter/intra blend 

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

96 sizes_per_dataset = [ 

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

98 for _ in weights 

99 ] 

100 return prefixes, weights, sizes_per_dataset 

101 

102 

103def _build_low_level_dataset_and_split_indices( 

104 dataset_type: type, 

105 dataset_path: str, 

106 split_matrix: SplitMatrix, 

107 config: GPTDatasetConfig, 

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

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

110 low_level_dataset = dataset_type.build_low_level_dataset(dataset_path, config) 

111 num_elements = dataset_type.numel_low_level_dataset(low_level_dataset) 

112 

113 split_indices = [] 

114 for split_range in split_matrix: 

115 if split_range is None: 

116 split_indices.append(None) 

117 continue 

118 

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

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

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

122 split_indices.append(split_indice) 

123 

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

125 dataset_and_split_indices = (low_level_dataset, split_indices) 

126 return dataset_and_split_indices 

127 

128 

129class IndexedDatasetSplitBuilder: 

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

131 

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

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

134 self.dataloader_context = dataloader_context 

135 

136 def build( 

137 self, 

138 dataset_type: type, 

139 train_valid_test_num_samples: Sequence[int] | None, 

140 config: GPTDatasetConfig, 

141 blend_mode: BlendMode, 

142 ) -> DatasetSplits: 

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

144 if train_valid_test_num_samples is None: 

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

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

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

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

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

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

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

152 if blend_mode != "no" and not config.is_dataset_from_mr: 

153 raise ValueError("inter/intra blending requires is_dataset_from_mr=True") 

154 

155 logger.debug( 

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

157 dataset_type.__name__, 

158 tuple(train_valid_test_num_samples), 

159 config.mock, 

160 blend_mode, 

161 config.data_lazy_load, 

162 ) 

163 if config.mock: 

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

165 

166 if config.blend: 

167 if config.split_matrix is None: 

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

169 return self._build_splits_from_blend( 

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

171 ) 

172 

173 if config.blend_per_split: 

174 return self._build_splits_from_independent_blends( 

175 dataset_type, train_valid_test_num_samples, config, blend_mode 

176 ) 

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

178 

179 def _build_mock_dataset_splits( 

180 self, 

181 dataset_type: type, 

182 sizes: Sequence[int], 

183 config: GPTDatasetConfig, 

184 ) -> DatasetSplits: 

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

186 datasets = [] 

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

188 if size == 0: 

189 datasets.append(None) 

190 continue 

191 

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

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

194 datasets.append(mock_dataset) 

195 return cast(DatasetSplits, tuple(datasets)) 

196 

197 def _build_splits_from_blend( 

198 self, 

199 dataset_type: type, 

200 blend: Sequence[str], 

201 split_matrix: SplitMatrix, 

202 sizes: Sequence[int], 

203 config: GPTDatasetConfig, 

204 blend_mode: BlendMode, 

205 ) -> DatasetSplits: 

206 """ 

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

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

209 """ 

210 if len(blend) == 1: 

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

212 

213 prefixes, weights, sizes_per_dataset = _get_prefixes_weights_and_sizes_for_blend( 

214 blend, sizes, blend_mode, config.dataset_margin 

215 ) 

216 logger.debug( 

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

218 blend_mode, 

219 len(prefixes), 

220 weights[0], 

221 sizes_per_dataset[0], 

222 ) 

223 dataset_splits = self._build_multiple_source_splits( 

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

225 ) 

226 return dataset_splits 

227 

228 def _build_splits_from_independent_blends( 

229 self, 

230 dataset_type: type, 

231 sizes: Sequence[int], 

232 config: GPTDatasetConfig, 

233 blend_mode: BlendMode, 

234 ) -> DatasetSplits: 

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

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

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

238 

239 datasets = [] 

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

241 if not blend: 

242 datasets.append(None) 

243 continue 

244 

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

246 split_matrix[split_index] = (0.0, 1.0) 

247 split_sizes = [0] * len(_Split) 

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

249 split_datasets = self._build_splits_from_blend( 

250 dataset_type, blend, split_matrix, split_sizes, config, blend_mode 

251 ) 

252 datasets.append(split_datasets[split_index]) 

253 return cast(DatasetSplits, tuple(datasets)) 

254 

255 def _build_multiple_source_splits( 

256 self, 

257 dataset_type: type, 

258 prefixes: Sequence[str], 

259 weights: Sequence[float], 

260 sizes_per_dataset: Sequence[Sequence[int]], 

261 split_matrix: SplitMatrix, 

262 config: GPTDatasetConfig, 

263 blend_mode: BlendMode, 

264 ) -> DatasetSplits: 

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

266 

267 Example with two sources:: 

268 

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

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

271 

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

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

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

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

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

277 

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

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

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

281 

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

283 with one GPTDataset/GPTFromMRDataset per source and split: 

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

285 -> tokens = text[:-1], labels = text[1:] 

286 datasets_per_source = [ 

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

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

289 ] 

290 

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

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

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

294 test components = [], size = 0 

295 

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

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

298 dataset_sample_index = [0, 0, ...] 

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

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

301 

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

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

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

305 

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

307 """ 

308 # pylint: disable=too-many-locals 

309 datasets_per_source = [] 

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

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

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

313 

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

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

316 source_datasets = self._build_mid_level_dataset_splits( 

317 dataset_type, prefix, split_matrix, sizes_for_source, config 

318 ) 

319 datasets_per_source.append(source_datasets) 

320 

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

322 blended_splits = [] 

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

324 if split_range is None or size == 0: 

325 blended_splits.append(None) 

326 continue 

327 

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

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

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

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

332 

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

334 if blend_mode == "no": 

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

336 else: 

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

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

339 blended_splits.append(blended_dataset) 

340 

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

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

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

344 return dataset_splits 

345 

346 def _build_mid_level_dataset_splits( 

347 self, 

348 dataset_type: type, 

349 dataset_path: str, 

350 split_matrix: SplitMatrix, 

351 sizes_for_source: Sequence[int], 

352 config: GPTDatasetConfig, 

353 ) -> DatasetSplits: 

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

355 # pylint: disable=too-many-locals 

356 if ( 

357 config.data_lazy_load 

358 and self.dataloader_context.distributed_enabled 

359 and not (self.dataloader_context.data_index_cache or self.dataloader_context.build_on_rank()) 

360 ): 

361 return cast(DatasetSplits, (None, None, None)) 

362 

363 low_level_dataset = None 

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

365 if not config.data_lazy_load: 

366 low_level_dataset, split_indices = _build_low_level_dataset_and_split_indices( 

367 dataset_type, dataset_path, split_matrix, config 

368 ) 

369 

370 datasets = [] 

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

372 if split_range is None or size == 0: 

373 datasets.append(None) 

374 continue 

375 

376 size = int(size) 

377 if config.data_lazy_load: 

378 lazy_builder = self._build_lazy_mid_level_dataset_split 

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

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

381 mid_level_dataset = LazyDatasetProxy(dataset_factory, unique_identifiers=unique_identifiers) 

382 else: 

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

384 mid_level_dataset = self._build_dataset_for_dataloader( 

385 dataset_factory, barrier_needed=not config.is_dataset_from_mr 

386 ) 

387 datasets.append(mid_level_dataset) 

388 return cast(DatasetSplits, tuple(datasets)) 

389 

390 def _build_lazy_mid_level_dataset_split( 

391 self, 

392 dataset_type: type, 

393 dataset_path: str, 

394 split: _Split, 

395 split_range: SplitRange, 

396 size: int, 

397 config: GPTDatasetConfig, 

398 ) -> Any | None: 

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

400 

401 def _dataset_factory() -> Any: 

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

403 low_level_dataset, split_indices = _build_low_level_dataset_and_split_indices( 

404 dataset_type, dataset_path, [split_range], config 

405 ) 

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

407 return mid_level_dataset 

408 

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

410 return local_dataset 

411 

412 def _build_dataset_for_dataloader( 

413 self, 

414 dataset_factory: Callable[[], Any], 

415 *, 

416 barrier_needed: bool, 

417 ) -> Any | None: 

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

419 local_dataset = build_dataset_for_dataloader( 

420 dataset_factory, 

421 self.dataloader_context, 

422 barrier_needed=barrier_needed, 

423 ) 

424 return local_dataset