天天看点

关于Numba你可能不了解的七个方面

当我和人们谈论Numba时,我发现他们很快就在Python中编写了CUDA内核的基础知识。 但是我们经常没有时间使用Numba为GPU程序员提供的一些更先进的功能。 在这篇文章中,我想深入了解一下,并展示了在GPU上使用Numba的几个往往被忽视的方面。我会快速讲述一些主题,但是我会提供链接以供阅读。

你看到这是列表中的第一个项可能会感到惊讶,但我经常碰到一些人,他们并没有意识到Numba,特别是其CUDA支持,是完全开源的。 困惑是可以理解的,因为Numba从2012年的半专有性开始到现在的状态已经走了很长的路程。 当Numba项目开始时,实际上有两个不同的代码库:Numba,一个用于CPU的开源Python编译器,以及NumbaPro(后来更名为“Accelerate”),这是GPU的专有Python编译器。 在接下来的几年中,我们将来自NumbaPro的GPU支持的组件合并到开源的Numba项目中,最终于2017年中期发布Pyculib。

CUBLAS

CUFFT

cuSPARSE

cuRAND

这些包装器曾经是Anaconda Accelerate的一部分,Numba用户主要感兴趣的原因是它们兼容了CPU上的标准NumPy阵列以及Numba分配的GPU阵列。 因此,将标准操作(如FFT)与使用Numba编写的自定义CUDA内核组合起来非常容易,如下代码片段所示:

关于Numba你可能不了解的七个方面

图1.Jupyter Notebook的截图显示了一些在这篇文章中使用的CUDA Python代码。

为什么Numba和Jupyter如此适合GPU计算的实验?有几个原因:

作为一个即时编译器,Numba即时编译你的CUDA代码,因此可以通过重新执行Jupyter代码单元立即获得更改。不需要保存外部文件,不需要构建步骤。您的CUDA内核可以直接嵌入到笔记本本身中,并按照Shift-Enter快速更新。

如果将NumPy数组传递给CUDA函数,Numba将分配GPU内存并自动处理主机到设备和设备到主机副本。这可能不是最有效的使用GPU的方法,但是在原型设计时非常方便。那些NumPy数组可以随时更改为Numba GPU设备阵列。

Jupyter Notebook可以通过SSH进行隧道传输,从而可以在台式机或笔记本电脑上使用Web浏览器编辑笔记本,但在远程Linux服务器上执行代码。 (例如,我们用DGX-1做了这个)。在我的笔记本电脑上,我运行如下命令:

它记录我登录到我们的GPU服务器,并将端口9999转发回我的笔记本电脑。 然后我可以使用此命令在远程系统上启动Jupyter(这假设你在服务器上安装了Jupyter):

Jupyter将开始并打印一个URL粘贴到浏览器中以访问笔记本接口。 SSH转发端口将加密数据,并在远程服务器和本地计算机之间进行路由。 现在,你可以从你的网络浏览器中方便地运行Tesla P100上的算法实验!

在编写应用程序时,很方便的是在无需复制功能内容的情况下,让辅助功能在CPU和GPU上都可以工作。这样,你可以确定这两个地方的实现是相同的。此外,可以更容易地在CPU上对CUDA设备功能进行单元测试,以验证逻辑,而不必总是编写专门的CUDA内核包装器,只需在GPU上运行设备功能即可进行测试。

在CUDA C ++中,使用函数定义上的__host__和__device__关键字的组合可以从CPU(主机)或GPU(设备)调用。 例如,我可以在CUDA C ++中写这个:

那么我可以直接在主机和其他CUDA C ++函数中使用clamp()函数。

使用Numba,我可以使用正常的CPU编译器装饰器在Python中编写相同的函数:

但是我可以直接从CUDA内核使用这个函数,而无需重新声明它,就像这样:

当我从CUDA内核中调用它时,Numba编译器会自动编译一个CUDA版本的clamp()。 请注意,Numba GPU编译器比CPU编译器要严格得多,因此某些功能可能无法重新编译GPU。 这里有一些提示。

GPU支持NumPy数组,但数组函数和数组分配并不支持。

使用Python数学模块中的数学函数,而不是numpy模块。

不要在@jit装饰器中使用显式类型签名。 通常在CPU上使用64位数据类型,而在GPU上,32位类型更为常见。 Numba将自动重新编译正确的数据类型,无论何处需要它们。

