Teoria + prática, leva você a entender o treinamento distribuído

Este artigo foi compartilhado pela Huawei Cloud Community " Treinamento Distribuído de Grandes Modelos LLM ", autor: Hua Shanghua_Lancer.

Com o rápido crescimento na quantidade de parâmetros do modelo de linguagem e dos dados de treinamento necessários, os recursos limitados em uma única máquina não podem mais atender aos requisitos para o treinamento de modelos de linguagem em grande escala. Um sistema de treinamento distribuído (Treinamento Distribuído) precisa ser projetado para resolver o problema de enormes requisitos de computação e recursos de memória.

Em um ambiente de sistema de treinamento distribuído, é necessário dividir uma tarefa de treinamento de modelo em múltiplas subtarefas e distribuir as subtarefas para vários dispositivos de computação para resolver gargalos de recursos. Mas como podemos usar um cluster que inclui dezenas de milhares de chips de aceleração computacional para treinar um modelo de linguagem em larga escala com centenas de bilhões ou até trilhões de parâmetros de modelo? Isso envolve uma série de tecnologias como arquitetura de cluster, estratégia paralela, arquitetura de modelo, otimização de memória e otimização de computação.

Apresentarei em detalhes os conceitos básicos de sistemas distribuídos de aprendizado de máquina, arquitetura de cluster de treinamento distribuído, estratégias paralelas de treinamento distribuído e usarei o DeepSpeed ​​​​como exemplo para apresentar como treinar grandes modelos de linguagem em um cluster.

1. Visão geral do treinamento distribuído

Treinamento distribuído refere-se à decomposição de tarefas de treinamento de modelo de aprendizado de máquina ou aprendizado profundo em várias subtarefas e treiná-las em paralelo em vários dispositivos de computação. A Figura 1 dá um exemplo de um único dispositivo de computação e vários dispositivos de computação. O dispositivo de computação aqui pode ser uma unidade de processamento central (CPU), uma unidade de processamento gráfico (GPU) ou uma unidade de processamento de tensor (Unidade de Processamento de Tensor (). TPU) também pode ser um processador de rede neural (Unidade de Processamento de Rede Neural, NPU).

Como a memória não pode ser compartilhada entre vários dispositivos de computação dentro do mesmo servidor, independentemente de esses dispositivos de computação estarem em um servidor ou em vários servidores, a arquitetura do sistema se enquadra na categoria de sistemas distribuídos. Uma tarefa de treinamento de modelo geralmente tem um grande número de amostras de treinamento como entrada, que podem ser concluídas usando um dispositivo de computação, ou toda a tarefa de treinamento de modelo pode ser dividida em subtarefas e distribuída para diferentes dispositivos de computação para obter computação paralela.

Depois disso, a saída de cada dispositivo computacional precisa ser combinada para finalmente obter o resultado do cálculo equivalente ao de um único dispositivo computacional. Como cada dispositivo de computação só precisa ser responsável por subtarefas e vários dispositivos de computação podem ser executados em paralelo, ele pode concluir o cálculo geral mais rapidamente e, em última análise, acelerar todo o processo de computação.

Figura 1 Exemplos de computação com dispositivo único e vários dispositivos de computação

Uma das razões mais importantes que levam as pessoas a projetar sistemas de treinamento distribuídos é que o poder computacional de um único dispositivo de computação não é mais suficiente para suportar o treinamento de modelos. A Figura 2 mostra os requisitos de poder de computação do modelo de aprendizado de máquina e o poder de computação que um único dispositivo de computação pode fornecer durante o mesmo período. Conforme mostrado na figura, os modelos de aprendizado de máquina estão se desenvolvendo rapidamente. Desde AlexNet em 2013 até o modelo Palm com 540 bilhões de parâmetros em 2022, os modelos de aprendizado de máquina estão se desenvolvendo a uma taxa de 56 vezes a cada 18 meses. À medida que a escala dos parâmetros do modelo aumenta, os requisitos para a quantidade de dados de treinamento também aumentam exponencialmente, o que intensifica a demanda por poder computacional.

No entanto, nos últimos anos, o aumento no poder de computação da CPU tem sido muito menor do que a Lei de Moore. Embora os dispositivos de aceleração de computação (como GPU, TPU, etc.) forneçam uma grande quantidade de poder de computação para modelos de aprendizado de máquina, sua taxa de crescimento ainda. não excedeu a Lei de Moore de duplicar a cada 18 meses. Para poder atender ao desenvolvimento de modelos de aprendizado de máquina, apenas um sistema de treinamento distribuído pode atender aos crescentes requisitos de poder computacional do modelo.

Figura 2 Comparação entre o crescimento dos parâmetros do modelo de aprendizado de máquina e o crescimento do poder computacional do hardware de computação

O objetivo geral do treinamento distribuído é aumentar a velocidade geral do treinamento e reduzir o tempo geral de treinamento do modelo. A velocidade total de treinamento pode ser estimada brevemente usando a seguinte fórmula:

Velocidade total de treinamento ∝ Velocidade de computação de dispositivo único × Número total de dispositivos de computação × Taxa de aceleração de vários dispositivos

Entre eles, a velocidade de computação de um único dispositivo é determinada principalmente pela velocidade de computação e pelas capacidades de E/S de dados de um único chip de aceleração de computação. Para otimizar a eficiência de treinamento de um único dispositivo, os principais meios técnicos incluem treinamento de precisão mista, operador. fusão, acumulação de gradiente, etc.; Quanto maior o número de dispositivos de computação em um sistema de treinamento distribuído, maior será seu pico teórico de velocidade de computação. No entanto, afetado pela eficiência da comunicação, um aumento no número de dispositivos de computação causará um rápido aumento. diminuição na taxa de aceleração; a taxa de aceleração de vários dispositivos é determinada pelo cálculo e a eficiência da comunicação é determinada pela combinação de algoritmos e topologia de rede para otimização. O objetivo principal da estratégia paralela de treinamento distribuído é melhorar a taxa de aceleração de vários dispositivos em. o sistema de treinamento distribuído.

