引言:CUDA编程的重要性与挑战

CUDA(Compute Unified Device Architecture)是NVIDIA推出的并行计算平台和编程模型,它允许开发者利用GPU的强大计算能力来加速各种计算密集型任务。随着深度学习、科学计算和大数据处理的快速发展,CUDA编程已成为高性能计算领域不可或缺的技能。

然而,CUDA编程也面临着诸多挑战:内存层次结构的复杂性、线程同步的微妙之处、以及GPU架构的特殊性都可能导致性能瓶颈和难以调试的错误。本文将系统地介绍CUDA编程的最佳实践,从基础概念到高级优化技巧,帮助开发者充分利用GPU的计算潜力。

第一部分:CUDA基础概念回顾

1.1 GPU架构概述

现代GPU采用SIMT(Single Instruction, Multiple Thread)架构,其核心组件包括:

  • 流处理器(Streaming Multiprocessors, SM):GPU的基本计算单元
  • CUDA核心(CUDA Cores):SM中的基本处理单元
  • 共享内存(Shared Memory):SM内的低延迟内存
  • 全局内存(Global Memory):GPU的主内存

1.2 CUDA执行模型

CUDA采用三层执行层次:

  • 网格(Grid):由多个线程块组成
  • 线程块(Block):包含多个线程,可以在SM上独立调度
  • 线程(Thread):最基本的执行单元
// CUDA核函数基本结构
__global__ void vectorAdd(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];
    }
}

第二部分:内存优化策略

2.1 理解CUDA内存层次结构

CUDA提供多种内存类型,每种都有不同的访问特性和用途:

内存类型 访问速度 可见范围 典型用途
寄存器 最快 线程私有 临时变量
共享内存 线程块内 线程间通信
常量内存 所有线程 只读数据
全局内存 所有线程 主要数据存储

2.2 全局内存访问优化

合并访问(Coalesced Access)是全局内存优化的关键。理想情况下,连续的线程应该访问连续的内存地址。

// 不好的全局内存访问模式(非合并)
__global__ void badMemoryAccess(float* data) {
    int tid = threadIdx.x;
    // 每个线程访问间隔很大的地址
    data[tid * 1024] = tid;
}

// 好的全局内存访问模式(合并)
__global__ void goodMemoryAccess(float* data) {
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    // 连续线程访问连续地址
    data[tid] = tid;
}

2.3 共享内存的使用技巧

共享内存是优化性能的重要工具,特别适用于数据重用和线程间通信。

// 矩阵乘法中的共享内存优化
__global__ void matrixMulShared(float* A, float* B, float* C, int N) {
    __shared__ float As[TILE_SIZE][TILE_SIZE];
    __shared__ float Bs[TILE_SIZE][TILE_SIZE];
    
    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    
    float sum = 0.0f;
    
    // 分块计算
    for (int t = 0; t < N / TILE_SIZE; ++t) {
        // 将数据从全局内存加载到共享内存
        As[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
        Bs[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        
        __syncthreads(); // 等待所有线程完成加载
        
        // 计算子矩阵乘积
        for (int k = 0; k < TILE_SIZE; ++k) {
            sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        }
        
        __syncthreads(); // 等待计算完成
    }
    
    if (row < N && col < N) {
        C[row * N + col] = sum;
    }
}

2.4 常量内存和纹理内存

对于只读数据,可以使用常量内存或纹理内存来获得缓存优势:

// 常量内存声明
__constant__ float constData[256];

// 在主机代码中设置常量内存
void setConstantData(float* data, int size) {
    cudaMemcpyToSymbol(constData, data, size);
}

// 纹理内存使用示例
texture<float, 2> texRef;

void setupTexture(float* data, int width, int height) {
    cudaChannelFormatDesc desc = cudaCreateChannelDesc<float>();
    cudaBindTexture2D(0, &texRef, data, &desc, width, height, width * sizeof(float));
}

__global__ void useTexture(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);
    }
}

