如何优化执行性能
使用jit_class
使用场景:使用@jit_class装饰器修饰自定义类,提高执行性能。jit_class应用于静态图模式,在动态图模式下,@jit_class会被忽略,不影响动态图模式的执行逻辑。
jit_class的介绍
用户在网络脚本中定义一个类时,可以写成继承于Cell的类、自定义类、@jit_class修饰的类,它们的用法和区别如下:
- 继承于Cell的类
Cell是MindSpore中神经网络的基本构成单元,模型或者神经网络层应当继承该类。静态图模式下,使用Cell类并且在construct函数中编写执行代码,此时construct函数的代码会被编译成静态计算图。
- 自定义类
定义自定义类后,可以对类进行实例化、调用类对象的属性和方法,请参考自定义类的使用。相比于Cell的类定义,自定义类更贴近用户调用Python类的使用习惯。自定义类在静态图模式下的实现方式与Cell不同,例如,调用自定义类对象的函数方法时,其函数方法中的代码不会被编译成静态计算图,而是通过Python解释器进行解释执行。
- @jit_class修饰的类
为了兼顾用户的Python使用习惯和静态图编译带来的性能优势,提供了@jit_class装饰器。给自定义类修饰@jit_class装饰器后,该类的函数代码会被编译成静态计算图,基于图优化、静态图整图下沉等技术,编译器可以针对计算图进行全局的优化,从而获得较好的执行性能。
在静态图模式下,通过使用@jit_class修饰自定义类,用户可以创建、调用该类的实例,并且可以获取其属性和方法。
jit_class装饰器的使用
jit_class装饰器仅支持修饰自定义类,不支持修饰继承于Cell的类。
jit_class支持自定义类嵌套使用、自定义类与Cell嵌套使用的场景。需要注意的是,类继承时,如果父类使用了jit_class,子类也会具有jit_class的能力。
获取类的属性和方法
支持通过类名或类实例调用属性和方法。
创建类的实例
对于将会被编译成静态计算图的函数,如Cell的construct函数、@jit修饰的函数或前两者调用的子函数,如果需要在函数内创建@jit_class所修饰的类的实例,参数要求为常量。
调用类的实例
调用@jit_class所修饰的类的实例时,将会调用该类的__call__函数方法。
使用select算子
使用场景:Select算子来替代if控制流语句,减少静态图子图生成,提高执行性能(也可以提高编译性能)。
编写网络时,会经常使用到if语句,如果if语句的条件是变量条件,每个if语句都会产生额外的子图。在静态图模式下,子图数量越多,编译耗时越久,因此部分场景可以通过Select算子等价替换if语句来优化编译性能。
需要注意的是,使用Select算子替换if语句会影响网络的运行性能。一方面,Select算子会同时执行true分支和false分支,而if语句只执行其一个分支,因此使用if运行耗时相比使用Select算子耗时减少;另一方面,Select算子性能优于if语句产生的控制流算子,使用if运行耗时相比使用Select算子运行耗时增加。综合上述两种因素,最终运行性能变化情况需要结合实际情况判断。一般来讲,当分支中算子数量较少,建议使用Select算子;当分支中算子数量较多,建议使用if语句。
一个使用Select算子替代if语句来优化编译性能的代码样例如下:
使用Vmap进行批处理
使用场景:在处理无依赖关系的批量数据且相关的算子支持Vmap功能时,可以使用Vmap替代for循环处理批量数据来优化执行性能(也可以提高编译性能)。
MindSpore已支持Vmap特性,Vmap的详细介绍可参考自动向量化Vmap。
一个使用Vmap替换for循环处理批量数据来优化编译性能的代码样例如下:
代码的运行结果如下(实际耗时与硬件环境有关,以下数据仅供参考):
依赖控制保证执行序
如果函数的运行结果依赖或影响外部状态,我们认为该函数具有副作用,比如函数会改变外部全局变量、函数的结果依赖全局变量的值。如果算子会改变输入参数的值或者算子的输出依赖全局参数的值,我们认为这是带副作用的算子。
根据内存属性和IO状态,将副作用划分为内存副作用和IO副作用。当前内存副作用主要有Assign、优化器算子等等,IO副作用主要有Print算子。详细可以查看算子定义,内存副作用算子在定义中有side_effect_mem属性,IO副作用算子在定义中有side_effect_io属性。
Depend用于处理依赖项操作。在大多数情况下,如果操作符有IO副作用或内存副作用,则将根据用户的语义执行它们,不需要另外使用Depend算子来保证执行顺序。在某些情况下,如果两个运算符A和B没有顺序依赖关系,并且A必须在B之前执行,我们建议使用Depend指定它们的执行顺序。使用方法如下:
a = A(x)
b = B(y)
在插入Depend算子后,如下:
a = A(x)
y = Depend(y, a)
b = B(y)
值得说明的是,用于浮点数溢出状态检测的一组特殊算子它们存在隐含副作用,但又不属于IO副作用或内存副作用。此外,使用时还有严格的顺序要求,即:在使用NPUClearFloatStatus算子前需要保证NPUAllocFloatStatus已经执行,使用NPUGetFloatStatus算子前需要保证NPUClearFloatStatus已经执行。因为这些算子使用较少,目前的方案是保持它们的定义为无副作用形式,以Depend确保执行顺序。注意:此类浮点数溢出状态检测的算子仅在Ascend平台支持。如下:
优化冗余显存拷贝操作
在函数式编程中,通过参数和返回值之外的渠道和外界存在数据交换的函数,被称为非纯函数,被认为是存在副作用的。在MindSpore框架内部,针对副作用的问题会插入Load算子,该算子属于虚拟算子,不需要在后端执行,不占用显存,仅用于表示需要读取全局变量的值。在图模式下,需要编译完整个图之后才将图中的各个算子下发到后端执行,使用Load算子多次读取全局变量,而不是多次使用真实算子多次保存全局变量的值,这样可以减少显存的消耗。
但是,全局变量的值可能是变化的,如果没有真实算子保存值,某些场景下会存在精度问题。针对这种情况,MindSpore框架内部会插入真实算子,占用一定的显存来保存全局变量的值,从而避免出现精度问题。
我们提供了MS_DEV_SIDE_EFFECT_LOAD_ELIM开关来优化显存占用的程度,即设置export MS_DEV_SIDE_EFFECT_LOAD_ELIM=0/1/2/3。
- 当将MS_DEV_SIDE_EFFECT_LOAD_ELIM设置为0时,表示对框架内部的Load算子都插入真实算子,即占用显存最多,保证网络精度没有问题。
- 当将MS_DEV_SIDE_EFFECT_LOAD_ELIM设置为1或者没有设置值时(即默认模式),表示对框架内部的Load算子可能出现精度问题的场景保守地插入真实算子,保证网络精度没有问题。
- 当将MS_DEV_SIDE_EFFECT_LOAD_ELIM设置为2,在损耗一定编译性能的前提下,尽量少地插入真实算子,优化显存较多,且保证网络精度没有问题。
- 当将MS_DEV_SIDE_EFFECT_LOAD_ELIM设置为3,不插入真实算子,不保证网络的精度,显存消耗最少。
我们可以通过用例和生成的中间表示(即IR)来进一步理解。