Formación distribuida desde el ingreso hasta el abandono

¡Acostúmbrate a escribir juntos! Este es el séptimo día de mi participación en el "Nuevo plan diario de Nuggets · Desafío de actualización de abril", haga clic para ver los detalles del evento .

Cuando entrenamos modelos de redes neuronales, preferimos conjuntos de datos a gran escala y estructuras de red complejas. Aunque puede hacer que nuestro modelo sea más representativo, también plantea desafíos computacionales de tiempo y espacio.

Referirse a

Referirse a

Referirse a

Referirse a

Por qué usar entrenamiento distribuido

Con la expansión continua del volumen del conjunto de datos, el volumen de datos de TB o incluso PB hace que el entrenamiento con una sola tarjeta sea un fantasma. Incluso para conjuntos de datos y modelos ordinarios, mayor velocidad, mayor memoria de video y mayor utilización de la tarjeta gráfica. Y mayor tamaño de lote. trae un mejor rendimiento son también lo que estamos persiguiendo.

Algunos algoritmos de entrenamiento distribuido

El llamado distribuido significa que los nodos informáticos no comparten la memoria y necesitan intercambiar datos a través de la comunicación de la red. A continuación se presentan varios algoritmos de entrenamiento distribuido diferentes.

Chispa MLlib

Principio de computación distribuida de Spark:

En primer lugar, debe comprender el principio de la computación distribuida Spark

avance

Bueno, no lo entiendo muy bien , tal vez es

  • Controlador: crea SparkContext para proporcionar el entorno requerido para la operación del programa y es responsable de comunicarse con el Administrador de clústeres para implementar funciones como la aplicación de recursos, la asignación de tareas y el monitoreo. Cuando se cierra el Ejecutor, el Controlador también cierra el SparkContext;

  • Ejecutor: Un proceso en el Nodo Trabajador , responsable de ejecutar la Tarea;

  • Tarea: La unidad de trabajo que se ejecuta en el Ejecutor;

  • RDD: Resilient Distributed Dataset es un concepto abstracto de memoria distribuida que proporciona un modelo de memoria compartida altamente restringido con las siguientes propiedades:

    1. Solo lectura no se puede modificar, y un nuevo RDD solo se puede generar mediante conversión;
    2. Se puede distribuir en múltiples máquinas para procesamiento paralelo;
    3. Elasticidad: la memoria insuficiente en la informática intercambiará datos con el disco;
    4. Basado en la memoria: se puede almacenar total o parcialmente en la memoria caché y reutilizarse en múltiples cálculos.
  • partition: RDD基础处理单元是partition(分区),一个Work Node中有多个partition,每个partition的计算都在一个不同的task中进行

  • DAG: 有向无环图,反映RDD之间的依赖关系,在执行具体任务之前,Spark会将程序拆解成一个任务DAG;处理DAG最关键的过程是找到哪些是可以并行处理的部分,哪些是必须shuffle和reduce的部分。

  • shuffle: 指的是所有partition的数据必须进行洗牌后才能得到下一步的数据

  • Job: 一个Job包含多个RDD及作用于相应RDD上的各种操作;

  • Stage: 是Job的基本调度单位,一个Job会分为多组Task,每组Task被称为Stage,或者也被称为TaskSet,代表一组关联的,相互之间没有Shuffle依赖关系的任务组成的任务集;

  • Cluter Manager: 指的是在集群上获取资源的外部服务。目前有三种类型

    1. Standalon : spark原生的资源管理,由Master负责资源的分配;
    2. Apache Mesos:与hadoop MR兼容性良好的一种资源调度框架;
    3. Hadoop Yarn: 主要是指Yarn中的ResourceManager。

Spark MLlib并行训练原理:

简单的数据并行,缺点如下:

  1. 采用全局广播的方式,在每轮迭代前广播全部模型参数。
  2. 采用阻断式的梯度下降方式,每轮梯度下降由最慢的节点决定。 Spark等待所有节点计算完梯度之后再进行汇总。
  3. Spark MLlib并不支持复杂网络结构和大量可调超参,对深度学习支持较弱。

Parameter Server

Parameter Server由李沐大佬提出,如今已被各个框架应用于分布式训练当中。论文地址

在分布式训练当中,输入数据、模型、反向传播计算梯度都是可以并行的,但是更新参数依赖于所有的训练样本的梯度,这是不能并行的,如下图所示:

image.png

Parameter Server包含一个参数(server)服务器(或者GPU等)来复制分发model到各个工作(worker)服务器上,计算示意图如下:

