Week 2 – CUDA Programming Model(超详细教程)

Week 2 -- CUDA Programming Model(超详细教程)

学习目标

  • 正确理解并使用线程/块/网格(thread/block/grid)三层结构。
  • 掌握索引变量:threadIdxblockIdxblockDimgridDim
  • 掌握 kernel 启动语法:kernel<<<grid, block[, sharedMem, stream]>>>(...)
  • 能将 1D/2D/3D 的问题映射到线程网格中(含越界保护)。
  • 实战:矩阵相加(2D 网格)向量/矩阵标量乘法(1D 网格-栅格化循环)

1. 理解线程层次与索引

1.1 三层结构与关键内置变量

  • Block 由若干 Thread 组成;Grid 由若干 Block 组成。

  • 每个线程可读:

    • threadIdx.{x,y,z}:线程在块内的 3D 坐标;
    • blockIdx.{x,y,z}:块在网格内的 3D 坐标;
    • blockDim.{x,y,z}:块维度(每个维度线程数);
    • gridDim.{x,y,z}:网格维度(每个维度块数)。
  • 这些都是 内置变量,在 device 端可用。

1.2 1D 映射(最常见)

把长度为 N 的一维数组映射到线程:

ini 复制代码
int i = blockIdx.x * blockDim.x + threadIdx.x; // 线性线程 ID
if (i < N) { /* 处理 data[i] */ }

1.3 2D 映射(矩阵、图像常用)

假设矩阵大小 height x width(行优先 row-major):

arduino 复制代码
int col = blockIdx.x * blockDim.x + threadIdx.x; // x → 列
int row = blockIdx.y * blockDim.y + threadIdx.y; // y → 行
if (row < height && col < width) {
    int idx = row * width + col; // 线性地址
    /* 处理 A[row, col] 对应的一维下标 idx */
}

1.4 3D 映射(体数据/三维网格)

假设体素尺寸 depth x height x width,坐标 (z, y, x)

ini 复制代码
int x = blockIdx.x * blockDim.x + threadIdx.x;
int y = blockIdx.y * blockDim.y + threadIdx.y;
int z = blockIdx.z * blockDim.z + threadIdx.z;
if (z < depth && y < height && x < width) {
    int idx = (z * height + y) * width + x;
}

边界保护 :必须做 if (index < limit) 守卫,避免越界访问。


2. Kernel 启动语法与网格尺寸计算

2.1 基本语法

bash 复制代码
kernel<<<gridDim, blockDim, sharedMemBytes, stream>>>(args...);
  • gridDim / blockDim 类型通常是 dim3,可 1~3 维。
  • sharedMemBytesstream 可省略(默认 0 / 0)。

2.2 典型尺寸选择(起步建议)

  • 1D:blockDim.x = 128256(常见起点);gridDim.x = (N + blockDim.x - 1) / blockDim.x
  • 2D:blockDim = dim3(16, 16)gridDim = dim3(ceilDiv(width,16), ceilDiv(height,16))

实际最佳尺寸取决于设备与算法。可在运行时查询设备上限(如 maxThreadsPerBlock),并用 Nsight 工具后续优化。本教程先保证正确性。


3. 实战一:向量加法扩展到矩阵加法(2D 网格)

3.1 设计与数据布局

  • 使用 行优先(row-major) 连续内存:idx = row * width + col
  • 每个线程处理一个元素 C[row,col] = A[row,col] + B[row,col]

