熟悉MXNet的异步计算原理,有助于开发更高效的程序,也可以在内存资源有限的情况下主动降低计算性能从而减小内存的开销,根据具体的情况做不同的操作,这是很重要的。
- from mxnet import autograd,gluon,nd
- from mxnet.gluon import loss as gloss,nn
- import os
- import subprocess
- import time
-
- #d2lzh包中已有,里面是一些魔法函数,with操作将自动开始与结束
- class Benchmark():
- def __init__(self,prefix=None):
- self.prefix=prefix+' ' if prefix else ''
- def __enter__(self):
- self.start=time.time()
- def __exit__(self,*args):
- print('%s 耗时:%.4f 秒'%(self.prefix,time.time()-self.start))
-
-
- with Benchmark('加载到队列'):
- x=nd.random.uniform(shape=(4000,4000))
- y=nd.dot(x,x).sum()
-
-
- with Benchmark('计算结果'):
- print('点积之和=',y)
-
- '''
- 加载到队列 耗时:0.0010 秒
- 点积之和=
- [1.5990974e+10]
- 计算结果 耗时:2.1746 秒
- '''
第一个Benchmark中的nd.dot(x,x).sum()计时与下面比较快很多,这说明这个计算是不需要等它全部计算完再返回。
第二个Benchmark是打印出y的结果,这个时候就必须等待它全部计算完。
也就是说前端线程不执行计算,只需放在队列里面,交给后端的C++程序来处理即可,只要数据是保存在NDArray里并使用MXNet提供的运算符,MXNet将默认使用异步计算来获取高性能计算。
除了print函数之外,wait_to_read,waitall函数也需要等待所有计算完成,也时常拿来做性能测试。
来看下wait_to_read与waitall函数
- with Benchmark():
- y=nd.dot(x,x)
- y.wait_to_read()
- # 耗时:1.6327 秒
-
- with Benchmark():
- y=nd.dot(x,x)
- z=nd.dot(x,x)
- nd.waitall()
- # 耗时:3.2405 秒
我们从名字包含wait也可以知道这两个函数都需要等待计算的全部完成才可以。
另外对于NDArray转换成其他不支持异步计算的数据结构的操作,也都需要等待计算的全部完成,比如:asnumpy与asscalar函数
- with Benchmark():
- y=nd.dot(x,x)
- y.asnumpy()
- # 耗时:1.5788 秒
- with Benchmark():
- y=nd.dot(x,x)
- y.norm().asscalar()
- # 耗时:1.7495 秒
像这些需要等待计算完成的都属于同步函数,在计算耗时方面肯定要多点,那是不是都使用异步不更好吗,也不是,这个需要看情况,因为异步操作,会将这些计算任务在极短时间内丢给后端,这样就会占用更多内存,尤其是深度学习的模型一般都是比较大的,这就需要很大的内存来存放,这也是它的缺点(特点),所以有时内存不够的情况,我们就需要使用同步函数,减小这个内存的占用。
我们来看下异步计算对内存的影响,异步对内存的影响(其中检测内存的函数只能在Linux或macOX系统上运行),没有Linux环境,可以在windows系统安装linux子系统,安装方法:Windows系统运行Linux(Windows Subsystem for Linux)
- def data_iter():
- start=time.time()
- num_batches,batch_size=100,1024
- for i in range(num_batches):
- X=nd.random.normal(shape=(batch_size,512))
- y=nd.ones((batch_size,))
- yield X,y
- if (i+1)%50==0:
- print('batch %d,耗时:%f秒'%(i+1,time.time()-start))
-
- #定义多层感知机、优化算法和损失函数
- net=nn.Sequential()
- net.add(nn.Dense(2048,activation='relu'),nn.Dense(512,activation='relu'),nn.Dense(1))
- net.initialize()
- trainer=gluon.Trainer(net.collect_params(),'sgd',{'learning_rate':0.005})
- loss=gloss.L2Loss()
-
- #辅助函数来检测内存的使用情况
- def get_mem():
- res=subprocess.check_output(['ps','u','-p',str(os.getpid())])
- return int(str(res).split()[15])/1e3
-
- for X,y in data_iter():
- break
-
- loss(y,net(X)).wait_to_read()
-
- l_sum,mem=0,get_mem()
- for X,y in data_iter():
- with autograd.record():
- l=loss(y,net(X))
- l_sum+=l.mean().asscalar()#asscalar同步函数
- l.backward()
- trainer.step(X.shape[0])
-
- nd.waitall()
-
- print('增加的内存:%f MB'%(get_mem()-mem))
- '''
- batch 50,耗时:3.952379秒
- batch 100,耗时:7.925409秒
- 增加的内存:41.824000 MB
- '''
可以看到,耗时比较多,但是内存占用比较小,然后我们去掉同步函数,看下异步对内存的影响
- mem=get_mem()
- for X,y in data_iter():
- with autograd.record():
- l=loss(y,net(X))
- l.backward()
- trainer.step(X.shape[0])
-
- nd.waitall()
-
- print('增加的内存:%f MB'%(mem))
-
- '''
- batch 50,耗时:0.101752秒
- batch 100,耗时:0.178621秒
- 增加的内存:323.216000 MB
- '''
可以看到耗时小,但是内存占用大
其中进程打印看下:
b'USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
tony 36 2.3 2.8 1220736 373352 pts/0 Sl+ 10:15 2:51 python\n'
["b'USER", 'PID', '%CPU', '%MEM', 'VSZ', 'RSS', 'TTY', 'STAT', 'START', 'TIME', 'COMMAND
tony', '36', '2.3', '2.8', '1220736', '373352', 'pts/0', 'Sl+', '10:15', '2:51', "python\\n'"]
MXNet后端会自动构建计算图,通过计算图,系统可以知道所有计算的依赖关系,对没有依赖关系的计算执行并行计算提高性能。
比如,a=nd.ones((1,2))和b=nd.ones((1,2))这两个计算是没有依赖关系的,所以可以并行计算。
这里主要介绍CPU和GPU的并行计算
- import d2lzh as d2l
- import mxnet as mx
- from mxnet import nd
-
- def run(x):
- return [nd.dot(x,x) for _ in range(10)]
-
- x_cpu=nd.random.uniform(shape=(2000,2000))
- x_gpu=nd.random.uniform(shape=(6000,6000),ctx=mx.gpu(0))
-
- run(x_cpu)
- run(x_gpu)
- nd.waitall()
-
- with d2l.Benchmark('运行在CPU上'):
- run(x_cpu)
- nd.waitall()
- #运行在CPU上 time: 1.9970 sec
-
- with d2l.Benchmark('运行在GPU上'):
- run(x_gpu)
- nd.waitall()
- #运行在GPU上 time: 0.0266 sec
-
- with d2l.Benchmark('CPU和GPU上进行并行计算'):
- run(x_gpu)
- run(x_gpu)
- nd.waitall()
- #CPU和GPU上进行并行计算 time: 0.0447 sec
运行多了,占了很多显存,内存溢出,于是将x_gpu调小了测试,可以看出并行计算的耗时分别比在CPU和GPU上执行的耗时之和小很多
我们在使用并行计算的时候,经常需要在内存和显存之间进行数据的复制,造成数据的通信,比如说,我们在GPU上计算,然后将计算的结果复制到CPU使用的内存,我们来看下这个GPU的耗时以及GPU到CPU的内存的通信时间。
- import d2lzh as d2l
- import mxnet as mx
- from mxnet import nd
-
- def copy_to_cpu(x):
- return [y.copyto(mx.cpu()) for y in x]
-
- with d2l.Benchmark('运行在CPU上'):
- y=run(x_cpu)
- nd.waitall()
-
- #运行在CPU上 time: 1.9947 sec
-
- with d2l.Benchmark('拷贝到CPU的内存'):
- copy_to_cpu(y)
- nd.waitall()
-
- #拷贝到CPU的内存 time: 0.0995 sec
看下并行的耗时
- with d2l.Benchmark('运行和复制并行计算'):
- y=run(x_cpu)
- copy_to_cpu(y)
- nd.waitall()
-
- #运行和复制并行计算 time: 2.0218 sec
可以看出并行计算的耗时小于两者的耗时之和。这里的并行与前面的同时使用CPU和GPU有点区别,因为这里存在了依赖关系,也就是说y[i]必须先在GPU上计算好才能复制到CPU使用的内存上,所幸的是,在计算y[i]的时候,系统可以复制y[i-1],从而减小计算和通信的总的运行时间。