NVIDIA/DALI

Problem with multigpu parallel External Source operator

Open

#3,980 建立於 2022年6月13日

在 GitHub 查看
 (9 留言) (0 反應) (1 負責人)C++ (670 fork)auto 404
PyTorch Lightningexternal sourcehelp wanted

倉庫指標

Star
 (5,722 star)
PR 合併指標
 (PR 指標待抓取)

描述

So I was following the tutorials for parallel External Source operator and have chosen to setup py_start_method to "spawn". Unfortunately while running it with pytorch lightning I've only recieved errors similar to ones attached here 4gpus_error.log.

I'm not sure whether it can be attributed to DALI or pytorch-lightning. I've tried all of the pickling methods mentioned in tutorials but none resolved this issue. The example below uses DDP but the error occured also in 1gpu setup. Locally on my laptop it runs smoothly, but I cannot reproduce the setup with multigpu. In my normal setup I'm running it on GCP NVIDIA 4xA100 machine.

Self-contained example reproducing the issue for 4-gpus based machine:

import random

from pathlib import Path
from typing import Union, Callable, List, Optional, Dict
import numpy as np
from PIL import Image
import cv2

from nvidia import dali
from nvidia.dali import pipeline_def, Pipeline
from nvidia.dali import fn
from nvidia.dali.plugin.pytorch import DALIGenericIterator
from nvidia.dali import pickling as dali_pickle
from pytorch_lightning import LightningDataModule, LightningModule, Trainer
import torch
from torch import nn


@dali_pickle.pickle_by_value
def SegmentationCallback(
    imgs_list: List[str],
    depths_list: List[str],
    labels_list: List[str],
    batch_size: int,
    seed: int,
    shard_id: int = 0,
    num_shards: int = 1,
    **kwargs,
):
    indices = np.arange(len(imgs_list))
    np.random.shuffle(indices)

    dataset_size = len(imgs_list)
    shard_size = dataset_size // num_shards
    shard_offset = shard_size * shard_id
    n_iterations = shard_size // batch_size
    perm = None
    last_seen_epoch = None

    def callback(sample_info):
        nonlocal perm, last_seen_epoch
        if sample_info.iteration >= n_iterations:
            raise StopIteration
        if last_seen_epoch != sample_info.epoch_idx:
            last_seen_epoch = sample_info.epoch_idx
            perm = np.random.default_rng(seed=seed + sample_info.epoch_idx).permutation(dataset_size)

        index = perm[sample_info.idx_in_epoch + shard_offset]
        img = np.frombuffer(open(imgs_list[index], "rb").read(), dtype=np.uint8)
        dsm = cv2.imread(depths_list[index], cv2.IMREAD_ANYDEPTH)[..., np.newaxis]
        label = np.frombuffer(open(labels_list[index], "rb").read(), dtype=np.uint8)
        return img, dsm, label

    return callback


@pipeline_def
def EnhancedPipeline(
    data_iterator: Callable,
    precision: int = 32,
):

    use_gpu = Pipeline.current().device_id is not None
    decoder_device, device = ("mixed", "gpu") if use_gpu else ("cpu", "cpu")

    img_type = dali.types.FLOAT16 if precision == 16 else dali.types.FLOAT
    imgs, depth, labels = fn.external_source(
        source=data_iterator,
        num_outputs=3,
        batch=False,
        parallel=True,
        dtype=[dali.types.UINT8, dali.types.FLOAT, dali.types.UINT8],
    )
    if use_gpu:
        depth = depth.gpu()

    imgs = fn.decoders.image(imgs, device=decoder_device)
    labels = fn.decoders.image(labels, device=decoder_device, output_type=dali.types.GRAY)

    imgs /= 255

    labels = fn.cast(labels, dtype=dali.types.INT64)
    imgs = fn.cast(imgs, dtype=img_type)
    depth = fn.cast(depth, dtype=img_type)

    imgs = fn.cat(imgs, depth, axis=2)
    imgs, labels = fn.transpose(
        [imgs, labels],
        perm=[2, 0, 1],
    )

    return imgs, labels


class DALIGenericIteratorWrapper(DALIGenericIterator):
    """
    Class wrapping DALIGenericIterator in order to obtain the batches in the pytorches iterable format (input, label).
    """

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

    def __next__(self):
        out = super().__next__()
        out = [out[0][output_name] for output_name in self.output_map]
        return out


