Toriskia 's Blog

13 篇文章 · 12 个标签 · 6 个友链

← 返回文章列表

2026.04.24

CUDA 笔记(二):CUDA C++ 概述

一个向量加法 kernel:

cpp
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}

编译运行:

bash
1nvcc vecAdd.cu -o vecAdd 2./vecAdd 4096

以下分段拆解。


一、Kernel#

CUDA 内核(在 GPU 执行的函数)使用 __global__ 修饰符声明,表示编译成 GPU 代码,并且在 kernel launch 时调用。通常从 CPU 代码调用内核。

cpp
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:

cpp
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 代码会继续执行,因此需要显式同步

cpp
1cudaDeviceSynchronize();

在 kernel 中,线程可以访问到以下参数:

  • threadIdx:thread 在 block 内的索引(block 内每个线程唯一)
  • blockDim:每个 block 的维度
  • blockIdx:block 在 grid 内的索引
  • gridDim:grid 的维度

后三者都是在 kernel 启动时指定的。它们都是长度为 3 的向量,分别.x.y.z 来访问各维度的值

  • threadIdx.x 的取值范围是 0, 1, ..., blockDim.x-1
  • blockIdx.x 的取值范围是 0, 1, ..., gridDim.x-1

运行时没有指定 grid 和 block 的维度时,默认都是 1。例如 vecAdd<<<1, 256>>> 等价于 gridDim = (1, 1, 1)blockDim = (256, 1, 1)

实际上线程在整个 grid 中某个维度的全局索引可以通过以下方式计算:

cpp
1int workIndex = threadIdx.x + blockIdx.x*blockDim.x;

vecAdd 的例子中,vectorLength 不一定是 256 的倍数,所以线程需要检查 workIndex 是否越界:

cpp
1if(workIndex < vectorLength) 2{ 3 C[workIndex] = A[workIndex] + B[workIndex]; 4}

对于最后一个 block 里面那些越界的 workIndex,线程会退出而不执行任何操作。

所需要的 block 数量可以通过 vectorLength / 256 向上取整来计算:

cpp
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 的第四个参数指定了复制的方向,常用的值有 cudaMemcpyHostToDevicecudaMemcpyDeviceToHostcudaMemcpyDeviceToDevice。也可以使用 cudaMemcpyDefault 让 CUDA 根据指针地址自动推导。
  • cudaMemcpy 是一个同步操作,调用后 CPU 线程会等待 GPU 上的复制完成。异步复制需要使用 cudaMemcpyAsync
  • 使用 cudaMallocHost 分配的主机内存是页锁定(page-locked)内存,可以提供更高的复制性能。如果异步复制,使用页锁定内存是必须的。最佳实践是在需要与 GPU 收发数据的内存上使用 cudaMallocHost
cpp
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 之间都可访问,并且在需要时自动迁移。

缺点是性能上限可能不如显式内存管理高(因为可以让数据迁移和计算重叠)。

cpp
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 来获取错误码对应的字符串描述。

cpp
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 执行期间的错误)可以这样做:

cpp
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:

cpp
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}