Loading [MathJax]/jax/output/CommonHTML/jax.js
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【TVM 三代优化巡礼】在X86上将普通的矩阵乘法算子提速90倍

【TVM 三代优化巡礼】在X86上将普通的矩阵乘法算子提速90倍

作者头像
BBuf
发布于 2022-05-27 00:22:40
发布于 2022-05-27 00:22:40
1.3K00
代码可运行
举报
文章被收录于专栏:GiantPandaCVGiantPandaCV
运行总次数:0
代码可运行

【GiantPandaCV导语】最近在整理一些编译器方面的基础知识翻译,回顾了一下TVM的Schedule然后想起自己1年前做的一些GEMM相关的实验和探索没做什么总结。所以基于TVM的三个教程也即TVM的三代优化来做对之前的学习做一个简单的总结,在本篇文章中我原创的内容主要集中在对各个Schedule的单独详解以及引入了TIR的伪代码描述帮助读者更好的理解TVM Schedule。还有深入解读了Blocking技术以及Auto Schedule搜出的高性能程序,简要分析了micro Kernel的汇编。另外我制作了一个控制Schedule变量进行测试得到的GFLOPS柱状图,方便大家对比各代优化的效果。感慨一下,我认为当前做优化的人以及独特的优化技巧都绝不在少数。如果基于通用的自动代码生成就可以达到任意硬件GFLOPS的80-90%,那么深度学习编译器可能就真的“收敛”了。但毕竟基于Ansor在Jetson Nano上搜一下GEMM的优化程序,性能非常差是我实验过的一个事实。无论是编译器还是深度学习框架对某些常见场景有过拟合是可以理解的,这并不妨碍Ansor是一个非常优秀的工作。我个人觉得我们对深度编译器的看法不能过于激进,目前深度学习编译器在代码生成上上仍然处于一个半自动状态,应该没有任何一家做业务的公司可以放心把各种要上线的业务的性能交给编译器来保证,所以应当稳中求快好一点。和性能快慢相比,我个人现阶段最在意的事情是代码是否鲁棒,很多情况下代码写正确且稳定运行的难度不小于一些优化的奇淫技巧。综上,请辩证的看待本篇文章中隐含的个人想法,包括对此提出批评。如果你读完本文觉得对你有一点用的话欢迎点一个赞,整理这篇文章陆续花了我2天时间。本文相关代码都在:https://github.com/BBuf/tvm_mlir_learn ,后面会考虑出一篇解读TVM CUDA优化教程的类似文章。

0x0. 前言