class Model(LightningModule):
    def __init__(self, num_classes=int, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.model = nn.Conv2d(4, num_classes, 1)
        self.criterion = nn.CrossEntropyLoss()

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

    def training_step(self, batch, batch_idx):
        return self._step(batch)

    def validation_step(self, batch, batch_idx):
        return self._step(batch)

    def test_step(self, batch, batch_idx, dataloader_idx: Optional[int] = 0):
        return self._step(batch, dataloader_idx=dataloader_idx)

    def _step(self, batch, dataloader_idx=None):
        inputs, masks = batch
        outputs = self.forward(inputs)
        loss = self.criterion(outputs, masks.squeeze(1))
        return loss

    def configure_optimizers(self):
        optimizer = torch.optim.Adam(self.parameters(), lr=1e-3)
        return optimizer


class SegmentationDataModule(LightningDataModule):
    def __init__(
        self,
        imgs_list: str,
        depth_list: str,
        labels_list: str,
        batch_size: int,
        num_workers: int,
        gpus: Optional[Union[List[int], str, int]] = None,
        accelerator: str = None,
        precision: int = 32,
        seed: int = 42,
        **kwargs,
    ):
        super().__init__()
        self.imgs_list = imgs_list
        self.depth_list = depth_list
        self.labels_list = labels_list

        self.pipeline_config = {
            "num_threads": num_workers if num_workers != 0 else 1,
            "device_id": None if not gpus else 0,
            "seed": seed,
            "batch_size": batch_size,
            "precision": precision,
            "py_num_workers": num_workers if num_workers != 0 else 1,
            "py_start_method": "spawn",
        }
        self.accelerator = accelerator

    def setup(self, stage: Optional[str] = None):
        paths = {"imgs": self.imgs_list, "depths": self.depth_list, "labels": self.labels_list}
        self.train_dataset = {**paths}
        self.val_dataset = {**paths}
        self.test_dataset = {**paths}

    def train_dataloader(self):
        return self._get_iterator(
            self.train_dataset["imgs"],
            self.train_dataset["depths"],
            self.train_dataset["labels"],
            self.pipeline_config,
        )

    def val_dataloader(self):
        return self._get_iterator(
            self.val_dataset["imgs"],
            self.val_dataset["depths"],
            self.val_dataset["labels"],
            self.pipeline_config,
        )

    def test_dataloader(self):
        return self._get_iterator(
            self.test_dataset["imgs"],
            self.test_dataset["depths"],
            self.test_dataset["labels"],
            self.pipeline_config,
        )

    def _get_iterator(self, imgs_list: List, depths_list: List, labels_list: List, pipeline_config: Dict):
        assert len(imgs_list) == len(labels_list)
        if (
            self.accelerator
        ):  # has to be assigned for DDP, is available only after pytorch-lightning trainer initialization
            pipeline_config = {
                **pipeline_config,
                "device_id": self.trainer.local_rank,
            }

        data_callable = SegmentationCallback(
            imgs_list,
            depths_list,
            labels_list,
            shard_id=self.trainer.global_rank,
            num_shards=self.trainer.world_size,
            **pipeline_config,
        )
        pipe = EnhancedPipeline(data_callable, **pipeline_config)
        pipe.build()

        return DALIGenericIteratorWrapper(
            pipelines=pipe, output_map=["imgs", "labels"], size=len(imgs_list) // (self.trainer.world_size)
        )


if __name__ == "__main__":
    seed = 42
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)

    img = np.zeros((512, 512, 3), dtype=np.uint8)
    img[256:, 256:, 0] = 255

    depth = np.zeros((512, 512))
    depth[256:, 256:] = 100

    label = np.zeros((512, 512), dtype=np.uint8)
    label[256:, 256:] = 1

    data_root = Path("imgs")
    data_root.mkdir(parents=True, exist_ok=True)
    Image.fromarray(img).save(data_root / "img.png")
    Image.fromarray(depth, mode="F").save(data_root / "depth.tif", compression="tiff_lzw")
    Image.fromarray(label).save(data_root / "label.png")

    imgs_list = [str(data_root / "img.png")] * 100
    depths_list = [str(data_root / "depth.tif")] * 100
    labels_list = [str(data_root / "label.png")] * 100

    batch_size = 4
    num_workers = 6
    gpus = 4
    accelerator = "ddp"
    precision = 16
    model = Model(2)
    datamodule = SegmentationDataModule(
        imgs_list, depths_list, labels_list, batch_size, num_workers, gpus, accelerator, precision
    )

    trainer_config = {"gpus": gpus, "max_epochs": 10, "accelerator": accelerator, "precision": precision}
    trainer = Trainer(**trainer_config)
    trainer.fit(model=model, datamodule=datamodule)

With this one solved, it would be the last of one from my odyssey of DALI issues, thanks a lot for your help!

貢獻者指南