"Ajuste fino de modelo grande" usando DDP para realizar o guia paralelo de várias placas de máquina única do programa

Recentemente, sob a influência da tendência geral, começou a se envolver em grandes modelos. Devido à limitação dos recursos de computação do laboratório, é necessário executar o programa em um modo paralelo multicartão de máquina única. Aqui, o modelo BLOOM-560m é usado como exemplo para demonstrar como ajustar e concluir tarefas de downstream através de um modo paralelo DDP multicartão de máquina única.


0. Noções básicas

- Dois métodos de treinamento distribuídos

⚠️: A distribuição Pytorch atualmente suporta apenas Linux. Existem basicamente duas maneiras de realizar o paralelismo do programa: DataParallel e DistributedDataParallel:

  • DataParallel (DP): A implementação é simples, a quantidade de código é pequena e a velocidade de inicialização é mais rápida. Mas a velocidade é lenta e há um problema de carga desequilibrada. Processo único, multi-threaded . O uso de memória do cartão principal será muito maior do que o de outros cartões. O treinamento de precisão misto para Apex não é suportado. É uma solução dada pelo funcionário da Pytorch há muito tempo. Limitado pelo Python GIL, o princípio operacional do DP é dividir os dados de entrada de um tamanho de lote em várias GPUs para cálculos separados (observe aqui que o tamanho do lote deve ser maior que o número de GPUs a serem divididos).

  • DistributedDataParallel (DDP): Modo All-Reduce, originalmente planejado para treinamento distribuído (multi-cartão multimáquina), mas também pode ser usado para multi-cartão de máquina única. A configuração é um pouco mais complicada. Múltiplos processos . A distribuição dos dados é relativamente equilibrada. É uma nova geração do método de treinamento Doka. O paralelismo é obtido usando a biblioteca arch.distributed. Suporte distribuído, incluindo suporte de treinamento distribuído para GPUs e CPUs, é fornecido pela biblioteca arch.distributed, que fornece uma interface semelhante a MPI para troca de dados de tensor em uma rede de várias máquinas. Ele oferece suporte a vários back-ends e métodos de inicialização diferentes. O DDP melhora a eficiência da comunicação por meio do método de troca de dados Ring-Reduce e alivia a limitação do Python GIL iniciando vários processos, aumentando assim a velocidade de treinamento.

  • O princípio do treinamento de cartões múltiplos DDP

    1. Copie o modelo em cada GPU;
    2. O total de dados do lote é igualmente dividido em diferentes GPUs para cálculo (a ordem aleatória é interrompida) e cada processo carrega seus próprios dados do disco;
    3. Durante o treinamento do modelo, a propagação direta e o cálculo da função de perda são executados independentemente em cada GPU, portanto, não há necessidade de coletar a saída da rede. Durante a retropropagação, cada processo se comunica com outros processos por meio de um método chamado Ring-Reduce, trocando seus próprios gradientes, de modo a obter o gradiente médio de todos os processos; então, use esse valor para realizar gradiente descendente em todas as GPUs, de modo que cada uma das GPUs termina com uma cópia idêntica do gradiente médio no final da retropropagação;
    4. Cada processo atualiza seus próprios parâmetros com o gradiente médio, porque os parâmetros iniciais e os gradientes de atualização de cada processo são consistentes, então os parâmetros atualizados também são exatamente os mesmos.

- Paralelo de Dados e Paralelo de Modelo

  • O paralelismo de dados significa que várias GPUs usam a mesma cópia do modelo, mas usam dados diferentes no mesmo lote para treinamento.
  • O paralelismo do modelo significa que várias GPUs usam o mesmo lote de dados para treinar diferentes partes do modelo separadamente.

Simplesmente lembre-se: paralelismo é dividir objetos paralelos para melhorar a eficiência da computação.


1. Modificação do programa

Este tutorial usa o método DDP para obter o paralelismo do programa. Consulte este tutorial para realizar a replicação de vários cartões de modelo e o paralelismo de dados.

1.1 Importar pacotes de chaves

A seguir estão os pacotes que serão utilizados no processo de modificação do programa; entre eles, dist é responsável pela comunicação multiplaca e DDP é responsável pela transferência de modelo e outros trabalhos.

import torch.distributed as dist
import torch.multiprocessing as mp
from torch.cuda.amp import GradScaler
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP

1.2 Definir as principais funções

  • init_ddp(local_rank)

Inicialize o processo, use o back-end nccl e use env como o método init.

local_rank = dist.get_rank()
world_size = dist.get_world_size()

def init_ddp(local_rank):
    # 有了这一句之后,在转换device的时候直接使用 a=a.cuda()即可,否则要用a=a.cuda(local_rank)
    torch.cuda.set_device(local_rank)
    os.environ['RANK'] = str(local_rank)
    dist.init_process_group(backend='nccl', init_method='env://')

Depois de concluir a inicialização, você pode facilmente obter local_rank, quando necessário world_size, sem main()passar da função camada por camada como parâmetros adicionais. printPor exemplo, quando , log, é necessário save_model, como vários processos possuem a mesma cópia, apenas um processo é necessário para executar, por exemplo:

