"Ajuste fino del modelo grande" usando DDP para realizar la guía paralela multitarjeta de una sola máquina del programa

Recientemente, bajo la influencia de la tendencia general, ha comenzado a involucrarse en modelos grandes. Debido a la limitación de los recursos informáticos del laboratorio, es necesario ejecutar el programa en un modo paralelo de tarjeta múltiple de una sola máquina. Aquí, el modelo BLOOM-560m se toma como ejemplo para demostrar cómo ajustar y completar las tareas posteriores. a través de un modo paralelo DDP multitarjeta de una sola máquina.


0. Fundamentos

- Dos métodos de entrenamiento distribuidos

⚠️: La distribución de Pytorch actualmente solo es compatible con Linux. Existen principalmente dos formas de realizar el paralelismo del programa: DataParallel y DistributedDataParallel:

  • DataParallel (DP): La implementación es simple, la cantidad de código es pequeña y la velocidad de inicio es más rápida. Pero la velocidad es lenta y hay un problema de carga desequilibrada. Proceso único, multihilo . El uso de memoria de la tarjeta principal será mucho mayor que el de otras tarjetas. No se admite el entrenamiento mixto de precisión para Apex. Es una solución dada por el funcionario de Pytorch hace mucho tiempo. Limitado por Python GIL, el principio operativo de DP es dividir los datos de entrada de un tamaño de lote en múltiples GPU para cálculos separados (tenga en cuenta que el tamaño del lote debe ser mayor que la cantidad de GPU que se dividirán).

  • DistributedDataParallel (DDP): Modo All-Reduce, originalmente pensado para entrenamiento distribuido (multimáquina multitarjeta), pero también se puede utilizar para una sola máquina multitarjeta. La configuración es un poco más complicada. Múltiples procesos . La distribución de datos es relativamente equilibrada. Es una nueva generación del método de entrenamiento Doka. El paralelismo se logra utilizando la biblioteca torch.distributed. El soporte distribuido, incluido el soporte de entrenamiento distribuido para GPU y CPU, es proporcionado por la biblioteca torch.distributed, que proporciona una interfaz similar a MPI para intercambiar datos de tensor a través de una red de múltiples máquinas. Admite varios backends y métodos de inicialización diferentes. DDP mejora la eficiencia de la comunicación a través del método de intercambio de datos Ring-Reduce y alivia la limitación de Python GIL al iniciar múltiples procesos, lo que aumenta la velocidad de entrenamiento.

  • El principio del entrenamiento multitarjeta DDP

    1. Copie el modelo en cada GPU;
    2. Los datos totales del lote se dividen por igual en diferentes GPU para el cálculo (se interrumpe el orden aleatorio) y cada proceso carga sus propios datos del disco;
    3. Durante el entrenamiento del modelo, la propagación directa y el cálculo de la función de pérdida se realizan de forma independiente en cada GPU, por lo que no es necesario recopilar la salida de la red. Durante la retropropagación, cada proceso se comunica con otros procesos a través de un método llamado Ring-Reduce, intercambiando sus propios gradientes, para obtener el gradiente promedio de todos los procesos; luego use este valor para realizar un descenso de gradiente en todas las GPU, de modo que cada GPU termina con una copia idéntica del gradiente promedio al final de la retropropagación;
    4. Cada proceso actualiza sus propios parámetros con el gradiente promedio, porque los parámetros iniciales y los gradientes de actualización de cada proceso son consistentes, por lo que los parámetros actualizados también son exactamente iguales.

- Datos paralelos y modelos paralelos

  • El paralelismo de datos significa que varias GPU usan la misma copia del modelo, pero usan diferentes datos en el mismo lote para el entrenamiento.
  • El paralelismo del modelo significa que varias GPU usan el mismo lote de datos para entrenar diferentes partes del modelo por separado.

Simplemente recuerde: el paralelismo consiste en dividir objetos paralelos para mejorar la eficiencia informática.


1. Modificación del programa

Este tutorial utiliza el método DDP para lograr el paralelismo del programa. Consulte este tutorial para realizar la replicación de múltiples tarjetas modelo y el paralelismo de datos.

