张量 矩阵乘法优化

张量 矩阵乘法优化
在SIMT架构下, 不使用TensorCore进行矩阵乘法,计算所需要的访存相关的优化。通过逐步迭代优化,深入理解GPU的性能特征和内存访问优化。
测试环境为一块A10 GPU,  驱动版本: 550.54.15, CUDA版本: 12.4 . 矩阵M=N=K=4092,见表6-5。
表6-5 cuBLAS调用,在每种大小下调用的内核

内核

GFLOPs/s

相对于cuBLAS的性能

0: cuBLAS

14765.9

100.0%

1: Naive

588.5

3.9%

2: GMEM Coalescing

1165.7

7.9%

3: SMEM Caching

2166.7

14.6%

4: 1D Blocktiling

6082.0

41.2%

5: 2D Blocktiling

11279.0

76.4%

6: Vectorized Mem Access

12861.4

87.1%

7: WarpTiling

14766.3

100.0%

 
6.4.1 cuBLAS基线
1. cuBLAS基线概述
采用cuBLAS作为性能测试基线, 测试环境是一张A10的推理卡, 测试矩阵规模如下:
 

   (6-11)

const int M = 4092;
const int K = 4092;
const int N = 4092;
float alpha = 1.0f;
float beta = 0.5f;
cuBLAS测试代码如下所示:
#include <stdio.h>
#include <stdlib.h>
#include <cublas_v2.h>
#include "util.h"

int main()
{
  cudaError_t cudaStat;  // cudaMalloc status
  cublasStatus_t stat;   // cuBLAS functions status
  cublasHandle_t handle; // cuBLAS context

  stat = cublasCreate(&handle); // initialize CUBLAS context

  float *d_a, *d_b, *d_c;
  cudaMalloc(&d_a, M * K * sizeof(float));
  cudaMalloc(&d_b, K * N * sizeof(float));
  cudaMalloc(&d_c, M * N * sizeof(float));

  cudaEvent_t start, end;
  cudaEventCreate(&start);
  cudaEventCreate(&end);

  cudaEventRecord(start);
  for (int i = 0; i < ITER; i++)
    stat = cublasSgemm(handle,
                       CUBLAS_OP_N, CUBLAS_OP_N,
                       N, M, K,
                       &alpha, d_b, N,
                       d_a, K, &beta, d_c, N);
  cudaEventRecord(end);
  cudaEventSynchronize(end);

  float msec;
  cudaEventElapsedTime(&msec, start, end);

  long workload = long(M) * N * K * 2 * ITER;
  double avg_Gflops = ((double)workload / 1e9) / (double(msec) / 1e3);
  printf("cuBLAS AveragePerformance  %10.1lf Gflops\n", avg_Gflops);

  cudaFree(d_a);
  cudaFree(d_b);
  cudaFree(d_c);
  cublasDestroy(handle); // destroy CUBLAS context
}
2. 简单实现
按照三层的循环结构进行编程。从结果C矩阵来看, 可以编排每个线程负责一个位置的值。
  

 (6-12)

__global__ void gmem_coalescing_gemm(int M, int N, int K, float alpha, const float *A, float beta, float *C)
{
  //交换矩阵C的X/Y索引
  const uint y = blockIdx.x * blockDim.x + threadIdx.x;
  const uint x = blockIdx.y * blockDim.y + threadIdx.y;

  if (x < M && y < N)
  {
    float tmp = 0.0;
    for (int i = 0; i < K; ++i)
    {
      tmp += A[x * K + i] * B[i * N + y];
    }
    // C = α*(A@B)+β*C
    C[x * N + y] = alpha * tmp + beta * C[x * N + y];
   
  }
}

