Concurrent CPU execution when Out-of-memory

saiden89: <p>Hi PyTorch community, I hope this is the right section to post my issue in.</p>
<p>I have a quite big dataset of biological (protein) sequences, around 68M, that I have to process with <a href="https://huggingface.co/Rostlab/prot_t5_xl_uniref50/tree/main" class="inline-onebox" rel="noopener nofollow ugc">Rostlab/prot_t5_xl_uniref50 at main</a>, a pre-trained NLP model that generates embeddings for downstream tasks. I have set up a decent workflow that is able to recover when my single V100 GPU goes OOM when the sequence is too big (even with batch size set to 1), effectively resorting to CPU and RAM for the calculations.<br>
As of now, I used the suggested PyTorch implementation in the for loop:</p>
<pre><code class="lang-auto">oom = False
try:
    embedding = model(input_ids=input_ids, attention_mask=attention_mask)
except RuntimeError:
    oom = True
if oom:
    model.cpu()
    embedding = model(input_ids=input_ids.cpu(), attention_mask=attention_mask.cpu())
</code></pre>
<p>This approach solves the OOM issues, but I realized that has the side effect of having the GPU sitting around doing nothing while the big sequence is being processed, only to resume working with the next iterations.<br>
Is there any possibility to send the problematic sequence to CPU while continuing to process the rest normally?</p>
<p>Things I’ve tried:</p>
<ul>
<li>DataParallel is not the right answer:<code>ValueError: Expected a non cpu device, but got: cpu</code>
</li>
<li>Python multiprocessing expects a pickleable Dataset: <code>AttributeError: Can't pickle local object 'main.&lt;locals&gt;.SequenceDataset'</code>
</li>
</ul>

eqy: <p>If this is an inference task without the need to do things like broadcasting gradients, could you “outsource” the CPU inference to a subprocess <a href="https://docs.python.org/3/library/subprocess.html" rel="noopener nofollow ugc">subprocess — Subprocess management — Python 3.10.2 documentation</a> e.g., via <code>POpen</code> as a workaround?</p>

saiden89: <p>Thank you for pointing out that indeed I don’t need gradients, as the model is put into evaluation mode straight away.<br>
About your solution, is it possible to use it without having to code additional scripts that get called via command line? I’m asking because that would require some coding on my part to generate the called script, and then merga again the CPU-processed sequences into the main dataset.</p>

eqy: <p>You might want to check out the multiprocessing module–either the torch version or the native version available in Python: <a href="https://pytorch.org/docs/stable/multiprocessing.html" rel="noopener nofollow ugc">Multiprocessing package - torch.multiprocessing — PyTorch 1.10.1 documentation</a></p>

H-Huang: <p>+1 to delegating cpu inference to a subprocess as mentioned by eqy. Additionally you can look into using the RPC framework for this as well (<a href="https://pytorch.org/docs/master/rpc.html?highlight=rpc#module-torch.distributed.rpc" class="inline-onebox" rel="noopener nofollow ugc">Distributed RPC Framework — PyTorch master documentation</a>) which allows you to send tensors between processes easily.</p>
<p>For your use case, here is a basic example of spawning multiple processes programmatically (without command line) and provides a basic example of using RPC to delegate tensors to CPU or GPU model based on the contents of the tensor.</p>
<pre><code class="lang-auto">import os
import random
import torch
import torch.multiprocessing as mp
import torch.distributed.rpc as rpc
import torch.nn as nn
import time

os.environ["MASTER_ADDR"] = "localhost"
os.environ["MASTER_PORT"] = "5678"

class Model(nn.Module):
    def __init__(self):
        super(Model, self).__init__()
        self.layer = nn.Linear(1, 1)

    def forward(self, x):
        return self.layer(x)

model = Model()
shutdown = 0

def set_shutdown():
    global shutdown
    shutdown = 1

def process_data(input, use_cuda):
    if use_cuda:
        input = input.to("cuda:0")
        model.cuda()
    else:
        model.cpu()
    print(next(model.parameters()).device)

    # return tensor output as cpu
    return model(input).cpu()

def worker_0():
    global shutdown
    rpc.init_rpc("cpu_worker", rank=0, world_size=3)

    # spins there until shutdown is set
    while not shutdown:
        continue
    rpc.shutdown()

def worker_1():
    global shutdown
    rpc.init_rpc("cuda_worker", rank=1, world_size=3)

    # spins there until shutdown is set
    while not shutdown:
        continue
    rpc.shutdown()

def worker_2():
    rpc.init_rpc("controller", rank=2, world_size=3)
    data = torch.rand(10, 1)
    futures = []
    for rand_tensor in data:
        # if random number is &gt; 0.5, then use cpu to process, else use gpu
        if rand_tensor &gt; 0.5:
            fut = rpc.rpc_async("cpu_worker", process_data, args=(rand_tensor, False))
        else:
            fut = rpc.rpc_async("cuda_worker", process_data, args=(rand_tensor, True))
        futures.append(fut)
    for fut in futures:
        result = fut.wait()
        print(result)
    rpc.rpc_sync("cpu_worker", set_shutdown)
    rpc.rpc_sync("cuda_worker", set_shutdown)
    rpc.shutdown()

if __name__ == "__main__":
    p0 = mp.Process(target=worker_0)
    p1 = mp.Process(target=worker_1)
    p2 = mp.Process(target=worker_2)
    p0.start()
    p1.start()
    p2.start()
    p0.join()
    p1.join()
    p2.join()
</code></pre>