CUDA Techniques to Maximize Compute and Instruction Throughput [S72685]

Ben Pinzone, Compute Developer Technology Engineer
David Clark, Compute Developer Technology Engineer
GTC 2025, March 17th, 2025

目录

目标 (Goals)

议程 (Agenda)

GPU 线程层级结构、SIMT、线程束分化 (Warp divergence)


GPU 概览

NVIDIA H100 SXM

Page 5: NVIDIA H100 SXM 架构图
Page 5: NVIDIA H100 SXM 架构图

该图展示了 NVIDIA H100 SXM GPU 的整体架构,主要组件包括:
- 132 个 SMs (Streaming Multiprocessors)第四代 Tensor Cores
- PCIe Gen 5:128 GB/s 双向带宽。
- 50 MB L2 缓存
- 80 GB HBM3:3.3 TB/s 双向带宽。
- 第四代 NVLink:900 GB/s 双向带宽。

流式多处理器 (Streaming Multiprocessor - SM)

Hopper 架构

Page 6: Hopper SM 架构图
Page 6: Hopper SM 架构图

Hopper 架构的 SM 包含以下特性:
- SM 拥有 4 个子分区 (sub-partitions)。
- 128 个 FP32 单元。
- 64 个 FP64 单元。
- 64 个 INT32 单元。
- 4 个混合精度 Tensor Cores。
- 16 个特殊功能单元 (special function units, 用于超越函数)。
- 4 个线程束调度器 (warp schedulers)。
- 32 个加载/存储 (LD/ST) 单元。
- 64k 个 32-bit 寄存器。
- 256 KiB 统一 L1 数据缓存和共享内存。
- Tensor 内存加速器 (Tensor Memory Accelerator - TMA)。

线程层级结构:网格 (Grid) 与线程块 (Blocks)

Page 7: 线程层级结构-网格与线程块示意图
Page 7: 线程层级结构-网格与线程块示意图

线程层级结构:集群 (Clusters)

Page 8: 线程块集群示意图
Page 8: 线程块集群示意图

线程层级结构:线程束 (Warps)

Page 9: 线程束概念示意图
Page 9: 线程束概念示意图
<blockquote>

"编织,第一种并行技术" - CUDA C++ 编程指南

</blockquote>

SIMT 架构

单指令,多线程 (Single-Instruction, Multiple-Thread)

Page 10: SIMT 架构示意图
Page 10: SIMT 架构示意图

在共享内存中进行工作排队 (Work Queueing in shared memory)

对于某些工作负载,其昂贵的计算由一个轻量级的检查来守护。一个简单的实现可能会因为并非所有线程都能通过该检查而遭受高度的分歧。

解决方案:

该过程分为两个阶段:
1. 侦察阶段 (Scouting phase): 线程块中的线程并行地寻找工作。当某些线程(图中以绿色'x'标记)找到有效工作时,它们会将工作项添加到共享内存中的队列。
2. 处理阶段 (Process phase): 当队列中累积的工作项达到一定数量(Invoke process size)时,整个线程块会切换到处理阶段,共同从队列中取出并执行这些工作项。这确保了在处理阶段所有线程都在执行相同的任务,从而避免了分歧。

Page 16: 工作排队的侦察阶段
Page 16: 工作排队的侦察阶段

线程束调度器 (Warp scheduler) 和内核性能分析概览

Warp调度器统计数据:性能分析的思维模型

调度器运行的可视化

以下几页通过一个简化的、逐周期的示例,构建了一个用于理解 Warp 调度器行为和相关性能指标的思维模型。

Warp 状态定义:
- Unused (未使用): 空闲的 Warp 插槽。
- Active (活动): Warp 已被分配到该插槽。
- Stalled (停滞): 活动的 Warp 正在等待某个依赖项(例如,内存读取、指令执行完成),当前周期无法被调度。
- Eligible (符合条件): 活动的 Warp 已准备好执行下一条指令。
- Selected (已选择): 在当前周期,从所有符合条件的 Warp 中被调度器选中并发射指令的 Warp。

周期 N:
在一个给定的周期(Cycle N)中,调度器从所有符合条件的 Warp(图中浅绿色块)中选择一个(图中带斜线的浅绿色块)来发射(Issue)其指令。

Page 31: Warp调度器在周期N的状态
Page 31: Warp调度器在周期N的状态

周期 N+1:
调度器选择下一个符合条件的 Warp(插槽4)。上一个周期被选择的 Warp(插槽3)现在变为停滞状态,因为它正在等待指令完成,因此不符合被调度的条件。(当然,如果它有其他独立的指令可以执行,情况可能会有所不同)。

Page 32: Warp调度器在周期N+1的状态
Page 32: Warp调度器在周期N+1的状态

周期 N+2:
位于插槽4的 Warp 执行完毕并退出。此时,没有任何符合条件的 Warp,因此调度器的发射单元(Issue Slot)在本周期处于空闲状态,未发射任何指令。

