带有 Eigen 和 IPOPT 的矢量化标志

Vectorization flags with Eigen and IPOPT

我有一些正在使用 IPOPT 优化的 C++ 函数。虽然成本函数、约束函数等是用C++写的,但代码最初是为了使用C接口而编写的。除非事实证明这是问题所在,否则我还没有费心去改变它。

无论如何...我们正在观察一些意想不到的行为,当我们编译程序 with/without 向量化标志时优化器收敛不同。具体来说,在 CMakeLists 文件中,我们有

set(CMAKE_CXX_FLAGS "-Wall -mavx -mfma")

当我们 运行 优化器使用这些设置时,优化器会在大约 100 次迭代后收敛。到目前为止,一切都很好。

但是,我们有理由相信,在为 ARM(具体为 Android)编译时,不会发生矢量化,因为性能与 Intel 处理器上的性能大不相同。 Eigen 文档说 NEON 指令应该始终为 64 位 ARM 启用,但我们有理由怀疑这并没有发生。无论如何,这不是这里的问题。

出于这种怀疑,我们想看看如果禁用矢量化,我们的 Intel 处理器的性能会有多差。这应该让我们了解矢量化的程度,以及我们期望在 ARM 中看到多少改进。但是,当我们将编译器标志更改为

set(CMAKE_CXX_FLAGS "-Wall")

(或者只是针对我们只使用 AVX(没有 fma)的情况),然后我们从优化器得到相同的通用解决方案,但收敛性能却大不相同。具体来说,在没有矢量化的情况下,优化器需要大约 500 次迭代才能收敛到解决方案。

总而言之:

With AVX and FMA      : 100 iterations to converge
With AVX              : 200 iterations to converge
Without AVX and FMA   : 500 iterations to converge

我们实际上只是更改了 cmake 文件中的那一行,而不是源代码。

我想要一些关于为什么会发生这种情况的建议。


我的想法和更多背景信息:

在我看来,有或没有矢量化的版本必须进行一些舍入,这使得 IPOPT 收敛不同。我的印象是添加 AVX 和 FMA 标志不会改变函数的输出,而只会改变计算它们所花费的时间。我似乎错了。

我们观察到的现象对我来说显得特别奇怪,因为一方面我们观察到优化器总是收敛到相同的解决方案。这在某种程度上表明问题不能太病态。然而,另一方面,优化器表现不同 with/without 向量化标志的事实表明该问题确实对向量化指令生成的任何小残差敏感。

要记住的另一件事是我们将 IPOPT 预编译到一个库中,并且只是将我们的代码链接到该预编译库中。所以我认为 AVX 和 FMA 标志不会影响优化器本身。这似乎意味着我们的函数必须输出具有明显不同值的值,具体取决于是否启用矢量化。


对于那些感兴趣的人,这里是完整的 cmake 文件

cmake_minimum_required(VERSION 3.5)

# If a build type is not passed to cmake, then use this...
if(NOT CMAKE_BUILD_TYPE)
    # set(CMAKE_BUILD_TYPE Release)
    set(CMAKE_BUILD_TYPE Debug)
endif()

# If you are debugging, generate symbols.
set(CMAKE_CXX_FLAGS_DEBUG "-g")

# If in release mode, use all possible optimizations
set(CMAKE_CXX_FLAGS_RELEASE "-O3")

# We need c++11
set(CMAKE_CXX_STANDARD 11)

# Show us all of the warnings and enable all vectorization options!!!
# I must be crazy because these vectorization flags seem to have no effect.
set(CMAKE_CXX_FLAGS "-Wall -mavx -mfma")

if (CMAKE_SYSTEM_NAME MATCHES "CYGWIN")
    include_directories(../../Eigen/
            /cygdrive/c/coin/windows/ipopt/include/coin/
            /cygdrive/c/coin/windows/ipopt/include/coin/ThirdParty/)
    find_library(IPOPT_LIBRARY ipopt HINTS /cygdrive/c/coin/windows/ipopt/lib/)
else ()
    include_directories(../../Eigen/
            ../../coin/CoinIpopt/build/include/coin/
            ../../coin/CoinIpopt/build/include/coin/ThirdParty/)
    find_library(IPOPT_LIBRARY ipopt HINTS ../../coin/CoinIpopt/build/lib/)
endif ()

# Build the c++ functions into an executable
add_executable(trajectory_optimization main.cpp)

# Link all of the libraries together so that the C++-executable can call IPOPT
target_link_libraries(trajectory_optimization ${IPOPT_LIBRARY})

启用 FMA 会导致不同的舍入行为,如果您的算法在数值上不稳定,这会导致截然不同的结果。此外,在 Eigen 中启用 AVX 将导致不同的加法顺序,并且由于浮点数学是 non-associative,这也会导致行为略有不同。

为了说明为什么 non-associativity 会有所不同,当使用 SSE3 或 AXV 添加 8 个连续的双数 a[8] 时,Eigen 通常会生成等同于以下内容的代码:

// SSE:
double t[2]={a[0], a[1]};
for(i=2; i<8; i+=2)
   t[0]+=a[i], t[1]+=a[i+1]; // addpd
t[0]+=t[1];                  // haddpd

// AVX:
double t[4]={a[0],a[1],a[2],a[3]};
for(j=0; j<4; ++j) t[j]+=a[4+j]; // vaddpd
t[0]+=t[2]; t[1]+=t[3];          // vhaddpd
t[0]+=t[1];                      // vhaddpd

没有更多详细信息,很难说出您的情况到底发生了什么。