Python 中的并行性:使用NumExpr 进行向量化指令
在这篇文章中,我们将探讨如何重构Python代码以利用NumExpr的功能。
Python* 有几种实现向量化(即指令级并行)的方法,从使用Numba*1进行即时(JIT)编译到使用Cython*编写类似C的代码。实现Python并行化的一种有趣方法是通过NumExpr*,其中符号求值器将数值Python表达式转换为高性能的向量化代码。NumExpr通过逐块向量化元素而不是一次性编译所有内容来实现这一点——从而创建可从Python代码中使用的加速对象内核。在这篇文章中,我们将探讨如何重构Python代码以利用NumExpr的功能。
数值表达式的并行化
Python凭借其简单的语法具有灵活性,使开发人员能够在NumPy*和SciPy*等库的帮助下快速原型化数值计算。但是Python语言的开发并非以并行化为目的——尽管这是从现代向量和多核处理器中获得性能的关键要求。那么如何使用Python向量化数值表达式呢?
数值表达式是一个数学语句,它包含数字和数学符号以执行计算(例如,11*a-42*b)。在Python中,此表达式也可以对从NumExpr包定义的数组a和b进行运算。在这种情况下,与标准Python中的相同计算相比,对数组进行类似表达式的计算会得到加速,从而利用了内在的并行性和向量化。
为了提高性能,NumExpr可以使用优化的英特尔®向量数学函数库(英特尔®VML),该库包含在英特尔®数学核心函数库(英特尔®MKL)中。这使得可以加速对存储在内存中连续的向量进行运算的数学函数(例如,正弦、指数或平方根)的计算。
重构常见的NumPy调用以用于NumExpr
要使用NumExpr包,您只需要将计算字符串传递给evaluate
函数。然后将其编译成一个对象,在完成之前将整个计算留在低级代码中。之后,结果将返回到Python层,避免了对Python解释器的过多调用。
让我们来看一个例子,我们为NumPy数组计算一个简单的表达式
import numpy as np import numexpr as ne a = np.arrange(1e6) b = np.arrange(1e6) %timeit 11*a-42*b 14.2 ms ± 826 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) %timeit ne.evaluate("11*a-42*b") 3.51 ms ± 248 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
在这种情况下,由于英特尔VML启用的内在向量化,我们实现了4倍的加速。该库还可以执行就地操作,其中复制开销可以忽略不计。
现在让我们评估当我们使用数学函数时NumExpr的加速效果,其中英特尔VML的好处更加明显
import numpy as no import numexpr as ne a = np.arrange(1e6) b = np.arrange(1e6) %timeit np.exp(a)**2 + np.sqrt(b)**3 498 ms ± 11.2 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) %timeit ne.evaluate("exp(a)**2 + sqrt(b)**3") 26.4 ms ± 2.23 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)
在这种情况下,由于在英特尔MKL中优化了sqrt
函数,我们获得了更高的性能。加速接近19倍。这表明NumPy库并没有为某些表达式提供我们期望的加速。此外,NumExpr实现避免了对中间结果的内存分配,从而提高了缓存利用率并降低了内存开销。在对大型数组进行计算时,我们可以真正看到这些优化的优势。
控制NumExpr求值器
由于NumExpr内部使用英特尔VML库,因此它只计算库允许的类型的数学函数。它还可以对具有单位增量、整数和布尔值的实数和复数向量参数进行运算。如果数组的类型在evaluate表达式中不匹配,则根据通常的推断规则进行转换。
性能取决于许多因素,包括向量化和内存开销。因此,您可以使用一些英特尔VML的函数来调整性能和控制数值精度(并最终控制线程数)。
要获取有关英特尔VML库版本的信息,您可以调用函数get_vml_version()
,这对于检查安装可能很有用。所有向量函数都通过函数set_vml_accuracy_mode(mode)
支持以下精度模式。模式可以设置为
- 高,相当于高精度 (HA),默认模式。
- 低,相当于低精度 (LA),它通过降低两位最低有效位的精度来提高性能。
- 快速,相当于增强性能 (EP),它以显著降低精度为代价提供更好的性能。大约一半的尾数位是正确的。
有关更多信息,请参见英特尔MKL开发者参考2和NumExpr的官方文档3。
NumExpr还可以用于控制线程数。函数set_num_threads(nthreads)
设置英特尔VML操作要使用的最大线程数。返回值是当前环境中线程数的先前设置。让我们修改前面的示例以使用线程来进一步提高性能
import numpy as np import numexpr ad ne a = np/arrange(1e6) b = np/arrange(1e6) ne.set_num_threads(4) %timeit ne.evaluate("exp(a)**2 + sqrt(b)**3") 7.2 ms ± 137 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
ne.set_num_threads(8) %timeit ne.evaluate("exp(a)**2 + sqrt(b)**3") 3.94 ms ± 191 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
加速为3.7倍,并行效率为93%。在这个例子中,更多的线程等于更好的性能。由于英特尔VML性能库,将NumExpr作为NumPy的替代方案可以为使用数组和数值表达式进行计算带来显著的性能优势。语法与NumPy非常相似,只需几个简单的函数调用,您就可以将代码转换为NumExpr。
参考文献
获取软件