首页 > 编程语言 >TensorIR_张量程序抽象案例研究

TensorIR_张量程序抽象案例研究

时间:2024-04-03 15:45:47浏览次数:14  
标签:sch mm 张量 relu np 抽象 128 TensorIR float32

!pip install apache-tvm
import tvm
from tvm.ir.module import IRModule
from tvm.script import tir as T
import numpy as np
  • \(Y_{ij} = \sum_kA_{ik}B_{kj}\)
  • \(C_{ij} = \mathbb{relu}(Y_{ij}) = \mathbb{max}(Y_{ij}, 0)\)
dtype = "float32"
a_np = np.random.rand(128, 128).astype(dtype)
b_np = np.random.rand(128, 128).astype(dtype)
%%time
# a @ b is equivalent to np.matmul(a, b)
c_mm_relu = np.maximum(a_np @ b_np, 0)

我们使用low-level Numpy

  • 尽量不使用numpy提供的函数,使用循环
  • 尽量显式开对应的数组
def llnp_mm_relu(A: np.array, B: np.array, C: np.array):
    Y = np.empty([128, 128], dtype="float32")
    # 外层循环遍历矩阵A的行
    for i in range(128):
        # 中层循环遍历矩阵B的列
        for j in range(128):
            # 内层循环遍历矩阵A的列(同时也是矩阵B的行)
            for k in range(128):
                if k == 0:
                    Y[i, j] = 0
                Y[i, j] = Y[i, j] + A[i, k] * B[k, j]

    for i in range(128):
        for j in range(128):
            C[i, j] = max([Y[i, j], 0])
c_np = np.empty([128, 128], dtype=dtype)
llnp_mm_relu(a_np, b_np, c_np)
np.testing.assert_allclose(c_mm_relu, c_np, rtol=1e-5)

这里的代码是用一种名为 TVMScript 的语言实现的,它是一种嵌入在 Python AST 中的特定领域方言。

@tvm.script.ir_module
class MyModule:
    @T.prim_func
    def mm_relu(A: T.Buffer((128, 128), "float32"),
                B: T.Buffer((128, 128), "float32"),
                C: T.Buffer((128, 128), "float32")):
        # global_symbol: 最后的函数名; noalias: 相当于Cpp的restrict
        T.func_attr({"global_symbol": "mm_relu", "tir.noalias": True})
        Y = T.alloc_buffer((128, 128), dtype="float32")
        for i, j, k in T.grid(128, 128, 128):   # 多层循环语法糖
            with T.block("Y"):
                vi = T.axis.spatial(128, i)
                vj = T.axis.spatial(128, j)
                vk = T.axis.reduce(128, k)
                with T.init():
                    Y[vi, vj] = T.float32(0)
                Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
        for i, j in T.grid(128, 128):
            with T.block("C"):
                vi = T.axis.spatial(128, i)
                vj = T.axis.spatial(128, j)
                C[vi, vj] = T.max(Y[vi, vj], T.float32(0))

img

块轴绑定的语法糖

@tvm.script.ir_module
class MyModuleWithAxisRemapSugar:
    @T.prim_func
    def mm_relu(A: T.Buffer((128, 128), "float32"),
                B: T.Buffer((128, 128), "float32"),
                C: T.Buffer((128, 128), "float32")):
        T.func_attr({"global_symbol": "mm_relu", "tir.noalias": True})
        Y = T.alloc_buffer((128, 128), dtype="float32")
        for i, j, k in T.grid(128, 128, 128):
            with T.block("Y"):
                # 块轴绑定的语法糖 SSR: Sepatial Sepatial Reduce
                vi, vj, vk = T.axis.remap("SSR", [i, j, k])
                with T.init():
                    Y[vi, vj] = T.float32(0)
                Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]
        for i, j in T.grid(128, 128):
            with T.block("C"):
                # 块轴绑定的语法糖 SS: Sepatial Sepatial
                vi, vj = T.axis.remap("SS", [i, j])
                C[vi, vj] = T.max(Y[vi, vj], T.float32(0))
