如何在CPU上优化GEMM(上)

(TL;DR)TVM提供了抽象接口,用户分别描述算法和算法的实现组织(所谓的调度)。通常,在高性能调度中编写算法会破坏算法的可读性和模块性。尝试各种看似有希望的时间表是很耗时的。在TVM的帮助下,可以有效地尝试这些调度来提高性能。

本文将演示如何使用TVM优化平方矩阵乘法,并通过简单地添加18行额外的代码来实现比baseline基线快200倍的速度。

在CPU上执行的高强度计算应用程序有两个重要的优化:

提高内存访问的缓存命中率。高速缓存命中率可以加速复杂的数值计算和热点内存访问。这需要我们将源内存访问模式转换为适合缓存策略的模式。

SIMD(单指令多数据)或称之为向量处理单元。每次都会处理一小批数据,而不是单个网格。这就要求将循环体中的数据访问模式转换为统一模式,以便LLVM后端能够将其降低到SIMD。

实际上,使用的所有方法都是本文所述技巧的子集。其中一些已经被TVM抽象自动应用,但有些由于TVM的约束而不能简单地应用。

下面提到的所有实验结果,都是在配备Intel i7-4770HQ CPU的2015款15寸MacBook上执行的。对于所有x86 CPU,缓存线大小应为64字节。

Preparation and Baseline

本文将演示如何使用TVM优化矩阵乘法。在实际演示之前,首先定义这些变量。然后编写了一个基线实现,这是在TVM中编写矩阵乘法的最简单方法。

import tvm

import tvm.testing

from tvm import te

import numpy

import timeit

# The size of the matrix

# (M, K) x (K, N)

# You are free to try out different shapes, sometimes TVM optimization outperforms numpy with MKL.

M = 1024

K = 1024

N = 1024

# The default tensor type in tvm

dtype = "float32"

# using Intel AVX2(Advanced Vector Extensions) ISA for SIMD

# To get the best performance, please change the following line

# to llvm -mcpu=core-avx2, or specific type of CPU you use

target = "llvm"

ctx = tvm.context(target, 0)

# Random generated tensor for testing

a = tvm.nd.array(numpy.random.rand(M, K).astype(dtype), ctx)

b = tvm.nd.array(numpy.random.rand(K, N).astype(dtype), ctx)

np_repeat = 100

np_runing_time = timeit.timeit(

setup="import numpy\n"

"M = " + str(M) + "\n"

"K = " + str(K) + "\n"

"N = " + str(N) + "\n"

'dtype = "float32"\n'

"a = numpy.random.rand(M, K).astype(dtype)\n"

"b = numpy.random.rand(K, N).astype(dtype)\n",

stmt="answer = numpy.dot(a, b)",

number=np_repeat,

)

print("Numpy running time: %f" % (np_runing_time / np_repeat))

answer = numpy.dot(a.asnumpy(), b.asnumpy())

# Algorithm

k = te.reduce_axis((0, K), "k")

A = te.placeholder((M, K), name="A")

B = te.placeholder((K, N), name="B")

C = te.compute((M, N), lambda x, y: te.sum(A[x, k] * B[k, y], axis=k), name="C")

# Default schedule

s = te.create_schedule(C.op)

func = tvm.build(s, [A, B, C], target=target, name="mmult")

assert func

c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), ctx)

func(a, b, c)

tvm.testing.assert_allclose(c.asnumpy(), answer, rtol=1e-5)

evaluator = func.time_evaluator(func.entry_name, ctx, number=1)

print("Baseline: %f" % evaluator(a, b, c).mean)

Out:

Numpy running time: 0.006963

Baseline: 3.516655

In TVM, we can always inspect lower level IR to debug or optimize our schedule. Here is the generated IR using our baseline schedule.

print(tvm.lower(s, [A, B, C], simple_mode=True))

Out:

primfn(A_1: handle, B_1: handle, C_1: handle) -> ()

attr = {"global_symbol": "main", "tir.noalias": True}

buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),

B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], []),

A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], [])}

buffer_map = {A_1: A, B_1: B, C_1: C} {

for (x: int32, 0, 1024) {

for (y: int32, 0, 1024) {

C_2[((x*1024) + y)] = 0f32

for (k: int32, 0, 1024) {

C_2[((x*1024) + y)] = ((float32*)C_2[((x*1024) + y)] + ((float32*)A_2[((x*1024) + k)]*(float32*)B_2[((k*1024) + y)]))

}

}

}

}

