GPU存储器架构

news2024/12/26 11:43:51

在这里插入图片描述
上表表述了各种存储器的各种特性。作用范围栏定义了程序的哪个部分能使用该存储器。而生存期定义了该存储器中的数据对程序可见的时间。除此之外,Ll和L2缓存也可以用于GPU程序以便更快地访问存储器。
总之,所有线程都有一个寄存器堆,它是最快的。共享内存只能被块中的线程访问,但比全局内存块。全局内存是最慢的,但可以被所有的块访问。

全局内存

所有的块都可以对全局内存进行读写。该存储器较慢,但是可以从代码的任何地方进行读写。缓存可加速对全局内存的访问。所有通过cudaMalloc分配的存储器都是全局内存。下面的简单代码演示了如何从程序中使用全局内存:

#include <stdio.h>
#define N 5

__global__ void gpu_global_memory(int *d_a)
{
	// "array" is a pointer into global memory on the device
	d_a[threadIdx.x] = threadIdx.x;
}

int main()
{
	// Define Host Array
	int h_a[N];
	//Define device pointer	
	int *d_a;       
						
	cudaMalloc((void **)&d_a, sizeof(int) *N);
	// now copy data from host memory to device memory 
	cudaMemcpy((void *)d_a, (void *)h_a, sizeof(int) *N, cudaMemcpyHostToDevice);
	// launch the kernel 
	gpu_global_memory << <1, N >> >(d_a);  
	// copy the modified array back to the host memory
	cudaMemcpy((void *)h_a, (void *)d_a, sizeof(int) *N, cudaMemcpyDeviceToHost);
	printf("Array in Global Memory is: \n");
	//Printing result on console
	for (int i = 0; i < N; i++) 
	{
		printf("At Index: %d --> %d \n", i, h_a[i]);
	}

	return 0;
}

本地内存和寄存器堆

本地内存和寄存器堆对每个线程都是唯一的。寄存器是每个线程可用的最快存储器。当内核中使用的变量在寄存器堆中装不下的时候,将会使用本地内存存储它们,这叫寄存器溢出。请注意使用本地内存有两种情况:一种是寄存器不够了,-种是某些情况根本就不能放在寄存器中,例如对一个局部数组的下标进行不定索引的时候。基本上可以将本地内存看成是每个线程的唯一的全局内存部分。相比寄存器堆,本地内存要慢很多。虽然本地内存通过Ll缓存和L2缓存进行了缓冲,但寄存器溢出可能会影响你的程序的性能。
下面演示一个简单的程序:

#include <stdio.h>
#define N 5

__global__ void gpu_local_memory(int d_in)
{
	int t_local;    
	t_local = d_in * threadIdx.x;     
	printf("Value of Local variable in current thread is: %d \n", t_local);
}

int main()
{

	printf("Use of Local Memory on GPU:\n");
	gpu_local_memory << <1, N >> >(5);  
	cudaDeviceSynchronize();
	return 0;
}

代码中的t_local变量是每个线程局部唯一的,将被存储在寄存器堆中。用这种变量计算的时候,计算速度将是最快速的。

共享内存

共享内存位于芯片内部,因此它比全局内存快得多。(CUDA里面存储器的快慢有两方面,一个是延迟低,一个是带宽大。这里特指延迟低),相比没有经过缓存的全局内存访问,共享内存大约在延迟上低100倍。同一个块中的线程可以访问相同的一段共享内存(注意:不同块中的线程所见到的共享内存中的内容是不相同的),这在许多线程需要与其他线程共享它们的结果的应用程序中非常有用。但是如果不同步,也可能会造成混乱或错误的结果。如果某线程的计算结果在写入到共享内存完成之前被其他线程读取,那么将会导致错误。因此,应该正确地控制或管理内存访问。这是由_syncthreads()指令完成的,该指令确保在继续执行程序之前完成对内存的所有写入操作。这也被称为barrier。barrier 的含义是块中的所有线程都将到达该代码行,然后在此等待其他线程完成。当所有线程都到达了这里之后,它们可以一起继续往下执行。为了演示共享内存和线程同步的使用,我们这里给出一个计算MA的例子:

