TinyML-TVM是如何驯服Tiny的(上)

低成本、人工智能驱动的消费类设备的激增,导致了ML研究人员和从业者对“裸智能”(低功耗,通常没有操作系统)设备的广泛兴趣。虽然专家已经可以在一些裸机设备上运行某些模型,但是为不同设备集优化模型是一个挑战,通常需要手动优化特定于设备的库。对于那些没有Linux支持的平台,没有可伸缩的模型部署解决方案。正因为如此,为了瞄准新设备,开发人员必须实现一次性定制软件堆栈,以管理系统资源和调度模型执行。

机器学习软件的手动优化并不是裸机领域所独有的。事实上,这一直是与其他硬件后端(例如,gpu和fpga)一起工作的开发人员的共同主题。TVM已经被证明能够抵御新硬件目标的冲击,但直到现在,它还无法与微控制器的独特配置相抗衡。为了解决这个领域的问题,扩展了TVM,使其具有一个微控制器后端,称为µTVM(脚注:发音为“MicroTVM”)。µTVM有助于在裸机设备上执行tensor程序,并通过TVM的内置tensor程序优化器AutoTVM自动优化这些程序。下图显示了µTVM+AutoTVM基础设施的鸟瞰图:

让看看它的行动

在讨论什么是TVM/MicroTVM或它是如何工作之前,先看一个它在实际中的快速示例。

A standard µTVM setup, where the host communicates with the device via JTAG.

上面,有一个STM32F746ZG板,里面有一个ARM Cortex-M7处理器,考虑到它在低功耗封装中的强大性能,这是边缘人工智能的理想部件。使用它的USB-JTAG端口将其连接到台式机。在桌面上,运行OpenOCD来打开与设备的JTAG连接;反过来,OpenOCD允许µTVM使用与设备无关的TCP套接字控制M7处理器。有了这个设置,可以使用TVM代码运行CIFAR-10分类器,如下所示(此处为完整脚本):

OPENOCD_SERVER_ADDR = '127.0.0.1'

OPENOCD_SERVER_PORT = 6666

TARGET = tvm.target.create('c -device=micro_dev')

DEV_CONFIG = stm32f746xx.default_config(OPENOCD_SERVER_ADDR, OPENOCD_SERVER_PORT)

module, params = get_cifar10_cnn()

with micro.Session(device_config) as sess:

graph, c_module, params = relay.build(module['main'], target=TARGET, params=params)

micro_mod = micro.create_micro_mod(c_module, DEV_CONFIG)

graph_mod = graph_runtime.create(graph, micro_mod, ctx=tvm.micro_dev(0))

graph_mod.run(data=data_np)

prediction = CIFAR10_CLASSES[np.argmax(graph_mod.get_output(0).asnumpy())]

print(f'prediction was {prediction}')

下面是MicroTVM的性能结果,与CMSIS-NN版本5.7.0(commit a65b7c9a)相比,后者是一个手工优化的ML内核库。

开箱即用的性能不是很好,但这正是AutoTVM的救命稻草。可以为设备编写一个调度模板,进行一轮自动调整,然后获得显著更好的结果。要插入自动调谐结果,只需要替换这一行:

graph, c_module, params = relay.build(module['main'], target=TARGET, params=params)

with these lines:

with TARGET, autotvm.apply_history_best(TUNING_RESULTS_FILE):

graph, c_module, params = relay.build(module['main'], target=TARGET, params=params)

And our results now look like this:

性能提高了约2倍,现在离CMSIS-NN更近了。尽管MicroTVM CIFAR10的实现与类似的TFLite/CMSIS-NN模型相比具有竞争力,但这项工作刚刚开始利用TVM的优化特性。通过加速其他运营商(如密集/全连接dense/fully-connected)和利用TVM的模型特定量化和运算符融合功能,还有进一步优化的空间。带有µTVM的TVM能够发挥最佳性能。如何工作的呢?幕后是怎么回事?现在就开始吧。

Design

The µTVM Device Memory Layout in RAM

µTVM旨在通过最小化必须满足的一组要求来支持设备的最低公分母。特别是,用户只需提供:

  1. 设备的C交叉编译器工具链
  2. 一种读/写设备存储器并在设备上执行代码的方法
  3. 包含设备内存布局和一般体系结构特征的规范
  4. 为设备准备函数执行的代码段

大多数裸机设备都支持C和JTAG(调试协议),所以(1)和(2)通常是免费的!此外,(3)和(4)通常是非常小的要求。以下是STM32F746系列板的(3)和(4)示例。

device_config = {
    'device_id': 'arm.stm32f746xx',        # unique identifier for the device
    'toolchain_prefix': 'arm-none-eabi-',  # prefix of each binary in the cross-compilation toolchain (e.g., arm-none-eabi-gcc)
    'base_addr': 0x20000000,               # first address of RAM
    'section_sizes': {                     # dictionary of desired section sizes in bytes
         'text': 18000,
         'rodata': 100,
         'data': 100,
         ...
    },
    'word_size': 4,                        # device word size
    'thumb_mode': True,                    # whether to use ARM's thumb ISA
    'comms_method': 'openocd',             # method of communication with the device
    'server_addr': '127.0.0.1',            # OpenOCD server address (if 'comms_method' is 'openocd')
    'server_port': 6666,                   # OpenOCD server port (if 'comms_method' is 'openocd')
}
.syntax unified
.cpu cortex-m7
.fpu softvfp
.thumb
 