type(MyModule)
type(MyModule["mm_relu"])

机器学习编译过程中的一个 IRModule 可以包含多个张量函数

@tvm.script.ir_module
class MyModuleWithTwoFunctions:
    @T.prim_func
    def mm(A: T.Buffer((128, 128), "float32"),
           B: T.Buffer((128, 128), "float32"),
           Y: T.Buffer((128, 128), "float32")):
        T.func_attr({"global_symbol": "mm", "tir.noalias": True})
        for i, j, k in T.grid(128, 128, 128):
            with T.block("Y"):
                vi, vj, vk = T.axis.remap("SSR", [i, j, k])
                with T.init():
                    Y[vi, vj] = T.float32(0)
                Y[vi, vj] = Y[vi, vj] + A[vi, vk] * B[vk, vj]

    @T.prim_func
    def relu(A: T.Buffer((128, 128), "float32"),
             B: T.Buffer((128, 128), "float32")):
        T.func_attr({"global_symbol": "relu", "tir.noalias": True})
        for i, j in T.grid(128, 128):
            with T.block("B"):
                vi, vj = T.axis.remap("SS", [i, j])
                B[vi, vj] = T.max(A[vi, vj], T.float32(0))

元张量函数的变换

def llnp_mm_relu_v2(A: np.array, B: np.array, C: np.array):
    Y = np.empty((128, 128), dtype='float32')
    for i in range(128):
        for j0 in range(32):
            for k in range(128):
                for j1 in range(4):
                    j = j0 * 4 + j1
                    if k == 0:
                        Y[i, j] = 0
                    Y[i, j] = Y[i, j] + A[i, k] * B[k, j]
    for i in range(128):
        for j in range(128):
            C[i, j] = max(Y[i, j], 0)
c_np = np.empty((128, 128), dtype=dtype)
llnp_mm_relu_v2(a_np, b_np, c_np)
np.testing.assert_allclose(c_mm_relu, c_np, rtol=1e-5)
import IPython

def code2html(code):
    """
    Helper function to use pygments to turn the code string into hightlight huml.
    """
    import pygments
    from pygments.lexers import Python3Lexer
    from pygments.formatters import HtmlFormatter
    formatter = HtmlFormatter(style='monokai')
    html = pygments.highlight(code, Python3Lexer(), formatter)
    return "<style>%s</style>%s\n" % (formatter.get_style_defs(".highlight"), html)
IPython.display.HTML(code2html(MyModule.script()))

我们首先创建一个以给定的 MyModule 作为输入的 Schedule 辅助类

sch = tvm.tir.Schedule(MyModule)

然后我们执行以下操作以获得对块 Y 和相应循环的引用

block_Y = sch.get_block("Y", func_name="mm_relu")
i, j, k = sch.get_loops(block_Y)

我们将执行的第一个变换是将循环 j 分成两个循环,其中内部循环的长度为 4。

请注意,变换是程序性的,因此如果你不小心执行了两次该代码块,我们将得到“变量 j 不再存在”的错误。如果发生这种情况,你可以从头(创建 sch 的位置)开始再次运行

j0, j1 = sch.split(j, factors=[None, 4])

我们可以查看存储在 sch.mod 中的变换结果

IPython.display.HTML(code2html(MyModule.script()))
IPython.display.HTML(code2html(sch.mod.script()))
sch.reorder(j0, k, j1)
IPython.display.HTML(code2html(sch.mod.script()))

我们使用名为 reverse_compute_at 的原语将块 C 移动到 Y 的内循环里

block_C = sch.get_block("C", "mm_relu")
sch.reverse_compute_at(block_C, j0)
IPython.display.HTML(code2html(sch.mod.script()))

到目前为止,我们将归约初始化和更新放在一个块体中。这种组合形式为循环变换带来了便利(因为初始化和更新的外循环 i、j 通常需要彼此保持同步)

