[导读]工业4.0、人工智能、大数据对计算规模增长产生了重大需求。近年来,中国高性能计算机得到突飞猛进的发展,从“天河二号”到“神威·太湖之光”,中国超级计算机在世界Top500连续排名第一。云计算、人工智能、大数据的发展对并行计算既是机遇又是挑战。如何提高应用的性能及扩展性,提高计算机硬件的使用效率,显得尤为重要。从主流大规模并行硬件到能够充分发挥其资源性能的并行应用,中间有着巨大的鸿沟。
本次讲座由清华-青岛数据科学研究院邀请到了北京并行科技股份有限公司研发总监黄新平先生,从高性能并行计算发展趋势,到高性能并行计算性能优化基础,包括从系统级到代码级分析,常用的优化方法与工具,并行计算优化实战等方面进行了深入浅出的讲解。
黄新平先生长期从事半导体、编译器和操作系统内核方面的工作,有10年的编译器设计, 包括GCC、英特尔、摩托摩拉公司的多款编译器的开发经验。Solaris 内核的x86 移植和优化,以及国内一些顶级互联网企业的技术支持工作,如新浪、360、京东、爱奇艺等。2014年加入北京并行科技,担任研发总监,负责整个产品研发,提供高性能计算的公有云及私有云服务。
课程精华笔记
▼
一、高性能和并行计算发展的趋势
黄新平先生首先阐述了为什么要关注计算的性能,因为世界上总有一些大问题,还有更多新问题需要大量的计算去解决。比如全基因的排序,精准医疗等应用。石油能源行业的地震勘探数据处理,最终得到油藏数据,价值以亿计。还有一些是没法做实验的,比如核试验,只能通过计算模拟。在现代化工业制造中,比如汽车制造中需要确定风阻系数,以前需要做模型进行风洞试验,又贵又慢,而现在做一个计算机CAD模型,用流体模拟软件去计算,就变得非常便宜而且灵活。
2008年奥运会的天气预报,需要精确到每个场馆,而以前是以城市为单位的,精度从百公里缩小到以1公里为单位的网格进行计算,整个计算量提高了不止成千上万倍,但计算必须在规定时间期限内算完,所以,必须要以更快的速度去进行计算。所有这种种,都体现了人类对计算性能的需求永无止境。
然后,黄新平先生阐述了CPU体系架构发展的趋势。
2007年以前,CPU的性能基本等同于频率,逐年按照指数级曲线增长。但是从2007年开始,CPU的频率已基本上不再增长,甚至还往下掉,这是因为纯粹的频率的上升导致了CPU功耗急剧加大,最终散热问题成为瓶颈。但是著名的摩尔定律还是被很好地遵守着,每年仍然不断按指数级增长的晶体管数目被用来做成CPU内部的多核,进行并行计算,以提高系统的性能。
这些年来,在处理器体系架构技术里面,对性能影响最大的、程序员可见的,一个是多核和超线程技术,另一个就是向量化。所谓向量化,比方说在矩阵相乘的运算中,普通的做法是一个循环分别取一行和一列里面的一个数,做乘加运算。如果先把行和列的数集中放在一个比较宽的寄存器里面,比如512位的寄存器可以放16个32位的单精度浮点数,一个乘加指令就能完成对这16个数的计算,从而得到16倍的速度提高。
还有一些是程序员不可见的底层技术,一个是指令流水线,类似于工厂生产流水线,多个指令在不同的执行阶段被同时处理,提高了吞吐率。一个是超标量,这个技术就是有多条指令同时在执行单元被执行。还有一个是乱序执行技术。指令按顺序执行的时候,假设其中有一条指令需要读取数据,一般来讲,从一级高速缓存中取得数据需要经历16个时钟周期左右的时间,在等待数据取回的时间里面,它后面的指令需要跟它一起等,这显然是极大的浪费。
如果后面的指令没有用到这个数据的话,实际上完全不用等,所以可以设计一个乱序执行机制,把一批指令都扔到执行单元里面去算。如果不需要等数据的指令就可以直接把结果算出来,在这个过程中需要等的数据到了,那条指令就可以执行了,然后按照指令顺序,一条一条指令完成退出,最终的执行结果还是按顺序的,但是执行的过程是乱序的。
乱序过程提高了整个运算单元的硬件利用率,掩盖掉慢速指令执行导致的延迟,能够带来非常大的性能增长。《计算机体系结构 - 量化研究方法》一书中有专门的章节讲乱序执行技术,如果大家感兴趣的话可以找来看看。在wiki上搜索Tomasulo算法也有比较详细的解释。
另外一个影响比较大的是集成内存控制器,原来内存控制器是做在主板上,现在做在CPU里面,这样使内存访问的速度比以前快很多,大幅度降低了延迟,提高了内存访问的速度。现实中CPU运算速度的提高是远远高于内存访问速度的提高的,这两者之间的差距越来越大,大多数指令执行仅需要一个时钟周期,而服务器内存的数据访问则大约需要300个时钟周期左右。
有很多的应用性能不高,就是因为有大量集中的内存访问,由于内存的速度跟不上计算速度,所以处理器不得不停下来等待数据到达。除了加大高速缓存的容量之外,还有一个办法就是增加高带宽的近核内存。例如大家比较熟悉的GPU,当处理器系统还在使用DDR3内存的时候,GPU早就使用DDR5内存了。
黄新平先生指出,如果不利用多核来写多线程的程序,以及线程中没有利用向量化指令来做运算,例如实测使用英特尔至强CPU的服务器,2007年和2014年相比,跑单线程并且没有使用向量化指令的程序,性能几乎没有增长。而经过多线程并行化和向量化调优之后,性能就会有102倍的提高了。
黄新平先生同时详细介绍了intel近年推出的众核新产品Xeon Phi KNL。
在不断追求高性能的处理器当中,英特尔推出被称为众核处理器的一系列产品。第一代产品的代号是KNC。在全球高性能系统中,连续三年排名第一的天河二号,就使用了KNC。KNC看起来像GPU,插在PCIe 插槽上。一块KNC已经做到了1 TFlops的浮点性能, 也就是每秒钟可以执行1T条双精度浮点型运算指令。而现在的KNL一块CPU就可以达到3 TFlops。它的最高配置达到72个核,每个核4个线程,每个核有两个512位的向量处理单元。另外它的封装里面带了一个16G的近核内存,读写带宽高达400GB每秒以上,系统DDR4内存的带宽达到90GB每秒。
GPU是图形处理单元,增加了通用处理功能后,被称为GPGPU。它的设计思路与上面所说的类似,通过使用高带宽内存减少内存的访问延迟,使用数以千计的大量向量处理单元并行处理数据,从而获得极高的性能。现在GPGPU在机器学习里用得较多,GPGPU也针对机器学习做了较多的优化,比如降低浮点数精度,16位的浮点数,因为很多算法对精度要求很低,从而进一步提高了处理速度。
黄新平先生提出在高性能/并行计算系统越来越碎片化和普遍采用异构架构的今天,性能调优显得尤其重要。
从发展趋势来看,高性能运算的机器越来越普及,越来越碎片化。最早从巨型机,分布式处理机、向量机,到最后走向集群,而现在出现GPGPU之后,单台服务器,甚至是一台笔记本的性能就可以媲美几年前看起来很强大的高性能运算中心。高性能并行计算不再远在天边,而在你的指尖。第二个变化是异构架构的普及,协处理器,GPGPU,FPGA等高性能处理部件飞入寻常百姓家。在这样两个大趋势下,高性能和并行计算编程、调优不再是一个很遥远的事情,而是一个必须每天要面对的事情。
黄新平先生同时指出并行计算编程常用的有两个技术,一是OpenMP技术,一是MPI技术。
针对单台服务器,准确地说是共享内存系统,充分利用多核、多线程的并行处理能力,通常使用OpenMP技术。对于大量的数据做类似的处理的应用,通常在编程中使用计算密集循环来完成数据处理。这个循环一般就可以通过OpenMP 技术,添加编译器指导指令使其自动变成一个多线程程序,每个线程处理其中一部分数据,在执行完以后自动把结果收拢起来,得到最终结果,这样就能充分利用多核的处理性能了。
对于问题规模超过单节点处理能力的应用,可以使用MPI技术。利用很多台机器同时运算,比如天河二号上面有的应用需要使用上百万个核做处理,显然不可能有一台机器可以拥有100万个核,那么当使用这么多台机器一起处理数据的时候,一个重要的问题就是要通过网络互联来交换数据。如果自己从底层开始编写网络交换代码是不现实的,你会发现大部分时间都在调试各种网络问题,没有时间写算法了。而有一个通用的标准API把它封装起来,做这些底层工作,这个API就叫做MPI,用于发送信息和接收信息,实现数据交换。
异构程序的开发也比几年前好了很多。英特尔的KNL的开发就跟普通的CPU编程没有区别,针对GPGPU编程也可以使用CUDA或者OpenCL,这些都大幅度降低了开发的难度。因为时间的原因这里就不具体展开编程方面的介绍了,有兴趣的可以参阅相关书籍。
二、高性能和并行计算优化基础
黄新平先生首先介绍了决定性能调优上限的两个定律:阿曼达定律和Gustafson定律。
阿曼达定律说的是,如果一个程序包括并行和串行,随着机器数量增加,并行执行时间会越来越短,最后趋向于0,串行的时间没有变,这就是加速比,如果串行部分占到了整个执行时间的50%,意味着加到1024台机器也只能加速一倍,非常浪费,哪怕并行的部分占到90%,其实也就是加速了9倍。这个讲得是当工作量是固定的时候,可以并行处理的部分所占比例越高越好,描述的是程序的强可扩展性特性。
Gustafson定律说得是,在不断增加处理工作量的情形下,增加系统的规模是有用的。比如天气预报计算,如果增加了1万倍的处理量,又必须在规定时间之内算完,怎么办。很多情况下,工作规模增加的部分都是可以并行处理的部分,因此增加系统规模自然会缩短处理时间。这是一个非常乐观的定律,也就是说汇聚了很多机器一定能干更大的事。
阿曼达定律描述了随着增加更多的处理器,串行部分处理没变,只是并行部分的执行时间会不断减少,但总的加速比是受串行部分所占比例限制的。而Gustafson定律则描述了随着工作量的增加,加上更多的处理器单元,可以缩短并行处理的时间,从而在规定时间内,处理的工作量增加了。
黄新平先生然后介绍了调优的方法论:
调优是一个看起来很庞杂、很细致的工作。需要一个方法论做指导,才会事半功倍。调优过程是这样一个循环,首先要找到一个典型的、可重复的测试用例,然后使用这个测试用例得到一个基准,记录这个基准。在基准的测试过程中,需要收集大量的性能数据,这些性能数据将会指出问题所在。
系统的性能遵循木桶原理,也就是整体性能是由系统中最短的那块板决定的,在性能数据收集的过程中可以通过性能指标发现哪个地方有问题,根据具体问题就可以找到相应的解决方案来解决。使用解决方案解决这个问题之后,再重新测试收集,找到下一块短板。以此循环往复,直至性能达到期望或者无法继续增进为止。
整个优化应该采用自上而下的方法,顺序一定不能乱。首先通过标准性能基准测试程序确保系统的工作状态正常, 比如使用SpecInt, SpecFP,Linpack等得到处理器的性能,对比设计性能指标,可以得知CPU是否工作正常,BIOS或者操作系统中的相关设置是否正确。使用Stream测试程序测试内存系统的性能,Netperf测试网络性能,Fio或者iozone等测试文件系统性能是否正常。在所有调优开始之前,一定要先把基础做好,一定要了解你的系统性能极限。
再来是应用调优,可以调节运行环境,或者有代码的可以调整代码,最后才会到处理器级别的调优,这里是榨干最后一滴性能的地方。
比如说我发现内存访问是一个问题,第一个步骤是检查硬件设置,服务器的内存插法是有黑魔法的,怎么插,插在不同位置上,性能是不一样的,BIOS内存访问方式设置也会有比较大的影响,可以翻看相关服务器的产品手册。
另外内存控制器是有不同通道的,每个通道速度不一样,而且一般来讲当内存所有通道都被插满内存条的时候,内存会被自动降频,因此每CPU使用单条大内存的性能会超过同样内存总容量下,所有内存插槽都插满了的小内存的速度。当服务器有多个处理器存在的时候,通常是一个非对称统一内存地址访问的架构(NUMA架构),也就是说每个CPU的内存控制器都有可能挂着自己的内存,它离得最近,别的CPU就远,因此内存是有距离的。BIOS中设置成对称多处理(SMP),内存编址将会混合远近不同的物理内存地址,达到均匀访问的目的,但是会损失性能。而现在几乎所有的主流操作系统都是NUMA 感知的,能充分利用NUMA架构,尽可能使用近距离内存以提高性能。
硬件调整做完之后还有软件调整,比如操作系统里调整内存页的大小,缺省的4K一个的小页在海量内存容量的情况下,意味着页表条目非常巨大。会增加DTLB Miss的机率,导致系统忙于装载切换DTLB和内存页,从而损失性能。
在应用代码中最主要相关的是数据结构和算法。比如说,某一点的坐标float X,Y,Z,有大量的点需要处理,而有时候会用不同的方式分别处理X,Y,Z,假设定义一个结构,包含X,Y,Z三个值,然后再定义一个结构的数组,每个元素是一个结构,如果这么处理的话,处理X值的任务是怎么访问内存的呢?隔一段距离跳着访问,内存访问的效率是很低的,尤其是高速缓存的利用率,还会产生所谓假共享(false sharing)的性能问题。
这里有一个改进办法,就是定义一个结构,包含三个数组,所有X放在一个数组,所有Y放在一个数组,所有Z放在一个数组。它有几个好处,首先,这样所有的X值就连续放在一起了,尽管在程序中访问内存的时候是可以按字节存取的,但是CPU在实际执行的时候是以高速缓存线(cache line)最小单位存取的,缓存线一般是64个字节大小,第一次读取X的第一个数的时候,实际上一次就取64个字节上去,第二个数也被读取了,因此访问第二个数的时候,在高速缓存线中,可以大幅度节省时间。还有因为特别有规律,一个一个连取,CPU内部会自动预取数据,它会在使用之前就把数据准备好,这个数据结构的更改会大幅度提高性能。
同样因为按高速缓存线为单位存取的原因,假如处理过程中都需要修改X,Y,Z的值,在原来基于结构的数组的情况下,处理X数据的任务,修改了X值,会导致同一个点的Y值在高速缓存中也被标记为修改了,因为它们在同一个高速缓存线中,会导致处理Y的任务,在读取Y值的时候,被迫刷新高速缓存线,从内存中重新读取数据,这就是所谓的假共享问题,会导致性能急剧下降。
黄新平先生介绍了性能调优的方法分类:硬件级、运行级、编译器级和代码级。
(一)硬件级
性能优化的方法有很多种,第一个叫硬件级调优,就是简单粗暴直接换掉性能低的硬件,比如网卡从千兆换到万兆的,硬盘从机械的换成SSD等等。很多时候这也不失为一个好办法。
(二)运行级
另外一个所谓运行级调优,是从运行环境上调整,通过监控整个系统的性能及各项指标看问题所在,然后看能不能通过一些运行参数的调整,比如说内存的使用率非常高,可以试试在操作系统中调整内存页的大小。如果是网络带宽压力特别大,可以试试将网络包的处理程序绑定在某一个核上面。对于网络小包特别多的情况,有一些网卡带包聚合功能,等很多小包会聚合到一定的程度,再统一处理,大量减少中断数量,降低系统消耗。这些调整的成本很低,难点在于对技术人员要求很高,需要对整个系统非常熟。
(三)编译器级
还有编译器级调优,需要有代码,但是不修改代码,使用编译器的优化选项,有的时候也能够获得巨大的性能提高。比如引入自动向量化,深度优化,性能剖析指导的优化(PGO)等等。需要技术人员熟悉编译的使用,以及对优化过程的理解。
(四)代码级
还有代码级调优,也就是直接改动实现代码,改代码收益或许非常大,比如换个算法,因为有的算法就是比别的算法快很多倍,甚至快几个数量级,但是修改的难度极其大,成本极高,代码改了以后正确性要重新验证,所有测试的步骤都要做。
在原有串行单线程程序中,如果有比较明显的计算密集型循环,可以引入OpenMP进行并行化,结合编译器的自动向量化编译选项,可以只改极小一部分代码,获得比较大的性能收益。
黄新平先生紧接着介绍了在性能调优中常用的一些关键指标:系统性能指标包括CPU利用率、内存使用率,swap分区、带宽使用率;处理器性能指标包括CPI、浮点运算的峰值、向量化比例、cache miss、DTLB miss等。
第一个,CPU利用率,CPU利用率其实内部包括了几个方面,光看利用率数值是不够的,比如CPU利用率100%,但是system time占了30%,这样其实性能并不好,user time是CPU跑用户程序的时间,越高越好,而system time 是消耗在内核里面的时间,通常是由网络、磁盘的一些中断导致的,更常见的是锁。还有一个是io wait,当大量磁盘读取的时候就会有比较明显的占比。
还有一个是swap的使用, 当使用到swap的时候会感觉很慢,几乎没有响应了,这是为什么?是因为硬盘的访问及其缓慢。 大家通常对特别小的数据没有太多概念。一般来讲,CPU取第一级Cache典型的时间是十来个时钟周期,如果按2G赫兹算的话是几个纳秒,而硬盘访问的延迟大概是几十个毫秒。有一个很形象的比喻,如果从一级缓存取数需要1秒的时间的话,那么从硬盘中读取数据就需要116天。
另外一个是网络,网络分为千兆网、万兆网、infiniband网络,这个千兆网就是1000比特每秒,万兆就是10000比特每秒。infiniband是一个非常快速的网络,它能达到40G到100G。
到了CPU里面,有一个重要的指标叫CPI(Cycles Pre Instruction),就是每条指令需要多少个时钟周期完成,现在主流处理器的理论值是0.25,因为它有4个算术逻辑运算单元。实际上测下来,比如Linpack能够达到0.33。为什么CPI会升高,因为指令有内存访问的时候它不得不等待,虽然乱序执行能够掩盖一部分延迟,但是不可能完全填补运算速度与内存访问直接的差距,这个指标值就上来了。
另外一个指标是浮点运算的峰值,如果大家关心过GPU的一般都会知道这个数,每秒钟执行多少浮点型运算。实际运行的浮点型运算的性能值和理论峰值之间的比值就是浮点运算单元的使用率,很不幸的是并行科技运维过很多计算中心,发现绝大多数应用的这个使用率小于10%, 也就是说买了一个160公里时速的车永远以10多公里的时速开, 问题基本都在软件层面上。有时候优化过后,就可以达到60%到70%多,一下子性能提升了六七倍。
向量化比例是刚才浮点型运算效率偏低的原因,因为没用向量化,还在用单个标量运算,肯定性能比较烂,利用向量化之后,一条指令执行16-32条数据就处理完了,而标量计算才处理一个,很多时候就是改一改编译器的选项就完成了。
Cache miss比例是指数据访问的时候没有在高速缓存里找到的,需要到内存中去取,肯定会带来较大的性能损失,这个miss比率对性能的影响比较大, 性能好的时候是很低的数据,一般要小于10%。
还有一个是DTLB miss。进程访问的地址空间是逻辑地址空间,比如64位的程序,地址范围从0到2的64次方减一,然而这么大的地址空间,实际上不可能配置这么多物理内存的,需要建立一个从逻辑内存到物理内存的映射表,这个映射表的管理是按照页为最小单位的,缺省的页大小是4K。但是页大小可调,越大的页,页表条目越少,页大小为4M的跟4k的页表条目就差了1024倍。这些页表不可能完全放在CPU里面,CPU只能放在一个称为DTLB的缓冲区中,更多的部分放在内存里面,当映射相应条目的时候,如果在缓冲区找不到就会产生DTLB miss的异常,这个异常处理会从内存中找到相关条目,然后放入DTLB缓冲区。这个操作极其耗时,经常发生时对性能影响极大,通常可以通过调整页大小,或者在程序中使用内存池等内存管理技术减少耗时。
还有像内存带宽,内存带宽体现了内存访问的忙闲程度,这个值高到一定程度,会导致内存延迟迅速增加,有一些工具比如并行的Paramon和Intel的VTune可以帮助测量这个值。
三、并行应用调优工具
工欲善其事必先利其器,工具按照刚才调优的级别会有不同,一个是系统级的分析,包括CPU、GPU、网络、内存、swap等。一是应用级分析,包括代码中各函数的耗时、MPI计算与通信占比等。
并行科技的Paramon应用性能收集监控工具,会实时收集大量性能指标,实时显示并分析常见性能问题,并将这些数据记录下来,供Paratune应用性能分析工具进行详细分析,Paratune可以实时回放集群各服务器性能指标,并且将这一段的运行特征分析出来,以四个象限展示,是CPU密集型、内存访问密集型、磁盘密集型还是网络密集型一目了然。这是并行最早的产品,现在这一块作为HPC云计算服务的一个基础的工具存在,我们给客户服务的时候全是拿这些东西做分析。建立了一个大量的运行特征库,可以直接指导应用的调优方向及匹配的硬件服务器的选型与采购。
英特尔的工具VTune,这是一个调优神器,实际用起来操作很简单,难在它给出了一大堆报告和数据之后怎么样解读它,怎么样利用它。它可以迅速定位应用中最耗时的部分以及发现并行程序中不正确的同步导致的性能损失。直接对应到代码帮助调优者迅速找到原因。
代码级性能优化即代码现代化的方法:
代码级的性能优化,有一个非常好的,就是称为代码现代化的一系列方法。 最主要的有并行化,就是要多线程化,充分利用多核资源;另外一个是向量化,充分利用处理器向量位宽,实现单指令多数据的处理;还有是内存访问优化,在KNL或者GPU这样的有高速高带宽内存的时候,需要充分利用这些资源,将最常访问的数据与代码放入高带宽内存区域,缩小处理器处理速度与内存访问速度之间的差距。
四、并行应用调优实战
黄新平先生通过演示矩阵相乘程序阐述编译参数的调整进行性能调优。
不改变代码,仅利用编译器参数进行调优。为我们演示了2048乘以2048的矩阵相乘的算法。仅仅通过编译器编译选项的调整就可以大幅度提高运行效率。执行时间从25秒缩短到0.14秒,性能提高178倍,却不需要改一行代码。
黄新平先生通过Black Sholes算法程序的调优过程演示了少量改变部分代码的方式进行性能调优。
Black Sholes算法分了两个部分,第一个部分用随机数产生器产生大量随机数,模拟买卖单。 第二个部分利用模拟数据计算当前的投资价值。首先用VTune寻找问题, 先找热点,发现指数运算函数、对数运算函数加上一个随机数产生器函数被大量调用。而且它是一个单线程的程序,所以第一件事就是在模拟计算部分的计算密集的for循环处加了OpenMP编译指令,同时使用编译器的自动向量化编译选项,获得了4倍的性能提升。继续考察程序,发现初始化部分的随机数产生器, 在英特尔的MKL库里有一个非常好的实现,因此可以直接换上这个实现,最终总体程序获得了22.8倍的性能提升。
演示中跑50万个模拟,三次迭代。基准性能数据是平均1472个时钟周期做一个模拟,初始化用了1073个时钟周期,模拟计算花了399个时钟周期。使用OpenMP和向量化指令优化后, 总时间变成了364个时钟周期,初始化用了338个,计算用了26个。换成MKL库的随机数生成函数后,总时间变成了64, 初始化用了35,计算用了29。在一个双核的笔记本上,计算时间从1472变成64,22.8倍的提升,实际代码量改动非常小。如果在更多核的服务器上将会有更大的性能提升。
问答精选
Q:Paramon性能采集工具是怎样采集CPU以外的性能数据的,采集程序是否影响结果的准确性?
A:Paramon性能采集工具不需要修改运行程序,它对运行指标的收集都是从内核的数据结构里面提取的,不会影响被监测程序实际的运行,额外负载非常低,通常单核在0.5%以下。
Q:您一直讲得是比较靠近底层的,我们现在做人工智能、深度学习,很多人并不会直接学习底层,如python语言,您今天讲得调优有什么帮助?
A:Intel有许多很好的工具,可以支持高性能的python程序。比如英特尔最近发布了一个Python解释器,速度比以前快了7倍,大家可以去下载下来试试看。
Q:您今天讲的并行计算里面的一些技术方法主要以CPU为主,GPU可不可以运用?
A:可以的。并行计算在方法论上没有任何区别, 但是GPU有自己的特点,需要针对这些特点做相应调整,比如GPU有更大规模的硬件线程,在使用上需要更好地划分并行任务和并行数据集,以充分并行化利用硬件资源,在写GPU程序的时候,非常常见的并行编程模式是流水线模式,也可以考虑使用函数式编程的思想,函数编程很容易实现并行。另外GPU相对CPU来讲内存的容量较小,PCIe数据传输的速度很慢,要考虑好数据的传输问题。