GPU与CPU部署大模型的区别:CUDA执行模型和GPU架构详解
GPU与CPU部署大模型的区别:CUDA执行模型和GPU架构详解
CUDA执行模型揭示了GPU并行架构的抽象视图,使我们能够据此分析线程的并发。GPU架构是围绕一个流式多处理器(SM)的可扩展阵列搭建的。本文将重点介绍GPU的执行模型、架构特点以及性能优化方法。
GPU架构概述
GPU架构是围绕一个流式多处理器(SM)的可扩展阵列搭建的。可以通过复制这种架构的构建块来实现GPU的硬件并行。
CUDA采用单指令多线程(SIMT)架构来管理和执行线程,每32个线程为一组,被称为线程束(warp)。线程束中的所有线程同时执行相同的指令。每个线程都有自己的指令地址计数器和寄存器状态,利用自身的数据执行当前的指令。每个SM都将分配给它的线程块划分到包含32个线程的线程束中,然后在可用的硬件资源上调度执行。
Fermi架构
Fermi架构是第一个完整的GPU计算架构,能够为大多数高性能计算应用提供所需要的功能。Fermi已经被广泛应用于加速生产工作负载中。
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架构。
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在各种学科上的适用性。动态地启动小型和中型的并行工作负载,这在以前是需要很高代价的。
配置文件驱动优化
性能分析是通过检测来分析程序性能的行为:
- 应用程序代码的空间(内存)或时间复杂度
- 特殊指令的使用
- 函数调用的频率和持续时间
性能分析是程序开发中的关键一步,特别是对于优化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内核的性能指标。
除了预定义的指标,还可以利用基于分析器获得的硬件计数器来自定义指标。
事件和指标
在CUDA性能分析中,事件是可计算的活动,它对应一个在内核执行期间被收集的硬件计数器。指标是内核的特征,它由一个或多个事件计算得到。请记住以下概念事件和指标:
- 大多数计数器通过流式多处理器来报告,而不是通过整个GPU。
- 一个单一的运行只能获得几个计数器。有些计数器的获得是相互排斥的。多个性能分析运行往往需要获取所有相关的计数器。
- 由于GPU执行中的变化(如线程块和线程束调度指令),经重复运行,计数器值可能不是完全相同的。
选择合适的性能指标以及将检测性能与理论峰值性能进行对比对于寻找内核的性能瓶颈是很重要的。在本书的示例和练习中,你将了解用命令行分析器分析内核的适当指标,以及掌握使用配置文件驱动的方法来编写高效的核函数的技巧。
在本书中主要使用nvprof来提高内核性能。本书还介绍了如何选择合适的计数器和指标,并使用命令行中的nvprof来收集分析数据,以便用于设计优化策略。你还将会学习如何使用不同的计数器和指标,从多个角度分析内核。
有3种常见的限制内核性能的因素:
- 存储带宽
- 计算资源
- 指令和内存延迟
本章主要介绍指令延迟的问题,其次会介绍一些计算资源限制的问题。后续章节将讨论其余的性能限制因素。
了解硬件资源的详细信息
作为一个C程序员,如果编写代码只追求正确性,那么可以忽略缓存行的大小。然而,当调整代码以获得最佳性能时,必须考虑代码结构中高速缓存的特性。
这对于CUDA C编程来说也一样。作为CUDA C程序员,如果想改善内核的性能,必须对硬件资源有一定的了解。
即使不懂硬件架构,CUDA编译器仍然能很好地优化内核,但它能做的只有这么多。即使仅掌握最基本的GPU体系架构的知识,你也能够编写出更好的代码,并且能够充分开发设备的性能。
在本章的后续部分,你将看到硬件的概念是如何与性能指标联系起来的,以及性能指标是如何被用于指导性能的。