Author: Jian Weng, Ruofei Yu

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

在本教程中,我们将演示如何使用 TVM 优化方阵乘法,并通过简单地添加 18 行额外的代码实现比基线快 200 倍。

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

  • 提高内存访问的缓存命中率。 高缓存命中率可以加速复杂的数值计算和热点内存访问。 这需要我们将原始内存访问模式转换为适合缓存策略的模式。
  • SIMD(单指令多数据),或者我们称之为向量处理单元。 每次都会处理一小批数据,而不是单个网格。 这需要我们将循环体中的数据访问模式转换为统一模式,以便 LLVM 后端可以将其降低到 SIMD。

实际上,本教程中使用的所有方法都是这个 repo 中提到的技巧的一个子集。 其中一些已被 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"
dev = tvm.device(target, 0)# Random generated tensor for testing
a = tvm.nd.array(numpy.random.rand(M, K).astype(dtype), dev)
b = tvm.nd.array(numpy.random.rand(K, N).astype(dtype), dev)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.numpy(), b.numpy())# 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 m, n: te.sum(A[m, k] * B[k, n], axis=k), name="C")# Default schedule
s = te.create_schedule(C.op)
func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=1)
print("Baseline: %f" % evaluator(a, b, c).mean)
Out:
Numpy running time: 0.014836
Baseline: 3.256232

在 TVM 中,我们始终可以检查较低级别的 IR 以调试或优化我们的调度。 这是使用我们的基线计划生成的 IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}buffer_map = {A_1: A, B_1: B, C_1: C} {for (m: int32, 0, 1024) {for (n: int32, 0, 1024) {C_2[((m*1024) + n)] = 0f32for (k: int32, 0, 1024) {let cse_var_2: int32 = (m*1024)let cse_var_1: int32 = (cse_var_2 + n)C_2[cse_var_1] = ((float32*)C_2[cse_var_1] + ((float32*)A_2[(cse_var_2 + k)]*(float32*)B_2[((k*1024) + n)]))}}}
}

Blocking

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

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)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), 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, dev, number=10)
print("Opt1: %f" % evaluator(a, b, c).mean)
Opt1: 0.289397

这是阻塞后生成的IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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_2[((((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: int32 = (n.outer*32)let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024))let cse_var_1: int32 = ((cse_var_2 + cse_var_3) + n.inner)C_2[cse_var_1] = ((float32*)C_2[cse_var_1] + ((float32*)A_2[((cse_var_2 + (k.outer*4)) + k.inner)]*(float32*)B_2[((((k.outer*4096) + (k.inner*1024)) + cse_var_3) + n.inner)]))}}}}}}
}

Vectorization

另一个重要技巧是矢量化。 当内存访问模式一致时,编译器可以检测到这种模式并将连续内存传递给向量处理器。 在 TVM 中,我们可以使用 vectorize 接口来提示编译器这种模式,这样我们就可以大大加速它。
在本教程中,我们选择向量化内部循环行数据,因为它是缓存友好的。

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, ki, mi, ni)# Vectorization
s[C].vectorize(ni)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=10)
print("Opt2: %f" % evaluator(a, b, c).mean)
Opt2: 0.328705

这是矢量化后生成的 IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@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, [1024, 1024], []),C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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_2[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: int32 = (n.outer*32)let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024))let cse_var_1: int32 = (cse_var_2 + cse_var_3)C_2[ramp(cse_var_1, 1, 32)] = ((float32x32*)C_2[ramp(cse_var_1, 1, 32)] + (broadcast((float32*)A_2[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*(float32x32*)B_2[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)]))}}}}}
}

Loop Permutation

如果我们查看上面的 IR,我们可以看到内部循环行数据针对 B 和 C 进行了矢量化。接下来我们将查看 A 的访问模式。在当前调度中,A 被逐列访问,即 不缓存友好。 如果我们改变 ki 和内轴 mi 的嵌套循环顺序,A 矩阵的访问模式对缓存更友好。

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)# re-ordering
s[C].reorder(mo, no, ko, mi, ki, ni)
s[C].vectorize(ni)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=10)
print("Opt3: %f" % evaluator(a, b, c).mean)
Opt3: 0.113126