Blocking

提高缓存命中率的一个重要技巧是分块——数据块将逐块计算。块内的内存访问是一个具有高内存局部性的小邻域。本文选择了32作为阻塞因子。因此,块将填充32*32*sizeof(float),即缓存中的4KB,其总大小为32KB(一级数据缓存)

bn = 32

s = te.create_schedule(C.op)

# Blocking by loop tiling

xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)

(k,) = s[C].op.reduce_axis

ko, ki = s[C].split(k, factor=4)

# Hoist reduction domain outside the blocking loop

s[C].reorder(xo, yo, ko, ki, xi, yi)

func = tvm.build(s, [A, B, C], target=target, name="mmult")

assert func

c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), ctx)

func(a, b, c)

tvm.testing.assert_allclose(c.asnumpy(), answer, rtol=1e-5)

# By simply tiling the loop 32x32, and hoisting ko, ki outside the blocking loops,

# we can see big speedup compared with the baseline.

evaluator = func.time_evaluator(func.entry_name, ctx, number=10)

print("Opt1: %f" % evaluator(a, b, c).mean)

Out:

Opt1: 0.284967

Here is the generated IR after blocking.

print(tvm.lower(s, [A, B, C], simple_mode=True))

Out:

primfn(A_1: handle, B_1: handle, C_1: handle) -> ()

attr = {"global_symbol": "main", "tir.noalias": True}

buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),

B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], []),

A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], [])}

buffer_map = {A_1: A, B_1: B, C_1: C} {

for (x.outer: int32, 0, 32) {

for (y.outer: int32, 0, 32) {

for (x.inner.init: int32, 0, 32) {

for (y.inner.init: int32, 0, 32) {

C_2[((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)) + y.inner.init)] = 0f32

}

}

for (k.outer: int32, 0, 256) {

for (k.inner: int32, 0, 4) {

for (x.inner: int32, 0, 32) {

for (y.inner: int32, 0, 32) {

C_2[((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)) + y.inner)] = ((float32*)C_2[((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)) + y.inner)] + ((float32*)A_2[((((x.outer*32768) + (x.inner*1024)) + (k.outer*4)) + k.inner)]*(float32*)B_2[((((k.outer*4096) + (k.inner*1024)) + (y.outer*32)) + y.inner)]))

}

}

}

}

}

}

}

Vectorization

另一个重要的技巧是矢量化。当内存访问模式是一致的时,编译器可以检测到这种模式并将连续内存传递给向量处理器。在TVM中,可以使用向量化接口来提示编译器这个模式,这样就可以大大加速它。

本文选择将内部循环行数据矢量化,因为它对缓存友好。

s = te.create_schedule(C.op)

xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)

(k,) = s[C].op.reduce_axis

ko, ki = s[C].split(k, factor=4)

s[C].reorder(xo, yo, ko, ki, xi, yi)

# Vectorization

s[C].vectorize(yi)

func = tvm.build(s, [A, B, C], target=target, name="mmult")

assert func

c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), ctx)

func(a, b, c)

tvm.testing.assert_allclose(c.asnumpy(), answer, rtol=1e-5)

evaluator = func.time_evaluator(func.entry_name, ctx, number=10)

print("Opt2: %f" % evaluator(a, b, c).mean)

Out:

Opt2: 0.321595

Here is the generated IR after vectorization.

print(tvm.lower(s, [A, B, C], simple_mode=True))

Out:

primfn(A_1: handle, B_1: handle, C_1: handle) -> ()

attr = {"global_symbol": "main", "tir.noalias": True}

buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),

B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], []),

A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], [])}

buffer_map = {A_1: A, B_1: B, C_1: C} {

for (x.outer: int32, 0, 32) {

for (y.outer: int32, 0, 32) {

for (x.inner.init: int32, 0, 32) {

C_2[ramp((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)), 1, 32)] = broadcast(0f32, 32)

}

for (k.outer: int32, 0, 256) {

for (k.inner: int32, 0, 4) {

for (x.inner: int32, 0, 32) {

C_2[ramp((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)), 1, 32)] = ((float32x32*)C_2[ramp((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)), 1, 32)] + (broadcast((float32*)A_2[((((x.outer*32768) + (x.inner*1024)) + (k.outer*4)) + k.inner)], 32)*(float32x32*)B_2[ramp((((k.outer*4096) + (k.inner*1024)) + (y.outer*32)), 1, 32)]))

}

}

}

}

}

}

