GPU Hackathon 2019 日记

1. GPU Hackathon 介绍

据NVIDIA的员工说,GPU Hackathon 最早是美国橡树岭国家实验室发起的,因为在近些年,GPU的架构非常适合执行并行计算,GPGPU的概念流行后,显卡的GPU可以支持大规模的并行计算,并行计算的速度远超CPU,所以当时的橡树岭在他们的超算平台上配置了不少的GPU。但是如果想用GPU加速计算的话,就需要修改自己的程序,使得程序能够运行在GPU上,然而在当时编写GPU加速代码的门槛比较高,很少的程序能够充分利用GPU的性能,所以橡树岭希望举办一些活动推广GPU计算的技术。后来NVIDIA作为一个GPU厂商,自然也希望更多的用户学会使用GPU,这样他们就能卖出更多的显卡。

这个活动的流程是这样的,每一支队伍拿出自己原先运行在CPU上的程序,然后在为期5天的活动中,NVIDIA会为每一支队伍安排两名导师,在导师的指导下我们修改自己的代码。此外,每一天每个组都要做一个简单的报告,介绍自己的进展,瓶颈,和下一步的工作。最后,会有一场大型的报告,各组总结自己的工作,并汇报所取得的加速效果。

2. 活动前的准备

GPU Hackathon建议我们提前两周,团队就要开始进行一些初步的工作。然而,我因为假期参加了NUS Summer Workshop,活动开始前两天我才回到了学校。

我们组实际上被划分为两个小组,每个小组会负责一份代码的优化,另一个组的成员全部是我们学校力学与航空航天系的一位教授的课题组的研究生和研究助理,而我们组都是对并行计算感兴趣的本科生,我们要优化的代码也是他们课题组提供的。

活动开始前几天的晚上,我去到他们的课题组,大家简单的介绍了一下自己,接着他们打包了一份原始代码让我先看看。他们的代码都是用Fortran写的,我一开始觉得我学过不少现代的高级编程语言,看懂这古老的高级编程语言应该不是什么困难的事,然而后来活动开始后,事情越来越不妙了。

3. 困难和坑

踩坑可以说是第一天就开始踩,不过对于这点我还是有所心理准备的,只不过我没有预计到之后我天天踩而已。

Day 1-2

第一天我们小组还好,还有一个经验丰富的学长坐镇,踩坑可以一起踩,摔得没有那么惨。我们遇到的第一个问题就是VPN登录不了。一般来说,超算的全部节点,包括登录节点全部隐藏在内网里,需要用一个VPN隧道访问内网,其实这就是VPN最原始的工作。NVIDIA的测试集群也是如此,需要借助一个叫NVIDIA Next Generation VPN Gateway的东西才能连接到测试集群。然而一开始我们去下载客户端Cisco Anyconnect Secure Mobility Client的时候,官网先让你注册,注册后告诉你你没资格下载。接着我们试着从第三方网站下载客户端,不是版本太老就是版本太新。最后发现NVIDIA有提供客户端下载,安装后发现连接不上深圳上海北京香港的Gateway,直到连接美国的Gateway才连接上并下载配置文件,借助配置文件才能连接其他国家的Gateway。其实最迷惑人的是,NVIDIA的工程师再次期间也不知道怎么办。

解决VPN的问题花掉了上午大部分时间,接着试着编译他们课题组给的Fortran代码。首先使用Linux Environment Modules这种古老的东西配置环境。

1
2
3
4
module load PrgEnv/PGI+OpenMPI/2018-03-19
module load cuda
module load pgi
module load openmpi

CUDA是NVIDIA GPU的支持库,PGI是个编译器,OpenMPI是CPU进程间通信使用的库,Hackathon主要推广的技术是OpenACC,这个库由CUDA和PGI支持。

本想一句make编译一遍过,又不是什么大工程像什么CESM这种编译起来一堆坑的,结果这份代码却依赖一个极其古老的库,FFTW2,1999年last updated,连SSE这种古老的SIMD指令都没用上的东西。与此同时在NVIDIA的测试集群只有FFTW3的库。更加坑的事情是,FFTW3的接口完全不兼容FFTW2的,这就意味着要是修改原始代码以适配FFTW3,那工作量够我们喝一壶的。学长直接按照README编译FFTW2,想编译出静态链接库,结果因为时代变迁,当时写的编译脚本已经不能在现在的系统下运行了。我一开始试着去解包CentOS的FFTW2的yum二进制包,得到了动态链接库和头文件,编译是能编译了,但是运行程序的时候动态链接库不在指定路径里,并且我们的权限不能将动态链接库复制到指定路径下。不得已,只能借助Arch Linux编译安装的脚本去编译FFTW2,才得到了静态链接库。

