Coverage for  / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed_data / api.py: 87%

299 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"""Public builder for sample-balanced distributed dynamic packing. 

16 

17Source modes and buffering 

18-------------------------- 

19Online mode without ``batch_sampler`` requires ``external_step_source``. 

20The source emits one complete local step of raw-sample bins. HP derives 

21metadata, freezes the sample union, 

22balances it across Data Constructors, and routes payloads when necessary. 

23Metadata mode requires ``batch_sampler`` to define step/sample boundaries. 

24It looks up ``metadata[index]`` before any Dataset read; target constructors 

25then directly read their assigned indices from the shared Dataset and skip 

26payload A2A. Metadata-only streaming selection is not supported. 

27 

28With ``batch_sampler``, native HP sampling owns step selection: 

29``balance_window_size`` consecutive sampler yields per DP Constructor are 

30planned together, stopping at optimizer and epoch boundaries. Each iterator 

31yield still returns exactly one forward/backward round. The current window 

32and at most one future window are double-buffered on Host. Device staging 

33independently retains up to ``device_prefetch_depth`` future micro-batches; 

34model-parallel input broadcast runs when the current batch is consumed. 

35Checkpoints advance only at complete optimizer-step boundaries. 

36Dataset outputs stay whole by default. Online ``split_fn`` decomposes each 

37selected output into independent sequences; ``pack_fn`` rebuilds the planned 

38bins without changing the original sampler window's sample membership. 

39Set ``config.repack_sequences=True`` to use built-in text callbacks instead 

40of supplying split_fn/pack_fn. ``config.pad_token_id`` controls source PAD 

41trimming and new PAD; ``config.log_balance`` enables balancing reports. 

42 

43Checkpoint and distributed contracts 

44----------------------------------- 

45Native sampler checkpoint replay requires a deterministic Dataset stream 

46for a given epoch; arbitrary worker-side RNG state is not captured. Metadata and 

47Dataset lengths must agree across all Data Constructor ranks; the caller 

48must ensure this consistency because the builder only validates local inputs. Metadata 

49entries must describe deterministic, rank-independent Dataset outputs. 

50An external_step_source uses pure DP, node-local 

51communication and buffered H2D. Every step evaluates a candidate; sample 

52exchange occurs only when its objective improves by more than 

53min_balance_gain. HCCL is the default backend; Gloo transports control 

54and CPU payloads on the host. HCCL transports control and payload 

55tensors on the rank-local NPU. 

56Checkpoint/resume remains available only on the sampler path, at complete 

57optimizer-step boundaries. Save training state after the optimizer update; 

58an incomplete step retains its starting cursor and replays in full. 

59The caller must keep topology, configuration and custom policies 

60consistent across ranks and compatible with restored checkpoints. 

61""" 

62# This distributed-data package is intentionally PyTorch-only. 

63 

64from __future__ import annotations 

65 

66import math 

67from collections.abc import Callable, Mapping, Sequence 

68from dataclasses import dataclass 

69from functools import partial 

70from multiprocessing.context import BaseContext 

71from typing import Any, Literal 

72 

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

74 

75from hyper_parallel.distributed_data.batch_sampler import ( 

76 BatchSamplerReader, 

77 preserve_sample, 

78 validate_native_sampler, 

79) 

80from hyper_parallel.distributed_data.data_constructor import ( 

81 PackingDataConstructor, 

82 default_collate_fn, 

83 default_pack_fn, 

84) 

85from hyper_parallel.distributed_data.device_prefetch import _create_device_prefetcher, _resolve_device 

86from hyper_parallel.distributed_data.distributed_dataloader import DistributedDataLoader 

87from hyper_parallel.distributed_data.balancing_algorithm import BalancingAlgorithm 

88from hyper_parallel.distributed_data.cost_model import CausalAttentionCostModel, CostModel 

89from hyper_parallel.distributed_data.model_batch import TensorFieldSpec 

90from hyper_parallel.distributed_data.indexed_text import ( 

91 _validate_pad_token_id, 

92 collate_indexed_text_sequences, 

93 pack_indexed_text_samples, 

94 split_packed_text_sample, 

95) 

96from hyper_parallel.distributed_data.packed_balancing import LocalBalancingDataLoader, build_local_balancing_dataloader 

97from hyper_parallel.distributed_data.external_step import ExternalStepSource 

98from hyper_parallel.distributed_data.planner import DynamicPackingPlanner, OversizedPolicy 

99from hyper_parallel.distributed_data.schema import PackingConstraints, SampleMetadata 

100from hyper_parallel.distributed_data.metadata import PlannedSampleLoader 

101from hyper_parallel.distributed_data.dataset_reader import _validate_worker_options 

102from hyper_parallel.distributed_data.topology import DataTopology 

103from hyper_parallel.distributed_data.transport import ( 

104 DataPlaneTransport, 

105 ModelParallelTransport, 

106 create_data_groups, 

107) 

108 

109_DEFAULT_DATALOADER_KWARGS = { 

110 "num_workers": 0, 

111 "pin_memory": False, 

112 "prefetch_factor": None, 

113 "persistent_workers": False, 

114} 

115_OPTIONAL_DATALOADER_KWARGS = frozenset({ 

116 "in_order", 

117 "multiprocessing_context", 

118 "pin_memory_device", 

119 "timeout", 

120 "worker_init_fn", 

121}) 