本文主要梳理一下在21年接触到优化gemm的知识,做一个学习总结。行文的顺序大概为:

  • 介绍本文依赖的硬件环境和本文要完成的任务。
  • 回顾gflops的计算方法并计算本地机器的GFLOPS。
  • RoofLine模型。
  • How to optimize GEMM on CPU 教程讲解。(https://github.com/apache/tvm/blob/main/gallery/how_to/optimize_operators/opt_gemm.py) 并细致梳理每一种优化方法的作用以及它们在IR中的表示。
  • Optimizing Operators with Schedule Templates and AutoTVM 教程讲解。(https://github.com/apache/tvm/blob/main/gallery/tutorial/autotvm_matmul_x86.py)
  • Optimizing Operators with Auto-scheduling 教程讲解。(https://github.com/apache/tvm/blob/main/gallery/tutorial/auto_scheduler_matmul_x86.py)
  • 总结。

先放一张总览图:

基于Auto Schedule得到的最好结果,搜索了20分钟

0x1. 本文依赖的硬件环境以及本文要完成的任务

本文的相关实验以及测试数据在同一硬件环境以及Ubuntu操作系统中完成,下图为机器的配置硬件信息如下:

机器的配置信息

注意到此CPU的核心为64,主频的最大值最小值分别为3900和1000Mhz,以及L1d,L2,L3 cache的大小分别为32K,1024K,22528K。

接下来说一下本文要完成的任务是什么?非常简单,本文的任务就是实现 ,其中 的维度是 的维度是 的维度是 ,那么矩阵乘法的原始实现就是:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
// gemm C = A * B + C
void MatrixMultiply(int m, int n, int k, float *a, float *b, float *c)
{
    for(int i = 0; i < m; i++){
        for (int j=0; j<n; j++ ){    
            for (int p=0; p<k; p++ ){      
                C(i, j) = C(i, j) + A(i, p) * B(p, j);
            }
        }
    }
}

为了让这个问题更加简单,在本文我们约定m,n,k的值都为1024。

0x2. 回顾GFLOPS的计算方法并计算本地机器的GFLOPS

FLOPs:注意s小写,是FLoating point OPerations的缩写(s表复数),意指浮点运算数,理解为计算量。可以用来衡量模型的复杂度。针对神经网络模型的复杂度评价,应指的是FLOPs,而非FLOPS。FLOPS:注意全大写,是floating point operations per second的缩写,意指每秒浮点运算次数,理解为计算速度。是一个衡量硬件性能的指标。比如nvidia官网上列举各个显卡的算力(Compute Capability)用的就是这个指标。

在讲解TVM的几个教程之前,我们需要先来计算一下机器的GFLOPS并以此判断后面几种TVM教程中生成的GEMM代码的性能相比于硬件浮点峰值大概到达了什么水平。

浮点峰值一般是计算单位时间内乘法和加法的最大吞吐量,单位为GFLOPS或者TFLOPS,表示每一秒可以计算的乘法和加法的总次数。我们可以根据硬件的结构计算一个理论的浮点峰值,并且也可以基于汇编指令来实测硬件的浮点峰值。由于对这一款CPU的架构了解比较有限,我们这里就直接来实测浮点峰值,后续的讲解基本也只和实测的浮点峰值有关。

基于高叔叔的https://github.com/pigirons/cpufp 进行测试,这里的基本原理是在循环内安排尽可能多的无数据依赖的乘加汇编指令掩盖因寄存器依赖浪费的时间周期,具体见:https://zhuanlan.zhihu.com/p/28226956。我们执行如下命令进行测试:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
git clone git@github.com:pigirons/cpufp.git
sh build.sh
./cpufp num_threads

分别测试num_threads=1,2,4时的GFLOPS,也即单核心,双核心以及四核心的GFLOPS。结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Thread(s): 1
avx512_vnni int8 perf: 267.7062 GFLOPS.
avx512f fp32 perf: 66.9300 GFLOPS.
avx512f fp64 perf: 33.4678 GFLOPS.
fma fp32 perf: 73.3017 GFLOPS.
fma fp64 perf: 36.6564 GFLOPS.
avx fp32 perf: 36.6528 GFLOPS.
avx fp64 perf: 18.3270 GFLOPS.
sse fp32 perf: 22.3124 GFLOPS.
sse fp64 perf: 11.1580 GFLOPS.

Thread(s): 2
avx512_vnni int8 perf: 535.5030 GFLOPS.
avx512f fp32 perf: 133.8812 GFLOPS.
avx512f fp64 perf: 66.9439 GFLOPS.
fma fp32 perf: 146.6221 GFLOPS.
fma fp64 perf: 73.3141 GFLOPS.
avx fp32 perf: 73.3229 GFLOPS.
avx fp64 perf: 36.6583 GFLOPS.
sse fp32 perf: 44.6286 GFLOPS.
sse fp64 perf: 22.3151 GFLOPS.

Thread(s): 4
avx512_vnni int8 perf: 1070.9130 GFLOPS.
avx512f fp32 perf: 267.7571 GFLOPS.
avx512f fp64 perf: 133.8734 GFLOPS.
fma fp32 perf: 293.1998 GFLOPS.
fma fp64 perf: 146.6209 GFLOPS.
avx fp32 perf: 146.6289 GFLOPS.
avx fp64 perf: 73.3129 GFLOPS.
sse fp32 perf: 89.2652 GFLOPS.
sse fp64 perf: 44.6303 GFLOPS.

容易观察到GFLOPs和CPU核心数是一个正比例的关系。本文的优化只关注在单核心上,后面的程序都使用 os.environ['TVM_NUM_THREADS']=str(1) 将程序绑定在一个CPU核心上。

0x3. RoofLine模型

上面已经介绍了GFLOPs(1GFLOPs=10^9FLOPs)和GFLOPS的概念,这里要简单引入一下计算密度和RoofLine模型。所谓计算密度指的是单位访存量下所需的计算量,单位是 FLOPs/Bytes。对于本文的任务来说,FLOPs即浮点的计算次数为

,这里的Bytes指的是访存量,在这个例子中访存量是

,所以这里的计算密度为

。而RoofLine模型是一个用来评估程序在硬件上能达到的性能上界的模型,可用下图表示:

RoofLine 模型,来自RoofLine 模型

注意到我们计算出的计算密度183.5FLOPs/Bytes是远大于单核心的fma fp32 perf: 73.3017 GFLOPS的,所以很显然我们的算子是一个计算密集形算子,那么计算速度的上限就是峰值计算速度不会受制于带宽。那么我们就可以放心的进行接下来的讲解了。

指的一提的是理论上的RoofLine模型和硬件真实的RoofLine模型还有一定的Gap,以及对于矩阵乘来说某些参数的改变可能会让这个算子从计算密集型朝着访存密集形发生改变。推荐商汤田子宸兄的这篇《深度学习模型大小与模型推理速度的探讨》文章,里面对RoofLine模型做了更加详细的解释以及思考。https://zhuanlan.zhihu.com/p/411522457

Whatever,我们现在知道本文的 规模的矩阵乘是一个计算密集型的算子,那我们就去看看TVM针对这种访存密集型算子相比浮点峰值可以优化到什么水平吧。

0x4. How to optimize GEMM on CPU 教程讲解。

这一篇教程全面讲解了TVM里面对schedule的优化方法,这些优化基本都来自于我以前介绍过的 https://github.com/flame/how-to-optimize-gemm 这个仓库,比如分块,矢量化,循环重排,Array Packing等等技巧。TVM这个教程将所有的优化技巧基本都用上了,然后直接给出了最终的结果。我觉得这对不太了解TVM的人跨度稍大。所以接下来我将逐一对这些技巧进行介绍并且逐步应用于Naive的TVM张量表达式并展示每一种优化技巧对应的TIR的变化。

这一节教程涉及到的优化技巧为:

  • Blocking(分块)
  • Vectorization(向量化)
  • Loop Permutation(调整分块的计算顺序)
  • Array Packing(数据重排)
  • Write cache for blocks(为写操作数据块创建连续内存缓存)
  • Parallel(并发运算)

接下来细说。

0x4.1 Blocking(分块)

Blocking通常也叫Tiling,是优化循环一种常见并且很有效的策略。我们先说一下Blocking是要解决什么问题然后我们来看是怎么解决的。看如下的例子:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
int *A = new float [N];
int *B = new float[M];
for(int i = 0; i < N; i++){
 for(int j = 0; j < M; j++) {
  A[i] += B[j];
 }
}

如果这个例子中M是比较大的,那么整个算法一定是不高效的。原因在于计算机在访问数组时并不是逐个从内存中访问的,而是以CacheLine为单位去访问,CacheLine的大小一般为64K。也就是说当我们访问B[0]的时候,实际上B[0]-B[15]已经在同一个CacheLine中进而被放进了Cache中,这样访问的速度就会更快。但由于CPU的Cache时有限的,当Cache满了之后如果还要新的CacheLine要进来,那么Cache就要把旧的CacheLine驱逐出去。

比如这里M比较大,那么我们在访问B[200]的时候,B[0]对应的CacheLine已经被驱逐出缓存了,这样随着i的递增下次要重新访问B[0]就不得不再次从内存中去加载。因此整个算法由于需要循环访问的数据在缓存中频繁的换入换出,导致性能比较差。

为了解决这个问题,引入了Blocking或者叫Tiling。Blocking就是在总的数据量无法放进Cache的情况下,把总数据分成一个小块去访问,让每个Tile都可以在Cache中。具体的做法就是把一个内层循环分解成 outer loop * inner loop。然后把 outer loop 移到更外层去,从而确保 inner loop 的数据访问一定能在Cache中。

对于下面的这段代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for(int i = 0; i < N; i++){
 for(int j = 0; j < M; j++) {
  A[i] += B[j];
 }
}

当M比较大的时候,我们可以简单计算一下它Cache Miss的次数,对于A来说,Cache Miss的次数为 ,其中B的Cache Miss次数为 ,乘以N是因为i每次递增的时候,由于M很大B开头的一部分数据已经被驱逐出缓存了。这样总的Cache Miss次数就为

按照Blocking的解决方案,我们把内层循环for(int j = 0; j < M; j++)按T的大小来进行分解。那么上面的代码变成:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (int j_o = 0; j_o < M; j_o += T){
 for (int i = 0; i < N; i++) {
  for (int j_i = j_o; j_i < j_o + T; j_i++){
   A[i] += B[j_i];
  }
 }
}

很明显我们发现循环 现在变成了inner loop,我们同样对它进行分解。那么伪代码变成:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (int i_o = 0; i_o < N; i_o += T){
 for (int j_o = 0; j_o < M; j_o += T) {
  for (int i_i = i_o; i_i < i_o + T; i_i++){
   for (int j_i = j_o; j_i < j_o + T; j_i++){
    A[i_i] += B[j][i];
   }
  }
 }
}

最终得到Cache Miss数量级相比于原始的循环将会减少倍。这就是Blocking或者Tiling的基本原理,通过分块降低数据访问的Cache Miss从而提升性能。接下来,我们基于TVM的教程看一下对矩阵乘算子进行Tiling得到了什么结果。

我们先补充一下TVM以默认Schedule运行矩阵乘的TIR,对应的实验代码在https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_without_tune_default_schedule.py:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  for (m: int32, 0, 1024) {
    for (n: int32, 0, 1024) {
      C[((m*1024) + n)] = 0f32
      for (k: int32, 0, 1024) {
        let cse_var_2 = (m*1024)
        let cse_var_1 = (cse_var_2 + n)
        C[cse_var_1] = (C[cse_var_1: int32] + (A[(cse_var_2: int32 + k)]*B[((k*1024) + n)]))
      }
    }
  }
}

可以看到这就是简单的三重for循环嵌套,没有任何优化技巧,自然这份代码的效率也是比较低的,可以当作本文的BaseLine。我根据它的执行时间计算了GFLOPS,结果如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
0.687 GFLOPS

然后我们看一下加了Blocking优化对应的TIR,代码在https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_without_tune_only_blocking.py

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  for (m.outer: int32, 0, 32) {
    for (n.outer: int32, 0, 32) {
      for (m.inner.init: int32, 0, 32) {
        for (n.inner.init: int32, 0, 32) {
          C[((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)) + n.inner.init)] = 0f32
        }
      }
      for (k.outer: int32, 0, 256) {
        for (k.inner: int32, 0, 4) {
          for (m.inner: int32, 0, 32) {
            for (n.inner: int32, 0, 32) {
              let cse_var_3 = (n.outer*32)
              let cse_var_2 = ((m.outer*32768) + (m.inner*1024))
              let cse_var_1 = ((cse_var_2 + cse_var_3) + n.inner)
              C[cse_var_1] = (C[cse_var_1: int32] + (A[((cse_var_2: int32 + (k.outer*4)) + k.inner)]*B[((((k.outer*4096) + (k.inner*1024)) + cse_var_3: int32) + n.inner)]))
            }
          }
        }
      }
    }
  }
}

