GPU存储器架构-- 全局内存 本地内存 寄存器堆 共享内存 常量内存 纹理内存

news2024/11/18 1:31:08

在这里插入图片描述
上表表述了各种存储器的各种特性。作用范围栏定义了程序的哪个部分能使用该存储器。而生存期定义了该存储器中的数据对程序可见的时间。除此之外,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/133638.html

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

相关文章

【PDPTW】python调用guribo求解PDPTW问题(Li Lim‘s benchmark)之二

原文连接&#xff1a;知乎《使用Python调用Gurobi求解PDPTW问题&#xff08;Li & Lim’s benchmark&#xff09;》 分析文章&#xff1a;文章目录修改utlis.pytest.py运行DataPath"lc101.txt"修改 以及修改公示约束&#xff08;8&#xff09;与代码不符合的问题…

【QT开发笔记-基础篇】| 第五章 绘图QPainter | 5.13 抗锯齿

本节对应的视频讲解&#xff1a;B_站_视_频 https://www.bilibili.com/video/BV1YP4y1B7Ex 本节讲解抗锯齿效果 前面实现的效果中&#xff0c;仔细观看能看到明显的锯齿的效果&#xff0c;如下&#xff1a; 此时&#xff0c;可以增加抗锯齿的效果。 1. 关联信号槽 首先&…

22年12月日常实习总结

12月结束了&#xff0c;8月末开始准备的日常实习也算是告一段落了 准备了2个多月&#xff0c;面试了一个月&#xff0c;也拿了一些offer 算是小有感触&#xff0c;遂写下此文&#xff0c;供还在准备或者要准备日常实习的同学参考。 个人背景及投递的日常实录在这篇文章里 24…

RegNet——颠覆常规神经网络认知的卷积神经网络(网络结构详解+详细注释代码+核心思想讲解)——pytorch实现

RegNet的博客的准备我可谓是话费了很多的时间&#xff0c;参考了诸多大佬的资料&#xff0c;主要是网上对于这个网络的讲解有点少&#xff0c;毕竟这个网络很新。网上可以参考的资料太少&#xff0c;耗费了相当多的时间&#xff0c;不过一切都是值得的&#xff0c;毕竟学完之后…

第二证券|下周解禁市值超980亿元,多家机构参与解禁股评级

宁德年代迎来431.8亿元解禁。 下周A股解禁市值超980亿元 证券时报数据宝统计&#xff0c;1月3日至6日&#xff0c;A股商场将有53家上市公司迎来限售股解禁。以个股最新价计算&#xff0c;53股解禁市值合计981.68亿元。 从解禁规模来看&#xff0c;宁德年代和中国移动居前&…

4.搭建配置中心-使用SpringCloud Alibaba-Nacos

naocs除了做服务注册、发现&#xff0c;还可以做为配置中心&#xff0c;使用分以下几步 1.pom引入nacos-config依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> &…

python中的多态和抽象类接口

目录 一.多态 抽象类&#xff08;接口&#xff09; 小结 一.多态 多态&#xff0c;指的是:多种状态&#xff0c;即完成某个行为时&#xff0c;使用不同的对象会得到不同的状态。 同样的行为&#xff08;函数&#xff09;&#xff0c;传入不同的对象得到不同的状态 演示 cl…

降维算法-sklearn

1.概述 维度&#xff1a; 对于数组和series&#xff0c;维度就是功能shape返回的结果&#xff0c;shape中返回了几个数字&#xff0c;就是几个维度。降维算法中的”降维“&#xff0c;指的是降低特征矩阵中特征的数量。降维的目的是为了让算法运算更快&#xff0c;效果更好&am…

LabVIEW​​开关模块与万用表DMM扫描模式

LabVIEW​​开关模块与万用表DMM扫描模式 在同步扫描模式下(Synchronous scanning)&#xff0c;扫描列表里面的每一条目都会在开关模块收到一个来自多功能数字万用表(DMM)的数字脉冲(触发输入)后执行.而DMM被编程设置为以一个固定的时间间隔去测量以及在每次测量完产生一个数字…

机器学习--数据清理、数据变换、特征工程