A quantidade de grandes parâmetros do modelo de linguagem e a quantidade de dados utilizados são muito grandes, portanto, uma arquitetura de treinamento distribuída é usada para completar o treinamento. O documento [5] apresenta apenas o processo de treinamento do GPT-3 usando GPUs NVIDIA V100. O documento [31] apresenta que o OPT usa 992 GPUs NVIDIA A100 80G e adota Fully Shared Data Parallel [129] e Megatron-LM Tensor Parallelism [130]. , o tempo total de treinamento é de quase 2 meses.

Os pesquisadores do modelo BLOOM[33] divulgaram mais detalhes sobre o hardware e a arquitetura do sistema utilizado. O treinamento do modelo durou um total de 3,5 meses e utilizou 48 nós de computação. Cada nó contém 8 GPUs NVIDIA A100 80G (384 GPUs no total) e usa 4*NVLink para comunicação entre GPUs dentro do nó. Os nós se comunicam entre si usando uma rede de topologia global hipercubo de 8 dimensões aprimorada, construída com quatro placas de rede Omni-Path de 100 Gbps.

A literatura [37] não fornece a configuração específica e a topologia de rede do cluster usado no treinamento do modelo LLaMA, mas fornece o total de horas de GPU para diferentes escalas de parâmetros. O treinamento do modelo LLaMA usa GPU A100-80GB, o treinamento do modelo LLaMA-7B requer 82.432 horas de GPU, o treinamento do modelo LLaMA-13B requer 135.168 horas de GPU, o treinamento do modelo LLaMA-33B leva 530.432 horas de GPU e o treinamento do modelo LLaMA-65B custa até 1022.362 GPU Hora. Como a quantidade de dados de treinamento usados ​​pelo LLaMA excede em muito a dos modelos OPT e BLOOM, embora o número de parâmetros do modelo seja muito menor do que os dois modelos acima, a quantidade de cálculo necessária ainda é muito surpreendente.

Ao usar um sistema de treinamento distribuído, o ciclo de treinamento de grandes modelos de linguagem pode ser reduzido de décadas em um único dispositivo de computação para dezenas de dias usando milhares de dispositivos de computação. No entanto, os sistemas de treinamento distribuído ainda precisam superar vários desafios, como paredes de computação, paredes de memória de vídeo e paredes de comunicação, para garantir que todos os recursos do cluster sejam totalmente utilizados, acelerando assim o processo de treinamento e encurtando o ciclo de treinamento.

• Parede computacional: Há uma enorme discrepância entre o poder de computação que um único dispositivo de computação pode fornecer e a quantidade total de computação necessária para um modelo de linguagem grande. O NVIDIA H100 SXM lançado em março de 2022 tem um poder de computação FP16 de placa única de apenas 2.000 TFLOPs, enquanto o GPT-3
requer um poder de computação total de 314 ZFLOPs.

• Parede de memória de vídeo: um único dispositivo de computação não pode armazenar completamente os parâmetros de um modelo de linguagem grande. GPT-3 contém 175 bilhões de parâmetros. Se armazenado no formato FP16, requer 700 GB de espaço de memória do dispositivo de computação, e a GPU NVIDIA H100 possui apenas 80 GB de memória de vídeo.

• Parede de comunicação: São necessárias transmissão e sincronização freqüentes de parâmetros entre dispositivos de computação em um sistema de treinamento distribuído. Devido à latência de comunicação e às limitações de largura de banda, isso pode se tornar um gargalo no processo de treinamento. Durante o processo de treinamento do GPT-3, se houver 128 cópias do modelo no sistema distribuído, pelo menos 89,6 TB de dados de gradiente deverão ser transmitidos durante cada iteração. Em agosto de 2023, um único link InfiniBand não pode fornecer mais de 800 Gb/s de largura de banda. A parede de computação e a parede de memória de vídeo surgem do conflito entre as capacidades limitadas de computação e armazenamento de um único dispositivo de computação e os enormes requisitos de computação e armazenamento do modelo. Este problema pode ser resolvido usando um método de treinamento distribuído, mas o treinamento distribuído enfrentará o desafio das paredes de comunicação. No treinamento de múltiplas máquinas e cartões, esses problemas surgiram gradativamente. À medida que os parâmetros dos modelos grandes aumentam, o tamanho do cluster correspondente também aumenta e estes problemas tornam-se mais proeminentes. Ao mesmo tempo, quando grandes clusters são treinados por um longo período, falhas no equipamento podem afetar ou interromper o processo de treinamento, o que também impõe altas demandas ao sistema distribuído.

2. Estratégia paralela de treinamento distribuído

O objetivo do sistema de treinamento distribuído é converter o treinamento de modelo de nó único em treinamento de modelo paralelo distribuído equivalente. Para modelos de linguagem grandes, o processo de treinamento é o processo de atualização dos parâmetros do modelo de rede neural usando algoritmos de otimização baseados em dados e funções de perda. A estrutura do sistema de treinamento do modelo de nó único é mostrada na Figura 3, que consiste principalmente em duas partes: dados e modelo. O processo de treinamento será concluído por vários minilotes de dados (Minilote).

Os dados na figura representam um pequeno lote de dados. O sistema de treinamento usa pequenos lotes de dados para gerar gradientes baseados em funções de perda e algoritmos de otimização para corrigir parâmetros do modelo. O processo de execução de uma rede neural multicamadas para um modelo de linguagem grande pode ser representado por um gráfico computacional (Gráfico Computacional). Este gráfico possui vários operadores interconectados (Operadores), cada operador implementa uma camada de rede neural (Neural Network Layer), e os parâmetros representam os pesos atualizados por esta camada durante o treinamento.

Figura 3 Sistema de treinamento de modelo de dispositivo único