122_SUPPORTED_DATALOADER_KWARGS = frozenset(_DEFAULT_DATALOADER_KWARGS) | _OPTIONAL_DATALOADER_KWARGS 

123_MANAGED_DATALOADER_KWARGS = frozenset({ 

124 "batch_sampler", 

125 "batch_size", 

126 "collate_fn", 

127 "dataset", 

128 "drop_last", 

129 "generator", 

130 "sampler", 

131 "shuffle", 

132}) 

133 

134 

135@dataclass(frozen=True) 

136class DistributedDatasetConfig: 

137 """Configure sample planning, dynamic packing, and data-plane communication. 

138 

139 ``seq_len`` and ``local_batch_size`` are the only required sizing inputs. 

140 One iterator yield contains ``local_batch_size`` packed sequences on each 

141 DP rank. Native sampler global batch size defines optimizer boundaries. 

142 

143 Args: 

144 seq_len: Maximum token count in one non-oversized packed sequence. 

145 local_batch_size: Packed sequences constructed per DP rank and yield. 

146 balance_window_size: Maximum native sampler micro-batches planned together. 

147 Native prefetch buffers the current window and at most one future window, 

148 each clipped at the optimizer-step boundary. 

149 Defaults to one. Windows stop at optimizer and epoch boundaries; 

150 gradient accumulation is inferred from the native sampler's global 

151 batch size. External-step sources support only one. 

152 device_prefetch_depth: Future native NPU/CUDA micro-batches copied ahead 

153 on Constructors. Model peers receive each batch only at delivery. 

154 Defaults to one. Zero stages only on 

155 demand while retaining Host window prefetch. Must be a non-negative 

156 integer and identical across ranks. Independent of balance_window_size; 

157 ignored on CPU. External-step sources support only the default value. 

158 dp_dim_names: Named mesh dimensions that define DP coordinates. 

159 balance_group_size: Optional number of DP ranks in each independent 

160 local-balancing group. ``None`` preserves launcher/node grouping. 

161 This applies only to external-step 

162 loading; native ``batch_sampler`` loading remains global. 

163 dataset_reader_ranks: Optional Dataset Reader ranks. They read raw 

164 samples online or metadata only in metadata mode. Defaults to the 

165 Data Constructor ranks. 

166 dataset_already_sharded: Legacy sharding configuration flag. 

167 Native BatchSampler requires False because it owns DP slicing; 

168 external sources manage their own partitioning. 

169 planner_rank: Optional centralized Planner rank. Defaults to the lowest 

170 Data Constructor rank. 

171 oversized_policy: ``error`` by default; ``single`` explicitly permits 

172 one oversized sample to occupy a bin alone. 

173 drop_last: Whether to drop a tail with fewer than one sample per global 

174 packing bin. Only ``True`` is supported in this first version. 

175 seed: DataLoader worker seed. Sampling order is configured on the sampler 

176 or external step producer. 

177 min_balance_gain: Minimum relative improvement in the algorithm's 

178 objective. A candidate is accepted only when its gain is strictly 

179 greater; zero rejects equal or worse placements. 

180 packing_budgets: Optional per-packed-sequence additive hard limits. 

181 Each configured stage must 

182 occur in every sample's packing_costs. These limits are independent 

183 of cost-model balancing scores. 

184 communication_backend: Backend for HP data-plane collectives. ``hccl`` 

185 is the default for NPU training and serializes control objects into 

186 accelerator tensors. ``gloo`` keeps control and CPU payload 

187 communication on Gloo. HCCL requires an NPU communication device. 

188 metadata_mode: Whether metadata is precomputed and available by Dataset 

189 index. ``False`` means the Dataset Reader loads samples and calls 

190 ``metadata_fn`` during native sampler planning. 

191 broadcast_plan: Whether one Planner computes and broadcasts each plan. 

192 Defaults to ``True``. If ``False``, every data-plane rank computes 

193 the plan from gathered metadata. Configuration and deterministic 

194 cost/assignment policies must produce identical plans across ranks. 

195 log_balance: Whether to emit INFO-level balancing reports. Defaults to 

196 ``False``; enabling INFO logging alone does not enable these reports. 

197 repack_sequences: Use built-in text splitting and packing on the native 

198 online sampler path. Defaults to ``False``. Records must contain 

199 input_ids, pre-shifted labels and explicit cu_seq_lens. Cannot be 

200 combined with custom split_fn or pack_fn. Unless overridden, use 

201 the text collator and causal Attention square cost including new PAD. 

202 pad_token_id: Optional non-negative source PAD ID for built-in repacking. 

203 Only trailing matching tokens with labels=-100 are removed. The same 

204 ID fills new PAD. ``None`` preserves source tokens and fills with 0. 

205 Has no effect when repack_sequences is false. 

206 """ 

207 

208 seq_len: int 

209 local_batch_size: int 

210 dp_dim_names: tuple[str, ...] | None = None 

211 dataset_reader_ranks: tuple[int, ...] | None = None 

212 dataset_already_sharded: bool = False 

213 planner_rank: int | None = None 

214 oversized_policy: OversizedPolicy = "error" 

215 drop_last: bool = True 

216 seed: int = 1234 

217 min_balance_gain: float = 0.0 

218 packing_budgets: dict[str, float] | None = None 

219 communication_backend: Literal["gloo", "hccl"] = "hccl" 