image.png

  1. 将数据和初始化参数加载到server当中,若无法一次加载进来也可分多次加载;
  2. server将输入数据进行切片,分发给各个的worker;
  3. server复制模型,传递给各个worker;
  4. 各个worker并行进行计算(forward和backward);
  5. 将各个worker求得的梯度求平均,返回server进行更新(push:worker将计算的梯度传送给server),同时回到第二步,重新分发更新过后的模型参数(pull:worker从server拉取参数)。

通过上图可以看到每个worker之间没有任何信息交换,它们都只与server通信

上述过程貌似和Spark差不多,实际上PS中的server和worker中存在很多节点,它们分别组成server groupworker group,功能与上述基本一致,对于server group,存在server manager来维护和分配各个server node的数据,如下图所示:

缺点

上文提到的Spark使用的是同步阻断的方式进行更新,只有等所有节点的梯度计算完成后才能进行参数更新,会浪费大量时间;

对此,PS使用了异步非阻断的方式进行更新,当某个worker节点完成了push之后,其他节点没有进行push,这表示该节点无法进行pull来拉取新模型,此时该节点会直接再次进行计算,并且将当次计算的梯度在下一次push提交给server。

这种取舍虽然使得训练速度大幅增加,但是却造成模型的一致性有所丧失,具体影响还是有待讨论。

该方法可以通过最大延迟来限制这种异步操作,即某个worker节点最多只能超过server几轮迭代,超过之后必须停下来等待pull。

多server节点的协同和效率问题:

Spark效率低下的另一个原因是每次更新参数之后都需要使用单个master节点将模型参数广播至各个worker节点;

由于Parameter Server使用了多server node结构的server group,每个server node负责模型参数中的K-V对,并行地进行数据传播,大大加快了速度;

使用哈希一致性来保证每个server node负责对应的key range,并且能保证已有key range变化不大的情况下添加新的节点(看不懂)

image-20210923201858076

Ring AllReduce

摒弃了使用server来进行输入传输,而是将各个worker连成环,进行循环传递来达到“混合”的效果,主要流程:

演示

  1. 对于N个worker,将其连成一个环,并且将每个worker中的梯度分成N份;
  2. 对于第k个worker,其会将第k份梯度发送给下一个节点,同时从前一个节点收到第k-1份梯度;
  3. 将收到的k-1份梯度和原有的梯度整合,循环N次,这样每个节点都会包含所有节点梯度的对应的一份
  4. 每个worker再将整合好的梯度发给下一个worker即可,需要注意的是,这里直接使用前一个worker的梯度覆盖当前的梯度,依然循环N次。
  5. 最后每个worker都会得到所有梯度,除以N即可进行参数更新。

更多算法请看

NCCL

NCCL是Nvidia Collective multi-GPU Communication Library的简称,它是一个实现多GPU的collective。

pytorch code

pytorch的分布式训练目前仅支持linux系统。

pytorch数据分布式训练类型主要有:

  1. 单机单卡: 最简单的训练类型;
  2. 单机多卡: 代码改动较少;
  3. 多机多卡: 多台机器上的多张显卡,机制较为复杂;
  4. 其他: 其他的一些情况,不介绍。

DataParallel

单机多卡的情况,代码改动较少,主要基于nn.DataParallel,是一种单进程多线程的训练方式,存在GIL冲突。

torch.nn.DataParallel定义如下:

 class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)
复制代码

包含三个参数:

  • module:表示需要Parallel的模型,实际上最终得到的是一个nn.Module
  • device_ids:表示训练用GPU的编号
  • output_device:表示输出的device,用于汇总梯度和参数更新,默认选择0卡
  • dim:表示处理loss的维度,默认0表示在batch上处理,使用了多少GPU就会返回多少个loss

不想使用0卡可以用如下方式指定训练设备

 os.environ["CUDA_DEVICE_ORDER"] = "PCI_BUS_ID" # 表示按照PCI_BUS_ID顺序从0开始排列GPU设备,不使用指定多卡时会报错
 os.environ["CUDA_VISIBLE_DEVICES"] = "2, 3" # -1表示禁用GPU
复制代码

训练代码为:

 device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
 if torch.cuda.device_count()>1:
     model = nn.DataParallel(model)
 model.to(device)
复制代码

更优雅点可以这样:

 if torch.cuda.device_count() > 1:
     model = nn.DataParallel(model,device_ids=range(torch.cuda.device_count())) # 直接全用
复制代码

也可以使用如下方式指定训练GPU:

 device_ids = [2, 3]
 net = torch.nn.DataParallel(net, device_ids=device_ids)
复制代码

机制很简单,即将模型复制到各个GPU上进行forward和backward,由server卡汇总计算平均梯度,再分发更新后的参数至各个worker。这就导致了一个问题——负载不均衡,server卡的占用会很高,其原因主要是在server上计算loss,可以在每个gpu上计算loss之后返回给server求平均。

