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

570 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"""Dataset Reader, Planner, and Data Constructor orchestration.""" 

16 

17from __future__ import annotations 

18 

19import copy 

20import logging 

21import math 

22from collections import deque 

23from collections.abc import Iterator, Mapping 

24from concurrent.futures import Future 

25from contextlib import nullcontext 

26from dataclasses import dataclass, field 

27from queue import Queue 

28from threading import Thread 

29from typing import Any, Union 

30 

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

32 

33from hyper_parallel.data.parallel.batch_sampler import ( 

34 get_sampler_checkpoint_state, has_torchdata_state, sampler_optimizer_step_progress, sampler_state_for_load, 

35) 

36from hyper_parallel.distributed_data.balance_logging import format_balance_stats, logger as balance_logger 

37from hyper_parallel.distributed_data.data_constructor import PackingDataConstructor 

38from hyper_parallel.distributed_data.batch_sampler import BatchSamplerReader, _ReaderPlanInput 

39from hyper_parallel.distributed_data.device_prefetch import DevicePrefetchedStep, DeviceStepPrefetcher, _record_stream 

40from hyper_parallel.distributed_data.planner import DynamicPackingPlanner 

41from hyper_parallel.distributed_data.schema import ( 

42 DistributedPackingPlan, 

43 SampleKey, 

44 SampleMetadata, 

45) 

46from hyper_parallel.distributed_data.metadata import PlannedSampleLoader 

47from hyper_parallel.distributed_data.topology import DataTopology 

48from hyper_parallel.distributed_data.transport import ( 

49 DataPlaneTransport, 

50 ModelParallelTransport, 

51 PreparedPayloadExchange, 

52) 

53 

54 

55_PlanControl = Union[DistributedPackingPlan, tuple[DistributedPackingPlan, ...], None] 

56 

57 

58def _validate_loader_components( 

59 *, 

60 topology: DataTopology, 

61 dataset_reader_ranks: tuple[int, ...], 

62 dataset_reader: Any | None, 

63 metadata_reader: BatchSamplerReader | None, 

64 direct_sample_loader: PlannedSampleLoader | None, 

65 metadata_mode: bool, 

66) -> None: 

67 """Check Reader ownership and require mode-compatible loading components.""" 

68 if not isinstance(metadata_mode, bool): 

69 raise ValueError("metadata_mode must be boolean.") 

70 if metadata_mode and dataset_reader is not None: 

71 raise ValueError("Metadata mode must not configure an online Dataset Reader.") 

72 if not metadata_mode and (metadata_reader is not None or direct_sample_loader is not None): 

73 raise ValueError("Online mode must not configure metadata loading components.") 

74 is_reader = topology.global_rank in dataset_reader_ranks 

75 planning_reader = metadata_reader if metadata_mode else dataset_reader 

76 if is_reader != (planning_reader is not None): 

77 raise ValueError("Dataset Reader ownership does not match dataset_reader_ranks.") 

78 if not metadata_mode and planning_reader is not None and not callable( 

79 getattr(planning_reader, "prepare_next_step", None) 

80 ): 

81 raise ValueError("Online Dataset Reader must provide prepare_next_step().") 

82 _validate_metadata_loader_owner( 

83 topology, 

84 metadata_mode=metadata_mode, 

85 direct_sample_loader=direct_sample_loader, 

86 ) 

87 

88 

89def _validate_metadata_loader_owner( 

90 topology: DataTopology, 

91 *, 

92 metadata_mode: bool, 

93 direct_sample_loader: PlannedSampleLoader | None, 

94) -> None: 

95 if not metadata_mode: 

96 return 

97 if topology.is_constructor != (direct_sample_loader is not None): 

98 raise ValueError("Every metadata Data Constructor must own one plan-aware sample loader.") 

99 

100 

101@dataclass 

102class _PreparedWindow: 

103 """Retain one Host balancing window and an optional complete-step checkpoint.""" 

104 

105 batches: deque[Any] 

106 exhausted: bool 

107 checkpoint: dict[str, Any] | None 

108 balance_logs: list[str] = field(default_factory=list) 

109 

110 

111@dataclass 

112class _PreparedBatch: 

113 """Carry one batch and its delivery state through caller-side staging.""" 

114 

115 batch: Any 

116 checkpoint: dict[str, Any] | None = None 

117 balance_logs: list[str] = field(default_factory=list) 

118 ready_event: Any = None 

119 error: BaseException | None = None 

120 

121 

122class DistributedDataLoader(Iterator[Any]): 

123 """Prepare balancing windows and deliver one local micro-batch at a time. 

124 

125 One Host producer owns reads, planning, data-plane collectives and pinning. 

126 The caller prefetches H2D and broadcasts only the batch being delivered. 

127 Host windows and device inputs have independent buffering limits. 

128 Checkpoints advance at complete optimizer-step delivery boundaries. 

129 """ 

130 