if local_rank == 0:
    print(f'begin validating')  

......

if local_rank == 0:
    save_model(actual_epoch, model, scaler, args['model_save_dir'] + '/best_macro_model_DDP_direct.pt')
  • reduzir_tensor(tensor)

Resuma os resultados do cálculo de vários processos, como indicadores de perda e avaliação.

def reduce_tensor(tensor: torch.Tensor):
    '''
    对多个进程计算的多个 tensor 类型的 输出值取平均操作
    '''
    rt = tensor.clone()  # tensor(9.1429, device='cuda:1')
    dist.all_reduce(rt, op=dist.reduce_op.SUM)
    rt /= dist.get_world_size()
    return rt
  • get_ddp_generator(semente)

Usado no processo de treinamento para aumentar a aleatoriedade do treinamento.

def get_ddp_generator(seed=3407):
    '''
    对每个进程使用不同的随机种子,增强训练的随机性
    '''
    local_rank = dist.get_rank()
    g = torch.Generator()
    g.manual_seed(seed + local_rank)
    return g

1.3 Entrada do programa

Em if __name__ == '__main__':, use spawn()a função para iniciar o DDP, os principais parâmetros dessa função incluem:

  1. fn: Funções que requerem paralelismo. Aqui está main()a função, que será executada uma vez por thread;
  2. args: Argumentos exigidos por fn. Nota: Os parâmetros passados ​​para fn devem ser escritos na forma de tuplas, mesmo que haja apenas um parâmetro;
  3. nprocs: O número de processos a serem iniciados, o valor padrão é 1. Aqui pode ser definido como world_size. O valor de nprocs é inconsistente com world_size, o que fará com que o processo aguarde a sincronização e estagne.
if __name__ == '__main__':

    parser = argparse.ArgumentParser()
    parser.add_argument('-args', help="priority", type=bool, required=False, default=True)
    parser.add_argument('-gpu', default='0,1', type=str, help='gpu device ids for CUDA_VISIBLE_DEVICES')
    parser.add_argument('-mode', help="train&test", type=str, required=False, default='train')
    parser.add_argument('-requires_grad', help="whether to weight_decay", type= bool, required=False, default=True)
    args = parser.parse_args()
    
    os.environ['MASTER_ADDR'] = 'localhost'  # 0号机器的IP
    os.environ['MASTER_PORT'] = '19198'  # 0号机器的可用端口
    os.environ['CUDA_VISIBLE_DEVICES'] = args['gpu']  # 使用哪些GPU
    world_size = torch.cuda.device_count()
    os.environ['WORLD_SIZE'] = str(world_size)
    os.environ["PYTORCH_CUDA_ALLOC_CONF"] = "max_split_size_mb:128"
    os.environ["TOKENIZERS_PARALLELISM"] = "false"  # 指定程序在分词时不并行执行
    
    if args['mode'] == 'train':
        time_start = time.time()
        mp.spawn(fn=main, args=(args, ), nprocs=world_size)
        time_elapsed = time.time() - time_start
        print(f'\ntime elapsed: {
      
      time_elapsed:.2f} seconds.')

    elif args['mode'] == 'test':  
        time_start = time.time()
        mp.spawn(fn=test, args=(args, ), nprocs=world_size)
        time_elapsed = time.time() - time_start
        print(f'\ntime elapsed: {
      
      time_elapsed:.2f} seconds.')

1.4 função main()

A função aqui é o primeiro parâmetro passado na função main()mencionada acima . spawn()As partes principais do código são modificadas da seguinte forma:

  • Atualização da lista de parâmetros: adicione parâmetros adicionais local_rank, que não precisam ser mp.spawn()passados ​​na função, e o sistema os atribuirá automaticamente;

  • Inicialização do processo: init_ddp()implementação da função de chamada;

  • Sincronização da camada BN: Chame convert_sync_batchnorm()a função para concluir o BN de maneira síncrona para simular o máximo possível um cenário de placa única. Embora reduza a utilização da GPU, pode melhorar o desempenho do modelo em cenários de várias placas (consulte este blog para detalhes); Sincronização da camada BN A necessidade depende do tamanho do batch_size de um único cartão. Se o batch_size de um único cartão for muito pequeno, usar SyncBN pode melhorar o desempenho. No entanto, se batch_size for grande, não há necessidade de usar SyncBN, pois isso requer comunicação entre vários cartões, o que diminuirá a velocidade de treinamento.

  • Paralelismo de dados: DistributedDataParallel()implementação de funções de chamada;

  • Especifique o treinamento de precisão mista: chame GradScaler()a implementação da função e passe-a para train()a função como um parâmetro;

  • Configurações do amostrador de treinamento: cada época define uma ordem de amostragem diferente;

  • Evite a execução repetida de cópias: use if local_rank==0:a instrução para restringir;

  • Eliminar grupos de processos: destroy_process_group()implementação de função de chamada.

