使用Nvc ++和Cython在GPU上加速Python

2020-12-25 18:40:21

C ++标准库包含丰富的容器,迭代器和算法的集合,这些容器,迭代器和算法可以组合起来以产生复杂问题的优雅解决方案。最重要的是,它们速度很快,使C ++成为编写高性能代码的诱人选择。

NVIDIA最近推出了stdpar:一种使用nvc ++编译器在GPU上自动加速C ++标准库算法执行的方法。这意味着使用标准库容器和算法的C ++程序现在可以运行得更快。

在本文中,我探索了将GPU加速的C ++算法引入Python生态系统的方法。我使用Cython作为从Python调用C ++的方式,并向您展示如何使用nvc ++构建Cython代码。我提供两个示例:对数字序列进行排序的简单任务,以及更复杂的实际应用程序Jacobi方法。在这两种情况下,您都将获得比使用NumPy的传统方法更为出色的性能。最后,我讨论了一些当前的局限性和后续步骤。

如果您以前从未使用过Cython或可以使用更新,这里有一个在Cython中编写函数的示例,该函数使用C ++的sort函数对一组数字进行排序。将以下代码放在文件cppsort.pyx中:

第一行告诉Cython使用C ++模式而不是C(默认设置)。

第二行使C ++排序功能可用。关键字cimport用于导入C / C ++功能。

最后,该程序定义一个函数cppsort,该函数执行以下操作:接受单个参数x。前面的int [:]指定x的类型,在这种情况下,是一维整数数组。

调用std :: sort对x的元素进行排序。参数& x [0]和& x [-1] +1分别指向数组的最后一个元素之后的第一个和最后一个。

与.py文件不同,.pyx文件不能直接导入。要从Python调用函数,必须将cppsort.pyx构建到可以从Python导入的扩展中。运行以下命令:

此命令会发生一些事情(图1)。首先,Cython将cppsort.pyx中的代码转换为C ++,并生成文件cppsort.cpp。接下来,C ++编译器(在本例中为g ++)将该C ++代码编译为Python扩展模块。扩展模块的名称类似于cppsort.cpython-38-x86_64-linux-gnu.so。

在[2]中:将numpy导入为npIn [3]:x = np.array([4,3,2,1],dtype =" int32")在[4]中:print(x)[ 4 3 2 1]在[5]中:cppsort(x)在[6]中:print(x)[1 2 3 4]

可以使用其他并行执行策略参数来调用C ++标准库算法,例如std :: sort。此参数告诉编译器您希望并行执行算法。诸如g ++之类的编译器可以选择使用CPU线程并行执行。但是,如果使用nvc ++编译器编译代码,并传递-stdpar选项,则GPU会加快执行速度。有关更多信息,请参阅使用stdpar使用GPU加速标准C ++。

进行的另一个重要更改是在cppsort函数中创建输入数组的本地副本。这是因为GPU只能访问由nvc ++和-stdpar选项编译的代码中分配的数据。在此示例中,输入数组由NumPy分配,该数组可能无法使用nvc ++进行编译。

以下代码示例是重写的cppsort函数,以包括以前的更改。它包括使用向量来管理输入数组的本地副本,以及使用copy_n函数来与之复制数据。

来自libcpp.algorithm cimport排序,来自libcpp.vector的copy_n来自libcpp.execution的cimport向量cimport pardef cppsort(int [:] x):cdef vector [int] temp ,len(x),temp.begin())sort(par,temp.begin(),temp.end())copy_n(temp.begin(),len(x),& x [0])

当您运行cythonize命令时,将使用常规的主机C ++编译器(g ++)来构建扩展(图1)。为了将使用par执行策略调用的算法的执行卸载到GPU,您必须使用nvc ++编译器构建扩展(图2)。您还必须将一些自定义选项(例如-stdpar)传递给编译器和链接器命令。当构建过程涉及诸如此类的其他步骤时,通常最好将setup.py脚本替换为cythonize命令。有关详细实现的更多信息,请参见shwina / stdpar-cython GitHub存储库中的setup.py文件,该文件将构建过程修改为使用nvc ++以及适当的编译器和链接器标志。

从Python导入和使用该函数的方式没有改变,但是排序现在在GPU上进行:

在[3]中:x = np.array([4,3,2,1],dtype =" int32")在[4]中:cppsort(x)#使用GPU!

图3显示了cppsort函数与NumPy .sort方法的比较。可以在sort.ipynb Jupyter笔记本中找到用于生成这些结果的代码。提供了该函数的三个版本:

