Le problème de la formation de grands réseaux de neurones profonds (DNN) à l'aide de grands ensembles de données constitue un défi majeur dans le domaine de l'apprentissage profond. À mesure que la taille des DNN et des ensembles de données augmente, les besoins en calcul et en mémoire pour la formation de ces modèles augmentent également. Cela rend difficile, voire impossible, la formation de ces modèles sur une seule machine avec des ressources informatiques limitées. Certains des défis majeurs liés à la formation de grands DNN à l'aide de grands ensembles de données incluent :
Pour relever ces défis, diverses techniques ont été développées pour étendre la formation de grands DNN avec de grands ensembles de données, notamment le parallélisme des modèles, le parallélisme des données et le parallélisme hybride, ainsi que l'optimisation du matériel, des logiciels et des algorithmes.
Dans cet article, nous démontrerons le parallélisme des données et le parallélisme des modèles à l'aide de PyTorch.
Ce que nous appelons le parallélisme fait généralement référence à la formation de réseaux de neurones profonds (dnn) sur plusieurs GPU ou plusieurs machines, pour réduire le temps de formation. L'idée de base derrière le parallélisme des données est de diviser les données d'entraînement en morceaux plus petits et de laisser chaque GPU ou machine traiter un morceau de données distinct. Les résultats de chaque nœud sont ensuite combinés et utilisés pour mettre à jour les paramètres du modèle. Dans le parallélisme des données, l'architecture du modèle est la même sur chaque nœud, mais les paramètres du modèle sont répartis entre les nœuds. Chaque nœud entraîne son propre modèle local à l'aide de blocs de données alloués, et à la fin de chaque itération de formation, les paramètres du modèle sont synchronisés sur tous les nœuds. Ce processus est répété jusqu'à ce que le modèle converge vers un résultat satisfaisant.
Ci-dessous, nous utilisons les ensembles de données ResNet50 et CIFAR10 pour un exemple de code complet :
Dans le parallélisme des données, l'architecture du modèle reste la même sur chaque nœud, mais les paramètres du modèle sont partitionnés entre les nœuds, et chaque nœud utilise Allouer des morceaux de données pour formez votre propre modèle local.
La bibliothèque DistributedDataParallel de PyTorch peut communiquer et synchroniser efficacement les gradients et les paramètres du modèle entre les nœuds pour réaliser une formation distribuée. Cet article fournit des exemples sur la façon d'implémenter le parallélisme des données avec PyTorch à l'aide des ensembles de données ResNet50 et CIFAR10, où le code est exécuté sur plusieurs GPU ou machines, chaque machine traitant un sous-ensemble des données d'entraînement. Le processus de formation est parallélisé à l'aide de la bibliothèque DistributedDataParallel de PyTorch.
import os from datetime import datetime from time import time import argparse import torchvision import torchvision.transforms as transforms import torch import torch.nn as nn import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel
Ensuite, nous vérifierons le GPU.
import subprocess result = subprocess.run(['nvidia-smi'], stdout=subprocess.PIPE) print(result.stdout.decode())
Parce que nous devons exécuter sur plusieurs serveurs, il n'est pas pratique de les exécuter un par un manuellement, un planificateur est donc nécessaire. Ici, nous utilisons des fichiers SLURM pour exécuter le code (slurmplanificateur de tâches gratuit et open source pour les noyaux Linux et Unix),
def main(): # get distributed configuration from Slurm environment parser = argparse.ArgumentParser() parser.add_argument('-b', '--batch-size', default=128, type =int, help='batch size. it will be divided in mini-batch for each worker') parser.add_argument('-e','--epochs', default=2, type=int, metavar='N', help='number of total epochs to run') parser.add_argument('-c','--checkpoint', default=None, type=str, help='path to checkpoint to load') args = parser.parse_args() rank = int(os.environ['SLURM_PROCID']) local_rank = int(os.environ['SLURM_LOCALID']) size = int(os.environ['SLURM_NTASKS']) master_addr = os.environ["SLURM_SRUN_COMM_HOST"] port = "29500" node_id = os.environ['SLURM_NODEID'] ddp_arg = [rank, local_rank, size, master_addr, port, node_id] train(args, ddp_arg)
Ensuite, nous utilisons la bibliothèque DistributedDataParallel pour effectuer une formation distribuée.
def train(args, ddp_arg): rank, local_rank, size, MASTER_ADDR, port, NODE_ID = ddp_arg # display info if rank == 0: #print(">>> Training on ", len(hostnames), " nodes and ", size, " processes, master node is ", MASTER_ADDR) print(">>> Training on ", size, " GPUs, master node is ", MASTER_ADDR) #print("- Process {} corresponds to GPU {} of node {}".format(rank, local_rank, NODE_ID)) print("- Process {} corresponds to GPU {} of node {}".format(rank, local_rank, NODE_ID)) # configure distribution method: define address and port of the master node and initialise communication backend (NCCL) #dist.init_process_group(backend='nccl', init_method='env://', world_size=size, rank=rank) dist.init_process_group( backend='nccl', init_method='tcp://{}:{}'.format(MASTER_ADDR, port), world_size=size, rank=rank ) # distribute model torch.cuda.set_device(local_rank) gpu = torch.device("cuda") #model = ResNet18(classes=10).to(gpu) model = torchvision.models.resnet50(pretrained=False).to(gpu) ddp_model = DistributedDataParallel(model, device_ids=[local_rank]) if args.checkpoint is not None: map_location = {'cuda:%d' % 0: 'cuda:%d' % local_rank} ddp_model.load_state_dict(torch.load(args.checkpoint, map_location=map_location)) # distribute batch size (mini-batch) batch_size = args.batch_size batch_size_per_gpu = batch_size // size # define loss function (criterion) and optimizer criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(ddp_model.parameters(), 1e-4) transform_train = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) # load data with distributed sampler #train_dataset = torchvision.datasets.CIFAR10(root='./data', # train=True, # transform=transform_train, # download=False) # load data with distributed sampler train_dataset = torchvision.datasets.CIFAR10(root='./data', train=True, transform=transform_train, download=False) train_sampler = torch.utils.data.distributed.DistributedSampler(train_dataset, num_replicas=size, rank=rank) train_loader = torch.utils.data.DataLoader(dataset=train_dataset, batch_size=batch_size_per_gpu, shuffle=False, num_workers=0, pin_memory=True, sampler=train_sampler) # training (timers and display handled by process 0) if rank == 0: start = datetime.now() total_step = len(train_loader) for epoch in range(args.epochs): if rank == 0: start_dataload = time() for i, (images, labels) in enumerate(train_loader): # distribution of images and labels to all GPUs images = images.to(gpu, non_blocking=True) labels = labels.to(gpu, non_blocking=True) if rank == 0: stop_dataload = time() if rank == 0: start_training = time() # forward pass outputs = ddp_model(images) loss = criterion(outputs, labels) # backward and optimize optimizer.zero_grad() loss.backward() optimizer.step() if rank == 0: stop_training = time() if (i + 1) % 10 == 0 and rank == 0: print('Epoch [{}/{}], Step [{}/{}], Loss: {:.4f}, Time data load: {:.3f}ms, Time training: {:.3f}ms'.format(epoch + 1, args.epochs, i + 1, total_step, loss.item(), (stop_dataload - start_dataload)*1000, (stop_training - start_training)*1000)) if rank == 0: start_dataload = time() #Save checkpoint at every end of epoch if rank == 0: torch.save(ddp_model.state_dict(), './checkpoint/{}GPU_{}epoch.checkpoint'.format(size, epoch+1)) if rank == 0: print(">>> Training complete in: " + str(datetime.now() - start)) if __name__ == '__main__': main()
Le code divise les données et le modèle sur plusieurs GPU et met à jour le modèle de manière distribuée. Voici quelques explications du code :
train(args, ddp_arg) a deux paramètres, args et ddp_arg, où args est le paramètre de ligne de commande transmis au script, et ddp_arg contient les paramètres distribués liés à la formation.
rank, local_rank, size, MASTER_ADDR, port, NODE_ID = ddp_arg : décompressez les paramètres liés à la formation distribuée dans ddp_arg.
Si le rang est 0, imprimez le nombre de GPU actuellement utilisés et les informations sur l'adresse IP du nœud maître.
dist.init_process_group(backend='nccl', init_method='tcp://{}:{}'.format(MASTER_ADDR, port), world_size=size,rank=rank) : Initialisez le processus distribué à l'aide du groupe backend NCCL.
torch.cuda.set_device(local_rank) : sélectionnez le GPU spécifié pour ce processus.
model = torchvision.models. ResNet50 (pretrained=False).to(gpu) : chargez le modèle ResNet50 à partir du modèle torchvision et déplacez-le vers le GPU spécifié.
ddp_model = DistributedDataParallel(model, device_ids=[local_rank]) : Enveloppez le modèle dans le module DistributedDataParallel, ce qui signifie que nous pouvons effectuer une formation distribuée
Chargez l'ensemble de données CIFAR-10 et appliquez la transformation d'amélioration des données.
train_sampler=torch.utils.data.distributed.DistributedSampler(train_dataset,num_replicas=size,rank=rank) : créez un objet DistributedSampler pour diviser l'ensemble de données sur plusieurs GPU.
train_loader =torch.utils.data.DataLoader(dataset=train_dataset,batch_size=batch_size_per_gpu,shuffle=False,num_workers=0,pin_memory=True,sampler=train_sampler) : créez un objet DataLoader et les données seront chargées dans le modèle par lots. Ceci est cohérent avec nos étapes de formation habituelles, sauf qu'un échantillonnage de données distribué DistributedSampler est ajouté.
Entraînez le modèle pour le nombre d'époques spécifié et mettez à jour les poids à l'aide d'optimizer.step() de manière distribuée.
rank0 enregistre un point de contrôle à la fin de chaque tour.
rank0 affiche la perte et le temps d'entraînement tous les 10 lots.
A la fin de la formation, le temps total passé à imprimer le modèle de formation est également au rang0.
a été formé en utilisant 1 nœud avec 1/2/3/4 GPU, 2 nœuds avec 6/8 GPU et chaque nœud avec 3/4 GPU. Le test de Resnet50 sur Cifar10 est comme indiqué ci-dessous. , la taille du lot reste la même pour chaque test. Le temps nécessaire pour terminer chaque test a été enregistré en secondes. À mesure que le nombre de GPU utilisés augmente, le temps nécessaire pour terminer le test diminue. Lors de l'utilisation de 8 GPU, cela a pris 320 secondes, ce qui est le temps le plus rapide enregistré. C'est sûr, mais nous pouvons voir que la vitesse d'entraînement n'augmente pas linéairement avec l'augmentation du nombre de GPU. Cela peut être dû au fait que Resnet50 est un modèle relativement petit et ne nécessite pas d'entraînement parallèle.
L'utilisation du parallélisme des données sur plusieurs GPU peut réduire considérablement le temps nécessaire à la formation d'un réseau neuronal profond (DNN) sur un ensemble de données donné. À mesure que le nombre de GPU augmente, le temps nécessaire pour terminer le processus de formation diminue, ce qui indique que les DNN peuvent être formés plus efficacement en parallèle.
Cette approche est particulièrement utile lorsqu'il s'agit de grands ensembles de données ou d'architectures DNN complexes. En exploitant plusieurs GPU, le processus de formation peut être accéléré, permettant une itération et une expérimentation plus rapides du modèle. Cependant, il convient de noter que les améliorations de performances obtenues grâce au parallélisme des données peuvent être limitées par des facteurs tels que la surcharge de communication et les limitations de la mémoire GPU, et nécessitent un réglage minutieux pour obtenir les meilleurs résultats.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!