.section .text.UTVMInit
.type UTVMInit, %function
UTVMInit:
  /* enable fpu */
  ldr r0, =0xE000ED88
  ldr r1, [r0]
  ldr r2, =0xF00000
  orr r1, r2
  str r1, [r0]
  dsb
  isb
  /* set stack pointer */
  ldr sp, =_utvm_stack_pointer_init
  bl UTVMMain
.size UTVMInit, .-UTVMInit

µTVM基础架构和设备runtime的构建仅仅是为了利用这些需求,正在努力通过支持常见的开源runtime平台(如mBED OS)来处理编译和链接过程来减少这些需求。

设备会话

考虑到微控制器交互的网络特性,引入微会话的概念,稍微偏离了标准的TVM代码。

µTVM中的每一项功能都依赖于与目标设备的开放会话。如果熟悉TVM,可能已经注意到在第一个代码片段中有一行代码偏离了规范,即这一行:

...

with micro.Session(device_config) as sess:

...

此with块内的每一行都可以调用µTVM中的函数,上下文是device_config指定的设备。这条线在hood下面做了很多事情,让把它拆开。

首先,它使用指定的任何通信方法(通常是OpenOCD)初始化与设备的连接。然后使用指定的交叉编译器交叉编译µTVM设备 runtime。最后,主机为编译后的二进制文件分配空间,并使用打开的连接将二进制文件加载到设备上。

由于 runtime现在位于设备上,自然需要一些函数来运行它。

Module Loading

TVM的核心抽象之一是模块。模块为特定的设备/ runtime目标存储一组相关函数。考虑到微控制器通常没有操作系统,µTVM需要做大量额外的工作来维护这种高级抽象。为了了解发生了什么,将跟踪创建和加载µTVM兼容模块的过程。

假设有一个微型会议打开设备和实现二维卷积的TVM调度。如果想把它加载到微控制器上,需要它发出C代码。要做到这一点,只需要设定目标tvm.build or relay.build. Example:

graph, c_module, params = relay.build(module['main'], target='c -device=micro_dev', params=params)

通过这样设置目标,构建过程将通过C代码生成后端运行。但是,生成的C模块仍然驻留在主机上。为了将其加载到设备上,通过µTVM基础设施中的一个核心功能来运行它:create_micro_mod。

例子:

micro_mod = micro.create_micro_mod(c_module, DEV_CONFIG)

上面的行交叉编译模块中的C源代码,为生成的二进制文件分配空间(这样它就可以与设备内存中的 runtime共存),然后将二进制文件的每个部分发送到设备上分配的插槽中。一旦模块二进制文件在设备内存中处于合适的位置,二进制文件中的函数指针将被修补,使模块能够在设备 runtime访问help函数(例如,分配草稿行)。

现在,在设备上加载内核后,可以获取卷积函数的远程句柄,如下所示:

micro_func = micro_mod['conv2d']

Tensor Loading

If we want to call an operator, we first need some tensors as arguments:

data_np, kernel_np = get_conv_inputs()

ctx = tvm.micro_dev(0)

data = tvm.nd.array(data_np, ctx=ctx)

kernel = tvm.nd.array(kernel_np, ctx=ctx)

根据其数据类型(例如int8、float32等)和形状,计算每个张量的字节大小,主机在设备堆上分配内存区域。然后将张量的数据加载到分配的区域中。

函数调用

算子执行可能是这个系统中最棘手的部分。为了简化它的表示,将首先讨论严格执行(算子一被调用就立即执行),然后是延迟执行(只有在需要算子的结果时才执行算子)——后者是系统的实际工作方式。

严格执行

调用函数时,输入和输出张量都作为参数传递,这就是所谓的目标传递样式:

conv2D(data, kernel, output)

考虑到这些张量已经在设备上分配,只需要向设备发送元数据(device address, shape, and data type)(设备地址、形状和数据类型),这样设备就知道要使用哪个驻留张量。下面显示的是一个名为“runtime”的函数的调用。在构造这个表示之前,需要将元数据序列化到专门为此目的而存在的设备上的arguments部分中。

/*

 * task struct for uTVM

 */

typedef struct {

/* pointer to function to call for this task */

int32_t (*func)(void*, void*, int32_t);

/* array of argument tensors */

TVMValue* arg_values;

/* array of datatype codes for each argument */

int* arg_type_codes;

/* number of arguments */

int32_t num_args;

} UTVMTask;

在严格的设置中,有一个全局UTVMTask实例,从主机端写入该实例。一旦写入任务,runtime就拥有了执行函数所需的一切,可以在runtime的入口点开始执行。runtime将执行一些轻量级初始化,运行算子,然后将控制权返回给主机。