Loop Permutation

上面的IR,可以看到内循环行数据被矢量化,B被转换成PackedB。PackedB的遍历现在是连续的。因此,将研究A的访问模式。在当前调度中,A被逐列访问,这对缓存不友好。如果改变了KI和内轴席的嵌套循环顺序,则矩阵的访问模式更容易缓存。

s = te.create_schedule(C.op)

xo, yo, xi, yi = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)

(k,) = s[C].op.reduce_axis

ko, ki = s[C].split(k, factor=4)

# re-ordering

s[C].reorder(xo, yo, ko, xi, ki, yi)

s[C].vectorize(yi)

func = tvm.build(s, [A, B, C], target=target, name="mmult")

assert func

c = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), ctx)

func(a, b, c)

tvm.testing.assert_allclose(c.asnumpy(), answer, rtol=1e-5)

evaluator = func.time_evaluator(func.entry_name, ctx, number=10)

print("Opt3: %f" % evaluator(a, b, c).mean)

Out:

Opt3: 0.111657

Here is the generated IR after loop permutation.

print(tvm.lower(s, [A, B, C], simple_mode=True))

Out:

primfn(A_1: handle, B_1: handle, C_1: handle) -> ()

attr = {"global_symbol": "main", "tir.noalias": True}

buffers = {B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], []),

C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),

A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], [])}

buffer_map = {A_1: A, B_1: B, C_1: C} {

for (x.outer: int32, 0, 32) {

for (y.outer: int32, 0, 32) {

for (x.inner.init: int32, 0, 32) {

C_2[ramp((((x.outer*32768) + (x.inner.init*1024)) + (y.outer*32)), 1, 32)] = broadcast(0f32, 32)

}

for (k.outer: int32, 0, 256) {

for (x.inner: int32, 0, 32) {

for (k.inner: int32, 0, 4) {

C_2[ramp((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)), 1, 32)] = ((float32x32*)C_2[ramp((((x.outer*32768) + (x.inner*1024)) + (y.outer*32)), 1, 32)] + (broadcast((float32*)A_2[((((x.outer*32768) + (x.inner*1024)) + (k.outer*4)) + k.inner)], 32)*(float32x32*)B_2[ramp((((k.outer*4096) + (k.inner*1024)) + (y.outer*32)), 1, 32)]))

}

}

}

}

}

}

 