可以看到和我们介绍的Blocking的原理差不多,通过将i, j, k三重循环分别分解为outer和inner两重新的循环对原始计算进行分块,减少Cache Miss。从TIR我们看到矩阵A被分成 的小块,矩阵B被分成 的小块,矩阵C被分成 的小块,然后对C的每一个 小块应用Naive的矩阵乘法。下面的代码是脚本种如何设置这个Schedule的。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 计算C(M, N) = A(M, K) x B(K, N)
def matmul(M, N, K, dtype):
    # Algorithm
    k = te.reduce_axis((0, K), "k")
    A = te.placeholder((M, K), name="A", dtype=dtype)
    B = te.placeholder((K, N), name="B", dtype=dtype)
    C = te.compute((M, N), lambda m, n: te.sum(A[m, k] * B[k, n], axis=k), name="C")
    
    bn = 32
    kfactor = 4
    s = te.create_schedule(C.op)

    # Blocking by loop tiling
    mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)
    (kaxis,) = s[C].op.reduce_axis
    ko, ki = s[C].split(kaxis, factor=kfactor)

    # Hoist reduction domain outside the blocking loop
    s[C].reorder(mo, no, ko, ki, mi, ni)
    return s, [A, B, C]

基于Blocking优化的GFLOPS为5.929 GFLOPS

综上所述,Blocking主要通过对矩阵进行分块来缓解因为Cache容量小导致的Cache Miss,有效率提升了矩阵乘法的运行效率。

0x4.2 Vectorize(向量化)

接下来,我们介绍本教程中的第二个优化技巧:vectorize(向量化)。现代CPU对于浮点运算基本都是支持SIMD的运算的,所以我们可以基于这个特性对矩阵乘进行优化。vectorize把iter方向上的循环迭代替换成ramp,从而通过SIMD指令实现数据的批量计算,并且只有在数据size为常数、且分割的iter为2的幂(即满足SIMD的计算数量)时才会发生替换,是SIMD计算设备的常用Schedule。相比于Blocking的优化,在TVM张量表达式种我们只需要添加一行代码即可实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# Vectorization
s[C].vectorize(ni)

然后它产生的TIR如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  for (m.outer: int32, 0, 32) {
    for (n.outer: int32, 0, 32) {
      for (m.inner.init: int32, 0, 32) {
        C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32)
      }
      for (k.outer: int32, 0, 256) {
        for (k.inner: int32, 0, 4) {
          for (m.inner: int32, 0, 32) {
            let cse_var_3 = (n.outer*32)
            let cse_var_2 = ((m.outer*32768) + (m.inner*1024))
            let cse_var_1 = (cse_var_2 + cse_var_3)
            C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1: int32, 1, 32)] + (broadcast(A[((cse_var_2: int32 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3: int32), 1, 32)]))
          }
        }
      }
    }
  }
}

由于在最内层循环进行了向量化,相比于上一节的TIR这里最内层循环被消除了,直接用ramp来替代。另外值得注意的是,这里有一个broadcast操作,这是因为在n.inner这层循环中A的下标和这个循环本身是没有关系的,所以相当于一个标量和一个向量来做乘法。所以这里将其广播变成一个向量,可以直接利用SIMD指令做向量的乘法。

测试一下GFLOPS(运行https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_without_tune_blocking_vectorize.py):

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
TVM Without Tune GFLOPS: 5.951689373974107

