发表博客之:cutlass demo讲解,在 sm75 机器上用 cuda core计算 fp32 矩阵乘法!对cutlass 感兴趣的看客别走开!!

文章目录

发表博客之:cutlass demo讲解,在 sm75 机器上用 cuda core计算 fp32 矩阵乘法!对cutlass 感兴趣的看客别走开!!

  • 各位老板好,我今儿要给各位演示一下,在 sm75 机器上用 cuda core计算 fp32 矩阵乘法!
    • 同时和cublas比较性能呢!
  • 由于sm75机器上没有tf32 的tensor core,因此在sm75 如T4机器上计算sgemm,只能用cuda core计算!
  • 下面我将逐步解释代码,要让小白都能听得懂!

  • 估计很多用户想立刻把我写的这个demo先跑起来看看是啥效果,那就请你把我下面的代码放到文件里面
  • 文件起个名字叫A.cu
  • 然后用 编译命令

nvcc A.cu -o a.out -arch sm_75 -lcublas -I /root/cutlass/include/ -std=c++17
/root/cutlass是cutlass仓库的路径,如果cutlass仓库在别的目录下,请小可爱你自己修改!

代码里面比较了cublascutlass的性能,请各位看官自己测试下!我测试的性能是下面这样的,你也快来测试下吧!

T4,fp32,性能

MNK cublas cutlass
512 * 512 * 512 1.316768 ms 1.597312 ms
128 * 4096 * 4096 15.638016 ms 20.250624 ms

  • 下面开始看我写的代代码吧!请用户先看 void init函数以及它之前的部分。
  • 首先映入眼帘的是一些头文件和宏定义,其中宏定义WARMUP和REPEATE是用来测试性能的!
  • init函数用来初始化矩阵,这里用随机数初始化!

  • 紧接着是下面的函数,CutlassSgemmNN,这个函数就是将cutlass提供的device级别的Gemm类以及与其相关的一些函数封装了一下。
  • 这个函数假设A,B,C都是column major的哦!请你一定要注意哦!
    • 他的参数有M,N,K alpha,beta,A,B,C,都是好理解的参数
    • lda表示啥呢?
      • (1)如果A是row major,他表示的是A矩阵的第[0][0][1][0]之间的物理距离,也就是两行之间同一个元素间的距离!
      • (2)如果A是col major,他表示的是A矩阵的第[0][0][0][1]之间的物理距离,也就是两列之间同一个元素间的距离!
        • 有人很好奇,卧槽,这个lda不就应该是M吗?其实不一定哦,你想一下如果A矩阵是某个更大矩阵的子矩阵呢!是不是就不能说他是M啦!
      • 这里由于我们假设A是col major,lda的含义就是(2)了!并且由于我这个例子里面 A A A是一个完整的矩阵,因此lda其实就是M啦!

  • 下面就是main函数啦!
  • 我们构建了一个a和一个b和一个c,我们在cpu上计算 c = a ∗ b c=a*b c=a∗b(也就是代码里面baseline的计算哦!)时候是将abc都看成row major的矩阵的!
  • 可以我们将要调用的CutlassSgemmNN默认输入输出都是col major的呢,这个咋办呀?
  • 这里面涉及到一个技巧
    • 那就是row majorA矩阵shape为[M,K]
    • A.T的shape显然是[K,M]
    • 如果A.T也是row major,那么A.TA的在内存中的线性数据肯定不同的!
    • 如果A.Tcol major,那么A.TA在内存中的线性数据就相同了!
      • 上面这两个数据在内存中线性排布是一摸一样的!!!仔细想想啊!
  • 关于矩阵运算在数学上有,如果c=a*b,那么c.T=b.T*a.T
    • 也就是说我们为了求得到row majorc矩阵,可以通过获得col majorc.T来求得!因为他俩在内存中线性排布是一摸一样的!
    • 也就是用col majorb.Tcol majora.T来求得即可!
    • col majorb.T 的地址,显然就是b地址啊!
    • col majora.T 的地址,显然就是a地址啊!
  • 其他的一些注释我都放到代码里啦!请各位看官看下哦!