220 balance_group_size: int | None = None 

221 metadata_mode: bool = False 

222 broadcast_plan: bool = True 

223 balance_window_size: int = 1 

224 log_balance: bool = False 

225 repack_sequences: bool = False 

226 pad_token_id: int | None = None 

227 device_prefetch_depth: int = 1 

228 

229 def __post_init__(self) -> None: 

230 """Validate topology-independent configuration boundaries.""" 

231 self._validate_integer_fields() 

232 self._validate_policy_and_flags() 

233 if self.pad_token_id is not None: 

234 _validate_pad_token_id(self.pad_token_id) 

235 if self.repack_sequences and self.metadata_mode: 

236 raise ValueError("repack_sequences requires online metadata (metadata_mode=False).") 

237 self._validate_rank_tuple(self.dataset_reader_ranks, "dataset_reader_ranks") 

238 self._validate_name_tuple(self.dp_dim_names, "dp_dim_names") 

239 self._validate_planner_rank() 

240 PackingConstraints(self.seq_len, self.oversized_policy, self.packing_budgets) 

241 

242 def _validate_integer_fields(self) -> None: 

243 """Validate window sizes, batch dimensions and non-negative integer options.""" 

244 for name in ("seq_len", "local_batch_size", "balance_window_size"): 

245 value = getattr(self, name) 

246 if not isinstance(value, int) or isinstance(value, bool) or value < 1: 

247 raise ValueError(f"{name} must be a positive integer, but got {value!r}.") 

248 for name in ("seed", "device_prefetch_depth"): 

249 value = getattr(self, name) 

250 if not isinstance(value, int) or isinstance(value, bool) or value < 0: 

251 raise ValueError(f"{name} must be a non-negative integer, but got {value!r}.") 

252 if ( 

253 self.balance_group_size is not None 

254 and ( 

255 not isinstance(self.balance_group_size, int) 

256 or isinstance(self.balance_group_size, bool) 

257 or self.balance_group_size < 1 

258 ) 

259 ): 

260 raise ValueError("balance_group_size must be a positive integer or None.") 

261 

262 def _validate_policy_and_flags(self) -> None: 

263 """Validate policy names, boolean flags and the relative-gain threshold.""" 

264 if self.oversized_policy not in ("error", "single"): 

265 raise ValueError("oversized_policy must be 'error' or 'single'.") 

266 for name in ( 

267 "dataset_already_sharded", 

268 "drop_last", 

269 "metadata_mode", 

270 "broadcast_plan", 

271 "log_balance", 

272 "repack_sequences", 

273 ): 

274 if not isinstance(getattr(self, name), bool): 

275 raise ValueError(f"{name} must be boolean.") 

276 if ( 

277 not isinstance(self.min_balance_gain, (int, float)) 

278 or isinstance(self.min_balance_gain, bool) 

279 or not math.isfinite(self.min_balance_gain) 

280 or not 0.0 <= self.min_balance_gain < 1.0 

281 ): 

282 raise ValueError("min_balance_gain must be in [0, 1).") 

283 if self.communication_backend not in ("gloo", "hccl"): 

284 raise ValueError("communication_backend must be 'gloo' or 'hccl'.") 

285 if not self.drop_last: 

286 raise ValueError( 

287 "Dynamic distributed packing currently requires drop_last=True so every DP rank receives the " 

288 "same number of non-empty packing bins." 

289 ) 

290 

291 def _validate_planner_rank(self) -> None: 

292 if self.planner_rank is not None and ( 

293 not isinstance(self.planner_rank, int) 

294 or isinstance(self.planner_rank, bool) 

295 or self.planner_rank < 0 

296 ): 

297 raise ValueError("planner_rank must be a non-negative integer or None.") 

298 

299 @staticmethod 

300 def _validate_rank_tuple(value: tuple[int, ...] | None, name: str) -> None: 

301 if value is None: 

302 return 

303 if not isinstance(value, tuple) or not value: 

304 raise ValueError(f"{name} must be a non-empty tuple or None.") 

305 if any(not isinstance(rank, int) or isinstance(rank, bool) or rank < 0 for rank in value): 

306 raise ValueError(f"{name} must contain non-negative integer ranks.") 

307 if len(value) != len(set(value)): 

308 raise ValueError(f"{name} must not contain duplicate ranks.") 

309 

310 @staticmethod 

311 def _validate_name_tuple(value: tuple[str, ...] | None, name: str) -> None: 

312 if value is None: 

313 return 

314 if not isinstance(value, tuple) or not value or any(not isinstance(item, str) or not item for item in value): 

315 raise ValueError(f"{name} must be a non-empty tuple of strings or None.") 

316 if len(value) != len(set(value)): 

317 raise ValueError(f"{name} must not contain duplicate names.") 

318 

319 

320def _resolve_service_ranks( 

321 topology: DataTopology, 

322 config: DistributedDatasetConfig, 

323) -> tuple[tuple[int, ...], int]: 

324 """Resolve Reader and planner ownership within the root mesh.""" 

325 dataset_reader_ranks = config.dataset_reader_ranks or topology.constructor_ranks 

326 unknown_ranks = set(dataset_reader_ranks) - set(topology.rank_list) 

327 if unknown_ranks: 

328 raise ValueError(f"dataset_reader_ranks are outside the root mesh: {sorted(unknown_ranks)}.") 

