CUDA 补充教程 - 进阶与深入
目录
- [第九课:CUDA 错误处理](#第九课:CUDA 错误处理)
- 第十课:原子操作
- [第十一课:CUDA 流与异步执行](#第十一课:CUDA 流与异步执行)
- [第十二课:CUDA 事件与性能计时](#第十二课:CUDA 事件与性能计时)
- 第十三课:统一内存
- 第十四课:常量内存
- 第十五课:纹理内存
- 第十六课:并行归约算法
- 第十七课:前缀和(扫描)算法
- [第十八课:Warp 级编程](#第十八课:Warp 级编程)
- 第十九课:动态并行
- [第二十课:多 GPU 编程](#第二十课:多 GPU 编程)
- [第二十一课:CUDA 调试技术](#第二十一课:CUDA 调试技术)
- 第二十二课:性能分析工具
- 第二十三课:内存访问模式深入
- 第二十四课:寄存器与缓存优化
第九课:CUDA 错误处理
知识点
为什么需要错误处理?
CUDA API 调用可能失败,常见原因:
- 内存不足
- 设备不存在
- 内核启动失败
- 驱动程序错误
不检查错误会导致:
- 程序崩溃
- 结果错误
- 难以调试
CUDA 错误类型
cpp
typedef enum cudaError {
cudaSuccess = 0, // 成功
cudaErrorInvalidValue = 1, // 无效参数
cudaErrorMemoryAllocation = 2, // 内存分配失败
cudaErrorInvalidDevice = 10, // 无效设备
cudaErrorInvalidMemcpyDirection = 21, // 无效拷贝方向
// ... 更多错误码
} cudaError;
错误检查函数
cpp
// 基本错误检查
cudaError_t err = cudaMalloc(&d_data, size);
if (err != cudaSuccess) {
printf("CUDA 错误: %s\n", cudaGetErrorString(err));
exit(1);
}
封装错误检查宏
cpp
// 定义错误检查宏
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA 错误 at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(1); \
} \
} while(0)
// 使用宏
CUDA_CHECK(cudaMalloc(&d_data, size));
CUDA_CHECK(cudaMemcpy(d_data, h_data, size, cudaMemcpyHostToDevice));
内核启动错误检查
cpp
__global__ void myKernel(int *data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
data[idx] = idx * 2;
}
}
int main() {
// 启动内核
myKernel<<<grid, block>>>(d_data, n);
// 检查内核启动错误
cudaError_t err = cudaGetLastError();
if (err != cudaSuccess) {
printf("内核启动失败: %s\n", cudaGetErrorString(err));
return -1;
}
// 等待内核完成并检查执行错误
err = cudaDeviceSynchronize();
if (err != cudaSuccess) {
printf("内核执行失败: %s\n", cudaGetErrorString(err));
return -1;
}
return 0;
}
完整的错误处理模板
cpp
#include <stdio.h>
#include <stdlib.h>
#define CUDA_CHECK(call) \
do { \
cudaError_t err = call; \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA 错误 at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(1); \
} \
} while(0)
#define CUDA_KERNEL_CHECK() \
do { \
cudaError_t err = cudaGetLastError(); \
if (err != cudaSuccess) { \
fprintf(stderr, "内核启动错误 at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(1); \
} \
err = cudaDeviceSynchronize(); \
if (err != cudaSuccess) { \
fprintf(stderr, "内核执行错误 at %s:%d: %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(1); \
} \
} while(0)
int main() {
int n = 1000;
size_t size = n * sizeof(float);
float *d_data;
CUDA_CHECK(cudaMalloc(&d_data, size));
myKernel<<<grid, block>>>(d_data, n);
CUDA_KERNEL_CHECK();
CUDA_CHECK(cudaFree(d_data));
return 0;
}
练习题 9
- CUDA 错误码
cudaSuccess的值是什么? cudaGetLastError()和cudaDeviceSynchronize()分别检查什么错误?- 为什么内核启动后需要调用
cudaDeviceSynchronize()才能检测到执行错误?
第十课:原子操作
知识点
什么是原子操作?
原子操作是不可分割的操作,在多线程环境下保证数据一致性。
问题场景:
cpp
// 非原子操作(危险!)
int count = 0;
__global__ void increment(int *count) {
(*count)++; // 多个线程同时执行,结果不确定
}
解决方案:使用原子操作
CUDA 原子函数
| 函数 | 操作 | 说明 |
|---|---|---|
atomicAdd() |
加法 | *addr += val |
atomicSub() |
减法 | *addr -= val |
atomicExch() |
交换 | *addr = val |
atomicMin() |
最小值 | *addr = min(*addr, val) |
atomicMax() |
最大值 | *addr = max(*addr, val) |
atomicInc() |
递增 | *addr = (*addr >= val) ? 0 : *addr + 1 |
atomicDec() |
递减 | `*addr = (*addr == 0) |
atomicCAS() |
比较并交换 | 条件交换 |
atomicAnd() |
与运算 | *addr &= val |
atomicOr() |
或运算 | `*addr |
atomicXor() |
异或运算 | *addr ^= val |
atomicAdd 示例
cpp
#include <stdio.h>
__global__ void atomicAddKernel(int *count, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
atomicAdd(count, 1); // 原子递增
}
}
int main() {
int n = 10000;
int h_count = 0;
int *d_count;
cudaMalloc(&d_count, sizeof(int));
cudaMemcpy(d_count, &h_count, sizeof(int), cudaMemcpyHostToDevice);
int blockSize = 256;
int gridSize = (n + blockSize - 1) / blockSize;
atomicAddKernel<<<gridSize, blockSize>>>(d_count, n);
cudaMemcpy(&h_count, d_count, sizeof(int), cudaMemcpyDeviceToHost);
printf("计数结果: %d (预期: %d)\n", h_count, n);
cudaFree(d_count);
return 0;
}
atomicCAS(比较并交换)
cpp
// atomicCAS(int *addr, int compare, int val)
// 如果 *addr == compare,则 *addr = val
// 返回 *addr 的旧值
__global__ void casExample(int *data, int old_val, int new_val) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx == 0) {
int old = atomicCAS(data, old_val, new_val);
printf("旧值: %d, 新值: %d\n", old, new_val);
}
}
原子操作实现锁
cpp
struct Lock {
int *mutex;
Lock() {
cudaMalloc(&mutex, sizeof(int));
cudaMemset(mutex, 0, sizeof(int));
}
~Lock() {
cudaFree(mutex);
}
__device__ void lock() {
while (atomicCAS(mutex, 0, 1) != 0) {
// 等待锁释放
}
}
__device__ void unlock() {
atomicExch(mutex, 0);
}
};
__global__ void kernelWithLock(int *data, Lock lock) {
lock.lock();
// 临界区代码
(*data)++;
lock.unlock();
}
原子操作性能考虑
- 原子操作比普通操作慢
- 多个线程对同一地址原子操作会串行化
- 尽量减少原子操作的使用
- 考虑使用共享内存减少全局内存原子操作
练习题 10
- 为什么多线程环境下普通递增操作
(*count)++会产生错误结果? atomicAdd(addr, val)的作用是什么?返回值是什么?- 如何使用原子操作实现一个简单的互斥锁?
第十一课:CUDA 流与异步执行
知识点
什么是 CUDA 流?
CUDA 流是一系列按顺序执行的命令队列。不同流中的命令可以并发执行。
默认流(Stream 0):
┌─────────────────────────────────────┐
│ 内核A → 拷贝1 → 内核B → 拷贝2 │ 串行执行
└─────────────────────────────────────┘
多流并发:
Stream 1: ┌─────────────────────────────┐
│ 内核A → 拷贝1 │
└─────────────────────────────┘
Stream 2: ┌─────────────────────────────┐
│ 内核B → 拷贝2 │ 并发执行
└─────────────────────────────┘
创建和使用流
cpp
cudaStream_t stream1, stream2;
// 创建流
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
// 在指定流中执行操作
cudaMemcpyAsync(d_a1, h_a1, size, cudaMemcpyHostToDevice, stream1);
kernel1<<<grid, block, 0, stream1>>>(d_a1, d_c1);
cudaMemcpyAsync(h_c1, d_c1, size, cudaMemcpyDeviceToHost, stream1);
cudaMemcpyAsync(d_a2, h_a2, size, cudaMemcpyHostToDevice, stream2);
kernel2<<<grid, block, 0, stream2>>>(d_a2, d_c2);
cudaMemcpyAsync(h_c2, d_c2, size, cudaMemcpyDeviceToHost, stream2);
// 同步流
cudaStreamSynchronize(stream1);
cudaStreamSynchronize(stream2);
// 销毁流
cudaStreamDestroy(stream1);
cudaStreamDestroy(stream2);
异步内存拷贝
cpp
// 同步拷贝(阻塞)
cudaMemcpy(dst, src, size, cudaMemcpyHostToDevice);
// 异步拷贝(非阻塞)
cudaMemcpyAsync(dst, src, size, cudaMemcpyHostToDevice, stream);
流同步
cpp
// 同步单个流
cudaStreamSynchronize(stream);
// 同步所有流
cudaDeviceSynchronize();
// 等待多个流
cudaStreamWaitEvent(stream, event);
流优先级
cpp
// 创建高优先级流
int priority_high, priority_low;
cudaDeviceGetStreamPriorityRange(&priority_low, &priority_high);
cudaStream_t stream_high;
cudaStreamCreateWithPriority(&stream_high, cudaStreamNonBlocking, priority_high);
完整示例:多流并发
cpp
#include <stdio.h>
#define N_STREAMS 4
#define N 1000000
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = N;
size_t size = n * sizeof(float);
// 分配主机内存(页锁定内存,用于异步传输)
float *h_a[N_STREAMS], *h_b[N_STREAMS], *h_c[N_STREAMS];
for (int i = 0; i < N_STREAMS; i++) {
cudaMallocHost(&h_a[i], size);
cudaMallocHost(&h_b[i], size);
cudaMallocHost(&h_c[i], size);
}
// 分配设备内存
float *d_a[N_STREAMS], *d_b[N_STREAMS], *d_c[N_STREAMS];
for (int i = 0; i < N_STREAMS; i++) {
cudaMalloc(&d_a[i], size);
cudaMalloc(&d_b[i], size);
cudaMalloc(&d_c[i], size);
}
// 创建流
cudaStream_t streams[N_STREAMS];
for (int i = 0; i < N_STREAMS; i++) {
cudaStreamCreate(&streams[i]);
}
// 并发执行
int blockSize = 256;
int gridSize = (n + blockSize - 1) / blockSize;
for (int i = 0; i < N_STREAMS; i++) {
cudaMemcpyAsync(d_a[i], h_a[i], size, cudaMemcpyHostToDevice, streams[i]);
cudaMemcpyAsync(d_b[i], h_b[i], size, cudaMemcpyHostToDevice, streams[i]);
vectorAdd<<<gridSize, blockSize, 0, streams[i]>>>(d_a[i], d_b[i], d_c[i], n);
cudaMemcpyAsync(h_c[i], d_c[i], size, cudaMemcpyDeviceToHost, streams[i]);
}
// 同步所有流
cudaDeviceSynchronize();
// 清理
for (int i = 0; i < N_STREAMS; i++) {
cudaStreamDestroy(streams[i]);
cudaFree(d_a[i]);
cudaFree(d_b[i]);
cudaFree(d_c[i]);
cudaFreeHost(h_a[i]);
cudaFreeHost(h_b[i]);
cudaFreeHost(h_c[i]);
}
return 0;
}
页锁定内存(Pinned Memory)
cpp
// 普通主机内存(可分页)
float *h_data = (float*)malloc(size);
// 页锁定主机内存(不可分页,用于异步传输)
float *h_data_pinned;
cudaMallocHost(&h_data_pinned, size);
// 释放
cudaFreeHost(h_data_pinned);
优点:
- 支持异步传输
- 传输速度更快
- DMA 直接访问
缺点:
- 占用物理内存
- 分配速度较慢
练习题 11
- CUDA 流的作用是什么?
cudaMemcpy和cudaMemcpyAsync的区别是什么?- 为什么异步传输需要使用页锁定内存?
第十二课:CUDA 事件与性能计时
知识点
什么是 CUDA 事件?
CUDA 事件是 GPU 上的时间标记,用于:
- 测量内核执行时间
- 流同步
- 性能分析
创建和使用事件
cpp
cudaEvent_t start, stop;
// 创建事件
cudaEventCreate(&start);
cudaEventCreate(&stop);
// 记录事件
cudaEventRecord(start);
myKernel<<<grid, block>>>(...);
cudaEventRecord(stop);
// 等待事件完成
cudaEventSynchronize(stop);
// 计算时间
float milliseconds = 0;
cudaEventElapsedTime(&milliseconds, start, stop);
printf("执行时间: %.3f ms\n", milliseconds);
// 销毁事件
cudaEventDestroy(start);
cudaEventDestroy(stop);
完整的性能测试示例
cpp
#include <stdio.h>
__global__ void vectorAdd(float *a, float *b, float *c, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = 10000000;
size_t size = n * sizeof(float);
// 分配内存
float *h_a = (float*)malloc(size);
float *h_b = (float*)malloc(size);
float *h_c = (float*)malloc(size);
float *d_a, *d_b, *d_c;
cudaMalloc(&d_a, size);
cudaMalloc(&d_b, size);
cudaMalloc(&d_c, size);
// 初始化数据
for (int i = 0; i < n; i++) {
h_a[i] = (float)i;
h_b[i] = (float)(i * 2);
}
// 拷贝数据
cudaMemcpy(d_a, h_a, size, cudaMemcpyHostToDevice);
cudaMemcpy(d_b, h_b, size, cudaMemcpyHostToDevice);
// 创建事件
cudaEvent_t start, stop;
cudaEventCreate(&start);
cudaEventCreate(&stop);
// 测试不同 Block 大小
int blockSizes[] = {32, 64, 128, 256, 512, 1024};
int numTests = sizeof(blockSizes) / sizeof(int);
for (int i = 0; i < numTests; i++) {
int blockSize = blockSizes[i];
int gridSize = (n + blockSize - 1) / blockSize;
// 预热
vectorAdd<<<gridSize, blockSize>>>(d_a, d_b, d_c, n);
cudaDeviceSynchronize();
// 计时
cudaEventRecord(start);
vectorAdd<<<gridSize, blockSize>>>(d_a, d_b, d_c, n);
cudaEventRecord(stop);
cudaEventSynchronize(stop);
float ms;
cudaEventElapsedTime(&ms, start, stop);
printf("Block=%4d, Grid=%6d, 时间=%.3f ms\n",
blockSize, gridSize, ms);
}
// 清理
cudaEventDestroy(start);
cudaEventDestroy(stop);
cudaFree(d_a);
cudaFree(d_b);
cudaFree(d_c);
free(h_a);
free(h_b);
free(h_c);
return 0;
}
事件同步
cpp
// 等待单个事件
cudaEventSynchronize(event);
// 流等待事件
cudaStreamWaitEvent(stream, event, 0);
// 事件完成检查
cudaError_t err = cudaEventQuery(event);
if (err == cudaSuccess) {
printf("事件已完成\n");
}
流间同步
cpp
cudaStream_t stream1, stream2;
cudaEvent_t event;
cudaStreamCreate(&stream1);
cudaStreamCreate(&stream2);
cudaEventCreate(&event);
// Stream1 记录事件
kernel1<<<grid, block, 0, stream1>>>(...);
cudaEventRecord(event, stream1);
// Stream2 等待事件
cudaStreamWaitEvent(stream2, event, 0);
kernel2<<<grid, block, 0, stream2>>>(...); // 等待 kernel1 完成
练习题 12
- CUDA 事件的主要用途是什么?
cudaEventRecord()和cudaEventSynchronize()的区别?- 如何使用事件实现两个流之间的同步?
第十三课:统一内存
知识点
什么是统一内存?
统一内存(Unified Memory)创建一个在 CPU 和 GPU 之间共享的内存池,自动管理数据传输。
传统内存模型:
┌─────────┐ ┌─────────┐
│ CPU 内存 │ ←─────→ │ GPU 内存 │
└─────────┘ 手动传输 └─────────┘
统一内存模型:
┌─────────────────────────────┐
│ 统一内存池 │
│ CPU 和 GPU 共享访问 │
│ 自动管理数据迁移 │
└─────────────────────────────┘
创建统一内存
cpp
// 分配统一内存
float *data;
cudaMallocManaged(&data, size);
// CPU 和 GPU 都可以直接访问
data[0] = 1.0f; // CPU 写入
myKernel<<<grid, block>>>(data); // GPU 读取和修改
cudaDeviceSynchronize();
printf("%f\n", data[0]); // CPU 读取 GPU 修改后的值
// 释放
cudaFree(data);
完整示例
cpp
#include <stdio.h>
__global__ void add(float *a, float *b, float *c, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
c[idx] = a[idx] + b[idx];
}
}
int main() {
int n = 1000000;
size_t size = n * sizeof(float);
// 分配统一内存
float *a, *b, *c;
cudaMallocManaged(&a, size);
cudaMallocManaged(&b, size);
cudaMallocManaged(&c, size);
// CPU 初始化
for (int i = 0; i < n; i++) {
a[i] = (float)i;
b[i] = (float)(i * 2);
}
// GPU 计算
int blockSize = 256;
int gridSize = (n + blockSize - 1) / blockSize;
add<<<gridSize, blockSize>>>(a, b, c, n);
// 等待 GPU 完成
cudaDeviceSynchronize();
// CPU 验证结果
bool success = true;
for (int i = 0; i < n; i++) {
if (c[i] != a[i] + b[i]) {
success = false;
break;
}
}
printf("验证: %s\n", success ? "成功" : "失败");
// 释放
cudaFree(a);
cudaFree(b);
cudaFree(c);
return 0;
}
内存迁移提示
cpp
// 提示内存将在设备上访问
cudaMemPrefetchAsync(data, size, deviceId, stream);
// 提示内存访问模式
cudaMemAdvise(data, size, cudaMemAdviseSetPreferredLocation, deviceId);
cpp
// 预取示例
int deviceId;
cudaGetDevice(&deviceId);
// 初始化数据(CPU)
for (int i = 0; i < n; i++) {
data[i] = i;
}
// 预取到 GPU
cudaMemPrefetchAsync(data, size, deviceId);
// GPU 计算
kernel<<<grid, block>>>(data, n);
统一内存的优势
| 优势 | 说明 |
|---|---|
| 简化编程 | 无需手动管理内存拷贝 |
| 减少代码 | 不需要 cudaMalloc/cudaMemcpy |
| 自动迁移 | 数据按需在 CPU/GPU 间迁移 |
| 超额订阅 | 可使用超过 GPU 内存的数据 |
统一内存的限制
- 性能可能不如手动优化
- 需要调用
cudaDeviceSynchronize()同步 - 频繁迁移会有开销
- 旧 GPU 不支持某些特性
练习题 13
- 统一内存与传统内存模型的主要区别是什么?
cudaMallocManaged()分配的内存可以被谁访问?- 为什么使用统一内存后还需要调用
cudaDeviceSynchronize()?
第十四课:常量内存
知识点
什么是常量内存?
常量内存是只读内存,具有缓存优化,适合存储不会改变的数据。
常量内存特点:
- 大小限制:64KB
- 只读
- 有缓存
- 适合广播读取(所有线程读取相同地址)
声明和使用常量内存
cpp
// 声明常量内存(全局作用域)
__constant__ float constData[256];
// 从主机拷贝到常量内存
cudaMemcpyToSymbol(constData, h_data, size);
// 在内核中使用
__global__ void kernel(float *output) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
output[idx] = constData[idx % 256]; // 读取常量内存
}
完整示例:使用常量内存存储滤波器
cpp
#include <stdio.h>
#define FILTER_SIZE 5
// 声明常量内存
__constant__ float filter[FILTER_SIZE];
__global__ void convolution(float *input, float *output, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx >= FILTER_SIZE / 2 && idx < n - FILTER_SIZE / 2) {
float sum = 0.0f;
for (int i = 0; i < FILTER_SIZE; i++) {
sum += input[idx - FILTER_SIZE / 2 + i] * filter[i];
}
output[idx] = sum;
}
}
int main() {
int n = 1000;
size_t size = n * sizeof(float);
// 准备滤波器数据
float h_filter[FILTER_SIZE] = {0.1f, 0.2f, 0.4f, 0.2f, 0.1f};
// 拷贝到常量内存
cudaMemcpyToSymbol(filter, h_filter, FILTER_SIZE * sizeof(float));
// 分配内存
float *h_input = (float*)malloc(size);
float *h_output = (float*)malloc(size);
float *d_input, *d_output;
cudaMalloc(&d_input, size);
cudaMalloc(&d_output, size);
// 初始化输入
for (int i = 0; i < n; i++) {
h_input[i] = (float)i;
}
// 拷贝数据
cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
// 执行卷积
int blockSize = 256;
int gridSize = (n + blockSize - 1) / blockSize;
convolution<<<gridSize, blockSize>>>(d_input, d_output, n);
// 拷贝结果
cudaMemcpy(h_output, d_output, size, cudaMemcpyDeviceToHost);
// 清理
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
free(h_output);
return 0;
}
常量内存 vs 全局内存
| 特性 | 常量内存 | 全局内存 |
|---|---|---|
| 访问权限 | 只读 | 读写 |
| 大小限制 | 64KB | 大 |
| 缓存 | 有(常量缓存) | 有(L1/L2) |
| 广播优化 | 是 | 否 |
| 适用场景 | 常量数据、滤波器、查找表 | 通用数据 |
何时使用常量内存?
- 数据不会改变
- 所有线程读取相同数据(广播)
- 数据量小于 64KB
- 需要缓存优化
练习题 14
- 常量内存的大小限制是多少?
- 使用什么函数将数据拷贝到常量内存?
- 常量内存适合什么场景?
第十五课:纹理内存
知识点
什么是纹理内存?
纹理内存是专门为图像处理优化的只读内存,支持:
- 硬件插值
- 边界处理
- 缓存优化
纹理内存特点
纹理内存特性:
- 只读
- 支持插值(线性、最近邻)
- 支持边界模式(截断、环绕、镜像)
- 2D 空间局部性缓存优化
- 适合图像处理
使用纹理内存
cpp
// 声明纹理引用(旧方式,CUDA 10 之前)
texture<float, 2, cudaReadModeElementType> texRef;
// 绑定纹理
cudaBindTextureToArray(texRef, texArray);
// 在内核中读取
__global__ void kernel(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) {
// 使用纹理读取(支持插值)
float val = tex2D(texRef, x, y);
output[y * width + x] = val;
}
}
// 解绑纹理
cudaUnbindTexture(texRef);
现代方式:纹理对象(CUDA 11+)
cpp
// 创建纹理对象
cudaResourceDesc resDesc;
memset(&resDesc, 0, sizeof(resDesc));
resDesc.resType = cudaResourceTypeLinear;
resDesc.res.linear.devPtr = devPtr;
resDesc.res.linear.desc = cudaCreateChannelDesc<float>();
resDesc.res.linear.sizeInBytes = size;
cudaTextureDesc texDesc;
memset(&texDesc, 0, sizeof(texDesc));
texDesc.addressMode[0] = cudaAddressModeClamp;
texDesc.addressMode[1] = cudaAddressModeClamp;
texDesc.filterMode = cudaFilterModeLinear;
texDesc.readMode = cudaReadModeElementType;
cudaTextureObject_t texObj;
cudaCreateTextureObject(&texObj, &resDesc, &texDesc, NULL);
// 在内核中使用
__global__ void kernel(cudaTextureObject_t texObj, float *output, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
output[idx] = tex1Dfetch<float>(texObj, idx);
}
}
// 销毁纹理对象
cudaDestroyTextureObject(texObj);
纹理寻址模式
cpp
// 边界模式
texDesc.addressMode[0] = cudaAddressModeClamp; // 截断(默认)
texDesc.addressMode[0] = cudaAddressModeWrap; // 环绕
texDesc.addressMode[0] = cudaAddressModeMirror; // 镜像
texDesc.addressMode[0] = cudaAddressModeBorder; // 边界值
// 过滤模式
texDesc.filterMode = cudaFilterModePoint; // 最近邻
texDesc.filterMode = cudaFilterModeLinear; // 线性插值
纹理内存应用场景
- 图像处理(缩放、旋转)
- 纹理映射
- 数据插值
- 查找表
练习题 15
- 纹理内存的主要优势是什么?
- 纹理内存支持哪两种过滤模式?
- 纹理内存适合什么类型的应用?
第十六课:并行归约算法
知识点
什么是归约?
归约是将数组中所有元素通过某种操作合并为单个结果的过程。
求和归约:
[1, 2, 3, 4, 5, 6, 7, 8] → 36
求最大值归约:
[1, 2, 3, 4, 5, 6, 7, 8] → 8
串行归约
cpp
// CPU 串行归约
int sum = 0;
for (int i = 0; i < n; i++) {
sum += data[i];
}
并行归约策略
步骤1: [1, 2, 3, 4, 5, 6, 7, 8]
↓
步骤2: [3, 7, 11, 15 ] (相邻配对)
↓
步骤3: [10, 26 ]
↓
步骤4: [36 ]
基本并行归约实现
cpp
__global__ void reduceSum(float *input, float *output, int n) {
__shared__ float sdata[256];
int tid = threadIdx.x;
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 加载数据到共享内存
sdata[tid] = (idx < n) ? input[idx] : 0;
__syncthreads();
// 归约
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
if (tid < s) {
sdata[tid] += sdata[tid + s];
}
__syncthreads();
}
// 写回结果
if (tid == 0) {
output[blockIdx.x] = sdata[0];
}
}
完整示例
cpp
#include <stdio.h>
#define BLOCK_SIZE 256
__global__ void reduceSum(float *input, float *output, int n) {
__shared__ float sdata[BLOCK_SIZE];
int tid = threadIdx.x;
int idx = blockIdx.x * blockDim.x + threadIdx.x;
sdata[tid] = (idx < n) ? input[idx] : 0;
__syncthreads();
for (int s = blockDim.x / 2; s > 0; s >>= 1) {
if (tid < s) {
sdata[tid] += sdata[tid + s];
}
__syncthreads();
}
if (tid == 0) {
output[blockIdx.x] = sdata[0];
}
}
int main() {
int n = 1000000;
size_t size = n * sizeof(float);
// 分配内存
float *h_input = (float*)malloc(size);
float *d_input, *d_output;
cudaMalloc(&d_input, size);
// 初始化
float h_sum = 0;
for (int i = 0; i < n; i++) {
h_input[i] = 1.0f; // 每个元素为1,总和应为n
h_sum += h_input[i];
}
cudaMemcpy(d_input, h_input, size, cudaMemcpyHostToDevice);
// 计算需要的 Block 数量
int blockSize = BLOCK_SIZE;
int gridSize = (n + blockSize - 1) / blockSize;
// 分配输出内存
cudaMalloc(&d_output, gridSize * sizeof(float));
// 执行归约
reduceSum<<<gridSize, blockSize>>>(d_input, d_output, n);
// 如果结果数量大于1,需要继续归约
while (gridSize > 1) {
int newGridSize = (gridSize + blockSize - 1) / blockSize;
reduceSum<<<newGridSize, blockSize>>>(d_output, d_output, gridSize);
gridSize = newGridSize;
}
// 获取结果
float result;
cudaMemcpy(&result, d_output, sizeof(float), cudaMemcpyDeviceToHost);
printf("CPU 结果: %.0f\n", h_sum);
printf("GPU 结果: %.0f\n", result);
printf("误差: %.6f\n", fabs(h_sum - result));
// 清理
cudaFree(d_input);
cudaFree(d_output);
free(h_input);
return 0;
}
优化技术
- 展开循环
- 避免 Bank 冲突
- 减少同步
cpp
// 优化版本:展开最后一个 warp
__global__ void reduceOptimized(float *input, float *output, int n) {
__shared__ float sdata[BLOCK_SIZE];
int tid = threadIdx.x;
int idx = blockIdx.x * (BLOCK_SIZE * 2) + threadIdx.x;
// 每个线程加载两个元素
sdata[tid] = (idx < n) ? input[idx] : 0;
sdata[tid] += (idx + BLOCK_SIZE < n) ? input[idx + BLOCK_SIZE] : 0;
__syncthreads();
// 归约
for (int s = blockDim.x / 2; s > 32; s >>= 1) {
if (tid < s) {
sdata[tid] += sdata[tid + s];
}
__syncthreads();
}
// 展开最后一个 warp(不需要同步)
if (tid < 32) {
sdata[tid] += sdata[tid + 32];
sdata[tid] += sdata[tid + 16];
sdata[tid] += sdata[tid + 8];
sdata[tid] += sdata[tid + 4];
sdata[tid] += sdata[tid + 2];
sdata[tid] += sdata[tid + 1];
}
if (tid == 0) {
output[blockIdx.x] = sdata[0];
}
}
练习题 16
- 什么是归约操作?举三个例子。
- 为什么并行归约需要使用共享内存?
- 为什么最后一个 warp 的归约不需要
__syncthreads()?
第十七课:前缀和(扫描)算法
知识点
什么是前缀和?
前缀和是将数组转换为每个位置包含该位置之前所有元素和的数组。
输入: [1, 2, 3, 4, 5, 6, 7, 8]
前缀和: [1, 3, 6, 10, 15, 21, 28, 36]
解释:
位置0: 1
位置1: 1 + 2 = 3
位置2: 1 + 2 + 3 = 6
位置3: 1 + 2 + 3 + 4 = 10
...
串行前缀和
cpp
// CPU 串行前缀和
void prefixSum(float *input, float *output, int n) {
output[0] = input[0];
for (int i = 1; i < n; i++) {
output[i] = output[i - 1] + input[i];
}
}
并行前缀和:Blelloch 算法
分为两个阶段:
-
上扫描(Up-sweep):构建二叉树
-
下扫描(Down-sweep):计算前缀和
上扫描阶段:
[1, 2, 3, 4, 5, 6, 7, 8]
↓
[1, 3, 3, 7, 5, 11, 7, 15]
↓
[1, 3, 3, 10, 5, 11, 7, 26]
↓
[1, 3, 3, 10, 5, 11, 7, 36]下扫描阶段:
[1, 3, 3, 10, 5, 11, 7, 0]
↓
[1, 3, 3, 0, 5, 11, 7, 10]
↓
[1, 3, 0, 3, 5, 6, 7, 10]
↓
[0, 1, 3, 6, 10, 15, 21, 28]
CUDA 实现
cpp
__global__ void prefixSum(float *input, float *output, int n) {
__shared__ float temp[256];
int tid = threadIdx.x;
int idx = blockIdx.x * blockDim.x + threadIdx.x;
// 加载数据
temp[tid] = (idx < n) ? input[idx] : 0;
__syncthreads();
// 上扫描
int offset = 1;
for (int d = blockDim.x >> 1; d > 0; d >>= 1) {
__syncthreads();
if (tid < d) {
int ai = offset * (2 * tid + 1) - 1;
int bi = offset * (2 * tid + 2) - 1;
temp[bi] += temp[ai];
}
offset *= 2;
}
// 清除最后一个元素
if (tid == 0) {
temp[blockDim.x - 1] = 0;
}
// 下扫描
for (int d = 1; d < blockDim.x; d *= 2) {
offset >>= 1;
__syncthreads();
if (tid < d) {
int ai = offset * (2 * tid + 1) - 1;
int bi = offset * (2 * tid + 2) - 1;
float t = temp[ai];
temp[ai] = temp[bi];
temp[bi] += t;
}
}
__syncthreads();
// 写回结果
if (idx < n) {
output[idx] = temp[tid];
}
}
使用 Thrust 库
cpp
#include <thrust/scan.h>
#include <thrust/device_vector.h>
int main() {
int n = 8;
thrust::device_vector<int> d_input(8);
thrust::device_vector<int> d_output(8);
d_input[0] = 1; d_input[1] = 2; d_input[2] = 3; d_input[3] = 4;
d_input[4] = 5; d_input[5] = 6; d_input[6] = 7; d_input[7] = 8;
// 计算前缀和
thrust::exclusive_scan(d_input.begin(), d_input.end(), d_output.begin());
// 输出: [0, 1, 3, 6, 10, 15, 21, 28]
return 0;
}
练习题 17
- 什么是前缀和?与归约的区别是什么?
- 前缀和算法的两个阶段分别是什么?
- 前缀和有什么应用场景?
第十八课:Warp 级编程
知识点
什么是 Warp?
Warp 是 GPU 执行的基本单位,包含 32 个线程。同一 Warp 内的线程:
- 同时执行相同指令(SIMT)
- 可以高效交换数据
- 可以使用 Warp 级原语
Warp 级原语
cpp
// Warp 归约
unsigned __ballot_sync(unsigned mask, int predicate); // 统计满足条件的线程
int __all_sync(unsigned mask, int predicate); // 所有线程都满足条件
int __any_sync(unsigned mask, int predicate); // 任一线程满足条件
unsigned __activemask(); // 获取活跃线程掩码
// Warp shuffle(线程间交换数据)
int __shfl_sync(unsigned mask, int var, int srcLane); // 从指定 lane 获取值
int __shfl_up_sync(unsigned mask, int var, unsigned delta); // 向上获取值
int __shfl_down_sync(unsigned mask, int var, unsigned delta); // 向下获取值
int __shfl_xor_sync(unsigned mask, int var, int laneMask); // XOR 获取值
Warp 归约示例
cpp
__device__ float warpReduceSum(float val) {
for (int offset = 16; offset > 0; offset >>= 1) {
val += __shfl_down_sync(0xffffffff, val, offset);
}
return val;
}
__global__ void reduce(float *input, float *output, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
float val = (idx < n) ? input[idx] : 0;
// Warp 归约
val = warpReduceSum(val);
// 只有每个 Warp 的第一个线程写入结果
if (threadIdx.x % 32 == 0) {
atomicAdd(output, val);
}
}
Warp Shuffle 示例
cpp
__global__ void shuffleExample() {
int laneId = threadIdx.x & 31;
int value = laneId;
// 从 lane 0 获取值
int value_from_0 = __shfl_sync(0xffffffff, value, 0);
// 向上获取值(delta=1)
int value_from_up = __shfl_up_sync(0xffffffff, value, 1);
// 向下获取值(delta=1)
int value_from_down = __shfl_down_sync(0xffffffff, value, 1);
// XOR 获取值
int value_from_xor = __shfl_xor_sync(0xffffffff, value, 1);
if (laneId < 4) {
printf("Lane %d: original=%d, from_0=%d, up=%d, down=%d, xor=%d\n",
laneId, value, value_from_0, value_from_up, value_from_down, value_from_xor);
}
}
mask 参数说明
cpp
// mask 指定参与操作的线程
0xffffffff // 所有 32 个线程参与
0x0000000f // 只有前 4 个线程参与
__activemask() // 当前活跃的线程
练习题 18
- Warp 的大小是多少个线程?
__shfl_sync()的作用是什么?- 为什么 Warp 级操作比共享内存更快?
第十九课:动态并行
知识点
什么是动态并行?
动态并行允许 GPU 内核在运行时启动新的内核,无需 CPU 介入。
传统模式:
CPU 启动内核 → GPU 执行 → CPU 启动新内核 → GPU 执行
动态并行:
CPU 启动内核 → GPU 执行 → GPU 启动新内核 → GPU 执行
启用动态并行
编译时需要链接 CUDA device runtime:
bash
nvcc -rdc=true myprogram.cu
基本示例
cpp
__global__ void childKernel(int *data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
data[idx] *= 2;
}
}
__global__ void parentKernel(int *data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx == 0) {
// 在 GPU 中启动子内核
childKernel<<<1, n>>>(data, n);
// 等待子内核完成
cudaDeviceSynchronize();
}
}
int main() {
int n = 10;
int *d_data;
cudaMalloc(&d_data, n * sizeof(int));
// 启动父内核
parentKernel<<<1, 1>>>(d_data, n);
cudaDeviceSynchronize();
cudaFree(d_data);
return 0;
}
动态并行的应用场景
- 递归算法
- 不规则并行
- 自适应细分
- 图遍历
注意事项
- 需要额外的内存资源
- 启动开销
- 递归深度限制
- 需要同步
练习题 19
- 什么是动态并行?
- 动态并行与传统内核启动的区别是什么?
- 编译时需要什么选项启用动态并行?
第二十课:多 GPU 编程
知识点
多 GPU 架构
┌─────────┐ ┌─────────┐ ┌─────────┐
│ GPU 0 │ │ GPU 1 │ │ GPU 2 │
│ 内存 0 │ │ 内存 1 │ │ 内存 2 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┴───────────────┘
│
┌──────┴──────┐
│ CPU │
└─────────────┘
选择 GPU
cpp
int deviceCount;
cudaGetDeviceCount(&deviceCount);
for (int i = 0; i < deviceCount; i++) {
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, i);
printf("GPU %d: %s\n", i, prop.name);
}
// 设置当前 GPU
cudaSetDevice(deviceId);
多 GPU 执行
cpp
int main() {
int n = 1000000;
int numGPUs = 2;
int chunkSize = n / numGPUs;
// 在每个 GPU 上分配内存
float *d_a[2], *d_b[2], *d_c[2];
for (int i = 0; i < numGPUs; i++) {
cudaSetDevice(i);
cudaMalloc(&d_a[i], chunkSize * sizeof(float));
cudaMalloc(&d_b[i], chunkSize * sizeof(float));
cudaMalloc(&d_c[i], chunkSize * sizeof(float));
}
// 在每个 GPU 上执行计算
for (int i = 0; i < numGPUs; i++) {
cudaSetDevice(i);
cudaMemcpy(d_a[i], h_a + i * chunkSize, chunkSize * sizeof(float), cudaMemcpyHostToDevice);
cudaMemcpy(d_b[i], h_b + i * chunkSize, chunkSize * sizeof(float), cudaMemcpyHostToDevice);
int blockSize = 256;
int gridSize = (chunkSize + blockSize - 1) / blockSize;
vectorAdd<<<gridSize, blockSize>>>(d_a[i], d_b[i], d_c[i], chunkSize);
cudaMemcpy(h_c + i * chunkSize, d_c[i], chunkSize * sizeof(float), cudaMemcpyDeviceToHost);
}
// 清理
for (int i = 0; i < numGPUs; i++) {
cudaSetDevice(i);
cudaFree(d_a[i]);
cudaFree(d_b[i]);
cudaFree(d_c[i]);
}
return 0;
}
GPU 间通信
cpp
// 点对点访问
int canAccess;
cudaDeviceCanAccessPeer(&canAccess, gpu0, gpu1);
if (canAccess) {
cudaSetDevice(gpu0);
cudaDeviceEnablePeerAccess(gpu1, 0);
// GPU 0 可以直接访问 GPU 1 的内存
kernel<<<grid, block>>>(d_data_on_gpu1);
}
练习题 20
- 如何获取系统中 GPU 的数量?
- 如何切换当前活动的 GPU?
- 什么是点对点访问?
第二十一课:CUDA 调试技术
知识点
调试方法
- printf 调试
- cuda-gdb
- Nsight Compute
- cuda-memcheck
printf 在内核中使用
cpp
__global__ void debugKernel(int *data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
if (idx < 5) {
printf("Thread %d: data[%d] = %d\n", idx, idx, data[idx]);
}
data[idx] *= 2;
}
}
int main() {
// ...
debugKernel<<<grid, block>>>(d_data, n);
cudaDeviceSynchronize(); // 必须同步才能看到 printf 输出
// ...
}
cuda-memcheck
检查内存错误:
bash
cuda-memcheck ./my_program
检测:
- 越界访问
- 未初始化内存
- 内存泄漏
- 竞争条件
cuda-gdb
bash
cuda-gdb ./my_program
# 命令
(gdb) break myKernel # 在内核设置断点
(gdb) run # 运行
(gdb) cuda thread # 查看当前线程
(gdb) cuda block # 查看当前 block
(gdb) print data[0] # 打印变量
assert 在内核中使用
cpp
__global__ void kernel(int *data, int n) {
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < n) {
assert(data[idx] >= 0); // 断言检查
data[idx] = sqrt(data[idx]);
}
}
练习题 21
- 在内核中使用 printf 后需要调用什么函数?
- cuda-memcheck 可以检测哪些错误?
- 如何在 cuda-gdb 中设置断点?
第二十二课:性能分析工具
知识点
Nsight Systems
系统级性能分析:
bash
nsys profile ./my_program
分析内容:
- CPU/GPU 时间线
- 内核执行时间
- 内存传输时间
- API 调用开销
Nsight Compute
内核级性能分析:
bash
ncu ./my_program
分析内容:
- 内存吞吐量
- 计算吞吐量
- Warp 执行效率
- 内存访问模式
nvprof(旧工具)
bash
nvprof ./my_program
# 输出示例
==12345== Profiling result:
Type Time(%) Time Calls Avg Min Max Name
GPU activities: 65.43% 123.45ms 1 123.45ms 123.45ms 123.45ms myKernel(int*, int)
34.57% 65.43ms 2 32.72ms 32.71ms 32.72ms [CUDA memcpy HtoD]
性能指标
| 指标 | 说明 |
|---|---|
| GPU Time | 内核执行时间 |
| Grid Size | Block 数量 |
| Block Size | 每个 Block 的线程数 |
| Registers Per Thread | 每个线程使用的寄存器数 |
| Shared Memory Per Block | 每个 Block 使用的共享内存 |
| Theoretical Occupancy | 理论占用率 |
| Achieved Occupancy | 实际占用率 |
| Memory Throughput | 内存吞吐量 |
练习题 22
- Nsight Systems 和 Nsight Compute 的区别是什么?
- 如何使用 nvprof 分析程序?
- 什么是 Occupancy?
第二十三课:内存访问模式深入
知识点
全局内存访问
GPU 内存层次:
┌─────────────────────────────────┐
│ 全局内存 (DRAM) │ 大,慢
├─────────────────────────────────┤
│ L2 缓存 │
├─────────────────────────────────┤
│ L1 缓存 │
├─────────────────────────────────┤
│ 共享内存 │ 小,快
├─────────────────────────────────┤
│ 寄存器 │ 最快
└─────────────────────────────────┘
内存合并
理想情况:相邻线程访问相邻地址
线程: 0 1 2 3 4 5 6 7
地址: [0] [4] [8] [12][16][20][24][28]
└─────────────────────────────┘
一次内存事务
不理想情况:跳跃访问
线程: 0 1 2 3 4 5 6 7
地址: [0] [64][128][192][256][320][384][448]
└──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘ └──┘
8 次内存事务
Bank 冲突
共享内存分为 32 个 Bank:
Bank: 0 1 2 3 ... 31
[0] [1] [2] [3] ... [31]
[32][33][34][35]... [63]
...
Bank 冲突:多个线程访问同一 Bank 的不同地址
cpp
// 无冲突
sdata[threadIdx.x] = value; // 每个线程访问不同 Bank
// 冲突
sdata[threadIdx.x * 2] = value; // 线程 0 和 16 访问同一 Bank
解决 Bank 冲突
cpp
// 方法1:填充
__shared__ float sdata[256 + 16]; // 添加填充避免冲突
// 方法2:改变访问模式
__shared__ float sdata[32][33]; // 33 列避免冲突
float val = sdata[threadIdx.x][threadIdx.y];
练习题 23
- 什么是内存合并?
- 什么是 Bank 冲突?
- 如何避免 Bank 冲突?
第二十四课:寄存器与缓存优化
知识点
寄存器使用
每个线程有私有寄存器:
- 最快的存储
- 数量有限(通常 255 个)
- 影响占用率
cpp
// 查看寄存器使用
// 编译时添加 -Xptxas=-v 选项
nvcc -Xptxas=-v myprogram.cu
// 输出示例
ptxas info : Used 32 registers, 256 bytes smem, ...
占用率计算
占用率 = 活跃 Warp 数 / 最大 Warp 数
影响因素:
- 每个 Block 的线程数
- 每个线程使用的寄存器数
- 每个 Block 使用的共享内存
优化寄存器使用
cpp
// 减少寄存器使用
// 1. 重用变量
float temp = a[i];
temp = temp * 2; // 重用 temp
// 2. 避免过多局部变量
// 3. 使用编译提示
__launch_bounds__(256, 2) // 每个 Block 256 线程,最少 2 个 Block
__global__ void myKernel(...) {
// ...
}
缓存配置
cpp
// 配置 L1 缓存和共享内存比例
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferShared);
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferL1);
cudaFuncSetCacheConfig(myKernel, cudaFuncCachePreferEqual);
练习题 24
- 寄存器的特点是什么?
- 什么是占用率?
- 如何查看内核使用的寄存器数量?
附录:CUDA 最佳实践总结
内存优化
- 使用共享内存减少全局内存访问
- 确保内存合并访问
- 避免 Bank 冲突
- 使用常量内存存储只读数据
- 使用统一内存简化编程
执行优化
- 选择合适的 Block 大小(128、256、512)
- 保持高占用率
- 避免分支分歧
- 使用 Warp 级原语
- 使用流实现并发
算法优化
- 使用并行归约
- 使用前缀和算法
- 使用原子操作保证正确性
- 减少线程间同步
开发建议
- 始终检查 CUDA 错误
- 使用性能分析工具
- 先保证正确性,再优化性能
- 参考 NVIDIA 最佳实践指南
参考资料
- CUDA C Programming Guide : https://docs.nvidia.com/cuda/cuda-c-programming-guide/
- CUDA Best Practices Guide : https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
- CUDA Toolkit Documentation : https://docs.nvidia.com/cuda/
- NVIDIA Developer Blog : https://developer.nvidia.com/blog/
学习日期:2026-05-07