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

348 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"""Rebalance raw samples from already-selected rank-local packing batches.""" 

16 

17from __future__ import annotations 

18 

19import math 

20from collections.abc import Callable, Iterable, Iterator, Sequence 

21from contextlib import nullcontext 

22from dataclasses import dataclass, replace 

23from threading import Thread 

24from typing import TYPE_CHECKING, Any 

25 

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

27 

28from hyper_parallel.distributed_data.balance_logging import log_balance_stats 

29from hyper_parallel.distributed_data.balancing_algorithm import BalancingAlgorithm, resolve_balancing_algorithm 

30from hyper_parallel.distributed_data.cost_model import CostModel, resolve_cost_model 

31from hyper_parallel.distributed_data.device_prefetch import ( 

32 DeviceStepPrefetcher, 

33 _create_device_prefetcher, 

34 _resolve_device, 

35) 

36from hyper_parallel.distributed_data.locality import _create_locality_groups 

37from hyper_parallel.distributed_data.planner import DynamicPackingPlanner 

38from hyper_parallel.distributed_data.schema import ( 

39 DistributedPackingPlan, 

40 PackingConstraints, 

41 SampleKey, 

42 SampleMetadata, 

43) 

44from hyper_parallel.distributed_data.transport import ControlGatherWork, DataPlaneTransport, PayloadExchangeWork 

45 

46if TYPE_CHECKING: 

47 from hyper_parallel.distributed_data.api import DistributedDatasetConfig 

48 

49 

50@dataclass(frozen=True) 

51class _LocalBatch: 

52 data: Any 

53 stats: dict[str, Any] | None = None 

54 host_batch: Any = None 

55 

56 

57@dataclass(frozen=True) 

58class _PreparedSourceStep: 

59 """CPU-only source data prepared before the foreground collective phase.""" 

60 

61 metadata: tuple[Any, ...] 

62 local_payloads: dict[SampleKey, Any] 

63 

64 

65@dataclass(frozen=True) 

66class _PendingBatch: 

67 """A planned step whose payload exchange may still be in flight.""" 

68 

69 plan: DistributedPackingPlan 

70 stats: dict[str, Any] 

71 retained_payloads: dict[SampleKey, Any] 

72 payload_work: PayloadExchangeWork | None 

73 

74 

75class _LocalBalancingIterator(Iterator[Any]): 

76 """Keep one step ahead with caller-owned accelerator collective launches.""" 

77 

78 # This companion iterator implements the loader pipeline; these hooks are not public APIs. 

79 # pylint: disable=protected-access 

80 

81 def __init__(self, loader: LocalBalancingDataLoader) -> None: 

82 """Start source workers before entering the background pipeline.""" 

83 self._source = iter(loader.local_dataloader) 

84 self._loader = loader 

85 self._step = 0 

86 self.finished = False 

87 self._thread: Thread | None = None 

88 self._result: _LocalBatch | None = None 

89 self._prepared_source_step: _PreparedSourceStep | None = None 

90 self._planned: Any = None 

91 self._phase = "source" 

92 self._error: BaseException | None = None 

93 

94 def __next__(self) -> Any: 

95 """Consume a prepared step, completing omitted prefetch hooks inline.""" 

96 if self.finished: 

97 raise StopIteration 

98 try: 

99 if self._loader._uses_synchronous_collectives: 

100 self.prefetch() 

101 if self.finished: 

102 raise StopIteration 

103 elif self._thread is None: 

104 self._start_prefetch() 

105 self._join_worker() 

106 result = self._result 

107 self._result = None 

108 if result is None: 

109 raise RuntimeError("Local balancing prefetch completed without a batch.") 

110 except BaseException: 

111 self.finished = True 

112 raise 

113 self._step += 1 

114 self._loader.last_balance_stats = result.stats 

115 self._start_prefetch() 

116 return self._loader._deliver_batch(result, self._step) 

117 

118 def prefetch_plan(self) -> None: 

119 """Launch metadata gathering and finish decoding and planning in the worker. 

120 

121 All ranks call at the same training boundary, after enqueueing compute 

122 and before host synchronization. The variable-length size exchange 

123 remains synchronous; no device-wide wait is introduced. 

124 """ 

125 if not self._loader._uses_synchronous_collectives or self.finished: 

126 return 

127 if self._phase != "source": 

128 return 

129 if self._thread is None: 

130 self._start_prefetch() 

131 try: 

132 self._join_worker() 

133 except StopIteration: 

134 self.finished = True 

135 return 

