Choosing Between PyTorch DataLoader and DistributedSampler for Multi‑GPU Training
Decide when to use a plain DataLoader versus pairing it with DistributedSampler for efficient, non‑overlapping data access across multiple GPUs.
13 Jan 2026, 00:30 UTC

Decision point
You are setting up a training script that will run on more than one GPU (e.g., via torch.distributed.launch or torchrun). The core decision is whether to rely on a standard torch.utils.data.DataLoader alone or to combine it with a torch.utils.data.distributed.DistributedSampler to ensure each GPU processes a distinct subset of the data.
Constraints
- Training must be deterministic across epochs when shuffling is required.
- Each GPU should see a non‑overlapping slice of the dataset to avoid redundant gradient updates.
- I/O latency should be hidden by using multiple worker processes (
num_workers > 0). - The code must work with the default PyTorch distributed backend (NCCL or GLOO).
Options comparison
| Option | When it fits | Key requirement | Typical pitfalls |
|---|---|---|---|
| Standard DataLoader only | Single‑device training, or simple data streaming where order does not matter. | No distributed process group needed. | In multi‑GPU launches every process loads the entire dataset → duplicate work and biased gradients. |
| DataLoader + DistributedSampler | Multi‑GPU training where each rank must see a unique partition. | Initialize torch.distributed.init_process_group before creating the sampler. |
Setting shuffle=True in DataLoader conflicts with the sampler; shuffling must be done by the sampler itself. |
Trade‑offs
The plain DataLoader is simpler to code and incurs no extra synchronization overhead, but it scales poorly beyond a single GPU because each process redundantly reads the same samples. Adding DistributedSampler introduces a small amount of setup (process group initialization and sampler instantiation) and requires you to manage the sampler’s epoch‑based seed for shuffling, yet it guarantees that each GPU works on a disjoint subset, eliminates wasted compute, and lets you still benefit from num_workers for asynchronous I/O.
Concrete implementation
The following snippet shows how to create a dataset, a DistributedSampler, and a DataLoader that works with multiple workers. Place this code inside your training script after torch.distributed.init_process_group has been called.
import torch
import torch.distributed as dist
from torch.utils.data import Dataset, DataLoader, DistributedSampler
class DummyDataset(Dataset):
def __init__(self, size=1000):
self.size = size
def __len__(self):
return self.size
def __getitem__(self, idx):
# return a tensor and a label; here we just return the index
return torch.tensor([idx], dtype=torch.float32), torch.tensor(idx % 10)
# Assume dist.init_process_group(backend='nccl') has already been called
dataset = DummyDataset(size=5000)
sampler = DistributedSampler(
dataset,
num_replicas=dist.get_world_size(),
rank=dist.get_rank(),
shuffle=True, # shuffling handled by the sampler
seed=42 # same seed across ranks for deterministic epochs
)
loader = DataLoader(
dataset,
batch_size=32,
sampler=sampler,
num_workers=4, # adjust based on CPU cores and shared memory limits
pin_memory=True
)
# Example training loop (one epoch)
for epoch in range(2):
sampler.set_epoch(epoch) # crucial for shuffling to change each epoch
for batch_idx, (data, target) in enumerate(loader):
# forward / backward / optimizer step omitted for brevity
pass
if dist.get_rank() == 0:
print(f'Epoch {epoch} completed on rank {dist.get_rank()}')
Validation steps
- Launch the script with at least two processes, e.g.,
torchrun --nproc_per_node=2 script.py. - Add a temporary debug print inside the data loading loop to output the first element of each batch together with the current rank:
- Check the console output: the printed indices for rank 0 and rank 1 should belong to non‑overlapping ranges (e.g., rank 0 sees indices 0‑15, rank 1 sees 16‑31 for a batch size of 16). No index should appear in both ranks’ prints.
- To verify the effect of
num_workers, run the same script withnum_workers=0and then withnum_workers=4and measure epoch wall‑time (usingtime.time()around the training loop). You should observe a reduction in epoch time when workers are > 0, assuming the dataset is I/O bound.
if batch_idx == 0:
print(f'Rank {dist.get_rank()} first batch indices: {data.squeeze().tolist()}')
Limitations and practical checks
- The DistributedSampler assumes the dataset size is evenly divisible by
num_replicasbatch_size; otherwise the last few samples may be dropped. You can setdrop_last=Falsein the DataLoader to keep them, but then some ranks may receive one extra sample. - High
num_workersvalues can exhaust the/dev/shmshared memory on Linux. Monitor usage withdf -h /dev/shmand reduce workers if you see “Unable to allocate shared memory” errors. - Shuffling determinism relies on passing the same
seedto every rank and callingsampler.set_epoch(epoch)each epoch. Forgetting this call results in identical shuffling across epochs.
Takeaway: For any multi‑GPU training launch, pair DataLoader with DistributedSampler to guarantee each GPU works on a unique, shuffled partition of the data while still gaining I/O parallelism from num_workers. Use the validation steps above to confirm non‑overlapping index distribution and to tune worker count for your hardware.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.