Posted in

Como o treinamento distribuído funciona em pytorch: treinamento distribuído de dados e precisão mista

Como o treinamento distribuído funciona em pytorch: treinamento distribuído de dados e precisão mista

Neste tutorial, aprenderemos a usar nn.parallel.DistributedDataParallel Para treinar nossos modelos em várias GPUs. Vamos dar um exemplo mínimo de treinamento de um classificador de imagem e ver como podemos acelerar o treinamento.

Vamos começar com algumas importações.

import torch

import torchvision

import torchvision.transforms as transforms

import torch.nn as nn

import torch.nn.functional as F

import torch.optim as optim

import time

Usaremos o CIFAR10 em todos os nossos experimentos com um tamanho de lote de 256.

def create_data_loader_cifar10():

transform = transforms.Compose(

(

transforms.RandomCrop(32),

transforms.RandomHorizontalFlip(),

transforms.ToTensor(),

transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))))

batch_size = 256

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,

download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,

shuffle=True, num_workers=10, pin_memory=True)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,

download=True, transform=transform)

testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,

shuffle=False, num_workers=10)

return trainloader, testloader

Primeiro treinaremos o modelo em uma única GPU da NVIDIA A100 para 1 época. Material de pytorch padrão aqui, nada de novo. O tutorial é baseado no Tutorial oficial dos documentos de Pytorch.

def train(net, trainloader):

print("Start training...")

criterion = nn.CrossEntropyLoss()

optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

epochs = 1

num_of_batches = len(trainloader)

for epoch in range(epochs):

running_loss = 0.0

for i, data in enumerate(trainloader, 0):

inputs, labels = data

images, labels = inputs.cuda(), labels.cuda()

optimizer.zero_grad()

outputs = net(images)

loss = criterion(outputs, labels)

loss.backward()

optimizer.step()

running_loss += loss.item()

print(f'(Epoch {epoch + 1}/{epochs}) loss: {running_loss / num_of_batches:.3f}')

print('Finished Training')

O test A função é definida da mesma forma. O script principal apenas montará tudo:

if __name__ == '__main__':

start = time.time()

PATH = './cifar_net.pth'

trainloader, testloader = create_data_loader_cifar10()

net = torchvision.models.resnet50(False).cuda()

start_train = time.time()

train(net, trainloader)

end_train = time.time()

torch.save(net.state_dict(), PATH)

test(net, PATH, testloader)

end = time.time()

seconds = (end - start)

seconds_train = (end_train - start_train)

