Principe et mise en œuvre de la formation Pytorch Doka

Cet article a participé à l'activité "Newcomer Creation Ceremony" et a commencé ensemble la route de la création de Nuggets

Pytorche Doka Formation

1. Principes de la formation Doka

Le processus de formation Doka est généralement le suivant :

  1. Spécifiez le nœud hôte
  2. Le nœud hôte divise les données et un lot de données est réparti uniformément sur chaque machine
  3. Les modèles sont copiés de l'hôte vers chaque machine
  4. propagation vers l'avant par machine
  5. Calculer la perte pour chaque machine
  6. L'hôte collecte tous les résultats de perte et met à jour les paramètres
  7. Copiez le modèle de paramètre mis à jour sur chaque machine

image

image

2. Formation multi-cartes sur une seule machine

Utilisez le module torch.nn.DataParallel (module, device_ids), où module est le modèle et device_ids est la liste des identifiants GPU parallèles

Comment ça fonctionne: appelez le modèle pour effectuer des opérations sur cette interface

model = torch.nn.DataParallel(model)

Exemple : nous supposons que l'entrée du modèle est (32, input_dim), où 32 représente batch_size, la sortie du modèle est (32, output_dim) et qu'il est entraîné avec 4 GPU. La fonction de nn.DataParallel est de diviser ces 32 échantillons en 4 parties, de les envoyer à 4 GPU pour l'avant respectivement, puis de générer 4 sorties de taille (8, output_dim), puis de collecter ces 4 sorties sur cuda:0 et fusionner dans (32, output_dim).

On peut voir que nn.DataParallel ne change pas l'entrée et la sortie du modèle, donc les autres parties du code n'ont pas besoin d'être modifiées, ce qui est très pratique. Mais l'inconvénient est que le calcul de perte ultérieur ne sera effectué que sur cuda:0 et ne pourra pas être parallélisé, ce qui conduira au problème de charge déséquilibrée.

Le déséquilibre de charge ci-dessus peut être résolu par le calcul de perte intégré dans le modèle, et la perte finale est moyennée.

class Net:
    def __init__(self,...):
        # code
    
    def forward(self, inputs, labels=None)
        # outputs = fct(inputs)
        # loss_fct = ...
        if labels is not None:
            loss = loss_fct(outputs, labels)  # 在训练模型时直接将labels传入模型,在forward过程中计算loss
            return loss
        else:
            return outputs
复制代码

Selon la logique parallèle du modèle que nous avons mentionnée ci-dessus, une perte sera calculée sur chaque GPU, et ces pertes seront collectées sur cuda:0 et fusionnées dans un tenseur de longueur 4. À ce stade, avant de revenir en arrière, le tenseur de perte doit être fusionné dans un scalaire, généralement en prenant directement la moyenne. Ceci est mentionné dans la documentation officielle de Pytorch nn.DataParallel function :

When module returns a scalar (i.e., 0-dimensional tensor) in forward(), this wrapper will return a vector of length equal to number of devices used in data parallelism, containing the result from each device.
复制代码

3. Formation multi-machines multi-cartes

Cette méthode peut également réaliser plusieurs cartes sur une seule machine

使用torch.nn.parallel.DistributedDataParalleltorch.utils.data.distributed.DistributedSampler结合多进程实现。

  1. 从一开始就会启动多个进程(进程数小于等于GPU数),每个进程独享一个GPU,每个进程都会独立地执行代码。这意味着每个进程都独立地初始化模型、训练,当然,在每次迭代过程中会通过进程间通信共享梯度,整合梯度,然后独立地更新参数。

  2. 每个进程都会初始化一份训练数据集,当然它们会使用数据集中的不同记录做训练,这相当于同样的模型喂进去不同的数据做训练,也就是所谓的数据并行。这是通过torch.utils.data.distributed.DistributedSampler函数实现的,不过逻辑上也不难想到,只要做一下数据partition,不同进程拿到不同的parition就可以了,官方有一个简单的demo,感兴趣的可以看一下代码实现:Distributed Training

  3. 进程通过local_rank变量来标识自己,local_rank为0的为master,其他是slave。这个变量是torch.distributed包帮我们创建的,使用方法如下:

import argparse  # 必须引入 argparse 包
parser = argparse.ArgumentParser()
parser.add_argument("--local_rank", type=int, default=-1)
args = parser.parse_args()
复制代码

必须以如下方式运行代码:

python -m torch.distributed.launch --nproc_per_node=2 --nnodes=1 train.py
复制代码

这样的话,torch.distributed.launch就以命令行参数的方式将args.local_rank变量注入到每个进程中,每个进程得到的变量值都不相同。比如使用 4 个GPU的话,则 4 个进程获得的args.local_rank值分别为0、1、2、3。