1.1 Importar paquetes de claves

Los siguientes son los paquetes que se utilizarán en el proceso de modificación del programa, entre ellos, dist es responsable de la comunicación de tarjetas múltiples y DDP es responsable de la transferencia de modelos y otros trabajos.

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 funciones clave

  • init_ddp(clasificación_local)

Inicialice el proceso, use el backend nccl y use env como el 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://')

Después de completar la inicialización, puede obtener fácilmente local_rank, cuando sea necesario world_size, sin main()pasar de la función capa por capa como parámetros adicionales. Por ejemplo, cuando se necesita print, log, save_modelya que múltiples procesos tienen la misma copia, solo se requiere un proceso para ejecutar, por ejemplo:

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')
  • reduce_tensor(tensor)

Resuma los resultados de cálculo de múltiples procesos, como indicadores de pérdida y evaluación.

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(semilla)

Se utiliza en el proceso de entrenamiento para mejorar la aleatoriedad del entrenamiento.

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 de programa

En if __name__ == '__main__':, use spawn()la función para iniciar DDP, los principales parámetros de esta función incluyen:

  1. fn: Funciones que requieren paralelismo. Aquí está main()la función, que se ejecutará una vez por subproceso;
  2. args: Argumentos requeridos por fn. Nota: Los parámetros pasados ​​a fn deben escribirse en forma de tuplas, incluso si solo hay un parámetro;
  3. nprocs: el número de procesos a iniciar, el valor predeterminado es 1. Aquí se puede establecer en world_size. El valor de nprocs no es coherente con world_size, lo que hará que el proceso espere a que se sincronice y se estanque.
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 función principal()

La función aquí es el primer parámetro pasado en la función main()mencionada anteriormente . spawn()Las partes clave del código se modifican de la siguiente manera:

  • Actualización de la lista de parámetros: agregue parámetros adicionales local_rank, que no es necesario mp.spawn()pasar en la función, y el sistema los asignará automáticamente;

  • Inicialización del proceso: init_ddp()implementación de la función de llamada;

  • Sincronización de capa BN: Llame a convert_sync_batchnorm()la función para completar BN de forma síncrona para simular un escenario de una sola tarjeta tanto como sea posible. Aunque reducirá la utilización de GPU, puede mejorar el rendimiento del modelo en escenarios de varias tarjetas (ver este blog para más detalles); sincronización de la capa BN La necesidad depende del tamaño del lote_tamaño de una sola tarjeta. Si el lote_tamaño de una sola tarjeta es demasiado pequeño, el uso de SyncBN puede mejorar el rendimiento. Sin embargo, si el tamaño del lote es grande, no es necesario utilizar SyncBN, ya que esto requiere la comunicación entre varias tarjetas, lo que ralentizará la velocidad de entrenamiento.

  • Paralelismo de datos: DistributedDataParallel()implementación de la función de llamada;

  • Especifique el entrenamiento de precisión mixta: llame GradScaler()a la implementación de la función y pásela a train()la función como un parámetro;

  • Configuración del muestreador de entrenamiento: cada época establece un orden de muestreo diferente;

  • Evite la ejecución repetida de copias: use if local_rank==0:la declaración para restringir;

  • Eliminar grupos de procesos: destroy_process_group()implementación de funciones de llamadas.

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 相对
  • Además de las modificaciones anteriores, main()las tres funciones utilizadas en la función, get_dataloader()función, train()función y validate()función, también deben actualizarse en consecuencia, lo que se explicará por separado a continuación.

1.5 función get_dataloader()

Esta función DataLoader()modifica principalmente la función. Para las dos fases de "entrenamiento" y "prueba", defina train_sampler y test_sampler respectivamente, donde train_sampler se establece en muestreo aleatorio y test_sampler es muestreo secuencial. Además, en la fase de "entrenamiento", use get_ddp_generator()la función DataLoader()para pasar parámetros a la función generator(actuando sobre diferentes trabajadores), de lo contrario, se debilitará la aleatoriedad del entrenamiento.

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 función tren()