print(f"Total elapsed time: {seconds:.2f} seconds, \

Train 1 epoch {seconds_train:.2f} seconds")

Usamos um resnet50 para medir o desempenho de uma rede de tamanho decente.

Agora vamos treinar o modelo:

$ python -m train_1gpu

Accuracy of the network on the 10000 test images: 27 %

Total elapsed time: 69.03 seconds, Train 1 epoch 13.08 seconds

Ok, hora de chegar ao trabalho de otimização.

O código está disponível em Girub. Se você planeja solidificar seu conhecimento pytorch, há dois livros incríveis que recomendamos: Aprendizado profundo com pytorch das publicações de Manning e Aprendizado de máquina com Pytorch e Scikit-Learn Por Sebastian Raschka. Você sempre pode usar o código de desconto de 35% Blaisummer21 Para todos os produtos de Manning.

Torch.nn.dataparallelal: sem dor, sem ganho

O DataParallelel é um processo único, multi-thread e só funciona em uma única máquina. Para cada GPU, usamos o mesmo modelo para fazer o passe para a frente. Espalhamos os dados por toda a GPUs e realizamos passes para a frente em cada um deles. Essencialmente, o que acontece é que o tamanho do lote é dividido em todo o número de trabalhadores.

Neste caso de uso, essa funcionalidade não forneceu ganho. Isso ocorre porque o sistema que estou usando possui um gargalo de CPU e disco rígido. Outras máquinas que possuem disco muito rápido e CPU, mas lutam com a velocidade da GPU (gargalo da GPU) podem se beneficiar dessa funcionalidade.

Na prática, a única mudança que você precisa fazer no código é o seguinte:

net = torchvision.models.resnet50(False)

if torch.cuda.device_count() > 1:

print("Let's use", torch.cuda.device_count(), "GPUs!")

net = nn.DataParallel(net)

Ao usar nn.DataParallelo tamanho do lote deve ser divisível pelo número de GPUs.

nn.DataParallel divide o lote e o processa independentemente em todos os GPUs disponíveis. Em cada passagem para a frente, o módulo é replicado em cada GPU, que é uma sobrecarga significativa. Cada réplica lida com uma parte do lote (Batch_size / GPUs). Durante o passe para trás, os gradientes de cada réplica são somados no módulo original.

Mais informações sobre nosso anterior artigo no paralelismo de dados versus modelo.

Uma boa prática ao usar várias GPUs é definir com antecedência as GPUs que seu script vai usar:

import os

os.environ('CUDA_VISIBLE_DEVICES') = "0,1"

Isso deve ser feito antes Qualquer outra importação relacionada a Cuda.

Mesmo do pytorch documentação É óbvio que esta é uma estratégia muito ruim:

É recomendado usar nn.DistributedDataParallelem vez dessa classe, para fazer treinamento com várias GPU, mesmo que haja apenas um nó único.

O motivo é que o distributedDataparall usa um processo por trabalhador (GPU) enquanto o Dataparallelel encapsula toda a comunicação de dados em um único processo.

De acordo com os documentos, os dados podem estar em qualquer dispositivo antes de serem passados ​​para o modelo.

No meu experimento, Dataparallel foi Mais devagar do que treinar em uma única GPU. Mesmo com 4 GPUs. Depois de aumentar o número de trabalhadores, reduzi o tempo, mas ainda pior do que uma única GPU. Eu medro e relato o tempo necessário para treinar o modelo para uma época, ou seja, imagens de 50k 32×32.

Nota final: para comparar o desempenho com uma única GPU, multipliquei o tamanho do lote pelo número de trabalhadores, ou seja, 4 por 4 GPUs. Caso contrário, é mais de 2x mais lento.

Isso nos leva ao tópico hardcore de dados de dados distribuídos.

O código está disponível em Girub. Você sempre pode apoiar nosso trabalho por compartilhamento de mídia social, fazendo um doaçãoe comprando nosso livro e E-Course.

Pytorch distribuiu paralelo de dados

Os dados distribuídos paralelos são multiprocessos e trabalham para treinamento único e de várias máquinas. Em Pytorch, nn.parallel.DistributedDataParallel Paralelize o módulo dividindo a entrada nos dispositivos especificados. Este módulo também é adequado para treinamento em vários nó e multi-GPU. Aqui, experimentei apenas um único nó (1 máquina com 4 GPUs).

A principal diferença aqui é que cada GPU é tratada por um processo. Os parâmetros nunca são transmitidos entre processos, apenas gradientes.

O módulo é replicado em cada máquina e em cada dispositivo. Durante o passe para a frente, cada trabalhador (GPU) processa os dados e calcula seu próprio gradiente localmente. Durante o passe para trás, são calculados gradientes de cada nó. Por fim, cada trabalhador executa uma atualização de parâmetro e envia para todos os outros nós a atualização de parâmetros computados.

O módulo executa um All-Reduce Entende os gradientes e assume que eles serão modificados pelo otimizador em todos os processos da mesma maneira.

Abaixo estão as diretrizes para converter seu script de GPU único em treinamento multi-GPU.

Etapa 1: Inicialize os processos de aprendizado distribuídos

def init_distributed():

dist_url = "env://"

rank = int(os.environ("RANK"))

world_size = int(os.environ('WORLD_SIZE'))

local_rank = int(os.environ('LOCAL_RANK'))

dist.init_process_group(

backend="nccl",

init_method=dist_url,

world_size=world_size,

rank=rank)

torch.cuda.set_device(local_rank)

dist.barrier()

Esta inicialização funciona quando lançamos nosso script com torch.distributed.launch (Pytorch 1.7 e 1.8) ou torch.run (Pytorch 1.9+) de cada nó (aqui 1).

Etapa 2: Enrole o modelo usando DDP

net = torchvision.models.resnet50(False).cuda()

net = nn.SyncBatchNorm.convert_sync_batchnorm(net)

local_rank = int(os.environ('LOCAL_RANK'))

net = nn.parallel.DistributedDataParallel(net, device_ids=(local_rank))

Se cada processo tiver a classificação local correta, tensor.cuda() ou model.cuda() pode ser chamado corretamente em todo o script.

Etapa 3: use um amostrador distribuído no seu Dataloader

import torch

from torch.utils.data.distributed import DistributedSampler

from torch.utils.data import DataLoader

import torch.nn as nn

def create_data_loader_cifar10():

transform = transforms.Compose(

(

transforms.RandomCrop(32),

transforms.RandomHorizontalFlip(),

transforms.ToTensor(),

transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))))