第三部分:执行配置优化

3.1 线程块大小选择

线程块大小的选择对性能有显著影响:

// 不同块大小的性能对比示例
void benchmarkBlockSizes() {
    int sizes[] = {32, 64, 128, 256, 512, 1024};
    
    for (int size : sizes) {
        dim3 blockSize(size);
        dim3 gridSize((N + size - 1) / size);
        
        // 计时代码
        cudaEvent_t start, stop;
        cudaEventCreate(&start);
        cudaEventCreate(&stop);
        
        cudaEventRecord(start);
        vectorAdd<<<gridSize, blockSize>>>(d_a, d_b, d_c, N);
        cudaEventRecord(stop);
        
        float milliseconds = 0;
        cudaEventElapsedTime(&milliseconds, start, stop);
        
        printf("Block size %d: %.3f ms\n", size, milliseconds);
    }
}

最佳实践建议:

  • 选择256或512作为块大小通常是个好起点
  • 确保块大小是32的倍数(warp大小)
  • 考虑GPU的SM数量和每个SM的最大线程数

3.2 占用率(Occupancy)优化

占用率是衡量SM资源利用效率的指标。高占用率通常(但不总是)意味着更好的性能。

// 计算理论占用率的工具函数
float calculateOccupancy(int threadsPerBlock, int sharedMemPerBlock) {
    // 假设是Ampere架构(RTX 3080)
    int maxThreadsPerSM = 1536;  // Ampere SM的线程限制
    int maxBlocksPerSM = 16;     // 最大块数
    int maxSharedMemPerSM = 49152; // 48KB
    
    // 计算基于线程数的限制
    int threadsLimit = maxThreadsPerSM / threadsPerBlock;
    
    // 计算基于共享内存的限制
    int sharedMemLimit = maxSharedMemPerSM / sharedMemPerBlock;
    
    // 计算基于块数的限制
    int blocksLimit = maxBlocksPerSM;
    
    // 实际能运行的块数
    int actualBlocks = min(min(threadsLimit, sharedMemLimit), blocksLimit);
    
    // 理论占用率
    return (float)(actualBlocks * threadsPerBlock) / maxThreadsPerSM;
}

3.3 异步执行和流(Streams)

使用流可以实现计算和内存传输的重叠:

// 使用多个流实现异步执行
void asyncExecutionExample() {
    const int numStreams = 4;
    cudaStream_t streams[numStreams];
    for (int i = 0; i < numStreams; ++i) {
        cudaStreamCreate(&streams[i]);
    }
    
    // 分配数据
    float* d_data[numStreams];
    for (int i = 0; i < numStreams; ++i) {
        cudaMalloc(&d_data[i], DATA_SIZE);
    }
    
    // 异步执行
    for (int i = 0; i < numStreams; ++i) {
        // 异步拷贝
        cudaMemcpyAsync(d_data[i], h_data[i], DATA_SIZE, 
                       cudaMemcpyHostToDevice, streams[i]);
        
        // 异步执行核函数
        computeKernel<<<gridSize, blockSize, 0, streams[i]>>>(d_data[i]);
        
        // 异步拷贝回主机
        cudaMemcpyAsync(h_result[i], d_data[i], DATA_SIZE,
                       cudaMemcpyDeviceToHost, streams[i]);
    }
    
    // 同步所有流
    for (int i = 0;  i < numStreams; ++i) {
        cudaStreamSynchronize(streams[i]);
        cudaStreamDestroy(streams[i]);
    }
}

第四部分:计算优化

4.1 避免分支发散

分支发散会显著降低性能,因为同一warp内的线程必须执行不同的代码路径。

// 分支发散的例子(性能差)
__global__ void divergentBranch(int* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        if (i % 2 == 0) {
            // 偶数线程执行这里
            data[i] = data[i] * 2;
        } else {
            // 奇数线程执行这里
            data[i] = data[i] + 1;
        }
    }
}