Esta función utiliza principalmente reduce_tensor()la función para promediar la pérdida y modifica la forma de retropropagación: escala el gradiente a través del escalador para evitar que la pérdida se desborde debido al uso de precisión mixta y actualiza el estado del propio escalador. Múltiples procesos paralelos comparten el mismo escalador. Durante el proceso de guardado del modelo, si necesita continuar con el entrenamiento (como el modo de ajuste fino previo al entrenamiento), es mejor guardar el estado del escalador junto y cargarlo junto con los parámetros del modelo en el subsiguiente -proceso de sintonización.

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 función 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 función de prueba()

Como se puede ver en la Sección 1.3, mi procedimiento aquí es separar los procesos de "entrenamiento y verificación" y "prueba", guardar el modelo en la etapa anterior y verificar el modelo en la última etapa. Permítanme presentarles test()el contenido que debe modificarse en la función por separado. Esta parte implica la carga del modelo de punto de control. El método de inferencia acelerada se detalla en este 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()  # 消除进程组

Nota⚠️Durante la fase de prueba, el programa también debe ejecutarse en paralelo; de lo contrario, se informará un error (tome el guardado completo como ejemplo):

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)

En este punto, ¡el programa está completamente modificado!


2. Programa en ejecución

A continuación, se presentan varios métodos de inicio de tarjetas múltiples de DDP.

2.1 mp.spawn() comienza

El método de inicio utilizado por este programa es la función mp.spawn(), donde el módulo mp completa el empaquetado de la biblioteca de multiprocesamiento y no apunta específicamente a DDP.

Al principio, usé dos tarjetas gráficas 2080 Ti para ejecutar el programa en paralelo, sin embargo, descubrí que poco después de iniciar la Epoch 0, siempre se informaba un error, de la siguiente manera RuntimeError: CUDA out of memory.:

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

Después de estar desconcertado, traté de ejecutar el programa en el 3090 tal como está, ¡y descubrí que puede ejecutarse normalmente! ¡La lección aquí es que la memoria de tarjeta única de GPU también es importante para el paralelismo ! Al ajustar un modelo de alrededor de 1.2G, se necesitan alrededor de 40G de variables intermedias para retropropagación... Esta es una situación que realmente no esperaba...


2.2 inicio de sesión

En comparación con el uso de mp.spawn() para comenzar, torchrun controlará automáticamente la configuración de algunas variables de entorno, por lo que es más conveniente. Solo necesitamos configurar os.environ['CUDA_VISIBLE_DEVICES'] (los valores predeterminados no establecidos son todas las GPU en esta máquina), sin configurar os.environ['MASTER_ADDR'], etc. Además, la función main() ya no requiere un parámetro local_rank. La entrada del programa se convierte en:

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')

El comando para ejecutar el script se cambia de python a torchrun, de la siguiente manera:

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

Después de que el programa pueda ejecutarse con éxito, todavía quedan algunos detalles, que se resolverán uno por uno a continuación.

2.3 inicio de torch.distributed.launch()

De esta manera, la cantidad de código es menor y la velocidad de inicio es más rápida.

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

PD: Este método va a 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. Proceso de depuración

Los problemas encontrados durante el uso de DDP se analizan y las soluciones se dan a continuación.

Problema 1: recopilación de datos de computación multiproceso

Dado que estoy copiando el modelo en tarjetas duales para lograr el paralelismo de datos, al resumir los resultados, es necesario resumir los datos en diferentes procesos y calcular el valor medio. En este momento, se debe utilizar la función de recopilación mencionada en la Sección 1.2 all_reduce().

Tenga en cuenta aquí ⚠️: para datos que no son de tensor como float , si queremos calcular el valor promedio de múltiples procesos, primero podemos usar torch.tensor() para convertir las variables que deben resumirse en tensor y usar el comando .cuda()para colóquelos en la gpu y luego llame all_reduce()a la función de colección. validate()Para obtener más información, consulte la recopilación y el cálculo de variables en la función en la Sección 1.7 macro. Si la conversión de datos no se completa, se informará un error de la siguiente manera:

