CUDA 内存统一分析
关于CUDA 编程的基本知识,如何编写一个简单的程序,在内存中分配两个可供 GPU 访问的数字数组,然后将它们加在 GPU 上。
本文介绍内存统一,这使得分配和访问系统中任何处理器上运行的代码都可以使用的数据变得非常容易, CPU 或 GPU 。

图 1 .内存统一是可从系统中的任何处理器访问的单个内存地址空间。
以几个简单的“练习”介绍,其中一个练习,运行最近基于 Pascal 的 GPU ,看看会发生什么。
建议这样做有两个原因。首先,因为 PascalMIG 如 NVIDIA Titan X 和 NVIDIA Tesla P100 是第一个包含页 GPUs 定额引擎的 GPUs ,它是内存统一页错误处理和 MIG 比率的硬件支持。第二个原因是提供了一个很好的机会来学习更多的内存统一。
快 GPU ,快内存…对吗?
正确的!首先,我将重新打印在两个 NVIDIA 开普勒 GPUs 上运行的结果(一个在笔记本电脑上,一个在服务器上)。

现在尝试在一个非常快的 Tesla P100 加速器上运行,它基于 pascalgp100GPU 。

nvprof ./add_grid … Time(%) Time Calls Avg Min Max Name 100.00% 2.1192ms 1 2.1192ms 2.1192ms 2.1192ms add(int, float*, float*)
这低于 6gb / s :比在笔记本电脑基于开普勒的 GeForceGPU 上运行慢。不过,别灰心,可以解决这个问题的。为了理解这一点,将介绍更多关于内存统一的信息。
下面是要添加的完整代码,以供参考_网格. cu 从上次开始。
#include #include <math.h> // CUDA kernel to add elements of two arrays global void add(int n, float x, float y) { int index = blockIdx.x * blockDim.x + threadIdx.x; int stride = blockDim.x * gridDim.x; for (int i = index; i < n; i += stride) y[i] = x[i] + y[i]; } int main(void) { int N = 1<<20; float x, y; // Allocate Unified Memory – accessible from CPU or GPU cudaMallocManaged(&x, Nsizeof(float)); cudaMallocManaged(&y, Nsizeof(float)); // initialize x and y arrays on the host for (int i = 0; i < N; i++) { x[i] = 1.0f; y[i] = 2.0f; } // Launch kernel on 1M elements on the GPU int blockSize = 256; int numBlocks = (N + blockSize - 1) / blockSize; add<<<numBlocks, blockSize>>>(N, x, y); // Wait for GPU to finish before accessing on host cudaDeviceSynchronize(); // Check for errors (all values should be 3.0f) float maxError = 0.0f; for (int i = 0; i < N; i++) maxError = fmax(maxError, fabs(y[i]-3.0f)); std::cout << "Max error: " << maxError << std::endl; // Free memory cudaFree(x); cudaFree(y); return 0; }
对 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 内核的性能。

  1. 将数据初始化移动到另一个 CUDA 内核中的 GPU 。
  2. 多次运行内核,查看平均和最小运行时间。
  3. 在运行内核之前,将数据预取到 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, Nsizeof(float), device, NULL); cudaMemPrefetchAsync(y, Nsizeof(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 评级。
下一步?
本文帮助继续学习 CUDA 编程,并且有兴趣学习更多,并在计算中应用 CUDA C ++。
有关内存统一预取和使用提示( cudaMemAdvise() )的更多信息,请参阅文章
在 Pascal 上使用内存统一超出 GPU 内存限制 。如果想了解使用 cudaMemcpy 和 cudaMemcpy 在 CUDA 中进行显式内存管理的信息,请参阅文章 CUDA C / C ++的简单介绍 。
计划用更多的 CUDA 编程材料来跟进本文,可以继续阅读一系列比较老的介绍性文章。
• 如何在 CUDA C ++中实现性能度量
• 如何查询 CUDA C ++中的设备属性和处理错误
• 如何优化 CUDA C ++中的数据传输
• 如何在 CUDA C ++中重叠数据传输
• 如何在 CUDA C ++中高效访问全局内存
• 在 CUDA C ++中使用共享内存
• CUDA C ++中的一种高效矩阵转置
• CUDA C ++中的有限差分方法,第 1 部分
• CUDA C ++中的有限差分方法,第 2 部分
还有一系列的设备。
从技术上讲,这是一种简化。在带有 pre-Pascal GPUs 的 multi-GPU 系统上,如果某些 GPUs 禁用了对等访问,则将分配内存,使其最初驻留在 CPU 上。
严格地说,可以使用 cudaStreamAttachMemAsync() 将分配的可见性限制到特定的 CUDA 流。这允许驱动程序 MIG 只对附加到启动内核的流的页面进行评级。默认情况下,托管分配附加到所有流,因此任何内核启动都会触发 MIG 配额。 请阅读 CUDA 编程指南中的更多内容 。
设备属性 concurrentManagedAccess 说明 GPU 是否支持硬件页 MIG 比率以及它所启用的并发访问功能。值为 1 表示支持。目前,它只在运行 64 位 Linux 的 Pascal 和更新的 GPUs 上受支持。

CUDA 内存统一分析相关推荐

  1. 【GPU】Nvidia CUDA 编程基础教程——利用基本的 CUDA 内存管理技术来优化加速应用程序

    博主未授权任何人或组织机构转载博主任何原创文章,感谢各位对原创的支持! 博主链接 本人就职于国际知名终端厂商,负责modem芯片研发. 在5G早期负责终端数据业务层.核心网相关的开发工作,目前牵头6G ...

  2. CUDA技术体系分析

    CUDA技术体系分析 CUDA(Compute Unified Device Architecture)是一个新的基础架构,这个架构可以使用GPU来解决商业.工业以及科学方面的复杂计算问题.一个完整的 ...

  3. CUDA内存管理一文理清|参加CUDA线上训练营

    CUDA 内存概述 GPU的内存包括: 全局内存(global memory) 常量内存(constant memory) 纹理内存核表面内存(texture memory) 寄存器(register ...

  4. 利用MAT进行内存泄漏分析

    ##前言 对于程序员来说码代码容易,保证代码的稳定性很难.有时候写完一个功能可能只需要一天时间,但是这个功能隐藏的bug导致的线上问题排查可能需要一周或者更长时间.因此,拥有良好的代码结构和编码规范是 ...

  5. Android MVP(三)内存泄漏分析与动态代理

    博主声明: 转载请在开头附加本文链接及作者信息,并标记为转载.本文由博主 威威喵 原创,请多支持与指教. 本文首发于此   博主:威威喵  |  博客主页:https://blog.csdn.net/ ...

  6. 内存泄漏分析框架LeakCanary的使用与原理解析

    文章目录 1. 常见内存泄漏 1.1 "单例模式" 造成的内存泄漏 1.2 "静态实例" 造成内存泄漏 1.3 "Handler" 造成的内 ...

  7. Oracle内存全面分析(转)

    Oracle的内存配置与oracle性能息息相关.而且关于内存的错误(如4030.4031错误)都是十分令人头疼的问题.可以说,关于内存的配置,是最影响Oracle性能的配置.内存还直接影响到其他两个 ...

  8. 转自美团技术博客的jvm内存泄露分析

    Linux与JVM的内存关系分析 引言 在一些物理内存为8g的服务器上,主要运行一个Java服务,系统内存分配如下:Java服务的JVM堆大小设置为6g,一个监控进程占用大约600m,Linux自身使 ...

  9. 利用MAT进行内存泄露分析

    前言 对于程序员来说码代码容易,保证代码的稳定性很难.有时候写完一个功能可能只需要一天时间,但是这个功能隐藏的bug导致的线上问题排查可能需要一周或者更长时间.因此,拥有良好的代码结构和编码规范是一个 ...

最新文章

  1. position 再谈
  2. 微博收藏(机器学习代码与工具)(一)
  3. uboot 命令使用
  4. python嵌入到qt_在Qt图形用户界面中嵌入绘图
  5. python递归创建目录_Node.js和Python使用递归查看目录文件和创建目录
  6. 编程之美2014 资格赛题目3 : 格格取数
  7. Collectiont和Collections的区别
  8. 《移动平台应用开发实践》教学进程(12周)
  9. forEach遍历对象数组案例
  10. C# WinForm技巧“将Form嵌入到Panel”
  11. 内网通修改积分文件_【页游逆向】4399小游戏积分系统分析及修改积分
  12. 南京大学2020计算机考研分数线,2020南京大学考研复试分数线已公布
  13. 数据同步工具kettle
  14. windows10 wifi热点手机连接显示无网络连接问题解决
  15. 信息系统项目管理师2018年上半年下午案例分析题及答案
  16. noise levels descript
  17. 【MySQL】源码编译MySQL8.x+升级gcc+升级cmake(亲测完整版)
  18. 从SAP中查找BADI
  19. CSS样式实现选择按钮
  20. 解决Git add无法提交,报错fatal : LF would be replaced by CRLF...

热门文章

  1. IDEA自定义快捷指令,快捷生成代码、注释
  2. java命令行读入密码_java-在命令行上隐藏输入
  3. 错误提示没了_ESC错误排查-系统启动篇
  4. 2022-2028年中国PE膜产业竞争现状及发展前景分析报告
  5. ionic4中实现时间线
  6. 带你彻彻底底搞懂朴素贝叶斯公式
  7. Python标准库——collections模块的Counter类
  8. LeetCode简单题之找到最近的有相同 X 或 Y 坐标的点
  9. 3D点云点云分割、目标检测、分类
  10. [JAVAEE] Thymeleaf 基本语法: 迭代循环