Diff Coverage

Diff: origin/master...HEAD, staged and unstaged changes

Source File Diff Coverage (%) Missing Lines
hyper_parallel/data/batching/build_collate_fn.py 100%  
hyper_parallel/data/batching/build_dataloader.py 87.0% 128-129,131,195,198,200,210,236,238,240,284,351
hyper_parallel/data/batching/get_batch.py 88.0% 299,305,307
hyper_parallel/data/indexed/indexed_pretrain_dataset.py 73.7% 331,334-335,338-339
hyper_parallel/data/omni/metadata.py 83.3% 48,50,54,56
hyper_parallel/data/parallel/batch_parallel.py 83.3% 419,441
hyper_parallel/data/parallel/dataloader_parallel.py 88.9% 124
hyper_parallel/distributed_data/__init__.py 100%  
hyper_parallel/distributed_data/api.py 87.3% 265,284,286,297,303-308,315,317,328,332,343,347-348,368,387-388,395,408,413-414,418,424,536,546,548,600,602,604,606,719,721,723,725,727
hyper_parallel/distributed_data/balance_logging.py 100%  
hyper_parallel/distributed_data/balancing_algorithm.py 95.4% 94,303,315,358,393,480-481,483,486,502
hyper_parallel/distributed_data/batch_sampler.py 95.6% 65,108,110,185,230,237,305,307
hyper_parallel/distributed_data/cost_model.py 71.3% 71-77,122,124,130,134,136,138,147-149,153-159,163-165,214,259,261,264-270,288
hyper_parallel/distributed_data/data_constructor.py 88.2% 37,76,78,115
hyper_parallel/distributed_data/dataset_reader.py 91.7% 78-79,82
hyper_parallel/distributed_data/device_prefetch.py 82.3% 31,49,66-68,84-85,90-102,145,219-220
hyper_parallel/distributed_data/distributed_dataloader.py 94.1% 66,68,70,74,78,95,179,184,332-336,338,413,433,537-538,550-551,573,582,584,586,600,602,604,613,615,642,735
hyper_parallel/distributed_data/external_step.py 100%  
hyper_parallel/distributed_data/indexed_text.py 81.7% 32-33,35,64,102,104,112,116-117,119,126,155,157,162,169,171,173
hyper_parallel/distributed_data/locality.py 91.8% 76,78,85,89,154
hyper_parallel/distributed_data/metadata.py 85.7% 60,93,95,99,110,130-131,151
hyper_parallel/distributed_data/model_batch.py 93.8% 75,82,104,113,136
hyper_parallel/distributed_data/packed_balancing.py 89.9% 109,276,280-283,286,290-292,296-298,326,351,397-401,407-408,410-412,423,435,444,514-516,583,593,595,597
hyper_parallel/distributed_data/planner.py 94.6% 87,94,136,139,242,291,363
hyper_parallel/distributed_data/schema.py 66.5% 31,34,40,46,48,52-54,56,61,64-65,93,98,111,116,120,163,165,169,171,173,176,203,226,228,240-244,328-337,358-361,365-377,387,390-394,397-403,426
hyper_parallel/distributed_data/topology.py 88.8% 51-54,98,138,140,178,180,182,184,186
hyper_parallel/distributed_data/transport.py 84.0% 98,111,121-127,162,175-177,212-219,222-228,261,285,301,308,313,323,325,327,329,353,365,370,374,378,397,414,429-436,473,478,517,552,561,578,584,589,591,594,600,604,612,615,618,626,629,645,752-755,760-762,787,873,920,955,974,998
hyper_parallel/trainer/base.py 87.5% 905,918
hyper_parallel/trainer/config/data.py 100%  
hyper_parallel/trainer/config/resolver.py 100%  
hyper_parallel/trainer/text_trainer.py 23.7% 325-340,343-344,364-365,367,372-373,375-376,379,398-399,401
hyper_parallel/trainer/vlm_trainer.py 25.0% 71,329-343,346-347,367-368,370,373,375-378,381,398-399,401
hyper_parallel/data/batching/build_dataloader.py
124
125
126
127
128
129
130
131
132
133
134
135
) -> DistributedDatasetConfig:
    """Derive distributed-packing sizes and retain optional runtime tuning."""
    try:
        seq_len = int(data_config.get("seq_length", max_seq_len))
    except (KeyError, TypeError, ValueError) as exc:
        raise ValueError("Distributed loading requires data_config.seq_length or transform.max_seq_len") from exc
    if max_seq_len is not None and seq_len != max_seq_len:
        raise ValueError("data_config.seq_length must match transform.max_seq_len for native batch balancing")
    options = dict(dataloader_target.load_balance)
    trainer_managed_options = {
        "seq_len",
        "local_batch_size",
191
192
193
194
195
196
197
198
199
200
201
202
203
204
def _trainer_model_batch_spec(target: Any, collate_fn: Any, data_config: Mapping[str, Any]) -> Any:
    """Resolve declared model fields without reading samples on model peers."""
    configured = getattr(target, "model_batch_spec", None)
    if configured is not None:
        return configured.build() if callable(getattr(configured, "build", None)) else configured
    declared = getattr(collate_fn, "model_batch_spec", None)
    if declared is not None:
        return declared
    if isinstance(collate_fn, TextPackingCollator):
        return {
            "input_ids": TensorFieldSpec((1, None), torch.int64),
            "labels": TensorFieldSpec((1, None), torch.int64),
            "cu_seq_lens": TensorFieldSpec((None,), torch.int32),
        }
206
207
208
209
210
211
212
213
214
    # Their layouts must be configured explicitly rather than guessed from a sample.
    if collate_fn is default_collate and not data_config.get("is_instruction_dataset", False):
        spec = {name: TensorFieldSpec((None, None), torch.int64) for name in ("input_ids", "labels")}
        if data_config.get("create_ltor_fields_in_dataloader", False):
            spec.update({
                "loss_mask": TensorFieldSpec((None, None), torch.float32),
                "position_ids": TensorFieldSpec((None, None), torch.int64),
                "attention_mask": TensorFieldSpec((None, 1, None, None), torch.bool),
            })
232
233
234
235
236
237
238
239
240
241
242
243
244
        balancing_algorithm: BalancingAlgorithm | None = None,
) -> Any:
    """Reuse native Dataset outputs and the existing collator in collective loading."""
    if batch_sampler is None:
        raise ValueError("Native BatchSampler balancing requires a mapping Dataset")
    if getattr(mesh_context, "device_mesh", None) is None:
        raise ValueError("Native BatchSampler balancing requires mesh_context.device_mesh")
    if int(getattr(mesh_context, "pp_size", 1)) != 1:
        raise ValueError("Native BatchSampler balancing does not yet support pipeline parallelism")
    # Target exposes its resolved callable only as _target_; inspecting it
    # prevents replacing a second-stage dynamic selector without warning.
    target_callable = getattr(dataloader_target, "_target_", None)
    if isinstance(target_callable, type) and issubclass(target_callable, _DynamicBatchLoader):