O processo de execução do gráfico de cálculo pode ser dividido em duas etapas: cálculo direto e cálculo reverso. O processo de cálculo direto consiste em ler os dados no primeiro operador, calcular a estrutura de saída correspondente e, em seguida, repetir o processo de cálculo direto até o final do último operador. O processo de cálculo reverso é baseado na função de otimização e perda. Cada operador calcula o gradiente por vez e usa o gradiente para atualizar os parâmetros locais. Depois que o cálculo reverso for concluído e o cálculo do minilote de dados for concluído, o sistema lerá o próximo minilote de dados e continuará a próxima rodada de atualizações de parâmetros do modelo.

De acordo com o processo do sistema de treinamento de modelo de dispositivo único, podemos perceber que se a aceleração paralela for realizada, ela pode ser considerada a partir de duas dimensões: dados e modelo. Primeiro, os dados podem ser particionados (Partição), o mesmo modelo pode ser copiado para vários dispositivos e diferentes fragmentos de dados podem ser executados em paralelo. Este método é geralmente chamado de Paralelismo de Dados (DP). O modelo também pode ser dividido e os operadores no modelo podem ser distribuídos para vários dispositivos para conclusão, respectivamente. Este método é geralmente chamado de Paralelismo de Modelo (MP). Ao treinar modelos de linguagem de grande escala, muitas vezes é necessário dividir os dados e o modelo ao mesmo tempo para atingir um maior grau de paralelismo. Esse método costuma ser chamado de paralelismo híbrido (HP).

2.1. Paralelismo de dados

Em um sistema paralelo de dados, cada dispositivo de computação possui uma cópia completa de todo o modelo de rede neural (modelo réplica). Ao iterar, cada dispositivo de computação recebe apenas um subconjunto de um lote de amostras de dados e com base no lote de amostras. subconjunto de dados é usado para cálculo direto do modelo de rede. Suponha que o número de amostras de treinamento em um lote seja N, e M dispositivos de computação sejam usados ​​para cálculo paralelo, e cada dispositivo de computação receberá N/M amostras. Após a conclusão do cálculo direto, cada dispositivo de computação calculará o erro de perda com base na amostra local para obter o gradiente Gi (i é o número da placa aceleradora) e transmitirá o gradiente local Gi. Todos os dispositivos de computação precisam agregar os valores de gradiente fornecidos por outros cartões de aceleração e, em seguida, usar o gradiente médio (ΣNi=1Gi)/N para atualizar o modelo e completar o treinamento em lote. A Figura 4 mostra um exemplo de sistema de treinamento paralelo de dados que consiste em dois dispositivos de computação.

Figura 4 Exemplo de sistema de treinamento paralelo de dados de dois nós

O sistema de treinamento paralelo de dados pode efetivamente melhorar o rendimento geral do treinamento e o tamanho global do lote por segundo (tamanho global do lote por segundo) adicionando equipamentos de computação. Em comparação com o treinamento em um único dispositivo de computação, a principal diferença é que os gradientes no cálculo reverso precisam ser sincronizados em todos os dispositivos de computação para garantir que o resultado final em cada dispositivo de computação seja a média dos gradientes em todos os processos.

Estruturas de redes neurais comuns têm implementações específicas de paralelismo de dados, incluindo: TensorFlow DistributedStrategy, PyTorch Distributed, Horovod DistributedOptimizer, etc. Como cada operador em um modelo de linguagem grande baseado na arquitetura Transformer depende de dados únicos em vez de dados em lote, o paralelismo de dados não afetará sua lógica de cálculo. Em geral, o cálculo direto em cada dispositivo de treinamento é independente e não afeta a lógica de cálculo. . Envolve problemas de sincronização. O treinamento paralelo de dados tem a maior taxa de aceleração, mas requer backup de uma cópia do modelo em cada dispositivo e consome uma quantidade relativamente alta de memória de vídeo.

O código para usar PyTorch DistributedDataParallel para implementar o treinamento de múltiplas placas aceleradoras em um único servidor é o seguinte. Primeiro, construa a classe DistributedSampler para interromper aleatoriamente as amostras do conjunto de dados e distribuí-las para diferentes dispositivos de computação:

classe DistributedSampler(Amostrador):
  def __init__(self, conjunto de dados, num_replicas=Nenhum, classificação=Nenhum, shuffle=True, seed=0):
    se num_replicas for Nenhum:
        se não for dist.is_available():
            raise RuntimeError("Requer que o pacote distribuído esteja disponível")
        num_réplicas = dist.get_world_size()
    se a classificação for Nenhuma:
        se não for dist.is_available():
            raise RuntimeError("Requer que o pacote distribuído esteja disponível")
        classificação = dist.get_rank()
    self.dataset = conjunto de dados #dataset
    self.num_replicas = num_replicas #O número de processos é padronizado como world_size (número de GPUs)
    self.rank = rank # A qual processo/GPU pertence atualmente
    self.época = 0
    self.num_samples = int(math.ceil(len(self.dataset) * 1.0 / self.num_replicas))
    #Número de amostras por processo
    self.total_size = self.num_samples * self.num_replicas #O número total de amostras no conjunto de dados
    self.shuffle = shuffle # Se deve embaralhar o conjunto de dados
    self.seed = semente

def __iter__(self):
# 1. Processamento aleatório: interrompe a ordem do conjunto de dados
    se self.shuffle:
        # Ofuscar com base na época e na semente
        g = tocha.Generator()
        # Aqui self.seed é um valor fixo. Alterar self.epoch por set_epoch pode alterar nossa semente de inicialização.
        # Isso permite que a ordem embaralhada dos conjuntos de dados em cada época seja diferente, de modo que em cada época,
        # Cada GPU obtém dados diferentes, o que pode facilitar um melhor treinamento.
        g.manual_seed(self.seed + self.época)
        índices = torch.randperm(len(self.dataset), gerador=g).tolist()
    outro:
        índices = lista(intervalo(len(self.dataset)))
    # Suplemento de dados
    índices += índices[:(self.total_size - len(índices))]
    afirmar len(índices) == self.total_size
    # alocar dados
    índices = índices[self.rank:self.total_size:self.num_replicas]
    afirmar len (índices) == self.num_samples
    retornar iter (índices)
