SIMD优化

NumPy提供了一组宏来定义 Universal Intrinsics 为了抽象出典型的特定于平台的内部函数,SIMD代码只需编写一次。有三层:

  • 代码是 书面 只有在使用通用的宏保护时,编译器才能识别它们。在NumPy中,它们用于构造多个ufunc循环。当前的策略是创建三个循环:一个循环是默认的,不使用内部函数。一种是使用架构所需的最小内部函数。第三个是使用尽可能多的内部函数集编写的。

  • AT 编译 同时,distutils命令用于根据用户选择和编译器支持定义要支持的最小和最大特性。适当的宏与平台/体系结构内部函数重叠,并编译三个循环。

  • AT 运行时导入 ,将探测CPU以获取受支持的内在特性集。使用一种机制来获取指向最合适函数的指针,这将是函数调用的指针。

编译的生成选项

  • --cpu-baseline :所需优化的最小集合。默认值为 min 它提供了最低限度的CPU功能,可以安全地运行在处理器系列的各种平台上。

  • --cpu-dispatch :已调度一组附加优化。默认值为 max -xop -fma4 它支持除AMD遗留功能(在X86的情况下)之外的所有CPU功能。

命令参数在中可用 buildbuild_clibbuild_ext . 如果 build_clibbuild_ext 的参数 build 将改为使用,它还保留默认值。

优化名称可以是CPU特性或一组特性,这些特性集合了多个特性或 special options 执行一系列程序。

下表显示了当前支持的优化,从最低到最高排序。

x86-CPU功能名称

名字

暗示

SSE

SSE2

SSE2

SSE

SSE3

SSE SSE2

SSSE3

SSE SSE2 SSE3

SSE41

SSE SSE2 SSE3 SSSE3

POPCNT

SSE SSE2 SSE3 SSSE3 SSE41

SSE42

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT

AVX

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42

XOP

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

FMA4

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

F16C

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2

AVX512CD

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F

x86-组名

名字

收集

暗示

AVX512_KNL

AVX512ER AVX512PF

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD

AVX512_KNM

AVX5124FMAPS AVX5124VNNIW AVX512VPOPCNTDQ

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_KNL

AVX512_SKX

AVX512VL AVX512BW AVX512DQ

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD

AVX512_CLX

AVX512VNNI

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX

AVX512_CNL

AVX512IFMA AVX512VBMI

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX

AVX512_ICL

AVX512VBMI2 AVX512BITALG AVX512VPOPCNTDQ

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512CD AVX512_SKX AVX512_CLX AVX512_CNL

IBM/POWER big endian-CPU功能名称

名字

暗示

VSX

VSX2

VSX

VSX3

VSX VSX2

IBM/POWER little endian—CPU功能名称

名字

暗示

VSX

VSX2

VSX2

VSX

VSX3

VSX VSX2

功能部件A37/V7

名字

暗示

NEON

NEON_FP16

NEON

NEON_VFPV4

NEON NEON_FP16

ASIMD

NEON NEON_FP16 NEON_VFPV4

ASIMDHP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDDP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDFHM

NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP

ARMv8/A64-CPU功能名称

名字

暗示

NEON

NEON_FP16 NEON_VFPV4 ASIMD

NEON_FP16

NEON NEON_VFPV4 ASIMD

NEON_VFPV4

NEON NEON_FP16 ASIMD

ASIMD

NEON NEON_FP16 NEON_VFPV4

ASIMDHP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDDP

NEON NEON_FP16 NEON_VFPV4 ASIMD

ASIMDFHM

NEON NEON_FP16 NEON_VFPV4 ASIMD ASIMDHP


虽然上述表格基于GCC编译器,但下表显示了其他编译器中的差异:

x86::英特尔编译器-CPU功能名称

名字

暗示

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD

注解

x86::Intel编译器不支持以下功能: XOP FMA4型

X86:微软Visual C/C++ + CPU特征名称

名字

暗示

FMA3

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C AVX2

AVX2

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3

AVX512F

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512CD AVX512_SKX

AVX512CD

SSE SSE2 SSE3 SSSE3 SSE41 POPCNT SSE42 AVX F16C FMA3 AVX2 AVX512F AVX512_SKX

注解

x86:微软Visual C/C++的以下功能不支持: AVX512_KNL AVX512_KNM

