作为一名长期从事高性能计算的开发者,我见证了CUDA如何彻底改变GPU编程的格局。2007年NVIDIA推出CUDA时,它首次让开发者能够直接利用GPU的并行计算能力,而不再局限于图形渲染。如今,CUDA已成为科学计算、深度学习、金融建模等领域的标配技术。
CUDA的核心优势在于其并行计算模型。现代GPU通常包含数千个计算核心,能够同时执行大量线程。与CPU的少量高性能核心不同,GPU采用"多而简单"的设计理念,特别适合数据并行的计算任务。想象一下,如果你需要处理一张百万像素的图片,CPU可能需要逐个像素处理,而GPU可以同时处理成千上万个像素。
构建CUDA项目首先需要正确的工程配置。以下是一个最小但完整的CMake配置示例:
cmake复制cmake_minimum_required(VERSION 3.18)
project(hellocuda LANGUAGES CXX CUDA)
add_executable(main main.cu)
set_target_properties(main PROPERTIES
CUDA_STANDARD 17
CUDA_STANDARD_REQUIRED ON
)
# 需要跨.cu文件调用设备函数时开启
set_target_properties(main PROPERTIES CUDA_SEPARABLE_COMPILATION ON)
# 常用CUDA编译选项
target_compile_options(main PUBLIC
$<$<COMPILE_LANGUAGE:CUDA>:--expt-relaxed-constexpr>
$<$<COMPILE_LANGUAGE:CUDA>:--expt-extended-lambda>
)
这个配置做了几件关键事情:
--expt-relaxed-constexpr:放宽设备端constexpr限制--expt-extended-lambda:支持更丰富的lambda表达式特性实际项目中,你可能还需要添加CUDA架构标志,如
-arch=sm_75来指定目标GPU的计算能力。
可靠的错误处理对CUDA编程至关重要。我推荐使用以下自包含的错误检查宏:
cpp复制#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>
#define CUDA_CHECK(call) do { \
cudaError_t err = (call); \
if (err != cudaSuccess) { \
std::fprintf(stderr, "CUDA error %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
std::exit(1); \
} \
} while(0)
这个宏的优点是:
使用示例:
cpp复制cudaMalloc(&devPtr, size); // 不安全的调用
CUDA_CHECK(cudaMalloc(&devPtr, size)); // 安全的调用
CUDA使用特殊的关键字标记函数执行位置:
__global__:核函数(Kernel)
<<<...>>>语法)cpp复制__global__ void addVectors(float* A, float* B, float* C, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) C[i] = A[i] + B[i];
}
__device__:设备函数
__device__或__global__函数调用__host__:主机函数
__host__组合使用:
cpp复制__host__ __device__ float computeSomething(float x) {
// 这个函数既可以在CPU也可以在GPU上调用
return x * x + 1.0f;
}
启动核函数时,需要指定执行配置:
cpp复制kernel<<<grid, block, sharedMemSize, stream>>>(args...);
grid:网格维度(block的数量)block:块维度(thread的数量)sharedMemSize:动态共享内存大小(字节)stream:CUDA流(默认为0)CUDA使用层次化的线程组织:
blockIdx.x/y/z:块在网格中的位置threadIdx.x/y/z:线程在块中的位置blockDim.x/y/z获取理解线程索引是CUDA编程的基础。下面是一个打印线程信息的示例:
cpp复制__global__ void printThreadInfo() {
printf("Block %d/%d, Thread %d/%d\n",
blockIdx.x, gridDim.x,
threadIdx.x, blockDim.x);
}
int main() {
// 启动2个block,每个block3个thread
printThreadInfo<<<2, 3>>>();
cudaDeviceSynchronize();
return 0;
}
输出可能类似于:
code复制Block 0/2, Thread 0/3
Block 0/2, Thread 1/3
Block 0/2, Thread 2/3
Block 1/2, Thread 0/3
Block 1/2, Thread 1/3
Block 1/2, Thread 2/3
处理大于网格尺寸的数据时,Grid-Stride模式是通用解决方案:
cpp复制__global__ void processArray(float* data, int N) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
for (int i = tid; i < N; i += blockDim.x * gridDim.x) {
// 处理data[i]
}
}
这种模式的优点:
典型的CUDA内存操作流程包括:
cudaMalloc)cudaMemcpyHtoD)cudaMemcpyDtoH)cudaFree)完整示例:
cpp复制__global__ void initArray(int* arr, int value, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) arr[i] = value;
}
int main() {
const int N = 1000;
int *d_arr = nullptr;
int h_arr[N] = {0};
// 1. 分配设备内存
CUDA_CHECK(cudaMalloc(&d_arr, N * sizeof(int)));
// 2. 初始化设备数组
initArray<<<(N+255)/256, 256>>>(d_arr, 42, N);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());
// 3. 拷贝回主机
CUDA_CHECK(cudaMemcpy(h_arr, d_arr, N * sizeof(int),
cudaMemcpyDeviceToHost));
// 4. 验证结果
for (int i = 0; i < 10; ++i)
printf("h_arr[%d] = %d\n", i, h_arr[i]);
// 5. 释放内存
CUDA_CHECK(cudaFree(d_arr));
return 0;
}
统一内存简化了内存管理,系统自动在CPU和GPU之间迁移数据:
cpp复制__global__ void compute(float* data, int N) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < N) data[i] = sqrtf(data[i]);
}
int main() {
const int N = 1<<20;
float *data = nullptr;
// 分配统一内存
CUDA_CHECK(cudaMallocManaged(&data, N * sizeof(float)));
// 初始化
for (int i = 0; i < N; ++i) data[i] = i;
// 执行核函数
compute<<<(N+255)/256, 256>>>(data, N);
CUDA_CHECK(cudaDeviceSynchronize());
// CPU可以直接访问结果
printf("data[0] = %f, data[N-1] = %f\n", data[0], data[N-1]);
CUDA_CHECK(cudaFree(data));
return 0;
}
统一内存的优势:
对于性能关键的应用,手动控制数据位置往往更高效:
cpp复制// 预取数据到GPU设备
int device = 0;
CUDA_CHECK(cudaMemPrefetchAsync(data, N * sizeof(float), device, 0));
// 执行核函数
compute<<<...>>>(data, N);
// 预取回CPU
CUDA_CHECK(cudaMemPrefetchAsync(data, N * sizeof(float), cudaCpuDeviceId, 0));
预取的最佳实践:
使用C++ RAII(Resource Acquisition Is Initialization)模式管理CUDA资源可以避免内存泄漏:
cpp复制template<typename T>
class DeviceArray {
T* ptr_ = nullptr;
size_t size_ = 0;
public:
DeviceArray() = default;
explicit DeviceArray(size_t size) : size_(size) {
CUDA_CHECK(cudaMalloc(&ptr_, size_ * sizeof(T)));
}
~DeviceArray() {
if (ptr_) cudaFree(ptr_);
}
// 禁止拷贝
DeviceArray(const DeviceArray&) = delete;
DeviceArray& operator=(const DeviceArray&) = delete;
// 允许移动
DeviceArray(DeviceArray&& other) noexcept
: ptr_(other.ptr_), size_(other.size_) {
other.ptr_ = nullptr;
other.size_ = 0;
}
DeviceArray& operator=(DeviceArray&& other) noexcept {
if (this != &other) {
if (ptr_) cudaFree(ptr_);
ptr_ = other.ptr_;
size_ = other.size_;
other.ptr_ = nullptr;
other.size_ = 0;
}
return *this;
}
T* data() noexcept { return ptr_; }
const T* data() const noexcept { return ptr_; }
size_t size() const noexcept { return size_; }
// 从主机拷贝数据到设备
void copyFromHost(const T* host, size_t count) {
CUDA_CHECK(cudaMemcpy(ptr_, host,
std::min(count, size_) * sizeof(T),
cudaMemcpyHostToDevice));
}
// 从设备拷贝数据到主机
void copyToHost(T* host, size_t count) const {
CUDA_CHECK(cudaMemcpy(host, ptr_,
std::min(count, size_) * sizeof(T),
cudaMemcpyDeviceToHost));
}
};
使用示例:
cpp复制DeviceArray<float> devArray(1024);
std::vector<float> hostArray(1024, 1.0f);
// 拷贝数据到设备
devArray.copyFromHost(hostArray.data(), hostArray.size());
// 使用设备指针
someKernel<<<...>>>(devArray.data(), devArray.size());
同样可以封装统一内存:
cpp复制template<typename T>
class UnifiedArray {
T* ptr_ = nullptr;
size_t size_ = 0;
public:
UnifiedArray() = default;
explicit UnifiedArray(size_t size) : size_(size) {
CUDA_CHECK(cudaMallocManaged(&ptr_, size_ * sizeof(T)));
}
~UnifiedArray() {
if (ptr_) cudaFree(ptr_);
}
// ... 类似的移动操作和访问接口 ...
// 预取到GPU
void prefetchToGpu(int device = 0) {
CUDA_CHECK(cudaMemPrefetchAsync(ptr_, size_ * sizeof(T), device, 0));
}
// 预取到CPU
void prefetchToCpu() {
CUDA_CHECK(cudaMemPrefetchAsync(ptr_, size_ * sizeof(T),
cudaCpuDeviceId, 0));
}
};
CUDA提供多种原子操作,保证多线程安全访问:
cpp复制__global__ void atomicCounter(int* counter) {
atomicAdd(counter, 1); // 原子加
}
int main() {
int* d_counter = nullptr;
CUDA_CHECK(cudaMalloc(&d_counter, sizeof(int)));
int h_counter = 0;
CUDA_CHECK(cudaMemcpy(d_counter, &h_counter, sizeof(int),
cudaMemcpyHostToDevice));
atomicCounter<<<100, 128>>>(d_counter);
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(&h_counter, d_counter, sizeof(int),
cudaMemcpyDeviceToHost));
printf("Final counter: %d\n", h_counter); // 应该输出12800
CUDA_CHECK(cudaFree(d_counter));
return 0;
}
常用原子操作:
atomicAdd/atomicSub:加减atomicAnd/atomicOr/atomicXor:位运算atomicMin/atomicMax:最小/最大值atomicCAS:比较并交换(最强大的原子操作)使用atomicCAS可以实现更复杂的原子操作:
cpp复制__device__ float atomicMaxFloat(float* addr, float value) {
int* addr_as_int = (int*)addr;
int old = *addr_as_int;
int assumed;
do {
assumed = old;
old = atomicCAS(addr_as_int, assumed,
__float_as_int(fmaxf(value, __int_as_float(assumed))));
} while (assumed != old);
return __int_as_float(old);
}
归约(Reduction)是并行计算的常见模式,用于计算总和、最大值等:
cpp复制__global__ void reduceSum(const float* input, float* output, int N) {
extern __shared__ float sdata[];
int tid = threadIdx.x;
int i = blockIdx.x * blockDim.x + tid;
// 加载数据到共享内存
sdata[tid] = (i < N) ? input[i] : 0.0f;
__syncthreads();
// 在共享内存中执行归约
for (int s = blockDim.x/2; s > 0; s >>= 1) {
if (tid < s) {
sdata[tid] += sdata[tid + s];
}
__syncthreads();
}
// 第一个线程写入结果
if (tid == 0) {
atomicAdd(output, sdata[0]);
}
}
启动这个核函数时需要注意共享内存大小:
cpp复制int threads = 256;
int blocks = (N + threads - 1) / threads;
reduceSum<<<blocks, threads, threads*sizeof(float)>>>(d_input, d_output, N);
共享内存是块内线程共享的高速内存,访问延迟比全局内存低得多:
cpp复制__global__ void sharedMemExample(float* input, float* output, int N) {
extern __shared__ float sdata[];
int tid = threadIdx.x;
int i = blockIdx.x * blockDim.x + tid;
// 从全局内存加载数据到共享内存
sdata[tid] = (i < N) ? input[i] : 0.0f;
// 等待所有线程完成加载
__syncthreads();
// 使用共享内存中的数据
float result = sdata[tid] * 2.0f;
// 如果需要,可以再次同步
__syncthreads();
// 将结果写回全局内存
if (i < N) output[i] = result;
}
共享内存的关键点:
extern __shared__声明动态共享内存__syncthreads()同步线程共享内存可以显著提高内存访问密集型操作的性能,如矩阵转置:
cpp复制__global__ void transposeNaive(float* odata, const float* idata,
int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
if (x < width && y < height) {
odata[x * height + y] = idata[y * width + x];
}
}
__global__ void transposeShared(float* odata, const float* idata,
int width, int height) {
__shared__ float tile[BLOCK_DIM][BLOCK_DIM+1]; // +1避免bank冲突
int x = blockIdx.x * BLOCK_DIM + threadIdx.x;
int y = blockIdx.y * BLOCK_DIM + threadIdx.y;
if (x < width && y < height) {
tile[threadIdx.y][threadIdx.x] = idata[y * width + x];
}
__syncthreads();
x = blockIdx.y * BLOCK_DIM + threadIdx.x; // 转置块索引
y = blockIdx.x * BLOCK_DIM + threadIdx.y;
if (x < height && y < width) {
odata[y * height + x] = tile[threadIdx.x][threadIdx.y];
}
}
共享内存版本通过以下方式优化性能:
CUDA流表示一系列顺序执行的操作,不同流可以并发执行:
cpp复制cudaStream_t stream1, stream2;
CUDA_CHECK(cudaStreamCreate(&stream1));
CUDA_CHECK(cudaStreamCreate(&stream2));
// 在流1中执行
someKernel<<<grid, block, 0, stream1>>>(...);
CUDA_CHECK(cudaMemcpyAsync(..., stream1));
// 在流2中并发执行
otherKernel<<<grid, block, 0, stream2>>>(...);
CUDA_CHECK(cudaMemcpyAsync(..., stream2));
// 等待流完成
CUDA_CHECK(cudaStreamSynchronize(stream1));
CUDA_CHECK(cudaStreamSynchronize(stream2));
// 清理
CUDA_CHECK(cudaStreamDestroy(stream1));
CUDA_CHECK(cudaStreamDestroy(stream2));
异步拷贝需要使用页锁定(pinned)主机内存:
cpp复制float* h_data = nullptr;
CUDA_CHECK(cudaMallocHost(&h_data, N * sizeof(float))); // 页锁定分配
// 使用异步拷贝
CUDA_CHECK(cudaMemcpyAsync(d_data, h_data, N * sizeof(float),
cudaMemcpyHostToDevice, stream));
// ... 可以继续执行其他CPU工作 ...
CUDA_CHECK(cudaStreamSynchronize(stream));
CUDA_CHECK(cudaFreeHost(h_data)); // 释放页锁定内存
页锁定内存的特点:
CUDA事件可用于精确测量GPU操作时间:
cpp复制cudaEvent_t start, stop;
CUDA_CHECK(cudaEventCreate(&start));
CUDA_CHECK(cudaEventCreate(&stop));
CUDA_CHECK(cudaEventRecord(start, 0));
// 执行要测量的核函数或操作
someKernel<<<...>>>(...);
CUDA_CHECK(cudaEventRecord(stop, 0));
CUDA_CHECK(cudaEventSynchronize(stop));
float milliseconds = 0;
CUDA_CHECK(cudaEventElapsedTime(&milliseconds, start, stop));
printf("Time: %f ms\n", milliseconds);
CUDA_CHECK(cudaEventDestroy(start));
CUDA_CHECK(cudaEventDestroy(stop));
合并访问:确保相邻线程访问相邻内存位置
cpp复制// 好的访问模式(合并)
__global__ void goodAccess(float* data) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
float x = data[i]; // 相邻线程访问相邻地址
}
// 差的访问模式(不合并)
__global__ void badAccess(float* data, int stride) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
float x = data[i * stride]; // 可能导致非合并访问
}
利用共享内存:减少全局内存访问
适当使用常量内存:对只读数据有效
利用局部性:尽量重用已加载的数据
使用多流实现计算与通信重叠:
cpp复制cudaStream_t computeStream, copyStream;
CUDA_CHECK(cudaStreamCreate(&computeStream));
CUDA_CHECK(cudaStreamCreate(©Stream));
// 在copyStream中异步传输数据
CUDA_CHECK(cudaMemcpyAsync(d_data1, h_data1, size,
cudaMemcpyHostToDevice, copyStream));
// 在computeStream中执行计算(与传输重叠)
someKernel<<<..., computeStream>>>(d_data2, ...);
// 可能需要同步流
CUDA_CHECK(cudaStreamSynchronize(copyStream));
CUDA_CHECK(cudaStreamSynchronize(computeStream));
选择块大小的经验法则:
可以通过基准测试找到最佳配置:
cpp复制void benchmark() {
const int N = 1<<20;
float *d_data;
CUDA_CHECK(cudaMalloc(&d_data, N * sizeof(float)));
for (int threads = 32; threads <= 1024; threads *= 2) {
cudaEvent_t start, stop;
CUDA_CHECK(cudaEventCreate(&start));
CUDA_CHECK(cudaEventCreate(&stop));
int blocks = (N + threads - 1) / threads;
CUDA_CHECK(cudaEventRecord(start));
for (int i = 0; i < 100; ++i) {
someKernel<<<blocks, threads>>>(d_data, N);
}
CUDA_CHECK(cudaEventRecord(stop));
CUDA_CHECK(cudaEventSynchronize(stop));
float ms;
CUDA_CHECK(cudaEventElapsedTime(&ms, start, stop));
printf("%d threads: %f ms per iteration\n", threads, ms/100);
CUDA_CHECK(cudaEventDestroy(start));
CUDA_CHECK(cudaEventDestroy(stop));
}
CUDA_CHECK(cudaFree(d_data));
}
检查内存错误:
bash复制cuda-memcheck ./your_cuda_program
Nsight Systems:系统级性能分析
bash复制nsys profile -o output_report ./your_cuda_program
Nsight Compute:核函数级性能分析
bash复制ncu -o profile_output ./your_cuda_program
CUDA核函数中可以直接使用printf:
cpp复制__global__ void debugKernel(int* data, int N) {
int tid = blockIdx.x * blockDim.x + threadIdx.x;
if (tid < N) {
printf("tid=%d, data=%d\n", tid, data[tid]);
data[tid] *= 2;
}
}
注意:
无效的设备指针:
核函数启动失败:
错误的结果:
性能不如预期:
在实际项目中,我通常会先确保功能正确,然后再逐步优化性能。记住这句格言:"先让它正确,再让它快"(Make it right, then make it fast)。CUDA编程需要特别注意线程同步、内存一致性和并行算法设计,这些都是与传统的CPU编程不同的地方。