131 def __init__( 

132 self, 

133 *, 

134 topology: DataTopology, 

135 dataset_reader_ranks: tuple[int, ...], 

136 dataset_reader: Any | None, 

137 metadata_reader: BatchSamplerReader | None, 

138 direct_sample_loader: PlannedSampleLoader | None, 

139 metadata_mode: bool, 

140 planner: DynamicPackingPlanner, 

141 data_constructor: PackingDataConstructor, 

142 data_plane: DataPlaneTransport, 

143 model_transport: ModelParallelTransport, 

144 device_prefetch: DeviceStepPrefetcher | None = None, 

145 initial_epoch: int = 0, 

146 initial_consumed_samples: int = 0, 

147 broadcast_plan: bool = True, 

148 log_balance: bool = False, 

149 accumulation_steps: int = 1, 

150 balance_window_size: int = 1, 

151 device_prefetch_depth: int = 1, 

152 device: Any = "cpu", 

153 ) -> None: 

154 """Store the fully validated runtime components.""" 

155 _validate_loader_components( 

156 topology=topology, 

157 dataset_reader_ranks=dataset_reader_ranks, 

158 dataset_reader=dataset_reader, 

159 metadata_reader=metadata_reader, 

160 direct_sample_loader=direct_sample_loader, 

161 metadata_mode=metadata_mode, 

162 ) 

163 self._topology = topology 

164 self._dataset_reader_ranks = frozenset(dataset_reader_ranks) 

165 self._dataset_reader = dataset_reader 

166 self._metadata_reader = metadata_reader 

167 self._direct_sample_loader = direct_sample_loader 

168 self._metadata_mode = metadata_mode 

169 self._broadcast_plan = broadcast_plan 

170 self._log_balance = log_balance 

171 self._planner = planner 

172 self._data_constructor = data_constructor 

173 self._data_plane = data_plane 

174 self._model_transport = model_transport 

175 self._device_prefetch = device_prefetch 

176 self._epoch = initial_epoch 

177 self._step = 0 

178 self._initial_consumed_samples: int | None = initial_consumed_samples 

179 self._stopped = False 

180 self._pending_local_keys: set[SampleKey] = set() 

181 self._window_plans: deque[DistributedPackingPlan] = deque() 

182 for name, value in (("accumulation_steps", accumulation_steps), ("balance_window_size", balance_window_size)): 

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

184 raise ValueError(f"{name} must be a positive integer.") 

185 self._accumulation_steps = accumulation_steps 

186 self._balance_window_size = balance_window_size 

187 if (isinstance(device_prefetch_depth, bool) or not isinstance(device_prefetch_depth, int) 

188 or device_prefetch_depth < 0): 

189 raise ValueError("device_prefetch_depth must be a non-negative integer.") 

190 self._device_prefetch_depth = device_prefetch_depth 

191 self._device = torch.device(device) 

192 self._current: _PreparedWindow | None = None 

193 self._future: Future[_PreparedWindow] | None = None 

194 self._requests: Queue[Future[_PreparedWindow] | None] = Queue(maxsize=1) 

195 self._prefetch_thread: Thread | None = None 

196 self._device_batches: deque[_PreparedBatch] = deque() 

197 self._prefetch_target = 0 

198 self._consumer_stream: Any = None 

199 # Each staged batch spends one input slot; advancing returns it with the 

200 # previous consumer's completion event, even if that work is still running. 

201 # Include the active batch so device lookahead can overlap its computation. 

202 self._prefetch_slots: deque[Any] = deque([None] * (device_prefetch_depth + 1)) 

203 self._device_stopped = False 

204 self._batch_error: BaseException | None = None 

205 self._last_ready_event: Any = None 

206 self._has_delivered = False 

207 self._closed = False 

208 self._balance_logs: list[str] = [] 

209 self._delivered_state = self._snapshot_state() 

210 

211 def __iter__(self) -> DistributedDataLoader: 

212 """Return the rank-local iterator.""" 

213 return self 

214 

215 def __next__(self) -> Any: 

216 """Deliver one batch and refill the configured device lookahead.""" 

217 if self._closed or self._delivered_state["stopped"]: 

218 raise StopIteration 

219 if self._batch_error is not None: 

220 raise self._batch_error 

221 self._record_consumption() 

222 if self._device.type in ("npu", "cuda"): 

223 prepared = self._next_device_batch() 

224 else: 

225 prepared = self._next_host_batch() 

226 if prepared.batch is None: 

227 self._delivered_state["stopped"] = True 

228 raise StopIteration 

229 batch = prepared.batch 

230 if prepared.ready_event is not None: 

231 module = getattr(torch, self._device.type) 

232 self._consumer_stream = module.current_stream(self._device) 

233 _record_stream(batch, self._consumer_stream) 

234 if prepared.checkpoint is not None: 

235 self._delivered_state = prepared.checkpoint 

236 for message in prepared.balance_logs: 

237 balance_logger.info("%s", message) 

238 self._has_delivered = True 

239 self.prefetch() 

240 return batch 

241 

242 def _record_consumption(self) -> None: 

243 # Advancing the iterator retires the previous batch. Callers must finish 

244 # submitting its uses and release input references before advancing. 

245 if self._consumer_stream is not None: 

246 prefetch_wait_event = getattr(torch, self._device.type).Event() 

247 prefetch_wait_event.record(self._consumer_stream) 

248 self._prefetch_slots.append(prefetch_wait_event) 

249 self._consumer_stream = None 

250 

251 def _next_host_batch(self) -> _PreparedBatch: 

252 """Pop one Host batch and its delivery state, prefetching the following window.""" 

253 if self._current is None or not self._current.batches: 

