分析测试百科网

搜索

喜欢作者

微信支付微信支付
×

超算那么火 可到底是怎么算的?(一)

2020.10.26
头像

王辉

致力于为分析测试行业奉献终身

  最近一段时间,有关超算的话题成为热门,一时间大家都开始讨论超算,然而,笔者发现在所有这些讨论中,从没有在任何时间任何地点发现任何人问出就连小学生都经常问的问题:超算到底是怎么算的?不得不说是一件可悲的事情。

  【并行计算怎么算】

  我们知道,单个CPU核心只能串行计算,也就是一条一条的把机器指令读出来执行。要理解的一点是,串行执行并不表示指令顺序执行,跳转指令可以让CPU跳到其他地址执行,但是整个过程CPU只在执行一个单一的指令流,也就是一个线程,thread。某个线程完成某种任务,而线程对应的代码中的多个函数又各自分工完成对应的工序。要想同时执行多个线程的话,有两个办法,一个是增设额外的一个或者多个CPU,这样,在时间上可以做到Parallel/并行,同一时刻有多个任务同时执行;另一种办法则是让单个CPU执行一段时间线程1,然后再强行跳转到线程2执行一段时间,然后再跳回到线程1,这样就可以实现多个线程的Concurrency/并发,但是却不是并行,因为同一个时刻还是只有一个线程在执行,只不过每个线程执行的时间非常短,一般比如10ms的时间,就会跳转到其他线程执行,这样从表面看来,一段时间内,多个线程似乎是“同时”执行的。

  前者的方式看上去性能更高,但是其有2个惨痛的代价,第一个是线程之间的同步,第二个是缓存一致性。如果多个线程运行在同一个核心上,那么它们只能一个接一个的执行,执行线程1时线程2不可能得到执行,如果线程1和线程2要操作同一个变量,那么就轮流操作,不会有问题。但是多个线程运行在不同的核心上,事情就发生很大变化了,比如有两个线程,都需要操作某个变量,比如同时运行a=a+1这个逻辑,期望结果是线程1对a加了1,线程2要在线程1输出结果的基础上继续+1,而由于这两个线程运行在两个独立核心上彼此之间没有协调,可能导致线程1读到的a的初始值0,加1之后还没来得及将最新结果更改到a所在的地址之前,线程2也读到了a的初始值0,加1之后也尝试写入同样的地址,最后a的结果是1,而不是期望中的2。解决办法则是对变量a加互斥锁,当某个线程操作a之前,先将锁(也是个变量)置为1,其他线程不断的扫描锁是不是已经被置为0,如果是1则表示其他人正在操作a,如果是0则表示其他人已经释放了,那么其将锁改为1,也就是锁上,自己操作a,此时读到的a就会是被其他线程更新之后的最新数据了。这个过程叫做Consistency。所以,如果多个线程之间完全独立各干各的,没有任何交互,这是最理想的场景,这就像多台无须联网的独立的计算机各干各的一样,只不过共用了CPU和内存。

  然而,如果使用了缓存,又使用了共享变量,事情又变得复杂了。线程1所在的核心1抢到变量a的锁之后会将a的内容缓存到核心1的缓存中,更新了a内容之后,该更新也依然留在缓存中而不是被flush到主存。其释放锁之后,线程2抢到a的锁并将a读入核心2的缓存,此时如果不做任何处理,核心2从主存中读到的将是a的旧内容,从而计算出错。可以看到,即便是使用了锁来保证Consistency,也无法避免缓存所带来的一致性问题,后者则被称为Coherency。

  Consistency由软件来负责,而Coherency则要由硬件来负责保证,具体做法是将每一笔数据更新同步广播出去给所有其他核心/CPU,将它们缓存中的旧内容作废,收到其他所有核心的回应之后,该更新才被认为成功。所以核心/CPU之间需要一个超低时延的网络用于承载这个广播。这个过程对软件完全透明。除了需要广播作废外,当其他核心需要访问该变量时,拥有该变量最新内容的核心必须做出应答将该内容推送到发出访问请求的核心。

  在很早期的SMP/UMA架构下,由于那时的SMP总线本身就是一个广播域,任何核心的访存请求都会被其他所有核心收听到,包括更新了某个地址、读取某个地址,这样很天然的可以实现Coherency,比如,当某个核心更新了某个地址之后,其他核心后台默默的收听(或者说嗅探,Snoop),并在自己缓存中查询自己有没有缓存这个地址的内容,有则作废无则不动作。当某个核心发起对某个地址读的时候,其他核心收听之后也默默的搜索自己的缓存看看是否有该内容最新版本,有则使用特殊的信号抢占总线并压制主存控制器对总线的抢占,将数据返回到总线上,与此同时主存控制器也收听该内容并将该内容同步更新到该地址在主存中的副本。此时,该地址将拥有三个副本,分别位于:之前缓存它的那个核心的缓存、刚刚读它的那个核心的缓存、主存,而且内容一致。如果此时之前缓存它的那个核心再次发起读操作,就没有必要将读请求发送到总线上,而浪费电,同时也浪费其他核心的搜索运算耗费的电能以及对其他正常缓存访问的抢占。所以人们想了个办法,每个缓存条目(Cache Line,缓存行)增加一个字段,专门用来描述”该缓存当前处于什么状态“,上述状态称为Share态,而如果有人更新了某个地址,其他核心嗅探到之后,便将自己缓存里这份内容改为”Invalid“态,而刚刚更新内容的那个核心里缓存的该条目被改为”Modified“态,Invalid态的条目已经作废,再读就得走总线,M态的条目可以直接读,因为此时没有其它人有比你新的内容了。如果加电之后某个核心第一个抢到总线并发起该地址的读,则读入之后该条目就是Exclusive态,因为只有它一个人缓存了该条目,当另外核心再发起读之后,该核心嗅探到这个事件,于是将自己缓存里的该条目发送给刚才发起读的核心,那个核心从而知道其他核心也有该内容,于是两个核心一起将自己本地的该条目改为Share态,这两个核心中任何一个如果再发起该地址的读,就不用走总线了,直接缓存命中。可以看到,当某个核心需要访问的数据在其他核心的缓存中时,硬件会自动传递这份数据,软件根本无需关心。所以多个线程之间引用共享变量的时候,直接引用即可。

  上述方式被称为MESI协议,其目的是为了提升效率,不需要每一笔访问都走外部总线。后来过渡到NUMA架构之后,NUMA是通过一个分布式交换网络来广播同步这些消息以及进行变量内容传送的,由于该网络并非一跳直达的广播网络,所以过滤不必要的广播就更加重要了。不同的CPU厂商有不同的方式,MESI协议也有不少变种,比如MESIF等等。另外,由于失去了天然的总线嗅探机制,如果某个缓存行处于Invalid态,读取该缓存行之前硬件需要发出Probe操作,探寻,主动广播这个Probe请求给所有核心的缓存控制器,缓存了该行的缓存控制器会返回最新数据并将自己该行的状态改为S态。如果要更新某行,该行本地处于E或者M态则直接更新,处于S态则需要发出Probe请求作废其他缓存中的该行。总之,当MESI遇到NUMA,就是个非常复杂的状态机,笔者就不继续展开了,要想与笔者深聊NUMA和Cache Coherency,可接受预约面聊,前提是你得拿出笔者看得上的干货来咱们互换一下。

  【核心数能否再多点】

  可以看到,增加核心数量并不是那么容易的事情,除非不使用缓存,所有核心吧所有的更新都写到主存里。但是这样做性能将会不可接受。有人问,把缓存也集中共享不就没这么多事了么?的确,但是如果把缓存单独放到某个地方,多个CPU芯片通过某种总线集中访问该缓存,那么其总线速率一定不够高,因为其走到了芯片外面,导线长度变高,信号质量就会变差。这个思路就不现实了。

  所以,又得使用分布式缓存,又得要求核心数量越来越多的话,就得保证所有核心之间的网络足够高速才行,而核心数量越来越多,网络的直径就会越来越大,即便是MESI过滤,去效果也是有限的,广播的时延随着网络规模的增大变得越来越高。所以,核心数量达到一定程度之后,缓存一致性问题就变成了整个系统扩展性的瓶颈点所在。怎么办?没办。反正,硬件是不会再给你保证一致性了,因为如果成百上千个核心的网络,用硬件保证一致性得不偿失。

  此时,必须抛弃由硬件保证的缓存一致性,改为软件自行解决。NoC(Network on Chip)方案就是在一个芯片中将几十个上百个核心通过高速网络连接起来但是却不提供硬件缓存一致性,其网络直径很大。这种架构天然适合各干各的,如果不是各干各的,必须传递更新后的变量的话,需要由软件自行向位于每个核心前端的NoC网络控制器发送消息+目标节点地址并传递到对方,对方通过底层驱动+协议栈接收该变量并传递给其本地程序,这已经是赤裸裸的程序控制网络通信了,其实NUMA已经是这样了,只不过其网络通信程序跑在硬件微码或者硬状态机中,且该状态机可直接接收访存请求并将其通过网络发送从而对软件透明。

  另外,NoC架构的CPU很少会被设计为共享内存架构,因为此时主存也是通过主存控制器接入NoC,由于NoC时延过大,每一笔访存请求又是同步的,将代码直接放到主存,性能将会非常差,所以NoC上的RAM主要用于所有核心之间的最后一层共享缓存了,只不过是可寻址的缓存,由软件而不是硬件来管理。NoC架构下每个核心内部一般会有几百KB的SRAM可寻址空间,代码则运行在这里。外部主存可以使用虚拟驱动映射成某个带队列的设备,异步读写。也有支持直接映射到核心地址空间的但是访问性能会很差,所以鲜有。 

  【再多点!】 

  我说,能否让核心数量再多点?笔者大学是应用化学专业,但是笔者学到最后已经完全对化学失去了兴趣,尤其是看到薛定谔老哥们之后。记得某室友的毕业设计就是用程序来算分子结构,一台破pc,奔四的,导师/师兄给了个程序说:按照步骤输入参数,点确定,贴个条“别关机,计算中!“,行了。一个月以后,来看结果。的确,不够用啊,如果一分钟能算完,这哥们就可以更安逸的玩暗黑2和看玄幻了。钱啊,学费啊,就特么干这个。当然,笔者做的更让人寒心,当时笔者是搅和化合物,分析天平称点粉末,弄点液体放烧瓶里加热搅拌,一晚上第二天结晶,拿去做x光衍射分析,写论文,当时都不知道自己在干什么,为什么干。这比那哥们更耗钱,人家只耗电,笔者这一勺子下去,就是几千块!撒地上一点就是几百块!!哎,内疚啊!

  跑题了。核心数量再多,一个芯片真就搞不定了,NoC也白搭。必须用多个芯片,也不行,几万个CPU芯片怎么给它弄到一起?那就不能NoC了,得眼睛看得见的网络了,包括网卡、网线、交换机。这么大范围的网络,其速率相比NoC又降低一个档次,比如万兆以太、Infiniband等这种级别了。也就是说,这种规模之下,必须使用多台独立的机器来搭建,也就是所谓的超算集群。

  

  哦?一堆单独的机器用网络连起来就是超级计算机?难道不应该是科幻片里那些超脑类脑神秘机器么?哈哈,没那么科幻了,兄弟。可以这么说,整个Internet上的计算机也天然组成了一个超算集群,只要Internet用户同意,就可以用来计算。比如SETI@Home项目就是利用所有Internet上的计算机各自下载一部分数据然后用同样的方式去分析然后返回结果,其将程序作为一个屏保程序,在离开或者空闲的时候,便后台启动计算,屏保结束则自动停止。当然,这个计算过程中基本不会有网络通信,多个部分之间各算个的。

  使用了外部网络的话,多个不同机器上的线程之间就不能够共享内存来通信或者共同引用或处理某个变量或数据结构。如果需要通信或者共享变量,就得彻底使用这些外部网络来传递了。比如,利用TCP/IP,RDMA over IB等等。这么复杂了?老子之前写程序都是直接声明变量和数据结构,直接引用的,充其量加个锁,你现在让老子每次引用的时候要调用TCP/IP发个包给对方,再接收包,才能拿到?老子不干。。

  不干不行啊,必须干,你不干就别用几万个核,回去舒舒服服用你的8路服务器,或者小型机去,那个舒服,但是就是一算一个月,爽不?酸爽。那好吧,老子接受,但是底层这么多网络,老子不懂TCP/IP怎么调用,更不懂什么RDMA,几千上万台机器,老子光记录IP地址就得记多少个。。你说咋办吧。

  【MPI是干啥的】

  鉴于上面这位老兄的顾虑,人们开发了一种叫做Message Passing Interface,简称MPI的函数库。来来来,这位客官,您要把什么东西发送给谁,敬请吩咐~~。嗯~~~?好,我这有个数组a[100],现在想把a[0]到a[9]分别发给运行在1~9号节点上的各自进程。片刻后,“上~~菜!”。我去,这么快就完成了?

  上述场景成为MPI_Scatter场景,某个进程将一组数据一个一个的散播给多个进程。对应的函数为MPI_Scatter (&sendbuf,sendcnt,sendtype,&recvbuf,recvcnt,recvtype,root,comm),每个进程的代码中该处加上一句,就可以了,该收的收,该发的发,该函数为同步阻塞调用,执行到这一步时发送方发出数据,接收方等待接收,搞完之后大家再继续往下走。

  如果是共享内存架构,进程在代码里可以直接引用a[i],根本不需要接收/发送。比如int b=a[9]+1。该函数底层也是调用本机的网络协议栈、设备驱动从而将消息封包传递给其他机器的。

  同理,还有如下这些常用函数:MPI_Send, MPI_ISend, MPI_Recv, MPI_IRecv等用于点对点数据传递的;MPI_Bcase, MPI_Gather, MPI_Reduce, MPI_Barrier等用于集合通信的等,后者统称为Collective类MPI通信。

  利用MPI框架编写好对应的程序之后,需要使用mpirun脚本将程序load到集群里的每个节点上,具体如何load需要预先写好一份配置文件,大家有兴趣可以自行研究,笔者这里就不再多说了。

  【浪潮Caffe MPI版本】

  浪潮则是国内HPC领域的领军厂商,有一系列的软硬件解决方案,比如高密度刀片、GPU一体机、深度学习一体机、SmartRack整机柜服务器、InCloudRack整机柜服务器等等。

  超算那么火 可到底是怎么算的?

  在HPC生态环境和软件方面,浪潮专门为Caffe深度学习平台开发了对应的MPI版本(开源);提供高性能计算服务平台ClusterEngine,可实现集群监控,故障报警,系统管理,作业提交管理及调度,支持提供优先级,公平共享,资源限制,资源回填等多种调度策略,支持HPC应用集成,记账管理,统计分析等功能;

  超算那么火 可到底是怎么算的?

  超算那么火 可到底是怎么算的?

  以及集群诊断调优工具Teye。天眼是一个高性能应用特征监控分析系统,面向大规模HPC集群系统,用于提取应用程序对集群资源的使用情况,并实时反映其运行特征;可在现有硬件平台基础上,深度挖掘应用程序的计算潜力;为诊断应用程序瓶颈,改进应用算法,提高并行效率提供科学有力的指引,最终达到优化系统资源利用率,提高系统计算性能的目标。并提供热点走势图、数据分布律、特征雷达图等可视化监控。

  超算那么火 可到底是怎么算的?

  深度学习是近年来的热点技术,笔者在这方面的知识为0。虽然不知道它是怎么做的,但是看上去真的很厉害。Caffe是伯克利发布的一款深度学习计算框架,广泛用于图像识别等深度学习领域。

  超算那么火 可到底是怎么算的?

  浪潮发布的MPI集群版Caffe计算框架正是切中当下深度学习的迫切需求,其基于伯克利的caffe框架进行了MPI改造,让原本只支持单机+GPU环境的Caffe框架完美支持了多机MPI集群环境,并且完全保留了caffe原有的特性。

  浪潮的MPI版的Caffe计算框架已经在某超级计算机上进行部署并测试,结果显示,在保证正确率相同的情况下,浪潮MPI Caffe在16个GPU上并行计算效率上提升13倍。

  超算那么火 可到底是怎么算的?


互联网
仪器推荐
文章推荐