• 【CUDA编程】CUDA内存模型


    1. 内存结构

    在CUDA中可编程内存的类型有:

    • 寄存器(Registers)
    • 本地内存(Local Memory)
    • 共享内存(Shared Memory)
    • 常量内存(Constant Memory)
    • 纹理内存(Texture Memory)
    • 全局内存(Global Memory)

    CUDA中的内存模型分为以下几个层次:

    • thread:每个线程都用自己的registers(寄存器)和local memory(局部内存)
    • block:每个线程块(block)内都有自己的shared memory(共享内存),所有线程块内的所有线程共享这段内存资源
    • grid:每个grid都有自己的global memory(全局内存),constant memory(常量内存)和texture memory(纹理内存),不同线程块的线程都可使用。其中常量内存和纹理内存为只读内存空间

    线程访问这几类存储器的速度是:register > shared memory >Constant Memory > Texture Memory > Local Memory and Global Memory。下面这幅图表示这些内存在计算机架构中的所在层次。

    在这里插入图片描述

    2. GPU device内存

    2.1 寄存器(Registers)

    内核函数中声明且没有其他修饰符修饰的变量通常是存放在GPU的寄存器中,比如下面代码中的线程索引变量i。寄存器通常用于存放内核函数中需要频繁访问的线程私有变量,这些变量与内核函数的生命周期相同,内核函数执行完毕后,就不能再对它们进行访问了。

    特点:每个线程私有,速度快

    __global__ void VectorAddGPU(const float *const a, const float *const b,
                                 float *const c, const int n) {
      int i = blockDim.x * blockIdx.x + threadIdx.x;  //变量i 在寄存器中
      if (i < n) {
        c[i] = a[i] + b[i]; 
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    寄存器是GPU中访问速度最快的内存空间,但是一个SM中寄存器的数量比较有限,一旦内核函数使用了超过硬件限制的寄存器数量,则会使用本地内存来代替多占用的寄存器,这种寄存器溢出的情况会带来性能上的不利影响,实际编程过程中我们应该避免这种情况。

    使用nvcc的编译选项maxrregcount可以控制内核函数使用的寄存器的最大数量:

    -maxrregcount=32
    
    • 1

    2.2 本地内存(Local Memory)

    当register耗尽时,数据将被存储到local memory。如果每个线程中使用了过多的寄存器,或声明了大型结构体或数组,或编译器无法确定数组大小,线程的私有数据就会被分配到local memory中。,可能存放到本地内存中的变量有:

    • 编译时使用未知索引引用的本地数组
    • 可能会占用大量寄存器空间的较大本地结构体或者数组
    • 任何不满足内核函数寄存器限定条件的变量

    特点:每个线程私有;没有缓存,慢。
     
    溢出到本地内存中的变量 本质上与全局内存在同一块区域

    2.3 共享内存(Shared Memory)

    在内核函数中被__shared__修饰符修饰的变量被存储到共享内存中。每个SM都有一定数量由线程块分配的共享内存,它们在内核函数内进行声明,生命周期伴随整个线程块,一个线程块执行结束后,为其分配的共享内存也被释放以便重新分配给其他线程块进行使用。线程块中的线程通过使用共享内存中的数据可以实现互相之间的协作,不过使用共享内存必须调用如下函数进行同步:

    void __sybcthreads()
    
    • 1

    该函数为线程块中的所有线程设置了一个执行障碍点,使得同一线程块中的所有线程必须都执行到该障碍点才能往下执行,这样就可以避免一些潜在的数据冲突。

    特点:block中的线程共有;访问共享存储器几乎与register一样快.

    共享内存的定义方式有两种:静态共享内存和动态共享内存,静态共享内存在创建时候指明大小,态内存可以不指明大小。

    #include 
    __global__ void staticReverse(int *d, int n)
    {
      __shared__ int s[1000];//静态共享内存
      int t = threadIdx.x;
      int tr = n-t-1;
      s[t] = d[t];  //从global memory拷贝写入shared memory
    
      //因为数组s是所有线程共享的,如果不做同步执行下面语句则可能出现数据竞争问题
      __syncthreads();	//调用同步函数,只有当前block中所有线程都完成之后,再往下走
      //从shared memory读,然后写回到global memory
      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 = 1000;
      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);
      float time_gpu;
      cudaEvent_t start_GPU,stop_GPU;
      cudaEventCreate(&start_GPU);
      cudaEventCreate(&stop_GPU);
      cudaEventRecord(start_GPU,0);
      staticReverse<<<1,n>>>(d_d, n);//函数调用
      cudaEventRecord(stop_GPU,0);
      cudaEventSynchronize(start_GPU);
      cudaEventSynchronize(stop_GPU);
      cudaEventElapsedTime(&time_gpu, start_GPU,stop_GPU);
      printf("\nThe time from GPU:\t%f(ms)\n", time_gpu);
      cudaDeviceSynchronize();
      cudaEventDestroy(start_GPU);
      cudaEventDestroy(stop_GPU);
      
      cudaMemcpy(d, d_d, n*sizeof(int), cudaMemcpyDeviceToHost);
      //check
      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);
    
      cudaEventCreate(&start_GPU);
      cudaEventCreate(&stop_GPU);
      cudaEventRecord(start_GPU,0);
      dynamicReverse<<<1,n,n*sizeof(int)>>>(d_d, n);//函数调用
      cudaEventRecord(stop_GPU,0);
      cudaEventSynchronize(start_GPU);
      cudaEventSynchronize(stop_GPU);
      cudaEventElapsedTime(&time_gpu, start_GPU,stop_GPU);
      printf("\nThe time from GPU:\t%f(ms)\n", time_gpu);
      cudaDeviceSynchronize();
      cudaEventDestroy(start_GPU);
      cudaEventDestroy(stop_GPU);
      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]);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82

    输出:

    The time from GPU:	0.015424(ms)
    
    The time from GPU:	0.004672(ms)
    
    • 1
    • 2
    • 3

    __syncthreads() 是轻量级的,并且是以block 级别做同步。

    2.4 常量内存(Constant Memory)

    常量变量用__constant__修饰符进行修饰,它们必须在全局空间内和所有内核函数之外进行声明,对同一编译单元中的内核函数都是可见的。常量变量存储在常量内存中,内核函数只能从常量内存中读取数据。

    特点:只读;有缓存;空间小(64KB)

    注:定义常数存储器时,需要将其定义在所有函数之外,作用于整个文件 。

    常量内存必须在host端代码中使用下面的函数来进行初始化

    cudaError_t cudaMemcpyToSymbol(const void* symbol, const void* src,size_t count);
    
    • 1

    下面的例子展示了如何声明常量内存并与之进行数据交换:

    __constant__ float const_data[256];
    float data[256];
    cudaMemcpyToSymbol(const_data, data, sizeof(data));
    cudaMemcpyFromSymbol(data, const_data, sizeof(data));
    
    • 1
    • 2
    • 3
    • 4

    常量内存适合用于线程束中的所有线程都需要从相同的内存地址中读取数据的情况,比如所有线程都需要的常量参数,每个GPU只可以声明不超过64KB的常量内存。

    2.5 纹理内存(Texture Memory)

    纹理内存驻留在设备内存中,并在每个SM的只读缓存中缓存。纹理内存是一种通过指定的只读缓存访问的全局内存,是对二维空间局部性的优化,所以使用纹理内存访问二维数据的线程可以达到最优性能。

    特点:具有纹理缓存,只读。

    2.6 全局内存(Global Memory)

    全局内存是GPU中容量最大、延迟最高的内存空间,其作用域和生命空间都是全局的。一个全局内存变量可以在host代码中使用cudaMalloc函数进行动态声明,或者使用__device__修饰符在device代码中静态地进行声明。全局内存变量可以在任何SM设备中被访问到,其生命周期贯穿应用程序的整个生命周期。

    特点:所有线程都可以访问;没有缓存

    下面的例子展示了如何静态声明并使用全局变量:

    #include 
    #include 
    
    __device__ float dev_data;
    
    __global__ void AddGlobalVariable(void) {
      printf("device, global variable before add: %.2f\n", dev_data);
      dev_data += 2.0f;
      printf("device, global variable after add: %.2f\n", dev_data);
    }
    
    int main(void) {
      float host_data = 4.0f;
      cudaMemcpyToSymbol(dev_data, &host_data, sizeof(float)); //host拷贝数据值device
      printf("host, copy %.2f to global variable\n", host_data);
      AddGlobalVariable<<<1, 1>>>();
      cudaMemcpyFromSymbol(&host_data, dev_data, sizeof(float));//device拷贝数据值host
      printf("host, get %.2f from global variable\n", host_data);
      cudaDeviceReset();
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    上面的代码中需要注意的是,变量dev_data只是作为一个标识符存在,并不是device端的全局内存变量地址,所以不能直接使用cudaMemcpy函数把host上的数据拷贝到device端。不能直接在host端的代码中使用运算符&对device端的变量进行取地址操作,因为它只是一个表示device端物理位置的符号。但是在device端可以使用&对它进行取地址

    不过我们可以使用如下函数来获取它的地址:

    cudaError_t cudaGetSymbolAddress(void** devPtr, const void* symbol);
    
    • 1

    这个函数用于获取device端的全局内存物理地址,获取地址后,经过改造上述函数代码可改为:

    #include 
    #include 
    
    __device__ float dev_data;
    
    __global__ void AddGlobalVariable(void) {
      printf("device, global variable before add: %.2f\n", dev_data);
      dev_data += 2.0f;
      printf("device, global variable after add: %.2f\n", dev_data);
    }
    
    int main(void) {
      float host_data = 4.0f;
      float *dev_ptr = NULL;
      cudaGetSymbolAddress((void **)&dev_ptr, dev_data);
      cudaMemcpy(dev_ptr, &host_data, sizeof(float), cudaMemcpyHostToDevice);//host拷贝数据值device
      printf("host, copy %.2f to global variable\n", host_data);
      AddGlobalVariable<<<1, 1>>>();
      cudaMemcpy(&host_data, dev_ptr, sizeof(float), cudaMemcpyDeviceToHost);//device拷贝数据值host
      printf("host, get %.2f from global variable\n", host_data);
      cudaDeviceReset();
      return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    注意:在CUDA编程中,一般情况下device端的内核函数不能访问host端声明的变量,host端的函数也不能直接访问device端的变量,即使它们是在同一个文件内声明的。

    3. CPU Host内存

    对CUDA架构而言,主机端的内存被分为两种,一种是可分页内存(pageable memroy)页锁定内存(page-locked或 pinned)

    • 可分页内存 Pageable

    可分页内存是使用malloc()或者new在主机上分配

    • 页锁定内存 Pinned(Page-locked)

    页锁定内存是使用CUDA函数cudaMallocHost 或者cudaHostAlloc在主机内存上分配,cudaFreeHost()来释放

    注意:cudaMalloc()是在GPU上分配内存

    页锁定内存的重要属性是主机的操作系统将不会对这块内存进行分页和交换操作,确保该内存始终驻留在物理内存中。由于每个页锁定内存都需要分配物理内存,并且这些内存不能交换到磁盘上,所以页锁定内存比使用标准malloc()分配的可分页内存更消耗内存空间

    下面是页锁定内存与可分页内存的拷贝时间的比较。

    //锁页内存(page-locked或 pinned)与可分页内存(pageable memroy)比较
    
    #include "cuda_runtime.h"
    #include "device_launch_parameters.h"
    #include "iostream"
    #include 
    
    using namespace std;
    
    #define COPY_COUNTS 10
    #define MEM_SIZE 25 * 1024 * 1024
    
    //函数功能:拷贝到device再拷贝回host,重复执行10次
    //页锁定内存
    float cuda_host_alloc_test(int size, bool up)
    {
    	//耗时统计
    	cudaEvent_t start, stop;
    	float elapsedTime;
    	cudaEventCreate(&start);
    	cudaEventCreate(&stop);
    
    	int *a, *dev_a;
    	//在主机上分配页锁定内存
    	cudaMallocHost((void **)&a, size * sizeof(*a));
    	//在设备上分配内存空间
    	cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));
    	//计时开始
    	cudaEventRecord(start, 0);
    
    	for (int i = 0; i < COPY_COUNTS; i++)
    	{
    		//从主机到设备复制数据
    		cudaMemcpy(dev_a, a, size * sizeof(*dev_a), cudaMemcpyHostToDevice);
    		//从设备到主机复制数据
    		cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
    	}
    	cudaEventRecord(stop, 0);
    	cudaEventSynchronize(stop);
    	cudaEventElapsedTime(&elapsedTime, start, stop);
    
    	cudaFreeHost(a);
    	cudaFree(dev_a);
    	cudaEventDestroy(start);
    	cudaEventDestroy(stop);
    
    	return (float)elapsedTime / 1000;
    }
    
    //可分页内存
    float cuda_host_Malloc_test(int size, bool up)
    {
    	//耗时统计
    	cudaEvent_t start, stop;
    	float elapsedTime;
    	cudaEventCreate(&start);
    	cudaEventCreate(&stop);
    	int *a, *dev_a;
    
    	//在主机上分配可分页内存
    	a = (int *)malloc(size * sizeof(*a));
    
    	//在设备上分配内存空间
    	cudaMalloc((void **)&dev_a, size * sizeof(*dev_a));
    
    	//计时开始
    	cudaEventRecord(start, 0);
    
    	//执行从copy host to device 然后再 device to host执行100次,记录时间
    	for (int i = 0; i < COPY_COUNTS; i++)
    	{
    		//从主机到设备复制数据
    		cudaMemcpy(dev_a, a, size * sizeof(*dev_a), cudaMemcpyHostToDevice);
    		//从设备到主机复制数据
    		cudaMemcpy(a, dev_a, size * sizeof(*dev_a), cudaMemcpyDeviceToHost);
    	}
    	cudaEventRecord(stop, 0);
    	cudaEventSynchronize(stop);
    	cudaEventElapsedTime(&elapsedTime, start, stop);
    
    	free(a);
    	cudaFree(dev_a);
    	cudaEventDestroy(start);
    	cudaEventDestroy(stop);
    
    	return (float)elapsedTime / 1000;
    }
    
    int main()
    {
    	float allocTime = cuda_host_alloc_test(MEM_SIZE, true);
    	cout << "页锁定内存: " << allocTime << " s" << endl;
    	float mallocTime = cuda_host_Malloc_test(MEM_SIZE, true);
    	cout << "可分页内存: " << mallocTime << " s" << endl;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96

    输出:

    页锁定内存: 0.332271 s
    可分页内存: 0.364879 s
    
    • 1
    • 2

    自己测试下来,页锁定内存并没有起到多大的作用。。。


    参考:
    https://developer.nvidia.com/blog/how-optimize-data-transfers-cuda-cc/

    https://blog.csdn.net/chongbin007/article/details/123838980?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522166753182216782395390699%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=166753182216782395390699&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-22-123838980-null-null.142^v63^control,201^v3^control_2,213^v1^t3_control2&utm_term=cuda内存&spm=1018.2226.3001.4187?

  • 相关阅读:
    Star CCM+ 202206 (17.04) 详细安装步骤
    jmeter集群搭建
    阿里SQL规约
    springboot整合搭建webservice项目
    treeSelect树组件设置父节点禁用
    2022年PMP考试延迟了,该喜该忧?
    Unity 轮播图
    Halcon WPF 开发学习笔记(0):开篇介绍
    【洛谷 P1105】平台 题解(结构体+暴力枚举)
    Google Earth Engine(GEE)——对ndvi数据进行排序并获取倒数第二个值
  • 原文地址:https://blog.csdn.net/QLeelq/article/details/127683751