TinyML-TVM是如何驯服Tiny的(上)的更多相关文章

  1. TinyML-TVM是如何驯服Tiny的(下)

    TinyML-TVM是如何驯服Tiny的(下) Lazy Execution实际上,随着通信开销开始占主导地位,一旦用户请求,就执行算子的开销变得非常昂贵.可以通过延迟评估直到用户需要调用的结果来提高 ...

  2. 端到端TVM编译器(上)

    端到端TVM编译器(上) 摘要 将机器学习引入到各种各样的硬件设备中.AI框架依赖于特定于供应商的算子库,针对窄范围的服务器级gpu进行优化.将工作负载部署到新平台,例如手机.嵌入式设备和加速器(例如 ...

  3. TVM代码生成codegen

    TVM代码生成codegen 硬件后端提供程序(例如Intel,NVIDIA,ARM等),提供诸如cuBLAS或cuDNN之类的内核库以及许多常用的深度学习内核,或者提供框架例,如带有图形引擎的DNN ...

  4. 部署TVM Runtime

    部署TVM Runtime本文主要介绍如何在开发板上部署TVM Runtime, 在本地机器安装完整的TVM(包含了TVM Runtime以及编译功能), 并且使用一个简单的远程调用例子测试是否部署成 ...

  5. 端到端TVM编译器(下)

    端到端TVM编译器(下) 4.3 Tensorization DL工作负载具有很高的运算强度,通常可以分解为张量运算符,如矩阵乘法或一维卷积.这些自然分解导致了最近的添加张量计算原语.这些新的原语带来 ...

  6. TVM Pass IR如何使用

    TVM Pass IR如何使用 随着Relay / tir中优化遍数的增加,执行并手动维护其依赖关系变得很棘手.引入了一个基础结构来管理优化过程,并应用于TVM堆栈中IR的不同层. Relay / t ...

  7. 如何使用TVM Pass红外线

    如何使用TVM Pass红外线 随着Relay / tir中优化遍数的增加,执行并手动维护其依赖关系变得很棘手.引入了一个基础结构来管理优化过程,将其应用于TVM堆栈中IR的不同层. Relay / ...

  8. TVM自定义数据类型

    TVM自定义数据类型 本文将介绍"自定义数据类型"框架,该框架可在TVM中使用自定义数据类型. 介绍 在设计加速器时,关键是如何近似地表示硬件中的实数.这个问题具有长期的行业标准解 ...

  9. 将代码生成器带入TVM

    将代码生成器带入TVM 为了使数据科学家不必担心开发新模型时的性能,硬件后端提供程序(例如Intel,NVIDIA,ARM等)可以提供诸如cuBLAS或cuDNN之类的内核库以及许多常用的深度学习内核 ...

随机推荐

  1. 反病毒攻防研究第006篇:简单木马分析与防范part2

    一.前言 一般来说,木马是既有客户端也有服务器端的.上次讨论的不过是一种特殊情况,毕竟不是人人都懂得DOS命令,因此现在木马的客户端也都是做成非常直观的界面形式,方便操作.本篇文章会从客户端与服务器端 ...

  2. hdu4685 最大匹配可能性

    题意:       给你n个王子和m个公主,每个王子可以和自己喜欢的公主结婚,问你在不影响最大匹配的前提下每个王子都可以去哪些公主. 思路:       所有王子向他喜欢的公主连一条边,然后匹配一遍, ...

  3. POJ2239简单二分匹配

    题意:       一周有7天,每天可以上12节课,现在给你每科课的上课时间,问你一周最多可以上几科课,一科课只要上一节就行了. 思路:       简单题目,直接二分就行了,好久没写二分匹配了,练习 ...

  4. Win64 驱动内核编程-2.基本框架(安装.通讯.HelloWorld)

    驱动安装,通讯,Hello World 开发驱动的简单流程是这样,开发驱动安装程序,开发驱动程序,然后安装程序(或者其他程序)通过通讯给驱动传命令,驱动接到之后进行解析并且执行,然后把执行结果返回. ...

  5. Windows 驱动加载程序代码

    #include <windows.h> #include <winsvc.h> #include <conio.h> #include <stdio.h&g ...

  6. idea中properties配置文件没有代码提示及代码高亮问题解决方案

    更多精彩关注微信公众号 1.解决properties文件没有代码提示问题:首先,单击项目结构按钮,如下图: 然后,给项目添加Spring依赖支持,如下图: 2.解决代码不高亮问题:     代码不高亮 ...

  7. 02 CTF WEB 知识梳理

    1. 工具集 基础工具 Burpsuit, Python, FireFox(Hackbar, FoxyProxy, User-Agent Swither .etc) Burpsuit 代理工具,攻击w ...

  8. 反向解析 参数替换 reverse

  9. 定义私有属性: *String name; * int age; * String gender; * int salary; Date hiredate;//入职时间

    import java.text.SimpleDateFormat; import java.util.Date; /** * 定义私有属性: * String name; * int age; * ...

  10. [刷题] 437 Paths Sum III

    要求 给出一棵二叉树及一个数字sum,判断这棵二叉树上存在多少条路径,其路径上的所有节点和为sum 路径不一定始于根节点,终止于叶子节点 路径要一直向下 思路 分情况讨论:根节点在路径上(8) / 根 ...