前言
本节关注影响深度学习计算性能的因素
- 命令式编程和符号式编程
- 异步计算
- 多GPU计算
1、命令式编程和符号式编程
通常,我们编程都是命令式编程
比如
def add(a, b):
return a + b
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
print(fancy_func(1, 2, 3, 4))
使⽤命令式编程很⽅便,但它的运⾏可能很慢
- 函数重复调用
- 内存长时间占据
而符号式编程通常在计算流程完全定义好后才被执⾏
def add_str():
return '''
def add(a, b):
return a + b
'''
def fancy_func_str():
return '''
def fancy_func(a, b, c, d):
e = add(a, b)
f = add(c, d)
g = add(e, f)
return g
'''
def evoke_str():
return add_str() + fancy_func_str() + '''
print(fancy_func(1, 2, 3, 4))
'''
prog = evoke_str()
print(prog)
y = compile(prog, '', 'exec')
exec(y)
减少了函数调⽤,还节省了内存,但调试难度上升
目前的深度学习框架
- TensorFlow是用符号式编程
- pytorch是用命令式编程
- MXnet则是混用,通过HybridSequential类和HybridBlock类构建的模型可以调⽤hybridize函数将
命令式程序转成符号式程序
2、异步计算
前端线程⽆须等待当前指令从后端线程返回结果就继续执⾏后⾯的指令
先简单做个计时
class Benchmark(): # 本类已保存在d2lzh包中⽅便以后使⽤
def __init__(self, prefix=None):
self.prefix = prefix + ' ' if prefix else ''
def __enter__(self):
self.start = time.time()
def __exit__(self, *args):
print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))
做个对比
- 在for循环内使⽤同步函数wait_to_read时,每次赋值不使⽤异步计算
- 当在for循环外使⽤同步函数waitall时,则使⽤异步计算。
with Benchmark('synchronous.'):
for _ in range(1000):
y = x + 1
y.wait_to_read()
with Benchmark('asynchronous.'):
for _ in range(1000):
y = x + 1
nd.waitall()
结果是
synchronous. time: 0.5182 sec
asynchronous. time: 0.3986 sec
在每⼀次循环中,前端和后端的交互⼤约可以分为3个阶段:
- 前端令后端将计算任务y = x + 1放进队列;
- 后端从队列中获取计算任务并执⾏真正的计算;
- 后端将计算结果返回给前端。
将这3个阶段的耗时分别设为t1、t2、 t3。
如果不使⽤异步计算,执⾏1000次计算的总耗时⼤约为1000(t1 +t2 +t3)
如果使⽤异步计算,由于每次循环中前端都⽆须等待后端返回计算结果,执⾏1000次计算的总耗时可以降为t1 + 1000t2 + t3(假设1000t2 > 999t1)
所以异步计算可以大幅度减少耗时
3、多GPU计算
在模型训练的任意⼀次迭代中,给定⼀个随机小批量
- 将该批量中的样本划分成k份并分给每块显卡的显存⼀份
- 每块GPU将根据相应显存所分到的小批量⼦集和所维护的模型参数分别计算模型参数的本地梯度
- 把k块显卡的显存上的本地梯度相加,便得到当前的小批量随机梯度
- 每块GPU都使⽤这个小批量随机梯度分别更新相应显存所维护的那⼀份完整的模型参数
实现如下
import d2lzh as d2l
import mxnet as mx
from mxnet import autograd, gluon, init, nd
from mxnet.gluon import loss as gloss, nn, utils as gutils
import time
"""实现多GPU"""
# ResNet-18模型
def resnet18(num_classes):
def resnet_block(num_channels, num_residuals, first_block=False):
blk = nn.Sequential()
for i in range(num_residuals):
if i == 0 and not first_block:
blk.add(d2l.Residual(
num_channels, use_1x1conv=True, strides=2))
else:
blk.add(d2l.Residual(num_channels))
return blk
net = nn.Sequential()
# 这里使用了较小的卷积核、步幅和填充,并去掉了最大池化层
net.add(nn.Conv2D(64, kernel_size=3, strides=1, padding=1),
nn.BatchNorm(), nn.Activation('relu'))
net.add(resnet_block(64, 2, first_block=True),
resnet_block(128, 2),
resnet_block(256, 2),
resnet_block(512, 2))
net.add(nn.GlobalAvgPool2D(), nn.Dense(num_classes))
return net
net = resnet18(10)
# 在GPU上初始化
ctx = [mx.gpu(0), mx.gpu(1)]
net.initialize(init=init.Normal(sigma=0.01), ctx=ctx)
# 划分样本数据
x = nd.random.uniform(shape=(4, 1, 28, 28))
gpu_x = gutils.split_and_load(x, ctx)
print(net(gpu_x[0]), net(gpu_x[1]))
# 训练
def train(num_gpus, batch_size, lr):
train_iter, test_iter = d2l.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})
loss = gloss.SoftmaxCrossEntropyLoss()
for epoch in range(4):
start = time.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()
train_time = time.time() - start
test_acc = d2l.evaluate_accuracy(test_iter, net, ctx[0])
print('epoch %d, time %.1f sec, test acc %.2f' % (epoch + 1, train_time, test_acc))
train(num_gpus=1, batch_size=256, lr=0.1) #单GPU
train(num_gpus=2, batch_size=512, lr=0.2) #2个GPU
结语
了解了下提升计算性能的一些方法