Comparison Data Parallel Distributed data parallel

henry_Kang: <p>Hello. I hope you are very well.<br>
I am finalizing my experiment with pytorch. When I finish my paper, I hope I can share my paper in here.<br>
Anyway, is there any detailed documentation about data parallel(dp) and distributed data parallel(ddp)<br>
During my experiment, DP and DDP have big accuracy difference with same dataset, network, learning rate, and loss function. I hope I can put this experiment results in my paper but my professor asks the detailed explanation of why it happens. My dataset is a very unique image dataset and it is not a normal object such as imagenet or city scape stuff, so it can be a very different result than usual computer science paper. In this reason, I look around and read some articles.<br>
<a href="https://yangkky.github.io/2019/07/08/distributed-pytorch-tutorial.html" class="onebox" target="_blank" rel="nofollow noopener">https://yangkky.github.io/2019/07/08/distributed-pytorch-tutorial.html</a><br>
<a href="https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/" class="onebox" target="_blank" rel="nofollow noopener">https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/</a></p>
<p>However, I am still confused about this two different multi gpu training strategies.</p>
<ol>
<li>What is the “reduce” mean. The “reduce” is the weight update or loss reduction.</li>
<li>What is the major difference between DP and DDP in the weight update strategy? I think this is important.</li>
<li>DDP affects the batch normalization (BN) or DDP still needs the synchronized BN.<br>
Thank you for reading my question.</li>
</ol>

mrshenli: <p>There are some comparison between DP and DDP here: <a href="https://pytorch.org/tutorials/beginner/dist_overview.html" class="inline-onebox" rel="noopener nofollow ugc">PyTorch Distributed Overview — PyTorch Tutorials 2.1.1+cu121 documentation</a></p>
<blockquote>
<ol>
<li>What is the “reduce” mean. The “reduce” is the weight update or loss reduction.</li>
</ol>
</blockquote>
<p>What’s the context here? If you mean <code>all_reduce</code>, it is a collective communication operation. DDP uses it to synchronize gradients. see <a href="https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html#allreduce" rel="noopener nofollow ugc">https://docs.nvidia.com/deeplearning/nccl/user-guide/docs/usage/collectives.html#allreduce</a></p>
<blockquote>
<ol start="2">
<li>What is the major difference between DP and DDP in the weight update strategy? I think this is important.</li>
</ol>
</blockquote>
<p>Weight update is done by the optimizer, so if you are using the same optimizer the weight update strategy should be the same. The difference between DP and DDP is how they handle gradients. DP accumulates gradients to the same <code>.grad</code> field, while DDP first use <code>all_reduce</code> to calculate the gradient sum across all processes and divide that by <code>world_size</code> to compute the mean. More details can be found in <a href="https://arxiv.org/pdf/2006.15704.pdf" rel="noopener nofollow ugc">this paper</a>.</p>
<p>The above difference has impact on how lr should be configured. See this discussion: <a href="https://discuss.pytorch.org/t/should-we-split-batch-size-according-to-ngpu-per-node-when-distributeddataparallel/72769" class="inline-onebox">Should we split batch_size according to ngpu_per_node when DistributedDataparallel</a></p>
<blockquote>
<ol start="3">
<li>DDP affects the batch normalization (BN) or DDP still needs the synchronized BN.<br>
Thank you for reading my question.</li>
</ol>
</blockquote>
<p>By default, DDP will broadcast buffers from rank 0 to all other ranks, so yes, it does affect BN.</p>
<p>BTW, for distributed training related questions, could you please add a “distributed” tag to the post? There is a oncall team monitoring that tag.</p>

henry_Kang: <aside class="quote no-group" data-username="mrshenli" data-post="2" data-topic="93271">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/mrshenli/48/12220_2.png" class="avatar"> mrshenli:</div>
<blockquote>
<p>Weight update is done by the optimizer, so if you are using the same optimizer the weight update strategy should be the same. The</p>
</blockquote>
</aside>
<ol>
<li>Well, many people talk about “reduce” but in the context “reduce” does not seem like the literary “reduce”. That is why I ask the question. Because it keeps coming but no one defines this term first when they use it.</li>
<li>what is different between reducing gradients and weight update. Do you mean, DP and DDP exactly update the same weight and same updated each layer right? It is also confusing to me.<br>
Do you mean Batch size or LR size?  You link the batch size about it.</li>
<li>I face that there is no improvement when I use the DDP with synchronized BN. That is why I am asking third question.</li>
</ol>