和只用Blocking优化的GFLOPS相比,略微有一点提升,不过这里幅度不大。

0x4.3 Loop Permutation(调整分块的计算顺序)

我们先看一下这个优化对应在Schedule上的变化是什么?代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# re-ordering
s[C].reorder(mo, no, ko, mi, ki, ni)

相比于前2个优化的schedule(s[C].reorder(mo, no, ko, ki, mi, ni))来说,唯一的变化就是交换了 的位置,这有效果吗?测试一下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
TVM Without Tune GFLOPS: 21.480747047174887

可以看到GFLOPS提升了3-4倍,所以这里的关键是什么呢?我们重写一下Blocking Schedule的TIR对应的伪代码,比较好观察一点:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for m_o in range(0, M, T_m):
  for n_o in range(0, N, T_n):
    for k_o in range(0, K, T_k):
      for k_i in range(k_o, k_o + T_k):
        for m_i in range(m_o, m_o + T_m):
          for n_i in range(n_o, n_o + T_n):
            C[m_i][n_i] += A[m_i][k_i] * B[k_i][n_i]

可以发现在当前schedule中,A 是逐列访问的,这对Cache是不友好的。如果我们改变 和内轴 的嵌套循环顺序,A 矩阵的访问模式会对缓存更加友好。这也是为什么我们可以看到性能有较大提升的原因。

现在新的TIR表达式如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  for (m.outer: int32, 0, 32) {
    for (n.outer: int32, 0, 32) {
      for (m.inner.init: int32, 0, 32) {
        C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32)
      }
      for (k.outer: int32, 0, 256) {
        for (m.inner: int32, 0, 32) {
          for (k.inner: int32, 0, 4) {
            let cse_var_3 = (n.outer*32)
            let cse_var_2 = ((m.outer*32768) + (m.inner*1024))
            let cse_var_1 = (cse_var_2 + cse_var_3)
            C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1: int32, 1, 32)] + (broadcast(A[((cse_var_2: int32 + (k.outer*4)) + k.inner)], 32)*B[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3: int32), 1, 32)]))
          }
        }
      }
    }
  }
}

对应的伪代码可以写成:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for m_o in range(0, M, T_m):
  for n_o in range(0, N, T_n):
    for k_o in range(0, K, T_k):
      for m_i in range(m_o, m_o + T_m):
        for k_i in range(k_o, k_o + T_k):
          for n_i in range(n_o, n_o + T_n):
            C[m_i][n_i] += A[m_i][k_i] * B[k_i][n_i]

0x4.4 Array Packing(数据重排)

我们先看一下要在矩阵乘法的张量表达式中加入Array Packing的Schedule是怎么做的:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# 计算C(M, N) = A(M, K) x B(K, N)
def matmul(M, N, K, dtype):
    # Algorithm
    k = te.reduce_axis((0, K), "k")
    A = te.placeholder((M, K), name="A", dtype=dtype)
    B = te.placeholder((K, N), name="B", dtype=dtype)

    bn = 32
    kfactor = 4

    packedB = te.compute(
    (N / bn, K, bn), lambda bigN, k, littleN: B[k, bigN * bn + littleN], name="packedB"
    )
    C = te.compute(
        (M, N),
        lambda m, n: te.sum(A[m, k] * packedB[n // bn, k, tvm.tir.indexmod(n, bn)], axis=k),
        name="C",
    )

    s = te.create_schedule(C.op)

    mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)
    (kaxis,) = s[C].op.reduce_axis
    ko, ki = s[C].split(kaxis, factor=kfactor)

    s[C].reorder(mo, no, ko, mi, ki, ni)
    s[C].vectorize(ni)

    bigN, _, littleN = s[packedB].op.axis
    s[packedB].vectorize(littleN)
    s[packedB].parallel(bigN)
    return s, [A, B, C]

下图是Array Packing的一般性原理说明。

Array Packing的一般性原理说明

加入了Array Packing Schedule对应的新TIR如下所示:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global {
    for (bigN: int32, 0, 32) "parallel" {
      for (k: int32, 0, 1024) {
        packedB_1: Buffer(packedB, float32x32, [32768], [])[((bigN*1024) + k)] = B[ramp(((k*1024) + (bigN*32)), 1, 32)]
      }
    }
    for (m.outer: int32, 0, 32) {
      for (n.outer: int32, 0, 32) {
        for (m.inner.init: int32, 0, 32) {
          C[ramp((((m.outer*32768) + (m.inner.init*1024)) + (n.outer*32)), 1, 32)] = broadcast(0f32, 32)
        }
        for (k.outer: int32, 0, 256) {
          for (m.inner: int32, 0, 32) {
            for (k.inner: int32, 0, 4) {
              let cse_var_3 = ((m.outer*32768) + (m.inner*1024))
              let cse_var_2 = (k.outer*4)
              let cse_var_1 = (cse_var_3 + (n.outer*32))
              C[ramp(cse_var_1, 1, 32)] = (C[ramp(cse_var_1: int32, 1, 32)] + (broadcast(A[((cse_var_3: int32 + cse_var_2: int32) + k.inner)], 32)*packedB_1[(((n.outer*1024) + cse_var_2) + k.inner)]))
            }
          }
        }
      }
    }
  }
}

我们主要关注一下应用Array Packing之前对B的访问方式以及使用了Array Packing之后对B的访问方式。我们之前获得的TIR的伪代码表示为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for m_o in range(0, M, T_m):
  for n_o in range(0, N, T_n):
    for k_o in range(0, K, T_k):
      for m_i in range(m_o, m_o + T_m):
        for k_i in range(k_o, k_o + T_k):
          for n_i in range(n_o, n_o + T_n):
            C[m_i][n_i] += A[m_i][k_i] * B[k_i][n_i]

这个地方B的数据排布方式为 ,我们访问B[k_i][n_i]时需要跨维度N进行访问,跨度和N的大小即1024是相关的。由于Array Packing使得B的数据排布从 变成 ,在TIR中对应了(no, ko, ki, ni) 的顺序,现在每一个小块计算时对B的数据访问一定是连续按行访问的,这就实现了我们想要的效果。

其实细心点的朋友可以发现在分块之后A虽然是按行访问的,但实际上也会跨K维度,跨度和K的大小即1024是相关的。我们为什么没有对A进行Pack呢?大家可以思考一下,接下来在Auto Schedule的对Micro Kernel汇编代码的简要分析中给出了答案。

