0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

通过使用CUDA GPU共享内存

星星科技指导员 来源:NVIDIA 作者:Mark Harris 2022-04-11 10:03 次阅读

共享内存是编写优化良好的 CUDA 代码的一个强大功能。共享内存的访问比全局内存访问快得多,因为它位于芯片上。因为共享内存由线程块中的线程共享,它为线程提供了一种协作机制。利用这种线程协作使用共享内存的一种方法是启用全局内存合并,如本文中的数组反转所示。通过使用 CUDA GPU 共享内存,我们可以在 GPU 上执行所有读操作。在下一篇文章中,我将通过使用共享内存来优化矩阵转置来继续我们的讨论。


在 上一篇文章 中,我研究了如何将一组线程访问的全局内存合并到一个事务中,以及对齐和跨步如何影响 CUDA 各代硬件的合并。对于最新版本的 CUDA 硬件,未对齐的数据访问不是一个大问题。然而,不管 CUDA 硬件是如何产生的,在全局内存中大步前进都是有问题的,而且在许多情况下似乎是不可避免的,例如在访问多维数组中沿第二个和更高维的元素时。但是,在这种情况下,如果我们使用共享内存,就可以合并内存访问。在我在下一篇文章中向您展示如何避免跨越全局内存之前,首先我需要详细描述一下共享内存。

共享内存

因为它是片上的,共享内存比本地和全局内存快得多。实际上,共享内存延迟大约比未缓存的全局内存延迟低 100 倍(前提是线程之间没有内存冲突,我们将在本文后面讨论这个问题)。共享内存是按线程块分配的,因此块中的所有线程都可以访问同一共享内存。线程可以访问由同一线程块中的其他线程从全局内存加载的共享内存中的数据。此功能(与线程同步结合)有许多用途,例如用户管理的数据缓存、高性能的协作并行算法(例如并行缩减),以及在不可能实现全局内存合并的情况下促进全局内存合并。

线程同步

在线程之间共享数据时,我们需要小心避免争用情况,因为虽然块中的线程并行运行 逻辑上 ,但并非所有线程都可以同时执行 身体上 。假设两个线程 A 和 B 分别从全局内存加载一个数据元素并将其存储到共享内存中。然后,线程 A 想从共享内存中读取 B 的元素,反之亦然。我们假设 A 和 B 是两个不同翘曲中的线。如果 B 在 A 尝试读取它之前还没有完成它的元素的编写,我们就有一个竞争条件,它可能导致未定义的行为和错误的结果。

为了保证并行线程协作时的正确结果,必须同步线程。 CUDA 提供了一个简单的屏障同步原语 __syncthreads() 。一个线程的执行只能在其块中的所有线程都执行了 __syncthreads() 之后通过 __syncthreads() 继续执行。因此,我们可以通过在存储到共享内存之后和从共享内存加载任何线程之前调用 __syncthreads() 来避免上面描述的竞争条件。需要注意的是,在发散代码中调用 __syncthreads() 是未定义的,并且可能导致死锁,线程块中的所有线程都必须在同一点调用 __syncthreads()

共享内存示例

使用 Clara 变量 D __shared__ 指定说明符在 CUDA C / C ++设备代码中声明共享内存。在内核中声明共享内存有多种方法,这取决于内存量是在编译时还是在运行时已知的。下面的完整代码( 在 GitHub 上提供 )演示了使用共享内存的各种方法。

