GPUDirect over UB:打通分布式训练显存直达的最后一公里

发布时间:2026/10/19 5:37:40
GPUDirect over UB:打通分布式训练显存直达的最后一公里
GPU 再快也怕堵车。这是我最近在调分布式训练性能时最大的感受。模型并行度拉满算子算力堆得再高只要数据进出的路径上多了一次内存拷贝整个集群的效率就像高峰期被堵在立交桥上的车流——引擎呼啸寸步难行。很多人把注意力全放在 GPU 算力上却忽略了数据通路结果 InfiniBand 网卡买了NVMe 存储也配了跑起来却发现性能根本不对。后来我把数据通路彻底梳理了一遍改用“显存直达GPUDirectover UB”这套机制才算是把整条路真正打通。这篇文章我会从显存访问的底层路径讲起拆解 GPUDirect 的核心思路再重点说清楚“over UB”这个设计为什么是突破口最后给出我实际调优中踩过的坑和验证数据。如果你是正在搞分布式训练、推理服务或者负责大模型集群基础软件的同学这篇文章应该能帮你少走不少弯路。内容不依赖任何特定品牌的硬件只看技术原理和工程实践。1. 先从堵车现场说起显存访问的真实路径1.1 传统路径的四步接力每一步都有隐性代价先抛开抽象术语把一次典型的数据进入 GPU 的旅程画出来。假设我们有一台训练节点网卡收到了来自另一台机器的梯度数据接下来要把这批数据放进 GPU 显存供计算核心读取。传统做法通常是四步网卡通过 DMA 把网络报文写到主机内存中一块预先注册好的缓冲区。CPU 收到中断或者轮询到事件知道数据已经到达主机内存。CPU 驱动 GPU发起一个从主机内存到显存的拷贝这个拷贝一般也是通过 DMA 引擎完成的。GPU 的 DMA 引擎从主机内存把数据搬入显存完成后通知 CPU。看起来每一步都很合理但每一步都有隐性代价。第一次 DMA 要占用主机内存带宽第二次拷贝要再从主机内存读一次、往显存写一次等于同一份数据在主机内存里写了又读白白增加了内存控制器负载。而 CPU 在整个过程中需要处理中断、维护 DMA 描述符、发起拷贝请求、等待完成通知每一步都有微秒级别的时延累积。我曾经在我们集群上实测过一条 8KB 消息从网卡到显存的完整时延传统路径大约在 20 到 30 微秒之间。这在单机场景下你可能感觉不到但在分布式训练里每次通信操作都是这个量级而一个典型的 allreduce 又要经历多次这样的传输累积下来就是一个非常可观的常数。1.2 带宽账本多一次拷贝为什么这么伤人带宽账本比时延更直观。假设 PCIe 链路是 x16 Gen3理论单向带宽大约 16GB/s双向大约 32GB/s。走传统路径时一份数据从网卡到显存需要经历网卡 DMA 写入主机内存占一次 PCIe 写带宽。显存 DMA 读取主机内存这又是一次 PCIe 读流量同时占一次 PCIe 带宽和主机内存带宽。也就是说同一份数据在 PCIe 链路上被传输了两遍。表面上看网卡到主机内存只用了 16GB/s 的单向带宽实际上主机到显存的拷贝又把 PCIe 读方向跑满了。如果还有多张网卡同时接收多路数据PCIe 链路瞬间就变成瓶颈。更要命的是主机内存到显存的拷贝带宽通常到不了 PCIe 理论峰值。原因是实际的数据搬移需要经过多个桥接节点DMA 描述符的处理也要消耗 CPU 周期。我在一个 x16 Gen4 的平台上实测传统路径的有效吞吐大约只有 12 到 15GB/s而理论值接近 32GB/s等于你买了一条高速路实际只能上去一半的车。所以问题的根源很清楚GPU 算力不是瓶颈数据进出通路才是。这也解释了为什么网卡已经支持 RDMA存储已经支持 NVMe但端到端性能仍然上不去——因为中间那一次主机内存的中转让高级外设的能力全部打了折扣。2. GPUDirect 的核心思想让数据别拐弯2.1 GPUDirect 家族的三条专用通道GPUDirect 不是一个单一功能而是一整套“让外设绕过 CPU 和主机内存直接与显存打交道”的技术集合。我把它拆成三个主要形态来理解显存直达 RDMA网络数据不经过主机内存由网卡 DMA 引擎直接把报文写入 GPU 显存。显存直达存储NVMe SSD 或分布式存储中的数据绕过主机内存直接落到显存减少文件读取路径上的拷贝。显存间对传两个 GPU 之间直接通过 PCIe 交换数据不需要先把数据搬到主机内存再送进另一个 GPU。这三种形态解决的问题各不相同但底层机制高度一致都是把显存中的物理地址区间暴露给外设让外设能够通过 DMA 直接访问。区别仅在于发起访问的设备类型不同网卡也罢存储控制器也罢另一个 GPU 的 DMA 引擎也罢它们的本质动作都是“拿一个地址发起一次 DMA”。2.2 物理地址映射显存怎么让外设找到门牌号要让网卡直接访问显存首先要解决一个问题网卡怎么知道该往哪里写显存在系统里通常不是以一个连续大块暴露给 PCIe 设备的。GPU 的显存空间会被分成大量 4KB 或者 64KB 的物理页面这些页面散落在设备地址空间的各个位置。外设的 DMA 引擎只能访问系统给它映射出来的地址窗口这个窗口在 PC 系统里通常体现为设备 BARBase Address Register空间。GPUDirect 的做法是把显存页面转译为 PCIe 地址空间中的一段窗口让网卡能够通过这个窗口直接发起显存读写。这里有一个很微妙的点显存的物理页面和 PCIe 地址之间的映射关系不能是临时建立的必须保持稳定否则网卡正在写的时候映射变了数据都不知道散到哪儿去了。所以驱动层必须把这些页面锁在内存里同时把对应的地址转换表交给网卡侧驱动。听起来不复杂但工程上面临一个麻烦现代硬件生态里不同外设的页表机制和地址窗口大小差别很大。网卡希望的是大块连续地址空间而显存页面恰恰可能是离散的。如果直接把这种离散映射暴露给网卡网卡的 DMA 描述符数量和地址转换开销都会暴涨实际性能依然上不去。3. “over UB”解决了什么看似绕路实为捷径3.1 从“直接落显存”到“经过统一缓冲区”一次中间换乘的收益“显存直达 over UB”这串名字里UB 指 Unified Buffer统一缓冲区。我在第一次接触这个设计时也闪过一丝怀疑GPUDirect 不就是为了去掉中间缓冲吗为什么还要加一个统一缓冲区这不是又绕回去了实际用下来发现这个中间层绕得非常值。UB 不是通用内存而是设备上专门为外部数据访问设计的一块片上缓冲区。外部设备的 DMA 引擎先把数据写入 UB然后由设备内部的数据搬运引擎把 UB 中的数据刷进显存的最终位置或者让计算核心直接从 UB 读取。为什么要绕这么一下直接原因有三个显存地址不必再大规模对外暴露。显存中哪些页面可以被外设访问哪些不能可以用 UB 做成一个收敛的窗口集中控制。安全性和隔离性要强得多也避免了把大块显存映射到 PCIe 地址空间带来的地址碎片化问题。数据对齐和突发长度可控。网卡的 DMA 引擎喜欢对齐的、连续的大块传输而显存的访问模式可能更分散。UB 可以把外设侧的对齐需求在缓冲区里消化掉再以显存更友好的粒度批量写入。片上一致性有保障。如果多路数据同时进入显存不同路径之间可能出现访问冲突或者一致性问题。UB 相当于一个汇聚节点负责串行化和次序管理让并发访问变得可控。3.2 UB 的三板斧地址转换、流量调度、资源池化深入看 UB 在整体路径中做的事情其实可以归纳为三个关键职能。第一个职能是地址转换和窗格管理。UB 会把自身划分成若干个固定大小的窗口每个窗口对应一个 PCIe 地址区段。外设写数据时驱动只需要把“数据应该到哪个窗口”的信息告诉外设外设往窗口地址上做 DMA。窗口背后的显存页面由驱动动态映射不需要外设自己维护庞大的页表。第二个职能是流量调度。多个外设可能同时向 GPU 发数据例如两张网卡同时到达。UB 内部有仲裁逻辑决定哪一路先写入哪一路等待同时负责在数据写入的间隙插入合适的内存屏障保证数据在显存内可见性正确。没有这一层仲裁的话多路并发 DMA 很容易把显存控制器打晕产生严重的性能抖动。第三个职能是资源池化。UB 的容量通常比显存小得多但足以充当一个灵活的“交换区”。比如梯度数据不需要全部立刻进入显存先落在 UB再由设备侧引擎在计算核心准备读取时以最高的优先级搬进显存。这样做减少了显存带宽的浪费也能让计算和通信在时间上重叠。3.3 为什么不是纯用户态就能搞定驱动层和 DMA 引擎的联动值得提醒的是“over UB”并不是说业务代码写几行 API 调用就能直接获得高吞吐。整个机制依赖驱动层的深度联动。具体来说驱动需要完成以下工作在设备初始化阶段把 UB 的地址窗口通过 BAR 暴露出来并向总线系统注册这些资源。在接受业务侧的数据发送请求时分配 UB 窗口建立窗口到显存页面的映射关系并把映射表同步给外设驱动。在数据写入完成后触发一个设备侧的回读或者拷贝操作让数据从 UB 进入显存的预期位置再通知上层业务数据可见。这个过程中有大量的状态机管理、同步原语和错误处理逻辑。只有用户态 API 是不够的用户态只是发了一个“我要把数据从这头搬到那头”的请求真正的搬运动作全在驱动层和 DMA 引擎里完成。这也是为什么这类型技术快速上手难一旦摸清了数据流调优空间就会非常大。4. 实测效果延迟和带宽到底有多少变化4.1 一组有代表性的基准数据下面这组数据是我在一个 x16 Gen4 的平台上用同一套硬件、同一条物理链路、同一个测试负载跑出来的。测试负载是节点间的数据搬运数据包大小从 8KB 到 16MB 不等分别覆盖了分布式训练中最常见的小消息和大块张量两种情况。数据包大小传统路径时延GPUDirect over UB 时延有效带宽提升8KB22 微秒左右8 微秒左右约 2.5 倍64KB30 微秒左右12 微秒左右约 2 倍1MB45 微秒左右22 微秒左右约 1.8 倍16MB90 微秒左右55 微秒左右有效带宽从 13GB/s 提升到 24GB/s需要说明的是这些数字受网卡型号、PCIe 拓扑位置、驱动版本影响很大不同机器上复现出来的绝对数值肯定不完全一致但趋势是一致的小包场景时延优势非常明显大包场景带宽提升接近 PCIe 链路实际可用峰值。这组数据也告诉我们一个重要的结论GPUDirect over UB 的价值是双重的。小消息的收益来自路径变短——去掉了主机内存的中转和 CPU 中断的固定开销大消息的收益来自带宽复用——同一份数据只占一次 PCIe 流量而不是传统路径里的两次。4.2 从跑分到应用三种典型场景的收益差异基准数字归基准数字落到真实应用里收益并不是均匀的。我梳理了三种最常见的场景。第一种是集合通信密集的分布式训练场景。allreduce、allgather 这类操作的核心就是频繁的跨节点数据交换。如果用传统路径每次集合通信都要承担一次额外的主机内存拷贝改用 over UB 之后通信数据直达显存通信算子与计算算子的重叠度明显提高。我在一个模拟项目 X 的 8 机训练任务上做过对比端到端训练吞吐提升了大约 18%。这个提升幅度在优化空间已经不大的场景下算是非常可观的。第二种是推理服务的数据加载场景。推理服务往往需要从远端存储加载模型权重、从 KV Cache 读取状态。这些数据的特点是“量大、实时性要求高”。使用 over UB 之后数据加载路径变短推理服务的首个 token 延迟明显下降。我这边一个对话模型 Demo 的最近一次优化中P99 延迟大约降了 12%。第三种是数据预处理和重放场景。例如离线数据处理往 GPU 灌数据这种场景数据量很大但实时性要求低。此时 over UB 的优势体现在 CPU 占用上——传统路径需要 CPU 全程参与拷贝调度而 over UB 模式下 CPU 只需要处理控制面的少量请求主线上几乎可以不参与数据搬运。我在一台双路服务器上观察相同数据吞吐下 CPU 占用从接近两个满核降到了不到半个核。看到这里你应该已经明白这不是一个“改一行配置”就能搞定的小优化而是整个数据通路架构的变革。但要把它用好还有几个工程层面的坑必须提前知道。5. 工程落地最容易踩的四个坑5.1 环境检查清单先确认硬件和驱动是否真支持很多人一上来就照着文档调 API结果发现性能完全没变化或者直接报错。我建议在任何代码改动之前先做一次环境检查确认以下几点网卡和存储控制器是否支持使用设备内存作为 DMA 目标具体表现为驱动是否有专门接口来分配和注册显存缓冲区。GPU 设备和外设是否在同一个 PCIe 交换域内。如果外设挂在 CPU 侧的 root complex 上而 GPU 挂在另一个 IO 域跨域访问会多一次总分线仲裁时延不降反升。驱动版本是否足够新固件是否开启了解除地址窗口限制的特性。页表实现是否支持大页映射。如果仍然使用 4KB 小页DMA 描述符会爆炸性能会退化成比传统路径还差。这个检查清单看起来琐碎但每一项我都踩过。有些问题不是不支持而是固件默认关闭了对应功能位需要改配置才能打开。5.2 排查链路带宽上不去时的完整推导过程有一次我调一个 16MB 大消息传输的带宽怎么调都在 14GB/s 左右徘徊离期望的 24GB/s 差了整整一大截。当时我走了一遍完整的排查链路这里分享出来供参考。第一步先跑一个外设自身带宽的基线测试。用外设厂商自带的测试工具直接测外设和显存之间的最大吞吐。如果基线测试就不到 20GB/s说明瓶颈在外设本身和 over UB 无关。第二步检查 DMA 描述符和地址窗口设置。把窗口大小从 32MB 调整到 128MB带宽立刻从 14GB/s 升到了 19GB/s。原因在于窗口太小数据搬运引擎只能频繁地做窗口切换和地址重映射每次切换都有数百纳秒的停顿。第三步检查是否启用了大页。之前测试环境的 CPU 没有预留足够的大页内存导致 DMA 映射退回到离散的 4KB 页表每一次页表遍历都增加了额外的内存访问开销。预留大页之后带宽又升到了 21GB/s。第四步确认 PCIe 链路协商状态。用设备管理工具检查链路宽度和协商速度发现竟然只有 x8 Gen3远低于预期的 x16 Gen4。重新插拔并确认板卡连接稳定后最终带宽推到 24GB/s 以上。这一串排障下来最终有效的措施其实是最后一步但如果没有前面几步的系统排查我根本不会想到是物理链路问题。这也是我坚持要“先分清瓶颈层再动手优化”的原因。5.3 什么情况下不该用 GPUDirect over UB任何技术都有边界不建议盲目全量切换。基于我的经验以下三类情况可能得不偿失。第一类是数据量极小且频率极高的控制面消息。这类消息本身时延就要求苛刻但 where 路径很短UB 窗口的分配开销反而占了大头。这种场景用普通主机内存路径反而更稳。第二类是数据访问模式极度分散的小 IO。比如随机读显存中的一个个小片段每次读 4KB 到 8KB。这种访问没有空间局部性DMA 描述符很快占满UB 窗口也来不及复用性能大概率不如传统页表映射。第三类是没有足够多空闲显存做缓冲的环境。over UB 需要预留一部分显存作为 UB 窗口的映射池。如果显存已经接近占满强行启用就会出现频繁回收窗口的现象造成明显的吞吐抖动。判断该不该用最简单的方法就是做 A/B 对比同一份负载传统路径跑一遍over UB 跑一遍。对比时不仅看平均带宽还要看尾延迟和窗口切换次数。我一般会抓取驱动层的计数器和事件日志来辅助判断。6. 让这套机制真正跑起来的落地心得6.1 和软件栈对接时的关键配置从软件栈的角度看要把 over UB 用好不只是改数据搬运底层上层通信库和运行时也得配合。首先是通信库的选型和配置。TCP 协议栈肯定是没戏的必须走 RDMA 或者类似的用户态网络接口。通信库在建立连接时要显式声明缓冲区位于设备侧这样底层驱动才会去分配 UB 窗口而不是传统主机页面。其次是运行时环境的统一配置。我遇到过一次很隐蔽的问题节点上既有普通显存访问任务又有 over UB 任务结果 UB 窗口内存出现了抢占两边的性能都下降。最后通过给不同任务划分不同的 UB 资源分区解决。如果你的运行时不支持资源分区管理那至少得保证任务不会混跑。第三是拓扑感知调度。最好在任务调度层就把网卡、GPU 和 CPU 的拓扑关系纳入考虑尽量把通信任务调度到同一 PCIe 交换域内的组合上。跨域通信不是不能用但时延和带宽都会明显打折扣。6.2 我目前认为值得做的三步走如果你现在正在评估要不要引入这套机制我建议按以下三步推进不要一上来就全量改造。第一步用最小化验证环境跑通端到端数据流。只准备一张 GPU、一张网卡、一台远端机器先跑一个简单的数据搬运测试。确认时延和带宽符合预期再谈下一步。这一步的目的是排除环境因素。第二步在真实训练任务里做一个对比实验。选一个通信占比高的任务比如一个大模型的分布式训练跑两个版本传统路径 as baselineover UB as candidate。对比吞吐、CPU 占用和稳定性。这个对比实验至少持续几个小时不要只看前几分钟的数据因为 UB 窗口管理和地址映射的长期稳定性需要时间检验。第三步根据对比结果决定规模化策略。如果收益明确就在关键通信链路上启用如果收益不明确那说明瓶颈不在数据通路继续优化别的地方。不要为用而用。我个人的体会是这套机制最打动我的不只是一次性能提升而是它改变了我对“外部设备访问显存”这件事的思考方式。过去我们习惯把主机内存当作所有数据流动的必经枢纽所有外设都要到这里中转现在有了 UB 这样的统一缓冲区作为桥接外部数据可以找到一条更短、更可控的进入 GPU 的通道。这种架构上的转变比调一两个参数要有价值得多。如果你也在折腾大规模训练或者推理的数据通路建议花一个下午把你这套环境里“数据从网卡到显存”的路径完整画出来再对照本文的思路查一查你大概率也会找到那条属于自己的直达通道。