python 源码编译 ffi. h文件无路径_三、faster-rcnn源码阅读:C/C++/CU扩展的编译和安装...

作为一个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/benchmark​github.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开发,立刻劝退!

你可能感兴趣的:(python,源码编译,ffi.,h文件无路径)