Guillaume Thomas-Collignon, Vishal Mehta
DevTech Compute, GTC 2022
霍珀架构 (Hopper Architecture)
即将讨论的新特性
线程和内存层级结构 (Thread and Memory Hierarchy)
Hopper之前的CUDA线程和内存层级结构
Hopper中引入线程块集群 (Thread Block Clusters)
使用集群启动CUDA内核
示例:共享内存直方图
示例:分布式共享内存直方图
直方图性能
异步SIMT编程模型 (Asynchronous SIMT Programming Model)
异步事务屏障 (Hopper 新特性)
cuda::memcpy_async 全局 <-> 共享
Stencil 代码示例
TMA 示例:3D TTI 逆时偏移 (Reverse Time Migration)
cuda::memcpy_async生产者/消费者用例:Longstaff Schwartz 定价模型
结论与主要要点
H100 GPU引入了多项关键特性,旨在提升性能和功能。
主要特性包括:
* 第2代多实例GPU (Multi-Instance GPU)
* 机密计算 (Confidential Computing)
* PCIe Gen5
* 更大的50 MB L2缓存
* 80GB HBM3,3 TB/s带宽
* 132个流多处理器 (SMs)
* 第4代张量核心 (Tensor Core)
* 线程块集群 (Thread Block Clusters)
* 第4代NVLink,总带宽900 GB/s
更多信息可参考:
* Inside the NVIDIA Hopper Architecture (S42663)
* CUDA: New Features and Beyond (S41486)
H100 SMs的设计带来了显著的改进。
主要特性包括:
* 256 KB的组合L1缓存/共享内存,每个SM比A100增加33%。
* 新的线程块集群 (Thread Block Clusters) 和分布式共享内存 (Distributed Shared Memory)。
* 新的张量内存加速器 (Tensor Memory Accelerator) 和异步事务屏障 (Asynchronous Transaction Barriers)。
* 第4代张量核心,每个时钟周期性能提升2倍。
本次演讲将深入探讨以下Hopper架构中的新特性:
线程层级结构 (Thread Hierarchy)
异步SIMT编程模型 (Asynchronous SIMT Programming Model)
cuda::memcpy_async)在CUDA编程模型中,线程和内存的层级结构对于理解和优化CUDA应用程序至关重要。
在Hopper架构之前,CUDA的线程和内存层级结构有以下特点:
线程块中的所有线程都保证在单个流多处理器 (SM) 上协同调度。
线程块中的线程可以使用cooperative_groups::this_thread_block.sync() 或 __syncthreads() 进行同步/通信。
也可以使用 cuda::barrier<thread_scope_block>::arrive() 和 ::wait()。
线程块中的线程还可以执行集合操作,如 cooperative_groups::reduce()。
Hopper架构引入了线程块集群,为CUDA编程模型带来了新的可选层级结构。
Hopper架构为集群内的线程提供了硬件加速同步机制。
namespace cg = cooperative_groups;
auto block = cg::this_thread_block();
cg::cluster_group cluster = cg::this_cluster();
<...>
cluster.sync();
线程块集群内的所有块都可以使用分布式共享内存进行协作。
// All blocks in the cluster have the variable smem
__shared__ int smem;
namespace cg = cooperative_groups;
cg::cluster_group cluster = cg::this_cluster();
unsigned int BlockRank = cluster.block_rank();
int cluster_size = cluster.dim_blocks().x;
// Get a pointer to peer smem variable based on
// pointer from current block
int *remote_smem = cluster.map_shared_rank(&smem,
(BlockRank + 1) % cluster_size);
if (threadIdx.x == 0)
*remote_smem = 10; // Store to remote memory
cluster.sync(); // Sync to ensure store is done
* 免责声明:预发布CUDA API,可能会有变动。
<<< >>> 方式启动内核。global_ void cluster_dims_(2, 2, 1) clusterKernel() { ... }cudaLaunchConfig_t 配置对象,通过 attribute[0].Id = cudaLaunchAttributeClusterDimension; 和 attribute[0].val.clusterDim.x = 2; 等设置集群维度。cudaLaunchKernelEx(&config, (void*)clusterKernel, params);extern __shared__ int smem[];,cg::cluster_group cluster = cg::this_cluster();,unsigned int cluster_size = cluster.dim_blocks().x;smem[i] = 0;。cluster.sync();加载输入数据并找到直方图桶ID:
int dst_block_rank = (int)(binid / bins_per_block);int dst_offset = binid % bins_per_block;atomicAdd(&sh_hist[dst_block_rank] + dst_offset, 1);集群同步: cluster.sync();
// Perform global memory reductionscuda::memcpy_async硬件加速: 1D到5D张量内存拷贝。
硬件加速: 1D或元素级存储和规约。
关键机制: 使用异步事务屏障来信号数据传输完成。
异步屏障 (Ampere) 模型: 生产者/消费者模型。
屏障分为两步:
Arrive是非阻塞的: 等待时,提前到达的线程可以执行独立工作。
Async事务屏障 (Hopper新特性):
Arrival_count。Transaction_count。等待机制: 只有当 Arrival_count 和 Transaction_count 都达到预期值时才阻塞等待。
cuda::memcpy_async 单边数据交换的构建块。本节介绍异步 SIMT 编程,特别是 cuda::memcpy_async Global <-> Shared 的应用。它强调使用异步事务屏障(Asynchronous Transaction Barrier)来发出完成信号,实现完全异步的线程操作。
上图展示了 H100 SM 中异步事务屏障的工作流程:
1. Arrive (到达):一个事务屏障被初始化,设置 block.size() 和 sizeof(int) * block.size()。
2. Transaction (事务):一个事务被启动,例如通过 cuda::memcpy_async 进行数据拷贝。
3. Wait (等待):等待所有事务完成。
图中的代码片段展示了如何初始化合作线程组、设置屏障,并异步执行 memcpy 操作,最后通过 barrier.wait() 等待完成。
cuda::memcpy_async 全局 <-> 共享初始化: 屏障和块大小的初始化。
block.size() 定义预期计数。init(&abarrier, block.size()); 初始化屏障。GPU组件交互: 异步事务屏障与H100 SM中的共享内存、寄存器、L1缓存、TMA单元以及GPU内存交互。
cuda::memcpy_async(block, smem, gmem, block.size() * sizeof(int), // int/thread barrier);auto token = barrier.arrive(); 线程完成数据发送后,调用 arrive()。arrive() 后,线程可以立即执行独立工作,而无需等待数据传输完成。Stencil 代码是一种常见的 GPU 计算模式,其典型步骤包括:
__syncthreads() 进行同步。上图展示了典型的 Stencil 算子结构,包括中心(Center)和四周的 Halo 区域(X-, X+, Y-, Y+)。
在没有使用 TMA(Tensor Memory Access)的情况下加载 Stencil 数据,需要大量的测试来检查要加载到共享内存中的数据是否存在,如果不存在则用零填充。
上图的 C++ 代码示例显示了在加载 tile 和 halo 区域时,需要通过 if (idx < nx && sy < ny) 等条件判断来处理边界情况,这导致了代码的复杂性。
通过使用张量描述符(Tensor Descriptor)的 2D TMA 加载,可以简化 Stencil 代码。
张量描述符提供:
* 维度 NX, NY
* Y 维度步长 LDIMX >= NX
* 共享内存框大小
* 基指针
每个块可以使用以下方式加载数据:
* 张量描述符
* (X,Y) 偏移
上图解释了张量描述符如何定义数据区域,包括 NX, NY 尺寸和 LDIMX 步长,以及如何通过 Box size X 和 Box size Y 来定义加载范围。
TMA 的优势:
* TMA 可以通过一条指令加载整个共享内存区域。
* 单个线程可以向共享内存加载数十 KB 的数据。
* 如果数据不存在,TMA 会自动进行零填充。
* 使用标准的 cuda::barrier 同步,实现完全异步。
上图对比了传统 Stencil 区域(左)与 TMA 实现的单次加载(右),表明 TMA 能将复杂的加载操作简化为一次。
通过 TMA 简化数据加载。
与 Page 33 所示的复杂边界检查代码相比,使用 TMA 可以显著简化代码。Page 37 通过红叉强调了这部分复杂代码的移除。
使用 TMA 后,不再需要大量的边界检查代码,因为 TMA 提供了自动零填充。
上图中的伪代码显示,复杂的条件判断和手动加载被简化为一行 memcpy_async 调用,同时图示强调了 TMA 的“自动零填充”功能。
通过计算每个线程 4 x 4 个点而不是一个点,可以改进 Stencil 代码的性能。
上图展示了 Stencil 算子从单个中心点扩展到 4 x 4 区域的计算方式。
TMA 对 Stencil 代码性能的影响:
* 没有 TMA: 需要加载更多数据并进行适当的边界检查,导致大量的测试和加载指令。
* 使用 TMA: 张量描述符只需进行微小改动(更大的框尺寸),可以将重点放在 Stencil 计算本身。
全局内存、共享内存和 TMA 过滤实现器的相对性能:
上图对比了不同过滤半径下,TMA、共享内存 (Smem) 和全局内存 (Gmem) 的性能(相对 Smem 为 1.0x,更高表示更好):
* 小半径过滤器 (2):
* TMA: 1.04x
* Smem: 1.0x
* Gmem: 1.06x
结论: TMA 代码更简单,指令更少,性能更高。
3D TTI 逆时偏移是一个计算密集型、寄存器占用高的地震应用。它已经通过 cuda::memcpy_async (GTC'21 A31115) 进行了加速。
进一步使用 TMA:
上图展示了使用 TMA 的 3D TTI 逆时偏移循环流程:包括 TMA 加载模型、等待、计算 Y/Z 驱动、同步、TMA 加载下一数据、计算其他驱动、等待、TMA 写入结果等步骤。
使用 TMA 带来的改进:
* 寄存器使用率降低 25%。
* 占用率提高 66%。
* 性能提升 1.65x。
Hopper 架构引入了新的异步事务屏障,支持“数据到达跟踪”。
上图展示了新的异步事务屏障流程:
1. 计算值并存储结果:线程递增 Arrival_count。
2. Arrive (到达) & Transaction (事务):异步存储到共享内存,递增 Transaction_count。
3. 独立工作 (Independent Work)。
4. Wait (等待):等待直到 Arrival_count 和 Transaction_count 都达到预期值。
5. 处理结果 (Process results)。
这是 cuda::memcpy_async 生产者/消费者模型中单向数据交换的构建块。
cuda::memcpy_async 共享内存 <-> 共享内存在使用 cuda::memcpy_async Shared <-> Shared 进行生产者/消费者编程时,需要用生产者和消费者线程计数来初始化屏障。
上图展示了生产者(线程块 0,2 个线程)和消费者(线程块 1,2 个线程)通过共享内存进行交互的设置。屏障被初始化,例如“预期到达计数 = 4”,而“预期事务计数”在开始时为 0。
消费者线程抵达并等待生产者提供数据。图中展示了线程块0(生产者,2个线程)和线程块1(消费者,2个线程)的初始状态。消费者线程在各自的共享内存中调用 bar.arrive() 获取令牌,并随后调用 barrier.wait(token) 等待屏障解除。
生产者线程使用 memcpy_async 向消费者的共享内存发起异步内存拷贝操作,同时更新消费者屏障的状态。
图中展示了更多的 memcpy_async 操作,以及生产者线程0通过 barrier_arrive 抵达,进一步更新了消费者屏障的预期事务计数。
生产者线程1也执行了 memcpy_async 和 barrier_arrive 操作,使得预期事务计数达到4。
当所有线程和数据都抵达后,屏障解除阻塞。图中显示预期抵达计数和预期事务计数均达到0,表示屏障已成功同步。
这是一种定量金融中的定价技术。
Longstaff Schwartz 定价模型将问题空间可视化为一个三维结构,包含“资产”、“路径”和“到期时间”维度。
对于每个资产,模型会沿着时间维度进行逆向传播(Propagation (reverse in time)),计算并更新现金流(Cash flows)。这个过程通过迭代完成。
Longstaff Schwartz 定价模型的计算流程包括以下步骤:
1. 初始化 (Initialization)
2. 跨N路径的归约 (Reduction across N paths)
3. 伪逆计算 (Pseudo-inverse computation)
4. 现金流更新 (Cash flow update)
5. 现值 (Present value)
其中,步骤2-4在一个时间步长的迭代中重复进行。
当前实现存在内存限制:
* 仅 25,000 条路径可以放入共享内存,这可能太低了。
* 对于金融领域一个相关的用例,如果需要处理 100,000 条路径,当前的实现会将现金流存储在全局内存中。
* 评估 1260 个时间步,将启动 3780 个核函数(kernels)。
在 H100 上的 Longstaff Schwartz 定价模型的集群实现中,处理 100,000 条路径时,每个现金流数组为 800KB。这无法在一个块的共享内存中容纳,但可以容纳在 4 个块中。
整个集群处理流程如下:
* Thread Block 0, 1, 2, 3 各自执行对其 N/4 路径的归约 (Reduction across N/4 paths)。
* 归约结果汇集到 Thread Block 0 的线程 0 (Reduce to Thread 0, Block 0)。
* 通过 cluster.sync() 进行集群同步。
* 执行伪逆计算并分散值 (Pseudo-Inverse computation. Scatter values)。
* 再次通过 cluster.sync() 进行集群同步。
* 最终,各个线程块更新现金流 (Cash flow update)。
在评估 1260 个时间步时,这种方法只需启动 1 个内核,而传统方法可能需要 3780 个内核。
通过使用单边屏障通信(one-sided barrier communication)而非完整的集群同步,可以优化流程。
优化后的流程如下:
* Thread Block 0, 1, 2, 3 各自执行对其 N/4 路径的归约。
* 归约结果汇集到 Thread Block 0 的线程 0。
* Thread Block 0 执行 barrier.wait()。
* 在 1260 个时间步的循环中:
* Thread Block 0 执行伪逆计算并分散值。
* 所有线程块执行 barrier.wait()。
* 所有线程块更新现金流。
通过对 Longstaff Schwartz 定价模型的性能测试,结果显示:
* 未采用集群的 H100 (H100 w/o clusters) 作为基准。
* 采用集群并使用 cluster.sync() (H100 w/ Cluster + cluster.sync()) 的吞吐量达到基准的 2.7 倍。
* 采用集群并使用单边屏障 (H100 w/ Cluster + Barrier) 的吞吐量进一步提升 10%。
Hopper 架构的关键特性包括:
线程块集群 (Thread Block Cluster)
异步 SIMT 编程模型 (Asynchronous SIMT Programming Model)
cuda::memcpy_async 在所有方向上都得到硬件加速。
在 CUDA 内核级别实现内存操作和计算的重叠。
另请参阅: