7. 在支持 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 裸机上运行 TVM
单击 此处 下载完整的示例代码
作者:Grant Watson
本节使用示例说明如何使用 TVM 在带有 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 的裸机上运行模型。Cortex(R)-M55 是一款用于嵌入式设备的小型低功耗 CPU。CMSIS-NN 是针对 Arm(R) Cortex(R)-M CPU 优化的内核集合。Ethos(TM)-U55 是一种 microNPU,专门为在资源受限的嵌入式设备中,加速机器学习的推理而设计。
在没有 Cortex(R)-M55 和 Ethos(TM)-U55 开发板的情况下,若要运行 demo 程序,可在固定虚拟平台(FVP)上运行。基于 Arm(R) Corstone(TM)-300 软件的 FVP,对包含 Cortex(R)-M55 和 Ethos(TM)-U55 的硬件系统进行建模。它对于软件开发非常友好。
本教程将编译一个 MobileNet v1 模型并使用 TVM 尽可能将算子迁移到 Ethos(TM)-U55。
获取 TVM
为平台获取 TVM,请访问 https://tlcpack.ai/ 并按照说明进行操作。正确安装 TVM 后,可以通过命令行访问 tvmc
。
在命令行上键入 tvmc
应显示以下内容:
usage: tvmc [-h] [-v] [--version] {tune,compile,run} ...
TVM compiler driver
optional arguments:
-h, --help show this help message and exit
-v, --verbose increase verbosity
--version print the version and exit
commands:
{tune,compile,run}
tune auto-tune a model
compile compile a model.
run run a compiled module
TVMC - TVM driver command-line interface
安装额外的 Python 依赖
运行 demo 需要一些额外的 Python 包。通过下面的 requirements.txt 文件,来安装这些额外的 Python 包:
requirements.txt¶
attrs==21.2.0
cloudpickle==2.0.0
decorator==5.1.0
ethos-u-vela==3.8.0
flatbuffers==2.0.7
lxml==4.6.3
nose==1.3.7
numpy==1.19.5
Pillow==8.3.2
psutil==5.8.0
scipy==1.5.4
tflite==2.4.0
tornado==6.1
运行以下命令来安装这些软件包:
pip install -r requirements.txt
获取模型
本教程使用 MobileNet v1 模型(一种卷积神经网络,旨在对图像进行分类),该模型针对边缘设备进行了优化。要使用的模型已经预训练过,它可以可以识别 1001 个类别。该网络的输入图像大小为 224x224,任何输入图像在使用之前都需要调整到这个尺寸。
本教程使用 TFLite 格式的模型。
mkdir -p ./build
cd build
wget https://storage.googleapis.com/download.tensorflow.org/models/mobilenet_v1_2018_08_02/mobilenet_v1_1.0_224_quant.tgz
gunzip mobilenet_v1_1.0_224_quant.tgz
tar xvf mobilenet_v1_1.0_224_quant.tar
为具有 CMSIS-NN 的 Arm(R) Cortex(R)-M55 CPU 和 Ethos(TM)-U55 NPU 设备编译模型
下载 MobileNet v1 模型后,下一步是用 tvmc compile 进行编译。编译过程中得到的输出是模型的 TAR 包,该模型编译为 target 平台的模型库格式(MLF),能够用 TVM runtime 在 target 设备上运行该模型。
tvmc compile --target=ethos-u,cmsis-nn,c \
--target-ethos-u-accelerator_config=ethos-u55-256 \
--target-cmsis-nn-mcpu=cortex-m55 \
--target-c-mcpu=cortex-m55 \
--runtime=crt \
--executor=aot \
--executor-aot-interface-api=c \
--executor-aot-unpacked-api=1 \
--pass-config tir.usmp.enable=1 \
--pass-config tir.usmp.algorithm=hill_climb \
--pass-config tir.disable_storage_rewrite=1 \
--pass-config tir.disable_vectorize=1 \
./mobilenet_v1_1.0_224_quant.tflite \
--output-format=mlf
tvmc 编译参数说明:
--target=ethos-u,cmsis-nn,c
:在可能的情况下将算子迁移到 microNPU,回退到 CMSIS-NN 并最终生成 microNPU 不支持的算子的 C 代码。--target-ethos-u-accelerator_config=ethos-u55-256
:指定 microNPU 配置。--target-c-mcpu=cortex-m55
:针对 Cortex(R)-M55 进行交叉编译。--runtime=crt
:生成粘合代码以支持算子使用 C runtime。--executor=aot
:使用 Ahead Of Time 编译而非 Graph Executor。--executor-aot-interface-api=c
:生成一个 C 风格的接口,其结构专为在边界集成到 C 应用程序而设计。--executor-aot-unpacked-api=1
:在内部使用非压缩的 API。--pass-config tir.usmp.enable=1
:启用统一静态内存规划--pass-config tir.usmp.algorithm=hill_climb
:对 USMP 使用爬山算法--pass-config tir.disable_storage_rewrite=1
: 禁用存储重写--pass-config tir.disable_vectorize=1
:禁用向量化,因为 C 中没有标准的向量化类型。./mobilenet_v1_1.0_224_quant.tflite
:正在编译的 TFLite 模型。--output-format=mlf
:以模型库格式生成输出。
若不想用 microNPU 进行迁移
仅适用于 CMSIS-NN 的算子:
- 使用
--target=cmsis-nn,c
代替--target=ethos-u,cmsis-nn,c
- 删除 microNPU 配置参数
--target-ethos-u-accelerator_config=ethos-u55-256
将生成的代码解压到当前目录
tar xvf module.tar
获取 ImageNet 标签
在图像上运行 MobileNet v1 时,输出结果是 0 到 1000 的索引。为了使应用程序对用户更加友好,将显示相关标签,而非仅显示类别索引。将图像标签下载到一个文本文件中,然后用 Python 脚本将它们包含在 C 程序中。
curl -sS https://raw.githubusercontent.com/tensorflow/tensorflow/master/tensorflow/lite/java/demo/app/src/main/assets/labels_mobilenet_quant_v1_224.txt \
-o ./labels_mobilenet_quant_v1_224.txt
获取输入图像
本教程使用猫的图像作为输入,也可以自行替换为其他图像。
将图像下载到构建目录中,下一步用 Python 脚本,将图像转换为 C 头文件中的字节数组。
curl -sS https://s3.amazonaws.com/model-server/inputs/kitten.jpg -o ./kitten.jpg
预处理图像
以下脚本将在 src 目录中创建 2 个 C 头文件:
inputs.h
- 作为脚本参数提供的图像将被转换为整数数组,以输入到 MobileNet v1 模型。outputs.h
- 用一个全零的整数数组,为推理的输出保留 1001 个整数值。
convert_image.py¶
#!python ./convert_image.py
import os
import pathlib
import re
import sys
from PIL import Image
import numpy as np
def create_header_file(name, section, tensor_name, tensor_data, output_path):
"""
这个函数产生一个头文件,包含 numpy 数组提供的数据。
"""
file_path = pathlib.Path(f"{output_path}/" + name).resolve()
# 创建带有 npy_data 的头文件作为 C 数组
raw_path = file_path.with_suffix(".h").resolve()
with open(raw_path, "w") as header_file:
header_file.write(
"#include <tvmgen_default.h>\n"
+ f"const size_t {tensor_name}_len = {tensor_data.size};\n"
+ f'uint8_t {tensor_name}[] __attribute__((section("{section}"), aligned(16))) = "'
)
data_hexstr = tensor_data.tobytes().hex()
for i in range(0, len(data_hexstr), 2):
header_file.write(f"\x{data_hexstr[i:i+2]}")
header_file.write('";\n\n')
def create_headers(image_name):
"""
此函数为运行推理所需的输入和输出数组生成 C 头文件
"""
img_path = os.path.join("./", f"{image_name}")
# 将图像大小调整为 224x224
resized_image = Image.open(img_path).resize((224, 224))
img_data = np.asarray(resized_image).astype("float32")
# 将输入转换为 NCHW
img_data = np.transpose(img_data, (2, 0, 1))
# 创建输入头文件
input_data = img_data.astype(np.uint8)
create_header_file("inputs", "ethosu_scratch", "input", input_data, "./include")
# 创建输出头文件
output_data = np.zeros([1001], np.uint8)
create_header_file(
"outputs",
"output_data_sec",
"output",
output_data,
"./include",
)
if __name__ == "__main__":
create_headers(sys.argv[1])
用以下命令行运行脚本:
python convert_image.py ./kitten.jpg
预处理标签
以下脚本将在 src 目录中创建一个 labels.h
头文件,之前下载的 labels.txt 文件会变成字符串数组。该数组将用于显示图像分类后为的标签。
convert_labels.py¶
#!python ./convert_labels.py
import os
import pathlib
import sys
def create_labels_header(labels_file, section, output_path):
"""
此函数生成一个包含 ImageNet 标签的头文件作为字符串数组
"""
labels_path = pathlib.Path(labels_file).resolve()
file_path = pathlib.Path(f"{output_path}/labels.h").resolve()
with open(labels_path) as f:
labels = f.readlines()
with open(file_path, "w") as header_file:
header_file.write(f'char* labels[] __attribute__((section("{section}"), aligned(16))) = {{')
for _, label in enumerate(labels):
header_file.write(f'"{label.rstrip()}",')
header_file.write("};\n")
if __name__ == "__main__":
create_labels_header(sys.argv[1], "ethosu_scratch", "./include")
用以下命令行运行脚本:
python convert_labels.py
编写 demo
下面的 C 程序会在之前下载并转换为整数数组的图像上 ,运行 MobileNet v1 模型的单个推理。由于该模型是以「ethos-u ...」为 target 编译的,因此需要迁移 Ethos(TM)-U55 NPU 支持的算子,以便进行加速。一旦应用程序构建并运行,测试图像应该被正确地归类为「tabby」,并且结果会显示在控制台上。这个文件应该放在 ./src
。
demo.c¶
#include <stdio.h>
#include <tvm_runtime.h>
#include "ethosu_mod.h"
#include "uart_stdout.h"
// convert_image.py 和 convert_labels.py 生成的头文件
#include "inputs.h"
#include "labels.h"
#include "outputs.h"
int abs(int v) { return v * ((v > 0) - (v < 0)); }
int main(int argc, char** argv) {
UartStdOutInit();
printf("Starting Demo\n");
EthosuInit();
printf("Allocating memory\n");
StackMemoryManager_Init(&app_workspace, g_aot_memory, WORKSPACE_SIZE);
printf("Running inference\n");
struct tvmgen_default_outputs outputs = {
.output = output,
};
struct tvmgen_default_inputs inputs = {
.input = input,
};
struct ethosu_driver* driver = ethosu_reserve_driver();
struct tvmgen_default_devices devices = {
.ethos_u = driver,
};
tvmgen_default_run(&inputs, &outputs, &devices);
ethosu_release_driver(driver);
// 计算最大值的索引
uint8_t max_value = 0;
int32_t max_index = -1;
for (unsigned int i = 0; i < output_len; ++i) {
if (output[i] > max_value) {
max_value = output[i];
max_index = i;
}
}
printf("The image has been classified as '%s'\n", labels[max_index]);
// 当 FVP 在 UART 上接收到「EXITTHESIM」时,FVP 将关闭
printf("EXITTHESIM\n");
while (1 == 1)
;
return 0;
}
此外,需要将 github 的这些头文件放入 ./include
目录:
若要用 FreeRTOS 进行任务调度和队列,可以在 demo_freertos.c 找到示例程序。
创建链接脚本(linker script)
创建一个链接脚本,便于在下一节中构建应用程序时使用。链接脚本告诉链接器所有文件都放在内存中。下面的 corstone300.ld 链接脚本应该放在工作目录中。
查看 FVP 示例链接脚本 corstone300.ld
TVM 生成的代码会将模型权重和 Arm(R) Ethos(TM)-U55 命令流放在名为 ethosu_scratch
的部分中,对于 MobileNet v1 大小的模型,无法将权重和命令流全部放入 SRAM 的可用空间(因为 SRAM 空间十分有限)。因此,链接脚本将 ethosu_scratch
这部分放入 DRAM(DDR)中。
在构建和运行应用程序之前,需要更新 PATH 环境变量,使其包含 cmake 3.19.5 和 FVP 的路径。例如,若已将它们安装在 /opt/arm
中,则执行以下操作:
export PATH=/opt/arm/FVP_Corstone_SSE-300_Ethos-U55/models/Linux64_GCC-6.4:/opt/arm/cmake/bin:$PATH
使用 make 构建 demo
接下来可以使用 make 构建 demo。在命令行运行 make
之前,应将 Makefile 移在工作目录中:
Makefile 示例:Makefile
若使用的是 FreeRTOS,Makefile 会从指定的 FREERTOS_PATH 构建:
make FREERTOS_PATH=<FreeRTOS directory>
运行 demo
最后,使用以下命令在固定虚拟平台(Fixed Virtual Platform,简称 FVP)上运行 demo:
FVP_Corstone_SSE-300_Ethos-U55 -C cpu0.CFGDTCMSZ=15 \
-C cpu0.CFGITCMSZ=15 -C mps3_board.uart0.out_file=\"-\" -C mps3_board.uart0.shutdown_tag=\"EXITTHESIM\" \
-C mps3_board.visualisation.disable-visualisation=1 -C mps3_board.telnetterminal0.start_telnet=0 \
-C mps3_board.telnetterminal1.start_telnet=0 -C mps3_board.telnetterminal2.start_telnet=0 -C mps3_board.telnetterminal5.start_telnet=0 \
-C ethosu.extra_args="--fast" \
-C ethosu.num_macs=256 ./build/demo