在循环变换之后,我们可以将 Y 元素的初始化与归约更新分开。我们可以通过 decompose_reduction 原语来做到这一点。(注意:这也是 TVM 在以后编译的时候隐式做的,所以这一步的主要目的是让它显式,看看最终效果)

sch.decompose_reduction(block_Y, k)
IPython.display.HTML(code2html(sch.mod.script()))

最终变换后的代码类似于以下低级 NumPy 代码

def llnp_mm_relu_v3(A: np.ndarray, B: np.ndarray, C: np.ndarray):
    Y = np.empty((128, 128), dtype="float32")
    for i in range(128):
        for j0 in range(32):
            # Y_init
            for j1 in range(4):
                j = j0 * 4 + j1
                Y[i, j] = 0
            # Y_update
            for k in range(128):
                for j1 in range(4):
                    j = j0 * 4 + j1
                    Y[i, j] = Y[i, j] + A[i, k] * B[k, j]
            # C
            for j1 in range(4):
                j = j0 * 4 + j1
                C[i, j] = max(Y[i, j], 0)
c_np = np.empty((128, 128), dtype=dtype)
llnp_mm_relu_v3(a_np, b_np, c_np)
np.testing.assert_allclose(c_mm_relu, c_np, rtol=1e-5)
rt_lib = tvm.build(MyModule, target="llvm")
a_nd = tvm.nd.array(a_np)
b_nd = tvm.nd.array(b_np)
c_nd = tvm.nd.empty((128, 128), dtype="float32")
type(c_nd)
rt_lib_after = tvm.build(sch.mod, target="llvm")
rt_lib_after["mm_relu"](a_nd, b_nd, c_nd)

np.testing.assert_allclose(c_mm_relu, c_nd.numpy(), rtol=1e-5)
f_timer_before = rt_lib.time_evaluator("mm_relu", tvm.cpu())
print("Time cost of MyModule %g sec" % f_timer_before(a_nd, b_nd, c_nd).mean)
f_timer_after = rt_lib_after.time_evaluator("mm_relu", tvm.cpu())
print("Time cost of transformed sch.mod %g sec" % f_timer_after(a_nd, b_nd, c_nd).mean)
IPython.display.HTML(code2html(MyModule.script()))
IPython.display.HTML(code2html(sch.mod.script()))

img

现代 CPU 带有多级缓存,需要先将数据提取到缓存中,然后 CPU 才能访问它。

重要的是,访问已经在缓存中的数据要快得多。CPU 采用的一种策略是获取彼此更接近的数据。 当我们读取内存中的一个元素时,它会尝试将附近的元素(更正式的名称为“缓存行”)获取到缓存中。 因此,当你读取下一个元素时,它已经在缓存中。 因此,具有连续内存访问的代码通常比随机访问内存不同部分的代码更快。

img

现在让我们看看上面的迭代可视化,分析一下是怎么回事。 在这个分析中,让我们关注最里面的两个循环:k 和 j1。高亮的地方显示了当我们针对 k 的一个特定实例迭代 j1 时迭代触及的 Y、A 和 B 中的相应区域。

我们可以发现,j1 这一迭代产生了对 B 元素的连续访问。具体来说,它意味着在 j1=0 和 j1=1 时我们读取的值彼此相邻。这可以让我们拥有更好的缓存访问行为。此外,我们使 C 的计算更接近 Y,从而实现更好的缓存行为。

我们当前的示例主要是为了证明不同的代码变体可以导致不同的性能。更多的变换步骤可以帮助我们获得更好的性能,我们将在以后的章节中介绍。本练习的主要目标是首先让我们获得程序变换工具,并首先体验通过变换可能实现的功能。

练习

作为练习,尝试不同的 j_factor 选择,看看它们如何影响代码的性能

def transform(mod, jfactor):
    sch = tvm.tir.Schedule(mod)
    block_Y = sch.get_block("Y", func_name="mm_relu")
    i, j, k = sch.get_loops(block_Y)
    j0, j1 = sch.split(j, factors=[None, jfactor])
    sch.reorder(j0, k, j1)
    block_C = sch.get_block("C", "mm_relu")
    sch.reverse_compute_at(block_C, j0)
    return sch.mod

