从NumPy将Cython包装的C函数与BLAS链接

时间:2018-10-20 12:04:55

标签: numpy cython blas

我想在Cython扩展中使用.c文件中定义的一些C函数,这些函数使用BLAS子例程,例如

cfile.c

double ddot(int *N, double *DX, int *INCX, double *DY, int *INCY);

double call_ddot(double* a, double* b, int n){
    int one = 1;
    return ddot(&n, a, &one, b, &one);
}

(假设这些函数的作用不只是调用一个BLAS子例程)

pyfile.pyx

cimport numpy as np
import numpy as np

cdef extern from "cfile.c":
    double call_ddot(double* a, double* b, int n)

def pyfun(np.ndarray[double, ndim=1] a):
    return call_ddot(&a[0], &a[0], <int> a.shape[0])

setup.py:

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import numpy

setup(
    name  = "wrapped_cfun",
    packages = ["wrapped_cfun"],
    cmdclass = {'build_ext': build_ext},
    ext_modules = [Extension("wrapped_cfun.cython_part", sources=["pyfile.pyx"], include_dirs=[numpy.get_include()])]
)

我希望此软件包链接到已安装的NumPy或SciPy使用的同一BLAS库,并且希望可以在以numpy或scipy作为依赖项的不同操作系统下从PIP安装该文件,而无需任何其他与BLAS相关的依赖项

setup.py是否有任何破解方法可以使其与任何BLAS实施均能配合使用?

更新: 使用MKL,我可以通过修改Extension对象使其指向libmkl_rt使其工作,如果安装了MKL,则可以从numpy中提取它,例如: Extension("wrapped_cfun.cython_part", sources=["pyfile.pyx"], include_dirs=[numpy.get_include()], extra_link_args=["-L{path to python's lib dir}", "-l:libmkl_rt.{so, dll, dylib}"]) 但是,同一技巧不适用于OpenBLAS(例如-l:libopenblasp-r0.2.20.so)。如果该文件是指向libopenblas的链接,则指向libblas.{so,dll,dylib}无效,但是指向它是指向libmkl_rt的链接,效果很好。

更新2: OpenBLAS似乎在其C函数的最后加上一个下划线,例如不是ddot,而是ddot_。如果我在.c文件中将l:libopenblas更改为ddot,则上面带有ddot_的代码将起作用。我仍然想知道是否有某种(理想的运行时)机制来检测在c文件中应该使用哪个名称。

2 个答案:

答案 0 :(得分:2)

依靠链接器/加载器提供正确的blas功能的另一种方法是模拟必要的blas符号(例如ddot)的解析,并在转换过程中使用包装的blas-function provided by scipy运行时。

不确定,这种方法优于“常规的”构建方法,但是希望引起您的注意,即使只是因为我发现这种方法很有趣。

简而言之:

  1. 在以下代码段中将显式功能指针定义为ddot-功能,称为my_ddot
  2. 使用my_ddot指针,否则将使用ddot
  3. 在cython模块加载了scipy提供的功能后,初始化my_ddot指针。