def __len__(auto):
    retornar self.num_samples
def set_época(self, época):

    self.época = época

Use DistributedSampler para construir um exemplo completo de programa de treinamento main.py da seguinte maneira:

importar argparse
importe-nos
importar Shutil
hora de importação
avisos de importação
importar numpy como np
warnings.filterwarnings('ignorar')
importar tocha
importar torch.nn como nn
importar tocha.nn.parallel
importar torch.backends.cudnn como cudnn
importar tocha.distribuído como dist
importar tocha.optim
importar tocha.utils.data
importar torch.utils.data.distributed
de torch.utils.data.distributed importação DistributedSampler
de modelos importar DeepLab
da importação do conjunto de dados Cityscapes
analisador = argparse.ArgumentParser(descrição='DeepLab')
parser.add_argument('-j', '--workers', padrão=4, type=int, metavar='N',
help='número de trabalhadores de carregamento de dados (padrão: 4)')
parser.add_argument('--épocas', padrão=100, tipo=int, metavar='N',
help='número total de épocas a serem executadas')
parser.add_argument('--start-época', padrão=0, tipo=int, metavar='N',
help='número de época manual (útil em reinicializações)')
parser.add_argument('-b', '--batch-size', padrão = 3, tipo = int,
metavar='N')
parser.add_argument('--local_rank', default=0, type=int, help='node rank para treinamento distribuído')
args =parser.parse_args()
torch.distributed.init_process_group(backend="nccl") #Inicialização
print("Use GPU: {} para treinamento".format(args.local_rank))
#criar modelo
modelo = DeepLab()
torch.cuda.set_device(args.local_rank) #Placa gráfica atual
model = model.cuda() # O modelo é colocado na placa gráfica
modelo = torch.nn.parallel.DistributedDataParallel(modelo, device_ids=[args.local_rank],
    output_device=args.local_rank, find_unused_parameters=True) # Paralelismo de dados
critério = nn.CrossEntropyLoss().cuda()
otimizador = torch.optim.SGD(model.parameters(), args.lr,
    impulso=args.momentum, peso_decay=args.weight_decay)
train_dataset = Cityscapes()
train_sampler = DistributedSampler(train_dataset) # Distribui dados
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=args.batch_size,
    shuffle=Falso, num_workers=args.workers, pin_memory=True, sampler=train_sampler)

Inicie o programa acima através da seguinte linha de comando:

CUDA_VISIBLE_DEVICES=0,1 python -m torch.distributed.launch --nproc_per_node=2 main.py

2.2 Paralelismo de modelos

O paralelismo de modelo é frequentemente usado para resolver o problema de memória insuficiente em um único nó. Tomemos como exemplo o modelo GPT-3 contendo 175 bilhões de parâmetros. Se cada parâmetro do modelo for representado por um número de ponto flutuante de 32 bits, o modelo precisará ocupar 700 GB (ou seja, 175 G × 4 Bytes) de memória. representado por um número de ponto flutuante de 16 bits, cada cópia do modelo requer 350 GB de memória. A placa aceleradora H100 lançada pela NVIDIA em março de 2022 suporta apenas 80 GB de memória de vídeo e o modelo inteiro não pode ser totalmente colocado nela. O paralelismo do modelo pode ser dividido nas duas formas a seguir do ponto de vista dos gráficos computacionais:

(1) Dividir em diferentes dispositivos de acordo com as camadas do modelo, ou seja, paralelismo intercamadas ou paralelismo interoperador (Paralelismo Interoperador), também chamado de paralelismo de pipeline (Paralelismo de pipeline, PP);

(2) Dividir os parâmetros da camada de cálculo em diferentes dispositivos, ou seja, paralelismo intracamada ou paralelismo intra-operador (Paralelismo Intra-operador), também chamado de paralelismo tensorial (Paralelismo Tensor, TP).

Uma amostra de um sistema de treinamento paralelo de modelo de dois nós é mostrada na Figura 4.9. O lado esquerdo é o paralelismo de pipeline, e diferentes camadas do modelo são divididas em dispositivos diferentes, o lado direito é o paralelismo de tensor e diferentes parâmetros na mesma camada; são divididos em diferentes dispositivos no dispositivo.

Paralelismo de pipeline

Paralelismo de Pipeline (PP) é uma estratégia de computação paralela que processa cada camada do modelo em segmentos e distribui cada segmento em diferentes dispositivos de computação, para que as etapas anteriores e subsequentes possam funcionar em pipeline e em lotes. O paralelismo de pipeline é geralmente aplicado em sistemas paralelos de modelos de grande escala para resolver efetivamente o problema de memória insuficiente em um único dispositivo de computação. A Figura 4.6 mostra um sistema paralelo de pipeline composto por quatro dispositivos de computação, incluindo cálculo direto e cálculo regressivo. Entre eles, F1, F2, F3 e F4 representam respectivamente quatro caminhos de avanço, que estão localizados em dispositivos diferentes, enquanto B4, B3, B2 e B1 representam os caminhos de retorno inversos, que também estão localizados em quatro dispositivos diferentes; Porém, como pode ser visto na figura, o dispositivo downstream (dispositivo downstream) no gráfico de cálculo precisa permanecer ocioso por um longo tempo, aguardando que o dispositivo upstream (dispositivo upstream) conclua seus cálculos antes de poder começar a calcular seus próprios tarefas.

Figura 5 Exemplo de sistema de treinamento paralelo modelo de dois nós

Essa situação resultou em uma redução significativa na utilização média do dispositivo, formando uma Bolha de Paralelismo Modelo, também conhecida como Bolha de Pipeline.

Figura 6 Exemplo de pipeline paralelo

