CUDA统一内存分析
CUDA统一内存分析
PascalMIG 如 NVIDIA Titan X 和 NVIDIA Tesla P100 是第一个包含页 GPUs 定额引擎的 GPUs ,它是统一内存页错误处理和 MIG 比率的硬件支持。提供了一个很好的机会来学习更多的统一内存。
快 GPU ,快内存…对吗?
正确的!
首先,将重新打印在两个 NVIDIA 开普勒 GPUs 上运行的结果(一个在笔记本电脑上,一个在服务器上)。
现在尝试在一个非常快的 Tesla P100 加速器上运行,它基于pascalgp100GPU 。
低于 6gb / s :比在笔记本电脑基于开普勒的 GeForceGPU 上运行慢。不过,可以解决这个问题的。为了理解这一点,将介绍更多关于统一内存的信息。
下面是要添加的完整代码,以供参考_网格. cu 从上次开始。
对 27-19 行的内存进行初始化。
什么是统一内存?
统一内存是可从系统中的任何处理器访问的单个内存地址空间(请参见图 1 )。这种硬件/软件技术允许应用程序分配可以从 CPU s 或 GPUs 上运行的代码读取或写入的数据。分配统一内存非常简单,只需将对 malloc() 或 new 的调用替换为对 cudaMallocManaged() 的调用,这是一个分配函数,返回可从任何处理器访问的指针(以下为 ptr )。
cudaError_t cudaMallocManaged(void** ptr, size_t size);
当在 CPU 或 GPU 上运行的代码访问以这种方式分配的数据(通常称为 CUDA 管理 数据), CUDA 系统软件和/或硬件负责将 MIG 额定内存页分配给访问处理器的内存。这里重要的一点是, PascalGPU 体系结构是第一个通过页面 MIG 比率引擎对虚拟内存页错误处理和 MIG 比率提供硬件支持的架构。基于早期的 kezbr 架构和更为统一的 kezbr 形式的支持。
调用 cudaMallocManaged() 时,开普勒会发生什么?
在具有 pre-PascalGPUs 的系统上,如 Tesla K80 ,调用 cudaMallocManaged() 会分配 size 字节的托管内存 在 GPU 设备上 ,该内存在调用 1 时处于活动状态。在内部,驱动程序还为分配覆盖的所有页面设置页表条目,以便系统知道这些页驻留在 GPU 上。
在 Tesla K80GPU (开普勒架构)上运行, x 和 y 最初都完全驻留在 GPU 内存中。然后在第 6 行开始的循环中, CPU 逐步遍历两个数组,分别将元素初始化为 1.0f 和 2.0f 。由于这些页最初驻留在设备存储器中,所以它写入的每个数组页的 CPU 上都会发生一个页错误, GPU 驱动程序 MIG 会将设备内存中的页面分配给 CPU 内存。循环之后,两个数组的所有页都驻留在 CPU 内存中。
在初始化 CPU 上的数据之后,程序启动 add() 内核,将 x 的元素添加到 y 的元素中。
add<<<1, 256>>>(N, x, y);
在 pre-PascalGPUs 上,启动一个内核后, CUDA 运行时必须 MIG, 将以前 MIG 额定为主机内存或另一个 GPU 的所有页面重新评级到运行内核 2 的设备内存。由于这些早期的 GPUs 不能出现分页错误,所有数据都必须驻留在 GPU 以防万一,内核访问它(即使它不会访问)。这意味着每次启动内核时都可能存在 MIG 定额开销。
当在 K80 或 macbookpro 上运行程序时,就会发生这种情况。只是探查器显示的内核运行时间与 MIG 定额时间是分开的,因为 MIG 定额发生在内核运行之前。
==15638== Profiling application: ./add_grid ==15638== Profiling result: Time(%) Time Calls Avg Min Max Name 100.00% 93.471us 1 93.471us 93.471us 93.471us add(int, float*, float*) ==15638== Unified Memory profiling result: Device "Tesla K80 (0)" Count Avg Size Min Size Max Size Total Size Total Time Name 6 1.3333MB 896.00KB 2.0000MB 8.000000MB 1.154720ms Host To Device 102 120.47KB 4.0000KB 0.9961MB 12.00000MB 1.895040ms Device To Host Total CPU Page faults: 51
调用 cudaMallocManaged() 时, Pascal 上会发生什么?
在 Pascal 和更高版本的 GPUs 上, cudaMallocManaged() 返回时可能不会物理分配托管内存;它只能在访问(或预取)时填充。换言之,在 GPU 或 CPU 访问页和页表项之前,可能无法创建。页面可以在任何时候对任何处理器的内存进行 cudaMemPrefetchAsync() 速率,驱动程序使用启发式来维护数据的局部性并防止过多的页面错误 3 。(注意:应用程序可以使用 cudaMemAdvise() 指导驱动程序,并使用 MIG 显式地 MIG 对内存进行速率调整)。
与 pre-PascalGPUs 不同, Tesla P100 支持硬件页错误和 MIG 比率。所以,运行库在运行内核之前不会自动将全部的页面复制回 GPU 。内核在没有任何 MIG 定额开销的情况下启动,当它访问任何缺失的页时, GPU 会暂停访问线程的执行,页面 MIG 定额引擎 MIG 会在恢复线程之前对设备的页面进行评级。
这意味着在 Tesla P100 ( 2 . 1192ms )上运行程序时, MIG 定额的成本包含在内核运行中。在这个内核中,数组中的每一页都由 CPU 写入,然后由 GPU 上的 CUDA 内核访问,导致内核等待大量的页 MIG 配额。这就是为什么分析器在像 Tesla P100 这样的 PascalGPU 上测量的内核时间更长。看看 P100 上程序的完整 nvprof 输出。
==19278== Profiling application: ./add_grid ==19278== Profiling result: Time(%) Time Calls Avg Min Max Name 100.00% 2.1192ms 1 2.1192ms 2.1192ms 2.1192ms add(int, float*, float*) ==19278== Unified Memory profiling result: Device "Tesla P100-PCIE-16GB (0)" Count Avg Size Min Size Max Size Total Size Total Time Name 146 56.109KB 4.0000KB 988.00KB 8.000000MB 860.5760us Host To Device 24 170.67KB 4.0000KB 0.9961MB 4.000000MB 339.5520us Device To Host 12 - - - - 1.067526ms GPU Page fault groups Total CPU Page faults: 36
存在许多主机到设备页面错误,降低了 CUDA 内核的吞吐量。
怎么办?
在实际应用中, GPU 可能会在数据上执行更多的计算(可能多次),而不需要 CPU 来接触它。这个简单代码中的 MIG 定额开销是由于 CPU 初始化数据, GPU 只使用一次。有几种不同的方法可以消除或更改 MIG 比率开销,从而更准确地测量 vector add 内核的性能。
- 将数据初始化移动到另一个 CUDA 内核中的 GPU 。
- 多次运行内核,查看平均和最小运行时间。
- 在运行内核之前,将数据预取到 GPU 内存。
看看这三种方法。
初始化内核中的数据
如果我们将初始化从 CPU 移到 GPU ,则 add 内核不会出现页面错误。这里有一个简单的 CUDA C ++内核来初始化数据。可以用启动这个内核来替换初始化 x 和 y 的主机代码。
__global__ void init(int n, float *x, float *y) { int index = threadIdx.x + blockIdx.x * blockDim.x; int stride = blockDim.x * gridDim.x; for (int i = index; i < n; i += stride) { x[i] = 1.0f; y[i] = 2.0f; } }
在 Tesla P100GPU 的配置文件中看到两个内核:
==44292== Profiling application: ./add_grid_init ==44292== Profiling result: Time(%) Time Calls Avg Min Max Name 98.06% 1.3018ms 1 1.3018ms 1.3018ms 1.3018ms init(int, float*, float*) 1.94% 25.792us 1 25.792us 25.792us 25.792us add(int, float*, float*) ==44292== Unified Memory profiling result: Device "Tesla P100-PCIE-16GB (0)" Count Avg Size Min Size Max Size Total Size Total Time Name 24 170.67KB 4.0000KB 0.9961MB 4.000000MB 344.2880us Device To Host 16 - - - - 551.9940us GPU Page fault groups Total CPU Page faults: 12
add 内核现在运行得更快: 25 . 8us ,相当于接近 500gb / s 。
带宽=字节/秒=( 3 * 4194304 字节* 1e-9 字节/ GB )/ 25 . 8e-6s = 488 [UNK] GB / s
仍然存在设备到主机页错误,但这是由于在程序末尾检查 CPU 结果的循环造成的。
运行多次
另一种方法是只运行内核多次,并查看探查器中的平均时间。为此,需要修改错误检查代码,以便正确报告结果。以下是在 Tesla P100 上 100 次运行内核的结果:
==48760== Profiling application: ./add_grid_many ==48760== Profiling result: Time(%) Time Calls Avg Min Max Name 100.00% 4.5526ms 100 45.526us 24.479us 2.0616ms add(int, float*, float*) ==48760== Unified Memory profiling result: Device "Tesla P100-PCIE-16GB (0)" Count Avg Size Min Size Max Size Total Size Total Time Name 174 47.080KB 4.0000KB 0.9844MB 8.000000MB 829.2480us Host To Device 24 170.67KB 4.0000KB 0.9961MB 4.000000MB 339.7760us Device To Host 14 - - - - 1.008684ms GPU Page fault groups Total CPU Page faults: 36
最短的内核运行时间只有 24 . 5 微秒,意味着可以获得超过 500GB / s 的内存带宽。来自 nvprof 的统一内存分析输出,它显示了从主机到设备总共 8MB 的页面错误,对应于第一次运行 add 时通过页面错误复制到设备上的两个 4MB 数组( x 和 y )。
预取
第三种方法是在初始化后使用统一内存预取将数据移动到 GPU 。 CUDA 为此提供了 cudaMemPrefetchAsync() 。可以在内核启动之前添加以下代码。
// Prefetch the data to the GPU int device = -1; cudaGetDevice(&device); cudaMemPrefetchAsync(x, N*sizeof(float), device, NULL); cudaMemPrefetchAsync(y, N*sizeof(float), device, NULL); // Run kernel on 1M elements on the GPU int blockSize = 256; int numBlocks = (N + blockSize - 1) / blockSize; saxpy<<<numBlocks, blockSize>>>(N, 1.0f, x, y);
在 Tesla P100 上评测时,得到以下输出。
==50360== Profiling application: ./add_grid_prefetch ==50360== Profiling result: Time(%) Time Calls Avg Min Max Name 100.00% 26.112us 1 26.112us 26.112us 26.112us add(int, float*, float*) ==50360== Unified Memory profiling result: Device "Tesla P100-PCIE-16GB (0)" Count Avg Size Min Size Max Size Total Size Total Time Name 4 2.0000MB 2.0000MB 2.0000MB 8.000000MB 689.0560us Host To Device 24 170.67KB 4.0000KB 0.9961MB 4.000000MB 346.5600us Device To Host Total CPU Page faults: 36
在这里,可以看到内核只运行了一次,运行时间为 26 . 1us ,与前面显示的 100 次运行中最快的一次相似。还可以看到,不再报告任何 GPU 页错误,主机到设备的传输显示为四个 2MB 的传输,这要归功于预取。
在 P100 上运行得很快,将它添加到上次的结果表中。
并发性的分析
系统有多个处理器同时运行 CUDA 应用程序的部分:一个或多个 CPU 和一个或多个 GPUs 。这个简单的例子中,有一个 CPU 线程和一个 GPU 执行上下文,因此在访问任何一个处理器上的托管分配时都要小心,以确保没有竞争条件。
从计算能力低于 6 . 0 的 CPU 和 GPUs, 同时访问托管内存是不可能的。这是因为 pre-Pascal GPUs 缺少硬件页面错误,所以不能保证一致性。在这些 GPUs 上,内核运行时从 CPU 访问将导致分段错误。
在 Pascal 和更高版本的 GPUs 上, CPU 和 GPU 可同时访问托管内存,都可以处理页错误;但是,由应用程序开发人员来确保不存在由同时访问引起的竞争条件。
在内核启动后调用了 cudaDeviceSynchronize() 。可确保内核在 CPU 尝试从托管内存指针读取结果之前运行到完成。否则, CPU 可能会读取无效数据(在 Pascal 和更高版本上),或获得分段错误(在 pre-Pascal GPUs )。
Pascal 及更高版本上统一内存的好处 GPUs
从 PascalGPU 体系结构开始,通过 49 位虚拟寻址和按需分页 GPU 比率,统一内存功能得到了显著改善。 49 位虚拟地址足以使 GPUs 访问整个系统内存加上系统中所有 GPUs 的内存。页面 MIG 比率引擎允许 GPU 线程在非驻留内存访问时出现故障,因此系统可以根据需要从系统中的任何位置对 MIG 的内存中的页面进行 MIG 分级,以实现高效处理。
使用统一内存 cudaMallocManaged() 对统一内存进行分配。无论是在一个 GPU 上运行还是在多个 GPU 上运行,都不会对应用程序进行任何修改。
另外, Pascal 和 VoltaGPUs 支持系统范围的原子内存操作。意味着可以对系统中任何地方的多个 GPUs 值进行原子操作。这对于编写高效的 multi-GPU 协作算法非常有用。
请求分页对于以稀疏模式访问数据的应用程序尤其有利。在某些应用程序中,不知道特定处理器将访问哪些特定内存地址。如果没有硬件页面错误,应用程序只能预加载整个阵列,或者承受设备外访问的高延迟成本(也称为“零拷贝”)。但是页面错误意味着只有内核访问的页面需要被 MIG 评级。