#include <stdio.h>
#include <cuda.h>
#include <cuda_runtime.h>

__global__ void gpu_shared_memory(float *d_a)
{
	// Defining local variables which are private to each thread
	int i, index = threadIdx.x;
	float average, sum = 0.0f;

	//Define shared memory
	__shared__ float sh_arr[10];
	sh_arr[index] = d_a[index];
	__syncthreads();    // This ensures all the writes to shared memory have completed

	for (i = 0; i<= index; i++) 
	{ 
		sum += sh_arr[i]; 
	}
	average = sum / (index + 1.0f);
	d_a[index] = average; 
}

int main(int argc, char **argv)
{
	//Define Host Array
	float h_a[10];   
	//Define Device Pointer
	float *d_a;       
	
	for (int i = 0; i < 10; i++)
	{
		h_a[i] = i;
	}
	// allocate global memory on the device
	cudaMalloc((void **)&d_a, sizeof(float) * 10);
	// now copy data from host memory  to device memory 
	cudaMemcpy((void *)d_a, (void *)h_a, sizeof(float) * 10, cudaMemcpyHostToDevice);
	
	gpu_shared_memory << <1, 10 >> >(d_a);
	// copy the modified array back to the host memory
	cudaMemcpy((void *)h_a, (void *)d_a, sizeof(float) * 10, cudaMemcpyDeviceToHost);
	printf("Use of Shared Memory on GPU:  \n");
	//Printing result on console
	for (int i = 0; i < 10; i++) 
	{
		printf("The running average after %d element is %f \n", i, h_a[i]);
	}
	return 0;
}

MA操作很简单,就是计算数组中当前元素之前所有元素的平均值,很多线程计算的时候将会使用数组中的同样的数据。这就是一种理想的使用共享内存的用例,这样将会得到比全局内存更快的数据访问。这将减少每个线程的全局内存访问次数,从而减少程序的延迟。共享内存上的数字或者变量是通过__shared__修饰符定义的。我们在本例中,定义了具有10个float元素的共享内存上的数组。通常,共享内存的大小应该等于每个块的线程数。因为我们要处理10个(元素)的数组,所以我们也将共享内存的大小定义成这么大。
下一步就是将数据从全局内存复制到共享内存。每个线程通过自己的索引复制一个元素,这样块整体完成了数据的复制操作,这样数据写到了共享内存中。在下一行,我们开始读取使用这个共享内存中的数组,但是在继续之前,我们应当保证所有(线程)都已经完成了它们的写入操作。所以,让我们使用__syncthreads()进行一次同步。
接着就是(每个线程)通过for循环,利用这些存储在共享内存中的值(读取后)计算(从第一个元素)到当前元素的平均值,并且将对应每个线程的结果存放到全局内存中的相应位置。

常量内存

CUDA程序员会经常用到另外一种存储器——常量内存,NVIDIA GPU 卡从逻辑上对用户提供了64KB的常量内存空间,可以用来存储内核执行期间所需要的恒定数据。常量内存对一些特定情况下的小数据量的访问具有相比全局内存的额外优势。使用常量内存也一定程度上减少了对全局内存的带宽占用。在本小节中,我们将看看如何在CUDA中使用常量内存。我们将用一个简单的程序进行a * x + b的数学运算,其中a,b都是常数,程序代码如下:

#include "stdio.h"
#include <iostream>
#include <cuda.h>
#include <cuda_runtime.h>

//Defining two constants
__constant__ int constant_f;
__constant__ int constant_g;
#define N	5

//Kernel function for using constant memory
__global__ void gpu_constant_memory(float *d_in, float *d_out) 
{
	//Thread index for current kernel
	int tid = threadIdx.x;	
	d_out[tid] = constant_f*d_in[tid] + constant_g;
}

