前言
本節關注影響深度學習計算性能的因素
- 命令式編程和符號式編程
- 異步計算
- 多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
結語
瞭解了下提升計算性能的一些方法