136 with self._loader._data_stream_context(): 

137 work = self._loader._transport.begin_all_gather_object(self._prepared_source_step.metadata) 

138 self._phase = "plan" 

139 self._start_worker(self._prepare_plan, work) 

140 

141 def prefetch(self) -> None: 

142 """Launch next-step payload exchange on the caller, then finish off-thread. 

143 

144 All ranks must call at the same training boundary. Repeated calls 

145 before consumption are harmless; they do not advance source progress. 

146 """ 

147 self.prefetch_plan() 

148 if not self._loader._uses_synchronous_collectives or self.finished: 

149 return 

150 if self._phase != "plan": 

151 return 

152 self._join_worker() 

153 with self._loader._data_stream_context(): 

154 planned = self._planned 

155 if self._loader.config.broadcast_plan: 

156 planned = self._loader._transport.broadcast_from_planner(planned) 

157 plan, stats = planned 

158 pending = self._loader._begin_batch(self._prepared_source_step.local_payloads, plan, stats) 

159 self._prepared_source_step = None 

160 self._planned = None 

161 self._phase = "batch" 

162 self._start_worker(self._finish_batch, pending) 

163 

164 def _prepare_plan(self, work: ControlGatherWork) -> None: 

165 with self._loader._data_stream_context(): 

166 self._planned = self._loader._make_plan(work.wait(), self._step) 

167 

168 def _finish_batch(self, pending: _PendingBatch) -> None: 

169 with self._loader._data_stream_context(): 

170 self._result = self._loader._finish_batch(pending) 

171 

172 def _start_prefetch(self) -> None: 

173 self._phase = "source" 

174 self._result = None 

175 self._prepared_source_step = None 

176 self._start_worker(self._run_prefetch) 

177 

178 def _start_worker(self, callback: Callable[..., None], *args: Any) -> None: 

179 self._error = None 

180 self._thread = Thread( 

181 target=self._run_worker, args=(callback, args), 

182 name="hp-local-balance-prefetch", daemon=True, 

183 ) 

184 self._thread.start() 

185 

186 def _run_worker(self, callback: Callable[..., None], args: tuple[Any, ...]) -> None: 

187 try: 

188 callback(*args) 

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

190 # Relay every worker failure, including interrupts, to the consumer. 

191 self._error = exc 

192 

193 def _run_prefetch(self) -> None: 

194 if self._loader._uses_synchronous_collectives: 

195 raw_bins = next(self._source) 

196 metadata, payloads = self._loader._read_step(raw_bins, self._step) 

197 self._prepared_source_step = _PreparedSourceStep(metadata, payloads) 

198 else: 

199 self._result = self._loader._construct_batch(next(self._source), self._step) 

200 

201 def _join_worker(self) -> None: 

202 self.wait_for_prefetch() 

203 self._thread = None 

204 if self._error is not None: 

205 raise self._error 

206 

207 def wait_for_prefetch(self) -> None: 

208 """Drain the active background phase without consuming its result.""" 

209 if self._thread is not None: 

210 self._thread.join() 

211 

212 def close(self) -> None: 

213 """Drain the producer before resetting the source or its epoch.""" 

214 self.wait_for_prefetch() 

215 self.finished = True 

216 self._thread = None 

217 self._result = None 

218 self._prepared_source_step = None 

219 self._planned = None 

220 self._error = None 

221 

222 

223class LocalBalancingDataLoader: 

224 """Balance a frozen local-packer step using only one locality domain. 

225 

226 Raw samples, not collated tensors or entire indivisible packs, are routed. 

227 The source controls sampling/filtering/packing and microbatch membership; 

228 this wrapper never reads future samples to improve a current step's score. 

229 All ranks must yield the same number of steps. Metadata and planner output 

230 are trusted; local failures propagate without a collective error handshake. 

231 """ 

232 

233 def __init__( 

234 self, 

235 local_dataloader: Iterable[Sequence[Sequence[Any]]], 

236 *, 

237 config: DistributedDatasetConfig, 

238 metadata_fn: Callable[[Any], SampleMetadata], 

239 pack_fn: Callable[[Sequence[Any], int], Any], 

240 collate_fn: Callable[[Sequence[Any]], Any], 

241 planner: Any, 

242 transport: DataPlaneTransport, 

243 global_rank: int, 

244 device_prefetch: DeviceStepPrefetcher | None = None, 

245 balance_stats_callback: Callable[[dict[str, Any], int], None] | None = None, 

246 ) -> None: 

247 """Store the source, planner, and locality-scoped data plane.""" 