Problema derivado: Al realizar backpropagation, los datos de entrenamiento utilizados por cada proceso son diferentes, por lo que aún es necesario actualizar según la pérdida actualmente calculada por él mismo, en lugar de actualizar según el valor de pérdida obtenido por la función de recopilación, de lo contrario, un error se informará y el Ilógico.


Problema 2: faltan los parámetros de carga del modelo

En la función main() en la sección 1.4, el modelo se almacena en la forma de "solo guardar los parámetros del modelo". Durante la fase de prueba, al cargar el modelo de la forma correspondiente, el error es el siguiente:

(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 acuerdo con este blog , el método de procesamiento temporal aquí es: modificar load_state_dict()la función a:, model.load_state_dict(model_state_dict, strict=False)es decir, establecer strictel valor del parámetro en False.significa strict=False: no es estrictamente necesario state_dictque la entrada de clave coincida con la clave devuelta por la clave de este módulo .

El método de procesamiento mencionado anteriormente puede ignorar temporalmente el problema de parámetros faltantes mencionado anteriormente, pero puede tener un cierto grado de impacto en el rendimiento del modelo, y este problema debe resolverse en el futuro.

PD: dos métodos para guardar y cargar modelos

De acuerdo con este blog , hay dos formas de guardar el modelo. Una es guardar toda la información del modelo, y la otra es guardar solo los parámetros del modelo. Los métodos de carga del modelo correspondientes a los dos métodos de guardado son naturalmente diferente.

  • Guarda toda la información del modelo.
# 保存模型
checkpoint = {
    
    'model': model,\
              'scaler': scaler
                }
torch.save(checkpoint, save_path)

# 加载模型
checkpoint = torch.load(save_path)
model = checkpoint['model']  # 加载模型
  • Solo guarde los parámetros del modelo
    A diferencia del primer método, al cargar el modelo en este método, primero debe definir la misma estructura del modelo que el modelo guardado y luego cargar los parámetros del 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) #加载模型参数。

Pregunta 3: excepción de conversión de tipo de parámetro

En la función main() en la sección 1.4, el modelo se almacena en la forma de "solo guardar los parámetros del modelo". Durante la fase de prueba, al cargar el modelo de la forma correspondiente, el error es el siguiente:

Después de usar el método anterior para resolver el problema de los parámetros faltantes del modelo cargado, los problemas resultantes son los siguientes.

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

Las razones profundas necesitan ser exploradas más a fondo.


Problema 4: fuga de parámetros

Registro de errores: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 '

inserte la descripción de la imagen aquí
Como se puede ver en la figura anterior, el motivo de la advertencia anterior es el uso del Ctrl+Cprograma de interrupción. Las razones profundas necesitan ser exploradas más a fondo.

Nota ⚠️: cuando se usa PyTorch para configurar subprocesos múltiples para la lectura de datos, la operación en segundo plano real es abrir N subprocesos con números de PID consecutivos para simular el trabajo de subprocesos múltiples, por lo que si el programa termina de ejecutarse o elimina el proceso principal en en el medio, la memoria GPU de los subprocesos no se liberará, debe eliminar manualmente uno por uno.

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

Posdata: Este blog es un resumen de mi exploración continua. Si hay expresiones inapropiadas o significados poco claros, espero que me den su consejo y ¡progresemos juntos!


