Skip to content

Commit 66b028d

Browse files
author
liu jiawei
committed
[Doc][Polish] gemm optimize by 2d thread tile, modify doc by review
1 parent 2d37c86 commit 66b028d

File tree

2 files changed

+45
-20
lines changed

2 files changed

+45
-20
lines changed

docs/11_gemm_optimize/01_tiled2d/README.md

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,21 @@
66

77
在介绍二维 Thread Tile 之前,我们先来回顾一下一维 Thread Tile 的优化方法。在初级系列中,我们使用了一维线程块来优化矩阵乘法的性能,我们将矩阵乘法的计算任务分配给了一维线程块,每个线程块负责计算一个小的矩阵块。这样做的好处是可以充分利用共享内存,减少全局内存的访问次数,从而提高矩阵乘法的性能。
88

9-
我们在每个线程中计算了一维的矩阵块。想要继续优化这个 Kernel 的性能,我们可以使用二维线程块来计算二维的矩阵块。
9+
还记得一维 Thread Tile 中的例子吗?如果输入的 A 和 B 都是 7x7 的矩阵:
10+
11+
1. 如果我们一次读取 1 行 A 和 1 列 B,当每一个线程只计算一个结果的时候,我们需要从 A 中读取 7 个数据,从 B 中读取 7 个数据,从 C 中读取 1 个数据,然后写 1 次 C。这样的话,每个线程需要读取 15 个数据,写 1 次数据。计算 16 个结果需要 16 个线程,共 16x16 = 256 次 IO。
12+
2. 如果我们一次读取 4 行 A 和 1 列 B,那么每一个线程计算 4 个结果,此时需要从 A 中读取 4x7 个数据,从 B 中读取 7 个数据,从 C 中读取 4 个数据,然后写 4 次 C。计算 16 个结果需要 4 个线程,共 4x43 = 172 次 IO。
13+
3. 如果我们一次读取 4 行 A 和 4 列 B,那么每一个线程计算 16 个结果,此时需要从 A 中读取 4x7 个数据,从 B 中读取 4x7 个数据,从 C 中读取 16 个数据,然后写 16 次 C。计算 16 个结果一共需要 1 个线程,共 1x88 = 88 次 IO。
14+
15+
上述的 `2` 就是一维 Thread Tile 优化,上述的 `3` 就是 二维 Thread Tile 优化,计算结果不变的同时,减少 IO 次数,提升算法的执行时间。所以想要继续优化这个 Kernel 的性能,我们可以使用二维线程块来计算二维的矩阵块。
1016

1117
## 2. 二维 Thread Tile
1218

1319
### 2.1 优化思路
1420

1521
本文的主要优化思路就是让每个线程计算一个 8\*8 的网格。下面我们来看一下这个 Kernel 的主题思路图:
1622

17-
![picture 1](images/9047246849f79b5117961c15e1a3a340a44ab003566140ecc00600058c70a9e2.png)
23+
![picture 1](images/9047246849f79b5117961c15e1a3a340a44ab003566140ecc00600058c70a9e2.png)
1824

1925
首先在内核的第一阶段, 所有线程协同工作, 从全局内存中加载矩阵 A 和矩阵 B 到共享内存中。
2026

@@ -74,9 +80,9 @@ float thread_results[TM * TN] = {0.0};
7480
float reg_m[TM] = {0.0};
7581
float reg_n[TN] = {0.0};
7682

77-
A += c_row * BM * K;
78-
B += c_col * BN;
79-
C += c_row * BM * N + c_col * BN;
83+
A += c_row * BM * K;
84+
B += c_col * BN;
85+
C += c_row * BM * N + c_col * BN;
8086

8187
// 外层循环
8288
for (uint bkIdx = 0; bkIdx < K; bkIdx += BK)
@@ -107,7 +113,7 @@ B += BK * N;
107113

108114
下图可以更好的帮助我们理解上面的代码:
109115

110-
![picture 2](images/f507ad687528e8bbb14a85c1fa3016cce50be55b5670ebc425c549cc5c5bd5a6.png)
116+
![picture 2](images/f507ad687528e8bbb14a85c1fa3016cce50be55b5670ebc425c549cc5c5bd5a6.png)
111117

112118
图中画出了矩阵 A 加载共享内存的过程。在每一步中,每个线程负责加载一个元素到共享内存中。这个元素的索引是 `inner_row_A``inner_col_A` 。for 循环中的 `load_offset` 递增的步长是 `stride_A` 。在图中就是向下移动了 `stride_A` 个元素。
113119

@@ -130,7 +136,7 @@ for (uint dot_idx = 0; dot_idx < BK; ++dot_idx)
130136
{
131137
for (uint reg_idx_n = 0; reg_idx_n < TN; ++reg_idx_n)
132138
{
133-
thread_results[reg_idx_m * TN + reg_idx_n] +=
139+
thread_results[reg_idx_m * TN + reg_idx_n] +=
134140
reg_m[reg_idx_m] * reg_n[reg_idx_n];
135141
}
136142
}
@@ -158,7 +164,7 @@ for (uint reg_idx_m = 0; reg_idx_m < TM; ++reg_idx_m)
158164
{
159165
for (uint reg_idx_n = 0; reg_idx_n < TN; ++reg_idx_n)
160166
{
161-
C[(thread_row * TM + reg_idx_m) * N + thread_col * TN + reg_idx_n] =
167+
C[(thread_row * TM + reg_idx_m) * N + thread_col * TN + reg_idx_n] =
162168
thread_results[reg_idx_m * TN + reg_idx_n];
163169
}
164170
}
@@ -174,20 +180,20 @@ nvcc -o sgemm_tiled2d sgemm_tiled2d.cu
174180
## 3. 性能测试
175181