这是循环置换后生成的 IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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_2[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: int32 = (n.outer*32)let cse_var_2: int32 = ((m.outer*32768) + (m.inner*1024))let cse_var_1: int32 = (cse_var_2 + cse_var_3)C_2[ramp(cse_var_1, 1, 32)] = ((float32x32*)C_2[ramp(cse_var_1, 1, 32)] + (broadcast((float32*)A_2[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*(float32x32*)B_2[ramp((((k.outer*4096) + (k.inner*1024)) + cse_var_3), 1, 32)]))}}}}}
}

Array Packing

另一个重要的技巧是数组打包。 诀窍是对多维数组的存储进行重新排序,以便在展平并存储在一维内存中之后顺序访问它。

注意:此图是阵列打包如何工作的一般说明。

我们可以使用数组打包来解决 B 的访问模式。观察展平后 B 的数组访问模式,当我们迭代 K 维时,它不是顺序的。 我们可以用维度 [K][N] 对 B 重新排序,使其具有 [N/bn][K][bn] 维度,其中 bn 是阻塞因子,也是内循环中 B 的向量大小。 这种重新排序将 N 拆分为两个维度 - bigN (N/bn) 和 littleN (bn) - 新维度 [N/bn][K][bn] 匹配 B 从外部到内部循环的索引(no, ko, ki, ni) 在展平后导致 B 的顺序访问模式。

# We have to re-write the algorithm slightly.
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)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=10)
print("Opt4: %f" % evaluator(a, b, c).mean)
Opt4: 0.106034

这是数组打包后生成的 IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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[ramp(((bigN*32768) + (k*32)), 1, 32)] = (float32x32*)B_2[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_2[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_2: int32 = ((m.outer*32768) + (m.inner*1024))let cse_var_1: int32 = (cse_var_2 + (n.outer*32))C_2[ramp(cse_var_1, 1, 32)] = ((float32x32*)C_2[ramp(cse_var_1, 1, 32)] + (broadcast((float32*)A_2[((cse_var_2 + (k.outer*4)) + k.inner)], 32)*(float32x32*)packedB[ramp((((n.outer*32768) + (k.outer*128)) + (k.inner*32)), 1, 32)]))}}}}}}
}

Write cache for block

阻塞后,程序会逐块将结果写入C,访问模式不是顺序的。 因此,我们可以使用顺序缓存数组来保存块结果,并在所有块结果准备好时写入 C。

s = te.create_schedule(C.op)# Allocate write cache
CC = s.cache_write(C, "global")mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)# Write cache is computed at no
s[CC].compute_at(s[C], no)# New inner axes
mc, nc = s[CC].op.axis(kaxis,) = s[CC].op.reduce_axis
ko, ki = s[CC].split(kaxis, factor=kfactor)
s[CC].reorder(ko, mc, ki, nc)
s[CC].vectorize(nc)# TODO: Add separate optimization step to discuss loop unrolloing
# unrolling is a loop optimization strategy which can reduce branch
# prediction failures and increases the chance of concurrent execution
# unroll kfactor loops
s[CC].unroll(ki)bigN, _, littleN = s[packedB].op.axis
s[packedB].vectorize(littleN)
s[packedB].parallel(bigN)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=10)
print("Opt5: %f" % evaluator(a, b, c).mean)
Opt5: 0.096499

这是阻塞后生成的IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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[ramp(((bigN*32768) + (k*32)), 1, 32)] = (float32x32*)B_2[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[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_3: int32 = (m.c*32)let cse_var_2: int32 = ((n.outer*32768) + (k.outer*128))let cse_var_1: int32 = (((m.outer*32768) + (m.c*1024)) + (k.outer*4)){C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[cse_var_1], 32)*(float32x32*)packedB[ramp(cse_var_2, 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 1)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 32), 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 2)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 64), 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 3)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 96), 1, 32)]))}}}for (m.inner: int32, 0, 32) {for (n.inner: int32, 0, 32) {C_2[((((m.outer*32768) + (m.inner*1024)) + (n.outer*32)) + n.inner)] = (float32*)C.global[((m.inner*32) + n.inner)]}}}}}
}

