''' ----------------------------------------------------------------------------- Copyright (c) 2023, NVIDIA CORPORATION. All rights reserved. NVIDIA CORPORATION and its licensors retain all intellectual property and proprietary rights in and to this software, related documentation and any modifications thereto. Any use, reproduction, disclosure or distribution of this software and related documentation without an express license agreement from NVIDIA CORPORATION is strictly prohibited. ----------------------------------------------------------------------------- ''' import math import torch.distributed as dist import torch from torch.utils.data import Sampler from typing import TypeVar T_co = TypeVar('T_co', covariant=True) class DistributedSamplerPreemptable(Sampler[T_co]): r"""Sampler that supports loading from an iteration. This is very useful for preemptable jobs. Args: dataset (torch.utils.data.Dataset): Dataset object num_replicas (int): Number of replicas to the distribute the dataloader over. This is typically the world size in DDP jobs. rank (int): Rank of the current process. shuffle (bool): Whether to shuffle the dataloader in each epoch. seed (int): Random seed used for shuffling the dataloader. drop_last (bool): Whether to drop the last batch. """ def __init__(self, dataset, num_replicas=None, rank=None, shuffle=True, seed=0, drop_last=False): if num_replicas is None: if not dist.is_available(): raise RuntimeError("Requires distributed package to be available") num_replicas = dist.get_world_size() if rank is None: if not dist.is_available(): raise RuntimeError("Requires distributed package to be available") rank = dist.get_rank() if rank >= num_replicas or rank < 0: raise ValueError( "Invalid rank {}, rank should be in the interval" " [0, {}]".format(rank, num_replicas - 1)) self.dataset = dataset self.num_replicas = num_replicas self.rank = rank self.epoch = 0 # start_index is the index to begin the dataloader from. self.start_index = 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: # type: ignore[arg-type] # 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 # type: ignore[arg-type] ) else: self.num_samples = math.ceil(len(self.dataset) / self.num_replicas) # type: ignore[arg-type] self.total_size = self.num_samples * self.num_replicas self.shuffle = shuffle self.seed = seed def __iter__(self): if self.shuffle: # deterministically shuffle based on epoch and seed g = torch.Model() g.manual_seed(self.seed + self.epoch) indices = torch.randperm(len(self.dataset), generator=g).tolist() # type: ignore[arg-type] else: indices = list(range(len(self.dataset))) # type: ignore[arg-type] 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] assert len(indices) == self.total_size # subsample indices = indices[self.rank:self.total_size:self.num_replicas] assert len(indices) == self.num_samples # assert self.start_index < len(indices) if self.start_index >= len(indices): print('(Warning): Start index is less than len of dataloader. Goint to the last batch of dataset instead') # This is hardcoded to go one batch before. self.start_index = len(indices) - 64 indices = indices[self.start_index:] return iter(indices) def __len__(self): return self.num_samples def set_epoch(self, epoch): self.epoch = epoch def set_iteration(self, start_index): self.start_index = start_index