本节书摘来自华章计算机《CUDA C编程权威指南》一书中的第3章,第3.1节,作者 [美] 马克斯·格罗斯曼(Max Grossman),译 颜成钢 殷建 李亮,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
第3章
CUDA执行模型
本章内容:
- 通过配置文件驱动的方法优化内核
- 理解线程束执行的本质
- 增大GPU的并行性
- 掌握网格和线程块的启发式配置
- 学习多种CUDA的性能指标和事件
- 了解动态并行与嵌套执行
通过上一章的练习,你已经学会了如何在网格和线程块中组织线程以获得最佳的性能。尽管可以通过反复试验找到最佳的执行配置,但你可能仍然会感到疑惑,为什么选择这样的执行配置会更好。你可能想知道是否有一些选择网格和块配置的准则。本章将会回答这些问题,并从硬件方面深入介绍内核启动配置和性能分析的信息。
3.1 CUDA执行模型概述
一般来说,执行模型会提供一个操作视图,说明如何在特定的计算架构上执行指令。CUDA执行模型揭示了GPU并行架构的抽象视图,使我们能够据此分析线程的并发。在第2章里,已经介绍了CUDA编程模型中两个主要的抽象概念:内存层次结构和线程层次结构。它们能够控制大规模并行GPU。因此,CUDA执行模型能够提供有助于在指令吞吐量和内存访问方面编写高效代码的见解。
在本章会重点介绍指令吞吐量,在第4章和第5章里会介绍更多的关于高效内存访问的内容。
3.1.1 GPU架构概述
GPU架构是围绕一个流式多处理器(SM)的可扩展阵列搭建的。可以通过复制这种架构的构建块来实现GPU的硬件并行。
图3-1说明了Fermi SM的关键组件:
- CUDA核心
- 共享内存/一级缓存
- 寄存器文件
- 加载/存储单元
- 特殊功能单元
- 线程束调度器
GPU中的每一个SM都能支持数百个线程并发执行,每个GPU通常有多个SM,所以在一个GPU上并发执行数千个线程是有可能的。当启动一个内核网格时,它的线程块被分布在了可用的SM上来执行。线程块一旦被调度到一个SM上,其中的线程只会在那个指定的SM上并发执行。多个线程块可能会被分配到同一个SM上,而且是根据SM资源的可用性进行调度的。同一线程中的指令利用指令级并行性进行流水线化,另外,在CUDA中已经介绍了线程级并行。
CUDA采用单指令多线程(SIMT)架构来管理和执行线程,每32个线程为一组,被称为线程束(warp)。线程束中的所有线程同时执行相同的指令。每个线程都有自己的指令地址计数器和寄存器状态,利用自身的数据执行当前的指令。每个SM都将分配给它的线程块划分到包含32个线程的线程束中,然后在可用的硬件资源上调度执行。
SIMT架构与SIMD(单指令多数据)架构相似。两者都是将相同的指令广播给多个执行单元来实现并行。一个关键的区别是SIMD要求同一个向量中的所有元素要在一个统一的同步组中一起执行,而SIMT允许属于同一线程束的多个线程独立执行。尽管一个线程束中的所有线程在相同的程序地址上同时开始执行,但是单独的线程仍有可能有不同的行为。SIMT确保可以编写独立的线程级并行代码、标量线程以及用于协调线程的数据并行代码。
SIMT模型包含3个SIMD所不具备的关键特征。
- 每个线程都有自己的指令地址计数器
- 每个线程都有自己的寄存器状态
- 每个线程可以有一个独立的执行路径
一个线程块只能在一个SM上被调度。一旦线程块在一个SM上被调度,就会保存在该SM上直到执行完成。在同一时间,一个SM可以容纳多个线程块。
图3-2从逻辑视图和硬件视图的角度描述了CUDA编程对应的组件。
在SM中,共享内存和寄存器是非常重要的资源。共享内存被分配在SM上的常驻线程块中,寄存器在线程中被分配。线程块中的线程通过这些资源可以进行相互的合作和通信。
尽管线程块里的所有线程都可以逻辑地并行运行,但是并不是所有线程都可以同时在物理层面执行。因此,线程块里的不同线程可能会以不同的速度前进。
在并行线程中共享数据可能会引起竞争:多个线程使用未定义的顺序访问同一个数据,从而导致不可预测的程序行为。CUDA提供了一种用来同步线程块里的线程的方法,从而保证所有线程在进一步动作之前都达到执行过程中的一个特定点。然而,没有提供块间同步的原语。
尽管线程块里的线程束可以任意顺序调度,但活跃的线程束的数量还是会由SM的资源所限制。当线程束由于任何理由闲置的时候(如等待从设备内存中读取数值),SM可以从同一SM上的常驻线程块中调度其他可用的线程束。在并发的线程束间切换并没有开销,因为硬件资源已经被分配到了SM上的所有线程和块中,所以最新被调度的线程束的状态已经存储在SM上。
在下一节,将会介绍NVIDIA中的两个GPU架构:Fermi架构和Kepler架构,重点介绍它们的硬件资源。你将会通过示例和练习来学习它们的硬件特征,这有助于提高对内核性能的理解。
3.1.2 Fermi架构
Fermi架构是第一个完整的GPU计算架构,能够为大多数高性能计算应用提供所需要的功能。Fermi已经被广泛应用于加速生产工作负载中。
图3-3所示为Fermi架构的逻辑框图,其重点是GPU计算,它在很大程度上忽略了图形具体组成部分。Fermi的特征是多达512个加速器核心,这被称为CUDA核心。每个CUDA核心都有一个全流水线的整数算术逻辑单元(ALU)和一个浮点运算单元(FPU),在这里每个时钟周期执行一个整数或是浮点数指令。CUDA核心被组织到16个SM中,每一个SM含有32个CUDA核心。Fermi架构有6个384位的GDDR5 DRAM存储器接口,支持多达6GB的全局机载内存,这是许多应用程序关键的计算资源。主机接口通过PCIe总线将GPU与CPU相连。GigaThread引擎(图示左侧第三部分)是一个全局调度器,用来分配线程块到SM线程束调度器上。
Fermi架构包含一个耦合的768 KB的二级缓存,被16个SM所共享。在图3-3中,一个垂直矩形条表示一个SM,包含了以下内容:
- 执行单元(CUDA核心)
- 调度线程束的调度器和调度单元
- 共享内存、寄存器文件和一级缓存
每一个多处理器有16个加载/存储单元(如图3-1所示),允许每个时钟周期内有16个线程(线程束的一半)计算源地址和目的地址。特殊功能单元(SFU)执行固有指令,如正弦、余弦、平方根和插值。每个SFU在每个时钟周期内的每个线程上执行一个固有指令。
每个SM有两个线程束调度器和两个指令调度单元。当一个线程块被指定给一个SM时,线程块中的所有线程被分成了线程束。两个线程束调度器选择两个线程束,再把一个指令从线程束中发送到一个组上,组里有16个CUDA核心、16个加载/存储单元或4个特殊功能单元(如图3-4所示)。Fermi架构,计算性能2.x,可以在每个SM上同时处理48个线程束,即可在一个SM上同时常驻1 536个线程。
Fermi架构的一个关键特征是有一个64KB的片内可配置存储器,它在共享内存与一级缓存之间进行分配。对于许多高性能的应用程序,共享内存是影响性能的一个关键因素。共享内存允许一个块上的线程相互合作,这有利于芯片内数据的广泛重用,并大大降低了片外的通信量。CUDA提供了一个运行时API,它可以用来调整共享内存和一级缓存的数量。根据给定的内核中共享内存或缓存的使用修改片内存储器的配置,可以提高性能。这一部分内容将会在第4章和第5章详细介绍。
Fermi架构也支持并发内核执行:在相同的GPU上执行相同应用程序的上下文中,同时启动多个内核。并发内核执行允许执行一些小的内核程序来充分利用GPU,如图3-5所示。Fermi架构允许多达16个内核同时在设备上运行。从程序员的角度看,并发内核执行使GPU表现得更像MIMD架构。
3.1.3 Kepler架构
发布于2012年秋季的Kepler GPU架构是一种快速、高效、高性能的计算架构。Kepler的特点使得混合计算更容易理解。图3-6表示了Kepler K20X芯片框图,它包含了15个SM和6个64位的内存控制器。以下是Kepler架构的3个重要的创新。
- 强化的SM
- 动态并行
- Hyper-Q技术
Kepler K20X的关键部分是有一个新的SM单元,其包括一些结构的创新,以提高编程效率和功率效率。每个Kepler SM单元包含192个单精度CUDA核心,64个双精度单元,32个特殊功能单元(SFU)以及32个加载/存储单元(LD/ST)(如图3-7所示)。
每个Kepler SM包括4个线程束调度器和8个指令调度器,以确保在单一的SM上同时发送和执行4个线程束。Kepler K20X架构(计算能力3.5)可以同时在每个SM上调度64个线程束,即在一个SM上可同时常驻2048个线程。K20X架构中寄存器文件容量达到64KB,Fermi架构中只有32KB。同时,K20X还允许片内存储器在共享内存和一级缓存间有更多的分区。K20X能够提供超过1TFlop的峰值双精度计算能力,相较于Fermi的设计,功率效率提高了80%,每瓦的性能也提升了三倍。
动态并行是Kepler GPU的一个新特性,它允许GPU动态启动新的网格。有了这个特点,任一内核都能启动其他的内核,并且管理任何核间需要的依赖关系来正确地执行附加的工作。这一特点也让你更容易创建和优化递归及与数据相关的执行模式。如图3-8所示,它展示了没有动态并行时主机在GPU上启动每一个内核时的情况;有了动态并行,GPU能够启动嵌套内核,消除了与CPU通信的需求。动态并行拓宽了GPU在各种学科上的适用性。动态地启动小型和中型的并行工作负载,这在以前是需要很高代价的。
Hyper-Q技术增加了更多的CPU和GPU之间的同步硬件连接,以确保CPU核心能够在GPU上同时运行更多的任务。因此,当使用Kepler GPU时,既可以增加GPU的利用率,也可以减少CPU的闲置时间。Fermi GPU依赖一个单一的硬件工作队列来从CPU到GPU间传送任务,这可能会导致一个单独的任务阻塞队列中在该任务之后的所有其他任务。Kepler Hyper-Q消除了这个限制。如图3-9所示,Kepler GPU在主机与GPU之间提供了32个硬件工作队列。Hyper-Q保证了在GPU上有更多的并发执行,最大限度地提高了GPU的利用并提高了整体的性能。
表3-1简要地总结了主要架构特点的不同计算能力。
3.1.4 配置文件驱动优化
性能分析是通过检测来分析程序性能的行为:
- 应用程序代码的空间(内存)或时间复杂度
- 特殊指令的使用
- 函数调用的频率和持续时间
性能分析是程序开发中的关键一步,特别是对于优化HPC应用程序代码。性能分析往往需要对平台的执行模型有一个基本的理解以制定应用程序的优化方法。开发一个HPC应用程序通常包括两个主要步骤:
- 提高代码的正确性。
- 提高代码的性能。
对于第二步,使用配置文件驱动的方法是很自然的。配置文件驱动的发展对于CUDA编程尤为重要,原因主要有以下几个方面。
- 一个单纯的内核应用一般不会产生最佳的性能。性能分析工具能帮助你找到代码中影响性能的关键部分,也就是性能瓶颈。
- CUDA将SM中的计算资源在该SM中的多个常驻线程块之间进行分配。这种分配形式导致一些资源成为了性能限制者。性能分析工具能帮助我们理解计算资源是如何被利用的。
- CUDA提供了一个硬件架构的抽象,它能够让用户控制线程并发。性能分析工具可以检测和优化,并将优化可视化。
性能分析工具深入洞察内核的性能,检测核函数中影响性能的瓶颈。CUDA提供了两个主要的性能分析工具:nvvp,独立的可视化分析器;nvprof,命令行分析器。
nvvp是可视化分析器,它可以可视化并优化CUDA程序的性能。这个工具会显示CPU与GPU上的程序活动的时间表,从而找到可以改善性能的机会。此外,nvvp可以分析应用程序潜在的性能瓶颈,并给出建议以消除或减少这些瓶颈。该工具既可作为一个独立的应用程序,也可作为Nsight Eclipse Edition (nsight)的一部分。
nvprof在命令行上收集和显示分析数据。nvprof是和CUDA 5一起发布的,它是从一个旧的命令行CUDA分析工具进化而来的。跟nvvp一样,它可以获得CPU与GPU上CUDA关联活动的时间表,其中包括内核执行、内存传输和CUDA的API调用。它也可以获得硬件计数器和CUDA内核的性能指标。
除了预定义的指标,还可以利用基于分析器获得的硬件计数器来自定义指标。
选择合适的性能指标以及将检测性能与理论峰值性能进行对比对于寻找内核的性能瓶颈是很重要的。在本书的示例和练习中,你将了解用命令行分析器分析内核的适当指标,以及掌握使用配置文件驱动的方法来编写高效的核函数的技巧。
在本书中主要使用nvprof来提高内核性能。本书还介绍了如何选择合适的计数器和指标,并使用命令行中的nvprof来收集分析数据,以便用于设计优化策略。你还将会学习如何使用不同的计数器和指标,从多个角度分析内核。
有3种常见的限制内核性能的因素:
- 存储带宽
- 计算资源
- 指令和内存延迟
本章主要介绍指令延迟的问题,其次会介绍一些计算资源限制的问题。后续章节将讨论其余的性能限制因素。