#include
#include
__global__ void myfirstkernel(void) {
}
int main(void) {
myfirstkernel << <1, 1 >> >();
printf("Hello, CUDA!\n");
return 0;
}
与C编写的代码相比,有两个主要区别:
(1)一个名为myfirstkernel的空函数,前缀为__global__
__global__是CUDA C在标准C中添加的一个限定符,它告诉编译器在这个限定符后面的函数定义应该在设备上而不是在主机上运行。
对于main函数,NVCC编译器将把这个函数提供给C编译器,因为它没有被global关键字修饰,因此main函数将在主机上运行。
(2)使用<<1,1>>>调用myfirstkernel函数
代码中的第二个不同之处在于对空的myfirstkernel函数的调用带有一些尖括号和数值。这是一个CUDA C技巧:从主机代码调用设备代码。它被称为内核调用。尖括号内的值表示我们希望在运行时从主机传递给设备的参数。
简单来讲,它表示块的数量和将在设备上并行运行的线程数。因此,在这段代码中,<<<1,1>>>表示myfirstkernel将运行在设备上的一个块和一个线程或块上。
下面示例是一个简单的加法程序,它在设备上执行两个变量的加法。
#include
#include
#include
#include
//添加两个变量的核函数定义
__global__ void gpuAdd(int d_a, int d_b, int *d_c) {
*d_c = d_a + d_b;
}
//main function
int main(void) {
//定义主机变量来存储结果
int h_c;
//定义设备指针
int *d_c;
//为设备指针分配内存
cudaMalloc((void**)&d_c, sizeof(int));
//Kernel call by passing 1 and 4 as inputs and storing answer in d_c
//<< <1,1> >> means 1 block is executed with 1 thread per block
gpuAdd << <1, 1 >> > (1, 4, d_c);
//将结果从设备内存复制到主机内存
cudaMemcpy(&h_c, d_c, sizeof(int), cudaMemcpyDeviceToHost);
printf("1 + 4 = %d\n", h_c);
//Free up memory
cudaFree(d_c);
return 0;
}
gpuAdd函数与标准C中的一个普通add函数非常相似。它以两个整数变量d_a和d_b作为输入,并将加法存储在第三个整数指针d_c所指示的内存位置。设备函数的返回值为void,因为它将结果存储在设备指针指向的内存位置中,而不显式地返回任何值。
在main函数中,前两行定义主机和设备的变量。第三行使用cudaMalloc函数在设备上分配d_c变量的内存。cudaMalloc函数类似于C中的malloc函数。在main函数的第四行中,调用gpuAdd,其中1和4是两个输入变量,d_c是一个作为输出指针变量的设备显存指针。gpuAdd函数的独特语法(也称为内核调用)将在下一节中解释。如果gpuAdd的结果需要在主机上使用,那么它必须从设备的内存复制到主机的内存中,这是由cudaMemcpy函数完成的。然后,使用printf函数打印这个结果。倒数第二行使用cudaFree函数释放设备上使用的内存。从程序中释放设备上使用的所有内存是非常重要的,否则,你可能在某个时候耗尽内存。
小技巧:在编程的时候定义主机和设备变量分别使用不同的前缀有利于避免错误。
下面代码示例是通过引用传递参数来编写相同的程序。
#include
#include
#include
#include
//Kernel function to add two variables, parameters are passed by reference
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {
*d_c = *d_a + *d_b;
}
int main(void) {
//Defining host variables
int h_a,h_b, h_c;
//Defining Device Pointers
int *d_a,*d_b,*d_c;
//Initializing host variables
h_a = 1;
h_b = 4;
//Allocating memory for Device Pointers
cudaMalloc((void**)&d_a, sizeof(int));
cudaMalloc((void**)&d_b, sizeof(int));
cudaMalloc((void**)&d_c, sizeof(int));
//Coping value of host variables in device memory
cudaMemcpy(d_a, &h_a, sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(d_b, &h_b, sizeof(int), cudaMemcpyHostToDevice);
//Calling kernel with one thread and one block with parameters passed by reference
gpuAdd << <1, 1 >> > (d_a, d_b, d_c);
//Coping result from device memory to host
cudaMemcpy(&h_c, d_c, sizeof(int), cudaMemcpyDeviceToHost);
printf("Passing Parameter by Reference Output: %d + %d = %d\n", h_a, h_b, h_c);
//Free up memory
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}
gpuAdd函数不是使用整数变量d_a和d_b作为内核的输入,而是将指向设备*d_a和*d_b上这些变量的指针作为输入。相加后的答案会存储在第三个整数指针d_c所指向的内存位置。作为这个设备函数引用传递的指针应该用cudaMalloc函数分配内存。
h_a、h_b和h_c是主机内存中的变量。它们的定义类似于普通的C代码。另一方面,d_a、d_b和d_c是驻留在主机内存中的指针,它们指向设备显存。它们通过使用cudaMalloc函数从主机分配内存。使用cudaMemcpy函数将h_a和h_b的值复制到d_a和d_b指向的设备显存中,数据传输方向为从主机到设备。然后,在内核调用中,这三个设备指针作为参数传递给内核。内核计算加法并将结果存储在d_c指向的内存位置。结果再次使用cudaMemcpy函数复制回主机内存,但这次数据传输的方向为从设备到主机。
在程序末尾使用cudaFree释放三个设备指针使用的内存。
在配置内核参数时,并行启动多个块和多个线程。
#include
#include
__global__ void myfirstkernel(void) {
//blockIdx.x gives the block number of current kernel
printf("Hello!!!I'm thread in block: %d\n", blockIdx.x);
}
int main(void) {
//A kernel call with 16 blocks and 1 thread per block
myfirstkernel << <16,1>> >();
//Function used for waiting for all kernels to finish
cudaDeviceSynchronize();
printf("All threads are finished!\n");
return 0;
}
从代码可知启动了一个内核,它有16个并行块,每个块只有一个线程。在每个执行该段内核代码的线程里,我们打印出来它们各自获取到的块ID。我们可以认为,并行启动了16个执行相同myfirstkernel代码的线程副本。每个副本线程将拥有一个属于自己的块ID和线程ID。
块ID可以通过blockIdx.x的CUDA C的内置变量读取到。线程ID则可以通过threadIdx.x内置变量读取到。这两个ID将告诉我们正在执行内核的是具体哪个块和其中的哪个线程副本。当你多次运行程序时会发现,每次运行,线程块都是以不同的顺序执行的。
代码中的cudaDeviceSynchronize函数是同步函数。因启动内核是一个异步操作,内核启动命令,不会等内核执行完成,控制权就会立刻返回给调用内核的CPU线程。在上述的代码中,CPU线程返回,继续执行的下一句是printf()。而再之后,在内核完成之前,进程就会结束,终止控制台窗口。所以,如果不加上这句同步函数,你就看不到任何的内核执行结果输出。
在系统上获得多个支持CUDA设备,因为系统可能包含多个支持GPU的设备,还可以调用API查询各个设备的内存、线程、通用设备信息(比如流处理器数量)等等。
#include
#include
#include
#include
// Main Program
int main(void)
{
int device_Count = 0;
cudaGetDeviceCount(&device_Count);
// This function returns count of number of CUDA enable devices and 0 if there are no CUDA capable devices.
if (device_Count == 0)
{
printf("There are no available device(s) that support CUDA\n");
}
else
{
printf("Detected %d CUDA Capable device(s)\n", device_Count);
}
}
可以通过查询cudaDeviceProp结构体来找到每个设备的相关信息,这个结构体可以返回所有设备属性。如果有多个支持CUDA的设备,那么可以启动for循环遍历所有设备属性。
cudaDeviceProp结构体提供了可以用来识别设备以及确定使用的版本信息的属性。它提供的name属性,可以以字符串的形式返回设备的名称。我们还可以通过查询cudaDriverGetVersion和cudaRuntimeGetVersion属性获得设备使用的CUDA Driver和运行时引擎的版本。如果你有多个设备,并希望使用其中的具有最多流多处理器的那个,则可以通过multiProcessorCount属性来判断。该属性返回设备上的流多处理器个数。你还可以使用clockRate属性获取GPU的时钟速率。它以Khz返回时钟速率。
GPU上的内存为分层结构。它可以分为L1缓存、L2缓存、全局内存、纹理内存和共享内存。cudaDeviceProp提供了许多特性来帮助识别设备中可用的内存。memoryClockRate和memoryBusWidth分别提供显存频率和显存位宽。显存的读写速度是非常重要的。它会影响程序的总体速度。totalGlobalMem返回设备可用的全局内存大小。totalConstMem返回设备中可用的总常量显存。sharedMemPerBlock返回的是设备上每个块中的最大可用共享内存大小。使用regsPerBlock可以识别每个块的可用寄存器总数。可以使用I2CacheSize属性识别L2缓存的大小。
块和线程可以是多维的。因此,最好知道每个维度中可以并行启动多少线程和块。对于每个多处理器的线程数量和每个块的线程数量也有限制。这个数字可以通过maxThreadsPerMultiProcessor和maxThreadsPerBlock找到。它在内核参数的配置中非常重要。如果每个块中启动的线程数量超过每个块中可能的最大线程数量,则程序可能崩溃。可以通过maxThreadsDim来确定块中各个维度上的最大线程数量。同样,每个维度中每个网格的最大块可以通过maxGridSize来标识。它们都返回一个具有三个值的数组,分别显示x、y和z维度中的最大值。
cudaDeviceProp结构体还有很多其他的属性。可以查看CUDA编程指南了解其他属性的详细信息。
这是一个程向量加法序。关于向量,可以点击链接查看。
#include "stdio.h"
#include
#include
#include
//Defining number of elements in Array
#define N 5
//Defining Kernel function for vector addition
__global__ void gpuAdd(int *d_a, int *d_b, int *d_c) {
//Getting block index of current kernel
int tid = blockIdx.x; // handle the data at this index
if (tid < N)
d_c[tid] = d_a[tid] + d_b[tid];
}
int main(void) {
//Defining host arrays
int h_a[N], h_b[N], h_c[N];
//Defining device pointers
int *d_a, *d_b, *d_c;
// allocate the memory
cudaMalloc((void**)&d_a, N * sizeof(int));
cudaMalloc((void**)&d_b, N * sizeof(int));
cudaMalloc((void**)&d_c, N * sizeof(int));
//Initializing Arrays
for (int i = 0; i < N; i++) {
h_a[i] = 2*i*i;
h_b[i] = i ;
}
// Copy input arrays from host to device memory
cudaMemcpy(d_a, h_a, N * sizeof(int), cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, N * sizeof(int), cudaMemcpyHostToDevice);
//Calling kernels with N blocks and one thread per block, passing device pointers as parameters
gpuAdd << > >(d_a, d_b, d_c);
//Copy result back to host memory from device memory
cudaMemcpy(h_c, d_c, N * sizeof(int), cudaMemcpyDeviceToHost);
printf("Vector addition on GPU \n");
//Printing result on console
for (int i = 0; i < N; i++) {
printf("The sum of %d element is %d + %d = %d\n", i, h_a[i], h_b[i], h_c[i]);
}
//Free up memory
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
return 0;
}
在gpuAdd内核函数中,每个块中的这个线程,用当前块的ID来初始化tid变量。然后根据tid变量,每个线程将一对元素进行相加。这样如果块的总数等于每个数组中元素的总数,那么所有的加法操作将并行完成。
main函数首先先是定义CPU和GPU上的数组和指针。设备指针指向通过cudaMalloc分配的显存。
然后通过cudaMemcpy函数,将前两个数组,从主机内存传输到设备显存。
内核启动的时候,将这些设备指针作为参数传递给它。你看到的内核启动符号(<<<>>>)里面的N和1,它们分别是启动N个块,每个块里面只有1个线程。
再然后通过cudaMemcpy,将内核的计算结果从设备显存传输到主机内存,注意最后这次传输的方向是反的,设备到主机。
最后,用cudaFree释放掉3段在显存上分配的缓冲区。