329 planner_rank = min(topology.constructor_ranks) if config.planner_rank is None else config.planner_rank 

330 data_plane_ranks = set(dataset_reader_ranks) | set(topology.constructor_ranks) 

331 if planner_rank not in data_plane_ranks: 

332 raise ValueError(f"planner_rank {planner_rank} must be a Dataset Reader or Data Constructor rank.") 

333 return dataset_reader_ranks, planner_rank 

334 

335 

336def _normalize_dataloader_kwargs( 

337 dataloader_kwargs: Mapping[str, Any] | None, 

338) -> dict[str, Any]: 

339 """Resolve validated DataLoader execution options.""" 

340 if dataloader_kwargs is None: 

341 supplied_options = {} 

342 elif not isinstance(dataloader_kwargs, Mapping): 

343 raise ValueError("dataloader_kwargs must be a mapping or None.") 

344 else: 

345 try: 

346 supplied_options = dict(dataloader_kwargs) 

347 except Exception as exc: 

348 raise ValueError(f"dataloader_kwargs cannot be copied: {exc}") from exc 

349 _validate_dataloader_option_keys(supplied_options) 

350 options = dict(_DEFAULT_DATALOADER_KWARGS) 

351 options.update(supplied_options) 

352 _validate_worker_options(options) 

353 _validate_dataloader_timeout(options) 

354 worker_init_fn = options.get("worker_init_fn") 

355 if worker_init_fn is not None and not callable(worker_init_fn): 

356 raise ValueError("dataloader_kwargs worker_init_fn must be callable or None.") 

357 _validate_multiprocessing_context(options) 

358 pin_memory_device = options.get("pin_memory_device", "") 

359 if not isinstance(pin_memory_device, str): 

360 raise ValueError("dataloader_kwargs pin_memory_device must be a string.") 

361 return options 

362 

363 

364def _validate_dataloader_option_keys(supplied_options: Mapping[str, Any]) -> None: 

365 """Reject unsupported keys and overrides of internally managed DataLoader options.""" 

366 non_string_keys = [key for key in supplied_options if not isinstance(key, str)] 

367 if non_string_keys: 

368 raise ValueError(f"dataloader_kwargs keys must be strings, but got {non_string_keys!r}.") 

369 managed_keys = sorted(set(supplied_options) & _MANAGED_DATALOADER_KWARGS) 

370 if managed_keys: 

371 raise ValueError( 

372 f"dataloader_kwargs cannot override distributed sampling, batching, collation, or deterministic " 

373 f"worker-seeding options " 

374 f"{managed_keys}." 

375 ) 

376 unsupported_keys = sorted(set(supplied_options) - _SUPPORTED_DATALOADER_KWARGS) 

377 if unsupported_keys: 

378 raise ValueError(f"dataloader_kwargs contains unsupported options {unsupported_keys}.") 

379 

380 

381def _validate_dataloader_timeout(options: Mapping[str, Any]) -> None: 

382 """Require a finite timeout compatible with the configured worker count.""" 

383 num_workers = options["num_workers"] 

384 timeout = options.get("timeout", 0) 

385 try: 

386 normalized_timeout = float(timeout) 

387 except (TypeError, ValueError, OverflowError): 

388 normalized_timeout = math.nan 

389 if ( 

390 not isinstance(timeout, (int, float)) 

391 or isinstance(timeout, bool) 

392 or not math.isfinite(normalized_timeout) 

393 or normalized_timeout < 0 

394 ): 

395 raise ValueError(f"dataloader_kwargs timeout must be finite and non-negative, but got {timeout!r}.") 

396 if num_workers == 0 and timeout != 0: 

397 raise ValueError("dataloader_kwargs timeout must be zero when num_workers=0.") 

398 

399 

400def _validate_multiprocessing_context(options: Mapping[str, Any]) -> None: 

401 """Check that the worker context uses a supported multiprocessing start method.""" 

402 multiprocessing_context = options.get("multiprocessing_context") 

403 if multiprocessing_context is None: 

404 return 

405 if options["num_workers"] == 0: 

406 raise ValueError("dataloader_kwargs multiprocessing_context requires num_workers > 0.") 

407 if not isinstance(multiprocessing_context, (str, BaseContext)): 

408 raise ValueError("dataloader_kwargs multiprocessing_context is invalid.") 

409 start_method = multiprocessing_context 

410 if isinstance(multiprocessing_context, BaseContext): 

411 try: 

412 start_method = multiprocessing_context.get_start_method() 

413 except Exception as exc: 

414 raise ValueError( 

415 f"dataloader_kwargs multiprocessing_context cannot report its start method: {exc}" 

416 ) from exc 

417 if start_method not in torch.multiprocessing.get_all_start_methods(): 

418 raise ValueError(f"dataloader_kwargs multiprocessing_context={start_method!r} is not supported.") 

419 

420 

421def _normalize_communication_device(communication_device: Any) -> torch.device | None: 

422 """Normalize a rank-local A2A device.""" 

423 if communication_device is None: 

424 return None 

425 try: 

426 return torch.device(communication_device) 

427 except Exception as exc: 

428 raise ValueError(f"communication_device is invalid: {communication_device!r}.") from exc 

429 

430 

431class _BuildState: 

432 """Validated rank-local components needed to assemble a distributed DataLoader.""" 

433 

