Numba:"大规模优化武器"

Numba如何节省您的时间并加速代码

Numba:

Numba是一个Python编译器,专门用于数值函数,允许您使用直接用Python编写的高性能函数来加速应用程序。

Numba使用LLVM从纯Python代码生成优化的机器代码。 通过几个简单的更改,我们的Python代码(面向函数)可以"及时"优化,从而获得与C,C ++相似的性能,而无需更改语言。

您可以在我的GitHub中找到整个代码! :)

目录

  • 什么是Numba?
  • 第一步:为CPU编译
  • Numba for GPU
  • 结论

什么是Numba?

Numba是允许您使用CPU和GPU加速Python代码(数字函数)的编译器:

  • 函数编译器:Numba编译Python函数,而不是整个应用程序或部分代码。 基本上,Numba是另一个Python模块,可提高我们函数的性能。
  • 即时:(动态翻译)Numba在执行之前立即将字节码(比机器代码抽象的中间代码)翻译成机器代码,以提高执行速度。
  • 数值集中:Numba专注于数值数据,例如int,float,complex。 目前,将其与字符串数据一起使用存在一些限制。

Numba并不是在CUDA中编程的唯一方法,它通常直接用C / C ++进行编程。 但是Numba允许您直接使用Python进行编程,并且只需对我们的代码进行少量更改即可针对CPU和GPU对其进行优化。 关于Python,还有其他选择,例如pyCUDA,下面是它们之间的比较:

CUDA C / C ++:

  • 这是CUDA中最常见,最灵活的编程方式
  • 加速C,C ++中的应用程序。

pyCUDA

  • 这是用于Python的最有效的CUDA表单
  • 它要求在我们的Python代码中编程C,并且通常需要进行许多代码修改。
Numba:

Example PyCUDA

Numba

  • 效率不如pyCUDA
  • 它允许您用Python编写代码并进行少量修改来对其进行优化
  • 它还为CPU优化了Python代码

目标

演讲的目的如下:

  • 使用Numba在CPU上编译函数
  • 了解Numba的工作原理
  • 在GPU中加速Numpy ufunc
  • 使用Numba编写内核(下一教程)

第一步:为CPU编译

Numba除了能够加速GPU中的功能之外,还可以用于优化CPU中的功能。 为此,使用Python装饰器(函数修饰符)。

首先,我们将开始评估函数hypot,以尝试Numba的工作方式。 我们需要在函数中使用装饰器@jit。

<code>from numba import jitimport numpy as npimport math@jitdef hypot(x, y):  return math.sqrt(x*x + y*y)# Numba functionhypot(3.0, 4.0)# Python functionhypot.py_func(3.0, 4.0)/<code>

>>> # Numba function

>>> hypot(3.0, 4.0)

5.0


>>> # Python function

>>> hypot.py_func(3.0, 4.0)

5.0

Numba中的结果与Python函数中的结果相同,因为Numba将函数的原始实现保存在.py_func中。

Benchmarking

自然地,衡量我们的代码性能,检查Numba是否真的运行良好并观察Python实现与Numba实现之间的区别非常重要。 此外,math库已经包含了hypot函数,我们也可以对其进行评估。

<code>import math# Python function%timeit hypot.py_func(3.0, 4.0)# Numba function%timeit hypot(3.0, 4.0)# math function%timeit math.hypot(3.0, 4.0)/<code>

>>> # Python function

>>> %timeit hypot.py_func(3.0, 4.0)


The slowest run took 17.62 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 260 ns per loop


>>> # Numba function

>>> %timeit hypot(3.0, 4.0)


The slowest run took 33.89 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 216 ns per loop


>>> # math function

>>> %timeit math.hypot(3.0, 4.0)


The slowest run took 105.55 times longer than the fastest. This could mean that an intermediate result is being cached. 10000000 loops, best of 3: 133 ns per loop