As bolhas paralelas geradas pela estratégia ingênua de pipeline impedem que o sistema utilize totalmente os recursos computacionais e reduzem a eficiência computacional geral do sistema. Para reduzir bolhas paralelas, a literatura [131] propôs o método GPipe, que divide ainda o minilote em microlotes menores e usa o esquema paralelo de pipeline para processar um microlote de cada vez.

Após a conclusão do cálculo do estágio atual e a obtenção dos resultados, os resultados do microlote são enviados para o dispositivo downstream, e os dados do próximo microlote passam a ser processados ​​​​ao mesmo tempo, o que pode reduzir bolhas paralelas até certo ponto. Figura 7Exemplo paralelo de pipeline de política GPipe. Conforme mostrado na figura, o cálculo direto de F1 é dividido em F11, F12, F13 e F14. Após o cálculo de F11 ser concluído no dispositivo de computação 1, o cálculo de F21 será iniciado no dispositivo de computação 2 e no. ao mesmo tempo, F12 será iniciado em paralelo no cálculo do dispositivo de computação 1. Comparado com o método paralelo do pipeline original, o método do pipeline GPipe pode efetivamente reduzir as bolhas paralelas.

Figura 7 Exemplo paralelo de pipeline de política GPipe

Embora a estratégia GPipe possa reduzir certas bolhas paralelas, o cálculo regressivo só pode começar depois que todos os cálculos progressivos em um minilote forem concluídos. Portanto, muitas bolhas paralelas ainda serão geradas, reduzindo assim a eficiência paralela do sistema. Megatron-LM[132] propôs uma estratégia de pipeline 1F1B, que consiste em um canal direto e um canal reverso. A estratégia de pipeline 1F1B introduz um mecanismo de agendamento de tarefas, permitindo que dispositivos downstream executem outras tarefas paralelas enquanto aguardam cálculos upstream, melhorando assim a utilização do dispositivo. 1F1B fornece dois métodos de agendamento, não intercalado e intercalado, conforme mostrado na Figura 8.

O modo de agendamento não intercalado 1F1B pode ser dividido em três estágios. A primeira é uma fase de aquecimento na qual números variados de cálculos diretos são realizados no dispositivo de computação. A próxima fase é a fase de avanço e retrocesso, onde o dispositivo de computação executa sequencialmente um cálculo direto e, em seguida, um cálculo regressivo. A última fase é a fase retroativa, onde o dispositivo de computação completa o último cálculo retroativo. Comparado com a estratégia GPipe, o modo de agendamento não intercalado tem melhor desempenho na economia de memória. No entanto, requer o mesmo tempo que a estratégia GPipe para concluir uma rodada de cálculos.

O modo de programação intercalado 1F1B requer que o número de microlotes seja um múltiplo integral dos estágios do pipeline. Cada dispositivo não é mais o único responsável pelo cálculo de múltiplas camadas consecutivas, mas pode processar subconjuntos de múltiplas camadas, chamados de nuggets de modelo. Especificamente, no modelo anterior, o dispositivo 1 pode ser responsável pelas camadas 1 a 4, o dispositivo 2 pelas camadas 5 a 8 e assim por diante. No entanto, no novo modo, o dispositivo 1 pode lidar com as camadas 1, 2, 9, 10, o dispositivo 2 pode lidar com as camadas 3, 4, 11, 12 e assim por diante. Nesse modo, cada dispositivo é atribuído a vários estágios do pipeline. Por exemplo, o Dispositivo 1 pode estar envolvido em algum subconjunto de tarefas na fase de aquecimento, na fase de computação direta e na fase de computação regressiva. Cada dispositivo pode executar tarefas de computação em diferentes estágios em paralelo, aproveitando melhor o paralelismo do pipeline. Este modo não só tem um bom desempenho em termos de consumo de memória, mas também melhora a eficiência computacional, permitindo que sistemas paralelos para modelos grandes concluam tarefas de computação com mais eficiência.


Figura 8 Exemplo de estratégia paralela de pipeline 1F1B  

PyTorch também inclui a função API Pipe para implementar o pipeline. Para implementação específica, consulte a classe "torch.distributed.pipeline.sync.Pipe". Você pode usar esta API para construir uma amostra contendo duas camadas lineares, colocadas em dois dispositivos de computação diferentes, como segue:

{#
Etapa 0. Primeiro é necessário inicializar a estrutura RPC.
os.environ['MASTER_ADDR'] = 'localhost'
os.environ['MASTER_PORT'] = '29500'
torch.distributed.rpc.init_rpc('trabalhador', classificação=0, tamanho_mundo=1)
# Etapa 1: construir um modelo incluindo duas camadas lineares
fc1 = nn.Linear(16, 8).cuda(0)
fc2 = nn.Linear(8, 4).cuda(1)
# Etapa 2: envolva as duas camadas com nn.Sequential
modelo = nn.Sequencial(fc1, fc2)
# Etapa 3: construir Pipe (torch.distributed.pipeline.sync.Pipe)
modelo = Tubo (modelo, pedaços = 8)
#faça treinamento/inferência
entrada = tocha.rand(16, 16).cuda(0)
saída_rref = modelo (entrada)
}

paralelismo tensorial

O Paralelismo Tensor (TP) precisa resolver dois problemas: como dividir os parâmetros em diferentes dispositivos de acordo com a estrutura específica e o tipo de operador do modelo, e como garantir a consistência matemática após a divisão. Grandes modelos de linguagem são baseados na estrutura do Transformer. A estrutura do Transformer é composta principalmente pelos três operadores a seguir: cálculo de representação incorporada (Incorporação), multiplicação de matrizes (MatMul) e perda de entropia cruzada (Perda de entropia cruzada).

