一、前言
本文主要介绍如何基于ESP32的开发板通过microTVM进行一个卷积算子的调优。
二、microTVM
microTVM是TVM 编译器的扩展,它能够使TVM应用于微控制器,提供了在设备上运行 TVM RPC 服务以完成自动调优的方法,同时也提供了一套最小化 C 语言的runtime,使得裸机边缘设备可以独立完成模型推理。
- 基于TVM RPC服务
需要host端与设备端同时参与,由host端与设备端通过串口或USB等进行连接通信,host端将交叉编译完的固件程序烧录到设备端,该固件程序包括了TVM编译完成的模型设备端代码,TVM C runtime,设备的初始化操作以及TVM RPC server。而host端负责GrpahExecutor实例的创建,它会通过串口或USB等物理连接发送RPC命令到设备端进行模型推理。
- 独立运行
只需要设备端参与,与基于RPC服务的区别是GraphExecutor实例是由设备自己独立完成。
三、在ESP32上运行microTVM进行autotune
1、Zephyr安装与配置
1.1 配置zephyr sdk
Zephyr sdk的release地址在:https://github.com/zephyrproject-rtos/sdk-ng
需要下载最新的0.14.1版本,提供了esp相关的toolchain,如果使用west espressif进行安装会遇到newlibc的编译问题。
cd ~/
wget https://github.com/zephyrproject-rtos/sdk-ng/releases/download/v0.14.1/zephyr-sdk-0.14.1_linux-x86_64.tar.gz
tar -xvf zephyr-sdk-0.14.1_linux-x86_64.tar.gz
cd zephyr-sdk-0.14.1
./setup.sh
. environment-setup-x86_64-pokysdk-linux
我们需要的toolchain目录为 xtensa-espressif_esp32_zephyr-elf
1.2 安装依赖
这个按照Zephyr官方文档进行:
sudo apt install --no-install-recommends git cmake \
ninja-build gperf ccache dfu-util device-tree-compiler wget \
python3-dev python3-pip python3-setuptools python3-tk \
python3-wheel xz-utils file make gcc gcc-multilib \
g++-multilib libsdl2-dev
pip3 install -- user -U west
echo 'export PATH=~/.local/bin:"$PATH"' >> ~/.bashrc
source ~/.bashrc
zephyr对cmake版本有要求,如果需要升级,可以执行:
wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | sudo apt-key add -
sudo apt-add-repository 'deb https://apt.kitware.com/ubuntu/ focal main'
sudo apt update
sudo apt install cmake
1.3 初始化zephyr工程
west init ~/zephyrproject
cd ~/zephyrproject
west update
west zephyr-export
1.4 ESP32 newlibc支持
cd ~/zephyrproject/zephyr
git remote add upstream https://github.com/sylvioalves/zephyr.git
git fetch upstream
git checkout upstream/feature/newlibc_cpp_support
west update
虽然这个feature还没有入到主分支,但是不加这个支持的话,不出意外应该会遇到的错误应该是这样的:
In file included from ~/zephyrproject/zephyr/lib/posix/pthread_common.c:10:
~/zephyrproject/zephyr/include/posix/time.h:90:15: error: static declaration of 'clock_gettime' follows non-static declaration
__syscall int clock_gettime(clockid_t clock_id, struct timespec *ts);
^~~~~~~~~~~~~
In file included from ~/zephyrproject/zephyr/include/posix/time.h:12,
from ~/zephyrproject/zephyr/lib/posix/pthread_common.c:10:
/home/zgs/.espressif/tools/zephyr/xtensa-esp32-elf/xtensa-esp32-elf/sys-include/time.h:187:5: note: previous declaration of 'clock_gettime' was here
int clock_gettime (clockid_t clock_id, struct timespec *tp);
^~~~~~~~~~~~~
In file included from ~/zephyrproject/zephyr/lib/posix/pthread_common.c:10:
~/zephyrproject/zephyr/include/posix/time.h:94:5: error: conflicting types for 'timer_create'
int timer_create(clockid_t clockId, struct sigevent *evp, timer_t *timerid);
....
make[2]: *** [zephyr/lib/posix/CMakeFiles/lib__posix.dir/build.make:76: zephyr/lib/posix/CMakeFiles/lib__posix.dir/pthread_common.c.obj] Error 1
make[1]: *** [CMakeFiles/Makefile2:2950: zephyr/lib/posix/CMakeFiles/lib__posix.dir/all] Error 2
make[1]: *** Waiting for unfinished jobs....
1.5 导出环境变量
export ZEPHYR_BASE="${HOME}/zephyrproject/zephyr"
export ZEPHYR_TOOLCHAIN_VARIANT="espressif"
export ESPRESSIF_TOOLCHAIN_PATH="${HOME}/zephyr-sdk-0.14.1/xtensa-espressif_esp32_zephyr-elf"
export PATH=$PATH:$ESPRESSIF_TOOLCHAIN_PATH/bin
1.6 修改toolchain名称
下载的zephyr-sdk 0.14.1中的toolchain名称跟esp32所使用的默认名称不一致,需要修改编译脚本的默认值:
diff --git a/cmake/toolchain/espressif/target.cmake b/cmake/toolchain/espressif/target.cmake
index 5245bf9d08..f677bc9024 100644
--- a/cmake/toolchain/espressif/target.cmake
+++ b/cmake/toolchain/espressif/target.cmake
@@ -8,7 +8,7 @@ set(COMPILER gcc)
set(LINKER ld)
set(BINTOOLS gnu)
-set(CROSS_COMPILE_TARGET_xtensa_esp32 xtensa-esp32-elf)
+set(CROSS_COMPILE_TARGET_xtensa_esp32 xtensa-espressif_esp32_zephyr-elf)
set(CROSS_COMPILE_TARGET_xtensa_esp32s2 xtensa-esp32s2-elf)
set(CROSS_COMPILE_TARGET_riscv_esp32c3 riscv32-esp-elf)
2、TVM配置
2.1 使能 microTVM 编译
修改 config.cmake
set(USE_MICRO ON)
重新编译tvm。
2.2 增加esp32支持
在 ~/github/tvm/apps/microtvm/zephyr/template_project/boards.json 增加:
diff --git a/apps/microtvm/zephyr/template_project/boards.json b/apps/microtvm/zephyr/template_project/boards.json
index aae764a82..19a80397a 100644
--- a/apps/microtvm/zephyr/template_project/boards.json
+++ b/apps/microtvm/zephyr/template_project/boards.json
@@ -95,5 +95,13 @@
"fpu": true,
"vid_hex": "0483",
"pid_hex": "374b"
+ },
+ "esp32": {
+ "board": "esp32",
+ "model": "esp32",
+ "is_qemu": false,
+ "fpu": true,
+ "vid_hex": "",
+ "pid_hex": ""
}
}
2.3 增加esp32 flash runner串口获取方式
在~/github/tvm/apps/microtvm/zephyr/template_project/microtvm_api_server.py增加:
diff --git a/apps/microtvm/zephyr/template_project/microtvm_api_server.py b/apps/microtvm/zephyr/template_project/microtvm_api_server.py
index 059e76048..7e7b6e888 100644
--- a/apps/microtvm/zephyr/template_project/microtvm_api_server.py
+++ b/apps/microtvm/zephyr/template_project/microtvm_api_server.py
@@ -669,6 +669,10 @@ class ZephyrSerialTransport:
def _find_stm32cubeprogrammer_serial_port(cls, options):
return generic_find_serial_port()
+ @classmethod
+ def _find_esp32_serial_port(cls, options):
+ return generic_find_serial_port()
+
@classmethod
def _find_serial_port(cls, options):
flash_runner = _get_flash_runner()
@@ -685,6 +689,9 @@ class ZephyrSerialTransport:
if flash_runner == "stm32cubeprogrammer":
return cls._find_stm32cubeprogrammer_serial_port(options)
+ if flash_runner == "esp32":
+ return cls._find_esp32_serial_port(options)
+
raise RuntimeError(f"Don't know how to deduce serial port for flash runner {flash_runner}")
def __init__(self, options):
2.4 修改host_driven内存分配及头文件依赖
主要修改两个地方:
- zephyr从2.6.0开始power/reboot.h改成了sys/reboot.h
- 缩小tvm_heap分配的内存,否则最终的编译会出现region `dram0_1_seg' overflowed by xxxxx bytes的错误。
diff --git a/apps/microtvm/zephyr/template_project/src/host_driven/main.c b/apps/microtvm/zephyr/template_project/src/host_driven/main.c
index 44d656028..463f7e0d1 100644
--- a/apps/microtvm/zephyr/template_project/src/host_driven/main.c
+++ b/apps/microtvm/zephyr/template_project/src/host_driven/main.c
@@ -33,7 +33,7 @@
#include
#include
#include
-#include
+#include
#include
#include
#include
@@ -42,6 +42,7 @@
#include
#include
#include
+#include
#ifdef CONFIG_ARCH_POSIX
#include "posix_board_if.h"
@@ -130,7 +131,7 @@ tvm_crt_error_t TVMPlatformGenerateRandom(uint8_t* buffer, size_t num_bytes) {
}
// Heap for use by TVMPlatformMemoryAllocate.
-K_HEAP_DEFINE(tvm_heap, 216 * 1024);
+K_HEAP_DEFINE(tvm_heap, 50 * 1024);
// Called by TVM to allocate memory.
tvm_crt_error_t TVMPlatformMemoryAllocate(size_t num_bytes, DLDevice dev, void** out_ptr) {
按这样改完后,编译最终得到的内存区使用情况如下,可以看到dram0_1_seg的使用率已经到了96.22%:
3、算子调优
3.1 按microtvm_autotune的例程编写应用
import os
import json
import numpy as np
import pathlib
import shutil
import tvm
from tvm.relay.backend import Runtime
BOARD = os.getenv("TVM_MICRO_BOARD", default="esp32")
def create_module():
data_shape = (1, 3, 10, 10)
weight_shape = (6, 3, 5, 5)
# 输入数据
data = tvm.relay.var("data", tvm.relay.TensorType(data_shape, "float32"))
weight = tvm.relay.var("weight", tvm.relay.TensorType(weight_shape, "float32"))
# relay卷积算子
y = tvm.relay.nn.conv2d(
data,
weight,
padding=(2,2),
kernel_size=(5, 5),
kernel_layout="OIHW",
out_dtype="float32",
)
# 定义relay Function表达式
f = tvm.relay.Function([data, weight], y)
# 用卷积算子表达式构建一个module
relay_mod = tvm.IRModule.from_expr(f)
# 表达式类型推理
relay_mod = tvm.relay.transform.InferType()(relay_mod)
# weight随机值
weight_sample = np.random.rand(
weight_shape[0], weight_shape[1], weight_shape[2], weight_shape[3]
).astype("float32")
params = {"weight": weight_sample}
return relay_mod, params
def config_target():
runtime = Runtime("crt", {"system-lib": True})
boards_file = pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")) / "boards.json"
with open(boards_file) as fp:
boards = json.load(fp)
target = tvm.target.target.micro(boards[BOARD]["model"])
return runtime, target
relay_mod, params = create_module()
runtime, target = config_target()
# 配置优化pass
pass_context = tvm.transform.PassContext(opt_level=3, config={"tir.disable_vectorize": True})
with pass_context:
tasks = tvm.autotvm.task.extract_from_program(relay_mod["main"], {}, target)
assert len(tasks) > 0
zephyr_base = os.getenv("HOME") + "/zephyrproject/zephyr"
module_loader = tvm.micro.AutoTvmModuleLoader(
template_project_dir=pathlib.Path(tvm.micro.get_microtvm_template_projects("zephyr")),
project_options={
"zephyr_board": BOARD,
"west_cmd": "west",
"verbose": False,
"project_type": "host_driven",
"zephyr_base": zephyr_base,
},
)
builder = tvm.autotvm.LocalBuilder(
n_parallel=1,
build_kwargs={"build_option": {"tir.disable_vectorize": True}},
do_fork=False,
build_func=tvm.micro.autotvm_build_func,
runtime=runtime,
)
runner = tvm.autotvm.LocalRunner(number=1, repeat=1, timeout=100, module_loader=module_loader)
measure_option = tvm.autotvm.measure_option(builder=builder, runner=runner)
# ----------------run tune-----------------
autotune_log_file = pathlib.Path("microtvm_autotune.log.txt")
if os.path.exists(autotune_log_file):
os.remove(autotune_log_file)
num_trials = 10
for task in tasks:
tuner = tvm.autotvm.tuner.GATuner(task)
tuner.tune(
n_trial=num_trials,
measure_option=measure_option,
callbacks=[
tvm.autotvm.callback.log_to_file(str(autotune_log_file)),
tvm.autotvm.callback.progress_bar(num_trials, si_prefix="M"),
],
si_prefix="M",
)
# ------------------timing untune program-----------------
with pass_context:
lowered = tvm.relay.build(relay_mod, target=target, runtime=runtime, params=params)
temp_dir = os.getenv("HOME") + "/microtvm_esp32/untuned"
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
project = tvm.micro.generate_project(
str(tvm.micro.get_microtvm_template_projects("zephyr")),
lowered,
temp_dir,
{
"zephyr_board": BOARD,
"west_cmd": "west",
"verbose": False,
"project_type": "host_driven",
"zephyr_base": zephyr_base,
},
)
project.build()
project.flash()
with tvm.micro.Session(project.transport()) as session:
debug_module = tvm.micro.create_local_debug_executor(
lowered.get_graph_json(), session.get_system_lib(), session.device
)
debug_module.set_input(**lowered.get_params())
print("########## Build without Autotuning ##########")
debug_module.run()
del debug_module
# ------------------timing tuned program-----------------
with tvm.autotvm.apply_history_best(str(autotune_log_file)):
with pass_context:
lowered_tuned = tvm.relay.build(relay_mod, target=target, runtime=runtime, params=params)
temp_dir = os.getenv("HOME") + "/microtvm_esp32/tuned"
if os.path.exists(temp_dir):
shutil.rmtree(temp_dir)
project = tvm.micro.generate_project(
str(tvm.micro.get_microtvm_template_projects("zephyr")),
lowered_tuned,
temp_dir,
{
"zephyr_board": BOARD,
"west_cmd": "west",
"verbose": False,
"project_type": "host_driven",
"zephyr_base": zephyr_base,
},
)
project.build()
project.flash()
transporter = project.transport()
with tvm.micro.Session(transporter) as session:
debug_module = tvm.micro.create_local_debug_executor(
lowered_tuned.get_graph_json(), session.get_system_lib(), session.device
)
debug_module.set_input(**lowered_tuned.get_params())
print("########## Build with Autotuning ##########")
debug_module.run()
del debug_module
3.2 执行autotune
autotune过程中的打印,可以看到autotune的一个完整流程是每次都会重新编译然后flash到设备,再运行推理:
autotune完成后的打印:
同时会得到调优参数结果文件microtvm_autotune.log.txt:
{"input": ["c -keys=cpu -link-params=0 -model=esp32", "conv2d_NCHWc.x86", [["TENSOR", [1, 3, 10, 10], "float32"], ["TENSOR", [6, 3, 5, 5], "float32"], [1, 1], [2, 2, 2, 2], [1, 1], "NCHW", "NCHW", "float32"], {}], "config": {"index": 47, "code_hash": null, "entity": [["tile_ic", "sp", [-1, 3]], ["tile_oc", "sp", [-1, 6]], ["tile_ow", "sp", [-1, 10]], ["unroll_kw", "ot", true]]}, "result": [[0.002898416], 0, 40.33066153526306, 1650420190.1888804], "version": 0.2, "tvm_version": "0.9.dev0"}
...
{"input": ["c -keys=cpu -link-params=0 -model=esp32", "conv2d_NCHWc.x86", [["TENSOR", [1, 3, 10, 10], "float32"], ["TENSOR", [6, 3, 5, 5], "float32"], [1, 1], [2, 2, 2, 2], [1, 1], "NCHW", "NCHW", "float32"], {}], "config": {"index": 55, "code_hash": null, "entity": [["tile_ic", "sp", [-1, 3]], ["tile_oc", "sp", [-1, 6]], ["tile_ow", "sp", [-1, 1]], ["unroll_kw", "ot", false]]},
"result": [[0.003652516], 0, 43.680758237838745, 1650420592.7613897], "version": 0.2, > "tvm_version": "0.9.dev0"}
3.3 结果对比
-
未调优结果
-
调优结果
四、总结
本文对microTVM进行了简单的介绍,并通过一个实例详细说明了如何在ESP32开发板上通过microTVM调优卷积算子。