不要因为走了太远,而忘记为什么出发
前言 高性能计算(HPC),简称为HPC,是一种技术,它利用并行工作的强大处理器集群来处理海量多维数据集(大数据),以极高的速度解决复杂问题。
此文记录了我早期入门超算的一些内容,主要以曾经写过的报告为主题,并加以修改和改进。有些部分因知识不足,或有欠缺之处,还请见谅。
基础知识 The coming of the ship
编译器是一种计算机程序,负责把一种编程语言编写的源码转换成另外一种计算机代码,后者往往是以二进制的形式被称为目标代码 (object code)。这个转换的过程通常的目的是生成可执行的程序。 GCC/MSVC/Clang/ICC/NVCC
Frontier -US / Supercomputer Fugaku -JP
拓扑结构:一个物理处理器(Package),每个处理器有4个核心(Core),每个核心有1个线程(Thread)。这台机器的总线程数是4。这台机器支持NUMA,有一个NUMA节点。 CPU 型号:CPU型号是Intel® Xeon® Gold 6248 CPU @ 2.50GHz,这是一款GenuineIntel的产品,CPU主频是2.5GHz,支持virtual-8086 mode enhancement等特性。 缓存大小:每个核心有32KB的一级数据缓存(L1d)和32KB的一级指令缓存(L1i),4096KB的二级缓存(L2),物理处理器有16MB的三级缓存(L3)。 内存:总内存是16GB,分布在一个NUMA节点上。
基础操作 Empty and dark shall I raise my lantern
1. 编译器基础使用 此处借用学长的话。
在 HPC 中,编译器的使用是基础中的基础,但也是很多时候的踩坑点:不同科学计算软件会使用各种各样的依赖库,而软件本身可能是由 Makefile 或者 CMake 等进行自动化构建;做移植时需要你替换依赖库实现更好的性能,但是随之而来的是各式各样的报错;多核 CPU 带来的多核计算程序、异构架构如 CUDA 等、新型国产硬件平台使得各式编译器层出不穷。这些问题在你没掌握基础前都是问题,所以我们希望你能先踩一些比较基础的坑,这样才能应对 “编译器的黄金时代” 。
2. 编译的简要流程 预处理:处理代码,设置并行域,以便编译器使用。 编译:将代码进行语法分析,优化和生成代码。 汇编:将编译生成的代码翻译成机器指令,存储在.o中。 链接:以静态库或者动态库的方式链接编译好的其他函数。
3. 静态库、动态库的区别 静态库:在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中。静态库对函数库的链接是放在编译时期完成的。 动态库:动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。
4. 使用的编译参数的意义 -m32:生成32位机器的汇编代码。 -std=gnu++11:选择C语言编译标准。 -Wall:该选项意思是编译后显示所有警告。 -Wextra:检测更多的代码中的警告信息。 -Bstatic:在静态库中查找符号而不是动态库。
思考1:可不可以更换编译器如 Clang/LLVM 进行编译达到同样效果? 可以,但其对代码优化程度不同,所支持特性也不同。
扩展1:Linux 中动态库和静态库的区别? 静态库编译时链接,动态库运行时链接,存储空间占用/更新部署方式不同。
拓展2:哪些用于调试程序、分析二进制可执行文件的工具? 调试程序GDB,linux调试器;各种Debugger插件;分析二进制文件nm命令;IDA7,x64dbg常用于ctf中反编译程序;010 editor用来查看二进制信息。
自动化编译工具 Makefile 修改后的脚本源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 CXX := g++ CXXFLAGS := -Wall -Wextra -std=c++17 LDFLAGS := SRC_DIR := ./src INC_DIR := ./inc CXXFLAGS += -I$(INC_DIR) TARGET := main HEADERS := $(wildcard $(INC_DIR) /*.hpp) SRCS := $(wildcard $(SRC_DIR) /*.cpp) OBJS := $(SRCS:.cpp=.o) .PHONY : allall: $(TARGET) $(SRC_DIR) /%.o: $(SRC_DIR) /%.cpp $(HEADERS) $(CXX) -c -o $@ $< $(CXXFLAGS) $(TARGET) : $(OBJS) $(CXX) -o $@ $^ $(CXXFLAGS) $(LDFLAGS) clean: @rm -f $(OBJS) $(TARGET)
修改思路,对该 Makefile 构建程序流程进行解释 在Makefile 的开头,定义了一些变量,包括编译器(CXX
)、编译器标志(CXXFLAGS
)、链接器标志(LDFLAGS
)、源代码目录(SRC_DIR
)、头文件目录(INC_DIR
)等。
1 2 3 4 5 6 7 CXXFLAGS += -I$(INC_DIR) ``` 这行命令将头文件目录添加到编译器标志中。 ```HEADERS```、```SRCS``` 和 ```OBJS```变量存储上面传下的参数,```wildcard```找到符合匹配的文件。 ```makefile .PHONY : allall: $(TARGET)
定义命令集合all。 当运行make时,会生成 可执行文件$(TARGET)=main$。
1 $(SRC_DIR) /%.o: $(SRC_DIR) /%.cpp $(HEADERS)
表示每个 .o 目标文件都依赖于同名的 .cpp 源文件和所有的头文件。如果更新依赖或者目标文件不存在,则生成。
表示可执行文件依赖于所有的 .o 目标文件,包括CXX和LDF。 clean删除生成的 .o文件和可执行文件。
cmake 修改后的脚本源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 cmake_minimum_required (VERSION 3.16 )project (compile_cmake)SET (CMAKE_BUILD_TYPE "Debug" )SET (CMAKE_CXX_FLAGS_DEBUG "$ENV{CFLAGS} -DDEBUG -O0 -Wall -g -ggdb" )SET (CMAKE_CXX_FLAGS_RELEASE "$ENV{CFLAGS} -O3 -Wall" )include_directories (inc)add_library (v3 SHARED src/v3.cpp)add_library (particle SHARED src/particle.cpp)add_executable (main src/main.cpp)target_link_libraries (main PUBLIC particle v3)add_custom_target (run COMMAND ./src/main DEPENDS main WORKING_DIRECTORY ${CMAKE_PROJECT_DIR} )
修改思路,对该 CMake 构建程序流程进行解释 1 cmake_minimum_required (VERSION 3.16 )
指定cmake版本。
指定项目名称。
1 2 3 SET (CMAKE_BUILD_TYPE "Debug" )SET (CMAKE_CXX_FLAGS_DEBUG "$ENV{CFLAGS} -DDEBUG -O0 -Wall -g -ggdb" )SET (CMAKE_CXX_FLAGS_RELEASE "$ENV{CFLAGS} -O3 -Wall" )
设置编译类型,编译标志。
1 include_directories (inc)
添加头文件目录。
1 2 add_library (v3 SHARED src/v3.cpp)add_library (particle SHARED src/particle.cpp)
添加库文件。
1 add_executable (main src/main.cpp)
添加可执行文件。
1 target_link_libraries (main PUBLIC particle v3)
这里一开始报错找不到v3,因此调换了位置,先链接依赖的库。
1 2 3 add_custom_target (run... )
执行make时,先构建main,然后在目录下运行./src/main。
拓展1: 修改后的脚本源码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 cmake_minimum_required (VERSION 3.16 )project (compile_cmake)SET (CMAKE_BUILD_TYPE "Debug" )SET (CMAKE_CXX_FLAGS_DEBUG "$ENV{CFLAGS} -DDEBUG -O0 -Wall -g -ggdb" )SET (CMAKE_CXX_FLAGS_RELEASE "$ENV{CFLAGS} -O3 -Wall" )include_directories (inc)add_library (v3 SHARED ./step2/src/v3.cpp)add_library (particle SHARED ./step2/src/particle.cpp)add_executable (main ./step2/src/main.cpp)target_link_libraries (main PUBLIC particle v3)add_custom_target (m0 COMMAND echo "========" && echo "单文件编译" && g++ hello.cpp -o ${CMAKE_BINARY_DI R} /m0/hello WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step1 ) add_custom_target (m1 COMMAND echo "==============================" && echo "全部编译成目标文件,最后直 接进行链接" && g++ src/main.cpp -o ${CMAKE_BINARY_DIR} /m1/main.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/particle.cpp -o ${CMAKE_BINARY_DIR} /m1/particle.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/v3.cpp -o $ {CMAKE_BINARY_DIR}/m1/v3.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g ++ ${CMAKE_BINARY_DIR} /m1/main.o ${CMAKE_BINARY_DIR} /m1/particle.o ${CMAKE_BINARY_DIR} /m1/v3.o -o ${CMAKE_BINARY_DIR} /m1/main && echo "==============================" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step2 ) add_custom_target (m2 COMMAND echo "=================" && echo "Statically linked" && g++ src/main.cpp - o ${CMAKE_BINARY_DIR} /m2/main.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/particle.cpp -o ${CMAKE_BINARY_DIR} /m2/libparticle.so -shared -fPIC -g -gg db -O0 -std=c++17 -I./inc -Wall -march=native && g++ src/v3.cpp -o ${CMAKE_BINARY_DIR} /m2/libv3.so -shared -fPIC -g -ggdb -O0 -std=c++17 -Wall -I./inc -march=native && g++ ${CMAKE_BINARY_DIR} /m2/main.o ${CMAKE_BINARY_DIR} /m2/libparticle.so ${CMAKE_BINARY_DIR } /m2/libv3.so -o ${CMAKE_BINARY_DIR} /m2/main && echo "=================" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step2 ) add_custom_target (m3 COMMAND echo "===========" && echo "Shared Libs" && g++ src/main.cpp -o ${CMAKE_BI NARY_DIR} /m3/main.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/particle.cpp -o ${CMAKE_BINARY_DIR} /m3/particle.o -fPIC -shared -g -ggdb -O0 -std=c++1 7 -I./inc -Wall -march=native -c && g++ src/v3.cpp -o ${CMAKE_BINARY_DIR} /m3/v3.o -fPIC -shared -g -ggdb -O0 -std=c++17 -Wall -I./inc -march=native -c && g++ ${CMAKE_BINARY _DIR} /m3/v3.o -o ${CMAKE_BINARY_DIR} /m3/libv3.so -fPIC -shared && g++ ${CMAKE_BINARY_D IR} /m3/particle.o -o ${CMAKE_BINARY_DIR} /m3/libparticle_rpath-link.so -fPIC -shared -Wl,-L${CMAKE_BINARY_DIR} /m3 -Wl,-rpath-link=${CMAKE_BINARY_DIR} /m3 -lv3 && g++ ${CMAKE_ BINARY_DIR} /m3/particle.o -o ${CMAKE_BINARY_DIR} /m3/libparticle_rpath.so -fPIC -shared -Wl,-L${CMAKE_BINARY_DIR} /m3 -Wl,-rpath=${CMAKE_BINARY_DIR} /m3 -lv3 && g++ ${CMAKE_BI NARY_DIR} /m3/particle.o -o ${CMAKE_BINARY_DIR} /m3/libparticle.so -fPIC -shared -Wl,-L${CMAKE_BINARY_DIR}/m3 -lv3 && g++ ${CMAKE_BINARY_DIR} /m3/main.o -o ${CMAKE_BINARY_DIR} /m3/main_rpath-link -Wl,-L${CMAKE_BINARY_DIR} /m3 -Wl,-rpath-link=${CMAKE_BINARY_DIR} /m 3 -lparticle_rpath-link -lv3 && g++ ${CMAKE_BINARY_DIR} /m3/main.o -o ${CMAKE_BINARY_DI R} /m3/main_rpath -Wl,-L${CMAKE_BINARY_DIR} /m3 -Wl,-rpath=${CMAKE_BINARY_DIR} /m3 -lparticle_rpath -lv3 && g++ ${CMAKE_BINARY_DIR} /m3/main.o -o ${CMAKE_BINARY_DIR} /m3/main -W l,-L${CMAKE_BINARY_DIR} /m3 -lparticle -lv3 && echo "===========" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step2 ) add_custom_target (m4 COMMAND echo "===========" && echo "Static Libs" && g++ src/main.cpp -o ${CMAKE_BI NARY_DIR} /m4/main.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/particle.cpp -o ${CMAKE_BINARY_DIR} /m4/particle.o -g -ggdb -O0 -std=c++17 -I./inc -Wal l -march=native -c && g++ src/v3.cpp -o ${CMAKE_BINARY_DIR} /m4/v3.o -g -ggdb -O0 -std= c++17 -Wall -I./inc -march=native -c && ar crv ${CMAKE_BINARY_DIR} /m4/libv3.a ${CMAKE_ BINARY_DIR} /m4/v3.o && ar crv ${CMAKE_BINARY_DIR} /m4/libparticle.a ${CMAKE_BINARY_DIR} /m4/particle.o && ranlib ${CMAKE_BINARY_DIR} /m4/libv3.a && ranlib ${CMAKE_BINARY_DIR} / m4/libparticle.a && g++ ${CMAKE_BINARY_DIR} /m4/main.o -o ${CMAKE_BINARY_DIR} /m4/main - static -Wl,-L${CMAKE_BINARY_DIR} /m4 -lparticle -lv3 && g++ ${CMAKE_BINARY_DIR} /m4/main .o -o ${CMAKE_BINARY_DIR} /m4/main_1 -Wl,-L${CMAKE_BINARY_DIR} /m4 -lparticle -lv3 && ec ho "===========" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step2 ) add_custom_target (m5 COMMAND echo "===============================" && echo "Use both Shared and Static Libs" && g++ src/mixed_main.cpp -o ${CMAKE_BINARY_DIR} /m5/mixed_main.o -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/mixed_a.cpp -o ${CMAKE_BINARY_DIR} /m5/mixed_a.o -fPIC -shared -g -ggdb -O0 -std=c++17 -I./inc -Wall -march=native -c && g++ src/mixed_b.cpp -o ${CMAKE_BINARY_DIR} /m5/mixed_b.o -g -ggdb -O0 -std=c++17 -I./in c -Wall -march=native -c && g++ ${CMAKE_BINARY_DIR} /m5/mixed_a.o -o ${CMAKE_BINARY_DIR } /m5/libmixed_a.so -fPIC -shared && ar crv ${CMAKE_BINARY_DIR} /m5/libmixed_b.a ${CMAKE _BINARY_DIR} /m5/mixed_b.o && ranlib ${CMAKE_BINARY_DIR} /m5/libmixed_b.a && g++ ${CMAKE _BINARY_DIR} /m5/mixed_main.o -o ${CMAKE_BINARY_DIR} /m5/mixed_main -Wl,-L${CMAKE_BINARY _DIR} /m5 -lmixed_a -lmixed_b && g++ ${CMAKE_BINARY_DIR} /m5/mixed_main.o -o ${CMAKE_BIN ARY_DIR} /m5/mixed_main_v1 -Wl,-L${CMAKE_BINARY_DIR} /m5 -lmixed_a -Bstatic -lmixed_b && g++ ${CMAKE_BINARY_DIR} /m5/mixed_main.o -o ${CMAKE_BINARY_DIR} /m5/mixed_main_v2 -Wl,- L${CMAKE_BINARY_DIR} /m5 -lmixed_a -l:libmixed_b.a && echo "=========================== ====" WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} /step3 )
修改思路,构建流程的解释 指定版本,项目名,导入头文件目录,添加库文件,添加可执行文件copy。
1 2 3 add_custom_target (m0 ...)... add_custom_target (m5 ...)
这些代码添加了六个自定义目标m0到m5,它们分别对应于compile.sh脚本中的m0到m5函数。每个目标都使用了一个自定义命令来执行相应的编译步骤。改完了发现其实可以用 m0R = ${CMAKE_BINARY_DIR}/m0 来代替,不看开头导致的。
并行基础 Then he assigns you to his sacred fire
过程 inc 目录存放了头文件,src 目录存放了源代码文件read_data.hpp。进入 src 目录,首先编译 datagen.cpp
1 2 g++ -I../inc datagen.cpp -o datagen ./datagen 1000 1000 1000
编译命令如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 nvcc -I../inc matrix_cal_cuda.cu -o matrix_cal_cuda nvc++ -I../inc matrix_cal_cuda.cu -o matrix_cal_cuda -std=c++17 g++ -I../inc matrix_cal_general.cpp -o matrix_cal_general mpicxx -I../inc matrix_cal_mpi.cpp -o matrix_cal_mpi nvcc -I../inc -c matrix_cal_mpi_cuda.cu -o matrix_cal_mpi_cuda.o -I/usr/lib/x86_64-linux-gnu/openmpi/include mpicxx -I../inc matrix_cal_mpi_cuda.o -o matrix_cal_mpi_cuda -L/opt/nvidia/hpc_sdk/Linux_x86_64/23.9/cuda/lib64 -lcudart nvcc -I../inc -c matrix_cal_mpi_cuda.cu -lmpi -I/usr/lib/x86_64-linux-gnu/openmpi/include -o matrix_cal_mpi_cuda mpicxx -fopenmp -I../inc matrix_cal_mpi_openmp.cpp -o matrix_cal_mpi_openmp -lstdc++ gcc -o matrix_cal_openblas matrix_cal_openblas.cpp -lopenblas -I../inc -lstdc++ gcc -fopenmp -I../inc matrix_cal_openmp.cpp -o matrix_cal_openmp -lstdc++
运行命令如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 !/bin/bash ./matrix_cal_cuda 2>&1 | tee logs/matrix_cal_cuda.log ./matrix_cal_general 2>&1 | tee logs/matrix_cal_general.log mpirun -np 4 ./matrix_cal_mpi 2>&1 | tee logs/matrix_cal_mpi.log mpirun -np 4 ./matrix_cal_mpi_cuda 2>&1 | tee logs/matrix_cal_mpi_cuda.log mpirun -np 4 ./matrix_cal_mpi_openmp 2>&1 | tee logs/matrix_cal_mpi_openmp.log ./matrix_cal_openblas 2>&1 | tee logs/matrix_cal_openblas.log ./matrix_cal_openmp 2>&1 | tee logs/matrix_cal_openmp.log
分析 (以general为例) 源程序进行计算的流程
检查命令行参数,获取路径,获取文件
对每个文件中的矩阵进行如下操作:
初始化结果矩阵
循环计算矩阵
串行计算
验证 A B -> C
(优化实现) 前言:应该说每个有优化的程序必须有调库和调用的区域 例如,对于mpi来说,就是调用<mpi.h>,对openmp就是<omp.h> 然后,对openmp来说就是有一个并行域用来执行任务划分/调度 制导同步/解决数据竞争和cache冲突 对mpi来说就是调用头文件 编译执行/通信 同步 /广播 /分散 收集/归约这些操作(在注释里也不难看出,给大佬跪了orz)
mpi:首先由主进程将任务划分到各个进程,每个进程持有一部分矩阵数据进行计算; 但结果矩阵并没有再划分,而是将各个线程归约,将数据集中在一起比较,这里理解是通信是需要时间的,显然传两次比传一次效率高
openmp:直接划分线程
cuda:cudaMalloc 分配内存 cudaMemcpy 从内存cp到显存 定义gird大小根据mp也就是矩阵大小计算 ,定义block包含16*16个线程 速度很快,不愧是矩阵专业户,计算返回
openblas:不会,爆了(x) 还是看一眼怎么使用的吧 cblas_dgemm(CblasRowMajor, CblasNoTrans, CblasNoTrans, m, p, n, 1.0, m1, n, m2, p, 0.0, answer, p); 但实际上官方有用户手册,理解就简单多了https://github.com/OpenMathLib/OpenBLAS/wiki/User-Manual
CblasRowMajor 行优先存储
CblasNoTrans 不转置
m,p,n指定两个输入矩阵和结果矩阵的维度 m1(mn) m2(n p) answer(m*p)
1.0 计算结果=1.0*(m1*m2)
m1,n,m2,p 指定输入矩阵和其列数
0.0 answer p 结果矩阵行p初始值0.0 经典blas三级运算 查了一下发现是“在O(n2)时间复杂度实现O(n3)量级的浮点运算,能充分发挥现代处理器的性能,并且能为用户提供透明的并发机制。” 啊……懂了 但实际上仍然是白痴(指向自己)
OPENMP改写 修改的内容如下
1 2 3 4 5 6 7 8 9 #include <cstdlib> int main (int argc, char * argv[]) { char * env_threads = getenv("OMP_NUM_THREADS" ); int num_threads = env_threads ? std ::stoi(env_threads) : 16 ; #pragma omp parallel for shared(answer, m1, m2, m, p, n) private(i, j, k) num_threads(num_threads) }
1 2 3 4 gcc -fopenmp -I../inc matrix_cal_openmp.cpp -o matrix_cal_openmp export OMP_NUM_THREADS=64./matrix_cal_openmp
比较如下:
实验次数/线程数
1
2
4
8
16
32
64
1
7.66
4.02
1.96
1.96
1.91
1.87
1.93
2
7.38
3.93
1.94
1.93
1.86
1.88
1.85
3
7.36
3.91
1.91
1.86
1.88
1.92
1.87
4
7.35
3.91
1.94
1.89
1.85
1.87
1.89
明显可以看出,在4线程前,运行时间都是减少的;4线程后,运行时间的变化就不明显了 很显然,这是cpu核心数导致的
MPI测试 1 2 3 mpicxx -I../inc matrix_cal_mpi.cpp -o matrix_cal_mpi mpirun -np 4 ./matrix_cal_mpi mpirun --oversubscribe -np 8 ./matrix_cal_mpi
比较如下:
实验次数/进程数
1
2
4
8
1
11.137
5.68647
3.29428
3.64855
2
11.0721
5.7994
3.09677
3.68822
3
10.9346
5.68283
3.03596
3.68843
4
11.1715
5.72521
3.00175
3.62148
多了得申请超进程,不然会报错(然而超进程也不能超多了,不然会有无法整除的问题) 但很明显超进程会导致性能下降,推测原因是模拟逻辑核心 导致资源消耗/通信时间增长
MPI+OPENMP 测试 1 2 3 4 mpicxx -fopenmp -I../inc matrix_cal_mpi_openmp.cpp -o matrix_cal_mpi_openmp -lstdc++ export OMP_NUM_THREADS=1mpirun -np 1 ./matrix_cal_mpi_openmp
比较如下:
实验次数/进程数(MPI)*线程数(OPENMP)
1*1
1*2
2*1
2*2
4*1
4*2
4*4
4*64
1
16.2246
15.9681
8.36887
8.39439
4.42203
4.58054
4.60151
4.40564
2
15.9925
16.248
8.25618
8.26528
4.25193
4.39034
4.62932
4.33078
3
16.2064
15.9181
8.12511
8.35172
4.32775
4.40731
4.64253
4.45745
4
16.0636
16.0229
8.05308
8.29208
4.37209
4.49706
4.66099
4.3982
很有意思的结果,但不出所料。多线程负优化在之前的一些实验中也能发现,但总归来说多线程还是能起到一定作用的(特定数量下) 为了探究这个问题,关于openmp和mpi混合编译,我找到了以下资料https://lab.cs.tsinghua.edu.cn/hpc/doc/faq/binding/#mpi-openmp 其中特别提到:
在运行计算密集的程序时,通常需要将进程、线程与 CPU 核心进行绑定(binding / pinning),即控制进程与 CPU 核心的亲和性(affinity),消除上述的各类影响 (PS:综合前文,这里应该指上述提到的进程迁移,冷启动开销,性能波动等)
在使用 MPI + OpenMP 混合编程时,进程绑定对性能的影响尤为关键。
我猜这就是为什么下一项就是关于亲和度绑定,由此诞生的问题或许能得到解答
MPI+OPENMP 亲和度绑定 进程/线程亲和度应该指其在指定某个CPU核上尽量长时间运行而不被迁移。比如,linux内核调度器会倾向于减少进程/线程迁移;或者将进程/线程直接绑定到CPU核上。
查看机器的NUMA结构如下:
Intel的openmp有一个特殊的api实现绑定https://www.intel.com/content/www/us/en/docs/cpp-compiler/developer-guide-reference/2021-8/thread-affinity-interface.html 在科学计算软件中使用openmp+mpi混合也有参考样例https://www.vasp.at/wiki/index.php/Combining_MPI_and_OpenMP 在之前我使用openmp+mpi时,出现了线程全部分布在奇数位cpu核上的情况,是因为:
在使用 MPI + OpenMP 混合编程时,进程绑定对性能的影响尤为关键。每个 MPI 进程需要绑定在一组核心上(通常属于同一个 NUMA domain),并把它的 OpenMP 线程绑定在其中的每个核心上。
OpenMP 线程只能绑定于其“可见”的核心上,也就是父进程被绑定的核心。
1 2 3 4 5 6 7 8 9 10 mpirun -np 4 -x OMP_NUM_THREADS=4 -x OMP_PROC_BIND=close -x OMP_PLACES=cores --bind-to core --report-bindings ./matrix_cal_mpi_openmp
比较如下:
实验次数/进程数(MPI)*线程数(OPENMP)
1*1
1*2
2*1
2*2
4*4
1
15.9717
16.1274
8.10164
8.14833
4.68596
2
16.0037
16.0947
8.12477
8.11571
4.51245
3
16.1317
15.9645
8.18908
8.13617
4.47034
4
16.0732
15.9364
8.1226
8.11981
4.46143
在测试中,每个线程绑定到一个 core,线程在 socket 上连续分布(分别绑定到 core 0,1,2,3) 根据结果所示,其耗时均值均略小于未绑定
CUDA分析 从之前对代码的分析来看,应该是在第一次运行中加载库/分配空间/将数据从内存 复制到显存这些操作耗时较长,但在之后的运行中则不需要重复这些操作 其实从结果来看,任何并行库加载都会耗时,但没有CUDA这么明显
不同MPI实现,Blas库
openmpi
mpich
intel mpi
openblas
intel mkl
cublas
这项先跳了,如果还有时间再回来做
OPENMP MPI MPI+OPENMP的缓存利用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 perf stat -e cache-misses mpirun -np 4 matrix_cal_mpi perf record -e cache-misses mpirun -np 4 matrix_cal_mpi perf report sudo perf report -i perf.data > perf.txt export OMP_NUM_THREADS=4perf record -e cache-misses mpirun -np 4 matrix_cal_mpi_openmp Performance counter stats for 'mpirun -np 4 matrix_cal_mpi' : 332531131 cache-misses 33.672167156 seconds time elapsed 83.46% matrix_cal_mpi matrix_cal_mpi [.] std::vector<double, std::allocator<double> >::operator[] 12.82% matrix_cal_mpi matrix_cal_mpi [.] main 1.97% matrix_cal_mpi [kernel.kallsyms] [k] copy_user_enhanced_fast_string 0.41% matrix_cal_mpi [kernel.kallsyms] [k] clear_page_erms 44.44% matrix_cal_open matrix_cal_openmp [.] main._omp_fn.0 31.55% matrix_cal_open [kernel.kallsyms] [k] copy_user_enhanced_fast_string 9.60% matrix_cal_open matrix_cal_openmp [.] main 3.93% matrix_cal_open matrix_cal_openmp [.] std::abs 3.49% matrix_cal_open [kernel.kallsyms] [k] clear_page_erms 1.13% matrix_cal_open [kernel.kallsyms] [k] page_fault 0.84% matrix_cal_open [kernel.kallsyms] [k] get_page_from_freelist 0.63% matrix_cal_open [kernel.kallsyms] [k] page_remove_rmap 85.77% matrix_cal_mpi_ matrix_cal_mpi_openmp [.] std::vector<double, std::allocator<double> >::operator[] 8.21% matrix_cal_mpi_ matrix_cal_mpi_openmp [.] main._omp_fn.0 1.99% matrix_cal_mpi_ [kernel.kallsyms] [k] copy_user_enhanced_fast_string 1.25% matrix_cal_mpi_ matrix_cal_mpi_openmp [.] main 1.02% matrix_cal_mpi_ matrix_cal_mpi_openmp [.] std::__fill_a1<double*, double> 0.40% matrix_cal_mpi_ [kernel.kallsyms] [k] clear_page_erms
1 2 3 mpicxx -I../inc matrix_cal_mpi_change.cpp -o matrix_cal_mpi_change perf stat -e cache-misses mpirun -np 4 matrix_cal_mpi_change mpirun -np 4 ./matrix_cal_mpi_change
我有一个思路是将块划分的大小改为适合cache的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int L1d_cache_size = 32 * 1024 ; int double_size = sizeof (double ); int block_size_L1d = L1d_cache_size / double_size;int block_size_i = block_size_L1d / n; int start_i = rank * block_size_i; int end_i = (rank + 1 ) * block_size_i;for (size_t i = start_i; i < end_i; i++) { for (size_t j = 0 ; j < p; j++) { for (size_t k = 0 ; k < n; k++) { answer_local[i * p + j] += m1_local[i * n + k] * m2[k * p + j]; } } }
但是呃失败了 我猜可能是数组越界了,因为没有考虑block_size可能不能被m整除的情况,但这方面我不是很熟悉
我另一个想法是将矩阵循环划分给各个进程,使其适应L1 cache,但仍然不知其可实现性如何 std::vector 应该也有优化方法,比如说使用其它类型的存储方式 也许使用bcc的cachestat和cachetop,以及pcstat会有更多的发现
后记:看HPCGame那篇。
SIMD SIMD 是 Single Instruction, Multiple Data 的缩写,意为“单指令,多数据”。这是一种在现代微处理器中广泛使用的并行计算技术。在 SIMD 模式下,处理器可以在一个时钟周期内对多个数据元素执行相同的操作。
Roofline Model Roofline Model应该是用于计算理论性能上限,即程序在某计算平台上可以达到最快的浮点计算速度。
则题目中的浮点运算次数是 1999 * 1000 * 1000 = 1.999 * 10^9 FLOPs 访问的内存量是 2000 * 1000 * 1000 * 8 = 16 * 10^9 Bytes 假设内存带宽DDR4 2666:21.3 GB/s 假设平台峰值计算能力为 P FLOPs/s 理论性能上限(FLOPs) = min(P FLOPs,21.3 GB/s * 1.999 * 10^9 FLOPs / 16 * 10^9 字节) = min(P FLOPs,2.665625 GFLOPs)
CPU性能分析 A little while, a moment of rest upon the wind
CPU 微架构性能分析与优化 perf 1 2 3 4 5 apt install linux-tools-4.15.0-151-generic linux-cloud-tools-4.15.0-151-generic linux-cloud-tools-generic perf stat ./matrix_cal_general perf record ./matrix_cal_general sudo perf report -i perf.data > perf.txt
1 2 3 4 5 6 7 8 9 10 11 12 Performance counter stats for './matrix_cal_general' : 351615.883768 task-clock (msec) 447 context-switches 0 cpu-migrations 389249 page-faults 1114263653464 cycles 2104440335203 instructions 50689096703 branches 53960798 branch-misses 351.642911157 seconds time elapsed
vtune 1 2 3 4 5 6 7 wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/dfae6f23-6c90-4b9f-80e2-fa2a5037fe36/l_oneapi_vtune_p_2023.2.0.49485.sh sudo sh ./l_oneapi_vtune_p_2023.2.0.49485.sh
居家必用👍
解读 比如,以上文perf结果为例
task-clock:程序运行了约351615.88毫秒。
CPUs utilized:程序使用了1个CPU。
context-switches:程序发生447次上下文切换。
cpu-migrations:没有发生CPU迁移。
page-faults:程序执行中有389249次页错误。
cycles:程序执行中有1114263653464个CPU周期。
instructions:程序运行中执行了大约2104440335203条指令。
insn per cycle:每个CPU周期内执行了大约1.89条指令。
branches:程序运行期间有大约50689096703条分支指令。
branch-misses:程序运行期间大约0.11%的分支指令预测错误。
seconds time elapsed:程序运行了大约351.64秒。
优化 每个人都有一颗想看懂源码的心 先尝试编译器优化吧(
火焰图 perf和vtune都有火焰图,vtune的自带在hotspot结果中,所以这里试一下perf 这里以matrix_cal_mpi_openmp为例 (lamegraph读到栈损坏,程序不运行就没法分析了)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export OMP_NUM_THREADS=4perf record mpirun -np 4 matrix_cal_mpi_openmp perf report perf script -i perf.data &> perf.unfold /root/FlameGraph-master/stackcollapse-perf.pl perf.unfold &> perf.folded /root/FlameGraph-master/flamegraph.pl perf.folded > perf_o_m.svg perf script | /root/FlameGraph-master/stackcollapse-perf.pl | /root/FlameGraph-master/flamegraph.pl > process.svg ps -ef | grep matrix_cal_general perf record -F 50 -a -p 4869 -g -- sleep 60 perf report perf script -i perf.data &> perf.unfold /root/FlameGraph-master/stackcollapse-perf.pl perf.unfold &> perf.folded /root/FlameGraph-master/flamegraph.pl perf.folded > perf_o_m.svg
编译器相关优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/5c8e686a-16a7-4866-b585-9cf09e97ef36/l_dpcpp-cpp-compiler_p_2024.0.0.49524_offline.sh sh ./l_dpcpp-cpp-compiler_p_2024.0.0.49524_offline.sh source /opt/intel/oneapi/setvars.sh intel64g++ -I../inc matrix_cal_general.cpp -o matrix_cal_general -O0 icpx -I../inc matrix_cal_general.cpp -o matrix_cal_general -Wincompatible-compiler clang++ matrix_cal_general.cpp -o matrix_cal_general -std=c++17
-O0 不进行优化处理。 -O 或 -O1 优化生成代码。 -O2 进一步优化。 -O3 比 -O2 更进一步优化,包括 inline 函数。
编译器版本,下载地址
换个机器
gcc (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0
Intel(R) oneAPI DPC++/C++ Compiler 2024.0.0 (2024.0.0.20231017)
Ubuntu clang version 14.0.0-1ubuntu1.1
开启-O0 -O3 后的性能分析比较 设备A
gcc 12.3.0
实验次数/优化等级
无
-O0
-O3
1
7.46082
7.56615
2.42238
2
7.59183
7.45967
2.5212
3
7.59043
7.45407
2.51571
4
7.5156
7.47651
2.52421
icpx
实验次数/优化等级
无
-O0
-O3
1
3.0166
6.34876
2.86272
2
3.03364
6.42262
2.66049
3
3.03413
6.52243
2.65275
4
3.09894
6.47761
2.65943
设备B
gcc 11.4.0
实验次数/优化等级
无
-O0
-O3
1
3.50859
4.43146
1.08519
2
3.51196
4.46978
1.14748
3
4.14652
4.68874
1.08495
4
3.84136
4.75892
1.10398
icpx
实验次数/优化等级
无
-O0
-O3
1
1.45927
2.91811
1.03595
2
1.14828
2.91811
1.00384
3
1.07161
2.87911
1.02894
4
1.04854
3.19374
0.994133
clang
实验次数/优化等级
无
-O0
-O3
1
3.36724
3.428
0.996193
2
3.31483
3.63713
0.990838
3
3.31882
3.7065
1.05766
4
3.43748
3.69653
1.00913
不同编译器的性能分析比较 1 2 icpx -I../inc matrix_cal_general.cpp -o matrix_cal_general -Wincompatible-compiler -O3 -qopt-report clang++ matrix_cal_general.cpp -o matrix_cal_general -std=c++17 -O3 -Rpass 2>&1| tee >clang_log.txt
见附件 matrix_cal_general.opt.yaml 和 clang_log
更多编译优化参数参数 对于intel c++来说,比如: -fast:最大化整个程序的速度,相当于:-ipo, -O3, -no-prec-div, -static, 和-xHos -Ofast:设置某些激进参数优化程序速度(实际上启用这项速度对比-O3反而略有降低
拓展 GPU性能分析优化 Nsight Systems 使用的版本
NsightSystems-2023.4.1.97-3355750
生成分析报告 1 2 3 nvcc -I../inc matrix_cal_cuda.cu -o matrix_cal_cuda -arch sm_86 ./matrix_cal_cuda /home/
对分析报告的简短理解 有cpu的使用和时长,gpu显存的使用timeline 对于cudaapi,可以看到红色的cudamalloc 对于接下来的矩阵,每段cudamalloc,然后cudamemcpy 计算结束后,cudafree所有数据
简短来看,报告展示了程序运行时cpu和gpu的情况,以及cuda的行为
拓展 MPI优化 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 wget https://registrationcenter-download.intel.com/akdlm/IRC_NAS/2c45ede0-623c-4c8e-9e09-bed27d70fa33/l_mpi_oneapi_p_2021.11.0.49513_offline.sh sh l_mpi_oneapi_p_2021.11.0.49513_offline.sh mpiicpx -I../inc matrix_cal_mpi.cpp -o matrix_cal_mpi mpirun -np 4 ./matrix_cal_mpi mpiicx --version mpicc --version
OPENMPI 和 Intel MPI 的版本
mpirun (Open MPI) 4.1.6
Intel® MPI Library (version 2021.11.0)
不同 MPI 实现编译的性能比较 前面有openmpi的
比较如下:
实验次数/进程数
1
2
4
8
1
11.137
5.68647
3.29428
3.64855
2
11.0721
5.7994
3.09677
3.68822
3
10.9346
5.68283
3.03596
3.68843
4
11.1715
5.72521
3.00175
3.62148
intel mpi
比较如下:
实验次数/进程数
1
2
4
8
1
2.55897
1.48034
0.779943
1.0831
2
2.54911
1.44536
0.771445
0.974259
3
2.54947
1.44413
0.802421
1.01117
4
2.55782
1.44145
0.780028
0.981472
intel赢麻了
IPM 工具 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 mpicxx -I../inc matrix_cal_mpi.cpp -o matrix_cal_mpi -L$PREFIX /lib -lipm export LD_LIBRARY_PATH=$LD_LIBRARY_PATH :/root/IPM/src/.libs/mpirun -np 4 ./matrix_cal_mpi mpiicpx -I../inc matrix_cal_mpi.cpp -o matrix_cal_mpi -L$PREFIX /lib -lipm
mpi多机 1 mpirun -n 10 -hosts client,master ./matrix_cal_mpi
不得不品鉴之mpi https://en.wikipedia.org/wiki/Message_Passing_Interface https://docs.open-mpi.org/en/v5.0.x/mca.html https://www.open-mpi.org/ https://stackoverflow.com/questions/66228038/concurrent-communications-in-mpi-with-mpi-thread-multiple-access-level
OPENMPI 的主要功能是通过**模块化组件架构(MCA)**来实现。 OPENMPI 的软件体系结构包括以下几个层次:
OPENMPI 层(OMPI):顶级 MPI API 和支持逻辑
开放运行时环境(ORTE):后端运行时系统的接口
开放可移植性访问层(OPAL):操作系统/实用程序代码(列表、引用计数等)
在处理多种网络通信方面,MPI 保证了一对等级之间的消息是非超越的,这限制了什么和何时可以通信。这并不排除同时传输多个消息,只要网络允许,无论请求是顺序发布的还是来自多个线程。OPENMPI 利用多个网络连接(如果有多个接口提供连接性)。
后记 要学的还有很多,未知在等待我们。
每章节副标题出自纪伯伦《先知》
参考链接 什么是高性能计算 (HPC)?