音乐
暂未播放
LeetGPU | 02_Matrix_Multiplication
关注 LeetGPU repository,了解更多内容。
Problem Description#
Write a program that multiplies two matrices of 32-bit floating point numbers on a GPU. Given matrix A of dimensions M×N and matrix B of dimensions N×K, compute the product matrix C=A×B, which will have dimensions M×K. All matrices are stored in row-major format.
Implementation Requirements#
- Use only native features (external libraries are not permitted).
- The
solvefunction signature must remain unchanged. - The final result must be stored in matrix
C.
Example 1#
Input:
Matrix A (2×2):
[1.03.02.04.0]Matrix B (2×2):
[5.07.06.08.0]Output:
Matrix C (2×2):
[19.043.022.050.0]Example 2#
Input:
Matrix A (1×3):
[1.02.03.0]Matrix B (3×1):
4.05.06.0Output:
Matrix C (1×1):
[32.0]Constraints#
- 1 <=
M,N,K<= 8192. - Performance is measured with
M= 8192,N= 6144,K= 4096.
CUDA#
Approach#
本题需要计算 A[M, N] 与 B[N, K] 的乘积,并将结果写入 C[M, K]。每个输出元素都是 A 的一行与 B 的一列所形成的内积(inner product):
由于三个矩阵均采用行主序(row-major)存储,它们的二维坐标可分别转换为以下线性地址:
1A[row, inner] -> A[row * N + inner]2B[inner, col] -> B[inner * K + col]3C[row, col] -> C[row * K + col]不同 (row, col) 坐标对应的输出彼此独立,因此 CUDA 可以并行处理输出矩阵的两个维度;沿 N 维度进行的归约(reduction),则仍由单个线程(thread)或该线程负责的输出分块(tile)完成。
传统 CPU 实现通常使用三重嵌套循环:外侧两个循环遍历 M×K 个输出位置,最内层循环完成长度为 N 的归约,整体算术复杂度为 O(MNK)。GPU 优化并不会减少这些必要的算术运算,而是重新安排循环的执行位置、同时计算的输出数量,以及数据从各级内存中被读取的频率。

