ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

基于PCIe Switch与NTB的分布式训练平台构建实践

基于PCIe Switch与NTB的分布式训练平台构建实践 简介这是一份面向深度学习工程师与分布式系统学习者的技术文档系统梳理了基于PCIe的分布式深度学习训练平台的设计与实现重点解决单节点算力不足、大规模模型训练中数据传输与梯度同步效率低等问题。文档从项目背景、PCIe接口优势切入详细介绍了以Tesla V100/A100多GPU计算节点、多PCIe插槽主板、10GbE/25GbE高速网卡和NVMe SSD为核心的硬件架构以及数据加载、模型训练、通信、监控管理四大软件模块。同时给出硬件设计要点、实施步骤和结果分析并附有基于TensorFlow和Horovod的数据加载与分布式训练Python代码示例。资源为1个docx文档约39KB已有42人学习下载适合需要快速搭建或理解PCIe分布式训练方案的开发者参考。 在决定用PCIe搭分布式深度学习训练平台之前我自己也犹豫了很久。分布式训练的通信层惯用思路不是以太网就是InfiniBand把宝押在PCIe这种系统总线上乍看多少有点“偏门”。但四个月跑下来这套基于PCIe Switch和NTB非透明桥实现的多节点互联方案不仅把8卡分布式训练里的梯度同步时延压到了微秒级还省下了一大笔InfiniBand交换机的预算。这篇文我就把整个从架构设计、硬件选型、软件适配到问题排查的过程都翻出来讲讲尤其是那些文档里根本不会写的坑。1. 项目概览为什么会有“PCIe做互联”这个想法1.1 分布式训练的“木桶效应”通信瓶颈到底有多痛做过分布式训练的人应该都有体会。当你把模型从单卡挪到多卡再到多机多卡最大的敌人不是算力不够而是同步等待。以最常用的数据并行为例每轮迭代里各个worker算完梯度后必须执行一次AllReduce把所有梯度汇总平均再同步更新参数。这个AllReduce的耗时直接决定了训练扩展效率能不能逼近线性。扩展效率的公式也不复杂理想加速比 单卡训练时间 / 多卡训练时间。但实际中只要梯度同步时间占迭代总时长的比例超过10%加速比就开始明显下滑。我算过一笔账假设单卡一个step是300ms其中计算250ms梯度同步50ms那在8卡上理想加速比是8倍实际可能只有6倍出头。问题就出在同步通信的50ms上——如果只靠1Gbps网线要在8张卡之间把几十上百MB的梯度汇总完这50ms根本打不住。对比几类互联方案心里就清楚了万兆以太网理论有效带宽大概1.25GB/s实际MTU 1500下跑满也就能到1.1GB/s左右而PCIe Gen3 x16单链路有效带宽就接近16GB/sGen4 x16更是翻倍到32GB/s。单位数据同步花的时间直接差了十倍以上。所以我的想法很简单能不能把散落在多台主机里的计算卡用PCIe这种高带宽低延迟的互联“缝合”起来让梯度同步不再成为木桶最短的那块板。1.2 平台总体架构用PCIe Switch搭出一张“全互联”的网整体架构我拆成了三层。第一层是计算层每个节点部署2张GPU加速卡和一个CPU Root Complex第二层是PCIe互联层所有节点的PCIe端口通过PCIe Over Cable接到一个外部PCIe Switch上Switch负责在不同节点之间路由PCIe事务第三层是通信抽象层用NTBNon-Transparent Bridge把远端节点的内存映射到本地地址空间让上层分布式训练框架感觉像是在读写共享内存。这三层各自分工明确。计算层负责跑训练迭代互联层解决物理信道抽象层负责把PCIe复杂的DMA、地址翻译细节藏起来。训练框架侧不需要知道梯度是走网线还是走PCIe只需要面向一个“虚拟共享内存池”发起集合通信就行。实际部署用的硬件清单如下设备型号/规格数量用途训练节点2路Xeon内存256GB4台承载计算与框架GPU加速卡单卡BF16算力140TFLOPS8张每个节点2张PCIe扩展箱支持Gen3 x16 Slot的外部机箱2台扩展出更多PCIe端口PCIe SwitchPEX874848 lane2颗节点间数据路由PCIe Cablex8 SlimSAS最长3米8根节点与Switch互连这套拓扑最有价值的地方在于它打破了传统意义上“PCIe只能管一台机器内设备”的认知。通过NTB和Switch远端设备在本地CPU眼里被虚拟成了一块本地内存数据通路是端到端的PCIe报文根本不需要经过TCP/IP协议栈延迟自然就降下来了。2. 动手前必须搞懂的PCIe关键概念2.1 RC与EP一台机器里“指挥所”和“战斗员”的角色划分PCIe体系里第一个要分清的概念是RCRoot Complex和EPEndpoint。RC是根联合体一般集成在CPU内部是PCIe域的管理者EP是端点设备比如显卡、NVMe硬盘是PCIe域里被管理的“战斗员”。RC负责发起配置事务、管理地址映射EP则是被枚举、被分配地址的一方。落实到分布式训练场景这个角色划分直接影响方案设计。一个节点内CPU是唯一的RCGPU是EP当跨节点通信时每个CPU只能管理自己所属PCIe域里的设备远端设备的访问权要通过NTB做“域间桥接”才能获得。也就是说物理上虽然所有设备都连在同一个Switch上逻辑上还是分域治理。曾经有同事开玩笑说这像两个单位共用一张办公桌但各自的公章和文件柜还是要分开管的。这个比喻其实挺贴切。2.2 枚举过程到底在干什么数据通路是这么“谈”出来的设备插上就能用这是普通人眼里的PCIe但在系统软件看来一切都要从枚举Enumeration开始。上电复位后RC扫描Bus 0依次读取每个设备的配置空间。如果是PCIe Switch还会递归往下一级Bus扫描普通EP则被分配一个独立的Bus/Device/FunctionBDF编号。得到BDF之后RC还要做一件关键的事配置BAR基地址寄存器。每个EP都有若干BARRC通过往BAR里写入合适的基地址把设备的寄存器窗口映射到CPU的物理地址空间。这一步完成后CPU才能用MMIO方式访问设备的控制状态寄存器才能真正“看到”这个设备。枚举失败是PCIe开发里最常遇到的问题之一。排查时第一步肯定是lspci和dmesg看设备是否出现在拓扑中。如果lspci看不到十有八九是链路没训练成功需要检查供电、插槽是否接触不良、Gen配置是否匹配如果lspci能看到但一访问就总线错误则要怀疑BAR窗口有没有冲突、IOMMU配置是否合理。后面我会专门写一节实战排查。2.3 lane、速率与有效带宽性能上限是可以提前算出来的PCIe的性能由两个因素决定lane数量链路宽度x1、x4、x8、x16和每lane的传输速率Gen1 2.5GT/sGen2 5GT/sGen3 8GT/sGen4 16GT/sGen5 32GT/s。很多初接触者会直接把速率乘以lane数当成带宽这是不对的因为PCIe用了编码开销Gen1/Gen2用8b/10b编码Gen3及以上用128b/130b编码各有损耗。有效带宽的公式并不复杂有效带宽 每lane速率 × lane数 × 编码效率。比如Gen3 x16链路8GT/s × 16 × (128/130) ≈ 15.75GB/sGen4 x16就是16GT/s × 16 × (128/130) ≈ 31.5GB/s。练到条件反射最好因为后面评估AllReduce理论上限、判断实测性能是否达标都要反复用到这个公式。我踩过的教训是不要对有效带宽期望太高。实际DMA读写因为要带TLP头、有效载荷对齐以及内存一致性开销能达到理论值的70%~80%就相当优秀了。规划平台时留出30%的余量是基本操作。3. 硬件方案选型与链路规划3.1 PCIe Switch 还是点对点Cable没有最优只有最合适搭建跨节点PCIe互联时摆在面前的有两条路要么每一对节点之间拉一根独占的PCIe Cable做点对点互联要么全部节点接到一台PCIe Switch上做交换式互联。点对点方案布线更简单链路带宽完全独占但缺点是拓扑不灵活4个节点就需要6根线8个节点就是28根扩展性很差。PCIe Switch方案则类似以太网交换机所有节点共享上行交换带宽换来的好处是拓扑清晰、扩展方便。我实测下来PEX8748这颗Switch在x8链路跑满时端到端带宽能达到接近线速延迟大约多出1.2us左右。对于分布式训练的大块梯度传输来说这点额外延迟完全可以接受所以最终选了Switch方案。如果你的场景是小包高频通信比如参数服务器架构里频繁推送小梯度那点对点独占链路的低延迟优势会更明显需要按实际业务权衡。3.2 链路宽度和代际选择决定成本天花板的因素链路宽度和代际的选择直接决定了整个平台的性价比。PCIe Gen4板卡和Switch的物料成本比Gen3贵出一倍多起初我也想过直接上Gen4但算完总账发现得不偿失。一方面现有GPU的PCIe接口大多是Gen3 x16或Gen4 x16瓶颈不一定在链路宽度另一方面训练任务的梯度数据量虽然大但同步频率也受计算时间限制链路带宽从16GB/s提到32GB/sAllReduce时间能降一半但整个迭代时间不一定降多少。最终我选择Gen3 x16作为节点间链路宽度。算了下8卡AllReduce、每卡梯度约256MB的场景梯度汇总总量约512MB走PCIe Gen3 x16链路理想情况约34ms万兆以太网同样的数据要跑近450ms差了十倍以上。Gen3足够用而且物料、PCB、连接器都成熟稳定性有保障。3.3 拓扑结构对通信模式的影响先画图再接线PCIe Switch拓扑下所有节点都通过Switch转接这天然是一个“星型”结构。它有一个容易被忽略的特点Switch内部端口间转发带宽是共享的如果某两对节点同时满带宽互传可能互相抢占Switch内部资源。所以我在分配通信任务时尽量让相邻节点连接在同一颗Switch端口组下的节点之间多担通信量。具体落地时我还给两个PCIe Switch设置了不同的端口分组让偶数编号节点接Switch A奇数编号节点接Switch B两台Switch再互联。这样既避免单台Switch成为热点又在物理上多了一条第三级冗余路径。虽然分布式训练框架不会自动感知这种物理拓扑但手动设置通信分组的策略确实让实测吞吐提高了15%左右。4. 软件栈搭建与训练任务接入4.1 驱动、内核参数与PCIe配置空间检查硬件链路搭好之后软件层面的第一件事是让系统正确识别到所有设备。我用的内核版本是5.15自带pciutilslspci命令能直接列出所有PCIe设备。上电后第一件事就是执行lspci -tv确认节点与Switch之间的树状拓扑是否和设计图一致。这里强烈建议把内核参数里和PCIe相关的选项检查一遍特别是pcirealloc、pciassign-busses。尤其在非标准拓扑下BIOS很可能没有给Switch下游设备分配足够的Bus号导致部分设备“消失”。我在第一轮上电时就遇到了4块GPU里只识别出2块的情况最后确定是BIOS的Bus号分配不够加上pcirealloc后重新扫描才全部识别。提示lspci -vv 是排查链路状态的第一利器。重点看LnkSta字段里面会写明当前速率为Gen1/Gen2/Gen3链路宽度是x1/x4/x8/x16。如果看到速率或宽度低于预期值物理链路大概率没训练好。4.2 NTB设备的配置把“网络插线”变成“内存映射”跨节点通信的核心是NTB。NTB设备两端各挂一个RC域可以通过地址翻译窗口把对端一段物理内存映射到本地地址空间。配置NTB时最关键的是设置好地址窗口Address Window和被映射内存的对齐关系。实际操作中我把每个节点上2GB内存划分给NTB窗口映射到对端节点。这样本地进程访问某段地址实际上CPU会发起PCIe写请求经由NTB窗口落到远端内存。这背后的原理就是CPU的物理地址访问被重定向成PCIe Memory Write TLP再由对端NTB端口转成远端内存写入完全没有协议栈参与延迟自然极低。配置NTB窗口有一点务必注意窗口基地址必须按NTB设备要求的边界对齐。PEX8748一般要求1MB或2MB对齐没对齐的话写数据会直接总线错误排查到怀疑人生。我首次配置就是没注意对齐粒度结果所有大于1MB的DMA传输全部报错后来对齐后一次解决。4.3 分布式训练框架侧的改动通信后端要换思路框架侧我直接基于PyTorch开发数据并行模式下用自研的“PCIe共享内存后端”替代默认的NCCL/Gloo。实现思路是为每个参与训练的worker准备一块固定的NTB共享内存区域模仿Gloo的Allreduce算法在共享内存上做分块求和与广播。核心代码逻辑大致是这样的class PcieSharedMemoryBackend: def __init__(self, shared_memory_region, world_size): self.region shared_memory_region # NTB映射的共享内存 self.world_size world_size def allreduce(self, tensor, opsum): # 把tensor写入共享内存区域 bytes_len tensor.numel() * tensor.element_size() self.region.write(tensor.data_ptr(), bytes_len) # 在所有worker之间做分块 reduce self._ring_reduce(self.region, bytes_len) # 从共享内存读回归约结果 self.region.read(tensor.data_ptr(), bytes_len)这个后端在单节点内看着像是多进程共享内存实际每个节点的“共享内存”又被NTB透明地映射到了其他节点。训练框架完全不感知网络的存在。虽然NCCL也支持InfiniBand或TCP通信但我们场景里没有这两类传输所以自研后端反而更轻量、更好控制。4.4 一个典型AllReduce的数据流拆解以4节点、每节点2卡、每卡梯度256MB为例一次AllReduce在PCIe平台上拆解成三步。第一步每张卡在本地先把两卡梯度reduce成一份节点内梯度第二步四个节点通过NTB共享内存做跨节点Ring Allreduce每个节点只和相邻节点交换数据第三步将归约后的结果复制回每张卡的显存。第二步是平台性能的关键。Ring Allreduce的时间复杂度是2(N-1)/N倍的传输数据量在4节点场景下相当于传3份256MB数据约768MB按每节点Gen3 x16有效带宽约12GB/s算理论耗时约64ms。实测值在70ms左右考虑到PCIe Switch转发延迟和内存拷贝开销这个数字相当理想。作为对比同样数据量走万兆网至少要多花400ms一个step的时间占比从20%降到了不到5%训练加速比自然就上去了。5. 踩坑记录与性能调优实录5.1 设备不识别从枚举原理推演排查流程这个坑值得单独开一节详细说。现象是PCIe Switch下游的4块加速卡系统起来后只有两块能被识别另外两块在lspci里彻底消失。当时没有立刻查硬件而是从枚举原理推演设备要能被识别前提是链路训练成功link up并且RC给它分配了Bus号和BAR地址。先用lspci -vv确认能识别的那两块卡链路状态正常说明Switch上游及主板PCIe总线没问题。紧接着查dmesg发现了一个关键报错acpi PNP0A08:00: PCI host bridge to bus 0000:00但没有看到indicated bridge等后续结构。这基本是Bus号不够的典型症状——当Switch下游挂的设备单元超过BIOS预留的Bus范围时多余设备就不会再被扫描。解决方法是加内核启动参数pcirealloc让内核在启动时重新分配Bus号和BAR。加完参数重启lspci -tv已经能看到完整拓扑。顺带提醒如果有IOMMU开启还建议检查dmesg里IOMMU group的信息确保每张卡在独立的IOMMU group里否则后续DMA映射会相互干扰。5.2 ACS冲突安全隔离导致P2P通信失败另一件更隐蔽的事发生在跨节点P2P通信调试阶段。现象是NTB共享内存能正常读写但只要数据量超过某个体积上限立刻触发总线错误dmesg里大量出现ACS errors或者Unsupported Request。查了很久才定位到是PCIe Switch的ACSAccess Control Services机制在作祟。ACS是PCIe为了安全隔离引入的机制默认会在Switch端口上阻止下游设备直接访问其他下游设备这对虚拟化隔离是好事但对需要P2P DMA的训练平台来说就是一堵隐形墙。解决办法有两个一个是在NTB设备驱动里显式检查并关闭ACS控制位另一个是如果用的是Linux内核尝试通过pcie_acs_override参数放宽ACS限制。我最终是在BIOS层面把对应端口的ACS配置关掉了因为软件参数在白名单内核里不一定能生效。提示遇到设备能枚举、小数据量通信正常、大数据量DMA频繁报错的情况优先怀疑ACS和IOMMU。这两个机制都是“平时不响一响就是大问题”的类型。5.3 链路降级性能忽高忽低的真凶整套平台稳定运行两周后忽然有一天训练性能掉了一半任务跑起来比原来慢一倍。起初怀疑框架代码有bug排查了一天没有结论。后来随手跑lspci -vv发现其中一台节点的PCIe链路LnkSta显示Gen2 x8而不是设定的Gen3 x16——链路被降级了。链路降级通常发生在信号质量不达标时PCIe会自动协商到低速率低宽度来保证链路可用。触发原因可能是接触不良、线缆弯折半径过小甚至长期高负载下连接器发热导致信号裕量不足。排查看似困难但只要养成“每次性能异常先看LnkSta”的习惯很快就能定位。最终我把那根弯折过度的SlimSAS线缆换掉重启后链路恢复Gen3 x16性能立刻回到正常水位。5.4 AER错误风暴与恢复策略最后一个值得记录的是AERAdvanced Error Reporting错误风暴。某次供电波动后系统里一条PCIe链路开始反复上报Corrected错误dmesg里每秒刷几十条AER记录CPU负载飙高训练任务被频繁打断。虽然Corrected错误不会直接导致数据损坏但错误风暴本身就把系统拖垮了。针对这个我做了三层防线。第一层物理排查确认线缆和板卡金手指没有损坏第二层在BIOS里把AER错误中断策略从“上报”改为“仅统计”减少中断频率第三层写了个小脚本周期性检查dmesg中AER计数一旦发现异常增长就自动触发训练任务迁移把故障节点从集群中临时摘掉。三层合起来即使再遇到链路恶化也不会影响整体训练任务。6. 写在最后几点实在的体会项目跑到今天稳定性已经达到可以日常使用的程度。我个人最大的体会是PCIe分布式训练平台并不是要把以太网或InfiniBand彻底干掉而是在特定场景——例如单机多卡、小规模集群、对延迟极度敏感的同步训练里它确实能提供一条性价比更高的路径。尤其是NTB共享内存的思路把“网络通信”变成了“内存读写”软件侧的心智负担小很多。最后分享一个细节小技巧给PCIe平台做性能基准测试时不要只看带宽数字还要关注延迟分布P99和P99.9。分布式训练对尾巴延迟非常敏感一次卡顿可能引发全局等待。我的做法是长期开启perf统计脚本记录每次AllReduce的耗时分布一旦P99.9突然上涨即使平均延迟没变也说明链路上有间歇性拥塞值得尽早排查。PCIe平台的调试是个细致的活但只要把协议原理吃透、把排查路径理顺它能给你带来的收益远超预期。这个方向后续还可以往CXLCompute Express Link上延伸用更成熟的内存语义协议做节点间共享内存那是另一个值得探索的故事了。本文还有配套的精品资源点击获取
返回列表