链接 Cython 包装的 C 函数对抗来自 NumPy 的 BLAS



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

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 文件中将ddot更改为ddot_,上面带有l:libopenblas的代码将起作用。我仍然想知道是否有一些(理想的运行时(机制来检测 c 文件中应该使用哪个名称。

依靠链接器/加载器来提供正确的blas功能的另一种方法是模拟必要的blas符号的分辨率(例如ddot(,并在运行时使用 Scipy 提供的包装 blas 函数。

不确定这种方法是否优于"正常方式"的建筑,但想引起您的注意,即使只是因为我觉得这种方法很有趣。

简而言之,这个想法:

  1. 定义一个指向ddot功能的显式函数指针,在下面的代码片段中称为my_ddot
  2. 否则使用my_ddot指针,否则使用ddot-。
  3. cython 模块加载 scipy 提供的功能时初始化my_ddot指针。

这是一个工作原型(我使用 C 代码逐字使片段独立且易于在 jupiter 笔记本中测试,相信您可以将其转换为您需要/喜欢的格式(:

%%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甚至自己的实现(,而无需重新编译/重新链接。

另一方面,还有一些额外的样板代码,还有危险,一些符号的初始化将被遗忘。

我终于想出了一个丑陋的技巧。我不确定它是否永远有效,但至少它适用于Windows(mingw和Visual Studio(,Linux,MKL和OpenBlas的cobmiation。我仍然想知道是否有更好的选择,但如果没有,这将做到这一点:

编辑:现在针对视觉工作室进行了更正

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

    双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://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=[])]
)

作为更新的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);

最新更新