主要为个人笔记,不太便于阅读,后续如有时间出一个易于阅读的版本。
目录
CUDA C++ Best Practices Guide(笔记)
优化四部曲APOD
1Assessing Your Application
2、Heterogeneous Computing(异构计算)
2.1. Differences between Host and Device(Device和Host之间的差异)
2.2. What Runs on a CUDA-Enabled Device?
5.1. Parallel Libraries
5.2. Parallelizing Compilers
5.3. Coding to Expose Parallelism
6. Getting the Right Answer
6.1. Verification
6.1.1. Reference Comparison
6.1.2. Unit Testing
6.2. Debugging
6.3. Numerical Accuracy and Precision
6.3.1. Single vs. Double Precision
6.3.2. Floating Point Math Is not Associative
6.3.3. IEEE 754 Compliance
6.3.4. x86 80-bit Computations
7. Optimizing CUDA Applications
8. Performance Metrics
8.1. Timing
8.1.1. Using CPU Timers
8.1.2. Using CUDA GPU Timers
8.2. Bandwidth
8.2.1. Theoretical Bandwidth Calculation
8.2.2. Effective Bandwidth Calculation
8.2.3. Throughput Reported by Visual Profiler
9. Memory Optimizations
9.1. Data Transfer Between Host and Device
9.1.1. Pinned Memory
9.1.2. Asynchronous and Overlapping Transfers with Computation
9.1.3. Zero Copy
9.1.4. Unified Virtual Addressing
9.2. Device Memory Spaces
9.2.1. Coalesced Access to Global Memory
9.2.1.1. A Simple Access Pattern
9.2.1.2. A Sequential but Misaligned Access Pattern
9.2.1.3. Effects of Misaligned Accesses
9.2.1.4. Strided Accesses
9.2.2. L2 Cache
9.2.2.1. L2 Cache Access Window
9.2.2.2. Tuning the Access Window Hit-Ratio
9.2.4. Local Memory
9.2.5. Texture Memory
9.2.5.1. Additional Texture Capabilities
9.2.6. Constant Memory
9.2.7. Registers
9.2.7.1. Register Pressure
9.3. Allocation
9.4. NUMA Best Practices
10. Execution Configuration Optimizations
10.1. Occupancy
10.1.1. Calculating Occupancy
10.2. Hiding Register Dependencies
10.3. Thread and Block Heuristics
10.5. Concurrent Kernel Execution
10.6. Multiple contexts
11. Instruction Optimization
11.1. Arithmetic Instructions
11.1.1. Division Modulo Operations
11.1.2. Loop Counters Signed vs. Unsigned
11.1.3. Reciprocal Square Root
11.1.4. Other Arithmetic Instructions
11.1.5. Exponentiation With Small Fractional Arguments
11.1.6. Math Libraries
11.2. Memory Instructions
12. Control Flow
12.1. Branching and Divergence(没看明白)
12.2. Branch Predication
13. Deploying CUDA Applications
14. Understanding the Programming Environment
14.1. CUDA Compute Capability
14.2. Additional Hardware Data
14.3. Which Compute Capability Target
14.4. CUDA Runtime
15. CUDA Compatibility and Upgrades
15.1. CUDA Runtime and Driver API Version
15.2. Standard Upgrade Path
15.3. Flexible Upgrade Path
15.4. CUDA Compatibility Platform Package
15.5. Extended nvidia-smi
16. Preparing for Deployment
16.1. Testing for CUDA Availability
Detecting a CUDA-Capable GPU
Detecting Hardware and Software Configuration
16.3. Building for Maximum Compatibility
16.4. Distributing the CUDA Runtime and Libraries
Statically-linked CUDA Runtime
Dynamically-linked CUDA Runtime
Other CUDA Libraries
16.4.1. CUDA Toolkit Library Redistribution
17. Deployment Infrastructure Tools
17.1. Nvidia-SMI
17.1.1. Queryable state
17.1.2. Modifiable state
17.2. NVML
17.3. Cluster Management Tools
17.4. Compiler JIT Cache Management Tools
17.5. CUDA_VISIBLE_DEVICES
A. Recommendations and Best Practices
A.1. Overall Performance Optimization Strategies
B. nvcc Compiler Switches
B.1. nvcc
Notices
前言
1、Asses(评估从哪里优化,并且分析优化的上限)
2、Parallelize(并行化)
3、optimize(优化,逐步的)
4、Deploy(实施,及时部署到产品上)
5、Recommendations and Best Practices
文档对于优化有个优先级的评价,确保在较低优先级优化进行之前,完成了所有的高级优化,在本文中为了简洁,忽略了错误检查,在工程代码中要使用cudaGetLastError()来后去错误。
啰嗦的一些话
线程资源(数量):Cuda远远大于CPU
线程:CPU的线程比较重量级,线程切换需要消耗比较多的时间,GPU上的线程都是轻量级的。
内存:host和device拥有各自的内存,是分开的,
(1)需要大量线程同时进行计算(几千以上)
(2)应该最小化Device和Host之间的数据传输
①应该对比算法复杂度与数据传输,例如,对于二个(N*N)的加法操作,算法复杂度是,但是有
的数据传输,所以计算和传输的比例是(1:3),是O(1),所以算法加速不明显。只有当这个比例变大后,性能提升才比较明显,如一个矩阵乘法需要
的算法复杂度,所以计算和传输的比例变成了(1:N)也就是O(N);
②数据应该尽可能的在device中,不要多次传输。
(3)在device上运行的邻近线程读取的内存应该是连贯的
这是由于在某些内存操作模式中,硬件会将内存读取和写入操作合并为一个操作。
借助分析工具,开发者可以识别出那些地方需要并行化
(1)有许多代码分析方法,但共同点就是分析哪段代码使用时间最多。
(2)注意:高优先级:为了最大化开发者的效能,分析应用代码,并检测瓶颈。
(3)分析时要保证工作负载时真实的
(4)有许多工具可以用来对程序进行解析,gprof,是一个Linux上的开元工具
上图中genTimeStep()使用了三分之一的时间,这个应该第一个考虑被优化,但是同时也应该注意到其他的几个函数也比较耗时,但是由于APOD原则是一个循环的过程,我们最好在后续的优化中进行优化,从而将我们每次的优化工作范围限制在较小的改动中。
参考自:
http://blog.sina.com.cn/s/blog_82ba625301013lfu.html
废话
对于并行化串行化得代码有一些关键策略,这些策略无论是在多CPU还是GPU上都适用,主要策略如下:
可以充分利用已经存在的库,例如cuBLAS和cuFFT库,比较重要的是Thrust 库,一个类似c++标标准库的并行C++模板库。
另外一种将串行代码并行化得方法是利用编译器,即通过标记的方式告诉编译器哪里需要并行化,从而免去修改代码,例如:OpenACC 标准库提供了一系列说明循环以及应该被加载到GPU等设备的编译指令。
具体的请到http://www.openacc.org/ 查看信息信息
对于以上两种无法自动进行并行化的代码片段,此时并行语言CUDA C++就很有必要,一但我们确定了应用程序的瓶颈,即可以使用CUDAC++将该部分代码改写成CUDA核,然后发射到GPU计算并取回,这不会影响应用的其他部分。
对于整个应用耗时比较分散的应用,应该先重构,然后再进行并行化,这样做会在未来收益。
本章说明影响返回数据正确性的原因,并指明解决方法。
对于某些改动正确性评估的一个关键方面是建立一个事先知道的参照,使用二进制位的方式检验是不可取的,尤其是对于浮点型点集合,对于其他算法,当误差在一定范围内时,被认为是正确的。这种验证数值结果的方法的也可以用来做性能评估。
(1)为了更好的比较,可以将一个比较大的核函数拆分成多个较小的设备函数。
(2)可以将大多数函数写成host代码和device代码,这样如果有差异可以尽早的发现
(1)CUDA-GDB是允许在linuxs和mac的GNU Debugger。
https://developer.nvidia.com/cuda-gdb.
(2)VS插件NVIDIA Nsight Visual Studio Edition是Windows平台的调试工具
https://developer.nvidia.com/nsight-visual-studio-edition.
(3)第三方CUDA debugging 调试工具如下
https://developer.nvidia.com/debugging-solutions
在计算能力1.3以上的设备支持双精度(64)浮点型数据,使用相同的计算方法,使用双精度和单精度数据返回的结果是不一样的,在使用之前要允许这些差异。
每个读点数据的计算包含了部分的舍入操作(rounding),因此计算的顺序是重要的,对于三个浮点型矩阵A,B,C,不能保证(A+B)+C像符号计算那样完全等于A+(B+C),当使用并行计算时会调整这种顺序,因此不能保证与串行计算结果一致,这种限制是由于浮点数据在并行计算时本身固有的问题引起的,并不是具体的CUDA的问题。
对于二进制浮点型代表,除了一些意外的情况,所有的CUDA计算设备遵循IEEE 754标准,这些意外情况在Features and Technical Specifications of the CUDA C++ Programming Guide白皮书中进行了详细描述,可以导致与host计算结果不同,一个关键的不同是FMA指令,将多集相加操作合并成一个指令,这轻微的和分开的两个操作结果不一样。
X86处理器在执行浮点型计算时候会使用扩充的80位精度计算,这和在cuda设备中纯使用64位的执行结果不一样。
废话,在每个部分并行化完成后,可以着手开始对CUDA并行程序部分进行迭代式优化。
在优化CUDA程序时,知道如何准确衡量性能以及明白带宽在性能测量中的角色是至关重要的,本章讨论如何使用cup时间以及cuda时间正确的衡量性能。
(1)由于GPU是异步执行的,所以会在没有完成计算时将控制权交回CPU,为了准确的测量所用时间,应该在开始和结束计时的模块前添加cudaDeviceSynchronize()同步函数。
(2)此外也可以将特定流和事件和CPU线程进行同步,主要通过cudaStreamSynchronize()和cudaEventSynchronize,这些同步函数不适用除在默认流外的其他流中进行函数计时。另外,由于驱动会交错的调用其他非默认CUDA call流,所以其他流的执行时间也会被加进可靠的来。由于默认流(0流)在设备中建立了序列化的工作流程,在默认流中的函数可以可靠的用来计时。
(3)由于CPU到GPU的同步会阻碍GPU管线执行,索引这些函数应该被谨慎的使用
cuda事件api提供了创建和销毁以及保存事件的回调,并且可以将时间戳的转换成毫秒级的浮点型数据。例子如下:
Note:High Priority: 当衡量性能以及优化的收益时候,使用计算的有效带宽来度量
提供了理论带宽的计算方法
如何计算有效带宽的方法
在计算能力2.0及以上的设备可以借助Visual Profile,如下的吞吐量衡量方法被可视化展现
Requested Global Load Throughput
Requested Global Store Throughput
Global Load Throughput
Global Store Throughput
DRAM Read Throughput
DRAM Write Throughput
内存优化是性能优化里面最重要的区域,通过最大化带宽的目的是最大化利用硬件,对于带宽来说,尽可能的使用快速的低延迟的内存,本章讨论了在host和device上的多种内存,并且如何通过设置数据条目来更有效的利用内存。
在GPU中带宽远比device和host之间的带宽稿高,所以要尽可能的减少device和host之间的数据传输,
Note:HighPriority:减少device和host之间的数据传输,即使在GPU上某些计算并不见得比CPU快。
另外,可以考虑将许多小的数据传输合并或者压缩,然后在传输结束之后解压。
使用Page-locked 或者pinned memory可以加快device和host设备之间的带宽, Pinned memory使用运行时apicudaHostAlloc()分配,bandwidthTest例子演示了如何衡量内存转换性能
对于已经分配的内存,使用cudaHostRegister可以在不需要分配和赋值数据的情况向将内存pin到运行时。
需要注意的是Pinned memory是一种稀缺资源,并不能广泛使用,具体使用多少,需要自己测试
(1)host和device之间使用cudaMemcpy()传输数据会阻塞host线程,cudaMemcpyAsync()采用异步的方式传输,控制权可以立即返回给host线程,异步传输需要host的pinned memory,并且包含一个额外的ID流参数,这个参数简单的表明了在device中序列化执行的操作。在不同的流中的操作可以交叉重叠,这种属性可以隐藏host和device之间的数据传输
(2)异步传输允许传输和计算两种不同类型的操作同时发生,在使用CUDA的设备上,允许将host计算和异步数据传输以及device计算进行重叠,如下的例子 Overlapping computation and data transfers 表明了在host的常规函数cpuFunction()如何在数据传输和核函数在device上运行时同步执行
cudaMemcpyAsync()函数中最后一个参数0表示使用默认流,由于核函数执行也使用默认流,所以在数据传输完成后核函数才会执行,因此不需要显式的进行同步,由于内存拷贝和核函数会立即返回控制权给host,所以cpuFunction()与他们执行会同步。
在例子 Overlapping computation and data transfers中 ,内存拷贝和核函数执行序列化执行,在一些可以具备同时拷贝和计算的设备上,将数据传输和执行进行重叠是可行的,cudaDeviceProp 结构体中asyncEngineCount 参数指明了设备是否有这种能力(deviceQuery的cuda例子上),在具备这种能力的设备上,重叠执行需要:
(1) pinned host memory
(2)数据传输和执行必须使用非默认流(即流值非0),使用非默认流的原因是,使用默认流的内存拷贝,内存设置以及核函数调用等操作只有在设备中任意流的先前操作完成,才可以开始,设备的先前操作没有结束时,设备上的任意操作都无法开始。
上述代码知识了一个基本的传输和执行交叉执行的例子,当数据可以被分块执行时,可以参照如下例子执行
在上例中,N被分为nThreads*nStreams,由于在一个流中的执行是序列化的,在数据没有在格子的流中传输完成时,核函数无法发射。现在的GPU可以同时处理异步数据传输和执行核函数,具有一个拷贝引擎(copy engine)的GPU可以执行一个异步数据传输以及核函数,而具有两个拷贝引擎的的设备可以同时执行:一个来自host到device的数据传输,一个device到host的数据传输以及核函数。在cudaDeviceProp结构体中的asyncEngineCount参数说明了GPU中复制引擎的梳理,在deviceQuery 的CUDA Sample中也有提及。 (需要说明的一点是,将阻塞传输和异步传输重叠是不可行的,这是由于,阻塞传输在默认流中出现,默认流在所有先前的cuda call执行结束之前其并不会开始,同时在其执行结束之前也不允许其他的call开始运行),Sequential copy and execute和Staged concurrent copy and execute,两个例子执行时间线如下图所示
在CUDA Tookit的2.2以上版本中加入了Zero Copy,这使得GPU线程可以直接访问host内存,为了达到这种目的,需要使用pinned memory.由于内存在GPU上没有缓存,所以应该映射的内存应该在GPU上被读写一次。读写内存的加载和存储操作应该被合并。Zero copy可以替代streams ,因为原始核数据的传输自动的和核执行叠加,同时不需要额外的设置和检测最佳的stream数量。
例子如下:
注意:映射的pinned host memory在避免流操作的同时允许叠加CPU-GPU内存交换,但是如果有重复的内存获取操作将导致重复的CPU-GPU内存交换,在这种情况下考虑在GPU第二区域上上手动缓存主机内存数据。
使用TCC驱动程序模式时,具有2.0和更高版本的计算能力的设备在64位Linux,Mac OS和Windows上支持一种称为统一虚拟寻址(UVA)的特殊寻址模式。 使用UVA,主机内存和所有已安装支持的设备的设备内存共享一个虚拟地址空间
在UVA之前,应用程序必须为每个指针跟踪哪个指向device内存(或哪个设备)以及哪个指向host内存作为作为元数据的一部分, 另一方面,使用UVA,只需使用cudaPointerGetAttributes()检查指针的值,即可确定指针指向的物理存储空间。
在UVA规则中,使用cudaHostAlloc() 分配的 pinned host memory 将具有统一的host和device指针,所以没必要调用cudaHostGetDevicePointer()来进行配置,但是,事后通过cudaHostRegister()固定的主机内存分配将继续具有与其主机指针不同的设备指针,因此在这种情况下cudaHostGetDevicePointer()仍然是必需的。
对于支持配置中支持的GPU,UVA也是直接通过PCIe总线或NVLink,绕过host内存,进行对等(P2P)数据传输的必要先决条件。
CUDA设备具有不同的内存,分别是global, local, shared, texture, and registers,如下图所示
Global, local, and texture访问具有较高的延迟,接下来是constant,shared以及register
这些内存类型的主要特性如下表所示:
合并内存访问是CUDA使能的GPU设备中最重要的一条性能考虑,被束中线程处理的
全局的内存加载和存储被Device合并为尽可能少的处理。合并概念将在接下来几个简单的例子中进行说明,除非有特别说明,例子假设计算能力在6.0及以上,并且访问有四个字节
在计算能力6.0及以上的设备中,在32字节对齐的序列中,第K个线程访问第k个字节,不必所有的线程都参与。
如果一个束中的线程获取邻近的4个字节,4个32字节处理将合并在一起,如下图所示:
以上红色部分表面4个32字节的合并处理。
如果只是请求4个32字节中的部分,那么还是全部或被获取。(与CUDA获取方法有关)
如果一个束中的线程在获取连续的内存,但没有和32字节对齐,那么将会请求5个32字节分区。如下图所示:
由cuda 运行时api cudaMalloc()创建的内存保证至少是256个字节对齐的,因此,选择合适的blocl尺寸,例如设置为束尺寸的倍数(当前GPU中是32),可以被对齐的束更快的获取内存。
下图说明了没有对齐的内存获取带来的影响,可以看到影响并没有那么大,将原先的4个32分区内存获取更改为5个32分区内存获取后,带宽并没有编程4/5,而是成为9/10,原因是邻近的束对线程获取的内存的缓存进行了再利用,所以影响并没有我们想象中那么大,但是当毗邻的束不能很大程度上继承束已经获取的缓存时,那么影响会变大。
在上述非对齐序列化获取内存例子中,缓存减轻了对性能的影响,但对于当处理多尺寸的数据和矩阵时使用的non-unit-strided accesses获取将会不同, 因此尽可能的保证每次在缓存中抓取的数据被使用是在内存获取性能优化方面非常重要的。
为说明stride对性能的影响,使用如下核函数
访问内存方式如下图所示:
可以看到每个毗邻的线程按照步长为2访问内存,可以看到有50%的内存没有被使用,造成了带宽浪费。随着步长的增加对性能的影响如下图:
如上图所示,non-unit-stride 全局内存获取应该在尽可能的时候避免,一种方法是使用共享内存(shard memory),在接下来的章节进行讨论。
从CUDA 11.0开始,计算能力8.0及以上的设备可以影响数据在L2缓存器的驻留,由于L2在芯片中(这条保留疑问),所以与全局内存获取相比,可以提供更高的带宽和低延迟
当CUDA内核重复访问全局内存中的数据区域时,可以认为此类数据访问是持久的。 另一方面,如果仅访问数据一次,则可以将此类数据访问视为流式传输。 可以留出一部分L2高速缓存以持久访问全局存储器中的数据区域。 如果永久访问未使用此预留部分,则流或常规数据访问都可以使用它。
可以在以下限制内调整用于持久访问的L2缓存预留空间大小:
在cuda流或者cuda绘制核节点使用访问策略窗可以控制用户数据到 L2 set-aside portion的映射。
hitRatio参数可用于指定接收hitProp属性的访问的比例。 例如,如果hitRatio值为0.6,则全局内存区域(ptr..ptr + num_bytes)中60%的内存访问具有持久性,而40%的内存访问具有流媒体属性。 为了了解hitRatio和num_bytes的影响,我们使用了滑动窗口微基准
此微基准使用GPU全局内存中的1024 MB区域。 首先,如上所述,我们留出了30 MB的L2高速缓存用于使用cudaDeviceSetLimit()进行持久访问。 然后,如下图所示,我们指定对存储区域的前freqSize * sizeof(int)字节的访问是持久的。 因此,该数据将使用L2预留部分。 在我们的实验中,我们将持久性数据区域的大小从10 MB更改为60 MB,以对各种情况进行建模,其中数据适合或超过30 MB的可用L2预留部分。 请注意,NVIDIA Tesla A100 GPU具有40 MB的总二级缓存容量。 对存储区域的剩余数据(即流数据)的访问被视为正常访问或流访问,因此将使用未预留的L2部分的剩余10 MB。
在滑动窗实验中映射持久数据获取到预留L2缓冲如下图所示。
代码如下:
核函数执行的性能最终如下图所示,当持久数据区域很好地适合L2缓存的30 MB预留空间时,可以看到性能提高了50%。 但是,一旦此持久数据区域的大小超过了L2预留高速缓存部分的大小,由于L2高速缓存行的抖动,将导致大约10%的性能下降。
为了优化当持久数据大于L2缓冲区大小时的性能,我们调整num_bytes and hitRatio这两个参数,如下图所示:
我们将访问窗口中的num_bytes固定为20 MB,并调整hitRatio,以使总持久数据中的随机20 MB驻留在L2预留缓存部分中。 使用流属性可以访问此持久数据的其余部分。 这有助于减少缓存抖动。 结果显示在下表中,无论持久数据是否适合L2预留,我们都可以看到良好的性能。
由于共享内存在芯片上,因此与本地和全局内存相比,共享内存具有更高的带宽和更低的延迟-前提是线程之间不存在存储体冲突,如以下部分所述
为了实现并发访问的高内存带宽,共享内存被分为大小相等的内存模块(bank),可以同时访问这些内存模块。 因此,可以同时处理跨越n个不同存储体的n个地址的任何存储负载或存储,产生的有效带宽是单个存储体带宽的n倍
但是,如果一个内存请求的多个地址映射到相同的memory bank,则访问将被序列化。 硬件将具有存储体冲突的内存请求拆分为所需的多个独立的无冲突请求,从而将有效带宽降低了等于独立内存请求数量的倍数。 这里的一个例外是,当线程束中的多个线程寻址同一共享内存位置时,会导致广播。 在这种情况下,来自不同bank的多个广播合并为从请求的共享内存位置到线程的单个广播。
为了最大程度地减少存储体冲突,重要的是要了解内存地址如何映射到memory bank以及如何优化调度内存请求.
在具有5.x或更高版本的计算能力的设备上,每个时钟周期,每个存储体都有32位的带宽,并且将连续的32位字分配给连续的bank。 束大小为32个线程,那么bank的数量也是也为32,因此在束中的任何线程之间都可能发生组冲突。 有关更多详细信息,请参阅《 CUDA C ++编程指南》中的Compute Capability5.x。
补充说明:上述的bank confilt解释不太清楚,这里解释下,共享内存被分成32个bank,每个bank中的宽度可以是32位或者64位(不同的GPU架构会有所不同),共享内存的访问是按照束来进行的,所以理想情况下,如果束中的32个线程不重叠的访问32个bank,那么不会发生冲突,但如果其中两个以上线程访问同一个bank有可能会发生冲突,为什么说有可能呢,因为如果束中多个线程访问同一个bank中同一个地址,那么会触发广播机制,广播机制触发时,这时访问同一bank中同一个地址的线程相当于一次访问,但束中其他线程如果访问同一个bank但不同地址则会触发序列化执行,这样效率是比较低的。
共享内存运行在一个block中的线程协作,当在一个块中的多个线程使用来自全局内存的数据时,共享内存可以使只获取一次全局内存。通过在合并模式中加载和存储全局内存并保存在共享内存中,共享内存可以用来避免未合并的内存操作。
对于矩阵A为Mxw,B为wxN,C为MxN的情况,通过矩阵乘法C = AB的简单示例说明了共享内存的使用。 为了使内核保持简单,M和N为32的倍数,因为当前设备的束大小(w)为32。
一个没有优化的例子如下:
在上例中,a,b,c都是指向全局内存,使用共享内存改写A的内存获取方式的程序如下:
对B也进行了改写,程序如下
需要说明的是,性能的提示并不是来自于合并操作,而是避免了来自全局内存的无效转换。这三个程序最终性能评估如下:
接下来处理一个简单的例子,将以上例子中的B换成A的转置。未优化的例子如下:
在上例中,这个核的带宽才12.8GB/s远低于原先C=A*B的核函数,主要原因是,a[col*TILE_DIS+i]代表使用w步长跨域访问,导致了大量浪费的带宽。
改进后的例子如下:
但是改进后的有效带宽只有140.2GB/s,还是低于原先的C=A*B的结果,差异的原因是shared memory bank冲突,改进方法如下:
改进之后的效果如下:
本章节中这些例子说明使用共享内存的三个原因:
为了合并获取共享内存,尤其是避免大的步长(对于大部分矩阵,步长大于32)
为了减少或者消除从全局内存的冗余加载
为了避免浪费带宽。
CUDA 11.0引入了异步复制功能,可在设备代码中使用该功能来显式管理从全局内存到共享内存的数据异步复制。 此功能使CUDA内核可以通过计算将复制数据从全局复制到共享内存中。 它还避免了传统上在全局存储器读取和共享存储器写入之间存在中间寄存器文件访问。
举例如下:
核函数同步版本将元素从全局内存加载到中间寄存器,然后将中间寄存器的值存储到共享内存。 在核函数的异步版本中,一旦调用memcpy_async()函数,就会发出从全局内存加载并直接存储到共享内存的指令。 pipe.wait_prior <0>()将等待,直到管道对象中的所有指令均已执行。 使用异步副本不使用任何中间寄存器。 不使用中间寄存器可以帮助减少寄存器压力,并可以增加内核占用率。 使用异步复制指令从全局内存复制到共享内存的数据可以缓存在L1缓存中,也可以选择绕过L1缓存。 如果单个CUDA线程正在复制16个字节的元素,则可以绕过L1缓存。 下图说明了这种差异。
我们使用每个线程大小分别为4B,8B和16B的元素(即使用int,int2和int4作为模板参数)来评估两种核函数的性能。 我们调整内核中的copy_count,以使每个线程块拷贝从512字节到48 MB。 内核的性能如图14所示
通过本次实验的性能图表中,可以得出如下观察
(1)当copy_cout是全部三个元素大小的四倍时,同步拷贝可以获取最好的性能,编译器可以优化4的加载和存储指令性能,当所有三个元素大小的copy_count参数均为4的倍数时,可以实现同步复制的最佳性能。 编译器可以优化4个加载和存储指令的组。 从锯齿曲线可以明显看出这一点。
(2)异步拷贝在几乎所有的情况下获得更好的性能。
(3)异步拷贝为了通过编译器优化获取最高性能,不需要将copy_cont设置为4的倍数。
(4)总之,当异步拷贝使用8字节或者16字节的元素时,可以获取最佳性能。
命名本地内存是因为其范围是线程本地的,而不是因为其物理位置。 实际上,本地存储器是不在芯片上。 因此,访问本地内存与访问全局内存一样昂贵。 换句话说,名称中的术语本地并不意味着访问速度更快
本地存储器仅用于保存自动变量。 当nvcc编译器确定没有足够的寄存器空间来容纳变量时,将执行此操作。 可能放置在本地存储器中的自动变量是大型结构或数组,它们会占用过多的寄存器空间,并且编译器确定的数组可能会动态索引。
检查PTX汇编代码(通过使用-ptx或-keep命令行选项编译到nvcc获得)可以发现在第一个编译阶段是否已将变量放置在本地内存中。 如果有,它将使用.local标记符声明,并使用ld.local和st.local标记进行访问。 如果没有,后续编译阶段可能仍会做出其他决定,如果他们发现该变量消耗了目标体系结构过多的寄存器空间。 无法检查是否有特定变量,但是使用--ptxas-options = -v选项运行时,编译器会报告每个内核(lmem)的总本地内存使用情况。
只读纹理存储空间在缓冲中。 因此,纹理获取仅在一次高速缓存未命中时消耗一个设备内存读取; 否则,它只会从纹理缓存中读取一次。 纹理高速缓存针对2D空间局部性进行了优化,因此在相同束中读取邻近的纹理将获得最佳性能,纹理存储器还设计用于以恒定的延迟流式获取。 也就是说,缓存命中减少了DRAM带宽需求,但没有延迟获取。
在某些寻址情况下,通过纹理获取读取设备内存可能是从全局或常量内存读取设备内存的有利替代方案。
如果使用tex1D(),tex2D()或tex3D()而不是tex1Dfetch()来获取纹理,则硬件会提供其他功能,这些功能可能对某些应用程序有用,例如图像处理,如表4所示。
在内核调用中,纹理缓存相对于全局内存写入未保持一致,因此从已通过全局存储在同一内核调用中写入的地址进行纹理提取会返回未定义的数据。 也就是说,如果某个位置已被先前的内核调用或内存副本更新,则线程可以通过纹理安全地读取该内存位置,但如果先前已被同一线程或同一内核调用中的另一个线程更新,则线程无法安全读取该位置。
设备上总共有64 KB的Constant Memory。Constant Memory在缓存中。从Constant Memory中进行读取仅会在高速缓存未命中时从设备内存中读取一次; 否则,它只会从常量缓存中读取一次。束中的线程对不同地址的访问被序列化,因此读取代价与束中的所有线程读取的唯一地址的数量成线性比例。 因此,当同一warp中的线程仅访问几个不同的位置时,恒定缓存是最佳的。 如果warp的所有线程都访问同一位置,则恒定内存可以与寄存器访问一样快。
通常,访问寄存器每条指令消耗零个额外的时钟周期,但是由于寄存器写后读的依赖性和寄存器bank冲突可能会导致延迟。
编译器和硬件线程调度程序将尽可能最佳地调度指令,以避免寄存器存bank冲突。 应用程序无法直接控制这些冲突。 特别的,没有与寄存器相关的理由将数据打包为向量数据类型,例如float4或int4类型。
当给定任务没有足够的寄存器时,就会发生寄存器压力。 即使每个multiprocessor 包含数千个32位寄存器(请参阅《 CUDA C ++编程指南》的功能和技术规范),这些寄存器仍在并发线程之间进行分区。 为防止编译器分配过多的寄存器,请使用-maxrregcount = N编译器命令行选项(请参阅nvcc)或启动边界内核定义限定符(请参阅《 CUDA C ++编程指南》的“执行配置”)来控制最大寄存器数。 分配给每个线程。
通过cudaMalloc()和cudaFree()进行设备内存分配和取消分配是昂贵的操作,因此应尽可能由应用程序重用和/或子分配设备内存,以最大程度地减少分配对整体性能的影响。
默认情况下,一些最新的Linux发行版启用自动NUMA平衡(或“ AutoNUMA”)。 在某些情况下,由自动NUMA平衡执行的操作可能会降低NVIDIA GPU上运行的应用程序的性能。 为了获得最佳性能,用户应手动调整其应用程序的NUMA特性。
最佳NUMA调整将取决于每个应用程序和节点的特性和所需的硬件亲和力,但通常建议在NVIDIA GPU上进行计算的应用程序选择禁用自动NUMA平衡的策略。 例如,在IBM Newell POWER9节点(CPU对应于NUMA节点0和8的节点)上,使用:
将内存分配绑定到CPU
良好性能的关键之一是使设备上的多处理器尽可能繁忙。 在多处理器之间工作平衡不佳的设备将提供次优的性能。 因此,设计应用程序以最大程度地利用硬件并限制妨碍工作自由分配的做法来使用线程和块非常重要。 占用率在这种优化工作中是一个关键概念,以下部分对此进行了说明。
在某些情况下,可以通过设计应用程序来提高硬件利用率,以便可以同时执行多个独立的内核。 同时执行的多个内核称为并发内核执行。 并发内核执行如下所述。
另一个重要的概念是管理分配给特定任务的系统资源。 本章最后几节将讨论如何管理这种资源利用。
线程指令在CUDA中按顺序执行,因此,当一个warp暂停或停止时执行其他warp是隐藏延迟并保持硬件繁忙的唯一方法。 因此,与多处理器上活动的束有关指标对于确定硬件保持繁忙状态的效率很重要。 此指标是占用率。占用率是每个多处理器的活动束数量与可能的活动束最大数量之比。 (要确定后者的数量,请参阅deviceQuery CUDA示例或参阅《 CUDA C ++编程指南》中的“计算功能”。)另一种查看占用率的方法是硬件处理正在使用的束能力的百分比。更高的占用率并不总是等同于更高的性能-在这一点上,额外的占用率并不能提高性能。 但是,低占用率始终会干扰隐藏内存延迟的能力,从而导致性能下降。
决定占用率的几个因素之一是寄存器的可用性。 寄存器存储使线程可以将局部变量保持在附近,以进行低延迟访问。 但是,寄存器集(称为寄存器文件)是有限的资源,驻留在多处理器上的所有线程都必须共享。 寄存器一次分配到整个块。 因此,如果每个线程块使用许多寄存器,则可以驻留在多处理器上的线程块数量会减少,从而降低了多处理器的占用率。 可以在编译时使用-maxrregcount选项对每个文件手动设置每个线程的最大寄存器数,或者使用__launch_bounds__限定符对每个内核进行手动设置(请参见寄存器压力)。
为了计算占用率,每个线程使用的寄存器数量是关键因素之一。 例如,在计算能力为7.0的设备上,每个多处理器具有65,536个32位寄存器,并且最多可以驻留2048个并行线程(64个束x32个线程/束)。 这意味着在这些设备之一中,要使多处理器具有100%的占用率,每个线程最多可以使用32个寄存器。 但是,这种确定寄存器计数如何影响占用率的方法并未考虑寄存器分配的粒度。 例如,在计算能力为7.0的设备上,具有 “128-线程” 块的内核每个线程使用37个寄存器导致每个多处理器有12个活动的“128-线程”的占用率为75%,而具有320个线程块的内核 每个线程使用相同的37个寄存器将导致63%的占用率,因为多处理器上只能驻留四个320个线程块。 此外,在具有7.0计算能力的设备上,寄存器分配被四舍五入到最接近的每块256个寄存器。
讲占有率的一篇参考博文
https://bbs.csdn.net/topics/390869656?utm_medium=distribute.pc_relevant.none-task-discussion_topic-BlogCommendFromBaidu-1.control&depth_1-utm_source=distribute.pc_relevant.none-task-discussion_topic-BlogCommendFromBaidu-1.control
可用的寄存器数量,每个多处理器上驻留的最大并发线程数以及寄存器分配粒度随计算能力的不同而变化。 由于寄存器分配中的这些细微差别,并且多处理器的共享内存也在常驻线程块之间进行分区,因此很难确定寄存器使用率和占用率之间的确切关系。 nvcc的--ptxas options = v选项详细说明每个内核每个线程使用的寄存器数。 有关各种计算功能的设备的寄存器分配公式,请参见《 CUDA C ++编程指南》的“硬件多线程”;有关这些设备上可用的寄存器总数,请参见《 CUDA C ++编程指南》的功能和技术规范。 另外,NVIDIA还提供Excel电子表格形式的占用计算器,使开发人员可以在最佳平衡中进行磨练,并更轻松地测试各种可能的情况。 该电子表格如图15所示,称为CUDA_Occupancy_Calculator.xls,位于CUDA Toolkit安装目录的tools子目录中。
除计算器电子表格外,可使用NVIDIA Nsight Compute Profiler确定占用率。 有关占用的详细信息显示在“占用”部分中。
应用程序还可以使用CUDA运行时中的Occupancy API,例如cudaOccupancyMaxActiveBlocksPerMultiprocessor,用于基于运行时参数动态选择启动配置。
注意:中等优先级:要隐藏由于寄存器依赖性而引起的延迟,请为每个多处理器保持足够数量的活动线程(即足够的占用率)。
当指令使用在此指令之前生成的储存在寄存器中的结果时候,就会出现寄存器依赖性。 在计算能力为7.0的设备上,大多数算术指令的等待时间通常为4个周期。 因此,线程在使用算术结果之前必须等待大约4个周期。 但是,此延迟可以通过其他束中的线程执行完全隐藏。 有关详细信息,请参见寄存器。
注意:中等优先级:每个块的线程数应该是32个线程的倍数,因为这可以提供最佳的计算效率并促进合并。
每个网格的块的尺寸和大小以及每个块的线程的尺寸和大小都是重要因素。 这些参数的多维方面允许将多维问题更轻松地映射到CUDA,并且在性能上不起作用。 因此,本节讨论尺寸,而不是尺寸。
隐藏延迟、和占用率取决于每个多处理器的活动束数量,该数目由执行参数以及资源(寄存器和共享内存)约束隐式确定。 选择执行参数是在延迟隐藏(占用)和资源利用率之间取得平衡的问题。
选择执行配置参数应该一前一后进行; 但是,某些启发式方法分别适用于每个参数。 选择第一个执行配置参数(每个网格的块数或网格大小)时,首要考虑的是保持整个GPU繁忙。 网格中的块数应大于多处理器的数,以便所有多处理器至少具有一个要执行的块。 此外,每个多处理器应该有多个活动块,以便不等待__syncthreads()的块可使硬件保持忙碌状态。 该建议取决于资源的可用性; 因此,应在第二个执行参数(每个块的线程数或块大小)以及共享内存使用情况的上下文中确定它。 为了扩展到将来的设备,每次内核启动的块数应为数千。
选择块大小时,重要的是要记住,多个并发块可以驻留在一个多处理器上,因此占用率不是仅由块大小决定的。 特别是,较大的块大小并不意味着较高的占用率。
如占用率中所述,较高的占用率并不总是等同于更好的性能。 例如,将占用率从66%提高到100%通常不会转化为类似的性能提升。 占用率较低的内核将比占用率较高的内核具有更多的每个线程可用寄存器,这可能导致较少的寄存器溢出到本地内存; 特别是,在某些情况下,使用高度公开的指令级并行性(ILP),可以完全覆盖低占用率的延迟。
选择块大小涉及许多因素,因此不可避免地需要进行一些实验。 但是,应遵循一些经验法则:
(1·)每个块的线程数应为束大小的倍数,以避免在稀少线程束上浪费计算时间,并有助于合并。
(2)每个块至少应使用64个线程,并且每个多处理器必须有多个并发块。
(3)每个块128至256个线程之间是一个适合不同块大小的实验的良好初始范围。
如果延迟会影响性能,则每个多处理器使用几个较小的线程块,而不是一个大线程块。 这对于经常调用__syncthreads()的内核特别有用。
请注意,当线程块分配的寄存器多于多处理器上可用的寄存器时,内核启动将失败,因为当请求太多共享内存或太多线程时,内核启动将失败。
共享内存在几种情况下可能会有所帮助,例如有助于合并或消除对全局内存的冗余访问。 但是,它也可以作为占有率的限制。 在许多情况下,内核所需的共享内存量与所选的块大小有关,但是线程到共享内存元素的映射不必一对一。 例如,可能希望在内核中使用64x64元素共享内存阵列,但是由于每个块的最大线程数为1024,因此无法启动每个块具有64x64线程的内核。 在这种情况下,可以启动具有32x32或64x16线程的内核,每个线程处理共享内存阵列的四个元素。 即使没有问题(例如每个块的线程数),使用单个线程来处理共享内存阵列的多个元素的方法也可能是有益的。 这是因为线程可以一次执行每个元素共有的某些操作,从而在线程处理的共享内存元素数量上分摊成本。
确定性能对占用的敏感性的一种有用技术是通过试验动态分配的共享内存的数量进行试验,如执行配置的第三个参数所指定。 通过简单地增加此参数(无需修改内核),可以有效地减少内核的占用并衡量其对性能的影响。
如“通过计算进行异步传输和重叠传输”中所述,CUDA流可用于将内核执行与数据传输重叠。 在能够并行执行内核的设备上,流还可以用于同时执行多个内核,以更充分地利用设备的多处理器。 设备是否具有此功能由cudaDeviceProp结构的并发内核字段指示(或在deviceQuery CUDA示例的输出中列出)。 并发执行需要非默认流(流0以外的流),因为使用默认流的内核调用仅在设备(在任何流中)上的所有先前调用完成之后才开始,并且在设备(在任何流中)上均不执行任何操作 流)开始,直到完成为止。
以下示例说明了基本技术。 因为kernel1和kernel2是在不同的非默认流中执行的,所以有能力的设备可以同时执行内核。
CUDA工作发生在特定GPU(称为上下文)的处理空间内。 上下文封装了该GPU的内核启动和内存分配,以及诸如页表之类的支持构造。 上下文在CUDA驱动程序API中是显式的,但在CUDA运行时API中则是完全隐式的,CUDA运行时API可自动创建和管理上下文。
使用CUDA驱动程序API,CUDA应用程序进程可以为给定GPU创建多个上下文。 如果多个CUDA应用程序进程同时访问同一个GPU,则几乎总是意味着多个上下文,因为除非使用多进程服务,否则上下文将绑定到特定的主机进程。
虽然可以在给定的GPU上同时分配多个上下文(及其关联的资源,例如全局内存分配),但是这些上下文中只有一个可以在该GPU上的任何给定时刻执行工作; 共享同一GPU的上下文是按时间划分的。 创建其他上下文会增加每个上下文数据的内存开销和上下文切换的时间开销。 此外,当来自多个上下文的工作可以同时执行时,上下文切换的需求会降低利用率(另请参见并发内核执行)。
因此,在同一CUDA应用程序中每个GPU避免多个上下文。 为此,CUDA驱动程序API提供了访问和管理每个GPU上称为主上下文的特殊上下文的方法。 当没有线程的当前上下文时,这些上下文与CUDA运行时隐式使用的上下文相同。
注意:NVIDIA-SMI可用于将GPU配置为独占进程模式,从而将每个GPU的上下文数限制为一个。 在创建过程中,此上下文可以根据需要使用任意数量的线程,并且如果设备上已经存在使用CUDA驱动程序API创建的非主要上下文,则cuDevicePrimaryCtxRetain将失败。
知道执行指令如何允许低级优化是很有用的,尤其是在频繁运行的代码中(程序中的所谓热点)。 最佳实践建议在完成所有更高级别的优化之后执行此优化。
单精度浮点可提供最佳性能,因此强烈建议使用它们。 CUDA C ++编程指南中详细介绍了单个算术运算的吞吐量。
注意:低优先级:使用移位运算可避免昂贵的除法和模运算。
整数除法和模运算的成本特别高,应尽可能避免或以位运算代替:如果n为2的幂,则(i / n)等于(i≫ log2 n)并且(i%n)等效 到(i&n-1)。
如果n是立即数,则编译器将执行这些转换。 (有关更多信息,请参阅《 CUDA C ++编程指南》中的“性能准则”)。
注意:中低优先级:使用有符号整数而不是无符号整数作为循环计数器。
在C语言标准中,无符号整数溢出语义得到了很好的定义,而有符号整数溢出导致未定义的结果。 因此,与无符号算术相比,编译器可以更有效地使用有符号算术进行优化。 对于循环计数器,尤其要注意这一点:由于循环计数器的值始终为正数是很常见的,因此可能很想将计数器声明为无符号。 但是,为了获得更好的性能,应该将它们声明为已签名。
例如,考虑以下代码:
在这里,子表达式stride * i可能会溢出32位整数,因此,如果将i声明为无符号整数,则溢出语义将阻止编译器使用某些可能会应用的优化,例如 strength reduction。 相反,如果将i明为带符号的,其中未定义溢出语义,则编译器有更多的余地来使用这些优化。
倒数平方根对于单精度应始终显式地作为rsqrtf()调用,对于双精度应始终作为rsqrt()显式调用。 仅在不违反IEEE-754语义的情况下,编译器才将1.0f / sqrtf(x)优化为rsqrtf()。
注意:优先级低:避免将双精度型自动转换为浮点型。
编译器有时必须插入转换指令,从而引入其他执行周期。 这种情况适用于:
(1)在char或short上操作的函数,其操作数通常需要转换为int
(2)双精度浮点常量(没有任何类型后缀的定义)用作单精度浮点计算的输入
后一种情况可以通过使用单精度浮点常量来避免,该常量使用f后缀定义,例如3.141592653589793f,1.0f,0.5f。
对于单精度代码,强烈建议使用浮点类型和单精度数学函数。
还应注意,CUDA数学库的互补误差函数erfcf()特别快,并且具有完全的单精度精度。
对于某些分数指数,与使用pow()相比,通过使用平方根,立方根和它们的逆,可以大大加快幂运算。 对于那些指数不能精确表示为浮点数(例如1/3)的指数,这也可以提供更准确的结果,因为pow()的使用会放大初始表示误差。
下表中的公式对于x> = 0,x!= -0(即signbit(x)== 0)有效。
注意:中优先级:只要速度需求超过精度,就使用快速数学库。
支持两种类型的运行时数学运算。 它们可以通过名称区分:某些名称的名称前带有下划线,而另一些则没有(例如__functionName()与functionName())。 遵循__functionName()命名约定的函数直接映射到硬件级别。 它们速度更快,但精度较低(例如__sinf(x)和__expf(x))。 遵循functionName()命名约定的函数速度较慢,但精度较高(例如sinf(x)和expf(x))。 __sinf(x),__cosf(x)和__expf(x)的吞吐量远远大于sinf(x),cosf(x)和expf(x)的吞吐量。 如果需要减小参数x的大小,后者将变得更加昂贵(大约慢一个数量级)。 此外,在这种情况下,自变量减少代码使用本地内存,由于local内存的高延迟,这可能对性能产生更大的影响。 更多详细信息,请参见《 CUDA C ++编程指南》。
还要注意,每当计算同一个参数的正弦和余弦时,都应使用sincos指令系列来优化性能:
(1)__sincosf()用于单精度快速数学(请参阅下一段)
(2)sincosf()用于常规单精度
(3)sincos()可实现双精度
nvcc的-use_fast_math编译器选项将每个functionName()调用强制转换为等效的__functionName()调用。 通常,它还会禁用单精度非正规支持,并降低单精度除法的精度。 这是一项激进的优化,可以降低数值精度并更改特殊情况的处理。 一种更健壮的方法是仅在性能提高有好处并且可以容忍更改的行为的情况下,选择性地将调用引入快速内在函数。 请注意,此开关仅对单精度浮点有效。
注意:中优先级:在可能的情况下,优先选择速度更快,更专业的数学函数,而不是速度较慢,更通用的数学函数。
对于较小的整数幂(例如x2或x3),几乎可以肯定,显式乘法比使用诸如pow()之类的通用乘幂例程要快。 尽管编译器优化的改进不断寻求缩小这一差距,但显式乘法(或使用等效的专门构建的内联函数或宏)可以具有显着的优势。 当需要相同基数的多个幂时(例如,x2和x5都在附近计算时),此优势会得到增强,因为这有助于编译器进行常见的子表达式消除(CSE)优化。
对于使用2或10为底的取幂,请使用函数exp2()或expf2()和exp10()或expf10(),而不要使用函数pow()或powf()。 就寄存器压力和指令计数而言,pow()和powf()都是重量级函数,这是由于在一般指数运算中出现了许多特殊情况,并且难以在基数和指数的整个范围内实现良好的精度。 另一方面,函数exp2(),exp2f(),exp10()和exp10f()在性能方面类似于exp()和expf(),其速度可比其快10倍。 pow()/ powf()等效项。
对于指数为1/3的幂,请使用cbrt()或cbrtf()函数,而不要使用通用的幂函数pow()或powf(),因为前者的速度明显快于后者。 同样,对于指数为-1/3的指数,请使用rcbrt()或rcbrtf()。
将sin(π*
默认情况下,nvcc编译器生成符合IEEE的代码,但它还提供了一些选项来生成精度稍差但速度更快的代码:
-ftz = true(将规范化的数字刷新为零)
-prec-div = false(精确度较低的除法)
-prec-sqrt = false(精确度较低的平方根)
另一个更具有效的选项是-use_fast_math,它将每个functionName()调用强制为等效的__functionName()调用。 这使代码运行更快,但代价是精度和准确性降低。 请参阅数学库。
注意:高优先级:最小化全局内存的使用。 尽可能选择共享内存访问。
内存指令包括从共享,本地或全局内存中读取或写入的任何指令。 访问未缓存的本地或全局内存时,有数百个时钟周期的内存延迟。
例如,以下示例代码中的赋值运算符具有很高的吞吐量,但是至关重要的是,从全局内存中读取数据存在数百个时钟周期的延迟:
如果有足够多的独立算术指令可以在等待全局内存访问完成时发出,则线程调度程序可以掩盖大部分此类全局内存延迟。 但是,最好避免在可能的情况下访问全局内存。
注意:高优先级:避免在同一束内使用不同的执行路径。
流控制指令(如果执行,切换,执行,执行,执行,等待)会导致同一线程的线程分叉,从而极大地影响指令吞吐量。 即遵循不同的执行路径。 如果发生这种情况,则必须分别执行不同的执行路径。 这增加了为此束执行的指令总数。为了在控制流取决于线程ID的情况下获得最佳性能,应编写控制条件,以最大程度地减少发散束的数量。这是可能的,因为如CUDA C ++编程指南的SIMT体系结构中所述,跨块的束分布是确定的。 一个简单的例子是,控制条件仅取决于(threadIdx / WSIZE),其中WSIZE是束大小。这种情况下,由于控制条件与束完全一致,因此没有束发散。对于仅包含少量指令的分支,束偏差通常会导致边际性能损失。 例如,编译器可以使用声明来避免实际分支。 相反,所有指令都已调度,但是每个线程的条件代码或声明控制哪些线程执行指令。 带有错误谓词的线程不会写入结果,也不会求值地址或读取操作数。从Volta架构开始,独立线程调度允许束与数据相关的条件块之外保持发散。 显式的__syncwarp()可用于确保warp已重新收敛以用于后续指令。
注意:低优先级:使编译器更容易使用分支声明来代替循环或控制语句。
有时,编译器可能会改为使用分支声明来展开循环或优化if或switch语句。 在这些情况下,任何束都不会发散。 程序员还可以使用以下命令控制循环展开
#pragma unroll
有关此编译指示的更多信息,请参阅《 CUDA C ++编程指南》。
使用分支声明时,不会跳过其执行取决于控制条件的指令。 相反,每个这样的指令都与根据控制条件设置为true或false的每个线程的条件代码或声明相关联。 尽管这些指令中的每一个都计划执行,但实际上只有具有声明为true的指令才能执行。 带有false声明的指令不会写入结果,也不会评估地址或读取操作数。
仅当分支条件控制的指令数量小于或等于某个阈值时,编译器才用声明指令替换分支指令。
完成应用程序一个或多个组件的GPU加速后,可以将结果与原始期望进行比较。 回想一下原先通过加速给定热点可获得的提速上限
在解决其他热点以提高总体速度之前,开发人员应考虑将已经加速的部分付诸实践。 这很重要,原因有很多; 例如,它允许用户尽早从他们的优化中受益(提速可能是部分的,但仍然是有价值的),并且通过为开发者提供渐进而不是革命性的变更,将开发人员和用户的风险降至最低。
随着每一代NVIDIA处理器的加入,CUDA可以利用GPU的新功能。 因此,了解架构的特征很重要。
程序员应注意两个版本号。 第一个是计算功能,第二个是CUDA运行时和CUDA驱动程序API的版本号。
计算能力描述了硬件的功能,并反映了设备支持的指令集以及其他规范,例如每个块的最大线程数和每个多处理器的寄存器数。 较高的计算能力版本是较低(即较早)版本的超集,因此它们是向后兼容的。
可以以编程方式查询设备中GPU的计算能力,如deviceQuery CUDA示例中所示。 该程序的输出如图16所示。此信息是通过调用cudaGetDeviceProperties()并访问其返回的结构中的信息而获得的。
计算功能的主要和次要修订号如图16的第七行所示。该系统的设备0具有7.0的计算能力。
有关各种GPU的计算功能的更多详细信息,请参见《 CUDA C ++编程指南》中的“启用CUDA的GPU”和“计算功能”。 特别是,开发人员应注意设备上的多处理器数量,寄存器数量和可用内存量,以及设备的任何特殊功能。
计算功能未描述某些硬件功能。 例如,无论计算能力如何,大多数但并非所有GPU都具有将内核执行与主机和设备之间的异步数据传输重叠的功能。 在这种情况下,请调用cudaGetDeviceProperties()确定设备是否具有某些功能。 例如,设备属性结构的asyncEngineCount字段指示是否可以进行重叠的内核执行和数据传输(如果可以,则可以进行多少次并发传输)。 同样,canMapHostMemory字段指示是否可以执行零拷贝数据传输。
要定位NVIDIA硬件和CUDA软件的特定版本,请使用nvcc的-arch,-code和-gencode选项。 例如,必须使用-arch = sm_30(或更高的计算能力)来编译使用warp shuffle操作的代码。
CUDA软件环境的主机运行时组件只能由主机功能使用。 它提供处理以下内容的功能:
(1)设备管理
(2)上下文管理
(3)内存管理
(4)代码模块管理
(5)执行控制
(6)纹理引用管理
(7)OpenGL和Direct3D的互操作性
与较低级别的CUDA驱动程序API相比,CUDA运行时通过提供隐式初始化,上下文管理和设备代码模块管理,大大简化了设备管理。 nvcc生成的C ++主机代码利用CUDA Runtime,因此链接到该代码的应用程序将取决于CUDA运行时; 同样,使用cuBLAS,cuFFT和其他CUDA工具包库的任何代码也将取决于这些库在内部使用的CUDA运行时。
CUDA工具包参考手册中解释了构成CUDA Runtime API的功能。
CUDA运行时在启动内核之前处理内核加载,设置内核参数和启动配置。 隐式驱动程序版本检查,代码初始化,CUDA上下文管理,CUDA模块管理(多维数据集到功能映射),内核配置和参数传递均由CUDA运行时执行。
它包括两个主要部分:
(1)C样式的函数接口(cuda_runtime_api.h)。
(2)在C样式函数之上构建的C ++样式方便包装器(cuda_runtime.h)。
有关运行时API的更多信息,请参阅《 CUDA C ++编程指南》的CUDA运行时。
标准的更新路径
从CUDA 10开始,具有Tesla GPU的企业用户可以选择以更大的灵活性升级到较新版本的CUDA。 使用此选项,用户无需更新GPU内核模式驱动程序组件,只要它们在特定的企业驱动程序分支上得到了验证即可。 参见图19
灵活的升级是通过使用CUDA 10发行版中CUDA兼容性平台软件包中的文件来完成的。 该软件包包括三个文件,即:
安装后,兼容性平台的用户和系统管理员应配置系统加载程序以使用新的用户模式组件集。 通常,可以通过设置环境变量LD_LIBRARY_PATH或更新ld.so.conf文件,然后运行ldconfig来确保正确链接ld.so.conf的更新来完成此操作。 这将使CUDA 10工具包能够在现有的内核模式驱动程序组件上运行,即,无需将这些组件升级到CUDA 10版本。
CUDA兼容性平台软件包的组织应如图20所示。
以下适用于CUDA兼容性平台软件包:
CUDA兼容性平台软件包中的库旨在与现有驱动程序安装一起使用。
CUDA兼容性平台软件包文件应位于单个路径中,并且不应拆分。
CUDA兼容性平台软件包根据其支持的运行时版本进行版本控制。
为了帮助管理员和用户,对nvidia-smi进行了增强以在其显示屏中显示CUDA版本。 它将使用当前配置的路径来确定正在使用哪个CUDA版本。
有关运行时API的更多信息,请参阅《 CUDA C ++编程指南》的CUDA运行时。
部署CUDA应用程序时,通常需要确保即使目标计算机未安装具有CUDA功能的GPU和/或未安装足够版本的NVIDIA Driver,该应用程序仍将继续正常运行。 (针对配置已知的单台计算机的开发人员可以选择跳过此部分。
当将应用程序部署到具有任意/未知配置的目标计算机时,该应用程序应明确测试是否具有支持CUDA的GPU,以便在没有此类设备可用时采取适当的措施。 cudaGetDeviceCount()函数可用于查询可用设备的数量。 像所有CUDA Runtime API函数一样,如果没有安装支持CUDA的GPU或cudaErrorInsufficientDriver(如果未安装适当版本的NVIDIA驱动程序),则此函数将正常运行并返回cudaErrorNoDevice到应用程序。 如果cudaGetDeviceCount()报告错误,则应用程序应退回到备用代码路径。
具有多个GPU的系统可能包含具有不同硬件版本和功能的GPU。 当使用来自同一应用程序的多个GPU时,建议使用相同类型的GPU,而不是混合使用硬件世代。 cudaChooseDevice()函数可用于选择与一组所需功能最匹配的设备。
当应用程序依赖某些硬件或软件功能的可用性来启用某些功能时,可以查询CUDA API,以获取有关可用设备配置和已安装软件版本的详细信息。
cudaGetDeviceProperties()函数报告可用设备的各种功能,包括设备的CUDA计算能力(另请参见《 CUDA C ++编程指南》的“计算能力”部分)。 有关如何查询可用的CUDA软件API版本的详细信息,请参阅CUDA运行时和驱动程序API版本。
注意:CUDA工具包示例提供了一些帮助程序功能,用于使用各种CUDA API进行错误检查。 这些帮助器功能位于CUDA工具包的sample / common / inc / helper_cuda.h文件中。
每一代具有CUDA功能的设备都有一个关联的计算功能版本,该版本指示该设备支持的功能集(请参阅CUDA计算能力)。 构建文件时,可以为nvcc编译器指定一个或多个计算功能版本; 为应用程序的目标GPU进行本机计算功能的编译对于确保应用程序内核达到最佳性能并能够使用给定一代GPU上可用的功能非常重要。
当同时为多个计算功能构建应用程序时(使用-gencode标志的多个实例到nvcc),用于指定计算功能的二进制文件将合并到可执行文件中,并且CUDA驱动程序会在运行时根据以下内容选择最合适的二进制文件: 本设备的计算能力。 如果没有合适的本机二进制文件(cubin),但是中间的PTX代码(针对抽象虚拟指令集并用于前向兼容)可用,则内核将被编译为即时(JIT)(请参见 从PTX到设备的本机cubin的编译器JIT缓存管理工具)。 如果PTX也不可用,则内核启动将失败
CUDA应用程序是根据CUDA运行时库构建的,该库可处理设备,内存和内核管理。 与CUDA驱动程序不同,CUDA运行时不保证版本之间的向前或向后二进制兼容性。 因此,当使用动态链接或针对CUDA Runtime进行静态链接时,最好将CUDA Runtime库与应用程序一起重新分发。 这将确保即使用户未安装针对其构建应用程序的相同CUDA Toolkit,该可执行文件也将能够运行。
注意:当静态链接到CUDA运行时时,多个版本的运行时可以同时在同一个应用程序进程中共存。 例如,如果某个应用程序使用一个版本的CUDA运行时,并且该应用程序的插件静态链接到其他版本,则只要安装的NVIDIA驱动程序足以满足这两个要求,则完全可以接受
最简单的选择是针对CUDA运行时进行静态链接。 如果使用nvcc链接CUDA 5.5及更高版本,则为默认设置。 静态链接使可执行文件略大,但可以确保在应用程序二进制文件中包含正确版本的运行时库功能,而无需单独重新分发CUDA运行时库。
如果由于某种原因无法对CUDA运行时进行静态链接,则还可以使用CUDA运行时库的动态链接版本。 (这是CUDA 5.0版或更早版本中提供的默认且唯一的选项。)
要使用CUDA 5.5或更高版本中的nvcc链接应用程序时,要在CUDA运行时中使用动态链接,请在链接命令行中添加--cudart = shared标志。 否则,默认情况下将使用静态链接的CUDA运行时库。
在将应用程序与CUDA运行时动态链接后,应将此版本的运行时库与应用程序捆绑在一起。 可以将其复制到与应用程序可执行文件相同的目录中,或复制到该安装路径的子目录中。
尽管CUDA运行时提供了静态链接选项,但CUDA工具包中包含的其他库(cuBLAS,cuFFT等)仅以动态链接的形式提供。 与CUDA运行时库的动态链接版本一样,在分发该应用程序时,这些库应与应用程序可执行文件捆绑在一起。
CUDA工具包的最终用户许可协议(EULA)允许在某些条款和条件下重新分发许多CUDA库。 这允许依赖于这些库的应用程序重新分发针对其构建和测试的库的确切版本,从而避免为可能安装了不同版本CUDA工具包(或根本没有安装)的最终用户带来任何麻烦。 他们的机器。 有关详细信息,请参考EULA。
注意:这不适用于NVIDIA驱动程序。 最终用户仍必须下载并安装适用于其GPU和操作系统的NVIDIA驱动程序
NVIDIA系统管理界面(nvidia-smi)是一个命令行实用程序,可帮助管理和监视NVIDIA GPU设备。 该实用程序允许管理员查询GPU设备状态,并使用适当的权限允许管理员修改GPU设备状态。 nvidia-smi面向Tesla和某些Quadro GPU,尽管其他NVIDIA GPU也提供有限的支持。 nvidia-smi随Linux以及64位Windows Server 2008 R2和Windows 7一起提供了NVIDIA GPU显示驱动程序。nvidia-smi可以将查询的信息以XML或人类可读的纯文本格式输出到标准输出或文件中。 有关详细信息,请参见nvidia-smi文档。 请注意,不保证新版本的nvidia-smi与以前的版本向后兼容
ECC error counts
报告可纠正的一位错误和可检测的两位错误。 为当前引导周期和GPU的生命周期提供了错误计数。
GPU utilization
报告了GPU的计算资源和内存接口的当前利用率。
Active compute process
报告在GPU上运行的活动进程的列表,以及相应的进程名称/ ID和分配的GPU内存。
Clocks and performance state
报告了几个重要时钟域的最大和当前时钟速率,以及当前的GPU性能状态(pstate)。
Temperature and fan speed
报告了当前GPU内核温度,以及具有主动冷却功能的产品的风扇速度。
Power management
报告这些测量结果的产品会报告当前的电路板功耗和功率限制。
Identification
报告各种动态和静态信息,包括板序列号,PCI设备ID,VBIOS / Inforom版本号和产品名称。
ECC mode
关闭和打开ECC报告
ECC reset
清除一位和两位ECC错误计数。
Compute mode
指示计算过程是否可以在GPU上运行,以及它们是否与其他计算过程一起运行或同时运行
Persistence mode
指示在没有应用程序连接到GPU时NVIDIA驱动程序是否保持加载状态。 在大多数情况下,最好启用此选项。
GPU reset
通过辅助总线重置来重新初始化GPU硬件和软件状态
NVIDIA管理库(NVML)是基于C的界面,可直接访问通过nvidia-smi公开的查询和命令,nvidia-smi旨在作为构建第三方系统管理应用程序的平台。 NVML API随CUDA Toolkit(从8.0版开始)一起提供,并且还可以通过单个头文件(随附PDF文档,存根库和示例应用程序)在NVIDIA开发人员网站上作为GPU部署工具包的一部分独立获得; 参见https://developer.nvidia.com/gpu-deployment-kit。 NVML的每个新版本都是向后兼容的。
为NVML API提供了一组额外的Perl和Python绑定。 这些绑定公开与基于C的接口相同的功能,并提供向后兼容性。 Perl绑定通过CPAN提供,Python绑定通过PyPI提供。
所有这些产品(nvidia-smi,NVML和NVML语言绑定)都随每个新的CUDA版本进行更新,并提供大致相同的功能。
有关其他信息,请参见http://developer.nvidia.com/nvidia-management-library-nvml。
管理您的GPU群集将有助于最大程度地利用GPU,并帮助您和您的用户获得最佳性能。 业界最流行的许多群集管理工具都通过NVML支持CUDA GPU。 有关其中一些工具的列表,请参见http://developer.nvidia.com/cluster-management。
应用程序在运行时加载的任何PTX设备代码都会由设备驱动程序进一步编译为二进制代码。 这称为即时编译(JIT)。 即时编译增加了应用程序的加载时间,但允许应用程序受益于最新的编译器改进。 这也是应用程序在编译应用程序时不存在的设备上运行的唯一方法。
使用PTX设备代码的JIT编译时,NVIDIA驱动程序将生成的二进制代码缓存在磁盘上。 可以通过使用环境变量来控制此行为的某些方面,例如缓存位置和最大缓存大小。 请参阅《 CUDA C ++编程指南》的“及时编译”。
可以通过CUDA_VISIBLE_DEVICES环境变量来重新排列已安装的CUDA设备的集合,该集合在该应用程序启动之前对CUDA应用程序可见并枚举。
就系统范围内的可枚举设备列表而言,应将应用程序可见的设备作为逗号分隔的列表包括在内。 例如,要仅使用系统范围的设备列表中的设备0和2,请在启动应用程序之前设置CUDA_VISIBLE_DEVICES = 0,2。 然后,应用程序会将这些设备枚举为设备0和设备1。
本附录包含本文档中说明的优化建议摘要。
性能优化围绕三个基本策略:
(1)最大化并行执行
(2)优化内存使用率以实现最大内存带宽
(3)优化指令使用率以实现最大指令吞吐量
最大限度地提高并行执行能力始于以尽可能多的并行性来构造算法的结构。 一旦公开了算法的并行性,就需要将其尽可能高效地映射到硬件。 这是通过仔细选择每个内核启动的执行配置来完成的。 通过通过流显式公开设备上的并发执行,以及使主机与设备之间的并发执行最大化,应用程序还应在更高级别上最大化并行执行。
优化内存使用首先要最小化主机与设备之间的数据传输,因为这些传输的带宽比内部设备数据传输的带宽低得多。 还应该通过最大程度地使用设备上的共享内存来最小化对全局内存的内核访问。 有时,最佳的优化甚至可能是通过只在需要时重新计算数据来避免任何数据传输。
有效带宽可以根据每种类型的存储器的访问模式而变化一个数量级。 因此,优化内存使用率的下一步是根据最佳内存访问模式来组织内存访问。 这种优化对于全局存储器访问尤为重要,因为访问延迟会花费数百个时钟周期。 相反,共享内存访问通常仅在存在高度存储体冲突时才值得优化。
至于优化指令的使用,应避免使用吞吐量低的算术指令。 这建议在不影响最终结果的情况下以速度为代价进行精度交换,例如使用内在函数而不是常规函数或使用单精度而不是双精度。 最后,由于设备的SIMT(单指令多线程)特性,必须特别注意控制流指令。
NVIDIA nvcc编译器驱动程序将.cu文件转换为主机系统和设备的CUDA汇编或二进制指令的C ++。 它支持许多命令行参数,其中的以下参数对于优化和相关最佳实践特别有用:
-maxrregcount = N指定内核在每个文件级别可以使用的最大寄存器数。 请参阅Register Pressure.。 (另请参见《 CUDA C ++编程指南》的“执行配置”中讨论的__launch_bounds__限定符,以控制每个内核使用的寄存器数。)
--ptxas-options = -v或-Xptxas = -v列出每个内核的寄存器,共享的和恒定的内存使用情况。
-ftz = true(将规范化的数字刷新为零)
-prec-div = false(精确度较低的除法)
-prec-sqrt = false(精确度较低的平方根)
nvcc的-use_fast_math编译器选项将每个functionName()调用强制转换为等效的__functionName()调用。 这使代码运行更快,但代价是精度和准确性降低。 请参阅数学库。
权利声明的一些内容