Avoid OOM due to optimizer state in DDP

username: mseeger
cooked: <p>I have a model for which gradient computations work fine, but then I get OOM when runnning <code>optimizer.step()</code> with Adam. I understand the Adam optimizer state needs much more memory due to first and second deltas and <code>float32</code>. But I fail to understand why that is really an issue.</p>
<p>Would it not be very simple to offload this optimizer state to CPU and then do the update computations in blocks of smaller size? The Adam updates are fully separable between parameters, and CPU offloading and doing it in blocks should be highly subdominant to computing gradients.</p>
<p>Am I missing something? Is this implemented somewhere?</p>

username: mseeger
cooked: <p>Even simpler would be to do keep model and optimizer on CPU and do Adam updates there. This would need the model to be copied to each GPU device and the gradients copied back to CPU, but this is the same effort than needed in DDP anyway per round. And while CPU computation is slow compared to GPU, the Adam updates are trivial compared to gradient computations, so this should not make a difference.</p>
<p>Is this implemented somewhere?</p>

username: ptrblck
cooked: <aside class="quote no-group" data-username="mseeger" data-post="2" data-topic="222716">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/m/6bbea6/48.png" class="avatar"> mseeger:</div>
<blockquote>
<p>but this is the same effort than needed in DDP anyway per round</p>
</blockquote>
</aside>
<p>That’s not the case as <code>DDP</code> keeps the model on the used devices without triggering any copies. Moving the model back and forth in each iteration would cause a large performance regression.</p>
<p>You might be interested in <a href="https://www.deepspeed.ai/tutorials/zero-offload/"><code>ZeRO-Offload</code></a>for your use case.</p>

username: mseeger
cooked: <p>Thanks. When I read their pitch, it sounds like more or less what I am saying here. Cool, thanks.</p>
<p>About your comment (just for me to understand):<br>
In DDP we need to send weights to all devices and collect gradients from all devices. Isn’t this just the same as sending the model across (assuming that dealing with the weights is far more expensive than creating all the objects)? However, it may be that transfer CPU ←→ GPU is slower than transfer between GPUs. I am still learning about this space.</p>

username: ptrblck
cooked: <aside class="quote no-group" data-username="mseeger" data-post="4" data-topic="222716">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/m/6bbea6/48.png" class="avatar"> mseeger:</div>
<blockquote>
<p>In DDP we need to send weights to all devices and collect gradients from all devices. Isn’t this just the same as sending the model across</p>
</blockquote>
</aside>
<p><code>DDP</code> will only clone the model once at the initialization from <code>rank0</code> to all other ranks. Afterwards, each rank will use its own clone of the model and only the gradients will be synchronized.</p>

username: mseeger
cooked: <p>OK, but that is once a reduce of gradients to rank 0, and then a broadcast back of gradients to all ranks. I suppose what you say is that this is faster than a reduce of gradients to rank 0, optimizer step there, and broadcast of new weights back. I am just not so much on top of the hardware details.</p>
<p>Thanks, I learned something!</p>

username: Arunprakash-A
cooked: <aside class="quote no-group" data-username="mseeger" data-post="6" data-topic="222716">
<div class="title">
<div class="quote-controls"></div>
<img alt="" width="24" height="24" src="https://discuss.pytorch.org/letter_avatar_proxy/v4/letter/m/6bbea6/48.png" class="avatar"> mseeger:</div>
<blockquote>
<p>OK, but that is once a reduce of gradients to rank 0, and then a broadcast back of gradients to all ranks</p>
</blockquote>
</aside>
<p>No, in DDP, it reduces the gradients at rank 0; however, it doesn’t need to broadcast!</p>
<p>Since we have reduced the gradients, we can also update the parameters and broadcast the updated parameters instead, right? That setup would be a <strong>parameter server architecture</strong>, which is conceptually different from DDP</p>
<p><strong>In DDP:</strong></p>
<p>Suppose we have four GPUs (ranks 0–3) on a single node. Each rank computes gradients on its local mini-batch and <em>broadcasts its gradient to all other ranks</em>. Then, each rank receives gradients from all other ranks. For example, rank-2 receives gradients from ranks 0,1, and 3, then these <strong>gradients are averaged</strong> across all ranks (in rank-2, it sums all the gradients and divides the sum by 4). Therefore, there is no need to broadcast the reduced gradients (it is simply useless, think about it <img src="https://discuss.pytorch.org/images/emoji/apple/slight_smile.png?v=14" title=":slight_smile:" class="emoji" alt=":slight_smile:" loading="lazy" width="20" height="20"> )</p>
<p>Once the averaging is complete, every rank updates its <strong>own copy</strong> of the model parameters. As a result, the model weights remain identical across all ranks. This process repeats for each training batch.</p>
<p>Hope it clarifies!</p>