概述数据并行是最常用的并行训练方式,用于加速模型训练和处理大规模数据集。在数据并行模式下,训练数据被划分成多份,然后将每份数据分配到不同的计算节点上,例如多卡或者多台设备。每个节点独立地处理自己的数据子集,并使用相同的模型进行前向传播和反向传播,最终对所有节点的梯度进行同步后,进行模型参数更新。相关接口:mindspore.set_auto_parallel_context(parallel_mode=ParallelMode.DATA_PARALLEL):设置数据并行模式。mindspore.nn.DistributedGradReducer():进行多卡梯度聚合。原有代码不需要做太多的改动就可以支持数据并行。
环境依赖每次开始进行并行训练前,通过调用mindspore.communication.init接口初始化通信资源,并自动创建全局通信组WORLD_COMM_GROUP。通信组能让通信算子在卡间和机器间进行信息收发,全局通信组是最大的一个通信组,包括了当前训练的所有设备。通过调用mindspore.set_auto_parallel_context(parallel_mode=ParallelMode.DATA_PARALLEL)设置当前模式为数据并行模式。数据分发(Data distribution)数据并行的核心在于将数据集在样本维度拆分并下发到不同的卡上。在mindspore.dataset模块提供的所有数据集加载接口中都有num_shards和shard_id两个参数,它们用于将数据集拆分为多份并循环采样的方式,采集batch大小的数据到各自的卡上,当出现数据量不足的情况时将会从头开始采样。网络构图数据并行网络的书写方式与单卡网络没有差别,这是因为在正反向传播(Forward propagation & Backward propagation)过程中各卡的模型间是独立执行的,只是保持了相同的网络结构。唯一需要特别注意的是为了保证各卡间训练同步,相应的网络参数初始化值应当是一致的,在DATA_PARALLEL模式下可以通过mindspore.set_seed接口来设置seed或通过使能mindspore.set_auto_parallel_context中的parameter_broadcast达到多卡间权重初始化一致的目的。梯度聚合(Gradient aggregation)数据并行理论上应该实现和单卡一致的训练效果,为了保证计算逻辑的一致性,通过调用mindspore.nn.DistributedGradReducer()接口,在梯度计算完成后自动插入AllReduce算子实现各卡间的梯度聚合操作。DistributedGradReducer()接口中提供了mean开关,用户可以选择是否要对求和后的梯度值进行求平均操作,也可以将其视为超参项。参数更新(Parameter update)因为引入了梯度聚合操作,所以各卡的模型会以相同的梯度值一起进入参数更新步骤。教程中以Ascend或者GPU单机8卡为例,由于没有相关环境,只能在CPU环境下执行。cpu下如何实现数据并行呢?目前GPU、Ascend和CPU分别支持多种启动方式。主要有动态组网、mpirun和rank table三种方式:动态组网:此方式不依赖第三方库,具有容灾恢复功能,安全性好,支持三种硬件平台,建议用户优先使用此种启动方式。mpirun:此方式依赖开源库OpenMPI,启动命令简单,多机需要保证两两之间免密登录,推荐有OpenMPI使用经验的用户使用此种启动方式。rank table:此方式需要在Ascend硬件平台使用,不依赖第三方库。手动配置rank_table文件后,就可以通过脚本启动并行程序,多机脚本一致,方便批量部署。
因此可以采用device_target="CPU"动态组网。动态组网相关代码地址:MindSpore/docs和数据并行(https://gitee.com/mindspore/docs/tree/master/docs/sample_code...)比较除了网络稍有差异外,其他都是一样的。配置分布式环境通过context接口可以指定运行模式、运行设备、运行卡号等,与单卡脚本不同,并行脚本还需指定并行模式parallel_mode为数据并行模式,并通过init根据不同的设备需求初始化HCCL、 NCCL或者MCCL 通信。在数据并行模式还可以设置gradients_mean指定梯度聚合方式。此处不设置device_target会自动指定为MindSpore包对应的后端硬件设备。这里强制设定 device_target="CPU", 否则在单GPU的环境下还是会以GPU去运行。import os
import mindspore as ms
import mindspore.dataset as ds
from mindspore import nn
from mindspore.communication import init, get_rank, get_group_size
ms.set_context(device_target="CPU", mode=ms.GRAPH_MODE)
ms.set_auto_parallel_context(parallel_mode=ms.ParallelMode.DATA_PARALLEL, gradients_mean=True)
init()
ms.set_seed(1)其中,gradients_mean=True是为了在反向计算时,框架内部会将数据并行参数分散在多台机器的梯度值进行聚合,得到全局梯度值后再传入优化器中更新。首先通过AllReduce(op=ReduceOp.SUM)对梯度做规约求和,接着根据gradients_mean的值来判断是否求均值(设置为True则求均值,否则不求,默认为False)。数据并行模式加载数据集数据并行模式跟其他模式最大区别在于数据加载方式的不同,数据是以并行的方式导入的。下面我们以MNIST数据集为例,介绍以数据并行方式导入MNIST数据集的方法,dataset_path是指数据集的路径。def create_dataset(batch_size):
"""create dataset"""
dataset_path = os.getenv("DATA_PATH")
rank_id = get_rank()
rank_size = get_group_size()
dataset = ds.MnistDataset(dataset_path, num_shards=rank_size, shard_id=rank_id)
image_transforms = [
ds.vision.Rescale(1.0 / 255.0, 0),
ds.vision.Normalize(mean=(0.1307,), std=(0.3081,)),
ds.vision.HWC2CHW()
]
label_transform = ds.transforms.TypeCast(ms.int32)
dataset = dataset.map(image_transforms, 'image')
dataset = dataset.map(label_transform, 'label')
dataset = dataset.batch(batch_size)
return dataset其中,与单卡不同的是,在数据集接口需要传入num_shards和shard_id参数,分别对应卡的数量和逻辑序号,建议通过mindspore.communication接口获取:get_rank:获取当前设备在集群中的ID。get_group_size:获取集群数量。定义网络数据并行模式下,网络定义方式与单卡网络写法一致,网络的主要结构如下:class Network(nn.Cell):
"""Network"""
def __init__(self):
super().__init__()
self.flatten = nn.Flatten()
self.fc = nn.Dense(28*28, 10, weight_init="normal", bias_init="zeros")
self.relu = nn.ReLU()
def construct(self, x):
x = self.flatten(x)
logits = self.relu(self.fc(x))
return logits
net = Network()训练网络在这一步,我们需要定义损失函数、优化器以及训练过程。与单卡模型不同的地方在于,数据并行模式还需要增加mindspore.nn.DistributedGradReducer()接口,来对所有卡的梯度进行聚合,该接口第一个参数为需要更新的网络参数:data_set = create_dataset(32)
loss_fn = nn.CrossEntropyLoss()
optimizer = nn.SGD(net.trainable_params(), 1e-2)
def forward_fn(inputs, target):
logits = net(inputs)
loss = loss_fn(logits, target)
return loss, logits
grad_fn = ms.value_and_grad(forward_fn, None, net.trainable_params(), has_aux=True)
grad_reducer = nn.DistributedGradReducer(optimizer.parameters)
for epoch in range(10):
i = 0
for data, label in data_set:
(loss_value, _), grads = grad_fn(data, label)
grads = grad_reducer(grads)
optimizer(grads)
if i % 10 == 0:
print("epoch: %s, step: %s, loss is %s" % (epoch, i, loss_value))
i += 1执行如下指令,即可启动单机8卡训练网络:bash run_dynamic_cluster.sh脚本会在后台运行,日志文件会保存到device目录下,结果保存在worker_*.log中,Loss结果如下:
训练结束。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。