254 if self._current is not None and self._current.exhausted: 

255 return _PreparedBatch(None) 

256 self._prefetch_host_window() 

257 self._current = self._future.result() 

258 self._future = None 

259 if not self._current.batches: 

260 return _PreparedBatch(None) 

261 self._prefetch_host_window() 

262 batch = self._current.batches.popleft() 

263 logs, self._current.balance_logs = self._current.balance_logs, [] 

264 checkpoint = self._current.checkpoint if not self._current.batches else None 

265 return _PreparedBatch(batch, checkpoint, logs) 

266 

267 def prefetch(self) -> None: 

268 """Prefetch Constructor H2D without any model-peer communication.""" 

269 if self._closed or self._delivered_state["stopped"] or self._batch_error is not None: 

270 return 

271 if (self._device.type not in ("npu", "cuda") or self._device_prefetch_depth == 0 

272 or not self._topology.is_constructor): 

273 self._prefetch_target = 0 

274 self._prefetch_host_window() 

275 return 

276 self._prefetch_device_batches(self._device_prefetch_depth) 

277 

278 def _host_batch_ready(self) -> bool: 

279 """Check Host readiness without waiting for speculative window work.""" 

280 if self._current is not None and (self._current.batches or self._current.exhausted): 

281 return True 

282 self._prefetch_host_window() 

283 return self._future.done() 

284 

285 def _prefetch_device_batches(self, count: int, *, blocking: bool = False) -> None: 

286 """Fill an active Constructor's H2D queue after caller-side lifecycle checks.""" 

287 self._prefetch_target = count 

288 while len(self._device_batches) < count and self._prefetch_slots and not self._device_stopped: 

289 prefetch_wait_event = self._prefetch_slots[0] 

290 if not blocking and ( 

291 not self._host_batch_ready() 

292 or (prefetch_wait_event is not None and not prefetch_wait_event.query()) 

293 ): 

294 return 

295 try: 

296 prepared = self._stage_device_batch(prefetch_wait_event) 

297 except BaseException as exc: # pylint: disable=broad-exception-caught 

298 # Preserve FIFO delivery: a speculative failure must not prevent 

299 # earlier batches from training or enter a peer collective early. 

300 prepared = _PreparedBatch(None, error=exc) 

301 self._device_stopped = True 

302 self._prefetch_slots.popleft() 

303 self._device_batches.append(prepared) 

304 

305 def _prefetch_host_window(self) -> None: 

306 """Submit one Host window for an active loader, preserving double buffering.""" 

307 if self._future is not None: 

308 return 

309 if self._current is not None and self._current.exhausted: 

310 return 

311 self._future = Future() 

312 if self._prefetch_thread is None: 

313 self._prefetch_thread = Thread( 

314 target=self._run_prefetch, name=f"hp-data-prefetch-rank-{self._topology.global_rank}", daemon=True, 

315 ) 

316 self._prefetch_thread.start() 

317 self._requests.put(self._future) 

318 

319 def _stage_device_batch(self, prefetch_wait_event: Any) -> _PreparedBatch: 

320 """Keep copy events local until the corresponding batch is consumed.""" 

321 prepared = self._next_host_batch() 

322 if prefetch_wait_event is not None: 

323 prefetch_wait_event.synchronize() 

324 prepared.batch = self._stage_batch(prepared.batch) 

325 if isinstance(prepared.batch, DevicePrefetchedStep): 

326 prepared.ready_event = prepared.batch.ready_event 

327 self._device_stopped = prepared.batch is None 

328 return prepared 

329 

330 def _next_device_batch(self) -> _PreparedBatch: 

331 """Broadcast on the training stream after waiting for only the current copy.""" 

332 try: 

333 if self._topology.is_constructor: 

334 self._prefetch_device_batches(1, blocking=True) 

335 prepared = self._device_batches.popleft() 

336 else: 

337 prepared = self._next_host_batch() 

338 prefetch_wait_event = self._prefetch_slots.popleft() 

339 if prefetch_wait_event is not None: 

340 prefetch_wait_event.synchronize() 

341 except BaseException as exc: # pylint: disable=broad-exception-caught 

342 # All preparation failures must reach peers through the broadcast error header. 

343 prepared = _PreparedBatch(None, error=exc) 

344 module = getattr(torch, self._device.type) 

345 consumer_stream = module.current_stream(self._device) 

346 try: 

347 batch = prepared.batch 

348 if isinstance(batch, DevicePrefetchedStep): 

349 batch = batch.take_batch() 

350 prepared.batch = self._broadcast_batch(batch) if prepared.error is None else ( 

351 self._model_transport.broadcast(None, error=prepared.error) 

352 ) 

353 except BaseException as exc: 

354 self._batch_error = exc 

355 self._device_batches.clear() 

356 raise 

357 finally: 

358 # Explicit drains must cover partially submitted broadcasts on errors 

359 # without synchronizing the caller's later training work. 

360 prepared.ready_event = module.Event() 

361 prepared.ready_event.record(consumer_stream) 

362 self._last_ready_event = prepared.ready_event 

363 return prepared 

364 

365 def _run_prefetch(self) -> None: 

366 """Serve window requests on a persistent producer and propagate failures via futures.""" 

367 stream = None 

368 try: 

369 while True: 

370 future = self._requests.get() 

