CUDA Techniques to Maximize Memory Bandwidth and Hide Latency [S72683]

Athena Elafrou, Sr. Developer Technology Engineer
Allard Hendriksen, Sr. Developer Technology Engineer
GTC, March 17th 2025

目录

议程 (Agenda)

"问题在于内存,笨蛋!" ("It's the Memory, Stupid!")

<blockquote>

"It's the Memory, Stupid!"
- Richard Sites, Multiprocessor Report, 1996

</blockquote>

GPU 内存层次结构 (GPU Memory Hierarchy)

硬件趋势 (Hardware trends)

下图展示了典型的GPU内存层次结构,以及各级别内存的发展趋势。

GPU内存层次结构图示 (Page 4)
GPU内存层次结构图示 (Page 4)

每个GPU架构的演进趋势如下:
- 更多的流多处理器 (SMs) 以执行计算。
- 每个SM拥有更大的L1缓存/共享内存
- 更大的L2缓存和更高的带宽
- 更大的DRAM和更高的带宽

在接下来的几页中,我们将分别关注L1/共享内存、L2和DRAM的发展。

首先,关注每个SM的L1缓存/共享内存。

GPU内存层次结构图示,聚焦于L1/共享内存 (Page 5)
GPU内存层次结构图示,聚焦于L1/共享内存 (Page 5)

硬件趋势 - 共享内存 (Hardware trends - Shared memory)

从Kepler到Hopper Blackwell架构,共享内存(Shared Memory)的大小在持续增加。增加共享内存可以减少访问全局内存的往返次数。

Hopper Blackwell架构引入了分布式共享内存(Distributed Shared Memory)。

各代GPU共享内存大小对比 (Page 6)
各代GPU共享内存大小对比 (Page 6)

分布式共享内存 (Distributed shared memory)

Hopper Blackwell架构中的分布式共享内存允许线程块集群(Thread Block Cluster)之间高效地共享数据。这引出了一个关键问题:如何在线程块集群内高效地同步和交换数据?

分布式共享内存与线程块集群 (Page 7)
分布式共享内存与线程块集群 (Page 7)

硬件趋势 - L2缓存

接下来,关注L2缓存。每一代GPU架构都带来了更大的L2缓存和更高的带宽。

GPU内存层次结构图示,聚焦于L2缓存 (Page 8)
GPU内存层次结构图示,聚焦于L2缓存 (Page 8)

硬件趋势 - DRAM

最后,关注DRAM。每一代GPU架构都配备了更大的DRAM和更高的带宽。

GPU内存层次结构图示,聚焦于DRAM (Page 9)
GPU内存层次结构图示,聚焦于DRAM (Page 9)

硬件趋势 - DRAM 带宽

下图展示了从P100到B200架构,DRAM总带宽、SM数量以及每个SM的带宽的变化趋势。

DRAM带宽硬件趋势图表 (Page 10)
DRAM带宽硬件趋势图表 (Page 10)

这引出了一个核心问题:如何饱和带宽 (How to saturate bandwidth)?

最大化内存带宽 (Maximizing Memory Bandwidth)