3.2 完整可运行示例(matrix_add_2d.cu

可直接复制到文件 matrix_add_2d.cu,用 nvcc 编译运行。

arduino 复制代码
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <cassert>
#include <cuda_runtime.h>

#define CUDA_CHECK(expr) do { \
    cudaError_t err__ = (expr); \
    if (err__ != cudaSuccess) { \
        fprintf(stderr, "CUDA error at %s:%d: %s (%s)\n", \
                __FILE__, __LINE__, cudaGetErrorString(err__), #expr); \
        exit(EXIT_FAILURE); \
    } \
} while (0)

__global__ void MatAdd2D(const float* __restrict__ A,
                         const float* __restrict__ B,
                         float* __restrict__ C,
                         int width, int height) {
    int col = blockIdx.x * blockDim.x + threadIdx.x; // x -> column
    int row = blockIdx.y * blockDim.y + threadIdx.y; // y -> row
    if (row < height && col < width) {
        int idx = row * width + col; // row-major
        C[idx] = A[idx] + B[idx];
    }
}

static void fill_matrix(std::vector<float>& M, int width, int height, float bias) {
    for (int r = 0; r < height; ++r) {
        for (int c = 0; c < width; ++c) {
            M[r*width + c] = r * 0.1f + c * 0.01f + bias; // 可视化友好的模式
        }
    }
}

static bool verify_add(const std::vector<float>& A,
                       const std::vector<float>& B,
                       const std::vector<float>& C,
                       int width, int height, float eps = 1e-6f) {
    for (int i = 0; i < width * height; ++i) {
        float ref = A[i] + B[i];
        if (fabsf(C[i] - ref) > eps) return false;
    }
    return true;
}

int main() {
    // 小尺寸用于打印验证;可改大测试性能
    int width = 8;
    int height = 5;
    size_t bytes = size_t(width) * height * sizeof(float);

    std::vector<float> hA(width * height), hB(width * height), hC(width * height, 0.f);
    fill_matrix(hA, width, height, 1.0f);
    fill_matrix(hB, width, height, 2.0f);

    float *dA = nullptr, *dB = nullptr, *dC = nullptr;
    CUDA_CHECK(cudaMalloc(&dA, bytes));
    CUDA_CHECK(cudaMalloc(&dB, bytes));
    CUDA_CHECK(cudaMalloc(&dC, bytes));

    CUDA_CHECK(cudaMemcpy(dA, hA.data(), bytes, cudaMemcpyHostToDevice));
    CUDA_CHECK(cudaMemcpy(dB, hB.data(), bytes, cudaMemcpyHostToDevice));

    dim3 block(16, 16); // 每块 256 线程
    dim3 grid((width + block.x - 1) / block.x,
              (height + block.y - 1) / block.y);

    MatAdd2D<<<grid, block>>>(dA, dB, dC, width, height);
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    CUDA_CHECK(cudaMemcpy(hC.data(), dC, bytes, cudaMemcpyDeviceToHost));

    // 打印小矩阵结果
    printf("A + B = C (showing %dx%d):\n", height, width);
    for (int r = 0; r < height; ++r) {
        for (int c = 0; c < width; ++c) {
            int idx = r * width + c;
            printf("%6.2f ", hC[idx]);
        }
        printf("\n");
    }

    // 校验正确性
    bool ok = verify_add(hA, hB, hC, width, height);
    printf("Verification: %s\n", ok ? "PASS" : "FAIL");

    CUDA_CHECK(cudaFree(dA));
    CUDA_CHECK(cudaFree(dB));
    CUDA_CHECK(cudaFree(dC));
    return ok ? 0 : 1;
}

编译与运行

bash 复制代码
nvcc -O2 matrix_add_2d.cu -o matrix_add_2d
./matrix_add_2d

想测大矩阵,把 width/height 改成如 1024/1024,其他代码无需改动。


4. 实战二:标量乘法(GPU) ------推荐用 1D + Grid-Stride Loop

4.1 为什么用 Grid-Stride Loop

  • 可在任何网格尺寸 下覆盖任意长度 N

  • 写一次适配小/大数据,不需精确匹配 grid 大小。

  • 模式(官方推荐用法之一):

    css 复制代码
    for (int i = blockIdx.x * blockDim.x + threadIdx.x;
         i < N;
         i += blockDim.x * gridDim.x) {
        // 处理 i
    }

4.2 完整可运行示例(scalar_multiply.cu

将矩阵展平成 1D 数组,对每个元素乘以常数 alpha。同样适用于纯向量。

arduino 复制代码
#include <cstdio>
#include <cstdlib>
#include <vector>
#include <cassert>
#include <cuda_runtime.h>

#define CUDA_CHECK(expr) do { \
    cudaError_t err__ = (expr); \
    if (err__ != cudaSuccess) { \
        fprintf(stderr, "CUDA error at %s:%d: %s (%s)\n", \
                __FILE__, __LINE__, cudaGetErrorString(err__), #expr); \
        exit(EXIT_FAILURE); \
    } \
} while (0)

// Grid-Stride Loop 版本:任何 N 都能覆盖
__global__ void ScalarMul1D(float* __restrict__ data,
                            float alpha, int N) {
    for (int i = blockIdx.x * blockDim.x + threadIdx.x;
         i < N;
         i += blockDim.x * gridDim.x) {
        data[i] *= alpha;
    }
}

static void fill(std::vector<float>& v, float start) {
    for (int i = 0; i < (int)v.size(); ++i) v[i] = start + i * 0.5f;
}

static bool verify_scalar(const std::vector<float>& in,
                          const std::vector<float>& out,
                          float alpha, float eps = 1e-6f) {
    for (int i = 0; i < (int)in.size(); ++i) {
        float ref = in[i] * alpha;
        if (fabsf(out[i] - ref) > eps) return false;
    }
    return true;
}

int main() {
    // 支持:直接把矩阵展平:N = width * height
    int width = 8, height = 5;
    int N = width * height;
    size_t bytes = size_t(N) * sizeof(float);
    float alpha = 3.0f;

    std::vector<float> hIn(N), hOut(N, 0.f);
    fill(hIn, 1.0f);

    float* dBuf = nullptr;
    CUDA_CHECK(cudaMalloc(&dBuf, bytes));
    CUDA_CHECK(cudaMemcpy(dBuf, hIn.data(), bytes, cudaMemcpyHostToDevice));

    // 选择一个起步配置:block 256 线程,grid 足够大覆盖 GPU SM
    int block = 256;
    int grid = (N + block - 1) / block;    // 合理覆盖
    grid = (grid == 0) ? 1 : grid;         // N=0 时保护
    // 可根据设备调大到如 2~4 倍 SM 数以利并行,这里保持简洁

    ScalarMul1D<<<grid, block>>>(dBuf, alpha, N);
    CUDA_CHECK(cudaGetLastError());
    CUDA_CHECK(cudaDeviceSynchronize());

    CUDA_CHECK(cudaMemcpy(hOut.data(), dBuf, bytes, cudaMemcpyDeviceToHost));

    // 打印小尺寸
    printf("Scalar multiply (alpha=%.1f) result %dx%d (first few):\n", alpha, height, width);
    for (int i = 0; i < N && i < 16; ++i) {
        printf("%6.2f ", hOut[i]);
    }
    printf("\n");

    bool ok = verify_scalar(hIn, hOut, alpha);
    printf("Verification: %s\n", ok ? "PASS" : "FAIL");

    CUDA_CHECK(cudaFree(dBuf));
    return ok ? 0 : 1;
}

编译与运行

bash 复制代码
nvcc -O2 scalar_multiply.cu -o scalar_multiply
./scalar_multiply

若要对二维矩阵用 2D 网格做标量乘法,也可照搬 MatAdd2D 的 2D 索引,改成 data[idx] *= alpha。本教程用 1D + 栅格化循环 是为了演示一种尺寸无关的通用写法。


5. 1D → 2D → 3D 映射练习与要点

练习 1:把 1D 索引"反解"为 2D 坐标

给定 iwidth,恢复 (row, col)

ini 复制代码
int row = i / width;
int col = i % width;

练习 2:把 2D 坐标打平为 1D

arduino 复制代码
int idx = row * width + col; // row-major

练习 3:3D ↔ 1D

(z,y,x),打平:

arduino 复制代码
int idx = (z * height + y) * width + x;

idx,反解:

ini 复制代码
int z = idx / (height * width);
int rem = idx % (height * width);
int y = rem / width;
int x = rem % width;

6. 校验、调试与常见坑

6.1 错误检查

  • 启动后立即 cudaGetLastError()
  • 之后 cudaDeviceSynchronize() 捕获运行期错误。

示例(上面代码已包含):

scss 复制代码
kernel<<<grid, block>>>(...);
CUDA_CHECK(cudaGetLastError());
CUDA_CHECK(cudaDeviceSynchronize());

6.2 越界访问

  • 始终做边界守卫(if (i < N) / if (row < H && col < W))。
  • 越界会导致 silent memory corruption 或 "unspecified launch failure"。

6.3 线程块上限

  • 每块线程数必须 ≤ 设备 maxThreadsPerBlock(常见 1024)。
  • 单维上限也有限制(例如 blockDim.x 的最大值),可用 cudaGetDeviceProperties 查询,避免超配。

6.4 内存合并访问(性能相关)

  • 同一 warp(NVIDIA 设备上 warpSize=32)中连续线程访问连续地址,可提升带宽利用率。
  • 2D 映射时让 threadIdx.x 对应列(行内连续),更有利于合并访问(行优先布局)。

可在运行时读取 prop.warpSize 验证(NVIDIA 当前硬件该值为 32;不同设备以实际查询为准)。


7. 可选:设备信息查询(自检模板)

perl 复制代码
cudaDeviceProp prop; 
int dev = 0; 
CUDA_CHECK(cudaGetDevice(&dev));
CUDA_CHECK(cudaGetDeviceProperties(&prop, dev));
printf("Device: %s\n", prop.name);
printf("maxThreadsPerBlock: %d\n", prop.maxThreadsPerBlock);
printf("maxThreadsDim: %d x %d x %d\n", prop.maxThreadsDim[0], prop.maxThreadsDim[1], prop.maxThreadsDim[2]);
printf("maxGridSize: %d x %d x %d\n", prop.maxGridSize[0], prop.maxGridSize[1], prop.maxGridSize[2]);
printf("warpSize: %d\n", prop.warpSize);

8. 作业与扩展练习

  1. 矩阵加法(完成版) :把示例的 width/height 调到更大(如 2048x2048),用 cudaEvent 计时 GPU 与 CPU 的加法耗时对比(关注正确性优先)。
  2. 标量乘法(二维网格版) :用 2D 索引改写 ScalarMul1D,对 height x width 矩阵逐元素乘以 alpha
  3. 3D 练习 :实现 3D 张量 C = A + B 的 kernel(用 3D grid/block),并写 host 侧校验。
  4. 边界条件 :刻意选择不能被 blockDim 整除的尺寸,确保你的守卫逻辑正确。

本周小结

  • 你已经掌握了线程索引与一维/二维/三维映射的标准写法;
  • 学会了 kernel 启动与网格尺寸计算;
  • 完成了**矩阵加法(2D 网格)标量乘法(Grid-Stride Loop)**两项实战,并提供了可编译运行的完整工程代码与验证逻辑。
相关推荐
kkcodeer11 分钟前
大模型Prompt原理、编写原则与技巧以及衡量方法
人工智能·prompt·ai大模型
DevSecOps选型指南21 分钟前
SBOM风险预警 | NPM前端框架 javaxscript 遭受投毒窃取浏览器cookie
前端·人工智能·前端框架·npm·软件供应链安全厂商·软件供应链安全工具
rocksun23 分钟前
MCP利用流式HTTP实现实时AI工具交互
人工智能·mcp
xiaok1 小时前
docker network create langbot-network这条命令在dify输入还是在langbot中输入
人工智能
It_张1 小时前
LLM(大语言模型)的工作原理 图文讲解
人工智能·语言模型·自然语言处理
Darach1 小时前
坐姿检测Python实现
人工智能·python
xiaok1 小时前
LangBot 和消息平台均运行在 Docker 容器中
人工智能
queeny1 小时前
Datawhale AI夏令营 科大讯飞AI大赛(大模型技术) Task3 心得
人工智能
ToTensor1 小时前
Paraformer实时语音识别中的碎碎念
人工智能·语音识别·xcode