__global__ voidmatrix_multiplication_kernel(constfloat *A, constfloat *B, float *C, int M, int N, int K){ int col = blockDim.x * blockIdx.x + threadIdx.x; int row = blockDim.y * blockIdx.y + threadIdx.y; if (col >= K || row >= M) { return; }
// A(N, 1), B(K, 1), C(K, 1) float sum = 0.0f; for (int n = 0; n < N; ++n) { sum += A[row * N + n] * B[n * K + col]; } C[row * K + col] = sum; }
// A, B, C are device pointers (i.e. pointers to memory on the GPU) extern"C"voidsolve(constfloat *A, constfloat *B, float *C, int M, int N, int K){ dim3 threadsPerBlock(16, 16); dim3 blocksPerGrid((K + threadsPerBlock.x - 1) / threadsPerBlock.x, (M + threadsPerBlock.y - 1) / threadsPerBlock.y);
matrix_multiplication_kernel<<<blocksPerGrid, threadsPerBlock>>>(A, B, C, M, N, K); cudaDeviceSynchronize(); }
循环的每次迭代执行一次乘法和一次加法,忽略循环控制开销时,每个输出元素约需要 2N FLOPs。若从单线程视角估算,并假设每次访问都需要从 Global Memory 读取、不考虑缓存和线程之间的复用,则需要读取 A、B 各 N 个 float,共 8N Bytes,对应的算术强度为
8N2N=0.25FLOPs/Byte
这一数值是朴素访存模型下的上层估算,并不表示硬件一定产生 2N 次彼此独立的内存事务。在同一次循环迭代中,具有相同 row 的相邻线程读取同一个 A[row * N + n],可由缓存或广播机制复用;相邻 col 的线程读取连续的 B[n * K + col],能够形成合并访问,最终写入 C 时地址也连续。不过这些数据没有被程序显式保存在 Block 内供后续线程复用,矩阵较大、缓存不能容纳工作集时,同一元素仍可能被不同线程或 Block 反复从更低层存储读取,因此性能通常受内存流量限制。
__global__ voidmatrix_multiplication_kernel(constfloat *A, constfloat *B, float *C, int M, int N, int K){ int row = blockDim.y * blockIdx.y + threadIdx.y; int col = blockDim.x * blockIdx.x + threadIdx.x;
float acc = 0.0f; for (int tile = 0; tile < (N + TILE_SIZE - 1) / TILE_SIZE; ++tile) { int Acol = tile * TILE_SIZE + threadIdx.x; if (row < M && Acol < N) { As[threadIdx.y][threadIdx.x] = A[row * N + Acol]; } else { As[threadIdx.y][threadIdx.x] = 0.0f; } int Brow = tile * TILE_SIZE + threadIdx.y; if (Brow < N && col < K) { Bs[threadIdx.y][threadIdx.x] = B[Brow * K + col]; } else { Bs[threadIdx.y][threadIdx.x] = 0.0f; }
__syncthreads();
for (int n = 0; n < TILE_SIZE; ++n) { acc += As[threadIdx.y][n] * Bs[n][threadIdx.x]; } __syncthreads(); } if (row < M && col < K) { C[row * K + col] = acc; } }
// A, B, C are device pointers (i.e. pointers to memory on the GPU) extern"C"voidsolve(constfloat *A, constfloat *B, float *C, int M, int N, int K){ dim3 threadsPerBlock(TILE_SIZE, TILE_SIZE); dim3 blocksPerGrid((K + threadsPerBlock.x - 1) / threadsPerBlock.x, (M + threadsPerBlock.y - 1) / threadsPerBlock.y);
matrix_multiplication_kernel<<<blocksPerGrid, threadsPerBlock>>>(A, B, C, M, N, K); cudaDeviceSynchronize(); }
Tiling 版本把归约维度 N 划分为长度为 TILE_SIZE 的小块。一个 16×16 线程 Block 对应 C 的一个 16×16 输出子块,每个线程仍然只计算一个输出元素,但 Block 内的线程会协作完成当前 Tile 的数据加载:线程 (threadIdx.y, threadIdx.x) 分别从 A 和 B 读取一个元素,写入 As 和 Bs。加载地址沿 threadIdx.x 连续,有利于形成合并的 Global Memory 访问。
当矩阵尺寸不是 TILE_SIZE 的整数倍时,最后一个 Tile 或边缘 Block 会落在矩阵范围之外。代码将越界的 A、B 元素填为 0,使它们参与乘加时不改变结果。这里不能像朴素版本一样让越界线程提前返回,因为 Block 中的所有线程都必须继续参与共享内存加载和 __syncthreads();最终只有满足 row < M && col < K 的线程才写回 C。
讨论
评论