371 if future is None: 

372 return 

373 try: 

374 if stream is None and self._device.type in ("npu", "cuda"): 

375 module = getattr(torch, self._device.type) 

376 module.set_device(self._device) 

377 stream = module.Stream(device=self._device) 

378 prefetch_stream_context = ( 

379 getattr(torch, self._device.type).stream(stream) if stream is not None else nullcontext() 

380 ) 

381 with prefetch_stream_context: 

382 window = self._prepare_window() 

383 except BaseException as exc: # pylint: disable=broad-exception-caught 

384 # A worker interrupt must complete its future instead of stranding result(). 

385 future.set_exception(exc) 

386 else: 

387 future.set_result(window) 

388 del window 

389 # An idle producer must not retain a discarded epoch's inputs. 

390 del future 

391 finally: 

392 if stream is not None: 

393 stream.synchronize() 

394 

395 def _prepare_window(self) -> _PreparedWindow: 

396 """Prepare Host batches up to the window, optimizer-step or epoch boundary.""" 

397 window_micro_batch_num = min( 

398 self._balance_window_size, self._accumulation_steps - self._step % self._accumulation_steps, 

399 ) 

400 self._balance_logs = [] 

401 self._prepare_local_metadata(window_micro_batch_num) 

402 batches = deque() 

403 exhausted = False 

404 for _ in range(window_micro_batch_num): 

405 try: 

406 batch = self._prepare_batch() 

407 except StopIteration: 

408 exhausted = True 

409 break 

410 batches.append(batch) 

411 # Intermediate windows must replay from the optimizer step's beginning. 

412 checkpoint = self._snapshot_state() if not exhausted and self._step % self._accumulation_steps == 0 else None 

413 return _PreparedWindow(batches, exhausted, checkpoint, balance_logs=self._balance_logs) 

414 

415 def _prepare_batch(self) -> Any: 

416 """Construct one Host round; accelerator peers receive it at device delivery.""" 

417 if self._stopped: 

418 raise StopIteration 

419 batch = self._prepare_next_batch() 

420 if batch is not None and self._device_prefetch is not None: 

421 batch = self._device_prefetch.prepare_host(batch) 

422 if self._device.type == "cpu": 

423 batch = self._broadcast_batch(self._stage_batch(batch)) 

424 self._stopped = batch is None 

425 if self._stopped: 

426 raise StopIteration 

427 return self._consume_batch(batch) 

428 

429 def _stage_batch(self, batch: Any) -> Any: 

430 if batch is not None and self._device_prefetch is not None: 

431 return self._device_prefetch.stage_prepared(batch) 

432 return batch 

433 

434 def _prepare_next_batch(self) -> Any: 

435 """Select, balance, route, and construct samples without model-group broadcast.""" 

436 if self._data_plane.is_member: 

437 return self._produce_on_data_plane() 

438 return None 

439 

440 def _broadcast_batch(self, constructed_batch: Any) -> Any: 

441 """Broadcast one batch on the caller stream, or on the Host producer for CPU.""" 

442 return self._model_transport.broadcast(constructed_batch) 

443 

444 def _consume_batch(self, received: Any) -> Any: 

445 """Commit Host preparation; delivery publishes checkpoints only after broadcast.""" 

446 # Non-Constructors hold placeholders; the caller receives both 

447 # real batches and the EOF sentinel from their Constructor's broadcast. 

448 planning_reader = self._planning_reader() 

449 if planning_reader is not None: 

450 planning_reader.commit(self._pending_local_keys) 

451 self._pending_local_keys.clear() 

452 self._step += 1 

453 return received 

454 

455 def _fill_requested_device_prefetch(self) -> None: 

456 """Complete requested Constructor lookahead before waiting or closing.""" 

457 # Only accelerator Constructor staging sets a positive target. 

458 if (not self._prefetch_target or self._closed or self._delivered_state["stopped"] 

459 or self._batch_error is not None): 

460 return 

461 self._prefetch_device_batches(self._prefetch_target, blocking=True) 

462 

463 def wait_for_prefetch(self) -> None: 

464 """Drain buffered window communication before checkpoint teardown or shutdown.""" 

465 self._fill_requested_device_prefetch() 

466 if self._last_ready_event is not None: 

467 self._last_ready_event.synchronize() 

468 ready_event = None 

469 for prepared in self._device_batches: 

470 if prepared.ready_event is not None: 

471 ready_event = prepared.ready_event 

472 # Queued events belong to the copy stream, independently of the last 

473 # delivered batch's training-stream marker. Neither drain broadcasts ahead. 

474 if ready_event is not None: 

475 ready_event.synchronize() 

476 if self._future is not None: 

477 self._future.result() 

478 

479 def close(self) -> None: 

480 """Drain the producer before releasing buffered inputs and reader resources.""" 

481 if self._closed: 

482 return 

483 # Constructors may have skipped optional staging at different times. 

484 # Complete the requested lookahead before any producer leaves its collectives; 

485 # discarded input errors still belong to next()/wait_for_prefetch(), not close(). 

486 self._fill_requested_device_prefetch() 

487 if self._last_ready_event is not None: 

488 self._last_ready_event.synchronize() 

489 if self._prefetch_thread is not None: 

490 if self._prefetch_thread.is_alive(): 

491 self._requests.put(None) 