一个输出元素由 A 的一行和 B 的一列计算得到。
这些 CUDA 文件展示了一条逐步演进的优化路径。所有版本都保持相同的 solve(...) 接口和数学结果,主要区别在于每个 thread 负责多少输出,以及数据如何在全局内存(global memory)、共享内存(shared memory)与寄存器(register)之间流动。
整个优化过程围绕两个相互关联的目标展开:
- 提高复用率。 从 global memory 读取的数据,应在被替换前尽可能参与多个输出元素的计算。共享内存分块(shared-memory tiling)可以在同一个 block 内复用数据,线程分块(thread tiling)则能进一步在 register 中复用数据。
- 让数据移动与计算重叠,或提高二者的执行效率。 向量化传输可以减少 load/store 指令开销;软件流水线和硬件流水线可以让相邻 reduction tile 的数据移动与计算交叠;如果能够接受较低的 TF32 输入精度,还可以使用 Tensor Core 取代标量 FP32 乘加指令。
| Version | Main change | Work owned by one thread or warp |
|---|---|---|
| V0 | Direct global-memory dot products | One output element per thread |
| V1 shared | Cooperative shared-memory tiling | One output element per thread |
| V1 1D | Register reuse along the row dimension | 16 x 1 outputs per thread |
| V1 2D | Register-level outer products | 8 x 4 outputs per thread |
| V2 | float4 cooperative transfers | 8 x 4 outputs per thread |
| V3 | Register prefetch plus two shared-memory buffers | 8 x 4 outputs per thread |
| V4 | Hardware-asynchronous cp.async transfers | 8 x 4 outputs per thread |
| V5 | WMMA TF32 Tensor Core operations | One 16 x 16 output tile per warp |
V0: Naive Global-Memory Kernel#
Approach#
V0 启动一个二维网格(grid),其中每个线程块(block)包含 16 x 16 个 thread。每个 thread 按如下方式确定自己负责的输出坐标:
1row = blockIdx.y * blockDim.y + threadIdx.y2col = blockIdx.x * blockDim.x + threadIdx.xgrid 在两个输出维度上都使用向上取整除法计算大小。每个 thread 首先检查 row < M && col < K;只有坐标有效的 thread 才会计算完整的点积,并将结果写入 C 中对应的一个元素。
这种映射用二维 CUDA grid 取代了 CPU 实现最外侧的两个循环,但每个 thread 仍需串行完成整个内部归约。该实现结构直接,因而也很容易验证正确性。
V0 的主要弱点在于数据移动。分别计算 C[row, col0] 和 C[row, col1] 的两个 thread 都会遍历 A 的同一行,却各自重新加载这行数据;同样,计算同一输出列中不同行元素的 thread,也会反复读取 B 同一列中的值。硬件缓存(cache)或许能够捕获一部分复用机会,但该 kernel 并未显式地把这些数据保留在片上。因此,V0 虽然暴露了充足的输出并行性,却没有改善底层点积计算的数据局部性。
Solution#
1#include <cuda_runtime.h>2
3__global__ void matrix_multiplication_v0(const float* A, const float* B, float* C,4 int M, int N, int K) {5 const int row = blockIdx.y * blockDim.y + threadIdx.y;6 const int col = blockIdx.x * blockDim.x + threadIdx.x;7 if (row >= M || col >= K) return;8
9 float sum = 0.0f;10 for (int inner = 0; inner < N; ++inner) {11 sum += A[row * N + inner] * B[inner * K + col];12 }13 C[row * K + col] = sum;14}15
16extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {17 const dim3 block(16, 16);18 const dim3 grid((K + block.x - 1) / block.x, (M + block.y - 1) / block.y);19 matrix_multiplication_v0<<<grid, block>>>(A, B, C, M, N, K);20}V1: Original 16 x 16 Shared-Memory Tiling#
Approach#
这个原始版本仍让每个 thread 负责一个输出元素,但把归约维度划分为长度为 16 的 tile。每个 16 x 16 block 负责生成 C 中的一个 16 x 16 tile,从而形成一条明确的数据流动路径:
1global memory -> shared memory -> thread registers -> Cglobal memory 保存完整矩阵,shared memory 充当 block 内部的暂存区,用于保存当前参与计算的一对输入 tile;每个 thread 则在 register 中维护自己的标量累加器。
处理 reduction tile tile 时,thread (ty, tx) 负责加载:
1A[row, tile * 16 + tx] -> A_shared[ty][tx]2B[tile * 16 + ty, col] -> B_shared[ty][tx]全部 256 个 thread 完成加载后,每个 A 值都可由负责不同输出列的 16 个 thread 共享,每个 B 值也可由负责不同输出行的 16 个 thread 共享。随后,每个 thread 执行 16 次乘加运算:
1S += A_shared[ty][inner] * B_shared[inner][tx]这样一来,每个从 global memory 读取一次的输入值,在同一个 block 内最多可以参与 16 次乘加运算。对于一个完整的 reduction tile,block 分别从两个输入矩阵加载 16×16 个值,随后利用这些数据完成 16×16×16 次乘加运算。其算术计算与 V0 完全相同,但大量重复的 global-memory 流量被延迟更低的 shared-memory 访问取代。
每个 reduction tile 都需要两次同步屏障(barrier)。第一次 barrier 保证协作式加载(cooperative load)全部完成后才开始计算;第二次 barrier 则保证所有 thread 都用完当前 tile 后,下一个 tile 才能覆盖 shared memory 中的现有数据。
最后一个 reduction tile 可能不足 16 个元素。遇到无效的输入坐标时,程序不会提前返回,而是向 shared memory 写入零。零填充(zero padding)不会改变点积结果,同时能让所有 thread 都到达相同的 barrier,避免在同步位置发生分支发散。最终写回输出时,再通过 row < M && col < K 单独进行边界保护。
Solution#
1#include <cuda_runtime.h>2
3#define BLOCK_SIZE 164__global__ void matrix_multiplication_kernel(const float* A, const float* B, float* C, int M, int N,5 int K) {6 __shared__ float A_shared[BLOCK_SIZE][BLOCK_SIZE];7 __shared__ float B_shared[BLOCK_SIZE][BLOCK_SIZE];8 int row = threadIdx.y + blockIdx.y * blockDim.y;9 int col = threadIdx.x + blockIdx.x * blockDim.x;10 int tx = threadIdx.x;11 int ty = threadIdx.y;12 float S = 0.0f;13 for (int i = 0 ; i < (N + BLOCK_SIZE - 1) / BLOCK_SIZE ; i++){14 int col2 = tx + i * BLOCK_SIZE;15 int row2 = ty + i * BLOCK_SIZE;16 A_shared[ty][tx] = (row < M && col2 < N) ? A[row * N + col2] : 0.0f;17 B_shared[ty][tx] = (row2 < N && col < K) ? B[row2 * K + col] : 0.0f;18 __syncthreads();19 for (int j = 0 ; j < BLOCK_SIZE ; j++){20 S += A_shared[ty][j] * B_shared[j][tx];21 }22 __syncthreads();23 }24 if(row < M && col < K){25 C[row * K + col] = S ;26 }27}28
29extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {30 dim3 threadsPerBlock(16, 16);31 dim3 blocksPerGrid((K + threadsPerBlock.x - 1) / threadsPerBlock.x,32 (M + threadsPerBlock.y - 1) / threadsPerBlock.y);33
34 matrix_multiplication_kernel<<<blocksPerGrid, threadsPerBlock>>>(A, B, C, M, N, K);35}V1: One-Dimensional Thread Tiling#
Approach#
一维线程分块(1D Thread Tiling)版本使用以下参数:
1BM = 642BN = 643BK = 164TM = 16每个 block 生成一个 64 x 64 输出 tile。block 的维度为 (64, 4),共包含 256 个 thread;每个 thread 负责同一输出列中的 16 行:
1row = blockIdx.y * 64 + ty * 16 + local_row2col = blockIdx.x * 64 + tx对应的 16 个部分和保存在 float sum[16] 中。在一次归约步骤(reduction step)内,thread 从 shared memory 读取一个 B 值,并在全部 16 个累加器之间复用它:
在每个 thread 只负责一个输出的 kernel 中,每到一个归约位置(reduction position),thread 都要从 shared memory 各读取一个 A 值和一个 B 值,才能执行一次乘加运算。这里,同一个保存 B 值的 register 可以连续支持 16 次乘加。虽然额外的累加器会消耗更多 register,但它们增加了每次 shared-memory 访问所对应的算术工作量,也让 block 级同步的成本分摊到每个 thread 的更多计算上。
block 仍以协作方式填充 SA[64][16] 和 SB[16][64]。将 thread index 展平后,输入加载便不再依赖 thread 与输出位置之间的归属映射。协作式加载和最终写回都会进行边界检查,因此位于矩阵边缘的不完整 tile 也能得到正确结果。
Solution#
1#include <cuda_runtime.h>2
3template <int BM = 64, int BN = 64, int BK = 16, int TM = 16>4__global__ void matrix_multiplication_v1_1d(const float* __restrict__ A,5 const float* __restrict__ B,6 float* __restrict__ C,7 int M, int N, int K) {8 constexpr int NT = (BM / TM) * BN;9 __shared__ float SA[BM][BK];10 __shared__ float SB[BK][BN];11 const int tx = threadIdx.x;12 const int ty = threadIdx.y;13 const int tid = ty * blockDim.x + tx;14 const int col = blockIdx.x * BN + tx;15 float sum[TM] = {0.0f};16
17 for (int tile = 0; tile < N; tile += BK) {18 #pragma unroll19 for (int i = tid; i < BM * BK; i += NT) {20 const int local_row = i / BK;21 const int local_col = i % BK;22 const int row = blockIdx.y * BM + local_row;23 const int inner = tile + local_col;24 SA[local_row][local_col] =25 (row < M && inner < N) ? A[row * N + inner] : 0.0f;26 }27 #pragma unroll28 for (int i = tid; i < BK * BN; i += NT) {29 const int local_row = i / BN;30 const int local_col = i % BN;31 const int inner = tile + local_row;32 const int output_col = blockIdx.x * BN + local_col;33 SB[local_row][local_col] =34 (inner < N && output_col < K) ? B[inner * K + output_col] : 0.0f;35 }36 __syncthreads();37
38 #pragma unroll39 for (int inner = 0; inner < BK; ++inner) {40 const float b = SB[inner][tx];41 #pragma unroll42 for (int row = 0; row < TM; ++row) {43 sum[row] += SA[ty * TM + row][inner] * b;44 }45 }46 __syncthreads();47 }48
49 #pragma unroll50 for (int local_row = 0; local_row < TM; ++local_row) {51 const int row = blockIdx.y * BM + ty * TM + local_row;52 if (row < M && col < K) C[row * K + col] = sum[local_row];53 }54}55
56extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {57 constexpr int BM = 64, BN = 64, BK = 16, TM = 16;58 const dim3 block(BN, BM / TM);59 const dim3 grid((K + BN - 1) / BN, (M + BM - 1) / BM);60 matrix_multiplication_v1_1d<BM, BN, BK, TM><<<grid, block>>>(A, B, C, M, N, K);61}V1: Two-Dimensional Thread Tiling#
Approach#
二维线程分块(2D Thread Tiling)版本使用以下参数:
1BM = 642BN = 643BK = 164TM = 85TN = 4block 的维度为 (16, 8),即总共 128 个 thread。每个 thread 负责一个 8 x 4 微分块(micro-tile),其中包含 32 个输出元素:
1row = blockIdx.y * 64 + ty * 8 + local_row2col = blockIdx.x * 64 + tx * 4 + local_col在一个 reduction position 上,thread 会把 8 个 A 值和 4 个 B 值从 shared memory 读入 register,并通过二者的外积(outer product)更新全部 32 个累加器:
这提供了观察矩阵乘法的另一种视角:单个标量输出可以自然地描述为内积,而整个输出 tile 则可以看作沿 reduction 维度不断累加一系列秩一外积。在某个 reduction position 上,由 8 个元素组成的 A 列片段与由 4 个元素组成的 B 行片段,会共同更新该 thread 负责的整个 8 x 4 输出 tile。

每个 reduction position 都会为输出 tile 贡献一个秩一外积。
每个保存 A 值的 register 都会参与 4 个输出列的计算,每个保存 B 值的 register 则会参与 8 个输出行的计算。因此,thread 只需从 shared memory 读取 12 个标量,就可以执行 32 次乘加运算。
这正是二维 thread tiling 的核心优势:必要的数学工作量没有改变,但两个操作数都能先在 register 中得到充分复用,之后才需要再次访问 shared memory。
Solution#
1#include <cuda_runtime.h>2
3template <int BM = 64, int BN = 64, int BK = 16, int TM = 8, int TN = 4>4__global__ void matrix_multiplication_v1_2d(const float* __restrict__ A,5 const float* __restrict__ B,6 float* __restrict__ C,7 int M, int N, int K) {8 constexpr int NT = (BM / TM) * (BN / TN);9 __shared__ float SA[BM][BK];10 __shared__ float SB[BK][BN];11 const int tx = threadIdx.x;12 const int ty = threadIdx.y;13 const int tid = ty * blockDim.x + tx;14 float sum[TM][TN] = {{0.0f}};15
16 for (int tile = 0; tile < (N + BK - 1) / BK; ++tile) {17 const int base_inner = tile * BK;18 #pragma unroll19 for (int i = tid; i < BM * BK; i += NT) {20 const int local_row = i / BK;21 const int local_col = i % BK;22 const int row = blockIdx.y * BM + local_row;23 const int inner = base_inner + local_col;24 SA[local_row][local_col] =25 (row < M && inner < N) ? A[row * N + inner] : 0.0f;26 }27 #pragma unroll28 for (int i = tid; i < BK * BN; i += NT) {29 const int local_row = i / BN;30 const int local_col = i % BN;31 const int inner = base_inner + local_row;32 const int col = blockIdx.x * BN + local_col;33 SB[local_row][local_col] =34 (inner < N && col < K) ? B[inner * K + col] : 0.0f;35 }36 __syncthreads();37
38 #pragma unroll39 for (int inner = 0; inner < BK; ++inner) {40 float a_reg[TM], b_reg[TN];41 #pragma unroll42 for (int row = 0; row < TM; ++row) a_reg[row] = SA[ty * TM + row][inner];43 #pragma unroll44 for (int col = 0; col < TN; ++col) b_reg[col] = SB[inner][tx * TN + col];45 #pragma unroll46 for (int row = 0; row < TM; ++row)47 #pragma unroll48 for (int col = 0; col < TN; ++col)49 sum[row][col] += a_reg[row] * b_reg[col];50 }51 __syncthreads();52 }53
54 #pragma unroll55 for (int local_row = 0; local_row < TM; ++local_row) {56 const int row = blockIdx.y * BM + ty * TM + local_row;57 if (row >= M) continue;58 #pragma unroll59 for (int local_col = 0; local_col < TN; ++local_col) {60 const int col = blockIdx.x * BN + tx * TN + local_col;61 if (col < K) C[row * K + col] = sum[local_row][local_col];62 }63 }64}65
66extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {67 constexpr int BM = 64, BN = 64, BK = 16, TM = 8, TN = 4;68 const dim3 block(BN / TN, BM / TM);69 const dim3 grid((K + BN - 1) / BN, (M + BM - 1) / BM);70 matrix_multiplication_v1_2d<BM, BN, BK, TM, TN><<<grid, block>>>(A, B, C, M, N, K);71}V2: Vectorized float4 Transfers (BK = 32)#
Approach#
V2 保留了 8 x 4 register micro-tile,并把 reduction tile 的深度改为 BK = 32。一个 float4 包含 4 个连续的 FP32 值,共占 16 字节。kernel 在以下数据传输位置使用它,一次处理 4 个相邻值:
- 从 global memory 加载
A和B; - 将数据写入 shared memory;
- 从 shared memory 读取相邻的
B值; - 最终写入
C的相邻列。
只有当访问地址满足对齐要求,并且对应的 4 个元素全部有效时,向量加载才是合法的。因此,仅当 N 和 K 都能被 4 整除时,程序才会选择向量化 kernel;任一条件不满足,solve(...) 都会启动能够安全处理边界的标量实现。通用的 LeetGPU 接口必须保留这一回退路径(fallback),不能为了向量化而牺牲不规则矩阵形状(shape)的正确性。
在向量化路径中,最后一个 block 的输出行仍需单独进行边界保护。由于此时 K 是 4 的倍数,一个由 4 列组成的分组要么完整落在矩阵范围内,要么完整位于矩阵范围外,不会只剩部分列有效。
行主序布局让同一行中的连续列在内存中相邻,因此适合沿 A 的归约坐标、B 的输出列坐标以及 C 的输出列坐标进行向量化。在这些传输位置,一条向量指令可以替代 4 条标量指令。实际传输的总字节数,以及矩阵乘法所需的 2MNK 次浮点运算都没有改变;向量化主要减少的是指令数量和地址生成开销。因此,实际收益取决于地址对齐、指令发射效率,以及数据移动与算术计算之间的平衡,并不会仅仅因为使用了更宽的数据类型就自动出现。
Solution#
1#include <cuda_runtime.h>2
3#ifndef MATMUL_V2_BM4#define MATMUL_V2_BM 645#endif6#ifndef MATMUL_V2_BN7#define MATMUL_V2_BN 648#endif9#ifndef MATMUL_V2_BK10#define MATMUL_V2_BK 3211#endif12#ifndef MATMUL_V2_TM13#define MATMUL_V2_TM 814#endif15#ifndef MATMUL_V2_TN16#define MATMUL_V2_TN 417#endif18
19__global__ void scalar_fallback(const float* A, const float* B, float* C,20 int M, int N, int K) {21 const int row = blockIdx.y * blockDim.y + threadIdx.y;22 const int col = blockIdx.x * blockDim.x + threadIdx.x;23 if (row >= M || col >= K) return;24 float sum = 0.0f;25 for (int inner = 0; inner < N; ++inner) {26 sum += A[row * N + inner] * B[inner * K + col];27 }28 C[row * K + col] = sum;29}30
31template <int BM = MATMUL_V2_BM, int BN = MATMUL_V2_BN,32 int BK = MATMUL_V2_BK, int TM = MATMUL_V2_TM,33 int TN = MATMUL_V2_TN>34__global__ void matrix_multiplication_v2(const float* __restrict__ A,35 const float* __restrict__ B,36 float* __restrict__ C,37 int M, int N, int K) {38 static_assert(BK % 4 == 0 && BN % 4 == 0 && TN % 4 == 0,39 "Vectorized dimensions must be multiples of four");40 constexpr int NT = (BM / TM) * (BN / TN);41 __shared__ float SA[BM][BK];42 __shared__ float SB[BK][BN];43 const int tx = threadIdx.x;44 const int ty = threadIdx.y;45 const int tid = ty * blockDim.x + tx;46 float sum[TM][TN] = {{0.0f}};47
48 for (int tile = 0; tile < (N + BK - 1) / BK; ++tile) {49 const int base_inner = tile * BK;50 #pragma unroll51 for (int i = tid; i < BM * BK / 4; i += NT) {52 const int local_row = i / (BK / 4);53 const int local_col = (i % (BK / 4)) * 4;54 const int row = blockIdx.y * BM + local_row;55 const int inner = base_inner + local_col;56 float4 value = make_float4(0, 0, 0, 0);57 if (row < M && inner + 3 < N) {58 value = *reinterpret_cast<const float4*>(&A[row * N + inner]);59 }60 *reinterpret_cast<float4*>(&SA[local_row][local_col]) = value;61 }62 #pragma unroll63 for (int i = tid; i < BK * BN / 4; i += NT) {64 const int local_row = i / (BN / 4);65 const int local_col = (i % (BN / 4)) * 4;66 const int inner = base_inner + local_row;67 const int col = blockIdx.x * BN + local_col;68 float4 value = make_float4(0, 0, 0, 0);69 if (inner < N && col + 3 < K) {70 value = *reinterpret_cast<const float4*>(&B[inner * K + col]);71 }72 *reinterpret_cast<float4*>(&SB[local_row][local_col]) = value;73 }74 __syncthreads();75
76 #pragma unroll77 for (int inner = 0; inner < BK; ++inner) {78 const float4 b = *reinterpret_cast<const float4*>(&SB[inner][tx * TN]);79 #pragma unroll80 for (int row = 0; row < TM; ++row) {81 const float a = SA[ty * TM + row][inner];82 sum[row][0] += a * b.x;83 sum[row][1] += a * b.y;84 sum[row][2] += a * b.z;85 sum[row][3] += a * b.w;86 }87 }88 __syncthreads();89 }90
91 #pragma unroll92 for (int local_row = 0; local_row < TM; ++local_row) {93 const int row = blockIdx.y * BM + ty * TM + local_row;94 const int col = blockIdx.x * BN + tx * TN;95 if (row < M && col + 3 < K) {96 *reinterpret_cast<float4*>(&C[row * K + col]) =97 make_float4(sum[local_row][0], sum[local_row][1],98 sum[local_row][2], sum[local_row][3]);99 }100 }101}102
103extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {104 if ((N & 3) != 0 || (K & 3) != 0) {105 const dim3 block(16, 16);106 const dim3 grid((K + 15) / 16, (M + 15) / 16);107 scalar_fallback<<<grid, block>>>(A, B, C, M, N, K);108 } else {109 constexpr int BM = MATMUL_V2_BM, BN = MATMUL_V2_BN;110 constexpr int BK = MATMUL_V2_BK, TM = MATMUL_V2_TM;111 constexpr int TN = MATMUL_V2_TN;112 const dim3 block(BN / TN, BM / TM);113 const dim3 grid((K + BN - 1) / BN, (M + BM - 1) / BM);114 matrix_multiplication_v2<BM, BN, BK, TM, TN>115 <<<grid, block>>>(A, B, C, M, N, K);116 }117}V2 Control: Vectorized Single Buffer (BK = 16)#
Approach#
这个对照版本仍编译同一个单缓冲(single-buffered)V2 kernel,但将 BK = 16,以匹配 V3 的归约分块深度(reduction-tile depth)。V2 的其余设计均保持不变,包括每个 thread 负责的输出、向量化传输、register micro-tile、边界 fallback 和启动布局(launch geometry)。
这一参数匹配的配置,可以把 reduction-tile depth 的影响与双缓冲(double buffering)的影响区分开来。在两款受测 GPU 上,single-buffered BK = 16 对照版本仍然快于 V3。因此,不能仅凭 V2 原本使用 BK = 32、而 V3 使用 BK = 16 来解释 V3 较低的实测性能;在当前实现中,额外的预取 register、两套 shared-memory buffer、buffer 切换与同步操作,同样没有带来足以抵消自身成本的收益。
Solution#
1#define MATMUL_V2_BK 162
3#include "02_Matrix_Multiplication_v2_vectorized.cu"V2 Control: Vectorized Single Buffer (128 x 128)#
Approach#
这个对照版本保留 V2 的 single-buffered 算法,只把编译期 tile 参数改为 BM = 128、BN = 128 和 BK = 16。每个 thread 负责的 8 x 4 micro-tile、float4 传输、标量边界 fallback 以及累加逻辑均保持不变。
block 的规模从 128 个 thread 增加到 512 个 thread,生成的输出元素数量也变为原来的 4 倍;然而,更大的 tile 并未提升这个 single-buffered kernel 的性能。在两款受测 GPU 上,它都慢于参数匹配的 64 x 64、BK = 16 对照版本。这说明额外的 block 级数据复用本身还不足以抵消更大 block 带来的调度成本和资源开销。
Solution#
1#define MATMUL_V2_BM 1282#define MATMUL_V2_BN 1283#define MATMUL_V2_BK 164#define MATMUL_V2_TM 85#define MATMUL_V2_TN 46
7#include "02_Matrix_Multiplication_v2_vectorized.cu"V3: Register Prefetch and Double-Buffered Shared Memory#
Base 64 x 64 Configuration#
Approach#
V3 使用以下参数:
1BM = 642BN = 643BK = 164TM = 85TN = 4它为每个 shared-memory tile 分配两个副本:
1SA[2][64][16]2SB[2][16][64]第一个 tile 在主循环之前加载。这一阶段称为流水线序言(pipeline prologue):只有一对完整的输入 tile 已进入 shared memory 后,计算才能开始。
对于后续的每个 tile,thread 首先发出 global load,将数据加载到私有的预取 register。随后,它基于当前 shared-memory buffer 进行计算,等待所有 thread 都用完该 buffer,再将预取值写入另一个 buffer,最后在交换读写 buffer index 之前完成同步。这一阶段称为稳态(steady state),其中 reduction tile i 的计算与 tile i+1 的数据移动交错进行。
最后一个预取 tile 成为当前 tile 后,kernel 会完成其计算,但不再请求下一个 tile。这一阶段称为流水线尾声(pipeline epilogue)。
预期的流水线如下:
1tile i + 1: global memory -> prefetch registers2tile i: shared memory -> accumulator registers3tile i + 1: prefetch registers -> alternate shared-memory buffer这是带 register prefetch 的软件双缓冲(software double buffering),而不是异步 cp.async 流水线。global load 会在当前 tile 被使用之前发出,从而使编译器和 GPU 有机会将尚未完成的内存操作与独立的算术计算重叠执行。不过,当 shared buffer 切换角色时,该实现仍需要 block 范围的 barrier。
实际源码使用直接的行主序 tile 布局保存 SA[buffer][row][inner] 和 SB[buffer][inner][col],并未暗中加入转置 shared-memory 布局或其他未实现的访问变换。这一点很重要,因为评价一条流水线时,不能脱离其实际加载模式、register footprint、shared-memory layout 和同步调度。
double buffering 并不会自动提升速度。它使 shared-memory 存储量翻倍,并增加预取 register、控制流、pipeline prologue 和 epilogue。这些开销可能降低 occupancy,或抵消隐藏延迟带来的收益。在实测工作负载中,基础 64 x 64 V3 在两款受测 GPU 上都略慢于参数匹配的 single-buffer V2 对照版本。后面的 128 x 128 V3 配置更快,是因为更大的输出 tile 同时改变了复用和调度;保留各个独立对照版本,可以避免将该结果错误地完全归因于 double buffering。
Solution#
1#include <cuda_runtime.h>2
3#ifndef MATMUL_BM4#define MATMUL_BM 645#define MATMUL_BN 646#define MATMUL_BK 167#define MATMUL_TM 88#define MATMUL_TN 49#endif10
11__global__ void scalar_fallback_v3(const float* A, const float* B, float* C,12 int M, int N, int K) {13 const int row = blockIdx.y * blockDim.y + threadIdx.y;14 const int col = blockIdx.x * blockDim.x + threadIdx.x;15 if (row >= M || col >= K) return;16 float sum = 0.0f;17 for (int inner = 0; inner < N; ++inner) {18 sum += A[row * N + inner] * B[inner * K + col];19 }20 C[row * K + col] = sum;21}22
23template <int BM = 64, int BN = 64, int BK = 16, int TM = 8, int TN = 4>24__global__ void matrix_multiplication_v3(const float* __restrict__ A,25 const float* __restrict__ B,26 float* __restrict__ C,27 int M, int N, int K) {28 constexpr int NT = (BM / TM) * (BN / TN);29 constexpr int A_FLOAT4_PER_THREAD = (BM * BK / 4 + NT - 1) / NT;30 constexpr int B_FLOAT4_PER_THREAD = (BK * BN / 4 + NT - 1) / NT;31 __shared__ float SA[2][BM][BK];32 __shared__ float SB[2][BK][BN];33
34 const int tx = threadIdx.x;35 const int ty = threadIdx.y;36 const int tid = ty * blockDim.x + tx;37 float a_prefetch[A_FLOAT4_PER_THREAD][4];38 float b_prefetch[B_FLOAT4_PER_THREAD][4];39 float sum[TM][TN] = {{0.0f}};40
41 auto load_tile = [&](int tile) {42 const int base_inner = tile * BK;43 #pragma unroll44 for (int i = 0; i < A_FLOAT4_PER_THREAD; ++i) {45 const int index = (i * NT + tid) * 4;46 float4 value = make_float4(0, 0, 0, 0);47 if (index < BM * BK) {48 const int local_row = index / BK;49 const int local_col = index % BK;50 const int row = blockIdx.y * BM + local_row;51 const int inner = base_inner + local_col;52 if (row < M && inner + 3 < N) {53 value = *reinterpret_cast<const float4*>(&A[row * N + inner]);54 }55 }56 a_prefetch[i][0] = value.x; a_prefetch[i][1] = value.y;57 a_prefetch[i][2] = value.z; a_prefetch[i][3] = value.w;58 }59 #pragma unroll60 for (int i = 0; i < B_FLOAT4_PER_THREAD; ++i) {61 const int index = (i * NT + tid) * 4;62 float4 value = make_float4(0, 0, 0, 0);63 if (index < BK * BN) {64 const int local_row = index / BN;65 const int local_col = index % BN;66 const int inner = base_inner + local_row;67 const int col = blockIdx.x * BN + local_col;68 if (inner < N && col + 3 < K) {69 value = *reinterpret_cast<const float4*>(&B[inner * K + col]);70 }71 }72 b_prefetch[i][0] = value.x; b_prefetch[i][1] = value.y;73 b_prefetch[i][2] = value.z; b_prefetch[i][3] = value.w;74 }75 };76
77 auto store_tile = [&](int buffer) {78 #pragma unroll79 for (int i = 0; i < A_FLOAT4_PER_THREAD; ++i) {80 const int index = (i * NT + tid) * 4;81 if (index < BM * BK) {82 const int local_row = index / BK;83 const int local_col = index % BK;84 *reinterpret_cast<float4*>(&SA[buffer][local_row][local_col]) =85 make_float4(a_prefetch[i][0], a_prefetch[i][1],86 a_prefetch[i][2], a_prefetch[i][3]);87 }88 }89 #pragma unroll90 for (int i = 0; i < B_FLOAT4_PER_THREAD; ++i) {91 const int index = (i * NT + tid) * 4;92 if (index < BK * BN) {93 const int local_row = index / BN;94 const int local_col = index % BN;95 *reinterpret_cast<float4*>(&SB[buffer][local_row][local_col]) =96 make_float4(b_prefetch[i][0], b_prefetch[i][1],97 b_prefetch[i][2], b_prefetch[i][3]);98 }99 }100 };101
102 const int tiles = (N + BK - 1) / BK;103 load_tile(0);104 store_tile(0);105 __syncthreads();106
107 int read_buffer = 0;108 for (int tile = 0; tile < tiles; ++tile) {109 if (tile + 1 < tiles) load_tile(tile + 1);110
111 #pragma unroll112 for (int inner = 0; inner < BK; ++inner) {113 #pragma unroll114 for (int local_col = 0; local_col < TN; local_col += 4) {115 const float4 b = *reinterpret_cast<const float4*>(116 &SB[read_buffer][inner][tx * TN + local_col]);117 #pragma unroll118 for (int row = 0; row < TM; ++row) {119 const float a = SA[read_buffer][ty * TM + row][inner];120 sum[row][local_col + 0] += a * b.x;121 sum[row][local_col + 1] += a * b.y;122 sum[row][local_col + 2] += a * b.z;123 sum[row][local_col + 3] += a * b.w;124 }125 }126 }127 __syncthreads();128
129 if (tile + 1 < tiles) {130 const int write_buffer = read_buffer ^ 1;131 store_tile(write_buffer);132 __syncthreads();133 read_buffer = write_buffer;134 }135 }136
137 #pragma unroll138 for (int local_row = 0; local_row < TM; ++local_row) {139 const int row = blockIdx.y * BM + ty * TM + local_row;140 #pragma unroll141 for (int local_col = 0; local_col < TN; local_col += 4) {142 const int col = blockIdx.x * BN + tx * TN + local_col;143 if (row < M && col + 3 < K) {144 *reinterpret_cast<float4*>(&C[row * K + col]) =145 make_float4(sum[local_row][local_col + 0],146 sum[local_row][local_col + 1],147 sum[local_row][local_col + 2],148 sum[local_row][local_col + 3]);149 }150 }151 }152}153
154#ifndef MATMUL_NO_SOLVE155extern "C" void solve(const float* A, const float* B, float* C, int M, int N, int K) {156 if ((N & 3) != 0 || (K & 3) != 0) {157 const dim3 block(16, 16);158 const dim3 grid((K + 15) / 16, (M + 15) / 16);159 scalar_fallback_v3<<<grid, block>>>(A, B, C, M, N, K);160 } else {161 constexpr int BM = MATMUL_BM, BN = MATMUL_BN, BK = MATMUL_BK;162 constexpr int TM = MATMUL_TM, TN = MATMUL_TN;163 const dim3 block(BN / TN, BM / TM);164 const dim3 grid((K + BN - 1) / BN, (M + BM - 1) / BM);165 matrix_multiplication_v3<BM, BN, BK, TM, TN>166 <<<grid, block>>>(A, B, C, M, N, K);167 }168}169#endifLarge-Tile 128 x 128 Configuration#
Approach#
large-tile 配置将 V3 的编译期参数改为:
1BM = 1282BN = 1283BK = 164TM = 85TN = 4block 包含 (128 / 4) x (128 / 8) = 32 x 16 = 512 个 thread。每个 thread 仍负责 32 个输出元素,而整个 block 共同生成一个 128 x 128 tile。其 double-buffered 输入存储占用 32 KiB:
12 * (128 * 16 + 16 * 128) * sizeof(float)更大的输出 tile 提高了 block 级复用,并将 input-tile 加载开销分摊到更多输出元素上,同时完整保留基础 V3 配置的 register-prefetch 和 double-buffer 机制。它也增加了 block 的 thread 数量,并改变 register 需求、occupancy 和调度,因此它是 V3 的一种参数配置,而不是独立的优化原理。
在修订后的 benchmark 中,该配置是两款受测 GPU 上本项目最快的 CUDA 实现。然而,参数匹配的 single-buffered 实验在扩大到相同的 128 x 128 tile 后反而变慢。因此,收益并非单独来自 tile 大小:只有当更大的输出 tile 与这条 double-buffered 数据路径结合时,收益才会出现。实测结果表明这两种选择存在相互作用,而不能由此得出更大 tile 普遍更快的结论。
Solution#
1#define MATMUL_BM 1282#define MATMUL_BN 1283#define MATMUL_BK 164#define MATMUL_TM 85#define MATMUL_TN 46
7#include "02_Matrix_Multiplication_v3_double_buffered.cu"Controlled Comparison#
下表 4 行均使用 BK = 16、每个 thread 一个 8 x 4 micro-tile、确定性的非零输入、5 次 warm-up iteration 和 10 次 measured iteration。
| Buffering | Output Tile | RTX 5070 Ti Laptop GPU | RTX 4090 |
|---|---|---|---|
| Single buffer | 64 x 64 | 38.761 ms | 11.314 ms |
| Single buffer | 128 x 128 | 38.842 ms | 13.146 ms |
| Double buffer | 64 x 64 | 39.642 ms | 11.514 ms |
| Double buffer | 128 x 128 | 34.590 ms | 10.052 ms |
在 64 x 64 配置下,double buffering 的影响很小且取决于平台:在 RTX 4090 上略快,在 RTX 5070 Ti Laptop GPU 上略慢。将 single-buffered kernel 扩大到 128 x 128 后,两款 GPU 上的速度都更慢。相比之下,128 x 128 double-buffered 配置明显快于 single-buffered large tile,也是表中最快的通用配置。因此,实测加速来自更大输出 tile 与 double-buffered 数据路径的相互作用,不能由此得出任一选择单独使用时普遍更快的结论。
以下编译器资源报告由面向 RTX 4090 target 的 nvcc -O2 -arch=sm_89 -Xptxas=-v 生成:
| Buffering | Output Tile | Threads per Block | Registers per Thread | Shared Memory per Block | Spills |
|---|---|---|---|---|---|
| Single buffer | 64 x 64 | 128 | 86 | 8 KiB | 0 |
| Single buffer | 128 x 128 | 512 | 86 | 16 KiB | 0 |
| Double buffer | 64 x 64 | 128 | 122 | 16 KiB | 0 |
| Double buffer | 128 x 128 | 512 | 100 | 32 KiB | 0 |
该报告证实了资源方面的权衡。single-buffered large tile 仍为每个 thread 使用 86 个 register,但 thread 数量增加到 4 倍,因此每个 block 分配的 register 从 11,008 个增加到 44,032 个。基础 double-buffered kernel 因预取状态而将每个 thread 的 register 用量提高到 122 个。对于 large double-buffered 实例,编译器为每个 thread 使用 100 个 register,且没有 spill;block 在保持相同 per-thread micro-tile 的同时,完成了 4 倍的输出工作。这些事实解释了为何 occupancy 和复用程度会随配置发生显著变化,但仅凭 register 与 shared-memory 数量并不能确定实际运行时的 stall 行为。
V4: Hardware-Asynchronous cp.async Pipeline#
Approach#
V4 保留 large V3 配置的 128 x 128 输出 tile、BK = 16 和每个 thread 一个 8 x 4 micro-tile,但改变了下一个输入 tile 进入 shared memory 的方式。
V3 首先将 global-memory 中的值加载到普通 thread register,随后再把这些 register 中的值存入另一个 shared-memory buffer。V4 则改为发出 Ampere 或更新架构支持的 PTX 指令:
1cp.async.cg.shared.global每条指令直接从 global memory 向 shared memory 复制 16 字节。cp.async.commit_group 提交一个 thread 发出的复制操作,cp.async.wait_group 0 则等待 block 所需的全部未完成 group 执行完毕。
该流水线包含 prologue、steady state 和 epilogue:
- prologue 异步填充 shared buffer 0,并在首次使用前等待填充完成。
- 在一次 steady-state iteration 中,block 会先将 tile
i + 1的复制操作发往另一个 buffer,再计算 tilei。 - 算术循环只读取当前 buffer,因此 GPU 可将 fused multiply-add 工作与尚未完成的 global-to-shared 复制重叠执行。
- buffer 交换角色前,
cp.async.wait_group 0保证复制完成,__syncthreads()则使新 tile 对所有 thread 可见。 - 最后一个 tile 没有后继,因此循环结束时不会再发出另一组复制操作。
越界的 16 字节 vector 会使用有效字节数为零的 source-size operand。硬件将目标 shared-memory vector 填零,而不会解引用无效的 global address。reduction 维度或 output-column 维度不适合 4 个 float 向量传输的输入会使用标量 fallback。compute capability 低于 8.0 的设备因不支持 cp.async,也会使用 fallback。
与 V3 的 register-prefetch 软件流水线不同,这是一条真正的硬件异步流水线(hardware-asynchronous pipeline)。不过,它仍不保证更快:两个版本在 shared buffer 切换角色时都需要同步,而包含 512 个 thread 的 block、地址生成、copy-group 管理和算术指令流都会争用执行资源。
Solution#
1#include <cuda_runtime.h>2
3namespace {4
5constexpr int BM = 128;6constexpr int BN = 128;7constexpr int BK = 16;8constexpr int TM = 8;9constexpr int TN = 4;10constexpr int THREADS_X = BN / TN;11constexpr int THREADS_Y = BM / TM;12constexpr int THREAD_COUNT = THREADS_X * THREADS_Y;13
14__global__ void scalar_fallback_v4(const float* A, const float* B, float* C,15 int M, int N, int K) {16 const int row = blockIdx.y * blockDim.y + threadIdx.y;17 const int col = blockIdx.x * blockDim.x + threadIdx.x;18 if (row >= M || col >= K) return;19
20 float sum = 0.0f;21 for (int inner = 0; inner < N; ++inner) {22 sum += A[row * N + inner] * B[inner * K + col];23 }24 C[row * K + col] = sum;25}26
27#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 80028__device__ __forceinline__ void cp_async_16(void* shared_destination,29 const void* global_source,30 int valid_bytes) {31 const unsigned int shared_address =32 static_cast<unsigned int>(__cvta_generic_to_shared(shared_destination));33 asm volatile(34 "cp.async.cg.shared.global [%0], [%1], 16, %2;\n"35 :36 : "r"(shared_address), "l"(global_source), "r"(valid_bytes));37}38
39__device__ __forceinline__ void cp_async_commit() {40 asm volatile("cp.async.commit_group;\n" : :);41}42
43__device__ __forceinline__ void cp_async_wait_all() {44 asm volatile("cp.async.wait_group 0;\n" : :);45}46#endif47
48__global__ void matrix_multiplication_v4_cp_async(49 const float* __restrict__ A,50 const float* __restrict__ B,51 float* __restrict__ C,52 int M, int N, int K) {53#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 80054 __shared__ __align__(16) float shared_a[2][BM][BK];55 __shared__ __align__(16) float shared_b[2][BK][BN];56
57 const int tx = threadIdx.x;58 const int ty = threadIdx.y;59 const int tid = ty * blockDim.x + tx;60 float accumulators[TM][TN] = {{0.0f}};61
62 auto issue_tile = [&](int tile, int buffer) {63 constexpr int A_VECTORS = BM * BK / 4;64 constexpr int B_VECTORS = BK * BN / 4;65 const int inner_base = tile * BK;66
67 for (int vector_index = tid;68 vector_index < A_VECTORS;69 vector_index += THREAD_COUNT) {70 const int element_index = vector_index * 4;71 const int local_row = element_index / BK;72 const int local_inner = element_index % BK;73 const int row = blockIdx.y * BM + local_row;74 const int inner = inner_base + local_inner;75 const int valid_bytes = (row < M && inner + 3 < N) ? 16 : 0;76 const float* source = A + static_cast<size_t>(row < M ? row : 0) * N +77 (inner + 3 < N ? inner : 0);78 cp_async_16(&shared_a[buffer][local_row][local_inner],79 source, valid_bytes);80 }81
82 for (int vector_index = tid;83 vector_index < B_VECTORS;84 vector_index += THREAD_COUNT) {85 const int element_index = vector_index * 4;86 const int local_inner = element_index / BN;87 const int local_col = element_index % BN;88 const int inner = inner_base + local_inner;89 const int col = blockIdx.x * BN + local_col;90 const int valid_bytes = (inner < N && col + 3 < K) ? 16 : 0;91 const float* source = B + static_cast<size_t>(inner < N ? inner : 0) * K +92 (col + 3 < K ? col : 0);93 cp_async_16(&shared_b[buffer][local_inner][local_col],94 source, valid_bytes);95 }96 cp_async_commit();97 };98
99 const int tile_count = (N + BK - 1) / BK;100 issue_tile(0, 0);101 cp_async_wait_all();102 __syncthreads();103
104 int read_buffer = 0;105 for (int tile = 0; tile < tile_count; ++tile) {106 const bool has_next_tile = tile + 1 < tile_count;107 if (has_next_tile) {108 issue_tile(tile + 1, read_buffer ^ 1);109 }110
111 #pragma unroll112 for (int inner = 0; inner < BK; ++inner) {113 const float4 b = *reinterpret_cast<const float4*>(114 &shared_b[read_buffer][inner][tx * TN]);115 #pragma unroll116 for (int local_row = 0; local_row < TM; ++local_row) {117 const float a =118 shared_a[read_buffer][ty * TM + local_row][inner];119 accumulators[local_row][0] += a * b.x;120 accumulators[local_row][1] += a * b.y;121 accumulators[local_row][2] += a * b.z;122 accumulators[local_row][3] += a * b.w;123 }124 }125
126 if (has_next_tile) {127 cp_async_wait_all();128 __syncthreads();129 read_buffer ^= 1;130 }131 }132
133 #pragma unroll134 for (int local_row = 0; local_row < TM; ++local_row) {135 const int row = blockIdx.y * BM + ty * TM + local_row;136 const int col = blockIdx.x * BN + tx * TN;137 if (row < M && col + 3 < K) {138 *reinterpret_cast<float4*>(&C[static_cast<size_t>(row) * K + col]) =139 make_float4(accumulators[local_row][0],140 accumulators[local_row][1],141 accumulators[local_row][2],142 accumulators[local_row][3]);143 }144 }145#endif146}147
148} // namespace149
150extern "C" void solve(const float* A, const float* B, float* C,151 int M, int N, int K) {152 static const bool supports_cp_async = [] {153 int device = 0;154 cudaDeviceProp properties{};155 cudaGetDevice(&device);156 cudaGetDeviceProperties(&properties, device);157 return properties.major >= 8;158 }();159
160 if (!supports_cp_async || (N & 3) != 0 || (K & 3) != 0) {161 const dim3 block(16, 16);162 const dim3 grid((K + 15) / 16, (M + 15) / 16);163 scalar_fallback_v4<<<grid, block>>>(A, B, C, M, N, K);164 return;165 }166
167 const dim3 block(THREADS_X, THREADS_Y);168 const dim3 grid((K + BN - 1) / BN, (M + BM - 1) / BM);169 matrix_multiplication_v4_cp_async<<<grid, block>>>(A, B, C, M, N, K);170}V5: WMMA TF32 Tensor Core Kernel#
Approach#
V5 同时改变了执行单元和数值格式。公共接口仍然接收和返回 FP32 矩阵,但 fast path 会将输入值显式舍入为 TensorFloat-32,并在 Tensor Core 上使用 FP32 累加器执行矩阵乘加运算。
TF32 保留 FP32 的 8 位指数范围,但使用 10 位显式尾数。转换通过以下指令完成:
1cvt.rna.tf32.f32这意味着 V5 在数值上与严格的 FP32 CUDA Core 版本并不完全相同。它以降低输入尾数精度为代价,换取使用 Tensor Core 矩阵指令的能力。因此,测试采用 5e-3 的缩放容差,同时仍会拒绝所有非有限结果,并将每个输出元素与 CPU reference 进行比较。
一个 block 生成一个 128 x 128 输出 tile,并以 32 为单位分块归约 K。该 block 包含 16 个 warp。每个 warp 负责一条 16 x 64 输出带,由 4 个 16 x 16 accumulator fragment 表示。两个 warp 分别覆盖同一个 16 行输出带的左半部分和右半部分。
对于每个 K=32 shared-memory tile:
- 全部 512 个 thread 协作加载
A[128, 32]和B[32, 128],并将每个值转换为 TF32。 - 一次 block 同步使两个 tile 均可见。
- 由于 TF32 WMMA 使用
16 x 16 x 8fragment,该 tile 被划分为 4 个 WMMA reduction step。 - 每个 warp 在每个 step 加载一个
Afragment 和 4 个Bfragment。 wmma::mma_sync更新 4 个 FP32 accumulator fragment。- 所有 reduction tile 完成后,
wmma::store_matrix_sync将 fragment 写入行主序 FP32 output memory。
fast path 要求 M 和 K 与 128 x 128 输出 tile 对齐,N 与 BK=32 对齐。其他 shape、较旧架构和边界情况会使用标量 FP32 fallback。这样既保留了题目接口和一般情况下的正确性,也使 WMMA kernel 无需处理 partial-fragment store。
Tensor Core 能够提供很高的峰值吞吐率,但该实现也需要承担显式 TF32 转换、shared-memory staging、频繁 fragment load、block 同步和 WMMA fragment 归属管理等开销。因此,这些测量结果证明的是一个正确的底层 WMMA 实现,并不意味着它必然优于经过精细调优的 CUDA Core kernel。
Solution#
1#include <cuda_runtime.h>2#include <mma.h>3
4namespace {5
6constexpr int BM = 128;7constexpr int BN = 128;8constexpr int BK = 32;9constexpr int WARPS_PER_BLOCK = 16;10constexpr int THREADS_PER_BLOCK = WARPS_PER_BLOCK * 32;11constexpr int WMMA_K = 8;12constexpr int OUTPUT_FRAGMENTS_PER_WARP = BN / 32;13
14__device__ __forceinline__ float to_tf32(float value) {15#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 80016 unsigned int tf32_bits;17 asm("cvt.rna.tf32.f32 %0, %1;" : "=r"(tf32_bits) : "f"(value));18 return __uint_as_float(tf32_bits);19#else20 return value;21#endif22}23
24__global__ void scalar_fallback_v5(const float* A, const float* B, float* C,25 int M, int N, int K) {26 const int row = blockIdx.y * blockDim.y + threadIdx.y;27 const int col = blockIdx.x * blockDim.x + threadIdx.x;28 if (row >= M || col >= K) return;29
30 float sum = 0.0f;31 for (int inner = 0; inner < N; ++inner) {32 sum += A[row * N + inner] * B[inner * K + col];33 }34 C[row * K + col] = sum;35}36
37__global__ void matrix_multiplication_v5_wmma_tf32(38 const float* __restrict__ A,39 const float* __restrict__ B,40 float* __restrict__ C,41 int M, int N, int K) {42#if defined(__CUDA_ARCH__) && __CUDA_ARCH__ >= 80043 using namespace nvcuda;44
45 __shared__ __align__(16) float shared_a[BM][BK];46 __shared__ __align__(16) float shared_b[BK][BN];47
48 const int tid = threadIdx.x;49 const int warp_id = tid / 32;50 const int warp_row = warp_id / 2;51 const int warp_col_group = warp_id % 2;52
53 wmma::fragment<wmma::accumulator, 16, 16, WMMA_K, float>54 accumulators[OUTPUT_FRAGMENTS_PER_WARP];55 #pragma unroll56 for (int fragment = 0; fragment < OUTPUT_FRAGMENTS_PER_WARP; ++fragment) {57 wmma::fill_fragment(accumulators[fragment], 0.0f);58 }59
60 for (int inner_base = 0; inner_base < N; inner_base += BK) {61 for (int index = tid; index < BM * BK;62 index += THREADS_PER_BLOCK) {63 const int local_row = index / BK;64 const int local_inner = index % BK;65 const int row = blockIdx.y * BM + local_row;66 const float value = A[static_cast<size_t>(row) * N +67 inner_base + local_inner];68 shared_a[local_row][local_inner] = to_tf32(value);69 }70 for (int index = tid; index < BK * BN;71 index += THREADS_PER_BLOCK) {72 const int local_inner = index / BN;73 const int local_col = index % BN;74 const int col = blockIdx.x * BN + local_col;75 const float value = B[static_cast<size_t>(inner_base + local_inner) * K +76 col];77 shared_b[local_inner][local_col] = to_tf32(value);78 }79 __syncthreads();80
81 const int local_row = warp_row * 16;82 #pragma unroll83 for (int inner = 0; inner < BK; inner += WMMA_K) {84 wmma::fragment<wmma::matrix_a, 16, 16, WMMA_K,85 wmma::precision::tf32, wmma::row_major> a_fragment;86 wmma::load_matrix_sync(87 a_fragment, &shared_a[local_row][inner], BK);88
89 #pragma unroll90 for (int fragment = 0;91 fragment < OUTPUT_FRAGMENTS_PER_WARP;92 ++fragment) {93 wmma::fragment<wmma::matrix_b, 16, 16, WMMA_K,94 wmma::precision::tf32,95 wmma::row_major> b_fragment;96 wmma::load_matrix_sync(97 b_fragment,98 &shared_b[inner][warp_col_group * 64 + fragment * 16],99 BN);100 wmma::mma_sync(accumulators[fragment], a_fragment,101 b_fragment, accumulators[fragment]);102 }103 }104 __syncthreads();105 }106
107 const int output_row = blockIdx.y * BM + warp_row * 16;108 const int output_col = blockIdx.x * BN + warp_col_group * 64;109 #pragma unroll110 for (int fragment = 0; fragment < OUTPUT_FRAGMENTS_PER_WARP; ++fragment) {111 wmma::store_matrix_sync(112 &C[static_cast<size_t>(output_row) * K +113 output_col + fragment * 16],114 accumulators[fragment], K, wmma::mem_row_major);115 }116#endif117}118
119} // namespace120
121extern "C" void solve(const float* A, const float* B, float* C,122 int M, int N, int K) {123 static const bool supports_wmma_tf32 = [] {124 int device = 0;125 cudaDeviceProp properties{};126 cudaGetDevice(&device);127 cudaGetDeviceProperties(&properties, device);128 return properties.major >= 8;129 }();130
131 if (!supports_wmma_tf32 ||132 M % BM != 0 || N % BK != 0 || K % BN != 0) {133 const dim3 block(16, 16);134 const dim3 grid((K + 15) / 16, (M + 15) / 16);135 scalar_fallback_v5<<<grid, block>>>(A, B, C, M, N, K);136 return;137 }138
139 const dim3 block(THREADS_PER_BLOCK);140 const dim3 grid(K / BN, M / BM);141 matrix_multiplication_v5_wmma_tf32<<<grid, block>>>(A, B, C, M, N, K);142}Test Methodology#
所有 CUDA 方法都使用同一个测试文件,并且必须通过 9 组正确性 shape,其中包括标量、矩形、对齐、未对齐、partial-tile 和 multi-block 维度。检查器会拒绝非有限输出值,并使用 5e-3 的缩放容差将每个输出元素与 CPU reference 比较。该容差覆盖 V5 有意进行的 TF32 输入舍入;严格的 FP32 方法也使用同一测试评估,从而确保对比中的每一行都遵循统一流程。129 x 96 乘 96 x 132 的 case 用于覆盖 V4 处理不完整输出 tile 的 asynchronous fast path,而 256 x 96 乘 96 x 256 的 case 则用于覆盖多个 WMMA output block。
性能测试 case 使用 M=8192、N=6144 和 K=4096。输入被初始化为确定性的非零值,以免全零分配产生缺乏代表性的内存行为。每种方法都针对远程 GPU 的原生架构编译。在 10 次基于 CUDA event 的测量前,先执行 5 次 warm-up iteration。下表给出这 10 次测量的平均值、最小值、最大值和计算得到的浮点吞吐率。
Test Code#
1#include <cuda_runtime.h>2
3#include <array>4#include <cmath>5#include <iostream>6
7extern "C" void solve(const float* a, const float* b, float* c,8 int m, int n, int k);9
10int main() {11 constexpr int m = 2;12 constexpr int n = 2;13 constexpr int k = 2;14 const std::array<float, 4> matrix_a = {1.0f, 2.0f, 3.0f, 4.0f};15 const std::array<float, 4> matrix_b = {5.0f, 6.0f, 7.0f, 8.0f};16 const std::array<float, 4> expected = {19.0f, 22.0f, 43.0f, 50.0f};17 std::array<float, 4> actual = {};18 constexpr size_t bytes = 4 * sizeof(float);19
20 float* device_a = nullptr;21 float* device_b = nullptr;22 float* device_c = nullptr;23
24 auto check_cuda = [](cudaError_t status, const char* operation) {25 if (status == cudaSuccess) {26 return true;27 }28 std::cerr << "Test failed. CUDA error in " << operation << ": "29 << cudaGetErrorString(status) << '\n';30 return false;31 };32
33 bool success = check_cuda(cudaMalloc(&device_a, bytes), "cudaMalloc(device_a)") &&34 check_cuda(cudaMalloc(&device_b, bytes), "cudaMalloc(device_b)") &&35 check_cuda(cudaMalloc(&device_c, bytes), "cudaMalloc(device_c)") &&36 check_cuda(cudaMemcpy(device_a, matrix_a.data(), bytes,37 cudaMemcpyHostToDevice), "copy matrix_a") &&38 check_cuda(cudaMemcpy(device_b, matrix_b.data(), bytes,39 cudaMemcpyHostToDevice), "copy matrix_b");40
41 if (success) {42 solve(device_a, device_b, device_c, m, n, k);43 success = check_cuda(cudaGetLastError(), "solve") &&44 check_cuda(cudaDeviceSynchronize(), "synchronize") &&45 check_cuda(cudaMemcpy(actual.data(), device_c, bytes,46 cudaMemcpyDeviceToHost), "copy result");47 }48
49 cudaFree(device_a);50 cudaFree(device_b);51 cudaFree(device_c);52
53 if (!success) {54 return 1;55 }56
57 for (size_t index = 0; index < actual.size(); ++index) {58 if (std::fabs(actual[index] - expected[index]) > 1e-5f) {59 std::cerr << "Test failed at index " << index60 << ". Expected: " << expected[index]61 << ", Actual: " << actual[index] << '\n';62 return 1;63 }64 }65
66 std::cout << "Test passed.\n";67 return 0;68}Test Result#
以下表格特意将同一款 GPU 的结果集中展示。它们用于比较同一平台上的不同方法,而不是为两款 GPU 排名。Default 当前选择 V3 large-tile 实现,因此该行是对同一源码路径的重复运行,并非另一种算法。
NVIDIA GeForce RTX 5070 Ti Laptop GPU#
| Method | Arithmetic | Status | Average Time | Minimum Time | Maximum Time | Performance |
|---|---|---|---|---|---|---|
| V0 naive | FP32 | PASS | 383.370 ms | 348.679 ms | 417.879 ms | 1075.506 GFLOPS |
| V1 shared memory | FP32 | PASS | 250.884 ms | 224.301 ms | 302.463 ms | 1643.454 GFLOPS |
| V1 1D thread tiling | FP32 | PASS | 135.891 ms | 128.005 ms | 150.389 ms | 3034.181 GFLOPS |
| V1 2D thread tiling | FP32 | PASS | 123.423 ms | 117.553 ms | 135.046 ms | 3340.669 GFLOPS |
V2 vectorized, BK=32 | FP32 | PASS | 46.376 ms | 44.035 ms | 48.737 ms | 8890.723 GFLOPS |
V2 vectorized, BK=16 | FP32 | PASS | 38.761 ms | 36.343 ms | 40.281 ms | 10637.490 GFLOPS |
V2 vectorized, 128 x 128 | FP32 | PASS | 38.842 ms | 37.423 ms | 41.546 ms | 10615.226 GFLOPS |
V3 software double buffer, 64 x 64 | FP32 | PASS | 39.642 ms | 34.580 ms | 42.673 ms | 10400.993 GFLOPS |
V3 software double buffer, 128 x 128 | FP32 | PASS | 34.590 ms | 32.101 ms | 37.409 ms | 11920.091 GFLOPS |
V4 hardware cp.async | FP32 | PASS | 34.572 ms | 31.259 ms | 37.045 ms | 11926.402 GFLOPS |
| V5 WMMA Tensor Core | TF32 input, FP32 accumulation | PASS | 57.805 ms | 52.967 ms | 66.704 ms | 7132.925 GFLOPS |
Default (V3 128 x 128) | FP32 | PASS | 34.486 ms | 31.874 ms | 37.132 ms | 11956.000 GFLOPS |
在这款 GPU 上,V4 在最终统一测试中的平均时间最低,但考虑到观察到的运行间波动,其 34.572 ms 结果实际上与匹配的 large V3 的 34.590 ms 持平。V4 多次运行的时间大约在 32 至 35 ms 之间。因此,证据表明 hardware-asynchronous copy 在该平台上具有竞争力,而不是 0.018 ms 的最终差异具有显著意义。V5 在文档所述的 TF32 容差下结果正确,但它慢于经过调优的 CUDA Core kernel,因为这个手写 WMMA 映射需要承担大量转换、shared-memory、同步和 fragment 管理开销。
NVIDIA GeForce RTX 4090#
| Method | Arithmetic | Status | Average Time | Minimum Time | Maximum Time | Performance |
|---|---|---|---|---|---|---|
| V0 naive | FP32 | PASS | 83.140 ms | 82.831 ms | 83.375 ms | 4959.321 GFLOPS |
| V1 shared memory | FP32 | PASS | 59.379 ms | 58.985 ms | 59.865 ms | 6943.818 GFLOPS |
| V1 1D thread tiling | FP32 | PASS | 19.484 ms | 19.393 ms | 19.505 ms | 21161.508 GFLOPS |
| V1 2D thread tiling | FP32 | PASS | 13.446 ms | 13.420 ms | 13.465 ms | 30665.525 GFLOPS |
V2 vectorized, BK=32 | FP32 | PASS | 11.646 ms | 11.098 ms | 12.106 ms | 35404.086 GFLOPS |
V2 vectorized, BK=16 | FP32 | PASS | 11.314 ms | 11.213 ms | 11.345 ms | 36441.761 GFLOPS |
V2 vectorized, 128 x 128 | FP32 | PASS | 13.146 ms | 13.135 ms | 13.173 ms | 31363.839 GFLOPS |
V3 software double buffer, 64 x 64 | FP32 | PASS | 11.514 ms | 11.380 ms | 11.654 ms | 35809.707 GFLOPS |
V3 software double buffer, 128 x 128 | FP32 | PASS | 10.052 ms | 9.939 ms | 10.213 ms | 41019.869 GFLOPS |
V4 hardware cp.async | FP32 | PASS | 10.293 ms | 10.230 ms | 10.415 ms | 40056.186 GFLOPS |
| V5 WMMA Tensor Core | TF32 input, FP32 accumulation | PASS | 12.675 ms | 12.107 ms | 13.133 ms | 32528.826 GFLOPS |
Default (V3 128 x 128) | FP32 | PASS | 10.096 ms | 10.010 ms | 10.144 ms | 40840.572 GFLOPS |
在这款 GPU 上,large V3 软件流水线仍是实测最快的实现。V4 将复制与算术计算重叠执行,但未能抵消额外的 copy-group 和同步开销。V5 再次证明 Tensor Core 执行功能正常,却没有超越已有的 FP32 路径。这一结果说明了一个有用的边界:当数据准备和 fragment 调度尚未达到生产级 GEMM 库的成熟程度时,仅凭 Tensor Core 的峰值吞吐率并不能保证端到端 kernel 更快。
Triton#
Approach#
Triton 实现计算与 CUDA 相同的行主序矩阵乘积:
C[m,k]=n=0∑N−1A[m,n]B[n,k].一个 Triton program instance 负责一个 64 x 64 输出 tile。与 CUDA 源码不同,该实现不会将单个标量输出显式分配给 threadIdx 坐标,而是描述完整的 index block 和 value block;Triton 编译器再将这些操作映射到 GPU thread 和 warp。reduction 维度以 32 为单位分块处理:
1BLOCK_SIZE_M = 642BLOCK_SIZE_N = 323BLOCK_SIZE_K = 644GROUP_SIZE_M = 8输出 grid 被展平为一维。tl.program_id(0) 用于标识当前 program instance。kernel 根据这个 linear ID 推导 PID_M 和 PID_K,二者分别标识输出行 tile 和输出列 tile。完整 grid 包含:
个 program instance。
program 按最多 8 个 M tile 分组排序,之后才沿 K 继续推进。这只会改变执行顺序,不会改变每个 program 所负责的输出 tile。相邻 program 更有可能复用 A 和 B 的 cache 区域;相比之下,简单的行主序排列可能会沿一个输出维度前进较远后,才重新访问可复用的输入数据。
对于一个 program,行列 offset 分别为:
1a_m_offset = PID_M * 64 + [0, ..., 63]2b_k_offset = PID_K * 64 + [0, ..., 63]在每个 reduction step 中,tl.arange 创建行、reduction 和列 offset。使用 [:, None] 和 [None, :] 添加 singleton dimension 后,这些 vector 会被广播为来自 A 的 64 x 32 pointer tile 和来自 B 的 32 x 64 pointer tile。A、B 和 C 的行主序 stride 分别以 (N, 1)、(K, 1) 和 (K, 1) 显式传入。
mask 在正确性方面所起的作用,与 CUDA kernel 中的边界检查和零填充相同。被 mask 的输入位置会按零加载,因此不会对 reduction 产生贡献;被 mask 的输出位置则不会被存储。因此,同一个 kernel 即可处理不是 64 或 32 的倍数的维度,无需启动单独的边界 kernel。
tl.dot 将两个输入 tile 相乘,并累加到一个 float32 64 x 64 block 中。allow_tf32=False 使计算保持在该实现采用的 non-TF32 路径上。reduction loop 结束后,最终的 mask 会防止向 C 范围外存储。
launch 为每个 program 使用 4 个 warp 和 3 个 pipeline stage。与此前固定的 32 x 32 输出 tile 和未分组的二维 grid 相比,更大的 tile 使每个 program 完成更多工作,分组排序则改善了局部性。在统一 benchmark 下,这使 RTX 4090 上的平均时间从 16.907 ms 降至 9.803 ms,并使 RTX 5070 Ti Laptop GPU 上的平均时间从 78.710 ms 降至 32.518 ms。
Solution#
1import torch2import triton3import triton.language as tl4
5
6@triton.jit7def matrix_multiplication_kernel(8 a, b, c, M, N, K, stride_am, stride_an, stride_bn, stride_bk, stride_cm, stride_ck,9 BLOCK_SIZE_M: tl.constexpr, BLOCK_SIZE_N: tl.constexpr,10 BLOCK_SIZE_K: tl.constexpr, GROUP_SIZE_M: tl.constexpr11):12 pid = tl.program_id(0)13 num_pid_m = tl.cdiv(M, BLOCK_SIZE_M)14 num_pid_k = tl.cdiv(K, BLOCK_SIZE_K)15 num_pid_in_group = GROUP_SIZE_M * num_pid_k16 group_id = pid // num_pid_in_group17 first_pid_m = group_id * GROUP_SIZE_M18 group_size_m = tl.minimum(num_pid_m - first_pid_m, GROUP_SIZE_M)19 PID_M = first_pid_m + (pid % num_pid_in_group) % group_size_m20 PID_K = (pid % num_pid_in_group) // group_size_m21 MAX_N = tl.cdiv(N, BLOCK_SIZE_N)22
23 accumulated_block = tl.zeros((BLOCK_SIZE_M, BLOCK_SIZE_K), dtype=tl.float32)24
25 start_a_m = PID_M * BLOCK_SIZE_M26 a_m_offset = start_a_m + tl.arange(0, BLOCK_SIZE_M)27
28 start_b_k = PID_K * BLOCK_SIZE_K29 b_k_offset = start_b_k + tl.arange(0, BLOCK_SIZE_K)30
31 for n in tl.range(MAX_N):32 start_a_n = n * BLOCK_SIZE_N33 a_n_offset = start_a_n + tl.arange(0, BLOCK_SIZE_N)34
35 a_mn_mask = (a_m_offset[:, None] < M) & (a_n_offset[None, :] < N)36 a_mn_ptrs = a + a_m_offset[:, None] * stride_am + a_n_offset[None, :] * stride_an37 block_a_mn = tl.load(a_mn_ptrs, mask=a_mn_mask, other=0.0)38
39 start_b_n = n * BLOCK_SIZE_N40 b_n_offset = start_b_n + tl.arange(0, BLOCK_SIZE_N)41
42
43 b_nk_mask = (b_n_offset[:, None] < N) & (b_k_offset[None, :] < K)44 b_nk_ptrs = b + b_n_offset[:, None] * stride_bn + b_k_offset[None, :] * stride_bk45
46 block_b_nk = tl.load(b_nk_ptrs, mask=b_nk_mask, other=0.0)47
48 accumulated_block = tl.dot(block_a_mn, block_b_nk, accumulated_block, allow_tf32=False)49 # block_ab = tl.dot(block_a_mn, block_b_nk)50 # accumulated_block += block_ab51
52 c_mk_ptrs = c + a_m_offset[:, None] * stride_cm + b_k_offset[None, :] * stride_ck53 c_mk_mask = (a_m_offset[:, None] < M) & (b_k_offset[None, :] < K)54 tl.store(c_mk_ptrs, accumulated_block, mask=c_mk_mask)55
56# a, b, c are tensors on the GPU57def solve(a: torch.Tensor, b: torch.Tensor, c: torch.Tensor, M: int, N: int, K: int):58 stride_am, stride_an = N, 159 stride_bn, stride_bk = K, 160 stride_cm, stride_ck = K, 161
62 BLOCK_SIZE_M = 6463 BLOCK_SIZE_N = 3264 BLOCK_SIZE_K = 6465 GROUP_SIZE_M = 866
67 grid = (triton.cdiv(M, BLOCK_SIZE_M) * triton.cdiv(K, BLOCK_SIZE_K),)68 matrix_multiplication_kernel[grid](69 a, b, c, M, N, K, stride_am, stride_an, stride_bn, stride_bk, stride_cm, stride_ck,70 BLOCK_SIZE_M = BLOCK_SIZE_M,71 BLOCK_SIZE_N = BLOCK_SIZE_N,72 BLOCK_SIZE_K = BLOCK_SIZE_K,73 GROUP_SIZE_M = GROUP_SIZE_M,74 num_warps=4,75 num_stages=3,76 )Test Code#
1import importlib.util2import sys3from pathlib import Path4
5import torch6
7
8def load_implementation():9 source_path = Path(__file__).resolve().parents[2] / "src" / "triton" / "02_Matrix_Multiplication.py"10 spec = importlib.util.spec_from_file_location("matrix_multiplication_triton_minimal", source_path)11 if spec is None or spec.loader is None:12 raise RuntimeError(f"Unable to load implementation: {source_path}")13 module = importlib.util.module_from_spec(spec)14 spec.loader.exec_module(module)15 return module16
17
18def main():19 matrix_a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], device="cuda")20 matrix_b = torch.tensor([[5.0, 6.0], [7.0, 8.0]], device="cuda")21 expected = torch.tensor([[19.0, 22.0], [43.0, 50.0]], device="cuda")22 actual = torch.empty_like(expected)23
24 load_implementation().solve(matrix_a, matrix_b, actual, 2, 2, 2)25 torch.cuda.synchronize()26 torch.testing.assert_close(actual, expected)27 print("Test passed.")28
29
30if __name__ == "__main__":31 try:32 main()33 except Exception as error:34 print(f"Test failed: {error}", file=sys.stderr)35 raiseTest Result#
| Platform | Status | Problem Size | Iterations | Average Time | Minimum Time | Maximum Time | Performance |
|---|---|---|---|---|---|---|---|
| NVIDIA GeForce RTX 5070 Ti Laptop GPU | PASS | M=8192, N=6144, K=4096 | 10 | 32.518 ms | 30.066 ms | 34.157 ms | 12679.673 GFLOPS |
| NVIDIA GeForce RTX 4090 | PASS | M=8192, N=6144, K=4096 | 10 | 9.803 ms | 8.994 ms | 10.005 ms | 42061.461 GFLOPS |
PyTorch#
Approach#
PyTorch 实现刻意保持简短,因为该框架已经以内置 tensor operation 的形式提供了矩阵乘法:
1torch.matmul(A, B, out=C)在本题中,A 和 B 是 shape 分别为 M x N 和 N x K 的二维 tensor。torch.matmul 会检查内部维度是否兼容,计算 M x K 的矩阵乘积,并将结果直接写入传入的输出 tensor C。使用 out=C 很重要,因为题目要求最终结果必须存放在调用方提供的输出 tensor 中。
PyTorch 提供了多种乘法接口,但它们的含义并不完全相同:
A * B执行逐元素乘法,并非矩阵乘法。A @ B是矩阵乘法的 Python 运算符语法,遵循torch.matmul语义。torch.mm(A, B)专用于二维矩阵输入。torch.bmm(A, B)用于处理成批的三维矩阵,不支持通用 broadcasting。torch.matmul(A, B)支持 vector、matrix、更高维 batch 和 broadcasting。torch.einsum(...)通过 index notation 显式表达 contraction,适用于更复杂的 tensor 关系。
torch.mm 和 A @ B 都可以表达本题的二维矩阵运算,但实际源码使用的是带 out argument 的 torch.matmul,因此本文档说明和测试的也是这一操作。
该实现中没有显式的 grid、thread index、tile size、mask 或同步。PyTorch 根据 tensor 的 device 和 dtype 分派操作,并由其 CUDA backend 选择底层 GPU 实现。所需的 solve signature 仍保留 M、N 和 K argument,但不会直接读取它们;实际执行的矩阵乘法由 tensor shape 决定。
Solution#
1import torch2
3# A, B, C are tensors on the GPU4def solve(A: torch.Tensor, B: torch.Tensor, C: torch.Tensor, M: int, N: int, K: int):5 torch.matmul(A,B,out=C)Test Code#
1import importlib.util2import sys3from pathlib import Path4
5import torch6
7
8def load_implementation():9 source_path = Path(__file__).resolve().parents[2] / "src" / "pytorch" / "02_Matrix_Multiplication.py"10 spec = importlib.util.spec_from_file_location("matrix_multiplication_pytorch_minimal", source_path)11 if spec is None or spec.loader is None:12 raise RuntimeError(f"Unable to load implementation: {source_path}")13 module = importlib.util.module_from_spec(spec)14 spec.loader.exec_module(module)15 return module16
17
18def main():19 matrix_a = torch.tensor([[1.0, 2.0], [3.0, 4.0]], device="cuda")20 matrix_b = torch.tensor([[5.0, 6.0], [7.0, 8.0]], device="cuda")21 expected = torch.tensor([[19.0, 22.0], [43.0, 50.0]], device="cuda")22 actual = torch.empty_like(expected)23
24 load_implementation().solve(matrix_a, matrix_b, actual, 2, 2, 2)25 torch.cuda.synchronize()26 torch.testing.assert_close(actual, expected)27 print("Test passed.")28
29
30if __name__ == "__main__":31 try:32 main()33 except Exception as error:34 print(f"Test failed: {error}", file=sys.stderr)35 raiseTest Result#
| Platform | Status | Problem Size | Iterations | Average Time | Minimum Time | Maximum Time | Performance |
|---|---|---|---|---|---|---|---|
| NVIDIA GeForce RTX 5070 Ti Laptop GPU | PASS | M=8192, N=6144, K=4096 | 10 | 29.305 ms | 25.729 ms | 31.689 ms | 14069.752 GFLOPS |
| NVIDIA GeForce RTX 4090 | PASS | M=8192, N=6144, K=4096 | 10 | 7.469 ms | 7.188 ms | 7.559 ms | 55204.880 GFLOPS |
References#
Acknowledgements#
本文档参考了 Du Ziyuan 的文章 CUDA Learning Journey [11] — A Detailed Explanation of Matrix Multiplication 中的讲解材料、图示和动画。原作采用 CC BY-NC-SA 4.0 许可;改编后的文档和在本地重新制作的媒体内容也按相同许可发布。改动包括围绕 LeetGPU 接口重新组织讲解,使讨论和代码与本项目的实现相匹配,并加入项目专用测试和实测结果。
关注 LeetGPU repository,了解更多内容。
文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!
部分内容可能已过时
评论区
分享你的想法,与大家交流讨论
音乐
暂未播放



