【CUDA手册002】CUDA 基础执行模型:写出第一个正确的 Kernel

【CUDA手册002】CUDA 基础执行模型:写出第一个正确的 Kernel

------ 用最少的概念写出第一个正确的 Kernel

在医学图像处理场景中(例如 CT / MRI 切片),输入数据通常以二维矩阵形式存在。将这类数据映射到 GPU 上并行处理时,真正需要解决的问题并不复杂:

如何让每一个 GPU 线程,准确且唯一地对应到一个像素位置。

只要这个映射关系是正确的,后续的性能优化才有讨论价值。本篇的目标,即是用尽量少的执行模型概念,完成这一映射。


1. 执行模型的最小认知集合

在初次编写 CUDA Kernel 时,无需引入 SM、Warp 或调度细节。对绝大多数图像算子而言,以下三个层级已经足够支撑正确实现。

  • Thread(线程):CUDA 中的最小执行单元,每个线程执行一次 Kernel 函数。
  • Block(线程块):一组线程的集合,是共享内存和线程同步的基本边界。
  • Grid(网格):所有线程块的集合,代表一次 Kernel 调用的整体计算范围。

这三者构成了 CUDA 的基本执行层级。


2. 坐标映射问题:二维图像与一维显存

这是 CUDA 图像算子中最容易出错、也最容易被忽视的部分。

GPU 的全局显存(Global Memory)在物理上是一维线性空间 ,而医学图像在逻辑上是二维结构。Kernel 的首要任务,是在这两种表示之间建立一致的映射关系。

二维到一维的基本公式

假设图像宽度为 widthwidthwidth,像素坐标为 (x,y)(x, y)(x,y),则其在一维内存中的索引为:

idx=y×width+xidx = y \times width + xidx=y×width+x

这一公式在几乎所有基于行优先(row-major)存储的图像处理中都成立。

CUDA 中的线程坐标计算

在 Kernel 内部,CUDA 通过一组内置变量提供线程在执行层级中的位置信息:

  • blockIdx:当前线程块在 Grid 中的位置。
  • blockDim:线程块在各维度上的尺寸。
  • threadIdx:线程在当前 Block 内的位置。

对应到二维布局时:

  • 线程的绝对 xxx 坐标:

    x=blockIdx.x×blockDim.x+threadIdx.xx = blockIdx.x \times blockDim.x + threadIdx.xx=blockIdx.x×blockDim.x+threadIdx.x

  • 线程的绝对 yyy 坐标:

    y=blockIdx.y×blockDim.y+threadIdx.yy = blockIdx.y \times blockDim.y + threadIdx.yy=blockIdx.y×blockDim.y+threadIdx.y

一旦 xxx 与 yyy 被正确计算,后续所有图像算子都可以建立在这一坐标体系之上。


3. 示例:一个完整且正确的二维图像 Kernel

下面以一个简单的像素偏移(Offset)算子为例,展示一个结构完整、边界处理正确的二维 CUDA Kernel。

cpp 复制代码
// CUDA Kernel:对图像像素值做常量偏移
// 使用二维 block / grid 布局
__global__ void imageOffsetKernel(
    float* input,
    float* output,
    int width,
    int height,
    float offset
) {
    // 1. 计算当前线程对应的像素坐标
    int x = blockIdx.x * blockDim.x + threadIdx.x;
    int y = blockIdx.y * blockDim.y + threadIdx.y;

    // 2. 边界检查
    // Grid 通常按 Block 尺寸向上取整,线程总数往往大于实际像素数
    if (x < width && y < height) {
        // 3. 二维坐标映射到一维内存索引
        int idx = y * width + x;

        // 4. 业务逻辑
        output[idx] = input[idx] + offset;
    }
}

该示例体现了二维图像 Kernel 的三个关键要素:

  1. 明确的线程到像素坐标映射;
  2. 必不可少的边界保护;
  3. 与内存布局一致的索引计算方式。

4. Host 端的 Kernel 启动方式

在 CPU 侧,需要为 Kernel 定义 Block 与 Grid 的尺寸,使其覆盖整张图像。

cpp 复制代码
void ProcessImage(float* d_input, float* d_output, int width, int height) {
    // 每个 Block 使用 16x16 的二维线程布局(共 256 线程)
    dim3 blockSize(16, 16);

    // 通过向上取整,确保 Grid 覆盖完整图像区域
    dim3 gridSize(
        (width  + blockSize.x - 1) / blockSize.x,
        (height + blockSize.y - 1) / blockSize.y
    );

    // 启动 Kernel
    imageOffsetKernel<<<gridSize, blockSize>>>(
        d_input, d_output, width, height, 10.0f
    );
}

在图像处理场景中,二维 Block 布局通常比一维布局更符合数据访问模式,有利于后续的缓存与访存优化。


5. 编写第一个 Kernel 时的三条硬性检查项

在完成第一个 CUDA 图像 Kernel 后,建议逐条对照以下原则进行自检:

  1. Grid 尺寸必须向上取整

    若图像宽度为 100100100,而 Block 宽度为 323232,则 Grid 至少需要 444 个 Block(共 128128128 个线程),而非 333 个。公式通常写为:

    gridDim=dataDim+blockDim−1blockDimgridDim = \frac{dataDim + blockDim - 1}{blockDim}gridDim=blockDimdataDim+blockDim−1

  2. 必须存在边界保护条件

    向上取整意味着会产生"多余线程",这些线程必须通过 if (x < width && y < height) 等条件加以约束,否则将访问非法内存。

  3. 索引计算应保持内存连续性

    一维索引计算中,应始终保证横向坐标变化最快,即:

    idx=y×width+xidx = y \times width + xidx=y×width+x

    这一约定直接影响全局内存的合并访问(Coalesced Access)效率。


小结

  • CUDA 的执行模型可以在不引入底层硬件细节的情况下完成正确建模。
  • 二维图像 Kernel 的核心在于线程到像素坐标的确定性映射。
  • 边界检查与索引计算方式是正确性的底线,而非优化手段。

在此基础上,后续章节将逐步讨论访存模式、共享内存与性能分析工具。

相关推荐
专注于大数据技术栈2 小时前
什么是召回(Recall)
ai
企业对冲系统官2 小时前
基差风险管理系统集成说明与接口规范
大数据·运维·python·算法·区块链·github
程序员-King.2 小时前
day134—快慢指针—环形链表(LeetCode-141)
算法·leetcode·链表·快慢指针
Swift社区2 小时前
LeetCode 376 摆动序列
算法·leetcode·职场和发展
oioihoii2 小时前
程序员如何系统入门Vibe Coding?
c++
高洁012 小时前
AIGC技术与进展(1)
深度学习·算法·机器学习·transformer·知识图谱
C+++Python2 小时前
C++类型判断
开发语言·c++
未来之窗软件服务2 小时前
计算机等级考试——二叉树考点和坑——东方仙盟
算法·二叉树·计算机软考·仙盟创梦ide·东方仙盟
张张努力变强2 小时前
C++类和对象(一):inline函数、nullptr、类的定义深度解析
开发语言·前端·jvm·数据结构·c++·算法