CUDA进阶学习与深入

CUDA 补充教程 - 进阶与深入

目录

  1. [第九课:CUDA 错误处理](#第九课:CUDA 错误处理)
  2. 第十课:原子操作
  3. [第十一课:CUDA 流与异步执行](#第十一课:CUDA 流与异步执行)
  4. [第十二课:CUDA 事件与性能计时](#第十二课:CUDA 事件与性能计时)
  5. 第十三课:统一内存
  6. 第十四课:常量内存
  7. 第十五课:纹理内存
  8. 第十六课:并行归约算法
  9. 第十七课:前缀和(扫描)算法
  10. [第十八课:Warp 级编程](#第十八课:Warp 级编程)
  11. 第十九课:动态并行
  12. [第二十课:多 GPU 编程](#第二十课:多 GPU 编程)
  13. [第二十一课:CUDA 调试技术](#第二十一课:CUDA 调试技术)
  14. 第二十二课:性能分析工具
  15. 第二十三课:内存访问模式深入
  16. 第二十四课:寄存器与缓存优化

第九课: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

  1. CUDA 错误码 cudaSuccess 的值是什么?
  2. cudaGetLastError()cudaDeviceSynchronize() 分别检查什么错误?
  3. 为什么内核启动后需要调用 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

  1. 为什么多线程环境下普通递增操作 (*count)++ 会产生错误结果?
  2. atomicAdd(addr, val) 的作用是什么?返回值是什么?
  3. 如何使用原子操作实现一个简单的互斥锁?

第十一课: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

  1. CUDA 流的作用是什么?
  2. cudaMemcpycudaMemcpyAsync 的区别是什么?
  3. 为什么异步传输需要使用页锁定内存?

第十二课: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

  1. CUDA 事件的主要用途是什么?
  2. cudaEventRecord()cudaEventSynchronize() 的区别?
  3. 如何使用事件实现两个流之间的同步?

第十三课:统一内存

知识点

什么是统一内存?

统一内存(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

  1. 统一内存与传统内存模型的主要区别是什么?
  2. cudaMallocManaged() 分配的内存可以被谁访问?
  3. 为什么使用统一内存后还需要调用 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

  1. 常量内存的大小限制是多少?
  2. 使用什么函数将数据拷贝到常量内存?
  3. 常量内存适合什么场景?

第十五课:纹理内存

知识点

什么是纹理内存?

纹理内存是专门为图像处理优化的只读内存,支持:

  • 硬件插值
  • 边界处理
  • 缓存优化

纹理内存特点

复制代码
纹理内存特性:
- 只读
- 支持插值(线性、最近邻)
- 支持边界模式(截断、环绕、镜像)
- 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. 纹理内存适合什么类型的应用?

第十六课:并行归约算法

知识点

什么是归约?

归约是将数组中所有元素通过某种操作合并为单个结果的过程。

复制代码
求和归约:
[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;
}

优化技术

  1. 展开循环
  2. 避免 Bank 冲突
  3. 减少同步
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

  1. 什么是归约操作?举三个例子。
  2. 为什么并行归约需要使用共享内存?
  3. 为什么最后一个 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 算法

分为两个阶段:

  1. 上扫描(Up-sweep):构建二叉树

  2. 下扫描(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

  1. 什么是前缀和?与归约的区别是什么?
  2. 前缀和算法的两个阶段分别是什么?
  3. 前缀和有什么应用场景?

第十八课: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

  1. Warp 的大小是多少个线程?
  2. __shfl_sync() 的作用是什么?
  3. 为什么 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

  1. 什么是动态并行?
  2. 动态并行与传统内核启动的区别是什么?
  3. 编译时需要什么选项启用动态并行?

第二十课:多 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

  1. 如何获取系统中 GPU 的数量?
  2. 如何切换当前活动的 GPU?
  3. 什么是点对点访问?

第二十一课:CUDA 调试技术

知识点

调试方法

  1. printf 调试
  2. cuda-gdb
  3. Nsight Compute
  4. 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

  1. 在内核中使用 printf 后需要调用什么函数?
  2. cuda-memcheck 可以检测哪些错误?
  3. 如何在 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

  1. Nsight Systems 和 Nsight Compute 的区别是什么?
  2. 如何使用 nvprof 分析程序?
  3. 什么是 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

  1. 什么是内存合并?
  2. 什么是 Bank 冲突?
  3. 如何避免 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

  1. 寄存器的特点是什么?
  2. 什么是占用率?
  3. 如何查看内核使用的寄存器数量?

附录:CUDA 最佳实践总结

内存优化

  1. 使用共享内存减少全局内存访问
  2. 确保内存合并访问
  3. 避免 Bank 冲突
  4. 使用常量内存存储只读数据
  5. 使用统一内存简化编程

执行优化

  1. 选择合适的 Block 大小(128、256、512)
  2. 保持高占用率
  3. 避免分支分歧
  4. 使用 Warp 级原语
  5. 使用流实现并发

算法优化

  1. 使用并行归约
  2. 使用前缀和算法
  3. 使用原子操作保证正确性
  4. 减少线程间同步

开发建议

  1. 始终检查 CUDA 错误
  2. 使用性能分析工具
  3. 先保证正确性,再优化性能
  4. 参考 NVIDIA 最佳实践指南

参考资料

  1. CUDA C Programming Guide : https://docs.nvidia.com/cuda/cuda-c-programming-guide/
  2. CUDA Best Practices Guide : https://docs.nvidia.com/cuda/cuda-c-best-practices-guide/
  3. CUDA Toolkit Documentation : https://docs.nvidia.com/cuda/
  4. NVIDIA Developer Blog : https://developer.nvidia.com/blog/

学习日期:2026-05-07