ctypes、Cython、pyCUDA和pybind11来对矩阵相关python代码进行加速。
向量化操作与局限性
在矩阵相关的问题中,可以使用numpy等库来高效的完成计算,因为这些库的底层往往也是lapack、mkl等高效的矩阵库,所以效率很高,但是这要求使用时必须以向量化的语法来进行操作,而如果使用python的循环来写的话,效率就会非常低。
比如
1 | import numpy as np |
在这个例子里numpy的内置函数比python的循环快非常多,原因在之前的文章中已经提到过,这里就不再赘述。
当然,这篇文章不是说如何去使用向量化语法,经常用numpy等库的人应该对此非常熟悉才对。但是由于内置的库的方法是有限的,有的时候可能没法使用向量化语法,尤其是对于某些需要求解雅可比矩阵的地方,求解雅可比矩阵的时候基本上都是单独操作矩阵中每一个元素的,比如下面这个4*4大小的雅可比矩阵
1 | J=torch.tensor( |
可能会觉得上述内容有点太不常见,但是如果你的方法中用到了某种参数化的方法,那么它的雅可比矩阵往往就是这么复杂的。更麻烦的是,如果用到了雅可比矩阵,那么往往会跟迭代法联系在一起,这意味着需要在一个循环中多次去计算雅可比矩阵!如果还是上面那个东西,在python里计算一个雅可比矩阵就要花费几十毫秒,那么在一个循环中计算几百次(迭代法几百次也很少了),那么就会花费十来秒。这还不算完,深度学习这么火,不给你的方法加个网络总说不过去吧,假设一个batch_size是128,那么在一个batch_size的前向传播里计算jacobian都得花个一两分钟,如果再考虑一个复杂的网络,算上训练集的大小,那么一个epoch都会花好长时间!最关键的是,上述复杂的计算中很多一部分东西都是python引入的不必要的对象创建和销毁、内存分配与释放等等,并没有用在关键的计算上,所以这些时间都是浪费的。
既然python本身会浪费掉很多时间,那么如果用其他的语言来重写这些耗时的计算,那么就可以大大提高效率。这里介绍几种方法,分别是ctypes、Cython、pyCUDA和pybind11。
涉及到矩阵不是那么有效的方法
这里的不是那么有效针对的是涉及到传输numpy矩阵以及后续矩阵处理的任务,并不是说这些方法不好用。
ctypes与动态链接库
ctypes是Python的一个库,提供了与C兼容的数据类型,允许调用动态链接库中的函数,这样就可以在python中调用C语言的函数,从而提高效率。
ctypes使用起来非常简单,在编译C++动态库的时候也不需要考虑python的版本,C++与python基本上完全无关。但是C++代码中不涉及到python的对象,所以他们之间传递的参数也会非常有限,当然这也是优势,这可以让C++代码与python尽可能解耦。
而且C++编译成动态库完全不会涉及到GIL这种东西,所以可以在python用事件循环或线程池之类的东西,来在python里实现真正的多核多线程。
当然,Ctypes在传输numpy矩阵上不是很方便,但是假如矩阵是图像的话,可以试着把图像保存下来,然后只传递文件名过去,这样也是一种节省时间的做法。这里给一个linux下的简单例子(但是因为偷懒所以没真的去跑,之前写的类似的找不到了),因为linux下可以直接存在/dev的内存挂载点里,更加合理。这里用asyncio的原因也是因为如果需要大量存取图像的话,用协程会更高效。
1 | import ctypes |
Cython
注意是Cython而不是CPython,后者是python的一个实现,而前者是一个库,通过一种很新的语法来直接在python中实现强类型的变量,"像写python那样去写C",从本质上来说,Cython就是包含C数据类型的Python。
Cython我没有正式在项目里用过,所以不好评价,这里大部分内容参考的是公众号:古明地觉的编程教室中的文章
以及,"古明地觉的编程教室"这位大佬还写了几个pdf来教大家写Cython和C扩展,有兴趣可以去Ta公众号里免费下载下来看一下
这里简单列出一下Cython的代码
1 | def fib(int n): |
另外,Cython的话也可以实现真·多线程代码,也就是不去管GIL的代码,这点还是很nice的。
但是其实我觉得这种方法还是有点怪的,首先是它构建Cython的代码长得非常奇怪,无论是喜欢python的还是喜欢c的都很难去喜欢Cython的代码,然后是它似乎没法直接和numpy的矩阵直接交互,虽然可以通过其他的方法完成转换,但是想要去使用矩阵一些方法,比如求逆、求特征值等等,就会比较麻烦。
涉及到矩阵很有效的方法
下面涉及到的一些方法跟python中的矩阵有非常大的关系,"天生"对矩阵有很好的支持。
pyCUDA
CUDA就不用多说了吧,在GPU上真的不能再真的并行计算了,pyCUDA是一个把CUDA核函数直接编译成python可以调用的接口,这样就可以在python中直接调用CUDA核函数,从而实现GPU加速。
它本质上还是需要写C++代码的,主要是去实现核函数部分。
1 | import pycuda.driver as cuda |
但是上述过程可以看出来,矩阵传递还是需要拉平成一维数组的样子传进去,而且这样子不能去使用CUDA的非常丰富的生态库,比如cuBLAS、cuDNN等等,这样子就会比较麻烦。
对于我们之前的求解非常复杂的雅可比矩阵的例子,可以用pycuda来编写一个求解雅可比矩阵的核函数,每个cuda thread只求解雅可比矩阵中的一个元素。但是这样只能说它比python快,未必会比C++实现的同等串行代码要快,因为数据传输需要成本。另外还有一个弊端就是,想要在核函数里面求逆矩阵(这种也是迭代运算非常常见的操作)就会非常麻烦,适合不需要矩阵之间有复杂运算,只是简单的代数运算的那种算子,单独使用起来可能不是很方便。
pybind11
pybind11感觉可以是对于这种涉及到矩阵无法直接向量化处理时的一个大杀器了,它能直接把python的二维矩阵变成eigen3的矩阵!当然比如数组、列表什么的都不在话下。此外,pybind11还允许C++代码中可以使用CUDA。
pybind11也是把C++代码编译成一个动态链接库,只是它跟ctypes不一样,它需要包含python的头文件,并链接python的两个lib,所以他是跟pyhton版本绑定的,谁让他交互起来那么方便呢。另外,pybind11跟eigen3一样,都是head-only的库,所以用起来非常方便。当然,它的编译安装和编码的注意就不提了,网上很容易找到资料。
1 |
|
然后编译成动态库后,改成pyd后缀,就可以当作一个包来调用了
1 | import example |
能够把numpy的矩阵直接转成eigen3能省去很多力,使用eigen3的静态矩阵,能够在编译期间把大部分工作都给做了,最大化的提升执行时间,而且eigen3作为完善的矩阵库,各种处理基本直接调用,广播机制也很完善,感觉跟写python的numpy差不了多少。
另外,转成eigen3的话,做batch之间的矩阵运算也相对容易了一点,因为不需要去考虑向量化操作,可以肆无忌惮的遍历,增加不了多少开销。
不过值得一提的是,pybind11只能够完美衔接最多就是2维矩阵,而如果是图像处理的话,在网络中出现的往往是b*m*n这种三维张量。遇到这种情况可以把高维张量拉平后传进来,然后在C++代码里手动组装。不过实测的话,直接遍历batch_size维度,然后一次只传输一个样本,这样耗时也是可以接受的,大致是下面这种调用
1 | import torch |
总结
介绍了ctypes、Cython、pyCUDA和pybind11这几种方法。推荐轻量级低耦合的C++代码接口用ctypes,而需要处理矩阵相关的东西直接用pybind11就好了。