我想在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文件中应该使用哪个名称。
答案 0 :(得分:2)
依靠链接器/加载器提供正确的blas功能的另一种方法是模拟必要的blas符号(例如ddot
)的解析,并在转换过程中使用包装的blas-function provided by scipy运行时。
不确定,这种方法优于“常规的”构建方法,但是希望引起您的注意,即使只是因为我发现这种方法很有趣。
简而言之:
ddot
-功能,称为my_ddot
。my_ddot
指针,否则将使用ddot
。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=[])]
)