Esses três tipos de operadores são bastante diferentes, e estratégias paralelas de tensores correspondentes [130] precisam ser projetadas para dividir os parâmetros em diferentes dispositivos. Para o operador de Embedding, se o número total de vocabulários for muito grande, a memória de vídeo de um único dispositivo computacional não será capaz de acomodar os parâmetros da camada de Embedding. Por exemplo, se o número de vocabulários for 64.000, a dimensão de representação de incorporação for 5.120 e o tipo usar números de ponto flutuante de precisão de 32 bits, então a memória de vídeo necessária para toda a camada de parâmetros será de aproximadamente 64.000 × 5120 × 4/1024 /1024 = 1250 MB, e o gradiente reverso é o mesmo. Requer 1250 MB, quase 2,5 GB apenas para armazenamento.

Os parâmetros incorporados na camada de apresentação podem ser divididos de acordo com a dimensão da palavra. Cada dispositivo de computação armazena apenas parte do vetor de palavras e, em seguida, o vetor de palavras completo é obtido resumindo os vetores de palavras parciais em cada dispositivo. A Figura 4.9 mostra um diagrama esquemático de incorporação de nó único e paralelismo de tensor de dois nós.

Em um único nó, execute a operação de incorporação, bz é o tamanho do lote (tamanho do lote), o tamanho do parâmetro de incorporação é [word_size, hidden_size] e o tensor [bz, hidden_size] é calculado. O exemplo do tensor paralelo de incorporação na Figura 4.9 divide os parâmetros de incorporação em dois blocos ao longo da dimensão word_size. Cada bloco tem um tamanho de [word_size/2, hidden_size] e é armazenado em dois dispositivos, respectivamente. Quando cada nó consulta sua própria lista de palavras, se não puder ser encontrada, a representação da palavra é 0. Após consultar o respectivo dispositivo, o tensor de resultado [bz, hidden_size] é obtido. Finalmente, através da comunicação AllReduce_Sum ¬, somando-se os dispositivos. , obtemos A partir dos resultados completos, pode-se ver que os resultados de saída aqui são consistentes com os resultados executados por um único dispositivo de computação.

Figura 9 Exemplo de tensor paralelo do operador de incorporação de dois nós

O paralelismo tensorial da multiplicação de matrizes (MatMul) deve fazer pleno uso do princípio da multiplicação de blocos de matrizes. Por exemplo, para implementar a seguinte multiplicação de matrizes Y = X ×A, onde X é a matriz de entrada com dimensão M × N, A é a matriz de parâmetros com dimensão N ×K e Y é a matriz de resultado com dimensão M ×K. Se a matriz de parâmetros A for muito grande, ou mesmo exceder a capacidade de memória de vídeo de uma única placa, então a matriz de parâmetros A pode ser dividida em vários cartões, e os resultados podem ser coletados por meio de comunicação coletiva para garantir que o resultado final seja matematicamente equivalente a um único dispositivo de computação. Resultados de cálculo. Existem duas maneiras de segmentar a matriz de parâmetros A:

(1) A matriz de parâmetros A é cortada em colunas e a matriz A é cortada em colunas: A = [A1,A2]

(2) A matriz de parâmetros A é cortada em linhas e a matriz A é cortada em linhas:

A Figura 10 mostra um exemplo de divisão da matriz de parâmetros por colunas. A matriz de parâmetros A coloca A1 e A2 em dois dispositivos de computação, respectivamente. Dois dispositivos de computação calculam Y1 = X ×A1 e Y2 = X ×A2 respectivamente. Após a conclusão do cálculo, vários dispositivos de computação se comunicam entre si para obter os resultados do cálculo em outros dispositivos de computação e os unem para obter a matriz de resultado final Y. Este resultado é matematicamente equivalente ao resultado do cálculo de um único dispositivo de computação.

Figura 10 Exemplo de divisão paralela de tensores do operador de multiplicação de matrizes de dois nós por colunas

A Figura 11 mostra um exemplo de divisão da matriz de parâmetros por colunas e linhas Para satisfazer as regras de multiplicação de matrizes, a matriz de entrada X precisa ser dividida pelas colunas X = [X1|X2]. Ao mesmo tempo, a matriz é dividida em blocos e colocada em dois dispositivos de computação. Cada dispositivo de computação calcula Y1 =X1 ×A1 e Y2 = X2 ×A2 respectivamente. Após a conclusão do cálculo, vários dispositivos de computação se comunicam para obter e reduzir os resultados do cálculo em outros cartões, e a matriz Y do resultado final pode ser obtida. Da mesma forma, este método de divisão pode não apenas garantir a equivalência de cálculo matemático, mas também resolver o problema de que um único dispositivo de computação não pode acomodar a memória de vídeo e também pode garantir que um único dispositivo de computação possa acomodar o parâmetro A por meio da divisão.

A estrutura FFN no Transformer contém duas camadas totalmente conectadas (FC), ou seja, existem duas multiplicações de matrizes, e essas duas multiplicações de matrizes adotam os dois métodos de segmentação acima, conforme mostrado na Figura 4.12. A matriz de parâmetros da primeira camada FC é cortada em blocos por colunas, e a matriz de parâmetros da segunda camada FC é cortada em blocos por linhas. Desta forma, a saída da primeira camada FC atende exatamente aos requisitos de entrada de dados da segunda camada FC (dividida por colunas), de modo que a operação de comunicação resumida após a primeira camada FC pode ser omitida. O paralelismo tensorial do mecanismo de autoatenção com múltiplas cabeças é semelhante ao FFN. Por ter múltiplas cabeças independentes, é mais fácil obter paralelismo do que o FFN. Seu método de segmentação de matriz é mostrado na Figura 4.13. Para obter detalhes, consulte [130].

A última camada da rede de classificação geralmente usa os operadores Softmax e Cross_entropy para calcular a perda de entropia cruzada (Perda de entropia cruzada). Se o número de categorias for muito grande, a memória de um único dispositivo de computação não será capaz de armazenar e calcular a matriz logit. Para este tipo de operador, ela pode ser dividida de acordo com a dimensão da categoria e, ao mesmo tempo, a perda final de entropia cruzada global pode ser obtida por meio da comunicação intermediária dos resultados.