mod_transformed = transform(MyModule, jfactor=8)

rt_lib_transformed = tvm.build(mod_transformed, "llvm")
f_timer_transformed = rt_lib_transformed.time_evaluator("mm_relu", tvm.cpu())
print("Time cost of transformed mod_transformed %g sec" % f_timer_transformed(a_nd, b_nd, c_nd).mean)
# display the code below
IPython.display.HTML(code2html(mod_transformed.script()))

创建 TensorIR 并与之交互的方法

通过 TVMScript 创建 TensorIR

获取 TensorIR 函数的第一种方法是直接在 TVMScript 中编写函数,这也是我们在上一节中使用的方法。 TVMScript 还允许我们在必要时跳过某些信息部分。 例如,T.axis.remap 使我们能够缩短迭代器大小注释。

TVMScript 也是一种在变换过程中检查张量函数的有用方法。在某些情况下,一种很有帮助的做法是打印出 TVMScript,进行一些手动编辑,然后将其反馈给机器学习编译流程以调试和尝试可能的(手动)变换,然后将变换后的程序重新应用到 MLC 流程中。
使用张量表达式生成 TensorIR 代码

在许多情况下,我们的开发形式是不在循环级别的更高级别的抽象。 所以另一种常见的获取 TensorIR 的方式是务实地生成相关代码。

张量表达式 (TE) 是一种特定领域的语言,它通过 API 之类的表达式描述一系列计算。

from tvm import te
A = te.placeholder((128, 128), "float32", name="A")
B = te.placeholder((128, 128), "float32", name="B")
k = te.reduce_axis((0, 128), "k")
Y = te.compute((128, 128), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k), name="Y")
C = te.compute((128, 128), lambda i, j: te.max(Y[i, j], 0), name="C")

这里 te.compute 采用签名 te.compute(output_shape, fcompute)。 fcompute 函数描述了对于给定的索引 (i, j) 我们要如何计算元素 Y[i, j] 的值。

lambda i, j: te.sum(A[i, k] * B[k, j], axis=k)

上面的 lambda 表达式描述了计算 \(Y_{ij} = \sum_kA_{ik}B_{kj}\)。在描述计算之后,我们可以通过传递我们感兴趣的相关参数来创建一个 TensorIR 函数。在这种特殊情况下,我们想要创建一个具有两个输入参数(A,B)和一个输出参数(C)的函数。

te_func = te.create_prim_func([A, B, C]).with_attr({"global_symbol": "mm_relu"})
MyModuleFromTE = tvm.IRModule({"mm_relu": te_func})
IPython.display.HTML(code2html(MyModuleFromTE.script()))

张量表达式 API 作为一个有用的工具,帮助为给定的更高级别的输入生成 TensorIR 函数。

讨论

我们了解到,一个常见的机器学习编译过程遵循一系列程序变换。将 TensorIR 变换过程与低级 NumPy 参考开发过程进行比较是很有趣的。

img

上图显示了标准的开发过程。我们需要重复开发不同程序变体的过程,然后(如果它是编译语言,则构建)在感兴趣的平台上运行它们。

机器学习编译流程(如下图所示)的主要区别在于 IRModule(程序)之间的程序变换。所以我们不仅可以通过开发(通过手动编写代码或生成代码)提出程序变体,还可以通过变换张量程序来获得变体。

变换是一个非常强大的工具,可以帮助我们简化开发成本并为流程引入更多自动化。本节介绍了通过 TensorIR 对元张量函数的特定视角,我们将在未来涵盖更多视角。

img

总结

  • TensorIR 抽象
  • 包含循环、多维缓冲区等常用元素
    • 引入了一个封装循环计算要求的新结构块。
    • 可以在 Python AST 中构建(通过 TVMScript)
  • 我们可以使用变换来创建不同的 TensorIR 变体。
  • 通用 MLC 流程:开发、变换、构建。