// 优化版本:减少分支
__global__ void optimizedBranch(int* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        // 使用位运算避免条件分支
        int mask = -(i % 2);  // 0或-1
        data[i] = data[i] * (1 + (mask & 1)) + (mask & 1);
    }
}

// 更好的优化:重构算法避免分支
__global__ void noDivergentBranch(int* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        // 分别处理偶数和奇数索引
        if (i * 2 < n) {
            data[i * 2] = data[i * 2] * 2;
        }
        if (i * 2 + 1 < n) {
            data[i * 2 + 1] = data[i * 2 + 1] + 1;
        }
    }
}

4.2 循环展开和指令级并行

循环展开可以减少循环开销并提高指令级并行:

// 普通循环
__global__ void normalLoop(float* a, float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        for (int j = 0; j < 10; ++j) {
            c[i] += a[i] * b[i] + j;
        }
    }
}

// 循环展开
__global__ void unrolledLoop(float* a, float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        float sum = 0;
        sum += a[i] * b[i] + 0;
        sum += a[i] * b[i] + 1;
        sum += a[i] * b[i] + 2;
        sum += a[i] * b[i] + 3;
        sum += a[i] * b[i] + 4;
        sum += a[i] * b[i] + 5;
        sum += a[i] * b[i] + 6;
        sum += a[i] * b[i] + 7;
        sum += a[i] * b[i] + 8;
        sum += a[i] * b[i] + 9;
        c[i] = sum;
    }
}

// 使用宏进行循环展开
#define UNROLL_10(sum, a, b, i) \
    sum += a[i] * b[i] + 0; \
    sum += a[i] * b[i] + 1; \
    sum += a[i] * b[i] + 2; \
    sum += a[i] * b[i] + 3; \
    sum += a[i] * b[i] + 4; \
    sum += a[i] * b[i] + 5; \
    sum += a[i] * b[i] + 6; \
    sum += a[i] * b[i] + 7; \
    sum += a[i] * b[i] + 8; \
    sum += a[i] * b[i] + 9;

__global__ void unrolledLoopMacro(float* a, float* b, float* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        float sum = 0;
        UNROLL_10(sum, a, b, i);
        c[i] = sum;
    }
}

4.3 使用向量类型

CUDA提供向量类型(如float2, float4)来提高内存带宽利用率:

// 使用float4进行向量化内存访问
__global__ void vectorizedAdd(float4* a, float4* b, float4* c, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        // 一次处理4个float
        float4 va = a[i];
        float4 vb = b[i];
        float4 vc;
        vc.x = va.x + vb.x;
        vc.y = va.y + vb.y;
        vc.z = va.z + vb.z;
        vc.w = va.w + vb.w;
        c[i] = vc;
    }
}

// 主机代码中的内存对齐
void allocateAlignedMemory(float4** d_ptr, int n) {
    // 分配对齐的内存(256字节对齐)
    cudaMalloc(d_ptr, n * sizeof(float4));
    // 确保内存分配是256字节对齐的
    // 在实际应用中,可能需要使用cudaMallocPitch或手动对齐
}

第五部分:常见错误与陷阱

5.1 内存错误

错误1:越界访问

// 错误示例:数组越界
__global__ void outOfBounds(int* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    // 危险:没有边界检查
    data[i] = i;
}

// 正确做法:始终进行边界检查
__global__ void safeAccess(int* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {  // 必须的边界检查
        data[i] = i;
    }
}

错误2:主机内存与设备内存混淆

// 错误示例:直接访问设备内存
void wrongMemoryAccess() {
    float* d_data;
    cudaMalloc(&d_data, 100 * sizeof(float));
    
    // 错误:直接在主机代码中使用设备指针
    // d_data[0] = 1.0f;  // 这会崩溃或产生未定义行为
    
    // 正确做法:使用cudaMemcpy
    float h_data = 1.0f;
    cudaMemcpy(d_data, &h_data, sizeof(float), cudaMemcpyHostToDevice);
}