Parallel

此外,我们还可以利用多核处理器进行线程级并行化。

s = te.create_schedule(C.op)CC = s.cache_write(C, "global")mo, no, mi, ni = s[C].tile(C.op.axis[0], C.op.axis[1], bn, bn)s[CC].compute_at(s[C], no)mc, nc = s[CC].op.axis(kaxis,) = s[CC].op.reduce_axis
ko, ki = s[CC].split(kaxis, factor=kfactor)
s[CC].reorder(ko, mc, ki, nc)
s[CC].vectorize(nc)
s[CC].unroll(ki)# parallel
s[C].parallel(mo)bigN, _, littleN = s[packedB].op.axis
s[packedB].vectorize(littleN)
s[packedB].parallel(bigN)func = tvm.build(s, [A, B, C], target=target, name="mmult")
assert funcc = tvm.nd.array(numpy.zeros((M, N), dtype=dtype), dev)
func(a, b, c)
tvm.testing.assert_allclose(c.numpy(), answer, rtol=1e-5)evaluator = func.time_evaluator(func.entry_name, dev, number=50)
opt6_time = evaluator(a, b, c).mean
print("Opt6: %f" % opt6_time)
Opt6: 0.125081

这是并行化后生成的 IR。

print(tvm.lower(s, [A, B, C], simple_mode=True))
@main = primfn(A_1: handle, B_1: handle, C_1: handle) -> ()attr = {"from_legacy_te_schedule": True, "global_symbol": "main", "tir.noalias": True}buffers = {C: Buffer(C_2: Pointer(float32), float32, [1024, 1024], []),A: Buffer(A_2: Pointer(float32), float32, [1024, 1024], []),B: Buffer(B_2: Pointer(float32), float32, [1024, 1024], [])}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[ramp(((bigN*32768) + (k*32)), 1, 32)] = (float32x32*)B_2[ramp(((k*1024) + (bigN*32)), 1, 32)]}}for (m.outer: int32, 0, 32) "parallel" {allocate(C.global: Pointer(global float32), float32, [1024]), storage_scope = global;for (n.outer: int32, 0, 32) {for (m.c.init: int32, 0, 32) {C.global[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_3: int32 = (m.c*32)let cse_var_2: int32 = ((n.outer*32768) + (k.outer*128))let cse_var_1: int32 = (((m.outer*32768) + (m.c*1024)) + (k.outer*4)){C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[cse_var_1], 32)*(float32x32*)packedB[ramp(cse_var_2, 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 1)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 32), 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 2)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 64), 1, 32)]))C.global[ramp(cse_var_3, 1, 32)] = ((float32x32*)C.global[ramp(cse_var_3, 1, 32)] + (broadcast((float32*)A_2[(cse_var_1 + 3)], 32)*(float32x32*)packedB[ramp((cse_var_2 + 96), 1, 32)]))}}}for (m.inner: int32, 0, 32) {for (n.inner: int32, 0, 32) {C_2[((((m.outer*32768) + (m.inner*1024)) + (n.outer*32)) + n.inner)] = (float32*)C.global[((m.inner*32) + n.inner)]}}}}}
}

Summary

应用上述简单优化后,仅用 18 行代码,我们生成的代码可以达到使用 MKL 的 60% 的 numpy 性能。 请注意,网页上的输出反映了非专有 Docker 容器上的运行时间,因此它们是不可靠的。 强烈建议您自己运行本教程,以观察 TVM 所获得的性能提升。

