资料原文

一、概述思路

假设一台机器上有个GPU。给定需要训练的模型,每个GPU将分别独立维护一份完整的模型参数。

在模型训练的任意一次迭代中,给定一个小批量,我们将该批量中的样本划分成份并分给每个GPU一份。

然后,每个GPU将分别根据自己分到的训练数据样本和自己维护的模型参数计算模型参数的梯度。

接下来,我们把k个GPU上分别计算得到的梯度相加,从而得到当前的小批量梯度。

之后,每个GPU都使用这个小批量梯度分别更新自己维护的那一份完整的模型参数。

二、网络以及辅助函数

使用“卷积神经网络——从零开始”里的LeNet来作为本节的样例:

# 初始化模型参数。
scale = 0.01
W1 = nd.random.normal(scale=scale, shape=(20, 1, 3, 3))
b1 = nd.zeros(shape=20)
W2 = nd.random.normal(scale=scale, shape=(50, 20, 5, 5))
b2 = nd.zeros(shape=50)
W3 = nd.random.normal(scale=scale, shape=(800, 128))
b3 = nd.zeros(shape=128)
W4 = nd.random.normal(scale=scale, shape=(128, 10))
b4 = nd.zeros(shape=10)
params = [W1, b1, W2, b2, W3, b3, W4, b4] # 定义模型。
def lenet(X, params):
h1_conv = nd.Convolution(data=X, weight=params[0], bias=params[1],
kernel=(3, 3), num_filter=20)
h1_activation = nd.relu(h1_conv)
h1 = nd.Pooling(data=h1_activation, pool_type="avg", kernel=(2, 2),
stride=(2, 2))
h2_conv = nd.Convolution(data=h1, weight=params[2], bias=params[3],
kernel=(5, 5), num_filter=50)
h2_activation = nd.relu(h2_conv)
h2 = nd.Pooling(data=h2_activation, pool_type="avg", kernel=(2, 2),
stride=(2, 2))
h2 = nd.flatten(h2)
h3_linear = nd.dot(h2, params[4]) + params[5]
h3 = nd.relu(h3_linear)
y_hat = nd.dot(h3, params[6]) + params[7]
return y_hat # 交叉熵损失函数。
loss = gloss.SoftmaxCrossEntropyLoss()

参数列表复制到指定设备

下面函数将模型参数[参数一,参数二,……]复制到某个特定GPU,并标记梯度求解:

def get_params(params, ctx):
new_params = [p.copyto(ctx) for p in params]
for p in new_params:
p.attach_grad()
return new_params

同一参数设备间同步

以下函数可以把各个GPU上的同一参数数据加起来,然后再广播到所有GPU上:

def allreduce(data):  # 输入为list,包含位于不同设备上的同一参数
for i in range(1, len(data)):
data[0][:] += data[i].copyto(data[0].context) # 将i位复制到0位设备上,并加给0位
for i in range(1, len(data)):
data[0].copyto(data[i]) # 使用累计后的0位替换i位

数据划分到设备

给定一个批量的数据样本,以下函数可以划分它们并复制到各个GPU上:

def split_and_load(data, ctx):
n, k = data.shape[0], len(ctx)
m = n // k
assert m * k == n, '# examples is not divided by # devices.'
return [data[i * m: (i + 1) * m].as_in_context(ctx[i]) for i in range(k)]

三、训练过程

将完整的模型参数复制到多个GPU上,并在每次迭代时对单个小批量上进行多GPU训练:

def train(num_gpus, batch_size, lr):
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
ctx = [mx.gpu(i) for i in range(num_gpus)] # 设备代号list
print('running on:', ctx)
# 将模型参数复制到 num_gpus 个 GPU 上。
gpu_params = [get_params(params, c) for c in ctx] # 每个元素为一个设备上的参数
for epoch in range(1, 6):
start = time()
for X, y in train_iter:
# 对单个小批量上进行多 GPU 训练。
train_batch(X, y, gpu_params, ctx, lr)
nd.waitall()
print('epoch %d, time: %.1f sec' % (epoch, time() - start))
# 在 GPU0 上验证模型。
net = lambda x: lenet(x, gpu_params[0])
test_acc = gb.evaluate_accuracy(test_iter, net, ctx[0])
print('validation accuracy: %.4f' % test_acc)

实现单个小批量上的多GPU训练:

