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

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

学习目标

  • 正确理解并使用线程/块/网格(thread/block/grid)三层结构。
  • 掌握索引变量:threadIdx、blockIdx、blockDim、gridDim。
  • 掌握 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 维。
  • sharedMemBytes、stream 可省略(默认 0 / 0)。

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

  • 1D:blockDim.x = 128 或 256(常见起点);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 坐标

给定 i 和 width,恢复 (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)**两项实战,并提供了可编译运行的完整工程代码与验证逻辑。
相关推荐
2501_938140072 小时前
做内容很省心,2026好用AI笔记,支持抖音小红书视频无水印保存
人工智能
sell-cloud4 小时前
Prompt Engineering要凉了?Fable 5.5能“猜“意图,但银行正在封杀AI Agent——企业AI的真正护城河是什么
人工智能·prompt
Navicat中国6 小时前
Navicat 18 内置 AI 助手让数据库开发从繁琐操作迈向智能协作
大数据·人工智能·数据库开发
4SAPI6 小时前
多模型请求路由怎么设计?成本、速度和能力如何平衡
人工智能·python·gateway·php
魔乐社区6 小时前
翼芯・智算测试与适配优化专区,让国产算力“选得准、跑得通、用得好“
人工智能·开源·大模型
XCDoris8 小时前
郴州logo设计机构
大数据·人工智能·python
Goboy8 小时前
一个人带三个Agent,我用全新TRAE从0到1搭了个资讯周刊站
人工智能·trae
俊哥V8 小时前
每日 AI 研究简报 · 2026-10-19
人工智能·ai
AI_AGENT_DEV_AI9 小时前
AI英语教培智能体的开发
人工智能
物联网软硬件开发-轨物科技9 小时前
【技术白皮书】光伏清检一体化机器人系统解决方案技术白皮书
大数据·运维·网络·人工智能·机器人