cpp 复制代码
#include <stdio.h>
#include <chrono>
#include <ctime>
#include <iostream>
#include <ratio>
#include "cublas_v2.h"
#include "cutlass/gemm/device/gemm.h"

#define WARMUP 10
#define REPEATE 10

using DATATYPE = float;
using ACCU_DATATYPE = float;

void init(DATATYPE *a, int size) {
  for (int i = 0; i < size; i++) {
    a[i] = (rand() % 9999) / 10000.0;
  }
}

cudaError_t CutlassSgemmNN(int M, int N, int K, 
                           float alpha, float const *A, int lda, 
                           float const *B, int ldb, 
                           float beta,
                           float *C, int ldc) {
  // 这个 ColumnMajor 这句话太好理解了,就是col major的意思!
  using ColumnMajor = cutlass::layout::ColumnMajor;
  // 下面实例化了一个模版类`class LinearCombination`,这个类的一些参数请看这个文件
  // https://github.com/NVIDIA/cutlass/blob/main/include/cutlass/epilogue/thread/linear_combination.h吧!
  using EpilogueOutputOp = cutlass::epilogue::thread::LinearCombination<float, 1, float, float>;
  // 下面这个牛逼了!实例化了一个模版类`class Gemm`,这个模版类的参数很多,请你们看这个文件吧。
  // https://github.com/NVIDIA/cutlass/blob/main/include/cutlass/gemm/device/gemm.h
  using CutlassGemm = cutlass::gemm::device::Gemm<float, ColumnMajor,
                                                  float, ColumnMajor,
                                                  float, ColumnMajor,
                                                  float,
                                                  cutlass::arch::OpClassSimt,
                                                  cutlass::arch::Sm75,
                                                  cutlass::gemm::GemmShape<128,128, 8>,
                                                  cutlass::gemm::GemmShape<32,64, 8>,
                                                  cutlass::gemm::GemmShape<1,1, 1>, 
                                                  EpilogueOutputOp,
                                                  cutlass::gemm::threadblock::GemmIdentityThreadblockSwizzle<>,
                                                  2,
                                                  1,
                                                  1,
                                                  true >;
  // 上面只是实例化了模版类,下面这行是真的用模版类实例化了对象!
  CutlassGemm gemm_operator;
  // 下面这些是参数哦!
  // 计算的是D = alpha * A * B + beta * C
  // 这里面D也就是C哦,又因为beta我们在demo里面给了他是0,alpha给了是1,因此就相当于计算C=A*B啦!
  CutlassGemm::Arguments args({M, N, K},  // Gemm Problem dimensions
                              {A, lda},   // Tensor-ref for source matrix A
                              {B, ldb},   // Tensor-ref for source matrix B
                              {C, ldc},   // Tensor-ref for source matrix C
                              {C, ldc},   // Tensor-ref for destination matrix D
                              {alpha, beta}, // Scalars used in the Epilogue
                              2); // 这个2表示用split-k算法,且k=2哦!
  size_t bytes = CutlassGemm::get_workspace_size(args);
  void * workspace;
  cudaMalloc( (void**)&workspace, bytes);
  cutlass::Status status = gemm_operator(args, workspace);

  if (status != cutlass::Status::kSuccess) {
    return cudaErrorUnknown;
  }

  return cudaSuccess;
}