mrshenli: <blockquote>
<p>what is different between reducing gradients and weight update.</p>
</blockquote>
<p>There are many weight updating algorithms, e.g., Adam, SGD, Adagrad, etc. (see more <a href="https://pytorch.org/docs/stable/optim.html" rel="noopener nofollow ugc">here</a>). And they are all independent from DP or DDP. So even if the gradient is the same, different optimizers can update the weight to a different value.</p>
<p>Reducing gradients in DDP basically means communicating gradients across processes.</p>
<blockquote>
<p>Do you mean, DP and DDP exactly update the same weight and same updated each layer right?</p>
</blockquote>
<p>Neither DP nor DDP touches model weight. In the following code, it is the <code>optimzer.step()</code> that updates model weights. What DP and DDP do are preparing the <code>.grad</code> field for all parameters.</p>
<pre><code class="lang-auto">output = model(input)
output.sum().backward()
# DP and DDP not involved in the below this point.
opt.step()
</code></pre>
<blockquote>
<p>It is also confusing to me. Do you mean Batch size or LR size? You link the batch size about it.</p>
</blockquote>
<p>Quoting some discussion from that link. If you search for “lr”, you will find almost all comments in that thread discusses how to configure LR and batch size.</p>
<aside class="quote quote-modified" data-post="3" data-topic="72769">
  <div class="title">
    <div class="quote-controls"></div>
    <img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/hhxx/48/21482_2.png" class="avatar">
    <a href="https://discuss.pytorch.org/t/should-we-split-batch-size-according-to-ngpu-per-node-when-distributeddataparallel/72769/3">Should we split batch_size according to ngpu_per_node when DistributedDataparallel</a> <a class="badge-category__wrapper " href="/c/distributed/12"><span data-category-id="12" data-drop-close="true" class="badge-category "><span class="badge-category__name">distributed</span></span></a>
  </div>
  <blockquote>
    Another question is if we do not divide batch-size by 8, the total images processed in one epoch will be the same as usual or eight times? 
As for learning rate, if we have 8-gpus in total, there wiil be 8 DDP instances. If the batch-size in each DDP distances is 64 (has been divides manually), then one iteration will process 64×4=256 images per node. Taking all gpu into account (2 nodes, 4gpus per node), then one iteration will process 64×8=512 images.  Assuming in one-gpu-one-node scenario, we…
  </blockquote>
</aside>

<aside class="quote quote-modified" data-post="6" data-topic="72769">
  <div class="title">
    <div class="quote-controls"></div>
    <img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/mrshenli/48/12220_2.png" class="avatar">
    <a href="https://discuss.pytorch.org/t/should-we-split-batch-size-according-to-ngpu-per-node-when-distributeddataparallel/72769/6">Should we split batch_size according to ngpu_per_node when DistributedDataparallel</a> <a class="badge-category__wrapper " href="/c/distributed/12"><span data-category-id="12" data-drop-close="true" class="badge-category "><span class="badge-category__name">distributed</span></span></a>
  </div>
  <blockquote>
    I agree with all your analysis on the magnitude of the gradients, and I agree that it depends on the loss function. But even with MSE loss fn, it can lead to different conclusions: 

If the fw-bw has processed 8X data, we should set lr to 8X, meaning that the model should take a larger step if it has processed more data as the gradient is more accurate. (IIUC, this is what you advocate for)
If the gradient is of the same magnitude, we should use 1X lr, especially when approaching convergence. Ot…
  </blockquote>
</aside>

<blockquote>
<p>I face that there is no improvement when I use the DDP with synchronized BN. That is why I am asking third question.</p>
</blockquote>
<p>Right, <code>SyncBatchNorm</code> has its own way for communication, which is out of control of DDP. Using DDP won’t change how <code>SyncBatchNorm</code> behaves.</p>
<aside class="onebox githubblob" data-onebox-src="https://github.com/pytorch/pytorch/blob/f64d24c941a00bc81b3017008ae212cca761d393/torch/nn/modules/_functions.py#L79-L81">
  <header class="source">

      <a href="https://github.com/pytorch/pytorch/blob/f64d24c941a00bc81b3017008ae212cca761d393/torch/nn/modules/_functions.py#L79-L81" target="_blank" rel="noopener nofollow ugc">github.com</a>
  </header>

  <article class="onebox-body">
    <h4><a href="https://github.com/pytorch/pytorch/blob/f64d24c941a00bc81b3017008ae212cca761d393/torch/nn/modules/_functions.py#L79-L81" target="_blank" rel="noopener nofollow ugc">pytorch/pytorch/blob/f64d24c941a00bc81b3017008ae212cca761d393/torch/nn/modules/_functions.py#L79-L81</a></h4>



    <pre class="onebox"><code class="lang-py">
      <ol class="start lines" start="79" style="counter-reset: li-counter 78 ;">
          <li>torch.distributed.all_reduce(</li>
          <li>    combined, torch.distributed.ReduceOp.SUM, process_group, async_op=False)</li>
          <li>sum_dy, sum_dy_xmu = torch.split(combined, num_channels)</li>
      </ol>
    </code></pre>



  </article>

  <div class="onebox-metadata">
    
    
  </div>

  <div style="clear: both"></div>
