掘金 人工智能 06月23日 15:53
【TVM 教程】在 TVM 中使用 Bring Your Own Datatypes
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍了如何在 Apache TVM 深度学习编译框架中使用自定义数据类型,利用 Bring Your Own Datatypes (BYODT) 框架,允许用户注册自己的数据类型实现,并将其插入 TVM 中。文章通过一个名为“myfloat”的示例,演示了如何设置、注册自定义类型,以及解决在编译过程中遇到的错误。最终,展示了如何在 MobileNet 模型中应用自定义数据类型,并运行模型。

🔑 Bring Your Own Datatypes (BYODT) 框架允许用户在 TVM 中注册自定义数据类型,例如位置库、定点数库等,扩展了 TVM 的数据类型支持。

⚙️ 使用自定义数据类型前,需要在 TVM 中注册该数据类型,并提供相应的降级函数,告诉 TVM 如何将操作降级为 TVM 理解的数据类型。

💡 通过示例“myfloat”,演示了如何处理编译过程中出现的错误,例如“Cast”和“Add”降级函数未找到的问题,并提供了相应的解决方案。

🖼️ 展示了如何在 MobileNet 模型中使用自定义数据类型,需要转换网络、转换参数和输入,才能使模型在内部使用自定义数据类型运行。

Apache TVM 是一个深度的深度学习编译框架,适用于 CPU、GPU 和各种机器学习加速芯片。更多 TVM 中文文档可访问 →tvm.hyper.ai/作者Gus SmithAndrew Liu

本教程将展示如何利用 Bring Your Own Datatypes 框架在 TVM 中使用自定义数据类型。注意,Bring Your Own Datatypes 框架目前仅处理数据类型的软件模拟版本。该框架不支持开箱即用地编译自定义加速器数据类型。

数据类型库

Bring Your Own Datatypes 允许用户在 TVM 的原生数据类型(例如 float)旁边注册自己的数据类型实现。这些数据类型实现通常以库的形式出现。例如:

Bring Your Own Datatypes 使用户能够将这些数据类型实现插入 TVM!

本节中我们将用到一个已经实现的示例库(位于 3rdparty/byodt/myfloat.cc)。这种称之为「myfloat」的数据类型实际上只是一个 IEE-754 浮点数,但它提供了一个有用的示例,表明任何数据类型都可以在 BYODT 框架中使用。

设置

由于不使用任何 3rdparty 库,因此无需设置。

若要用自己的数据类型库尝试,首先用 CDLL 把库的函数引入进程空间:

ctypes.CDLL('my-datatype-lib.so', ctypes.RTLD_GLOBAL)

一个简单的 TVM 程序

从在 TVM 中编写一个简单的程序开始,之后进行重写,从而使用自定义数据类型。

import tvmfrom tvm import relay# 基本程序:Z = X + Yx = relay.var("x", shape=(3,), dtype="float32")y = relay.var("y", shape=(3,), dtype="float32")z = x + yprogram = relay.Function([x, y], z)module = tvm.IRModule.from_expr(program)

现使用 numpy 为程序创建随机输入:

import numpy as npnp.random.seed(23)  # 可重复性x_input = np.random.rand(3).astype("float32")y_input = np.random.rand(3).astype("float32")print("x: {}".format(x_input))print("y: {}".format(y_input))

输出结果:

x: [0.51729786 0.9469626  0.7654598 ]y: [0.28239584 0.22104536 0.6862221 ]

最后,准备运行程序:

z_output = relay.create_executor(mod=module).evaluate()(x_input, y_input)print("z: {}".format(z_output))

输出结果:

/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.  "target_host parameter is going to be deprecated. "z: [0.7996937 1.168008  1.4516819]

添加自定义数据类型

接下来使用自定义数据类型进行中间计算。

使用与上面相同的输入变量 x 和 y,但在添加 x + y 之前,首先通过调用 relay.cast(...) 将 x 和 y 转换为自定义数据类型。

注意如何指定自定义数据类型:使用特殊的 custom[...] 语法来表示。此外,注意数据类型后面的「32」:这是自定义数据类型的位宽,告诉 TVM myfloat 的每个实例都是 32 位宽。

