PyTorch DataLoader and IterableDataset: Worker-Level Data Shuffling
0 reputation · 20 Mar 2021, 02:02 UTC
0 reputation · 20 Mar 2021, 02:02 UTC
When scaling data pipelines for large-scale datasets, torch.utils.data.IterableDataset is used to stream data and avoid loading the entire index into memory. This approach is essential for datasets that exceed available system RAM.
A challenge arises when integrating this streaming behavior with multi-process loading via the num_workers parameter. While DataLoader manages the distribution of data across workers, the IterableDataset does not natively synchronize indices or provide global shuffling across these separate worker processes.
To maintain training stochasticity, developers often implement a worker_init_fn to seed each process differently. However, this does not inherently prevent multiple workers from yielding the same data shards if the dataset logic is not explicitly partitioned.
DataLoader ensure a unique, non-overlapping distribution of data across multiple workers when using an IterableDataset?34775 reputation · 20 Mar 2021, 03:32 UTC
To prevent multiple workers from yielding the same data shards, you must explicitly partition the dataset within the __iter__ method using torch.utils.data.get_worker_info(). Because IterableDataset does not support the shuffle=True argument in DataLoader, the dataset itself is responsible for determining which subset of data each worker process should stream.
get_worker_info() inside the __iter__ method to access the id and num_workers of the current process.index % num_workers == worker_id.num_workers is set to 0 (main process).True global shuffling is impossible with a pure stream without loading the entire index into memory. The recommended mechanism to approximate this is a Shuffle Buffer. This approach maintains a fixed-size window of samples and yields them randomly, providing local stochasticity that scales with the buffer size.
N samples from the partitioned stream.worker_init_fn to ensure each worker's random number generator is seeded differently, preventing synchronized shuffling patterns across processes.# Conceptual implementation within __iter__
worker_info = torch.utils.data.get_worker_info()
if worker_info is None: # single-process loading
iter_start = 0
iter_end = len(self.data)
else:
# Partition data
per_worker = int(math.ceil(len(self.data) / float(worker_info.num_workers)))
worker_id = worker_info.id
iter_start = worker_id * per_worker
iter_end = min(iter_start + per_worker, len(self.data))
# Use a buffer for local shuffling
buffer = []
for i in range(iter_start, iter_end):
item = self.data[i]
if len(buffer) < buffer_size:
buffer.append(item)
else:
idx = random.randint(0, buffer_size - 1)
yield buffer[idx]
buffer[idx] = item
# Drain remaining buffer
random.shuffle(buffer)
for item in buffer: yield item
This implementation assumes PyTorch 1.7+ where get_worker_info() is stable. To verify the setup, print the worker_id and the first few sample IDs in each worker; they should be distinct and non-overlapping. Warning: Be cautious with buffer_size; since each worker maintains its own buffer, the total memory consumption is buffer_size * num_workers * sample_size.
Diagnostic Detail Needed: Are you streaming from a single large file (like a TFRecord or WebDataset) or a directory of many small files? The partitioning logic differs significantly between these two sources.
Use comments to ask for clarification. Post a solution as an answer.
34,775 reputation · 20 Mar 2021, 09:32 UTC
One gap in the shard-offset formula: under DistributedDataParallel, every rank process builds its own DataLoader, and each of those spawns workers numbered 0..num_workers-1. Partitioning on worker_id alone makes rank 0's worker 0 and rank 1's worker 0 stream identical shards. On PyTorch 2.x the usual fix is to widen the partition:
info = torch.utils.data.get_worker_info()
wid = info.id if info else 0
nw = info.num_workers if info else 1
gid = torch.distributed.get_rank() * nw + wid
total = torch.distributed.get_world_size() * nw
# stream shards where shard_index % total == gidTwo related caveats worth checking against your pinned version:
torch.initial_seed() inside __iter__ already differs per worker and, in default operation, per epoch, because the DataLoader derives a fresh base seed each pass — a convenient seed for per-epoch shard permutations without threading epoch numbers through worker_init_fn.persistent_workers=True, worker processes survive across epochs, so attribute changes made on the dataset object in the main process won't reach them after the first pass.