Figura 11 Exemplo de divisão paralela do tensor do operador de multiplicação de matriz de dois nós por linhas

Figura 12 Diagrama paralelo do tensor da estrutura FNN

A primeira coisa a calcular é o valor softmax, a fórmula é a seguinte:

Entre eles, p representa o número do dispositivo de paralelismo tensorial. Após obter o resultado do cálculo Softmax, o rótulo Target é dividido por categoria ao mesmo tempo, e cada dispositivo fica com parte da perda. Por fim, outra comunicação é realizada para obter a perda de todas as categorias. Todo o processo requer apenas três pequenas quantidades de comunicação para completar o cálculo da perda de entropia cruzada. PyTorch fornece uma API paralela de nível de tensor refinada, DistributedTensor. Ele também fornece uma API de nível de modelo de granulação grossa para realizar paralelismo de tensor em "nn.Module". Você pode fragmentar um tensor grande com as seguintes linhas de código:

importar tocha
de torch.distributed._tensor importar DTensor, DeviceMesh, Shard, distribuir_tensor
# constrói uma malha de dispositivos com dispositivos disponíveis (multi-host ou host único)
device_mesh = DeviceMesh("cuda", [0, 1, 2, 3])
# se quisermos fazer a fragmentação por linha
rowwise_placement=[Fragmento(0)]
# se quisermos fazer fragmentação conjunta
colwise_placement=[Fragmento(1)]
grande_tensor = tocha.randn(888, 12)
# tensor distribuído retornado será fragmentado na dimensão especificada nas veiculações
rowwise_tensor = distribuir_tensor(big_tensor, device_mesh=device_mesh, colocações=rowwise_placement)

Para módulos como "nn.Linear" que já possuem "torch.Tensor" como parâmetro, a API de nível de módulo "distribute_module" também é fornecida para realizar o paralelismo de tensor no nível do modelo.

importar tocha
de torch.distributed._tensor importar DeviceMesh, Shard, distribuir_tensor,distribute_module
classe MeuMódulo(nn.Module):
    def __init__(self):
        super().__init__()
        self.fc1 = nn.Linear(8, 8)
        self.fc2 = nn.Linear(8, 8)
        self.relu = nn.ReLU()
        def forward(self, entrada):
            retornar self.relu(self.fc1(entrada) + self.fc2(entrada))
    malha = DeviceMesh(device_type="cuda", malha=[[0, 1], [2, 3]])
    def shard_params(mod_name, mod, malha):
        rowwise_placement = [Fragmento(0)]
        def to_dist_tensor (t): retornar distribuir_tensor (t, malha, rowwise_placement)
        mod._apply(to_dist_tensor)
    módulo_sharded = distribuir_module(MeuMódulo(), malha, partição_fn=shard_params)
    def shard_fc(mod_name, mod, malha):
        rowwise_placement = [Fragmento(0)]
        se mod_name == "fc1":
            mod.weight = torch.nn.Parameter(distribute_tensor(mod.weight, mesh, rowwise_placement))
    módulo_sharded = distribuir_module(MeuMódulo(), malha, partição_fn=shard_fc)

2.3 Paralelismo híbrido

Paralelismo Híbrido (HP) é uma mistura de múltiplas estratégias paralelas, como paralelismo de dados, paralelismo de pipeline e paralelismo de tensor. Ao combinar diferentes estratégias paralelas, o paralelismo híbrido pode aproveitar ao máximo várias estratégias paralelas para maximizar o desempenho e a eficiência da computação.

Para modelos de linguagem grandes com escala de centenas de bilhões, uma estratégia paralela de tensor é geralmente usada em cada servidor. Como essa estratégia envolve uma grande quantidade de comunicação de rede, é necessário utilizar largura de banda de comunicação de alta velocidade entre diferentes dispositivos de computação dentro do servidor. servidor. Através do paralelismo de pipeline, diferentes camadas do modelo são divididas em vários estágios, e cada estágio é calculado por uma máquina diferente. Desta forma, o poder de computação de múltiplas máquinas pode ser totalmente utilizado e os resultados de cálculos e dados intermediários podem ser transferidos através de comunicação de alta velocidade entre máquinas para melhorar a velocidade e eficiência geral da computação.

Finalmente, a estratégia paralela de dados é sobreposta à camada externa para aumentar o número de simultaneidades e melhorar a velocidade geral de treinamento. Através do paralelismo de dados, os dados de treinamento são distribuídos para vários grupos de servidores para processamento paralelo, e cada grupo de servidores processa diferentes lotes de dados. Isso pode aproveitar ao máximo os recursos de computação de vários servidores e aumentar a simultaneidade do treinamento, acelerando assim a velocidade geral do treinamento.

BLOOM usa a estrutura Megatron-DeepSpeed[104] para treinamento, que consiste principalmente em duas partes: Megatron-LM fornece recursos paralelos de tensor e primitivos de carregamento de dados fornece o otimizador ZeRO, pipeline de modelo e componentes de treinamento distribuídos convencionais; Desta forma, o paralelismo tridimensional de dados, tensores e pipelines pode ser alcançado. A estrutura de computação paralela usada no treinamento do modelo BLOOM é mostrada na Figura 14.

O treinamento do modelo BLOOM usa um cluster de 48 servidores NVIDIA DGX-A100. Cada servidor DGX-A100 contém 8 GPUs NVIDIA A100 de 80 GB, totalizando 384. A estratégia adotada no treinamento BLOOM é primeiro dividir o cluster em grupos de 48 para paralelização dos dados.

A seguir, todo o modelo é dividido em 12 estágios para paralelização do pipeline. O modelo de cada estágio é dividido em 4 GPUs para paralelismo de tensores. Ao mesmo tempo, o BLOOM também usa ZeRO (Zero Redundancy Optimizer) [134] para reduzir ainda mais a ocupação da memória de vídeo do modelo. Através das quatro etapas acima, a computação paralela eficiente de centenas de GPUs pode ser alcançada.

