将Cython包装的C函数链接到来自NumPy的BLAS库

3

我希望在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库,并且希望它可以作为PIP的依赖项,在不同操作系统下使用numpy或scipy进行安装,而无需任何额外的BLAS相关依赖关系。
是否有任何hack方法可以在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_。上面的代码与l:libopenblas一起使用将起作用,如果我在.c文件中将ddot更改为ddot_。 我仍在想是否有某种(理想情况下是运行时)机制来检测应在c文件中使用哪个名称。

1
scipy已经包装了BLAS函数,您可以直接使用它 https://docs.scipy.org/doc/scipy-0.19.0/reference/linalg.cython_blas.html#module-scipy.linalg.cython_blas - ead
但这是为了在Cython中使用它们(在.pyx文件中)。那么,它是否可以以某种方式传递到.c文件中的C函数中? - anymous.asker
3个回答

4
依赖于链接器/加载器提供正确的blas功能的替代方法是模拟解析所需的blas符号(例如ddot)并在运行时使用scipy提供的包装的blas函数
不确定这种方法是否优于“正常方式”构建,但我想让您了解这种方法,即使只是因为我认为这种方法很有意思。
简而言之,思路如下:
1. 在下面的片段中定义一个显式函数指针my_ddot,用于ddot功能。 2. 在其他情况下使用my_ddot指针替代ddot。 3. 在加载cython模块时使用scipy提供的功能初始化my_ddot指针。
以下是一个可行的原型(我使用C代码原样引用以使片段独立并且可以在jupiter-notebook中轻松测试,相信您可以将其转换为需要/喜欢的格式):
%%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甚至自己实现的引擎),而无需重新编译/链接。
然而,缺点是需要编写额外的样板代码,并且有可能会忘记某些符号的初始化。

0

我终于想出了一个丑陋的解决方法。我不确定它是否总是有效,但至少对于Windows(mingw和visual studio)、Linux、MKL和OpenBlas的组合有效。我仍然想知道是否有更好的替代方案,但如果没有,这个方法就可以:

编辑:现在已经为visual studio进行了更正

  1. 修改C文件以考虑带下划线的名称(对每个调用的BLAS函数都要这样做)-需要声明每个函数两次并为每个函数添加一个if语句。

    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)

    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)

    ...等等

  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://dev59.com/kHRB5IYBdhLWcg3wFz8g
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=[])]
    )

我认为如果您使用libraries选项,就不需要构造正确的名称 - distutils会为您完成:https://docs.python.org/2/distutils/setupscript.html#library-options - ead

0
作为使用较新版本Cython的另一种选择,可以创建一个“公共”Cython函数(将可用于C代码并自动生成公共头文件),该函数将简单地调用相应的BLAS函数:
from scipy.linalg.cython_blas cimport ddot
cdef public double ddot_(int *n, double *x, int *ldx, double *y, int *ldy):
    return ddot(n, x, ldx, y, ldy)

然后在C代码中声明它或包含头文件,Cython扩展构建器的其余部分将负责链接:

extern double ddot_(int *n, double *x, int *ldx, double *y, int *ldy);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接