referencias

  1. Práctica de código DDP que se puede entender de un vistazo - Zhihu (zhihu.com)
  2. Guía de uso conciso de Pytorch DistributedDataParallel - Zhihu (zhihu.com)
  3. PyTorch DistributedDataParallel registro de foso escalonado de entrenamiento de varias tarjetas en una sola máquina
  4. Dos formas de guardar el modelo en pytorch_SCU-JJkinging's Blog-CSDN Blog
  5. Aparece el modelo de carga RuntimeError: error(es) al cargar state_dict para el modelo: faltan claves en state_dict_sovits al cargar el modelo de difusión superficial error_大海Git的博客-CSDN博客
  6. La diferencia entre model.to(device) y map_location=device en pytorch - Se busca programador
  7. RuntimeError: CUDA sin memoria Algunos viajes de depuración de errores - Zhihu (zhihu.com)
  8. ¿Qué hoyos/errores ha encontrado en la computación distribuida de pytorch? -Zhihu (zhihu.com)
  9. Sobre el uso de la función de muestra distribuida en pytorch - Se busca programador
  10. Resuelva el problema de CUDA: Falta de memoria causado por la fragmentación de la memoria de video de Pytorch configurando max_split_size_mb en PYTORCH_CUDA_ALLOC_CONF_梦音Yune's Blog-CSDN Blog
  11. Ejemplo de uso de torch.cuda.amp.autocast()
  12. El error de PyTorch set_seed que el 99% de las personas puede cometer destruirá la aleatoriedad, y el trabajador_init_fn oficial no puede resolverlo. Saber (zhihu.com)
  13. La tercera parte de la serie PyTorch DDP original en profundidad: combate y habilidades reales - Zhihu (zhihu.com)
  14. Blog de torch.distributed_Wanderer001 - Blog de CSDN

La siguiente serie de recursos son todos de este blogger , que se puede decir que es un tutorial muy detallado sobre el paralelismo de datos.

  1. Pytorch (11) - paralelismo de entrenamiento distribuido (multi-GPU/multi-tarjeta) (DP y DDP)_pytorch gpu distribuido_blog de hxxjxw-CSDN
  2. El concepto básico de PyTorch multitarjeta/multi-GPU/DPP distribuido (nodo y rango y rango local y nnodos y rango de nodo y nproc_por_nodo y tamaño mundial): se busca programador
  3. torch.distributed multi-card/multi-GPU/distributed DPP (1) - blog de torch.distributed.launch & all_gather & init_process_group_hxxjxw - blog CSDN
  4. torch.distributed multi-card/multi-GPU/distributed DPP (2)—torch.distributed.all_reduce(reduce_mean)orden de ejecución del proceso de control de barrera &seed random seed_torch dpp_hxxjxw's blog-CSDN blog
  5. Entrenamiento distribuido de Pytorch / entrenamiento de múltiples tarjetas DDP - inicialización del modelo (diferencia entre torch.distribute y DDP) - pytorch distribuir torchtrun-CSDN blog
  6. BN (BatchNorm) en Doka training_Doka batchnorm_hxxjxw's Blog-CSDN Blog
  7. ¿Por qué el entrenamiento de tarjetas múltiples de Pytorch hace que la memoria GPU no se libere fácilmente? Se busca programador
  8. Entrenamiento distribuido Pytorch/entrenamiento multitarjeta (1) - Data Parallel paralelo (DP)_model = nn.dataparallel(model, device_ids=[0, 1])_hxxjxw's blog-CSDN blog
  9. Entrenamiento distribuido de Pytorch / entrenamiento de tarjetas múltiples (2) - Paralelo paralelo de datos (DDP) (2.1) (concepto básico y marco de código)_slurm_procid_hxxjxw's blog-CSDN blog
  10. Entrenamiento distribuido de Pytorch/entrenamiento de tarjetas múltiples (2) - Paralelo paralelo de datos (DDP) (2.2) (ejemplo de código) (sincronización de BN y almacenamiento de tarjeta principal y acumulación de gradientes e inferencia de prueba de tarjetas múltiples y semilla aleatoria) _ddp program seed_hxxjxw Blog -Blog CSDN
  11. Entrenamiento distribuido de Pytorch/entrenamiento de tarjetas múltiples (2) - Paralelo paralelo de datos (DDP) (2.3) (torch.multiprocessing (spawn) & Apex)_torch.multiprocessing.spawn_hxxjxw's blog-CSDN blog
  12. Entrenamiento distribuido de Pytorch / entrenamiento de tarjetas múltiples (3) - Model Parallel model blog de parallel_hxxjxw-CSDN blog

Supongo que te gusta

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