Run inference on multiple GPUs

clems: <p>Working on Ubuntu 20.04, Python 3.9, PyTorch 1.12.0, and with nvidia gpus .</p>
<p>I trained an encoder and I want to use it to encode each image in my dataset. Because my dataset is huge, I’d like to leverage multiple gpus to do this.</p>
<p>Below is a snippet of the code I use.</p>
<details>
<summary>
Inference code snippet
</summary>
<pre><code class="lang-auto">import os
import sys
import tqdm
import wandb
import torch
import hydra
import shutil
import datetime
import pandas as pd
import torch.nn as nn
import torch.backends.cudnn as cudnn

from pathlib import Path
from omegaconf import DictConfig

from source.dataset import RegionFilepathsDataset
from source.utils import initialize_df, is_main_process


@hydra.main(
    version_base="1.2.0", config_path="config/feature_extraction", config_name="default"
)
def main(cfg: DictConfig):

    distributed = torch.cuda.device_count() &gt; 1
    if distributed:
        torch.distributed.init_process_group(backend="nccl")
        gpu_id = int(os.environ["LOCAL_RANK"])
        if gpu_id == 0:
            print(f"Distributed session successfully initialized")
    else:
        gpu_id = -1

    if is_main_process():
        print(f"torch.cuda.device_count(): {torch.cuda.device_count()}")
        run_id = datetime.datetime.now().strftime("%Y-%m-%d_%H_%M")
        output_dir = Path(cfg.output_dir, cfg.experiment_name, run_id)
        if not cfg.resume and is_main_process():
            if output_dir.exists():
                print(f"{output_dir} already exists! deleting it...")
                shutil.rmtree(output_dir)
                print("done")
                output_dir.mkdir(parents=False)
            else:
                output_dir.mkdir(parents=True, exist_ok=True)

    cudnn.benchmark = True

    # preparing data
    df = pd.read_csv(cfg.tiles_csv)
    dataset = RegionFilepathsDataset(df)

    if distributed:
        sampler = torch.utils.data.DistributedSampler(dataset, shuffle=True)
    else:
        sampler = torch.utils.data.RandomSampler(dataset)

    loader = torch.utils.data.DataLoader(
        subset,
        sampler=sampler,
        batch_size=cfg.batch_size_per_gpu,
        num_workers=cfg.num_workers,
        pin_memory=True,
        shuffle=False,
    )

    model = ...
  
    # move network to gpu
    if distributed:
        model = nn.parallel.DistributedDataParallel(model, device_ids=[gpu_id], output_device=gpu_id)
    else:
        model = model.cuda()

    with tqdm.tqdm(
        loader,
        desc="Image Encoding",
        unit=" img",
        unit_scale=cfg.batch_size_per_gpu,
        ncols=80,
        position=0,
        leave=True,
        disable=not (gpu_id in [-1, 0]),
    ) as t:

        with torch.no_grad():

            for batch in t:

                idx, img = batch
                if gpu_id == -1:
                    img = img.cuda(non_blocking=True)
                else:
                    device = torch.device(f"cuda:{gpu_id}")
                    img = img.to(device, non_blocking=True)
                feature = model(img)
                (...)

if __name__ == "__main__":

    main()
</code></pre>
</details>
<p>I kick off the script via:</p>
<p><code>python3 -m torch.distributed.run --standalone --nproc_per_node=gpu main.py</code></p>
<p>Running the previous command results in the following error:</p>
<p><code>RuntimeError: DistributedDataParallel is not needed when a module doesn't have any parameter that requires a gradient.</code></p>
<p>It seems one cannot use <code>DDP</code> to run inference. Hence my question boils down to: <strong>what’s the easiest way to run inference using multiple GPUs?</strong><br>
Looking for help!</p>
<p>NB: I’ve also tried to create as many subset of my dataset as the number of GPUs available, and spawn one process per GPU using and <code>multiprocessing.Pool</code> and <code>starmap</code>, but ran into another error.</p>

eqy: <p>Why do you need to wrap your model with DDP when you are running in inference? DDP is used to synchronize model parameters such as weights and normalization statistics, which wouldn’t be needed during inference.</p>
<p>You would likely only need something like <code>torch.utils.data.distributed.DistributedSampler</code> to split the dataset across the GPUs, but DDP should not be needed.</p>

clems: <p>indeed, I didn’t quite get what <code>DDP</code> should be used for.<br>
I managed to get things working using <code>torch.utils.data.distributed.DistributedSampler</code> &amp; sending inputs to the right gpus. I’ve added hereunder a snippet of the code working, in case someone is interested.</p>
<details>
<summary>
Distributed inference code snippet
</summary>
<pre><code class="lang-auto">import os
import sys
import tqdm
import wandb
import torch
import hydra
import shutil
import datetime
import pandas as pd
import torch.nn as nn
import torch.backends.cudnn as cudnn

from pathlib import Path
from omegaconf import DictConfig

from source.dataset import RegionFilepathsDataset
from source.utils import initialize_df, is_main_process


@hydra.main(
    version_base="1.2.0", config_path="config/feature_extraction", config_name="default"
)
def main(cfg: DictConfig):

    distributed = torch.cuda.device_count() &gt; 1
    if distributed:
        torch.distributed.init_process_group(backend="nccl")
        gpu_id = int(os.environ["LOCAL_RANK"])
        if gpu_id == 0:
            print(f"Distributed session successfully initialized")
    else:
        gpu_id = -1

    if is_main_process():
        print(f"torch.cuda.device_count(): {torch.cuda.device_count()}")

    cudnn.benchmark = True

    # preparing data
    df = pd.read_csv(cfg.tiles_csv)
    dataset = RegionFilepathsDataset(df)

    if distributed:
        sampler = torch.utils.data.DistributedSampler(dataset, shuffle=True)
    else:
        sampler = torch.utils.data.RandomSampler(dataset)

    loader = torch.utils.data.DataLoader(
        subset,
        sampler=sampler,
        batch_size=cfg.batch_size_per_gpu,
        num_workers=cfg.num_workers,
        pin_memory=True,
        shuffle=False,
    )

    model = ...
  
    # move network to gpu
    if distributed:
        model = model.to(gpu_id)
    else:
        model = model.cuda()

    with tqdm.tqdm(
        loader,
        desc="Image Encoding",
        unit=" img",
        unit_scale=cfg.batch_size_per_gpu,
        ncols=80,
        position=0,
        leave=True,
        disable=not (gpu_id in [-1, 0]),
    ) as t:

        with torch.no_grad():

            for batch in t:

                idx, img = batch
                if gpu_id == -1:
                    device = torch.device(f"cuda")
                else:
                    device = torch.device(f"cuda:{gpu_id}")
                img = img.to(device, non_blocking=True)
                feature = model(img)
                (...)

if __name__ == "__main__":

    main()
</code></pre>
</details>