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 大小。
-
模式(官方推荐用法之一):
cssfor (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. 作业与扩展练习
- 矩阵加法(完成版) :把示例的
width/height
调到更大(如2048x2048
),用cudaEvent
计时 GPU 与 CPU 的加法耗时对比(关注正确性优先)。 - 标量乘法(二维网格版) :用 2D 索引改写
ScalarMul1D
,对height x width
矩阵逐元素乘以alpha
。 - 3D 练习 :实现 3D 张量
C = A + B
的 kernel(用 3D grid/block),并写 host 侧校验。 - 边界条件 :刻意选择不能被
blockDim
整除的尺寸,确保你的守卫逻辑正确。
本周小结
- 你已经掌握了线程索引与一维/二维/三维映射的标准写法;
- 学会了 kernel 启动与网格尺寸计算;
- 完成了**矩阵加法(2D 网格)与标量乘法(Grid-Stride Loop)**两项实战,并提供了可编译运行的完整工程代码与验证逻辑。