int main() 
{
	//Defining Arrays for host
	float h_in[N], h_out[N];
	//Defining Pointers for device
	float *d_in, *d_out;
	int h_f = 2;
	int h_g = 20;
	// allocate the memory on the cpu
	cudaMalloc((void**)&d_in, N * sizeof(float));
	cudaMalloc((void**)&d_out, N * sizeof(float));
	//Initializing Array
	for (int i = 0; i < N; i++) 
	{
		h_in[i] = i;
	}
	//Copy Array from host to device
	cudaMemcpy(d_in, h_in, N * sizeof(float), cudaMemcpyHostToDevice);
	//Copy constants to constant memory
	cudaMemcpyToSymbol(constant_f, &h_f, sizeof(int), 0, cudaMemcpyHostToDevice);
	cudaMemcpyToSymbol(constant_g, &h_g, sizeof(int));

	//Calling kernel with one block and N threads per block
	gpu_constant_memory << <1, N >> >(d_in, d_out);
	//Coping result back to host from device memory
	cudaMemcpy(h_out, d_out, N * sizeof(float), cudaMemcpyDeviceToHost);
	//Printing result on console
	printf("Use of Constant memory on GPU \n");
	for (int i = 0; i < N; i++) 
	{
		printf("The expression for input %f is %f\n", h_in[i], h_out[i]);
	}
	//Free up memory
	cudaFree(d_in);
	cudaFree(d_out);
	return 0;
}

常量内存中的变量使用__constant__ 关键字修饰。在之前的代码中,两个浮点数constant_f,constant_g 被定义成在内核执行期间不会改变的常量。需要注意的第二点是,使用__constant__ (在内核外面)定义好了它们后,它们不应该再次在内核内部定义。内核函数将用这两个常量进行一个简单的数学运算,在main 函数中,我们用一个特殊的方式将这两个常量的值传递到常量内存中。
在main 函数中,h_f, h_g两个常量在主机上被定义并初始化,然后将被复制到设备上的常量内存中。我们将用cudaMemcpyToSymbol函数把这些常量复制到内核执行所需要的常量内存中。该函数具有五个参数:第一个参数是(要写入的)目标,也就是我们刚才用__constant__ 定义过的h_f或者h_g常量;第二个参数是源主机地址;第三个参数是传输大小;第四个参数是写人目标的偏移量,这里是0;第五个参数是设备到主机的数据传输方向;最后两个参数是可选的,因此后面我们第二次cudaMemcpyToSymbol函数调用的时候省略掉了它们。

纹理内存

纹理内存是另外一种当数据的访问具有特定的模式的时候能够加速程序执行,并减少显存带宽的只读存储器。像常量内存一样,它也在芯片内部被cache缓冲。该存储器最初是为了图形绘制而设计的,但也可以被用于通用计算。当程序进行具有很大程度上的空间邻近性的访存的时候,这种存储器变得非常高效。空间邻近性的意思是,每个线程的读取位置都和其他线程的读取位置邻近。这对那些需要处理4个邻近的相关点或者8个邻近的点的图像处理应用非常有用。
通用的全局内存的cache将不能有效处理这种空间邻近性,可能会导致进行大量的显存读取传输。纹理存储被设计成能够利用这种访存模型,这样它只会从显存读取1次,然后缓冲掉,所以执行速度将会快得多。纹理内存支持2D和3D的纹理读取操作,在你的CUDA程序里面使用纹理内存可没有那么轻易,特别是对那些并非编程专家的人来说。我们将在本小节中为你解释一个如何通过纹理存储进行数组赋值的例子:

#include "stdio.h"
#include <iostream>
#include <cuda.h>
#include <cuda_runtime.h>

#define NUM_THREADS 10
#define N 10
texture <float, 1, cudaReadModeElementType> textureRef;

__global__ void gpu_texture_memory(int n, float *d_out)
{
	int idx = blockIdx.x*blockDim.x + threadIdx.x;
	if (idx < n)
	{
		float temp = tex1D(textureRef, float(idx));
		d_out[idx] = temp;
	}
}