0x4.5 Write cache for blocks(为写操作数据块创建连续内存缓存)

分块后,程序将结果逐块写入C,访问模式不是顺序的。所以我们可以使用一个顺序缓存数组来保存块结果并在所有块的结果准备好时写入 C。操作也比较简单,在上一节的代码基础上加一行:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
# Allocate write cache
CC = s.cache_write(C, "global")

测试一下结果:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
TVM Without Tune GFLOPS: 42.88957971622241

可以看到性能进一步提升了,我们看一下TIR的变化:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  allocate(packedB: Pointer(global float32x32), float32x32, [32768]), storage_scope = global;
  allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global {
    for (bigN: int32, 0, 32) "parallel" {
      for (k: int32, 0, 1024) {
        packedB_1: Buffer(packedB, float32x32, [32768], [])[((bigN*1024) + k)] = B[ramp(((k*1024) + (bigN*32)), 1, 32)]
      }
    }
    for (m.outer: int32, 0, 32) {
      for (n.outer: int32, 0, 32) {
        for (m.c.init: int32, 0, 32) {
          C.global_1: Buffer(C.global, float32, [1024], [])[ramp((m.c.init*32), 1, 32)] = broadcast(0f32, 32)
        }
        for (k.outer: int32, 0, 256) {
          for (m.c: int32, 0, 32) {
            let cse_var_4 = (k.outer*4)
            let cse_var_3 = (m.c*32)
            let cse_var_2 = ((n.outer*1024) + cse_var_4)
            let cse_var_1 = (((m.outer*32768) + (m.c*1024)) + cse_var_4: int32)
             {
              C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3: int32, 1, 32)] + (broadcast(A[cse_var_1: int32], 32)*packedB_1[cse_var_2: int32]))
              C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 1)], 32)*packedB_1[(cse_var_2 + 1)]))
              C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 2)], 32)*packedB_1[(cse_var_2 + 2)]))
              C.global_1[ramp(cse_var_3, 1, 32)] = (C.global_1[ramp(cse_var_3, 1, 32)] + (broadcast(A[(cse_var_1 + 3)], 32)*packedB_1[(cse_var_2 + 3)]))
            }
          }
        }
        for (m.inner: int32, 0, 32) {
          for (n.inner: int32, 0, 32) {
            C[((((m.outer*32768) + (m.inner*1024)) + (n.outer*32)) + n.inner)] = C.global_1[((m.inner*32) + n.inner)]
          }
        }
      }
    }
  }
}

一个Block的结果被一个一维的顺序结果记录下来,然后在一个块计算完成后统一从这个数组中把结果取出来写入C,避免之前因为写入C不连续导致的Cache Miss。

并行和多核有关,这篇文章只关注单核我就不介绍了,感兴趣的可以看TVM的教程。

0x4.6 小结

接下来我们画一张图来展示目前使用了哪些优化,以及使用上这些优化之后相比于实测的浮点峰值已经达到了什么水平。

目前用到的优化和浮点峰值的对比

上面图表中的B,V,R,A,W分别是Blocking,Vectorize,Reorder,Array Packing以及Write Cache的缩写。可以看到基于这些Schedule优化,我们的性能可以来到浮点峰值的58.5%左右。

0x5. Optimizing Operators with Schedule Templates and AutoTVM 教程讲解。

我们这里直接跑一下官方的tune代码看看FLOPS就好,tune的配置和文档中完全一致:https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_autotvm_tune.py