248 self.local_dataloader = local_dataloader 

249 self.config = config 

250 self.group_ranks = transport.ranks 

251 self.prefetches_to_device = device_prefetch is not None 

252 self.last_balance_stats: dict[str, Any] | None = None 

253 self.last_host_batch: Any = None 

254 self._metadata_fn = metadata_fn 

255 self._pack_fn = pack_fn 

256 self._collate_fn = collate_fn 

257 self._planner = planner 

258 self._transport = transport 

259 self._global_rank = global_rank 

260 self._data_rank = self.group_ranks.index(global_rank) 

261 self._iterator: _LocalBalancingIterator | None = None 

262 self._device_prefetch = device_prefetch 

263 self._data_stream = None 

264 self._balance_stats_callback = balance_stats_callback 

265 

266 @property 

267 def _uses_synchronous_collectives(self) -> bool: 

268 """Whether data collectives must stay on the training thread.""" 

269 backend = getattr(self._transport, "communication_backend", "gloo").lower() 

270 return "hccl" in backend or "nccl" in backend 

271 

272 def _data_stream_context(self) -> Any: 

273 """Reuse the data communication stream independently of the training stream.""" 

274 device = getattr(self._transport, "communication_device", None) 

275 if device is None: 

276 return nullcontext() 

277 device = torch.device(device) 

278 if device.type == "cpu": 

279 return nullcontext() 

280 accelerator = getattr(torch, device.type) 

281 accelerator.set_device(device) 

282 if self._data_stream is None: 

283 self._data_stream = accelerator.Stream(device=device) 

284 # Both launch and completion use this stream; neither waits on the 

285 # training stream just to move independent next-step data. 

286 return accelerator.stream(self._data_stream) 

287 

288 def prefetch_plan(self) -> None: 

289 """Gather metadata and start planning at a rank-consistent compute boundary.""" 

290 if self._iterator is None: 

291 self._iterator = _LocalBalancingIterator(self) 

292 self._iterator.prefetch_plan() 

293 

294 def prefetch(self) -> None: 

295 """Launch next-step exchange before synchronizing current-step computation.""" 

296 if self._iterator is None: 

297 self._iterator = _LocalBalancingIterator(self) 

298 self._iterator.prefetch() 

299 

300 def __len__(self) -> int: 

301 """Return the source length when available.""" 

302 return len(self.local_dataloader) 

303 

304 @property 

305 def step(self) -> int: 

306 """Return the number of delivered steps in the current iterator.""" 

307 # The owned iterator holds the delivered cursor, not speculative source progress. 

308 return self._iterator._step if self._iterator is not None else 0 # pylint: disable=protected-access 

309 

310 def __next__(self) -> Any: 

311 """Deliver a step without requiring a client-side iterator wrapper.""" 

312 if self._iterator is None: 

313 self._iterator = _LocalBalancingIterator(self) 

314 return next(self._iterator) 

315 

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

317 """Forward epoch selection to the local source before starting iteration. 

318 

319 Args: 

320 epoch: Source epoch to start. 

321 """ 

322 if self._iterator is not None: 

323 self._iterator.close() 

324 self._iterator = None 

325 if self._device_prefetch is not None: 

326 self._device_prefetch.close() 

327 self.last_host_batch = None 

328 setter = getattr(self.local_dataloader, "set_epoch", None) 

329 if callable(setter): 

330 setter(epoch) 

331 

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

333 """Reject checkpointing until rank-local pending packs can be restored.""" 

334 raise NotImplementedError("Local balancing does not yet support dataloader checkpoint/resume.") 

335 

336 def load_state_dict(self, state: dict[str, Any]) -> None: 

337 """Reject restoring incomplete locality/source state. 

338 

339 Args: 

340 state: Unsupported loader checkpoint. 

341 """ 

342 del state 

343 raise NotImplementedError("Local balancing does not yet support dataloader checkpoint/resume.") 

344 

345 def __iter__(self) -> Iterator[Any]: 

346 """Yield constructed local microbatches after scoped metadata and sample exchange.""" 

347 # Re-entering an unfinished iterator must not drop its speculative batch 

348 # after the stateful source has already advanced to the following step. 

349 if self._iterator is None or self._iterator.finished: 

350 if self._iterator is not None: 

351 self._iterator.close() 

352 self._iterator = _LocalBalancingIterator(self) 

353 return self._iterator 

354 

355 def wait_for_prefetch(self) -> None: 

