TensorPlay
latest (dev)
Copy
View Markdown

Latest development documentation · Updated 2026-10-15

Source code for tensorplay.utils.data

"""Data loading utilities: Dataset, DataLoader, Sampler, collate.

The public names, validation order, and error messages remain stable; the
implementation is built on ``tensorplay.Tensor``.

The implementation also has native batch-fetch hooks for TensorPlay-backed
the default collate path to return a single indexed batch instead of creating
one Python tensor view per sample.
"""

from __future__ import annotations

import bisect
import copy
import contextlib
import itertools
import math
import multiprocessing
import os
import queue
import random
import re
import sys
import threading
import traceback
import warnings
from collections.abc import Mapping, MutableMapping, MutableSequence, Sequence, Sized
from typing import Any, Callable, Generic, Iterable, Iterator, List, Optional, TypeVar

import numpy as np

import tensorplay as tp
from tensorplay import Tensor

__all__ = [
    "BatchSampler",
    "ChainDataset",
    "ConcatDataset",
    "DataLoader",
    "Dataset",
    "DistributedSampler",
    "IterableDataset",
    "RandomSampler",
    "Sampler",
    "SequentialSampler",
    "StackDataset",
    "Subset",
    "SubsetRandomSampler",
    "TensorDataset",
    "WeightedRandomSampler",
    "_DatasetKind",
    "default_collate",
    "default_convert",
    "get_worker_info",
    "random_split",
]

_T_co = TypeVar("_T_co", covariant=True)
_T = TypeVar("_T")


# A private sentinel used by dataset wrappers to say that their zero-copy
# batch path cannot represent the requested batch.  The fetcher then falls back
_FAST_BATCH_UNAVAILABLE = object()


def _make_batch_index(indices, tensor: Tensor) -> Tensor:
    """Build an index tensor on the same device as ``tensor``.

    Keep batch indices in an explicit int64 tensor so the dataset fast paths
    use one ``index_select`` on both CPU and CUDA.  This avoids constructing a
    Python tensor view for every sample even though Tensor now also supports
    the public ``tensor[[...]]`` spelling.
    """
    if isinstance(indices, Tensor):
        values = indices.tolist()
    else:
        values = [int(index) for index in indices]
    return tp.tensor(values, dtype=tp.int64, device=tensor.device)


# ---------------------------------------------------------------------------
# Datasets
# ---------------------------------------------------------------------------

[docs]
class Dataset(Generic[_T_co]):
    """An abstract class representing a :class:`Dataset`.

    All datasets that represent a map from keys to data samples should subclass
    it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching
    a data sample for a given key. Subclasses could also optionally overwrite
    :meth:`__len__`, which is expected to return the size of the dataset by many
    :class:`~tensorplay.utils.data.Sampler` implementations and the default
    options of :class:`~tensorplay.utils.data.DataLoader`.

    .. note::
      :class:`~tensorplay.utils.data.DataLoader` by default constructs an index
      sampler that yields integral indices. To make it work with a map-style
      dataset with non-integral indices/keys, a custom sampler must be provided.
    """

    def __getitem__(self, index) -> _T_co:
        raise NotImplementedError("Subclasses of Dataset should implement __getitem__.")

    def __add__(self, other: "Dataset[_T_co]") -> "ConcatDataset[_T_co]":
        return ConcatDataset([self, other])




[docs]
class IterableDataset(Dataset[_T_co], Iterable[_T_co]):
    """An iterable Dataset.

    All datasets that represent an iterable of data samples should subclass it.
    Such form of datasets is particularly useful when data come from a stream.

    All subclasses should overwrite :meth:`__iter__`, which would return an
    iterator of samples in this dataset.

    When a subclass is used with :class:`~tensorplay.utils.data.DataLoader`,
    each item in the dataset will be yielded from the DataLoader iterator.
    """

    def __add__(self, other: Dataset[_T_co]):
        return ChainDataset([self, other])




[docs]
class TensorDataset(Dataset[tuple[Tensor, ...]]):
    """Dataset wrapping tensors.

    Each sample will be retrieved by indexing tensors along the first dimension.

    Args:
        *tensors (Tensor): tensors that have the same size of the first dimension.
    """

    tensors: tuple[Tensor, ...]

    def __init__(self, *tensors: Tensor) -> None:
        if any(tensors[0].size(0) != tensor.size(0) for tensor in tensors):
            raise AssertionError("Size mismatch between tensors")
        self.tensors = tensors

    def __getitem__(self, index):
        if isinstance(index, list):
            return tuple(
                tp.index_select(tensor, 0, _make_batch_index(index, tensor))
                for tensor in self.tensors
            )
        return tuple(tensor[index] for tensor in self.tensors)

    def _tp_get_batch(self, indices):
        """Return the default-collated batch without per-row Python views."""
        return [
            tp.index_select(tensor, 0, _make_batch_index(indices, tensor))
            for tensor in self.tensors
        ]

    def __getitems__(self, indices: list[int]):
        # default-collate path uses ``_tp_get_batch`` above to avoid rebuilding
        # this list and stacking it a second time.
        batch = self._tp_get_batch(indices)
        return [tuple(tensor[i] for tensor in batch) for i in range(len(indices))]

    def __len__(self) -> int:
        return self.tensors[0].size(0)




[docs]
class StackDataset(Dataset[_T]):
    """Dataset as a stacking of multiple datasets.

    This class is useful to assemble different parts of complex input data,
    given as datasets.

    Example:
        >>> images = ImageDataset()
        >>> texts = TextDataset()
        >>> tuple_stack = StackDataset(images, texts)
        >>> tuple_stack[0] == (images[0], texts[0])
        >>> dict_stack = StackDataset(image=images, text=texts)
        >>> dict_stack[0] == {"image": images[0], "text": texts[0]}

    Args:
        *args (Dataset): Datasets for stacking returned as tuple.
        **kwargs (Dataset): Datasets for stacking returned as dict.
    """

    datasets: tuple | dict

    def __init__(self, *args: Dataset[_T_co], **kwargs: Dataset[_T_co]) -> None:
        if args:
            if kwargs:
                raise ValueError(
                    "Supported either ``tuple``- (via ``args``) or"
                    "``dict``- (via ``kwargs``) like input/output, but both types are given."
                )
            self._length = len(args[0])
            if any(self._length != len(dataset) for dataset in args):
                raise ValueError("Size mismatch between datasets")
            self.datasets = args
        elif kwargs:
            tmp = list(kwargs.values())
            self._length = len(tmp[0])
            if any(self._length != len(dataset) for dataset in tmp):
                raise ValueError("Size mismatch between datasets")
            self.datasets = kwargs
        else:
            raise ValueError("At least one dataset should be passed")

    def __getitem__(self, index):
        if isinstance(self.datasets, dict):
            return {k: dataset[index] for k, dataset in self.datasets.items()}
        return tuple(dataset[index] for dataset in self.datasets)

    def __getitems__(self, indices: list):
        # add batched sampling support when parent datasets supports it.
        if isinstance(self.datasets, dict):
            dict_batch: list[dict] = [{} for _ in indices]
            for k, dataset in self.datasets.items():
                if callable(getattr(dataset, "__getitems__", None)):
                    items = dataset.__getitems__(indices)
                    if len(items) != len(indices):
                        raise ValueError(
                            "Nested dataset's output size mismatch."
                            f" Expected {len(indices)}, got {len(items)}"
                        )
                    for data, d_sample in zip(items, dict_batch, strict=True):
                        d_sample[k] = data
                else:
                    for idx, d_sample in zip(indices, dict_batch, strict=True):
                        d_sample[k] = dataset[idx]
            return dict_batch

        # tuple data
        list_batch: list[list] = [[] for _ in indices]
        for dataset in self.datasets:
            if callable(getattr(dataset, "__getitems__", None)):
                items = dataset.__getitems__(indices)
                if len(items) != len(indices):
                    raise ValueError(
                        "Nested dataset's output size mismatch."
                        f" Expected {len(indices)}, got {len(items)}"
                    )
                for data, t_sample in zip(items, list_batch, strict=True):
                    t_sample.append(data)
            else:
                for idx, t_sample in zip(indices, list_batch, strict=True):
                    t_sample.append(dataset[idx])
        tuple_batch: list[tuple] = [tuple(sample) for sample in list_batch]
        return tuple_batch

    def _tp_get_batch(self, indices):
        """Compose native batches from children when every child supports it."""
        if isinstance(self.datasets, dict):
            result = {}
            for key, dataset in self.datasets.items():
                getter = getattr(dataset, "_tp_get_batch", None)
                if not callable(getter):
                    return _FAST_BATCH_UNAVAILABLE
                batch = getter(indices)
                if batch is _FAST_BATCH_UNAVAILABLE:
                    return _FAST_BATCH_UNAVAILABLE
                result[key] = batch
            return result

        result = []
        for dataset in self.datasets:
            getter = getattr(dataset, "_tp_get_batch", None)
            if not callable(getter):
                return _FAST_BATCH_UNAVAILABLE
            batch = getter(indices)
            if batch is _FAST_BATCH_UNAVAILABLE:
                return _FAST_BATCH_UNAVAILABLE
            result.append(batch)
        return result

    def __len__(self) -> int:
        return self._length




