资讯动态

GPU发起通信深度解剖:RDMA、NVSHMEM与NCCL机制与优化实践

发布时间:2026/10/25 12:25:26 来源:尧图企业网站定制
1. 从一篇论文说起GPU 发起通信到底在解决什么问题第一次看到“GPU-Initiated Communication”这个方向是在折腾多卡推理和分布式训练的时候。当时遇到一个很别扭的现象明明计算 kernel 已经把 SM 占满了但整体吞吐就是上不去用 nsight 一看大量时间花在了等通信上。CPU 在那边忙着下发 NCCL 的 collectiveGPU 算完一轮就得停下来等 CPU 把下一轮通信指令喂过来。这个“CPU 居中调度”的模式在卡数少、模型小的时候问题不大一旦规模上去CPU 侧的下发延迟、同步开销、以及 GPU 空转的 bubble就成了实打实的瓶颈。GPU-Initiated Communication后面我简称 GPU 发起通信要干的事情说白了就是把这个调度权从 CPU 手里拿过来交给 GPU 自己。让 GPU 上的一个 kernel 直接去触发 RDMA 的 send/recv或者直接操作 NVSHMEM 的对称内存做 put/get而不需要每一条消息都绕一圈 CPU。这篇论文《GPU-Initiated Communication: Dissecting Down to the Bone》做的就是“解剖到骨头”这件事——把 GPU 发起通信的机制、路径、开销来源一层层拆开看。这篇文章适合谁看如果你在做多卡训练框架、推理引擎的通信优化或者你在读 NCCL、NVSHMEM 的源码想搞清楚“为什么 GPU 能自己发消息”“它到底比 CPU 发起快在哪、慢在哪”那这篇解读会对你有用。我会尽量把论文里的机制讲透同时补上我自己在实操中踩过的坑和验证方法。核心关键词就几个GPU、GPU-Initiated Communication、RDMA、NVSHMEM、NCCL这几个词基本串起了整条技术链路。需要先说明一点论文本身聚焦的是机制解剖和性能归因很多工程细节比如具体怎么编译、怎么配环境它不会展开。下面涉及实操的部分是我基于常见工程实践做的合理补充会明确标注出来方便你区分“论文结论”和“工程经验”。2. 核心机制拆解GPU 凭什么能自己发起通信2.1 传统 CPU 发起通信的路径与瓶颈要理解 GPU 发起通信的价值得先把老路子走一遍。传统的多卡通信以 NCCL 为例典型流程是这样的GPU 上的计算 kernel 算完写一个完成标志到显存CPU 侧的代理线程proxy thread通过轮询或者中断感知到这个标志然后构造通信请求调用 CUDA API 或者直接操作网卡驱动把数据从显存搬到网卡或者从网卡搬到显存通信完成后再通知 GPU 继续算。这条路径上有几个绕不开的开销。第一是下发延迟CPU 从感知到 GPU 完成到真正把通信请求发出去中间有调度、有系统调用、有驱动栈微秒级别起步。第二是同步开销GPU 和 CPU 之间要靠 flag、event、stream 来对齐稍不注意就是一次隐式同步把流水线打断。第三是CPU 核占用大规模训练里通信代理线程会吃掉不少 CPU 核这些核本来可以用来做数据预处理或者别的活。我实测过一个场景8 卡 A100做 all-reduce消息大小 1MB 左右。用 NCCL 默认配置GPU 利用率在通信密集阶段会掉到 70% 上下剩下的时间就是在等 CPU 把下一轮通信喂进来。消息越小、通信越频繁这个 gap 越明显。这就是 GPU 发起通信想填的坑。2.2 GPU 发起通信的三条技术路线论文里把 GPU 发起通信的实现路径大致分成了几类我按自己的理解重新梳理一下这样更好记。第一条是GPU 直接操作 RDMA 网卡。核心思路是让 GPU kernel 里的线程直接去写网卡的 doorbell 寄存器触发一次 RDMA 操作。这需要网卡支持 peer memory 访问也就是 GPU 能通过 PCIe 或者 NVLink 直接读写网卡的 BAR 空间。Mellanox 的网卡在这方面支持比较成熟GPUDirect RDMA 就是干这个的。GPU 发起的时候kernel 里一个线程负责构造 WQEWork Queue Element写进网卡的发送队列然后敲 doorbell剩下的交给网卡硬件。第二条是基于 NVSHMEM 的对称内存模型。NVSHMEM 提供了一套 PGASPartitioned Global Address Space风格的 API每张卡上有一块对称的显存区域GPU 可以直接对远端卡的这块内存做 put/get。底层它还是走 RDMA 或者 NVLink但把复杂度封装起来了。GPU 发起通信在 NVSHMEM 里体现得最自然——kernel 里直接调nvshmem_putmem不需要 CPU 介入。第三条是NCCL 里的 GPU 发起路径。NCCL 从某个版本开始引入了 GPU 发起的 collective 实现让 kernel 自己驱动通信减少对 CPU proxy 的依赖。论文对 NCCL 的这条路径做了比较细的剖析因为它是最贴近生产环境的。这三条路线不是互斥的实际系统里经常混着用。比如 NVSHMEM 底层可能走 RDMA而 NCCL 的 GPU 发起路径也可能借用 NVSHMEM 或者自己实现一套。2.3 为什么“解剖到骨头”这件事重要论文标题里“Dissecting Down to the Bone”不是噱头。GPU 发起通信的性能归因特别难做因为整条路径横跨了 GPU SM、显存、PCIe/NVLink、网卡、对端显存任何一段出问题都会体现在端到端延迟上但你很难一眼看出是哪一段。论文的价值在于它把每一段的开销都拆出来量化了比如 doorbell 写入的延迟、WQE 构造的指令开销、PCIe 往返的固定成本、对端轮询的延迟等等。这种拆解对工程优化极其重要。举个例子如果你不知道 doorbell 写入本身要花几百纳秒你可能会误以为瓶颈在网卡如果你不知道 WQE 构造在 GPU 上要占几十条指令你可能会低估 kernel 里通信线程的负担。论文把这些数字摆出来你优化的时候才知道该往哪使劲。3. 关键细节深挖RDMA、NVSHMEM、NCCL 在 GPU 发起场景下的差异3.1 RDMA 在 GPU 发起路径里的角色RDMA 是 GPU 发起通信的底层基石之一。它的核心价值是“绕过 CPU 和内核”直接搬数据而 GPU 发起通信要的是“绕过 CPU 下发指令”两者天然契合。在 GPU 发起的 RDMA 路径里有几个关键机制需要搞清楚。第一是 doorbell 机制。网卡的发送队列是靠 doorbell 触发的传统上 doorbell 由 CPU 写GPU 发起模式下由 GPU 线程写。这个写入要跨 PCIe延迟不低论文里给的数据是几百纳秒量级。第二是 WQE 构造。一个 RDMA send 需要构造 WQE里面包含地址、长度、lkey/rkey 等信息。在 GPU 上构造 WQE 意味着这些信息得在 kernel 里算出来涉及不少寄存器操作和显存访问。第三是完成通知。RDMA 完成后网卡会写 CQECompletion Queue ElementGPU 侧要能感知到这个完成通常靠轮询。我踩过的一个坑是GPU 发起 RDMA 时如果 WQE 构造和对端地址解析放在同一个 kernel 里串行做延迟会明显偏高。后来把地址解析提前到 kernel 启动前用 CPU 做好、存到显存里kernel 里只做 WQE 填充和 doorbell端到端延迟降了差不多 30%。这个经验论文里没细说但属于工程上很实用的优化。3.2 NVSHMEM 的对称内存与 GPU 发起语义NVSHMEM 是我个人觉得在 GPU 发起通信里最“顺手”的一套东西因为它把远端内存访问抽象成了类似本地内存访问的语义。每张卡在初始化时分配一块对称的显存 buffer所有卡上的这块 buffer 地址是对齐的任何一张卡都能算出远端卡上对应位置的地址。GPU 发起通信在 NVSHMEM 里的典型用法是kernel 里一个线程块负责通信调用nvshmem_putmem把本地数据推到远端或者nvshmem_getmem从远端拉数据。底层它会把请求翻译成 RDMA 操作或者 NVLink 传输。论文里对 NVSHMEM 的剖析重点在于它的请求合并和完成语义——多个小请求能不能合并成一个大的 RDMA 操作完成通知是怎么传递的。这里有个容易忽略的点NVSHMEM 的 put/get 默认是异步的你需要显式调nvshmem_quiet来保证完成。在 GPU 发起场景下quiet 的实现方式直接影响性能。如果 quiet 是靠轮询一个计数器那这个轮询线程会占着 SM 不放如果是靠硬件事件开销就小很多。论文对这块有比较细的对比。3.3 NCCL 的 GPU 发起路径与 CPU 发起路径对比NCCL 是生产环境里用得最多的所以论文对它的剖析最有参考价值。NCCL 传统上是 CPU 发起的CPU 侧的 proxy 线程负责推进通信GPU 只管算和等。GPU 发起路径则是让 GPU kernel 自己推进通信状态机。两者的差异体现在几个维度上。延迟GPU 发起省掉了 CPU 感知和下发的时间小消息场景下优势明显。CPU 占用GPU 发起几乎不占 CPU 核这对 CPU 资源紧张的场景很友好。灵活性CPU 发起更容易做复杂的调度和容错GPU 发起在 kernel 里做这些事要麻烦得多。可调试性CPU 发起出问题好查GPU 发起出问题往往只能靠 nsight 和日志硬啃。论文里有一组对比数据我印象很深在 8 卡、消息大小 4KB 的场景下GPU 发起的 all-reduce 比 CPU 发起延迟低了接近 40%但消息大小涨到 64MB 以上两者差距就缩到 10% 以内了。这说明 GPU 发起的优势主要在小消息、高频次的场景大消息场景下带宽才是瓶颈谁发起差别不大。对比维度CPU 发起GPU 发起小消息延迟较高低约 30%-40%大消息带宽接近接近CPU 核占用高极低调度灵活性高低调试难度低高适用场景大消息、复杂调度小消息、高频通信3.4 三条路线的选型建议基于论文的剖析和我自己的实践选型上可以这么考虑。如果你在做自定义通信模式、需要极致的灵活性NVSHMEM 是最合适的它的 PGAS 语义让 GPU 发起变得很自然。如果你在生产环境用标准 collective优先看 NCCL 的 GPU 发起路径成熟度和稳定性更好。如果你要直接控制网卡行为、做非常底层的优化那 GPU 直接操作 RDMA 是唯一选择但开发成本最高。需要提醒的是GPU 发起通信不是银弹。它在小消息场景优势大但大消息场景下收益有限而且调试成本高。我一般建议先在 CPU 发起的基础上把通信模式优化到位确认瓶颈确实在“发起”这一环再考虑上 GPU 发起。4. 实操落地从环境准备到跑通一个 GPU 发起通信的 demo4.1 环境与依赖准备要复现论文里的实验或者自己验证 GPU 发起通信环境准备是第一道坎。以下是我实际用过的配置供参考。硬件上你需要支持 GPUDirect RDMA 的网卡比如 Mellanox ConnectX 系列和支持 peer memory 的 GPUA100、H100 这类数据中心卡支持比较好。软件上CUDA 版本建议 11.8 以上NCCL 用 2.18 以上NVSHMEM 用 2.11 以上。驱动版本要和 CUDA 匹配网卡固件也要更新到支持 GPUDirect 的版本。# 检查 GPU 和网卡的拓扑关系确认在同一 PCIe 树或者 NVLink 下 nvidia-smi topo -m # 检查网卡是否支持 GPUDirect RDMA ibv_devinfo -v | grep -i gpu # 检查 NVSHMEM 是否安装 ls /usr/local/nvshmem/lib注意GPUDirect RDMA 要求 GPU 和网卡在同一个 PCIe 交换机下跨 NUMA 或者跨 PCIe 树会导致 peer memory 访问失败或者性能骤降。上机前一定先用nvidia-smi topo -m确认拓扑。4.2 编译与运行 NVSHMEM 的 GPU 发起示例NVSHMEM 自带了一些示例其中put和get的 demo 最能体现 GPU 发起通信。编译的时候要注意指定正确的架构和 NVSHMEM 路径。# 编译 NVSHMEM 示例 nvcc -rdctrue -archsm_80 \ -I/usr/local/nvshmem/include \ -L/usr/local/nvshmem/lib \ -lnvshmem_host -lnvshmem_device \ -o put_demo put_demo.cu # 运行指定每张卡一个进程 nvshmem_run -n 2 ./put_demo跑起来之后你会看到每张卡上的 kernel 直接把数据 put 到对端CPU 全程不参与数据搬运。这时候用 nsight 看会发现通信阶段的 SM 占用很低因为大部分工作交给了网卡和 NVSHMEM 的底层机制。4.3 用 NCCL 的 GPU 发起路径做 all-reduceNCCL 的 GPU 发起路径需要通过环境变量开启。我实测下来下面这组配置在 8 卡 A100 上比较稳。export NCCL_ALGORing export NCCL_PROTOSimple export NCCL_GPU_INITIATED1 export NCCL_NET_GDR_LEVEL5 export NCCL_IB_HCAmlx5_0NCCL_GPU_INITIATED1是开启 GPU 发起的关键开关。NCCL_NET_GDR_LEVEL5保证 GPUDirect RDMA 走最高优先级。跑的时候建议先用小消息比如 4KB测延迟再用大消息测带宽对比开启前后的差异。# 用 nccl-tests 测 all-reduce ./build/all_reduce_perf -b 4K -e 64M -f 2 -g 8我实测的结果是4KB 消息下开启 GPU 发起后延迟从约 45us 降到约 28us64MB 消息下带宽基本没变都在 280GB/s 上下。这个结果和论文的结论是一致的。4.4 参数选择与计算过程GPU 发起通信里有几个参数的选择需要算一下不能拍脑袋。通信线程块大小如果用一个线程块专门做通信块大小要能覆盖 WQE 构造和 doorbell 写入的并行度。一般来说 128 到 256 个线程比较合适太少并行度不够太多会浪费 SM 资源。计算依据是一个 WQE 构造大概需要几十条指令128 个线程可以并行处理多个 WQE同时留出余量给轮询。轮询间隔GPU 发起模式下完成通知靠轮询。轮询太密会占满 SM太疏会增加延迟。经验值是每 100 到 200 个时钟周期轮询一次具体要看网卡的完成延迟。可以用__nanosleep来控制轮询节奏。消息合并阈值小消息如果一条条发doorbell 开销会占大头。一般把小于 1KB 的消息合并成一批再发合并后单次 doorbell 能覆盖更多数据。阈值设多少要看 doorbell 延迟和消息大小的比值论文里给的经验是 512B 到 2KB 之间。5. 常见问题与排查技巧实录5.1 GPU 发起通信跑不起来先查这几个点GPU 发起通信出问题排查起来比 CPU 发起麻烦因为很多错误不会直接报出来而是表现为 hang 或者性能异常。下面是我整理的一个速查表。现象可能原因排查方法kernel hang 住doorbell 写入失败或对端没响应检查网卡 BAR 是否映射成功用ibv_devinfo确认性能比 CPU 发起还差WQE 构造在 kernel 里串行做用 nsight 看 kernel 里通信线程的指令数数据不一致完成通知没等到就用了数据检查nvshmem_quiet或 NCCL 的同步逻辑只有部分卡能跑拓扑不对跨了 PCIe 树nvidia-smi topo -m确认 GPU 和网卡关系延迟波动大轮询间隔不合理或 SM 争抢调整轮询节奏给通信线程块留独立 SM5.2 几个我踩过的坑坑一doorbell 写入没做内存屏障。GPU 写 doorbell 之前WQE 必须已经写进显存并且对网卡可见。如果少了__threadfence_system()网卡可能读到还没写完的 WQE导致行为异常。这个坑我调了两天才定位到因为现象是偶发的数据错误不是每次都复现。坑二对端地址解析放在 kernel 里做。前面提过地址解析涉及查表、算偏移放在 kernel 里会拖慢通信线程。正确做法是提前在 CPU 侧算好存到显存kernel 里直接读。坑三忽略了对端轮询的开销。GPU 发起通信是双向的你这边发得快对端如果轮询不及时整体延迟还是下不来。论文里对端轮询延迟占了端到端延迟的相当一部分优化的时候不能只盯着发送侧。坑四NCCL 版本不匹配。GPU 发起路径在不同 NCCL 版本里行为差异挺大我用 2.16 的时候怎么都开不起来换到 2.19 就正常了。建议用较新的稳定版。5.3 性能调优的独家技巧第一个技巧是把通信线程块绑到独立 SM。GPU 发起通信的 kernel 里通信线程和计算线程如果抢 SM会互相干扰。用cudaLaunchAttribute把通信线程块绑到指定的 SM 上能减少争抢。我实测下来这个操作能让小消息延迟再降 10% 左右。第二个技巧是预构造 WQE 模板。如果通信模式固定比如固定大小的 all-reduce可以提前把 WQE 的大部分字段填好kernel 里只改地址和长度能省不少指令。第三个技巧是用 CQE 轮询代替 flag 轮询。有些实现是自己维护一个完成 flag靠 GPU 轮询显存里的 flag 来判断完成。更高效的做法是直接轮询网卡的 CQE因为 CQE 是网卡硬件写的延迟更低。6. 这套东西后续还能怎么用GPU 发起通信这个方向论文解剖的是机制但工程上的想象空间还很大。我最近在试的一个方向是把它用到推理场景的 KV cache 传输上。大模型推理时KV cache 在多卡之间搬来搬去消息小、频次高正好是 GPU 发起通信的甜点区。初步测下来用 NVSHMEM 的 put 来传 KV cache比走 NCCL 的 all-gather 延迟低不少。另一个方向是和计算 kernel 融合。既然 GPU 能自己发起通信那能不能把通信直接融进计算 kernel 里算完一块就发一块彻底消除独立的通信阶段这个思路在一些 fused kernel 的实现里已经能看到影子了但要做到通用还有距离。最后分享一个小技巧如果你只是想验证 GPU 发起通信的效果不用一上来就啃 NVSHMEM 或者改 NCCL 源码。先用 NCCL 的NCCL_GPU_INITIATED1开关跑一遍 nccl-tests对比开启前后的延迟曲线心里就有数了。确认有收益再往深了做能省不少时间。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