</aside>


henry_Kang: <p>Thank you for detail explanation.<br>
Also, DDP and LR relationship are interesting. I used to find the LR with trial and error manner…</p>
<p>I got some understand about reduce and DDP. Please check my understanding.</p>
<p>So Basically DP and DDP do not directly change the weight “but it is a different way to calculate the gradient in multi GPU conditions”. If this is incorrect please let me know.<br>
The input data goes through the network, and loss calculate based on output and ground truth.<br>
During this loss calculation, DP or DDP work differently.<br>
However I thought that gradient is basically calculated from loss.<br>
Each loss in the GPU has the different loss result.<br>
DP used mean value because DP send every output result to main GPU and calculate the loss.<br>
If my understanding is incorrect please point out.<br>
However DDP used the different. I still do not get it this parts. In the paper they also use the average value.<br>
What is different between mean calculation and syncronized calculation?<br>
For update the weight in network, the optimizer updates the network using by gradient value.</p>
<p>The update part is the optimizer part no DP or DDP related with it.<br>
So the performance difference might come from LR difference? Because the bath size become different.  weight = previous weight - (gradient*learning_rate)</p>
<p>Really thank you for helping me.</p>

mrshenli: <aside class="quote no-group" data-username="henry_Kang" data-post="5" data-topic="93271">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/henry_kang/48/23282_2.png" class="avatar"> henry_Kang:</div>
<blockquote>
<p>So Basically DP and DDP do not directly change the weight “but it is a different way to calculate the gradient in multi GPU conditions”.</p>
</blockquote>
</aside>
<p>correct.</p>
<blockquote>
<p>The input data goes through the network, and loss calculate based on output and ground truth.<br>
During this loss calculation, DP or DDP work differently.</p>
</blockquote>
<p>correct.</p>
<blockquote>
<p>Each loss in the GPU has the different loss result.<br>
DP used mean value because DP send every output result to main GPU and calculate the loss.</p>
</blockquote>
<p>This is incorrect. DP’s forward pass 1) create a model replica on every GPU, 2) scatters input to every GPU 3) feed one input shard to a different model replica 4) use one thread per model replica to create output on each GPU 5) gather all outputs from different GPUs to one GPU and return. The loss with DP is calculated based on that gathered output, and hence there is only one loss with DP.</p>
<aside class="onebox githubblob" data-onebox-src="https://github.com/pytorch/pytorch/blob/d06f1818ada6405a30943f58548af958c2b83ff6/torch/nn/parallel/data_parallel.py#L147-L162">
  <header class="source">

      <a href="https://github.com/pytorch/pytorch/blob/d06f1818ada6405a30943f58548af958c2b83ff6/torch/nn/parallel/data_parallel.py#L147-L162" target="_blank" rel="noopener nofollow ugc">github.com</a>
  </header>

  <article class="onebox-body">
    <h4><a href="https://github.com/pytorch/pytorch/blob/d06f1818ada6405a30943f58548af958c2b83ff6/torch/nn/parallel/data_parallel.py#L147-L162" target="_blank" rel="noopener nofollow ugc">pytorch/pytorch/blob/d06f1818ada6405a30943f58548af958c2b83ff6/torch/nn/parallel/data_parallel.py#L147-L162</a></h4>



    <pre class="onebox"><code class="lang-py">
      <ol class="start lines" start="147" style="counter-reset: li-counter 146 ;">
          <li>def forward(self, *inputs, **kwargs):</li>
          <li>    if not self.device_ids:</li>
          <li>        return self.module(*inputs, **kwargs)</li>
          <li></li>
          <li>    for t in chain(self.module.parameters(), self.module.buffers()):</li>
          <li>        if t.device != self.src_device_obj:</li>
          <li>            raise RuntimeError("module must have its parameters and buffers "</li>
          <li>                               "on device {} (device_ids[0]) but found one of "</li>
          <li>                               "them on device: {}".format(self.src_device_obj, t.device))</li>
          <li></li>
          <li>    inputs, kwargs = self.scatter(inputs, kwargs, self.device_ids)</li>
          <li>    if len(self.device_ids) == 1:</li>
          <li>        return self.module(*inputs[0], **kwargs[0])</li>
          <li>    replicas = self.replicate(self.module, self.device_ids[:len(inputs)])</li>
          <li>    outputs = self.parallel_apply(replicas, inputs, kwargs)</li>
          <li>    return self.gather(outputs, self.output_device)</li>
      </ol>
    </code></pre>



  </article>

  <div class="onebox-metadata">
    
    
  </div>

  <div style="clear: both"></div>