How to Guides -- How to optimize GEMM on CPU相关推荐

  1. 【TVM 巡礼】How to optimize cpu(x86) gemm串讲

    [GiantPandaCV导语]最近在整理一些编译器方面的基础知识翻译,回顾了一下TVM的Schedule然后想起自己1年前做的一些GEMM相关的实验和探索没做什么总结.所以基于TVM的三个教程也即T ...

  2. 如何在CPU上优化GEMM矩阵乘法

    如何在CPU上优化GEMM矩阵乘法 How to optimize GEMM on CPU (TL;DR) TVM 提供抽象接口,允许用户分别描述算法和算法的实现组织(所谓的调度).通常,在高性能调度 ...

  3. 用Auto-TensorCore代码生成优化matmul

    用Auto-TensorCore代码生成优化matmul 将演示如何使用TVM Auto TensorCore CodeGen在Volta/Turing GPU上编写高性能matmul调度.这是一个透 ...

  4. Hinton等谈深度学习十年;PyTorch落地Linux基金会的影响;机器学习界的“GitHub”|AI系统前沿动态

    1. 重磅!PyTorch落地Linux基金会 扎克伯格亲自宣布,PyTorch基金会已新鲜成立,并归入Linux基金会旗下,管理委员会成员,包括Meta.AMD.AWS.谷歌云.微软和英伟达.Met ...

  5. e-a乘a的转置的秩_通用矩阵乘(GEMM)优化与卷积计算

    GEMM 主题文章写了两版,这是第一版,第二版参见我的博客. 采用知识共享 署名-非商业性使用-禁止演绎 4.0 国际许可授权,转载请注明出处. 引言 气象预报.石油勘探.核子物理等现代科学技术大多依 ...

  6. 矩阵乘法的优化及其在卷积中的应用

    公众号关注 "视学算法" 设为 "星标",DLCV消息即可送达! 作者:黎明灰烬 来源:https://zhuanlan.zhihu.com/p/6695839 ...

  7. gpu填充速率 计算_【经典回顾】Nvidia GPU 上的 CNN 计算速度变迁

    笔者从 2012 年初开始接触 GPU 编程,2014 年上半年开始接触 Caffe,可以毫不谦虚地说是"一天天看着 Nvidia GPU 和 Caffe 长大的". Nvidia ...

  8. bazel 工具函数

    文章目录 visibility 可配置的属性 config_setting glob select workspace repository_rule Python py_binary & p ...

  9. 如何在 CPU 上优化 GEMM

    如何在 CPU 上优化 GEMM (TL;DR) TVM 提供抽象接口,允许用户分别描述算法和算法的实施组织(所谓的调度).通常,在高性能调度中编写算法,会破坏算法的可读性和模块化.尝试各种看似有前途 ...

最新文章

  1. 同盾科技完成 7280 万美元 C 轮融资
  2. 批量处理Excel文件的模块----xlwings
  3. centos 7.x systemd service 配置方法整理
  4. 内核中 subsys_initcall 以及初始化标号
  5. java 执行oracle命令_利用oracle存储过程执行操作系统命令
  6. jquery中如何获得时间
  7. 大话数据结构4 - 初识单链表
  8. 夏普利值:全排列边际效益的平均
  9. ifv播放器android 版,ifv格式播放器
  10. 服务器系列和酷睿系列,至强cpu与酷睿两个系列之间有什么区别?
  11. jira是干什么_JIRA是什么? | 学步园
  12. OV2640拍摄jpg图像无法解析
  13. 人机融合智能时代的人心
  14. 华为nova8pro和荣耀60pro哪个好
  15. java实现给手机发短信验证码
  16. java实现栈计算器
  17. 解决安装Windows7后开启AHCI电脑无法启动
  18. python 计时器
  19. 13款宝马x5质量到底怎么样_为什么买宝马X5的车主都后悔不已?
  20. 1st Javascript Editor V3 8绝对完美破解版

热门文章

  1. 00.Sublime汉化、默认代码块、代码提示教程
  2. cisco 3560 QOS配置
  3. html5默认加载s文件夹,『总结』web前端开发常用代码整理
  4. 关于计算机方面的英语ppt,计算机专业英语ppt.ppt
  5. ElasticSearch7.17权限控制和规划实战
  6. 群晖DS918创建m.2 固态硬盘SSD读写缓存
  7. STM32F429 rtthread CAN总线
  8. linux没有网卡驱动能pxe吗,PXE所需要的网卡驱动制作
  9. 贝叶斯推理和机器学习中文版_机器学习如何使AI忘记知识表示和推理
  10. 雨中重装徒步攀登清凉峰