使用 Cythonized Python Wheels 指定精确的 CPU 指令集

Specifying Exact CPU Instruction Set with Cythonized Python Wheels

我有一个 Python 包,带有由 Cython 编译的本机扩展。由于一些性能需求,编译是用 -march=native, -mtune=native 标志完成的。这基本上使编译器能够使用任何可用的 ISA 扩展。

此外,我们保留了该软件包的非 cythonized 纯 python 版本。它应该在对性能不太敏感的环境中使用。

因此,我们总共发布了两个版本:

一些其他包依赖于这个包,一些机器与编译包的机器有点不同。由于我们使用 -march=native,结果我们得到 SIGILL,因为服务器上缺少一些 ISA 扩展。

所以,本质上,如果主机 CPU 与 wheel 不兼容,我想以某种方式让 pip 忽略本机 wheel。

原生 wheel 确实有 cp37 和平台名称,但我在这里看不到定义更精细的 ISA 要求的方法。我总是可以为 pip 使用 --implementation 标志,但我想知道是否有更好的方法让 pip 区分不同的 ISA。

谢谢,

pip 基础设施不支持这种粒度。

我认为更好的方法是编译两个版本的 Cython 扩展:有 -march=native 和没有,安装两个版本并在 运行 时间决定应该安装哪个版本已加载。

这是一个概念证明。

第一个要跳的箍:如何在运行时检查CPU/OS组合支持哪些指令。为简单起见,我们将检查 AVX(此 SO-post has more details) and I offer only a gcc-specific (see also this)解决方案 - 称为 impl_picker.pyx:

cdef extern from *:
    """
    int cpu_supports_avx(void){
        return __builtin_cpu_supports("avx");
    }
    """
    int cpu_supports_avx()

def cpu_has_avx_support():
    return cpu_supports_avx() != 0

第二个问题:pyx文件和模块必须同名。为避免代码重复,实际代码在 pxi 文件中:

# worker.pxi
cdef extern from *:
    """   
    int compiled_with_avx(void){
        #ifdef __AVX__
            return 1;
        #else
            return 0;
        #endif
    }
    """
    int compiled_with_avx()

def compiled_with_avx_support():
    return compiled_with_avx() != 0

正如你所看到的,函数 compiled_with_avx_support,这取决于它是否使用 -march=native 编译。

现在我们可以通过包含 *.pxi 文件中的实际代码来定义模块的两个版本。一个名为 worker_native.pyx 的模块:

# distutils: extra_compile_args=["-march=native"]

include "worker.pxi"

worker_fallback.pyx

include "worker.pxi"

构建一切,例如via cythonize -i -3 *.pyx,可以这样使用:

from impl_picker import cpu_has_avx_support

# overhead once when imported:
if cpu_has_avx_support():
    import worker_native as worker
else:
    print("using fallback worker")
    import worker_fallback as worker

print("compiled_with_avx_support:", worker.compiled_with_avx_support())

在我的机器上,上述会导致 compiled_with_avx_support: True,在旧机器上,将使用“较慢的”worker_fallback,结果将是 compiled_with_avx_support: False


这个 post 的目标不是给出一个工作 setup.py,而只是概述如何在 运行 时实现选择正确版本的目标.显然, setup.py 可能要复杂得多:例如需要使用不同的编译器设置编译多个 c 文件(请参阅此 SO-post,这是如何实现的)。