GEMM与AutoKernel算子优化

随着AI技术的快速发展,深度学习在各个领域得到了广泛应用。深度学习模型能否成功在终端落地应用,满足产品需求,一个关键的指标就是神经网络模型的推理性能。一大波算法工程师为了算法的部署转岗算子优化工程师。优化代码并不是一件简单的事,要求工程师既要精通计算机体系架构,又要熟悉算法的计算流程,稍微有经验的深度学习推理优化工程师都成了各家公司争抢的“香饽饽”。需求多,算子优化自动化成为了未来的一大趋势。

为了方便更多的工程师进行推理优化,一个致力于降低优化门槛,提升优化开发效率的算子自动优化工具AutoKernel宣布正式开源!

AutoKernel特色:

低门槛: 无需底层优化汇编的知识门槛简单易用: 提供docker环境,无需安装环境,plugin一键集成到推理框架Tengine高效率: 无需手写优化汇编,一键生成优化代码,一键部署AutoKernel使用业界广泛使用的自动代码生成项目Halide,通过输入计算描述和调度策略,自动生成底层代码。AutoKernel支持以plugin的形式,将生成的自动优化算子一键部署到推理框架Tengine中。

本文将带领大家一步步优化矩阵乘法GEMM。无需手工撸代码,编写繁杂冗长的底层汇编代码,只需十几行简洁的调度代码。

优化的本质。优化的时候,计算机底层做了什么?优化的”瓶颈“是什么?为什么通过一波”优化操作“,性能就能提升呢?AutoKernel使用的Halide是如何实现自动优化的呢?

需要了解一下硬件的基础的体系结构,了解硬件如何工作,才能在软件上实现算法的时候,尽可能去考虑利用硬件的一些特性,来做到高效的、极致的优化。

上图是典型的存储理器层次结构:主存容量大,访问速度慢,寄存器和缓存读取速度快,但容量有限。在寄存器的层级上,CPU可以在一个时钟周期内访问它们,如果CPU去访问外部的DDR的话,延迟是非常大的,大概是200个时钟周期左右。如果CPU去访问cache的话,一般需要6到12个cycle就够了。所以,一个很重要的一个优化宗旨是:优化内存访问,充分利用寄存器和高速缓存去存数据。

第二个优化宗旨则是提高并行性:充分利用SIMD进行指令向量化和多核心并行。大部分现代CPU支持SIMD(Single Instruction Multiple Data,单指令流多数据流)。在同一个CPU循环中,SIMD可在多个值上同时执行相同的运算/指令。在4个数据点上进行向量化,一次计算四个数据,理论上就可以实现4倍的加速。

运行环境搭建

AutoKernel提供了docker镜像,docker里已经配置好运行环境,进入docker即可直接运行demo代码:

# 拉取镜像docker pull openailab/autokernel# 启动容器,进入开发环境docker run -it openailab/autokernel /bin/bash# 获取代码git clone https://github.com/OAID/AutoKernel.gitcd AutoKernel/doc/tutorials/data/目录下的build.sh是demo的执行脚本,运行需要指定优化步骤step,可选的step是从1 到7,其中step= 1 是默认不优化的,step=7是最极致优化的。

优化效果

# 执行demo./build.sh 1./build.sh 7

下图展示了在Intel(R) Core(TM) i9-9900K CPU @ 3.60GHz的电脑上的优化效果,无需手工撸代码,无需编写繁杂冗长的底层汇编代码,只需十几行简洁的调度代码, 就能性能优化200+倍~

优化步骤

以下是更为详细的优化步骤:

STEP1

第一个步骤是不带任何优化的。用Halide语言直接描述GEMM的计算过程。

Var x,y; RDom k(0, K); Func gemm("gemm"); gemm(x, y) += A(k, y) * B(x, k);

计算M=N=K=640的矩阵乘法。运行脚本第一个参数指定step=1。耗时结果如下:

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 1step = 1M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 240.8523 ms 1.1376 ms

STEP2

这一步采用分块tile。分块的目的是为了充分利用缓存。如果原来的循环较大,tile分块改成小块数据去计算,可以使得每次计算的数据都比较舒适地呆在缓存里,不用经历重复的驱逐(在缓存中重复的添加和删除数据)。分块后进行reorder操作,交换两个嵌套循环的顺序,目的是最内层的内存访问友好。按照x,y维度划分成16x8的小分块去计算:

.gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo);

执行结果如下:

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 2step = 2M N K = 640 640 640 err 0.00 [rep 50] halide | blas 81.8148 ms 1.1281 ms

性能从240ms优化到82ms,提升了近3倍。

STEP3

