本文翻译自Compiling and Optimizing a Model with TVMC — tvm 0.9.dev0 documentation
在本节中,我们将使用TVM命令行驱动程序TVMC。TVMC是一个工具,它通过命令行接口公开TVM特性,如模型的自动调优、编译、分析和执行。
在完成这部分工作后,我们将利用TVMC完成以下工作:
- 为TVM运行时编译一个训练好的ResNet-50 v2模型。
- 在编译后的模型中运行一个真实的图像,并解释输出和模型性能。
- 使用TVM在CPU上调优模型。
- 使用TVM收集的调优数据重新编译优化模型。
- 通过优化后的模型运行图像,并比较输出和模型性能。
本节的目标是概述TVM和TVMC的功能,并为理解TVM的工作方式奠定基础。
TVMC的使用
TVMC是一个Python应用程序,是TVM Python包的一部分。当你使用Python包安装TVM时,将会安装命令行应用程序TVMC。TVMC命令的位置取决于你的平台和安装方法。
如果你在环境变量$PYTHONPATH中添加了TVM路径,就可以通过命令python -m tvm.driver.tvmc来访问命令行提供的功能。
为简单起见,本教程将介绍TVMC命令tvmc <options>的使用,它等同于python -m tvm.driver.tvmc <options>
你可以使用以下命令查看帮助页面:
tvmc --help
tvmc执行的命令涉及的TVM特性包括编译、运行和调优等。使用tvmc <subcommand> --help可以查看子命令subcommand的参数选项。我们将在本教程中介绍这些命令,但首先我们需要为此下载一个预先训练好的模型。
获取模型
在本教程中,我们将使用ResNet-50 v2模型。ResNet-50是一个用于图像分类的,50层的卷积神经网络。我们使用的模型是已经训练好的,训练的数据集有1000种分类100多万张图像。该网络的输入图像尺寸为224x224。如果你对ResNet-50模型的结构感兴趣,我们建议下载Netron,这是一个免费的ML模型查看器。
在本教程中,我们将使用ONNX格式的模型。
get https://github.com/onnx/models/raw/main/vision/classification/resnet/model/resnet50-v2-7.onnx
TVMC支持模型有Keras, ONNX, TensorFlow, TFLite和Torch。使用--model-format选项可以查看模型格式。有关更多信息,请参阅tvmc compile --help。
TVM依赖ONNX python库。你可以使用命令pip3 install --user onnx onnxoptimizer来安装ONNX。如果你有root权限并且想全局安装ONNX,可以去掉--user选项。onnxoptimizer依赖项是可选的,仅用于onnx>=1.9版本。
ONNX模型编译为TVM运行时
下载ResNet-50模型后,我们使用tvmc编译它。模型编译得到的将是一个目标设备平台上的动态库。我们可以使用TVM运行时在目标设备上运行该模型。
# This may take several minutes depending on your machine
tvmc compile \
--target "llvm" \
--output resnet50-v2-7-tvm.tar \
resnet50-v2-7.onnx
我们来看看tvmc compile生成了哪些文件:
mkdir model
tar -xvf resnet50-v2-7-tvm.tar -C model
ls model
看到的有三个文件:
- mod.so是一个用c++库表示的模型,可以被TVM运行时加载。
- mod.json是TVM Relay计算图的文本表示。
- mod.params是一个包含预训练模型参数的文件。
该模块可以由应用程序直接加载,模型可以通过TVM运行时API运行。
正确的指定目标(--target选项)可能会对编译得到的模块的性能产生巨大影响,因为它可以利用目标上可用的硬件特性。更多信息可参阅Auto-tuning a Convolutional Network for x86 CPU — tvm 0.9.dev0 documentation。我们建议你先确定使用的CPU和可选的特性,并适当地设置目标。
使用TVMC运行编译好的模型
现在我们已经将模型编译到模块中,然后就可以使用TVM运行时对其进行预测了。TVMC内置了TVM运行时,允许您运行编译后的TVM模型。为了使用TVMC运行模型并进行预测,我们需要具备两个条件:
- 刚刚编译生成的模块
- 对模型进行预测的有效输入
每个模型都有特定的张量形状、格式和数据类型。所以大多数模型需要一些预处理和后处理,以确保输入正确,并解释输出。TVMC的输入和输出数据都采用了NumPy的.npz格式。这是一种支持良好的NumPy格式,可以将多个数组序列化存入到一个文件中。
作为本教程的输入,我们将使用一只猫的图像,您也可以替换为其他任何图像。
输入预处理
我们使用的ResNet-50 v2模型预期输入为ImageNet格式。下面是一个对ResNet-50 v2输入图像做预处理的脚本示例。
这个脚本要求环境支持Python Image库。您可以使用pip3 install --user pillow安装Image
#!python ./preprocess.py
from tvm.contrib.download import download_testdata
from PIL import Image
import numpy as np
img_url = "https://s3.amazonaws.com/model-server/inputs/kitten.jpg"
img_path = download_testdata(img_url, "imagenet_cat.png", module="data")
# Resize it to 224x224
resized_image = Image.open(img_path).resize((224, 224))
img_data = np.asarray(resized_image).astype("float32")
# ONNX expects NCHW input, so convert the array
img_data = np.transpose(img_data, (2, 0, 1))
# Normalize according to ImageNet
imagenet_mean = np.array([0.485, 0.456, 0.406])
imagenet_stddev = np.array([0.229, 0.224, 0.225])
norm_img_data = np.zeros(img_data.shape).astype("float32")
for i in range(img_data.shape[0]):
norm_img_data[i, :, :] = (img_data[i, :, :] / 255 - imagenet_mean[i]) / imagenet_stddev[i]
# Add batch dimension
img_data = np.expand_dims(norm_img_data, axis=0)
# Save to .npz (outputs imagenet_cat.npz)
np.savez("imagenet_cat", data=img_data)
运行编译后的模型
有了模型和输入数据,我们现在可以运行TVMC进行预测:
tvmc run \
--inputs imagenet_cat.npz \
--output predictions.npz \
resnet50-v2-7-tvm.tar
回想一下,.tar模型文件包括一个C++库、一个Relay模型的描述,以及模型的参数。TVMC包括TVM运行时,运行时可以加载模型并根据输入进行预测。当运行上面的命令时,TVMC生成文件predictions. npz,它包含NumPy格式的模型输出张量。
在本例中模型编译和运行在同一台机器上。在某些情况下,我们可能希望通过RPC Tracker远程运行模型。要了解更多相关选项,请查看tvmc run --help。
输出后处理
正如前面提到的,每个模型都有自己特定的输出张量。
在我们的示例中,我们需要对输出做一些后处理,同时提供一个查找表,将ResNet-50 v2的输出翻译为便于人类阅读的形式。
下面的脚本是从输出中提取标签的后处理示例。
#!python ./postprocess.py
import os.path
import numpy as np
from scipy.special import softmax
from tvm.contrib.download import download_testdata
# Download a list of labels
labels_url = "https://s3.amazonaws.com/onnx-model-zoo/synset.txt"
labels_path = download_testdata(labels_url, "synset.txt", module="data")
with open(labels_path, "r") as f:
labels = [l.rstrip() for l in f]
output_file = "predictions.npz"
# Open the output and read the output tensor
if os.path.exists(output_file):
with np.load(output_file) as data:
scores = softmax(data["output_0"])
scores = np.squeeze(scores)
ranks = np.argsort(scores)[::-1]
for rank in ranks[0:5]:
print("class='%s' with probability=%f" % (labels[rank], scores[rank]))
运行脚本将产生下面的输出:
python postprocess.py
# class='n02123045 tabby, tabby cat' with probability=0.610553
# class='n02123159 tiger cat' with probability=0.367179
# class='n02124075 Egyptian cat' with probability=0.019365
# class='n02129604 tiger, Panthera tigris' with probability=0.001273
# class='n04040759 radiator' with probability=0.000261
试着用其他图片替换猫的图像,看看ResNet模型会做出什么样的预测。
ResNet模型自动调优
前面我们将模型编译为TVM运行时,不包括任何针对特定平台的优化。在本节中,我们将向您展示如何使用TVMC构建一个针对您的工作平台的优化模型。
在某些情况下,编译后的模型在推理时并没有获得预期的性能。此时我们可以使用自动调优器,为我们的模型找到更好的配置,从而提高性能。TVM中的调优是对模型进行优化,使其在给定目标上运行得更快。这与模型的训练或参数微调不同,因为调优不会影响模型的准确性,而只会影响运行性能。作为调优过程的一部分,TVM将尝试运行许多不同的算子实现变体,看哪一种性能最好。这些运行结果存储在调优记录文件中,该文件最终作为调优子命令的输出。
以最简单的形式来说,调优需要提供以下三件事:
- 运行模型的设备规格
- 存储调优记录输出文件的路径,
- 要调优的模型的路径。
下面的例子演示了它是如何实际工作的:
# The default search algorithm requires xgboost, see below for further
# details on tuning search algorithms
pip install xgboost
tvmc tune \
--target "llvm" \
--output resnet50-v2-7-autotuner_records.json \
resnet50-v2-7.onnx
在这个例子中,如果为--target选项指定一个更具体的目标,您将看到更好的结果。例如,在Intel i7处理器上,你可以使用--target llvm -mcpu=skylake。在这个调优示例中,我们使用LLVM作为指定架构的编译器在CPU上进行本地调优。
TVMC将对模型的参数空间进行搜索,尝试不同的算子配置,并选择在您的平台上运行最快的配置。虽然这是一个基于CPU和模型运算的引导搜索,但仍然需要几个小时才能完成搜索。此搜索的输出将保存到resnet50-v2-7-autotuner_records.json文件中,稍后将用于编译一个优化的模型。
默认情况下,使用XGBoost Grid算法引导搜索。根据模型的复杂性和可用时间,您可能想要选择不同的算法。可以通过tvmc tune --help获得完整的列表。
消费级Skylake CPU的输出如下所示:
tvmc tune \
--target "llvm -mcpu=broadwell" \
--output resnet50-v2-7-autotuner_records.json \
resnet50-v2-7.onnx
# [Task 1/24] Current/Best: 9.65/ 23.16 GFLOPS | Progress: (60/1000) | 130.74 s Done.
# [Task 1/24] Current/Best: 3.56/ 23.16 GFLOPS | Progress: (192/1000) | 381.32 s Done.
# [Task 2/24] Current/Best: 13.13/ 58.61 GFLOPS | Progress: (960/1000) | 1190.59 s Done.
# [Task 3/24] Current/Best: 31.93/ 59.52 GFLOPS | Progress: (800/1000) | 727.85 s Done.
# [Task 4/24] Current/Best: 16.42/ 57.80 GFLOPS | Progress: (960/1000) | 559.74 s Done.
# [Task 5/24] Current/Best: 12.42/ 57.92 GFLOPS | Progress: (800/1000) | 766.63 s Done.
# [Task 6/24] Current/Best: 20.66/ 59.25 GFLOPS | Progress: (1000/1000) | 673.61 s Done.
# [Task 7/24] Current/Best: 15.48/ 59.60 GFLOPS | Progress: (1000/1000) | 953.04 s Done.
# [Task 8/24] Current/Best: 31.97/ 59.33 GFLOPS | Progress: (972/1000) | 559.57 s Done.
# [Task 9/24] Current/Best: 34.14/ 60.09 GFLOPS | Progress: (1000/1000) | 479.32 s Done.
# [Task 10/24] Current/Best: 12.53/ 58.97 GFLOPS | Progress: (972/1000) | 642.34 s Done.
# [Task 11/24] Current/Best: 30.94/ 58.47 GFLOPS | Progress: (1000/1000) | 648.26 s Done.
# [Task 12/24] Current/Best: 23.66/ 58.63 GFLOPS | Progress: (1000/1000) | 851.59 s Done.
# [Task 13/24] Current/Best: 25.44/ 59.76 GFLOPS | Progress: (1000/1000) | 534.58 s Done.
# [Task 14/24] Current/Best: 26.83/ 58.51 GFLOPS | Progress: (1000/1000) | 491.67 s Done.
# [Task 15/24] Current/Best: 33.64/ 58.55 GFLOPS | Progress: (1000/1000) | 529.85 s Done.
# [Task 16/24] Current/Best: 14.93/ 57.94 GFLOPS | Progress: (1000/1000) | 645.55 s Done.
# [Task 17/24] Current/Best: 28.70/ 58.19 GFLOPS | Progress: (1000/1000) | 756.88 s Done.
# [Task 18/24] Current/Best: 19.01/ 60.43 GFLOPS | Progress: (980/1000) | 514.69 s Done.
# [Task 19/24] Current/Best: 14.61/ 57.30 GFLOPS | Progress: (1000/1000) | 614.44 s Done.
# [Task 20/24] Current/Best: 10.47/ 57.68 GFLOPS | Progress: (980/1000) | 479.80 s Done.
# [Task 21/24] Current/Best: 34.37/ 58.28 GFLOPS | Progress: (308/1000) | 225.37 s Done.
# [Task 22/24] Current/Best: 15.75/ 57.71 GFLOPS | Progress: (1000/1000) | 1024.05 s Done.
# [Task 23/24] Current/Best: 23.23/ 58.92 GFLOPS | Progress: (1000/1000) | 999.34 s Done.
# [Task 24/24] Current/Best: 17.27/ 55.25 GFLOPS | Progress: (1000/1000) | 1428.74 s Done.
调优命令运行时间可能很长,所以tvmc调优提供了许多选项来定制调优过程,包括重复次数(例如--repeat和--number)、使用的调优算法等等。查看tvmc tune --help以获得更多信息。
使用调优数据编译优化模型
resnet50-v2-7-autotuner_records.json中存储了作为上面调优过程的输出的调优记录。这个文件有两种用法:
- 作为进一步调优的输入(通过tvmc tune --tuning-records)
- 作为编译器的输入
编译器将使用调优结果为您指定的目标上的模型生成高性能代码。编译命令为tvmc compile --tuning-records。可以查看tvmc compile --help以获得更多信息。
现在已经收集了模型的调优数据,我们可以使用优化后的算子重新编译模型,以加快计算速度。
tvmc compile \
--target "llvm" \
--tuning-records resnet50-v2-7-autotuner_records.json \
--output resnet50-v2-7-tvm_autotuned.tar \
resnet50-v2-7.onnx
运行优化后的模型,并产生和优化前相同的输出:
tvmc run \
--inputs imagenet_cat.npz \
--output predictions.npz \
resnet50-v2-7-tvm_autotuned.tar
python postprocess.py
可以看到模型预测的结果一致:
# class='n02123045 tabby, tabby cat' with probability=0.610550
# class='n02123159 tiger cat' with probability=0.367181
# class='n02124075 Egyptian cat' with probability=0.019365
# class='n02129604 tiger, Panthera tigris' with probability=0.001273
# class='n04040759 radiator' with probability=0.000261
比较调优和未调优模型
TVMC为您提供了在模型之间进行基本性能基准测试的工具。您可以指定重复次数,并在模型运行时(独立于运行时启动)报告给TVMC。我们可以大致了解调优在多大程度上提高了模型性能。例如,在测试Intel i7系统中,我们发现调优后的模型比未调优的模型运行速度快47%:
tvmc run \
--inputs imagenet_cat.npz \
--output predictions.npz \
--print-time \
--repeat 100 \
resnet50-v2-7-tvm_autotuned.tar
# Execution time summary:
# mean (ms) max (ms) min (ms) std (ms)
# 92.19 115.73 89.85 3.15
tvmc run \
--inputs imagenet_cat.npz \
--output predictions.npz \
--print-time \
--repeat 100 \
resnet50-v2-7-tvm.tar
# Execution time summary:
# mean (ms) max (ms) min (ms) std (ms)
# 193.32 219.97 185.04 7.11
小结
在本教程中,我们介绍了TVM的命令行驱动程序TVMC。我们演示了如何编译、运行和调优模型。我们还讨论了对输入和输出进行预处理和后处理的必要性。在调优过程之后,我们演示了如何比较未优化模型和优化模型的性能。
这里我们给出了一个在本地运行ResNet-50 v2的简单示例。但是,TVMC支持更多的特性,包括交叉编译、远程执行和分析/基准测试。
要了解其他可用选项,请查看tvmc --help。