Can torch.multiprocessing and torch.distributed be used within forward()?

username: Juans
cooked: <p>I am trying to implement a parallel evaluation of a function on different sections of my data within the forward() in my model. I am not even sure if this is possible.<br>
I saw that there is a torch.multiprocessing.Pool that I can use to map a function to a list of tensors, but when used within my forward method, it complains because I cannot use pool objects within a class (apparently):<br>
<code>NotImplementedError: pool objects cannot be passed between processes or pickled</code>.</p>
<p>Here more or less what I would like to try:</p>
<pre><code class="lang-auto">def forward(self,x):
    x = nf.unfold(x) #unfold the e.g. image to get patches
    x = evaluate_function_in_parallel(x) # parallelize this evaluation e.g. x = pool.map(function,x)
    x = torch.cat(x)
    return x
</code></pre>
<p>I have only seen examples of <a href="https://pytorch.org/tutorials/intermediate/dist_tuto.html#writing-distributed-applications-with-pytorch" rel="noopener nofollow ugc">distributed training</a> with torch.multiprocessing and torch.distributed but not examples for distributing the work within the forward function. Is it even possible? If so, are there any examples available?</p>
<p>Any comment on this would be really helpful. Thanks.</p>

username: osalpekar
cooked: <p><a class="mention" href="/u/juans">@Juans</a> Are you trying to use multiprocessing to parallelize processing in the forward pass on a single node, or are you trying to do this in a distributed setting?</p>
<p>Is there any more info you can provide about the function you are trying to parallelize in the forward pass? A search indicated that this error is thrown when you attempt to pickle the pool object itself (say the function you are trying to parallelize results in pickling the pool).</p>
<p>I am looking for some examples of this behavior, but the recommended method would be to parallelize the entire training iteration (the entire forward pass and backward pass on a single batch) using DDP instead of just parallelizing one part of the fwd pass.</p>

username: Juans
cooked: <p>Hi <a class="mention" href="/u/osalpekar">@osalpekar</a>, thank you for your response. I am looking to parallelize processing in the forward pass with the workers within a single node.<br>
My bottleneck is not the batch processing (choosing different batch sizes has little effect on the time spent in the forward pass). Rather, most of the time is spent on the part of the forward where I have a for loop, which I want to parallelize.</p>
<blockquote>
<p>Is there any more info you can provide about the function you are trying to parallelize in the forward pass?</p>
</blockquote>
<p>For example, when x is a tensor with an MNIST batch, and f_i are arbitrary functions with distinct learnable parameters. The f_i’s take as input a tensor of size (N,M) → (N,) and in the forward, we have:</p>
<pre><code class="lang-auto">x=torch.unfold(x,kernel_size=2,stride=2) #would give for MNIST size [N,196,4]
x=torch.cat([f_i(x[..., i]) for i in range(x.shape[-1])], dim=1) # is very slow!
</code></pre>
<p>Here, if I process 1 image or many more in a batch makes little difference. Every function f_i processes the whole batch, but only a part of the image. You could, I guess, think of it as the f_i’s being local receptive fields and we can parallelize the processing of their activations. Then, x passes to other modules in the model.<br>
The closest I have seen to my question is <a href="https://discuss.pytorch.org/t/how-to-write-parallel-for-loop-in-models-forward-method/51652">this</a> and <a href="https://discuss.pytorch.org/t/is-there-a-way-to-vectorize-a-class-function-like-numpy-vectorize/7120">this</a>. However, the former seems to be a parallelization on the data, while in the latter, more similar to what I intend, there is no solution.</p>
<blockquote>
<p>A search indicated that this error is thrown when you attempt to pickle the pool object itself (say the function you are trying to parallelize results in pickling the pool).</p>
</blockquote>
<p>I have tried <a href="https://github.com/pytorch/pytorch/issues/35472" rel="noopener nofollow ugc">this</a>, and this seems to avoid pickling the pool object, but the program just stops responding at some point, so I  guess this is not possible.</p>
<p>Sorry for the long answer. Any comment would be very appreciated!</p>

username: osalpekar
cooked: <p><a class="mention" href="/u/juans">@Juans</a> Thanks for clarifying your use case.</p>
<p>It sounds like <code>multiprocessing.Pool.map</code> (or something similar) would allow you to map a function like the <code>f_i</code> in your example on some chunk of the input tensor to a process pool that you can define. The exact chunksize can be configured using args like chunksize or a related operation called imap. Will this serve your purpose?</p>
<p>I tried the following (very simplistic) example using <code>Pool.map</code> in the model’s forward pass and it seems to work:</p>
<pre><code class="lang-auto">import torch
import torch.nn as nn
import torch.optim as optim
import torch.multiprocessing as mp

class DemoMultiProcModel(nn.Module):
    def __init__(self):
        super(DemoMultiProcModel, self).__init__()
        self.net1 = nn.Linear(10, 10)
        self.relu = nn.ReLU()
        self.net2 = nn.Linear(10, 5)
        
    def forward(self, x):    
        with mp.Pool(5) as p:
            self.result = p.map(sum, x)
        return self.net2(self.relu(self.net1(x)))


def train():
    print(f"Running Model with parallelized forward pass.")
    model = DemoMultiProcModel()

    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(model.parameters(), lr=0.001)
    
    for batch_idx in range(5):
        # Generate random inputs/labels
        inputs = torch.randn(20, 10)
        labels = torch.randn(20, 5)
        
        # Train
        optimizer.zero_grad()
        outputs = model(inputs)
        loss_fn(outputs, labels).backward()
        optimizer.step()
        print(f"Batch {batch_idx} done.")