其实我们从这个脚本可以发现,AutoTVM还是需要我们自己手工指定搜哪些Schedule模板从而确定搜索空间(https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_autotvm_tune.py#L28-L72),还是比较麻烦的,特别是对于没有调优经验的用户。后面Ansor也就是Auto Schedule将这一限制彻底放开,用户不再需要手动指定搜索参数。无论是AutoTVM还是Auto Scheduling都会给我一个搜索出的类似的结果,通过查看TIR,我们都可以了解到最终搜索出的性能更优的Schedule是什么样子。

另外AutoTVM的搜索时间也需要比较久,在服务器上卡了很久都没跑完,放弃了。所以这里直接跳过测试AutoTVM搜索出的结果,直接看TVM的下一代搜索方案Auto Scheduling的结果好了。

AutoTVM:好像不少人认为Ansor是AutoTVM的下一代,所以现在AutoTVM是不是不那么常用了?了解的小伙伴可以解答一下。

0x6. Optimizing Operators with Auto-Scheduling 教程讲解。

使用Auto-Scheduling来搜索Schedule不需要我们手动指定搜索空间,它会自动基于我们的默认Schedule产生更大的Schedule搜索空间并更加高效的进行搜索。在教程的基础改一改可以得到下面这个脚本:https://github.com/BBuf/tvm_mlir_learn/blob/main/optimize_gemm/optimize_matmul_in_gemm/tvm_autoschedule_tune.py 。执行python3 tvm_autoschedule_tune.py float32 tune 进行搜索并使用性能最好的Schedule进行测试。大约20分钟搜好了,获得的GFLOPS为:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
TVM autoscheduler tuned GFLOPS: 62.646

和之前的优化做一个比较,如下图所示:

基于Ansor得到的最好结果,搜索了20分钟

现在基于Ansor的最好结果,达到了浮点峰值的85.5%,感觉是相当不错的结果了,相比于默认Schedule的Naive程序性能提升了91倍。

接下来我们从这个TIR出发来探索一下为什么Ansor搜索出的方案比0x4节的手工调优的方案性能更优?

首先打印一下基于Ansor搜索出的Schedule对应的TIR:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()
  attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}
  buffers = {A: Buffer(A_2: Pointer(float32), float32, [1048576], []),
             B: Buffer(B_2: Pointer(float32), float32, [1048576], []),
             C: Buffer(C_2: Pointer(float32), float32, [1048576], [])}
  buffer_map = {A_1: A, B_1: B, C_1: C} {
  allocate(auto_scheduler_layout_transform: Pointer(global float32), float32, [1048576]), storage_scope = global {
    for (ax0.ax1.fused.ax2.fused: int32, 0, 16) "parallel" {
      for (ax4: int32, 0, 16) {
        for (ax5: int32, 0, 2) {
          for (ax6: int32, 0, 64) {
            for (ax7: int32, 0, 32) {
              auto_scheduler_layout_transform_1: Buffer(auto_scheduler_layout_transform, float32, [1048576], [])[(((((ax0.ax1.fused.ax2.fused*65536) + (ax4*4096)) + (ax5*2048)) + (ax6*32)) + ax7)] = B[(((((ax4*65536) + (ax6*1024)) + (ax0.ax1.fused.ax2.fused*64)) + (ax5*32)) + ax7)]
            }
          }
        }
      }
    }
    for (i.outer.outer.j.outer.outer.fused: int32, 0, 8) "parallel" {
      allocate(C.local: Pointer(local float32), float32, [2048]), storage_scope = local;
      for (i.outer.inner: int32, 0, 32) {
        for (j.outer.inner: int32, 0, 2) {
          C.local_1: Buffer(C.local, float32, [2048], [], scope="local")[ramp(0, 1, 32)] = broadcast(0f32, 32)
          C.local_1[ramp(64, 1, 32)] = broadcast(0f32, 32)
          // 省略了对C.local_1其它部分的初始化
          for (k.outer: int32, 0, 16) {
            for (i.c.outer.inner: int32, 0, 16) {
              for (j.c.outer.inner: int32, 0, 2) {
                for (k.inner: int32, 0, 64) {
                  let cse_var_4 = ((i.c.outer.inner*128) + (j.c.outer.inner*32))
                  let cse_var_3 = (cse_var_4 + 64)
                  let cse_var_2 = ((((i.outer.inner*32768) + (i.c.outer.inner*2048)) + (k.outer*64)) + k.inner)
                  let cse_var_1 = (((((i.outer.outer.j.outer.outer.fused*131072) + (j.outer.inner*65536)) + (k.outer*4096)) + (j.c.outer.inner*2048)) + (k.inner*32))
                   {
                    C.local_1[ramp(cse_var_4, 1, 32)] = (C.local_1[ramp(cse_var_4: int32, 1, 32)] + (broadcast(A[cse_var_2: int32], 32)*auto_scheduler_layout_transform_1[ramp(cse_var_1: int32, 1, 32)]))
                    C.local_1[ramp(cse_var_3, 1, 32)] = (C.local_1[ramp(cse_var_3: int32, 1, 32)] + (broadcast(A[(cse_var_2 + 1024)], 32)*auto_scheduler_layout_transform_1[ramp(cse_var_1, 1, 32)]))
                  }
                }
              }
            }
          }
          for (i.inner: int32, 0, 32) {
            for (j.inner: int32, 0, 64) {
              C[(((((i.outer.inner*32768) + (i.inner*1024)) + (i.outer.outer.j.outer.outer.fused*128)) + (j.outer.inner*64)) + j.inner)] = C.local_1[((i.inner*64) + j.inner)]
            }
          }
        }
      }
    }
  }
}

可以看到相比于0x4节手动指定Schedule的 分块,这里搜出来的分块大小是 ,C.local和C.local_1做的就是写缓存Schedule的优化。同样矩阵B被Pack到了auto_scheduler_layout_transform里。从for (i.outer.outer.j.outer.outer.fused: int32, 0, 8) "parallel" 可以判断矩阵C被分成了8个小块,每个小块用单独的线程计算,我们可以计算出每个小块的大小为