void launch_gemm(int M, int N, int K, float alpha, const float *A,
                 const float *B, float beta, float *C)
{
  //交换Grid编排
  dim3 gridDim(ceil(N / 32), ceil(M / 32), 1);
  dim3 blockDim(32, 32, 1);
  gmem_coalescing_gemm<<<gridDim, blockDim>>>(M, N, K, alpha, A, B, beta, C);
}
简单交换一下后性能可以提升到1165.7GFlops. 通过Profiling可以看到,在简单实现中访问内存带宽为52.29GB/s。
将沿着 A 的列和 B 的行移动块,对 C 执行部分求和,直到计算出结果。
template <const int CHUNK_SIZE>
__global__ void sgemm_shared_mem_block(int M, int N, int K, float alpha,
                                       const float *A, const float *B,
                                       float beta, float *C) {
  // 矩阵C按照BLOCK划分, cRow和cCol为该线程对应块的行号和列号
  const uint cRow = blockIdx.x;
  const uint cCol = blockIdx.y;

  // 分配共享内存, 共享内存可以被Block内所有的线程访问
  __shared__ float As[CHUNK_SIZE * CHUNK_SIZE];
  __shared__ float Bs[CHUNK_SIZE * CHUNK_SIZE];

  // BLOCK内线程在启动内核的时候分配的blockdim仅有一个维度
  // 通过threadIdx找到线程在BLOCK内部对应的行和列
  const uint threadCol = threadIdx.x % CHUNK_SIZE;
  const uint threadRow = threadIdx.x / CHUNK_SIZE;

  // 基于cRow和cCol计算矩阵开始的指针位置
  A += cRow * CHUNK_SIZE * K;                    // row=cRow, col=0
  B += cCol * CHUNK_SIZE;                        // row=0, col=cCol
  C += cRow * CHUNK_SIZE * N + cCol * CHUNK_SIZE; // row=cRow, col=cCol

  float tmp = 0.0;
  for (int bkIdx = 0; bkIdx < K; bkIdx += CHUNK_SIZE) {
    // 每个线程加载A和B的一个元素, 由于threadIdx.x是一个连续的分布
    // 因此访问GMEM是可以合并的
    As[threadRow * CHUNK_SIZE + threadCol] = A[threadRow * K + threadCol];
    Bs[threadRow * CHUNK_SIZE + threadCol] = B[threadRow * N + threadCol];

    // 同步等待所有thread完成数据加载
    __syncthreads();

    //将数据移动到下一个CHUNK
    A += CHUNK_SIZE;
    B += CHUNK_SIZE * N;

    // 进行BLOCK Level的内积计算
    for (int dotIdx = 0; dotIdx < CHUNK_SIZE; ++dotIdx) {
      tmp += As[threadRow * CHUNK_SIZE + dotIdx] *
             Bs[dotIdx * CHUNK_SIZE + threadCol];
    }
    // 考虑Cache影响, 需要在执行下一次加载时再进行一次同步
    __syncthreads();
  }
  C[threadRow * N + threadCol] =
      alpha * tmp + beta * C[threadRow * N + threadCol];
}

 
void launch_gemm(int M, int N, int K, float alpha, const float *A,
                 const float *B, float beta, float *C)
{
  dim3 gridDim(CEIL_DIV(M, 32), CEIL_DIV(N, 32));
  dim3 blockDim(32 * 32);

  sgemm_shared_mem_block<32>
      <<<gridDim, blockDim>>>(M, N, K, alpha, A, B, beta, C);
}
测试可以看到性能提升到2166GFLOPS, 同时,通过Profiling看到,访问内存的请求转移到了SMEM,如图6-38所示。
图6-38 代码测试性能模块分析

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/800210.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

通用矩阵乘法执行

通用矩阵乘法执行 使用两个手工实现的纯粹GEMM和分块GEMM的例子来解释矩阵分块乘法的原理和性能影响, 可以看到性能差距接近53倍. 按照测试的A10 GPU峰值FP32算力31TFFLOPS来算, 最朴素的算法由于访存效率的问题, 浮点算力仅为峰值的1%。 # ./naive AveragePerformance 0.233…

交易柜台系统技术名词

目录交互示意图柜台API前置机行情和交易接口生产环境服务器托管(Co-location)什么是高频交易 (HFT)?交互示意图 程序化交易用户是如何与期货公司、交易所进行信息交互的?柜台 依据国内监管要求,客户无法直连交易所系统,中间必须经过期货公司(Broker)的系统,这便是柜台系…

