Coverage for / home / jenkins / .local / lib / python3.10 / site-packages / hyper_parallel / distributed_data / metadata.py: 84%
56 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
« prev ^ index » next coverage.py v7.13.1, created at 2026-11-15 07:18 +0800
1# Copyright 2026 Huawei Technologies Co., Ltd
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14# ============================================================================
15"""Ahead-of-fetch metadata and plan-aware local sample loading."""
16# This distributed-data package is intentionally PyTorch-only.
18from __future__ import annotations
20from collections.abc import Mapping, Sequence
21from typing import Any
23import torch # pylint: disable=forbidden-backend-import
24from torch.utils.data import DataLoader, Dataset # pylint: disable=forbidden-backend-import
26from hyper_parallel.distributed_data.schema import SampleKey
27from hyper_parallel.distributed_data.dataset_reader import (
28 _build_worker_options,
29 _identity,
30)
33class PlannedSampleLoader:
34 """Use DataLoader workers to fetch only samples selected by the plan."""
36 def __init__(
37 self,
38 dataset: Any,
39 *,
40 num_workers: int,
41 pin_memory: bool,
42 prefetch_factor: int | None,
43 persistent_workers: bool,
44 seed: int,
45 dataloader_kwargs: Mapping[str, Any] | None = None,
46 ) -> None:
47 """Initialize a reusable plan-aware DataLoader.
49 Args:
50 dataset: Mapping-style Dataset aligned with the metadata sequence.
51 num_workers: Native DataLoader worker count.
52 pin_memory: Whether workers pin returned sample memory.
53 prefetch_factor: Samples prefetched by each worker.
54 persistent_workers: Whether workers persist for the loader lifetime.
55 seed: Base worker seed.
56 dataloader_kwargs: Additional validated DataLoader execution options.
57 """
58 getitem = getattr(type(dataset), "__getitem__", None)
59 if not callable(getitem) or getitem is Dataset.__getitem__ or not hasattr(dataset, "__len__"):
60 raise ValueError("Planned reads require a mapping-style Dataset with __len__ and __getitem__.")
61 self._dataset = dataset
62 self._indices: list[int] = []
63 self._seed = seed
64 self._epoch = 0
65 self._worker_generator = torch.Generator().manual_seed(seed)
66 worker_options = _build_worker_options(
67 num_workers=num_workers,
68 pin_memory=pin_memory,
69 prefetch_factor=prefetch_factor,
70 persistent_workers=persistent_workers,
71 generator=self._worker_generator,
72 dataloader_kwargs=dataloader_kwargs,
73 )
74 self._data_loader = DataLoader(
75 dataset,
76 batch_size=None,
77 sampler=self._indices,
78 collate_fn=_identity,
79 **worker_options,
80 )
82 def fetch_samples(self, sample_keys: Sequence[SampleKey]) -> dict[SampleKey, Any]:
83 """Fetch Dataset samples for the requested keys, preserving their order.
85 Args:
86 sample_keys: Keys whose local ``dataset_index`` values belong to
87 this loader's Dataset.
89 Returns:
90 Mapping from every requested key to its Dataset sample.
91 """
92 if any(not isinstance(key, SampleKey) for key in sample_keys):
93 raise ValueError("sample_keys must contain SampleKey values.")
94 if len(sample_keys) != len(set(sample_keys)):
95 raise ValueError("sample_keys must not contain duplicates.")
96 dataset_size = len(self._dataset)
97 invalid_indices = [key.dataset_index for key in sample_keys if key.dataset_index >= dataset_size]
98 if invalid_indices:
99 raise ValueError(
100 f"Sample plan references Dataset indices outside [0, {dataset_size}): {invalid_indices}."
101 )
102 # DataLoader retains this list, so replace its contents instead of rebinding it.
103 self._indices[:] = (key.dataset_index for key in sample_keys)
104 iterator = iter(self._data_loader)
105 samples_by_key = {}
106 for key in sample_keys:
107 try:
108 payload = next(iterator)
109 except StopIteration as exc:
110 raise ValueError("Plan-aware DataLoader exhausted before all requested samples were read.") from exc
111 samples_by_key[key] = payload
112 return samples_by_key
114 def state_dict(self) -> dict[str, Any]:
115 """Return the epoch required to seed future plan-aware workers."""
116 return {
117 "dataset_size": len(self._dataset),
118 "seed": self._seed,
119 "epoch": self._epoch,
120 }
122 def load_state_dict(self, state_dict: Mapping[str, Any]) -> None:
123 """Restore plan-aware reader state before its first fetch.
125 Args:
126 state_dict: State produced by :meth:`state_dict` on this constructor.
127 """
128 try:
129 state = dict(state_dict)
130 except Exception as exc:
131 raise ValueError(f"Plan-aware DataLoader state is not a mapping: {exc}") from exc
132 expected = {
133 "dataset_size": len(self._dataset),
134 "seed": self._seed,
135 }
136 for name, expected_value in expected.items():
137 if state.get(name) != expected_value:
138 raise ValueError(
139 f"Plan-aware DataLoader checkpoint {name}={state.get(name)!r} "
140 f"does not match {expected_value!r}."
141 )
142 self.set_epoch(state.get("epoch"))
144 def set_epoch(self, epoch: int) -> None:
145 """Record the epoch and reset the future worker-initialization seed.
147 Args:
148 epoch: Non-negative Dataset epoch.
149 """
150 if not isinstance(epoch, int) or isinstance(epoch, bool) or epoch < 0:
151 raise ValueError(f"epoch must be a non-negative integer, but got {epoch!r}.")
152 self._epoch = epoch
153 self._worker_generator.manual_seed(self._seed + epoch)
156__all__ = ["PlannedSampleLoader"]