Day 2-3

从第二天起,那位经验丰富的学长就跑路去准备雅思了。成功编译后,工程师发现,使用命令

1
mpirun -np 40 ./main

可以用40个进程运行,然而在使用20个进程运行程序的时候,MPI就会引发错误。工程师让我去排查问题的原因。起初我想用GDB之类的debugger,结果我发现GDB只能trace一个进程,而且往往不是trace的进程出错。于是我想用古老的print打印日志法调试,然而40个进程同时将log连同错误信息异步打印到屏幕上,根本不知道错误信息是哪个进程引发的,好死不死因为MPI出错导致MPI_Barrier也不工作了。然后我想着用stderr重定向错误信息到文件,在C语言里用freopen轻松实现,结果Fortran里没有这种函数。最后只能一句句的修改代码,让程序运行到某个位置终止程序,从而判断哪一行代码引发MPI错误。最后发现是block分割时相关代码,将进程数除8,也就意味着进程数需要是8的整数倍。滑稽的是,相关代码附近的注释写着,慎重处理8这个数,没有文档谁知道这个事。

Day 3-4

解决了MPI问题,random seed的相关代码引发了Runtime error,我看了一眼代码的上下文,又看了一遍上下文,还是没看懂这代码在干什么,于是我先问候了代码的作者的家人,再去问候“代码的作者”,然而“代码的作者”说这部分代码应该是十年前教授在美国的学生写的,他只是直接拿来用,但是他强调在Intel编译器下代码没有问题。一顿搜索猛如虎后发现Intel编译器的random seed的array size是2,同时PGI编译器的random seed的size是37。但是在我接触过的语言里,random seed就是个数,而不是数组,难怪我看不懂这段代码。

Day 4-5

代码能在CPU上运行了,与此同时其他组已经开始取得数十倍甚至百倍的加速效果了,哪怕这段代码跑出来的结果未必正确,先做个性能分析再直接加上OpenACC导语先跑起来再说。

1
2
mpirun -np 40 nvprof --cpu-profiling on --cpu-profiling-mode top-down --annotate-mpi openmpi  -o output.%h.%p.%q{OMPI_COMM_WORLD_RANK} ./main
# command to get the profiling results

使用上面这段命令既可启动NVPROF,配合NVIDIA Visual Profiler就可以做性能分析,加上NVTX的话就可以增加tag,对自定义的代码片段进行性能分析。

但是,运行nvprof的时候又引发了工程师都不知道怎么发生的错误,结果去掉--annotate-mpi openmpi这个开关就可以正常运行了,反正也不知道为什么就好了。

加上OpenACC导语后又引发了CUDA Kernel的错误,一番排查后发现Fortran的数组支持负数的下标,然而OpenACC隐性生成copyin代码的时候对数组大小识别错误,调整了导语后可以在GPU上运行。

然而不知道为什么,用GPU加速的程序比原来慢了很多,打开Profiler一看,显示只有一个GPU一个SMX的一个CUDA核心在运行串行的程序,换句话说就是一核有难,20479核围观。

在工程师的建议下,在活动结束之前,程序能够跑满一块GPU了,但是因为运算结果保存在GPU上,CPU需要取得这些数据时,CUDA为了维护Unified Memory的一致性开销巨大,在Profiler中大块大块的时间片都是Memory Page Fault。

最终我们运行在GPU上的程序的计算速度和原始版本相近,并行计算节约下来的时间全部被数据离散传输消耗。

4. 总结

总的来说,还是很感谢深圳超算中心和NVIDIA提供的这次宝贵的踩坑经验,所谓的程序员的经验其实绝大部分就是踩坑和从坑里跳出来的经验,对于很多人来说,他们连踩坑的机会都没有,毕竟这种核武器级的设施也不是随随便便就能接触到的。同时也要致敬那些用超算科研的科学家,因为在超算上还能看到大量上个世纪的计算机痕迹,实话说这玩意是真的很难用好,为了取得更高的计算效率需要付出大量的努力,然后代码变得难以维护和移植,而且科研是漫长的过程,维护上古代码也不是什么轻松的事情。

加上导语后却引发了CUDA Kernel的错误,一番排查后发现Fortran的数组支持负数的下标,然而OpenACC隐性生成copyin代码的时候对数组大小识别错误,调整了导语后可以在GPU上运行。