利特尔定律 (Little's Law)

利特尔定律指出:系统中的平均单元数 = 平均到达率 * 平均停留时间

我们可以用一个自动扶梯的例子来类比:
- 扶梯规格:
- 每级台阶1人
- 高度为20级台阶
- 每2秒到达一级新台阶

问题: 如果扶梯上只有1个人(in-flight),实现的吞吐量是多少?
吞吐量 = #人数 / 停留时间 = 0.025 人/秒

利特尔定律的扶梯类比 - 单人情况 (Page 12)
利特尔定律的扶梯类比 - 单人情况 (Page 12)

问题: 我们需要多少人同时在扶梯上(in-flight)才能最大化吞吐量?
并发数 = 峰值到达率 * 停留时间 = 0.5 人/秒 * 40 秒 = 20 人

利特尔定律的扶梯类比 - 多人情况 (Page 13)
利特尔定律的扶梯类比 - 多人情况 (Page 13)

利特尔定律应用于GPU内存

将利特尔定律应用于GPU内存,公式为:
在途字节数 (bytes-in-flight) = 带宽 (bandwidth) * 平均延迟 (mean latency)

其中,在途字节数由软件控制,而平均延迟由硬件决定。

下图显示了峰值带宽(占理论峰值百分比)与每个SM的在途字节数之间的关系。可以看出,为了达到高带宽利用率,需要有足够多的在途字节数(例如,H200需要约48 KiB,GB200-NVL需要约64 KiB)。

利特尔定律应用于GPU内存的性能图 (Page 14)
*图中的点代表使用不同操作、线程块维度、数据类型和并行加载数量的类STREAM负载。

简单内核能否饱和内存带宽?

我们能否用简单的内核来饱和内存带宽?考虑以下简单的向量加法内核:

__global__ void kernel(float *a, float *b, float *c)
{
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    c[i] = a[i] + b[i];
}

下图显示,尽管GPU的理论带宽(BW)不断提升,但对于这个简单内核,带宽利用率(BWUtil)却在下降,因为16 KiB的在途字节数不足以饱和新一代GPU的内存带宽。

简单内核的带宽性能分析 (Page 15)
简单内核的带宽性能分析 (Page 15)

我们再考虑一个稍复杂的内核: d[i] = a[i] + b[i] + c[i];

不同 GPU 架构下的带宽和峰值带宽利用率。
不同 GPU 架构下的带宽和峰值带宽利用率。

图表显示,从 V100 到 B200,尽管原始带宽(TB/s)持续增加,但对于这些简单的内核,峰值带宽利用率(BWUtil %)却在下降。这表明随着硬件的发展,简单内核越来越难以充分利用可用的内存带宽,即使我们增加了操作数。

增加在途字节 (Increasing Bytes-in-Flight)

有三种主要技术可用于增加在途字节(bytes-in-flight):

  1. 线程内更多的独立内存操作 (指令级并行,Instruction-level parallelism)。
  2. 线程内向量化的内存操作 (数据级并行,data-level parallelism)。
  3. 异步数据拷贝 (Asynchronous data copies)。

增加指令级并行 (Increasing ILP)

以下示例展示了通过增加指令级并行(ILP)来增加在途字节。

Page 19 的代码和指令序列图示
Page 19 的代码和指令序列图示

循环展开

使用循环展开(Loop unrolling)可以增加 ILP。

Page 20 的代码和展开后的指令序列图示
Page 20 的代码和展开后的指令序列图示

循环展开的问题

循环展开并非总是能按预期工作。

Page 21 的代码和编译器行为示意图
Page 21 的代码和编译器行为示意图

手动循环展开

手动循环展开是一种获得最佳性能的解决方案。

Page 22 的手动循环展开代码和指令序列图示
Page 22 的手动循环展开代码和指令序列图示

增加数据级并行 (Increasing DLP)

通过向量化加载可以增加数据级并行(DLP)。

Page 23 的 warp 内存访问图示
Page 23 的 warp 内存访问图示

向量化加载

Page 24 的 warp 使用 float2 进行内存访问的图示
Page 24 的 warp 使用 float2 进行内存访问的图示
Page 25 的 warp 使用 float4 进行内存访问的图示
Page 25 的 warp 使用 float4 进行内存访问的图示

实现方法

Page 26 的向量化加载实现代码和图示
Page 26 的向量化加载实现代码和图示

性能比较

比较不同技术(循环展开和向量化)的有效性。

Page 27 的性能比较图表
Page 27 的性能比较图表

寄存器使用

增加 ILP 和 DLP 带来了一个权衡:寄存器压力增加。

Page 28 的峰值带宽与寄存器使用关系图
Page 28 的峰值带宽与寄存器使用关系图

异步数据拷贝 (Asynchronous Data Copies)

异步数据拷贝是一种可以绕过寄存器直接将数据拷贝到共享内存的技术。

Page 29 同步与异步数据拷贝流程对比图
Page 29 同步与异步数据拷贝流程对比图

允许内存传输与计算重叠

异步数据拷贝可以与计算操作重叠执行,从而隐藏内存延迟并增加在途字节。

Page 30 的串行加载与计算流程图
Page 30 的串行加载与计算流程图
Page 31
Page 31
Page 32
Page 32

启用生产者-消费者模式

异步数据拷贝天然支持生产者-消费者模式。生产者线程(producers)负责从全局内存(GMEM)加载数据到共享内存(SMEM),而消费者线程(consumers)则从共享内存中读取数据进行计算。这个过程可以流水线化,以实现高效的数据处理。

Page 33
Page 33

使用场景

概述

下表总结了不同内存空间之间的异步数据拷贝及其完成机制。

Page 35
Page 35

LDGSTS指令

LDGSTSsmem[sidx] = gmem[gidx] 的异步版本,支持一次性拷贝4、8或16字节。

Page 36
Page 36

LDGSTS APIs

实现LDGSTS功能可以通过以下几组API:

代码示例

切换到异步拷贝以进行批处理计算

Primitives API 示例

以下代码展示了如何将一个标准的同步数据加载计算循环转换为使用 Primitives API 的异步版本。异步版本通过在计算前发起异步内存拷贝并等待其完成,实现了计算与数据传输的重叠。

Page 38
Page 38
Libcudacxx API 示例

使用 libcudacxx API 可以实现类似的功能。该API支持大于16字节的拷贝,并使用 cuda::aligned_size_t 帮助编译器进行优化。

Page 39
Page 39

数据预取

使用 cuda::pipeline (1/2): 序言 (Prologue)

数据预取通常分为两部分:序言和主循环。序言部分负责为第一次迭代预取数据。为了避免线程束分化(warp entanglement),producer_acquire()producer_commit() 应该在收敛的代码路径中调用。

Page 40
Page 40
使用 cuda::pipeline (2/2): 主循环 (Main loop)

主循环中,在处理当前阶段的数据之前,会为下一个迭代预取数据。cuda::pipeline_consumer_wait_prior<1>(pipe) 用于等待当前阶段的数据拷贝完成。计算完成后,通过 pipe.consumer_release() 释放已使用的阶段。

Page 41
Page 41

多阶段数据预取

序言 (1/2)

通过使用多级缓存(Multi-Stage Buffering),可以隐藏更高的内存延迟。使用编译时常量 NUM_STAGES 可以确保编译器消除内部的簿记指令。预取距离等于 NUM_STAGES - 1。序言部分会加载所有流水线阶段的数据。

Page 42
Page 42
主循环 (2/2)

在主循环中,等待操作 cuda::pipeline_consumer_wait_prior<NUM_STAGES - 1>(pipe) 会一直等到除了最近的 NUM_STAGES - 1 个阶段外所有数据都加载完毕。然后进行计算,释放已消耗的阶段,并为 NUM_STAGES 次迭代之后的数据发起新的预取。

Page 43
Page 43
使用生产者-消费者模式 (1/2): 序言

这种模式下,可以指定一部分线程(例如,memcpy_threads)专门用于内存拷贝。每个线程拷贝16字节可以启用L1 BYPASS模式,以获得更好的性能。

Page 44
Page 44
使用生产者-消费者模式 (2/2): 主循环

在主循环中,计算步骤前后需要同步(__syncthreads()),以确保所有线程在计算开始前都能访问到最新的数据,并在计算结束后再进行下一次数据预取,避免数据覆盖。

Page 45
Page 45

我需要多少个阶段 (How Many Stages Do I Need?)

调整内核以达到目标在途字节数 (Tuning our kernel to reach a target bytes-in-flight)

为了确定流水线所需的阶段数量,可以使用以下公式来计算每个SM(Streaming Multiprocessor)的在途字节数(bytes in flight):

Page 46 - 计算在途字节数的公式
Page 46 - 计算在途字节数的公式

公式分解如下:

根据这个公式,对于Hopper架构,我们需要2个阶段;对于Blackwell架构,需要3个阶段。

性能分析:简单计算内核

对于一个简单的计算任务 compute(a, b) = a * b,在NVIDIA H100上的性能表现如下:

Page 47 - H100上简单计算内核的性能数据
Page 47 - H100上简单计算内核的性能数据

性能分析工具NVIDIA Nsight Compute显示,该内核的主要瓶颈是Stall Long Scoreboard,即等待长延迟操作(如内存加载)完成。

Page 48 - 简单计算内核的Warp状态分析
Page 48 - 简单计算内核的Warp状态分析

性能分析:复杂计算内核

对于一个计算延迟更高的任务 compute(a, b) = sqrt(sqrt(a) / sqrt(b)),在NVIDIA H100上的性能表现如下:

Page 49 - H100上复杂计算内核的性能数据
Page 49 - H100上复杂计算内核的性能数据

当计算延迟增加时,尽管长计分板停滞(long scoreboard stalls)仍然是主要瓶颈,但增加在途字节数可以产生显著的影响。
- 2 stages: 相较于基准版本,获得了1.305倍的显著加速,带宽利用率从68.62%大幅提升至89.56%。
- 3 stages: 性能略有下降,但仍比基准版本快1.281倍。

Nsight Compute的分析再次确认Stall Long Scoreboard是主要的性能瓶颈。

Page 50 - 复杂计算内核的Warp状态分析
Page 50 - 复杂计算内核的Warp状态分析

张量内存加速器 (Tensor Memory Accelerator - TMA)

TMA是一种用于批量拷贝的高效异步数据传输机制。
- 两个编程模型:
- 一维连续数组的批量异步拷贝 (TMA 1D)。
- 多维数组的批量异步拷贝 (TMA ND)。
- 在 [S62192]: "Advanced Performance Optimization in CUDA" 中有广泛介绍。

下图展示了TMA在全局内存和共享内存之间传输数据的过程。
Page 51 - TMA架构示意图

TMA 1D (UBLKCP)

UBLKCP (Unified Bulk Copy) 是TMA一维拷贝的实现。

Page 52 - TMA 1D (UBLKCP) 工作流程
Page 52 - TMA 1D (UBLKCP) 工作流程

TMA 1D编程模式 (GMEM to SMEM)

异步拷贝的典型编程模式分为三个阶段:初始化(INIT)、触发(FIRE)和等待完成(WAIT FOR COMPLETION)。

  1. 使用cuda::memcpy_async:

    • 使用单个线程启动异步拷贝。如果源/目标指针是16字节对齐且大小是16的倍数,则使用TMA;否则,它会回退到同步拷贝。
      Page 53 - 使用memcpy_async的TMA 1D代码示例
    • 注意: 如果不满足大小和对齐要求,行为是未定义的。此功能仅在Hopper+架构上有效。
      Page 54 - TMA 1D的未定义行为警告
  2. 使用PTX内联汇编:

    • 提供更底层的控制。代码结构类似,但使用ptx::cp_async_bulk等指令。
      Page 55 - 使用PTX的TMA 1D代码示例
    • 编译器问题: 编译器不知道 if (threadIdx.x == 0) 这个条件对于整个线程束是恒定的,因此可能会为单个线程生成一个剥离循环,影响效率。
      Page 56 - PTX代码的编译器问题
    • 解决方案: 使用cooperative_groups::invoke_one来明确告知编译器,在线程束中只有一个活动的线程将执行TMA操作,从而避免生成不必要的代码。
      Page 57 - 使用cooperative_groups优化TMA 1D代码

下图展示了如何将一个标准的批处理计算内核重构为使用异步拷贝的模式,从而实现计算和数据传输的重叠。

Page 58 - 标准内核到异步拷贝内核的转换
Page 58 - 标准内核到异步拷贝内核的转换

零开销异步拷贝 (Zero-Effort Async Copies)

使用 Thrust::transform

Thrust库提供了一种更简单的方式来使用异步拷贝,几乎不需要手动管理。

Page 60 - 在Thrust中启用TMA
Page 60 - 在Thrust中启用TMA

数据预取 (Data Prefetching)

使用 TMA 1D

以下代码片段展示了使用一维张量内存加速器 (TMA 1D) 进行数据预取。

Page 61
Page 61
Page 62
Page 62

使用异步拷贝 (Using Asynchronous Copies)

下表总结了不同异步拷贝机制的对齐约束和额外优势。

建议:
- 优先选择 TMA 来拷贝较大数据块。
- TMA 指令的延迟高于 LDGSTS,因此需要更多数据来分摊其成本。

Page 63
Page 63

优化指南 (Optimization Guidelines)

该流程图为选择合适的内存优化策略提供了指导。

  1. 检查在途字节数 (bytes-in-flight):

    • 如果已经足够,则无需优化。
    • 如果不足,则考虑预取。
  2. 选择预取目标:

    • 寄存器 (REG): 进行循环展开或向量化。
    • 共享内存 (SMEM): 检查数据对齐。
  3. 基于对齐和数据块大小选择指令:

    • 4 或 8 字节对齐: 使用 LDGSTS
    • 16 字节对齐: 根据数据块(tile)大小决定:
      • < 1 KiB: 使用 LDGSTS
      • > 1KiB 且 < 2KiB: 使用 LDGSTSTMA
      • > 2KiB: 使用 TMA
Page 64
Page 64

关键要点 (Key Takeaways)

要点 #1:

要点 #2:
- CUDA 提供了异步数据拷贝机制(LDGSTS 和 TMA),这些机制不占用额外的寄存器。
- 使用这些特性编写内核会更复杂。
- 在某些情况下,我们可以利用库来“免费”启用 TMA。

Page 65
Page 65

内存模型 (Memory model)

Page 66
Page 66

什么是内存模型?

Page 67
Page 67

四个主题

本节将涵盖内存模型的四个主题,分别对应不同的 GPU 架构演进:
- 单线程 (Single thread)
- 多线程 (Multi-thread): Volta 架构
- 异步线程 (Async thread): Ampere 架构
- 异步代理 (Async proxy): Hopper 架构

Page 68
Page 68

单线程加载和存储

对于单个线程:
- 存储(store)操作对执行该存储的线程是可见的。
- 对同一地址的加载和存储操作会保持其顺序。这被称为同地址排序 (same-address ordering)
- 如下图代码所示,对 val 的写入和读取操作不会被重排,因此断言 assert(val == 42) 总是成立。
- 问题: 这种排序规则是否总是成立?是否存在例外?

Page 69
Page 69

非一致性:常量缓存

同地址排序并不总是成立。
- 对于常规的加载和存储,缓存是保持一致的。
- 但在某些情况下会存在非一致性 (non-coherence),常量缓存 (constant caches) 就是一个例子。

常量缓存的工作方式:
- 常量缓存与 L2 缓存有链接,但此链接独立于 L1 缓存。
- L1 缓存和常量缓存之间没有通信,因此它们之间的数据不是相互一致的。

Page 70
Page 70

以下代码展示了常量缓存可能导致的非一致性问题。
- 一个 __constant__ 变量 val 被修改。
- 即使 val 在全局内存(通过 L1/L2 路径)中被更新为 42,后续对 val 的加载操作可能会命中常量缓存,从而返回一个过时的值(stale value)。
- 这可能导致 assert(val == 42) 失败。
- 问题: 在多线程并行的情况下,排序是如何工作的?

Page 71
Page 71

多线程内存顺序: Relaxed / Release / Acquire

"内存顺序指定了内存访问(包括常规的非原子访问)如何围绕一个原子操作进行排序。"

以下是四种内存顺序的比较:

顺序类型 描述
Sequentially consistent - 加载和存储不能在原子操作之前或之后移动。
- 在单线程内保持同地址排序。
- 易于编程,但对硬件而言速度较慢。
Acquire - 加载和存储不能移动到 acquire 操作之前。
- 在单线程内保持同地址排序。
Release - 加载和存储不能移动到 release 操作之后。
- 在单线程内保持同地址排序。
Relaxed - 加载和存储可以在原子操作之前或之后移动。
- 在单线程内保持同地址排序。
Page 72
Page 72

顺序一致 (Sequentially consistent)

Page 73
Page 73

获取 (Acquire)

Page 74
Page 74

释放 (Release)

Page 75
Page 75

松散 (Relaxed)

内存模型:线程作用域

CUDA C++ 作用域:thread, block, device, system

作用域定义了哪些线程可以观察到当前线程的加载和存储操作。

Page 77
Page 77

CUDA PTX 作用域:block, cluster, device, system

PTX(并行线程执行)指令集体系结构有其自身的作用域定义。

Page 78
Page 78

内存模型:GPU内存层级与作用域

作用域与内存层级的关系

每个作用域都有一个关联的一致性点(point of coherency),它决定了在该作用域内,内存操作在何处变得对其他线程可见。

Page 83
Page 83

内存模型:多线程示例

块内松散原子操作示例 (Relaxed block example)

Page 84
Page 84

跨块松散原子操作的失败示例 (A not so relaxed device example)

Page 85
Page 85

跨块松散原子操作的正确示例 (A relaxed device example)

Page 86
Page 86

使用标志位的松散原子操作失败示例

当需要同步多个值时,仅使用松散原子操作可能会引入问题。

  1. L1缓存导致的问题
    Page 87

    • 场景: 生产者需要更新一个值 val 和一个标志 flag。消费者等待 flag 被设置,然后读取 val。所有操作都是 relaxed
    • 潜在失败: 消费者可能成功读取到 flag 的更新值(假设该读取操作命中了L2),但在读取 val 时,却命中了其本地L1缓存中的旧值。这导致断言失败。
  2. 乱序观察导致的问题
    Page 88

    • 场景: 与上一个类似。
    • 潜在失败: 即使 valflag 的读写都命中了L2/DRAM,relaxed 内存顺序也不保证操作的顺序。消费者可能会观察到 flag 的更新先于 val 的更新,即使在生产者的代码中 val 的更新在 flag 之前。这同样会导致消费者读取到旧的 val 值。

Release-Acquire 模式

Page 89
Page 89

Relaxed 与 Release-Acquire 的比较

Page 90
Page 90

内存模型:异步线程 (Async threads)

异步线程:一个激励性示例

PTX 指令 st.async 的作用是将一个值存储到集群中另一个块的分布式共享内存(Distributed Shared Memory)中。一旦存储完成,它会更新另一个块的共享内存中的一个共享内存屏障(shared memory barrier)。

Page 91
Page 91

然而,这种异步操作存在一个问题:后续的加载(load)或存储(store)操作可能会提前执行,从而违反了同一地址的顺序性(same-address ordering)。如下图所示,对 remote_addr 的加载操作可能会在 st.async 存储操作完成之前执行,导致数据竞争。

Page 92
Page 92

解决方案与模型

PTX 指令 st.async 相对于其后的加载或存储操作,不遵守同一地址顺序。这个问题的解决方法是:

Page 93
Page 93

内存模型:异步代理 (Async proxies)

代理(Proxies)代表了这样一种情况:从单个线程到单个物理内存位置存在多条不同的路径,而这些路径之间没有一致性/窥探(coherence/snooping)机制。

Page 94
Page 94

异步代理:共享内存示例

以下代码展示了一个数据竞争的例子。对共享内存 smem 的存储是通过通用代理进行的,而后续使用 ptx::cp_async_bulk 从共享内存复制到全局内存的操作(本质上是对 smem 的加载)是通过异步代理(TMA)进行的。这可能导致从 smem 的加载操作在对它的存储操作之前执行。

#include <cuda/ptx>
namespace ptx = cuda::ptx;

__device__ float4 gmem;

__global__ void kernel() {
  __shared__ float4 smem;

  // Store value to shared memory
  // (generic proxy)
  smem = {42., 42., 42., 42.};

  // Copy from shared to global memory
  // (async proxy)
  ptx::cp_async_bulk(
    ptx::space_global, ptx::space_shared,
    gmem, &smem, sizeof(smem)
  );
}
Page 95
Page 95

为了解决这个问题,需要在两个代理操作之间插入一个栅栏(fence)。ptx::fence_proxy_async 指令可以确保代理之间的加载和存储顺序。

// ...
  // Store value to shared memory
  // (generic proxy)
  smem = {42., 42., 42., 42.};
  // Fence between proxies
  ptx::fence_proxy_async(ptx::space_shared);
  // Copy from shared to global memory
  // (async proxy)
  ptx::cp_async_bulk(/*...*/);
// ...
Page 96
Page 96

异步代理:自动跨代理栅栏

在某些情况下,栅栏是自动插入的。例如,当一个异步代理操作(如 cp.async.bulk)之后跟着一个等待屏障的操作(如 mbarrier_try_wait),屏障的等待操作会隐式地创建一个跨代理的栅栏。这确保了在屏障状态翻转之前,所有先前的内存操作都已完成,从而保证了后续通用代理加载操作的顺序性。

Page 97
Page 97

异步线程与异步代理指令总结

下表总结了哪些指令属于异步线程模型,哪些属于异步代理模型。

Page 98
Page 98

低延迟集群同步 (Low-Latency Cluster Synchronization)

Page 99
Page 99

关键点

Page 100
Page 100

两种在集群中同步线程的方式

  1. 协作组 (Cooperative groups):

    • 使用 cluster::sync()
    • 必须由集群中的所有线程执行。
    • 总是使其之前的加载/存储对集群中的其他线程可见。
    • 速度慢:需要到 L2 的往返通信。
  2. CUDA PTX:

    • 使用 ptx::barrier_cluster_arriveptx::barrier_cluster_wait 分离到达和等待阶段。
    • arrive 必须由所有线程执行。
    • 可以选择性地使加载/存储可见:sem_release 使其可见,而 sem_relaxed 则不。
Page 101
Page 101

屏障初始化:简单方式

共享内存屏障是另一种同步机制。它们在使用前必须被初始化。初始化后的屏障必须对集群中的其他线程可见。简单的方法是使用 cluster::sync(),但这很慢。

Page 102
Page 102

屏障初始化:快速方式

为了避免 L2 的往返通信,可以采取以下措施:

Page 103
Page 103

数据通信

Page 104
Page 104

下面是一个使用 cluster::sync() 进行数据通信的简单基准测试示例。

Page 105
Page 105

使用 st.async() 通信数据

以下代码展示了使用 st.async() 和内存屏障(mbarrier)实现低延迟集群同步的 PTX 代码示例。

Page 106
Page 106

代码逻辑解析
- for 循环:在多次迭代中执行通信。
- // Send value:使用 ptx::st_async 异步发送一个值(42)到远程内存地址 remote_val
- // Arrive on local barrier:在本地内存屏障上执行 arrive 操作,并期望一次传输(expect_tx)。
- // Wait for value from other cluster:在一个 while 循环中,使用 ptx::mbarrier_try_wait 尝试在屏障上等待,直到从另一个集群接收到值。
- // Wait for other block to have received our value:使用 ptx::barrier_cluster_arriveptx::barrier_cluster_wait 来确保其他块已经收到了我们发送的值,这是一个集群范围的同步。

性能对比

此页面展示了异步与同步通信性能的基准测试对比。

Page 107
Page 107

简单基准测试 (Simple benchmark)
- 集群中有 2 个块。
- 在每次迭代中,它们通信一个整数。
- 测试平台为 H100。

结果 (Result)
- 同步(Synchronous)通信的性能为 1.3M 次迭代/秒。
- 异步(Asynchronous)通信的性能为 7M 次迭代/秒。
- 异步版本的速度比同步版本快 5倍以上

结论 (Conclusion)
- 避免在热循环(hot loops)中使用 cluster::sync()
- 使用 st.async 来获得显著的加速。

CUDA 开发者会议 (CUDA Developer Sessions)

此页面列出了一系列与 CUDA 相关的开发者会议,涵盖了从入门到高级优化的多个主题。

Page 108
Page 108