国产芯片跟 ARM 生态进步飞速,在此情形下,于 ARM 平台去构建高性能存储基础设施成了技术方面的焦点所在。Linaro 是个国际化技术组织,它专门围绕 Arm 生态以及开源软件开展工作,联合产业链上处于不同环节的众多厂商去解决共同面临的一些问题,还会帮助企业客户在开源的基础之上实现产品化的最后落地。在 MLPerf Storage 测试期间,Linaro 团队针对 JuiceFS 社区版(其元数据运用 Redis)展开了系统的压力测试,测试范围涵盖了多种具有典型特征的机器学习训练负载。
测试结果表明,系统性能于很大程度上被内存带宽以及元数据访问效率所影响,JuiceFS 的吞吐能力对 GPU 利用率以及训练效率起着直接决定作用。经由 UNet3D、ResNet - 50 和 CosmoFlow 等负载的测试,经分析发现:在单机场景当中,GPU 利用率主要受到内存拷贝延迟的限制;于双机或者多机场景之下,元数据访问以及节点间同步成为主要的瓶颈。文章同时给出了针对这些瓶颈的调优思路以及实践结果。
大概来说,规模庞大的AI专门用于锤炼效能的调整优化属于一类系统工程,得从存储体系、内存所具备的带宽、CPU的调度安排、缓存所采用的策略不等的多个方面一块儿进行优化,这样才能够于ARM平台上边达成高效的深度开展学习训练时的数据供应。
01 Arm64 与 x86_64 架构差异与并发特性概述
较之于x86,Arm的应用范畴持续拓展,先是从移动端延伸到了IoT,接着又扩展至可穿戴设备,随后又覆盖了PC,再之后进入汽车领域,最后还涉足服务器,其具备的高能效比乃是被广泛采用的关键缘由。
倘若站在了架构设计的视角加以审视,那么 Arm 是归属于 RISC 的,即 Reduced Instruction Set Computer,也就是精简指令集计算机,而 x86 则是属于 CISC 的,那便是 Complex Instruction Set Computer,也就是复杂指令集计算机。这样的一种设计方面的差异,同样对处理器的执行方式产生了影响。将指令长度固定为4字节的是Arm64 ,对x86而言指令长度则可变,其长度大概是在1–15字节之间 ,所以x86常常需要更为复杂的译码器。与之相比,Arm的指令更为简洁,并且对编译器与代码生成阶段在指令有效组织方面的依赖程度更高,故而需要更长的编译时间。
从工程师能够感知的那种角度而言,会有一部分架构方面的差异,这些差异会直接对程序行为产生作用。好多在x86上面看上去符合直觉的代码,在Arm上面不一定是这样,后面即将讲到的几个容易踩到坑的点,基本上都和这些底层的差异存在关联。
这当中存在着一个典型的问题,那就是原子操作对于地址对齐是有着要求的。不管是LL/SC(也就是Load-Link/Store-Conditional),又或者是LSE(即Large System Extensions),在开展原子加减等读改写操作的时候,一般而言都是要求访问地址要满足对齐条件的。相对较新的LSE2对于这一限制是有所放宽的,开始支持在16-byte window之内的非对齐访问。对x86而言,数据对齐并非是必须要做到的,然而,维持良好的对齐状况,却有助于性能的提升。参考文档为:Arm Architecture Reference Manual for A-profile architecture。
另外一个要着重予以关注的特性之处在于,其所采用的是弱内存序模型,是那种带有弱序之态或者松弛内存模式的,二者所存在的差异体现于针对内存访问序列的规约强弱方面乃是不一样的。在多线程所处场景当中,同样的读写行为操作,于x86架构之上普遍更易于展现为趋近程序书写的序列状态,然而在Arm架构之上却准许更多的重排序情况发生,所以其他线程观察到的读写序列有可能与源码序列并不一致。在Arm架构上所出现的异常状况尤其需要对内存序的影响加以特别考量。关乎更多细节,能够参考的是,Arm组织所发布的白皮书,其具体名称为,Synchronization Overview and Case Study on Arm Architecture。
02 JuiceFS 与 MLPerf 概述
JuiceFS是一款开源的分布式文件系统,它具备很高性能,这个性能建立在对象存储的基础上。同时它能够充分利用对象存储有低成本优势,可以提供使用体验,这种体验是接近传统文件系统的来获取。进而它支持POSIX、HDFS SDK、Python SDK以及S3兼容接口,能够适配不同类型的应用和数据处理框架;并且支持云原生扩展、数据安全与压缩,可广泛应用于AI训练、推理、大数据处理等场景。
可透过 MLPerf Storage 基准测试,去评估 JuiceFS 于 AI 训练之类高负载状况下的数据供给能力,此测试系由 MLCommons 来开发,其着重考量存储系统是否能够持续以高效的状态向计算侧供给数据。
2.0版本对测试进行分类,分成训练负载和检查点负载这两类,其中训练负载涵盖3D U-Net、ResNet-50以及CosmoFlow,这三者在样本大小方面存在显著差异,在访问特征方面也存在显著差异,同时设置了最低GPU利用率要求,3D U-Net与ResNet-50的要求是90%,CosmoFlow的要求是70%。
在测试流程里,数据先是从存储系统被读进主机内存,而后进入计算阶段。训练所耗时间通过模拟方式进行复现,以此来模拟真实训练场景的数据流,进而不需要实际去部署 GPU,降低实验门槛并且提升操作便利性。
着重于03 MLPerf Storage v2.0,测试的原理,以及调优的相关内容。
在介绍具体的模型测试结果以前,要先知晓分布式训练的数据访问原理,这对读者明白GPU利用率、存储吞吐以及性能瓶颈的缘由来讲很有帮助,进而能更好地理解后续的测试结果以及调优策略。
一种被称作分布式机器学习的方式,通常会采用数据并行这种模式,也就是存在多个并行的进程,它们共同去共享同一个数据集,并且每个进程各自分别承担起读取以及处理与之对应的训练批次的责任。
MLPerf Storage的训练测试同样依照此思路,每个训练进程会按照批次从存储系统那里读取数据,并且凭借模拟计算去评估存储系统的持续供数能力。
为了明白测试里性能表现的出处,还得明白JuiceFS客户端的数据处理链路,在使用JuiceFS做测试的时候,跟图里显示的一样,其执行流程大体上能分成三部分。
以性能分析的视角而言,这条链路存在两类问题需重点予以关注。其中第一类问题表现为数据拷贝,如同对应在图里的 2.1、3、4、5、6 等步骤,这些特定位置皆会致使额外的内存复制开销产生,所以在对延迟以及 CPU 开销展开分析的时候,这些位置常常位居重点之处。
第二类是同步跟异步的边界,从图里能够瞧出,1步骤、2步骤、3步骤、4步骤、5步骤、6步骤总体属于同步的路径,该路径就是说请求发起之后,要等待当前阶段结束才可以继续朝下面推进,而7步骤属于异步路径,是由后台的goroutine负责去和后端存储进行交互。
测试 1:Unet 3d
在这个里头的测试当中,样本是有着 146 MiB 大小的图像文件,我们着重去关注那大块数据的读取性能,测试之后的结果表明:
为了使数据读取效率得到提升,针对训练参数开展了优化工作:把reader并发线程数由4调整成16,借此加快数据生成速度,并且把数据读取方式转变为direct I/O,以此减少缓冲区以及内存拷贝开销。
业务指标呈现出这样的情况,单机挂载 6 块 GPU 之际,GPU 利用率不过才 83%,与之对应的带宽大概是 15.1 GB/s,并未达成预期的高利用率目标。进一步借助 FIO 对存储侧展开测试之后发觉,其带宽同样大约是 15.1 GB/s,这表明此时系统瓶颈已然落在了 JuiceFS 客户端带宽之上,而非 GPU 计算侧自身。
优化分析 1:绑定 CPU
咱是以深入剖析客户端带宽受限缘由为目的,才针对进程搞了 CPU 绑定,把它固定到 CPU1 上去运行,此 CPU1 处于 NUMA 2、3 节点。借助工具去观察,48 个 CPU 核心差不多全都被占满了,再进一步剖析 top - down、memory 以及 miss 这些指标,就发现系统呈现出显著的 Memory Bound,主要耗费时间集中于内存拷贝这一方面。这表明,于CPU绑定情形之中,JuiceFS性能瓶颈主要源自CPU处理能力,以及跨NUMA节点内存拷贝所引发的额外延迟。
优化分析 2: 不进行 CPU 绑定
我们进一步观察了不绑定CPU的情况,是以理解系统在更通用条件下的带宽限制。通过观察能看到,CPU并未被完全用满,不过devkit tuner numafast指标显示,系统中的remote内存访问比例高达约80%。这意味着,大量内存访问已经跨越本地NUMA节点,更甚至可能跨越CPU socket,进而引入了显著的带宽损失跟访问时延。
从硬件带宽所具备的特性来进行观察,跨片内存访问这件事情自身便存在着显著的限制,举例来说,在 Arm 这个平台之上,跨 socket 的理论物理带宽大概是 60 GB/s,再进一步借助实际测量之后晓得,跨片 copy 带宽在 Arm1 那儿大约是 48 GB/s,然而在两组 x86 平台之上分别大概是 37 GB/s 和 28 GB/s。
这表明,在未对CPU进行绑定的情形下,尽管计算核表面看来未被彻底耗尽,然而大量跨越节点、跨越socket的远端内存访问却已然成为全新的主要开销源头。所以,能够推测出,此时JuiceFS带宽无法持续提高,极有可能并非仅仅受限于CPU算力,而是受限于跨片内存访问的带宽以及时延。也就是说,系统瓶颈已从“本地CPU忙不过来”转变为“远端内存访问代价过高”。
综合起来看,处在两种场景之中,JuiceFS带宽没办法得到提升的缘由并非是一样的:
测试 2:Resnet50
进行 ResNet - 50 测试时,单个样本规模较小,其大小大概约为 150 KiB,每一个 batch 涵盖 400 个样本,每个 batch 的总体数据量大约是 58.5 MiB。此次 I/O 测试聚焦于 GPU 处于高并发状态下的数据加载效率以及训练吞吐情况。测试表明系统能够在大规模 GPU 环境下保持较高的利用率:
实施测试期间,我们把关键参数reader.read_threads由8加以调整成为 1,此为中等尺寸图像模型,拥有单线程便可达成数据供应所需之处。
优化分析 1: 单机性能瓶颈与内存带宽影响
单机配置达到 55 块 GPU 的时候,GPU 的利用率降低到了 86%,带宽依旧是 9.2 GB/s,这表明系统瓶颈已经转移到了 JuiceFS 客户端带宽那里。
作进一步的分析之后能够发现,ResNet-50进行测试的时候采用的是Buffer I/O模式,在处理数据集时,除了要读取数据之外,内存拷贝这一行为会消耗掉一部分内存带宽。
系统内存拷贝时的带宽,受到内存通道数的影响,也受到内存频率的制约,并且还受CPU频率的作用。借助对多台配置不一样的机器开展stream测试,所获取的单机顺序读带宽,跟系统内存带宽测量得出的可比带宽是一样的,这表明读数据吞吐的能力,在很大的程度上是取决于系统内存带宽的。对于那些需要高吞吐以及高GPU利用率的训练任务,建议首先挑选内存带宽比较高的机型,如此能够显著地提升数据供给的能力以及训练的效率。
单 CPU 内存拷贝带宽数据JuiceFS 单机部署读带宽
Arm3
Arm3: 171 GB/s
25.3 GiB/s
Arm2
114 GB/s
21.6 GiB/s
Arm1
106 GB/s
18.3 GiB/s
x862
90 GB/s
17.9 GiB/s
x861
82 GB/s
16.6 GiB/s
优化分析 2:双机扩展瓶颈与分布式限制
在多节点开展部署工作期间,除去单机自身性能方面存在的限制以外,跨节点的内存访问情况、网络传输状况以及元数据延迟现象,均会演变成全新的瓶颈所在,所以在完成单机分析操作之后,开展双机测试工作,这对于识别那些分布式约束极为有益,并且能够对系统优化给予指导。
于双机情形之下,从理论层面来讲能够支撑100块GPU,然而在实际开展测试期间却仅仅能够达成96块GPU。经由剖析发觉,每一个操作的读取延迟出现了一定程度的增加。虽然文件数据已然被缓存在本地盘之上,但是元数据访问延迟依旧成为了占据主导地位的限制因素。
为解决这一问题,对系统进行了多方面优化:
将CPU核心进行分组,要确保训练线程跟I/O线程在同一个NUMA节点上运行,把纯数据处理弄到位,还要将元数据访问分别分配到不一样的CPU核心哟又分配到不同的存储路径上,对Redis缓存以及本地缓存策略加以调整,以此减小高并发访问元数据之际的延迟!
进行了上述那般的调优之后,双机这种场景,是能够稳定地去支撑 100 块 GPU 运行的,并且 GPU 的利用率,还达到了预期的那个水平。
测试 3:cosmoflow
在前述模型与之相较时,此模型的单一样本数据量显得更小,这种情况也意味着针对I/O以及元数据访问要有更高的要求,处于单机以及双机场景之际,CosmoFlow测试展现出:
优化分析 1:单机瓶颈:内存拷贝限制 GPU 利用率
试着去增加 GPU 的数量,使其超过 10 块的时候,就发觉 GPU 的利用率降低了。在对日志以及性能数据予以分析之后,发现:
依此推断,在系统运用更多内存带宽之际,内存拷贝延迟变作制约读取性能以及 GPU 利用率的关键因素。
优化分析 2:双机瓶颈:分布式同步与元数据延迟
当处于双机的场景状况之下,在对20块GPU展开尝试操作的时候,第一轮所进行的测试之中,GPU的利用率显著地呈现出偏低的状态。进一步地去做分析,结果发现:
为把此问题给解决掉,于代码当中添加同步机制,借此确保所有的节点,在开始开展训练以前,将 Dataset 预处理给完成。经由这一调整之后,双机测试能够持续平稳地支撑 20 块 GPU,GPU 的利用率达到了预期的水平。
04 总结
首要的是,MLPerf Storage借由不同样本、文件以及batch大小结成的组合,去考察文件系统的各项能力,这其中涵盖了大中小块顺序读能力、文件并发性能、总读带宽、元数据访问时延、文件读取时延以及文件操作的稳定性。在只读文件的场景情形里,借着充分利用高速近端缓存这种方式(这高速近端缓存包含原数据和元数据缓存)能够显著地提升读取性能。需要予以留意瞩目的一点是,文件越是小,对于IOPS以及延迟的要求就越是高。
其次,我们察觉到系统的内存以及带宽对于性能所产生的影响颇为显著,在Memory copy密集型的应用里头,内存拷贝不但消耗内存带宽,与此同时还占用CPU,表面上会呈现出“CPU忙”的虚假现象,实际上CPU的大部分时间是处于等待数据的状态,测试结果表明,系统内存带宽对JuiceFS的吞吐能力具备决定性作用,这也就为选择服务器给出了参考标准:内存带宽越高的系统,其存储吞吐性能也就越好。
其三,Go运行时针对NUMA的感知存在局限,在大规模CPU核心运行情形下,性能或许比不上小规模核心运行。针对多NUMA系统,应当尽可能规避跨NUMA,特别是跨CPU socket的访问,这是由于跨socket内存带宽通常比较低(大概几十GB/s),会致使延迟有所增加并对整体性能造成影响。所以说,实际进行部署的时候只需要分配充足的CPU核心就可以了,不需要过度运用全部核心,以此来防止额外的内存访问延迟。
第四,于系统层面而言,存在一些潜在的优化之处。比如说,在 Memory copy 较为密集的操作方面,部分 Arm 新系统给出了针对内存访问的指令优化举措,我们积极与 Arm 社区展开合作,把配置提升推送至社区里,此新系统能够明显提高内存拷贝效率,在部分场景当中,带宽提升幅度可达数十个百分点。
此外,针对那些涉及好多内核以及用户态交互的操作,像文件读写以及元数据处理这类,能够借助优化用户态跟内核态的交互,削减没必要的调用次数,进而降低延迟。在实践当中也察觉到,把文件处理尽可能集中于同一个生产节点之内,防止跨 NUMA 或者跨 socket 的访问,能够进一步提高性能以及稳定性。
最终,系统配置的优化同样展现于缓存策略方面。比如说,在单机处于高负载的情景之下,借助对JuiceFS的内存缓存策略予以调整,减除无效内存带宽的占用,能够切实提升GPU利用率以及存储吞吐。从整体上而言,MLPerf Storage Benchmark属于一项系统工程,需要文件系统、内存带宽、CPU调度以及缓存策略等多个方面协同配合,方可达成最优性能。
对于本文之中的某些实践经验,我们抱以期望,其能够为正处于面临类同问题境况下的开发者给予参考条件,要是存在其他方面的疑问,欢迎加入JuiceFS社区,与众人一起去开展交流活动。
