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.
Índice
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
- Copie o modelo em cada GPU;
- 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;
- 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;
- 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. print
Por 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:
- fn: Funções que requerem paralelismo. Aqui está
main()
a função, que será executada uma vez por thread; - 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;
- 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 sermp.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 paratrain()
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 evalidate()
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 strict
o valor do parâmetro False
para .significa strict=False
: não é estritamente necessário state_dict
que 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 '
Como pode ser visto na figura acima, o motivo do aviso acima é o uso do Ctrl+C
programa 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
- Prática de código DDP que pode ser entendida rapidamente - Zhihu (zhihu.com)
- Guia de uso conciso do Pytorch DistributedDataParallel - Zhihu (zhihu.com)
- PyTorch DistributedDataParallel treinamento de vários cartões de máquina única pisando registro de poço
- Duas maneiras de salvar o modelo no Blog-CSDN de pytorch_SCU-JJkinging
- 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博客
- A diferença entre model.to(device) e map_location=device em pytorch - programador procurado
- RuntimeError: CUDA sem memória. Algumas jornadas de depuração de bugs - Zhihu (zhihu.com)
- Quais falhas/bugs você encontrou na computação distribuída do pytorch? - Zhihu (zhihu. com)
- Sobre o uso da função Distributionsampler no pytorch - programador procurado
- 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
- Exemplo de uso do arch.cuda.amp.autocast()
- 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)
- A terceira parte da série PyTorch DDP original em profundidade: combate e habilidades reais - Zhihu (zhihu.com)
- 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!
- Pytorch (11) - paralelismo de treinamento distribuído (multi-GPU/multi-placa) (DP e DDP)_pytorch gpu distribuído_hxxjxw's blog-CSDN blog
- 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
- Torch.distributed multi-card/multi-GPU/distributed DPP (1) - arch.distributed.launch & all_gather & init_process_group_hxxjxw's blog - blog CSDN
- 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
- 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
- BN (BatchNorm) em Doka training_Doka batchnorm_hxxjxw's Blog-CSDN Blog
- Por que o treinamento multiplaca Pytorch facilmente faz com que a memória da GPU não seja liberada - programador procurado
- Treinamento distribuído Pytorch/treinamento multiplaca (1) - Data Parallel parallel (DP)_model = nn.dataparallel(model, device_ids=[0, 1])_hxxjxw's blog-CSDN blog
- 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
- 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
- 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
- Treinamento distribuído Pytorch / treinamento multi-cartão (3) - Modelo Paralelo modelo parallel_hxxjxw's blog-CSDN blog