Page 33: Warp调度器在周期N+2的状态
Page 33: Warp调度器在周期N+2的状态

周期 N+3:
新的 Warp 被调度到之前空闲的插槽中。调度器从新的符合条件的 Warp 中选择一个(插槽2)并发射指令。

Page 34: Warp调度器在周期N+3的状态
Page 34: Warp调度器在周期N+3的状态

聚合性能指标

基于以上4个周期的示例,我们可以计算出一系列聚合的性能指标。

Page 35: 4个周期的Warp调度器状态总览
Page 35: 4个周期的Warp调度器状态总览

1. cycles_active (活动周期数): 4
- 观察到的

延迟绑定内核:调度器统计

每个调度器一个 warp 的情况下的度量(报告中的数据,已四舍五入):

Page 46
Page 46

上图显示了调度器发出指令的摘要。每个调度器维护一个可以发出指令的 warp 池。理论上,池的上限受启动配置的限制。在每个周期,调度器检查池中已分配 warp 的状态(Active Warps)。未停滞的 warp(Eligible Warps)准备好发出它们的下一条指令。调度器从合格 warp 集合中选择一个 warp 来发出一或多条指令(Issued Warps)。在没有合格 warp 的周期中,发出槽被跳过,没有指令发出。许多被跳过的发出槽表明存在延迟隐藏问题。

延迟隐藏 (Latency hiding) 和提升指令吞吐量

延迟绑定内核:改进方案

GPU通过并行处理大量任务(in-flight work)来掩盖延迟。增加Warp的数量可以隐藏延迟。

下图展示了通过增加Warp数量来填充指令流水线,避免了因单个Warp停滞(Stalled)而导致的“无Warp分发”(No warp issuing)的空闲周期。当Warp 0停滞时,调度器可以选择Warp 1或Warp 2来执行,从而保持硬件的繁忙状态。