int main(void) {
    int m = 512;
    int n = 512;
    int k = 512;
    DATATYPE *a, *b;
    a = (DATATYPE *)malloc(sizeof(DATATYPE) * m * k);
    b = (DATATYPE *)malloc(sizeof(DATATYPE) * k * n);
  
    init(a, m * k);
    init(b, k * n);
  
    ACCU_DATATYPE *c;
    c = (ACCU_DATATYPE *)malloc(sizeof(ACCU_DATATYPE) * m * n);
    memset(c, 0, sizeof(ACCU_DATATYPE) * m * n);
  
    float *c_cpu_fp32 = (float *)malloc(sizeof(float) * m * n);
    memset(c_cpu_fp32, 0, sizeof(float) * m * n);
  
    DATATYPE *dev_a, *dev_b;
    ACCU_DATATYPE *dev_c;
    cublasHandle_t handle;
    cublasCreate(&handle);
  
    // allocate the memory on the GPU 
    cudaMalloc((void **)&dev_a, m * k * sizeof(DATATYPE));
    cudaMalloc((void **)&dev_b, k * n * sizeof(DATATYPE));
    cudaMalloc((void **)&dev_c, m * n * sizeof(ACCU_DATATYPE));
  
    cudaMemcpy(dev_a, a, m * k * sizeof(DATATYPE), cudaMemcpyHostToDevice);
    cudaMemcpy(dev_b, b, k * n * sizeof(DATATYPE), cudaMemcpyHostToDevice);
  
  
    cudaEvent_t beg, end;
  
    for (int i = 0; i < WARMUP + REPEATE; i++) {
  
      if (i == WARMUP) {
        cudaEventCreate(&beg);
        cudaEventCreate(&end);
        cudaEventRecord(beg);
      }
  
      const float alpha = 1.0f;
      const float beta = 0.0f;
      // 这里为了求得`row major`的`c`矩阵,选择计算`col major`的`c.T`,因为这俩个地址是一模一样的!
      // `c.T=b.T*a.T`我们为了获得col major的c.T,
      // `col major`的`b.T` 的shape是[n,k],地址就是dev_b
      // `col major`的`a.T` 的shape是[k,m],地址就是dev_a
      // 因此下面的参数是下面填写的那样哦!
      cublasSgemm(handle,CUBLAS_OP_N,CUBLAS_OP_N,
                                n,m,k,
                                &alpha,
                                dev_b,n,
                                dev_a,k,
                                &beta,
                                dev_c,n);
      // 这个是cutlass的gemm!
      // CutlassSgemmNN(n, m, k, alpha, dev_b, n, dev_a, k, beta, dev_c, n);
    }
  
    cudaEventRecord(end);
    cudaEventSynchronize(end);
    float elapsed_time;
    cudaEventElapsedTime(&elapsed_time, beg, end);
    printf("gpu gemm compute time: %f ms\n", elapsed_time);
    
    // 把gpu运算的结果拷贝到host端c上!
    cudaMemcpy(c, dev_c, m * n * sizeof(ACCU_DATATYPE), cudaMemcpyDeviceToHost);
    // 在cpu上计算结果,这个结果作为baseline,用来确保cutlass没有算错哦!
    for (int i = 0; i < m; i++) {
      for (int j = 0; j < n; j++) {
        double sum = 0.f;
        for (int ii = 0; ii < k; ii++) {
          sum += a[i * k + ii] * b[ii * n + j];
        }
        c_cpu_fp32[i * n + j] = sum;
      }
    }
    
    // 看看baseline和gpu上的diff,然后输出!
    double max_diff = -1.;
    for (int i = 0; i < m; i++) {
      for (int j = 0; j < n; j++) {
        double c_gpu_fp32 = c[i * n + j];
        if (std::abs(c_cpu_fp32[i * n + j] - c_gpu_fp32) > max_diff) {
          max_diff = std::abs(c_cpu_fp32[i * n + j] - c_gpu_fp32);
        }
      }
    }
  
    printf("max_diff: %f\n", max_diff);
  
    cudaDeviceReset();
    free(a);
    free(b);
    free(c);
    free(c_cpu_fp32);
    return 0;
}