int main()
{
	//Calculate number of blocks to launch
	int num_blocks = N / NUM_THREADS + ((N % NUM_THREADS) ? 1 : 0);
	//Declare device pointer
	float *d_out;
	// allocate space on the device for the result
	cudaMalloc((void**)&d_out, sizeof(float) * N);
	// allocate space on the host for the results
	float *h_out = (float*)malloc(sizeof(float) * N);
	//Declare and initialize host array
	float h_in[N];
	for (int i = 0; i < N; i++) 
	{
		h_in[i] = float(i);
	}
	//Define CUDA Array
	cudaArray *cu_Array;
	cudaMallocArray(&cu_Array, &textureRef.channelDesc, N, 1);
	//Copy data to CUDA Array
	cudaMemcpyToArray(cu_Array, 0, 0, h_in, sizeof(float)*N, cudaMemcpyHostToDevice);
	
	// bind a texture to the CUDA array
	cudaBindTextureToArray(textureRef, cu_Array);
	//Call Kernel	
  	gpu_texture_memory << <num_blocks, NUM_THREADS >> >(N, d_out);
	
	// copy result back to host
	cudaMemcpy(h_out, d_out, sizeof(float)*N, cudaMemcpyDeviceToHost);
	printf("Use of Texture memory on GPU: \n");
	for (int i = 0; i < N; i++)
	{
		printf("Texture element at %d is : %f\n",i, h_out[i]);
	}
	free(h_out);
	cudaFree(d_out);
	cudaFreeArray(cu_Array);
	cudaUnbindTexture(textureRef);	
}

通过“纹理引用”来定义一段能进行纹理拾取的纹理内存。纹理引用是通过texture<>类型的变量进行定义的。定义的时候,它具有3个参数:第一个是texture<>类型的变量定义时候的参数,用来说明纹理元素的类型。在本例中,是float类型;第二个参数说明了纹理引用的类型,可以是1D的,2D的,3D的。在本例中,是1D的纹理引用;第三个参数则是读取模式,这是一个可选参数,用来说明是否要执行读取时候的自动类型转换。请一定要确保纹理引用被定义成全局静态变量,同时还要确保它不能作为参数传递给任何其他函数。在这个内核函数中,每个线程通过纹理引用读取自己线程ID作为索引位置的数据,然后复制到d_out 指针指向的全局内存中。
在main函数中,定义并分配了内存和显存上的数组后,主机上的数组(中的元素)被初始化为0-9的值。本例中,你会第一次看到CUDA数组的使用。它们类似于普通的数组,但是却是纹理专用的。CUDA数组对于内核函数来说是只读的。但可以在主机上通过cudaMemcpyToArray函数写入,如同你在之前的代码中看到的那样。在cudaMemcpyToArray函数中,第二个和第三个参数中的0代表传输到的目标CUDA数组横向和纵向上的偏移量。两个方向上的偏移量都是О代表我们的这次传输将从目标CUDA数组的左上角(0,0)开始。CUDA数组中的存储器布局对用户来说是不透明的,这种布局对纹理拾取进行过特别优化。
cudaBindTextureToArray函数,将纹理引用和CUDA数组进行绑定。我们之前写入内容的CUDA数组将成为该纹理引用的后备存储。纹理引用绑定完成后我们调用内核,该内核将进行纹理拾取,同时将结果数据写入到显存中的目标数组。注意:CUDA对于显存中常见的大数据量的存储方式有两种,一种是普通的线性存储,可以直接用指针访问。另外一种则是CUDA数组,对用户不透明,不能在内核里直接用指针访问,需要通过texture或者surface的相应函数进行访问。本例的内核中,从texture reference进行的读取使用了相应的纹理拾取函数,而写入直接用普通的指针(d_out[])进行。当内核执行完成后,结果数组被复制回到主机上的内存中,然后在控制台窗口中显示出来。当使用完纹理存储后,我们需要执行解除绑定的代码,这是通过调用cudaUnbindTexture函数进行的。然后使用
cudaFreeArray()函数释放刚才分配的CUDA数组空间。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/132551.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue 页面渲染的流程