</aside>

<p>DDP is multi-processing parallel, and hence it can scale across multiple machines. In this case, every process has its own loss and so there are multiple different losses. Gradients are synchronized during the backward pass using autograd hook and allreduce. For more details, I recommend reading the paper I linked above.</p>
<blockquote>
<p>What is different between mean calculation and syncronized calculation?</p>
</blockquote>
<p>Because DP is single-process-multi-thread, the scatter, parallel_apply, gather ops used in the forward pass are automatically recorded by the autograd graph. So during the backward pass, the gradients will be accumulated to the <code>.grad</code> feld. There is no grad synchronization in DP, because autograd engine does all grad accumulation already.</p>
<p>As DP is multi-process and every process has its own autograd engine, we need additional code to synchronize grad.</p>
<blockquote>
<p>So the performance difference might come from LR difference?</p>
</blockquote>
<p>Yep, that’s one possible source. It also relates to what loss function you are using. If the loss function cannot guarantee <code>f([x, y]) == (f(x) + f(y)) /2</code>, then the result can also be different, as it is not compatible with gradient averaging used in DDP.</p>

henry_Kang: <p>Well, what I facing is that, DDP has better result than DP.<br>
However many question in here actually said that DP give better result than DDP.<br>
Second, our task is class imbalance and binary semantic segmentation. The task is real world image with very complex background. In this cases, DP gives us the 82 % mIoU and DDP achieves the 88% in the same loss function and same learning rate.<br>
The Loss function is the IoU Loss.<br>
What grad syncronization and accumulation is another new question. I will read your paper first and ask question again. Thank you. It is really difficult but I hope I can make it.</p>

mrshenli: <aside class="quote no-group" data-username="henry_Kang" data-post="7" data-topic="93271">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/henry_kang/48/23282_2.png" class="avatar"> henry_Kang:</div>
<blockquote>
<p>However many question in here actually said that DP give better result than DDP.</p>
</blockquote>
</aside>
<p>No, this is not guaranteed. The only conclusion we can draw is that DP should be able to produce the same result model as non-parallel training, and DDP cannot guarantee this. But regarding which one is better, it needs to quantitatively measured, as it is affected by a lot of factors, e.g. batch size, lr, loss function, etc.</p>

henry_Kang: <aside class="quote no-group" data-username="mrshenli" data-post="8" data-topic="93271">
<div class="title">
<div class="quote-controls"></div>
<img loading="lazy" alt="" width="24" height="24" src="https://discuss.pytorch.org/user_avatar/discuss.pytorch.org/mrshenli/48/12220_2.png" class="avatar"> mrshenli:</div>
<blockquote>
<p>it needs to quantitatively measured, as it is affected by a lot of factors, e.g. batch size, lr, loss function, etc.</p>
</blockquote>
</aside>
<p>Ok I see. So It can be really dangerous to say that DDP is better or DP is better. I will just keep it and do not put into my the paper. Anyway I will cite your paper since I am using DDP.</p>

sakh251: <p>Hey <a class="mention" href="/u/mrshenli">@mrshenli</a><br>
About the loss function and LR, Which loss functions are effected by LR?<br>
optimizers are also important?</p>

CDhere: <p>Hi Shen, I’m also encountering performance drop with DDP. Could you please elaborate on what <code>f([x, y]) == (f(x) + f(y)) /2</code> means? I don’t quite understand the notation here. Thanks!</p>

111469: <p>Hi! Thanks for the fantastic explanation!</p>
<p>From my understanding, the DP is able to produce the same result model as non-parallel training only when the forward pass is fixed.</p>
<p>That is to say, if my forward function is like:</p>
<pre><code class="lang-auto">def forward(self, x)
    if random.random() &gt; 0.5:
        forward_path 1
        loss = xxx
    else:
        forward_path 2
        loss = yyy
    return loss
</code></pre>
<p>Then DP may not be applicable since it gathers the outputs and the gradients are calculated by only one auto_grad machine. While the DDP strategy wont get any problem since the gradients are calculated separately in each different process.</p>

ramen: <p>I think he means this: f((x + y)/2) == (f(x) + f(y)) /2 i.e linearity of function.<br>
Also, I think he made a mistake because the averaging operation in DDP is applied to the losses(scalars) despite the choice of loss function.</p>
<p>I don’t see any reason why DDP can’t reproduce the non-parallel training, it does the same thing as gradient accumulation.</p>
<p>Let’s say here is your loss vector of size 6(samples): [1, 2, 3, 4, 5, 6].<br>
IF 1 GPU, you just take mean(), and get 3.5;<br>
IF 2 GPU, you get 2 losses: [1+2+3/3 , 4+5+6/3] = [2, 5], take average and get same 3.5;</p>
<p>Right?</p>