math.hypot函数甚至比Numba还快!! 这是因为Numba会给每个函数调用带来一定的开销,该开销要大于Python函数调用的开销,因此非常快的函数(如上一个)会受到此影响。

(但是,如果您从另一个函数调用Numba函数,则开销很少,如果编译器将该函数集成到另一个函数中,则有时甚至为零。总之,请检查这些函数是否真的在Numba中加速了)。


Numba如何工作?

当我们初始化hypot函数时:

Numba:

How Numba works

  • IR 中间人
  • 字节码分析 中间代码比机器代码更抽象
  • LLVM低层虚拟机,用于开发编译器的基础结构
  • NVVM是基于LLVM的IR编译器,旨在表示GPU内核

每行python之前都有几行Numba IR代码。 最有用的是查看向我们展示Numba如何处理变量的类型注释,例如,在"pyobject"中,它表示Numba不知道np.sin函数,并且他应该从Python调用它。 我们可以使用.inspect_types()检查该hypot的过程。

<code>


示例:创建分形

我们将测量使用Mandelbrot集创建分形的性能,我们将看到Numba如何帮助我们改善性能。

<code>

1 loop, best of 3: 4.62 s per loop

<matplotlib.image.axesimage>

Numba:

使用Mandelbrot集生成分形大约需要4.62秒,现在我们要使用Numba改善性能,我们只需要添加@jit装饰器即可。

<code>

The slowest run took 4.17 times longer than the fastest. This could mean that an intermediate result is being cached. 1 loop, best of 3: 52.4 ms per loop


我们可以观察到如何将建立分形的时间从4.62秒减少到52.4 ms……并且仅通过添加装饰器就可以做到这一点!

一些常见错误

我们已经说过,Numba仅适用于数字函数,尽管Numba可以编译并运行任何Python代码,但是有些类型的数据尚不能编译(例如字典),并且编译它们也没有任何意义。

>>> @jit

>>> def dictionary(dict_test):

>>> return dict_test['house']dictionary({'house': 2, 'car': 35})2


但是它并没有失败! 我们已经说过Numba不会编译字典……这里的意思是Numba创建了2个函数,其中一个在Python中,另一个在Numba中。 因此,在这里我们看到了python解决方案,我们可以通过执行nopython = True来验证这一点。

jit(nopython = True)等效于njit

<code>


Numba for GPU

使用Numba在GPU中进行编程的方法有两种:

  1. ufuncs / gufuncs__
  2. CUDA Python内核(下一个教程)

功能ufunc

GPU的主要设计功能之一是能够并行处理数据,因此numpy(ufunc)的通用功能是在GPU编程中实现它们的理想选择。

注意:ufunc是对numpy数组的每个元素执行相同操作的函数。 例如:

<code>

动手:为GPU创建功能ufunc

如前所述,由于ufunc函数具有并行性,因此是将其与GPU配合使用的理想选择。 因此,Numba无需使用C就可以创建编译的ufunc函数。为此,我们必须使用装饰器@vectorize。

让我们从使用@vectorize编译和优化CPU ufunc的示例开始。

<code>
<code>

而不是使用CPU来编译和执行先前的功能,我们将在GPU中使用CUDA,为此,我们必须使用"目标属性"。 我们将指出每个变量的类型(参数和返回值)。

return_value_type(argument1_value_type,arguments2_value_type,...)

为此,我们将使用先前的函数,该函数期望2个int64值并返回另一个int64值。 我们将指定target ='cuda'以便能够在GPU中执行它。

<code>

array([ 24, 343, 15, 9])

我们可以检查在CPU或GPU上运行它的速度:

>>> %timeit np.add(a, b) # Numpy en CPU


The slowest run took 38.66 times longer than the fastest. This could mean that an intermediate result is being cached. 1000000 loops, best of 3: 511 ns per loop


>>> %timeit add_ufunc_gpu(a, b) # Numpy en GPU


The slowest run took 4.01 times longer than the fastest. This could mean that an intermediate result is being cached. 1000 loops, best of 3: 755 µs per loop