[docs]
class ConcatDataset(Dataset[_T_co]):
    """Dataset as a concatenation of multiple datasets.

    This class is useful to assemble different existing datasets.

    Args:
        datasets (sequence): List of datasets to be concatenated
    """

    datasets: list[Dataset[_T_co]]
    cumulative_sizes: list[int]

    @staticmethod
    def cumsum(sequence):
        r, s = [], 0
        for e in sequence:
            l = len(e)
            r.append(l + s)
            s += l
        return r

    def __init__(self, datasets: Iterable[Dataset]) -> None:
        super().__init__()
        self.datasets = list(datasets)
        if len(self.datasets) == 0:
            raise AssertionError("datasets should not be an empty iterable")
        for d in self.datasets:
            if isinstance(d, IterableDataset):
                raise AssertionError("ConcatDataset does not support IterableDataset")
        self.cumulative_sizes = self.cumsum(self.datasets)

    def __len__(self) -> int:
        return self.cumulative_sizes[-1]

    def __getitem__(self, idx):
        dataset_idx, sample_idx = self._resolve_index(idx)
        return self.datasets[dataset_idx][sample_idx]

    def _resolve_index(self, idx):
        if idx < 0:
            if -idx > len(self):
                raise ValueError(
                    "absolute value of index should not exceed dataset length"
                )
            idx = len(self) + idx
        dataset_idx = bisect.bisect_right(self.cumulative_sizes, idx)
        if dataset_idx >= len(self.datasets):
            # Match the natural IndexError raised by the scalar path while
            # giving the batch path a useful, deterministic failure.
            raise IndexError("list index out of range")
        if dataset_idx == 0:
            sample_idx = idx
        else:
            sample_idx = idx - self.cumulative_sizes[dataset_idx - 1]
        return dataset_idx, sample_idx

    def __getitems__(self, indices: list[int]):
        """Fetch a batch while grouping requests by child dataset.

        This preserves the order of the caller's indices, but lets child
        datasets use their own batch-aware ``__getitems__`` implementation.
        It removes a Python-level binary search and dispatch for every child
        sample in the common concatenated-dataset case.
        """
        if not indices:
            return []

        grouped: dict[int, list[tuple[int, int]]] = {}
        for output_position, index in enumerate(indices):
            dataset_idx, sample_idx = self._resolve_index(index)
            grouped.setdefault(dataset_idx, []).append((output_position, sample_idx))

        result = [None] * len(indices)
        for dataset_idx, positions in grouped.items():
            child = self.datasets[dataset_idx]
            child_indices = [sample_idx for _, sample_idx in positions]
            getter = getattr(child, "__getitems__", None)
            if callable(getter):
                child_items = getter(child_indices)
                if len(child_items) != len(child_indices):
                    raise ValueError(
                        "Nested dataset's output size mismatch."
                        f" Expected {len(child_indices)}, got {len(child_items)}"
                    )
            else:
                child_items = [child[index] for index in child_indices]
            for (output_position, _), item in zip(positions, child_items, strict=True):
                result[output_position] = item
        return result

    def _tp_get_batch(self, indices):
        # A batch crossing a child boundary needs structure-aware merging.  The
        # public __getitems__ path handles that case; keep the direct native
        # path for the frequent single-child batch where it is unambiguous.
        if not indices:
            return _FAST_BATCH_UNAVAILABLE
        dataset_idx, sample_idx = self._resolve_index(indices[0])
        local_indices = [sample_idx]
        for index in indices[1:]:
            next_dataset_idx, next_sample_idx = self._resolve_index(index)
            if next_dataset_idx != dataset_idx:
                return _FAST_BATCH_UNAVAILABLE
            local_indices.append(next_sample_idx)
        getter = getattr(self.datasets[dataset_idx], "_tp_get_batch", None)
        if not callable(getter):
            return _FAST_BATCH_UNAVAILABLE
        return getter(local_indices)

    @property
    def cummulative_sizes(self):
        warnings.warn(
            "`cummulative_sizes` attribute is renamed to `cumulative_sizes`",
            FutureWarning,
            stacklevel=2,
        )
        return self.cumulative_sizes




[docs]
class ChainDataset(IterableDataset):
    """Dataset for chaining multiple :class:`IterableDataset` s.

    This class is useful to assemble different existing dataset streams. The
    chaining operation is done on-the-fly, so concatenating large-scale
    datasets with this class will be efficient.

    Args:
        datasets (iterable of IterableDataset): datasets to be chained together
    """

    def __init__(self, datasets: Iterable[Dataset]) -> None:
        super().__init__()
        self.datasets = datasets

    def __iter__(self):
        for d in self.datasets:
            if not isinstance(d, IterableDataset):
                raise AssertionError("ChainDataset only supports IterableDataset")
            yield from d

    def __len__(self) -> int:
        total = 0
        for d in self.datasets:
            if not isinstance(d, IterableDataset):
                raise AssertionError("ChainDataset only supports IterableDataset")
            total += len(d)
        return total




[docs]
class Subset(Dataset[_T_co]):
    """Subset of a dataset at specified indices.

    .. note::
        When subclassing `Subset` and overriding `__getitem__`, you **must** also
        override `__getitems__` to ensure `DataLoader` works correctly with your
        custom logic. If you override only `__getitem__`, a `NotImplementedError`
        will be raised when using `DataLoader`.

    Args:
        dataset (Dataset): The whole Dataset
        indices (sequence): Indices in the whole set selected for subset
    """

    dataset: Dataset[_T_co]
    indices: Sequence[int]

    def __init__(self, dataset: Dataset[_T_co], indices: Sequence[int]) -> None:
        self.dataset = dataset
        self.indices = indices

        # Check if __getitem__ is overridden but __getitems__ is not
        if (
            type(self).__getitem__ is not Subset.__getitem__
            and type(self).__getitems__ is Subset.__getitems__
        ):
            raise NotImplementedError(
                f"{type(self).__name__} overrides __getitem__ but not __getitems__. "
                "When subclassing Subset and overriding __getitem__, you must also override "
                "__getitems__ to ensure DataLoader works correctly with your custom logic. "
                "A simple implementation:\n\n"
                "def __getitems__(self, indices):\n"
                "    return [self.__getitem__(idx) for idx in indices]"
            )

    def __getitem__(self, idx):
        if isinstance(idx, list):
            return self.dataset[[self.indices[i] for i in idx]]
        return self.dataset[self.indices[idx]]

    def __getitems__(self, indices: list[int]) -> list[_T_co]:
        # add batched sampling support when parent dataset supports it.
        if callable(getattr(self.dataset, "__getitems__", None)):
            return self.dataset.__getitems__([self.indices[idx] for idx in indices])
        else:
            return [self.dataset[self.indices[idx]] for idx in indices]

    def _tp_get_batch(self, indices):
        # Never bypass a subclass' explicitly customized fetch contract.
        if type(self) is not Subset:
            return _FAST_BATCH_UNAVAILABLE
        getter = getattr(self.dataset, "_tp_get_batch", None)
        if not callable(getter):
            return _FAST_BATCH_UNAVAILABLE
        return getter([self.indices[idx] for idx in indices])

    def __len__(self) -> int:
        return len(self.indices)



_GENERATOR_LOCK = threading.RLock()


@contextlib.contextmanager
def _use_generator(generator: Optional["tp.Generator"]):
    """Run TensorPlay's global-generator kernels against ``generator``.

    The native random operators currently consume the process-global CPU
    generator.  Swapping its complete serialized state (and restoring it in a
    explicit-generator APIs: the supplied generator advances, while the
    process-global generator is left untouched.  A foreign generator object
    is retained as a compatibility fallback and is seeded by ``initial_seed``
    when its state tensor cannot be exchanged with TensorPlay.
    """
    if generator is None:
        yield
        return

    with _GENERATOR_LOCK:
        global_state = tp.get_rng_state()
        try:
            generator_state = generator.get_state()
            if not isinstance(generator_state, Tensor):
                raise TypeError("generator state is not a TensorPlay Tensor")
            tp.set_rng_state(generator_state)
        except (AttributeError, TypeError, RuntimeError):
            # Keep the old best-effort behavior for foreign Generator-like
            # objects.  Most callers use tp.Generator, for which the branch
            # above is fully stateful.
            tp.manual_seed(generator.initial_seed())
            try:
                yield
            finally:
                tp.set_rng_state(global_state)
            return

        try:
            yield
        finally:
            generator.set_state(tp.get_rng_state())
            tp.set_rng_state(global_state)


def random_split(
    dataset: Dataset[_T],
    lengths: Sequence[int | float],
    generator: Optional["tp.Generator"] = None,
) -> list[Subset[_T]]:
    """Randomly split a dataset into non-overlapping new datasets of given lengths.

    If a list of fractions that sum up to 1 is given, the lengths will be
    computed automatically as floor(frac * len(dataset)) for each fraction
    provided. After computing the lengths, if there are any remainders, 1 count
    will be distributed in round-robin fashion to the lengths until there are
    no remainders left.

    Args:
        dataset (Dataset): Dataset to be split
        lengths (sequence): lengths or fractions of splits to be produced
        generator (Generator): Generator used for the random permutation.
    """
    if math.isclose(sum(lengths), 1) and sum(lengths) <= 1:
        subset_lengths: list[int] = []
        for i, frac in enumerate(lengths):
            if frac < 0 or frac > 1:
                raise ValueError(f"Fraction at index {i} is not between 0 and 1")
            n_items_in_split = math.floor(len(dataset) * frac)
            subset_lengths.append(n_items_in_split)
        remainder = len(dataset) - sum(subset_lengths)
        # add 1 to all the lengths in round-robin fashion until the remainder is 0
        for i in range(remainder):
            idx_to_add_at = i % len(subset_lengths)
            subset_lengths[idx_to_add_at] += 1
        lengths = subset_lengths
        for i, length in enumerate(lengths):
            if length == 0:
                warnings.warn(
                    f"Length of split at index {i} is 0. "
                    f"This might result in an empty dataset.",
                    stacklevel=2,
                )

    # Cannot verify that dataset is Sized
    if sum(lengths) != len(dataset):
        raise ValueError(
            "Sum of input lengths does not equal the length of the input dataset!"
        )

    with _use_generator(generator):
        indices = tp.randperm(sum(lengths)).tolist()
    return [
        Subset(dataset, indices[offset - length : offset])
        for offset, length in zip(itertools.accumulate(lengths), lengths, strict=True)
    ]


# ---------------------------------------------------------------------------
# Samplers
# ---------------------------------------------------------------------------

[docs]
class Sampler(Generic[_T_co]):
    r"""Base class for all Samplers.

    Every Sampler subclass has to provide an :meth:`__iter__` method, providing
    a way to iterate over indices or lists of indices (batches) of dataset
    elements, and may provide a :meth:`__len__` method that returns the length
    of the returned iterators.

    .. note:: The :meth:`__len__` method isn't strictly required by
              :class:`~tensorplay.utils.data.DataLoader`, but is expected in any
              calculation involving the length of a DataLoader.
    """

    def __iter__(self) -> Iterator[_T_co]:
        raise NotImplementedError




[docs]
class SequentialSampler(Sampler[int]):
    """Samples elements sequentially, always in the same order.

    Args:
        data_source (Sized): data source to sample from. Must implement __len__.
    """

    data_source: Sized

    def __init__(self, data_source: Sized) -> None:
        self.data_source = data_source

    def __iter__(self) -> Iterator[int]:
        return iter(range(len(self.data_source)))

    def __len__(self) -> int:
        return len(self.data_source)