def train_batch(X, y, gpu_params, ctx, lr):
# 划分小批量数据样本并复制到各个 GPU 上。
gpu_Xs = split_and_load(X, ctx)
gpu_ys = split_and_load(y, ctx)
# 在各个 GPU 上计算损失。
with autograd.record():
ls = [loss(lenet(gpu_X, gpu_W), gpu_y) # 不同设备上的loss对象
for gpu_X, gpu_y, gpu_W in zip(gpu_Xs, gpu_ys, gpu_params)]
# 在各个 GPU 上反向传播。
for l in ls:
l.backward()
# 把各个 GPU 上的梯度加起来,然后再广播到所有 GPU 上。
for i in range(len(gpu_params[0])): # gpu_params[0]:位于设备0上的全部参数
allreduce([gpu_params[c][i].grad for c in range(len(ctx))]) # 汇总梯度并广播
# 在各个 GPU 上更新自己维护的那一份完整的模型参数。
for param in gpu_params: # 各个设备分别更新
gb.sgd(param, lr, X.shape[0])

四、Gluon实现

模型参数初始化于各个设备

前我们介绍了如何使用initialize函数的ctx参数在CPU或单个GPU上初始化模型参数。事实上,ctx可以接受一系列的CPU/GPU,从而使初始化好的模型参数复制到ctx里所有的CPU/GPU上。

ctx = [mx.gpu(0), mx.gpu(1)]
net.initialize(init=init.Normal(sigma=0.01), ctx=ctx)

此时的net对象对应一系列相同结构不同设备的实体。

数据分发到各个设备

Gluon提供了上一节中实现的split_and_load函数。它可以划分一个小批量的数据样本并复制到各个CPU/GPU上。之后,根据输入数据所在的CPU/GPU,模型计算会发生在相同的CPU/GPU上。

x = nd.random.uniform(shape=(4, 1, 28, 28))
gpu_x = gutils.split_and_load(x, ctx)
net(gpu_x[0]), net(gpu_x[1])

默认下weight.data()会返回CPU上的参数值。由于我们指定了2个GPU来初始化模型参数,我们需要指定GPU访问。我们看到,相同参数在不同的GPU上的值一样。

weight = net[1].params.get('weight')
try:
weight.data()
except:
print('not initialized on', mx.cpu())
weight.data(ctx[0])[0], weight.data(ctx[1])[0]
not initialized on cpu(0)
(
[[[-0.01473444 -0.01073093 -0.01042483]
[-0.01327885 -0.01474966 -0.00524142]
[ 0.01266256 0.00895064 -0.00601594]]]
<NDArray 1x3x3 @gpu(0)>,
[[[-0.01473444 -0.01073093 -0.01042483]
[-0.01327885 -0.01474966 -0.00524142]
[ 0.01266256 0.00895064 -0.00601594]]]
<NDArray 1x3x3 @gpu(1)>)

参数同步

当我们使用多个GPU来训练模型时,gluon.Trainer会自动做数据并行,例如划分小批量数据样本并复制到各个GPU上,对各个GPU上的梯度求和再广播到所有GPU上。这样,我们就可以很方便地实现训练函数了。

另外net.collect_params().reset_ctx()可以重置设备

loss = gloss.SoftmaxCrossEntropyLoss()

def train(num_gpus, batch_size, lr):
train_iter, test_iter = gb.load_data_fashion_mnist(batch_size)
ctx = [mx.gpu(i) for i in range(num_gpus)]
print('running on:', ctx)
net.initialize(init=init.Normal(sigma=0.01), ctx=ctx, force_reinit=True) # 网络初始化于各个设备
trainer = gluon.Trainer(
net.collect_params(), 'sgd', {'learning_rate': lr}) # 优化器会自动识别net对象的设备列表
for epoch in range(1, 6):
start = time()
for X, y in train_iter:
gpu_Xs = gutils.split_and_load(X, ctx) # 数据划分到各个设备
gpu_ys = gutils.split_and_load(y, ctx)
with autograd.record():
ls = [loss(net(gpu_X), gpu_y) for gpu_X, gpu_y in zip(
gpu_Xs, gpu_ys)] # 记录各个设备的损失函数
for l in ls:
l.backward() # 各个设备分别反向传播
trainer.step(batch_size) # 优化时自动同步各个设备参数
nd.waitall()
print('epoch %d, training time: %.1f sec'%(epoch, time() - start))
test_acc = gb.evaluate_accuracy(test_iter, net, ctx[0])
print('validation accuracy: %.4f'%(test_acc)) train(num_gpus=2, batch_size=512, lr=0.3)