492 self._prefetch_thread.join() 

493 self._prefetch_thread = None 

494 try: 

495 if self._device_prefetch is not None: 

496 self._device_prefetch.close() 

497 finally: 

498 reader = self._planning_reader() 

499 if reader is not None: 

500 reader.close() 

501 self._current = None 

502 self._future = None 

503 self._device_batches.clear() 

504 self._consumer_stream = None 

505 self._prefetch_slots.clear() 

506 self._batch_error = None 

507 self._last_ready_event = None 

508 self._balance_logs = [] 

509 self._closed = True 

510 

511 def __enter__(self) -> "DistributedDataLoader": 

512 """Support deterministic cleanup on early loop exit.""" 

513 return self 

514 

515 def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None: 

516 """Release prefetch resources before process-group teardown.""" 

517 self.close() 

518 

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

520 """Snapshot the last fully delivered optimizer step without waiting. 

521 

522 Save training checkpoints only after the corresponding optimizer update. 

523 An incomplete step leaves this cursor at that step's beginning. 

524 """ 

525 state = self._delivered_state 

526 if "_index_sampler_state" not in state: 

527 # Old non-Reader checkpoints may lack an absolute source cursor. 

528 # Keep their same-mode progress without inventing a portable position. 

529 return copy.deepcopy(dict(state, global_rank=self._topology.global_rank)) 

530 return copy.deepcopy({ 

531 "_index_sampler_state": state["_index_sampler_state"], 

532 "_iterator_finished": state["stopped"], 

533 "dataset_state": state["dataset_state"], 

534 }) 

535 

536 def state_dict_for_load(self) -> dict[str, Any]: 

537 """Request the shared cursor and legacy Reader aliases without defaulting to zero.""" 

538 return sampler_state_for_load(self.state_dict()) 

539 

540 def _snapshot_state(self) -> dict[str, Any]: 

541 """Capture producer progress at an optimizer-step boundary.""" 

542 reader = self._planning_reader() 

543 reader_state = reader.state_dict() if reader is not None else None 

544 payload_state = ( 

545 self._direct_sample_loader.state_dict() if self._direct_sample_loader is not None 

546 else (reader_state or {}).get("sample_loader") 

547 ) 

548 state = { 

549 "epoch": self._epoch, 

550 "step": self._step, 

551 "stopped": self._stopped, 

552 "dataset_state": ( 

553 {key: payload_state[key] for key in ("dataset_size", "seed")} if payload_state is not None else None 

554 ), 

555 } 

556 if reader_state is not None: 

557 state["_index_sampler_state"] = reader_state["sampler"] 

558 elif self._initial_consumed_samples is not None: 

559 # Model peers have no Reader; advance their cursor from the known initial position. 

560 state["_index_sampler_state"] = { 

561 "consumed_samples": self._initial_consumed_samples + ( 

562 self._step * self._planner.local_batch_size * self._topology.data_parallel_size 

563 ), 

564 "epoch": self._epoch, 

565 "global_batch_size": ( 

566 self._accumulation_steps * self._planner.local_batch_size * self._topology.data_parallel_size 

567 ), 

568 "dp_world_size": self._topology.data_parallel_size, 

569 } 

570 sampler = state["_index_sampler_state"] 

571 offset = self._initial_consumed_samples % sampler["global_batch_size"] 

572 if offset: 

573 sampler["optimizer_step_offset"] = offset 

574 try: 

575 copied_state = copy.deepcopy(state) 

576 except Exception as exc: 

577 raise ValueError(f"Distributed DataLoader state is not checkpointable: {exc}") from exc 

578 return copied_state 

579 

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

581 """Restore a shared native cursor or a legacy rank-local checkpoint. 

582 

583 Args: 

584 state_dict: A complete-step cursor; legacy rank-local state must match this rank. 

585 """ 

586 self._validate_restore_boundary() 

587 try: 

588 state = copy.deepcopy(dict(state_dict)) 

589 except Exception as exc: 

590 raise ValueError(f"Distributed DataLoader state is not copyable: {exc}") from exc 

591 if state.get("global_rank") not in (None, self._topology.global_rank): 

592 raise ValueError( 

593 f"Distributed DataLoader checkpoint global_rank={state.get('global_rank')!r} " 

594 f"does not match {self._topology.global_rank!r}." 

595 ) 

596 epoch, step, stopped = self._validate_checkpoint_values(state) 

597 sampler = get_sampler_checkpoint_state(state) 

598 # Legacy step counters use the source accumulation size. The sampler 

599 # cursor validates complete steps independently of the target GBS/DP. 

600 layout_changed = sampler is not None and ( 

601 sampler["global_batch_size"] != ( 

602 self._accumulation_steps * self._planner.local_batch_size * self._topology.data_parallel_size 

603 ) or sampler["dp_world_size"] != self._topology.data_parallel_size 

604 ) 

605 if ( 

606 (not layout_changed and step % self._accumulation_steps) or state.get("window_plans") 

607 or state.get("optimizer_step_remaining", self._accumulation_steps) != self._accumulation_steps 

608 or any(isinstance(state.get(key), Mapping) and "window" in state[key] 

609 for key in ("dataset_reader", "metadata_reader", "sidecar_reader")) 

610 ): 