GPU版本也使用并行执行策略,但是使用nvc ++和-stdpar编译器选项进行编译。

对于更大的问题,GPU版本的执行速度明显快于其他版本-比NumPy .sort快20倍!

作为更复杂的示例,请看使用Jacobi方法求解二维热方程。例如,可以使用该数学方程式来预测在一侧加热的方板中的稳态温度。

雅可比(Jacobi)方法包括用二维点网格近似正方形板。二维数组用于表示这些点中每个点的温度。每次迭代使用以下更新方案,根据上一步计算的值更新数组的元素:

重复此过程,直到达到收敛为止:当在两个后续迭代结束时获得的值没有显着差异时。

从编程的角度来看,可以使用标准库算法std :: for_each来执行更新步骤,并使用std :: any_of来检查收敛性,从而在C ++中实现Jacobi方法。以下Cython代码使用这些C ++函数来实现Jacobi求解器。有关实现的更多信息,请参见jacobi.ipynb Jupyter笔记本。

#distutils:语言= c ++#cython:cdivision =来自libcpp.algorithm cimport swapfrom libcpp.vector cimport vectorfrom libcpp cimport bool,floatfrom libc.math cimport fabsfrom算法cimport for_each,any_of,复制自执行cimport par,seq cdef cpp float avg T1 float * T2 int M,N avg(float * T1,float * T2,int M,int N):this.T1,this.T2,this.M,this.N = T1,T2,M,N内联无效呼叫" operator()"(int i):如果(i%this.N!= 0和i%this.N!= this.N-1):(this.T2 [i] =( this.T1 [i-this.N] + this.T1 [i + this.N] + this.T1 [i-1] + this.T1 [i + 1])/ 4.0cdef cppclass聚合:float * T1 float * T2浮点数max_diff收敛(float * T1,float * T2,float max_diff):this.T1,this.T2,this.max_diff = T1,T2,max_diff inline bool call" operator()"( int i):返回工厂(this.T2 [i]-this.T1 [i])> this.max_diff.def jacobi_solver(float [:,:] data,float max_diff,int max_iter = 10_000):M,N = data.shape [0],data.shape [1] cdef vector [float]局部cdef vector [float ] temp local.resize(M * N)temp.resize(M * N)cdef vector [int]索引= range(N + 1,(M-1)* N-1)copy(seq,& data [0 ,0],& data [-1,-1],local.begin())复制(par,local.begin(),local.end(),temp.begin())cdef int迭代= 0 cdef浮点数* T1 = local.data()cdef float * T2 = temp.data()keep_going = True,而keep_going和迭代< max_iter:迭代+ = 1 for_each(par,indexs.begin(),indexs.end(),avg(T1,T2,M,N))keep_going = any_of(par,indexs.begin(),indexs.end() ,converged(T1,T2,max_diff))如果(T2 == local.data()):swap(seq,local.begin(),local.end(),& data [0, 0]),否则:copy(seq,temp.begin(),temp.end(),& data [0,0])返回迭代

图5显示了与Jacobi迭代的NumPy实现相比,从三种不同的基于C ++的Cython实现获得的加速。用于生成这些结果的代码也可以在早期的笔记本中找到。

stdpar引入了一种用于C ++标准库算法的方法,例如在GPU上执行计数,聚合,转换和搜索等。使用Cython,您可以使用Python中的这些GPU加速算法,而无需任何C ++编程。

Cython与其他Python软件包进行自然交互以进行科学计算和数据分析,并具有对NumPy数组和Python缓冲区协议的本地支持。这使您可以使用Cython和nvc ++将现有Python代码的计算密集型部分卸载到GPU。可以构建相同的代码以在CPU或GPU上运行,从而使在没有GPU的系统上的开发和测试更加容易。

当前,这种方法的一个重要限制是经常需要输入数据的副本。在Python中,内存是在NumPy之类的库中分配的。 GPU无法访问此类外部分配的内存。记住上一篇文章中提到的局限性(使用stdpar使用GPU加速标准C ++)也很重要。

按照自述文件中的说明进行操作,并在此shwina / stdpar-cython GitHub存储库中运行示例笔记本。

这篇文章代表了结合使用Cython和nvc ++的一些早期探索。我很高兴您自己尝试一下,并听听您所解决的问题!通过发布到NVIDIA开发者论坛开始讨论或报告您可能遇到的任何问题。