『MXNet』第七弹_多GPU并行程序设计的更多相关文章

  1. 『TensorFlow』第七弹_保存&载入会话_霸王回马

    首更: 由于TensorFlow的奇怪形式,所以载入保存的是sess,把会话中当前激活的变量保存下来,所以必须保证(其他网络也要求这个)保存网络和载入网络的结构一致,且变量名称必须一致,这是caffe ...

  2. 『MXNet』第十弹_物体检测SSD

    全流程地址 一.辅助API介绍 mxnet.image.ImageDetIter 图像检测迭代器, from mxnet import image from mxnet import nd data_ ...

  3. 『MXNet』第十一弹_符号式编程初探

    一.符号分类 符号对我们想要进行的计算进行了描述, 下图展示了符号如何对计算进行描述. 我们定义了符号变量A, 符号变量B, 生成了符号变量C, 其中, A, B为参数节点, C为内部节点! mxne ...

  4. 『MXNet』第八弹_数据处理API_下_Image IO专题

    想学习MXNet的同学建议看一看这位博主的博客,受益良多. 在本节中,我们将学习如何在MXNet中预处理和加载图像数据. 在MXNet中加载图像数据有4种方式. 使用 mx.image.imdecod ...

  5. 『MXNet』第八弹_数据处理API_上

    一.Gluon数据加载 下面的两个dataset处理类一般会成对出现,两个都可做预处理,但是由于后面还可能用到原始图片,.ImageFolderDataset不加预处理的话可以满足,所以建议在.Dat ...

  6. 『MXNet』第四弹_Gluon自定义层

    一.不含参数层 通过继承Block自定义了一个将输入减掉均值的层:CenteredLayer类,并将层的计算放在forward函数里, from mxnet import nd, gluon from ...

  7. 『PyTorch』第四弹_通过LeNet初识pytorch神经网络_下

    『PyTorch』第四弹_通过LeNet初识pytorch神经网络_上 # Author : Hellcat # Time : 2018/2/11 import torch as t import t ...

  8. 『PyTorch』第十弹_循环神经网络

    RNN基础: 『cs231n』作业3问题1选讲_通过代码理解RNN&图像标注训练 TensorFlow RNN: 『TensotFlow』基础RNN网络分类问题 『TensotFlow』基础R ...

  9. 『TensorFlow』第十一弹_队列&多线程&TFRecod文件_我辈当高歌

    TF数据读取队列机制详解 一.TFR文件多线程队列读写操作 TFRecod文件写入操作 import tensorflow as tf def _int64_feature(value): # val ...

随机推荐

  1. 【示例】Spring Quartz入门

    JAVA 针对定时任务,有 Timer,Scheduler, Quartz 等几种实现方式,其中最常用的应该就是 Quartz 了. 一. Quartz的基本概念 在开始之前,我们必须了解以下的几个基 ...

  2. 到底什么是 ROI Pooling Layer ???

    到底什么是 ROI Pooling Layer ??? 只知道 faster rcnn 中有 ROI pooling, 而且其他很多算法也都有用这个layer 来做一些事情,如:SINT,检测的文章等 ...

  3. [关于前端数据] - serialize()的使用

    虽然是拼接字符串,但是也能通过post方式提交 $("#signUpForm").serialize() 结果 controller照样使用实例接收数据即可

  4. TIM定时器的应用

    TIM定时器的应用   ①输入捕获的应用: 上一节,我已阐述TIM的输入捕获具体作用有两个(如下图):     对输入信号的测量:                                    ...

  5. CIFAR-10与ImageNet图像识别

    2.1.2 下载CIFAR-10 数据 python cifar10_download.py # Copyright 2015 The TensorFlow Authors. All Rights R ...

  6. POJ 1426 Find The Multiple(寻找倍数)

    POJ 1426 Find The Multiple(寻找倍数) Time Limit: 1000MS    Memory Limit: 65536K Description - 题目描述 Given ...

  7. Leetcode88_Merge Sorted Array_Easy

    Given two sorted integer arrays nums1 and nums2, merge nums2 into nums1 as one sorted array. Note: T ...

  8. open()、fwrite()、fread()函数使用说明与示例

    fopen()函数: 1.作用: 在C语言中fopen()函数用于打开指定路径的文件,获取指向该文件的指针. 2.函数原型: FILE * fopen(const char * path,const ...

  9. GYM 101064 2016 USP Try-outs G. The Declaration of Independence 主席树

    G. The Declaration of Independence time limit per test 1 second memory limit per test 256 megabytes ...

  10. win10 下载安装eclipse

    官网:https://www.eclipse.org 选择下载包 选择下载win 64版本 解压后目录结构如下: 点击运行eclipse