# 四、第一个内核

## 将两个整数相加

第二章[清单 2.1](02.html#Listing2_1) 中描述的项目将作为后续项目的模板。我在本书中介绍的所有代码都应该放在。cu 文件如前所述(除非另有说明)。这些文件应该放在一个项目中，该项目链接到 CUDA 库和适当的 CUDA 头，如第 2 章所示。

我们的第一个内核将在设备上把两个整数相加，并将结果总和返回给主机。

| ![](img/tip.png) | 提示:将代码键入集成开发环境，而不是使用复制和粘贴，尤其是对于这些早期的例子。复制和粘贴代码可能会粘贴一些不符合编译器要求的字符。 |

```cpp
    #include <iostream>
    #include <cuda.h>

    using namespace std;

    __global__ void AddInts(int* a, int *b) {
    a[0]+=b[0];
    }

    int main() {
    int a, b;            // Host copies
    int *d_a, *d_b;      // Device copies

    // Read some integers from the user
    cout<<"Input a number? ";
    cin>>a;
    cout<<"And another? ";
    cin>>b;

    // Allocate RAM on the device
    if(cudaMalloc(&d_a, sizeof(float)) != CUDA_SUCCESS) {
           cout<<"There was a problem allocating memory on the GPU"<<endl;
           cudaFree(d_a);
           cudaDeviceReset();
           return 0;
           }

    if(cudaMalloc(&d_b, sizeof(float)) != CUDA_SUCCESS)    {
           cout<<"There was a problem allocating memory on the GPU"<<endl;
           cudaFree(d_a);
           cudaFree(d_b);
           cudaDeviceReset();
           return 0;
           }

    // Copy host values to device
    cudaMemcpy(d_a, &a, sizeof(float), cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, &b, sizeof(float), cudaMemcpyHostToDevice);

    // Run kernel
    AddInts<<<1, 1>>>(d_a, d_b);

    // Copy results back to host
    cudaMemcpy(&a, d_a, sizeof(float), cudaMemcpyDeviceToHost);

    cout<<"The GPU addition results in "<<a<<endl;

    // Free device memory
    cudaFree(d_a);
    cudaFree(d_b);

    // Reset device and write performance indicators
    cudaDeviceReset();
    return 0;
    }

```

清单 4.1:添加整数

清单 4.1 使用`std::cout`和`std::cin`向用户请求两个整数。它将它们复制到设备中，并使用 CUDA 内核将它们添加在一起。添加的结果从设备内存复制回系统内存，并显示给用户。

| ![](img/tip.png) | 提示:控制台窗口打开和关闭太快，无法读取结果。若要防止窗口关闭，可以在返回 0 上放置断点；语句放在主函数的末尾。当一个程序被调试时，当它到达这一行的断点时，Visual Studio 将暂停执行，这将使程序员有时间查看输出控制台。 |

### 函数限定符

功能可以设计为在主机、设备或两者上执行。CUDA 提供了几个函数限定符，它们被放在函数声明的开始，并描述了函数要在其上执行的硬件。

*   `__global__` 标记为`__global__`的功能可从主机调用，但它们在设备上运行。它们是主机调用的 CUDA 内核。它们不能是递归的，不能有变量参数列表，并且必须返回 void。
*   `__host__`标记为`__host__`的函数由主机调用并执行。这些都是非常正常的 C++ 函数。`__host__`是默认的，如果一个函数没有任何限定符(比如清单 4.1 中的`main`，那么它就被认为是一个普通的 C++ 宿主函数。
*   `__device__`在设备上调用并执行标记为设备的功能。它们通常是内核的辅助函数。`__device__`不能从主机调用函数。它们只能从内核或其他设备函数中调用。
*   `__host__ __device__`标记为`__host__`和`__device__`的功能可从主机或设备调用。这个限定符实际上会导致编译两个函数:一个用于主机，另一个用于设备。主机版本是一个完全正常的 C++ 函数，不能改变设备的内存。同样，该函数的设备版本是`__device__`函数，不能改变任何主机变量。这个限定符用于定义主机和设备都可能调用的帮助函数。

### CUDA API 内存函数和 cudaError _ t

CUDA API 函数返回一个`cudaError_t`，表示调用是否成功。您可以对照`CUDA_SUCCESS`检查 CUDA API 函数的返回值，以确定某个函数是否执行正确。CUDA API 可以返回许多`cudaError_t`值。完整列表见`cuda.h`表头。

`cudaError_t cudaMalloc((void**) devPtr, size_t size);`

`cudaMalloc`在设备上分配全局内存。第一个参数是指向分配内存的指针的地址。如果调用成功，这个指针将被初始化，指向设备上新分配的内存。第二个参数是分配给请求的内存区域的大小，以字节为单位。

`cudaError_t cudaFree(void* devPtr);`

此功能释放设备上先前分配的内存。当不再需要内存时，应始终释放设备上的内存。第一个参数是先前在`cudaMalloc`调用中分配的指针；每次对`free`的呼叫都应该与之前对`Malloc`的呼叫相匹配。

`cudaError_t cudaDeviceReset();`

此功能清理分配的内存并重置设备的状态。此功能类似于中的垃圾收集器。NET 应用程序:它整理东西。但是与垃圾收集器不同，程序员必须显式调用这个函数。一旦复位，设备可以通过调用任何 CUDA API 函数返回到其初始化状态。调用此函数还会导致设备写入所有性能计数器。如果你计划分析一个内核，或者你的 CUDA 代码，那么你应该确保在你的程序关闭之前调用这个函数。这可确保数据被正确写入性能计数器，以便探查器使用。

`cudaError_t cudaMemcpy(void* dest, void* src, size_t size, cudaMemcpyKind direction);`

该功能将内存从`src`指针(源)复制到`dest`指针(目的)。`size`参数是要复制的数据的大小，以字节为单位。最后一个参数指定复制的方向。方向可以是以下任意一种:

*   `cudaMemcpyHostToHost`
*   `cudaMemcpyHostToDevice`
*   `cudaMemcpyDeviceToHost`
*   `cudaMemcpyDeviceToDevice`

作为`dest`和`src`提供的指针必须与选择的方向一致。例如，使用`cudaMemcpyHostToDevice`，`src`指针是主机指针，`dest`指针是设备指针。

## 在图形处理器之间复制数据

下一个示例说明了将数据块从设备复制到主机，并使用 CUDA API 函数`cudaMemset`将数组归零。`cudaMemcpy`功能通过 PCI 总线将数据复制到设备的全局内存中或从设备的全局内存中复制数据。在下面的例子中，图形处理器用于将浮点值的数组清零至`0.0f`。

```cpp
    #include <iostream>
    #include <cuda.h>

    using namespace std;

    int main() {
    float values[100]; // CPU copy of an array
    float* d_Values;     // Pointer to device copy of values

    // Print the initial values to screen
    for(int i = 0; i < 100; i++)
           cout<<values[i]<<" "; // These will initially be random garbage

    // Allocate RAM on the GPU the same size as the values
    if(cudaMalloc(&d_Values, sizeof(float) * 100) != CUDA_SUCCESS) {
           cout<<"There was a problem allocating ram on the GPU"<<endl;
           return 0;
           }

    // Set the GPU ram to 0, floats with all bits as 0 in IEEE are = 0.0f
    if(cudaMemset(d_Values, 0, sizeof(float) * 100) != CUDA_SUCCESS) {
           cout<<"There was a problem setting the values to 0"<<endl;
           }
    else {
           // Copy this array of 0s to the CPU's array of values
           if(cudaMemcpy(values, d_Values, sizeof(float) * 100,
                         cudaMemcpyDeviceToHost) != CUDA_SUCCESS) {
                  cout<<"There was a problem copying the data from the GPU"<<endl;
                  }
           }

    // Free the GPU's array
    cudaFree(d_Values);

    // Print out the CPU's array to make sure they have all been set to 0.0f
    for(int i = 0; i < 100; i++)
           cout<<values[i]<<" ";

    cudaDeviceReset();

    return 0;
    }

```

清单 4.2: cudaMemset

清单 4.2 中新的 API 函数是`cudaMemset`:

`cudaError_t cudaMemset(void* devPtr, int value, size_t size);`

这个函数可以用来将数组的元素设置为某个初始值。第一个操作数是指向要设置的数据的设备指针。第二个操作数是您希望在分配的内存中设置每个字节的值，最后一个操作数是要设置的内存区域的大小(以字节为单位)。该函数将分配的内存设置为一个字节大小的值，这意味着它在设置浮点或整数值等方面的用处非常有限，除非您将所有内容初始化为 0。

使用 CUDA 应用编程接口将数组清零需要几个步骤。首先，同一数据必须有主机和设备副本。主机副本在清单 4.3 中称为`values[]`，设备副本称为`d_Values`。设备上分配的数据大小与主机上的数据大小相同。然后调用`cudaMemset`将其设置为 0，然后通过 PCI 总线将设备内存复制回主机，再次使用`cudaMemcpy`功能。

虽然清单 4.3 说明了设备的一个非常基本的用途(简单地用主机将阵列归零会更实用)，但是所涉及的步骤(即通过 PCI 总线用`cudaMemcpy`来回复制数据)在 CUDA 编程中非常常见。同样非常常见的是使用多个指针来指向相同数据的主机和设备副本。主机指针不指向设备的敏感区域，反之亦然。

### 向量加法核

下一个示例说明了将两个浮点向量相加。

```cpp
    #include <iostream>
    #include <ctime>
    #include <cuda.h>

    using namespace std;

    __global__ void AddVectors(float* a, float* b, int count)
    {
    int idx = threadIdx.x + blockIdx.x * blockDim.x;
    if(idx < count)      {
           a[idx] += b[idx];
           }
    }

    int main() {
    // Number of items in the arrays
    const int count = 100;

    // CPU arrays
    float a[count], b[count];

    // Device pointers
    float* d_a, *d_b;

    // Set the random seed for rand()
    srand(time(NULL));

    // Set the initial values of the CPU arrays
    for(int i = 0; i < count; i++) {
           a[i] = (float)(rand() % 100);
           b[i] = (float)(rand() % 100);
           }

    // Allocate data on the device
    if(cudaMalloc(&d_a, sizeof(float) * count) != CUDA_SUCCESS) {
           cout<<"Memory could not be allocated on the device!"<<endl;
           cudaDeviceReset();
           return 0;
           }
    if(cudaMalloc(&d_b, sizeof(float) * count) != CUDA_SUCCESS)   {
           cout<<"Memory could not be allocated on the device!"<<endl;
           cudaFree(d_a);
           cudaDeviceReset();
           return 0;
           }

    // Copy from host to device
    cudaMemcpy(d_a, &a, sizeof(float) * count, cudaMemcpyHostToDevice);
    cudaMemcpy(d_b, &b, sizeof(float) * count, cudaMemcpyHostToDevice);

    dim3 gridSize((count / 512) + 1);
    dim3 blockSize(512);
    AddVectors<<<gridSize, blockSize>>>(d_a, d_b, count);

    cudaMemcpy(&a, d_a, sizeof(float) * count, cudaMemcpyDeviceToHost);

    // Print out the results
    for(int i = 0; i < count; i++)
           cout<<"Result["<<i<<"]="<<a[i]<<endl;

    // Free resources
    cudaFree(d_a);
    cudaFree(d_b);
    cudaDeviceReset();

    return 0;
    }

```

清单 4.3:添加向量

清单 4.3 定义了主机上的两个浮点数组，`a`和`b`。它使用`srand`和`rand`函数将数组的值设置为随机整数。然后，它将这两个数组复制到设备指针`d_a`和`d_b`中，并调用一个名为`AddVectors`的内核。内核将`b`向量中的值与`a`向量中的相应值相加，并将结果存储在`a`向量中。然后将其复制回主机并打印到屏幕上。注意`dim3`参数在启动配置中的使用，以及一次启动多个线程。

内核说明了一种非常常见的模式，以及用 CUDA 编码和常规串行代码之间的主要区别。在串行代码中，前面的算法将通过类似于清单 4.4 所示的循环来执行。

```cpp
    for(int i = 0; i < count; i++) {
           a[i] += b[i];
           }

```

清单 4.4:串行向量加法

该算法的串行版本使用一个循环来迭代两个数组，在循环的每次迭代中添加一个元素。并行版本(如清单 4.3 中的内核所示)使用多线程来代替循环。

![](img/image033.png)

图 4.1:并行向量加法

图 4.1 说明了矢量加法是如何在图形处理器上进行的。向量数组`a`和`b`(它们实际上是作为参数传递给内核的设备指针`d_a`和`d_b`)显示为一系列垂直堆叠的框，每个框包含一个最初从主机复制的随机值。每个线程只负责一个加法，并且都同时运行(理论上执行只是同时的，有些线程实际上可能被安排在其他线程之后执行。请参见下面的注释)。第 20<sup>螺纹将数值`a[20]`和`b[20]`相加，第 13<sup>螺纹将数值`a[13]`和`b[13]`相加。没有循环；每个线程计算一个唯一的 ID，在清单 4.3 中称为`idx`。`idx`变量用于索引数组中特定于线程的数据元素。</sup></sup>

| ![](img/note.png) | 注意:我们正在编程的设备是大规模并行的。并行线程的绝对数量使图形处理器在许多计算上优于中央处理器。但是根据网格中线程的数量，设备通常会安排线程块一个接一个地执行，而不是同时执行。设备没有无限的资源；它在资源允许的情况下执行尽可能多的块，并在资源变得可用时调度剩余的块供以后执行。最大化并发线程的数量需要一个叫做占用率的指标。我们将在[第 8 章](08.html#_Chapter_8_)中研究如何使用剖析器查看内核的占用率。 |

为每个线程计算一个唯一的 ID(如清单 4.3 中的`idx`变量)是非常常见的。我们经常需要将一个问题划分成更小的子问题，这样每个线程就可以处理整个问题的一小部分。常见的情况是，每个线程处理问题的某个唯一单元，如图 4.1 中的加法，其中每个线程执行一次加法。例如，观察下面一行。

`int idx = threadIdx.x + blockIdx.x * blockDim.x;`

该行计算唯一的线程标识。标识的顺序是从 0 到启动的线程数减 1。只要网格和块具有单一维度，就可以使用此计算。如果您正在使用多维网格、块或两者，您仍然需要在计算中考虑额外的维度。

## 递归设备功能

清单 4.5 显示了计算整数阶乘的递归设备函数。

计算能力 1.x 设备不支持递归设备函数。如果您有这样的设备，下面示例中的代码将不起作用。

但是，如果您有计算能力 2.x 或更高版本的设备，代码将运行良好；但是，您需要确保正在为这种计算能力编译代码。

计算能力和 SM 设置在项目的属性中指定。在名为**设备**的 **CUDA C/C++** 部分，您将看到一个用于**代码生成**的选项。将此设置为与您的设备相匹配(例如，`compute_20,sm_20`)。我在第 5 章的[提示和技巧](05.html#_Tips_and_Tricks)部分中，在设置代码生成选项的细节下提供了更多的信息。

```cpp
    // Illustration of Factorial with recursive device function
    #include <iostream>
    #include <cuda.h>
    using namespace std;
    // Recursive device function
    __device__ int Factorial(int x) {
    if(x == 0) return 1;
    else return x * Factorial(x-1);
    }

    // Kernel
    __global__ void MyKernel(int* answer) {
    answer[0] = Factorial(answer[0]);
    }

    int main() {
    int i, answer, *d_i;

    // Read an int from user
    cout<<"Input a number: ";
    cin>>i;

    // Copy int to device
    cudaMalloc(&d_i, sizeof(int));
    cudaMemcpy(d_i, &i, sizeof(int), cudaMemcpyHostToDevice);

    // Launch kernel
    MyKernel<<<1, 1>>>(d_i);

    // Copy answer back to host
    cudaMemcpy(&answer, d_i, sizeof(int), cudaMemcpyDeviceToHost);

    // Print out
    cout<<"The answer is "<<answer<<endl;

    cudaFree(d_i);
    return 0;
    }

```

清单 4.5:递归设备函数

清单 4.5 展示了一个简单的递归设备函数，但是用户可以输入的数量非常有限。`Factorial`函数快速溢出 32 位整数。对于任何大于 12 的用户输入，这会导致不正确的结果。