batch_size = 256

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,

download=True, transform=transform)

train_sampler = DistributedSampler(dataset=trainset, shuffle=True)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,

sampler=train_sampler, num_workers=10, pin_memory=True)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,

download=True, transform=transform)

test_sampler =DistributedSampler(dataset=testset, shuffle=True)

testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,

shuffle=False, sampler=test_sampler, num_workers=10)

return trainloader, testloader

No modo distribuído, chamando o data_loader.sampler.set_epoch() método no início de cada época antes criando o DataLoader O iterador é necessário para fazer o rebaixamento funcionar corretamente em várias épocas. Caso contrário, a mesma ordem será sempre usada.

def train(net, trainloader):

print("Start training...")

criterion = nn.CrossEntropyLoss()

optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

epochs = 1

num_of_batches = len(trainloader)

for epoch in range(epochs):

trainloader.sampler.set_epoch(epoch)

De uma forma mais geral:

for epoch in range(epochs):

data_loader.sampler.set_epoch(epoch)

train_one_epoch(...)

Boas práticas para DDP

Quaisquer métodos que baixem os dados devem ser isolados no processo mestre. Quaisquer métodos que executem E/S de arquivo devem ser isolados no processo mestre.

import torch.distributed as dist

import torch

def is_dist_avail_and_initialized():

if not dist.is_available():

return False

if not dist.is_initialized():

return False

return True

def save_on_master(*args, **kwargs):

if is_main_process():

torch.save(*args, **kwargs)

def get_rank():

if not is_dist_avail_and_initialized():

return 0

return dist.get_rank()

def is_main_process():

return get_rank() == 0

Com base nesta função, você pode ter certeza de que alguns comandos são executados apenas no processo principal:

if is_main_process():

Inicie o script usando torch.distributed.launch ou torch.run

$ python -m torch.distributed.launch --nproc_per_node=4 main_script.py

Erros ocorrerão. Certifique -se de matar qualquer processo de treinamento distribuído indesejado por:

$ kill $(ps aux | grep main_script.py | grep -v grep | awk '{print $2}')

Substituir main_script.py com o seu nome de script. Outra opção mais simples é $ kill -9 PID. Caso contrário, você pode ir a coisas mais avançadas, como matar todos os processos relacionados à GPU da CUDA quando não são mostrados em nvidia-smi

lsof /dev/nvidia* | awk '{print $2}' | xargs -I {} kill {}

Isso é apenas para o caso em que você não consegue encontrar o PID do processo em execução na GPU.

Um livro muito bom sobre treinamento distribuído é Aprendizado de máquina distribuído com Python: acelerando o treinamento e servir modelo com sistemas distribuídos por Guanhua Wang.

Treinamento de precisão mista em Pytorch

A precisão mista combina o ponto flutuante (FP) 16 e FP 32 em diferentes etapas do treinamento. O treinamento FP16 também é conhecido como treinamento de meia precisão, que vem com desempenho inferior. A precisão mista automática é literalmente a melhor dos dois mundos: tempo de treinamento reduzido com desempenho comparável ao FP32.

