作为一个copyer,下载了别人的代码首先就是要编译一些扩展库,如果编译不成功,就运行不了,用别人的代码都不会,实在不是一个称职的copyer,所以还是决心把各种扩展方法学习一下。
由于设计到很多办法,其中又有很多细节,所以这篇笔记特别的冗杂,前方高能,小心驾驶。
前面读了两个cu(CUDA)文件,要把这些扩展让python能够调用,还需要编译安装,这个过程方法多种多样,花样繁多,每个源码库采用的编译和安装方式都不一样。不过最终基本都是要求执行类似这样的命令:
python setup.py build_ext --inplace
#或sh make.sh
运气好的话就能成功编译了,运气不好的话会报一堆的错误,可能需要做一定的修改,要么改环境,要么改代码,要么重新找一份能编译通过的源码,因此了解一下编译安装c扩展的基本知识还是有必要的。
一、原始的方式
其他方式都是对原始方式的包装简化,所以先了解一下原始的方式是啥样子。
比如用C实现了一个函数:
int add(int a,int b) {
return a + b;
}
这个函数python直接是用不了的,需要进行一系列的包装:
//test.c//1,这个头文件必须要的#include "Python.h"//2, 函数主体int add(int a,int b) {
return a + b;
}
//3, 包裹函数static PyObject *Exten_add(PyObject *self,PyObject *args) {
int a,b;
// 获取数据,i代表int,ii代表两个int // 如果没有获取到,则返回NULL if (!PyArg_ParseTuple(args,"ii",&a,&b)) {
return NULL;
}
return (PyObject*)Py_BuildValue("i",add(a,b));
}
//4, 添加PyMethodDef ModuleMethods[]数组static PyMethodDef ExtenMethods[] = {
// add:可用于Python调用的函数名,Exten_add:C++中对应的函数名 {"add",Exten_add,METH_VARARGS},
{NULL,NULL},
};
//5, 初始化函数static struct PyModuleDef ExtenModule = {
PyModuleDef_HEAD_INIT,
"Exten",//模块名称 NULL,
-1,
ExtenMethods
};
//6.初始化模块//注意:函数名称PyInit_Exten要与模块名称Exten匹配void PyInit_Exten() {
PyModule_Create(&ExtenModule);
}
还么结束,还需要写一个setup.py文件:
#setup.py
from distutils.core import setup,Extension
#这里改为from setuptools import setup, Extension也可以,通常没什么区别
#distutils是python标准库的一部分,setuptools是distutils的增强版。具体我也不清楚,得看文档吧
MOD = 'Exten' #模块名
setup(name=MOD,ext_modules=[Extension(MOD,sources=['test.c'])])
现在只要执行
python setup.py build
就编译好了,可以用一段代码测试一下
#test.py
import Exten
print(Exten.add(1,3))
但是,说实在话,这样的代码看着有点让人难受,编程本来是一件非常美好的事情。。。
而且,C++是调用不了的,要调用C++,还得加一层C包装函数。。。
所以就有了很多的种方式来简化包装过程。下面介绍几种我看到的方式(都是在不同版本的faster-rcnn源码库中看到的)
二、torch.utils.ffi
这种方式是pytorch实现的,优点是可以用pytorch的底层aten库,与pytorch深度融合,缺点只能在0.4中使用,到了pytorch1.0就已经废弃了,被torch.utils.cpp_extension取代。
三、torch.utils.cpp_extension
这种方式实现的优点是包装了C++/cuda,在C++代码里用pybind11把C++包装成C接口,不用再为C++发愁,而且与pytorch深度融合,让代码与标准库无异,比如自动微分。所以这是pytorch推荐的方式。
看看pybind11如何包装C++接口的吧:
//vision.cpp// Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.#include "nms.h"#include "ROIAlign.h"#include "ROIPool.h"#include "SigmoidFocalLoss.h"#include "deform_conv.h"#include "deform_pool.h"
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("nms", &nms, "non-maximum suppression");
m.def("roi_align_forward", &ROIAlign_forward, "ROIAlign_forward");
m.def("roi_align_backward", &ROIAlign_backward, "ROIAlign_backward");
m.def("roi_pool_forward", &ROIPool_forward, "ROIPool_forward");
m.def("roi_pool_backward", &ROIPool_backward, "ROIPool_backward");
m.def("sigmoid_focalloss_forward", &SigmoidFocalLoss_forward, "SigmoidFocalLoss_forward");
m.def("sigmoid_focalloss_backward", &SigmoidFocalLoss_backward, "SigmoidFocalLoss_backward");
// dcn-v2 m.def("deform_conv_forward", &deform_conv_forward, "deform_conv_forward");
m.def("deform_conv_backward_input", &deform_conv_backward_input, "deform_conv_backward_input");
m.def("deform_conv_backward_parameters", &deform_conv_backward_parameters, "deform_conv_backward_parameters");
m.def("modulated_deform_conv_forward", &modulated_deform_conv_forward, "modulated_deform_conv_forward");
m.def("modulated_deform_conv_backward", &modulated_deform_conv_backward, "modulated_deform_conv_backward");
m.def("deform_psroi_pooling_forward", &deform_psroi_pooling_forward, "deform_psroi_pooling_forward");
m.def("deform_psroi_pooling_backward", &deform_psroi_pooling_backward, "deform_psroi_pooling_backward");
}
是不是优雅了很多?这14个函数如果用原始方式包装得该有多难看。
再写一个极简的玩具例程:
1、在头文件test.h中定义一个函数add
//这里#include 是必须的,里面包含了pybind11,还有pytorch的基础库aten等等
#include
int add(int a,int b);
2、test.cpp中实现这个函数,并用pybind11包装接口
//这里可以加#include "test.h",也可以不要
int add(int a,int b) {
return a + b;
}
3、写一个包装文件vision.cpp
#include "test.h"
//包装接口
PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) {
m.def("add", &add, "TEST toy");
}
当然,2和3可以放进一个文件里,不过分开写更模块化一些
4、setup.py文件中用setuptools和torch.utils.cpp_extension写编译安装过程
from setuptools import setup
from torch.utils.cpp_extension import BuildExtension, CppExtension
setup(
name='test_cpp', # 模块名称,需要在python中调用
version="0.1",
ext_modules=[
CppExtension('test_cpp', sources=["test.cpp"], include_dirs=["."]),
],
cmdclass={
'build_ext': BuildExtension
}
)
完成!Great,非常简洁!
只需要执行
python setup.py build_ext --inplace
写一个测试代码看看
#注意:如果#include ,
#那么在使用的时候,先import torch,
#否则在cpp与cu混编的时候import test_cpp会失败
import torch
import test_cpp
print(test_cpp.add(1,2))
pybind11本身是独立于pytorch的,ertorch.utils.cpp_extension中的BuildExtension, CppExtension,CUDAExtension帮助我们生成扩展代码,不需要处理复杂的编译命令,环境变量,源文件目录,头文件目录,库文件目录,gcc、nvcc编译选项……一切都有了默认配置(自己配置也可以),用起来特别爽!关键是——注意到没有,这还是个C++文件而不是C文件!
四、纯pybind11:
纯pybind11没看到哪个实现里用,不过pybind11实在太好了,所以这里按照文档写一个helloword:
如果没有安装pybind11,就先:
pip install pybind11
1.写一个example.cpp 文件:
#include
int add(int i, int j) {
return i + j;
}
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring m.def("add", &add, "A function which adds two numbers");
}
2.编译:
c++ -O3 -Wall -shared -std=c++11 -fPIC `python3 -m pybind11 --includes` example.cpp -o example`python3-config --extension-suffix`
//windows:nvcc --shared example.cpp -o example.pyd -I D:\Miniconda3\include
My GOD!就这样成功了!
测试一个:
import example
print(example.add(1, 2))
OK,没问题,这也太爽了吧!
与cuda混合编程:
文档):
1、test.h:头文件
#include #include #include #include
using std::vector;
namespace py = pybind11;
py::array_t add(py::array_t a_h,py::array_t b_h);
2、sum_arrays.cu:两个整数数组相加
#include #include #include "test.h"#define CHECK(call)\{\const cudaError_t error=call;\if(error!=cudaSuccess)\{\printf("ERROR: %s:%d,",__FILE__,__LINE__);\printf("code:%d,reason:%s\n",error,cudaGetErrorString(error));\exit(1);\}\} __global__ void sumArraysGPU(int*a,int*b,int*res,int nElem)
{
int i=blockIdx.x*blockDim.x+threadIdx.x;
if(i
{
res[i]=a[i]+b[i];
}
}
py::array_t add(py::array_t a_h,py::array_t b_h)
{
py::buffer_info buf1 = a_h.request();
py::buffer_info buf2 = b_h.request();
int nElem=buf1.shape[0];
int nByte=sizeof(int)*nElem;
int dev = 0;
cudaSetDevice(dev);
int *a_d,*b_d,*res_d;
CHECK(cudaMalloc((int**)&a_d,nByte));
CHECK(cudaMalloc((int**)&b_d,nByte));
CHECK(cudaMalloc((int**)&res_d,nByte));
CHECK(cudaMemcpy(a_d,(int*)buf1.ptr,nByte,cudaMemcpyHostToDevice));
CHECK(cudaMemcpy(b_d,(int*)buf2.ptr,nByte,cudaMemcpyHostToDevice));
dim3 block(32);
dim3 grid(nByte/block.x+nByte%block.x>0);
sumArraysGPU<<>>(a_d,b_d,res_d,nElem);
py::array_t result = py::array_t(nElem);
py::buffer_info buf3 = result.request();
CHECK(cudaMemcpy(buf3.ptr,res_d,nByte,cudaMemcpyDeviceToHost));
cudaFree(a_d);
cudaFree(b_d);
cudaFree(res_d);
return result;
}
3、example.cpp(或者example.cu)
#include "test.h"
PYBIND11_MODULE(example, m) {
m.doc() = "pybind11 example plugin"; // optional module docstring m.def("add", &add, "A function which adds two numbers");
}
nvcc编译
nvcc --shared -Xcompiler -fPIC sum_arrays.cu example.cpp -o example.so -I /home/fms/anaconda3/include/python3.7m
windows:
nvcc --shared -Xcompiler -fPIC sum_arrays.cu example.cpp -o example.pyd
编译最好还是写一个setup.py(这样在Windows等各种环境下基本都是相同的,否则编译命令在每个环境都不一样,缺乏移植性):
from setuptools import setup
from torch.utils.cpp_extension import CUDAExtension,BuildExtension
setup(
name="example",
ext_modules=[
CUDAExtension(
"example",
sources=["example.cpp","sum_arrays.cu"],
)
],
cmdclass={"build_ext": BuildExtension},
)
#然后运行:python setup.py build_ext --inplace
这里的
cpp和cu文件千万别同名,否则会编译错误,这里虽然用到了CUDAExtension,BuildExtension,但这只是编译工具,就是只在编译的时候用到了prtorch,所以在使用不用import torch也能运行,说明脱离了pytorch环境,是一个纯pybind11绑定的程序。
test.py:测试一下
import example
import numpy as np
a=np.array([1,2,3],dtype=np.int32)
b=np.array([100,2,3],dtype=np.int32)
c=example.add(a,b)
print(c)
更复杂的例程(使用numpy数组)请看fmscole/benchmarkgithub.com
五、用cython作为包装工具
import numpy as np
cimport numpy as np
assert sizeof(int) == sizeof(np.int32_t)
cdef extern from "gpu_nms.hpp":
void _nms(np.int32_t*, int*, np.float32_t*, int, int, float, int)
def gpu_nms(np.ndarray[np.float32_t, ndim=2] dets, np.float thresh,
np.int32_t device_id=0):
cdef int boxes_num = dets.shape[0]
cdef int boxes_dim = dets.shape[1]
cdef int num_out
cdef np.ndarray[np.int32_t, ndim=1] \
keep = np.zeros(boxes_num, dtype=np.int32)
cdef np.ndarray[np.float32_t, ndim=1] \
scores = dets[:, 4]
cdef np.ndarray[np.int_t, ndim=1] \
order = scores.argsort()[::-1]
cdef np.ndarray[np.float32_t, ndim=2] \
sorted_dets = dets[order, :]
_nms(&keep[0], &num_out, &sorted_dets[0, 0], boxes_num, boxes_dim, thresh, device_id)
keep = keep[:num_out]
return list(order[keep])
在setup.py里需要把pyx文件和cu文件都扩展源文件里:
Extension('nms.gpu_nms',
sources=['nms/nms_kernel.cu', 'nms/gpu_nms.pyx'],
library_dirs=[CUDA['lib64']],
libraries=['cudart'],
language='c++',
runtime_library_dirs=[CUDA['lib64']],
# this syntax is specific to this build system
# we're only going to use certain compiler args with nvcc and not with gcc
# the implementation of this trick is in customize_compiler() below
extra_compile_args={'gcc': ["-Wno-unused-function"],
'nvcc': ['-arch=sm_52',
'--ptxas-options=-v',
'-c',
'--compiler-options',
"'-fPIC'"]},
include_dirs = [numpy_include, CUDA['include']]
)
感觉也不简单啊!而且Cython的语法是独立的需要额外学习,而且比较琐碎,看看这类型定义:
cdef extern ...
np.int32_t
np.int32_t*
np.float32_t* #c与python好混合啊
np.ndarray[np.float32_t, ndim=2] dets
cdef np.ndarray[np.float32_t, ndim=1] scores = dets[:, 4]
是不是也觉得很蛋疼啊,还是pybind11大法好,不用操心这么多数据类型转换问题!
优点是本身就可以是C加速了,有些c文件就不需要了,写好cu文件就好了。总体来看,还是一种很不错的方案。
把cython包装的过程再撸一遍:
1. 头文件test.h,定义一个c的方法
//注意函数名称是 c_add而不是add,是为了不与pyx文件中的对外接口add冲突
int c_add(int a, int b);
2. 实现c方法,source.c,注意文件名不能为test.c,因为后面的test.pyx会编译出一个test.c文件出来,会把这个文件覆盖掉
//注意,这里不需要#include "test.h",不过加上去好像也行
int c_add(int a,int b) {
return a + b;
}
3. cython本身的pyx文件test.pyx,调用c方法
import numpy as np
cimport numpy as np
np.import_array()
cdef extern from "test.h":
int c_add( int a, int b)
#接口转接,add转接到c_add
def add( a, b):
return c_add(a,b)
4. python执行编译的文件setup.py
from distutils.core import setup, Extension
from Cython.Distutils import build_ext
import numpy
setup(
cmdclass={'build_ext': build_ext},
ext_modules=[
Extension("Ext",
#注意要把pyx和c源文件文件交给sources
sources=["test.pyx", "source.c"],
#如果要用numpy的话,需要通过numpy.get_include()把头文件目录给include_dirs
include_dirs=[numpy.get_include()])
],
)
#python setup.py build
#或python setup.py install
哎,注意点还是蛮多的。
执行 python setup.py build_ext --inplace 可以编译成功,但有一个链接警告,不知道为什么,不影响结果:
test.obj : warning LNK4197: 多次指定导出“PyInit_Ext”;使用第一个规范
好吧,看我这一知半解写的hello word不如看真正的好文
六、用ctypes库调用动态库,没看到那个faster-rcnn的实现里用这个,估计有些什么问题,比如数据类型、平台兼容性,具体会有什么问题我也不知道,不探究了,跟着大佬们走就是了,省去很多时间,不如好好学习算法,这是我学习numba得来的教训,后面会提到numba。
七、用SWIG包装,相当于要学习一门新的接口语言,估计嫌复杂,也没看到哪个faster-rcnn实现里用。
八、Boost.Python,也没看到有哪个faster-rcnn实现用,估计被pybind11给取代了。
九、cffi也没看到有哪个faster-rcnn实现在用。据说是因为只能gcc编译,不支持msvc编译,window平台下嫌烦。
十、cupy:
simple-faster-rcnn-pytorch 这个实现在用,直接调用cu源码,不用编译安装(运行的时候cupy自己负责编译),很简单。
其实pytorch也能这样直接load源码,不需要编译安装这个过程,但是不能有外部依赖。
十一、numba
我自己试过,也相当于要学一门python子集语言,也没看到有哪个faster-rcnn实现里用,嫌烦吧,关键是性能不咋样,不如直接编译cu文件性能好,差距很大。
优点是只要一行代码就能实现加速python代码,但不是所有的python代码,估计没多少人愿意研究它到底能加速哪些代码,改代码可能比写代码还烦人,毕竟学习需要付出时间成本,到后来性能还打折,所以感觉用处不大。
为了证明我的确做过这种无用功,贴一下nms用numba加速的两种实现:
cpu版nms
from __future__ import absolute_import
import numba
import numpy as np
@numba.jit(nopython=True)
def nms_cpu(dets, thresh):
x1 = dets[:, 0]
y1 = dets[:, 1]
x2 = dets[:, 2]
y2 = dets[:, 3]
scores = dets[:, 4]
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
order = scores.argsort()[::-1]
keep = []
while order.size > 0:
i = order[0]
keep.append(i)
xx1 = np.maximum(x1[i], x1[order[1:]])
yy1 = np.maximum(y1[i], y1[order[1:]])
xx2 = np.minimum(x2[i], x2[order[1:]])
yy2 = np.minimum(y2[i], y2[order[1:]])
w = np.maximum(0.0, xx2 - xx1 + 1)
h = np.maximum(0.0, yy2 - yy1 + 1)
inter = w * h
ovr = inter / (areas[i] + areas[order[1:]] - inter)
inds = np.where(ovr <= thresh)[0]
order = order[inds + 1]
return keep
if __name__ == "__main__":
bbox=np.load("bbox.npy")
print(bbox.shape)
keep=nms_cpu(bbox,0.7)
print(len(keep))
GPU版nms
from __future__ import absolute_import
from numba import guvectorize,vectorize,cuda
import numpy as np
import numba as nb
import torch
@cuda.jit(device=True)
def DIVUP(m,n):
return ((m) // (n) + ((m) % (n) > 0))
@cuda.jit(device=True)
def devIoU(bbox_a,bbox_b):
top = max(bbox_a[0], bbox_b[0])
bottom = min(bbox_a[2], bbox_b[2])
left = max(bbox_a[1], bbox_b[1])
right = min(bbox_a[3], bbox_b[3])
height = max(bottom - top, 0)
width = max(right - left, 0)
area_i = height * width
area_a = (bbox_a[2] - bbox_a[0]) * (bbox_a[3] - bbox_a[1])
area_b = (bbox_b[2] - bbox_b[0]) * (bbox_b[3] - bbox_b[1])
return area_i / (area_a + area_b - area_i)
@cuda.jit
def nms_kernel(n_bbox, dev_bbox,dev_mask):
pass
thresh=0.7
row_start = cuda.blockIdx.y
col_start = cuda.blockIdx.x
threadsPerBlock =64
row_size =min(n_bbox - row_start * threadsPerBlock, threadsPerBlock)
col_size =min(n_bbox - col_start * threadsPerBlock, threadsPerBlock)
block_bbox=cuda.shared.array((256),dtype=nb.float32)
if cuda.threadIdx.x < col_size:
block_bbox[cuda.threadIdx.x * 4 + 0] =dev_bbox[(threadsPerBlock * col_start + cuda.threadIdx.x) * 4 + 0]
block_bbox[cuda.threadIdx.x * 4 + 1] =dev_bbox[(threadsPerBlock * col_start + cuda.threadIdx.x) * 4 + 1]
block_bbox[cuda.threadIdx.x * 4 + 2] =dev_bbox[(threadsPerBlock * col_start + cuda.threadIdx.x) * 4 + 2]
block_bbox[cuda.threadIdx.x * 4 + 3] =dev_bbox[(threadsPerBlock * col_start + cuda.threadIdx.x) * 4 + 3]
cuda.syncthreads()
if cuda.threadIdx.x < row_size:
cur_box_idx = threadsPerBlock * row_start + cuda.threadIdx.x
cur_box = dev_bbox[cur_box_idx * 4:cur_box_idx * 4+4]
i = 0
t = np.int64(0)
start = 0
if row_start == col_start:
start = cuda.threadIdx.x + 1
for i in range( start ,col_size ):
if devIoU(cur_box, block_bbox [i * 4:i * 4+4]) >= thresh:
t |= np.int64(1)<< i
col_blocks = DIVUP(n_bbox, threadsPerBlock)
dev_mask[cur_box_idx * col_blocks + col_start] = t
if __name__ == "__main__":
bbox=np.load("bbox.npy")
n_bbox = bbox.shape[0]
threads_per_block = 64
col_blocks = np.ceil(n_bbox / threads_per_block).astype(np.int32)
blocks = (col_blocks, col_blocks, 1)
threads = (threads_per_block, 1, 1)
mask_dev = cuda.device_array((n_bbox * col_blocks,), dtype=np.uint64)
bbox=bbox.reshape(-1)
bbox =cuda.to_device(bbox)
nms_kernel[blocks, threads](n_bbox, bbox, mask_dev)
mask_host=mask_dev.copy_to_host()
print(mask_host)
性能大概是这样:
numpy版:115ms
numba的cpu版:23ms
numba的gpu版:20.9ms,my god!跟cpu版差不多啊.
cupy加载cu字符串源码版:1.33ms,这才像GPU加速啊。
我开始以为是自己那个地方不符合numba的标准,去github上提问numba/issues/4531,才发现别人也有这样的问题,开发人员是非常友好的给了回应,nice!cuda加速时灵时不灵,差的时候跟cpu差不多,真的是个渣,不提了。但反过来说,对python的cpu加速,的确是非常的简单,一行代码搞定,而且非常有效的,不过要知道它能加速的边界在哪里,还是要付出学习成本,如果想用它做cuda开发,立刻劝退!