全网最适合入门的面向对象编程教程:50 Python函数方法与接口-接口和抽象基类

在Python中,接口和抽象基类(Abstract Base Classes, ABCs)都用于定义类的结构和强制子类实现特定的方法,Python 没有内建的接口机制,但可以通过抽象基类(ABC)来模拟接口的行为。全网最适合入门的面向对象编程教程:50 Python 函数方法与接口-接口和抽象基类摘要: 在 Py…

javafx jlink 遇到的非模块化的依赖打包报错“模块异常”的问题和处理

javafx jlink 遇到的问题和处理 简介 javafx:jlink 是 javafx-maven-plugin 插件中的一个目标,用于创建一个自包含的 JavaFX 应用程序运行时映像。这个目标利用 Java 的 jlink 工具来生成一个包含应用程序及其所有依赖的定制化运行时映像,从而简化部署和分发。创建自包含运行…

The minimum required version for Powerlevel10k is 5.1

目录一、背景二、原因三、解决1、安装 ZSH 最新版本2、效果3、下载了还是显示 ZSH 版本为 5.0.2 怎么办 一、背景 安装 ZSH 主题 Powerlevel10k 时报错:You are using ZSH version 5.0.2. The minimum required version for Powerlevel10k is 5.1. Type echo $ZSH_VERSION to …

Python pycryptodome类库使用学习总结

AES数据加解密 以下代码生成一个新的AES-128密钥,并将一段数据加密到一个文件中。我们使用 CTR 模式(这是一种 经典操作模式, 简单但不再推荐)。 仅使用CTR,接收者无法检测到密文(即加密数据)在传输过程中是否被修改。为了应对这种风险,例中还附加了一个MAC身份验证标签…

电脑设置系统不自动更新

1、win + R 2、计算机\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\WindowsUpdate\UX\StateVariables 3、右边空白处右击 -> 新建 -> DWORD值,命名为FlightSettingsMaxPauseDays,点击基数选择十进制,数值设置为9999(表示不更新的天数)

同花顺--涨停板改变颜色

复制以下代码 IF(C>=REF(C,1)*1.095 AND C=H) RETURN "涨停"; 然后进行操作: 1、打开同花顺软件,右击K线,单击修改K线2、光标挪到代码首行行首,回车换行3、粘贴一下4、点击设置标志5、命名为涨停,选颜色,填充打勾6、点击确定

关于零值和nil

1. 零值 零值是指当你声明变量(分配内存)并未显式初始化时,始终为你的变量自动设置一个默认初始值的策略。 对于值类型:布尔类型为 false, 数值类型为 0,字符串为 "",数组和结构会递归初始化其元素或字段,即其初始值取决于元素或字段。 对于引用类型: 均为 n…

利用AutoGpt将任何模型支持o1模型的推理实现

利用AutoGpt将任何模型支持o1模型的推理实现 相信大家都对于OpenAI最新出的o1模型都非常关注,它已经能通过推理让回复的效果更加理想, 但是目前o1的限制太大,而且使用o1至少也是需要购买OpenAI官方的会员价格也在20美刀(好贵!!),于是乎社区出现非常多相似的实现,通过更…

C语言类型与强制类型转换

目录类型关键字sizeof如何理解强制类型转化不同类型的0null字符设备(补充) char有有符号和无符号两种类型,字符是无符号类型.(补充) getchar的返回值为什么是int键盘输入的内容,以及往显示器中打印的内容,都是字符 --> 键盘/显示器称为字符设备 类型C语言为何有类型? 让我们…

如何在 ASP.NET Core Web API 方法执行前后 “偷偷“ 作一些 “坏“ 事?初识 ActionFilterAttribute

ActionFilterAttribute 是一种作用于控制器 Action 方法的特性(Attribute),通过它,你可以在操作执行前后、异常处理时等不同的阶段插入自定义逻辑。 比如在执行操作方法之前修改请求参数、记录日志、进行权限验证等操作,在执行操作方法之后发送邮件、同步数据等等。 本文主…