上述命令行参数nproc_per_node表示每个节点需要创建多少个进程(使用几个GPU就创建几个);nnodes表示使用几个节点,做单机多核训练设为1。

  1. 因为每个进程都会初始化一份模型,为保证模型初始化过程中生成的随机权重相同,需要设置随机种子。方法如下:
def set_seed(seed):
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
复制代码

使用方式如下:

from torch.utils.data.distributed import DistributedSampler  # 负责分布式dataloader创建,也就是实现上面提到的partition。

# 负责创建 args.local_rank 变量,并接受 torch.distributed.launch 注入的值
parser = argparse.ArgumentParser()
parser.add_argument("--local_rank", type=int, default=-1)
args = parser.parse_args()

# 每个进程根据自己的local_rank设置应该使用的GPU
torch.cuda.set_device(args.local_rank)
device = torch.device('cuda', args.local_rank)

# 初始化分布式环境,主要用来帮助进程间通信
torch.distributed.init_process_group(backend='nccl')

# 固定随机种子
seed = 42
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
torch.cuda.manual_seed_all(seed)

# 初始化模型
model = Net()
model.to(device)
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(model.parameters(), lr=0.1)

# 只 master 进程做 logging,否则输出会很乱
if args.local_rank == 0:
    tb_writer = SummaryWriter(comment='ddp-training')

# 分布式数据集
train_sampler = DistributedSampler(train_dataset)
train_loader = torch.utils.data.DataLoader(train_dataset, sampler=train_sampler, batch_size=batch_size)  # 注意这里的batch_size是每个GPU上的batch_size

# 分布式模型
model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank], output_device=args.local_rank, find_unused_parameters=True)
复制代码

torch.distributed.init_process_group()包含四个常用的参数:

  • backend: 后端, 实际上是多个机器之间交换数据的协议
  • init_method: 机器之间交换数据, 需要指定一个主节点, 而这个参数就是指定主节点的
  • world_size: 介绍都是说是进程, 实际就是机器的个数, 例如两台机器一起训练的话, world_size就设置为2
  • rank: 区分主节点和从节点的, 主节点为0, 剩余的为了1-(N-1), N为要使用的机器的数量, 也就是world_size

后端初始化

pytorch提供下列常用后端:

image

初始化init_method

  1. TCP初始化
import torch.distributed as dist

dist.init_process_group(backend, init_method='tcp://10.1.1.20:23456',
                        rank=rank, world_size=world_size)
复制代码

注意这里使用格式为tcp://ip:端口号, 首先ip地址是你的主节点的ip地址, 也就是rank参数为0的那个主机的ip地址, 然后再选择一个空闲的端口号, 这样就可以初始化init_method了.

  1. 共享文件系统初始化
import torch.distributed as dist

dist.init_process_group(backend, init_method='file:///mnt/nfs/sharedfile',
                        rank=rank, world_size=world_size)
复制代码

初始化rank和world_size

这里其实没有多难, 你需要确保, 不同机器的rank值不同, 但是主机的rank必须为0, 而且使用init_method的ip一定是rank为0的主机, 其次world_size是你的主机数量, 你不能随便设置这个数值, 你的参与训练的主机数量达不到world_size的设置值时, 代码是不会执行的.

四、模型保存

模型的保存与加载,与单GPU的方式有所不同。这里通通将参数以cpu的方式save进存储, 因为如果是保存的GPU上参数,pth文件中会记录参数属于的GPU号,则加载时会加载到相应的GPU上,这样就会导致如果你GPU数目不够时会在加载模型时报错

Ou contrôlez le processus lorsque le modèle est enregistré et enregistrez-le uniquement dans le processus principal. Les modèles sont enregistrés de la même manière, mais plusieurs processus s'exécutent en même temps dans une opération distribuée, de sorte que plusieurs modèles seront enregistrés dans le stockage. Si vous utilisez un stockage partagé, vous devez faire attention au nom du fichier. Bien sûr, les paramètres ne sont généralement enregistrés que sur le processus de rang 0. C'est-à-dire parce que les paramètres de modèle de tous les processus sont synchronisés.

torch.save(model.module.cpu().state_dict(), "model.pth")
复制代码

Chargement des paramètres :

param=torch.load("model.pth")
复制代码

Voici le code de sauvegarde du modèle utilisé dans le code huggingface/transformers

if torch.distributed.get_rank() == 0:
    model_to_save = model.module if hasattr(model, "module") else model  # Take care of distributed/parallel training
    model_to_save.save_pretrained(args.output_dir)
    tokenizer.save_pretrained(args.output_dir)
复制代码

Lien de référence

formation parallèle multi-gpu pytorch

Méthode et principe de formation multi-GPU sur une seule machine PyTorch

Je suppose que tu aimes

Origine juejin.im/post/7082591377581670431
conseillé
Classement