#include __global__ void staticReverse(int *d, int n)
{ __shared__ int s[64]; int t = threadIdx.x; int tr = n-t-1; s[t] = d[t]; __syncthreads(); d[t] = s[tr];
} __global__ void dynamicReverse(int *d, int n)
{ extern __shared__ int s[]; int t = threadIdx.x; int tr = n-t-1; s[t] = d[t]; __syncthreads(); d[t] = s[tr];
} int main(void)
{ const int n = 64; int a[n], r[n], d[n]; for (int i = 0; i < n; i++) { a[i] = i; r[i] = n-i-1; d[i] = 0; } int *d_d; cudaMalloc(&d_d, n * sizeof(int)); // run version with static shared memory cudaMemcpy(d_d, a, n*sizeof(int), cudaMemcpyHostToDevice); staticReverse<<<1,n>>>(d_d, n); cudaMemcpy(d, d_d, n*sizeof(int), cudaMemcpyDeviceToHost); for (int i = 0; i < n; i++) if (d[i] != r[i]) printf("Error: d[%d]!=r[%d] (%d, %d)n", i, i, d[i], r[i]); // run dynamic shared memory version cudaMemcpy(d_d, a, n*sizeof(int), cudaMemcpyHostToDevice); dynamicReverse<<<1,n,n*sizeof(int)>>>(d_d, n); cudaMemcpy(d, d_d, n * sizeof(int), cudaMemcpyDeviceToHost); for (int i = 0; i < n; i++) if (d[i] != r[i]) printf("Error: d[%d]!=r[%d] (%d, %d)n", i, i, d[i], r[i]); 

}此代码使用共享内存反转 64 元素数组中的数据。这两个内核非常相似,只是在共享内存数组的声明方式和内核的调用方式上有所不同。

静态共享内存

如果共享内存数组大小在编译时已知,就像在 staticReverse 内核中一样,那么我们可以显式地声明一个该大小的数组,就像我们对数组 s 所做的那样。

__global__ void staticReverse(int *d, int n)
{ __shared__ int s[64]; int t = threadIdx.x; int tr = n-t-1; s[t] = d[t]; __syncthreads(); d[t] = s[tr];

}在这个内核中, ttr 是分别表示原始顺序和反向顺序的两个索引。线程使用语句 s[t] = d[t] 将数据从全局内存复制到共享内存,然后在两行之后使用语句 d[t] = s[tr] 完成反转。但是在执行最后一行之前,每个线程访问共享内存中由另一个线程写入的数据,请记住,我们需要通过调用 __syncthreads() 来确保所有线程都已完成对共享内存的加载。

在这个例子中使用共享内存的原因是为了在旧的 CUDA 设备(计算能力 1 . 1 或更早版本)上促进全局内存合并。由于全局内存总是通过线性对齐索引 t 访问,所以读写都可以实现最佳的全局内存合并。反向索引 tr 仅用于访问共享内存,它不具有全局内存的顺序访问限制以获得最佳性能。共享内存的唯一性能问题是银行冲突,我们将在后面讨论。(请注意,在计算能力为 1 . 2 或更高版本的设备上,内存系统甚至可以将反向索引存储完全合并到全局内存中。但是这种技术对于其他访问模式仍然有用,我将在下一篇文章中展示。)

动态共享内存

本例中的其他三个内核使用动态分配的共享内存,当编译时共享内存的数量未知时,可以使用该内存。在这种情况下,必须使用可选的第三个执行配置参数指定每个线程块的共享内存分配大小(以字节为单位),如下面的摘录所示。

dynamicReverse<<<1, n, n*sizeof(int)>>>(d_d, n);

动态共享内存内核 dynamicReverse() 使用未大小化的外部数组语法 extern shared int s[] 声明共享内存数组(注意空括号和 extern 说明符的使用)。大小在内核启动时由第三个执行配置参数隐式确定。内核代码的其余部分与 staticReverse() 内核相同。

如果在一个内核中需要多个动态大小的数组怎么办?您必须像前面一样声明一个 extern 非大小数组,并使用指向它的指针将其划分为多个数组,如下面的摘录所示。

extern __shared__ int s[];
int *integerData = s; // nI ints
float *floatData = (float*)&integerData[nI]; // nF floats
char *charData = (char*)&floatData[nF]; // nC chars

在内核中指定启动所需的总内存。

myKernel<<>>(...);

共享内存库冲突

为了实现并发访问的高内存带宽,共享内存被分成大小相等的内存模块(库),这些模块可以同时访问。因此,任何跨越 b 不同内存组的 n 地址的内存负载或存储都可以同时进行服务,从而产生的有效带宽是单个存储库带宽的 b 倍。

但是,如果多个线程的请求地址映射到同一个内存库,则访问将被序列化。硬件根据需要将冲突内存请求拆分为多个独立的无冲突请求,将有效带宽减少一个与冲突内存请求数量相等的因子。一个例外情况是,一个 warp 中的所有线程都使用同一个共享内存地址,从而导致广播。计算能力 2 . 0 及更高版本的设备具有多播共享内存访问的额外能力,这意味着在一个 warp 中通过任意数量的线程对同一个位置的多个访问同时进行。