611 raise ValueError("Distributed DataLoader checkpoint must align to a complete optimizer step.") 

612 if sampler is not None: 

613 epoch, step = self._progress_from_sampler(sampler) 

614 elif self._dataset_reader is not None or self._metadata_reader is not None or state.get("global_rank") is None: 

615 raise ValueError("Distributed DataLoader checkpoint has no native sampler cursor.") 

616 self._restore_component_states(state, sampler, epoch) 

617 self._epoch = epoch 

618 self._step = step 

619 self._stopped = stopped 

620 self._initial_consumed_samples = None 

621 if sampler is not None: 

622 self._initial_consumed_samples = sampler["consumed_samples"] - ( 

623 step * self._planner.local_batch_size * self._topology.data_parallel_size 

624 ) 

625 

626 self._delivered_state = self._snapshot_state() 

627 

628 def _progress_from_sampler(self, sampler: Mapping[str, Any]) -> tuple[int, int]: 

629 """Validate the shared cursor and derive the epoch and micro-batch position.""" 

630 global_micro_batch_size = self._planner.local_batch_size * self._topology.data_parallel_size 

631 global_batch_size = self._accumulation_steps * global_micro_batch_size 

632 consumed, epoch = sampler["consumed_samples"], sampler["epoch"] 

633 if any(isinstance(value, bool) or not isinstance(value, int) for value in (consumed, epoch)): 

634 raise ValueError("Sampler consumed_samples and epoch must be integers") 

635 if sampler_optimizer_step_progress(sampler) or epoch < 0: 

636 raise ValueError("Sampler cursor must align to a complete optimizer step") 

637 if sampler["global_batch_size"] != global_batch_size: 

638 return epoch, 0 

639 offset = sampler.get("optimizer_step_offset") or 0 

640 return epoch, (consumed - offset) // global_micro_batch_size 

641 

642 def _validate_restore_boundary(self) -> None: 

643 if self._last_ready_event is not None or self._prefetch_thread is not None or self._step != 0: 

644 raise ValueError("load_state_dict must run before distributed iteration starts.") 

645 

646 @staticmethod 

647 def _validate_checkpoint_values(state: Mapping[str, Any]) -> tuple[int, int, bool]: 

648 """Validate and return the saved epoch, micro-batch index and stopped flag.""" 

649 epoch = state.get("epoch", 0) 

650 step = state.get("step", 0) 

651 stopped = state.get("_iterator_finished") 

652 if stopped is None: 

653 stopped = state.get("stopped", False) 

654 if not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 0: 

655 raise ValueError("Distributed DataLoader checkpoint epoch must be non-negative.") 

656 if not isinstance(step, int) or isinstance(step, bool) or step < 0: 

657 raise ValueError("Distributed DataLoader checkpoint step must be non-negative.") 

658 if not isinstance(stopped, bool): 

659 raise ValueError("Distributed DataLoader checkpoint stopped must be boolean.") 

660 return epoch, step, stopped 

661 

662 def _restore_component_states( 

663 self, 

664 state: Mapping[str, Any], 

665 sampler_state: Mapping[str, Any] | None, 

666 epoch: int, 

667 ) -> None: 

668 """Rebuild current Readers from shared progress and optional Dataset checks.""" 

669 reader = self._planning_reader() 

670 reader_state = reader.state_dict() if reader is not None else None 

671 payload_state = ( 

672 self._direct_sample_loader.state_dict() if self._direct_sample_loader is not None 

673 else (reader_state or {}).get("sample_loader") 

674 ) 

675 # Native torchdata Dataset state belongs to the user's Dataset. Only 

676 # cursor-only and legacy balanced states carry our Dataset compatibility fields. 

677 if not has_torchdata_state(state): 

678 candidates = [state.get("dataset_state"), state.get("direct_sample_loader")] 

679 for key in ("dataset_reader", "metadata_reader", "sidecar_reader"): 

680 legacy_reader = state.get(key) 

681 if isinstance(legacy_reader, Mapping): 

682 candidates.append(legacy_reader.get("sample_loader")) 

683 for saved in candidates: 

684 if saved is None or payload_state is None: 

685 continue 

686 if not isinstance(saved, Mapping): 

687 raise ValueError("Dataset checkpoint state must be a mapping.") 

688 for key in ("dataset_size", "seed"): 

689 if saved.get(key) is not None and saved[key] != payload_state[key]: 

690 raise ValueError(f"Dataset checkpoint {key} does not match the current loader.") 

691 if payload_state is not None: 

692 payload_state["epoch"] = epoch 

693 if reader is not None: 

694 reader_state.update(epoch=epoch, sampler=sampler_state) 

695 if reader_state["sample_loader"] is not None: 

696 reader_state["sample_loader"] = payload_state 

697 reader.load_state_dict(reader_state) 

698 if self._direct_sample_loader is not None: 

699 self._direct_sample_loader.load_state_dict(payload_state) 

700 

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

702 """Reset the native source while retaining the Host thread and device streams. 

703 

704 Args: 

705 epoch: Non-negative Dataset epoch. 

706 """ 

707 if isinstance(epoch, bool) or not isinstance(epoch, int) or epoch < 0: 

708 raise ValueError("epoch must be a non-negative integer.") 

709 if self._closed: 

710 raise ValueError("set_epoch cannot restart a closed loader.") 

