带有 Eigen 和 IPOPT 的矢量化标志
Posted
技术标签:
【中文标题】带有 Eigen 和 IPOPT 的矢量化标志【英文标题】:Vectorization flags with Eigen and IPOPT 【发布时间】:2018-03-23 09:45:35 【问题描述】:我有一些 C++ 函数正在使用 IPOPT 进行优化。虽然成本函数、约束函数等是用 C++ 编写的,但代码最初是使用 C 接口编写的。除非事实证明这是问题所在,否则我还没有费心去改变它。
无论如何...我们正在观察一些意想不到的行为,当我们编译带有/不带有矢量化标志的程序时,优化器会以不同的方式收敛。具体来说,在 CMakeLists 文件中,我们有
set(CMAKE_CXX_FLAGS "-Wall -mavx -mfma")
当我们使用这些设置运行优化器时,优化器会在大约 100 次迭代中收敛。到现在为止还挺好。
但是,我们有理由相信,在为 ARM(特别是 android)编译时,不会发生矢量化,因为其性能与 Intel 处理器上的性能截然不同。 Eigen 文档说应该始终为 64 位 ARM 启用 NEON 指令,但我们有理由怀疑这不会发生。无论如何,这不是这里的问题。
由于这种怀疑,我们想看看如果我们禁用矢量化,我们的英特尔处理器的性能会有多糟糕。这应该给我们一些关于向量化正在发生多少的迹象,以及我们可能期望在 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 标志不会改变函数的输出,而只会改变计算它们所需的时间。我好像错了。
我们观察到的现象对我来说特别奇怪,因为一方面我们观察到优化器总是收敛到相同的解决方案。这以某种方式表明问题不能太病态。然而,另一方面,优化器在有/没有矢量化标志的情况下表现不同的事实表明,该问题确实对矢量化指令生成的任何小残差很敏感。
要记住的另一件事是,我们将 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)
【问题讨论】:
您的其他选项是否包括-ffast-math
?这将使 AVX 和非 AVX 之间的区别更容易理解。
没有。我意识到快速数学会四舍五入,但我没有明确启用它
FP 数学总是四舍五入。但是-ffast-math
将允许编译器生成与 C 源代码不同 的 asm,即更改操作顺序,以便您拥有不同的临时对象。没有它,编译器仍有一些(但不多,尤其是没有 x87 80 位 FP 寄存器)范围来执行任何会给出不同数值结果的操作。
顺便说一句,您应该使用-march=native
以获得最佳性能,而不仅仅是-mavx -mfma
。您希望 gcc 专门为您的 CPU 调整,并且只启用它支持的一些指令集,但仍针对通用 CPU 进行调整。你用的是-O3
,对吧?
正确。我们使用 03 进行发布构建。我使用-g
进行调试,并能够使用 Callgrind 进行分析。但是,上面的结果是在发布模式下
【参考方案1】:
如果您的算法在数值上不稳定,启用 FMA 将导致不同的舍入行为,从而导致非常不同的结果。此外,在 Eigen 中启用 AVX 会导致不同的加法顺序,并且由于浮点数学是非关联的,这也会导致行为略有不同。
为了说明为什么非关联性会产生影响,当使用 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
如果没有更多细节,很难说出你的情况到底发生了什么。
【讨论】:
这是一个很大的优化问题。可能是解决方案中出现了一些振铃。除此之外,这个问题归结为几个矩阵乘法。 如果没有-ffast-math
,编译器更改数值结果的范围非常有限,除了将 FP 压缩为 FMA 以及它在编译时评估的内容。硬件 FP add 和 mul 操作are commutative except for the payload of NaN。 C++ +
运算符 is not,但 AVX 与 SSE2 的重要性并不明显。
FP 数学不是associative,但编译器不允许假装它是,除非你使用-ffast-math
。
@PeterCordes Eigen 将使用不同的编译路径,具体取决于 AVX/FMA/... 是否可用。 (不可交换是一个错字,我的意思是联想,当然)
@bremen_matt:related:Associativity gives us parallelizability. But what does commutativity give? 在问题中有一个很好的链接,其中包含一些关于关联性如何提供并行性的背景。以上是关于带有 Eigen 和 IPOPT 的矢量化标志的主要内容,如果未能解决你的问题,请参考以下文章