1 int i = blockIdx.x * blockDim.x + thredIdx.x
CUDA 为每一个线程提供了四个内置变量,用于定位自己在整个任务中的位置:
量
含义
维度范围
threadIdx.x/y/z
线程在块内的局部索引
日 blockDim-1
blockIdx.x/y/z
线程块在网格内的索引
日 gridDim-1
blockDim.x/y/z
每个块每维的线程数
由启动参数 决定 $<<\ldots,\quad{\mathrm{t h r e a d s}}>>>$
gridDim.x/y/z
网格每维的块数
由启动参数 <<
所谓全局索引,就是跳过前面所有块的线程,再加上当前块内的偏移。
全局坐标计算方式 在内存中,数据始终按照一维排布,因此全局坐标计算就是将多维逻辑索引映射为一维物理地址
一维坐标计算 1 int global_id = blockIdx.x * blockDim.x + threadIdx.x;
blockIdx * blockDim.x 计算之前的块有多少线程(基地址),用来定位到当前块
‘threadIdx.x’ 加上当前线程块内的偏移
边界保护
为什么要这个判断?
网格覆盖线程总数是块大小整数倍,可能超过数据总量N,不加判断会越界
二维坐标计算 二维数据(比如图像)需要两个全局索引: ‘row’ 和 ‘col’
1 2 3 4 5 6 7 8 9 10 int col = blockIdx.x * blockDim.x + threadIdx.x;int row = blockIdx.y * blockDim.y + threadIdx.y;if (row < height && col < width) { int global_idx = row * width + col; }
示例:RGB图像转灰度图 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 __global__ void rgbToGray (const uchar3* d_img, unsigned char * d_gray, int width, int height) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < width && y < height) { int idx = y * width + x; uchar3 pixel = d_img[idx]; unsigned char gray = static_cast <unsigned char >(0.299f * pixel.x + 0.587f * pixel.y + 0.114f * pixel.z); d_gray[idx] = gray; } }
在主机端,根据图像实际尺寸配置Grid和Block,确保覆盖所有像素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int main () { dim3 blockSize (16 , 16 ) ; dim3 gridSize ((width + blockSize.x - 1 ) / blockSize.x, (height + blockSize.y - 1 ) / blockSize.y) ; rgbToGray<<<gridSize, blockSize>>>(d_img, d_gray, width, height); }
完整代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 #include <iostream> #include <opencv2/opencv.hpp> #include <cuda_runtime.h> #include <string> __global__ void rgbToGray (const uchar3* d_img, unsigned char * d_gray, int width, int height) { int x = blockIdx.x * blockDim.x + threadIdx.x; int y = blockIdx.y * blockDim.y + threadIdx.y; if (x < width && y < height) { int idx = y * width + x; uchar3 pixel = d_img[idx]; d_gray[idx] = static_cast <unsigned char >(0.299f * pixel.x + 0.587f * pixel.y + 0.114f * pixel.z); } } using namespace std;int main () { string imagePath = "input.png" ; cv::Mat img = cv::imread (imagePath); if (img.empty ()) { cerr << "无法加载图像: " << imagePath << endl; return -1 ; } int width = img.cols; int height = img.rows; int channels = img.channels (); uchar* d_img; unsigned char * d_gray; cout << "图像尺寸: " << width << "x" << height << ", 通道数: " << channels << endl; dim3 blockSize (1 , 256 ) ; const int iterations = 10000 ; size_t imgSize = width * height * sizeof (uchar3); size_t graySize = width * height * sizeof (unsigned char ); cudaMalloc (&d_img, imgSize); cudaMalloc (&d_gray, graySize); cudaMemcpy (d_img, img.data, imgSize, cudaMemcpyHostToDevice); dim3 gridSize ((width + blockSize.x - 1 ) / blockSize.x, (height + blockSize.y - 1 ) / blockSize.y) ; rgbToGray<<<gridSize, blockSize>>>(reinterpret_cast <uchar3*>(d_img), d_gray, width, height); cudaGetLastError (); cudaDeviceSynchronize (); cudaEvent_t start, stop; float total_time = 0.0f ; cudaEventCreate (&start); cudaEventCreate (&stop); cout << "正在测试GPU性能..." << endl; for (int i = 0 ; i < iterations; i++) { cudaEventRecord (start); rgbToGray<<<gridSize, blockSize>>>(reinterpret_cast <uchar3*>(d_img), d_gray, width, height); cudaEventRecord (stop); cudaEventSynchronize (stop); float single_time = 0.0f ; cudaEventElapsedTime (&single_time, start, stop); total_time += single_time; } float avg_time = total_time / iterations; cout << "平均每次转换时间: " << avg_time << " ms" << endl; cv::Mat grayImg (height, width, CV_8UC1) ; cudaMemcpy (grayImg.data, d_gray, graySize, cudaMemcpyDeviceToHost); cv::imwrite ("output.png" , grayImg); cudaFree (d_img); cudaFree (d_gray); return 0 ; }
三维坐标计算 示例:图像转化归一化 将一个 256 x 256 x 128 CT图像变成浮点数,除以最大灰度值,得到归一化3D数组
内存布局 将三维数据展平为一维数据
1 2 3 4 5 6 7 8 9 10 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 (x < dimX && y < dimY && z < dimZ) { int global_idx = z * dimY * dimX + y * dimX + x; }
注意:CUDA规定每个Block总线程数不能超过1024,三维块尺寸成绩必须 小于等于1024 核心kernel编写如下:1 2 3 4 5 6 7 8 9 10 11 12 13 __global__ void normalizeVolume (const unsigned short * d_in, float * d_out, int dimX, int dimY, int dimZ, float maxVal) { 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 (x < dimX && y < dimY && z < dimZ) { int idx = z * dimY * dimX + y * dimX + x; d_out[idx] = (float )d_in[idx] / maxVal; } }
启动 Kernel 有了索引,我们还需要告诉GPU启动多少个线程,怎么分组,这需要 <<<gridDim,blockDim>>>
内置变量和类型 CUDA内核函数 __global__ 中可以直接使用一下内置变量,无需声明. 比如之前的treadIdx, blockIdx, blockDim, gridDim 实际上的类型都是 dim3,dim3是CUDA内置的一个结构体,包含xyz三个无符号整数字段。 无论维度为几,核心公式都是:
1 全局线程ID = blockIdx维度 * blockDim维度 + threadIdx维度
如果数据是一维,只需要计算x;如果是二维或三维,则需要对其进行扩展。 因此启动Kerrnel时,需要对其进行配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 int threadsPerBlock = 256 ;int blocksPerGrid = (N + threadsPerBlock - 1 ) / threadsPerBlock;kernel<<<blocksPerGrid, threadsPerBlock>>>(d_data, N); dim3 blockSize (16 , 16 ) ; dim3 gridSize ( (width+15 )/16 , (height+15 )/16 ) ;kernel<<<gridSize, blockSize>>>(d_image, width, height); dim3 blockSize (8 , 8 , 4 ) ; dim3 gridSize ( (dimX+7 )/8 , (dimY+7 )/8 , (dimZ+3 )/4 ) ;kernel<<<gridSize, blockSize>>>(d_volume, dimX, dimY, dimZ);
易错点
没有赋值的维度默认为1
内核中访问threadIdx.z是安全的,但如果启动时没给z值,它始终为0
用整数直接传给 <<<<>>>> 时,相当于只设置.x分量。例如 kernel<<<10,256>>>等价于 gridDim.x=10,blockDim.x=256,其他维为1
参数解释
treadsPerBlock:每个block的线程数,一般取32倍数防止warp资源浪费
blocksPerGrid:向上取整确保覆盖全部数据,公式等价与 $\text{ceil}(N / threadsPerBlock)$ 为什么二维配置一般使用16 x 166或者 32 x 8
合并访问:x 维度的线程连续访问内存,性能最优
warp对齐:32个线程为一个Warp,块尺寸最好时Warp大小倍数
共享内存:二维块切除的tile更规整,便于利用共享内存
一维、二维、三维的选择策略
数据形状
推荐网格/块维度
理由
一维数组
一维
索引计算最简单,开销最小
二维图像/矩阵
二维块+二维网格
直观,便于二维tile划分,合并访问最优
三维体数据
三维块+三维网格
逻辑清晰,利于3D局部性
多层二维数据
二维网格+循环
用二维网格处理每层,代码复用性高
黄金法则:让 x 维度的线程连续访问连续的内存地址, 一个Warp内的32个线程时沿x维度连续的,合并访问时免费的性能提升