711 if (self._has_delivered or self._delivered_state["step"] != 0) and not self._delivered_state["stopped"]: 

712 raise ValueError("set_epoch requires a fresh or exhausted optimizer-step loader.") 

713 if epoch == self._delivered_state["epoch"] and not self._delivered_state["stopped"]: 

714 return 

715 self.wait_for_prefetch() 

716 if self._device_prefetch is not None: 

717 self._device_prefetch.close() 

718 if self._dataset_reader is not None: 

719 self._dataset_reader.set_epoch(epoch) 

720 if self._metadata_reader is not None: 

721 self._metadata_reader.set_epoch(epoch) 

722 if self._direct_sample_loader is not None: 

723 self._direct_sample_loader.set_epoch(epoch) 

724 self._epoch = epoch 

725 self._step = 0 

726 self._initial_consumed_samples = 0 

727 self._stopped = False 

728 self._pending_local_keys.clear() 

729 self._window_plans.clear() 

730 self._current = None 

731 self._future = None 

732 self._device_batches.clear() 

733 self._consumer_stream = None 

734 self._prefetch_slots = deque([None] * (self._device_prefetch_depth + 1)) 

735 self._device_stopped = False 

736 self._prefetch_target = 0 

737 self._batch_error = None 

738 self._last_ready_event = None 

739 self._has_delivered = False 

740 self._balance_logs = [] 

741 self._delivered_state = self._snapshot_state() 

742 

743 def _produce_on_data_plane(self) -> Any: 

744 """Execute one plan through direct reads or payload exchange, then construct the batch.""" 

745 plan = self._next_plan_control() 

746 if plan is None: 

747 self._stopped = True 

748 return None 

749 

750 self._pending_local_keys = { 

751 key for key in plan.selected_keys if key.reader_rank == self._topology.global_rank 

752 } 

753 if self._metadata_mode: 

754 if not self._topology.is_constructor: 

755 return None 

756 received_payloads = self._direct_sample_loader.fetch_samples( 

757 plan.local_sample_keys(self._topology.data_rank) 

758 ) 

759 else: 

760 prepared_exchange = self._prepare_outgoing(plan, self._pending_local_keys) 

761 received_payloads = self._data_plane.exchange_prepared(prepared_exchange) 

762 # Reader-only ranks must finish routing before leaving this round. 

763 if not self._topology.is_constructor: 

764 return None 

765 return self._construct_received_payloads(plan, received_payloads) 

766 

767 def _construct_received_payloads( 

768 self, 

769 plan: DistributedPackingPlan, 

770 received_payloads: dict[SampleKey, Any], 

771 ) -> Any: 

772 """Construct one local batch; the caller must be a Data Constructor.""" 

773 local_batch_plan = plan.local_batch_for(self._topology.data_rank) 

774 local_batch = self._data_constructor.construct(local_batch_plan, received_payloads) 

775 # None is reserved for distributed end-of-stream. 

776 if local_batch is None: 

777 raise ValueError("The Data Constructor must return a non-None batch.") 

778 return local_batch 

779 

780 def _next_plan_control(self) -> DistributedPackingPlan | None: 

781 """Pop one micro-batch plan, planning a new window only when the cache is empty.""" 

782 if self._window_plans: 

783 return self._window_plans.popleft() 

784 planning_reader = self._planning_reader() 

785 local_input = planning_reader.planning_input() if planning_reader is not None else _ReaderPlanInput() 

786 if self._broadcast_plan: 

787 gathered_inputs = self._data_plane.gather_object_to_planner(local_input) 

788 plan = None 

789 if self._topology.global_rank == self._data_plane.planner_rank: 

790 plan = self._build_plan_control(gathered_inputs) 

791 plan = self._data_plane.broadcast_from_planner(plan) 

792 else: 

793 plan = self._build_plan_control(self._data_plane.all_gather_object(local_input)) 

794 if isinstance(plan, tuple): 

795 self._window_plans.extend(plan) 

796 return self._window_plans.popleft() 

797 return plan 

798 

799 def _prepare_local_metadata(self, window_micro_batch_num: int) -> None: 

800 """Read one explicitly sized source window before constructing its batches.""" 

801 if self._topology.global_rank not in self._dataset_reader_ranks: 

802 return 

803 planning_reader = self._planning_reader() 

804 planning_reader.prepare_next_step(window_micro_batch_num) 

805 

806 def _build_plan_control( 

807 self, gathered_inputs: tuple[_ReaderPlanInput, ...], 

808 ) -> _PlanControl: 

809 """Return the native window's plans; None means end of stream.""" 

810 # Collective rank order can differ from the mesh's logical DP order. 

811 inputs_by_rank = dict(zip(self._data_plane.ranks, gathered_inputs)) 

812 reader_inputs = [ 

813 inputs_by_rank[rank] for rank in self._topology.constructor_ranks 

814 if rank in self._dataset_reader_ranks 

815 ] 

816 return self._plan_native_batch(reader_inputs) 

817 

818 def _native_window_micro_batch_num(self, reader_inputs: list[_ReaderPlanInput]) -> int: 

819 """Validate complete synchronized source rounds before constructing reference bins.""" 

820 if all(not item.original_batches for item in reader_inputs): 

821 return 0 

822 if any(not item.original_batches for item in reader_inputs): 