前言 在 Vue 核心中除了响应式原理外&#xff0c;视图渲染也是重中之重。我们都知道每次更新数据&#xff0c;都会走视图渲染的逻辑&#xff0c;而这当中牵扯的逻辑也是十分繁琐。 本文主要解析的是初始化视图渲染流程&#xff0c;你将会了解到从挂载组件开始&#xff0c;Vue…

2022年度总结与2023未来规划

这里写自定义目录标题2022年计划完成情况原始计划生活方面技术方面完成情况2023年规划初步安排技术方面生活方面2022年计划完成情况 原始计划 生活方面 健身&#xff08;体重增到145&#xff09;争取发展一个新的爱好&#xff08;游泳、拳击&#xff09;会做6个菜&#xff0c…

卷径计算详解(通过卷绕的膜长和膜厚进行计算)

有关卷绕+张力控制可以参看专栏的系列文章,文章链接如下: 变频器简单张力控制(线缆收放卷应用)_RXXW_Dor的博客-CSDN博客_收放卷应用张力控制的开闭环算法,可以查看专栏的其它文章,链接地址如下:PLC张力控制(开环闭环算法分析)_RXXW_Dor的博客-CSDN博客。https://blo…

元旦绚丽3D烟花代码

每天就是元旦了&#xff0c;新年怎么能少得了烟花呢&#xff1f;虽然绝大部分地区禁止燃放烟花&#xff0c;但该欣赏的烟花还是要欣赏滴~~ 最近整理文件&#xff0c;找到了一份烟花代码&#xff0c;3D特效&#xff0c;今天分享给大家&#xff0c;希望大家喜欢。 链接: https://…

深入浅出索引(下)

在上一篇文章中,我和你介绍了 InnoDB 索引的数据结构模型,今天我们再继续聊聊跟 MySQL 索引有关的概念。 在开始这篇文章之前,我们先来看一下这个问题: 在下面这个表 T 中,如果我执行 select * from T where k between 3 and 5,需要执行几次树的搜索操作,会扫描多少行…

搜索二叉树及其实现(迭代和递归实现)

二叉搜索树 二叉树搜索树又叫二叉排序树&#xff0c;它还有可能为一个空树。搜索二叉树的性质有 若他的左子树不为空&#xff0c;则左子树上所有节点的值都小于根节点。若他的右子树不为空&#xff0c;则右子树上所有节点的值都大于根节点。他的左右子树均为二叉搜索树 迭代…

微信小程序登录鉴权

小程序使用微信登录态进行授权登录 1、调用wx.login生成code wx.login()这个API的作用为当前用户生成一个临时的登录凭证&#xff0c;这个临时登录凭证有效期只有5分钟。拿到登录凭证后就可进行下一步操作&#xff0c;获取openid和session_key Taro.login().then((res) > …

Java:基于注解对类实例字段进行通用校验

前言 后台服务处理前端的请求时&#xff0c;会有这样的一种需求&#xff0c;即校验请求中的参数是否符合校验规则。校验参数是否符合的一种方法是&#xff0c;罗列请求参数&#xff0c;基于校验规则一个一个的校验参数&#xff0c;如果存在不符合的&#xff0c;就返回字段值不…

第20章 离差

第20章 离差 20.1 马尔可夫定理 一般来说&#xff0c;马尔可夫定理能够粗略估计一个随机变量的值等于一个比它的平均值大得多的值的概率。 例子&#xff1a;IQ的平均值是100。我们可以得到&#xff1a;最多1/3的人IQ可以达到300及以上&#xff0c;因为如果IQ>300的人超过…

基于情感词典、k-NN、Bayes、最大熵、SVM的情感极性分析及对比,含数据集

完整代码下载地址&#xff1a;基于情感词典、k-NN、Bayes、最大熵、SVM的情感极性分析及对比&#xff0c;含数据集 1、预处理 &#xff08;1&#xff09;、特征提取 对应文件&#xff1a;feature_extraction.py 最后结果&#xff1a; X^2值前几名的词语。能看出这些词都是一…