从这两代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
for (i.outer.inner: int32, 0, 32) {
        for (j.outer.inner: int32, 0, 2)
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
.LBB3_25:
        vbroadcastss    -4100(%rdi,%rbp,4), %ymm8
        vmovaps -128(%rdx), %ymm9
        vmovaps -96(%rdx), %ymm10
        vmovaps -64(%rdx), %ymm11
        vmovaps -32(%rdx), %ymm12
        vmovaps %ymm7, %ymm13
        vfmadd231ps     %ymm12, %ymm8, %ymm13
        vfmadd231ps     %ymm11, %ymm8, %ymm6
        vfmadd231ps     %ymm9, %ymm8, %ymm4
        vfmadd231ps     %ymm8, %ymm10, %ymm5
        vbroadcastss    -4(%rdi,%rbp,4), %ymm7
        vfmadd231ps     %ymm12, %ymm7, %ymm3
        vfmadd231ps     %ymm11, %ymm7, %ymm2
        vfmadd231ps     %ymm9, %ymm7, %ymm0
        vfmadd231ps     %ymm10, %ymm7, %ymm1
        vbroadcastss    -4096(%rdi,%rbp,4), %ymm7
        vmovaps 96(%rdx), %ymm8
        vmovaps 64(%rdx), %ymm9
        vmovaps (%rdx), %ymm10
        vmovaps 32(%rdx), %ymm11
        vfmadd231ps     %ymm11, %ymm7, %ymm5
        vfmadd231ps     %ymm10, %ymm7, %ymm4
        vfmadd231ps     %ymm9, %ymm7, %ymm6
        vfmadd213ps     %ymm13, %ymm8, %ymm7
        vbroadcastss    (%rdi,%rbp,4), %ymm12
        vfmadd231ps     %ymm11, %ymm12, %ymm1
        vfmadd231ps     %ymm10, %ymm12, %ymm0
        vfmadd231ps     %ymm9, %ymm12, %ymm2
        vfmadd231ps     %ymm8, %ymm12, %ymm3
        addq    $2, %rbp
        addq    $256, %rdx
        cmpq    $64, %rbp
        jne     .LBB3_25

在一个C小块( )的计算中,C的所有数据驻寄存器ymm0~ymm7。k从0-16循环,将b(1x2)的值加载到了ymm8中,然后将a(1x32)个值加载到了ymm9,ymm10,ymm11,ymm12中并和ymm8乘加完成计算。另外,我们可以看到数组B的元素是连续存放的,而数组A的元素跨度为4096:vbroadcastss -4096(%rdi,%rbp,4), %ymm7 ,代表A没有做任何Pack。在这个小块里面,数组A完全可以放到寄存器中是否做Pack也对性能没什么影响。

0x7. 总结

最近在整理一些编译器方面的基础知识翻译,回顾了一下TVM的Schedule然后想起自己1年前做的一些GEMM相关的实验和探索没做什么总结。所以基于TVM的三个教程也即TVM的三代优化来做对之前的学习做一个简单的总结,在本篇文章中我原创的内容主要集中在对各个Schedule的单独详解以及引入了TIR的伪代码描述帮助读者更好的理解TVM Schedule。还有深入解读了Blocking技术以及Auto Schedule搜出的高性能程序,简要分析了micro Kernel的汇编。另外我制作了一个控制Schedule变量进行测试得到的GFLOPS柱状图,方便大家对比各代优化的效果。

0x8. 参考资料

  • https://zhuanlan.zhihu.com/p/477023757
  • https://zhuanlan.zhihu.com/p/494347227
  • TVM三篇优化x86 gemm的文档
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-05-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 GiantPandaCV 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
【从零开始学深度学习编译器】二,TVM中的scheduler
在【从零开始学深度学习编译器】一,深度学习编译器及TVM 介绍我们已经知道TVM可以将各种深度学习训练框架的模型(计算图)转化为内部的Graph IR(Relay),然后通过TVM提供的指令生成模块将Graph IR翻译成特定硬件可执行的指令或者代码。总的来说的TVM的思想可以总结为表示和调度分离,所谓表示就是IR,调度就是scheduler。同时,在高性能计算方面TVM提供了多种调度源语(scheduler),包含了大多数常见的优化手段如算子融合,读写缓存,分块计算,并行计算等等,这些计算方法都可以通过scheduler进行实现。所以这一节,我们就一起来探索一下TVM中的scheduler。
BBuf
2021/04/16
2K0
TVM 学习指南(个人版)
最近粗略的看完了天奇大佬的MLC课程(顺便修了一些语法和拼写错误,也算是做了微弱的贡献hh),对TVM的近期发展有了一些新的认识。之前天奇大佬在《新一代深度学习编译技术变革和展望》一文中(链接:https://zhuanlan.zhihu.com/p/446935289)讲解了TVM Unify也即统一多层抽象的概念。这里的统一多层抽象具体包括AutoTensorization用来解决硬件指令声明和张量程序对接,TVM FFI(PackedFunc)机制使得我们可以灵活地引入任意的算子库和运行库函数并且在各个编译模块和自定义模块里面相互调用。TensorIR负责张量级别程序和硬件张量指令的整合。Relax (Relax Next) 引入relay的进一步迭代,直接引入first class symbolic shape的支持 (摘抄自《新一代深度学习编译技术变革和展望》一文)。然后这些抽象可以相互交互和联合优化来构造深度学习模型对应的最终部署形式。我个人感觉TVM Unify类似于MLIR的Dialect,但是这几个抽象的直接交互能力相比于MLIR的逐级lower我感觉是更直观方便的,毕竟是Python First(这个只是我最近看MLC课程的一个感觉)。对这部分内容感兴趣的读者请查看天奇大佬的TVM Unify介绍原文以及MLC课程。
BBuf
2022/09/28
4K0
TVM 学习指南(个人版)
cuBLAS矩阵乘法性能分析(附代码示例)
矩阵乘法是神经网络中最基础、最重要的一个运算。在用CUDA实现矩阵乘法时,不需要我们手动写,cuBLAS库提供了现成的矩阵乘法算子,例如cublasGemmEx和cublasLtMatmul。其中后者是轻量级版本,API调用更灵活。例如对于整数乘法,cublasLtMatmul支持int8的输入输出,而cublasGemmEx只支持int8输入,int32输出。
godweiyang
2021/09/08
2.7K0
【从零开始学深度学习编译器】九,TVM的CodeGen流程
【GiantPandaCV导语】这里主要是走读了一下TVM的Codegen流程,从Relay的前端一直梳理到了Graph节点的内存分配,Relay IR到TIR节点的转换,TIR图节点的Schedule优化以及Lower function发生在哪里。这篇文章只是关注了调用链,一些具体的操作比如Schedule的优化,IR到TIR节点的转化以及Lower Function没有具体解释,后面会结合更多实例去尝试理解。
BBuf
2021/07/23
2K0
【AI系统】Auto-Tuning 原理
在硬件平台驱动算子运行需要使用各种优化方式来提高性能,然而传统的手工编写算子库面临各种窘境,衍生出了自动生成高性能算子的的方式,称为自动调优。在本文我们首先分析传统算子库面临的挑战,之后介绍基于 TVM 的业界领先的三个自动调优系统。
用户11307734
2024/11/29
2410
基于how-to-optimize-gemm初探矩阵乘法优化
这次,我们来聊一个轻松一点的话题,那就是给你一个矩阵A和一个矩阵B,使用矩阵乘法获得目标矩阵C,相信大家都不难写出下面的代码:
BBuf
2020/11/09
1.4K0
基于how-to-optimize-gemm初探矩阵乘法优化
我是如何实现Go性能5倍提升的?
代码的稳健、可读和高效是每一位 coder 的共同追求,写出更高效的代码不仅让自己爽、让 reviewer 赏心悦目,更能对业务带来实际的正面影响。本文将从实践及源码层面对 Go 的高性能编程进行解析,带你进入 Go 的高性能世界。
腾讯云开发者
2024/01/04
2.5K0
我是如何实现Go性能5倍提升的?
道阻且长_再探矩阵乘法优化
【GiantPandaCV导语】本文记录了笔者最近的一些优化gemm的思路和实现,这些思路大多是公开的方案,例如来自how-to-optimize-gemm工程的一些优化手段,来自ncnn的一些优化手段等。最终,笔者目前实现的版本在armv7a上可以达到50%左右的硬件利用率(这个利用率的确还不高,笔者也是一步步学习和尝试,大佬轻喷),本文记录了这些思路以及核心实现方法。改好的行主序代码(x86+armv7a版本)可以直接访问https://github.com/BBuf/how-to-optimize-gemm获取。
BBuf
2020/12/08
6900
道阻且长_再探矩阵乘法优化
可以让深度学习编译器来指导算子优化吗
之前在阅读Ansor论文的时候(https://zhuanlan.zhihu.com/p/390783734)我就在想这样一个问题,既然Ansor是在人为指定的推导规则下启发式的生成高性能的Scheduler模板。那么这个算子生成的Scheduler模板是否可以反过来指导我们写程序呢?嗯,然后我就开启了这个实验,但最近因为工作的事情delay得厉害,终于在这个周末抽出时间来更新这个实验结果并且记录了这篇文章。由于笔者只对GEMM的优化熟悉,这里就以优化X86的GEMM为例子来探索。希望这篇文章能为你带来启发,文章所有的实验代码都放到了https://github.com/BBuf/tvm_learn ,感兴趣的可以点个star一起学习(学习TVM的4个月里,这个工程已经收到了快100star了,我很感激)。
BBuf
2021/09/14
9860
可以让深度学习编译器来指导算子优化吗
【从零开始学TVM】三,基于ONNX模型结构了解TVM的前端
【GiantPandaCV导语】本文基于Pytorch导出的ONNX模型对TVM前端进行了详细的解析,具体解答了TVM是如何将ONNX模型转换为Relay IR的,最后还给出了一个新增自定义OP的示例。其实在TVM中支持编译多种目前主流的深度学习框架如TensorFlow,Pytorch,MxNet等,其实它们的前端交互过程和本文介绍的ONNX也大同小异,希望对TVM感兴趣的读者在阅读这篇文章之后对新增OP,或者说在TVM中支持一种新的DL框架有一个整体把握。本文实验相关的代码在https://github.com/BBuf/tvm_learn。
BBuf
2021/04/30
2.2K0
【AI系统】TVM 实践案例
在本文我们探讨一下,如何利用 AI 编译器在新的硬件上部署一个神经网络,从算法设计到实际运行,有哪些需要考虑的地方?本文将以 TVM 为例,首先介绍一下 TVM 的工作流:
用户11307734
2024/12/02
4660
性能比拼!超详细的Tengine GEMM矩阵乘法汇编教程
Tengine 是OPEN AI LAB 针对前端智能设备开发的软件开发包,核心部分是一个轻量级,模块化,高性能的AI 推断引擎,并支持用DLA、GPU、xPU作为硬件加速计算资源异构加速。
CV君
2019/12/27
2.4K0
发掘 ARM GPU 的全部深度学习性能,TVM 优化带来高达 2 倍性能提升
本文是由来自上海交通大学 Apex 实验室的本科生 Lianmin Zheng 发表于 TVM 的一篇博客,文中阐述了如何使用 TVM 优化移动端上的 ARM GPU 的深度学习。 AI 研习社对原文
AI研习社
2018/03/16
3.4K0
发掘 ARM GPU 的全部深度学习性能,TVM 优化带来高达 2 倍性能提升
开发 | 如何利用 TVM 优化深度学习GPU op?教你用几十行Python代码实现2-3倍提升
数天前,陈天奇团队宣布推出 TVM,在微博上表示,「我们今天发布了 TVM,和 NNVM 一起组成深度学习到各种硬件的完整优化工具链,支持手机,cuda, opencl, metal, javascript 以及其它各种后端。欢迎对于深度学习,编译原理,高性能计算,硬件加速有兴趣的同学一起加入 dmlc 推动领导开源项目社区 。」 AI科技评论了解,大多数现有系统针对窄范围的服务器级 GPU 进行优化,且需要在包括手机、IOT 设备及专用加速器上部署大量工作。而 TVM 是一种将深度学习工作负载部署到硬件的
AI科技评论
2018/03/13
1.9K0
开发 | 如何利用 TVM 优化深度学习GPU op?教你用几十行Python代码实现2-3倍提升
将矩阵乘法的性能提升200倍!AutoKernel算子优化工具正式开源
随着AI技术的快速发展,深度学习在各个领域得到了广泛应用。深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能。于是,一大波算法工程师为了算法的部署转岗算子优化工程师。然而,优化代码并不是一件简单的事,它要求工程师既要精通计算机体系架构,又要熟悉算法的计算流程,于是,稍微有经验的深度学习推理优化工程师都成了各家公司争抢的“香饽饽”。相关人才少,但需求多,算子优化自动化成为了未来的一大趋势。
AI科技评论
2020/12/18
1.3K0
将矩阵乘法的性能提升200倍!AutoKernel算子优化工具正式开源
DeepSeek开源周 Day03:从DeepGEMM看大模型算力提速的矩阵乘法
今天是DeepSeek开源周的第三天,继FlashMLA和DeepEP之后,DeepSeek开源了DeepGEMM库。作为一个专注于FP8精度通用矩阵乘法的高性能库,DeepGEMM在提供极致性能的同时保持了令人惊讶的代码简洁性。
致Great
2025/02/27
3660
DeepSeek开源周 Day03:从DeepGEMM看大模型算力提速的矩阵乘法
GPU编程(三): CPU与GPU的矩阵乘法对比
前言 在上一篇的最后, 我提到了一个矩阵乘法, 这次与CPU进行对比, 从中可以很明显GPU在并行计算上的优势. ---- 计时函数 在贴出代码之前, 来看下我常用的计时函数, 可以精确到微秒级. 首先头文件是#include<sys/time.h>. 结构体为: struct timeval{ long tv_sec; /*秒*/ long tv_usec; /*微秒*/ }; 来看下使用的小栗子: struct timeval start, end; double t
sean_yang
2019/01/28
1.8K0
GPU编程(三): CPU与GPU的矩阵乘法对比
mlc-llm 推理优化和大语言模型搭建解析(文末送书)
本文解析一下mlc-llm(https://github.com/mlc-ai/mlc-llm)对大模型推理的流程以及使用的图优化,算子优化策略。mlc-llm的模型部署流程可以查看官方文档:https://mlc.ai/mlc-llm/docs/ ,也可以参考我前段时间写的这篇MLC-LLM 部署RWKV World系列模型实战(3B模型Mac M2解码可达26tokens/s) 。
BBuf
2023/09/26
1.9K0
mlc-llm 推理优化和大语言模型搭建解析(文末送书)
Go 语言学习之基础数据类型
整数类型按照有/无符号划分,可分为有符号整数和无符号整数,二者按照大小划分,有 8 位、16 位、32 位和 64 位:
frank.
2020/07/23
4020
【论文解读】基于MLIR生成矩阵乘法的高性能GPU代码,性能持平cuBLAS
本文是对 https://arxiv.org/abs/2108.13191 这篇论文进行解读,学习一下如何基于MLIR编译器基础设施生成高效的GPU代码。本文的阅读的先后顺序分别为:
BBuf
2022/04/06
2.9K0
【论文解读】基于MLIR生成矩阵乘法的高性能GPU代码,性能持平cuBLAS
推荐阅读
相关推荐
【从零开始学深度学习编译器】二,TVM中的scheduler
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档