823 raise ValueError("Native BatchSampler ranks exhausted at different forward/backward rounds.") 

824 counts = {len(item.original_batches) for item in reader_inputs} 

825 if len(counts) != 1 or any( 

826 len(batch) != self._planner.local_batch_size 

827 for item in reader_inputs for batch in item.original_batches 

828 ): 

829 raise ValueError("Native BatchSampler ranks must provide equal complete micro-batch windows.") 

830 return next(iter(counts)) 

831 

832 def _plan_native_batch( 

833 self, reader_inputs: list[_ReaderPlanInput], 

834 ) -> _PlanControl: 

835 """Plan the native sampler window, returning one plan per training round.""" 

836 window_micro_batch_num = self._native_window_micro_batch_num(reader_inputs) 

837 if not window_micro_batch_num: 

838 return None 

839 reference_bins = tuple( 

840 keys for batches in zip(*(item.original_batches for item in reader_inputs)) 

841 for batch in batches for keys in batch 

842 ) 

843 metadata_by_key = {metadata.key: metadata for item in reader_inputs for metadata in item.metadata} 

844 samples = tuple(metadata_by_key[key] for keys in reference_bins for key in keys) 

845 plan = self._planner.plan if window_micro_batch_num == 1 else self._planner.plan_window 

846 plans = plan(samples, reference_bins=reference_bins, step=self._step) 

847 self._capture_balance_log(samples, reference_bins, plans) 

848 return plans 

849 

850 def _capture_balance_log( 

851 self, samples: tuple[SampleMetadata, ...], reference_bins: tuple[tuple[SampleKey, ...], ...], 

852 plans: DistributedPackingPlan | tuple[DistributedPackingPlan, ...], 

853 ) -> None: 

854 """Freeze scalar window statistics before the producer advances to another plan.""" 

855 if ( 

856 not self._log_balance 

857 or self._topology.global_rank != self._data_plane.planner_rank 

858 or not balance_logger.isEnabledFor(logging.INFO) 

859 ): 

860 return 

861 plans = plans if isinstance(plans, tuple) else (plans,) 

862 metadata = {item.key: item for item in samples} 

863 costs = self._planner.last_sample_costs 

864 

865 def describe(keys: tuple[SampleKey, ...]) -> dict[str, Any]: 

866 """Combine evaluated document costs with this layout's constructor padding. 

867 

868 Args: 

869 keys: Sample occurrences assigned to one pack. 

870 """ 

871 tokens = sum(metadata[key].pack_tokens for key in keys) 

872 padding = max(0, self._planner.seq_len - tokens) 

873 return { 

874 "seq_len": tokens, "samples": len(keys), "padding": padding, 

875 "cost": math.fsum(costs[key].llm for key in keys) + self._planner.padding_cost(padding).llm, 

876 } 

877 

878 local_size = self._planner.local_batch_size 

879 bin_count = self._planner.distributed_bin_count 

880 rounds = [] 

881 for offset, plan in enumerate(plans): 

882 original = reference_bins[offset * bin_count:(offset + 1) * bin_count] 

883 rounds.append({ 

884 "micro_batch": plan.step % self._accumulation_steps + 1, 

885 "bins_before": tuple( 

886 tuple(describe(keys) for keys in original[start:start + local_size]) 

887 for start in range(0, bin_count, local_size) 

888 ), 

889 "bins_after": tuple( 

890 tuple(describe(packing_bin.sample_keys) for packing_bin in local_batch) 

891 for local_batch in plan.local_batches 

892 ), 

893 }) 

894 stats = { 

895 **self._planner.last_balance_decision, 

896 "objective": self._planner.objective, "epoch": self._epoch, 

897 "planner_rank": self._data_plane.planner_rank, 

898 "constructor_ranks": self._topology.constructor_ranks, 

899 "group_ranks": tuple(range(self._topology.data_parallel_size)), "rounds": rounds, 

900 } 

901 self._balance_logs.append(format_balance_stats(stats, self._step // self._accumulation_steps + 1)) 

902 

903 def _prepare_outgoing( 

904 self, 

905 plan: DistributedPackingPlan, 

906 local_selected_keys: set[SampleKey], 

907 ) -> PreparedPayloadExchange: 

908 """Group source-owned payloads by destination Constructor for transport encoding.""" 

909 payloads_by_target_rank: dict[int, list[tuple[SampleKey, Any]]] = {} 

910 if local_selected_keys: 

911 payloads = self._dataset_reader.selected_payloads(local_selected_keys) 

912 target_by_key = { 

913 key: self._topology.constructor_ranks[data_rank] 

914 for data_rank, local_batch in enumerate(plan.local_batches) 

915 for packing_bin in local_batch 

916 for key in packing_bin.sample_keys 

917 } 

918 for key, payload in payloads: 

919 payloads_by_target_rank.setdefault(target_by_key[key], []).append((key, payload)) 

920 return self._data_plane.prepare_exchange(payloads_by_target_rank) 

921 

922 def _planning_reader(self) -> Any | None: 

923 """Return this rank's online or metadata-only Dataset Reader.""" 

924 if self._dataset_reader is not None: 

925 return self._dataset_reader 

926 return self._metadata_reader 

927 

928 

929 

930__all__ = ["DistributedDataLoader"]