176182
我们将上该内核的性能和之前的内核进行比较,我们分别计算 256x256、512x512、1024x1024、2048x2048 (Matrix 1、Matrix 2、Matrix 3、Matrix 4、Matrix 5)的矩阵乘法的性能 (us)。在 1080Ti 上运行,结果如下:
177-
178183

179-
| Algorithm | Matrix 1 | Matrix 2 | Matrix 3 | Matrix 4 |
180-
| --------- | -------- | -------- | -------- | -------- |
181-
| Naive | 95.5152 | 724.396 | 28424 | 228681 |
182-
| 共享内存缓存块 | 40.5293 | 198.374 | 8245.68 | 59048.4 |
183-
| 一维 Thread Tile | 35.215 | 174.731 | 894.779 | 5880.03 |
184-
| 二维 Thread Tile | 34.708 | 92.946 | 557.829 | 3509.920 |
184+
185+
| Algorithm | Matrix 1 | Matrix 2 | Matrix 3 | Matrix 4 |
186+
| ---------------- | -------- | -------- | -------- | -------- |
187+
| Naive | 95.5152 | 724.396 | 28424 | 228681 |
188+
| 共享内存缓存块 | 40.5293 | 198.374 | 8245.68 | 59048.4 |
189+
| 一维 Thread Tile | 35.215 | 174.731 | 894.779 | 5880.03 |
190+
| 二维 Thread Tile | 34.708 | 92.946 | 557.829 | 3509.920 |
185191

186192
## 4. 总结
187193

188194
本文我们介绍了二维 Thread Tile 并行优化方法。我们将矩阵乘法的计算任务分配给了二维线程块,每个线程块负责计算一个小的矩阵块。这样做的好处是可以充分利用共享内存,减少全局内存的访问次数,从而提高矩阵乘法的性能。
189195

190-
## Reference
196+
## Reference
191197

192198
1. https://siboehm.com/articles/22/CUDA-MMM
193199
2. https://space.keter.top/docs/high_performance/GEMM%E4%BC%98%E5%8C%96%E4%B8%93%E9%A2%98/%E4%BA%8C%E7%BB%B4Thread%20Tile%E5%B9%B6%E8%A1%8C%E4%BC%98%E5%8C%96

docs/11_gemm_optimize/01_tiled2d/sgemm_tiled2d.cu

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,22 @@
22
#include <cuda_runtime.h>
33
#include <cassert>
44

5-
#define CEIL_DIV(M, N) (((M) + (N)-1) / (N))
5+
#define CEIL_DIV(M, N) (((M) + (N) - 1) / (N))
6+
7+
template <typename T>
8+
void free_resource(T *p_cpu, T *p_cuda)
9+
{
10+
if (nullptr != p_cpu)
11+
{
12+
delete[] p_cpu;
13+
p_cpu = nullptr;
14+
}
15+
if (nullptr != p_cuda)
16+
{
17+
cudaFree(p_cuda);
18+
p_cuda = nullptr;
19+
}
20+
}
621

722
void sgemm_naive_cpu(float *A, float *B, float *C, int M, int N, int K)
823
{
@@ -155,7 +170,7 @@ int main(int argc, char *argv[])
155170

156171
// Allocate memory for matrices
157172
float *A, *B, *C, *C_ref;
158-
float *d_A, *d_B, *d_C, *d_C_ref;
173+
float *d_A, *d_B, *d_C;
159174

160175
A = new float[m * k];
161176
B = new float[k * n];
@@ -176,13 +191,11 @@ int main(int argc, char *argv[])
176191
cudaMalloc((void **)&d_A, m * k * sizeof(float));
177192
cudaMalloc((void **)&d_B, k * n * sizeof(float));
178193
cudaMalloc((void **)&d_C, m * n * sizeof(float));
179-
cudaMalloc((void **)&d_C_ref, m * n * sizeof(float));
180194

181195
// Copy matrices to device
182196
cudaMemcpy(d_A, A, m * k * sizeof(float), cudaMemcpyHostToDevice);
183197
cudaMemcpy(d_B, B, k * n * sizeof(float), cudaMemcpyHostToDevice);
184198
cudaMemcpy(d_C, C, m * n * sizeof(float), cudaMemcpyHostToDevice);
185-
cudaMemcpy(d_C_ref, C_ref, m * n * sizeof(float), cudaMemcpyHostToDevice);
186199

187200
run_sgemm_blocktiling_2d(d_A, d_B, d_C, m, n, k);
188201

@@ -219,5 +232,11 @@ int main(int argc, char *argv[])
219232
cudaEventElapsedTime(&elapsed_time, start, stop);
220233
float avg_run_time = elapsed_time * 1000 / 100;
221234
printf("Average run time: %f us\n", avg_run_time);
235+
236+
free_resource(A, d_A);
237+
free_resource(B, d_B);
238+
free_resource(C, d_C);
239+
free_resource(C_ref, (float *)nullptr);
240+
222241
return 0;
223242
}

0 commit comments

Comments
 (0)