如何在CPU上优化GEMM(上)的更多相关文章

  1. 如何在CPU上优化GEMM(下)

    如何在CPU上优化GEMM(下) Array Packing 另一个重要的技巧是数组打包.这个技巧是对数组的存储维度进行重新排序,将某个维度上的连续访问模式在平滑后转换为顺序模式. 如上图所示,在阻塞 ...

  2. TVM在ARM GPU上优化移动深度学习

    TVM在ARM GPU上优化移动深度学习 随着深度学习的巨大成功,将深度神经网络部署到移动设备的需求正在迅速增长.与在台式机平台上所做的类似,在移动设备中使用GPU可以提高推理速度和能源效率.但是,大 ...

  3. MySQL数据库的优化(上)单机MySQL数据库的优化

    MySQL数据库的优化(上)单机MySQL数据库的优化 2011-03-08 08:49 抚琴煮酒 51CTO 字号:T | T 公司网站访问量越来越大,导致MySQL的压力越来越大,让我们自然想到的 ...

  4. 教你如何在Drcom下使用路由器上校园网(以广东工业大学、极路由1S HC5661A为例)

    免责声明: 在根据本教程进行实际操作时,如因您操作失误导致出现的一切意外,包括但不限于路由器变砖.故障.数据丢失等情况,概不负责: 该技术仅供学习交流,请勿将此技术应用于任何商业行为,所产生的法律责任 ...

  5. 如何在Linux中使用sFTP上传或下载文件与文件夹

    如何在Linux中使用sFTP上传或下载文件与文件夹 sFTP(安全文件传输程序)是一种安全的交互式文件传输程序,其工作方式与 FTP(文件传输协议)类似. 然而,sFTP 比 FTP 更安全;它通过 ...

  6. 【转】如何在Ubuntu 14.04 LTS上设置Nginx虚拟主机

    介绍 转自http://www.pandacademy.com/%E5%A6%82%E4%BD%95%E5%9C%A8ubuntu-14-04-lts%E4%B8%8A%E8%AE%BE%E7%BD% ...

  7. [Unity优化] Unity CPU性能优化

    前段时间本人转战unity手游,由于作者(Chwen)之前参与端游开发,有些端游的经验可以直接移植到手游,比如项目框架架构.代码设计.部分性能分析,而对于移动终端而言,CPU.内存.显卡甚至电池等硬件 ...

  8. 编译TensorFlow CPU指令集优化版

    编译TensorFlow CPU指令集优化版 如题,CPU指令集优化版,说的是针对某种特定的CPU型号进行过优化的版本.通常官方给的版本是没有针对特定CPU进行过优化的,有网友称,优化过的版本相比优化 ...

  9. Webfrom 上传 单个上传 多个上传

    文件上传控件:FileUpload - 控件,界面+方法+属性Button/LinkButton/ImageButton FileUpload控件:1.SaveAs("要上传到服务器的绝对路 ...

随机推荐

  1. hdu1960 最小路径覆盖

    题意:       给你明天的出租车订单,订单中包含每个人的起点和终点坐标,还有时间,如果一辆出租车想接一个乘客必须在每个订单前1分钟到达,也就是小于等于time-1,问你完成所有订单要最少多少量出租 ...

  2. hdu2167 方格取数 状态压缩dp

    题意:      方格取数,八个方向的限制. 思路:      八个方向的不能用最大流了,四个的可以,八个的不能抽象成二分图,所以目测只能用dp来跑,dp[i][j]表示的是第i行j状态的最优,具体看 ...

  3. node-util

    Node.js 常用工具 util 是一个Node.js 核心模块,提供常用函数的集合,用于弥补核心JavaScript 的功能 过于精简的不足. util.inherits util.inherit ...

  4. 【前端】vue2.x 配合 bootstrapTable 动态添加元素和绑定点击事件,事件无效 解决

    背景: 使用bootstrap-table 表格插件时,每一行的最后一班会加操作按钮列.如果不加入vue的话,使用插件自己的列属性formatter:function(value, row, inde ...

  5. Elastic-Job原理

    概述Elastic-Job是一个分布式调度解决方案,由两个相互独立的子项目Elastic-Job-Lite和Elastic-Job-Cloud组成. Elastic-Job-Lite定位为轻量级无中心 ...

  6. Python数模笔记-Sklearn(3)主成分分析

    主成分分析(Principal Components Analysis,PCA)是一种数据降维技术,通过正交变换将一组相关性高的变量转换为较少的彼此独立.互不相关的变量,从而减少数据的维数. 1.数据 ...

  7. Java GUI入门手册-AWT篇

    Java GUI入门手册: AWT是基本的GUI设计工具,重点学习其中的布局格式以及事件监听事件. 首先创建一个窗口,我们先分析Frame类中的方法: 通过上图,可以看出frame是由构造方法的重载: ...

  8. .NET Core 基于 Grafana Loki 日志初体验

    介绍 Loki: like Prometheus, but for logs. Loki是一个轻量级的日志系统,受到Prometheus项目的启发,由Grafana团队设计和开发,所以在Grafana ...

  9. 关于MySQL参数,这些你要知道

    前言: 在前面一些文章中,经常能看到介绍某某参数的作用,可能有些小伙伴仍搞不清楚 MySQL 参数是啥.本篇文章我们来聊聊 MySQL 参数,学习下如何管理维护 MySQL 参数. 1.MySQL参数 ...

  10. 新代(Syntec)机床的IP设置

    一.前言 通过以太网来做机床联网数据采集时,第一步通常是设置机床的IP和找网口 二.机床IP如何设置? 步骤一.找到设置IP的界面 [维护]>[网络设定] 步骤二.设置IP 设定[IP地址取得方 ...