Distributed Training with Multiple GPUs
In DDP, each GPU holds the whole model. What is shared across GPUs is the training work, not the model's pieces.
Starting From a Wrong Picture
When people first hear that a model is being trained on several GPUs at once, a natural picture forms: the network is cut into pieces, each GPU holds one piece, and the pieces pass data back and forth like an assembly line. That picture is a fair description of some other parallel techniques. It is not how PyTorch's DistributedDataParallel strategy works. This article starts from that picture on purpose, so you can test it against the mechanism.
What do you think happens?
Two GPUs are training one network with DDP. What does each GPU hold in its memory?
Reveal answer
A complete copy of the whole network
DDP runs a separate process on each GPU, and each process keeps a complete copy of the model rather than a piece of it. The assembly-line picture describes other techniques, not DDP.
Replication Instead of Splitting the Model
DDP uses replication. The model starts on the CPU, and a copy of it is placed on each GPU. Every copy is the full network, with all of its layers and all of its weights. Each GPU runs its own process, and that process works with its own copy. Nothing about the model is cut apart to fit across devices.
In DDP, each GPU holds the whole model. What is shared across GPUs is the training work, not the model's pieces.
The two copies start out identical. Keeping them identical during training takes extra work, which the later sections explain. First, the data side of the strategy needs to be clear.
Giving Each GPU Different Data
Each model copy is fed a different minibatch from the data loader in every training iteration. The copies therefore never process the same examples at the same time. A DistributedSampler is the tool that keeps those minibatches from overlapping, so two GPUs are not both working on the same samples. Replication gives each device a full model, and the sampler gives each device a distinct slice of the data.
Splitting a 1,000-Sample Dataset Across 4 GPUs
A dataset has 1,000 samples and there are 4 GPUs. For one epoch, how many samples does each GPU process, and why is there no overlap?
- 1Count the samples
The dataset holds 1,000 samples, indexed from 0 to 999.
- 2Divide among GPUs
With 4 GPUs, each GPU's share is 1,000 divided by 4, which is 250 samples.
- 3Assign nonoverlapping ranges
GPU 0 takes samples 0 to 249, GPU 1 takes 250 to 499, GPU 2 takes 500 to 749, and GPU 3 takes 750 to 999. Each index belongs to exactly one GPU.
- 4Check for overlap
Because the ranges are disjoint, no two GPUs process the same example during that epoch. The DistributedSampler is responsible for making this split hold.
Each GPU processes 250 distinct samples, and together the four GPUs cover all 1,000 samples exactly once. This is a simplified illustration of the nonoverlapping idea, not a description of the sampler's internal ordering.
Combining Gradients So Copies Stay Identical
Because each copy sees a different slice of the data, its outputs differ from the others. Each copy produces its own logits and computes its own gradients during the backward pass. If nothing were shared, the copies would gradually drift apart and stop being the same model. DDP prevents this by combining the gradients from all GPUs and using the combined result to update every copy in the same way.
Trace one iteration in order. Each GPU runs its forward pass on its own minibatch, producing its own logits. Each GPU then runs its backward pass, producing its own local gradients. The forward and backward passes run independently, and only after both finish do the GPUs exchange gradient information. Once the gradients are synchronized, every replica applies the same update, so every copy ends the iteration with the same weights.
Ideal Speedup and Where It Falls Short
The ideal speedup from DDP is near-linear in the number of GPUs. If a single GPU needs a given amount of time for a full training epoch, the idealized estimate divides that time by the number of GPUs.
- 1$t_{\text{single}}$ is the time one GPU needs for a full training epoch.
- 2$N$ is the number of GPUs taking part in training.
- 3$t_{\text{ideal}}$ is the epoch time predicted for N GPUs under the idealized estimate.
- 4The formula ignores communication overhead, so it describes the best case rather than a guaranteed result.
Ideal Epoch Time on 4 GPUs
If one GPU takes 100 seconds per epoch, what would 4 GPUs ideally take, and why is the real result likely slower?
- 1Identify the inputs
t_singleis 100 seconds and N is 4. - 2Apply the idealized estimate
Dividing 100 seconds by 4 gives 25 seconds.
- 3Add the missing cost
The estimate ignores communication overhead, which is always present with DDP. The gradient exchange and the synchronization it requires take time that the ideal formula leaves out.
The ideal estimate is 25 seconds per epoch. The real epoch time is expected to be longer than 25 seconds, though still shorter than the 100-second single-GPU run, because the gains from DDP are smaller than the theoretical estimate.
| Aspect | Single-GPU training | DDP training |
|---|---|---|
| Model copies | One model | One full copy of the model on each GPU |
| Data per iteration | One stream of batches | A different, nonoverlapping minibatch for each GPU |
| Gradient step | Gradients from one device | Gradients from all GPUs are synchronized and combined |
| Expected epoch time | t_single | Ideally t_single divided by N, with communication overhead making the real gain smaller |
| Overall training goal | Train one model | Same training goal, with the work organized across GPUs |
The differences that matter most when deciding whether DDP is worth using.
Why DDP Runs as a Standalone Script
DDP code is written as a script that is launched outside the notebook. The process model explains why. DDP runs a separate process on each GPU, and those processes run independently while coordinating at the gradient step. A notebook, by contrast, gives you a single interactive session. The DDP structure of one process per GPU does not fit inside that one session.
Moving from a single-GPU script to a DDP script is mostly a matter of setup. You import the additional distributed submodules, classes, and functions from PyTorch, and you arrange for each process to operate on its own GPU and its own data subset. The core training logic stays familiar.
Mistakes Beginners Make With DDP
Common mistake Assuming DDP splits the network into pieces, one piece per GPU
What it looks likePicturing a four-layer network with each GPU holding one layer and passing activations down an assembly line.
Why it failsDDP runs a separate process on each GPU, and each process keeps a complete copy of the model rather than a piece of it.
Do this insteadPicture four identical full copies of the model, each working on a different minibatch.
Common mistake Expecting the speedup to equal the number of GPUs
What it looks likeExpecting 4 GPUs to finish a 100-second epoch in exactly 25 seconds.
Why it failsThe 25-second figure is an idealized estimate that ignores communication overhead, which is always present with DDP.
Do this insteadTreat t_single divided by N as a best-case target, and expect the real gain to be smaller.
Common mistake Giving every GPU the same data
What it looks likeFeeding each GPU the same batch from an unsharded loader.
Why it failsEach copy is supposed to process a different minibatch, and overlapping data wastes the parallel work.
Do this insteadUse a DistributedSampler so that each GPU receives a distinct, nonoverlapping slice of the data.
Common mistake Assuming the copies stay identical without any shared step
What it looks likeLetting each GPU update its own copy from its own gradients only.
Why it failsEach copy sees different data and computes different gradients, so the copies would gradually drift apart.
Do this insteadSynchronize the gradients across GPUs so every copy applies the same update.
Common mistake Running DDP code inside a notebook cell
What it looks likePasting the full DDP training loop into one notebook kernel and running it cell by cell.
Why it failsDDP needs a separate process per GPU, and a notebook's single interactive kernel does not fit that process model.
Do this insteadWrite the DDP code as a standalone Python script and launch it outside the notebook.
Checking Your Understanding With a New Setup
A DDP run uses 8 GPUs, and the dataset has 800 samples. For one epoch, how many distinct samples does each GPU process, and what is the idealized epoch time if one GPU takes 160 seconds per epoch? Explain why the real epoch time would be expected to be longer than the ideal value.
Hints
- Divide the dataset size by the number of GPUs to find the nonoverlapping share for each GPU.
- Apply
t_ideal=t_singledivided by N, witht_single= 160 and N = 8. - Think about what the idealized estimate leaves out, using the phrase communication overhead.
What do you think happens?
If you run the same DDP script inside a notebook cell instead of launching it as a standalone script, what is the most likely outcome?
Reveal answer
It does not fit the one-process-per-GPU model, so the setup breaks down
DDP runs a separate process on each GPU, while a notebook gives a single interactive kernel. That is why DDP code must run as a standalone script launched outside the notebook.
Key Takeaways
- 1DDP gives each GPU its own process and a complete copy of the model, rather than splitting one model across devices.
- 2A DistributedSampler gives each GPU a different, nonoverlapping minibatch, so the copies process different data.
- 3Gradients are combined across GPUs after the local forward and backward passes, so every copy applies the same update and stays identical.
- 4The ideal speedup is
t_singledivided by N, but communication overhead means real gains are smaller than this estimate. - 5DDP code runs as a standalone Python script launched outside the notebook, because each GPU needs its own process.
At a glance
- DDP runs a separate process on each GPU, and each process keeps a complete copy of the model rather than a piece of it.
- A DistributedSampler gives each GPU a different, nonoverlapping minibatch, so the copies process different data.
- Gradients from all GPUs are synchronized, so every copy applies the same update and ends each iteration with identical weights.
- The ideal speedup is near-linear, estimated as
t_singledivided by N, but communication overhead makes real gains smaller. - DDP code must run as a standalone Python script launched outside the notebook, because each GPU runs its own process.