356 """Drain data communication before teardown, preserving the next batch. 

357 

358 All ranks must call this at the same consumed-step boundary. 

359 """ 

360 if self._iterator is not None: 

361 self._iterator.wait_for_prefetch() 

362 if self._device_prefetch is not None: 

363 self._device_prefetch.close() 

364 

365 def close(self) -> None: 

366 """Drain the producer and release unconsumed Host/device views.""" 

367 if self._iterator is not None: 

368 self._iterator.close() 

369 if self._device_prefetch is not None: 

370 self._device_prefetch.close() 

371 self.last_host_batch = None 

372 

373 def _deliver_batch(self, result: _LocalBatch, step: int) -> Any: 

374 batch = result.data 

375 self.last_host_batch = result.host_batch 

376 if self.prefetches_to_device: 

377 batch = batch.take_batch() 

378 if result.stats is not None and self._balance_stats_callback is not None and self._global_rank == 0: 

379 self._balance_stats_callback(result.stats, step) 

380 return batch 

381 

382 def _construct_batch( 

383 self, 

384 raw_bins: Sequence[Sequence[Any]], 

385 step: int, 

386 ) -> _LocalBatch: 

387 metadata, local_payloads = self._read_step(raw_bins, step) 

388 gathered = self._transport.all_gather_object(metadata) 

389 plan, stats = self._plan_step(gathered, step) 

390 return self._finish_batch(self._begin_batch(local_payloads, plan, stats)) 

391 

392 def _begin_batch( 

393 self, local_payloads: dict[SampleKey, Any], plan: DistributedPackingPlan, stats: dict[str, Any], 

394 ) -> _PendingBatch: 

395 """Start payload exchange for moved samples while retaining local destinations.""" 

396 if stats["moved_samples"]: 

397 outgoing: dict[int, list[tuple[SampleKey, Any]]] = {} 

398 retained_payloads = {} 

399 for data_rank, local_batch in enumerate(plan.local_batches): 

400 target_rank = self.group_ranks[data_rank] 

401 owned = [ 

402 (key, local_payloads[key]) 

403 for packing_bin in local_batch 

404 for key in packing_bin.sample_keys 

405 if key.reader_rank == self._global_rank 

406 ] 

407 if target_rank == self._global_rank: 

408 retained_payloads.update(owned) 

409 else: 

410 outgoing[target_rank] = owned 

411 prepared = self._transport.prepare_exchange(outgoing) 

412 work = self._transport.begin_exchange_prepared(prepared) 

413 else: 

414 # Every rank sees the same plan and skips an unchanged exchange. 

415 retained_payloads = local_payloads 

416 work = None 

417 return _PendingBatch(plan, stats, retained_payloads, work) 

418 

419 def _finish_batch(self, pending: _PendingBatch) -> _LocalBatch: 

420 """Complete payload exchange, pack and collate the local batch, then stage H2D.""" 

421 received = pending.retained_payloads 

422 if pending.payload_work is not None: 

423 received.update(pending.payload_work.wait()) 

424 target = pending.plan.local_batch_for(self._data_rank) 

425 host_batch = self._collate_fn([ 

426 self._pack_fn([received[key] for key in packing_bin.sample_keys], self.config.seq_len) 

427 for packing_bin in target 

428 ]) 

429 batch = self._device_prefetch(host_batch) if self._device_prefetch is not None else host_batch 

430 return _LocalBatch(batch, pending.stats, host_batch) 

431 

432 def _read_step(self, raw_bins: Sequence[Sequence[Any]], step: int) -> tuple[tuple[Any, ...], dict[SampleKey, Any]]: 

433 """Bind raw source bins to keyed metadata and cached payloads for one step.""" 

434 if len(raw_bins) != self.config.local_batch_size or any(not raw_bin for raw_bin in raw_bins): 

435 raise ValueError("Each source step must contain local_batch_size non-empty raw-sample bins.") 

436 key_bins = [] 

437 entries = [] 

438 payloads = {} 

439 for raw_bin in raw_bins: 

440 keys = [] 

441 for sample in raw_bin: 

442 item_metadata = self._metadata_fn(sample) 

443 if not isinstance(item_metadata, SampleMetadata): 

444 raise ValueError("metadata_fn must return SampleMetadata.") 

445 key = SampleKey(self._global_rank, step, len(entries)) 

446 entries.append(replace(item_metadata, key=key)) 

447 payloads[key] = sample 

448 keys.append(key) 

449 key_bins.append(tuple(keys)) 

450 return (tuple(key_bins), tuple(entries)), payloads 