在保存和加载模型时

 # 保存模型需要使用.module
 torch.save(net.module.state_dict(), path)
 # 或者
 torch.save(net.module, path)
 # 正常加载模型
 net.load_state_dict(torch.load(path))
 # 或者
 net = torch.load(path)
复制代码

另一个需要注意的点是,使用DataParallel时的batch_size表示总的batch_size,每个GPU上会分到n分之一。

DistributedDataParallel

多机多卡的训练方式,多进程不存在GIL冲突,也适用于单机单卡,并且速度较快。

DistributedDataParallel需要一个init_process_group的步骤来启动

 class torch.distributed.init_process_group(backend, 
                                      init_method=None, 
                                      timeout=datetime.timedelta(0, 1800), 
                                      world_size=-1, 
                                      rank=-1, 
                                      store=None)
复制代码
  • backend:str:gloo,mpi,nccl 。指定当前要使用的通信后端,通常使用 nccl;
  • init_method:指定当前进程组的初始化方式;
  • timeout:指定每个进程的超时时间,仅可用于"gloo"后端;
  • world_size:总进程数;
  • store:所有 worker 可访问的 key / value,用于交换连接 / 地址信息。与 init_method 互斥。

其他函数:

 torch.distributed.get_backend(group=group) # group是可选参数,返回字符串表示的后端 group表示的是ProcessGroup类
 torch.distributed.get_rank(group=group) # group是可选参数,返回int,执行该脚本的进程的rank
 torch.distributed.get_world_size(group=group) # group是可选参数,返回全局的整个的进程数
 torch.distributed.is_initialized() # 判断该进程是否已经初始化
 torch.distributed.is_mpi_avaiable() # 判断MPI是否可用
 torch.distributed.is_nccl_avaiable() # 判断nccl是否可用
复制代码

简单的使用

初始化

 torch.distributed.init_process_group(backend='nccl', init_method='env://')
复制代码

DistributedSampler

Tenga en cuenta que, a diferencia de DataParallel, el tamaño de batch_size representa el tamaño de cada GPU y el cargador de datos debe dividirse.

 train_sampler = torch.utils.data.distributed.DistributedSampler(train_data)
 train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=False,
                                                num_workers=2, pin_memory=True, sampler=train_sampler,)
 ​
复制代码

Esto debe configurarse shuffle=False, y luego, antes de cada época, train_sampler.set_epoch(epoch)el efecto de reproducción aleatoria se logra llamando.

Inicialización del modelo:

 model = torch.nn.parallel.DistributedDataParallel(model, device_ids=[args.local_rank])
复制代码

Sincronizar BN:

La capa BN tiene un mejor rendimiento para un tamaño de lote más grande, por lo que para BN, los datos de todas las tarjetas deben usarse para el cálculo.

Uso de Apex:

 from apex.parallel import convert_syncbn_model
 from apex.parallel import DistributedDataParallel
  
 # 注意顺序:三个顺序不能错
 model = convert_syncbn_model(UNet3d(n_channels=1, n_classes=1)).to(device)
 model, optimizer = amp.initialize(model, optimizer, opt_level='O1')
 model = DistributedDataParallel(model, delay_allreduce=True)
复制代码

o use el código aquí en su lugarnn.BatchNorm

tren:

previsto torch.distributed.launchpara iniciar la formación

 export CUDA_VISIBLE_DEVICES=0,1,2,3 python -m torch.distributed.launch --nproc_per_node=4 train.py
复制代码

El parámetro nproc_per_node especifica la cantidad de procesos creados para el host actual. Generalmente se establece en el GPUnúmero

modelo:

 import torch.distributed as dist
 import torch.utils.data.distributed
 ​
 # ......
 parser = argparse.ArgumentParser(description='PyTorch distributed training on cifar-10')
 parser.add_argument('--rank', default=0,
                     help='rank of current process')
 parser.add_argument('--word_size', default=2,
                     help="word size")
 parser.add_argument('--init_method', default='tcp://127.0.0.1:23456',
                     help="init-method")
 args = parser.parse_args()
 ​
 # ......
 dist.init_process_group(backend='nccl', init_method=args.init_method, rank=args.rank, world_size=args.word_size)
 ​
 # ......
 trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=download, transform=transform)
 train_sampler = torch.utils.data.distributed.DistributedSampler(trainset)
 trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size, sampler=train_sampler)
 ​
 # ......
 net = Net()
 net = net.cuda()
 net = torch.nn.parallel.DistributedDataParallel(net)
复制代码

Supongo que te gusta

Origin juejin.im/post/7083872898703851528
Recomendado
Clasificación