434 def __init__( 

435 self, 

436 dataset: Any | None, 

437 mesh: Any, 

438 config: DistributedDatasetConfig, 

439 metadata_fn: Callable[[Any], SampleMetadata] | None, 

440 metadata: Sequence[SampleMetadata] | None, 

441 dataloader_kwargs: Mapping[str, Any] | None, 

442 pack_fn: Callable[[Sequence[Any], int], Any] | None, 

443 collate_fn: Callable[[Sequence[Any]], Any] | None, 

444 communication_device: Any, 

445 batch_sampler: Any = None, 

446 *, 

447 split_fn: Callable[[Any], Sequence[Any]] | None = None, 

448 model_config: Any = None, 

449 cost_model: CostModel | None = None, 

450 balancing_algorithm: BalancingAlgorithm | None = None, 

451 ) -> None: 

452 """Validate inputs and construct all rank-local components. 

453 

454 Args: 

455 dataset: Mapping Dataset on each Reader rank. 

456 mesh: Named root device mesh. 

457 config: Explicit loading mode, topology and packing options. 

458 metadata_fn: Derives metadata from loaded samples in online mode. 

459 metadata: Indexed precomputed metadata in metadata mode. 

460 dataloader_kwargs: Rank-local sample loading options. 

461 pack_fn: Optional packed-sequence constructor. 

462 collate_fn: Optional local-batch collator. 

463 communication_device: Rank-local control and payload device. 

464 batch_sampler: Native sampler defining each local batch. 

465 split_fn: Optional decomposition of each selected record into independent sequences. 

466 model_config: Backbone dimensions for the default cost model. 

467 cost_model: Optional workload estimator. 

468 balancing_algorithm: Optional assignment and objective policy. 

469 """ 

470 if not isinstance(config, DistributedDatasetConfig): 

471 raise ValueError(f"config must be DistributedDatasetConfig, but got {type(config)}.") 

472 _validate_builder_callbacks(metadata_fn, metadata, pack_fn, collate_fn) 

473 split_fn, pack_fn, collate_fn = _resolve_repacking_callbacks( 

474 config, batch_sampler, split_fn, pack_fn, collate_fn, 

475 ) 

476 if split_fn is not None and (not callable(split_fn) or config.metadata_mode): 

477 raise ValueError("split_fn requires online metadata and must be callable.") 

478 normalized_options = _normalize_dataloader_kwargs(dataloader_kwargs) 

479 loader_options = {name: normalized_options[name] for name in _DEFAULT_DATALOADER_KWARGS} 

480 loader_options["dataloader_kwargs"] = { 

481 name: value 

482 for name, value in normalized_options.items() 

483 if name not in _DEFAULT_DATALOADER_KWARGS 

484 } 

485 self.communication_device = _normalize_communication_device(communication_device) 

486 if batch_sampler is None: 

487 if config.metadata_mode: 

488 raise ValueError("Metadata mode requires batch_sampler to define step/sample boundaries.") 

489 raise ValueError("Online mode requires batch_sampler or external_step_source to define complete steps.") 

490 effective_pack_fn, effective_collate_fn = _resolve_constructor_callbacks( 

491 pack_fn, collate_fn, split_fn, 

492 ) 

493 self.topology = DataTopology.from_mesh(mesh, dp_dim_names=config.dp_dim_names) 

494 self.dataset_reader_ranks, self.planner_rank = _resolve_service_ranks(self.topology, config) 

495 self.dataset_reader: BatchSamplerReader | None = None 

496 self.metadata_reader: BatchSamplerReader | None = None 

497 self.direct_sample_loader: PlannedSampleLoader | None = None 

498 if config.metadata_mode: 

499 if metadata_fn is not None: 

500 raise ValueError("metadata_mode=True requires precomputed metadata, not metadata_fn.") 

501 if metadata is None: 

502 raise ValueError("metadata_mode=True requires metadata.") 

503 elif metadata is not None: 

504 raise ValueError("metadata_mode=False requires metadata_fn, not precomputed metadata.") 

505 elif metadata_fn is None: 

506 raise ValueError("metadata_mode=False requires metadata_fn.") 

507 self._configure_batch_sampler_sources( 

508 dataset, metadata_fn, metadata, config, batch_sampler, loader_options, split_fn, 

509 ) 

510 self.planner, self.constructor = self._build_components( 

511 config, 

512 effective_pack_fn, 

513 effective_collate_fn, 

514 model_config, 

515 cost_model, 

516 balancing_algorithm, 

517 ) 

518 

519 @property 

520 def is_reader(self) -> bool: 

521 """Return whether this rank owns a Dataset Reader.""" 

522 return self.topology.global_rank in self.dataset_reader_ranks 

523 

524 def _configure_batch_sampler_sources( 

525 self, 

526 dataset: Any, 

527 metadata_fn: Callable[[Any], SampleMetadata] | None, 

528 metadata: Sequence[SampleMetadata] | None, 

529 config: DistributedDatasetConfig, 

530 batch_sampler: Any, 

531 loader_options: dict[str, Any], 

532 split_fn: Callable[[Any], Sequence[Any]] | None, 

533 ) -> None: 

534 """Use native DP sampler owners as Readers without applying a second stride.""" 

535 if self.dataset_reader_ranks != self.topology.constructor_ranks: 

536 raise ValueError("Native batch_sampler currently requires Dataset Readers on the Data Constructor ranks.") 

537 if config.dataset_already_sharded: 