错误3:内存泄漏

// 错误示例:忘记释放内存
void memoryLeak() {
    float* d_data;
    cudaMalloc(&d_data, 100 * sizeof(float));
    // ... 使用d_data
    // 忘记调用cudaFree(d_data);
}

// 正确做法:RAII模式管理资源
class CudaMemory {
private:
    void* ptr;
public:
    CudaMemory(size_t size) {
        cudaMalloc(&ptr, size);
    }
    ~CudaMemory() {
        if (ptr) cudaFree(ptr);
    }
    void* get() { return ptr; }
};

void noMemoryLeak() {
    CudaMemory mem(100 * sizeof(float));
    // 自动释放内存
}

5.2 同步错误

错误1:过度使用全局同步

// 错误:在核函数中使用__syncthreads()的条件分支
__global__ void conditionalSync(int* data) {
    int tid = threadIdx.x;
    if (tid < 32) {
        // 只有部分线程到达这里
        __syncthreads();  // 错误:不是所有线程都会到达这里
    }
}

// 正确做法:确保所有线程都到达同步点
__global__ void properSync(int* data) {
    int tid = threadIdx.x;
    // 所有线程都执行的操作
    if (tid < 32) {
        data[tid] = tid;
    } else {
        data[tid] = 0;
    }
    __syncthreads();  // 所有线程都会到达这里
    
    // 继续其他操作
}

错误2:忘记同步数据传输

// 错误:异步操作后没有同步
void asyncError() {
    float* d_data;
    cudaMalloc(&d_data, 100 * sizeof(float));
    
    cudaMemcpyAsync(d_data, h_data, 100 * sizeof(float), 
                   cudaMemcpyHostToDevice);
    
    // 错误:立即使用数据,但拷贝可能未完成
    kernel<<<grid, block>>>(d_data);
    
    // 正确做法:同步或使用流
    cudaStream_t stream;
    cudaStreamCreate(&stream);
    cudaMemcpyAsync(d_data, h_data, 100 * sizeof(float), 
                   cudaMemcpyHostToDevice, stream);
    kernel<<<grid, block, 0, stream>>>(d_data);
    cudaStreamSynchronize(stream);
}

5.3 资源限制错误

错误1:超出资源限制

// 错误:声明过大的共享内存
__global__ void tooMuchSharedMemory() {
    __shared__ float sharedData[50000];  // 可能超过SM限制
    // ...
}

// 正确做法:检查资源使用
void checkResourceLimits() {
    cudaDeviceProp prop;
    cudaGetDeviceProperties(&prop, 0);
    
    printf("Max shared memory per block: %zu bytes\n", 
           prop.sharedMemPerBlock);
    printf("Max threads per block: %d\n", prop.maxThreadsPerBlock);
    printf("Max threads per SM: %d\n", prop.maxThreadsPerMultiProcessor);
}

5.4 数值精度问题

错误:单精度与双精度混用

// 错误:精度不匹配
__global__ void precisionError(float* result, double* input) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    // input是double,但result是float,可能导致精度损失
    result[i] = input[i] * 0.1;  // 潜在精度问题
}

// 正确做法:明确精度要求
__global__ void preciseComputation(float* result, float* input) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    result[i] = input[i] * 0.1f;  // 明确使用float
}

// 或者对于需要高精度的场景
__global__ void highPrecision(double* result, double* input) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    result[i] = input[i] * 0.1;  // 使用double
}

第六部分:高级优化技术

6.1 动态并行(Dynamic Parallelism)

动态并行允许GPU线程启动新的核函数:

// 动态并行示例:递归计算
__global__ void recursiveCompute(float* data, int depth, int maxDepth) {
    if (depth >= maxDepth) {
        // 基础情况
        int idx = blockIdx.x * blockDim.x + threadIdx.x;
        data[idx] = idx * 1.0f;
        return;
    }
    
    // 递归情况:启动新的核函数
    if (threadIdx.x == 0) {
        // 只有第一个线程启动新网格
        dim3 newGrid(1);
        dim3 newBlock(blockDim.x);
        recursiveCompute<<<newGrid, newBlock>>>(data, depth + 1, maxDepth);
    }
    
    __syncthreads();
}

6.2 协同组(Cooperative Groups)

协同组提供了更灵活的线程同步机制:

#include <cooperative_groups.h>

using namespace cooperative_groups;

__global__ void cooperativeExample(float* data, int n) {
    // 获取当前线程块
    thread_block block = this_thread_block();
    
    // 在块内同步
    block.sync();
    
    // 获取块内的子组
    thread_block_tile<32> warp = tiled_partition<32>(block);
    
    // 在warp内同步
    warp.sync();
    
    // 使用warp级操作
    int warpId = warp.meta_group_size();
    int laneId = warp.thread_rank();
    
    // 并行归约示例
    float value = data[block.thread_rank()];
    
    // warp内归约
    for (int offset = 16; offset > 0; offset /= 2) {
        value += warp.shfl_down(value, offset);
    }
    
    if (laneId == 0) {
        // warp的第一个线程保存结果
        data[warpId] = value;
    }
}

6.3 图(Graph)捕获

CUDA 10+引入的图捕获可以优化重复执行的核函数序列:

// 图捕获示例
void graphCaptureExample() {
    cudaGraph_t graph;
    cudaGraphExec_t graphExec;
    
    // 开始捕获
    cudaStream_t stream;
    cudaStreamCreate(&stream);
    cudaStreamBeginCapture(stream);
    
    // 在流中执行操作序列
    kernel1<<<grid1, block1, 0, stream>>>(d_data1);
    kernel2<<<grid2, block2, 0, stream>>>(d_data2);
    kernel3<<<grid3, block3, 0, stream>>>(d_data3);
    
    // 结束捕获
    cudaStreamEndCapture(stream, &graph);
    
    // 实例化图
    cudaGraphInstantiate(&graphExec, graph, NULL, NULL, 0);
    
    // 多次执行图
    for (int i = 0; i < 1000; ++i) {
        cudaGraphLaunch(graphExec, stream);
    }
    
    cudaStreamSynchronize(stream);
    
    // 清理
    cudaGraphDestroy(graph);
    cudaGraphExecDestroy(graphExec);
    cudaStreamDestroy(stream);
}

第七部分:性能分析与调试工具

7.1 NVIDIA Nsight Systems

Nsight Systems是NVIDIA的系统级性能分析工具:

# 使用命令行收集性能数据
nsys profile --stats=true ./my_cuda_app

# 生成报告
nsys profile -o report ./my_cuda_app
# 然后在Nsight GUI中打开report.qdrep

7.2 NVIDIA Nsight Compute

Nsight Compute用于内核级性能分析:

# 分析特定内核
ncu --set detailed ./my_cuda_app

# 只分析特定内核
ncu --kernel-name myKernel --set detailed ./my_cuda_app

7.3 CUDA-MEMCHECK

CUDA-MEMCHECK用于检测内存错误:

# 运行内存检查
cuda-memcheck ./my_cuda_app

# 详细检测
cuda-memcheck --tool memcheck ./my_cuda_app

# 检测竞争条件
cuda-memcheck --tool racecheck ./my_cuda_app

7.4 使用CUDA事件进行性能测量

// 精确测量核函数执行时间
void profileKernel() {
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    
    // 预热运行
    kernel<<<grid, block>>>(d_data);
    cudaDeviceSynchronize();
    
    // 开始测量
    cudaEventRecord(start);
    
    // 执行核函数
    kernel<<<grid, block>>>(d_data);
    
    // 结束测量
    cudaEventRecord(stop);
    cudaEventSynchronize(stop);
    
    float milliseconds = 0;
    cudaEventElapsedTime(&milliseconds, start, stop);
    
    printf("Kernel execution time: %.3f ms\n", milliseconds);
    
    cudaEventDestroy(start);
    cudaEventDestroy(stop);
}

第八部分:实际案例研究

8.1 案例1:矩阵乘法优化

从朴素实现到高度优化的演进:

// 阶段1:朴素实现
__global__ void matMulNaive(float* A, float* B, float* C, 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;
        for (int k = 0; k < N; ++k) {
            sum += A[row * N + k] * B[k * N + col];
        }
        C[row * N + col] = sum;
    }
}

// 阶段2:使用共享内存
__global__ void matMulShared(float* A, float* B, float* C, int N) {
    __shared__ float As[TILE_SIZE][TILE_SIZE];
    __shared__ float Bs[TILE_SIZE][TILE_SIZE];
    
    int row = blockIdx.y * TILE_SIZE + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    
    float sum = 0;
    
    for (int t = 0; t < (N + TILE_SIZE - 1) / TILE_SIZE; ++t) {
        // 加载数据到共享内存
        if (row < N && (t * TILE_SIZE + threadIdx.x) < N) {
            As[threadIdx.y][threadIdx.x] = A[row * N + t * TILE_SIZE + threadIdx.x];
        } else {
            As[threadIdx.y][threadIdx.x] = 0;
        }
        
        if (col < N && (t * TILE_SIZE + threadIdx.y) < N) {
            Bs[threadIdx.y][threadIdx.x] = B[(t * TILE_SIZE + threadIdx.y) * N + col];
        } else {
            Bs[threadIdx.y][threadIdx.x] = 0;
        }
        
        __syncthreads();
        
        for (int k = 0; k < TILE_SIZE; ++k) {
            sum += As[threadIdx.y][k] * Bs[k][threadIdx.x];
        }
        
        __syncthreads();
    }
    
    if (row < N && col < N) {
        C[row * N + col] = sum;
    }
}

// 阶段3:使用寄存器分块和向量化
#define TILE_SIZE 128
#define BLOCK_ROWS 8

__global__ void matMulOptimized(float* A, float* B, float* C, int N) {
    __shared__ float As[TILE_SIZE][TILE_SIZE];
    __shared__ float Bs[TILE_SIZE][TILE_SIZE];
    
    int row = blockIdx.y * BLOCK_ROWS + threadIdx.y;
    int col = blockIdx.x * TILE_SIZE + threadIdx.x;
    
    float c[BLOCK_ROWS] = {0};
    float a[BLOCK_ROWS];
    
    for (int t = 0; t < N / TILE_SIZE; ++t) {
        // 加载A到共享内存
        for (int i = 0; i < BLOCK_ROWS; ++i) {
            As[threadIdx.y + i * blockDim.y][threadIdx.x] = 
                A[(row + i * blockDim.y) * N + t * TILE_SIZE + threadIdx.x];
        }
        
        // 加载B到共享内存
        Bs[threadIdx.y][threadIdx.x] = 
            B[(t * TILE_SIZE + threadIdx.y) * N + col];
        
        __syncthreads();
        
        // 计算
        for (int k = 0; k < TILE_SIZE; ++k) {
            for (int i = 0; i < BLOCK_ROWS; ++i) {
                c[i] += As[threadIdx.y + i * blockDim.y][k] * Bs[k][threadIdx.x];
            }
        }
        
        __syncthreads();
    }
    
    // 写回结果
    for (int i = 0; i < BLOCK_ROWS; ++i) {
        if (row + i * blockDim.y < N && col < N) {
            C[(row + i * blockDim.y) * N + col] = c[i];
        }
    }
}

8.2 案例2:归约操作优化

// 阶段1:朴素归约
__global__ void reduceNaive(float* input, float* output, int n) {
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    if (tid < n) {
        // 串行归约(性能差)
        for (int i = 1; i < n; ++i) {
            input[0] += input[i];
        }
        if (tid == 0) *output = input[0];
    }
}

// 阶段2:并行归约
__global__ void reduceParallel(float* input, float* output, int n) {
    __shared__ float sdata[256];
    
    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    
    // 加载到共享内存
    sdata[tid] = (i < n) ? input[i] : 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];
    }
}

// 阶段3:完全展开的归约(最优)
__global__ void reduceCompleteUnroll(float* input, float* output, int n) {
    __shared__ float sdata[256];
    
    int tid = threadIdx.x;
    int i = blockIdx.x * blockDim.x * 2 + threadIdx.x;
    
    // 加载两个元素
    float sum = (i < n) ? input[i] : 0;
    if (i + blockDim.x < n) sum += input[i + blockDim.x];
    sdata[tid] = sum;
    __syncthreads();
    
    // 完全展开归约
    if (tid < 128) { sdata[tid] += sdata[tid + 128]; } __syncthreads();
    if (tid < 64) { sdata[tid] += sdata[tid + 64]; } __syncthreads();
    
    // warp内归约(不需要同步)
    if (tid < 32) {
        volatile float* vdata = sdata;
        vdata[tid] += vdata[tid + 32];
        vdata[tid] += vdata[tid + 16];
        vdata[tid] += vdata[tid + 8];
        vdata[tid] += vdata[tid + 4];
        vdata[tid] += vdata[tid + 2];
        vdata[tid] += vdata[tid + 1];
    }
    
    if (tid == 0) {
        output[blockIdx.x] = sdata[0];
    }
}

第九部分:最佳实践总结

9.1 性能优化检查清单

  1. 内存访问优化

    • [ ] 确保全局内存访问是合并的
    • [ ] 最大化共享内存使用
    • [ ] 使用常量/纹理内存用于只读数据
    • [ ] 避免不必要的主机-设备数据传输
  2. 执行配置优化

    • [ ] 选择合适的块大小(256-512)
    • [ ] 考虑占用率但不要过度优化
    • [ ] 使用多个流实现异步执行
  3. 计算优化

    • [ ] 避免分支发散
    • [ ] 使用循环展开
    • [ ] 考虑向量化内存访问
    • [ ] 使用适当的精度
  4. 资源管理

    • [ ] 使用RAII模式管理资源
    • [ ] 检查资源限制
    • [ ] 避免内存泄漏
    • [ ] 正确处理错误

9.2 调试技巧

// 使用printf调试(CUDA 11.0+)
__global__ void debugKernel(float* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        if (i == 0) {  // 只打印第一个线程的信息
            printf("Block (%d,%d,%d), Thread (%d,%d,%d)\n",
                   blockIdx.x, blockIdx.y, blockIdx.z,
                   threadIdx.x, threadIdx.y, threadIdx.z);
        }
        data[i] = i;
    }
}

// 使用assert进行调试
__global__ void assertKernel(float* data, int n) {
    int i = blockIdx.x * blockDim.x + threadIdx.x;
    if (i < n) {
        assert(data[i] >= 0);  // 如果条件不满足会中断
        data[i] = sqrt(data[i]);
    }
}

9.3 持续学习资源

  • 官方文档: NVIDIA CUDA Programming Guide
  • 示例代码: CUDA Toolkit Samples
  • 社区: NVIDIA Developer Forums
  • 书籍: “Programming Massively Parallel Processors”
  • 在线课程: NVIDIA DLI (Deep Learning Institute)

结论

CUDA优化是一个持续的过程,需要理论知识与实践经验的结合。通过理解GPU架构、掌握内存层次结构、优化执行配置、避免常见错误,并使用合适的工具进行性能分析,开发者可以显著提升CUDA应用程序的性能。

记住,优化应该基于实际的性能分析结果,而不是猜测。每个优化都应该通过基准测试来验证其效果。随着GPU架构的不断发展,保持学习和适应新技术也是成功的关键。

希望本文提供的最佳实践和示例代码能够帮助你在CUDA编程的道路上从入门走向精通。