451 

452 def _plan_step(self, gathered: Sequence[Any], step: int) -> tuple[DistributedPackingPlan, dict[str, Any]]: 

453 planned = self._make_plan(gathered, step) 

454 return self._transport.broadcast_from_planner(planned) if self.config.broadcast_plan else planned 

455 

456 def _make_plan(self, gathered: Sequence[Any], step: int) -> Any: 

457 """Build a plan and balancing statistics on the designated planning ranks.""" 

458 result = None 

459 if not self.config.broadcast_plan or self._global_rank == self._transport.planner_rank: 

460 samples = [] 

461 reference_bins = [] 

462 for bins, entries in gathered: 

463 reference_bins.extend(bins) 

464 samples.extend(entries) 

465 plan = self._planner.plan(samples, reference_bins=reference_bins, step=step) 

466 bin_stats = self._bin_statistics(gathered, plan) 

467 stats = self._statistics(plan, step, bin_stats["bins_before"]) 

468 if self._transport.planner_rank == 0: 

469 stats.update(bin_stats) 

470 result = (plan, stats) 

471 return result 

472 

473 def _bin_statistics(self, gathered: Sequence[Any], plan: DistributedPackingPlan) -> dict[str, Any]: 

474 """Summarize original and accepted bins using gathered metadata.""" 

475 # Gathered metadata predates cost-model evaluation. Reuse the accepted 

476 # plan's estimates for both layouts so before/after have the same units. 

477 metadata_by_key = { 

478 metadata.key: metadata 

479 for _, entries in gathered 

480 for metadata in entries 

481 } 

482 cost_by_key = {key: cost.llm for key, cost in self._planner.last_sample_costs.items()} 

483 

484 def _describe(keys: Sequence[SampleKey]) -> dict[str, Any]: 

485 tokens = sum(metadata_by_key[key].pack_tokens for key in keys) 

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

487 return { 

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

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

490 } 

491 

492 before = tuple(tuple(_describe(keys) for keys in key_bins) for key_bins, _ in gathered) 

493 after = tuple( 

494 tuple(_describe(packing_bin.sample_keys) for packing_bin in local_batch) 

495 for local_batch in plan.local_batches 

496 ) 

497 return {"bins_before": before, "bins_after": after} 

498 

499 def _statistics( 

500 self, plan: DistributedPackingPlan, step: int, bins_before: Sequence[Sequence[dict[str, Any]]], 

501 ) -> dict[str, Any]: 

502 """Count cross-rank raw-sample transfers, excluding locally retained samples.""" 

503 before = [math.fsum(packing_bin["cost"] for packing_bin in bins) for bins in bins_before] 

504 after = [cost.llm for cost in plan.rank_costs] 

505 send_samples = [0] * len(self.group_ranks) 

506 recv_samples = [0] * len(self.group_ranks) 

507 moved = 0 

508 source_indices = {rank: index for index, rank in enumerate(self.group_ranks)} 

509 for data_rank, local_batch in enumerate(plan.local_batches): 

510 for packing_bin in local_batch: 

511 for key in packing_bin.sample_keys: 

512 source_index = source_indices[key.reader_rank] 

513 if source_index != data_rank: 

514 send_samples[source_index] += 1 

515 recv_samples[data_rank] += 1 

516 moved += 1 

517 mean_before = math.fsum(before) / len(before) 

518 mean_after = math.fsum(after) / len(after) 

519 max_before, max_after = max(before), max(after) 

520 return { 

521 "step": step, 

522 "scope": "node", 

523 "objective": self._planner.objective, 

524 "group_ranks": self.group_ranks, 

525 "cost_before": tuple(before), 

526 "cost_after": tuple(after), 

527 "max_cost_before": max_before, 

528 "max_cost_after": max_after, 

529 "relative_spread_before": (max_before - min(before)) / mean_before if mean_before else 0.0, 

530 "relative_spread_after": (max_after - min(after)) / mean_after if mean_after else 0.0, 

531 "moved_samples": moved, 

532 "send_samples": tuple(send_samples), 

533 "recv_samples": tuple(recv_samples), 

534 **self._planner.last_balance_decision, 

535 } 

536 

537 

538def build_local_balancing_dataloader( 

539 local_dataloader: Iterable[Sequence[Sequence[Any]]], 

540 mesh: Any, 

541 config: DistributedDatasetConfig, 

542 *, 

543 metadata_fn: Callable[[Any], SampleMetadata], 

544 pack_fn: Callable[[Sequence[Any], int], Any], 

545 collate_fn: Callable[[Sequence[Any]], Any] = list, 

546 model_config: Any = None, 

547 cost_model: CostModel | None = None, 

548 balancing_algorithm: BalancingAlgorithm | None = None, 

549 device: Any = None, 

550) -> LocalBalancingDataLoader: 