Em Treinamento de precisão mistatodas as operações computacionais (passagem para a frente, passe para trás, gradientes de peso) veja a versão fundida do FP16. Para fazer isso, é necessária uma cópia do FP32 do peso, além de calcular a perda no FP32 após o passe para a frente no FP16 para evitar o fluxo acima e abaixo. Os gradientes de peso são lançados de volta ao FP32 para atualizar os pesos do modelo. Além disso, a perda no FP32 é ampliada para evitar o subfluxo de gradiente antes de ser lançado para o FP16 para executar o passe para trás. Como compensação, os pesos FP32 serão reduzidos pelo mesmo escalar antes da atualização do peso.

https://www.youtube.com/watch?v=i1fibtdhjig

Aqui estão as mudanças na função do trem:

fp16_scaler = torch.cuda.amp.GradScaler(enabled=True)

for epoch in range(epochs):

trainloader.sampler.set_epoch(epoch)

running_loss = 0.0

for i, data in enumerate(trainloader, 0):

inputs, labels = data

images, labels = inputs.cuda(), labels.cuda()

optimizer.zero_grad()

with torch.cuda.amp.autocast():

outputs = net(images)

loss = criterion(outputs, labels)

fp16_scaler.scale(loss).backward()

fp16_scaler.step(optimizer)

fp16_scaler.update()

Resultados e resumir

Em um mundo paralelo utópico, N trabalhadores dariam uma aceleração de N. Aqui você vê que precisa de 4 GPUs no modo distribuídoDataparallel para obter uma aceleração de 2x. O treinamento misto de precisão normalmente fornece uma aceleração substancial, mas a GPU A100 e outras arquiteturas de GPU baseadas em amperes têm ganhos limitados (até onde eu li on-line).

Resultados abaixo relatam o tempo em segundos para 1 Epoch no CIFAR10 com um resnet50 (tamanho do lote 256, NVIDIA A100 40GB GPU Memory):

Tempo em segundos
GPU único (linha de base) 13.2
DataParallelel 4 GPUs 19.1
DistributedDataParallel 2 GPUS 9.8
DistributedDataParallel 4 GPUS 6.1
DistributedDataParallel 4 GPUS + Precisão mista 6.5

Uma nota muito importante aqui é que DistribuídoDataparallelal usa um tamanho eficaz em lote de 4*256 = 1024, então faz Menos atualizações de modelo. É por isso que acredito que ele obtém uma precisão de validação muito menor (14% em comparação com 27% na linha de base).

O código está disponível em Girub Se você quiser brincar. Os resultados variam com base no seu hardware. Sempre há o caso de perdi algo em meus experimentos. Se você encontrar uma falha, por favor me avise em nosso Discord Server.

Essas descobertas proporcionariam a você um sólido começo para treinar seus modelos. Espero que você os ache úteis. Nos apoia por compartilhamento de mídia social, fazendo um doaçãocomprando nosso livro ou E-Course. Sua ajuda nos ajudaria a produzir mais conteúdo gratuito e conteúdo de IA acessível. Como sempre, obrigado pelo seu interesse em nosso blog.

Aprendizagem profunda no livro de produção 📖

Aprenda a construir, treinar, implantar, escalar e manter modelos de aprendizado profundo. Entenda a infraestrutura de ML e os MLOPs usando exemplos práticos.

Saber mais

* Divulgação: Observe que alguns dos links acima podem ser links de afiliados e, sem nenhum custo adicional, ganharemos uma comissão se você decidir fazer uma compra depois de clicar.

Luis es un experto en Ciberseguridad, Computación en la Nube, Criptomonedas e Inteligencia Artificial. Con amplia experiencia en tecnología, su objetivo es compartir conocimientos prácticos para ayudar a los lectores a entender y aprovechar estas áreas digitales clave.

Leave a Reply

Your email address will not be published. Required fields are marked *

betting sites philippines