try:    with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):        x_myfloat = relay.cast(x, dtype="custom[myfloat]32")        y_myfloat = relay.cast(y, dtype="custom[myfloat]32")        z_myfloat = x_myfloat + y_myfloat        z = relay.cast(z_myfloat, dtype="float32")except tvm.TVMError as e:    # 打印最后一行错误    print(str(e).split("\n")[-1])

尝试生成此程序会从 TVM 引发错误。TVM 不知道如何创造性地处理所有自定义数据类型!因此首先要从 TVM 注册自定义类型,给它一个名称和一个类型代码:

tvm.target.datatype.register("myfloat", 150)

注意,类型代码 150 目前由用户手动选择。参阅 include/tvm/runtime/c_runtime_api.h 中的 TVMTypeCode::kCustomBegin。下面再次生成程序:

x_myfloat = relay.cast(x, dtype="custom[myfloat]32")y_myfloat = relay.cast(y, dtype="custom[myfloat]32")z_myfloat = x_myfloat + y_myfloatz = relay.cast(z_myfloat, dtype="float32")program = relay.Function([x, y], z)module = tvm.IRModule.from_expr(program)module = relay.transform.InferType()(module)

现在有了一个使用 myfloat 的Relay 程序!

print(program)

输出结果:

fn (%x: Tensor[(3), float32], %y: Tensor[(3), float32]) {  %0 = cast(%x, dtype="custom[myfloat]32");  %1 = cast(%y, dtype="custom[myfloat]32");  %2 = add(%0, %1);  cast(%2, dtype="float32")}

现在可以准确无误地表达程序,尝试运行!

try:    with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):        z_output_myfloat = relay.create_executor("graph", mod=module).evaluate()(x_input, y_input)        print("z: {}".format(y_myfloat))except tvm.TVMError as e:    # 打印最后一行错误    print(str(e).split("\n")[-1])

输出结果:

Check failed: (lower) is false: Cast lowering function for target llvm destination type 150 source type 2 not found

编译该程序会引发错误,下面来剖析这个报错。

该报错发生在代码降级的过程中,即将自定义数据类型代码,降级为 TVM 可以编译和运行的代码。TVM 显示,当从源类型 2(float,在 TVM 中)转换到目标类型 150(自定义数据类型)时,它无法找到 Cast 操作的降级函数

当对自定义数据类型进行降级时,若 TVM 遇到对自定义数据类型的操作,它会查找用户注册的降级函数,这个函数告诉 TVM 如何将操作降级为 TVM 理解的数据类型的操作。由于我们还没有告诉 TVM 如何降级自定义数据类型的 Cast 操作,因此会报错。

要修复这个错误,只需要指定一个降级函数:

tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func(        {            (32, 32): "FloatToCustom32",  # cast from float32 to myfloat32 # 从 float32 转换为 myfloat32        }    ),    "Cast",    "llvm",    "float",    "myfloat",)

register_op(...) 调用接受一个降级函数和一些参数,这些参数准确地指定了应该使用提供的降级函数降级的操作。在这种情况下,传递的参数指定此降级函数用于将 target “llvm” 的 Cast 从 float 降级到 myfloat

传递给此调用的降级函数非常通用:它应该采用指定类型的操作(在本例中为 Cast)并返回另一个仅使用 TVM 理解的数据类型的操作。

通常,我们希望用户借助对外部库的调用,来对其自定义数据类型进行操作。在示例中,myfloat 库在函数 FloatToCustom32 中实现了从 float 到 32 位 myfloat 的转换。一般情况下,创建一个辅助函数 create_lower_func(...),它的作用是:给定一个字典,它将给定的 Call的操作,替换为基于操作和位宽的适当函数名称。它还通过将自定义数据类型存储在适当宽度的不透明 uint 中,从而删除自定义数据类型的使用;在我们的例子中,如 uint32_t。有关更多信息,参阅 源代码

# 现在重新尝试运行程序:try:    with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):        z_output_myfloat = relay.create_executor("graph", mod=module).evaluate()(x_input, y_input)        print("z: {}".format(z_output_myfloat))except tvm.TVMError as e:    # 打印最后一行错误    print(str(e).split("\n")[-1])

输出结果:

Check failed: (lower) is false: Add lowering function for target llvm type 150 not found

新报错提示无法找到 Add 降级函数,这并不是坏事儿,这表明错误与 Cast无关!接下来只需要在程序中为其他操作注册降级函数。

注意,对于 Addcreate_lower_func 接受一个键(key)是整数的字典。对于 Cast 操作,需要一个 2 元组来指定 src_bit_length 和 dest_bit_length,对于其他操作,操作数之间的位长度相同,因此只需要一个整数来指定 bit_length

tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Add"}),    "Add",    "llvm",    "myfloat",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({(32, 32): "Custom32ToFloat"}),    "Cast",    "llvm",    "myfloat",    "float",)# 现在,可以正常运行程序了。with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):    z_output_myfloat = relay.create_executor(mod=module).evaluate()(x_input, y_input)print("z: {}".format(z_output_myfloat))print("x:\t\t{}".format(x_input))print("y:\t\t{}".format(y_input))print("z (float32):\t{}".format(z_output))print("z (myfloat32):\t{}".format(z_output_myfloat))# 或许正如预期的那样,``myfloat32`` 结果和 ``float32`` 是完全一样的!

输出结果:

/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.  "target_host parameter is going to be deprecated. "z: [0.7996937 1.168008  1.4516819]x:              [0.51729786 0.9469626  0.7654598 ]y:              [0.28239584 0.22104536 0.6862221 ]z (float32):    [0.7996937 1.168008  1.4516819]z (myfloat32):  [0.7996937 1.168008  1.4516819]

使用自定义数据类型运行模型

首先选择要使用 myfloat 运行的模型,本示例中,我们使用的是 Mobilenet。选择 Mobilenet 是因为它足够小。在 Bring Your Own Datatypes 框架的这个 alpha 状态下,还没有为运行自定义数据类型的软件仿真实现任何软件优化;由于多次调用数据类型仿真库,导致性能不佳。

首先定义两个辅助函数,获取 mobilenet 模型和猫图像。

def get_mobilenet():    dshape = (1, 3, 224, 224)    from mxnet.gluon.model_zoo.vision import get_model    block = get_model("mobilenet0.25", pretrained=True)    shape_dict = {"data": dshape}    return relay.frontend.from_mxnet(block, shape_dict)def get_cat_image():    from tvm.contrib.download import download_testdata    from PIL import Image    url = "https://gist.githubusercontent.com/zhreshold/bcda4716699ac97ea44f791c24310193/raw/fa7ef0e9c9a5daea686d6473a62aacd1a5885849/cat.png"    dst = "cat.png"    real_dst = download_testdata(url, dst, module="data")    img = Image.open(real_dst).resize((224, 224))    # CoreML's standard model image format is BGR    img_bgr = np.array(img)[:, :, ::-1]    img = np.transpose(img_bgr, (2, 0, 1))[np.newaxis, :]    return np.asarray(img, dtype="float32")module, params = get_mobilenet()

输出结果:

Downloading /workspace/.mxnet/models/mobilenet0.25-9f83e440.zipe0e3327d-26bc-4c47-aed4-734a16b0a3f8 from https://apache-mxnet.s3-accelerate.dualstack.amazonaws.com/gluon/models/mobilenet0.25-9f83e440.zip...

用原生 TVM 很容易执行 MobileNet:

ex = tvm.relay.create_executor("graph", mod=module, params=params)input = get_cat_image()result = ex.evaluate()(input).numpy()# 打印前 10 个元素print(result.flatten()[:10])

输出结果:

/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.  "target_host parameter is going to be deprecated. "[ -7.5350165   2.0368009 -12.706646   -5.63786   -12.684058    4.0723605   2.618876    3.4049501  -9.867913  -24.53311  ]

若要更改模型在内部使用 myfloat,需要转换网络。为此首先定义一个函数来帮助转换张量:

def convert_ndarray(dst_dtype, array):    """Converts an NDArray into the specified datatype"""    x = relay.var("x", shape=array.shape, dtype=str(array.dtype))    cast = relay.Function([x], x.astype(dst_dtype))    with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):        return relay.create_executor("graph").evaluate(cast)(array)

为了实际转换整个网络,我们在 Relay 中编写了 一个 pass,它简单地将模型中的所有节点转换为使用新的数据类型。

from tvm.relay.frontend.change_datatype import ChangeDatatypesrc_dtype = "float32"dst_dtype = "custom[myfloat]32"module = relay.transform.InferType()(module)# 目前,自定义数据类型仅在预先运行 simple_inference 时才有效module = tvm.relay.transform.SimplifyInference()(module)# 在更改数据类型之前运行类型推断module = tvm.relay.transform.InferType()(module)# 将数据类型从 float 更改为 myfloat 并重新推断类型cdtype = ChangeDatatype(src_dtype, dst_dtype)expr = cdtype.visit(module["main"])module = tvm.relay.transform.InferType()(module)# 转换参数:params = {k: convert_ndarray(dst_dtype, v) for k, v in params.items()}# 还需要转换输入:input = convert_ndarray(dst_dtype, input)# 最后,可以尝试运行转换后的模型:try:    # 向量化不是用自定义数据类型实现的。    with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):        result_myfloat = tvm.relay.create_executor("graph", mod=module).evaluate(expr)(            input, **params        )except tvm.TVMError as e:    print(str(e).split("\n")[-1])

输出结果:

/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.  "target_host parameter is going to be deprecated. "  Check failed: (lower) is false: Intrinsic lowering function for target llvm, intrinsic name tir.sqrt, type 150 not found

尝试运行模型时,会收到一个熟悉的报错,提示需要为 myfloat 注册更多函数。

因为这是一个神经网络,所以需要更多的操作。下面注册所有需要的函数:

tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "FloatToCustom32"}),    "FloatImm",    "llvm",    "myfloat",)tvm.target.datatype.register_op(    tvm.target.datatype.lower_ite, "Call", "llvm", "myfloat", intrinsic_name="tir.if_then_else")tvm.target.datatype.register_op(    tvm.target.datatype.lower_call_pure_extern,    "Call",    "llvm",    "myfloat",    intrinsic_name="tir.call_pure_extern",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Mul"}),    "Mul",    "llvm",    "myfloat",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Div"}),    "Div",    "llvm",    "myfloat",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Sqrt"}),    "Call",    "llvm",    "myfloat",    intrinsic_name="tir.sqrt",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Sub"}),    "Sub",    "llvm",    "myfloat",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Exp"}),    "Call",    "llvm",    "myfloat",    intrinsic_name="tir.exp",)tvm.target.datatype.register_op(    tvm.target.datatype.create_lower_func({32: "Custom32Max"}),    "Max",    "llvm",    "myfloat",)tvm.target.datatype.register_min_func(    tvm.target.datatype.create_min_lower_func({32: "MinCustom32"}, "myfloat"),    "myfloat",)