551 """Build the fixed node-local balancing pipeline over complete raw steps. 

552 

553 Args: 

554 local_dataloader: Each yield contains exactly local_batch_size non-empty 

555 raw-sample bins. The source owns sampling and worker prefetch. 

556 mesh: WORLD-covering named mesh with model-parallel dimensions of size one. 

557 config: Packing limits and the minimum relative improvement required. 

558 metadata_fn: CPU-only per-sample metadata and cost features. 

559 pack_fn: Construct one packed sequence from its assigned raw samples. 

560 collate_fn: Assemble the local step as a sequence of microbatches. 

561 model_config: Effective backbone dimensions for the default FLOPs model. 

562 Required when cost_model is omitted. 

563 cost_model: Optional user estimate replacing the default cost model. 

564 balancing_algorithm: Optional sample assignment and objective policy. 

565 Costs and the improvement gate remain framework-owned. 

566 device: Training device; defaults to CPU with Gloo, otherwise the current 

567 accelerator. Pass an explicit NPU/CUDA device for H2D with Gloo. 

568 

569 Returns: 

570 A local-step loader using the configured node-local communication 

571 backend, automatic one-step buffering and final H2D prefetch. Each step 

572 retains its source packs unless the candidate improves its objective by 

573 more than min_balance_gain. 

574 

575 Note: 

576 The external source controls iteration length and all ranks must consume 

577 the same number of steps. Checkpoint/resume and 

578 nontrivial model parallelism are not supported by this local-step path. 

579 Iteration returns device-ready batches. last_host_batch retains the CPU 

580 view for metering without a device-to-host copy. 

581 """ 

582 if config.balance_window_size != 1: 

583 raise ValueError("balance_window_size > 1 requires a native batch_sampler.") 

584 if config.device_prefetch_depth != 1: 

585 raise ValueError("Configurable device_prefetch_depth requires a native batch_sampler.") 

586 if config.repack_sequences: 

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

588 cost_model = resolve_cost_model(cost_model, model_config) 

589 balancing_algorithm = resolve_balancing_algorithm(balancing_algorithm) 

590 device = _resolve_device(device, communication_backend=config.communication_backend) 

591 device_prefetch = _create_device_prefetcher(device) 

592 if not all(callable(callback) for callback in (metadata_fn, pack_fn, collate_fn)): 

593 raise ValueError("metadata_fn, pack_fn and collate_fn must be callable.") 

594 if not hasattr(local_dataloader, "__iter__"): 

595 raise ValueError("local_dataloader must be iterable.") 

596 if config.dataset_reader_ranks is not None or config.planner_rank is not None: 

597 raise ValueError("Node-local balancing assigns readers and planners automatically.") 

598 PackingConstraints(config.seq_len, config.oversized_policy, config.packing_budgets) 

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

600 topology, groups = _create_locality_groups( 

601 mesh, 

602 dp_dim_names=config.dp_dim_names, 

603 balance_group_size=config.balance_group_size, 

604 communication_backend=config.communication_backend, 

605 communication_device=communication_device, 

606 ) 

607 planner = DynamicPackingPlanner( 

608 data_parallel_size=len(groups.data_plane_ranks), 

609 seq_len=config.seq_len, 

610 local_batch_size=config.local_batch_size, 

611 oversized_policy=config.oversized_policy, 

612 packing_budgets=config.packing_budgets, 

613 cost_model=cost_model, 

614 balancing_algorithm=balancing_algorithm, 

615 min_balance_gain=config.min_balance_gain, 

616 validate=False, 

617 ) 

618 return LocalBalancingDataLoader( 

619 local_dataloader, 

620 config=config, 

621 metadata_fn=metadata_fn, 

622 pack_fn=pack_fn, 

623 collate_fn=collate_fn, 

624 planner=planner, 

625 transport=DataPlaneTransport( 

626 groups, 

627 topology.global_rank, 

628 communication_device=communication_device, 

629 ), 

630 global_rank=topology.global_rank, 

631 device_prefetch=device_prefetch, 

632 balance_stats_callback=log_balance_stats if config.log_balance else None, 

633 ) 

634 

635 

636__all__ = ["LocalBalancingDataLoader", "build_local_balancing_dataloader"]