[docs]
class RandomSampler(Sampler[int]):
    """Samples elements randomly. If without replacement, then sample from a
    shuffled dataset. If with replacement, then user can specify
    :attr:`num_samples` to draw.

    Args:
        data_source (Sized): data source to sample from. Must implement __len__.
        replacement (bool): samples are drawn on-demand with replacement if
            ``True``, default=``False``.
        num_samples (int): number of samples to draw, default=`len(dataset)`.
        generator (Generator): Generator used in sampling.
    """

    data_source: Sized
    replacement: bool

    def __init__(
        self,
        data_source: Sized,
        replacement: bool = False,
        num_samples: Optional[int] = None,
        generator: Optional["tp.Generator"] = None,
    ) -> None:
        self.data_source = data_source
        self.replacement = replacement
        self._num_samples = num_samples
        self.generator = generator

        if not isinstance(self.replacement, bool):
            raise TypeError(
                f"replacement should be a boolean value, but got replacement={self.replacement}"
            )

        if not isinstance(self.num_samples, int) or self.num_samples <= 0:
            raise ValueError(
                f"num_samples should be a positive integer value, but got num_samples={self.num_samples}"
            )

    @property
    def num_samples(self) -> int:
        # dataset size might change at runtime
        if self._num_samples is None:
            return len(self.data_source)
        return self._num_samples

    def __iter__(self) -> Iterator[int]:
        n = len(self.data_source)
        if self.generator is None:
            # Fresh random seed per epoch, drawn from the global generator
            seed = int(tp.empty((), dtype=tp.int64).random_(0, 2**63 - 1).item())
            generator = tp.Generator()
            generator.manual_seed(seed)
        else:
            generator = self.generator

        # Materialize before yielding so the global-generator swap never
        # remains active while a caller pauses a partially consumed sampler.
        with _use_generator(generator):
            if self.replacement:
                indices = []
                for _ in range(self.num_samples // 32):
                    indices.extend(tp.randint(0, n, (32,)).tolist())
                indices.extend(tp.randint(0, n, (self.num_samples % 32,)).tolist())
            else:
                indices = []
                for _ in range(self.num_samples // n):
                    indices.extend(tp.randperm(n).tolist())
                indices.extend(tp.randperm(n).tolist()[: self.num_samples % n])
        yield from indices

    def __len__(self) -> int:
        return self.num_samples




[docs]
class SubsetRandomSampler(Sampler[int]):
    """Samples elements randomly from a given list of indices, without replacement.

    Args:
        indices (sequence): a sequence of indices
        generator (Generator): Generator used in sampling.
    """

    indices: Sequence[int]

    def __init__(self, indices: Sequence[int], generator: Optional["tp.Generator"] = None) -> None:
        self.indices = indices
        self.generator = generator

    def __iter__(self) -> Iterator[int]:
        with _use_generator(self.generator):
            permutation = tp.randperm(len(self.indices)).tolist()
        yield from (self.indices[i] for i in permutation)

    def __len__(self) -> int:
        return len(self.indices)




[docs]
class WeightedRandomSampler(Sampler[int]):
    """Samples elements from ``[0,..,len(weights)-1]`` with given probabilities (weights).

    Args:
        weights (sequence): a sequence of weights, not necessary summing up to one
        num_samples (int): number of samples to draw
        replacement (bool): if ``True``, samples are drawn with replacement.
            If not, they are drawn without replacement, which means that when a
            sample index is drawn for a row, it cannot be drawn again for that row.
        generator (Generator): Generator used in sampling.
    """

    weights: Tensor
    num_samples: int
    replacement: bool

    def __init__(
        self,
        weights: Sequence[float],
        num_samples: int,
        replacement: bool = True,
        generator: Optional["tp.Generator"] = None,
    ) -> None:
        if (
            not isinstance(num_samples, int)
            or isinstance(num_samples, bool)
            or num_samples <= 0
        ):
            raise ValueError(
                f"num_samples should be a positive integer value, but got num_samples={num_samples}"
            )
        if not isinstance(replacement, bool):
            raise ValueError(
                f"replacement should be a boolean value, but got replacement={replacement}"
            )

        weights_tensor = tp.as_tensor(np.asarray(weights, dtype=np.float64))
        if len(weights_tensor.shape) != 1:
            raise ValueError(
                "weights should be a 1d sequence but given "
                f"weights have shape {tuple(weights_tensor.shape)}"
            )

        self.weights = weights_tensor
        self.num_samples = num_samples
        self.replacement = replacement
        self.generator = generator

    def __iter__(self) -> Iterator[int]:
        with _use_generator(self.generator):
            rand_tensor = tp.multinomial(
                self.weights, self.num_samples, self.replacement
            )
            indices = rand_tensor.tolist()
        yield from indices

    def __len__(self) -> int:
        return self.num_samples




[docs]
class BatchSampler(Sampler[list[int]]):
    """Wraps another sampler to yield a mini-batch of indices.

    Args:
        sampler (Sampler or Iterable): Base sampler. Can be any iterable object
        batch_size (int): Size of mini-batch.
        drop_last (bool): If ``True``, the sampler will drop the last batch if
            its size would be less than ``batch_size``
    """

    def __init__(
        self,
        sampler: Sampler[int] | Iterable[int],
        batch_size: int,
        drop_last: bool,
    ) -> None:
        # Since collections.abc.Iterable does not check for `__getitem__`, which
        # is one way for an object to be an iterable, we don't do an `isinstance`
        # check here.
        if (
            not isinstance(batch_size, int)
            or isinstance(batch_size, bool)
            or batch_size <= 0
        ):
            raise ValueError(
                f"batch_size should be a positive integer value, but got batch_size={batch_size}"
            )
        if not isinstance(drop_last, bool):
            raise ValueError(
                f"drop_last should be a boolean value, but got drop_last={drop_last}"
            )
        self.sampler = sampler
        self.batch_size = batch_size
        self.drop_last = drop_last

    def __iter__(self) -> Iterator[list[int]]:
        sampler_iter = iter(self.sampler)
        if self.drop_last:
            # Create multiple references to the same iterator
            args = [sampler_iter] * self.batch_size
            for batch_droplast in zip(*args, strict=False):
                yield [*batch_droplast]
        else:
            batch = [*itertools.islice(sampler_iter, self.batch_size)]
            while batch:
                yield batch
                batch = [*itertools.islice(sampler_iter, self.batch_size)]

    def __len__(self) -> int:
        # Can only be called if self.sampler has __len__ implemented
        if self.drop_last:
            return len(self.sampler) // self.batch_size
        else:
            return (len(self.sampler) + self.batch_size - 1) // self.batch_size



# ---------------------------------------------------------------------------
# Distributed sampling
# ---------------------------------------------------------------------------
def _dist_world_size_and_rank():
    """Best-effort world size / rank from tensorplay.distributed."""
    try:
        import tensorplay.distributed as dist

        if dist.is_available() and dist.is_initialized():
            return dist.get_world_size(), dist.get_rank()
    except ImportError:
        pass
    raise RuntimeError("Requires distributed package to be available")


class DistributedSampler(Sampler[_T_co]):
    r"""Sampler that restricts data loading to a subset of the dataset.

    It is especially useful in conjunction with distributed data parallel
    training: each process passes a :class:`DistributedSampler` as its
    :class:`~tensorplay.utils.data.DataLoader` sampler and loads an exclusive
    subset of the dataset.

    .. note::
        Dataset is assumed to be of constant size and that any instance of it
        always returns the same elements in the same order.

    Args:
        dataset: Dataset used for sampling.
        num_replicas (int, optional): Number of processes participating in
            distributed training. By default, :attr:`world_size` is retrieved
            from the current distributed group.
        rank (int, optional): Rank of the current process within
            :attr:`num_replicas`. By default, :attr:`rank` is retrieved from
            the current distributed group.
        shuffle (bool, optional): If ``True`` (default), sampler will shuffle
            the indices.
        seed (int, optional): random seed used to shuffle the sampler if
            :attr:`shuffle=True`. This number should be identical across all
            processes in the distributed group. Default: ``0``.
        drop_last (bool, optional): if ``True``, then the sampler will drop
            the tail of the data to make it evenly divisible across the number
            of replicas. If ``False``, the sampler will add extra indices to
            make the data evenly divisible across the replicas. Default:
            ``False``.

    .. warning::
        In distributed mode, calling the :meth:`set_epoch` method at the
        beginning of each epoch **before** creating the DataLoader iterator is
        necessary to make shuffling work properly across multiple epochs.
        Otherwise, the same ordering will be always used.
    """

    def __init__(
        self,
        dataset: Dataset[_T_co],
        num_replicas: Optional[int] = None,
        rank: Optional[int] = None,
        shuffle: bool = True,
        seed: int = 0,
        drop_last: bool = False,
    ) -> None:
        if num_replicas is None or rank is None:
            default_world_size, default_rank = _dist_world_size_and_rank()
            if num_replicas is None:
                num_replicas = default_world_size
            if rank is None:
                rank = default_rank
        if rank >= num_replicas or rank < 0:
            raise ValueError(
                f"Invalid rank {rank}, rank should be in the interval [0, {num_replicas - 1}]"
            )
        self.dataset = dataset
        self.num_replicas = num_replicas
        self.rank = rank
        self.epoch = 0
        self.drop_last = drop_last
        # If the dataset length is evenly divisible by # of replicas, then
        # there is no need to drop any data, since the dataset will be split
        # equally.
        if self.drop_last and len(self.dataset) % self.num_replicas != 0:
            # Split to nearest available length that is evenly divisible. This
            # is to ensure each rank receives the same amount of data when
            # using this Sampler.
            self.num_samples = math.ceil(
                (len(self.dataset) - self.num_replicas) / self.num_replicas
            )
        else:
            self.num_samples = math.ceil(len(self.dataset) / self.num_replicas)
        self.total_size = self.num_samples * self.num_replicas
        self.shuffle = shuffle
        self.seed = seed

    def __iter__(self) -> Iterator[_T_co]:
        if self.shuffle:
            # deterministically shuffle based on epoch and seed
            g = tp.Generator()
            g.manual_seed(self.seed + self.epoch)
            with _use_generator(g):
                indices = tp.randperm(len(self.dataset)).tolist()
        else:
            indices = list(range(len(self.dataset)))

        if not self.drop_last:
            # add extra samples to make it evenly divisible
            padding_size = self.total_size - len(indices)
            if padding_size <= len(indices):
                indices += indices[:padding_size]
            else:
                indices += (indices * math.ceil(padding_size / len(indices)))[
                    :padding_size
                ]
        else:
            # remove tail of data to make it evenly divisible.
            indices = indices[: self.total_size]
        if len(indices) != self.total_size:
            raise AssertionError(
                f"Number of indices ({len(indices)}) does not match total_size ({self.total_size})"
            )

        # subsample
        indices = indices[self.rank : self.total_size : self.num_replicas]
        if len(indices) != self.num_samples:
            raise AssertionError(
                f"Number of subsampled indices ({len(indices)}) does not match num_samples ({self.num_samples})"
            )

        return iter(indices)

    def __len__(self) -> int:
        return self.num_samples

    def set_epoch(self, epoch: int) -> None:
        r"""
        Set the epoch for this sampler.

        When :attr:`shuffle=True`, this ensures all replicas use a different
        random ordering for each epoch. Otherwise, the next iteration of this
        sampler will yield the same ordering.

        Args:
            epoch (int): Epoch number.
        """
        self.epoch = epoch


# ---------------------------------------------------------------------------
# Collation
# ---------------------------------------------------------------------------
np_str_obj_array_pattern = re.compile(r"[SaUO]")



[docs]
def default_convert(data):
    """Convert each NumPy array element into a :class:`tensorplay.Tensor`.

    If the input is a `Sequence`, `Collection`, or `Mapping`, it tries to
    convert each element inside to a :class:`tensorplay.Tensor`. If the input is
    not a NumPy array, it is left unchanged. This is used as the default
    function for collation when both `batch_sampler` and `batch_size` are NOT
    defined in :class:`~tensorplay.utils.data.DataLoader`.

    Args:
        data: a single data point to be converted
    """
    elem_type = type(data)
    if isinstance(data, Tensor):
        return data
    elif (
        elem_type.__module__ == "numpy"
        and elem_type.__name__ != "str_"
        and elem_type.__name__ != "string_"
    ):
        # array of string classes and object
        if (
            elem_type.__name__ == "ndarray"
            and np_str_obj_array_pattern.search(data.dtype.str) is not None
        ):
            return data
        return tp.as_tensor(data)
    elif isinstance(data, Mapping):
        try:
            if isinstance(data, MutableMapping):
                # The mapping type may have extra properties, so we can't just
                # use `type(data)(...)` to create the new mapping.
                # Create a clone and update it if the mapping type is mutable.
                clone = copy.copy(data)
                clone.update({key: default_convert(data[key]) for key in data})
                return clone
            else:
                return elem_type({key: default_convert(data[key]) for key in data})
        except TypeError:
            # The mapping type may not support `copy()` / `update(mapping)`
            # or `__init__(iterable)`.
            return {key: default_convert(data[key]) for key in data}
    elif isinstance(data, tuple) and hasattr(data, "_fields"):  # namedtuple
        return elem_type(*(default_convert(d) for d in data))
    elif isinstance(data, tuple):
        return [default_convert(d) for d in data]  # Backwards compatibility.
    elif isinstance(data, Sequence) and not isinstance(data, (str, bytes)):
        try:
            if isinstance(data, MutableSequence):
                # The sequence type may have extra properties, so we can't just
                # use `type(data)(...)` to create the new sequence.
                # Create a clone and update it if the sequence type is mutable.
                clone = copy.copy(data)
                for i, d in enumerate(data):
                    clone[i] = default_convert(d)
                return clone
            else:
                return elem_type([default_convert(d) for d in data])
        except TypeError:
            # The sequence type may not support `copy()` / `__setitem__(index, item)`
            # or `__init__(iterable)` (e.g., `range`).
            return [default_convert(d) for d in data]
    else:
        return data



default_collate_err_msg_format = (
    "default_collate: batch must contain tensors, numpy arrays, numbers, "
    "dicts or lists; found {}"
)


def collate(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    """General collate function that handles collection type of element within
    each batch. The function also opens function registry to deal with specific
    element types. `default_collate_fn_map` provides default collate functions
    for tensors, numpy arrays, numbers and strings.

    Args:
        batch: a single batch to be collated
        collate_fn_map: Optional dictionary mapping from element type to the
            corresponding collate function.
    """
    elem = batch[0]
    elem_type = type(elem)

    if collate_fn_map is not None:
        if elem_type in collate_fn_map:
            return collate_fn_map[elem_type](batch, collate_fn_map=collate_fn_map)

        for collate_type in collate_fn_map:
            if isinstance(elem, collate_type):
                return collate_fn_map[collate_type](
                    batch, collate_fn_map=collate_fn_map
                )

    if isinstance(elem, Mapping):
        try:
            if isinstance(elem, MutableMapping):
                # The mapping type may have extra properties, so we can't just
                # use `type(data)(...)` to create the new mapping.
                # Create a clone and update it if the mapping type is mutable.
                clone = copy.copy(elem)
                clone.update(
                    {
                        key: collate(
                            [d[key] for d in batch], collate_fn_map=collate_fn_map
                        )
                        for key in elem
                    }
                )
                return clone
            else:
                return elem_type(
                    {
                        key: collate(
                            [d[key] for d in batch], collate_fn_map=collate_fn_map
                        )
                        for key in elem
                    }
                )
        except TypeError:
            # The mapping type may not support `copy()` / `update(mapping)`
            # or `__init__(iterable)`.
            return {
                key: collate([d[key] for d in batch], collate_fn_map=collate_fn_map)
                for key in elem
            }
    elif isinstance(elem, tuple) and hasattr(elem, "_fields"):  # namedtuple
        return elem_type(
            *(
                collate(samples, collate_fn_map=collate_fn_map)
                for samples in zip(*batch, strict=False)
            )
        )
    elif isinstance(elem, Sequence):
        # check to make sure that the elements in batch have consistent size
        it = iter(batch)
        elem_size = len(next(it))

        if not all(len(elem) == elem_size for elem in it):
            raise RuntimeError("each element in list of batch should be of equal size")
        transposed = list(
            zip(*batch, strict=False)
        )  # It may be accessed twice, so we use a list.

        if isinstance(elem, tuple):
            return [
                collate(samples, collate_fn_map=collate_fn_map)
                for samples in transposed
            ]  # Backwards compatibility.
        else:
            try:
                if isinstance(elem, MutableSequence):
                    # The sequence type may have extra properties, so we can't just
                    # use `type(data)(...)` to create the new sequence.
                    # Create a clone and update it if the sequence type is mutable.
                    clone = copy.copy(elem)
                    for i, samples in enumerate(transposed):
                        clone[i] = collate(samples, collate_fn_map=collate_fn_map)
                    return clone
                else:
                    return elem_type(
                        [
                            collate(samples, collate_fn_map=collate_fn_map)
                            for samples in transposed
                        ]
                    )
            except TypeError:
                # The sequence type may not support `copy()` / `__setitem__(index, item)`
                # or `__init__(iterable)` (e.g., `range`).
                return [
                    collate(samples, collate_fn_map=collate_fn_map)
                    for samples in transposed
                ]

    raise TypeError(default_collate_err_msg_format.format(elem_type))


def collate_tensor_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    return tp.stack(batch, 0)


def collate_numpy_array_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    elem = batch[0]
    # array of string classes and object
    if np_str_obj_array_pattern.search(elem.dtype.str) is not None:
        raise TypeError(default_collate_err_msg_format.format(elem.dtype))

    return collate([tp.as_tensor(b) for b in batch], collate_fn_map=collate_fn_map)


def collate_numpy_scalar_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    return tp.as_tensor(np.asarray(batch))


def collate_float_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    return tp.tensor(np.asarray(batch), dtype=tp.float64)


def collate_int_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    return tp.tensor(np.asarray(batch))


def collate_str_fn(
    batch,
    *,
    collate_fn_map: Optional[dict] = None,
):
    return batch


default_collate_fn_map: dict = {
    Tensor: collate_tensor_fn,
    np.ndarray: collate_numpy_array_fn,
    (np.bool_, np.number, np.object_): collate_numpy_scalar_fn,
    float: collate_float_fn,
    int: collate_int_fn,
    str: collate_str_fn,
    bytes: collate_str_fn,
}



[docs]
def default_collate(batch):
    """Take in a batch of data and put the elements within the batch into a
    tensor with an additional outer dimension - batch size.

    The exact output type can be a :class:`tensorplay.Tensor`, a `Sequence` of
    :class:`tensorplay.Tensor`, a Collection of :class:`tensorplay.Tensor`, or
    left unchanged, depending on the input type. This is used as the default
    function for collation when `batch_size` or `batch_sampler` is defined in
    :class:`~tensorplay.utils.data.DataLoader`.

    Args:
        batch: a single batch to be collated
    """
    return collate(batch, collate_fn_map=default_collate_fn_map)



# ---------------------------------------------------------------------------
# Worker helpers (multiprocessing)
# ---------------------------------------------------------------------------
class _DatasetKind:
    Map = 0
    Iterable = 1

    @staticmethod
    def create_fetcher(dataset_kind, dataset, auto_collation, collate_fn, drop_last):
        if dataset_kind == _DatasetKind.Iterable:
            return _IterableDatasetFetcher(dataset, auto_collation, collate_fn, drop_last)
        else:
            return _MapDatasetFetcher(dataset, auto_collation, collate_fn, drop_last)


_MP_STATUS_CHECK_INTERVAL = 5.0

_worker_info: Optional["WorkerInfo"] = None


class WorkerInfo:
    """Information about the current DataLoader worker process.

    Attributes:
        id: The current worker id (0 to num_workers - 1).
        num_workers: Total number of workers.
        seed: Random seed set for this worker (``base_seed + worker_id``).
        dataset: Copy of the dataset object in this worker.
    """

    __slots__ = ("id", "num_workers", "seed", "dataset")

    def __init__(self, id, num_workers, seed, dataset):
        self.id = id
        self.num_workers = num_workers
        self.seed = seed
        self.dataset = dataset

    def __repr__(self):
        return (
            f"<WorkerInfo(id={self.id}, num_workers={self.num_workers}, "
            f"seed={self.seed}, dataset={self.dataset!r})>"
        )



[docs]
def get_worker_info() -> Optional[WorkerInfo]:
    """Returns the information about the current DataLoader iterator worker
    process. When called in a worker process, returns a WorkerInfo object with
    information about that worker process; otherwise returns ``None``.
    """
    return _worker_info



class _KeyErrorMessage(str):
    # repr() of a str is surrounded by quotes, which is unreadable for KeyError
    def __repr__(self):
        return self


class ExceptionWrapper:
    """Wraps an exception plus traceback to communicate across processes."""

    def __init__(self, exc_info=None, where="in background"):
        # It is important that we don't store exc_info in a variable.
        if exc_info is None:
            exc_info = sys.exc_info()
        self.exc_type = exc_info[0]
        self.exc_msg = "".join(traceback.format_exception(*exc_info))
        self.where = where

    def reraise(self):
        msg = f"Caught {self.exc_type.__name__} {self.where}.\nOriginal {self.exc_msg}"
        if self.exc_type is KeyError:
            msg = _KeyErrorMessage(msg)
        elif getattr(self.exc_type, "message", None):
            raise self.exc_type(message=msg)
        try:
            exception = self.exc_type(msg)
        except Exception:
            raise RuntimeError(msg) from None
        raise exception


# ---------------------------------------------------------------------------
# Dataset fetchers (shared by single- and multi-process loading)
# ---------------------------------------------------------------------------
class _BaseDatasetFetcher:
    def __init__(self, dataset, auto_collation, collate_fn, drop_last) -> None:
        self.dataset = dataset
        self.auto_collation = auto_collation
        self.collate_fn = collate_fn
        self.drop_last = drop_last

    def fetch(self, possibly_batched_index):
        raise NotImplementedError


class _IterableDatasetFetcher(_BaseDatasetFetcher):
    def __init__(self, dataset, auto_collation, collate_fn, drop_last) -> None:
        super().__init__(dataset, auto_collation, collate_fn, drop_last)
        self.dataset_iter = iter(dataset)
        self.ended = False

    def fetch(self, possibly_batched_index):
        if self.ended:
            raise StopIteration

        if self.auto_collation:
            data = []
            for _ in possibly_batched_index:
                try:
                    data.append(next(self.dataset_iter))
                except StopIteration:
                    self.ended = True
                    break
            if len(data) == 0 or (
                self.drop_last and len(data) < len(possibly_batched_index)
            ):
                raise StopIteration
        else:
            data = next(self.dataset_iter)
        return self.collate_fn(data)


class _MapDatasetFetcher(_BaseDatasetFetcher):
    def fetch(self, possibly_batched_index):
        if self.auto_collation:
            # TensorPlay-backed datasets can return the already-collated
            # result of a batch index_select.  This is deliberately gated on
            # the stock collate function: a user collate_fn must still receive
            fast_getter = getattr(self.dataset, "_tp_get_batch", None)
            if self.collate_fn is default_collate and callable(fast_getter):
                data = fast_getter(possibly_batched_index)
                if data is not _FAST_BATCH_UNAVAILABLE:
                    return data

            getitems = getattr(self.dataset, "__getitems__", None)
            if callable(getitems):
                data = getitems(possibly_batched_index)
            else:
                data = [self.dataset[idx] for idx in possibly_batched_index]
        else:
            data = self.dataset[possibly_batched_index]
        return self.collate_fn(data)


# The function `_generate_state` is adapted from `numpy.random.SeedSequence`
# It generates an array of int32 as the seed for `numpy.random`, in order to
# prevent state collision due to same seed and algorithm for `numpy.random`
# and `random` modules.
def _generate_state(base_seed, worker_id):
    INIT_A = 0x43B0D7E5
    MULT_A = 0x931E8875
    INIT_B = 0x8B51F9DD
    MULT_B = 0x58F38DED
    MIX_MULT_L = 0xCA01F9DD
    MIX_MULT_R = 0x4973F715
    XSHIFT = 4 * 8 // 2
    MASK32 = 0xFFFFFFFF

    entropy = [worker_id, base_seed & MASK32, base_seed >> 32, 0]
    pool = [0] * 4

    hash_const_A = INIT_A

    def hash(value):
        nonlocal hash_const_A
        value = (value ^ hash_const_A) & MASK32
        hash_const_A = (hash_const_A * MULT_A) & MASK32
        value = (value * hash_const_A) & MASK32
        value = (value ^ (value >> XSHIFT)) & MASK32
        return value

    def mix(x, y):
        result_x = (MIX_MULT_L * x) & MASK32
        result_y = (MIX_MULT_R * y) & MASK32
        result = (result_x - result_y) & MASK32
        result = (result ^ (result >> XSHIFT)) & MASK32
        return result

    for i in range(len(pool)):
        pool[i] = hash(entropy[i])

    for i_src in range(len(pool)):
        for i_dst in range(len(pool)):
            if i_src != i_dst:
                pool[i_dst] = mix(pool[i_dst], hash(pool[i_src]))

    hash_const_B = INIT_B
    state = []
    for i_dst in range(4):
        data_val = pool[i_dst]
        data_val = (data_val ^ hash_const_B) & MASK32
        hash_const_B = (hash_const_B * MULT_B) & MASK32
        data_val = (data_val * hash_const_B) & MASK32
        data_val = (data_val ^ (data_val >> XSHIFT)) & MASK32
        state.append(data_val)
    return state


class _IterableDatasetStopIteration:
    """Dummy class used to signal the end of an IterableDataset worker."""

    def __init__(self, worker_id):
        self.worker_id = worker_id


class _ResumeIteration:
    """Dummy class used to resume fetching when worker reuse is enabled."""

    def __init__(self, seed=None):
        self.seed = seed


class _ManagerWatchdog:
    def __init__(self) -> None:
        self.manager_pid = os.getppid()
        self.manager_dead = False

    def is_alive(self) -> bool:
        if not self.manager_dead:
            self.manager_dead = os.getppid() != self.manager_pid
        return not self.manager_dead


def _worker_loop(
    dataset_kind,
    dataset,
    index_queue,
    data_queue,
    done_event,
    auto_collation,
    collate_fn,
    drop_last,
    base_seed,
    init_fn,
    worker_id,
    num_workers,
    persistent_workers,
) -> None:
    # See NOTE [ Data Loader Multiprocessing Shutdown Logic ] in
    try:
        watchdog = _ManagerWatchdog()

        tp.set_num_threads(1)
        seed = base_seed + worker_id
        random.seed(seed)
        tp.manual_seed(seed)
        np.random.seed(_generate_state(base_seed, worker_id))

        global _worker_info
        _worker_info = WorkerInfo(
            id=worker_id, num_workers=num_workers, seed=seed, dataset=dataset
        )

        init_exception = None

        try:
            if init_fn is not None:
                init_fn(worker_id)

            fetcher = _DatasetKind.create_fetcher(
                dataset_kind, dataset, auto_collation, collate_fn, drop_last
            )
        except Exception:
            init_exception = ExceptionWrapper(
                where=f"in DataLoader worker process {worker_id}"
            )

        # When using Iterable mode, some workers can exit earlier than others
        # due to the IterableDataset behaving differently per worker. Such a
        # worker sends `_IterableDatasetStopIteration` so the main process
        # won't send more tasks to it and will send `None` to exit it.
        iteration_end = False

        while watchdog.is_alive():
            try:
                r = index_queue.get(timeout=_MP_STATUS_CHECK_INTERVAL)
            except queue.Empty:
                continue
            if isinstance(r, _ResumeIteration):
                # Acknowledge the main process
                data_queue.put((r, None))
                iteration_end = False
                # Recreate the fetcher for the worker-reuse policy
                fetcher = _DatasetKind.create_fetcher(
                    dataset_kind, dataset, auto_collation, collate_fn, drop_last
                )
                continue
            elif r is None:
                # Received the final signal
                break
            elif done_event.is_set() or iteration_end:
                # Keep getting until we see the final signal (`None`), skipping
                # the processing steps.
                continue
            idx, index = r
            if init_exception is not None:
                data = init_exception
                init_exception = None
            else:
                try:
                    data = fetcher.fetch(index)
                except Exception as e:
                    if (
                        isinstance(e, StopIteration)
                        and dataset_kind == _DatasetKind.Iterable
                    ):
                        data = _IterableDatasetStopIteration(worker_id)
                        iteration_end = True
                    else:
                        data = ExceptionWrapper(
                            where=f"in DataLoader worker process {worker_id}"
                        )
            data_queue.put((idx, data))
    except KeyboardInterrupt:
        # Main process will raise KeyboardInterrupt anyways.
        pass
    finally:
        _worker_info = None
    if done_event.is_set():
        data_queue.cancel_join_thread()
        data_queue.close()


# ---------------------------------------------------------------------------
# Pin memory
# ---------------------------------------------------------------------------
def _pin_memory(data):
    if isinstance(data, Tensor):
        return data.pin_memory()

    if hasattr(data, "pin_memory"):
        return data.pin_memory()

    if isinstance(data, (str, bytes)):
        return data

    if isinstance(data, Mapping):
        try:
            if isinstance(data, MutableMapping):
                clone = copy.copy(data)
                clone.update({k: _pin_memory(sample) for k, sample in data.items()})
                return clone
            else:
                return type(data)({k: _pin_memory(sample) for k, sample in data.items()})
        except TypeError:
            return {k: _pin_memory(sample) for k, sample in data.items()}

    if isinstance(data, tuple):
        if hasattr(data, "_fields"):  # namedtuple
            return type(data)(*(_pin_memory(sample) for sample in data))
        return type(data)(_pin_memory(sample) for sample in data)

    if isinstance(data, Sequence):
        try:
            if isinstance(data, MutableSequence):
                clone = copy.copy(data)
                for i, item in enumerate(data):
                    clone[i] = _pin_memory(item)
                return clone
            return type(data)([_pin_memory(sample) for sample in data])
        except TypeError:
            return [_pin_memory(sample) for sample in data]

    return data


def _pin_memory_loop(in_queue, out_queue, done_event) -> None:
    def do_one_step():
        try:
            r = in_queue.get(timeout=_MP_STATUS_CHECK_INTERVAL)
        except queue.Empty:
            return
        idx, data = r
        if not done_event.is_set() and not isinstance(data, ExceptionWrapper):
            try:
                data = _pin_memory(data)
            except Exception:
                data = ExceptionWrapper(where="in pin memory thread")
            r = (idx, data)
        while not done_event.is_set():
            try:
                out_queue.put(r, timeout=_MP_STATUS_CHECK_INTERVAL)
                break
            except queue.Full:
                continue

    while not done_event.is_set():
        do_one_step()


def _default_multiprocessing_context():
    try:
        methods = multiprocessing.get_all_start_methods()
    except Exception:
        methods = []
    if "fork" in methods:
        return multiprocessing.get_context("fork")
    return multiprocessing.get_context()


def _new_base_seed(generator: Optional["tp.Generator"]) -> int:
    with _use_generator(generator):
        return int(tp.empty((), dtype=tp.int64).random_(0, 2**63 - 1).item())


def _cpu_count() -> Optional[int]:
    """

    Prefers ``os.sched_getaffinity`` (respects cgroups / taskset) and falls
    back to ``os.cpu_count``.
    """
    if hasattr(os, "sched_getaffinity"):
        return len(os.sched_getaffinity(0))
    return os.cpu_count()


def _worker_rationality_warning_msg(num_worker_suggest, num_worker_created,
                                    cpuset_checked: bool) -> str:
    suggested_max_worker_msg = (
        (
            (
                "Our suggested max number of worker in current system is {}{}, which is smaller "
                "than what this DataLoader is going to create."
            ).format(
                num_worker_suggest,
                "" if cpuset_checked else " (`cpuset` is not taken into account)",
            )
        )
        if num_worker_suggest is not None
        else (
            "DataLoader is not able to compute a suggested max number of worker in current system."
        )
    )
    return (
        f"This DataLoader will create {num_worker_created} worker processes in total. {suggested_max_worker_msg} "
        "Please be aware that excessive worker creation might get DataLoader running slow or even freeze, "
        "lower the worker number to avoid potential slowness/freeze if necessary."
    )


class _InfiniteConstantSampler(Sampler[int]):
    r"""Analogous to ``itertools.repeat(0, None)``, but with a ``__len__`` that
    raises ``TypeError``."""

    def __iter__(self) -> Iterator[int]:
        while True:
            yield 0

    def __len__(self) -> int:
        raise TypeError("Cannot determine the length of an infinite sampler")


# ---------------------------------------------------------------------------
# DataLoader
# ---------------------------------------------------------------------------
class _BaseDataLoaderIter:
    def __init__(self, loader: "DataLoader"):
        self.loader = loader
        self._dataset_kind = loader._dataset_kind
        self._IterableDataset_len_called = loader._IterableDataset_len_called
        self._auto_collation = loader._auto_collation
        self._drop_last = loader.drop_last
        self._index_sampler = loader._index_sampler
        self._sampler_iter = iter(self._index_sampler)
        self._num_workers = loader.num_workers
        self._num_yielded = 0

        # memory is enabled only when an accelerator exists.  An explicit
        # device opts into the requested allocator and therefore keeps the
        # flag even when the current process has no default accelerator.
        if not loader.pin_memory_device:
            if loader.pin_memory and not tp.cuda.is_available():
                warnings.warn(
                    "'pin_memory' argument is set as true but no accelerator is found, "
                    "then device pinned memory won't be used.",
                    stacklevel=2,
                )
            self._pin_memory = bool(loader.pin_memory and tp.cuda.is_available())
            self._pin_memory_device = None
        else:
            if not loader.pin_memory:
                warnings.warn(
                    "'pin_memory_device' is set but 'pin_memory' argument is not set, "
                    "then device pinned memory won't be used."
                    "please set 'pin_memory' to true, if you need to use the device pin memory",
                    stacklevel=2,
                )
            self._pin_memory = bool(loader.pin_memory)
            self._pin_memory_device = loader.pin_memory_device
        self._base_seed = _new_base_seed(loader.generator)

    def __iter__(self) -> "_BaseDataLoaderIter":
        return self

    def _next_data(self):
        raise NotImplementedError

    def __next__(self) -> Any:
        data = self._next_data()
        self._num_yielded += 1
        if (
            self._dataset_kind == _DatasetKind.Iterable
            and self._IterableDataset_len_called is not None
            and self._num_yielded > self._IterableDataset_len_called
        ):
            # See NOTE [ IterableDataset and __len__ ] in ``DataLoader.__len__``.
            warn_msg = (
                f"Length of IterableDataset {self.loader.dataset} was reported to be {self._IterableDataset_len_called}"
                f"(when accessing len(dataloader)), but {self._num_yielded} samples have been fetched. "
            )
            if self.loader.num_workers > 0:
                warn_msg += (
                    "For multiprocessing data-loading, this could be caused by not properly configuring the "
                    "IterableDataset replica at each worker. Please see "
                )
            warnings.warn(warn_msg, stacklevel=2)
        return data


class _SingleProcessDataLoaderIter(_BaseDataLoaderIter):
    def __init__(self, loader: "DataLoader"):
        super().__init__(loader)
        self._dataset_fetcher = _DatasetKind.create_fetcher(
            loader._dataset_kind,
            loader.dataset,
            loader._auto_collation,
            loader.collate_fn,
            loader.drop_last,
        )

    def _next_data(self) -> Any:
        index = next(self._sampler_iter)  # may raise StopIteration
        data = self._dataset_fetcher.fetch(index)  # may raise StopIteration
        return self.loader._finalize_batch(data)


class _MultiProcessDataLoaderIter(_BaseDataLoaderIter):
    r"""Iterates once over the DataLoader's dataset, as specified by the sampler.

    each worker runs fetch + collate on a per-worker index queue, results are
    streamed back through a shared result queue with bounded prefetch, and an
    optional pin-memory thread of the main process pins the returned batches.
    """

    # NOTE [ Data Loader Multiprocessing Shutdown Logic ]
    #
    # Workers are daemonic. The protocol between the main process and the
    # workers is that the main process sets `workers_done_event` (or marks a
    # worker unavailable for IterableDataset exhaustion), then sends `None` on
    # the worker's index queue; workers exit upon receiving `None`. Normal
    # data always travels as a 2-tuple `(idx, data)` where `idx` is the task
    # index assigned by `_try_put_index`, so `None` can never be confused with
    # the full note.

    def __init__(self, loader: "DataLoader"):
        super().__init__(loader)
        self._timeout = loader.timeout
        self._collate_fn = loader.collate_fn
        self._num_workers = loader.num_workers
        self._prefetch_factor = loader.prefetch_factor
        self._persistent_workers = loader.persistent_workers
        self._in_order = loader.in_order

        if self._num_workers <= 0:
            raise AssertionError(
                "num_workers must be greater than 0 for MultiProcessingDataLoaderIter"
            )
        if self._prefetch_factor <= 0:
            raise AssertionError(
                "prefetch_factor must be greater than 0 for MultiProcessingDataLoaderIter"
            )

        if loader.multiprocessing_context is None:
            multiprocessing_context = _default_multiprocessing_context()
        else:
            multiprocessing_context = loader.multiprocessing_context

        self._worker_init_fn = loader.worker_init_fn

        self._worker_result_queue = multiprocessing_context.Queue()
        self._shutdown = False
        self._workers_done_event = multiprocessing_context.Event()

        self._index_queues = []
        self._workers = []
        for i in range(self._num_workers):
            index_queue = multiprocessing_context.Queue()
            # Need to `cancel_join_thread` here! See sections (2) and (3b) of
            # NOTE [ Data Loader Multiprocessing Shutdown Logic ].
            index_queue.cancel_join_thread()
            w = multiprocessing_context.Process(
                target=_worker_loop,
                args=(
                    self._dataset_kind,
                    loader.dataset,
                    index_queue,
                    self._worker_result_queue,
                    self._workers_done_event,
                    self._auto_collation,
                    self._collate_fn,
                    self._drop_last,
                    self._base_seed,
                    self._worker_init_fn,
                    i,
                    self._num_workers,
                    self._persistent_workers,
                ),
            )
            w.daemon = True
            w.start()
            self._index_queues.append(index_queue)
            self._workers.append(w)

        if self._pin_memory:
            self._pin_memory_thread_done_event = threading.Event()

            # The pin thread consumes the worker result queue and feeds a
            # plain `queue.Queue` that `__next__` reads.
            self._data_queue: Any = queue.Queue()
            pin_memory_thread = threading.Thread(
                target=_pin_memory_loop,
                args=(
                    self._worker_result_queue,
                    self._data_queue,
                    self._pin_memory_thread_done_event,
                ),
            )
            pin_memory_thread.daemon = True
            pin_memory_thread.start()
            self._pin_memory_thread = pin_memory_thread
        else:
            self._data_queue = self._worker_result_queue

        self._reset(loader, first_iter=True)

    def _reset(self, loader: "DataLoader", first_iter: bool = False) -> None:
        self._index_sampler = loader._index_sampler
        self._sampler_iter = iter(self._index_sampler)
        self._IterableDataset_len_called = loader._IterableDataset_len_called
        self._send_idx = 0  # idx of the next task to be sent to workers
        self._rcvd_idx = 0  # idx of the next task to be returned in __next__
        # map: task idx => (worker_id,)     if data isn't fetched (outstanding)
        #                  \ (worker_id, data) if already fetched (out-of-order)
        self._task_info = {}
        self._tasks_outstanding = 0
        # Whether each worker still has work to do *for this epoch* (i.e., has
        # not exhausted its IterableDataset replica). All `True`s for map-style
        # datasets.
        self._workers_status = [True for i in range(self._num_workers)]
        # Outstanding-task count per worker; each worker holds at most
        # `prefetch_factor` tasks when `in_order=False` load-balances dispatch.
        self._workers_num_tasks = [0 for i in range(self._num_workers)]
        # Reset the worker queue cycle so a new epoch resumes at worker 0.
        self._worker_queue_idx_cycle = itertools.cycle(range(self._num_workers))
        # We resume the prefetching in case it was enabled (persistent workers).
        if not first_iter:
            for idx in range(self._num_workers):
                self._index_queues[idx].put(_ResumeIteration())
            resume_iteration_cnt = self._num_workers
            while resume_iteration_cnt > 0:
                return_idx, return_data = self._get_data()
                if isinstance(return_idx, _ResumeIteration):
                    if return_data is not None:
                        raise AssertionError(
                            "Expected return_data to be None when resuming iteration"
                        )
                    resume_iteration_cnt -= 1
        # prime the prefetch loop
        for _ in range(self._prefetch_factor * self._num_workers):
            self._try_put_index()

    def _try_get_data(self, timeout=_MP_STATUS_CHECK_INTERVAL):
        # Tries to fetch data from `self._data_queue` once for a given timeout.
        # Raises RuntimeError if any worker died unexpectedly. Returns a
        # 2-tuple (success, data).
        try:
            data = self._data_queue.get(timeout=timeout)
            return (True, data)
        except Exception as e:
            # At timeout and error, we manually check whether any worker has
            # failed.
            failed_workers = []
            for worker_id, w in enumerate(self._workers):
                if self._workers_status[worker_id] and not w.is_alive():
                    failed_workers.append(w)
                    self._mark_worker_as_unavailable(worker_id)
            if len(failed_workers) > 0:
                pids_str = ", ".join(str(w.pid) for w in failed_workers)
                raise RuntimeError(
                    f"DataLoader worker (pid(s) {pids_str}) exited unexpectedly"
                ) from e
            if isinstance(e, queue.Empty):
                return (False, None)
            raise

    def _get_data(self):
        # Fetches data from `self._data_queue`, checking worker/pin-thread
        # status every `_MP_STATUS_CHECK_INTERVAL` seconds.
        if self._timeout > 0:
            success, data = self._try_get_data(self._timeout)
            if success:
                return data
            else:
                raise RuntimeError(
                    f"DataLoader timed out after {self._timeout} seconds"
                )
        elif hasattr(self, "_pin_memory_thread"):
            while self._pin_memory_thread.is_alive():
                success, data = self._try_get_data()
                if success:
                    return data
            # while condition is false, i.e., pin_memory_thread died.
            raise RuntimeError("Pin memory thread exited unexpectedly")
        else:
            while True:
                success, data = self._try_get_data()
                if success:
                    return data

    def _next_data(self):
        while True:
            # If the worker responsible for `self._rcvd_idx` has already ended
            # and was unable to fulfill this task (due to exhausting an
            # IterableDataset), advance `self._rcvd_idx` to find the next
            # valid index.
            while self._rcvd_idx < self._send_idx:
                info = self._task_info.get(self._rcvd_idx, None)
                if info:
                    worker_id = info[0]
                    if len(info) == 2 or self._workers_status[worker_id]:
                        break
                    del self._task_info[self._rcvd_idx]
                self._rcvd_idx += 1
            else:
                # no valid `self._rcvd_idx` is found (i.e., didn't break)
                if not self._persistent_workers:
                    self._shutdown_workers()
                raise StopIteration

            # Now `self._rcvd_idx` is the batch index we want to fetch

            # Check if the next sample has already been generated
            if len(self._task_info[self._rcvd_idx]) == 2:
                worker_id, data = self._task_info.pop(self._rcvd_idx)
                self._rcvd_idx += 1
                return self._process_data(data, worker_id)

            if self._shutdown or self._tasks_outstanding <= 0:
                raise AssertionError(
                    "Invalid iterator state: shutdown or no outstanding tasks when fetching next data"
                )
            idx, data = self._get_data()
            self._tasks_outstanding -= 1
            if self._dataset_kind == _DatasetKind.Iterable:
                # Check for _IterableDatasetStopIteration
                if isinstance(data, _IterableDatasetStopIteration):
                    if self._persistent_workers:
                        self._workers_status[data.worker_id] = False
                    else:
                        self._mark_worker_as_unavailable(data.worker_id)
                    self._try_put_index()
                    continue

            if idx != self._rcvd_idx:
                if not self._in_order:
                    # don't store it for later, process now; this keeps the
                    # object size manageable
                    worker_id = self._task_info.pop(idx)[0]
                    return self._process_data(data, worker_id)
                # store out-of-order samples
                self._task_info[idx] += (data,)
            else:
                worker_id = self._task_info.pop(idx)[0]
                self._rcvd_idx += 1
                return self._process_data(data, worker_id)

    def _try_put_index(self) -> None:
        max_tasks = self._prefetch_factor * self._num_workers
        if self._tasks_outstanding >= max_tasks:
            raise AssertionError(
                "Number of outstanding tasks exceeded maximum allowed tasks"
            )

        try:
            index = next(self._sampler_iter)
        except StopIteration:
            return
        for _ in range(self._num_workers):  # find the next active worker, if any
            worker_queue_idx = next(self._worker_queue_idx_cycle)
            if self._workers_status[worker_queue_idx]:
                if self._in_order:
                    break
                elif self._workers_num_tasks[worker_queue_idx] < max_tasks // sum(
                    self._workers_status
                ):
                    # when in_order is False, distribute work to a worker with
                    # capacity; _workers_status only changes in this thread, so
                    # the sum is guaranteed > 0.
                    break
        else:
            # not found (i.e., didn't break)
            return

        self._index_queues[worker_queue_idx].put((self._send_idx, index))
        self._task_info[self._send_idx] = (worker_queue_idx,)
        self._workers_num_tasks[worker_queue_idx] += 1
        self._tasks_outstanding += 1
        self._send_idx += 1

    def _process_data(self, data, worker_idx):
        self._workers_num_tasks[worker_idx] -= 1
        self._try_put_index()
        if isinstance(data, ExceptionWrapper):
            data.reraise()
        # Pinning (if enabled) already happened in the pin-memory thread;
        # the optional device move is applied here in the main process.
        return self.loader._apply_device(data)

    def _mark_worker_as_unavailable(self, worker_id, shutdown=False) -> None:
        # Mark a worker as having finished its work e.g., due to exhausting an
        # IterableDataset. Signal termination to that specific worker; joining
        # is deferred to `_shutdown_workers`.
        q = self._index_queues[worker_id]
        q.put(None)
        self._workers_status[worker_id] = False

    def _shutdown_workers(self) -> None:
        # Exit the pin memory thread first because exiting workers may leave
        # corrupted data in `worker_result_queue` which it reads from; then
        # signal and join the workers.
        if self._shutdown:
            return
        self._shutdown = True
        try:
            if hasattr(self, "_pin_memory_thread"):
                self._pin_memory_thread_done_event.set()
                # Send something to wake the pin thread up in case it waits.
                self._worker_result_queue.put((None, None))
                self._pin_memory_thread.join()
                self._worker_result_queue.cancel_join_thread()
                self._worker_result_queue.close()

            self._workers_done_event.set()
            for worker_id in range(len(self._workers)):
                if self._persistent_workers or self._workers_status[worker_id]:
                    self._mark_worker_as_unavailable(worker_id, shutdown=True)
            for w in self._workers:
                w.join(timeout=_MP_STATUS_CHECK_INTERVAL)
            for q in self._index_queues:
                q.cancel_join_thread()
                q.close()
        finally:
            for w in self._workers:
                if w.is_alive():
                    # Existing mechanisms try to make the workers exit
                    # peacefully, but in case we unfortunately reach here, we
                    # kill the worker.
                    w.terminate()
                    w.join(timeout=_MP_STATUS_CHECK_INTERVAL)

    def __del__(self):
        try:
            self._shutdown_workers()
        except Exception:
            pass



[docs]
class DataLoader(Generic[_T_co]):
    """Data loader combines a dataset and a sampler, and provides an iterable
    over the given dataset.

    The DataLoader supports both map-style and iterable-style datasets with
    single- or multi-process loading, customizing loading order and optional
    automatic batching (collation).

    Args:
        dataset (Dataset): dataset from which to load the data.
        batch_size (int, optional): how many samples per batch to load
            (default: ``1``).
        shuffle (bool, optional): set to ``True`` to have the data reshuffled
            at every epoch (default: ``False``).
        sampler (Sampler or Iterable, optional): defines the strategy to draw
            samples from the dataset. Can be any ``Iterable`` with ``__len__``
            implemented. If specified, :attr:`shuffle` must not be specified.
        batch_sampler (Sampler or Iterable, optional): like :attr:`sampler`, but
            returns a batch of indices at a time. Mutually exclusive with
            :attr:`batch_size`, :attr:`shuffle`, :attr:`sampler`, and
            :attr:`drop_last`.
        num_workers (int, optional): how many subprocesses to use for data
            loading. ``0`` means that the data will be loaded in the main
            process. (default: ``0``)
        collate_fn (Callable, optional): merges a list of samples to form a
            mini-batch of Tensor(s). Used when using batched loading from a
            map-style dataset.
        pin_memory (bool, optional): If ``True``, the data loader will copy
            Tensors into CUDA page-locked host memory before returning them.
        drop_last (bool, optional): set to ``True`` to drop the last incomplete
            batch, if the dataset size is not divisible by the batch size.
            (default: ``False``)
        timeout (numeric, optional): if positive, the timeout value for
            collecting a batch from workers. Should always be non-negative.
            (default: ``0``)
        worker_init_fn (Callable, optional): If not ``None``, this will be
            called on each worker subprocess with the worker id (an int in
            ``[0, num_workers - 1]``) as input, before data loading.
            (default: ``None``)
        multiprocessing_context (str or context, optional): start method or
            ``multiprocessing`` context used to spawn the workers, e.g.,
            ``"fork"`` or ``"spawn"``. If ``None``, the default context of the
            platform is used. (default: ``None``)
        generator (Generator, optional): If not ``None``, this RNG will be used
            by RandomSampler to generate random indexes. (default: ``None``)
        prefetch_factor (int, optional): Number of batches loaded in advance by
            each worker. ``2`` means there will be a total of
            2 * :attr:`num_workers` batches prefetched across all workers.
            (default: ``2`` when :attr:`num_workers > 0`; otherwise must be
            ``None``)
        persistent_workers (bool, optional): If ``True``, the data loader will
            not shut down the worker processes after a dataset has been
            consumed once. This allows to maintain the workers `Dataset`
            instances alive. (default: ``False``)
        pin_memory_device (str, optional): Deprecated device spelling kept for
            accelerator for pinned host allocations. (default: ``""``)
        in_order (bool, optional): If ``False``, the data loader will not
            enforce that batches returned from multiprocessing workers are
            provided in the order the sampler produced them. This enables
            faster delivery of batches that complete early, at the cost of
            batch order no longer being deterministic. (default: ``True``)
        device (str, optional): device to move batches to after collation.
    """

    dataset: Dataset[_T_co]
    batch_size: Optional[int]
    num_workers: int
    pin_memory: bool
    pin_memory_device: str
    drop_last: bool
    timeout: float
    sampler: Sampler | Iterable
    generator: Optional["tp.Generator"]
    __initialized = False

    def __init__(
        self,
        dataset: Dataset[_T_co],
        batch_size: Optional[int] = 1,
        shuffle: Optional[bool] = None,
        sampler: Optional[Sampler | Iterable] = None,
        batch_sampler: Optional[Sampler | Iterable] = None,
        num_workers: int = 0,
        collate_fn: Optional[Callable[[List[Any]], Any]] = None,
        pin_memory: bool = False,
        drop_last: bool = False,
        timeout: float = 0,
        worker_init_fn: Optional[Callable[[int], None]] = None,
        multiprocessing_context=None,
        generator: Optional["tp.Generator"] = None,
        *,
        prefetch_factor: Optional[int] = None,
        persistent_workers: bool = False,
        pin_memory_device: str = "",
        in_order: bool = True,
        device: Optional[str] = None,
    ) -> None:
        if num_workers < 0:
            raise ValueError(
                "num_workers option should be non-negative; "
                "use num_workers=0 to disable multiprocessing."
            )

        if timeout < 0:
            raise ValueError("timeout option should be non-negative")

        if num_workers == 0 and prefetch_factor is not None:
            raise ValueError(
                "prefetch_factor option could only be specified in multiprocessing."
                "let num_workers > 0 to enable multiprocessing, otherwise set prefetch_factor to None."
            )
        elif num_workers > 0 and prefetch_factor is None:
            prefetch_factor = 2
        elif prefetch_factor is not None and prefetch_factor < 0:
            raise ValueError("prefetch_factor option should be non-negative")

        if persistent_workers and num_workers == 0:
            raise ValueError("persistent_workers option needs num_workers > 0")

        self.dataset = dataset
        self.num_workers = num_workers
        self.prefetch_factor = prefetch_factor
        self.pin_memory = pin_memory
        self.pin_memory_device = pin_memory_device
        self.timeout = timeout
        self.worker_init_fn = worker_init_fn
        self.multiprocessing_context = multiprocessing_context
        self.persistent_workers = persistent_workers
        self.in_order = in_order
        self.generator = generator
        self.device = device

        # Arg-check dataset related before checking samplers because we want to
        # tell users that iterable-style datasets are incompatible with custom
        # samplers first, so that they don't learn that this combo doesn't work
        # after spending time fixing the custom sampler errors.
        if isinstance(dataset, IterableDataset):
            self._dataset_kind = _DatasetKind.Iterable
            # We cannot check `shuffle is not None` here, since previously
            # `shuffle=False` was the default.
            if shuffle not in {False, None}:
                raise ValueError(
                    f"DataLoader with IterableDataset: expected unspecified shuffle option, but got shuffle={shuffle}"
                )
            if sampler is not None:
                raise ValueError(
                    f"DataLoader with IterableDataset: expected unspecified sampler option, but got sampler={sampler}"
                )
            elif batch_sampler is not None:
                raise ValueError(
                    "DataLoader with IterableDataset: expected unspecified "
                    f"batch_sampler option, but got batch_sampler={batch_sampler}"
                )
        else:
            shuffle = bool(shuffle)
            self._dataset_kind = _DatasetKind.Map

        if sampler is not None and shuffle:
            raise ValueError("sampler option is mutually exclusive with shuffle")

        if batch_sampler is not None:
            # auto_collation with custom batch_sampler
            if batch_size != 1 or shuffle or sampler is not None or drop_last:
                raise ValueError(
                    "batch_sampler option is mutually exclusive "
                    "with batch_size, shuffle, sampler, and "
                    "drop_last"
                )
            batch_size = None
            drop_last = False
        elif batch_size is None:
            # no auto_collation
            if drop_last:
                raise ValueError(
                    "batch_size=None option disables auto-batching "
                    "and is mutually exclusive with drop_last"
                )

        if sampler is None:  # give default samplers
            if self._dataset_kind == _DatasetKind.Iterable:
                sampler = _InfiniteConstantSampler()
            else:  # map-style
                if shuffle:
                    sampler = RandomSampler(dataset, generator=generator)
                else:
                    sampler = SequentialSampler(dataset)

        if batch_size is not None and batch_sampler is None:
            # auto_collation without custom batch_sampler
            batch_sampler = BatchSampler(sampler, batch_size, drop_last)

        self.batch_size = batch_size
        self.drop_last = drop_last
        self.sampler = sampler
        self.batch_sampler = batch_sampler

        if collate_fn is None:
            if self._auto_collation:
                collate_fn = default_collate
            else:
                collate_fn = default_convert

        self.collate_fn = collate_fn
        self._IterableDataset_len_called = (
            None  # See NOTE [ IterableDataset and __len__ ] in ``__len__``
        )

        self._iterator = None

        # the sampler/data stream.  Mutating these while a worker iterator is
        # alive can otherwise silently mix epochs or strand queued indices.
        self.__initialized = True

        self.check_worker_number_rationality()

    def check_worker_number_rationality(self) -> None:
        # Warn when num_workers exceeds the logical CPUs available to this
        if not self.num_workers or self.num_workers == 0:
            return
        max_num_worker_suggest = _cpu_count()
        cpuset_checked = hasattr(os, "sched_getaffinity")
        if max_num_worker_suggest is None or self.num_workers > max_num_worker_suggest:
            warnings.warn(
                _worker_rationality_warning_msg(
                    max_num_worker_suggest, self.num_workers, cpuset_checked
                ),
                stacklevel=2,
            )

    @property
    def _auto_collation(self) -> bool:
        return self.batch_sampler is not None

    @property
    def _index_sampler(self) -> Sampler | Iterable:
        # The actual sampler used for generating indices to read data at each
        # time. This would be ``batch_sampler`` if in auto-collation mode, and
        # ``sampler`` otherwise. We can't change ``sampler`` and
        # ``batch_sampler`` attributes for BC reasons.
        if self._auto_collation:
            return self.batch_sampler
        else:
            return self.sampler

    @property
    def multiprocessing_context(self):
        return self.__multiprocessing_context

    @multiprocessing_context.setter
    def multiprocessing_context(self, multiprocessing_context):
        if multiprocessing_context is not None:
            if self.num_workers > 0:
                if isinstance(multiprocessing_context, str):
                    valid_start_methods = multiprocessing.get_all_start_methods()
                    if multiprocessing_context not in valid_start_methods:
                        raise ValueError(
                            "multiprocessing_context option "
                            f"should specify a valid start method in {valid_start_methods!r}, but got "
                            f"multiprocessing_context={multiprocessing_context!r}"
                        )
                    multiprocessing_context = multiprocessing.get_context(
                        multiprocessing_context
                    )

                if not isinstance(
                    multiprocessing_context, multiprocessing.context.BaseContext
                ):
                    raise TypeError(
                        "multiprocessing_context option should be a valid context "
                        "object or a string specifying the start method, but got "
                        f"multiprocessing_context={multiprocessing_context}"
                    )
            else:
                raise ValueError(
                    "multiprocessing_context can only be used with "
                    "multiprocessing (num_workers > 0)"
                )
        self.__multiprocessing_context = multiprocessing_context

    def __setattr__(self, attr, value):
        if self.__initialized and attr in (
            "batch_size",
            "batch_sampler",
            "sampler",
            "drop_last",
            "dataset",
            "persistent_workers",
        ):
            raise ValueError(
                f"{attr} attribute should not be set after "
                f"{self.__class__.__name__} is initialized"
            )
        super().__setattr__(attr, value)

    def _get_iterator(self):
        if self.num_workers == 0:
            return _SingleProcessDataLoaderIter(self)
        else:
            self.check_worker_number_rationality()
            return _MultiProcessDataLoaderIter(self)

    def __iter__(self) -> Iterator[Any]:
        if self.persistent_workers and self.num_workers > 0:
            if self._iterator is None:
                # Create a single copy of this iterator per `DataLoader`
                # instance, so it can be re-used across epochs.
                self._iterator = self._get_iterator()
            else:
                self._iterator._reset(self)
            return self._iterator
        else:
            return self._get_iterator()

    def __len__(self) -> int:
        if self._dataset_kind == _DatasetKind.Iterable:
            # NOTE [ IterableDataset and __len__ ]
            #
            # For `IterableDataset`, `__len__` could be inaccurate when one
            # naively does multi-processing data loading, since the samples
            # will be duplicated. However, no real use case should be actually
            # using that behavior, so it should count as a user error. We
            # should generally trust user code to do the proper thing (e.g.,
            # configure each replica differently in `__iter__`), and give us
            # the correct `__len__` if they choose to implement it (this will
            # still throw if the dataset does not implement a `__len__`).
            #
            # To provide a further warning, we track if `__len__` was called on
            # the `DataLoader`, save the returned value in
            # `_IterableDataset_len_called`, and warn if the iterator ends up
            # yielding more than this number of samples.

            # Cannot statically verify that dataset is Sized
            length = self._IterableDataset_len_called = len(self.dataset)
            if (
                self.batch_size is not None
            ):  # IterableDataset doesn't allow custom sampler or batch_sampler
                if self.drop_last:
                    length = length // self.batch_size
                else:
                    length = math.ceil(length / self.batch_size)
            return length
        else:
            return len(self._index_sampler)

    def _finalize_batch(self, data: Any) -> Any:
        # Post-processing applied in the main process after a batch has been
        # skipped when no accelerator is available.
        if self.pin_memory and tp.cuda.is_available():
            data = _pin_memory(data)
        return self._apply_device(data)

    def _apply_device(self, data: Any) -> Any:
        if self.device is not None:
            data = self._move_to_device(data)
        return data

    def _move_to_device(self, data: Any) -> Any:
        if isinstance(data, Tensor):
            if self.device == "cpu":
                return data.cpu()
            return data.cuda(self.device, non_blocking=self.pin_memory)
        if isinstance(data, tuple):
            return tuple(self._move_to_device(item) for item in data)
        if isinstance(data, list):
            return [self._move_to_device(item) for item in data]
        if isinstance(data, dict):
            return {key: self._move_to_device(value) for key, value in data.items()}
        return data
Ask DeepWiki