__syncthreads()
)实现的。那么有
线程索引:线程在其线程块内的二维索引 ****blockIdx
的x和y。线程索引(threadIdx
)表示一个线程在其所属线程块内的位置。在处理数组或矩阵时,线程索引可以用来计算要处理的元素的位置。
块索引:线程块在网格中的二维索引 blockIdx.x
和 blockIdx.y
。块索引(blockIdx
)表示一个线程块在整个网格(Grid)中的位置。用于确定线程块在整个问题空间中的位置。
线程块维度:blockDim.x
和 blockDim.y
表示线程块的维度。
这样就可以访问所有元素的位置地址,如果需要细节,请查看计算机组成原理课本。
例如,在二维数据处理中,一个线程的全局索引可以通过结合其线程索引和块索引来计算:
int xIndex = blockIdx.x * blockDim.x + threadIdx.x;
int yIndex = blockIdx.y * blockDim.y + threadIdx.y;
这里,blockDim.x
和 blockDim.y
表示线程块在x和y维度上的大小。通过这种方式,我们可以确定每个最小单元(thread)的地址,进行读取操作。
例如,这里给出一个2 * 2 的线程块(Thread Blocks)
。
根据矩阵乘法的最基础定义公式,我们知道:结果中的每个元素的计算不依赖于结果中的其他元素。这就说明矩阵乘法任务可以进行并行。然而,我们总不能提前写好每个元素的计算公式,这样太复杂了。这就引入了SIMD模型,用于简化代码。
首先来看如下代码:
if (i < N && j < N) { // 如果这个索引在矩阵的边界内(即 i < N && j < N)
int index = i + j * N; // 计算它的全局索引 i 和 j
C[index] = A[index] + B[index];//独立地读取 A 和 B 中的元素,计算它们的和,然后将结果写入 C。
}
直观上来看,这就是一个串行编码中的顺序执行循环。但是,如果定义在并行的方法中,这样的串行代码就会被编译器自动转换成M*N条指令。也就是自动翻译成并行的模式。
此时**if
** 语句并不是传统意义上的循环,而是一个并行执行的条件判断。
在更底层的层面,CUDA 运行时会将线程块分配给 GPU 上的流处理器(Streaming Multiprocessors, SMs)。SM内部包含多个CUDA核心,用于实际执行线程的计算。
线程块的调度:这个过程由CUDA运行时自动管理的,如果需要插手优化这环节,需要在核函数设计和块大小分配上间接干预。CUDA运行时会根据SM的数量和每个SM的资源情况(如寄存器、共享内存大小)来决定如何分配线程块。如果一个SM的资源不足以处理更多的线程块,新的线程块会被分配到其他SM。
每个 SM 可以同时执行多个线程,具体数量取决于 GPU 的架构和资源可用性。
矩阵相乘是一个非常典型的例子,用于展示CUDA编程和线程块(Block)及线程(Thread)的使用。
利用tread,做矩阵乘法。
例如,这里给出一个2 * 2 的线程块(Thread Blocks)
。
在这个核函数中,每个线程负责计算结果矩阵C中的一个元素。
__global__ void MatrixMultiply(float *A, float *B, float *C, int N) {
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < N && col < N) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {//遍历所有需要加法的地方 N 次
sum += A[row * N + k] * B[k * N + col]; // 得到一个元素上的结果
}
C[row * N + col] = sum;
}
}
主函数中调用上述核函数的方式如下:
int N = 1024; // 假设矩阵大小为1024x1024
dim3 threadsPerBlock(16, 16);
dim3 numBlocks(N / threadsPerBlock.x, N / threadsPerBlock.y);
float *d_A, *d_B, *d_C;
// ... 在这里为 d_A, d_B 和 d_C 分配设备内存,并初始化数据 ...
MatrixMultiply<<<numBlocks, threadsPerBlock>>>(d_A, d_B, d_C, N);
// ... 处理完成后,从设备内存拷贝数据回主机内存,清理资源 ...
刚才的代码中,可以观察到两个for循环,这里可以进行优化。
“coalescing writes”(合并写操作)是一种优化内存访问模式的技术,它能显著提高内存带宽的利用效率。这种技术尤其对于全局内存访问非常重要,因为全局内存访问速度相比于核心计算速度要慢得多。
__global__ void MatrixMultiplyCoalesced(float *A, float *B, float *C, int N) {
// 计算行和列索引
int row = blockIdx.y * blockDim.y + threadIdx.y;
int col = blockIdx.x * blockDim.x + threadIdx.x;
if (row < N && col < N) {
float sum = 0.0f;
for (int k = 0; k < N; k++) {
// 累加计算矩阵C中(row, col)位置的值
sum += A[row * N + k] * B[k * N + col];
}
// 写入计算结果到矩阵C中,利用合并写操作优化
// 每个线程按照顺序写入连续的内存地址
C[row * N + col] = sum;
}
}
优化点:
C[row * N + col]
)。这样,当多个线程同时写入时,由于它们访问的是连续的内存地址,这些写操作可以被合并成较少的内存事务。这种访问模式对于全局内存来说是高效的。row
和col
的计算方式),我们确保了线程以线性和有序的方式访问全局内存,这对于实现高效的合并写操作至关重要。