Figura 14 Estrutura de computação paralela usada no treinamento do modelo BLOOM

2.4 Otimização da memória do dispositivo de computação

O treinamento atual de modelos de linguagem grande geralmente usa o algoritmo de otimização Adam, que requer impulso de primeira ordem (Momentum) e impulso de segunda ordem (Variância), além do gradiente de cada parâmetro. Embora o algoritmo de otimização Adam seja geralmente melhor e mais estável que o algoritmo SGD, ele consome significativamente mais memória no dispositivo de computação.

Para reduzir o uso de memória, a maioria dos sistemas adotou o método Mixed Precision Training, ou seja, existem valores nos formatos FP16 (ponto flutuante de 16 bits) ou BF16 (Bfloat16) e FP32 (ponto flutuante de 32 bits). . FP32, FP16 e BF16 são representados conforme mostrado na Figura 4.15. No FP32, o bit 31 é o bit de sinal, os bits 30 a 23 são usados ​​para representar o expoente e os bits 22 a 0 são usados ​​para representar a mantissa. No FP16, o bit 15 é o bit de sinal, os bits 14 a 10 são usados ​​para representar o expoente e os bits 9 a 9 são usados ​​para representar a mantissa. No BF16, o bit 15 é o bit de sinal, os bits 14 a 7 são usados ​​para representar o expoente e os bits 6 a 0 são usados ​​para representar a mantissa. Como a faixa de valores de FP16 é muito menor que a de FP32, overflow e underflow podem ocorrer facilmente durante o processo de cálculo. Comparado com o FP16, o BF16 negocia precisão por uma faixa de valor maior. Porém, devido à menor precisão do FP16 e BF16 em comparação ao FP32, podem ocorrer problemas de desaparecimento do gradiente e instabilidade do modelo durante o processo de treinamento.

Portanto, algumas tecnologias precisam ser utilizadas para solucionar esses problemas, como escalonamento de perdas dinâmicas (Dynamic Loss Scaling) e otimizador de precisão mista (Mixed Precision Optimizer). O processo de otimização de precisão mista é mostrado na Figura 4.16. O estado do otimizador Adam inclui backup dos parâmetros do modelo salvos no FP32, e o momento de primeira ordem e o momento de segunda ordem também são armazenados no formato FP32. Supondo que o número de parâmetros do modelo seja Φ, e os parâmetros e gradientes do modelo sejam armazenados no formato FP16, um total de 2Φ + 2Φ + (4Φ + 4Φ + 4Φ) = 16Φ bytes de armazenamento são necessários.

Entre eles, o status de Adam representa 75%. Antes da retropropagação da escala de perda dinâmica, a alteração da perda (dLoss) é aumentada manualmente em 2K vezes, de modo que o gradiente da função de ativação obtido durante a retropropagação não irá transbordar após a retropropagação, o gradiente de peso é reduzido em 2K vezes e restaurado aos valores normais. Por exemplo, para um modelo contendo 7,5 bilhões de parâmetros, se usar o formato FP16, serão necessários apenas 15 GB de memória do dispositivo de computação, mas o estado do modelo realmente consome 120 GB durante a fase de treinamento.

Além do estado do modelo, a memória ocupada pela placa de computação também possui estados residuais (estados residuais), incluindo valores de ativação (ativação), vários buffers temporários (buffers) e fragmentos de memória de vídeo inutilizáveis ​​(fragmentação), etc. Como o valor de ativação pode usar checkpoint (ponto de verificação de ativação) para reduzir significativamente o consumo de memória do valor de ativação, como reduzir o estado do modelo, especialmente o estado do otimizador Adam, é a chave para resolver o problema de consumo de memória.

Figura 16 Processo de otimização de precisão mista

O texto acima é minha breve introdução aos conceitos básicos de sistemas distribuídos de aprendizado de máquina, arquitetura de cluster de treinamento distribuído e estratégias paralelas de treinamento distribuído. Continuarei apresentando um exemplo de como treinar um grande modelo de linguagem. para você no próximo artigo. Bem-vindo, preste atenção e apoie, seu apoio é a força motriz da minha criação.

Conteúdo de referência:

(1) Coleção丨Compartilhamento de 30 grandes conjuntos de dados relacionados ao treinamento de modelos de linguagem - Zhihu.

(2) Quatro métodos comuns de processamento para dados de treinamento de grandes modelos de linguagem - Zhihu.

(3) "Modelo de linguagem em grande escala: da teoria à prática" Zhang Qi et al.

(4) Revisão de grandes modelos de linguagem - Universidade Renmin da China http://ai.ruc.edu.cn/research/science/20230605100.html.

Clique para seguir e conhecer as novas tecnologias da Huawei Cloud o mais rápido possível~

 

Um programador nascido na década de 1990 desenvolveu um software de portabilidade de vídeo e faturou mais de 7 milhões em menos de um ano. O final foi muito punitivo! Alunos do ensino médio criam sua própria linguagem de programação de código aberto como uma cerimônia de maioridade - comentários contundentes de internautas: Contando com RustDesk devido a fraude desenfreada, serviço doméstico Taobao (taobao.com) suspendeu serviços domésticos e reiniciou o trabalho de otimização de versão web Java 17 é a versão Java LTS mais comumente usada no mercado do Windows 10 Atingindo 70%, o Windows 11 continua a diminuir Open Source Daily | Google apoia Hongmeng para assumir o controle de telefones Android de código aberto apoiados pela ansiedade e ambição da Microsoft; Electric desliga a plataforma aberta Apple lança chip M4 Google exclui kernel universal do Android (ACK) Suporte para arquitetura RISC-V Yunfeng renunciou ao Alibaba e planeja produzir jogos independentes para plataformas Windows no futuro
{{o.nome}}
{{m.nome}}

Acho que você gosta

Origin my.oschina.net/u/4526289/blog/11104017
Recomendado
Clasificación