{"id":13568568,"url":"https://github.com/wangzyon/NVIDIA_SGEMM_PRACTICE","last_synced_at":"2025-04-04T04:31:29.442Z","repository":{"id":132945388,"uuid":"465501913","full_name":"wangzyon/NVIDIA_SGEMM_PRACTICE","owner":"wangzyon","description":"Step-by-step optimization of CUDA SGEMM","archived":false,"fork":false,"pushed_at":"2022-03-30T23:56:15.000Z","size":1135,"stargazers_count":223,"open_issues_count":4,"forks_count":38,"subscribers_count":2,"default_branch":"master","last_synced_at":"2024-11-05T00:36:01.420Z","etag":null,"topics":["cuda","sgemm"],"latest_commit_sha":null,"homepage":"","language":"Cuda","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/wangzyon.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null}},"created_at":"2022-03-02T23:28:42.000Z","updated_at":"2024-10-31T01:37:46.000Z","dependencies_parsed_at":"2024-01-19T07:34:03.082Z","dependency_job_id":"f8b459d4-5f39-4329-9cb9-6db0eeee683e","html_url":"https://github.com/wangzyon/NVIDIA_SGEMM_PRACTICE","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wangzyon%2FNVIDIA_SGEMM_PRACTICE","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wangzyon%2FNVIDIA_SGEMM_PRACTICE/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wangzyon%2FNVIDIA_SGEMM_PRACTICE/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/wangzyon%2FNVIDIA_SGEMM_PRACTICE/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/wangzyon","download_url":"https://codeload.github.com/wangzyon/NVIDIA_SGEMM_PRACTICE/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247123072,"owners_count":20887259,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cuda","sgemm"],"created_at":"2024-08-01T14:00:28.352Z","updated_at":"2025-04-04T04:31:24.435Z","avatar_url":"https://github.com/wangzyon.png","language":"Cuda","funding_links":[],"categories":["Example Implementations 💡"],"sub_categories":["Blogs 🖋️"],"readme":"![](images/head.png)\n\n![](https://img.shields.io/badge/build-passing-brightgreen) ![](https://img.shields.io/badge/ubuntu-18.04-blue) ![](https://img.shields.io/badge/cuda-10.2-blue) ![](https://img.shields.io/badge/nvidia-RTX3090-blue) ![](https://img.shields.io/badge/cmake-3.21-blue)\n\n\n\n# 概述\n\n面向NVIDIA GPU，使用CUDA编程逐步优化矩阵乘法运算性能：\n\n| 核函数   | 描述                    | GFLOPS   | 自定义核函数/CUBLAS（%） |\n| -------- | ----------------------- | -------- | ------------------------ |\n| CUBLAS   | 官方库函数              | 14448.69 | 基准                     |\n| kernel_1 | 朴素实现                | 2262.168 | 15.65657                 |\n| kernel_2 | 共享内存缓存            | 4216.536 | 29.18283                 |\n| kernel_3 | 一维Thread Tile并行优化 | 7809.629 | 54.05078                 |\n| kernel_4 | 二维Thread Tile并行优化 | 12251.3  | 84.79179                 |\n| kernel_5 | 寄存器缓存              | 12177.95 | 84.28412                 |\n| kernel_6 | FLOAT4向量访存          | 13161.49 | 91.09125                 |\n| kernel_7 | 双缓存预取              | 13634.98 | 94.36832                 |\n\n\u003e NVIDIA GeForce RTX 3090，矩阵尺寸5120\n\n# 配置\n\n- 编译采用 `gcc 7.5.0` under Ubuntu 18.04.5 LTS\n- NVIDIA CUDA version: `CUDA 10.2`；\n\n# 目录\n\n```\nNVIDIA_SGEMM_PRACTICE                                   # 根目录\n    ├── images                                          # 图片结果\n    │     ├── describe_kernel_1.png  \n    │     ├── describe_kernel_x.png\n    │     └── kernel_x_vs_y.png\n    ├── test                                            # 测试结果\n    │     ├── test_kernel_0.txt \n    │     ├── test_kernel_1.txt \n    │     └── test_kernel_x.txt \n    └── src                                             # 源文件\n    │    ├── kernel\n    │    │  ├── kernel_1.cuh                            # 声明和定义\n    │    │  ├── kernel_2.cuh\n    │    │  └── kernel_x.cuh\n    │    ├── kernel.cuh\n    │    ├── utils.cuh                                  # 辅助函数\n    │    └── utils.cu\n    ├── plot.py                                         # 根据test结果绘图\n    ├── run.sh                                          # 运行编译后可执行文件\n    ├── sgemm.cu                                        # 主程序\n    └── CMakeLists.txt                                  # 编译相关\n```\n\n# 运行\n1. 配置NVCC编译参数\n\u003e 在CMakeLists.txt中修改`set(CUDA_NVCC_FLAGS -arch=compute_70;-code=compute_70)`\n2. 配置矩阵计算最大尺寸\n\u003e 在`sgemm.cu:16`中修改`size_len`，建议初次运行设置为16，过大尺寸可能导致电源超负荷主机重启；\n3. 编译\n`cd build \u0026\u0026 cmake .. \u0026\u0026 make`\n4. 运行run.sh，统计各个核函数计算效率，结果保存在test目录；\n5. 计算效率折线绘图\n\n\u003e `python plot.py 0 1`表示绘制CUBLAS和kernel_1计算效率对比图；\n\n# 逐步优化\n\n##  kernel 1 \n\n**Naive基础版矩阵乘法实现**\n\n将每个逻辑线程与矩阵C的每一个元素相对应，每个线程负责C中一个元素的计算；\n\n![](./images/describe_kernel_1.png)\n\n```cpp\n__global__ __launch_bounds__(1024) void\nmysgemm_v1(int M, int N, int K, float alpha, float *A, float *B, float beta, float *C) {\n\n    int gx = blockIdx.x * blockDim.x + threadIdx.x; // 全局x\n    int gy = blockIdx.y * blockDim.y + threadIdx.y; // 全局y\n\n    float tmp = 0.;\n    for (int i = 0; i \u003c K; i++) {\n        tmp += A[gy * K + i] * B[i * N + gx]; // 两次全局内存访问和一次FMA（累加乘）\n    }\n    C[gy * N + gx] = alpha * tmp + beta * C[gy * N + gx];\n}\n```\n\n![](./images/kernel_culas_vs_1.png)\n\n未经过优化的矩阵乘法性能不足CUBLAS的1/10，具体分析如下；\n\n- 计算访存比：每次迭代需要进行一次FMA（乘累加）和两次全局内存读取，计算访存比1/2；\n- 访存量：访问全局内存，C矩阵每个元素计算需要访问`2K`个单精度浮点数，完成全部计算需要` 2*K*M*N`；\n\n全局内存访问延迟高（几百cycle），同时相同位置元素被重复读取（C中同一行元素计算共享A中同一行元素，C中同一列元素计算共享B中同一列元素），另一方面，较低的计算访存比无法有效隐藏访存延迟，因此，访存延迟和计算访存比是导致kernel 1效率低下的原因。\n\n## kernel 2\n\n**利用共享内存缓存减少全局内存访存量和访存延迟**\n\n访存延迟来自于全局内存的高延迟和全局内存的重复访问。共享内存是片上内存，具有较低的访存延迟（几十cycle）,使用共享内存进行缓存可降低访存延迟；\n\n![](./images/describe_kernel_2.png)\n\n\u003e BM和BN表示block tile的高和宽，BK表示待缓存的全局内存的步长，即一个block的计算需要缓存K/BK次；\n\n共享内存缓存全局内存A tile和B tile，完成C block中所有元素的FMA计算，不断滑动缓存区域，更新block；\n\n```cpp\n/*\ndim3 blockDim(1024);\ndim3 gridDim(CEIL_DIV(M, 32), CEIL_DIV(N, 32));\nmysgemm_v2\u003c32\u003e\u003c\u003c\u003cgridDim, blockDim\u003e\u003e\u003e(M, N, K, alpha, A, B, beta, C);\n*/\n\ntemplate\u003cconst int BLOCK_SIZE\u003e\n__global__ void mysgemm_v2(int M, int N, int K, float alpha, float *A, float *B, float beta, float *C) {\n    int bx = blockIdx.x;\n    int by = blockIdx.y;\n\n    const int BM = BLOCK_SIZE;\n    const int BN = BLOCK_SIZE;\n    const int BK = BLOCK_SIZE;\n    \n    int tx = threadIdx.x % BN;\n    int ty = threadIdx.x / BN;\n\n    // 申请共享内存空间\n    __shared__ float As[BM * BK];\n    __shared__ float Bs[BK * BN];\n\n    // 移动到当前block\n    A = \u0026A[by * BM * K];\n    B = \u0026B[bx * BN];\n    C = \u0026C[by * BM * N + bx * BN];\n\n    float tmp = 0.;\n    for (int k = 0; k \u003c K; k += BK) {\n        // 缓存A_tile和B_tile\n        As[ty * BK + tx] = A[ty * K + tx];\n        Bs[ty * BN + tx] = B[ty * N + tx];\n        // 同步所有线程缓存完成\n        __syncthreads();\n        A += BK;\n        B += BK * N;\n        for (int i = 0; i \u003c BK; i++) {\n            tmp += As[ty * BK + i] * Bs[i * BN + tx];\n        }\n        // FMA计算需要读取缓存数据，在新一轮写入缓存前进行同步，确保所有线程计算完成\n        __syncthreads();\n    }\n    C[ty * N + tx] = alpha * tmp + beta * C[ty * N + tx];\n}\n```\n\n![](./images/kernel_1_vs_2.png)\n\n- 访存量：每个block需要从global memory中读取`(K/BK)*(BM*BK+BK*BN)`个单精度浮点数，整个C存在`(M/BM)*(N/BN)`个block，因此完成C中所有元素计算需要读取`(M/BM)*(N/BN)*(K/BK)*(BM*BK+BK*BN)`个单精度浮点数\n\nkernel 1受限于全局内存的访存延迟和重复访问，优化前全局访存量为`2*K*M*N`，共享内存缓存优化后，访存量减少为原来的`1/2*(1/BN)*(1/BM)`,当`BN=BM=32`时，访存减少至1/32；另一方面shared memory访存延迟远低于全局内存，因此计算效率得到了一定程度的提升。\n\n## kernel 3\n\n**利用一维thread tile优化**\n\n已知可以通过增加block大小（BM，BN）值，进一步降低全局内存的访问量，因此将BM和BN从32提升至64；\n\n\u003e **是否能通过无限增加block size降低全局访存？**\n\u003e\n\u003e 不能，一方面，block分块矩阵尺寸过大，block数量减少，这样会造成大量 SM（Streaming Multiprocessor）的闲置浪费；另一方面，BN和BM的增加，需要申请更多的共享内存，单线程内共享内存占用越多，活跃线程束越少，不利于隐藏指令延迟；\n\n因此，在增加BM和BN值的同时，为了减少共享内存占用，一方面减小BK值，降低为8；\n\n\u003e 当增加block size时，应尤其注意共享内存的消耗，限制共享内存尺寸和block中线程的数量，避免因资源不足无法启动核函数\n\n![](./images/describe_kernel_3_1.png)\n\n另一方面，通过共享内存缓存减少了全局内存访存量和FMA乘累加的访存延迟，但计算访存比没有得到改善，每次迭代计算都需要两个访存指令和一个计算指令，因此，引入thread tile，即一个线程负责block中多个元素的计算，TM和TN分别表示thread tile的高和宽。\n\n![](./images/describe_kernel_3_2.png)\n\n```cpp\n/*\ndim3 blockDim(512);\ndim3 gridDim(CEIL_DIV(M, 64), CEIL_DIV(N, 64));\nmysgemm_v3\u003c64, 64, 8, 8\u003e\u003c\u003c\u003cgridDim, blockDim\u003e\u003e\u003e(M, N, K, alpha, A, B, beta, C);\n*/\n\n\ntemplate\u003cconst int BM,\n        const int BN,\n        const int BK,\n        const int TM\u003e\n__global__ void mysgemm_v3(int M, int N, int K, float alpha, float *A, float *B, float beta, float *C) {\n    int bx = blockIdx.x;\n    int by = blockIdx.y;\n    int thread_num = BM * BN / TM; // 一个线程负责block中计算TM个元素\n\n    int tx = threadIdx.x % BN;\n    int ty = threadIdx.x / BN * TM;\n\n    __shared__ float As[BM * BK];\n    __shared__ float Bs[BK * BN];\n\n    // 移动到当前block\n    A = \u0026A[by * BM * K];\n    B = \u0026B[bx * BN];\n    C = \u0026C[by * BM * N + bx * BN];\n\n    /*\n    当前线程负责搬运全局内存中第a_tile_row行，第a_tile_col列元素至共享内存第a_tile_row行，第a_tile_col列\n    a_tile_stride表示block中线程可搬运a_tile_stride行至共享内存；\n\n    若BM=64,BK=8,thread_num=512,则a_tile_stride=64,a_tile_stride=BM，表示每个线程搬运一轮即可完成所需元素的搬运;\n    若BM=128,BK=8,thread_num=512,则a_tile_stride=64,表示每个线程搬运两轮即可完成所需元素的搬运;\n    */\n    int a_tile_row = threadIdx.x / BK;\n    int a_tile_col = threadIdx.x % BK;\n    int a_tile_stride = thread_num / BK;\n\n    int b_tile_row = threadIdx.x / BN;\n    int b_tile_col = threadIdx.x % BN;\n    int b_tile_stride = thread_num / BN;\n\n    float tmp[TM + 1] = {0.}; // 每个线程负责TM个元素，则需要申请TM个寄存器保存累加值，额外的一个寄存器用于缓存；\n    #pragma unroll\n    for (int k = 0; k \u003c K; k += BK) {\n        #pragma unroll\n        for (int i = 0; i \u003c BM; i += a_tile_stride) {\n            As[(a_tile_row + i) * BK + a_tile_col] = A[(a_tile_row + i) * K + a_tile_col];\n        }\n        #pragma unroll\n        for (int i = 0; i \u003c BK; i += b_tile_stride) {\n            Bs[(b_tile_row + i) * BN + b_tile_col] = B[(b_tile_row + i) * N + b_tile_col];\n        }\n        __syncthreads();\n        A += BK;\n        B += BK * N;\n        #pragma unroll\n        for (int i = 0; i \u003c BK; i++) {\n            tmp[TM] = Bs[tx + i * BN]; // 额外的一个寄存器，避免反复从共享内存中读取Bs[tx + i * BN]\n            #pragma unroll  // 循环展开，增加指令并行度\n            for (int j = 0; j \u003c TM; j++) {\n                tmp[j] += As[(ty + j) * BK + i] * tmp[TM];\n            }\n        }\n        __syncthreads();\n    }\n    #pragma unroll\n    for (int j = 0; j \u003c TM; j++) {\n        C[(ty + j) * N + tx] = alpha * tmp[j] + beta * C[(ty + j) * N + tx];\n    }\n}\n```\n\n![](./images/kernel_2_vs_3.png)\n\n本例从两方面进行优化：\n\n- 全局内存访存量：相比于初始版本，通过对`64*64`block size进行缓存，访存量降至1/64；\n- 计算访存比：引入thread tile，利用单个线程负责多个元素计算，增加计算访存比；当TM=8时，每执行共享内存As的8个次访存指令和共享内存Bs的1个访存指令，可执行8次计算指令，相比初始版本的计算访存比1:2，提高至8:9，有效隐藏访存延迟；\n\n通过本例的两方面优化，矩阵乘法计算效率显著提高近一倍；\n\n## kernel 4\n\n**利用二维thread tile优化**\n\n将thread tile设置为二维，即一个线程负责一小块元素的计算，从而进一步增加block尺寸，减少全局访存数量；\n\n\u003e  增加thread tile尺寸，可以在相同的线程数量或更少的线程数量下，计算更大的block size;\n\n更重要的是，单线程负责计算更多的C元素区域，可以增加指令级并行程度；\n\n\u003e 为什么可以提高指令并行程度？\n\u003e\n\u003e 单线程处理的指令数量越多，流水线级越长，由于单线程流水线可并行处理多条指令，虽然单条指令执行变慢，但单位时间内处理的指令数量变多，提高了吞吐量，隐藏指令延迟；指令级并发相比与线程级并发更具优势。\n\n![](./images/describe_kernel_4.png)\n\n设置一个线程负责8×8区域内元素计算，即thread tile=8×8，TM=8,TN=8；\n\n```cpp\n// BM=BN=128，BK=8，TM=TN=8，共享内存大小128*8\ndim3 blockDim(256);\ndim3 gridDim(CEIL_DIV(M, 128), CEIL_DIV(N, 128));\nmysgemm_v4\u003c128, 128, 8, 8, 8\u003e\u003c\u003c\u003cgridDim, blockDim\u003e\u003e\u003e(M, N, K, alpha, A, B, beta, C);\n\n    int a_tile_row = threadIdx.x / BK;\n    int a_tile_col = threadIdx.x % BK;\n    int a_tile_stride = thread_num / BK;  // 128*8/256=4，需要所有线程搬运4轮，可将全局内存中128*8大小区域搬运至共享内存\n\n    int b_tile_row = threadIdx.x / BN;\n    int b_tile_col = threadIdx.x % BN;\n    int b_tile_stride = thread_num / BN;\n\n// 每个线程负责TM*TN个元素，则需要申请TM*TN个寄存器保存累加值；\nfloat tmp[TM][TN] = {0.}; \n\n// 单个线程循环TM，TN完成thread tile内元素的乘累加\nfor (int j = 0; j \u003c TM; j++) {\n    for (int l = 0; l \u003c TN; l++)\n        tmp[j][l] += As[(ty + j) * BK + i] * Bs[tx + l + i * BN];\n}\n```\n\n全局访存量：相比未引入共享内存缓存版本，全局内存访存量减少至`1/2*(1/BM+1/BN)=1/128`,访存量显著降低。\n\n![](./images/kernel_3_vs_4.png)\n\n实际测试发现，相比与一维thread tile，由于二维thread tile进一步降低了全局访存量、提升计算访存比，矩阵乘法效率显著提升一倍。\n\n## kernel 5\n\n**寄存器缓存共享内存**\n\n![](./images/describe_kernel_5.png)\n\n由下方代码可知，单个线程计算thread tile元素乘累加时，共享内存会被重复访问。\n\n```cpp\nfor (int j = 0; j \u003c TM; j++) {\n    for (int l = 0; l \u003c TN; l++)\n        tmp[j][l] += As[(ty + j) * BK + i] * Bs[tx + l + i * BN];  //内层循环中 As[(ty + j) * BK + i] 重复访问TN次\n}\n```\n\n共享内存相比全局内存能够大大减少访存延迟，但共享内存延迟（几十cycle）相比于计算延迟（几cycle）仍然较大，因此，采用寄存器对共享内存As、Bs进行缓存，避免共享内存的重复访问；\n\n```cpp\nfloat a_frag[TM] = {0.};\nfloat b_frag[TN] = {0.};\n\nfor (int i = 0; i \u003c BK; i++) {\n    for (int j = 0; j \u003c TM; j++) {\n        a_frag[j] = As[(ty + j) * BK + i];     // 采用a_frag寄存器数组缓存thread tile所需的As共享内存数据；\n    }\n    for (int l = 0; l \u003c TN; l++) {\n        b_frag[l] = Bs[tx + l + i * BN];       // 采用b_frag寄存器数组缓存thread tile所需的Bs共享内存数据；\n    }\n    for (int j = 0; j \u003c TM; j++) {\n        for (int l = 0; l \u003c TN; l++)\n            tmp[j][l] += a_frag[j] * b_frag[l];\n    }\n}\n```\n\n当TM=TN=8时，经过寄存器缓存，每个thread tile需要执行8个As共享内存访存指令和8个Bs共享内存访存指令，可进行8×8=64个计算指令，计算访存比相比于初始版本的1/2提升至64:16，可有效隐藏访存延迟；\n\n![](./images/kernel_4_vs_5.png)\n\n实际测试发现，经寄存器缓存实际性能并未发生明显变化，原因可能是当前性能瓶颈并非共享内存的重复访问；\n\n## kernel 6\n\n**向量内存指令FLOAT4优化**\n\n- 计算指令：GPU是以4维向量为基本单位进行计算的，4个浮点数组成的float4向量是GPU最基本的类型，使用GPU对两个float4进行向量计算与对两个整数或两个浮点数进行计算一样，只需要一个指令即可完成；\n- 内存指令：与发出单个指令生成单独的内存事务获取相同数量的字节相比，通过向量内存指令所需的内存事务更少，减少了内存控制器的争用；另一方面，使用矢量加载每个字节需要更少的索引计算；\n\n![](./images/describe_kernel_6.png)\n\n例如，BM=128，BK=8，线程数量为256，若每个线程每次取1个浮点数，每个线程需要消耗4次内存指令，才能将全局内存搬运至共享内存，若采用float4向量内存指令，每个线程每次可以搬运4个浮点数，则每个线程仅需要执行一次内存指令即可完成搬运。\n\n关键代码示例如下：\n\n```cpp\n#define OFFSET(row, col, ld) ((row)*(ld)+(col))\n#define FETCH_FLOAT4(pointer) (reinterpret_cast\u003cfloat4*\u003e(\u0026(pointer))[0])\n\nfloat ldg_a_reg[4 * ldg_a_num] = {0.}; // 每个线程搬运ldg_a_num轮，寄存器缓存ldg_a_num个float4元素，用于转置As矩阵\n\n//  共享内存缓存全局内存\nfor (int i = 0; i \u003c BM; i += a_tile_stride) {\n    int ldg_index = i / a_tile_stride * 4;  // 第ldg_index轮\n    FETCH_FLOAT4(ldg_a_reg[ldg_index]) =\n            FETCH_FLOAT4(A[OFFSET(a_tile_row + i, a_tile_col, K)]);\n    // As转置存，其中ldg_a_reg做中间缓存，目的是读取时可以按FLOAT4读取\n    As[OFFSET(a_tile_col, i + a_tile_row, BM)] = ldg_a_reg[ldg_index];\n    As[OFFSET(a_tile_col + 1, i + a_tile_row, BM)] = ldg_a_reg[ldg_index + 1];\n    As[OFFSET(a_tile_col + 2, i + a_tile_row, BM)] = ldg_a_reg[ldg_index + 2];\n    As[OFFSET(a_tile_col + 3, i + a_tile_row, BM)] = ldg_a_reg[ldg_index + 3];\n}\n\nfor (int i = 0; i \u003c BK; i += b_tile_stride) {\n    FETCH_FLOAT4(Bs[OFFSET(b_tile_row + i, b_tile_col, BN)]) =\n        FETCH_FLOAT4(B[OFFSET(b_tile_row + i, b_tile_col, N)]); // 不需要转置\n}\n\n\n// 寄存器缓存共享内存\n// ty,tx为当前线程对应thread tile的左上角元素在block中的位置\n#pragma unroll\nfor (int m = 0; m \u003c TM; m += 4) {\n    FETCH_FLOAT4(a_frag[m]) = FETCH_FLOAT4(As[OFFSET(i, ty + m, BM)]); // 偏移到当前thread tile\n}\n#pragma unroll\nfor (int n = 0; n \u003c TN; n += 4) {\n    FETCH_FLOAT4(b_frag[n]) = FETCH_FLOAT4(Bs[OFFSET(i, tx + n, BN)]); // 偏移到当前thread tile\n}\n```\n\n全局内存无法直接写入共享内存，需要寄存器做中介，其中As写入将全局内存-\u003e将寄存器-\u003e共享内存过程显示的描述出来，而Bs写入并不是不需要寄存器参与，只是编译器隐藏了这段代码；As缓存显示运用寄存器的目的在于将As进行转置，转置前的一列在转置后变成一行，内存连续，便于float4读取；\n\n![kernel_1](./images/kernel_5_vs_6.png)\n\n实际测试，整体计算效率增加；\n\n## kernel 7\n\n**数据预取**\n\n单缓存是指申请单块共享内存，缓存全局数据，申请单块寄存器内存，缓存共享数据，单块缓存不能实现读取和存储并行进行，因为数据之间存在依赖。例如单缓存场景，计算依赖共享内存数据，为保证计算前全局内存完全存入共享内存，需要进行一次同步；同样因为计算依赖共享内存数据，所以在存新一轮全局内存到共享内存前也需要进行一次同步，保证上一轮计算完成。\n\n双缓存通过申请双倍存储空间，将读和写分开，计算数据读取一块存储空间同时，可以同时向另一块内存写入下一轮依赖的数据，因此，只需要保证计算前待读取共享内存完成写入，即一次同步即可。\n\n\u003e 双缓存使读写同步进行，实现数据预取，隐藏内存延迟。\n\n![](./images/describe_kernel_7.png)\n\n![](./images/kernel_6_vs_7.png)\n\n采用双缓存技术实现数据预取，计算效率得到了进一步提升；\n\n![](./images/kernel_culas_vs_7.png)\n\n基本可以接近CUBLAS官方矩阵乘法的计算效率；\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwangzyon%2FNVIDIA_SGEMM_PRACTICE","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fwangzyon%2FNVIDIA_SGEMM_PRACTICE","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fwangzyon%2FNVIDIA_SGEMM_PRACTICE/lists"}