为了最小化内存冲突,了解内存地址如何映射到内存库是很重要的。共享存储库被组织成这样,连续的 32 位字被分配给连续的存储库,带宽是每个库每个时钟周期 32 位。对于计算能力为 1 . x 的设备, warp 大小为 32 个线程,库的数量为 16 个。一个 warp 的共享内存请求被分为一个对 warp 前半部分的请求和一个对 warp 后半部分的请求。请注意,如果每个内存库只有一个内存位置被半个线程访问,则不会发生库冲突。

对于计算能力为 2 . 0 的设备, warp 大小是 32 个线程,而 bank 的数量也是 32 个。 warp 的共享内存请求不会像计算能力为 1 . x 的设备那样被拆分,这意味着 warp 前半部分的线程和同一 warp 后半部分的线程之间可能会发生库冲突。

计算能力为 3 . x 的设备具有可配置的存储大小,可以使用 CUDA Devicsetsharedmeconfig() 将其设置为四个字节( CUDA SharedMemBankSizeFourByte ,默认值)或八个字节( cudaSharedMemBankSizeEightByte) 。将存储大小设置为 8 字节有助于避免访问双精度数据时的共享内存库冲突。

配置共享内存量

在计算能力为 2 . x 和 3 . x 的设备上,每个多处理器都有 64KB 的片上内存,可以在一级缓存和共享内存之间进行分区。对于计算能力为 2 . x 的设备,有两个设置: 48KB 共享内存/ 16KB 一级缓存和 16KB 共享内存/ 48KB 一级缓存。默认情况下,使用 48KB 共享内存设置。这可以在运行时 API 期间使用 cudaDeviceSetCacheConfig() 为所有内核配置,也可以使用 cudaFuncSetCacheConfig() 在每个内核的基础上进行配置。它们接受以下三个选项之一: cudaFuncCachePreferNonecudaFuncCachePreferSharedcudaFuncCachePreferL1 。驱动程序将遵循指定的首选项,除非内核每个线程块需要比指定配置中可用的共享内存更多的共享内存。计算能力为 3 . x 的设备允许使用选项 cudaFuncCachePreferEqual 获得 32KB 共享内存/ 32kbl1 缓存的第三个设置。

关于作者

Mark Harris 是 NVIDIA 杰出的工程师,致力于 RAPIDS 。 Mark 拥有超过 20 年的 GPUs 软件开发经验,从图形和游戏到基于物理的模拟,到并行算法和高性能计算。当他还是北卡罗来纳大学的博士生时,他意识到了一种新生的趋势,并为此创造了一个名字: GPGPU (图形处理单元上的通用计算)。

审核编辑:郭婷

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    19155

    浏览量

    229057
  • NVIDIA
    +关注

    关注

    14

    文章

    4929

    浏览量

    102791