在上一步的基础上增加向量化vectorize。向量化是把几个标量计算(scale)转换为一个向量计算(vector),充分利用SIMD向量指令。大部分现代CPU支持SIMD(Single Instruction Multiple Data,单指令流多数据流)。在同一个CPU循环中,SIMD可在多个值上同时执行相同的运算/指令。

gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8);

执行结果:

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 3step = 3M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 27.5433 ms 1.1445 ms

性能从82ms优化到27ms,又加速了接近3倍。可以看到,围绕前面提到的两条优化宗旨:优化内存访问和提高并行性,从step1到step3,性能已经提升了近9倍。

STEP4

调度策略在step3的基础上增加并行化parallel。对一个循环并行化是把循环的每次迭代分给多个线程或者处理器去同时处理,每个线程处理通过代码段(loop body),但是处理不同的数据。

gemm(x, y) += A(k, y) * B(x, k); gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8) .parallel(yo);

执行结果:

root@bd3faab0f079:/home/chunying/AutoKernel/doc/tutorials# ./06_build.sh 4step = 4M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 7.2605 ms 1.1605 ms

增加并行化后,build.sh默认指定四线程,性能直接翻了近4倍,从27ms到7.3ms.

STEP5

调度策略在上一步的基础上增加unroll展开。如果循环体内的语句没有数据相关依赖,循环展开可以增加并发执行的机会,使得更充分利用寄存器,减少循环时每个操作内存加载和保存的次数。

gemm.update .tile(x, y, xo, yo, xi, yi, 16, 8) .reorder(xi, yi, k, xo, yo) .vectorize(xi, 8) .parallel(yo) .unroll(xi) .unroll(yi,2);

执行结果:

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 5step = 5M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 4.7617 ms 1.1597 ms

unroll展开后,性能从7.3ms优化到4.8ms.

STEP6

前面的分块成 16 x 8的小kernel, 这一步先划分成 16 x 32的分块,然后把每个分块再分成 16 x 8的子分块。把最外层的两层循环合并到一层,并对这一层进行并行化。这一步计算描述多了一个prod函数来定义子分块的计算,prod函数的计算公式和总的gemm是一样的,通过 compute_at指定在 yi维度之下计算prod,则prod计算的是 16x8的小kernel, 大致逻辑如下:

总的代码如下:

Func prod; prod(x, y) += A(k, y) * B(x, k); gemm(x, y) = prod(x, y); gemm.tile(x, y, xi, yi, 16, 32) .fuse(x, y, xy).parallel(xy) .split(yi, yi, yii, 4) .vectorize(xi, 8) .unroll(xi) .unroll(yii); prod.compute_at(gemm, yi) .vectorize(x, 8).unroll(y); prod.update .reorder(x, y, k) .vectorize(x, 8) .unroll(x) .unroll(y) .unroll(k, 2);

执行结果

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 6step = 6M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 3.1824 ms 1.1373 ms

这一步距离STEP1性能已经优化了近80倍了,性能越来越接近OpenBlas了。

STEP 7

这一步添加的操作是对矩阵B进行数据重排,使得在计算小kernel 16x8时,内存读取更顺畅。因为小kernel的x维度是按照16划分的,因此重排数据B的x维度也是按照16重排。

总的代码如下:

Func B_interleave("B"), Bs("Bs"); Bs(x, y, xo) = B(xo * 16 + x, y); B_interleave(x, y) = Bs(x % 16, y, x / 16); Func prod; prod(x, y) += A(k, y) * B_interleave(x, k); gemm(x, y) = prod(x, y); gemm.tile(x, y, xi, yi, 16, 32)

.fuse(x, y, xy).parallel(xy) .split(yi, yi, yii, 4) .vectorize(xi, 8) .unroll(xi) .unroll(yii); prod.compute_at(gemm, yi) .vectorize(x, 8).unroll(y); prod.update .reorder(x, y, k) .vectorize(x, 8) .unroll(x) .unroll(y) .unroll(k, 2); Bs.compute_root .split(y, yo, yi, 16) .reorder(x, yi, xo, yo) .unroll(x)

.vectorize(yi).parallel(yo, 4);

执行结果:

root@bd3faab0f079:/AutoKernel/doc/tutorials/data# ./06_build.sh 7step = 7M N K = 640 640 640 err 0.00 [rep 50] autokernel | blas 1.1957 ms 1.1425 ms

至此,的每一步调优策略始终都围绕两条优化宗旨“优化内存访问”,“提高并行性”展开优化,到最后性能已经与OpenBlAS差不多了,距离STEP1已经加速了200+倍了。

