一个向量加法 kernel:
1#include <cuda_runtime_api.h>
2#include <memory.h>
3#include <cstdlib>
4#include <ctime>
5#include <stdio.h>
6#include <cuda/cmath>
7
8__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
9{
10 int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
11 if(workIndex < vectorLength)
12 {
13 C[workIndex] = A[workIndex] + B[workIndex];
14 }
15}
16
17void initArray(float* A, int length)
18{
19 std::srand(std::time({}));
20 for(int i=0; i<length; i++)
21 {
22 A[i] = rand() / (float)RAND_MAX;
23 }
24}
25
26void serialVecAdd(float* A, float* B, float* C, int length)
27{
28 for(int i=0; i<length; i++)
29 {
30 C[i] = A[i] + B[i];
31 }
32}
33
34bool vectorApproximatelyEqual(float* A, float* B, int length, float epsilon=0.00001)
35{
36 for(int i=0; i<length; i++)
37 {
38 if(fabs(A[i] -B[i]) > epsilon)
39 {
40 printf("Index %d mismatch: %f != %f", i, A[i], B[i]);
41 return false;
42 }
43 }
44 return true;
45}
46
47//explicit-memory-begin
48void explicitMemExample(int vectorLength)
49{
50 // Pointers for host memory
51 float* A = nullptr;
52 float* B = nullptr;
53 float* C = nullptr;
54 float* comparisonResult = (float*)malloc(vectorLength*sizeof(float));
55
56 // Pointers for device memory
57 float* devA = nullptr;
58 float* devB = nullptr;
59 float* devC = nullptr;
60
61 //Allocate Host Memory using cudaMallocHost API. This is best practice
62 // when buffers will be used for copies between CPU and GPU memory
63 cudaMallocHost(&A, vectorLength*sizeof(float));
64 cudaMallocHost(&B, vectorLength*sizeof(float));
65 cudaMallocHost(&C, vectorLength*sizeof(float));
66
67 // Initialize vectors on the host
68 initArray(A, vectorLength);
69 initArray(B, vectorLength);
70
71 // start-allocate-and-copy
72 // Allocate memory on the GPU
73 cudaMalloc(&devA, vectorLength*sizeof(float));
74 cudaMalloc(&devB, vectorLength*sizeof(float));
75 cudaMalloc(&devC, vectorLength*sizeof(float));
76
77 // Copy data to the GPU
78 cudaMemcpy(devA, A, vectorLength*sizeof(float), cudaMemcpyDefault);
79 cudaMemcpy(devB, B, vectorLength*sizeof(float), cudaMemcpyDefault);
80 cudaMemset(devC, 0, vectorLength*sizeof(float));
81 // end-allocate-and-copy
82
83 // Launch the kernel
84 int threads = 256;
85 int blocks = cuda::ceil_div(vectorLength, threads);
86 vecAdd<<<blocks, threads>>>(devA, devB, devC, vectorLength);
87 // wait for kernel execution to complete
88 cudaDeviceSynchronize();
89
90 // Copy results back to host
91 cudaMemcpy(C, devC, vectorLength*sizeof(float), cudaMemcpyDefault);
92
93 // Perform computation serially on CPU for comparison
94 serialVecAdd(A, B, comparisonResult, vectorLength);
95
96 // Confirm that CPU and GPU got the same answer
97 if(vectorApproximatelyEqual(C, comparisonResult, vectorLength))
98 {
99 printf("Explicit Memory: CPU and GPU answers match\n");
100 }
101 else
102 {
103 printf("Explicit Memory: Error - CPU and GPU answers to not match\n");
104 }
105
106 // clean up
107 cudaFree(devA);
108 cudaFree(devB);
109 cudaFree(devC);
110 cudaFreeHost(A);
111 cudaFreeHost(B);
112 cudaFreeHost(C);
113 free(comparisonResult);
114}
115//explicit-memory-end
116
117
118int main(int argc, char** argv)
119{
120 int vectorLength = 1024;
121 if(argc >=2)
122 {
123 vectorLength = std::atoi(argv[1]);
124 }
125 explicitMemExample(vectorLength);
126 return 0;
127}编译运行:
1nvcc vecAdd.cu -o vecAdd
2./vecAdd 4096以下分段拆解。
一、Kernel#
CUDA 内核(在 GPU 执行的函数)使用 __global__ 修饰符声明,表示编译成 GPU 代码,并且在 kernel launch 时调用。通常从 CPU 代码调用内核。
1__global__ void vecAdd(float* A, float* B, float* C, int vectorLength)
2{
3 int workIndex = threadIdx.x + blockIdx.x*blockDim.x;
4 if(workIndex < vectorLength)
5 {
6 C[workIndex] = A[workIndex] + B[workIndex];
7 }
8}kernel 的返回类型必须是 void。
可以通过 <<< >>> 来启动 kernel:
1// 一维,1 个 block,256 个线程
2vecAdd<<<1, 256>>>(A, B, C);
3
4// 二维,16*16 个 block,每个 block 8*8 个线程
5dim3 grid(16,16);
6dim3 block(8,8);
7MatAdd<<<grid, block>>>(A, B, C);值得注意的是,一个 block 最多只能有 1024 个线程。例如 block 大小为 dim3 block(32, 32, 2) 就不合法。
kernel launch 是非阻塞的,CPU 代码会继续执行,因此需要显式同步:
1cudaDeviceSynchronize();在 kernel 中,线程可以访问到以下参数:
threadIdx:thread 在 block 内的索引(block 内每个线程唯一)blockDim:每个 block 的维度blockIdx:block 在 grid 内的索引gridDim:grid 的维度
后三者都是在 kernel 启动时指定的。它们都是长度为 3 的向量,分别用 .x、.y、.z 来访问各维度的值。
threadIdx.x的取值范围是0, 1, ..., blockDim.x-1blockIdx.x的取值范围是0, 1, ..., gridDim.x-1
运行时没有指定 grid 和 block 的维度时,默认都是 1。例如 vecAdd<<<1, 256>>> 等价于 gridDim = (1, 1, 1) 和 blockDim = (256, 1, 1)。
实际上线程在整个 grid 中某个维度的全局索引可以通过以下方式计算:
1int workIndex = threadIdx.x + blockIdx.x*blockDim.x;在 vecAdd 的例子中,vectorLength 不一定是 256 的倍数,所以线程需要检查 workIndex 是否越界:
1if(workIndex < vectorLength)
2{
3 C[workIndex] = A[workIndex] + B[workIndex];
4}对于最后一个 block 里面那些越界的 workIndex,线程会退出而不执行任何操作。
所需要的 block 数量可以通过 vectorLength / 256 向上取整来计算:
1int threads = 256;
2// 相当于 blocks = (vectorLength + threads - 1) / threads
3int blocks = cuda::ceil_div(vectorLength, threads);
4vecAdd<<<blocks, threads>>>(devA, devB, devC, vectorLength);二、Memory#
1. 显式内存管理#
流程:
- 在 CPU/GPU 上分别分配内存:
cudaMallocHost / new(主机)和cudaMalloc(设备) - 初始化数据
- 将数据从 CPU 复制到 GPU:
cudaMemcpy - 启动 kernel 进行计算
- 将结果从 GPU 复制回 CPU:
cudaMemcpy - 在 CPU 上验证结果
- 释放内存:
cudaFreeHost / delete(主机)和cudaFree(设备)
注意:
cudaMemcpy的第四个参数指定了复制的方向,常用的值有cudaMemcpyHostToDevice、cudaMemcpyDeviceToHost和cudaMemcpyDeviceToDevice。也可以使用cudaMemcpyDefault让 CUDA 根据指针地址自动推导。cudaMemcpy是一个同步操作,调用后 CPU 线程会等待 GPU 上的复制完成。异步复制需要使用cudaMemcpyAsync。- 使用
cudaMallocHost分配的主机内存是页锁定(page-locked)内存,可以提供更高的复制性能。如果异步复制,使用页锁定内存是必须的。最佳实践是在需要与 GPU 收发数据的内存上使用cudaMallocHost。
1void explicitMemExample(int vectorLength)
2{
3 // Pointers for host memory
4 float* A = nullptr;
5 float* B = nullptr;
6 float* C = nullptr;
7 float* comparisonResult = (float*)malloc(vectorLength*sizeof(float));
8
9 // Pointers for device memory
10 float* devA = nullptr;
11 float* devB = nullptr;
12 float* devC = nullptr;
13
14 //Allocate Host Memory using cudaMallocHost API. This is best practice
15 // when buffers will be used for copies between CPU and GPU memory
16 cudaMallocHost(&A, vectorLength*sizeof(float));
17 cudaMallocHost(&B, vectorLength*sizeof(float));
18 cudaMallocHost(&C, vectorLength*sizeof(float));
19
20 // Initialize vectors on the host
21 initArray(A, vectorLength);
22 initArray(B, vectorLength);
23
24 // start-allocate-and-copy
25 // Allocate memory on the GPU
26 cudaMalloc(&devA, vectorLength*sizeof(float));
27 cudaMalloc(&devB, vectorLength*sizeof(float));
28 cudaMalloc(&devC, vectorLength*sizeof(float));
29
30 // Copy data to the GPU
31 cudaMemcpy(devA, A, vectorLength*sizeof(float), cudaMemcpyDefault);
32 cudaMemcpy(devB, B, vectorLength*sizeof(float), cudaMemcpyDefault);
33 cudaMemset(devC, 0, vectorLength*sizeof(float));
34 // end-allocate-and-copy
35
36 // Launch the kernel
37 int threads = 256;
38 int blocks = cuda::ceil_div(vectorLength, threads);
39 vecAdd<<<blocks, threads>>>(devA, devB, devC, vectorLength);
40 // wait for kernel execution to complete
41 cudaDeviceSynchronize();
42
43 // Copy results back to host
44 cudaMemcpy(C, devC, vectorLength*sizeof(float), cudaMemcpyDefault);
45
46 // Perform computation serially on CPU for comparison
47 serialVecAdd(A, B, comparisonResult, vectorLength);
48
49 // Confirm that CPU and GPU got the same answer
50 if(vectorApproximatelyEqual(C, comparisonResult, vectorLength))
51 {
52 printf("Explicit Memory: CPU and GPU answers match\n");
53 }
54 else
55 {
56 printf("Explicit Memory: Error - CPU and GPU answers to not match\n");
57 }
58
59 // clean up
60 cudaFree(devA);
61 cudaFree(devB);
62 cudaFree(devC);
63 cudaFreeHost(A);
64 cudaFreeHost(B);
65 cudaFreeHost(C);
66 free(comparisonResult);
67}2. 统一内存#
使用 unified memory 更简单,不需要显式地在 CPU 和 GPU 之间复制数据,驱动会确保数据在 CPU 和 GPU 之间都可访问,并且在需要时自动迁移。
缺点是性能上限可能不如显式内存管理高(因为可以让数据迁移和计算重叠)。
1void unifiedMemExample(int vectorLength)
2{
3 // Pointers to memory vectors
4 float* A = nullptr;
5 float* B = nullptr;
6 float* C = nullptr;
7 float* comparisonResult = (float*)malloc(vectorLength*sizeof(float));
8
9 // Use unified memory to allocate buffers
10 cudaMallocManaged(&A, vectorLength*sizeof(float));
11 cudaMallocManaged(&B, vectorLength*sizeof(float));
12 cudaMallocManaged(&C, vectorLength*sizeof(float));
13
14 // Initialize vectors on the host
15 initArray(A, vectorLength);
16 initArray(B, vectorLength);
17
18 // Launch the kernel. Unified memory will make sure A, B, and C are
19 // accessible to the GPU
20 int threads = 256;
21 int blocks = cuda::ceil_div(vectorLength, threads);
22 vecAdd<<<blocks, threads>>>(A, B, C, vectorLength);
23 // Wait for the kernel to complete execution
24 cudaDeviceSynchronize();
25
26 // Perform computation serially on CPU for comparison
27 serialVecAdd(A, B, comparisonResult, vectorLength);
28
29 // Confirm that CPU and GPU got the same answer
30 if(vectorApproximatelyEqual(C, comparisonResult, vectorLength))
31 {
32 printf("Unified Memory: CPU and GPU answers match\n");
33 }
34 else
35 {
36 printf("Unified Memory: Error - CPU and GPU answers do not match\n");
37 }
38
39 // Clean Up
40 cudaFree(A);
41 cudaFree(B);
42 cudaFree(C);
43 free(comparisonResult);
44}三、异常处理#
每一个 CUDA API 都会返回一个 cudaError_t 类型的错误码,值为 cudaSuccess 表示成功。可以通过 cudaGetErrorString 来获取错误码对应的字符串描述。
1#define CUDA_CHECK(expr_to_check) do { \
2 cudaError_t result = expr_to_check; \
3 if(result != cudaSuccess) \
4 { \
5 fprintf(stderr, \
6 "CUDA Runtime Error: %s:%i:%d = %s\n", \
7 __FILE__, \
8 __LINE__, \
9 result,\
10 cudaGetErrorString(result)); \
11 } \
12} while(0)
13
14// 使用示例
15CUDA_CHECK(cudaMalloc(&devA, vectorLength*sizeof(float)));
16CUDA_CHECK(cudaMalloc(&devB, vectorLength*sizeof(float)));
17CUDA_CHECK(cudaMalloc(&devC, vectorLength*sizeof(float)));使用 triple chevron notation <<<>>> 启动的 kernel 不会直接返回错误码,而是需要在 kernel launch 后调用 cudaGetLastError 来检查是否有错误发生。如果返回 cudaSuccess,说明 kernel launch 的参数和配置是合法的,并且错误状态不是内核启动前的上一个错误或异步错误。
对于异步错误(比如 kernel 执行期间的错误)可以这样做:
1vecAdd<<<blocks, threads>>>(devA, devB, devC);
2// 检查 kernel launch 之后是否有错误发生(例如配置错误)
3CUDA_CHECK(cudaGetLastError());
4// 等待 kernel 执行完成
5// CUDA_CHECK 会报告 kernel 执行期间发生的错误
6CUDA_CHECK(cudaDeviceSynchronize());使用上述 macro 来检测和报告错误时,设置 CUDA_LOG_FILE 环境变量可以将更详细的错误信息输出到指定的日志文件中。
四、修饰符#
函数有以下修饰符:
__global__:编译成 GPU 代码,通常从 host 调用,也可以从另一个 kernel 调用(dynamic parallelism)__device__:编译成 GPU 代码,可以从另一个__global__或__device__函数调用__host__:编译成 CPU 代码,在 CPU 内部调用(默认)
变量有以下修饰符:
__device__:在 GPU global memory 上分配内存,所有线程共享__constant__:在 GPU constant memory 上分配内存,所有线程共享,但只能读不能写__managed__:在 unified memory 上分配内存,CUDA 会自动管理数据迁移__shared__:在 GPU shared memory 上分配内存,同一个 block 内的线程共享
五、Thread Block Clusters#
同一个 cluster 上的线程块一定在一个 GPU Process Cluster (GPC) 上调度。一个 cluster 中至多有 8 个 block。
cluster 中的线程使用 distributed shared memory 进行通信。
使用 cooperative groups API cluster.sync() 进行硬件支持的 block 之间的同步。
以下是一个带有 cluster 配置的 kernel launch:
1// Compile time cluster size 2 in X-dimension and 1 in Y and Z dimension
2__global__ void __cluster_dims__(2, 1, 1) cluster_kernel(float *input, float* output)
3{
4 // ...
5}
6
7int main()
8{
9 float *input, *output;
10 // Kernel invocation with compile time cluster size
11 dim3 threadsPerBlock(16, 16);
12 dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
13
14 // The grid dimension is not affected by cluster launch, and is still enumerated
15 // using number of blocks.
16 // The grid dimension must be a multiple of cluster size.
17 cluster_kernel<<<numBlocks, threadsPerBlock>>>(input, output);
18}