收藏 人收藏

    评论

    相关推荐

    【「算力芯片 | 高性能 CPU/GPU/NPU 微架构分析」阅读体验】--了解算力芯片GPU

    每个CUDA单元在 OpenCL 编程框架中都有对应的单元。 倒金字塔结构GPU存储体系 共享内存是开发者可配置的编程资源,使用门槛较高,编程上需要更多的人工显式处理。 在并行计算架构
    发表于 11-03 12:55

    有没有大佬知道NI vision 有没有办法通过gpucuda来加速图像处理

    有没有大佬知道NI vision 有没有办法通过gpucuda来加速图像处理
    发表于 10-20 09:14

    打破英伟达CUDA壁垒?AMD显卡现在也能无缝适配CUDA

    电子发烧友网报道(文/梁浩斌)一直以来,围绕CUDA打造的软件生态,是英伟达在GPU领域最大的护城河,尤其是随着目前AI领域的发展加速,市场火爆,英伟达GPU+CUDA的开发生态则更加稳固,AMD
    的头像 发表于 07-19 00:16 4507次阅读

    英国公司实现英伟达CUDA软件在AMD GPU上的无缝运行

    7月18日最新资讯,英国创新科技企业Spectral Compute震撼发布了其革命性GPGPU编程工具包——“SCALE”,该工具包实现了英伟达CUDA软件在AMD GPU上的无缝迁移与运行,标志着在GPU计算领域,NVIDI
    的头像 发表于 07-18 14:40 592次阅读

    软件生态上超越CUDA,究竟有多难?

    神坛的,还是围绕CUDA打造的一系列软件生态。   英伟达——CUDA的绝对统治   相信对GPU有过一定了解的都知道,英伟达的最大护城河就是CUDA
    的头像 发表于 06-20 00:09 3477次阅读

    Hugging Face提供1000万美元免费共享GPU

    全球最大的开源AI社区Hugging Face近日宣布,将提供价值1000万美元的免费共享GPU资源,以支持开发者创造新的AI技术。这一举措旨在帮助小型开发者、研究人员和初创公司,对抗大型AI公司的市场垄断,推动AI领域的公平竞争。
    的头像 发表于 05-20 09:40 605次阅读

    Keil使用AC6编译提示CUDA版本过高怎么解决?

    \' ArmClang: warning: Unknown CUDA version 10.2. Assuming the latest supported version 10.1
    发表于 04-11 07:56

    一文详解GPU硬件与CUDA开发工具

    CPU 和 GPU 的显著区别是:一个典型的 CPU 拥有少数几个快速的计算核心,而一个典型的 GPU 拥有几百到几千个不那么快速的计算核心。
    的头像 发表于 03-21 10:15 1020次阅读
    一文详解<b class='flag-5'>GPU</b>硬件与<b class='flag-5'>CUDA</b>开发工具

    GPU CUDA 编程的基本原理是什么

    神经网络能加速的有很多,当然使用硬件加速是最可观的了,而目前除了专用的NPU(神经网络加速单元),就属于GPU对神经网络加速效果最好了
    的头像 发表于 03-05 10:26 725次阅读
    <b class='flag-5'>GPU</b> <b class='flag-5'>CUDA</b> 编程的基本原理是什么

    内存共享原理解析

    内存共享是一种在多个进程之间共享数据的机制,它允许不同的进程直接访问同一块内存区域,从而实现数据的快速传递和通信。
    的头像 发表于 02-19 15:11 1196次阅读
    <b class='flag-5'>内存</b><b class='flag-5'>共享</b>原理解析

    为什么GPU比CPU更快?

    大规模数据集时比CPU更快的根本原因。内存带宽:GPU内存带宽比CPU高得多。内存带宽是指数据在内存之间传输的速度。
    的头像 发表于 01-26 08:30 2223次阅读
    为什么<b class='flag-5'>GPU</b>比CPU更快?

    GPU技术、生态及算力分析

    对比AMD从2013年开始建设GPU生态,近10年时间后用于通用计算的ROCm开放式软件平台才逐步有影响力,且还是在兼容CUDA的基础上。因此我们认为国内厂商在软件和生态层面与英伟达CUDA生态的差距较计算性能更为明显。
    的头像 发表于 01-14 10:06 1171次阅读
    <b class='flag-5'>GPU</b>技术、生态及算力分析

    FPGA、ASIC、GPU谁是最合适的AI芯片?

    CPU、GPU遵循的是冯·诺依曼体系结构,指令要经过存储、译码、执行等步骤,共享内存在使用时,要经历仲裁和缓存。 而FPGA和ASIC并不是冯·诺依曼架构(是哈佛架构)。以FPGA为例,它本质上是无指令、无需
    发表于 01-06 11:20 1397次阅读
    FPGA、ASIC、<b class='flag-5'>GPU</b>谁是最合适的AI芯片?

    什么是CUDA?谁能打破CUDA的护城河?

    在最近的一场“AI Everywhere”发布会上,Intel的CEO Pat Gelsinger炮轰Nvidia的CUDA生态护城河并不深,而且已经成为行业的众矢之的。
    的头像 发表于 12-28 10:26 1.2w次阅读
    什么是<b class='flag-5'>CUDA</b>?谁能打破<b class='flag-5'>CUDA</b>的护城河?

    OpenCV4.8 CUDA编程代码教程

    OpenCV4支持通过GPU实现CUDA加速执行,实现对OpenCV图像处理程序的加速运行,当前支持加速的模块包括如下。
    的头像 发表于 12-05 09:56 981次阅读
    OpenCV4.8 <b class='flag-5'>CUDA</b>编程代码教程