目录 一、数据清理 二、数据变换 三、特征工程 四、总结 一、数据清理 数据清理是提升数据的质量的一种方式。 数据不干净&#xff08;噪声多&#xff09;&#xff1f; 需要做数据的清理&#xff0c;将错误的信息纠正过来&#xff1b; 数据比较干净&#xff08;数据不是…

STM32 TIM PWM初阶操作:非互补PWM输出

STM32 TIM PWM初阶操作详解&#xff1a;非互补PWM输出 STM32 TIM可以输出管脚PWM信号适合多种场景使用&#xff0c;功能包括单线/非互补PWM输出&#xff0c;双线/互补PWM输出&#xff0c;以及死区时间和刹车控制等。 实际上&#xff0c;因为早期IP Core的缺陷&#xff0c;早期…

Android多线程编程

二.Android多线程编程 1.线程的相关概念 1&#xff09;相关概念&#xff1a; 程序&#xff1a;为了完成特定任务&#xff0c;用某种语言编写的一组指令集合(一组静态代码)进程&#xff1a;运行中的程序&#xff0c;系统调度与资源分配的一个独立单位&#xff0c;操作系统会 为…

leetcode 207. 课程表——java题解

题目所属分类 类似有向图的拓扑排序 入度为0就是起点 因为是要按照先后顺序的&#xff0c;所以是就是有向图 原题链接 你这个学期必须选修 numCourses 门课程&#xff0c;记为 0 到 numCourses - 1 。 在选修某些课程之前需要一些先修课程。 先修课程按数组 prerequisites …

Jetpack Compose中的Accompanist

accompanist是Jetpack Compose官方提供的一个辅助工具库&#xff0c;以提供那些在Jetpack Compose sdk中目前还没有的功能API。 权限 依赖配置&#xff1a; repositories {mavenCentral() }dependencies {implementation "com.google.accompanist:accompanist-permissi…

阳后买不到温度计 那么自己diy!(已开源)

这里写目录标题一 说明二 成品效果三 硬件材料四 硬件连接五 软件六 3D外盒模型一 说明 前段时间放开疫情后&#xff0c;身边人基本都阳了&#xff0c;自己也不出所料阳了&#xff0c;然后去药店买温度计&#xff0c;发现买不到&#xff0c;网上的买了也不发货&#xff0c;但是…

7.JS笔记-数组

1.数组的概念 使用数组Array可以把一组相关的数据存放在一起&#xff0c;并提供方便的获取方式。 数组是一组数据的集合&#xff0c;其中的每个数据被称作是元素&#xff0c;在数组中可以存放任意类型的元素。数组是一种将数据存储在单个变量名下的方式 2.创建数组 利用new关…

【Linux】Linux进程的理解 --- 进程状态、优先级、切换…

如果不改变自己&#xff0c;就别把跨年搞的和分水岭一样&#xff0c;记住你今年是什么吊样&#xff0c;明年就还会是什么吊样&#xff01;&#xff01;&#xff01; 文章目录一、冯诺依曼体系结构&#xff08;硬件&#xff09;二、操作系统&#xff08;软件&#xff09;1.操作…

git笔记2:Git基本理论,项目创建及克隆

目录 一、工作区域 二、工作流程 三、本地仓库搭建 1、创建全新的仓库 2、克隆远程仓库 一、工作区域 Git本地有三个工作区域&#xff1a; 工作目录&#xff08;Working Directory&#xff09;&#xff1a;平时存放代码的地方暂存区&#xff08;Stage/Index&#xff09;&a…

sec6-可派生类型和抽象类型

可派生类型 有两种类型&#xff0c;final类型和derivable类型。final类型没有任何子对象。derivable有子对象。 这两个对象之间的主要区别是它们的类。final类型对象没有自己的类区域。类的唯一成员是它的父类。 派生对象在类中有自己的区域。该类对其子类开放。 G_DECLARE…

【python系列】第三章 基本数据类型

*该系列内容来自于&#xff1a;中国大学MOOC&#xff08;幕客&#xff09;-python语言程序设计 ​​​​​​Python语言程序设计_北京理工大学_中国大学MOOC(慕课) 第三章 基本数据类型 方法论&#xff1a;Python语言数字及字符串类型 实践能力&#xff1a;初步学会编程进行…