深入理解 cutlass 在 sm75 cuda core 下的 fp32计算逻辑

  • 我们绝对不能满足于只是调用接口,我们要深入理解cutlass在sm75 fp32 cuda core上的计算逻辑,深入理解cutlass代码!
  • 那我们先需要看一下这个类了, cutlass::gemm::device::Gemm

cutlass::gemm::device::Gemm理解!

  • 首先这个显然是一个device级别的接口,也就是面向小白用户的接口。但是小白想要使用他的话,因为他是个模版类,所以必须要先实例化他的!
    • 因而必须得了解并掌握每个模版参数的含义!
  • 直接读代码是比较繁琐的,但是又是必不可少的过程的!因此我们直接读代码把!
  • 这个类的代码请点击这里
  • 先看前9个参数,这些都是简单的哦。
cpp 复制代码
template <
    /// Element type for A matrix operand
    typename ElementA_,
    /// Layout type for A matrix operand
    typename LayoutA_,
    /// Element type for B matrix operand
    typename ElementB_,
    /// Layout type for B matrix operand
    typename LayoutB_,
    /// Element type for C and D matrix operands
    typename ElementC_,
    /// Layout type for C and D matrix operands
    typename LayoutC_,
    /// Element type for internal accumulation
    typename ElementAccumulator_ = ElementC_,
    /// Operator class tag
    typename OperatorClass_ = arch::OpClassSimt,
    /// Tag indicating architecture to tune for
    typename ArchTag_ = arch::Sm70,
  • 参数OperatorClass_表示是由cuda core呢还是tensor core计算呢?默认是cuda core,也就是arch::OpClassSimt
    • 如果是让tensor core计算,那么就要用arch::OpClassTensorOp
  • ArchTag_就表示计算能力啦,默认是arch::Sm70
  • 上面几个参数平平无奇!下面看下面几个参数吧。

  • 再接着往下面是三个参数,这些参数是有默认值的。具体需要看DefaultGemmConfiguration这个类了。
  • 但是他们三个的含义是清晰的。
cpp 复制代码
   /// Threadblock-level tile size (concept: GemmShape)
    typename ThreadblockShape_ = typename DefaultGemmConfiguration<
        OperatorClass_, ArchTag_, ElementA_, ElementB_, ElementC_,
        ElementAccumulator_>::ThreadblockShape,
    /// Warp-level tile size (concept: GemmShape)
    typename WarpShape_ = typename DefaultGemmConfiguration<
        OperatorClass_, ArchTag_, ElementA_, ElementB_, ElementC_,
        ElementAccumulator_>::WarpShape,
    /// Instruction-level tile size (concept: GemmShape)
    typename InstructionShape_ = typename DefaultGemmConfiguration<
        OperatorClass_, ArchTag_, ElementA_, ElementB_, ElementC_,
        ElementAccumulator_>::InstructionShape,
相关推荐
roman_日积跬步-终至千里5 小时前
【线性代数】【第二章】矩阵习题
线性代数·矩阵
-指短琴长-5 小时前
BFS解决多源最短路问题_01矩阵_C++【含多源最短路问题介绍+dist数组介绍】
c++·矩阵·宽度优先
白拾7 小时前
使用NumPy进行线性代数的快速指南
线性代数·numpy
C++忠实粉丝16 小时前
前缀和(8)_矩阵区域和
数据结构·c++·线性代数·算法·矩阵
DogDaoDao1 天前
【预备理论知识——2】深度学习:线性代数概述
人工智能·深度学习·线性代数
quaer1 天前
Open-Sora全面开源?
开发语言·算法·机器学习·matlab·矩阵
winds~1 天前
数学基础-向量投影
线性代数
roman_日积跬步-终至千里2 天前
【线性代数】【第一章】行列式习题
线性代数
sml_54212 天前
【笔记】连续、可导、可微的概念解析
笔记·线性代数
吱吱鼠叔2 天前
MATLAB数据文件读写:2.矩阵数据读取
数据库·matlab·矩阵