Concepts / Distributed Training with Multiple GPUs

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.

  • Programming

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?

  • Only one piece (a subset of the layers) of the network

  • A complete copy of the whole network

Reveal answer
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.

Model on CPUstarting modelGPU 0full model copyGPU 1full model copyGPU 2full model copyGPU 3full model copycopycopycopycopy
If there are four GPUs, what does each GPU's memory contain under DDP, and how does that differ from splitting one model's layers across the GPUs?
Key point

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.

Datasetindices 0 to 999GPU 0samples 0 to 249GPU 1samples 250 to 499GPU 2samples 500 to 749GPU 3samples 750 to 999quarterquarterquarterquarter
When a dataset of 1,000 samples is split across 4 GPUs for one epoch, which sample indices does each GPU process, and why do none of them overlap?
Worked example

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?

  1. 1
    Count the samples

    The dataset holds 1,000 samples, indexed from 0 to 999.

  2. 2
    Divide among GPUs

    With 4 GPUs, each GPU's share is 1,000 divided by 4, which is 250 samples.

  3. 3
    Assign 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.

  4. 4
    Check 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.

Result

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.

Forward passeach GPU, its own batchBackward passlocal gradientsGradient exchangeacross all GPUsCombined gradientssame on every GPUWeight updateidentical copiesnextnextcombineapply
After each GPU computes its local gradients, in what order do the values move between GPUs, and how does every GPU end up applying the same update?

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.

Rule

Formula, step by step
  1. 1
    $t_{\text{single}}$ is the time one GPU needs for a full training epoch.
  2. 2
    $N$ is the number of GPUs taking part in training.
  3. 3
    $t_{\text{ideal}}$ is the epoch time predicted for N GPUs under the idealized estimate.
  4. 4
    The formula ignores communication overhead, so it describes the best case rather than a guaranteed result.
Worked example

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?

  1. 1
    Identify the inputs

    t_single is 100 seconds and N is 4.

  2. 2
    Apply the idealized estimate

    Dividing 100 seconds by 4 gives 25 seconds.

  3. 3
    Add 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.

Result

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.

AspectSingle-GPU trainingDDP training
Model copiesOne modelOne full copy of the model on each GPU
Data per iterationOne stream of batchesA different, nonoverlapping minibatch for each GPU
Gradient stepGradients from one deviceGradients from all GPUs are synchronized and combined
Expected epoch timet_singleIdeally t_single divided by N, with communication overhead making the real gain smaller
Overall training goalTrain one modelSame 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 like

    Picturing a four-layer network with each GPU holding one layer and passing activations down an assembly line.

    Why it fails

    DDP 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 instead

    Picture 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 like

    Expecting 4 GPUs to finish a 100-second epoch in exactly 25 seconds.

    Why it fails

    The 25-second figure is an idealized estimate that ignores communication overhead, which is always present with DDP.

    Do this instead

    Treat 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 like

    Feeding each GPU the same batch from an unsharded loader.

    Why it fails

    Each copy is supposed to process a different minibatch, and overlapping data wastes the parallel work.

    Do this instead

    Use 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 like

    Letting each GPU update its own copy from its own gradients only.

    Why it fails

    Each copy sees different data and computes different gradients, so the copies would gradually drift apart.

    Do this instead

    Synchronize the gradients across GPUs so every copy applies the same update.

  • Common mistake

    Running DDP code inside a notebook cell

    What it looks like

    Pasting the full DDP training loop into one notebook kernel and running it cell by cell.

    Why it fails

    DDP needs a separate process per GPU, and a notebook's single interactive kernel does not fit that process model.

    Do this instead

    Write the DDP code as a standalone Python script and launch it outside the notebook.

Checking Your Understanding With a New Setup

EASY

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_single divided by N, with t_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?

  • It runs exactly as it would as a script, with no changes needed

  • It does not fit the one-process-per-GPU model, so the setup breaks down

Reveal answer
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

  1. 1
    DDP gives each GPU its own process and a complete copy of the model, rather than splitting one model across devices.
  2. 2
    A DistributedSampler gives each GPU a different, nonoverlapping minibatch, so the copies process different data.
  3. 3
    Gradients are combined across GPUs after the local forward and backward passes, so every copy applies the same update and stays identical.
  4. 4
    The ideal speedup is t_single divided by N, but communication overhead means real gains are smaller than this estimate.
  5. 5
    DDP 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_single divided 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.

Related concepts

More concepts starting with D