标签:sch,mm,张量,relu,np,抽象,128,TensorIR,float32
From: https://www.cnblogs.com/moguw/p/18112826

相关文章

  • 【JAVA】抽象类是什么?为什么要用抽象类?
    抽象类是什么?在面向对象编程(OOP)中,抽象类(AbstractClass)是一种特殊的类,它主要用于表示一组相关类的共同特征,但不能直接创建实例对象。抽象类通常包含抽象方法(AbstractMethod),抽象方法没有具体实现,只有方法签名,即方法名、参数列表和返回类型,但没有方法体。抽象方法在抽象类中用......
  • 抽象工厂模式
    定义publicabstractclassYabo{publicabstractvoidprint();}publicclassWuhanYabo:Yabo{publicWuhanYabo(){Console.WriteLine("我是武汉鸭脖");}publicoverridevoidprint(){Console.WriteLine("我......
  • java的抽象类和接口
    抽象类:abstract,可以用此关键字修饰类和方法abstract修饰类就是抽象类,修饰方法就是抽象方法抽象类的注意事项,特点:抽象类不一定有抽象方法,但有抽象方法的类一定是抽象类类该有的成员(成员变量,方法,构造器)抽象类也可以有抽象类的特点:抽象类不能创建对象,仅仅作为一种特殊的父......
  • 工厂方法模式与抽象工厂模式的深度对比
    ​......
  • 抽象类、案例
    抽象类必须使用abstract修饰:   修饰符 abstract class 类名{}抽象方法:就是抽象类中定义的子类必须完成的功能的基本要求。没有方法体,只有方法签名,必须abstract修饰例子:父类:packageabstract_class;publicabstractclassAnimal{publicabstractv......
  • Java 抽象类、接口、内部类
    抽象类Java中的抽象类是一种不能被实例化的类,它用于定义子类必须实现的方法和属性。以下是一些关于Java抽象类的关键点:抽象方法:抽象类可以包含抽象方法,这些方法只有声明没有实现,且必须在任何非抽象子类中被覆写实现。构造方法:虽然抽象类不能直接实例化,但它可以有构造方法。......
  • 数据结构之————线性表ADT、以数组存储方式实现抽象类型的一个实例
    前言:基础填坑1、ADT在文章开始前,我们要弄明白什么是ADT(AbstractDataType)抽象数据类型1、ADT是用户定义的数据类型,它包含一组数据以及在这组数据上进行的操作。只定义操作的行为,没有具体的实现细节2、它存在的目的是使我们能够独立于程序的实现细节来理解数据结构的特......
  • 嵌入式开发中设备抽象的具体实现
    概要在嵌入式开发过程中,设备抽象具有极其重要的作用。以下是设备抽象的几个重要方面:平台无关性:设备抽象可以使得软件与底层硬件解耦,从而实现平台无关性。通过抽象出设备的功能接口和操作方法,可以使得相同的代码可以在不同的硬件平台上运行,而无需进行大量的修改。这对于......
  • Java抽象类详解:定义、特性与实例化限制(day12)
    抽象类总结一下今天老师上课的内容,前面几节课听得是有点懵,在讲到内存问题,也就是代码在栈、堆、以及方法区是怎么执行的,听得不是很懂,今天讲到抽象类以及重写的机制,似乎开始慢慢懂得了java的底层原理。父类:子类:上面的Cat类重写父类的eat()方法。Test:如果我们将父类的......
  • Scala第十三章节(作为值的函数及匿名函数、柯里化、闭包及控制抽象以及计算器案例)
    章节目标掌握作为值的函数及匿名函数的用法了解柯里化的用法掌握闭包及控制抽象的用法掌握计算器案例1.高阶函数介绍Scala混合了面向对象和函数式的特性,在函数式编程语言中,函数是“头等公民”,它和Int、String、Class等其他类型处于同等的地位,可以像其他类型的变量一样......