03-CUDA 编程入门¶
重要性:⭐⭐⭐⭐⭐ 实用度:⭐⭐⭐⭐⭐ 学习时间: 3 天 必须掌握:是
为什么学这一章¶
CUDA 是 NVIDIA 推出的并行计算平台和编程模型,是 GPU 编程的事实标准。掌握 CUDA 能让你: - 编写高性能的并行程序 - 理解深度学习框架的底层实现 - 优化 AI 训练和推理性能 - 为学习其他 GPU 编程技术打下基础
学完这一章,你将能够: - ✅ 编写基本的 CUDA 程序 - ✅ 理解 CUDA 内存管理 - ✅ 掌握 CUDA 线程组织 - ✅ 优化 CUDA 程序性能
📖 核心概念¶
1. CUDA 程序结构¶
┌─────────────────────────────────────────────────────────────────────┐
│ CUDA程序基本结构 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ CUDA程序 = CPU代码(Host)+ GPU代码(Device) │
│ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ CPU代码(Host Code) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ 1. 分配主机内存 │ │ │
│ │ │ float* h_data = new float[N]; │ │ │
│ │ │ │ │ │
│ │ │ 2. 初始化数据 │ │ │
│ │ │ for (int i = 0; i < N; i++) h_data[i] = i; │ │ │
│ │ │ │ │ │
│ │ │ 3. 分配设备内存 │ │ │
│ │ │ float* d_data; │ │ │
│ │ │ cudaMalloc(&d_data, N * sizeof(float)); │ │ │
│ │ │ │ │ │
│ │ │ 4. 拷贝数据到GPU(Host → Device) │ │ │
│ │ │ cudaMemcpy(d_data, h_data, N * sizeof(float), │ │ │
│ │ │ cudaMemcpyHostToDevice); │ │ │
│ │ │ │ │ │
│ │ │ 5. 启动内核(Kernel) │ │ │
│ │ │ myKernel<<<blocks, threads>>>(d_data, N); │ │ │
│ │ │ │ │ │
│ │ │ 6. 等待GPU完成 │ │ │
│ │ │ cudaDeviceSynchronize(); │ │ │
│ │ │ │ │ │
│ │ │ 7. 拷贝结果回CPU(Device → Host) │ │ │
│ │ │ cudaMemcpy(h_data, d_data, N * sizeof(float), │ │ │
│ │ │ cudaMemcpyDeviceToHost); │ │ │
│ │ │ │ │ │
│ │ │ 8. 释放内存 │ │ │
│ │ │ delete[] h_data; │ │ │
│ │ │ cudaFree(d_data); │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ ↓ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ GPU代码(Device Code) │ │
│ │ ┌─────────────────────────────────────────────────────┐ │ │
│ │ │ __global__ void myKernel(float* data, int n) { │ │ │
│ │ │ // 计算线程ID │ │ │
│ │ │ int i = blockIdx.x * blockDim.x + threadIdx.x; │ │ │
│ │ │ │ │ │
│ │ │ // 检查边界 │ │ │
│ │ │ if (i < n) { │ │ │
│ │ │ // 执行计算 │ │ │
│ │ │ data[i] = data[i] * 2.0f; │ │ │
│ │ │ } │ │ │
│ │ │ } │ │ │
│ │ └─────────────────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────┘
CUDA 函数类型修饰符¶
| 修饰符 | 执行位置 | 调用位置 | 说明 |
|---|---|---|---|
__global__ | GPU | CPU | 内核函数,<<< >>>启动 |
__device__ | GPU | GPU | 设备函数,只能被内核调用 |
__host__ | CPU | CPU | 普通函数(默认) |
__host__ __device__ | CPU/GPU | CPU/GPU | 可在两者上编译 |
2. CUDA 内存管理¶
内存分配与释放¶
// 主机内存(CPU内存)
float* h_data = new float[N]; // C++方式
cudaMallocHost(&h_data, N * sizeof(float)); // CUDA页锁定内存(更快传输)
delete[] h_data; // 释放C++内存
cudaFreeHost(h_data); // 释放页锁定内存
// 设备内存(GPU显存)
float* d_data;
cudaMalloc(&d_data, N * sizeof(float)); // 分配显存
cudaFree(d_data); // 释放显存
// 统一内存(Unified Memory)- 自动管理
float* u_data;
cudaMallocManaged(&u_data, N * sizeof(float)); // CPU和GPU都能访问
cudaFree(u_data);
内存拷贝¶
// 主机到设备(Host to Device)
cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice);
// 设备到主机(Device to Host)
cudaMemcpy(h_data, d_data, size, cudaMemcpyDeviceToHost);
// 设备到设备(Device to Device)
cudaMemcpy(d_dst, d_src, size, cudaMemcpyDeviceToDevice);
// 异步拷贝(非阻塞)
cudaMemcpyAsync(d_data, h_data, size, cudaMemcpyHostToDevice, stream);
内存类型对比¶
┌─────────────────────────────────────────────────────────────────────┐
│ CUDA内存类型对比 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 1. 页锁定内存(Pinned Memory) │
│ • 使用cudaMallocHost分配 │
│ • 不会被换出到磁盘 │
│ • CPU↔GPU传输速度更快(2倍以上) │
│ • 占用物理内存,不宜分配过多 │
│ │
│ 2. 零拷贝内存(Zero-Copy Memory) │
│ • 使用cudaHostAlloc分配,带cudaHostAllocMapped标志 │
│ • GPU可以直接访问CPU内存 │
│ • 适合小数据量、频繁访问的场景 │
│ • 访问速度慢于显存 │
│ │
│ 3. 统一内存(Unified Memory) │
│ • 使用cudaMallocManaged分配 │
│ • 自动在CPU和GPU间迁移数据 │
│ • 简化编程,但性能可能不如手动管理 │
│ • 适合快速原型开发 │
│ │
│ 性能排序:显存 > 页锁定内存 > 零拷贝内存 > 可分页内存 │
│ │
└─────────────────────────────────────────────────────────────────────┘
3. CUDA 线程组织¶
线程索引计算¶
// 一维网格和一维块
__global__ void kernel1D(float* data) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// idx范围:0 到 (gridDim.x * blockDim.x - 1)
}
// 二维网格和二维块
__global__ void kernel2D(float* data, int width) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int idx = y * width + x;
// 适合图像处理
}
// 三维网格和三维块
__global__ void kernel3D(float* data, int width, int height) {
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int z = blockIdx.z * blockDim.z + threadIdx.z;
int idx = z * width * height + y * width + x;
// 适合3D模拟
}
线程配置示例¶
// 向量加法(100万元素)
int n = 1000000;
int threadsPerBlock = 256;
int blocksPerGrid = (n + threadsPerBlock - 1) / threadsPerBlock;
vectorAdd<<<blocksPerGrid, threadsPerBlock>>>(d_A, d_B, d_C, n);
// 图像处理(1024x768图像)
dim3 threadsPerBlock(16, 16);
dim3 blocksPerGrid(
(1024 + threadsPerBlock.x - 1) / threadsPerBlock.x,
(768 + threadsPerBlock.y - 1) / threadsPerBlock.y
);
imageProcess<<<blocksPerGrid, threadsPerBlock>>>(d_image, 1024, 768);
4. CUDA 优化技术¶
内存访问优化¶
// 不好的内存访问模式(非合并访问)
__global__ void badAccess(float* out, float* in, int n) {
int idx = threadIdx.x;
for (int i = 0; i < n; i += blockDim.x) {
out[i] = in[idx]; // 线程0读in[0], 线程1读in[1]...
// 每个线程访问不同的内存位置,无法合并
}
}
// 好的内存访问模式(合并访问)
__global__ void goodAccess(float* out, float* in, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
out[idx] = in[idx]; // 相邻线程访问相邻内存
// 可以合并成一次内存事务
}
}
共享内存优化¶
// 使用共享内存的矩阵乘法(简化版)
#define TILE_SIZE 16
__global__ void matrixMulShared(float* C, float* A, float* B, int N) {
__shared__ float As[TILE_SIZE][TILE_SIZE];
__shared__ float Bs[TILE_SIZE][TILE_SIZE];
int bx = blockIdx.x, by = blockIdx.y;
int tx = threadIdx.x, ty = threadIdx.y;
int row = by * TILE_SIZE + ty;
int col = bx * TILE_SIZE + tx;
float sum = 0.0f;
for (int m = 0; m < N / TILE_SIZE; ++m) {
// 协作加载数据到共享内存
As[ty][tx] = A[row * N + m * TILE_SIZE + tx];
Bs[ty][tx] = B[(m * TILE_SIZE + ty) * N + col];
__syncthreads(); // 等待所有线程加载完成
// 计算部分结果
for (int k = 0; k < TILE_SIZE; ++k) {
sum += As[ty][k] * Bs[k][tx];
}
__syncthreads(); // 等待计算完成,防止覆盖数据
}
C[row * N + col] = sum;
}
避免线程发散¶
// 不好的代码(线程发散)
__global__ void divergent(float* out, float* in, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
if (idx % 2 == 0) {
out[idx] = in[idx] * 2; // 偶数线程执行
} else {
out[idx] = in[idx] + 1; // 奇数线程执行
}
// Warp内线程执行不同路径,性能下降
}
}
// 好的代码(避免发散)
__global__ void nonDivergent(float* out, float* in, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
float factor = (idx % 2 == 0) ? 2.0f : 0.0f;
float addend = (idx % 2 == 0) ? 0.0f : 1.0f;
out[idx] = in[idx] * factor + addend;
// 所有线程执行相同指令
}
}
🧪 动手实验¶
实验 1 :矩阵乘法优化¶
目的:学习 CUDA 优化技术
步骤:
- 创建基础版本
// matrix_mul_basic.cu
__global__ void matrixMulBasic(float* C, float* A, float* B, int N) {
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < N && col < N) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {
sum += A[row * N + k] * B[k * N + col];
}
C[row * N + col] = sum;
}
}
- 创建优化版本(使用共享内存)
// matrix_mul_optimized.cu
#define TILE_SIZE 32
__global__ void matrixMulOptimized(float* C, float* A, float* B, int N) {
__shared__ float As[TILE_SIZE][TILE_SIZE];
__shared__ float Bs[TILE_SIZE][TILE_SIZE];
int bx = blockIdx.x, by = blockIdx.y;
int tx = threadIdx.x, ty = threadIdx.y;
int row = by * TILE_SIZE + ty;
int col = bx * TILE_SIZE + tx;
float sum = 0.0f;
for (int m = 0; m < (N + TILE_SIZE - 1) / TILE_SIZE; ++m) {
if (row < N && m * TILE_SIZE + tx < N)
As[ty][tx] = A[row * N + m * TILE_SIZE + tx];
else
As[ty][tx] = 0.0f;
if (col < N && m * TILE_SIZE + ty < N)
Bs[ty][tx] = B[(m * TILE_SIZE + ty) * N + col];
else
Bs[ty][tx] = 0.0f;
__syncthreads();
for (int k = 0; k < TILE_SIZE; ++k) {
sum += As[ty][k] * Bs[k][tx];
}
__syncthreads();
}
if (row < N && col < N)
C[row * N + col] = sum;
}
- 对比性能
int main() {
int N = 1024;
size_t size = N * N * sizeof(float);
// 分配内存、初始化数据...
// 测试基础版本
dim3 threads(16, 16);
dim3 blocks((N + 15) / 16, (N + 15) / 16);
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
cudaEventRecord(start);
matrixMulBasic<<<blocks, threads>>>(d_C, d_A, d_B, N);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float time_basic;
cudaEventElapsedTime(&time_basic, start, stop);
// 测试优化版本
dim3 threads_opt(32, 32);
dim3 blocks_opt((N + 31) / 32, (N + 31) / 32);
cudaEventRecord(start);
matrixMulOptimized<<<blocks_opt, threads_opt>>>(d_C, d_A, d_B, N);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float time_optimized;
cudaEventElapsedTime(&time_optimized, start, stop);
std::cout << "Basic: " << time_basic << " ms" << std::endl;
std::cout << "Optimized: " << time_optimized << " ms" << std::endl;
std::cout << "Speedup: " << time_basic / time_optimized << "x" << std::endl;
return 0;
}
实验 2 :使用 CUDA 事件计时¶
目的:学习精确的性能测量
步骤:
// timing.cu
#include <cuda_runtime.h>
#include <iostream>
__global__ void workload(float* data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
float x = data[idx];
for (int i = 0; i < 1000; i++) {
x = sinf(x) + cosf(x);
}
data[idx] = x;
}
}
int main() {
int n = 1000000;
size_t size = n * sizeof(float);
float *d_data;
cudaMalloc(&d_data, size);
// 创建CUDA事件
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// 预热GPU
workload<<<(n + 255) / 256, 256>>>(d_data, n);
cudaDeviceSynchronize();
// 计时
cudaEventRecord(start);
workload<<<(n + 255) / 256, 256>>>(d_data, n);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
std::cout << "Kernel execution time: " << milliseconds << " ms" << std::endl;
// 清理
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaFree(d_data);
return 0;
}
实验 3 :使用统一内存¶
目的:简化内存管理
步骤:
// unified_memory.cu
#include <cuda_runtime.h> // 引入头文件
#include <iostream>
__global__ void scale(float* data, int n, float factor) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
data[idx] *= factor;
}
}
int main() {
int n = 1000000;
// 分配统一内存
float* data;
cudaMallocManaged(&data, n * sizeof(float));
// 在CPU上初始化
for (int i = 0; i < n; i++) {
data[i] = i * 0.1f;
}
// 在GPU上执行
scale<<<(n + 255) / 256, 256>>>(data, n, 2.0f);
cudaDeviceSynchronize();
// 在CPU上验证
std::cout << "data[100] = " << data[100] << std::endl;
std::cout << "Expected: " << 100 * 0.1f * 2.0f << std::endl;
// 释放
cudaFree(data);
return 0;
}
💡 核心要点总结¶
CUDA 编程流程¶
高级内存优化技术¶
1. Bank Conflict 优化¶
共享内存被划分为多个 bank ,同时访问同一 bank 的不同地址会导致 bank conflict 。
// 有Bank Conflict的代码
__global__ void bankConflictExample(float* data) {
__shared__ float shared[256];
int tid = threadIdx.x;
// 32个线程同时访问shared[tid * 2],产生2-way bank conflict
float val = shared[tid * 2];
}
// 无Bank Conflict的代码
__global__ void noBankConflictExample(float* data) {
__shared__ float shared[256];
int tid = threadIdx.x;
// 32个线程访问连续的bank
float val = shared[tid];
}
解决 Bank Conflict 的方法: - 使用 padding :在共享内存数组中添加额外的列 - 改变访问模式:使用转置或其他访问模式 - 使用广播:当多个线程读取同一地址时,自动广播无 conflict
2. 异步数据传输与计算重叠¶
// 使用CUDA Stream实现计算与数据传输重叠
cudaStream_t stream1, stream2;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// 将数据分成两部分,在stream1和stream2中并行处理
for (int i = 0; i < 2; i++) {
int offset = i * halfSize;
cudaMemcpyAsync(d_data + offset, h_data + offset,
halfSize * sizeof(float),
cudaMemcpyHostToDevice,
(i == 0) ? stream1 : stream2);
kernel<<<(halfSize + 255) / 256, 256, 0,
(i == 0) ? stream1 : stream2>>>(d_data + offset);
cudaMemcpyAsync(h_data + offset, d_data + offset,
halfSize * sizeof(float),
cudaMemcpyDeviceToHost,
(i == 0) ? stream1 : stream2);
}
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);
3. 纹理内存和常量内存优化¶
// 常量内存 - 适合只读的小数据(64KB)
__constant__ float constData[256];
cudaMemcpyToSymbol(constData, h_data, 256 * sizeof(float));
// 纹理内存 - 适合具有空间局部性的2D数据
// ⚠️ 注意:以下纹理引用(texture reference)API 在 CUDA 12.0 中已弃用
// 新代码应使用纹理对象(texture object)API,参见 CUDA Programming Guide
texture<float, cudaTextureType2D, cudaReadModeElementType> texRef;
cudaBindTexture2D(0, texRef, d_data, desc, width, height, pitch);
// 在内核中使用纹理内存
__global__ void textureKernel(float* output, 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) {
// 使用纹理缓存,自动处理边界
output[y * width + x] = tex2D(texRef, x, y);
}
}
4. 零拷贝内存( Zero-Copy )¶
// 零拷贝内存 - GPU直接访问CPU内存,适合小数据频繁访问
float* zeroCopyData; // 指针:存储变量的内存地址
cudaHostAlloc(&zeroCopyData, size, cudaHostAllocMapped);
// 获取设备指针
float* d_zeroCopyData;
cudaHostGetDevicePointer(&d_zeroCopyData, zeroCopyData, 0);
// GPU直接访问CPU内存(无需显式拷贝)
kernel<<<blocks, threads>>>(d_zeroCopyData);
优化检查清单¶
- 使用合并内存访问
- 利用共享内存减少全局内存访问
- 避免 Bank Conflict
- 避免线程发散
- 选择合适的 Block 大小( 32 的倍数)
- 隐藏内存延迟(足够的线程数)
- 使用页锁定内存加速传输
- 考虑使用统一内存简化开发
- 使用 CUDA Stream 实现计算与传输重叠
- 使用常量内存存储只读小数据
- 使用纹理内存处理 2D 数据
性能分析工具¶
# 使用nvprof分析性能(旧版本)
nvprof ./your_program
# 使用Nsight Compute(新版本)
ncu ./your_program
# 查看内存带宽利用率
ncu --metrics dram__bytes.sum.per_second ./your_program
# 查看共享内存bank conflict
ncu --metrics sm__sass_l1_bank_conflict ./your_program
常见错误¶
| 错误 | 原因 | 解决 |
|---|---|---|
| cudaErrorMemoryAllocation | 显存不足 | 减少数据量或分批处理 |
| cudaErrorLaunchFailure | 内核崩溃 | 检查数组越界和非法内存访问 |
| cudaErrorInvalidValue | 参数错误 | 检查线程配置和数据大小 |
| 结果不正确 | 竞争条件 | 使用__syncthreads()同步 |
❓ 常见问题¶
Q1 :如何确定最佳的 Block 大小?
A :一般原则: - 使用 Warp 大小的倍数( 32 ) - 常见选择: 128, 256, 512 - 考虑寄存器使用量( nvcc --ptxas-options=-v 查看) - 实验测试不同配置
Q2 :为什么我的 CUDA 程序比 CPU 还慢?
A :可能原因: - 数据量太小(传输开销 > 计算收益) - 内存访问模式不好(非合并访问) - 线程发散严重 - 没有充分利用并行性
Q3 :如何调试 CUDA 程序?
A :方法: - 使用 cuda-gdb (命令行调试器) - 使用 Nsight ( NVIDIA 的 IDE 插件) - 使用 printf 在内核中输出 - 检查 cudaError_t 返回值
Q4 : CUDA 程序可以移植到 AMD GPU 吗?
A :不能直接移植。但可以使用: - HIP ( AMD 的 CUDA 兼容层) - OpenCL (跨平台) - SYCL (现代 C++标准)
📚 扩展阅读¶
- 《 CUDA by Example 》 - Jason Sanders
- 《 Programming Massively Parallel Processors 》 - David Kirk
- NVIDIA CUDA 最佳实践指南: docs.nvidia.com/cuda/cuda-c-best-practices-guide/
🎯 下一步¶
继续学习 GPU 并行计算的后续内容,深入理解并行算法设计、内存模型和数据传输优化。