538 raise ValueError( 

539 "Native batch_sampler owns DP slicing; disable dataset_already_sharded." 

540 ) 

541 validate_native_sampler( 

542 batch_sampler, data_rank=self.topology.data_rank, 

543 dp_size=self.topology.data_parallel_size, local_batch_size=config.local_batch_size, 

544 ) 

545 if not self.is_reader: 

546 return 

547 if dataset is None: 

548 raise ValueError("Native batch_sampler requires a mapping Dataset on each Data Constructor.") 

549 sample_loader = PlannedSampleLoader(dataset, seed=config.seed, **loader_options) 

550 sample_loader.set_epoch(batch_sampler.epoch) 

551 if config.metadata_mode: 

552 if not callable(getattr(metadata, "__getitem__", None)) or len(metadata) != len(dataset): 

553 raise ValueError("Native batch_sampler metadata must align with the mapping Dataset.") 

554 self.direct_sample_loader = sample_loader 

555 reader = BatchSamplerReader( 

556 batch_sampler, reader_rank=self.topology.global_rank, 

557 metadata=metadata, metadata_fn=metadata_fn, 

558 sample_loader=None if config.metadata_mode else sample_loader, 

559 split_fn=split_fn, 

560 ) 

561 if config.metadata_mode: 

562 self.metadata_reader = reader 

563 else: 

564 self.dataset_reader = reader 

565 

566 def _build_components( 

567 self, 

568 config: DistributedDatasetConfig, 

569 effective_pack_fn: Callable[[Sequence[Any], int], Any], 

570 effective_collate_fn: Callable[[Sequence[Any]], Any], 

571 model_config: Any, 

572 cost_model: CostModel | None, 

573 balancing_algorithm: BalancingAlgorithm | None, 

574 ) -> tuple[DynamicPackingPlanner, PackingDataConstructor]: 

575 """Create the planner and data constructor.""" 

576 if cost_model is None and config.repack_sequences: 

577 cost_model = CausalAttentionCostModel() 

578 planner = DynamicPackingPlanner( 

579 data_parallel_size=self.topology.data_parallel_size, 

580 seq_len=config.seq_len, 

581 local_batch_size=config.local_batch_size, 

582 oversized_policy=config.oversized_policy, 

583 packing_budgets=config.packing_budgets, 

584 min_balance_gain=config.min_balance_gain, 

585 model_config=model_config, 

586 cost_model=cost_model, 

587 balancing_algorithm=balancing_algorithm, 

588 ) 

589 return planner, PackingDataConstructor(effective_pack_fn, effective_collate_fn, seq_len=config.seq_len) 

590 

591 

592def _validate_builder_callbacks( 

593 metadata_fn: Callable[[Any], SampleMetadata] | None, 

594 metadata: Sequence[SampleMetadata] | None, 

595 pack_fn: Callable[[Sequence[Any], int], Any] | None, 

596 collate_fn: Callable[[Sequence[Any]], Any] | None, 

597) -> None: 

598 """Validate callback types and mutually exclusive metadata providers.""" 

599 if metadata_fn is not None and not callable(metadata_fn): 

600 raise ValueError("metadata_fn must be callable or None.") 

601 if metadata_fn is not None and metadata is not None: 

602 raise ValueError("Provide either metadata or metadata_fn, but not both.") 

603 if pack_fn is not None and not callable(pack_fn): 

604 raise ValueError("pack_fn must be callable or None.") 

605 if collate_fn is not None and not callable(collate_fn): 

606 raise ValueError("collate_fn must be callable or None.") 

607 

608 

609def _resolve_repacking_callbacks( 

610 config: DistributedDatasetConfig, 

611 batch_sampler: Any, 

612 split_fn: Callable[[Any], Sequence[Any]] | None, 

613 pack_fn: Callable[[Sequence[Any], int], Any] | None, 

614 collate_fn: Callable[[Sequence[Any]], Any] | None, 

615) -> tuple[ 

616 Callable[[Any], Sequence[Any]] | None, 

617 Callable[[Sequence[Any], int], Any] | None, 

618 Callable[[Sequence[Any]], Any] | None, 

619]: 

620 """Select built-in text callbacks without mixing packing contracts.""" 

621 if not config.repack_sequences: 

622 return split_fn, pack_fn, collate_fn 

623 if batch_sampler is None: 

624 raise ValueError("repack_sequences requires a native batch_sampler.") 

625 if split_fn is not None or pack_fn is not None: 

626 raise ValueError("repack_sequences cannot be combined with custom split_fn or pack_fn.") 

627 return ( 

628 partial(split_packed_text_sample, pad_token_id=config.pad_token_id), 

629 partial(pack_indexed_text_samples, pad_token_id=0 if config.pad_token_id is None else config.pad_token_id), 

630 collate_indexed_text_sequences if collate_fn is None else collate_fn, 

631 ) 

632 

633 

634def _resolve_constructor_callbacks( 

635 pack_fn: Callable[[Sequence[Any], int], Any] | None, 

636 collate_fn: Callable[[Sequence[Any]], Any] | None, 

637 split_fn: Callable[[Any], Sequence[Any]] | None = None, 

638) -> tuple[Callable[[Sequence[Any], int], Any], Callable[[Sequence[Any]], Any]]: 

639 """Resolve constructor callbacks for a native BatchSampler source.""" 

640 if split_fn is not None and pack_fn is None: 