注意,我们使用的是:register_min_func 和 create_min_lower_func

register_min_func 接收一个整数 num_bits 作为位长,然后返回一个表示最小有限可表示值的操作,这个值是具有指定位长的自定义数据类型。

与 register_op 和 create_lower_func 类似,create_min_lower_func 处理通过调用一个外部库,实现最小可表示的自定义数据类型值的一般情况。

接下来运行模型:

# 向量化不是用自定义数据类型实现的。with tvm.transform.PassContext(config={"tir.disable_vectorize": True}):    result_myfloat = relay.create_executor(mod=module).evaluate(expr)(input, **params)    result_myfloat = convert_ndarray(src_dtype, result_myfloat).numpy()    # 打印前 10 个元素    print(result_myfloat.flatten()[:10])# 再次注意,使用 32 位 myfloat 的输出与 32 位浮点数完全相同,# 因为 myfloat 就是一个浮点数!np.testing.assert_array_equal(result, result_myfloat)

输出结果:

/workspace/python/tvm/driver/build_module.py:268: UserWarning: target_host parameter is going to be deprecated. Please pass in tvm.target.Target(target, host=target_host) instead.  "target_host parameter is going to be deprecated. "[ -7.5350165   2.0368009 -12.706646   -5.63786   -12.684058    4.0723605   2.618876    3.4049501  -9.867913  -24.53311  ]

下载 Python 源代码:bring_your_own_datatypes.py

下载 Jupyter Notebook:bring_your_own_datatypes.ipynb

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

Apache TVM 自定义数据类型 深度学习 BYODT
相关文章