利特尔定律(Little's Law)在此处的应用描述了我们需要多少在执行中的指令(instructions in flight)才能避免暴露延迟。

Page 61
Page 61

延迟绑定内核:通过增加指令级并行性改进

另一种隐藏延迟的方法是增加指令级并行性(Instruction Level Parallelism, ILP)。

如下面的代码和图表所示,通过使用float2代替float,在同一个循环迭代中执行两个独立的操作(result.x += 3.14f;result.y += 3.14f;)。这使得即使在一个Warp内部,当一条指令等待时(例如,等待前一指令的结果),硬件也可以调度执行另一条独立的指令,从而减少停滞,提高执行效率。

Page 62
Page 62

关于停滞(Stalling)的底线

  1. 如果SM或内存系统资源已经繁忙,则无需担心停滞或未使用的分发槽。

    • 更频繁地分发指令没有帮助,因为资源已经饱和。
  2. 否则,你的程序就是延迟绑定的。需要为硬件提供更多的并发工作。可以尝试以下方法:

    • 更频繁地分发指令。
    • 减少停滞的频率。
    • 在停滞期间处理其他事务。
    • 减少停滞的持续时间(例如,使用延迟更低的指令)。

占用率(Occupancy):是什么?如何确定?

<blockquote>

可以使用NVIDIA Nsight Compute分析CUDA内核的占用率。

</blockquote>

占用率详解

占用率可以用一个“发牌”的比喻来理解:线程块(Blocks)就像牌,它们有特定的资源需求(如共享内存、寄存器)。发牌官(SM)在有足够资源服务它们的情况下,将线程块并发地分发出去。

Page 65
Page 65

占用率限制器:寄存器

超出寄存器限制

<blockquote>

优化技巧
有限的本地内存使用可能对性能有益。

</blockquote>

下图显示了大量的本地内存请求(3.70M Req),这些请求被发送到L1/TEX缓存。

Page 67
Page 67

如何在NCU中定位寄存器压力

在NVIDIA Nsight Compute (NCU) 中,可以通过查看源代码视图和SASS(汇编代码)视图中的“Live Registers”(活跃寄存器)列来识别寄存器压力大的

减少指令数量并使吞吐量有效

数学运算的成本层级

使用尽可能轻量级的工具。数学运算的成本从高到低排列如下:

Page 76
Page 76

仅使用你的应用所需的精度

Page 77
Page 77

代数优化

静态考量

运行时考量

Page 78
Page 78

小改动可能产生大影响

无符号整数溢出是已定义行为,编译时需要考虑这一点,可能导致额外的指令。有符号整数溢出是未定义行为,这为编译器生成更快的代码提供了更大的灵活性。

优化技巧:使用有符号整数而不是无符号整数作为循环计数器。

下面的代码和图表展示了使用 unsigned intint 作为循环计数器的性能差异。

Page 79
Page 79

示例:线段相交

给定一个线段数组,计算有多少交点发生。

Page 80
Page 80

计算受限于除法慢速路径

与C语言除法运算符相关的SASS(汇编)调用了一个注入的辅助SASS(函数)。

Page 81
Page 81

线段相交示例:可能的优化

  1. 修改源码以避免除法。
  2. 改变算法以避免除法。
  3. 执行包围盒预检查。
  4. 在不同的浮点数范围上操作以避免慢速路径。
Page 82
Page 82

优化选项 1: 修改源码以避免除法

观察除法结果的使用方式。

Page 83
Page 83

优化选项 2: 采用不同算法以避免除法

新算法:线段分割二维平面。

Page 84
Page 84

优化选项 3: 添加包围盒预检查

这是一种轻量级的预检查:在运行任何相交测试之前,检查两个线段的包围盒是否重叠。

Page 85
Page 85

优化选项 4: 设法改变你的输入数据

Page 86
Page 86

线段相交结果 (A100)

下表展示了在不同浮点数量级、不同实现和优化策略下的运行时间。

Page 87
Page 87

优化多项式求值

本节讨论在编译时已知常数的情况下,重复计算高阶多项式的优化方法。

替换幂函数调用

通过将 pow 函数调用替换为运行中的指数计算来优化。初始基线使用 pow 函数,运行时间为 40,862 us。通过手动展开计算,避免调用通用函数,性能得到显著提升。

Page 91 - 避免通用数学函数
Page 91 - 避免通用数学函数

优化技巧:尽可能优先使用更快、更专业的数学函数,而不是更慢、更通用的函数。


采用霍纳(Horner)方法

使用霍纳方法可以减少指令数量,进一步优化多项式求值。该方法通过重构表达式来减少乘法操作。

Page 92 - 霍纳方法减少指令数
Page 92 - 霍纳方法减少指令数

优化技巧:检查表达式是否可以被重构以产生更少的指令。


使用融合乘加(Fused Multiply-Add)指令

通过使用融合乘加(FMA)指令,可以将乘法和加法操作合并为一条指令,从而提高性能。这可以通过编译器选项 fmad=true 或使用内部函数(intrinsics)如 __fmaf_rn 来实现。

Page 93 - 使用融合乘加指令
Page 93 - 使用融合乘加指令

优化技巧:在可能的情况下使用融合乘加指令。


增加指令级并行度(ILP)

霍纳方法会产生很少的指令级并行性(ILP),因为它创建了一个长的依赖指令链。当需要 ILP 时,可以采用 Estrin 方案。Estrin 方案可以用于添加指令序列化,而开销很小,因为它将多项式分解为两个子多项式,然后用霍纳方法分别求值。

下表比较了在不同并行度(通过每个 SM 的线程块数来体现)下霍nor方法和Estrin方案的性能。
- 在高并行度(8个线程块/SM)下,两种方案性能相近。
- 在低并行度(1个线程块/SM)下,Estrin方案(192 us)由于其更高的ILP,性能优于霍纳方法(326 us)。

Page 94 - Estrin方案提升ILP
Page 94 - Estrin方案提升ILP

优化技巧:如果需要指令级并行度(ILP),请打破依赖指令链。


Tensor Core 总结

Page 95 - Tensor Core 总结
Page 95 - Tensor Core 总结

矩阵乘法:深度学习中的关键操作

矩阵乘法是深度学习中的核心运算。在全连接层中,输入(inputs)乘以权重(weights)得到输出(outputs),这本质上是一个矩阵乘法操作。批处理维度(batch dimension)使输入成为一个二维矩阵。

Tensor Core 是专门用于矩阵乘法运算的硬件流水线。

Page 96 - 矩阵乘法示意图
Page 96 - 矩阵乘法示意图

示例:HMMA 1688

HMMA 1688 是一个 FP16 16x8x8 的 Tensor Core 操作示例。

Page 97 - HMMA 1688 操作示例
Page 97 - HMMA 1688 操作示例

Tensor Core 的历史与特性

Tensor Core 随着 NVIDIA GPU 架构的演进而不断发展,在数据中心和消费级 GPU 上均有部署。

Page 103 - Tensor Core 发展历程
Page 103 - Tensor Core 发展历程

注:峰值 FP16 Flops 数据均基于非稀疏计算。


GPU 特定指令

Tensor Core 指令通常是不可移植的,除非它们基于 Ampere SuperMMA API。为了更好的硬件抽象,推荐使用更高层次的 API,如 CUTLASS。

指令集演进:

Page 104 - GPU 特定指令演进
Page 104 - GPU 特定指令演进

Tensor Core 的使用者(Providers)

开发者可以在不同抽象层次上使用 Tensor Core,从低级 API 到高级 API。

Page 105 - Tensor Core 使用者层级
Page 105 - Tensor Core 使用者层级

Tensor Core APIs

编写您自己的 Tensor Core 内核

Page 106
Page 106

问答

+ GTC 2025 更多 CUDA 开发者会议

更多信息请访问:http://nvidia.com/gtc/sessions/cuda-developer

Page 107
Page 107