641 raise ValueError("Native split_fn requires pack_fn to rebuild the planned sequences.") 

642 if split_fn is None and pack_fn is not None: 

643 raise ValueError("Native batch_sampler preserves Dataset outputs; pack_fn must be omitted.") 

644 effective_pack_fn = preserve_sample if split_fn is None else pack_fn 

645 effective_collate_fn = default_collate_fn if collate_fn is None else collate_fn 

646 return effective_pack_fn, effective_collate_fn 

647 

648 

649def build_distributed_dataloader( 

650 dataset: Any | None, 

651 mesh: Any, 

652 config: DistributedDatasetConfig, 

653 *, 

654 metadata_fn: Callable[[Any], SampleMetadata] | None = None, 

655 metadata: Sequence[SampleMetadata] | None = None, 

656 dataloader_kwargs: Mapping[str, Any] | None = None, 

657 pack_fn: Callable[[Sequence[Any], int], Any] | None = None, 

658 split_fn: Callable[[Any], Sequence[Any]] | None = None, 

659 collate_fn: Callable[[Sequence[Any]], Any] | None = None, 

660 device: Any = None, 

661 model_batch_spec: Any = None, 

662 batch_sampler: Any = None, 

663 external_step_source: ExternalStepSource | None = None, 

664 model_config: Any = None, 

665 cost_model: CostModel | None = None, 

666 balancing_algorithm: BalancingAlgorithm | None = None, 

667) -> DistributedDataLoader | LocalBalancingDataLoader: 

668 """Build a sample-balanced loader using the source and checkpoint contracts above. 

669 

670 Args: 

671 dataset: Shared mapping Dataset on native Data Constructors. External sources 

672 own loading and may pass None; other ranks may also pass None. 

673 mesh: Named root HyperParallel or native DeviceMesh. 

674 config: Packing, repacking, logging, service-rank and communication options. 

675 metadata_fn: Derive metadata from each native Dataset output or external raw 

676 sample. With splitting, receives each independent sequence instead. 

677 metadata: Explicit indexable metadata on native Dataset Reader ranks in metadata 

678 mode. Must implement __len__ and __getitem__; metadata[index] must describe 

679 the corresponding deterministic, rank-independent dataset[index]. 

680 dataloader_kwargs: Native worker options: num_workers=0, pin_memory=False, 

681 prefetch_factor=None, persistent_workers=False by default. Sampling, 

682 batching, shuffling and DataLoader collation cannot be overridden. 

683 pack_fn: Build one packed sequence; the external default preserves a bin as a 

684 raw-sample tuple. Omit when config.repack_sequences selects text packing. 

685 split_fn: Online native callback returning independent sequences in original 

686 order. Requires pack_fn. Each original record must fit one bin and its 

687 sequences retain their fallback grouping. Preserve tokens, targets and 

688 attention isolation; contextual slices are not independent. Omit with 

689 config.repack_sequences; unsupported in metadata mode. 

690 collate_fn: Collate local_batch_size planned bins. Defaults to text collation 

691 with config.repack_sequences, otherwise a tuple; an explicit callback wins. 

692 device: Rank-local training device. HCCL uses its NPU for control, payload and 

693 final H2D. Gloo communicates on Host and defaults to CPU; pass an explicit 

694 NPU/CUDA device to enable H2D with Gloo. 

695 model_batch_spec: Identical layout on every MP rank: nested dict/list/tuple 

696 leaves are TensorFieldSpec or static scalars. Dynamic values must be tensors; 

697 fields and dtypes are not broadcast. Defaults to collate_fn.model_batch_spec 

698 or the built-in text layout. Required for custom MP batches, unnecessary for DP. 

699 batch_sampler: Native HP BatchSampler supplied on every rank; only Constructors 

700 advance it. Readers must coincide with Constructors and drop_last must be 

701 true. Dataset outputs stay whole unless split_fn/pack_fn or online text 

702 repacking is enabled. Metadata indexes outputs, not underlying documents. 

703 Checkpoint via this loader, not the sampler's speculative cursor. 

704 external_step_source: Online pure-DP iterable emitting one complete local step 

705 of local_batch_size raw-sample bins. Optional set_epoch is forwarded; 

706 exhaustion ends iteration. The caller owns recovery and step limits. 

707 model_config: Backbone dimensions for the default cost model when repacking 

708 is disabled and cost_model is omitted. 

709 cost_model: Optional workload callback. Text repacking defaults to 

710 CausalAttentionCostModel including new PAD; otherwise DefaultCostModel. 

711 balancing_algorithm: Optional assignment/objective policy receiving scored 

712 samples. HyperParallel enforces capacity and the gain gate. 

713 

714 Returns: 

715 Collective iterator yielding one constructed local batch per micro-batch. 

716 """ 

717 if external_step_source is not None: 

718 if model_batch_spec is not None: 

719 raise ValueError("model_batch_spec is only supported with a native batch_sampler.") 

720 if split_fn is not None: 

721 raise ValueError("split_fn is only supported with a native batch_sampler.") 

722 if config.metadata_mode: 

723 raise ValueError("external_step_source requires metadata_mode=False for online metadata.") 

724 if batch_sampler is not None or metadata is not None: 

725 raise ValueError("external_step_source cannot be combined with a sampler or metadata sequence.") 

726 if dataloader_kwargs: 