喜报丨武汉无名创新科技有限公司荣获国家“高新技术企业”证书,将助力高校科研与竞赛无人机产业加速发展!

2022年11月03日&#xff0c;高新技术企业认定管理官网公告了湖北省2022年第一批通过认定的高新技术企业名单&#xff0c;武汉无名创新科技有限公司&#xff08;简称“无名创新”&#xff09;榜上有名&#xff0c;证书编号为GR202242000480。“国家高新技术企业”认定是对无名创…

Java 并发编程知识总结【二】

3. 阻塞队列与线程池 3.1 阻塞队列 阻塞&#xff1a;必须要阻塞/不得不阻塞 阻塞队列是一个队列&#xff0c;在数据结构中是先进先出 线程1往阻塞队列里添加元素&#xff0c;线程2从阻塞队列里移除元素。 当队列是空的&#xff0c;从队列中获取元素的操作将会被阻塞 当队列…

OASIS协议标准文档的解读_第二部分

8 CELL REFERENCING 8.1 跟GDSII文件一样, 在OASIS文件中&#xff0c; cells也是用名字来标识的。一个CELL record不仅要包括一个cell的定义&#xff0c;还要包括它的名字。 PLACEMENT record根据cell的名字来指定cell的放置位置。跟GDSII一样&#xff0c;在OASIS中没有匿名的c…

APP应用渗透测试思路

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是APP应用渗透测试思路。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严禁对未授权…

vue后台系统管理项目-echarts柱状图实现订单统计

echarts柱状图实现订单统计 主要功能 不同订单状态切换显示不同的柱状图数据&#xff1b;根据条件切换选择年度视图、月度视图&#xff1b;根据条件切换指定年份、指定月份显示当前的数据&#xff1b;根据搜索条件查询查看柱状图数据&#xff1b;柱状图数据导出功能&#xff0c…

C#,核心基础算法——完整全面、简单易用、稳定可靠的统计学常用算法之原理介绍、算法精粹与完整的源代码

1、统计学常用算法 统计分析科学 在“政治算术”阶段出现的统计与数学的结合趋势逐渐发展形成了“统计分析科学”。 十九世纪末&#xff0c;欧洲大学开设的“国情纪要”或“政治算数”等课程名称逐渐消失&#xff0c;代之而起的是“统计分析科学”课程。当时的“统计分析科学”…

MongoDB:安装配置

MongoDB有两个服务器版本 &#xff1a;MongoDB 社区版 和 MongoDB 企业版。此篇主要介绍 MacOS 下 MongoDB 社区版的安装&#xff0c;在 “版本” 下拉列表中&#xff0c;选择要下载的 MongoDB 版本&#xff1b;在平台下拉列表中&#xff0c;选择 MacOS。在包下拉列表中&#x…

mysql一主双从环境搭建--docker-compose

mysql一主双从环境搭建–docker-compose 一、工作目录结构 ├── cluster01 │ ├── msql-master01 │ │ └── volumes │ │ ├── conf │ │ │ └── my.cnf │ │ ├── data │ │ ├── initdb │ │ │ …

基础数学(六)——非线性方程求根的数值解法

文章目录期末考核方式求解的一般步骤二分法求根二分法计算样例二分法的优缺点不动点迭代法全局收敛准则收敛性证明样例局部收敛性收敛阶数的定义迭代法具体例题&#xff08;考试必考&#xff09;牛顿迭代法例题&#xff08;使用牛顿法近似目标解&#xff09;&#xff08;考过&a…

Gem5模拟器,FS模式运行自定义程序(九)

FS模拟和SE模拟最大的区别是&#xff1a;FS模拟会启动Linux操作系统&#xff0c;会模拟系统的所有组件。因此需要给系统配置相应的Linux内核以及磁盘镜像&#xff0c;镜像文件作为Linux系统的文件系统。在FS模拟下&#xff0c;使用gem5自带的python脚本configs/example/fs.py。…