近期高性能计算报班培训学习总结
工作以来,我鲜有时间停下来做一些总结,偶有所得也是记录在公司内部的文档里。我细细想来,发现原因主要是这份工作实在是有些螺丝钉了,脱离了公司的环境就很难成立。去年10月底以来,我开始报班培训C++,为什么要这样做呢?
- 因为我发现大厂的算法工程师很容易变成“螺丝钉”,数据、训练和部署的环境都是现成的,你只需要关注训练,这样公司整体的效率当然是提高了,但对个人理解算法的生产和部署是不利的。
- 工作快九年,专业的基本知识有点遗忘,有个培训班督促你学习,明确进度也是好的。
C++培训班快结束时,我发现猿代码在培训高性能计算工程师,于是无缝又加入了,其实就主要是学习OpenMP、SIMD和MPI。要说高性能计算学习感触最深的点,那莫过于float32数值的精度问题了。高性能计算常常就是在原始串行程序的基础上找到热点函数(或者说消耗时间最多的代码),然后在多核或者多数据的环境下并行计算,最终加速整体程序的运行。那这里除了要衡量程序加速的性能,还需要保证并行程序的结果相比串行程序要完全一致,或者精度小于可接受的范围(比如10的负6次方)。那为什么会有精度的问题么?原因就在于:
- 程序并行时,原先a+b+c+d+e的顺序可能会发生变化,比如第一个CPU计算a+b+c,第二个CPU计算d+e;
- 浮点数据类型存储的数值本身可能不精确,比如0.2;
- 浮点数运算时,指数较小的数可能会因为指数对齐而减少有效数字。
所以在处理浮点数的并行时往往需要非常地细心去分析并行计算带来的精度差异,一般来说,对于有循环迭代的串行程序,哪怕一开始精度差异是10的负15次方,精度差异也可能会到个位数的水平。这里我总结几个我遇到的带来float32精度差异的原因:
- g++编译选项。串行O1,并行O3,因为O3可能会做for循环展开、交换或分块,带来浮点数计算顺序的差异。
- MPI分发数据后,原先串行的连加分块了。比如第一个CPU计算前5个数相加,第二个CPU计算后5个数相加,再汇总把结果再相加。
- SIMD函数计算顺序与串行不一致。比如vfmaq_f32与vaddq_f32(a ,vmulq_f32(b ,c))。
就我多次做项目和考试的心得来说,使用OpenMP、SIMD和MPI这三种技术进行并行化,为保证精度,需要小心地一步步来:
- MPI:在分发数据和接收数据时需要先确认发送和接收的数据无误,再进行下一步;
- OpenMP:多用default(none)来检查变量的属性,是共享、私有、还还拷贝私有等;
- SIMD:可以先用SIMD函数模拟串行程序看精度是否有问题,或者是for循环的第一步;
最后分享下自己常用的一些小例子:
OpenMP
#include <omp.h>
void main(){
#pragma omp parallel
{
}
}
// g++ -fopenmp foo.c
// export OMP_NUM_THREADS=4
// ./a.out
MPI
// mpicc -O1
#include <stdlib.h>
#include <mpi.h>
int main(int argc ,char* argv[]){
int mpi_rank ,mpi_size;
MPI_Init(&argc ,&argv);
MPI_Comm_rank(MPI_COMM_WORLD ,&mpi_rank);
MPI_Comm_size(MPI_COMM_WORLD ,&mpi_size);
MPI_Finalize();
return 0;
}
SIMD
#include <iostream>
#include <arm_neon.h>
int main(void){
float64x2_t x = {1.0 ,-287.617};
float64x2_t ret = vaddq_f64(x ,x);
return 0;
}
SIMD常用函数总结
API查询网址 https://developer.arm.com/architectures/instruction-sets/intrinsics/
加载:将数据从内存加载到寄存器,如ld
- ld1 int16x4 ld2 int16x4x2 ld3 int16x4x3
- lane 修改指定通道的元素
- dup 修改所有通道的元素
存储:将数据从寄存器转移到内存,如st
加/减法:c=a+b,如add,sub
乘/乘加法:c=a*b+d,如mul,mla,mls
- n 单独乘一个数值
- lane 单独乘给定通道的数值
- mla 先做乘法,再做加法,相乘结果会有精度传入,fma 为浮点数乘加操作专门设计,速度和精度都要更高
除法:recps recpe
- rec = vrecpeq_f32(src) // 先执行recpe求出src的低精度倒数rec
- recip1 = vmulq_f32(vrecpsq_f32(src ,rec) ,rec); // 达到百万分之一左右的精度
- recip2 = vmulq_f32(vrecpsq_f32(src ,recip1) ,recip1); // 基本达到完全精度
平方根:sqrt rsqrte(平方根倒数)
绝对值:abs
- abd r[i] = |a[i] - b[i]|
- aba r[i] = a[i] + |b[i] - c[i]|
最大值最小值 max min
相反数 neg
逻辑判断:>,<, =>,=< ,==,如eq ,gt,结果为整形
- 相等 vceq
- 大于 vcgt 大于等于 vcge
- 小于 vclt 小于等于 vcle
- 条件判断 vbslq_type(a ,b ,c) a[i]<>0?b[i]:c[i]
位操作:与或非,如and,orr
- 与 and 或 orr 非 mvn
- 异或 eor
位移:按位左移,右移,如shl,shr
- n 移动几位
初始化向量与数据转换,如dup,mov等等
- mov 在64位和128位寄存器间转移整形数据
- cvt 在64位和128位寄存器间转移浮点型数据,在同长度寄存器间转换浮点型与整型数据
- reinterpret 类型重解释
取寄存器的一半数据:get_low get_high
获取或设置寄存器某个通道的值:get_lane set_lane
数据重排
- trn 两个向量的两个通道为一组,将第一个向量的前个通道与第二个向量的后个通道数值进行交换。
- ext 拼接两个向量的首尾
点积:dot 仅用于8位数据