~14 min · dataset, dataloader, num_workers, collate
Level 0Tensor Curious
0 XP0/62 lessons0/13 achievements
0/120 XP to next level120 XP to go0% complete
Two abstractions that keep your GPU fed
PyTorch separates "what is a sample" from "how do we batch them efficiently":
Dataset — defines __len__ and __getitem__(idx). The minimal contract: how many samples are there, and how do I get the i-th one.
DataLoader — wraps a Dataset and adds batching, shuffling, parallel loading via worker processes, and pin-memory for fast GPU transfer.
The DataLoader knobs that matter
batch_size — obvious.
shuffle=True — randomize order each epoch (training only; never on val/test).
num_workers — number of subprocesses for parallel loading. Set to N (where N = number of CPU cores) for I/O-bound datasets. 0 means main-process loading (slowest, but easiest to debug).
pin_memory=True — allocate batches in page-locked memory for faster CPU→GPU transfer. Combined with x.to(device, non_blocking=True), this gives real speedups.
prefetch_factor — how many batches each worker prefetches ahead. Default 2; raising helps when the GPU finishes a step before the next batch is ready.
persistent_workers=True — keep worker processes alive across epochs. Avoids the cost of re-spinning workers each epoch.
drop_last=True — drop the last incomplete batch. Useful when you want consistent batch sizes (BatchNorm stats, distributed training).
The macOS num_workers gotcha
On macOS, multiprocessing default is "spawn" (not "fork" as on Linux). This means each worker re-imports your module — heavy datasets can be slow to start, and any object that doesn't pickle cleanly will error. Workaround: use num_workers=0 for development, raise it for production runs.
Code
Custom Dataset — the minimal contract·python
import torch
from torch.utils.data import Dataset, DataLoader
class TensorDataset(Dataset):
def __init__(self, X, y, transform=None):
self.X = X
self.y = y
self.transform = transform
def __len__(self):
return len(self.X)
def __getitem__(self, idx):
x = self.X[idx]
if self.transform is not None:
x = self.transform(x)
return x, self.y[idx]
X = torch.randn(1000, 10)
y = torch.randint(0, 5, (1000,))
ds = TensorDataset(X, y)
print(len(ds), ds[0][0].shape, ds[0][1])
Production-shaped DataLoader·python
import torch
from torch.utils.data import DataLoader
loader = DataLoader(
dataset,
batch_size=64,
shuffle=True,
num_workers=8, # match CPU cores
pin_memory=True, # fast CPU→GPU
prefetch_factor=4, # batches each worker prefetches ahead
persistent_workers=True, # keep workers alive across epochs
drop_last=True, # consistent batch shape
)
device = "cuda"
for x, y in loader:
x = x.to(device, non_blocking=True)
y = y.to(device, non_blocking=True)
# ...training step...
random_split — train / val from one Dataset·python
Build a Dataset that loads images from a directory tree (root/class_name/image.jpg). Implement __len__ and __getitem__. Wrap it in a DataLoader with num_workers=4 and pin_memory=True. Time the throughput in samples/second on your machine — useful baseline for spotting slowdowns later.
Progress
Progress is local-only — sign in to sync across devices.