280
281
282
283
284
285
286
287
288
        dataset: Any, dataloader_target: Any, training_config: Any, mesh_context: Any, seed: int,
) -> Any:
    """Build rank-local sampling for mapping datasets; iterable sources own their order."""
    if _is_iterable_dataset(dataset):
        return None
    return build_dataset_batch_sampler(
        total_samples=len(dataset),
        micro_batch_size=training_config.micro_batch_size,
        global_batch_size=training_config.global_batch_size,
347
348
349
350
351
352
353
354
    if len(datasets) != 3:
        raise ValueError("datasets must contain train, validation, and test entries")

    if all(dataset is None for dataset in datasets):
        return (None, None, None), (None, None, None)

    if dataloader_target is None:
        raise ValueError("dataloader_target must define a build target")
hyper_parallel/data/batching/get_batch.py
295
296
297
298
299
300
301
302
303
        if source_type in {"online", "packed_text"}:
            return OnlineBoundaryResolver()
        if source_type == "indexed":
            return IndexedBoundaryResolver(getattr(tokenizer, "eod", None))
        raise ValueError(f"Unsupported batch source type: {source_type!r}")

    def _validate_attention_inputs(self, attention_mode: str, create_attention_mask: bool) -> None:
        """Require shifted labels and document isolation for prepared text records."""
        if self.source_type == "packed_text":
301
302
303
304
305
306
307
308
309
310
311
    def _validate_attention_inputs(self, attention_mode: str, create_attention_mask: bool) -> None:
        """Require shifted labels and document isolation for prepared text records."""
        if self.source_type == "packed_text":
            if not self.labels_are_shifted:
                raise ValueError("Packed text batches require shifted labels; labels_are_shifted must be True")
            if not create_attention_mask or (attention_mode == "compressed" and self.runtime_input_adapter is None):
                raise ValueError(
                    "Packed text batches require document-boundary attention: enable "
                    "create_attention_mask_in_dataloader and supply a runtime_input_adapter for compressed attention"
                )
        if self.is_sft_dataset:
hyper_parallel/data/indexed/indexed_pretrain_dataset.py
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
        if num_epochs == 1:
            separate_final_epoch = False
            samples_without_final_epoch = 0
        else:
            samples_without_final_epoch = (
                (num_epochs - 1) * num_tokens_per_epoch - extra_token
            ) // self.config.sequence_length
            samples_from_final_epoch = self.num_samples - samples_without_final_epoch
            samples_per_epoch = (num_tokens_per_epoch - extra_token) // self.config.sequence_length

            # Keep a small final epoch separate from the main shuffled range.
            threshold = 0.80
            separate_final_epoch = samples_from_final_epoch < int(threshold * samples_per_epoch)
        return num_epochs, separate_final_epoch, samples_without_final_epoch

    def _build_document_sample_shuffle_indices(self) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
        """Build or load the document, sample, and shuffle indices.
hyper_parallel/data/omni/metadata.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
    pixels = sample.get("pixel_values")
    if input_ids is None or input_ids.ndim != 1 or input_ids.shape[0] == 0:
        raise ValueError("VLM metadata requires one non-empty 1-D input_ids tensor before collation")
    if grid is None or grid.ndim != 2 or grid.shape[1] != 3:
        raise ValueError("VLM metadata requires image_grid_thw with shape [num_images, 3]")
    if grid.device.type != "cpu":
        raise ValueError("VLM metadata must be extracted on CPU before device transfer")
    image_patches = 0
    for dimensions in grid.tolist():
        if any(not isinstance(dimension, int) or isinstance(dimension, bool) for dimension in dimensions):
            raise ValueError("VLM image_grid_thw must contain integer patch dimensions")
        if any(dimension <= 0 for dimension in dimensions):
            raise ValueError("VLM image_grid_thw dimensions must be positive")
        image_patches += dimensions[0] * dimensions[1] * dimensions[2]
    if pixels is None or pixels.ndim != 2 or pixels.shape[0] != image_patches:
        raise ValueError("VLM pixel_values rows must equal the raw patch count from image_grid_thw")
    seq_len = int(input_ids.shape[0])
hyper_parallel/data/parallel/batch_parallel.py
415
416
417
418
419
420
421
422
423
        if source_rank is None or self.parallel_context.tp_world_size == 1:
            parallel_batch = self._move_mapping_to_device(cp_local_batch)
            return parallel_batch

        is_source = self.parallel_context.tp_rank == source_rank
        if is_source:
            parallel_batch = self._move_mapping_to_device(cp_local_batch)
            metadata = []
            for name, value in parallel_batch.items():
437
438
439
440
441
442
443
444
445
                parallel_batch[name] = value
                continue
            if not is_source:
                parallel_batch[name] = torch.empty(shape, dtype=dtype, device=self.device)
            dist.broadcast(parallel_batch[name], group=self.parallel_context.tp_group, group_src=source_rank)
        return parallel_batch

    def _move_mapping_to_device(self, batch: Mapping[str, Any]) -> dict[str, Any]:
        """Transfer Omni tensors without changing their native dtypes."""
hyper_parallel/data/parallel/dataloader_parallel.py
120
121
122
123
124
125
126
127
128
        return dataloader_context

    tp_size = int(getattr(mesh_context, "tp_size", 1))
    if tp_source_rank is not None and not 0 <= tp_source_rank < tp_size:
        raise ValueError("tp_source_rank must be a rank within the TP group or None")
    build_on_rank = _always_build if tp_source_rank is None else partial(
        _is_dataloader_rank, mesh_context, tp_source_rank,
    )
    build_cache_on_rank = partial(_is_global_cache_builder_rank, shared_storage)
hyper_parallel/distributed_data/api.py
261
262
263
264
265
266
267
268
269

    def _validate_policy_and_flags(self) -> None:
        """Validate policy names, boolean flags and the relative-gain threshold."""
        if self.oversized_policy not in ("error", "single"):
            raise ValueError("oversized_policy must be 'error' or 'single'.")
        for name in (
                "dataset_already_sharded",
                "drop_last",
                "metadata_mode",
280
281
282
283
284
285
286
287
288
289
            or not 0.0 <= self.min_balance_gain < 1.0
        ):
            raise ValueError("min_balance_gain must be in [0, 1).")
        if self.communication_backend not in ("gloo", "hccl"):
            raise ValueError("communication_backend must be 'gloo' or 'hccl'.")
        if not self.drop_last:
            raise ValueError(
                "Dynamic distributed packing currently requires drop_last=True so every DP rank receives the "
                "same number of non-empty packing bins."
            )
293
294
295
296
297
298
299
300
301
                not isinstance(self.planner_rank, int)
                or isinstance(self.planner_rank, bool)
                or self.planner_rank < 0
        ):
            raise ValueError("planner_rank must be a non-negative integer or None.")

    @staticmethod
    def _validate_rank_tuple(value: tuple[int, ...] | None, name: str) -> None:
        if value is None:
299
300
301
302
303
304
305
306
307
308
309
310
311
312
    @staticmethod
    def _validate_rank_tuple(value: tuple[int, ...] | None, name: str) -> None:
        if value is None:
            return
        if not isinstance(value, tuple) or not value:
            raise ValueError(f"{name} must be a non-empty tuple or None.")
        if any(not isinstance(rank, int) or isinstance(rank, bool) or rank < 0 for rank in value):
            raise ValueError(f"{name} must contain non-negative integer ranks.")
        if len(value) != len(set(value)):
            raise ValueError(f"{name} must not contain duplicate ranks.")

    @staticmethod
    def _validate_name_tuple(value: tuple[str, ...] | None, name: str) -> None:
        if value is None:
311
312
313
314
315
316
317
318
319
320
321
    def _validate_name_tuple(value: tuple[str, ...] | None, name: str) -> None:
        if value is None:
            return
        if not isinstance(value, tuple) or not value or any(not isinstance(item, str) or not item for item in value):
            raise ValueError(f"{name} must be a non-empty tuple of strings or None.")
        if len(value) != len(set(value)):
            raise ValueError(f"{name} must not contain duplicate names.")


def _resolve_service_ranks(
        topology: DataTopology,
324
325
326
327
328
329
330
331
332
333
334
335
336
    """Resolve Reader and planner ownership within the root mesh."""
    dataset_reader_ranks = config.dataset_reader_ranks or topology.constructor_ranks
    unknown_ranks = set(dataset_reader_ranks) - set(topology.rank_list)
    if unknown_ranks:
        raise ValueError(f"dataset_reader_ranks are outside the root mesh: {sorted(unknown_ranks)}.")
    planner_rank = min(topology.constructor_ranks) if config.planner_rank is None else config.planner_rank
    data_plane_ranks = set(dataset_reader_ranks) | set(topology.constructor_ranks)
    if planner_rank not in data_plane_ranks:
        raise ValueError(f"planner_rank {planner_rank} must be a Dataset Reader or Data Constructor rank.")
    return dataset_reader_ranks, planner_rank


def _normalize_dataloader_kwargs(
339
340
341
342
343
344
345
346
347
348
349
350
351
352
    """Resolve validated DataLoader execution options."""
    if dataloader_kwargs is None:
        supplied_options = {}
    elif not isinstance(dataloader_kwargs, Mapping):
        raise ValueError("dataloader_kwargs must be a mapping or None.")
    else:
        try:
            supplied_options = dict(dataloader_kwargs)
        except Exception as exc:
            raise ValueError(f"dataloader_kwargs cannot be copied: {exc}") from exc
    _validate_dataloader_option_keys(supplied_options)
    options = dict(_DEFAULT_DATALOADER_KWARGS)
    options.update(supplied_options)
    _validate_worker_options(options)
364
365
366
367
368
369
370
371
372
def _validate_dataloader_option_keys(supplied_options: Mapping[str, Any]) -> None:
    """Reject unsupported keys and overrides of internally managed DataLoader options."""
    non_string_keys = [key for key in supplied_options if not isinstance(key, str)]
    if non_string_keys:
        raise ValueError(f"dataloader_kwargs keys must be strings, but got {non_string_keys!r}.")
    managed_keys = sorted(set(supplied_options) & _MANAGED_DATALOADER_KWARGS)
    if managed_keys:
        raise ValueError(
            f"dataloader_kwargs cannot override distributed sampling, batching, collation, or deterministic "
383
384
385
386
387
388
389
390
391
392
    num_workers = options["num_workers"]
    timeout = options.get("timeout", 0)
    try:
        normalized_timeout = float(timeout)
    except (TypeError, ValueError, OverflowError):
        normalized_timeout = math.nan
    if (
            not isinstance(timeout, (int, float))
            or isinstance(timeout, bool)
            or not math.isfinite(normalized_timeout)
391
392
393
394
395
396
397
398
            or isinstance(timeout, bool)
            or not math.isfinite(normalized_timeout)
            or normalized_timeout < 0
    ):
        raise ValueError(f"dataloader_kwargs timeout must be finite and non-negative, but got {timeout!r}.")
    if num_workers == 0 and timeout != 0:
        raise ValueError("dataloader_kwargs timeout must be zero when num_workers=0.")

404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
        return
    if options["num_workers"] == 0:
        raise ValueError("dataloader_kwargs multiprocessing_context requires num_workers > 0.")
    if not isinstance(multiprocessing_context, (str, BaseContext)):
        raise ValueError("dataloader_kwargs multiprocessing_context is invalid.")
    start_method = multiprocessing_context
    if isinstance(multiprocessing_context, BaseContext):
        try:
            start_method = multiprocessing_context.get_start_method()
        except Exception as exc:
            raise ValueError(
                f"dataloader_kwargs multiprocessing_context cannot report its start method: {exc}"
            ) from exc
    if start_method not in torch.multiprocessing.get_all_start_methods():
        raise ValueError(f"dataloader_kwargs multiprocessing_context={start_method!r} is not supported.")


def _normalize_communication_device(communication_device: Any) -> torch.device | None:
    """Normalize a rank-local A2A device."""
420
421
422
423
424
425
426
427
428

def _normalize_communication_device(communication_device: Any) -> torch.device | None:
    """Normalize a rank-local A2A device."""
    if communication_device is None:
        return None
    try:
        return torch.device(communication_device)
    except Exception as exc:
        raise ValueError(f"communication_device is invalid: {communication_device!r}.") from exc
532
533
534
535
536
537
538
539
540
            split_fn: Callable[[Any], Sequence[Any]] | None,
    ) -> None:
        """Use native DP sampler owners as Readers without applying a second stride."""
        if self.dataset_reader_ranks != self.topology.constructor_ranks:
            raise ValueError("Native batch_sampler currently requires Dataset Readers on the Data Constructor ranks.")
        if config.dataset_already_sharded:
            raise ValueError(
                "Native batch_sampler owns DP slicing; disable dataset_already_sharded."
            )
542
543
544
545
546
547
548
549
550
551
552
            batch_sampler, data_rank=self.topology.data_rank,
            dp_size=self.topology.data_parallel_size, local_batch_size=config.local_batch_size,
        )
        if not self.is_reader:
            return
        if dataset is None:
            raise ValueError("Native batch_sampler requires a mapping Dataset on each Data Constructor.")
        sample_loader = PlannedSampleLoader(dataset, seed=config.seed, **loader_options)
        sample_loader.set_epoch(batch_sampler.epoch)
        if config.metadata_mode:
            if not callable(getattr(metadata, "__getitem__", None)) or len(metadata) != len(dataset):
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
        collate_fn: Callable[[Sequence[Any]], Any] | None,
) -> None:
    """Validate callback types and mutually exclusive metadata providers."""
    if metadata_fn is not None and not callable(metadata_fn):
        raise ValueError("metadata_fn must be callable or None.")
    if metadata_fn is not None and metadata is not None:
        raise ValueError("Provide either metadata or metadata_fn, but not both.")
    if pack_fn is not None and not callable(pack_fn):
        raise ValueError("pack_fn must be callable or None.")
    if collate_fn is not None and not callable(collate_fn):
        raise ValueError("collate_fn must be callable or None.")


def _resolve_repacking_callbacks(
        config: DistributedDatasetConfig,
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
        Collective iterator yielding one constructed local batch per micro-batch.
    """
    if external_step_source is not None:
        if model_batch_spec is not None:
            raise ValueError("model_batch_spec is only supported with a native batch_sampler.")
        if split_fn is not None:
            raise ValueError("split_fn is only supported with a native batch_sampler.")
        if config.metadata_mode:
            raise ValueError("external_step_source requires metadata_mode=False for online metadata.")
        if batch_sampler is not None or metadata is not None:
            raise ValueError("external_step_source cannot be combined with a sampler or metadata sequence.")
        if dataloader_kwargs:
            raise ValueError("Configure source worker options on external_step_source, not the balancing wrapper.")
        return build_local_balancing_dataloader(
            external_step_source, mesh, config, metadata_fn=metadata_fn,
            pack_fn=default_pack_fn if pack_fn is None else pack_fn,
            collate_fn=default_collate_fn if collate_fn is None else collate_fn,
hyper_parallel/distributed_data/balancing_algorithm.py
90
91
92
93
94
95
96
97
98
        Args:
            objective: Maximum backbone load, or relative backbone-load variance.
        """
        if objective not in ("makespan", "balance"):
            raise ValueError("objective must be 'makespan' or 'balance'.")
        self.objective_name = objective
        self.padding_cost: Callable[[int], WorkloadCost] | None = None

    def assign(
299
300
301
302
303
304
305
306
307
    ) -> Sequence[Sequence[SampleKey]]:
        """Refine one-bin-per-slot assignments without token overflow or stage budgets."""
        lookup = {item.key: item for item in samples}
        if any(sum(lookup[key].pack_tokens for key in keys) > capacity for keys in candidate):
            return candidate
        if self.padding_cost is not None:
            candidate = min(
                (candidate, reference_bins),
                key=lambda bins: self._repair_objective(
311
312
313
314
315
316
317
318
319
        if not self._uses_scheduled_makespan() and (
                candidate is not reference_bins
                or any(sum(lookup[key].pack_tokens for key in keys) != capacity for keys in candidate)
        ):
            return candidate
        return self._repair_bin_pairs(lookup, candidate, capacity, group_size)

    def _uses_scheduled_makespan(self) -> bool:
        return self.objective_name == "makespan" and type(self).objective is LPTBalancingAlgorithm.objective
354
355
356
357
358
359
360
361
362
                    return self._pair_potential(costs, pair, group_size, total)

                pairs = sorted(pairs, key=lambda pair: (-_potential(pair), pair))
            else:
                pairs = sorted(pairs)
            for left, right in pairs:
                version = (left, right, versions[left], versions[right])
                if len(checked) == self.max_repair_pairs:
                    break
389
390
391
392
393
394
395
396
397
        order = sorted(range(len(costs)), key=lambda index: (-costs[index].llm, index))
        if self._uses_scheduled_makespan():
            leaders = order[::group_size]
        else:
            leaders = [
                max(range(start, start + group_size), key=lambda index: costs[index].llm)
                for start in range(0, len(costs), group_size)
            ]
        positions = {index: position for position, index in enumerate(order)}
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
                for side in range(2):
                    trial[pair[side]] += self._padding_llm_cost(capacity - trial_tokens[side])
                if scheduled_makespan:
                    score = math.fsum(sorted(trial, reverse=True)[::group_size])
                elif self.padding_cost is not None:
                    score = self._repair_objective([WorkloadCost(llm=value) for value in trial], group_size)
                else:
                    score = costs[rank]
                candidates.append((score, capacity - trial_tokens[rank], rank))
            if not candidates:
                return reference_bins
            rank = min(candidates)[-1]
            bins[rank].append(item.key)
            tokens[rank] += item.pack_tokens
            costs[rank] += item.cost.llm
498
499
500
501
502
503
504
505
506
        algorithm: Optional deterministic assignment and objective implementation.
    """
    resolved = FeasibleLPTBalancingAlgorithm() if algorithm is None else algorithm
    if not callable(getattr(resolved, "assign", None)) or not callable(getattr(resolved, "objective", None)):
        raise ValueError("balancing_algorithm must provide callable assign() and objective() methods.")
    return resolved


__all__ = [
hyper_parallel/distributed_data/batch_sampler.py
61
62
63
64
65
66
67
68
69
        The sole source sample, unchanged.
    """
    del seq_len
    if len(samples) != 1:
        raise ValueError("Native BatchSampler mode must keep one Dataset output per sequence bin.")
    return samples[0]


@dataclass(frozen=True)
104
105
106
107
108
109
110
111
112
113
114
            split_fn: Callable[[Any], Sequence[Any]] | None = None,
    ) -> None:
        """Store the validated native sampler and metadata/payload sources."""
        if metadata is None and (sample_loader is None or not callable(metadata_fn)):
            raise ValueError("Online BatchSampler Reader requires payload loading and metadata_fn.")
        if split_fn is not None and (not callable(split_fn) or metadata is not None):
            raise ValueError("split_fn requires online metadata and must be callable.")
        self._sampler = sampler
        self._reader_rank = reader_rank
        self._metadata = metadata
        self._metadata_fn = metadata_fn
181
182
183
184
185
186
187
188
189
            metadata = [self._metadata[key.dataset_index] for key in keys]
        if self._split_fn is None:
            original_batches = tuple(tuple((key,) for key in batch) for batch in source_batches)
        if any(not isinstance(item, SampleMetadata) for item in metadata):
            raise ValueError("BatchSampler metadata must contain SampleMetadata entries.")
        return _ReaderWindow(
            metadata=tuple(replace(item, key=key) for key, item in zip(keys, metadata)),
            samples=samples,
            sampler_state=copy.deepcopy(self._sampler.state_dict()),
226
227
228
229
230
231
232
233
234

    def _sample_keys(self, indices: Sequence[int], batch_position: int) -> tuple[SampleKey, ...]:
        """Bind local Dataset indices to distinct global sampling occurrences."""
        if len(indices) != self._sampler.micro_batch_size:
            raise ValueError("Native BatchSampler must yield exactly local_batch_size indices on every DP rank.")
        # A native sampler exposes local indices, not its internal permutation
        # positions. Stable DP-major occurrence slots preserve duplicates.
        start = batch_position + self._sampler.dp_rank * self._sampler.micro_batch_size
        keys = []
233
234
235
236
237
238
239
240
241
        start = batch_position + self._sampler.dp_rank * self._sampler.micro_batch_size
        keys = []
        for offset, index in enumerate(indices):
            if isinstance(index, bool):
                raise ValueError("Native BatchSampler indices must be non-negative integers.")
            keys.append(SampleKey(self._reader_rank, operator.index(index), start + offset))
        return tuple(keys)

    def metadata(self) -> tuple[SampleMetadata, ...]:
301
302
303
304
305
306
307
308
309
310
311
            ValueError: If the checkpoint epoch or payload reader is incompatible.
        """
        sampler_state = state["sampler"]
        if sampler_state.get("epoch") != state.get("epoch"):
            raise ValueError("Native BatchSampler checkpoint epoch is inconsistent.")
        if (state.get("sample_loader") is None) != (self._sample_loader is None):
            raise ValueError("Native BatchSampler checkpoint payload reader ownership changed.")
        if "window" in state or sampler_state["consumed_samples"] % self._sampler.global_batch_size:
            raise ValueError("Native BatchSampler checkpoint must align to a complete optimizer step.")
        self.close()
        self._sampler.load_state_dict(sampler_state)
hyper_parallel/distributed_data/cost_model.py
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81


def _configuration_dict(config: Any) -> dict[str, Any]:
    """Accept an ordinary dictionary or a configuration with ``to_dict``."""
    if isinstance(config, Mapping):
        return dict(config)
    if callable(getattr(config, "to_dict", None)):
        value = config.to_dict()
        if isinstance(value, Mapping):
            return dict(value)
    raise ValueError("model_config must be a mapping or a configuration exposing to_dict().")


def _integer(value: Any, name: str, minimum: int = 1) -> int:
    if not isinstance(value, int) or isinstance(value, bool) or value < minimum:
118
119
120
121
122
123
124
125
126
127
128
            _integer(getattr(self, name), name)
        for name in ("qk_nope_head_dim", "qk_rope_head_dim", "n_shared_experts"):
            _integer(getattr(self, name), name, 0)
        if self.q_lora_rank is not None:
            _integer(self.q_lora_rank, "q_lora_rank")
        if self.qk_nope_head_dim + self.qk_rope_head_dim == 0:
            raise ValueError("The combined query/key head dimension must be positive.")
        if (
                not isinstance(self.mlp_layer_types, tuple)
                or len(self.mlp_layer_types) != self.num_hidden_layers
                or any(kind not in ("dense", "sparse") for kind in self.mlp_layer_types)
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
                not isinstance(self.mlp_layer_types, tuple)
                or len(self.mlp_layer_types) != self.num_hidden_layers
                or any(kind not in ("dense", "sparse") for kind in self.mlp_layer_types)
        ):
            raise ValueError("mlp_layer_types must contain one 'dense' or 'sparse' entry per backbone layer.")
        for name in ("moe_intermediate_size", "n_routed_experts", "num_experts_per_tok"):
            _integer(getattr(self, name), name, int("sparse" in self.mlp_layer_types))
        if self.num_experts_per_tok > self.n_routed_experts:
            raise ValueError("num_experts_per_tok cannot exceed n_routed_experts.")
        if self.attn_block_mode not in ("tail", "blockmask"):
            raise ValueError("attn_block_mode must be 'tail' or 'blockmask'.")
        if self.diff_time_embed_mode not in ("add", "prepend", "adaln"):
            raise ValueError("diff_time_embed_mode must be 'add', 'prepend', or 'adaln'.")

    @classmethod
    def from_model_config(cls, model_config: Any) -> "BackboneFlopsConfig":
        """Read the final model configuration after architecture overrides.
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169

        Args:
            model_config: Configuration with explicit decoder dimensions and layer types.
        """
        config = _configuration_dict(model_config)
        text = _configuration_dict(config.get("text_config", config))
        required = (
            "hidden_size", "num_hidden_layers", "num_attention_heads", "intermediate_size",
            "kv_lora_rank", "q_lora_rank", "qk_nope_head_dim", "qk_rope_head_dim", "v_head_dim",
        )
        missing = [name for name in required if name not in text]
        if missing:
            raise ValueError(f"Backbone cost model is missing configuration fields: {missing}.")
        pattern = text.get("mlp_layer_types")
        if not isinstance(pattern, (list, tuple)):
            raise ValueError("model_config must provide mlp_layer_types in actual decoder layer order.")
        qk_dim = (
            _integer(text["qk_nope_head_dim"], "qk_nope_head_dim", 0)
            + _integer(text["qk_rope_head_dim"], "qk_rope_head_dim", 0)
        )
        if text.get("qk_head_dim", qk_dim) != qk_dim:
            raise ValueError("qk_head_dim must equal qk_nope_head_dim + qk_rope_head_dim.")
        return cls(
            **{name: text[name] for name in required},
            mlp_layer_types=tuple(pattern),
            **{name: text.get(name, 0) for name in (
                "moe_intermediate_size", "n_routed_experts", "num_experts_per_tok", "n_shared_experts",
210
211
212
213
214
215
216
217
218
                or isinstance(training_multiplier, bool)
                or not math.isfinite(training_multiplier)
                or training_multiplier <= 0
        ):
            raise ValueError("training_multiplier must be finite and positive.")
        self.training_multiplier = float(training_multiplier)
        self.config = (
            model_config if isinstance(model_config, BackboneFlopsConfig)
            else BackboneFlopsConfig.from_model_config(model_config)
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
        """
        prefix = _integer(metadata.features.get("P"), "metadata.features['P']", 0)
        diffusion = _integer(metadata.features.get("D"), "metadata.features['D']", 0)
        if prefix + diffusion != metadata.pack_tokens:
            raise ValueError("metadata features P + D must equal pack_tokens before optional timestep-token insertion.")
        if diffusion and self.config.diff_time_embed_mode == "prepend":
            diffusion += 1
        pairs = 0.5 * prefix * prefix + diffusion * (prefix + diffusion)
        if self.config.attn_block_mode == "blockmask":
            runs = metadata.features.get("cond_image_token_lengths")
            if not isinstance(runs, (list, tuple)):
                raise ValueError("Blockwise attention requires cond_image_token_lengths (empty for no images).")
            lengths = [_integer(length, "conditional-image run length") for length in runs]
            if sum(lengths) > prefix:
                raise ValueError("Conditional-image run lengths cannot exceed the prefix token count P.")
            pairs += 0.5 * sum(length * length for length in lengths)
        return float(self.linear_flops_per_token * (prefix + diffusion) + self.attention_flops_per_pair * pairs)

    def __call__(self, metadata: SampleMetadata) -> WorkloadCost:
        """Return backbone-only workload without changing physical packing caps."""
284
285
286
287
288
289
290
291
292
    """
    if cost_model is None:
        return DefaultCostModel(model_config)
    if not callable(cost_model):
        raise ValueError("cost_model must be callable: SampleMetadata -> WorkloadCost.")
    return cost_model


__all__ = ["BackboneFlopsConfig", "CostModel", "DefaultCostModel"]
hyper_parallel/distributed_data/data_constructor.py
33
34
35
36
37
38
39
40
41
    Returns:
        Immutable raw samples in their deterministic planned order.
    """
    if not isinstance(seq_len, int) or isinstance(seq_len, bool) or seq_len < 1:
        raise ValueError(f"seq_len must be a positive integer, but got {seq_len!r}.")
    return tuple(samples)


def default_collate_fn(packed_sequences: Sequence[Any]) -> tuple[Any, ...]:
72
73
74
75
76
77
78
79
80
81
            collate_fn: Called once per local batch with packed sequences.
            seq_len: Maximum non-oversized sequence length.
        """
        if not callable(pack_fn) or not callable(collate_fn):
            raise ValueError("pack_fn and collate_fn must be callable.")
        if not isinstance(seq_len, int) or isinstance(seq_len, bool) or seq_len < 1:
            raise ValueError(f"seq_len must be a positive integer, but got {seq_len!r}.")
        self._pack_fn = pack_fn
        self._collate_fn = collate_fn
        self._seq_len = seq_len
111
112
113
114
115
116
117
118
119

        packed_sequences = []
        for pack_index, packing_bin in enumerate(plan):
            if packing_bin.pack_tokens > self._seq_len and not packing_bin.oversized:
                raise ValueError(
                    f"Packing bin {pack_index} has {packing_bin.pack_tokens} tokens, "
                    f"exceeding seq_len={self._seq_len}."
                )
            raw_samples = [payloads[key] for key in packing_bin.sample_keys]
hyper_parallel/distributed_data/dataset_reader.py
74
75
76
77
78
79
80
81
82
83
84
85
86
) -> dict[str, Any]:
    """Merge additional DataLoader options without exposing owned semantics."""
    try:
        options = dict({} if dataloader_kwargs is None else dataloader_kwargs)
    except Exception as exc:
        raise ValueError(f"dataloader_kwargs cannot be copied: {exc}") from exc
    conflicting_options = sorted(set(options) & _INTERNAL_DATALOADER_OPTIONS)
    if conflicting_options:
        raise ValueError(f"dataloader_kwargs contains internally managed options {conflicting_options}.")
    options.update({
        "num_workers": num_workers,
        "pin_memory": pin_memory,
        "prefetch_factor": prefetch_factor,
hyper_parallel/distributed_data/device_prefetch.py
27
28
29
30
31
32
33
34
    """Return the PyTorch device module that owns streams and events."""
    module = getattr(torch, device.type, None)
    required = ("Event", "Stream", "current_stream", "stream")
    if module is None or any(not callable(getattr(module, name, None)) for name in required):
        raise ValueError(
            f"Device {device} does not expose PyTorch accelerator Stream/Event APIs."
        )
    return module
45
46
47
48
49
50
51
52
53
        return {key: _move_to_device(item, device) for key, item in value.items()}
    if isinstance(value, tuple):
        moved = tuple(_move_to_device(item, device) for item in value)
        if hasattr(value, "_fields"):
            return type(value)(*moved)
        return moved
    if isinstance(value, list):
        return [_move_to_device(item, device) for item in value]
    return value
62
63
64
65
66
67
68
69
70
71
72
        return
    visited.add(value_id)

    if isinstance(value, torch.Tensor):
        if value.device.type != "cpu":
            value.record_stream(stream)
        return
    record_stream = getattr(value, "record_stream", None)
    if callable(record_stream):
        record_stream(stream)
        return
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
        return

    attributes = getattr(value, "__dict__", None)
    if isinstance(attributes, dict):
        for item in attributes.values():
            _record_stream(item, stream, visited)


def _pin_memory(value: Any) -> Any:
    """Pin final collated CPU tensors without changing metadata or dtype."""
    if isinstance(value, torch.Tensor):
        if value.device.type == "cpu" and value.layout == torch.strided and not value.is_pinned():
            return value.pin_memory()
        return value
    if isinstance(value, Mapping):
        return {key: _pin_memory(item) for key, item in value.items()}
    if isinstance(value, tuple):
        pinned = tuple(_pin_memory(item) for item in value)
        return type(value)(*pinned) if hasattr(value, "_fields") else pinned
    if isinstance(value, list):
        return [_pin_memory(item) for item in value]
    pin = getattr(value, "pin_memory", None)
    return pin() if callable(pin) else value


class DevicePrefetchedStep:
    """Hand the complete device batch and its copy dependency to training."""
141
142
143
144
145
146
147
148
            device: Target CUDA or NPU device, independent of payload transport.
        """
        self.device = torch.device(device)
        if self.device.type not in ("cuda", "npu"):
            raise ValueError("DeviceStepPrefetcher requires a CUDA or NPU device.")
        self._device_module = _get_device_module(self.device)
        self._copy_stream = None
        self._pending_staging: list[tuple[Any, Any]] = []
215
216
217
218
219
220
221
222
223
    if device is None:
        for device_type in ("npu", "cuda"):
            device_module = getattr(torch, device_type, None)
            if device_module is not None and device_module.is_available():
                device = torch.device(device_type, device_module.current_device())
                break
        else:
            device = "cpu"
    return torch.device(device)
hyper_parallel/distributed_data/distributed_dataloader.py
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
        metadata_mode: bool,
) -> None:
    """Check Reader ownership and require mode-compatible loading components."""
    if not isinstance(metadata_mode, bool):
        raise ValueError("metadata_mode must be boolean.")
    if metadata_mode and dataset_reader is not None:
        raise ValueError("Metadata mode must not configure an online Dataset Reader.")
    if not metadata_mode and (metadata_reader is not None or direct_sample_loader is not None):
        raise ValueError("Online mode must not configure metadata loading components.")
    is_reader = topology.global_rank in dataset_reader_ranks
    planning_reader = metadata_reader if metadata_mode else dataset_reader
    if is_reader != (planning_reader is not None):
        raise ValueError("Dataset Reader ownership does not match dataset_reader_ranks.")
    if not metadata_mode and planning_reader is not None and not callable(
            getattr(planning_reader, "prepare_next_step", None)
    ):
        raise ValueError("Online Dataset Reader must provide prepare_next_step().")
    _validate_metadata_loader_owner(
        topology,
        metadata_mode=metadata_mode,
        direct_sample_loader=direct_sample_loader,
91
92
93
94
95
96
97
98
99
) -> None:
    if not metadata_mode:
        return
    if topology.is_constructor != (direct_sample_loader is not None):
        raise ValueError("Every metadata Data Constructor must own one plan-aware sample loader.")


@dataclass
class _PreparedWindow:
175
176
177
178
179
180
181
182
183
184
185
186
187
188
        self._pending_local_keys: set[SampleKey] = set()
        self._window_plans: deque[DistributedPackingPlan] = deque()
        for name, value in (("accumulation_steps", accumulation_steps), ("balance_window_size", balance_window_size)):
            if isinstance(value, bool) or not isinstance(value, int) or value < 1:
                raise ValueError(f"{name} must be a positive integer.")
        self._accumulation_steps = accumulation_steps
        self._balance_window_size = balance_window_size
        if (isinstance(device_prefetch_depth, bool) or not isinstance(device_prefetch_depth, int)
                or device_prefetch_depth < 0):
            raise ValueError("device_prefetch_depth must be a non-negative integer.")
        self._device_prefetch_depth = device_prefetch_depth
        self._device = torch.device(device)
        self._current: _PreparedWindow | None = None
        self._future: Future[_PreparedWindow] | None = None
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
            if self._topology.is_constructor:
                self._prefetch_device_batches(1, blocking=True)
                prepared = self._device_batches.popleft()
            else:
                prepared = self._next_host_batch()
                prefetch_wait_event = self._prefetch_slots.popleft()
                if prefetch_wait_event is not None:
                    prefetch_wait_event.synchronize()
        except BaseException as exc:  # pylint: disable=broad-exception-caught
            # All preparation failures must reach peers through the broadcast error header.
            prepared = _PreparedBatch(None, error=exc)
        module = getattr(torch, self._device.type)
        consumer_stream = module.current_stream(self._device)
        try:
            batch = prepared.batch
409
410
411
412
413
414
415
416
417

    def _prepare_batch(self) -> Any:
        """Construct one Host round; accelerator peers receive it at device delivery."""
        if self._stopped:
            raise StopIteration
        batch = self._prepare_next_batch()
        if batch is not None and self._device_prefetch is not None:
            batch = self._device_prefetch.prepare_host(batch)
        if self._device.type == "cpu":
429
430
431
432
433
434
435
436
437
    def _prepare_next_batch(self) -> Any:
        """Select, balance, route, and construct samples without model-group broadcast."""
        if self._data_plane.is_member:
            return self._produce_on_data_plane()
        return None

    def _broadcast_batch(self, constructed_batch: Any) -> Any:
        """Broadcast one batch on the caller stream, or on the Host producer for CPU."""
        return self._model_transport.broadcast(constructed_batch)
533
534
535
536
537
538
539
540
541
542
            ),
        }
        try:
            copied_state = copy.deepcopy(state)
        except Exception as exc:
            raise ValueError(f"Distributed DataLoader state is not checkpointable: {exc}") from exc
        return copied_state

    def load_state_dict(self, state_dict: Mapping[str, Any]) -> None:
        """Restore a fixed-topology rank-local checkpoint.
546
547
548
549
550
551
552
553
554
555
        """
        self._validate_restore_boundary()
        try:
            state = copy.deepcopy(dict(state_dict))
        except Exception as exc:
            raise ValueError(f"Distributed DataLoader state is not copyable: {exc}") from exc
        if state.get("global_rank") != self._topology.global_rank:
            raise ValueError(
                f"Distributed DataLoader checkpoint global_rank={state.get('global_rank')!r} "
                f"does not match {self._topology.global_rank!r}."
569
570
571
572
573
574
575
576
577
        self._delivered_state = self._snapshot_state()

    def _validate_restore_boundary(self) -> None:
        if self._last_ready_event is not None or self._prefetch_thread is not None or self._step != 0:
            raise ValueError("load_state_dict must run before distributed iteration starts.")

    @staticmethod
    def _validate_checkpoint_values(state: Mapping[str, Any]) -> tuple[int, int, bool]:
        """Validate and return the saved epoch, micro-batch index and stopped flag."""
578
579
580
581
582
583
584
585
586
587
588
589
590
        epoch = state.get("epoch")
        step = state.get("step")
        stopped = state.get("stopped")
        if not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 0:
            raise ValueError("Distributed DataLoader checkpoint epoch must be non-negative.")
        if not isinstance(step, int) or isinstance(step, bool) or step < 0:
            raise ValueError("Distributed DataLoader checkpoint step must be non-negative.")
        if not isinstance(stopped, bool):
            raise ValueError("Distributed DataLoader checkpoint stopped must be boolean.")
        return epoch, step, stopped

    def _validate_component_states(
            self,
596
597
598
599
600
601
602
603
604
605
606
607
608
        # Accept the old field name when restoring checkpoints written before the rename.
        metadata_reader_state = state.get("metadata_reader", state.get("sidecar_reader"))
        direct_sample_state = state.get("direct_sample_loader")
        if (self._dataset_reader is None) != (reader_state is None):
            raise ValueError("Distributed DataLoader checkpoint Dataset Reader ownership changed.")
        if (self._metadata_reader is None) != (metadata_reader_state is None):
            raise ValueError("Distributed DataLoader checkpoint metadata ownership changed.")
        if (self._direct_sample_loader is None) != (direct_sample_state is None):
            raise ValueError("Distributed DataLoader checkpoint direct-reader ownership changed.")
        for component_name, component_state in (
                ("Dataset Reader", reader_state),
                ("metadata reader", metadata_reader_state),
                ("direct reader", direct_sample_state),
609
610
611
612
613
614
615
616
617
618
619
        ):
            if component_state is None:
                continue
            if not isinstance(component_state, Mapping):
                raise ValueError(f"Distributed DataLoader checkpoint {component_name} state must be a mapping.")
            if component_state.get("epoch") != epoch:
                raise ValueError(
                    f"Distributed DataLoader checkpoint {component_name} epoch "
                    f"{component_state.get('epoch')!r} does not match loader epoch {epoch}."
                )
        return reader_state, metadata_reader_state, direct_sample_state
638
639
640
641
642
643
644
645
646
        Args:
            epoch: Non-negative Dataset epoch.
        """
        if isinstance(epoch, bool) or not isinstance(epoch, int) or epoch < 0:
            raise ValueError("epoch must be a non-negative integer.")
        if self._closed:
            raise ValueError("set_epoch cannot restart a closed loader.")
        if (self._has_delivered or self._delivered_state["step"] != 0) and not self._delivered_state["stopped"]:
            raise ValueError("set_epoch requires a fresh or exhausted optimizer-step loader.")
731
732
733
734
735
736
737
738
739

    def _prepare_local_metadata(self, window_micro_batch_num: int) -> None:
        """Read one explicitly sized source window before constructing its batches."""
        if self._topology.global_rank not in self._dataset_reader_ranks:
            return
        planning_reader = self._planning_reader()
        planning_reader.prepare_next_step(window_micro_batch_num)

    def _build_plan_control(
hyper_parallel/distributed_data/indexed_text.py
28
29
30
31
32
33
34
35
36
37
38
39
def _as_1d_long_tensor(value: Any, field: str) -> torch.Tensor:
    """Normalize one source field to a CPU int64 vector."""
    try:
        tensor = torch.as_tensor(value, dtype=torch.int64)
    except (TypeError, ValueError, RuntimeError) as exc:
        raise ValueError(f"Indexed source field {field!r} cannot be converted to an int64 tensor") from exc
    if tensor.ndim != 1 or tensor.numel() < 1:
        raise ValueError(f"Indexed source field {field!r} must be a non-empty one-dimensional sequence")
    return tensor


def _validate_pad_token_id(pad_token_id: int) -> None:
60
61
62
63
64
65
66
67
68
    input_ids = _as_1d_long_tensor(sample["input_ids"], "input_ids")
    labels = _as_1d_long_tensor(sample["labels"], "labels")
    boundaries = torch.as_tensor(sample["cu_seq_lens"])
    if input_ids.shape != labels.shape:
        raise ValueError("Packed input_ids and labels must have identical shapes")
    if boundaries.ndim != 1 or boundaries.dtype not in (torch.int32, torch.int64) or boundaries.numel() < 2:
        raise ValueError("Packed cu_seq_lens must be an integer boundary vector")
    offsets = boundaries.tolist()
    if offsets[0] != 0 or offsets[-1] != input_ids.numel() or any(
 98
 99
100
101
102
103
104
105
106
107
108
    Returns:
        One padded row and its exact source-sample boundaries.
    """
    if not samples:
        raise ValueError("Indexed text packing requires at least one source sample")
    if not isinstance(seq_len, int) or isinstance(seq_len, bool) or seq_len < 1:
        raise ValueError(f"seq_len must be a positive integer, but got {seq_len!r}")
    _validate_pad_token_id(pad_token_id)

    input_parts = []
    label_parts = []
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
    label_parts = []
    sample_lengths = []
    for sample in samples:
        if not isinstance(sample, Mapping):
            raise ValueError(f"Indexed text samples must be mappings, but got {type(sample)}")
        try:
            input_ids = _as_1d_long_tensor(sample["input_ids"], "input_ids")
            labels = _as_1d_long_tensor(sample["labels"], "labels")
        except KeyError as exc:
            raise ValueError(f"Indexed text sample is missing required field {exc.args[0]!r}") from exc
        if input_ids.shape != labels.shape:
            raise ValueError("Indexed text input_ids and labels must have identical shapes")
        input_parts.append(input_ids)
        label_parts.append(labels)
        sample_lengths.append(input_ids.numel())
122
123
124
125
126
127
128
129
130
        sample_lengths.append(input_ids.numel())

    packed_length = sum(sample_lengths)
    if packed_length > seq_len:
        raise ValueError(f"Indexed text bin has {packed_length} tokens, exceeding seq_len={seq_len}")
    input_ids = torch.cat(input_parts)
    labels = torch.cat(label_parts)
    padding_length = seq_len - packed_length
    if padding_length:
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
    Returns:
        Batched token fields and flattened global sequence boundaries.
    """
    if not packed_sequences:
        raise ValueError("Indexed text collation requires at least one packed sequence")
    if any(not isinstance(sequence, Mapping) for sequence in packed_sequences):
        raise ValueError("Every indexed packed sequence must be a mapping")

    input_ids = torch.stack([sequence["input_ids"] for sequence in packed_sequences])
    labels = torch.stack([sequence["labels"] for sequence in packed_sequences])
    if input_ids.shape != labels.shape or input_ids.ndim != 2:
        raise ValueError("Indexed packed input_ids and labels must form equal two-dimensional batches")

    seq_len = input_ids.shape[1]
    boundary_parts = [input_ids.new_zeros(1, dtype=torch.int32)]
    for row_index, sequence in enumerate(packed_sequences):
165
166
167
168
169
170
171
172
173
174
175
176
177
    boundary_parts = [input_ids.new_zeros(1, dtype=torch.int32)]
    for row_index, sequence in enumerate(packed_sequences):
        cu_seq_lens = sequence["cu_seq_lens"]
        if not isinstance(cu_seq_lens, torch.Tensor) or cu_seq_lens.dtype != torch.int32:
            raise ValueError("Indexed packed cu_seq_lens must be an int32 tensor")
        if cu_seq_lens.ndim != 1 or cu_seq_lens.numel() < 2 or int(cu_seq_lens[0]) != 0:
            raise ValueError("Indexed packed cu_seq_lens must be a leading-zero boundary vector")
        if int(cu_seq_lens[-1]) != seq_len:
            raise ValueError("Indexed packed cu_seq_lens must cover the complete padded row")
        boundary_parts.append(cu_seq_lens[1:] + row_index * seq_len)
    return {
        "input_ids": input_ids,
        "labels": labels,
hyper_parallel/distributed_data/locality.py
72
73
74
75
76
77
78
79
80
81
82
) -> DataTopology:
    """Resolve the WORLD-covering topology for independent DP readers."""
    topology = DataTopology.from_mesh(mesh, global_rank=rank, dp_dim_names=dp_dim_names)
    if set(topology.rank_list) != set(range(world_size)):
        raise ValueError("Local balancing requires a root mesh containing every WORLD rank.")
    if any(len(group) != 1 for group in topology.model_parallel_rank_groups):
        raise ValueError("Local balancing currently requires CP/TP/PP/Ulysses and other model parallel sizes = 1.")
    return topology


def _validate_communication_config(backend: str, device: Any, distributed: bool) -> None:
81
82
83
84
85
86
87
88
89
90
91
92
93

def _validate_communication_config(backend: str, device: Any, distributed: bool) -> None:
    """Validate the node-local collective backend and its rank-local device."""
    if backend not in ("gloo", "hccl"):
        raise ValueError("communication_backend must be 'gloo' or 'hccl'.")
    if backend != "hccl":
        return
    if distributed and (device is None or getattr(device, "type", None) != "npu"):
        raise ValueError("communication_backend='hccl' requires an NPU communication_device.")


def _gather_startup_statuses(
        status: Any,
150
151
152
153
154
155
156
157
        if distributed and len(ranks) > 1:
            control_group = dist.new_group(ranks=list(ranks), backend=communication_backend)
            payload_group = control_group
            if communication_backend == "hccl":
                payload_group = dist.new_group(ranks=list(ranks), backend=communication_backend)
        if rank in ranks:
            own_groups = DataGroups(ranks, control_group, payload_group, None, min(ranks), distributed)
    return topology, own_groups
hyper_parallel/distributed_data/metadata.py
56
57
58
59
60
61
62
63
64
            dataloader_kwargs: Additional validated DataLoader execution options.
        """
        getitem = getattr(type(dataset), "__getitem__", None)
        if not callable(getitem) or getitem is Dataset.__getitem__ or not hasattr(dataset, "__len__"):
            raise ValueError("Planned reads require a mapping-style Dataset with __len__ and __getitem__.")
        self._dataset = dataset
        self._indices: list[int] = []
        self._seed = seed
        self._epoch = 0
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
        Returns:
            Mapping from every requested key to its Dataset sample.
        """
        if any(not isinstance(key, SampleKey) for key in sample_keys):
            raise ValueError("sample_keys must contain SampleKey values.")
        if len(sample_keys) != len(set(sample_keys)):
            raise ValueError("sample_keys must not contain duplicates.")
        dataset_size = len(self._dataset)
        invalid_indices = [key.dataset_index for key in sample_keys if key.dataset_index >= dataset_size]
        if invalid_indices:
            raise ValueError(
                f"Sample plan references Dataset indices outside [0, {dataset_size}): {invalid_indices}."
            )
        # DataLoader retains this list, so replace its contents instead of rebinding it.
        self._indices[:] = (key.dataset_index for key in sample_keys)
106
107
108
109
110
111
112
113
114
        for key in sample_keys:
            try:
                payload = next(iterator)
            except StopIteration as exc:
                raise ValueError("Plan-aware DataLoader exhausted before all requested samples were read.") from exc
            samples_by_key[key] = payload
        return samples_by_key

    def state_dict(self) -> dict[str, Any]:
126
127
128
129
130
131
132
133
134
135
            state_dict: State produced by :meth:`state_dict` on this constructor.
        """
        try:
            state = dict(state_dict)
        except Exception as exc:
            raise ValueError(f"Plan-aware DataLoader state is not a mapping: {exc}") from exc
        expected = {
            "dataset_size": len(self._dataset),
            "seed": self._seed,
        }
147
148
149
150
151
152
153
154
        Args:
            epoch: Non-negative Dataset epoch.
        """
        if not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 0:
            raise ValueError(f"epoch must be a non-negative integer, but got {epoch!r}.")
        self._epoch = epoch
        self._worker_generator.manual_seed(self._seed + epoch)

hyper_parallel/distributed_data/model_batch.py
71
72
73
74
75
76
77
78
79
        if isinstance(spec, TensorFieldSpec):
            yield spec
        elif isinstance(spec, Mapping):
            if any(not isinstance(key, str) for key in spec):
                raise ValueError("model_batch_spec mapping keys must be strings.")
            for item in spec.values():
                yield from cls._fields(item)
        elif isinstance(spec, (tuple, list)):
            for item in spec:
78
79
80
81
82
83
84
85
        elif isinstance(spec, (tuple, list)):
            for item in spec:
                yield from cls._fields(item)
        elif spec is not None and not isinstance(spec, (bool, int, float, str)):
            raise ValueError("model_batch_spec leaves must be TensorFieldSpec or static scalar values.")

    def flatten(self, batch: Any) -> list[torch.Tensor]:
        """Validate source fields and return tensors in the declared order.
100
101
102
103
104
105
106
107
108
            for key, item in spec.items():
                yield from cls._flatten(item, value[key])
        elif isinstance(spec, (tuple, list)):
            if type(value) is not type(spec) or len(value) != len(spec):
                raise ValueError("Batch container does not match model_batch_spec.")
            for item, child in zip(spec, value):
                yield from cls._flatten(item, child)
        elif type(value) is not type(spec) or value != spec:
            raise ValueError("Non-tensor batch values must equal their static model_batch_spec values.")
109
110
111
112
113
114
115
116
117

    @staticmethod
    def _validate_tensor(spec: TensorFieldSpec, value: Any) -> torch.Tensor:
        if not isinstance(value, torch.Tensor) or value.layout != torch.strided:
            raise ValueError("model_batch_spec requires a dense tensor at every TensorFieldSpec leaf.")
        if value.dtype != spec.dtype or value.ndim != len(spec.shape) or any(
                dim is not None and dim != actual for dim, actual in zip(spec.shape, value.shape)
        ):
            raise ValueError(f"Tensor shape/dtype {value.shape}/{value.dtype} does not match {spec}.")
132
133
134
135
136
137
138
139
140
        Args:
            dimensions: Dynamic sizes received in declaration order.
        """
        if len(dimensions) != self.dynamic_dimensions or any(dim < 0 for dim in dimensions):
            raise ValueError("Invalid model batch dimension header.")
        pending = iter(dimensions)
        return [tuple(next(pending) if dim is None else dim for dim in field.shape) for field in self.fields]

    def rebuild(self, tensors: list[torch.Tensor]) -> Any:
hyper_parallel/distributed_data/packed_balancing.py
105
106
107
108
109
110
111
112
113
            self._join_worker()
            result = self._result
            self._result = None
            if result is None:
                raise RuntimeError("Local balancing prefetch completed without a batch.")
        except BaseException:
            self.finished = True
            raise
        self._step += 1
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
    def _data_stream_context(self) -> Any:
        """Reuse the data communication stream independently of the training stream."""
        device = getattr(self._transport, "communication_device", None)
        if device is None:
            return nullcontext()
        device = torch.device(device)
        if device.type == "cpu":
            return nullcontext()
        accelerator = getattr(torch, device.type)
        accelerator.set_device(device)
        if self._data_stream is None:
            self._data_stream = accelerator.Stream(device=device)
        # Both launch and completion use this stream; neither waits on the
        # training stream just to move independent next-step data.
        return accelerator.stream(self._data_stream)

    def prefetch_plan(self) -> None:
        """Gather metadata and start planning at a rank-consistent compute boundary."""
        if self._iterator is None:
            self._iterator = _LocalBalancingIterator(self)
        self._iterator.prefetch_plan()

    def prefetch(self) -> None:
        """Launch next-step exchange before synchronizing current-step computation."""
        if self._iterator is None:
            self._iterator = _LocalBalancingIterator(self)
        self._iterator.prefetch()

    def __len__(self) -> int:
        """Return the source length when available."""
        return len(self.local_dataloader)
322
323
324
325
326
327
328
329
330
        if self._iterator is not None:
            self._iterator.close()
            self._iterator = None
        if self._device_prefetch is not None:
            self._device_prefetch.close()
        self.last_host_batch = None
        setter = getattr(self.local_dataloader, "set_epoch", None)
        if callable(setter):
            setter(epoch)
347
348
349
350
351
352
353
354
355
        # Re-entering an unfinished iterator must not drop its speculative batch
        # after the stateful source has already advanced to the following step.
        if self._iterator is None or self._iterator.finished:
            if self._iterator is not None:
                self._iterator.close()
            self._iterator = _LocalBalancingIterator(self)
        return self._iterator

    def wait_for_prefetch(self) -> None:
393
394
395
396
397
398
399
400
401
402
403
404
405
            self, local_payloads: dict[SampleKey, Any], plan: DistributedPackingPlan, stats: dict[str, Any],
    ) -> _PendingBatch:
        """Start payload exchange for moved samples while retaining local destinations."""
        if stats["moved_samples"]:
            outgoing: dict[int, list[tuple[SampleKey, Any]]] = {}
            retained_payloads = {}
            for data_rank, local_batch in enumerate(plan.local_batches):
                target_rank = self.group_ranks[data_rank]
                owned = [
                    (key, local_payloads[key])
                    for packing_bin in local_batch
                    for key in packing_bin.sample_keys
                    if key.reader_rank == self._global_rank
403
404
405
406
407
408
409
410
411
412
413
414
415
416
                    for packing_bin in local_batch
                    for key in packing_bin.sample_keys
                    if key.reader_rank == self._global_rank
                ]
                if target_rank == self._global_rank:
                    retained_payloads.update(owned)
                else:
                    outgoing[target_rank] = owned
            prepared = self._transport.prepare_exchange(outgoing)
            work = self._transport.begin_exchange_prepared(prepared)
        else:
            # Every rank sees the same plan and skips an unchanged exchange.
            retained_payloads = local_payloads
            work = None
419
420
421
422
423
424
425
426
427
    def _finish_batch(self, pending: _PendingBatch) -> _LocalBatch:
        """Complete payload exchange, pack and collate the local batch, then stage H2D."""
        received = pending.retained_payloads
        if pending.payload_work is not None:
            received.update(pending.payload_work.wait())
        target = pending.plan.local_batch_for(self._data_rank)
        host_batch = self._collate_fn([
            self._pack_fn([received[key] for key in packing_bin.sample_keys], self.config.seq_len)
            for packing_bin in target
431
432
433
434
435
436
437
438
439

    def _read_step(self, raw_bins: Sequence[Sequence[Any]], step: int) -> tuple[tuple[Any, ...], dict[SampleKey, Any]]:
        """Bind raw source bins to keyed metadata and cached payloads for one step."""
        if len(raw_bins) != self.config.local_batch_size or any(not raw_bin for raw_bin in raw_bins):
            raise ValueError("Each source step must contain local_batch_size non-empty raw-sample bins.")
        key_bins = []
        entries = []
        payloads = {}
        for raw_bin in raw_bins:
440
441
442
443
444
445
446
447
448
            keys = []
            for sample in raw_bin:
                item_metadata = self._metadata_fn(sample)
                if not isinstance(item_metadata, SampleMetadata):
                    raise ValueError("metadata_fn must return SampleMetadata.")
                key = SampleKey(self._global_rank, step, len(entries))
                entries.append(replace(item_metadata, key=key))
                payloads[key] = sample
                keys.append(key)
510
511
512
513
514
515
516
517
518
519
520
            for packing_bin in local_batch:
                for key in packing_bin.sample_keys:
                    source_index = source_indices[key.reader_rank]
                    if source_index != data_rank:
                        send_samples[source_index] += 1
                        recv_samples[data_rank] += 1
                        moved += 1
        mean_before = math.fsum(before) / len(before)
        mean_after = math.fsum(after) / len(after)
        max_before, max_after = max(before), max(after)
        return {
579
580
581
582
583
584
585
586
587
        Iteration returns device-ready batches. last_host_batch retains the CPU
        view for metering without a device-to-host copy.
    """
    if config.balance_window_size != 1:
        raise ValueError("balance_window_size > 1 requires a native batch_sampler.")
    if config.device_prefetch_depth != 1:
        raise ValueError("Configurable device_prefetch_depth requires a native batch_sampler.")
    if config.repack_sequences:
        raise ValueError("repack_sequences requires a native batch_sampler.")
589
590
591
592
593
594
595
596
597
598
599
600
601
    balancing_algorithm = resolve_balancing_algorithm(balancing_algorithm)
    device = _resolve_device(device, communication_backend=config.communication_backend)
    device_prefetch = _create_device_prefetcher(device)
    if not all(callable(callback) for callback in (metadata_fn, pack_fn, collate_fn)):
        raise ValueError("metadata_fn, pack_fn and collate_fn must be callable.")
    if not hasattr(local_dataloader, "__iter__"):
        raise ValueError("local_dataloader must be iterable.")
    if config.dataset_reader_ranks is not None or config.planner_rank is not None:
        raise ValueError("Node-local balancing assigns readers and planners automatically.")
    PackingConstraints(config.seq_len, config.oversized_policy, config.packing_budgets)
    communication_device = device if config.communication_backend == "hccl" else None
    topology, groups = _create_locality_groups(
        mesh,
hyper_parallel/distributed_data/planner.py
83
84
85
86
87
88
89
90
91
                ("seq_len", seq_len),
                ("local_batch_size", local_batch_size),
        ):
            if not isinstance(value, int) or isinstance(value, bool) or value < 1:
                raise ValueError(f"{name} must be a positive integer, but got {value!r}.")
        if (
                not isinstance(min_balance_gain, (int, float))
                or isinstance(min_balance_gain, bool)
                or not math.isfinite(min_balance_gain)
90
91
92
93
94
95
96
97
98
                or isinstance(min_balance_gain, bool)
                or not math.isfinite(min_balance_gain)
                or not 0.0 <= min_balance_gain < 1.0
        ):
            raise ValueError("min_balance_gain must be in [0, 1).")
        self.data_parallel_size = data_parallel_size
        self.seq_len = seq_len
        self.local_batch_size = local_batch_size
        self.oversized_policy = oversized_policy
132
133
134
135
136
137
138
139
140
141
142
143
            Padding workload, or zero when the model has no padding estimate.
        """
        estimate = getattr(self.cost_model, "padding_cost", lambda _: WorkloadCost())
        if not callable(estimate):
            raise ValueError("cost_model.padding_cost must be callable")
        cost = estimate(padding_tokens)
        if not isinstance(cost, WorkloadCost):
            raise ValueError("cost_model.padding_cost must return WorkloadCost")
        return cost

    def plan(
            self,
238
239
240
241
242
243
244
245
246
            per-round objectives so improvements do not hide a slower window.
        """
        rounds, remainder = divmod(len(reference_bins), self.distributed_bin_count)
        if not rounds or remainder:
            raise ValueError("A balancing window must contain complete distributed micro-batches.")
        plan = self.plan(
            samples, reference_bins=reference_bins, step=step, target_slot_count=self.data_parallel_size * rounds,
        )
        return tuple(
287
288
289
290
291
292
293
294
295
            if not isinstance(item.key, SampleKey):
                raise ValueError("Planner samples must have a SampleKey.")
            cost = self.cost_model(item)
            if not isinstance(cost, WorkloadCost):
                raise ValueError(f"cost_model must return WorkloadCost for sample {item.key}, but got {type(cost)}.")
            # Preserve physical metadata without replaying recursive feature validation.
            estimated_item = copy(item)
            object.__setattr__(estimated_item, "cost", cost)
            estimated_samples.append(estimated_item)
359
360
361
362
363
364
365
366
367
        if (
                not isinstance(score, (int, float)) or isinstance(score, bool)
                or not math.isfinite(score) or score < 0.0
        ):
            raise ValueError("balancing_algorithm.objective must return a finite, non-negative number.")
        return float(score)


__all__ = ["DynamicPackingPlanner", "OversizedPolicy"]
hyper_parallel/distributed_data/schema.py
27
28
29
30
31
32
33
34
35
36
37
38

def _validate_nonnegative_costs(values: Mapping[str, float], name: str) -> None:
    """Validate named finite, non-negative scalar costs or budgets."""
    if not isinstance(values, Mapping):
        raise ValueError(f"{name} must be a mapping of non-empty strings to finite, non-negative numbers.")
    for key, value in values.items():
        if not isinstance(key, str) or not key:
            raise ValueError(f"{name} keys must be non-empty strings, but got {key!r}.")
        if (
                type(value) not in (int, float)
                or (isinstance(value, float) and not math.isfinite(value))
                or value < 0
36
37
38
39
40
41
42
43
44
                type(value) not in (int, float)
                or (isinstance(value, float) and not math.isfinite(value))
                or value < 0
        ):
            raise ValueError(f"{name}[{key!r}] must be finite and non-negative, but got {value!r}.")


def _validate_feature(value: Any, name: str, ancestors: set[int]) -> None:
    """Reject tensors, arbitrary objects, cycles, and non-finite feature values."""
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

def _validate_feature(value: Any, name: str, ancestors: set[int]) -> None:
    """Reject tensors, arbitrary objects, cycles, and non-finite feature values."""
    if value is None:
        return
    if type(value) not in (bool, int, float, str, dict, list, tuple):
        raise ValueError(f"{name} must contain only CPU-serializable basic types, but got {type(value)}.")
    if isinstance(value, (bool, int, str)):
        return
    if isinstance(value, float):
        if not math.isfinite(value):
            raise ValueError(f"{name} must be finite, but got {value!r}.")
        return
    if id(value) in ancestors:
        raise ValueError(f"{name} must not contain reference cycles.")
    ancestors.add(id(value))
    if isinstance(value, dict):
        for key, child in value.items():
            if not isinstance(key, str):
                raise ValueError(f"{name} keys must be strings, but got {key!r}.")
            _validate_feature(child, f"{name}[{key!r}]", ancestors)
    else:
        for index, child in enumerate(value):
            _validate_feature(child, f"{name}[{index}]", ancestors)
    ancestors.remove(id(value))


@dataclass(frozen=True)
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
                    or isinstance(value, bool)
                    or not math.isfinite(value)
                    or value < 0
            ):
                raise ValueError(f"WorkloadCost.{name} must be finite and non-negative, but got {value!r}.")

    def __add__(self, other: "WorkloadCost") -> "WorkloadCost":
        """Add corresponding stage costs."""
        if not isinstance(other, WorkloadCost):
            return NotImplemented
        return WorkloadCost(
            io=self.io + other.io,
            transform=self.transform + other.transform,
            encoder=self.encoder + other.encoder,
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124

    @property
    def dominant(self) -> float:
        """Return the largest stage cost."""
        return max(self.as_tuple())

    @property
    def total(self) -> float:
        """Return the sum of stage costs."""
        return sum(self.as_tuple())

    def as_tuple(self) -> tuple[float, ...]:
        """Return components in deterministic planner order."""
        return (self.io, self.transform, self.encoder, self.llm, self.memory, self.communication)

    def as_dict(self) -> dict[str, float]:
        """Return a serializable component mapping."""
        return {
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179

    def __post_init__(self) -> None:
        """Validate identity, workload, and lightweight packing metadata."""
        if not isinstance(self.pack_tokens, int) or isinstance(self.pack_tokens, bool) or self.pack_tokens < 1:
            raise ValueError(f"SampleMetadata.pack_tokens must be a positive integer, but got {self.pack_tokens!r}.")
        if not isinstance(self.cost, WorkloadCost):
            raise ValueError(f"SampleMetadata.cost must be WorkloadCost, but got {type(self.cost)}.")
        if self.sample_id is not None:
            valid_id = isinstance(self.sample_id, (int, str)) and not isinstance(self.sample_id, bool)
            if not valid_id or (isinstance(self.sample_id, int) and self.sample_id < 0):
                raise ValueError("SampleMetadata.sample_id must be a non-negative integer, non-empty string, or None.")
            if isinstance(self.sample_id, str) and not self.sample_id:
                raise ValueError("SampleMetadata.sample_id string must not be empty.")
        if not isinstance(self.features, dict):
            raise ValueError("SampleMetadata.features must be a dictionary.")
        _validate_feature(self.features, "SampleMetadata.features", set())
        if not isinstance(self.packing_costs, dict):
            raise ValueError("SampleMetadata.packing_costs must be a dictionary.")
        _validate_nonnegative_costs(self.packing_costs, "SampleMetadata.packing_costs")
        if not isinstance(self.key, (SampleKey, type(None))):
            raise ValueError("SampleMetadata.key must be a SampleKey or None.")
199
200
201
202
203
204
205
206
207
        """Validate non-negative routing coordinates."""
        for name in ("reader_rank", "dataset_index", "global_sample_position", "sequence_index"):
            value = getattr(self, name)
            if not isinstance(value, int) or isinstance(value, bool) or value < 0:
                raise ValueError(f"SampleKey.{name} must be a non-negative integer, but got {value!r}.")


@dataclass(frozen=True)
class PackingConstraints:
222
223
224
225
226
227
228
229
230
231

    def __post_init__(self) -> None:
        """Validate token and stage capacities and copy the caller's mapping."""
        if not isinstance(self.seq_len, int) or isinstance(self.seq_len, bool) or self.seq_len < 1:
            raise ValueError(f"seq_len must be a positive integer, but got {self.seq_len!r}.")
        if self.oversized_policy not in ("error", "single"):
            raise ValueError("oversized_policy must be 'error' or 'single'.")
        budgets = {} if self.packing_budgets is None else self.packing_budgets
        _validate_nonnegative_costs(budgets, "packing_budgets")
        object.__setattr__(self, "packing_budgets", dict(budgets))
236
237
238
239
240
241
242
243
244
245
246
247
248
        Args:
            item: Sample metadata with physical packing footprints.
        """
        for name, budget in self.packing_budgets.items():
            if name not in item.packing_costs:
                raise ValueError(f"Sample {item.key} is missing packing_costs[{name!r}] required by packing_budgets.")
            value = item.packing_costs[name]
            if value > budget:
                raise ValueError(
                    f"Sample {item.key} requires packing_costs[{name!r}]={value}, "
                    f"exceeding packing_budgets[{name!r}]={budget}. Stage budgets do not permit singleton overflow."
                )
        if item.pack_tokens > self.seq_len and self.oversized_policy == "error":
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
    def __post_init__(self, validate: bool) -> None:
        """Validate one non-empty sequence-packing bin."""
        if not validate:
            return
        if not self.sample_keys:
            raise ValueError("PackingBinPlan.sample_keys must not be empty.")
        if any(not isinstance(key, SampleKey) for key in self.sample_keys):
            raise ValueError("PackingBinPlan.sample_keys must contain SampleKey values.")
        if len(self.sample_keys) != len(set(self.sample_keys)):
            raise ValueError("PackingBinPlan.sample_keys must not contain duplicates.")
        if not isinstance(self.pack_tokens, int) or isinstance(self.pack_tokens, bool) or self.pack_tokens < 1:
            raise ValueError(f"PackingBinPlan.pack_tokens must be positive, but got {self.pack_tokens!r}.")
        if not isinstance(self.oversized, bool):
            raise ValueError(f"PackingBinPlan.oversized must be boolean, but got {self.oversized!r}.")


@dataclass(frozen=True)
class DistributedPackingPlan:
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
    def __post_init__(self, validate: bool) -> None:
        """Validate plan dimensions, slots, and unique sample assignments."""
        if not validate:
            return
        self._validate_dimensions()
        keys = self._validate_local_batches()
        if len(keys) != len(set(keys)):
            raise ValueError("A sample key may appear only once in a distributed packing plan.")

    def _validate_dimensions(self) -> None:
        """Require valid step and sequence length plus matching non-empty rank batches."""
        if not isinstance(self.step, int) or isinstance(self.step, bool) or self.step < 0:
            raise ValueError(f"DistributedPackingPlan.step must be non-negative, but got {self.step!r}.")
        if not isinstance(self.seq_len, int) or isinstance(self.seq_len, bool) or self.seq_len < 1:
            raise ValueError(f"DistributedPackingPlan.seq_len must be positive, but got {self.seq_len!r}.")
        if not self.local_batches:
            raise ValueError("DistributedPackingPlan.local_batches must not be empty.")
        if len(self.rank_costs) != len(self.local_batches):
            raise ValueError("DistributedPackingPlan.rank_costs must match local_batches.")
        if any(not isinstance(cost, WorkloadCost) for cost in self.rank_costs):
            raise ValueError("DistributedPackingPlan.rank_costs must contain WorkloadCost values.")
        batch_sizes = {len(local_batch) for local_batch in self.local_batches}
        if len(batch_sizes) != 1 or 0 in batch_sizes:
            raise ValueError("DistributedPackingPlan local batches must have equal non-zero sizes.")

    @property
    def data_parallel_size(self) -> int:
        """Return the number of target Data Constructor ranks."""
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407

    @property
    def local_batch_size(self) -> int:
        """Return the number of packed sequences per target rank."""
        return len(self.local_batches[0])

    def _validate_local_batches(self) -> list[SampleKey]:
        keys = []
        for local_batch in self.local_batches:
            self._validate_local_batch(local_batch)
            keys.extend(key for packing_bin in local_batch for key in packing_bin.sample_keys)
        return keys

    def _validate_local_batch(self, local_batch: tuple[PackingBinPlan, ...]) -> None:
        if any(not isinstance(packing_bin, PackingBinPlan) for packing_bin in local_batch):
            raise ValueError("DistributedPackingPlan local batches must contain PackingBinPlan values.")
        for packing_bin in local_batch:
            if packing_bin.pack_tokens > self.seq_len and not packing_bin.oversized:
                raise ValueError("A non-oversized packing bin exceeds seq_len.")
            if packing_bin.oversized and len(packing_bin.sample_keys) != 1:
                raise ValueError("An oversized sample must occupy its packing bin alone.")

    @property
    def selected_keys(self) -> tuple[SampleKey, ...]:
        """Return all selected keys in deterministic constructor order."""
422
423
424
425
426
427
428
429
430
            Target rank's local packing bins.
        """
        valid_rank = isinstance(data_rank, int) and not isinstance(data_rank, bool)
        if not valid_rank or not 0 <= data_rank < self.data_parallel_size:
            raise ValueError(f"data_rank must be in [0, {self.data_parallel_size}), but got {data_rank!r}.")
        return self.local_batches[data_rank]

    def local_sample_keys(self, data_rank: int) -> tuple[SampleKey, ...]:
        """Return the sample keys assigned to one target data rank.
hyper_parallel/distributed_data/topology.py
47
48
49
50
51
52
53
54
55
56
57
    rank_list = getattr(mesh, "rank_list", None)
    if mesh_shape is not None and rank_list is not None:
        return tuple(int(size) for size in mesh_shape), tuple(int(rank) for rank in rank_list)

    mesh_tensor = getattr(mesh, "mesh", None)
    if mesh_tensor is None:
        raise ValueError("mesh must expose mesh_shape/rank_list or a Torch mesh tensor.")
    return (
        tuple(int(size) for size in mesh_tensor.shape),
        tuple(int(rank) for rank in mesh_tensor.reshape(-1).tolist()),
    )
 94
 95
 96
 97
 98
 99
100
101
102
            Derived data topology.
        """
        mesh_dim_names = getattr(mesh, "mesh_dim_names", None)
        if not mesh_dim_names:
            raise ValueError("Distributed data loading requires a mesh with named dimensions.")
        rank = global_rank
        if rank is None:
            rank = dist.get_rank() if dist.is_available() and dist.is_initialized() else 0
        mesh_shape, rank_list = _mesh_layout(mesh)
134
135
136
137
138
139
140
141
142
143
144
        selected_dp_names = dp_dim_names
        if selected_dp_names is None:
            selected_dp_names = tuple(name for name in mesh_dim_names if name in _DEFAULT_DP_DIM_NAMES)
        if len(set(selected_dp_names)) != len(selected_dp_names):
            raise ValueError(f"dp_dim_names must be unique, but got {selected_dp_names}.")
        if any(name not in mesh_dim_names for name in selected_dp_names):
            raise ValueError(f"dp_dim_names {selected_dp_names} must be present in {mesh_dim_names}.")

        coordinate = _unflatten_coordinate(rank_list.index(global_rank), mesh_shape)
        dp_indices = tuple(mesh_dim_names.index(name) for name in selected_dp_names)
        dp_shape = tuple(mesh_shape[index] for index in dp_indices)
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
            not isinstance(size, int) or isinstance(size, bool) or size < 1
            for size in mesh_shape
        )
        if not mesh_shape or invalid_sizes:
            raise ValueError(f"mesh_shape must contain positive integers, but got {mesh_shape}.")
        if len(mesh_shape) != len(mesh_dim_names):
            raise ValueError("mesh_shape and mesh_dim_names must have the same length.")
        if len(set(mesh_dim_names)) != len(mesh_dim_names):
            raise ValueError(f"mesh_dim_names must be unique, but got {mesh_dim_names}.")
        if math.prod(mesh_shape) != len(rank_list) or len(set(rank_list)) != len(rank_list):
            raise ValueError("rank_list must uniquely fill every mesh position.")
        if global_rank not in rank_list:
            raise ValueError(f"global_rank {global_rank} is not present in rank_list {rank_list}.")

    @staticmethod
    def _build_constructor_ranks(
            mesh_shape: tuple[int, ...],
hyper_parallel/distributed_data/transport.py
 94
 95
 96
 97
 98
 99
100
101
102
            return self.completed_payloads
        if self.data_work is not None:
            self.data_work.wait()
        if self.received_tensor is None:
            received_bytes = b""
        else:
            received_bytes = self.received_tensor.cpu().numpy().tobytes()
        self.completed_payloads = _decode_received_payloads(received_bytes, self.output_splits)
        return self.completed_payloads
107
108
109
110
111
112
113
114
115


def _control_backend(group: Any, backend: str | None = None) -> str:
    if backend is not None:
        return backend.lower()
    try:
        return str(dist.get_backend(group)).lower()
    except (RuntimeError, ValueError):
        # Lightweight unit-test groups and single-process fakes represent Gloo
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
        return "gloo"


def _require_control_device(device: Any, backend: str) -> torch.device:
    if device is None:
        raise ValueError(f"{backend} control collectives require an accelerator communication_device.")
    resolved = torch.device(device)
    expected = "npu" if "hccl" in backend else "cuda"
    if resolved.type != expected:
        raise ValueError(f"{backend} control collectives require a {expected} device, got {resolved}.")
    return resolved


def _encode_control(value: Any, device: Any) -> tuple[torch.Tensor, int]:
    encoded = pickle.dumps(value, protocol=pickle.HIGHEST_PROTOCOL)
158
159
160
161
162
163
164
165
166

    Returns:
        Gathered values in process-group rank order.
    """
    return _begin_all_gather_control_object(value, group=group, device=device, backend=backend).wait()


def _begin_all_gather_control_object(
        value: Any,
171
172
173
174
175
176
177
178
179
180
181
) -> ControlGatherWork:
    """Exchange lengths on the caller, then launch accelerator bytes without decoding."""
    effective_backend = _control_backend(group, backend)
    if not _is_accelerator_backend(effective_backend):
        gathered = [None] * dist.get_world_size(group)
        dist.all_gather_object(gathered, value, group=group)
        return ControlGatherWork(completed_values=tuple(gathered))
    resolved = _require_control_device(device, effective_backend)
    encoded, encoded_size = _encode_control(value, resolved)
    local_size = torch.tensor([encoded_size], dtype=torch.int64, device=resolved)
    sizes = [torch.empty_like(local_size) for _ in range(dist.get_world_size(group))]
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232

    Returns:
        The source value on every participating rank.
    """
    effective_backend = _control_backend(group)
    if not _is_accelerator_backend(effective_backend):
        payload = [value]
        dist.broadcast_object_list(payload, src=src, group=group)
        return payload[0]
    resolved = _require_control_device(device, effective_backend)
    rank = dist.get_rank()
    encoded, encoded_size = _encode_control(value, resolved) if rank == src else (
        torch.zeros((1,), dtype=torch.uint8, device=resolved), 0
    )
    size = torch.tensor([encoded_size], dtype=torch.int64, device=resolved)
    dist.broadcast(size, src=src, group=group)
    target_size = int(size.item())
    if encoded.numel() < max(1, target_size):
        encoded = torch.zeros((max(1, target_size),), dtype=torch.uint8, device=resolved)
    dist.broadcast(encoded, src=src, group=group)
    return _decode_control(encoded, target_size)


def create_data_groups(
        topology: DataTopology,
257
258
259
260
261
262
263
264
265
        Groups relevant to the current rank.
    """
    data_plane_ranks = tuple(sorted(set(dataset_reader_ranks) | set(topology.constructor_ranks)))
    if planner_rank not in data_plane_ranks:
        raise ValueError(f"planner_rank {planner_rank} must belong to data-plane ranks {data_plane_ranks}.")
    distributed = dist.is_available() and dist.is_initialized()
    if not distributed:
        return _single_rank_data_groups(topology, data_plane_ranks, planner_rank)
    _validate_distributed_topology(topology)
281
282
283
284
285
286
287
288
289
    )
    tensor_backend = _resolve_model_tensor_backend(communication_device, payload_backend)
    model_device = torch.device(model_device) if model_device is not None else None
    if model_device is not None and model_device.type in ("cuda", "npu"):
        tensor_backend = "nccl" if model_device.type == "cuda" else "hccl"
    model_parallel_group, model_parallel_tensor_group = _create_model_parallel_process_groups(
        topology, cpu_backend, tensor_backend,
    )
    return DataGroups(
297
298
299
300
301
302
303
304
305
        data_plane_ranks: tuple[int, ...],
        planner_rank: int,
) -> DataGroups:
    if len(topology.rank_list) != 1:
        raise ValueError("torch.distributed must be initialized for a mesh containing more than one rank.")
    return DataGroups(data_plane_ranks, None, None, None, planner_rank, False, None)


def _validate_distributed_topology(topology: DataTopology) -> None:
304
305
306
307
308
309
310
311
312
313
314
315
316
317

def _validate_distributed_topology(topology: DataTopology) -> None:
    world_ranks = tuple(range(dist.get_world_size()))
    if set(topology.rank_list) != set(world_ranks):
        raise ValueError(
            "The distributed data root mesh must contain every torch.distributed world rank so all ranks create "
            "service groups in the same order."
        )
    if dist.get_rank() != topology.global_rank:
        raise ValueError("Topology global_rank does not match torch.distributed rank.")


def _validate_group_backends(
        cpu_backend: str,
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
        enable_payload_exchange: bool,
) -> None:
    """Validate control and payload backend options before creating process groups."""
    if not isinstance(cpu_backend, str) or not cpu_backend:
        raise ValueError("cpu_backend must be a non-empty backend name.")
    if cpu_backend.lower() not in ("gloo", "hccl"):
        raise ValueError("cpu_backend must be 'gloo' or 'hccl'.")
    if payload_backend is not None and (not isinstance(payload_backend, str) or not payload_backend):
        raise ValueError("payload_backend must be a non-empty backend name or None.")
    if not isinstance(enable_payload_exchange, bool):
        raise ValueError("enable_payload_exchange must be boolean.")


def _resolve_payload_backend(
        *,
349
350
351
352
353
354
355
356
357


def _default_payload_backend(device: torch.device | None, cpu_backend: str) -> str:
    if device is not None and device.type != "cpu":
        return str(dist.get_backend())
    return cpu_backend


def _validate_payload_device(
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
) -> None:
    """Require the communication device type expected by the payload backend."""
    accelerator_backend = "hccl" in normalized_backend or "nccl" in normalized_backend
    if accelerator_backend and (device is None or device.type == "cpu"):
        raise ValueError(
            f"Payload backend {effective_backend!r} requires a rank-local accelerator communication_device."
        )
    expected_device_type = None
    if "nccl" in normalized_backend:
        expected_device_type = "cuda"
    elif "hccl" in normalized_backend:
        expected_device_type = "npu"
    if expected_device_type is not None and device is not None and device.type != expected_device_type:
        raise ValueError(
            f"Payload backend {effective_backend!r} requires a {expected_device_type} device, but got {device}."
        )
    if not accelerator_backend and device is not None and device.type != "cpu":
        raise ValueError(f"Payload backend {effective_backend!r} cannot exchange tensors on device {device}.")


def _create_data_plane_process_groups(
        *,
393
394
395
396
397
398
399
400
401
    created_control_group = dist.new_group(ranks=list(data_plane_ranks), backend=cpu_backend)
    is_member = topology.global_rank in data_plane_ranks
    control_group = created_control_group if is_member else None
    if not enable_payload_exchange:
        return control_group, None
    if reuse_control_group:
        return control_group, control_group
    created_payload_group = dist.new_group(ranks=list(data_plane_ranks), backend=payload_backend)
    return control_group, created_payload_group if is_member else None
410
411
412
413
414
415
416
417
418
        return None
    candidate = payload_backend or str(dist.get_backend())
    normalized = candidate.lower()
    if "hccl" not in normalized and "nccl" not in normalized:
        return None
    return candidate


def _create_model_parallel_process_groups(
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
    model_parallel_tensor_group = None
    for rank_group in topology.model_parallel_rank_groups:
        if len(rank_group) == 1:
            continue
        is_member = topology.global_rank in rank_group
        created_group = dist.new_group(ranks=list(rank_group), backend=cpu_backend)
        if is_member:
            model_parallel_group = created_group
        if tensor_backend is not None:
            created_tensor_group = dist.new_group(ranks=list(rank_group), backend=tensor_backend)
            if is_member:
                model_parallel_tensor_group = created_tensor_group
    return model_parallel_group, model_parallel_tensor_group


def _encode_payload_segment(items: Sequence[tuple[SampleKey, Any]]) -> bytes:
469
470
471
472
473
474
475
476
477
478
479
480
481
482
        # Preserve compatibility with routes produced before the tensor-buffer
        # codec was introduced.  This path is used only for scalar-only routes.
        return pickle.loads(frame)
    if len(frame) < _PAYLOAD_HEADER.size:
        raise ValueError("Payload frame is truncated before its header.")
    _, metadata_size, binary_size = _PAYLOAD_HEADER.unpack_from(frame)
    payload_offset = _PAYLOAD_HEADER.size
    payload_size = metadata_size + binary_size
    if payload_offset + payload_size != len(frame):
        raise ValueError(
            "Payload frame size does not match its metadata and binary descriptors."
        )
    metadata_end = payload_offset + metadata_size
    encoded_items = pickle.loads(frame[payload_offset:metadata_end])
513
514
515
516
517
518
519
520
521
    """Replace binary leaves with descriptors and append their bytes."""
    if torch.is_tensor(value):
        tensor = value.detach()
        if tensor.device.type != "cpu":
            tensor = tensor.cpu()
        tensor = tensor.contiguous()
        # Flatten first: PyTorch does not permit a scalar tensor to change
        # dtype via ``view`` directly because it has no dimension to resize.
        raw = tensor.reshape(-1).view(torch.uint8).numpy().tobytes()
548
549
550
551
552
553
554
555
        tag = "__hp_tuple__" if isinstance(value, tuple) else "__hp_list__"
        return (tag, tuple(encoded_items)), binary_offset
    if value is None or isinstance(value, (bool, int, float, str)):
        return ("__hp_value__", value), binary_offset
    raise TypeError(
        "Payload tensor-buffer codec does not support a non-metadata value of "
        f"type {type(value).__qualname__}."
    )
557
558
559
560
561
562
563
564
565

def _decode_payload_value(schema: Any, binary: bytearray) -> Any:
    """Reconstruct one sample from metadata descriptors and binary storage."""
    if not isinstance(schema, tuple) or not schema:
        raise ValueError("Payload metadata contains an invalid schema node.")
    tag = schema[0]
    if tag == _TENSOR_TAG:
        return _decode_tensor_payload(schema, binary)
    if tag in (_BYTES_TAG, _BYTEARRAY_TAG):
574
575
576
577
578
579
580
581
582
    if tag == "__hp_list__":
        return [_decode_payload_value(child, binary) for child in schema[1]]
    if tag == "__hp_value__":
        return schema[1]
    raise ValueError(f"Unknown payload metadata schema tag: {tag!r}.")


def _decode_tensor_payload(schema: tuple, binary: bytearray) -> torch.Tensor:
    """Reconstruct a tensor view after validating its descriptor."""
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598

def _decode_tensor_payload(schema: tuple, binary: bytearray) -> torch.Tensor:
    """Reconstruct a tensor view after validating its descriptor."""
    if len(schema) != 5:
        raise ValueError("Tensor payload descriptor has an invalid field count.")
    _, dtype_name, shape, offset, size = schema
    dtype = _payload_dtype(dtype_name)
    shape = tuple(shape)
    if any(not isinstance(dim, int) or dim < 0 for dim in shape):
        raise ValueError(f"Tensor payload descriptor has an invalid shape: {shape!r}.")
    if not isinstance(offset, int) or not isinstance(size, int) or offset < 0 or size < 0:
        raise ValueError("Tensor payload descriptor has an invalid byte range.")
    end = offset + size
    if end > len(binary):
        raise ValueError("Tensor payload descriptor points outside the binary buffer.")
    element_count = 1
    for dim in shape:
        element_count *= dim
    expected_size = element_count * torch.empty((), dtype=dtype).element_size()
596
597
598
599
600
601
602
603
604
605
606
607
    for dim in shape:
        element_count *= dim
    expected_size = element_count * torch.empty((), dtype=dtype).element_size()
    if expected_size != size:
        raise ValueError(
            f"Tensor payload byte size mismatch: expected {expected_size}, got {size}."
        )
    if size == 0:
        return torch.empty(shape, dtype=dtype)
    tensor = torch.frombuffer(memoryview(binary)[offset:end], dtype=dtype, count=element_count)
    return tensor.reshape(shape)

608
609
610
611
612
613
614
615
616
617
618
619
620
621

def _decode_bytes_payload(schema: tuple, binary: bytearray) -> bytes | bytearray:
    """Reconstruct a byte sequence after validating its descriptor."""
    if len(schema) != 3:
        raise ValueError("Byte payload descriptor has an invalid field count.")
    tag, offset, size = schema
    if not isinstance(offset, int) or not isinstance(size, int) or offset < 0 or size < 0:
        raise ValueError("Byte payload descriptor has an invalid byte range.")
    end = offset + size
    if end > len(binary):
        raise ValueError("Byte payload descriptor points outside the binary buffer.")
    raw = bytes(binary[offset:end])
    return bytearray(raw) if tag == _BYTEARRAY_TAG else raw

622
623
624
625
626
627
628
629
630
631
632
633

def _payload_dtype(dtype_name: Any) -> torch.dtype:
    """Resolve the stable string form emitted by ``str(torch.dtype)``."""
    if not isinstance(dtype_name, str) or not dtype_name.startswith("torch."):
        raise ValueError(f"Payload tensor descriptor has an invalid dtype: {dtype_name!r}.")
    dtype = getattr(torch, dtype_name[6:], None)
    if not isinstance(dtype, torch.dtype):
        raise ValueError(f"Unsupported payload tensor dtype: {dtype_name!r}.")
    return dtype


def _exchange_payload_sizes(
641
642
643
644
645
646
647
648
649
    size_input = torch.tensor(tuple(input_splits), dtype=torch.int64, device=size_device)
    size_output = torch.empty_like(size_input)
    size_work = dist.all_to_all_single(size_output, size_input, group=control_group, async_op=True)
    if size_work is not None:
        size_work.wait()
    return [int(size) for size in size_output.tolist()]


def _decode_received_payloads(
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765

        Returns:
            Whether every participating rank supplied ``True``.
        """
        if len(self._ranks) == 1:
            return value
        backend = _control_backend(self._control_group)
        flag_device = (
            _require_control_device(self._communication_device, backend)
            if _is_accelerator_backend(backend)
            else torch.device("cpu")
        )
        flag = torch.tensor([int(value)], dtype=torch.int32, device=flag_device)
        dist.all_reduce(flag, op=dist.ReduceOp.MIN, group=self._control_group)
        return bool(flag.item())

    def gather_object_to_planner(self, value: Any) -> tuple[Any, ...] | None:
        """Gather Dataset Reader metadata only on the Planner rank.
783
784
785
786
787
788
789
790
791
            The Planner value on every data-plane rank.
        """
        if len(self._ranks) == 1:
            return value
        return broadcast_control_object(
            value if self._global_rank == self._planner_rank else None,
            src=self._planner_rank,
            group=self._control_group,
            device=self._communication_device,
869
870
871
872
873
874
875
876
877
                    prepared.input_splits,
                ),
            )
        if self._payload_group is None:
            raise ValueError("Sample payload exchange requires a payload process group.")

        input_splits = list(prepared.input_splits)
        output_splits = _exchange_payload_sizes(
            input_splits,
916
917
918
919
920
921
922
923
924
        self._device = torch.device(device or communication_device or "cpu")
        self._layout = None if model_batch_spec is None else ModelBatchLayout(model_batch_spec)
        if len(self._ranks) > 1:
            if not groups.distributed or self._control_group is None:
                raise ValueError("Multi-rank model broadcast requires an initialized process group.")
            if self._layout is None:
                raise ValueError("Model-parallel loading requires the same model_batch_spec on every rank.")

    def _control_tensor(self, values: list[int]) -> torch.Tensor:
951
952
953
954
955
956
957
958
959
        values = [0] * (1 + self._layout.dynamic_dimensions)
        if is_constructor:
            try:
                if error is not None:
                    raise error
                if batch is not None:
                    tensors = self._layout.flatten(batch)
                    values = [1, *self._layout.dimensions(tensors)]
            except BaseException as exc:  # pylint: disable=broad-exception-caught
970
971
972
973
974
975
976
977
978
            raise RuntimeError("The Data Constructor failed to prepare the declared tensor batch.")
        if values[0] == 0:
            return None
        if values[0] != 1:
            raise ValueError("Invalid model batch status header.")
        shapes = self._layout.shapes(values[1:])
        received = [self._broadcast_tensor(field, shape, tensors[index] if is_constructor else None)
                    for index, (field, shape) in enumerate(zip(self._layout.fields, shapes))]
        return self._layout.rebuild(received)
 994
 995
 996
 997
 998
 999
1000
1001
1002

    def _group_for_tensor(self, device_type: str) -> Any:
        if device_type == "cpu" or self._accelerator_tensor_group is None:
            return self._control_group
        return self._accelerator_tensor_group


__all__ = [
    "DataGroups",
hyper_parallel/trainer/base.py
901
902
903
904
905
906
907
908
909
                    step_token_counts=self.step_token_counts,
                )
                prefetch_plan = getattr(self.train_dataloader, "prefetch_plan", None)
                if callable(prefetch_plan):
                    prefetch_plan()  # pylint: disable=not-callable  # Guarded dynamic hook.
                # The loss graph owns everything required for backward. Releasing
                # the model output here avoids retaining large vocabulary logits
                # until the whole backward pass finishes.
                del outputs
914
915
916
917
918
919
920
921
                with self.model_bwd_context:
                    loss.backward()
            prefetch = getattr(self.train_dataloader, "prefetch", None)
            if callable(prefetch):
                prefetch()  # pylint: disable=not-callable  # Guarded dynamic hook.

            del micro_batch
            return loss, loss_dict
hyper_parallel/trainer/text_trainer.py
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
        }

    def _train_epoch(self, epoch: int, balanced_loader: bool) -> bool:
        """Consume complete optimizer steps and report whether the source epoch ended."""
        train_dataloader = self.base.train_dataloader
        data_iterator = iter(train_dataloader) if train_dataloader is not None else None
        start_step = self.base.state.global_step - epoch * self.base.train_steps
        train_steps = min(self.base.train_steps, self.base.train_iters - epoch * self.base.train_steps)
        if balanced_loader:
            start_step = 0
            train_steps = self.base.train_iters - self.base.state.global_step
        for _ in range(start_step, train_steps):
            try:
                self.train_step(data_iterator)
            except StopIteration:
                if balanced_loader:
                    optimizers = self.base.optimizer
                    for optimizer in optimizers if isinstance(optimizers, list) else [optimizers]:
                        optimizer.zero_grad()
                logger.info(
                    "epoch:%s Dataloader finished with drop_last %s", epoch, self.base.config.dataloader.drop_last
                )
                return True
        return False

    def train(self) -> None:
        """Run the configured global optimizer steps by Dataset epoch."""
        try:
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
            balanced_loader = isinstance(train_dataloader, DistributedDataLoader)
            start_epoch = self.base.state.epoch
            epochs = count(start_epoch) if balanced_loader else range(start_epoch, self.base.train_epochs)
            for epoch in epochs:
                if self.base.state.global_step >= self.base.train_iters:
                    break
                # The first collective iterator may hold a restored checkpoint cursor.
                if hasattr(train_dataloader, "set_epoch") and (not balanced_loader or epoch != start_epoch):
                    train_dataloader.set_epoch(epoch)

                self.base.state.epoch = epoch
                self.on_epoch_begin()
                epoch_start_step = self.base.state.global_step
                epoch_exhausted = self._train_epoch(epoch, balanced_loader)

                if balanced_loader and epoch != start_epoch and self.base.state.global_step == epoch_start_step:
                    raise ValueError("Indexed source epoch cannot provide one complete optimizer step")

                self.on_epoch_end()
                self.base.state.epoch = epoch + 1 if not balanced_loader or epoch_exhausted else epoch
                print_device_mem_info(f"VRAM usage after epoch {epoch + 1}")
                if self.base.state.global_step >= self.base.train_iters:
                    break
394
395
396
397
398
399
400
401
402
403
404
                self.base.abort_profilers()
            raise

        if balanced_loader:
            try:
                train_dataloader.wait_for_prefetch()
            finally:
                train_dataloader.close()

        synchronize()
        self.base.destroy_distributed()
hyper_parallel/trainer/vlm_trainer.py
67
68
69
70
71
72
73
74
75
        self.base._build_dataset()

        # dataloader
        self._build_collate_fn()
        self.base._build_dataloader(metadata_fn=vlm_sample_metadata)

        # get_batch
        self._build_get_batch()
        self.base.attach_model_integration_data_pipeline()
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
        return step_metrics

    def _train_epoch(self, epoch: int, balanced_loader: bool) -> bool:
        """Run complete optimizer steps and report actual Dataset exhaustion."""
        train_dataloader = self.base.train_dataloader
        data_iterator = iter(train_dataloader) if train_dataloader is not None else None
        start_step = self.base.state.global_step - epoch * self.base.train_steps
        train_steps = min(self.base.train_steps, self.base.train_iters - epoch * self.base.train_steps)
        if balanced_loader:
            start_step = 0
            train_steps = self.base.train_iters - self.base.state.global_step
        for _ in range(start_step, train_steps):
            try:
                self.train_step(data_iterator)
            except StopIteration:
                optimizers = self.base.optimizer
                for optimizer in optimizers if isinstance(optimizers, list) else [optimizers]:
                    optimizer.zero_grad()
                logger.info(
                    "epoch:%s Dataloader finished with drop_last %s", epoch, self.base.config.dataloader.drop_last
                )
                return True
        return False

    def train(self) -> None:
        """Run the VLM training loop."""
        try:
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
            balanced_loader = isinstance(train_dataloader, DistributedDataLoader)
            start_epoch = self.base.state.epoch
            epochs = count(start_epoch) if balanced_loader else range(start_epoch, self.base.train_epochs)
            for epoch in epochs:
                if self.base.state.global_step >= self.base.train_iters:
                    break
                # Do not erase the cursor restored by on_train_begin on the first epoch.
                if hasattr(train_dataloader, "set_epoch") and (not balanced_loader or epoch != start_epoch):
                    train_dataloader.set_epoch(epoch)

                self.base.state.epoch = epoch
                self.on_epoch_begin()
                epoch_start_step = self.base.state.global_step
                epoch_exhausted = self._train_epoch(epoch, balanced_loader)
                if balanced_loader and epoch != start_epoch and self.base.state.global_step == epoch_start_step:
                    raise ValueError("VLM Dataset epoch cannot provide one complete optimizer step")

                self.on_epoch_end()
                self.base.state.epoch = epoch + 1 if not balanced_loader or epoch_exhausted else epoch
                print_device_mem_info(f"VRAM usage after epoch {epoch + 1}")

            memory_profiler.stop()
            self.on_train_end()
394
395
396
397
398
399
400
401
402
403
404
                self.base.abort_profilers()
            raise

        if balanced_loader:
            try:
                train_dataloader.wait_for_prefetch()
            finally:
                train_dataloader.close()

        synchronize()
        self.base.destroy_distributed()