这是一个可行的原型(我使用C-code-verbatim使代码段独立并且可以在木星笔记本中轻松测试,相信您可以将其转换为所需的格式/喜欢的格式:

%%cython
# h-file:
cdef extern from *:
    """
    // blas-functionality,
    // will be initialized by cython when module is loaded:
    typedef double (*ddot_t)(int *N, double *DX, int *INCX, double *DY, int *INCY);
    extern ddot_t my_ddot;

    double call_ddot(double* a, double* b, int n);
    """
    ctypedef double (*ddot_t)(int *N, double *DX, int *INCX, double *DY, int *INCY)
    ddot_t my_ddot
    double call_ddot(double* a, double* b, int n)    

# init the functions of the c-library
# with blas-function provided by scipy
from scipy.linalg.cython_blas cimport ddot
my_ddot=ddot

# a simple function to demonstrate, that it works
def ddot_mult(double[:]a, double[:]b):
    cdef int n=len(a)
    return call_ddot(&a[0], &b[0], n)

#-------------------------------------------------
# c-file, added so the example is complete    
cdef extern from *:
    """  
    ddot_t my_ddot;
    double call_ddot(double* a, double* b, int n){
        int one = 1;
        return my_ddot(&n, a, &one, b, &one);
    }
    """
    pass

现在可以使用ddot_mult

import numpy as np
a=np.arange(4, dtype=float)

ddot_mult(a,a)  # 14.0 as expected!

这种方法的优点是,distutils不会有麻烦,您可以保证使用与scipy相同的blas功能。

另一种好处:在运行时可以切换使用的引擎(mkl,open_blas甚至是自己的实现),而无需重新编译/重新链接。

另一方面,还有一些额外的样板代码,还有可能会忘记初始化某些符号的危险。

答案 1 :(得分:0)

我终于找到了一个丑陋的骇客。我不确定它是否会一直有效,但是至少它适用于Windows(mingw和Visual Studio),Linux,MKL和OpenBlas。我仍然想知道是否还有更好的选择,但是如果没有,这可以做到:

编辑:已针对Visual Studio进行了修正

1)修改C文件以使用带下划线的名称(对每个被调用的BLAS函数执行此操作)-需要将每个函数声明两次,并为每个函数添加一个if

#ifndef ddot
double ddot_(int *N, double *DX, int *INCX, double *DY, int *INCY);
#define ddot(N, DX, INCX, DY, INCY) ddot_(N, DX, INCX, DY, INCY)
#endif

#ifndef daxpy
daxpy_(int *N, double *DA, double *DX, int *INCX, double *DY, int *INCY);
#define daxpy(N, DA, DX, INCX, DY, INCY) daxpy_(N, DA, DX, INCX, DY, INCY)
#endif

... etc

2)从NumPy或SciPy中提取库路径,并将其添加到链接参数中。

3)检测要使用的编译器是否为Visual Studio,在这种情况下,链接参数完全不同。

setup.py

from distutils.core import setup
from distutils.extension import Extension
from Cython.Build import cythonize
from Cython.Distutils import build_ext
import numpy
from sys import platform
import os

try:
    blas_path = numpy.distutils.system_info.get_info('blas')['library_dirs'][0]
except:
    if "library_dirs" in numpy.__config__.blas_mkl_info:
        blas_path = numpy.__config__.blas_mkl_info["library_dirs"][0]
    elif "library_dirs" in numpy.__config__.blas_opt_info:
        blas_path = numpy.__config__.blas_opt_info["library_dirs"][0]
    else:
        raise ValueError("Could not locate BLAS library.")


if platform[:3] == "win":
    if os.path.exists(os.path.join(blas_path, "mkl_rt.lib")):
        blas_file = "mkl_rt.lib"
    elif os.path.exists(os.path.join(blas_path, "mkl_rt.dll")):
        blas_file = "mkl_rt.dll"
    else:
        import re
        blas_file = [f for f in os.listdir(blas_path) if bool(re.search("blas", f))]
        if len(blas_file) == 0:
            raise ValueError("Could not locate BLAS library.")
        blas_file = blas_file[0]

elif platform[:3] == "dar":
    blas_file = "libblas.dylib"
else:
    blas_file = "libblas.so"

## https://stackoverflow.com/questions/724664/python-distutils-how-to-get-a-compiler-that-is-going-to-be-used
class build_ext_subclass( build_ext ):
    def build_extensions(self):
        compiler = self.compiler.compiler_type
        if compiler == 'msvc': # visual studio
            for e in self.extensions:
                e.extra_link_args += [os.path.join(blas_path, blas_file)]
        else: # gcc
            for e in self.extensions:
                e.extra_link_args += ["-L"+blas_path, "-l:"+blas_file]
        build_ext.build_extensions(self)


setup(
    name  = "wrapped_cfun",
    packages = ["wrapped_cfun"],
    cmdclass = {'build_ext': build_ext_subclass},
    ext_modules = [Extension("wrapped_cfun.cython_part", sources=["pyfile.pyx"], include_dirs=[numpy.get_include()], extra_link_args=[])]
    )