GEMM与AutoKernel算子优化的更多相关文章

  1. Spark为什么只有在调用action时才会触发任务执行呢(附算子优化和使用示例)?

    Spark算子主要划分为两类:transformation和action,并且只有action算子触发的时候才会真正执行任务.还记得之前的文章<Spark RDD详解>中提到,Spark ...

  2. 深度学习算子优化-FFT

    作者:严健文 | 旷视 MegEngine 架构师 背景 在数字信号和数字图像领域, 对频域的研究是一个重要分支. 我们日常"加工"的图像都是像素级,被称为是图像的空域数据.空域数 ...

  3. spark算子优化

    一.在聚合前在map端先预聚合 使用reduceByKey/aggregateByKey代替groupByKey 二.一次处理一个分区的数据,不过要注意一个分区里的数据不要太大,不然会报oom * 使 ...

  4. BigData-‘基于代价优化’究竟是怎么一回事?

    本文由  网易云发布. 本文具体讨论了Join基础算法的一种优化方案  – Runtime Filter,在本文最后还引申地聊了聊谓词 下推技术.同时,在本文文章开头,笔者引出了两个问题,SQL执行引 ...

  5. spark核心优化详解

    大家好!转眼又到了经验分享的时间了.吼吼,我这里没有摘要也没有引言,只有单纯的经验分享,请见谅哦! 言归正传,目前在大数据领域能够提供的核心计算的工具,如离线计算hadoop生态圈的mr计算模型,以及 ...

  6. flink任务性能优化

    如何提高 Flink 任务性能 一.Operator Chain 为了更高效地分布式执行,Flink 会尽可能地将 operator 的 subtask 链接(chain)在一起形成 task,每个 ...

  7. CUDA 矩阵乘法终极优化指南

    作者:马骏 | 旷视 MegEngine 架构师 前言 单精度矩阵乘法(SGEMM)几乎是每一位学习 CUDA 的同学绕不开的案例,这个经典的计算密集型案例可以很好地展示 GPU 编程中常用的优化技巧 ...

  8. Spark 3.x Spark Core详解 & 性能优化

    Spark Core 1. 概述 Spark 是一种基于内存的快速.通用.可扩展的大数据分析计算引擎 1.1 Hadoop vs Spark 上面流程对应Hadoop的处理流程,下面对应着Spark的 ...

  9. Halide应用开发

    Halide应用开发 1. 基本原理 1.1.介绍 随着人工智能的普及,深度学习网络的不断涌现,为了让各硬件(CPU, GPU, NPU,...)能够支持深度学习应用,各硬件芯片需要软件库去支持高性能 ...

随机推荐

  1. 【译】Android NDK API 规范

    [译]Android NDK API 规范 译者按: 修改R代码遇到Lint tool的报错,搜到了这篇文档,aosp仓库地址:Android NDK API Guidelines. 975a589 ...

  2. Salesforce 集成篇零基础学习(一)Connected App

    本篇参考: https://zhuanlan.zhihu.com/p/89020647 https://trailhead.salesforce.com/content/learn/modules/c ...

  3. POJ 2976 01分数规划基础题目

    题意:       给你一组"数",一共n个,每个数有两个权值,价钱a[i],代价b[i],让你选择n - k使得 sigma(a[i]) / sigma(b[i]) * 100 ...

  4. UVA10340子序列

    题意:       给你两个串,问你第二个第一个串是否是第一个串删除0个或多个字母得到的? 思路:       直接模拟就行了,在第二个串中去按顺序更新第一个串的下标,好像没说明白啊,不管了,水题,不 ...

  5. Iptables防火墙实验

    先说明一下环境,这里有四台主机,中间的Centos充当防火墙.右上角的win XP和右下角的Rhel7充当服务器,最左边的win7充当主机.四者之间的网卡都已经配置好.而且我们已经在Centos6.5 ...

  6. hdu2371 矩阵乘法(求序列位置改变m次后的序列)

    题意:       给你一个字符串,然后让你执行m次操作,每次操作把当前的字符串映射到他给你的位置序列的位置,比如给的是 3 1 2,第一步就是把原来的3的位置的字母变到1的位置,1的变到2的位置,2 ...

  7. JAVA的安装

    1.从JAVA官网 下载 注意选择自己需要的版本 2.百度云盘 链接:https://pan.baidu.com/s/1deOFGN1xB0mgz6s2mTRXdA 提取码:ke97 安装JAVA J ...

  8. maven下载Oracle jar包

    Oracle的jar包由于是收费的,所以当我们使用maven去下载时下载不下来,对于这种情况,可以用以下方式去处理: oracle官网下载应用地址:https://www.oracle.com/dow ...

  9. MarkDown写ppt

    首先给你的VSCode安装插件 MarkDown语法 例子 --- marp: true paginate: true theme: default class: - lead - invert si ...

  10. Java匿名对象导致的内存泄漏

    这几天与在某群与群友讨论了Runnable匿名对象导致内存泄漏的相关问题,特此记录一下. 示例代码如下: package com.memleak.memleakdemo; public class L ...