pytoch DistributedDataPalel多カードトレーニング結果が悪くなるソリューション


DDPデータshuffleの設定
DDPを使ってdataloaderにsamplerパラメータ(toch.utils.data.distributed.Distributed.Distributed Sampler(dataset,num_replicas=None、rank=None、shuffle=True、seed=0、drop_last=False)デフォルトはshuffle=Trueですが、pytoch Distributed Samplerの実装によると、

    def __iter__(self) -> Iterator[T_co]:
        if self.shuffle:
            # deterministically shuffle based on epoch and seed
            g = torch.Generator()
            g.manual_seed(self.seed + self.epoch)
            indices = torch.randperm(len(self.dataset), generator=g).tolist()  # type: ignore
        else:
            indices = list(range(len(self.dataset)))  # type: ignore
ランダムindixを生成するシードは、現在のepochと関係がありますので、トレーニング時に手動set epochの値で本物のshuffleを実現する必要があります。

for epoch in range(start_epoch, n_epochs):
    if is_distributed:
        sampler.set_epoch(epoch)
    train(loader)
DDP増大batch size効果が悪くなる問題
large batch size:
理論的な長所:
データ中の雑音の影響は小さくなるかもしれません。一番いいところに近づきやすいかもしれません。
短所と問題:
勾配を下げたvariance;理論的には、凸最適化問題に対して、低勾配varianceはより良い最適化効果を得ることができる。しかし、実際にKeskear et alはbatch sizeを大きくすると差の汎化能力が生じることを検証しました。
非凸最適化問題に対しては、損失関数は複数の局所的な最も優れた点を含み、小さいバッtSizeはノイズの干渉があり、局所的に最も優れたところから飛び出す可能性があり、大きなバッtsizeは局所的に最も優れたところでジャンプできない可能性がある。
解決方法:
レアルニングを増やすrateですが、問題があるかもしれません。訓練開始時には大きなlearning(u)を使います。rateはモデルが収束しない可能性がある(https://arxiv.org/abs/1609.04836)。
warming upを使用する(https://arxiv.org/abs/1706.02677
warmup
トレーニング初期には大きなレアルニングを使います。rateは訓練が収束しないという問題を招くかもしれません。warmupの思想は訓練の初期に小さい学習率を使って、訓練に従って徐々に学習率が大きくなります。rateは、他のdecay(CosinenenealingLR)を使ってトレーニングします。

# copy from https://github.com/ildoonet/pytorch-gradual-warmup-lr/blob/master/warmup_scheduler/scheduler.py
from torch.optim.lr_scheduler import _LRScheduler
from torch.optim.lr_scheduler import ReduceLROnPlateau
class GradualWarmupScheduler(_LRScheduler):
    """ Gradually warm-up(increasing) learning rate in optimizer.
    Proposed in 'Accurate, Large Minibatch SGD: Training ImageNet in 1 Hour'.
    Args:
        optimizer (Optimizer): Wrapped optimizer.
        multiplier: target learning rate = base lr * multiplier if multiplier > 1.0. if multiplier = 1.0, lr starts from 0 and ends up with the base_lr.
        total_epoch: target learning rate is reached at total_epoch, gradually
        after_scheduler: after target_epoch, use this scheduler(eg. ReduceLROnPlateau)
    """
    def __init__(self, optimizer, multiplier, total_epoch, after_scheduler=None):
        self.multiplier = multiplier
        if self.multiplier < 1.:
            raise ValueError('multiplier should be greater thant or equal to 1.')
        self.total_epoch = total_epoch
        self.after_scheduler = after_scheduler
        self.finished = False
        super(GradualWarmupScheduler, self).__init__(optimizer)
    def get_lr(self):
        if self.last_epoch > self.total_epoch:
            if self.after_scheduler:
                if not self.finished:
                    self.after_scheduler.base_lrs = [base_lr * self.multiplier for base_lr in self.base_lrs]
                    self.finished = True
                return self.after_scheduler.get_last_lr()
            return [base_lr * self.multiplier for base_lr in self.base_lrs]
        if self.multiplier == 1.0:
            return [base_lr * (float(self.last_epoch) / self.total_epoch) for base_lr in self.base_lrs]
        else:
            return [base_lr * ((self.multiplier - 1.) * self.last_epoch / self.total_epoch + 1.) for base_lr in self.base_lrs]
    def step_ReduceLROnPlateau(self, metrics, epoch=None):
        if epoch is None:
            epoch = self.last_epoch + 1
        self.last_epoch = epoch if epoch != 0 else 1  # ReduceLROnPlateau is called at the end of epoch, whereas others are called at beginning
        if self.last_epoch <= self.total_epoch:
            warmup_lr = [base_lr * ((self.multiplier - 1.) * self.last_epoch / self.total_epoch + 1.) for base_lr in self.base_lrs]
            for param_group, lr in zip(self.optimizer.param_groups, warmup_lr):
                param_group['lr'] = lr
        else:
            if epoch is None:
                self.after_scheduler.step(metrics, None)
            else:
                self.after_scheduler.step(metrics, epoch - self.total_epoch)
    def step(self, epoch=None, metrics=None):
        if type(self.after_scheduler) != ReduceLROnPlateau:
            if self.finished and self.after_scheduler:
                if epoch is None:
                    self.after_scheduler.step(None)
                else:
                    self.after_scheduler.step(epoch - self.total_epoch)
                self._last_lr = self.after_scheduler.get_last_lr()
            else:
                return super(GradualWarmupScheduler, self).step(epoch)
        else:
            self.step_ReduceLROnPlateau(metrics, epoch)
分散式マルチカードトレーニングDisttributedData Paralelピット
最近、マルチカードのトレーニングを研究したいですが、時間がかかりました。とても楽になると思いましたが、多くのピットを一歩ずつ踏んできました。普通の分散式トレーニングはシングルマシン多カードとマルチカードの二つのタイプに分けられています。
主に二つの方法で実現されます。
1、Data Paralel:Parameeter Serverモード、一枚のカードビットreducer、実現も超簡単で、一行のコード
DataParalelはParameeter serverのアルゴリズムに基づいています。負荷の不均衡が深刻です。モデルが大きい時、reducerのカードは3-4 gの現存占用が多くなります。
2、DisttributedDataParalel:公式の提案は新しいDDPを使って、all-reduceアルゴリズムを採用しています。本来の設計は主に多機多カードのために使うものですが、単独機でも使えます。
どうして分布式の訓練が必要ですか?
複数のカードを使って、全体的に速く走ることができます。
もっと大きいバッtSizeが得られます。
いくつかの分散はより良い効果をもたらす。
主に以下の部分に分けられます。
単機多カード、DataParalel(最もよく使われています。最も簡単です。)
シングルマシン多カード、DisttributedData Paralel(より高級)、マルチマシン多カード、DisttributedData Paralel(最高級)
どうやって訓練を開始しますか
モデルの保存と読み込み
注意事項
一、単機多カード(DATAPAパルALLEL)

from torch.nn import DataParallel
 
device = torch.device("cuda")
#  device = torch.device("cuda:0" if True else "cpu")
 
model = MyModel()
model = model.to(device)
model = DataParallel(model)
#  model = nn.DataParallel(model,device_ids=[0,1,2,3])
比較的簡単で、1行のコードを追加すればいいです。model=DataParalel(model)
二、多機多カード、単機多カード(DISTRIBTEDDATAPARALLEL)
まず注意事項を見終わって、コードを修正してください。不可解なバグが出ないように、トレーニングコードを修正してください。
その中のopt.local_rankはコードの前でこのパラメータを解析します。後ろに行って私の書いた注意事項を見てもいいです。

    from torch.utils.data.distributed import DistributedSampler
    import torch.distributed as dist
    import torch
 
    # Initialize Process Group
    dist_backend = 'nccl'
    print('args.local_rank: ', opt.local_rank)
    torch.cuda.set_device(opt.local_rank)
    dist.init_process_group(backend=dist_backend)
 
    model = yourModel()#     
    if torch.cuda.device_count() > 1:
        print("Let's use", torch.cuda.device_count(), "GPUs!")
        # 5)   
        # model = torch.nn.parallel.DistributedDataParallel(model,
        #                                                   device_ids=[opt.local_rank],
        #                                                   output_device=opt.local_rank)
        model = torch.nn.parallel.DistributedDataParallel(model.cuda(), device_ids=[opt.local_rank])
    device = torch.device(opt.local_rank)
    model.to(device)
    dataset = ListDataset(train_path, augment=True, multiscale=opt.multiscale_training, img_size=opt.img_size, normalized_labels=True)#          
    world_size = torch.cuda.device_count()
    datasampler = DistributedSampler(dataset, num_replicas=dist.get_world_size(), rank=opt.local_rank)
 
    dataloader = torch.utils.data.DataLoader(
        dataset,
        batch_size=opt.batch_size,
        shuffle=False,
        num_workers=opt.n_cpu,
        pin_memory=True,
        collate_fn=dataset.collate_fn,
        sampler=datasampler
    )#         sampler    
 
 
.....
 
     ,   cuda
      imgs = imgs.to(device)
      targets = targets.to(device)
三、トレーニングはどうやって開始しますか?
1、Data Paralel方式
正常に訓練すればいいです。
python 3 trin.py
2、DisttributedData Paralel方式
toch.distributed.launchで起動する必要があります。普通はシングルノードです。

CUDA_VISIBLE_DEVICES=0,1 python3 -m torch.distributed.launch --nproc_per_node=2 train.py
その中のCUDA_VISIBLE_DEVICES設定用のグラフィックカード番号--nproc_prenodeの各ノードのグラフィックカードの数量、普通はいくつのグラフィックカードを使っていくつのグラフィックカードがあります。
マルチノード

python3 -m torch.distributed.launch --nproc_per_node=NUM_GPUS_YOU_HAVE --nnodes=2 --node_rank=0
#    , 0   
訓練が成功すれば、いくつかの情報がプリントされます。いくつかのカードがあれば、いくつかの情報をプリントします。

四、モデル保存と読み込み
以下のa、bは対応しています。aで保存して、a方法でロードします。
1、保存
a、パラメータのみ保存する

torch.save(model.module.state_dict(), path)
b、パラメータとネットワークを保存する

torch.save(model.module,path)
2、ロード
a、マルチカードロードモデルの事前訓練。

model = Yourmodel()
if opt.pretrained_weights:
        if opt.pretrained_weights.endswith(".pth"):
            model.load_state_dict(torch.load(opt.pretrained_weights))
        else:
            model.load_darknet_weights(opt.pretrained_weights)
シングルカードのロードモデルは、モデルをロードする時にマスターカード読み取りモデルを指定します。そしてこの'cuda:0'は、あなたが訓練したモデルが0か1かを見ます。count()is 1.Please use touch.load with map_location to map your storage to an existing device)は、自分の変更に応じて、

model = Yourmodel()
if opt.pretrained_weights:
        if opt.pretrained_weights.endswith(".pth"):
            model.load_state_dict(torch.load(opt.pretrained_weights,map_location="cuda:0"))
        else:
            model.load_darknet_weights(opt.pretrained_weights)
b、シングルカードのロードモデル;
同じくリードモデルのカードを指定します。  

model = torch.load(opt.weights_path, map_location="cuda:0")
マルチカードはプリトレーニングモデルをロードして、bという方式でまだ走っていません。
五、注意事項
1、modelの後ろにmoduleを追加します。
ネットワークモデルを取得すると,並列法を用いて,ネットワークモデルとパラメータをGPUにシフトした。注意してください。ネットワークモジュールを修正したり、モデルのあるパラメータを獲得する必要がある場合は、必ずmodelの後に加えます。moduleは、そうでないとエラーが発生します。例えば、

model.img_size       model.module.img_size
2、cudaまたはto(device)などの問題
deviceは自分で設定します。もしcudaが間違ったら、対応するdeviceになります。model(例えば:model.to(device))input(通常はVarable包装を使用する必要があります。例えば、input=Varable(input).to(device)target(通常はVarableで包装する必要があります。nn.CrossEntropyLoss()(例:criterion=nn.Cons EntropyLoss().to(device)
3、args.local_rankのパラメータ
toch.distributed.launchでトレーニングを開始します。touch.distributed.launchはモデルにargs.local_を割り当てます。rankのパラメータですので、トレーニングコードでこのパラメータを解析するには、touch.distributed.get()を使ってもいいです。プロセスIDを取得します。

parser.add_argument("--local_rank", type=int, default=-1, help="number of cpu threads to use during batch generation")
 
以上は個人の経験ですので、参考にしていただければと思います。