特殊选项

  • NONE :不启用功能

  • NATIVE :启用当前系统支持的所有CPU功能

    此操作基于编译器标志 (-march=native, -xHost, /QxHost

  • MIN :启用可在多种平台上安全运行的最低CPU功能:

    拱门

    返回

    x86

    SSE SSE2

    x86 64-bit mode

    SSE SSE2 SSE3

    IBM/POWER big-endian mode

    NONE

    IBM/POWER little-endian mode

    VSX VSX2

    ARMHF

    NONE

    ARM64 AARCH64

    NEON NEON_FP16 NEON_VFPV4 ASIMD

  • MAX :启用编译器和平台支持的所有CPU功能。

  • Operators-/+ :删除或添加功能,与选项一起使用 MAXMINNATIVE .

NOTES

  • CPU功能和其他选项不区分大小写。

  • 所需优化的顺序无关紧要。

  • 逗号或空格都可以用作分隔符,例如。 --cpu-dispatch =“avx2 avx512f”或 --cpu-dispatch =“avx2,avx512f”两者都有效,但参数必须用引号括起来。

  • 操作数 + 仅出于名义原因添加: --cpu-basline= "min avx2" 等于 --cpu-basline="min + avx2" . --cpu-basline="min,avx2" 等于 --cpu-basline`="min,+avx2"

  • 如果用户平台或编译器不支持CPU功能,则将跳过该功能,而不是引发致命错误。

  • 任何指定的CPU功能 --cpu-dispatch 如果它是CPU基线功能的一部分,将被跳过

  • 这个 --cpu-baseline 参数强制启用隐含特性,例如。 --cpu-baseline =“sse42”相当于 --cpu-baseline =“sse sse2 sse3 sse3 sse41 popcnt sse42”

  • 价值 --cpu-baseline 如果编译器本机标志 -march=native-xHostQxHost 通过环境变量启用 CFLAGS

  • 所需优化的验证过程 --cpu-baseline 不严格。例如,如果用户请求 AVX2 但是编译器不支持它,那么我们只需跳过它并返回编译器可以处理的最大优化,具体取决于 AVX2 ,让我们假设 AVX .

  • 用户应始终通过构建日志检查最终报告,以验证启用的功能。

特殊情况

相关CPU功能 :某些特殊情况迫使我们在某些编译器或体系结构中将某些功能链接在一起,导致无法单独构建它们。这些条件可分为以下两部分:

  • 体系结构兼容性 :需要调整某些CPU功能,这些功能保证由同一体系结构的连续几代支持,例如:

    • 在ppc64le上 VSX(ISA 2.06)VSX2(ISA 2.07) 因为第一代支持小端模式的是Power-8`(ISA 2.07),所以这两种模式相互暗示`

    • 阿龙64 NEON FP16 VFPV4 ASIMD 因为它们是硬件基线的一部分,所以相互暗示。

  • 兼容性编译 :并非所有 C/C++ 编译器为所有CPU功能提供独立的支持。例如, 英特尔 的编译器没有为 AVX2FMA3 ,这是有意义的,因为 AVX2 也支持 FMA3 反之亦然,但这种方法与其他方法不兼容 x86 来自的CPU AMDVIA . 因此,在C/C++编译器之间描述CPU特性的差异,如 tables above .

行为和错误

用法和示例

报告和跟踪

了解CPU调度,NumPy调度器是如何工作的?

numpydispatcher是基于多源代码编译的,这意味着获取某个源代码,并使用不同的编译器标志和不同的 C 影响代码路径的定义,以便根据所需的优化为每个编译对象启用某些指令集,然后将返回的对象组合在一起。

../../_images/opt-infra.png

此机制应支持所有编译器,并且不需要任何特定于编译器的扩展,但同时,它为正常编译添加了几个步骤,解释如下:

1-配置

在开始通过上面解释的两个命令参数构建源文件之前,由用户配置所需的优化:

  • --cpu-baseline :所需优化的最小集合。

  • --cpu-dispatch :已调度一组附加优化。

2-发现环境

在这一部分中,我们检查编译器和平台架构,并缓存一些中间结果,以加快重建。

3-验证请求的优化

通过对编译器进行测试,并根据所请求的优化查看编译器可以支持什么。

4-生成主配置头

生成的标头 _cpu_dispatch.h 包含在上一步中已验证的所需优化的所有指令集定义和标头。

它还包含用于定义numpython级模块属性的额外C定义 __cpu_baseline____cpu_dispaٍtch__ .

这个标题是什么?

示例头由gcc在X86机器上动态生成。编译器支持 --cpu-baseline="sse sse2 sse3"--cpu-dispatch="ssse3 sse41" ,结果如下。

// The header should be located at numpy/numpy/core/src/common/_cpu_dispatch.h
/**NOTE
 ** C definitions prefixed with "NPY_HAVE_" represent
 ** the required optimzations.
 **
 ** C definitions prefixed with 'NPY__CPU_TARGET_' are protected and
 ** shouldn't be used by any NumPy C sources.
 */
/******* baseline features *******/
/** SSE **/
#define NPY_HAVE_SSE 1
#include <xmmintrin.h>
/** SSE2 **/
#define NPY_HAVE_SSE2 1
#include <emmintrin.h>
/** SSE3 **/
#define NPY_HAVE_SSE3 1
#include <pmmintrin.h>

/******* dispatch-able features *******/
#ifdef NPY__CPU_TARGET_SSSE3
  /** SSSE3 **/
  #define NPY_HAVE_SSSE3 1
  #include <tmmintrin.h>
#endif
#ifdef NPY__CPU_TARGET_SSE41
  /** SSE41 **/
  #define NPY_HAVE_SSE41 1
  #include <smmintrin.h>
#endif

基线特征 是否通过配置了所需优化的最小集合 --cpu-baseline . 它们没有预处理器保护,并且一直处于启用状态,这意味着它们可以在任何源代码中使用。

这是否意味着NumPy的基础结构将编译器的基线特性标志传递给所有源代码?

当然,是的。但是 dispatch-able sources 区别对待。

如果用户指定 基线特征 在构建期间,但在运行时,计算机甚至不支持这些功能?编译后的代码是通过这些定义之一调用的,还是编译器本身根据提供的命令行编译器标志自动生成/向量化的特定代码段?

在加载NumPy模块的过程中,会有一个验证步骤来检测此行为。它将引发一个Python运行时错误来通知用户。这是为了防止CPU出现非法指令错误,从而导致segfault。

Dispatch-able features 是我们通过 --cpu-dispatch . 它们在默认情况下不被激活,并且总是由其他以 NPY__CPU_TARGET_ . C定义 NPY__CPU_TARGET_ 仅在中启用 dispatch-able sources .

5-可调度源和配置语句

可调度的来源是特殊的 C 可以使用不同的编译器标志和不同的 C 定义。这些会影响代码路径,以便根据“”为每个编译对象启用某些指令集 配置语句 “必须声明 C 评论(/**/) 从一个特殊的标记开始 @目标 在每个可调度源的顶部。同时,可调度源将被视为正常 C 如果命令参数禁用了优化,则返回源 --disable-optimization .

什么是配置语句?

配置语句是某种关键字组合在一起,以确定可调度源所需的优化。

例子:

/*@targets avx2 avx512f vsx2 vsx3 asimd asimdhp */
// C code

关键字主要表示通过配置的其他优化 --cpu-dispatch ,但也可以表示其他选项:

  • 目标组:预先配置的配置语句,用于从可调度源外部管理所需的优化。

  • 策略:用于更改默认行为或强制编译器执行某些操作的选项集合。

  • “baseline”:一个惟一的关键字表示通过 --cpu-baseline

Numpy's infrastructure handles dispatch-able sources in four steps

  • (A) Recognition: Just like source templates and F2PY, the dispatch-able sources requires a special extension *.dispatch.c to mark C dispatch-able source files, and for C++ *.dispatch.cpp or *.dispatch.cxx NOTE: C++ not supported yet.

  • (B) 解析和验证 :在此步骤中,通过配置语句逐个解析和验证上一步骤筛选的可调度源,以确定所需的优化。

  • (C) 包装 :这是NumPy的基础结构所采用的方法,事实证明,这种方法足够灵活,可以用不同的代码多次编译单个源代码 C 影响代码路径的定义和标志。这个过程是通过创建一个临时 C 与附加优化相关的每个所需优化的源代码,其中包含 C 定义并包括通过 C 指令 #include . 有关更多说明,请参阅AVX512F的以下代码:

    /*
     * this definition is used by NumPy utilities as suffixes for the
     * exported symbols
     */
    #define NPY__CPU_TARGET_CURRENT AVX512F
    /*
     * The following definitions enable
     * definitions of the dispatch-able features that are defined within the main
     * configuration header. These are definitions for the implied features.
     */
    #define NPY__CPU_TARGET_SSE
    #define NPY__CPU_TARGET_SSE2
    #define NPY__CPU_TARGET_SSE3
    #define NPY__CPU_TARGET_SSSE3
    #define NPY__CPU_TARGET_SSE41
    #define NPY__CPU_TARGET_POPCNT
    #define NPY__CPU_TARGET_SSE42
    #define NPY__CPU_TARGET_AVX
    #define NPY__CPU_TARGET_F16C
    #define NPY__CPU_TARGET_FMA3
    #define NPY__CPU_TARGET_AVX2
    #define NPY__CPU_TARGET_AVX512F
    // our dispatch-able source
    #include "/the/absuolate/path/of/hello.dispatch.c"
    
  • (D) Dispatch-able configuration header :基础结构为每个可调度的源生成一个配置头,此头主要包含两个摘要 C 用于标识生成的对象的宏,因此它们可以用于运行时通过任何方式从生成的对象中分派某些符号 C 来源。它也用于转发声明。

    生成的头在排除扩展名后采用可调度源的名称,并将其替换为' .h ,例如,假设我们有一个可调度的源 hello.dispatch.c 并包含以下内容:

    // hello.dispatch.c
    /*@targets baseline sse42 avx512f */
    #include <stdio.h>
    #include "numpy/utils.h" // NPY_CAT, NPY_TOSTR
    
    #ifndef NPY__CPU_TARGET_CURRENT
      // wrapping the dispatch-able source only happens to the addtional optimizations
      // but if the keyword 'baseline' provided within the configuration statments,
      // the infrastructure will add extra compiling for the dispatch-able source by
      // passing it as-is to the compiler without any changes.
      #define CURRENT_TARGET(X) X
      #define NPY__CPU_TARGET_CURRENT baseline // for printing only
    #else
      // since we reach to this point, that's mean we're dealing with
        // the addtional optimizations, so it could be SSE42 or AVX512F
      #define CURRENT_TARGET(X) NPY_CAT(NPY_CAT(X, _), NPY__CPU_TARGET_CURRENT)
    #endif
    // Macro 'CURRENT_TARGET' adding the current target as suffux to the exported symbols,
    // to avoid linking duplications, NumPy already has a macro called
    // 'NPY_CPU_DISPATCH_CURFX' similar to it, located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h
    // NOTE: we tend to not adding suffixes to the baseline exported symbols
    void CURRENT_TARGET(simd_whoami)(const char *extra_info)
    {
        printf("I'm " NPY_TOSTR(NPY__CPU_TARGET_CURRENT) ", %s\n", extra_info);
    }
    

    现在假设你有 hello.dispatch.c 然后基础结构应该生成一个名为 hello.dispatch.h 可由源代码树中的任何源访问的,它应包含以下代码:

    #ifndef NPY__CPU_DISPATCH_EXPAND_
      // To expand the macro calls in this header
        #define NPY__CPU_DISPATCH_EXPAND_(X) X
    #endif
    // Undefining the following macros, due to the possibility of including config headers
    // multiple times within the same source and since each config header represents
    // different required optimizations according to the specified configuration
    // statements in the dispatch-able source that derived from it.
    #undef NPY__CPU_DISPATCH_BASELINE_CALL
    #undef NPY__CPU_DISPATCH_CALL
    // nothing strange here, just a normal preprocessor callback
    // enabled only if 'baseline' spesfied withiin the configration statments
    #define NPY__CPU_DISPATCH_BASELINE_CALL(CB, ...) \
      NPY__CPU_DISPATCH_EXPAND_(CB(__VA_ARGS__))
    // 'NPY__CPU_DISPATCH_CALL' is an abstract macro is used for dispatching
    // the required optimizations that specified within the configuration statements.
    //
    // @param CHK, Expected a macro that can be used to detect CPU features
    // in runtime, which takes a CPU feature name without string quotes and
    // returns the testing result in a shape of boolean value.
    // NumPy already has macro called "NPY_CPU_HAVE", which fit this requirment.
    //
    // @param CB, a callback macro that expected to be called multiple times depending
    // on the required optimizations, the callback should receive the following arguments:
    //  1- The pending calls of @param CHK filled up with the required CPU features,
    //     that need to be tested first in runtime before executing call belong to
    //     the compiled object.
    //  2- The required optimization name, same as in 'NPY__CPU_TARGET_CURRENT'
    //  3- Extra arguments in the macro itself
    //
    // By default the callback calls are sorted depending on the highest interest
    // unless the policy "$keep_sort" was in place within the configuration statements
    // see "Dive into the CPU dispatcher" for more clarification.
    #define NPY__CPU_DISPATCH_CALL(CHK, CB, ...) \
      NPY__CPU_DISPATCH_EXPAND_(CB((CHK(AVX512F)), AVX512F, __VA_ARGS__)) \
      NPY__CPU_DISPATCH_EXPAND_(CB((CHK(SSE)&&CHK(SSE2)&&CHK(SSE3)&&CHK(SSSE3)&&CHK(SSE41)), SSE41, __VA_ARGS__))
    

    根据上述内容使用config头的示例:

    // NOTE: The following macros are only defined for demonstration purposes only.
    // NumPy already has a collections of macros located at
    // numpy/numpy/core/src/common/npy_cpu_dispatch.h, that covers all dispatching
    // and declarations scenarios.
    
    #include "numpy/npy_cpu_features.h" // NPY_CPU_HAVE
    #include "numpy/utils.h" // NPY_CAT, NPY_EXPAND
    
    // An example for setting a macro that calls all the exported symbols at once
    // after checking if they're supported by the running machine.
    #define DISPATCH_CALL_ALL(FN, ARGS) \
        NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_ALL_CB, FN, ARGS) \
        NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_ALL_CB, FN, ARGS)
    // The preprocessor callbacks.
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_ALL_CB(CHECK, TARGET_NAME, FN, ARGS) \
      if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_ALL_CB(FN, ARGS) \
      FN NPY_EXPAND(ARGS);
    
    // An example for setting a macro that calls the exported symbols of highest
    // interest optimization, after checking if they're supported by the running machine.
    #define DISPATCH_CALL_HIGH(FN, ARGS) \
      if (0) {} \
        NPY__CPU_DISPATCH_CALL(NPY_CPU_HAVE, DISPATCH_CALL_HIGH_CB, FN, ARGS) \
        NPY__CPU_DISPATCH_BASELINE_CALL(DISPATCH_CALL_BASELINE_HIGH_CB, FN, ARGS)
    // The preprocessor callbacks
    // The same suffixes as we define it in the dispatch-able source.
    #define DISPATCH_CALL_HIGH_CB(CHECK, TARGET_NAME, FN, ARGS) \
      else if (CHECK) { NPY_CAT(NPY_CAT(FN, _), TARGET_NAME) ARGS; }
    #define DISPATCH_CALL_BASELINE_HIGH_CB(FN, ARGS) \
      else { FN NPY_EXPAND(ARGS); }
    
    // NumPy has a macro called 'NPY_CPU_DISPATCH_DECLARE' can be used
    // for forward declrations any kind of prototypes based on
    // 'NPY__CPU_DISPATCH_CALL' and 'NPY__CPU_DISPATCH_BASELINE_CALL'.
    // However in this example, we just handle it manually.
    void simd_whoami(const char *extra_info);
    void simd_whoami_AVX512F(const char *extra_info);
    void simd_whoami_SSE41(const char *extra_info);
    
    void trigger_me(void)
    {
        // bring the auto-gernreated config header
        // which contains config macros 'NPY__CPU_DISPATCH_CALL' and
        // 'NPY__CPU_DISPATCH_BASELINE_CALL'.
        // it highely recomaned to include the config header before exectuing
      // the dispatching macros in case if there's another header in the scope.
        #include "hello.dispatch.h"
        DISPATCH_CALL_ALL(simd_whoami, ("all"))
        DISPATCH_CALL_HIGH(simd_whoami, ("the highest interest"))
        // An example of including multiple config headers in the same source
        // #include "hello2.dispatch.h"
        // DISPATCH_CALL_HIGH(another_function, ("the highest interest"))
    }
    

潜入CPU调度程序

基线

调度员

组和策略

实例

报告和跟踪