Data loaders: preparing examples in parallel
Read these first if they are new to you
- Minibatches: a few examples per step Intermediate
- Reading Python Basic
The idea in one paragraph
Before the GPU can train on a minibatch, the computer must prepare it: read the examples from disk, change them (for example, rotate them at random), and stack them into one array. That work runs on the CPU. If the training program does it itself, the GPU sits idle while it waits. A data loader hands the work to several helper processes, called workers. They prepare the next batches while the GPU trains on the current one, so a batch is ready whenever the GPU asks for it.
A data loader turns a list of examples into a stream of batches
A data loader needs three things:
- A dataset: an object that knows how many examples there are and can prepare example number i on request.
- An order: usually a new random shuffle each epoch.
- A batch size: how many examples to stack into each batch. Stacking the examples into one array is called collating.
The snippet below builds all three in PyTorch. The dataset's two methods are the whole contract:
__len__ says how many examples there are, and __getitem__ prepares one. The
loader calls them for you. (Reading Python skips classes. The
Python Tutorial explains the class syntax in
A First Look at Classes.)
loader.py
import torch
# Load two names from torch.utils.data, so the code can write them without that prefix.
from torch.utils.data import Dataset, DataLoader
class Squares(Dataset):
# Ten examples; example i is the pair (i, i*i).
def __len__(self):
return 10
def __getitem__(self, i): # prepare one example
# A tensor, PyTorch's array, that holds i and i * i as 32-bit decimal numbers.
return torch.tensor([i, i * i], dtype=torch.float32)
if __name__ == "__main__":
# fix the random shuffle, so each run prints the same order
torch.manual_seed(0)
loader = DataLoader(Squares(), batch_size=4, shuffle=True, num_workers=2)
for epoch in range(2):
for batch in loader:
# Print the batch's shape, then the i of each example in it as a list
# of whole numbers.
print(epoch, tuple(batch.shape), batch[:, 0].int().tolist())
0 (4, 2) [6, 7, 1, 4]
0 (4, 2) [2, 0, 9, 8]
0 (2, 2) [3, 5]
1 (4, 2) [2, 4, 7, 0]
1 (4, 2) [8, 9, 5, 3]
1 (2, 2) [6, 1]
Each epoch visits all ten examples once, in a new order. The last batch is short, because 10 is not a
multiple of 4. num_workers=2 starts two worker processes. A process is a separate
running program with its own memory, so each worker can run on its own CPU core at the same time as the
others. Some systems start each worker as a fresh Python that reads the file again. The
if __name__ == "__main__": line keeps such a worker from running the training code
too.
Too few workers leave the GPU waiting
Each worker prepares a whole batch, then starts on another one. The training loop takes the batches in order. If the workers together prepare batches more slowly than the GPU uses them, the GPU waits.
Preparing one batch takes 300 ms of one CPU core. Training on one batch takes the GPU 100 ms.
- No workers. The training program prepares each batch, then trains on it: 300 + 100 = 400 ms per step. The GPU is busy 100 ms out of every 400 ms, or 25% of the time.
- One worker. It prepares the next batch while the GPU trains. A batch is ready every 300 ms, so a step takes 300 ms. The GPU is busy 100 / 300 = 33% of the time.
- Three workers. Each makes a batch every 300 ms, so together they make one every 300 / 3 = 100 ms. That matches the GPU. It is now busy all the time: 10 steps per second.
- Four workers. Still 100 ms per step. The GPU is now the slowest part, so the extra worker only adds batches to the waiting line.
The rule: with k workers, a step takes the longer of the training time and the preparation time divided by k. So you need about (preparation time) ÷ (training time) workers.
Your turn. Preparation takes 450 ms and training 150 ms. How many workers keep the GPU busy? (Answer: 450 ÷ 150 = 3.)
Try it. The timeline below starts with one worker. Predict how many workers you need for the numbers in the worked example. Then move the slider until the blue training blocks touch with no gaps. Then make preparation slower and find the new number.
The timeline gives every batch the same preparation time and ignores the time to copy a batch to the GPU. Real loaders also limit how far ahead workers may run: PyTorch lets each worker hold two batches by default. The CPU cores and memory set the real limit, because each worker is a separate process with its own copy of the dataset object.
Check yourself
-
Preparing a batch takes 500 ms and training on it takes 100 ms. You use 2 workers. How long does a step take, and what fraction of the time is the GPU busy?
Answer
The two workers make a batch every 500 / 2 = 250 ms. That is longer than 100 ms, so a step takes 250 ms, and the GPU is busy 100 / 250 = 40% of the time. Five workers would keep it busy.
-
You go from 8 workers to 16, and training gets no faster. What does that tell you?
Answer
The data is no longer the slow part. The GPU's own work, or something else such as copying to the GPU, now sets the pace. Extra workers only use more CPU and memory.
-
Why does the dataset in the snippet prepare one example in
__getitem__, rather than one batch?Answer
So that the loader can choose the order and the batch size. It asks for the examples it wants, in shuffled order, and collates them into a batch itself.
Go further
Optional background, for readers who want more depth.
-
Builds a dataset for image files with
__init__,__len__and__getitem__, then wraps it in a loader. -
How workers start, how they get their random seeds, and the platform details this unit leaves out.
Where this shows up
Units that build on this one
- The training loop Intermediate