你可以将共享内存数组作为参数传递到设备函数中,这样可以更轻松地编写可从CPU和GPU调用的实用程序函数。

在Python中编写完整的CUDA内核的能力非常强大,但对于元素方面的数组函数来说,这可能是乏味的。你必须决定适用于数组维度的线程和块索引策略,选择合适的CUDA启动配置等。 值得庆幸的是,Numba提供了一种简单的创建这些特殊数组函数(NumPy中称为“通用函数”或“ufuncs”)的方法,这几乎不需要CUDA知识!

除了用于编译常规函数的正常的@jit装饰器,Numba还提供了一个@vectorize装饰器,用于从“内核函数”创建ufunc。 这个内核函数(不要与CUDA内核混淆)是一个标量函数,它描述了对所有输入的数组元素执行的操作。 例如,我可以实现一个高斯分布:

与正常函数编译器不同,我需要给ufunc编译器一个参数的类型签名列表。 现在我可以用NumPy数组来调用这个函数并返回数组结果:

我不必使用特殊的内核或者选择启动配置来启动调用约定。 Numba自动处理所有的CUDA详细信息,并将输入的数组从CPU复制到GPU,结果返回给CPU。 (或者,我可以通过GPU设备内存,并避免CUDA内存复制。)

请注意,在第一个调用中,x是一个1D数组,x0和sigma是标量。 标量被Numba隐含地视为1D数组,以通过称为广播的过程匹配另一个输入参数。 广播是NumPy的一个非常强大的概念,可用于组合不同但兼容的维度的数组。 Numba自动处理所有的并行化和循环,无论你的函数输入的尺寸如何。

调试CUDA应用程序是棘手的,并且Python增加了一层复杂性。 使用Python和C中的函数调用堆栈,以及在CPU和GPU上运行的代码,都没有一个适合所有调试的解决方案。 因此,Numba开发人员一直在寻找新的方法来促进CUDA Python应用程序的调试。

几年前,我们引入了一个Numba功能,我们称之为CUDA模拟器。 模拟器的目的是直接在Python解释器中运行CUDA内核,以便使用标准的Python工具进行调试更容易。 有几个注意事项适用:

该模拟器旨在完全在Python解释器中重现并行内核执行的逻辑行为,但不能模拟GPU硬件特性。

模拟器并不是一个应用程序的高效CPU代码路径。 内核运行速度非常慢,只能用于测试目的。

在模拟器中运行的功能可以包含通常不允许在GPU上的代码。 这允许该函数执行诸如调用PDB(Python调试器)或执行其他日志记录。

模拟器可能不会重现设备上存在的竞争条件。

Numba编译的CPU和GPU功能(但不是ufuncs,由于一些技术问题)是专门设计来支持pickling。 当Numba编译的GPU功能被pickle时,NVVM IR和PTX都保存在序列化的字节流中。 一旦将此数据传输到远程工作人员,该功能将在内存中重新创建。 如果工作人员的GPU的CUDA架构与客户端匹配,则将使用PTX版本的功能。 如果CUDA架构不匹配,则CUDA内核将从NVVM IR重新编译,以确保最佳性能。 图2显示了这个过程。 最终的结果是您可以在移动开普勒GPU上测试和调试GPU代码,然后无缝地将其发送到Pascal GPU的Dask群集。

关于Numba你可能不了解的七个方面

图2.Numba如何通过网络将GPU功能传输给群集工作人员。

Numba社区认为分布式GPU使用Numba计算仍然具有出色的优势。 在这种情况下,Numba和Dask绝对有一些改进,所以如果你尝试使用此功能,请与Google Group上的Numba社区联系,以便我们更多地了解你的需求并提供指导。

我希望这篇文章向你展示了关于Numba的一些以前不了解的内容。如果你想了解更多有关这些高级Numba主题的信息,我建议以下资源:

<a href="https://github.com/ContinuumIO/gtc2017-numba" target="_blank">来自2017年GTC大会的Numba GPU教程的材料</a>

<a href="http://numba.pydata.org/numba-doc/latest/cuda/index.html" target="_blank">Numba CUDA文档</a>

<a href="https://devblogs.nvidia.com/parallelforall/numba-python-cuda-acceleration/" target="_blank">Numba博客简介</a>

文章原标题《<b>Seven Things You Might Not Know about Numba</b>》,作者:<b>Stanley Seibert</b>,Anaconda社区创新总监,曾是Mobi的首席数据科学家。译者:董昭男,审校: