4. 核函数启动配置:<<>>语法,动态并行,启动参数优化

说到CUDA编程,最基础也最容易被忽视的就是核函数启动配置。说白了就是那对尖括号里的参数——<<<grid, block>>>。我见过太多人随便填个数字就跑,结果性能惨不忍睹。今天咱们好好聊聊这里面的门道。

4.1 启动配置的基本语法

先看最基础的写法:

kernel_name<<<gridDim, blockDim, sharedMem, stream>>>(args);

四个参数,但很多人只用前两个。我个人习惯把四个都写上,哪怕sharedMem是0,stream是默认流。为什么?代码可读性更强,后期维护也方便。

参数 含义 典型范围
gridDim 网格维度(block数量) 1 ~ 数百万
blockDim 线程块维度(线程数量) 32 ~ 1024
sharedMem 动态共享内存大小(字节) 0 ~ 48KB
stream CUDA流 0 或 cudaStream_t

核心原则:blockDim必须是32的倍数。这不是建议,是硬性要求。因为warp是32个线程一组,不是32的倍数会导致最后一个warp浪费。

4.2 block大小的选择艺术

block大小怎么选?我直接给结论:128或256是最稳妥的选择。

为什么?你想想看:

  • block太小(比如32):SM利用率低,调度开销大
  • block太大(比如1024):寄存器压力大,occupancy反而下降

我在项目中遇到过这样一个案例:一个矩阵乘法核函数,用256线程的block比用512线程快了15%。当时团队里有人不信,觉得线程越多并行度越高。其实不是的,线程多了每个线程分到的寄存器就少了,编译器不得不把变量 spill 到local memory,性能就崩了。

经验公式:block大小 = (寄存器使用量 × 线程数) ≤ 65536。用nvcc的--ptxas-options=-v可以查看寄存器使用情况。

4.3 grid大小的计算

grid大小说白了就是你要启动多少个block。常见做法:

int blockSize = 256;
int gridSize = (N + blockSize - 1) / blockSize;
kernel<<<gridSize, blockSize>>>(d_data, N);

嗯,这里要注意:不要启动太多block。GPU能同时运行的block数量是有限的,取决于SM数量和每个SM能容纳的block数。启动几百万个block虽然语法上允许,但调度开销会让你怀疑人生。

我曾经犯过一个错误:处理1亿个元素,每个block只处理1个元素,启动了1亿个block。结果呢?比CPU还慢。后来改成每个block处理256个元素,性能提升了100倍。

4.4 动态并行:核函数里再调核函数

动态并行是CUDA里一个很酷的特性——在GPU上直接启动新的核函数。语法和host端一样:

__global__ void parent_kernel() {
    if (threadIdx.x == 0) {
        child_kernel<<<grid, block>>>();
    }
}

警告:动态并行有坑!子核函数启动是异步的,但父核函数不会自动等待子核函数完成。你需要手动加cudaDeviceSynchronize()或者用cudaStreamSynchronize()。我见过有人因为这个原因debug了三天。

动态并行适合什么场景?递归算法、树遍历、自适应网格细化。但说实话,能用静态并行解决的问题,尽量别用动态并行。为什么呢?

  • 动态并行有额外的启动开销(约10μs)
  • 调试困难,你没法在子核函数里打断点
  • 性能不稳定,取决于GPU的调度策略

4.5 启动参数优化实战

优化启动参数,我总结了三步走:

  1. 确定block大小:从128开始,用profiler跑一遍,然后试256、512,看哪个occupancy最高
  2. 调整grid大小:保证每个SM至少有4~8个active block,这样能隐藏访存延迟
  3. 微调shared memory:如果用了动态共享内存,尽量让sharedMem是32的倍数,避免bank conflict

避坑指南:我曾经在优化一个图像处理核函数时,发现occupancy高达100%,但性能就是上不去。后来发现是block大小选得太小,导致每个block的计算量太少,SM的指令发射单元一直在空转。记住:occupancy高不等于性能好,还要看计算密度。

最后分享一个实用技巧:用cudaOccupancyMaxPotentialBlockSize这个API来自动计算最优block大小。虽然不能保证100%最优,但至少是个不错的起点。

int minGridSize, blockSize;
cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize,
                                   kernel, 0, 0);
int gridSize = (N + blockSize - 1) / blockSize;

嗯,启动配置这块就聊这么多。记住:没有银弹,每个核函数都有自己的最佳配置。多跑profiler,多试几次,慢慢就有感觉了。

核函数启动配置核心逻辑 启动配置 gridDim: block数量 blockDim: 线程数量 (32倍数) sharedMem: 动态共享内存 优化建议 • block=128/256 • grid保证4-8 active block/SM • sharedMem 32倍数 • 用occupancy API • profiler验证 动态并行 • GPU内启动子核函数 • 适合递归/树遍历 • 注意同步问题