def main(local_rank, args):  # 参数列表更新
    init_ddp(local_rank)  ### 进程初始化

    best_macro = 0

    model, tokenizer = initialise_model(args['modelname'], args['num_labels'])
    model.cuda()
    model = nn.SyncBatchNorm.convert_sync_batchnorm(model)  # BN层同步
    
    num_gpus = torch.cuda.device_count()
    if num_gpus > 1:
        print('use {} gpus!'.format(num_gpus))
        model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)  ### 套 DDP
    
    num_training_steps = args['num_epochs'] * (args['num_samples'] // args['batch_size']) #总的训练步数
    
    if args['requires_grad']:  # 权重衰减
        param_optimizer = list(model.named_parameters())
        no_decay = ['bias', 'LayerNorm.bias', 'LayerNorm.weight']
        # 设置模型参数的权重衰减
        optimizer_grouped_parameters = [
            {
    
    'params': [p for n, p in param_optimizer if not any(nd in n for nd in no_decay)],
             'weight_decay': 0.01},
            {
    
    'params': [p for n, p in param_optimizer if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
        ]
        optimizer = AdamW(optimizer_grouped_parameters, lr=float(args['learning_rate'])) # 部分参数更新
    else:
        optimizer = AdamW(model.parameters(), lr=float(args['learning_rate'])) # 部分参数更新
    
    scheduler = get_linear_schedule_with_warmup(optimizer,  num_warmup_steps=100, num_training_steps=num_training_steps) #创建学习率调度器。
            
    scaler = GradScaler()  ###  用于混合精度训练
    criterion = BCEWithLogitsLoss().cuda() #定义损失函数。

    train_dataloader = get_dataloader(args['traincsvpath'], args, tokenizer, train=True)
    valid_dataloader = get_dataloader(args['valcsvpath'], args, tokenizer, train=False)

    for actual_epoch in trange(args['num_epochs'], desc="Epoch"):
        if local_rank == 0:  ### 防止每个进程都输出一次
            print("begin training of epoch %d / %d" % (actual_epoch + 1, args['num_epochs']))
        
        train_dataloader.sampler.set_epoch(actual_epoch)  # 训练时每次的 sampling 顺序不同
        train(model, train_dataloader, optimizer, scheduler, criterion, actual_epoch, scaler, args)   
        
        if local_rank == 0:
            print(f'begin validating')  
        macro = validate(model, valid_dataloader, criterion, actual_epoch, args) #在验证集上评估模型。
     
        if macro > best_macro:
            best_macro = macro
            if local_rank == 0:  # 防止每个进程都保存一次
                save_model(actual_epoch, model, scaler, args['model_save_dir'] + '/best_macro_model_DDP_direct.pt')
                    
    dist.destroy_process_group()  # 消除进程组,和 init_process_group 相对
  • Além das modificações acima, main()as três funções usadas na função, get_dataloader()função, train()função e validate()função também precisam ser atualizadas de acordo, o que será explicado separadamente abaixo.

1.5 Função get_dataloader()

Esta função DataLoader()modifica principalmente a função. Para as duas fases de "treinamento" e "teste", defina train_sampler e test_sampler respectivamente, onde train_sampler é definido como amostragem aleatória e test_sampler é amostragem sequencial. Além disso, na fase de "treinamento", use get_ddp_generator()a função DataLoader()para passar parâmetros para a função generator(atuando em diferentes trabalhadores), caso contrário, a aleatoriedade do treinamento será enfraquecida.

def get_dataloader(path, args, tokenizer, train:bool): 
    '''
    根据给定的路径获取数据,并将数据和训练标志传递给数据加载器,这样可以方便地从给定路径加载数据并生成数据加载器,以供后续的模型训练和评估使用。
    path:数据存放路径
    tokenizer:分词器
    train:是否是训练阶段
    '''
    texts, labels = load_dataset(path, args['num_labels'])
    texts = tokenizer(texts, padding='max_length', truncation=True, return_tensors='pt', max_length=args['max_length']) 
    data = TensorDataset(texts['input_ids'], texts['attention_mask'], torch.tensor(labels)) 
    
    if train:
        train_sampler = DistributedSampler(data, shuffle=True)  # #创建一个随机采样器。
        g = get_ddp_generator()
        dataloader = DataLoader(dataset=data,
                                batch_size=args['batch_size'],
                                num_workers=args['num_workers'],
                                pin_memory=True,
                                shuffle=False,
                                sampler=train_sampler, #采用随机采样器。
                                generator=g) 
        
    else:
        test_sampler = DistributedSampler(data, shuffle=False) #创建一个顺序采样器。
        dataloader = DataLoader(dataset=data,
                                batch_size=args['batch_size'],
                                num_workers=args['num_workers'],
                                pin_memory=True,
                                shuffle=False,
                                sampler=test_sampler #采用顺序采样器。
                                )
    return dataloader

1.6 função train()

Esta função usa principalmente reduce_tensor()a função para calcular a média da perda e modifica a forma de retropropagação - dimensionar o gradiente através do scaler para evitar que a perda ocorra devido ao uso de precisão mista e atualizar o estado do próprio scaler . Vários processos paralelos compartilham o mesmo scaler. Durante o processo de salvamento do modelo, se você precisar continuar treinando (como o modo pré-treinamento-ajuste fino), é melhor salvar o estado do scaler junto e carregá-lo junto com os parâmetros do modelo no ajuste fino subsequente - processo de sintonia.

def train(model, train_dataloader, optimizer, scheduler, criterion, actual_epoch, scaler, args):

    model.train()
    
    tr_loss = 0
    num_train_samples = 0
    
    for step, batch in enumerate(train_dataloader):
        
        batch = tuple(t.cuda(non_blocking=True) for t in batch)
        b_input_ids, b_input_mask, b_labels = batch
        with torch.cuda.amp.autocast():
	        output = model(b_input_ids, attention_mask=b_input_mask, labels=b_labels) # 运行到这一行会增加一下显存
	        loss = criterion(output.logits.view(-1,args['num_labels']), b_labels.type_as(output.logits).view(-1,args['num_labels']))
        
        reduced_loss = reduce_tensor(loss.data)  # 对并行进程计算的多个 loss 取平均
        if dist.get_rank() == 0:  # 防止重复输出
            print("\nOutput Loss: ", reduced_loss.item())
        tr_loss += reduced_loss.item()
        
        # 并行状态下的更新,不同进程分别根据自己计算的 loss 更新数据
        optimizer.zero_grad()
        scaler.scale(loss).backward()
        scaler.step(optimizer)  #  运行到这一行会增加一下显存
        # 下面四行,多个进程只执行一次
        scheduler.step()
        scaler.update()
        num_train_samples += b_labels.size(0) #将批次中的样本数量添加到 num_train_samples 中。
        torch.cuda.empty_cache()  # 释放GPU reserved memory显存
    
    epoch_train_loss = tr_loss / num_train_samples  # num_train_samples 代表每个进程承接的样本数量,由于上面已经有对loss取平均的操作,这里分母无需再乘以进程数

    if dist.get_rank() == 0:
        print("\nTrain loss after Epoch {} : {}".format(actual_epoch, epoch_train_loss))

1.7 função validar()

@torch.no_grad()
def validate(model, valid_dataloader, criterion, epoch, args, threshold=0.5):

    model.eval()

    eval_loss = 0.0
    num_eval_samples = 0
    
    pred_labels = []
    true_labels = []
    
    for step, batch in enumerate(valid_dataloader):
        
        batch = tuple(t.cuda(non_blocking=True) for t in batch)
        b_input_ids, b_input_mask, b_labels = batch
        
        with torch.no_grad():
            with torch.cuda.amp.autocast():
	            output = model(b_input_ids, attention_mask=b_input_mask)
	            logits = output.logits
                        
            loss = criterion(logits.view(-1,args['num_labels']), b_labels.type_as(logits).view(-1,args['num_labels']))
            
            reduced_loss = reduce_tensor(loss.data)
            eval_loss += reduced_loss.item()
            
            pred_label = torch.sigmoid(logits)
            pred_label = pred_label.to('cpu').numpy()
            b_labels = b_labels.to('cpu').numpy()
            
        pred_labels.append(pred_label)
        true_labels.append(b_labels)

        num_eval_samples += b_labels.shape[0]  # 这里是针对单个 进程 的 计算样本数
    
    epoch_eval_loss = eval_loss/num_eval_samples  
    
    if dist.get_rank() == 0:
        print("Validation loss after Epoch {} : {}".format(epoch, epoch_eval_loss))

    # 每个并行进程都会分别执行下列计算操作,得到各进程对应的macro评价指标
    pred_labels = [item for sublist in pred_labels for item in sublist]
    true_labels = [item for sublist in true_labels for item in sublist]
    pred_bools = [pl>threshold for pl in pred_labels]
    true_bools = [tl==1 for tl in true_labels]
    macro = f1_score(true_bools, pred_bools, average='macro')
    
    # 汇总不同进程的实验结果
    macro = reduce_tensor(torch.tensor(macro).cuda())
    
    return macro

1.8 função test()

Como pode ser visto na Seção 1.3, meu procedimento aqui é separar os processos de "treinamento e verificação" e "teste", salvar o modelo no estágio anterior e verificar o modelo no último estágio. Então deixe-me apresentar test()o conteúdo que precisa ser modificado na função separadamente.Esta parte envolve o carregamento do modelo de ponto de verificação. O método de inferência acelerada é detalhado neste blog .


@torch.no_grad()
def test(local_rank, args):

    init_ddp(local_rank)  # 进程初始化

    pred_labels = []
    true_labels = []

    if local_rank == 0:
        print(f'begin testing')    

    save_path = args['model_save_dir'] + '/best_macro_model_DDP_direct.pt'
    model, tokenizer = load_model(save_path, args['modelname'], args['num_labels'])
    model.cuda()
    model = nn.SyncBatchNorm.convert_sync_batchnorm(model)  ### 转换模型的 BN 层
    
    num_gpus = torch.cuda.device_count()
    if num_gpus > 1 and local_rank == 0:
        print('use {} gpus!'.format(num_gpus))
        model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)  ### 套 DDP

    model.eval()

    test_dataloader = get_dataloader(args['testcsvpath'], args, tokenizer, train=False)
    
    for idx, batch in enumerate(test_dataloader): #遍历测试集的数据加载器。

	......    

    dist.destroy_process_group()  # 消除进程组

Observação⚠️Durante a fase de teste, o programa também precisa ser executado em paralelo, caso contrário, um erro será relatado (tome o salvamento completo como exemplo):

python /data/gluo/CMLTES/codes/BLOOM_DDP_direct.py -mode "test"

torch.multiprocessing.spawn.ProcessRaisedException:
-- Process 1 terminated with the following error:
Traceback (most recent call last):
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 69, in _wrap
fn(i, *args)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/autograd/grad_mode.py", line 27, in decorate_context
return func(*args, **kwargs)
File "/data/gluo/CMLTES/codes/BLOOM_DDP_direct.py", line 449, in test
output = model(b_input_ids, attention_mask=b_input_mask, labels=b_labels) #获取模型的输出。
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1130, in _call_impl
return forward_call(*input, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/parallel/distributed.py", line 1008, in forward
output = self._run_ddp_forward(*inputs, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/parallel/distributed.py", line 969, in _run_ddp_forward
return module_to_run(*inputs[0], **kwargs[0])
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1130, in _call_impl
return forward_call(*input, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/parallel/distributed.py", line 1008, in forward
output = self._run_ddp_forward(*inputs, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/parallel/distributed.py", line 969, in _run_ddp_forward
return module_to_run(*inputs[0], **kwargs[0])
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1130, in _call_impl
return forward_call(*input, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/transformers/models/bloom/modeling_bloom.py", line 1030, in forward
transformer_outputs = self.transformer(
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1130, in _call_impl
return forward_call(*input, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/transformers/models/bloom/modeling_bloom.py", line 727, in forward
inputs_embeds = self.word_embeddings(input_ids)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1130, in _call_impl
return forward_call(*input, **kwargs)
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/sparse.py", line 158, in forward
return F.embedding(
File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/functional.py", line 2199, in embedding
return torch.embedding(weight, input, padding_idx, scale_grad_by_freq, sparse)
RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:1 and cuda:0! (when checking argument for argument index in method wrapper__index_select)

Neste ponto, o programa está todo modificado!


2. Programa em execução

A seguir são apresentados vários métodos de inicialização de várias placas do DDP.

2.1 mp.spawn() começa

O método de inicialização usado por este programa é a função mp.spawn(), onde o módulo mp completa o empacotamento da biblioteca de multiprocessamento e não visa especificamente o DDP.

No início, usei duas placas gráficas 2080 Ti para rodar o programa em paralelo, porém, descobri que logo após iniciar a 0ª Época, sempre era reportado um erro, RuntimeError: CUDA out of memory.conforme abaixo:

Traceback (most recent call last):
  File "/data/CMLTES_codes/experiment/bloom/BLOOM_DDP.py", line 690, in <module>
    mp.spawn(main, args=(args, ), nprocs=world_size)
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 240, in spawn
    return start_processes(fn, args, nprocs, join, daemon, start_method='spawn')
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 198, in start_processes
    while not context.join():
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 160, in join
    raise ProcessRaisedException(msg, error_index, failed_process.pid)
torch.multiprocessing.spawn.ProcessRaisedException: 

-- Process 1 terminated with the following error:
Traceback (most recent call last):
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 69, in _wrap
    fn(i, *args)
  File "/data/CMLTES_codes/experiment/bloom/BLOOM_DDP.py", line 603, in main
    epoch_train_loss = train(model, train_dataloader, optimizer, scheduler, loss_func, actual_epoch, scaler, args)
  File "/data/CMLTES_codes/experiment/bloom/BLOOM_DDP.py", line 336, in train
    scaler.scale(loss).backward()  ###
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/_tensor.py", line 396, in backward
    torch.autograd.backward(self, gradient, retain_graph, create_graph, inputs=inputs)
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/autograd/__init__.py", line 173, in backward
    Variable._execution_engine.run_backward(  # Calls into the C++ engine to run the backward pass
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/torch/autograd/function.py", line 253, in apply
    return user_fn(self, *args)
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/transformers/models/bloom/modeling_bloom.py", line 188, in backward
    tmp = bloom_gelu_back(grad_output, input)
  File "/root/anaconda3/envs/pytorch77/lib/python3.9/site-packages/transformers/models/bloom/modeling_bloom.py", line 175, in bloom_gelu_back
    ff = 0.5 * x * ((1 - tanh_out * tanh_out) * (0.79788456 + 0.1070322243 * x * x)) + 0.5 * (1 + tanh_out)
RuntimeError: CUDA out of memory. Tried to allocate 32.00 MiB (GPU 1; 10.76 GiB total capacity; 8.83 GiB already allocated; 28.56 MiB free; 8.94 GiB reserved in total by PyTorch) If reserved memory is >> allocated memory try setting max_split_size_mb to avoid fragmentation.  See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF

Depois de ficar confuso, tentei executar o programa no 3090 como está e descobri que ele pode ser executado normalmente! A lição aqui é que a memória de cartão único da GPU também é importante para o paralelismo ! Ao ajustar um modelo de cerca de 1,2G, cerca de 40G de variáveis ​​intermediárias são necessárias para retropropagação... Essa é uma situação que eu realmente não esperava...


2.2 tochrnn start

Comparado ao uso de mp.spawn() para iniciar, o archrun controlará automaticamente a configuração de algumas variáveis ​​de ambiente, portanto, é mais conveniente. Só precisamos definir os.environ['CUDA_VISIBLE_DEVICES'] (não definir os padrões para todas as GPUs na máquina), sem definir os.environ['MASTER_ADDR'] etc. Além disso, a função main() não requer mais um parâmetro local_rank. A entrada do programa torna-se:

if __name__ == '__main__':
    
    ......
    
    time_start = time.time()
    main(args)
    time_elapsed = time.time() - time_start
    local_rank = int(os.environ['LOCAL_RANK'])
    if local_rank == 0:
        print(f'\ntime elapsed: {
      
      time_elapsed:.2f} seconds')

O comando para executar o script é alterado de python para archunrun, conforme a seguir:

torchrun --standalone --nproc_per_node=2 ddp_main_torchrun.py --gpu 0,1

Depois que o programa rodar com sucesso, ainda faltam alguns detalhes, que serão resolvidos um a um a seguir.

2.3 início do arch.distributed.launch()

Dessa forma, a quantidade de código é menor e a velocidade de inicialização é mais rápida.

python -m torch.distributed.launch --nproc_per_node 8 xxx.py
# -m 意思是 run library module as a script
# -nproc_per_node 表示每台机器的进程数

PS: Este método será eliminado:

/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/distributed/launch.py:178: FutureWarning: The module torch.distributed.launch is deprecated
and will be removed in future. Use torchrun.

3. Processo de depuração

Os problemas encontrados durante o uso do DDP são analisados ​​e as soluções são apresentadas a seguir.

Problema 1: Coleta de dados de computação multiprocesso

Como estou copiando o modelo para cartões duplos para obter paralelismo de dados, ao resumir os resultados, é necessário resumir os dados em diferentes processos e calcular o valor médio. Neste momento, a função de coleta mencionada na Seção 1.2 precisa ser usada all_reduce().

Observe aqui ⚠️: Para dados não tensores, como float , se quisermos calcular o valor médio de vários processos, podemos primeiro usar o arch.tensor() para converter as variáveis ​​que precisam ser resumidas em tensor e usar o comando .cuda()para coloque-os na gpu e, em seguida, chame all_reduce()a função de coleta. validate()Para obter detalhes, consulte a coleta e cálculo de variáveis ​​na função na Seção 1.7 macro. Se a conversão de dados não for concluída, um erro será relatado da seguinte forma:

Problema derivado: Ao realizar o backpropagation, os dados de treinamento utilizados por cada processo são diferentes, portanto ainda é necessário atualizar de acordo com a perda atualmente calculada por ele mesmo, ao invés de atualizar de acordo com o valor de perda obtido pela função de coleta, caso contrário, um erro será relatado e o ilógico.


Problema 2: faltam parâmetros de carregamento do modelo

Na função main() na seção 1.4, o modelo é armazenado na forma de "salvar apenas os parâmetros do modelo". Durante a fase de teste, ao carregar o modelo da forma correspondente, o erro é o seguinte:

(CMLTES) ➜  CMLTES git:(master) ✗ python /data/gluo/CMLTES/codes/BLOOM_DDP.py -mode "test"
Model directory for bloom and batch size 4 already exists!
TEST FOR bloom and Batch Size4
[W socket.cpp:558] [c10d] The client socket has failed to connect to [localhost]:19198 (errno: 99 - Cannot assign requested address).
begin testing
Some weights of BloomForSequenceClassification were not initialized from the model checkpoint at /data/gluo/CMLTES/bloom_PRE and are newly initialized: ['score.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Some weights of BloomForSequenceClassification were not initialized from the model checkpoint at /data/gluo/CMLTES/bloom_PRE and are newly initialized: ['score.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
Traceback (most recent call last):
  File "/data/gluo/CMLTES/codes/BLOOM_DDP.py", line 586, in <module>
    mp.spawn(test, args=(args, ), nprocs=world_size)
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 240, in spawn
    return start_processes(fn, args, nprocs, join, daemon, start_method='spawn')
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 198, in start_processes
    while not context.join():
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 160, in join
    raise ProcessRaisedException(msg, error_index, failed_process.pid)
torch.multiprocessing.spawn.ProcessRaisedException: 

-- Process 0 terminated with the following error:
Traceback (most recent call last):
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 69, in _wrap
    fn(i, *args)
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/autograd/grad_mode.py", line 27, in decorate_context
    return func(*args, **kwargs)
  File "/data/gluo/CMLTES/codes/BLOOM_DDP.py", line 450, in test
    model, tokenizer = load_model(save_path, args['modelname'], args['num_labels']) #加载模型。
  File "/data/gluo/CMLTES/codes/BLOOM_DDP.py", line 95, in load_model
    model.load_state_dict(model_state_dict) #, strict=False) #加载模型的参数。
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/modules/module.py", line 1604, in load_state_dict
    raise RuntimeError('Error(s) in loading state_dict for {}:\n\t{}'.format(
RuntimeError: Error(s) in loading state_dict for BloomForSequenceClassification:
        Missing key(s) in state_dict: "transformer.word_embeddings.weight", "transformer.word_embeddings_layernorm.weight", "transformer.word_embeddings_layernorm.bias", "transformer.h.0.input_layernorm.weight", "transformer.h.0.input_layernorm.bias", "transformer.h.0.self_attention.query_key_value.weight", "transformer.h.0.self_attention.query_key_value.bias", "transformer.h.0.self_attention.dense.weight", "transformer.h.0.self_attention.dense.bias",

De acordo com este blog , o método de processamento temporário aqui é: modifique load_state_dict()a função para: model.load_state_dict(model_state_dict, strict=False), ou seja, definindo stricto valor do parâmetro Falsepara .significa strict=False: não é estritamente necessário state_dictque a chave in corresponda à chave retornada pela chave deste módulo .

O método de processamento mencionado acima pode ignorar temporariamente o problema de parâmetros ausentes mencionado acima, mas pode ter um certo grau de impacto no desempenho do modelo, e esse problema precisa ser resolvido no futuro.

PS: Dois métodos para salvar e carregar modelos

De acordo com este blog , existem duas maneiras de salvar o modelo. Uma é salvar todas as informações do modelo e a outra é salvar apenas os parâmetros do modelo. Os métodos de carregamento do modelo correspondentes aos dois métodos de salvamento são naturalmente diferente.

  • Salve todas as informações do modelo
# 保存模型
checkpoint = {
    
    'model': model,\
              'scaler': scaler
                }
torch.save(checkpoint, save_path)

# 加载模型
checkpoint = torch.load(save_path)
model = checkpoint['model']  # 加载模型
  • Salve apenas os parâmetros do modelo
    Diferente do primeiro método, ao carregar o modelo neste método, você precisa primeiro definir a mesma estrutura do modelo que o modelo salvo e, em seguida, carregar os parâmetros do modelo.
# 保存模型
checkpoint = {
    
    'state_dict': model.state_dict(),\
              'scaler': scaler.state_dict()
                }
torch.save(checkpoint, save_path)

# 加载模型
checkpoint = torch.load(save_path, map_location=torch.device('cpu'))
model_state_dict = checkpoint['state_dict']
model.load_state_dict(model_state_dict) #, strict=False) #加载模型参数。

Pergunta 3: Exceção de conversão de tipo de parâmetro

Na função main() na seção 1.4, o modelo é armazenado na forma de "salvar apenas os parâmetros do modelo". Durante a fase de teste, ao carregar o modelo da forma correspondente, o erro é o seguinte:

Depois de usar o método acima para resolver o problema de falta de parâmetros do modelo carregado, os problemas resultantes são os seguintes.

Traceback (most recent call last):
  File "/data/gluo/CMLTES/codes/BLOOM_DDP.py", line 587, in <module>
    mp.spawn(test, args=(args, ), nprocs=world_size)
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 240, in spawn
    return start_processes(fn, args, nprocs, join, daemon, start_method='spawn')
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 198, in start_processes
    while not context.join():
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 160, in join
    raise ProcessRaisedException(msg, error_index, failed_process.pid)
torch.multiprocessing.spawn.ProcessRaisedException: 

-- Process 0 terminated with the following error:
Traceback (most recent call last):
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/multiprocessing/spawn.py", line 69, in _wrap
    fn(i, *args)
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/autograd/grad_mode.py", line 27, in decorate_context
    return func(*args, **kwargs)
  File "/data/gluo/CMLTES/codes/BLOOM_DDP.py", line 459, in test
    model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank], output_device=local_rank)  ### 套 DDP
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/nn/parallel/distributed.py", line 646, in __init__
    _verify_param_shape_across_processes(self.process_group, parameters)
  File "/opt/conda/envs/CMLTES/lib/python3.9/site-packages/torch/distributed/utils.py", line 89, in _verify_param_shape_across_processes
    return dist._verify_params_across_processes(process_group, tensors, logger)
RuntimeError: value cannot be converted to type int without overflow

As razões profundas precisam ser mais exploradas.


Problema 4: Vazamento de Parâmetros

Registro de erros:UserWarning: resource_tracker: There appear to be 1 leaked semaphore objects to clean up at shutdown warnings.warn('resource_tracker: There appear to be %d '

insira a descrição da imagem aqui
Como pode ser visto na figura acima, o motivo do aviso acima é o uso do Ctrl+Cprograma de interrupção. As razões profundas precisam ser mais exploradas.

Nota ⚠️: Ao usar o PyTorch para configurar multi-threading para leitura de dados, a operação real em segundo plano é abrir N subprocessos com números PID consecutivos para simular o trabalho multi-thread, portanto, se o programa terminar a execução ou encerrar o processo principal em no meio, a memória da GPU dos subprocessos não será liberada, você precisa matar manualmente um por um.

> 本篇博客没有涉及到的知识点:dist.barrier()、Gradient Accumulation、Apex 实现混合精度训练&分布式训练、

Pós-escrito: Este blog é um resumo da minha exploração contínua. Se houver expressões inadequadas ou significados pouco claros, espero que você me dê seu conselho e progrediremos juntos!


referências

  1. Prática de código DDP que pode ser entendida rapidamente - Zhihu (zhihu.com)
  2. Guia de uso conciso do Pytorch DistributedDataParallel - Zhihu (zhihu.com)
  3. PyTorch DistributedDataParallel treinamento de vários cartões de máquina única pisando registro de poço
  4. Duas maneiras de salvar o modelo no Blog-CSDN de pytorch_SCU-JJkinging
  5. O modelo de carregamento aparece RuntimeError: Error(s) in loading state_dict for Model: Missing key(s) in state_dict_sovits loading raso diffusion model error_大海Git的博客-CSDN博客
  6. A diferença entre model.to(device) e map_location=device em pytorch - programador procurado
  7. RuntimeError: CUDA sem memória. Algumas jornadas de depuração de bugs - Zhihu (zhihu.com)
  8. Quais falhas/bugs você encontrou na computação distribuída do pytorch? - Zhihu (zhihu. com)
  9. Sobre o uso da função Distributionsampler no pytorch - programador procurado
  10. Resolva o problema CUDA: Out Of Memory causado pela fragmentação da memória de vídeo do Pytorch definindo max_split_size_mb em PYTORCH_CUDA_ALLOC_CONF_梦音Yune's Blog-CSDN Blog
  11. Exemplo de uso do arch.cuda.amp.autocast()
  12. O erro set_seed do PyTorch que 99% das pessoas podem cometer destruirá a aleatoriedade, e o worker_init_fn oficial não pode resolvê-lo-Know (zhihu.com)
  13. A terceira parte da série PyTorch DDP original em profundidade: combate e habilidades reais - Zhihu (zhihu.com)
  14. Blog do arch.distributed_Wanderer001 - Blog da CSDN

A série de recursos a seguir são todos deste blogger , que pode ser considerado um tutorial muito detalhado sobre paralelismo de dados!

  1. Pytorch (11) - paralelismo de treinamento distribuído (multi-GPU/multi-placa) (DP e DDP)_pytorch gpu distribuído_hxxjxw's blog-CSDN blog
  2. O conceito básico de PyTorch multi-card/multi-GPU/DPP distribuído (node&rank&local_rank&nnodes&node_rank&nproc_per_node&world_size) - programador procurado
  3. Torch.distributed multi-card/multi-GPU/distributed DPP (1) - arch.distributed.launch & all_gather & init_process_group_hxxjxw's blog - blog CSDN
  4. arch.distributed multi-card/multi-GPU/distributed DPP (2)—torch.distributed.all_reduce(reduce_mean)controle de barreira ordem de execução do processo &seed random seed_torch dpp_hxxjxw's blog-CSDN blog
  5. Treinamento distribuído Pytorch / treinamento multi-card DDP - inicialização do modelo (diferença entre o arch.distribute e o DDP) - pytorch distribui o blog do archtrun-CSDN
  6. BN (BatchNorm) em Doka training_Doka batchnorm_hxxjxw's Blog-CSDN Blog
  7. Por que o treinamento multiplaca Pytorch facilmente faz com que a memória da GPU não seja liberada - programador procurado
  8. Treinamento distribuído Pytorch/treinamento multiplaca (1) - Data Parallel parallel (DP)_model = nn.dataparallel(model, device_ids=[0, 1])_hxxjxw's blog-CSDN blog
  9. Treinamento distribuído Pytorch / treinamento multi-card (2) - Data Parallel parallel (DDP) (2.1) (conceito básico e estrutura de código)_slurm_procid_hxxjxw's blog-CSDN blog
  10. Treinamento distribuído Pytorch/treinamento multiplaca (2) - Paralelo paralelo de dados (DDP) (2.2) (exemplo de código) (sincronização BN e armazenamento da placa principal e acumulação de gradiente e inferência de teste multiplaca e semente aleatória) _ddp program seed_hxxjxw Blog -Blog CSDN
  11. Treinamento distribuído Pytorch / treinamento multi-cartão (2) - Data Parallel parallel (DDP) (2.3) (torch.multiprocessing (spawn) & Apex)_torch.multiprocessing.spawn_hxxjxw's blog-CSDN blog
  12. Treinamento distribuído Pytorch / treinamento multi-cartão (3) - Modelo Paralelo modelo parallel_hxxjxw's blog-CSDN blog

Acho que você gosta

Origin blog.csdn.net/qq_36332660/article/details/131061155
Recomendado
Clasificación