</code></pre>

username: Juans
cooked: <p><a class="mention" href="/u/osalpekar">@osalpekar</a> Thanks a lot for your suggestion.<br>
I had considered <code>torch.multiprocessing.Pool.map</code> since, as you correctly pointed out, seemed to be the solution.<br>
Unfortunately, I see two issues with this (please find the adapted code below):<br>
<strong>First:</strong> somehow pool.map seems to take quite some time (100x more than using a for-loop in this case, roughly 0.3 sec, which is around 3x longer than in my original problem). Do you know of any problems with this method? It seems strange, if you compare it with the built-in map function. Could it be some sort of overhead? seems quite high.<br>
<strong>Second:</strong> Indeed your suggestion works, I made some small changes, but it doesn’t raise any errors, or freezes. However, this solution does not translate to the case where you have registered parameters in the functions f_i. It throws following error:</p>
<pre><code class="lang-auto">multiprocessing.pool.MaybeEncodingError: Error sending result
Reason: 'RuntimeError('Cowardly refusing to serialize non-leaf tensor which 
requires_grad, since autograd does not support crossing process boundaries.  
If you just want to transfer the data, call detach() on the tensor before 
serializing (e.g., putting it on the queue).')'
</code></pre>
<p>So it is an issue with autograd. I have read this error already <a href="https://stackoverflow.com/questions/56174874/how-to-use-multiprocessing-in-pytorch" rel="noopener nofollow ugc">here</a> and since, it is relatively new to have <a href="https://pytorch.org/docs/stable/rpc/distributed_autograd.html" rel="noopener nofollow ugc">distributed autograd</a>, I’d need to dive into the docs. Any pointers are welcome!<br>
I tried it in version 1.6.0 and 1.7.0.</p>
<p><strong>Adapted code</strong></p>
<pre><code class="lang-auto">import torch
import torch.nn as nn
import torch.optim as optim
import torch.multiprocessing as mp
import time

class DemoMultiProcModel(nn.Module):
    def __init__(self):
        super(DemoMultiProcModel, self).__init__()
        self.net1 = nn.Linear(196, 10)
        self.relu = nn.ReLU()
        self.net2 = nn.Linear(10, 5)

        self.kernel = nn.Linear(4, 1) #let's try to make a feature map

    def func_to_apply(self, data):
        return self.kernel(data)
        # return torch.sum(data, 1)[None]

    def forward(self, x):
        start = time.time()
        with mp.Pool(5) as p:
            result = p.map(self.func_to_apply, x)
        # result = []
        # for x_i in x:
        #     result.append(self.func_to_apply(x_i))
        print(f'Time it takes whole op: {time.time()-start} s')
        x = torch.cat(result, dim=0)
        return self.net2(self.relu(self.net1(x)))


def train():
    print(f"Running Model with parallelized forward pass.")
    model = DemoMultiProcModel()

    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(model.parameters(), lr=0.001)

    for batch_idx in range(5):
        # Generate random inputs/labels
        inputs = torch.randn(256, 196, 4)
        labels = torch.randn(256, 5)

        # Train
        optimizer.zero_grad()
        outputs = model(inputs)
        loss_fn(outputs, labels).backward()
        optimizer.step()
        print(f"Batch {batch_idx} done.")


if __name__ == '__main__':
    train()

</code></pre>

username: osalpekar
cooked: <p>I see, I can confirm repro’ing this error case. It seems like autograd cannot handle computing gradients on tensors that have been created by operations that involve cross-process communication, so it simply refuses to serialize them for IPC in the fwd pass itself.</p>
<p><a class="mention" href="/u/pritamdamania87">@pritamdamania87</a> Is our assessment here correct? Is this use case supported with Distributed Autograd/some other alternative? I’m guessing one way of doing this would be to send RPC’s in the fwd pass to other processes to perform the <code>func_to_apply</code> on some chunk of data, and then collect the results (and distributed autograd would do the reverse in the bwd pass), but not sure if this is feasible/the best approach.</p>

username: Juans
cooked: <p><a class="mention" href="/u/osalpekar">@osalpekar</a> I can try the rpc approach and report it here.<br>
I guess this use case is not so common (although I saw some similar ones in the forum already, as I mentioned above), so I hope it works.<br>
Any further comments are of course welcome!</p>

username: osalpekar
cooked: <p>Just spoke about your use case with a few other folks, and the RPC/Distributed Autograd-based mechanism I described above should work.</p>
<p>Alternately, if you can Torchscript your model (see docs <a href="https://pytorch.org/docs/stable/jit.html?highlight=torchscript" rel="noopener nofollow ugc">here</a>), you may be able to use <a href="https://pytorch.org/docs/stable/generated/torch.jit.fork.html" rel="noopener nofollow ugc"><code>torch.jit.fork</code></a> to get this multiprocessing-style parallelism. There are a number of other performance benefits of using Torchscript (such as bypassing the Python Global Interpreter Lock) as well.</p>

username: Juans
cooked: <p>This is great news! Thanks a lot for your help.<br>
I’ll have a look into it. Hopefully I can come up with a solution to post here. Maybe it is also useful to others.</p>