2021年2月

[IPM] (http://ipm-hpc.sourceforge.net/)(Integrated Performance Monitoring)是用于MPI性能评测的一套软件库,声称可以收集MPI函数运行时间、通信拓扑、通信量统计、能耗统计等等功能。

https://stackoverflow.com/questions/43002936/what-are-the-differences-between-the-mpi-functions-and-pmpi-functions

这个问题解决了我在阅读IPM源码的时候发现的MPI_开头和PMPI_开头的函数的区别和关系的疑惑,同时也解决了我对于IPM运行原理的疑惑。

主要原理就是通过使用加了debug逻辑的IPM库代替原始MPI函数,达到既不影响原有程序执行,又能收集性能数据的目的。

达成目的的方法有两种,一是经典的显示链接,在编译时就指定要使用IPM版的MPI函数实现;二是利用动态库的骚操作,利用系统环境变量“劫持”运行时程序要调用的MPI实现,通过LD_PRELOAD的优先级实现。

在这些时候,我可以附和着笑,leader是决不责备的。而且leader见了孔乙己,也每每这样问他,引人发笑。孔乙己自己知道不能和他们谈天,便只好向孩子说话。有一回对我说道,“你写过代码么?”我略略点一点头。他说,“写过代码……我便考一考。C++的变量初始化,怎样写的?”我想,讨饭一样的人,也配考我么?便回过脸去,不再理会。孔乙己等了许久,很恳切的说道,“不能写罢?我教给你,记着!这些写法应该记着。将来做leader的时候,算KPI要用。”我暗想我和leader的等级还很远呢,而且leader也从不把C++语法算进KPI;又好笑,又不耐烦,懒懒的答他道,“谁要你教,不是一个等号的事么?”孔乙己显出极高兴的样子,将两个指头的长指甲敲着柜台,点头说,“对呀对呀!变量初始化有四样写法,你知道么?”我愈不耐烦了,努着嘴走远。孔乙己刚用指蘸了酒,想在柜上写代码,见我毫不热心,便又叹了口气,显出极惋惜的样子。

int val(4);
int val {4};    // *
int val = 4;
int val = {4};  // *

* 使用大括号{}进行变量初始化的功能来自于C++11标准,对于赋值过程有严格的检查,比如不允许使用类型缩窄(例如,把int类型值赋值给char类型变量,即使程序员知道这个int值可以使用char类型正确保存)。

DFTDiscrete Fourier Transformation离散傅里叶变换,适用于对采样性质的信号样本进行傅里叶变换。因为本人目前还没有系统地学习《信号与系统》这门课,对于傅里叶变换的原理不了解,因而会从感性的角度写下自己对于DFT的一些理解。

不论学不学《信号与系统》这门课,三角函数总是学过的。

接下来我们需要知道这么一个事实:有相当大一部分的信号,是可以通过一系列数学运算,使用一组周期不同的三角函数表示的。

信号需要满足狄氏条件。抛开这个复杂的条件,我们粗略地认为常见的声波(甚至电磁波,Asteroids@home曾今发布过DFT计算任务)都是符合条件的,因为至少目前我还没见过不符合条件的。这说明DFT具有一定的使用空间,进而具有研究意义,后续发明的、被誉为20世纪重要发明之一的FFT算法甚至被某些OI大佬拿去AK

由于DFT通常是给计算设备使用的,所以给定了输入长度有限的条件,下文假设有一个长度为N+1的信号序列x[n],均匀采样,总时长为T

设长度为N+1是为了能够方便地将每个采样点书写为x[0]x[1],...,x[N]。那么对于这个信号序列进行离散傅里叶变换,可以得到一个长度也为N+1的输出F[n]

这里使用傅里叶最原始的想法来表示信号在时域/频域的转换:

s_N[t] = \sum_{n=0}^{N} k\text{sin} (n\omega t + \theta)

而上述提到的,对信号x[n]进行离散傅里叶变换后得到的F[n],对应的是上式中的k\theta\omega则是通过采样频率\frac{N+1}{T}得到。

在进行实际计算后,可能会有一些发现:

  1. F[N]给出的是一个复数序列,即使输入是实数序列;
  2. x[n]是实数序列的时候,F[1]...F[N]是中心对称的,且对称的元素共轭(即a+bia-bi).

(1)是因为多出来的虚部和实部一起,共同记录了\theta中的信息,在实际还原成时域信号的过程中,虚部会在运算中消除的((2)从侧面印证了这一点)。

然后转到FFTW库的实现: http://www.fftw.org/doc/Multi_002dDimensional-DFTs-of-Real-Data.html#Multi_002dDimensional-DFTs-of-Real-Data

这里就提到,如果输入是实数的话,就会涉及到输出结果类型会变化的问题(从floatfftw_complex,实际上就是float[2])。

这里就正好与上述的两个发现对应,因为输出数据类型改变,因而需要重新规划空间;又因为数据存在冗余性,可以通过删除冗余数据的方式节省内存空间。

那么文档里的算术就可以理解了:F[n]一共需要保存的复数元素个数至少为\lceil \frac{(N+1)-1}{2} \rceil + 1个。当元素个数为奇数((n+1)%2==1)的时候,这个数是\frac{N}{2}+1,对应N+2个存储单元;偶数((n+1)%2==0)的情况下,这个数是\lceil \frac{N}{2} \rceil + 1,对应N+3个存储单元。由于原始的输入规模是N+1,因而在该规模为奇数或偶数的时候分别需要补充1或2个单位的数据规模,与文档中的叙述相符合。

之前用CentOS,试图使用fftw-mpi的时候,报出了奇怪的符号symbol不存在的问题,稍加思索发现,居然是库版本不匹配的问题。自带的AppStream仓库有fftw,而fftw-mpi则没有,因而选择了使用第三方的OKey源来提供fftw-mpi

结果编译的时候还是出了问题(好像是openmpi的),选择自己手动编译安装fftw的依赖库。

然后注意到,在使用dnf安装fftw的时候,允许同时安装多精度的库版本,而自己编译的时候似乎没有看到类似的选项。翻查fftw的文档后发现,官方预想到了这种情况,提供了一条道路( http://www.fftw.org/fftw2_doc/fftw_6.html#SEC69 )。然而这个文档提供的是fftw2的编译教程,与现在使用的fftw3肯定是有差距的,例如里面的--enable-type-prefix选项就并不能用,configure会警告说是未定义的选项。

后来简单参考了一下configure自带的帮助内容,发现有一系列参数,叫--program-什么的,可以修改生成的目标库/头文件的命名,prefix就是加前缀,suffix就是加后缀。这里使用suffix实现和包管理器提供的相同效果,将双精度版本设置为默认的话,就直接configuremakemake install一把梭,其余版本则在声明精度之后,使用--program-suffix=修改文件名。比如编译float版本的时候,加入--enable-float的同时,添加--program-suffix=f,这样编译出来的库的名字是libfftw3f.so,就可以和原版的libfftw3.so相区别开来。

例如,命令行:

./configure --prefix=/usr/local/ --enable-shared --enable-mpi
./configure --prefix=/usr/local/ --enable-shared --enable-mpi --enable-float --program-suffix=f

可以编译出两个精度不同,但可以共存的、支持mpifftw库,第一个后续可以生成libfftw3.so(双精度版),第二个可以生成libfftw3f.so(单精度版)。