GPU比CPU慢!!! 安静,这有一个解释……但是首先让我们看看调用该函数时会发生什么……

当我们执行此功能时,Numba会产生:

  • 编译CUDA内核以在输入数组的所有元素上并行执行ufunc函数
  • 将输入和输出分配给GPU内存
  • 将输入复制到GPU
  • 运行CUDA内核
  • 将结果从GPU复制回CPU
  • 以numpy数组形式返回结果

与用C实现相比,Numba允许您以更简洁的方式执行这些类型的任务。

为什么GPU比CPU慢?

  • 我们的输入量太小:GPU使用一次并行处理数千个值的并行处理来获得更好的性能。 我们的输入是4或64维,我们需要更大的数组来保持GPU的占用。
  • 非常简单的计算:与调用CPU函数相比,将计算结果发送到GPU需要很多"精力"。 如果我们的函数不需要过多的数学计算(通常称为算术强度),那么GPU所花费的时间可能比CPU中更长。
  • Numba将数据复制到GPU。
  • 输入的变量类型大于必要的变量:我们的示例使用int64,我们可能不需要它们。 实际上,在CPU中,32位和64位具有相同的计算速度,但是在GPU中,64位具有稍微提高的计算速度(与32位相比,其速度可分别降低多达24倍)。 因此,在GPU中执行功能时,请记住这一点,这一点很重要。

考虑到这一点,我们将尝试应用在前面几点中学到的知识,以了解在GPU上运行是否真的比在CPU上运行更快。 我们将要计算一个密度函数,这对于较大的数组来说是一个稍微复杂的操作。

在给定平均值和sigma的情况下,让我们计算x中的高斯密度函数的值:

<code>

>>> %timeit norm_pdf.pdf(x, loc=mean, scale=sigma) # CPU function

10 loops, best of 3: 60.8 ms per loop


>>> %timeit gaussian_dens_gpu(x, mean, sigma) # GPU function

100 loops, best of 3: 6.88 ms per loop


是啊!

我们甚至可以使用Numba定义要在CPU中执行的功能。

<code>import math sqrt_pi = np.float32((2*math.pi)**0.5)@vectorizedef gaussian_dens_cpu(x, mean, sigma):    return math.exp(-0.5 * ((x - mean) / sigma)**2) / (sigma * sqrt_pi)  x = np.random.uniform(-3, 3, size=1000000).astype(np.float32)mean = np.float32(0.0)sigma = np.float32(1.0)%timeit gaussian_dens_cpu(x, mean, sigma) # CPU/<code>

>>> %timeit gaussian_dens_cpu(x, mean, sigma) # CPU

10 loops, best of 3: 23.6 ms per loop



它甚至比用Python编写的函数还要快,但比在GPU中执行的函数要慢。

不幸的是,有一些函数不在ufunc定义的范围内,因此,为了在GPU中执行不满足该要求的函数,我们使用cuda.jit。 我们可以使用在GPU上运行的"设备功能"。

注意:"设备功能"是只能从内核或另一个"设备"功能调用的功能。

<code>

>>> %timeit polar_distance(rho1, theta1, rho2, theta2)


The slowest run took 23.16 times longer than the fastest. This could mean that an intermediate result is being cached. 1 loop, best of 3: 10.2 ms per loop


结论

总结起来,Numba是一个Python编译器,专门用于数值函数,它使您可以使用直接用Python编写的高性能函数来加速应用程序。

它是一个稳定的工具,可让您优化面向代码的数组以进行操作。 由于它的易用性(只需一个装饰器!)为我们提供了一个非常强大的工具来改善代码的性能。

欢迎提出建议和评论。 跟着我,谢谢您的阅读! :)

(本文翻译自Alejandro Diaz Santos的文章《Numba: "weapon of mass optimization"》,参考:https://towardsdatascience.com/numba-weapon-of-mass-optimization-43cdeb76c7da)


分享到:


相關文章: