第八章 计算性能
影响计算性能的重要因子:命令式编程、符号式编程、异步计算、自动并行计算和多GPU计算。
一、命令式和符号式混合编程
之前我们用的全是命令式编程,使用编程语句改变程序状态。
import timeit
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
t1 = timeit.Timer("fancy_func(1, 2, 3, 4)", "from __main__ import fancy_func")
print("time cost %.5f seconds \n" % t1.timeit(number=1000))
# time cost 0.00081 seconds
命令式编程很方便,但是运行速度慢:
- 函数add被多次重复调用。python 会逐一执行三条add命令。
- 需要保存变量e和f的值,直到整体执行结束。
符号式编程通常在计算流程完全定义好后才被执行。多个深度学习框架,如Theano和TensorFlow,都使用了符号式编程。
符号式编程的程序需要3个步骤:
1、定义计算流程;
2、把计算机流程编译成可执行的程序;
3、给定输入,调用编译好的程序执行。
下面使用符号式编程重新实现上面的命令式编程代码。
import timeit
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))
'''
def test():
prog = evoke_str()
# print(prog)
y = compile(prog, "", "exec")
exec(y)
t1 = timeit.Timer("test", "from __main__ import test")
print("time cost %.5f seconds \n" % t1.timeit(number=1000))
# time cost 0.00002 seconds
还可以使用如下方式测效率。在timeit中将要执行的语句保存为带三引号的字符串来执行测试。
from timeit import timeit
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
'''
p= '''
def test():
prog = evoke_str()
# print(prog)
y = compile(prog, '', 'exec')
exec(y)
'''
print("time cost %.5f seconds \n" % timeit(stmt=p, number=1000))
# time cost 0.00011 seconds
以上定义的3个函数都仅以字符串的形式返回计算流程。最后我们通过compile函数编译完整的计算流程并运行。
由于在编译时系统能够完整地获取整个程序,因此有更多空间优化计算。例如,编译的时候可以将程序改写成
print((1 + 2) + (3 + 4))
,甚至直接改写成print(10)
。这样不仅减少了函数调用,还节省了内存。
对比这两种编程方式:命令式编程和符号式编程。
- 命令式编程更方便,直观。方便获取并打印中间变量值,或使用调试工具。
- 符号式编程高效,并更容易移植。编译的时候系统容易做更多的优化;符号式编程将程序变成一个与Python无关的格式,从而可以使程序在非Python环境下运行,以避免Python解释器的性能问题。
用户应该⽤纯命令式编程进⾏开发和调试;当需要产品级别的计算性能和部署时,用户可以将⼤部分命令式程序转换成符号式程序来运⾏。
由于PyTorch仅仅采⽤用了了命令式编程,所以跳过本节剩余部分Pytorch。那我们就来看看Gluon提供的混合式编程方式。
使用HybridSequential类构造模型
在混合式编程中,我们可以通过使⽤HybridBlock类或者HybridSequential类构建模型。默认情况下,它们和Block类或者Sequential类⼀样依据命令式编程的方式执行。当我们调⽤hybridize函数后,Gluon会转换成依据符号式编程的方式执行。事实上,绝大多数模型都可以接受这样的混合式编程的执行方式。
之前用Sequential类来串联多个层。为了使用混合式编程,将Sequential类替换成HybridSequential类。
from mxnet import nd,sym
from mxnet.gluon import nn
import time
def get_net():
net = nn.HybridSequential()
net.add(nn.Dense(256, activation="relu"),
nn.Dense(128, activation="relu"),
nn.Dense(2))
net.initialize()
return net
x = nd.random.normal(shape=(1, 512))
net = get_net()
net(x)
然后通过调用hybridize函数来编译和优化HybridSequential实例中串联的层的计算。
net.hybridize()
net(x)
只有继承HybridBlock类的层才会被优化计算。
计算性能对比:
def benchmark(net, x):
start = time.time()
for i in range(1000):
_ = net(x)
nd.waitall()
return time.time() - start
net = get_net()
print("before hybridizing:%.4f sec" % (benchmark(net, x)))
net.hybridize()
print("after hybridizing:%.4f sec" % (benchmark(net, x)))
before hybridizing: 0.4733 sec
after hybridizing: 0.2195 sec
获取符号式程序:
通过export函数将符号式程序和模型参数保存在硬盘上。
net.export("my_mlp")
生成.json文件和.params文件。分别为符号式程序和模型参数。可以被Python和MXNet支持的其他前端语言读取,如C++、R、Scala、Perl和其他语言。这样我们就可以很方便地使用其他前端语言或其他设备上部署训练好的模型。同时由于部署时使用的是符号式编程程序,计算性能往往比命令式程序的性能更好。
使用HybridBlock类构造模型
符号式程序一旦获得,再运行时将不再访问Python代码,而是直接在C++后端执行符号式程序。这就是性能提升的原因,但损失了程序的灵活性。将无法使用打印语句来调试代码。执行符号式程序时会跳过他们无法打印。
二、异步计算
Pytorch资料少,后续有我再更新。当前且看MXNet。
MXNet使用异步来提升计算性能,有助于在内存资源有限的情况下主动降低计算性能从而减小内存开销。
MXNet支持的前端语言Python、R、Scala和C++。无论前端使用何种语言,后端主要发生在C++实现的后端。用户写好的前端MXNet程序会传给后端执行计算。后端有自己的线程在队列中不断收集任务并执行它们。
MXNet前端线程和后端线程的交互实现异步计算。
异步计算指:前端线程无须等待当前指令从后端线程返回结果就继续执行后面的指令。不等你计算完,我就继续给你发计算任务。后端用C++高效实现计算。不管前端编程语言性能如何。
除非我们需要打印或者保存计算结果,否则我们基本无须关心目前结果在内存中是否已经计算好了。
所以可以使用print函数让后端把结果传过来,也可以使用wait_to_read函数让前端等待后端结果,再执行后续语句。用waitall函数另前端等待前面所有计算结果完成。
所以要想好,到底哪个地方等,而且是干等,啥也不干的干等。哪个地方等的时候,可以去干点别的。这就是异步的本质。
训练模型时对每个小批量使用同步函数,在使用模型预测的时候也是对每个小批量都使用同步函数。因为可以节省内存开支。
建议使⽤每个小批量训练或预测时⾄少使⽤⼀个同步函数,从而避免在短时间内将过多计算任务丢给后端,占用内存资源。
三、自动并行计算
Pytorch和MXNet后端会自动构建计算图。通过计算图,系统可以知道所有计算的依赖关系,并可以选择将没有依赖关系的多个任务并行执行来获得计算性能的提升。
当两个计算任务⼀起执⾏时,执⾏总时间小于它们分开执⾏的总和。
能够通过⾃动并行计算提升计算性能,例如CPU和GPU的并行计算以及计算和通信的并行。
四、多GPU计算
主板上有多个PCIe插槽,支持多块GPU。
划分小批量数据样本并复制到各块显卡的显存上,以及对各块显卡的显存上的梯度求和再⼴播到所有显存上。
五、多GPU计算的简洁实现
感觉本章新知识:在符号式编程和同步异步上。