CUDA 编程基础
1. Hello world,Cuda in C
2. VectorAdd,Cuda的线程组织
3. CPU与GPU之间的数据传输
4. CUDA中的各级存储资源
5. CUDA的流与事件
第1部分cuda in c主要简单了解一下cuda的程序,比较重要的内容是核函数(声明、调用) 、主机与设备 、函数和变量类型修饰符 。
第2部分有两个方面,vectoradd 这部分能学到比较简单的并行计算的运用 ,线程组织这部分主要理解网格(grid)、线程块(block)、线程(thread) 这三级结构,能够计算线程唯一ID 。
第3部分主要讲数据传输 ,把数据从cpu端传输到gpu端、gpu端传输的cpu端。
第4部分主要讲存储资源 ,主要是是指寄存器 、全局内存 、共享内存 、常量内存 等之类的。
第5部分是流和事件 ,cuda操作会放到流中,然后进行执行,简单理解流就是个FIFO的队列 ,事件可以理解为断点,帮助cuda操作进行同步、测量 事件。
Hello world,Cuda in C 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <stdio.h> #include <cuda_runtime.h> __global__ void hello () { printf ("Hello World, Cuda In C !\n" ); } int main () { hello<<<1 , 4 >>>(); printf ("I am the CPU: Hello World!\n" ); cudaDeviceSynchronize (); return 0 ; }
头文件cuda_runtime.h
:CUDA运行时库的头文件,提供CUDA 的核心功能,这里主要是函数cudaDeviceSynchronize()
需要用到。
__global__
:声明内核函数,由CPU调用,在GPU上执行。返回值必须是void。
CUDA执行模型:线程(Thread)、线程块(Block)、网格(Grid)
cudaDeviceSynchronize()
的作用:同步CPU和GPU。运行到这行代码时,程序会暂停,直到GPU完成所有工作。代码的输出应该是先1行CPU的输出,再4行GPU的输出。如果没有这行代码,程序的输出会有所不同,可能GPU还没来得及输出,程序就结束了,也可能CPU的输出在GPU输出之前、之间、之后。
主机-设备架构(Host-Device Architecture) 主机=CPU 设备=GPU
将输入数据从 CPU 内存复制到 GPU 内存
加载 GPU 程序并执行,将数据缓存到芯片上以提高性能
将结果从 GPU 内存复制到 CPU 内存
函数类型限定符
__global__
由主机 调用,在设备 上执行
任何对 __global__
函数的调用都必须指定该调用的执行配置。执行配置定义将用于在该设备上执行函数的网格和块的维度,共享内存的大小,以及相关的流。
在函数名称和括号参数列表之间插入<<<Dg, Db, Ns, S >>>
表达式来指定。
1 myKernel<<<dim3 (2 , 3 ), dim3 (16 , 16 ), 1024 >>>();
__device__
__host__
变量类型限定符
__device__
位于全局内存空间
从它创建开始,持续保持,与CUDA上下文的生命周期一样,
可以被GPU上的所有线程访问 ,在主机代码中不可见,但是可以通过一些库函数在Host端被访问 。(cudaGetSymbolAddress()
/cudaGetSymbolSize()
/cudaMemcpyToSymbol()
/cudaMemcpyFromSymbol()
)
__constant__
可以和__device__
一起使用
位于常量内存空间
与CUDA上下文的生命周期一样,
可以被GPU上的所有线程访问 ,在主机代码中不可见,但是可以通过一些库函数在Host端被访问 。(cudaGetSymbolAddress()
/cudaGetSymbolSize()
/cudaMemcpyToSymbol()
/cudaMemcpyFromSymbol()
)
__shared__
可以和__device__
一起使用
位于共享内存 空间,每个线程块有一个独立的共享内存空间
与线程块的生命周期一样
只能被线程块内的所有线程访问
不能有常量地址(地址在每个线程块启动时动态确定)
__managed__
可以和__device__
一起使用
可以被设备端和主机端的代码访问,可以直接的被读或写;
与应用程序的生命周期一致;
被__managed__
修饰的变量的地址不会是常量不变的;因此不能被const
修饰
被__managed__
修饰的变量不能是引用类型;
在设备代码中声明的自动变量,如果不带 __device__
、 __shared__
和__constant__
限定符中的任何一个时,通常位于寄存器中。
VectorAdd,Cuda的线程组织 VectorAdd add 1.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 #include <stdio.h> #include <cuda_runtime.h> __global__ void add (int n, double * x, double * y) { printf ("Running(%d,%d)\n" , blockIdx.x, threadIdx.x); for (int i = 0 ; i < n; ++i) { y[i] = x[i] + y[i]; } } int main (int argc, char * argv[]) { int n = atoi (argv[1 ]); double *x, *y; cudaMallocManaged (&x, n * sizeof (double )); cudaMallocManaged (&y, n * sizeof (double )); for (int i = 0 ; i < n; ++i) { x[i] = 1.0 ; y[i] = 2.0 ; } add << <1 , 1 >> > (n, x, y); cudaDeviceSynchronize (); printf ("Result y:\n" ); for (int i = 0 ; i < n; ++i) { printf ("%f " , y[i]); } printf ("\n" ); cudaFree (x); cudaFree (y); return 0 ; }
1 cudaMallocManaged (&x, n * sizeof (double ));
分配的内存是 托管内存 ,这块内存可以被 CPU 和 GPU 同时访问。CUDA 的托管内存管理机制会在需要的时候自动处理数据在 CPU 和 GPU 之间的传输。这简化了编程,因为你不需要手动进行数据拷贝。
这份代码没有对线程进行识别,相当于每个线程都把n
个数字相加了,这不符合并行计算的初衷,并且只分配了1个线程。
gridDim.x
:网格中沿 x 轴的线程块数量。
blockDim.x
:线程块中沿 x 轴的线程数量。
blockIdx.x
:当前线程块在其所在的网格(Grid)中的一维索引。范围是 0
到 gridDim.x - 1
。
threadIdx.x
:当前线程在其所在的线程块(Block)中的一维索引。范围是 0
到 blockDim.x - 1
。
计算线程的全局索引:
1 int globalIdx = blockIdx.x * blockDim.x + threadIdx.x;
add 2.0 分配k个线程。
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 #include <stdio.h> #include <cuda_runtime.h> __global__ void add (int n, double * x, double * y) { int index = threadIdx.x; int stride = blockDim.x; printf ("Running(%d,%d)\n" , blockIdx.x, threadIdx.x); for (int i = index; i < n; i += stride) { printf ("Thr(%d,%d): computing x[%d] + y[%d]\n" , blockIdx.x, threadIdx.x, i, i); y[i] = x[i] + y[i]; } } int main (int argc, char * argv[]) { int n = atoi (argv[1 ]); int k = atoi (argv[2 ]); double *x, *y; cudaMallocManaged (&x, n * sizeof (double )); cudaMallocManaged (&y, n * sizeof (double )); for (int i = 0 ; i < n; ++i) { x[i] = 1.0 ; y[i] = 2.0 ; } add << <1 , k >> > (n, x, y); cudaDeviceSynchronize (); printf ("Result y:\n" ); for (int i = 0 ; i < n; ++i) { printf ("%f " , y[i]); } printf ("\n" ); cudaFree (x); cudaFree (y); return 0 ; }
add 3.0 分配NBIks
个线程块,每个线程块包含k
个线程。
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 #include <stdio.h> #include <cstdlib> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim; #endif __global__ void add (int n, double * x, double * y) { int index = blockIdx.x * blockDim.x + threadIdx.x; printf ("Running (%d,%d) - my index = %d\n" , blockIdx.x, threadIdx.x, index); if (index < n) { printf ("Thr(%d,%d): computing x[%d] + y[%d]\n" , blockIdx.x, threadIdx.x, index, index); y[index] = x[index] + y[index]; } } int main (int argc, char * argv[]) { int n = atoi (argv[1 ]); int k = atoi (argv[2 ]); int NBIks = (n + k - 1 ) / k; printf ("n = %d k = %d NBIks = %d\n" , n, k, NBIks); double * x, * y; cudaMallocManaged (&x, n * sizeof (double )); cudaMallocManaged (&y, n * sizeof (double )); for (int i = 0 ; i < n; ++i) { x[i] = 1.0 ; y[i] = 2.0 ; } add << <NBIks, k >> > (n, x, y); cudaDeviceSynchronize (); printf ("Result y:\n" ); for (int i = 0 ; i < n; ++i) { printf ("%f " , y[i]); } printf ("\n" ); cudaFree (x); cudaFree (y); return 0 ; }
CUDA线程的组织
这部分内容比较重要的就是线程唯一ID的计算,即上文中提到的线程全局索引。本质就是多维数组的索引计算,比较简单。注意区分线程唯一ID和线程索引两个概念。其次就是两类变量,~Dim
,一般指各种维度,变量类型是dim3
;~Idx
,一般指各种索引,类型是unit3
。
1 2 struct uint3 {x;y;z;};struct dim3 {x;y;z;};
CPU与GPU之间的数据传输 CUDA早期的CPU与GPU之间的数据传输 cudaMalloc() cudaMalloc()
是 CUDA编程中用于在 GPU 设备上分配内存的函数。它的作用类似于 C/C++ 中的 malloc()
,不过它是在 GPU 上分配内存,而不是在 CPU 上。
1 2 __host__ __device__ cudaError_t cudaMalloc (void ** devPtr, size_t size) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <cuda_runtime.h> #include <iostream> int main () { int *d_array; size_t size = 100 * sizeof (int ); cudaError_t err = cudaMalloc ((void **)&d_array, size); if (err != cudaSuccess) { std::cerr << "cudaMalloc failed: " << cudaGetErrorString (err) << std::endl; return -1 ; } cudaFree (d_array); return 0 ; }
cudaMallocHost() or cudaHostAlloc() cudaMallocHost()
和 cudaHostAlloc()
是 CUDA 中用于分配主机(CPU)端可固定页内存的函数。固定页内存(pinned memory)是指不能被操作系统交换到磁盘的物理内存,因此可以提供更快的 主机到设备(CPU 到 GPU)的数据传输速度 。
主机的数据分配默认是可分页的。GPU不能直接从可分页的主机内存中访问数据,所以当调用从可分页的主机内存到设备内存的数据传输时,CUDA驱动程序必须首先分配一个临时的分页锁,或“钉住 ”的主机内存页,将主机数据复制到钉住的页中,然后将数据从钉住的页传输到设备内存。
1 2 3 cudaError_t cudaMallocHost (void ** ptr, size_t size) ;cudaError_t cudaHostAlloc (void ** pHost, size_t size, unsigned int flags) ;
unsigned int flags :
这是一个标志位,用于指定分配内存的类型和属性。常用的标志包括:
cudaHostAllocDefault
: 默认行为,没有特殊标志。
cudaHostAllocPortable
: 分配的内存可以在多个 CUDA 上下文中共享。
cudaHostAllocMapped
: 分配的内存同时也可以映射到设备(GPU)地址空间,这样 GPU 也可以访问这块内存。
cudaHostAllocWriteCombined
: 分配的内存具有写合并属性,用于减少写入延迟,但读操作速度较慢
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 #include <cuda_runtime.h> #include <iostream> int main () { int *h_data; size_t size = 100 * sizeof (int ); cudaError_t err = cudaMallocHost ((void **)&h_data, size); if (err != cudaSuccess) { std::cerr << "cudaMallocHost failed: " << cudaGetErrorString (err) << std::endl; return -1 ; } for (int i = 0 ; i < 100 ; ++i) { h_data[i] = i; } int *d_data; cudaMalloc ((void **)&d_data, size); cudaMemcpy (d_data, h_data, size, cudaMemcpyHostToDevice); cudaFree (d_data); cudaFreeHost (h_data); 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 #include <cuda_runtime.h> #include <iostream> int main () { int *h_data; size_t size = 100 * sizeof (int ); cudaError_t err = cudaHostAlloc ((void **)&h_data, size, cudaHostAllocMapped); if (err != cudaSuccess) { std::cerr << "cudaHostAlloc failed: " << cudaGetErrorString (err) << std::endl; return -1 ; } for (int i = 0 ; i < 100 ; ++i) { h_data[i] = i; } int *d_data; cudaHostGetDevicePointer (&d_data, h_data, 0 ); cudaFreeHost (h_data); return 0 ; }
cudaMemcpy() cudaMemcpy()
是 CUDA 中用于在主机(CPU)和设备(GPU)之间或者在设备内存之间复制数据的函数。这个函数是 CUDA 编程中非常重要的一部分,因为在实际应用中,数据经常需要在主机和设备之间传输。
1 2 cudaError_t cudaMemcpy (void * dst, const void * src, size_t count, cudaMemcpyKind kind) ;
cudaMemcpyKind kind :
指定数据复制的方向,即从哪里复制到哪里。CUDA 提供了以下几种类型:
cudaMemcpyHostToHost
: 主机内存到主机内存的数据复制(不常见)。
cudaMemcpyHostToDevice
: 主机内存到设备内存的数据复制(常见于将数据从 CPU 传输到 GPU)。
cudaMemcpyDeviceToHost
: 设备内存到主机内存的数据复制(常见于将计算结果从 GPU 传输回 CPU)。
cudaMemcpyDeviceToDevice
: 设备内存到设备内存的数据复制(常见于在 GPU 内部进行数据复制)。
cudaMemcpyDefault
:传输的类型由指针的取值推测处理。
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 #include <cuda_runtime.h> #include <iostream> int main () { const int N = 10 ; int h_array[N]; int *d_array; for (int i = 0 ; i < N; ++i) { h_array[i] = i; } cudaMalloc ((void **)&d_array, N * sizeof (int )); cudaMemcpy (d_array, h_array, N * sizeof (int ), cudaMemcpyDefault); cudaMemcpy (h_array, d_array, N * sizeof (int ), cudaMemcpyDefault); for (int i = 0 ; i < N; ++i) { std::cout << h_array[i] << " " ; } cudaFree (d_array); return 0 ; }
cudaMemcpyToSymbol() cudaMemcpyToSymbol()
是 CUDA 中用于将数据从主机(CPU)内存复制到设备(GPU)内存的全局符号(全局变量)或常量内存的函数。这个函数特别用于在设备代码中共享数据,因为它能够将数据复制到设备端的全局变量或常量内存中,从而在 CUDA 核函数(kernel)中访问这些数据。
1 2 cudaError_t cudaMemcpyToSymbol (const void * symbol, const void * src, size_t count, size_t offset = 0 , cudaMemcpyKind kind = cudaMemcpyHostToDevice) ;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <cuda_runtime.h> #include <iostream> __device__ int deviceVar; __global__ void kernel () { printf ("Value of deviceVar: %d\n" , deviceVar); } int main () { int h_value = 42 ; cudaMemcpyToSymbol (deviceVar, &h_value, sizeof (int )); kernel<<<1 , 1 >>>(); cudaDeviceSynchronize (); return 0 ; }
关于symbol
:在主机代码 中,deviceVar
是一个符号,用来告诉 CUDA 运行时这个变量在设备内存中的具体位置(地址)。它是一个编译时的符号,用于内存管理和数据复制等操作。在设备代码 中(比如核函数内),deviceVar
作为变量使用时,它表示这个位置上存储的具体数据的值,而不是内存地址。
cudaMemcpyFromSymbol() cudaMemcpyFromSymbol()
是 CUDA中用于将数据从设备端的全局符号(如 __device__
或 __constant__
变量)复制回主机内存的函数,这与 cudaMemcpyToSymbol()
相对。
1 2 cudaError_t cudaMemcpyFromSymbol (void * dst, const void * symbol, size_t count, size_t offset = 0 , cudaMemcpyKind kind = cudaMemcpyDeviceToHost) ;
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 #include <cuda_runtime.h> #include <iostream> __device__ int deviceVar; __global__ void kernel () { deviceVar += 10 ; } int main () { int h_result; int h_value = 42 ; cudaMemcpyToSymbol (deviceVar, &h_value, sizeof (int )); kernel<<<1 , 1 >>>(); cudaDeviceSynchronize (); cudaMemcpyFromSymbol (&h_result, deviceVar, sizeof (int )); std::cout << "Modified value of deviceVar after kernel execution: " << h_result << std::endl; return 0 ; }
示例代码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 int main () { const unsigned int N = 1048576 ; const unsigned int bytes = N * sizeof (int ); int *h_a = (int *)malloc (bytes); int *d_a; cudaMalloc ((int **)&d_a, bytes); memset (h_a, 0 , bytes); cudaMemcpy (d_a, h_a, bytes, cudaMemcpyHostToDevice); cudaMemcpy (h_a, d_a, bytes, cudaMemcpyDeviceToHost); return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main () { unsigned int nElements = 4 *1024 *1024 ; const unsigned int bytes = nElements * sizeof (float ); float *h_aPageable, *h_aPinned; float *d_a; h_aPageable = (float *)malloc (bytes); checkCuda ( cudaMallocHost ((void **)&h_aPinned, bytes) ); checkCuda ( cudaMalloc ((void **)&d_a, bytes) ); checkCuda ( cudaMemcpy (d_a, h_aPageable, bytes, cudaMemcpyHostToDevice) ); checkCuda ( cudaMemcpy (d_a, h_aPinned, bytes, cudaMemcpyHostToDevice) ); checkCuda ( cudaMemcpy (h_aPageable, d_a, bytes, cudaMemcpyDeviceToHost) ); checkCuda ( cudaMemcpy (h_aPinned, d_a, bytes, cudaMemcpyDeviceToHost) ); cudaFree (d_a); cudaFreeHost (h_aPinned); free (h_aPageable); }
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 #include <stdio.h> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim; #endif __device__ int d_x = 1 ; __device__ int d_y[2 ]; void __global__ my_kernel (void ) { d_y[0 ] += d_x; d_y[1 ] += d_x; printf ("d_x = %d, d_y[0] = %d, d_y[1] = %d.\n" , d_x, d_y[0 ], d_y[1 ]); } int main (void ) { int h_y[2 ] = { 10 , 20 }; cudaMemcpyToSymbol (d_y, h_y, sizeof (int ) * 2 ); my_kernel << <1 , 1 >> > (); cudaDeviceSynchronize (); cudaMemcpyFromSymbol (h_y, d_y, sizeof (int ) * 2 ); printf ("h_y[0] = %d, h_y[1] = %d.\n" , h_y[0 ], h_y[1 ]); return 0 ; }
细粒度拷贝 :在数据传输时,你可以选择性地控制要传输的数据量和数据在符号变量中的位置。比如,上面代码中h_y
长度为10的话,也只会复制前2个元素。
最新的CUDA的CPU与GPU之间的数据传输 cudaMallocManaged() cudaMallocManaged()
用于分配“统一内存”(Unified Memory)。统一内存是一种内存管理技术,使得 GPU 和 CPU 可以共享同一块内存,而不需要显式地进行内存拷贝操作。
1 2 cudaError_t cudaMallocManaged (void **devPtr, size_t size, unsigned int flags = cudaMemAttachGlobal) ;
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 #include <iostream> #include <math.h> #include <cstdio> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __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; cudaMallocManaged (&x, N * sizeof (float )); cudaMallocManaged (&y, N * sizeof (float )); for (int i = 0 ; i < N; i++) { x[i] = 1.0f ; y[i] = 2.0f ; } int blockSize = 256 ; int numBlocks = (N + blockSize - 1 ) / blockSize; add << <numBlocks, blockSize >> > (N, x, y); cudaDeviceSynchronize (); cudaFree (x); cudaFree (y); return 0 ; }
统一虚拟地址(UVA) :2014; CUDA 4 引入,将CPU和GPU的内存映射到一个统一的虚拟地址上,这样CPU代码和GPU代码有可能直接访问对方的地址(利用指针). UVA支持零拷贝,锁页内存,分页可迁移内存等 。
统一内存(Unified Memory) : CUDA 6首次引入; 通过创建一个在CPU与GPU之间的管理的内存池实现。
统一内存机制下的__managed__
变量
CPU端的全局变量 ——可以 被GPU端共享
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 #include <iostream> #include <math.h> #include <cstdio> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __managed__ int x; __global__ void GPU_func () { printf ("GPU sees x = %d\n" , x); x = 4444 ; } int main (void ) { x = 1234 ; GPU_func << <1 , 1 >> > (); cudaDeviceSynchronize (); printf ("CPU sees x = %d\n" , x); return 0 ; }
CPU端动态分配的局部变量 ——可以 被GPU端共享
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 #include <iostream> #include <math.h> #include <cstdio> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __global__ void GPU_func (int * x) { printf ("GPU sees *x = %d\n" , *x); *x = 4444 ; } int main () { int * a; cudaMallocManaged (&a, sizeof (int )); *a = 1234 ; GPU_func << < 1 , 1 >> > (a); cudaDeviceSynchronize (); printf ("CPU sees *a = %d\n" , *a); return 0 ; }
不能被__managed__
修饰的变量
静态分配的局部变量
函数参数
寄存器变量
使用malloc
和new
分配的内存
1 int * p = (__managed__ int *)malloc (sizeof (int ));
CUDA中的各级存储资源 registers:寄存器
在GPU片上,执行单元可以以极低的延迟访问这些寄存器。
基本单元是寄存器文件(register file),每个寄存器文件大小为32bit。
寄存器文件数量虽然很多,但是平均分给并行执行的线程时,每个线程拥有的数量就非常有限了。
编程时,每个线程分配的私有变量就会占用寄存器。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __global__ void registerDemo (float *B, float *A, int wA) { int aBegin = wA * BLOCK_SIZE * blockIdx.y; int aEnd = aBegin + wA - 1 ; int aEnd = BLOCK_SIZE; for (int a = aBegin; a <= aEnd; a += aStep) { } }
local memory:本地内存 本地内存实际上是位于全局内存中的线程私有存储,用于存放寄存器无法容纳的局部变量。
位于堆栈中,不在寄存器中的所有内容
作用域为特定线程
存储在global内存空间中,速度比寄存器慢很多
对于每个线程,本地内存也是私有的 。
如果寄存器被消耗完,数据将被存储在本地内存中。
如果每个线程用了过多的寄存器,或声明了大型结构体或数组,或者编译期无法确定数组的大小,线程的私有数据就有可能被分配到本地内存中。
一个线程的输入和中间变量将被保存在寄存器或者本地内存中。
shared memory: 共享内存 共享内存(Shared Memory)是CUDA编程中位于每个线程块(Thread Block) 内的存储空间,线程块中的所有线程都可以共享和访问 这块内存。这使得共享内存成为CUDA中一种非常重要的、快速的存储资源,尤其适合需要线程间通信和协同工作的场景。
SM(SM = streaming multiprocessor)中的内存空间
可以配置容量大小,32KB/48KB/64KB等,作用域是线程块
共享内存是GPU片内存储器。它是一个可以被同一block中的所有线程访问的可读写存储器。
访问共享存储器的速度几乎和访问寄存器一样快。是实现线程间通信的延迟最小的有效方法。
共享存储器可用于实现多种功能,如用于保存共用的计数器(例如计算循环迭代次数)或者block的公共结果(例如规约的结果)。
共享内存与L1缓存一般共用存储空间 。
可以是动态 或者静态 分配,声明既可以在kernel内部 也可以作为全局变量 。
动态的使用,若shared Memory的大小在编译器未知的话,可以使用extern关键字修饰
1 extern __shared__ int tile[];
由于其大小编译器未知,故在调用的时候,动态分配,需要传第三个参数 。另外,只有一维数据可以动态使用。
1 kernel<<<grid, block, isize * sizeof (int ) >>>(...)
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 #include <iostream> #include <math.h> #include <cstdio> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __global__ void example (float *u) { int i = threadIdx.x; __shared__ int tmp[4 ]; tmp[i] = u[i]; u[i] = tmp[i] * tmp[i] + tmp[3 - i]; } int main () { float host_u[4 ] = { 1 ,2 ,3 ,4 }; for (int i = 0 ; i < 4 ; ++i) printf ("%d %f\n" , i, host_u[i]); float * dev_u; size_t size = 4 * sizeof (float ); cudaMalloc (&dev_u, size); cudaMemcpy (dev_u, host_u, size, cudaMemcpyHostToDevice); example << <1 , 4 >> > (dev_u); cudaMemcpy (host_u, dev_u, size, cudaMemcpyDeviceToHost); for (int i = 0 ; i < 4 ; ++i) printf ("%d %f\n" , i, host_u[i]); cudaFree (dev_u); 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 #include <iostream> #include <stdio.h> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __global__ void example (float * u) { int i = threadIdx.x; extern __shared__ int tmp[]; tmp[i] = u[i]; u[i] = tmp[i] * tmp[i] + tmp[3 - i]; } int main () { float host_u[4 ] = { 1 ,2 ,3 ,4 }; float * dev_u; size_t size = 4 * sizeof (float ); for (int i = 0 ; i < 4 ; ++i) printf ("%d %f\n" , i, host_u[i]); cudaMalloc (&dev_u, size); cudaMemcpy (dev_u, host_u, size, cudaMemcpyHostToDevice); example << <1 , 4 , size>>> (dev_u); cudaMemcpy (host_u, dev_u, size, cudaMemcpyDeviceToHost); for (int i = 0 ; i < 4 ; ++i) printf ("%d %f\n" , i, host_u[i]); cudaFree (dev_u); return 0 ; }
constant memory:常量内存
属于全局内存,大小64KB。每个SM拥有8KB的常量内存。
在运行中不变,所有constant变量的值必须在kernel启动前从host设置
只读的地址空间
在Cuda程序中用于存储需要频繁访问的只读参数。当来自同一half-warp的线程访问常量内存中的同一数据时,如果发生缓存命中 ,那么只需要一个周期就可以获得数据。
使用常量内存可以提升运算性能的原因 :
声明:
__constant__
修饰
使用cudaMemcpyToSymbol
(或cudaMemcpy
)把数据从主机拷贝到设备GPU中。
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 #include <iostream> #include <stdio.h> #include <cuda_runtime.h> using namespace std;#ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __constant__ int derVar = 100 ; __global__ void xminus (int * a) { int i = threadIdx.x; a[i] = derVar + i; } int main () { int * h_a = (int *)malloc (4 * 10 ); int * d_a; cudaMalloc (&d_a, 4 * 10 ); cudaMemset (d_a, 0 , 40 ); xminus << <1 , 4 >> > (d_a); cudaMemcpy (h_a, d_a, 4 * 10 , cudaMemcpyDeviceToHost); for (int i = 0 ; i < 4 ; i++) cout << h_a[i] << " " ; cout << endl; return 0 ; }
texture memory:纹理存储器 在 CUDA 编程中,纹理存储器(texture memory)是一种特殊类型的只读内存,旨在优化特定类型的内存访问模式。它用于存储纹理数据,通常用于图像处理和计算机视觉等应用。专门为那些在内存访问模式中存在大量空间局部性 的图形应用而设计,意味着一个线程读取的位置可能与邻近线程读取的位置“非常接近”。(利用空间局部性,便于加速! )
global memory:全局存储器 全局存储器(global memory)是 GPU 中最大的一种存储器类型,它用于存储整个程序执行期间所需的数据。
独立于GPU核心的硬件RAM
GPU绝大多数内存空间都是全局内存
全局内存的IO是GPU上最慢的IO形式(除了访问host端内存)
全局存储器位于显存(占据了显存的绝大部分),CPU、GPU都可以进行读写访问。
整个网格中的任意线程都能读写全局存储器的任意位置。全局存储器能够提供很高的带宽,但同时也具有较高的访存延迟。
可以使用__device__
关键字定义的变量分配全局存储器,这个变量应该在所有函数外定义,必须对使用这个变量的host端和device端函数都可见才能成功编译。
在定义__device__
变量的同时可以对其赋值。
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 #include <iostream> #include <stdio.h> #include <cuda_runtime.h> using namespace std;#ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __device__ float devU[4 ]; __device__ float devV[4 ]; __global__ void addUV () { int i = threadIdx.x; devU[i] += devV[i]; } int main () { float hostU[4 ] = { 1 ,2 ,3 ,4 }; float hostV[4 ] = { 5 ,6 ,7 ,8 }; int size = 4 * sizeof (float ); cudaMemcpyToSymbol (devU, hostU, size, 0 , cudaMemcpyHostToDevice); cudaMemcpyToSymbol (devV, hostV, size, 0 , cudaMemcpyHostToDevice); addUV << <1 , 4 >> > (); cudaMemcpyFromSymbol (hostU, devU, size, 0 , cudaMemcpyDeviceToHost); for (int i = 0 ; i < 4 ; ++i) cout << i << ' ' << hostU[i] << '\n' ; 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 #include <iostream> #include <stdio.h> #include <cuda_runtime.h> using namespace std;#ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif __global__ void add4f (float * x, float * y) { int id = threadIdx.x; x[id] += y[id]; } int main () { float hostU[4 ] = { 1 ,2 ,3 ,4 }; float hostV[4 ] = { 5 ,6 ,7 ,8 }; float * devU; float * devV; int size = sizeof (float ) * 4 ; cudaMalloc (&devU, size); cudaMalloc (&devV, size); cudaMemcpy (devU, hostU, size, cudaMemcpyHostToDevice); cudaMemcpy (devV, hostV, size, cudaMemcpyHostToDevice); add4f << <1 , 4 >> > (devU, devV); cudaMemcpy (hostU, devU, size, cudaMemcpyDeviceToHost); for (int i = 0 ; i < 4 ; ++i) cout << i << ' ' << hostU[i] << '\n' ; cudaFree (devV); cudaFree (devU); return 0 ; }
CUDA的流与事件 CUDA流:待执行的CUDA操作的FIFO队列
创建一个CUDA stream
1 cudaError_t cudaStreamCreate (cudaStream_t *stream) ;
1 2 cudaStream_t stream; cudaError_t err = cudaStreamCreate (&stream);
销毁一个 CUDA stream
1 cudaError_t cudaStreamDestroy (cudaStream_t stream) ;
同步等待一个流里的所有操作完成
1 2 cudaError_t cudaStreamSynchronize (cudaStream_t stream) ;
查询一个流里的操作是否已经全部完成
1 2 cudaError_t cudaStreamQuery (cudaStream_t stream) ;
CUDA Streams:一个数据流与执行流重叠的例子
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 for (int i = 0 ; i < NSTREAMS; ++i) { cudaStreamCreate (&streams[i]); } for (int i = 0 ; i < NSTREAMS; ++i) { int offset = i * eles_per_stream; cudaMemcpyAsync (&d_A[offset], &h_A[offset], eles_per_stream * sizeof (int ), cudaMemcpyHostToDevice, streams[i]); cudaMemcpyAsync (&d_B[offset], &h_B[offset], eles_per_stream * sizeof (int ), cudaMemcpyHostToDevice, streams[i]); vector_sum << <(eles_per_stream + 255 ) / 256 , 256 , 0 , streams[i] >> > (d_A + offset, d_B + offset, d_C + offset, eles_per_stream); cudaMemcpyAsync (&h_C[offset], &d_C[offset], eles_per_stream * sizeof (int ), cudaMemcpyDeviceToHost, streams[i]); } for (int i = 0 ; i < NSTREAMS; ++i) { cudaStreamSynchronize (streams[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 83 84 85 86 87 88 89 90 91 92 #include <iostream> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif #define N 1024 #define NSTREAMS 4 __global__ void vector_sum (const int * A, const int * B, int * C, int size) { int idx = threadIdx.x + blockIdx.x * blockDim.x; if (idx < size) { C[idx] = A[idx] + B[idx]; } } int main () { int * h_A, * h_B, * h_C; int * d_A, * d_B, * d_C; int size = N * sizeof (int ); cudaStream_t streams[NSTREAMS]; int eles_per_stream = N / NSTREAMS; h_A = (int *)malloc (size); h_B = (int *)malloc (size); h_C = (int *)malloc (size); for (int i = 0 ; i < N; ++i) { h_A[i] = i; h_B[i] = i * 2 ; } cudaMalloc (&d_A, size); cudaMalloc (&d_B, size); cudaMalloc (&d_C, size); for (int i = 0 ; i < NSTREAMS; ++i) { cudaStreamCreate (&streams[i]); } for (int i = 0 ; i < NSTREAMS; ++i) { int offset = i * eles_per_stream; cudaMemcpyAsync (&d_A[offset], &h_A[offset], eles_per_stream * sizeof (int ), cudaMemcpyHostToDevice, streams[i]); cudaMemcpyAsync (&d_B[offset], &h_B[offset], eles_per_stream * sizeof (int ), cudaMemcpyHostToDevice, streams[i]); vector_sum << <(eles_per_stream + 255 ) / 256 , 256 , 0 , streams[i] >> > (d_A + offset, d_B + offset, d_C + offset, eles_per_stream); cudaMemcpyAsync (&h_C[offset], &d_C[offset], eles_per_stream * sizeof (int ), cudaMemcpyDeviceToHost, streams[i]); } for (int i = 0 ; i < NSTREAMS; ++i) { cudaStreamSynchronize (streams[i]); } for (int i = 0 ; i < 10 ; ++i) { std::cout << h_C[i] << " " ; } std::cout << std::endl; free (h_A); free (h_B); free (h_C); cudaFree (d_A); cudaFree (d_B); cudaFree (d_C); for (int i = 0 ; i < NSTREAMS; ++i) { cudaStreamDestroy (streams[i]); } return 0 ; }
Stream的调度
Fermi架构最高支持16路并行,但是在物理上,所有stream是被塞进硬件上唯一一个工作队列来调度的。会出现伪依赖 。
伪依赖的情况在Kepler系列里得到了解决,采用的一种叫Hyper-Q的技术,简单粗暴的理解就是,既然工作队列不够用,那就增加好了,于是Kepler上出现了32个工作队列 。该技术也实现了TPC上可以同时运行运算(compute)和图形(graphic)的应用。当然,如果超过32个stream被创建了,依然会出现伪依赖的情况。
在较新的 CUDA Compute Capability(CC)版本中(>=3.5)引入了优先级。
1 2 3 4 cudaError_t cudaStreamCreateWithPriority (cudaStream_t* pStream,unsigned int flags,int priority) ;
CUDA Event 流(Stream) 在 CUDA 中提供了并行执行的机制,但不能简单依赖它们来实现任务的同步和顺序控制。在 CUDA 编程中,事件(Event) 是一种用于测量时间、同步 CUDA 任务的工具。它们是专门设计来追踪 GPU 上某个操作的开始或结束,并可以用来检查任务的完成状态、实现跨流同步等。
创建和销毁事件
记录事件
事件同步
检查事件状态
计算时间
CUDA Stream和event的例子 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 97 #include <iostream> #include <cuda_runtime.h> #ifdef __INTELLISENSE__ #define __global__ #define __device__ #define __host__ dim3 blockIdx, threadIdx, blockDim, gridDim; #endif #define N (1024*1024) #define FULL_DATA_SIZE N*20 __global__ void kernel (int * a, int * b, int * c) { int threadID = blockIdx.x * blockDim.x + threadIdx.x; if (threadID < N) { c[threadID] = (a[threadID] + b[threadID]) / 2 ; } } int main () { cudaEvent_t start, stop; float elapsedTime; cudaEventCreate (&start); cudaEventCreate (&stop); cudaEventRecord (start, 0 ); cudaStream_t stream, stream1; cudaStreamCreate (&stream); cudaStreamCreate (&stream1); int * host_a, * host_b, * host_c; int * dev_a, * dev_b, * dev_c; int * dev_a1, * dev_b1, * dev_c1; cudaMalloc ((void **)&dev_a, N * sizeof (int )); cudaMalloc ((void **)&dev_a1, N * sizeof (int )); cudaMalloc ((void **)&dev_b, N * sizeof (int )); cudaMalloc ((void **)&dev_b1, N * sizeof (int )); cudaMalloc ((void **)&dev_c, N * sizeof (int )); cudaMalloc ((void **)&dev_c1, N * sizeof (int )); cudaHostAlloc ((void **)&host_a, FULL_DATA_SIZE * sizeof (int ), cudaHostAllocDefault); cudaHostAlloc ((void **)&host_b, FULL_DATA_SIZE * sizeof (int ), cudaHostAllocDefault); cudaHostAlloc ((void **)&host_c, FULL_DATA_SIZE * sizeof (int ), cudaHostAllocDefault); for (int i = 0 ; i < FULL_DATA_SIZE; i++) { host_a[i] = i; host_b[i] = FULL_DATA_SIZE - i; } for (int i = 0 ; i < FULL_DATA_SIZE; i += 2 * N) { cudaMemcpyAsync (dev_a, host_a + i, N * sizeof (int ), cudaMemcpyHostToDevice, stream); cudaMemcpyAsync (dev_b, host_b + i, N * sizeof (int ), cudaMemcpyHostToDevice, stream); cudaMemcpyAsync (dev_a1, host_a + i + N, N * sizeof (int ), cudaMemcpyHostToDevice, stream1); cudaMemcpyAsync (dev_b1, host_b + i + N, N * sizeof (int ), cudaMemcpyHostToDevice, stream1); kernel << <N / 1024 , 1024 , 0 , stream >> > (dev_a, dev_b, dev_c); kernel << <N / 1024 , 1024 , 0 , stream1 >> > (dev_a, dev_b, dev_c1); cudaMemcpyAsync (host_c + i, dev_c, N * sizeof (int ), cudaMemcpyDeviceToHost, stream); cudaMemcpyAsync (host_c + i + N, dev_c1, N * sizeof (int ), cudaMemcpyDeviceToHost, stream1); } cudaStreamSynchronize (stream); cudaStreamSynchronize (stream1); cudaEventRecord (stop, 0 ); cudaEventSynchronize (stop); cudaEventElapsedTime (&elapsedTime, start, stop); std::cout << "消耗时间: " << elapsedTime << std::endl; cudaFreeHost (host_a); cudaFreeHost (host_b); cudaFreeHost (host_c); cudaFree (dev_a); cudaFree (dev_a1); cudaFree (dev_b); cudaFree (dev_b1); cudaFree (dev_c); cudaFree (dev_c1); cudaStreamDestroy (stream); cudaStreamDestroy (stream1); return 0 ; }