727 raise ValueError("Configure source worker options on external_step_source, not the balancing wrapper.") 

728 return build_local_balancing_dataloader( 

729 external_step_source, mesh, config, metadata_fn=metadata_fn, 

730 pack_fn=default_pack_fn if pack_fn is None else pack_fn, 

731 collate_fn=default_collate_fn if collate_fn is None else collate_fn, 

732 model_config=model_config, cost_model=cost_model, 

733 balancing_algorithm=balancing_algorithm, device=device, 

734 ) 

735 return _build_distributed_dataloader_impl( 

736 dataset, mesh, config, metadata_fn=metadata_fn, metadata=metadata, 

737 dataloader_kwargs=dataloader_kwargs, pack_fn=pack_fn, collate_fn=collate_fn, 

738 split_fn=split_fn, communication_device=device, model_batch_spec=model_batch_spec, 

739 batch_sampler=batch_sampler, model_config=model_config, 

740 cost_model=cost_model, balancing_algorithm=balancing_algorithm, 

741 ) 

742 

743 

744def _build_distributed_dataloader_impl( 

745 dataset: Any | None, 

746 mesh: Any, 

747 config: DistributedDatasetConfig, 

748 *, 

749 metadata_fn: Callable[[Any], SampleMetadata] | None, 

750 metadata: Sequence[SampleMetadata] | None, 

751 dataloader_kwargs: Mapping[str, Any] | None, 

752 pack_fn: Callable[[Sequence[Any], int], Any] | None, 

753 collate_fn: Callable[[Sequence[Any]], Any] | None, 

754 communication_device: Any, 

755 model_batch_spec: Any = None, 

756 batch_sampler: Any = None, 

757 split_fn: Callable[[Any], Sequence[Any]] | None = None, 

758 model_config: Any = None, 

759 cost_model: CostModel | None = None, 

760 balancing_algorithm: BalancingAlgorithm | None = None, 

761) -> DistributedDataLoader: 

762 """Build native sampler readers, planning, transport and device staging for this rank.""" 

763 if communication_device is None: 

764 communication_device = _resolve_device(communication_backend=getattr(config, "communication_backend", "hccl")) 

765 if getattr(config, "balance_group_size", None) is not None: 

766 raise ValueError("balance_group_size is only supported for local balancing paths.") 

767 state = _BuildState( 

768 dataset, 

769 mesh, 

770 config, 

771 metadata_fn, 

772 metadata, 

773 dataloader_kwargs, 

774 pack_fn, 

775 collate_fn, 

776 communication_device, 

777 batch_sampler, 

778 split_fn=split_fn, 

779 model_config=model_config, 

780 cost_model=cost_model, 

781 balancing_algorithm=balancing_algorithm, 

782 ) 

783 device = state.communication_device 

784 # Validate device/stream support on every rank before any group is created. 

785 device_prefetch = _create_device_prefetcher(device) 

786 communication_device = device if config.communication_backend == "hccl" else None 

787 if model_batch_spec is None: 

788 model_batch_spec = getattr(collate_fn, "model_batch_spec", None) 

789 if model_batch_spec is None and ( 

790 collate_fn is collate_indexed_text_sequences or (config.repack_sequences and collate_fn is None) 

791 ): 

792 shape = (config.local_batch_size, config.seq_len) 

793 model_batch_spec = { 

794 "input_ids": TensorFieldSpec(shape, torch.int64), 

795 "labels": TensorFieldSpec(shape, torch.int64), 

796 "cu_seq_lens": TensorFieldSpec((None,), torch.int32), 

797 } 

798 groups = create_data_groups( 

799 state.topology, 

800 state.dataset_reader_ranks, 

801 state.planner_rank, 

802 cpu_backend=config.communication_backend, 

803 payload_backend=config.communication_backend, 

804 communication_device=communication_device, 

805 enable_payload_exchange=not config.metadata_mode, 

806 model_device=device, 

807 ) 

808 

809 accumulation_steps = batch_sampler.global_batch_size // batch_sampler.global_micro_batch_size 

810 return DistributedDataLoader( 

811 accumulation_steps=accumulation_steps, 

812 balance_window_size=config.balance_window_size, 

813 device_prefetch_depth=config.device_prefetch_depth, 

814 device=device, 

815 initial_epoch=batch_sampler.epoch, 

816 initial_consumed_samples=batch_sampler.consumed_samples, 

817 topology=state.topology, 

818 dataset_reader_ranks=state.dataset_reader_ranks, 

819 dataset_reader=state.dataset_reader, 

820 metadata_reader=state.metadata_reader, 

821 direct_sample_loader=state.direct_sample_loader, 

822 metadata_mode=config.metadata_mode, 

823 broadcast_plan=config.broadcast_plan, 

824 log_balance=config.log_balance, 

825 planner=state.planner, 

826 data_constructor=state.constructor, 

827 data_plane=DataPlaneTransport( 

828 groups, 

829 state.topology.global_rank, 

830 communication_device=communication_device, 

831 ), 

832 model_transport=ModelParallelTransport( 

833 state.topology, 

834 groups, 

835 model_batch_spec=model_batch_spec, 

836 device=device, 

837 communication_device=communication_device, 

838 ), 

839 device_prefetch=device_prefetch if state.topology.is_constructor else None, 

840 ) 

841 

842 

843__all__ = ["DistributedDatasetConfig", "build_distributed_dataloader"]