赞
踩
之前写过TNN框架解析,其实早在TNN之前我就调研过AMD开源的推理框架MIGraphX,现在也是中科曙光(海光)主推的推理框架,对标的是英伟达的TensorRT。MIGraphX的架构跟TNN完全不同,TNN其实还是受到Caffe的影响比较大,里面很多方面的设计都有Caffe的影子,很多地方命名都是基本一致的,但是MIGraphX不同,MIGraphX的算子粒度更细,更加灵活,整体架构是按照AI编译器思路来构造的。其实MIGraphX整体架构还是非常清晰的,里面有很多东西值得我们去学习,比如里面用到了很多高级的编程技法(比如模板和函数式编程的各种高级特性),还有很多pass也值得学习,比如内存复用优化pass用到了图着色算法,指令调度pass。本文先重点阐述MIGraphX的基本设计思想以及基本使用方法,对框架更加深入的解析等后面有时间再详细展开讨论。
MIGraphX整体架构主要分为三层:
AI编译中的IR从层级上分一般可以分为两种类型:多级IR和单级IR。使用多级IR可以使得系统优化更加灵活,各级IR只需要负责本级优化,但是多级IR会带来如下的问题:
MIGraphX采用了单级IR的设计,MIGraphX IR是一种基于SSA形式的线性IR,这种形式的IR可以表达计算图中的控制流信息和数据依赖关系,方便后面的编译优化。MIGraphX IR由program,module,instruction等基本结构组成。
MIGraphX采用静态图模式,在编译优化阶段,MIGrahpX实现了如下的优化:
这里简要阐述MIGraphX中的一些基本概念和基本设计思想。
用来表示数据的形状。
可以通过如下方式构造一个shape对象:
其中:
shape中常用的成员函数:
示例:
resnet50中第一个卷积层的卷积核大小为7x7,输出特征图个数为64,即有64个7x7的卷积核,如果输入的是一个3通道的图像,则该卷积核的shape可以表示为{migraphx::shape::float_type, {64, 3, 7, 7}},其中float_type表示shape的数据类型,这里采用float类型,{64, 3, 7, 7}表示每一个维度的大小,对应的是NCHW的数据格式,由于这里没有提供每一维的步长,所以步长会自动计算,自动计算出来的每一维的步长为{147,49,7,1},所以完整的shape表示为{migraphx::shape::float_type, {64, 3, 7, 7},{147,49,7,1}}。对于该卷积核的shape,lens()函数的返回值为{64, 3, 7, 7},elements()的返回值为9408,bytes()的返回值为9408*4=37632。
argument类型用来保存数据,类似Pytorch中的Tensor,常用来保存模型的输入和输出数据。
可以通过如下方式构造一个argument对象:
第1种方式只需要提供shape就可以,系统会自动申请一段内存,该内存的大小等于shape的bytes()方法
返回值的大小。第2种方式除了提供shape之外,还需要提供该argument的数据指针,argument不会自动释放该数据。
argument中常用的成员函数:
MIGraphX中使用literal表示常量,比如可以使用literal表示卷积的权重。实际上literal是一种特殊的argument,literal中的值不能修改,而argument中的值可以修改。
可以通过如下方式构造一个literal对象:
第一种构造方法是使用std::vector来创建一个常量,第二种使用数据指针来构造,第三种是使用std::initializer_list来构造。也可以通过generate_literal()方法创建一个随机值的literal:
migraphx::literal liter =
migraphx::generate_literal(migraphx::shape{migraphx::shape::float_type, {64, 3,
7, 7}}, 0);
其中generate_literal()的第2个参数表示随机数的种子,不同种子会生成不同的随机数。
literal中常用的成员函数:
target表示支持的硬件平台,目前支持CPU和GPU,在编译模型的时候,需要指定一个target。
MIGraphX中使用program结构表示一个神经网络模型。
program中常用的成员函数:
注意:如果需要在不同的线程中使用MIGraphX推理,不同线程不能共用同一个program对象,每个线
程需要单独创建一个program对象执行推理。
现代神经网络模型中可能存在多个子图,MIGraphX中使用module表示子图,每个子图又是由指令组
成。创建program的时候,会自动创建一个主计算图,可以通过program的get_main_module()方法获
取主计算图。
module中常用的成员函数:
注意:
instruction表示指令,可以通过module中的add_instruction()成员函数添加指令。MIGraphX中的指令相当于ONNX模型中的一个节点或者caffe模型中的一个层。指令由操作符(算子)和操作数组成。
Pytorch中支持视图操作(view),Pytorch中一个tensor可以是另一个tensor的视图,视图tensor与原tensor共享内存,视图可以避免不必要的内存拷贝,让操作更加高效。比如通过view()方法可以获取一个tensor的视图:
>>> t = torch.rand(4, 4)
>>> b = t.view(2, 8) # 创建视图
>>> t.storage().data_ptr() == b.storage().data_ptr() # b和t共享内存
True
>>> b[0][0] = 3.14
>>> t[0][0] # 修改了b也会影响t
tensor(3.1400)
与Pytorch一样,MIGraphX也支持视图,一个argument可以是另一个argument的视图,视图和原argument共享内存,MIGraphX中支持视图操作的算子有:
下图表示一个4行6列的二维数组,在MIGraphX中使用argument存储该数组,该数组按照行主序的方式在内存中连续存储(与C语言中的数组一致),所以在列这个维度上步长为1,在行这个维度上的步长为6,假设该二维数组的数据类型为float类型,则该二维数组的shape可以表示为{migraphx::shape::float_type, {4,6}},这里没有显式指定每一维的步长,MIGraphX会自动计算出步长,该shape的完整表示为{migraphx::shape::float_type, {4,6},{6,1}}。
现在有一个切片操作(slice),该切片操作参数为:starts=[0,2],ends =[4,5],steps = [1, 1] ,由于MIGraphX中的slice算子是一个视图算子,所以切片操作的结果为原二维数组的一个视图,并与原数据共享内存,该视图表示的数据如下图黄色区域所示:
具体实现的时候,视图包含一个数据指针以及该数据的shape,为了方便说明,将shape拆分为2个部分表示:每一维的大小和步长,则视图包含的成员可以表示为:
{
// 视图成员
float *data_ptr;
std::vector<std::size_t> lens; // [4,3]
std::vector<std::size_t> strides; // [6,1]
}
本示例中该视图的数据指针指向原数组第三个元素,该视图的shape可以表示为{migraphx::shape::float_type, {4,3},{6,1}},所以视图中的成员lens为[4,3],strides为[6,1],注意由于与原数据共享内存,所以该视图的步长为[6,1]而不是[3,1],通过shape可以访问到正确的视图中的数据,比如要访问该视图的第2行第1列的元素,即下图中红色元素,该元素在视图中的二维索引index可以表示为[1,0],则在实际内存中的索引为二维索引和步长的内积: index*strides=1 * 6 + 0 * 1 =6,则二维索引为[1,0]表示的数据在内存中对应的数据为data_ptr+6,所以可以通过二维索引与步长的内积得到实际的内存索引。
MIGraphX中部分算子是不支持输入视图的,所以对于这些算子,如果输入的是一个视图,就需要通过contiguous操作将内存变得连续。对于上面slice操作返回的视图,contiguous算子会创建一个新的内存空间,将转换后得到的内存连续的数据保存在新的内存空间中,如下图所示:
从上图可以看出,经过contiguous操作之后,slice算子的输出变得内存连续了,所以contiguous算子的输出的shape可以表示为{migraphx::shape::float_type, {4,3},{3,1}},此时行步长是3而不是之前共享内存时的6了。
其实MIGraphX里面有很多设计值得我们学习,这里只讨论几个我觉得比较重要的,更多的设计思想有机会再展开讨论。
MIGraphX中没有采用手动管理内存的方式,因为这样容易导致内存泄漏,特别是在发生异常的时候。MIGraphX中的自动内存管理主要采用如下两种方式:
示例1:
using file_ptr = MIGRAPHX_MANAGE_PTR(FILE*, fclose);
file_ptr f{fopen("some_file", "r")};
示例2:
using hip_stream_ptr = MIGRAPHX_MANAGE_PTR(hipStream_t, hipStreamDestroy);
hip_stream_ptr create_stream()
{
hipStream_t result = nullptr;
auto status = hipStreamCreateWithFlags(&result, hipStreamNonBlocking);
if(status != hipSuccess)
MIGRAPHX_THROW("Failed to allocate stream");
return hip_stream_ptr{result};
}
MIGraphX中使用标准库中提供的算法来代替使用原始的循环结构,因为原始的循环接口有如下缺点:
示例:
对于下面一段程序
void f(vector<string>& v) { string val; cin >> val; // ... int index = -1; for (int i = 0; i < v.size(); ++i) { if (v[i] == val) { index = i; break; } } // ... }
我们可以使用标准库中的std::find算法来代替:
void f(vector<string>& v)
{
string val;
cin >> val;
// ...
auto p = find(begin(v), end(v), val);
// ...
}
MIGraphX中有许多函数的实现需要使用到多态机制,比如在MIGraphX中神经网络是使用program表示,program中包含了许多指令,添加指令需要用到如下函数:
instruction_ref module::add_instruction(const operation& op, std::vector<instruction_ref> args)
该函数的第一个参数表示该指令执行的操作,MIGraphX中使用算子表示,但是实际中有很多算子,比如卷积算子、relu算子等,也就是说operation类型需要能够被多种类型的算子赋值,能够表示不同类型的算子,这就是多态机制,MIGraphX采用了类型擦除机制来实现多态。
关于类型擦除的原理,参考这篇博客:C++多态的另一种实现:类型擦除
这里以ResNet50为例来说明如何通过C++ API加载ONNX模型进行图像分类模型的推理。
使用MIGraphX进行推理前需要将训练好的ResNet50模型转换为ONNX格式,本示例使用如下的ResNet50模型:https://download.pytorch.org/models/resnet50-19c8e357.pth,下载该模型后使用如下代码可以转换为ONNX格式(本示例代码基于Pytorch1.10):
# Pytorch模型文件 pathOfPytorchModel = "resnet50-19c8e357.pth" # 创建ResNet50模型 net = torchvision.models.resnet50(pretrained=False) # 定义输入 input = torch.randn(32,3,224,224) # 生成的ONNX模型的路径 pathOfONNX = "ResNet50.onnx" net.load_state_dict(torch.load(pathOfPytorchModel)) net.eval() # 导出ONNX模型 torch.onnx.export(net,input,pathOfONNX,input_names = ["input"])
生成好ResNet50.onnx模型后就可以进行推理了,如果没有特殊说明本教程使用的ResNet50模型都是使用的该模型。
使用C++ API加载ONNX模型进行推理主要包含三个步骤:
主要步骤示例代码如下:
// 头文件 #include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> // 加载模型 migraphx::program net = migraphx::parse_onnx("path/to/your/onnx/model"); // 编译模型 migraphx::compile_options options; net.compile(migraphx::gpu::target{},options); // 加载数据 std::unordered_map<std::string, migraphx::argument> inputData; ... // 执行推理 std::vector<migraphx::argument> results = net.eval(inputData);
下面以转好的ResNet50.onnx模型为例说明如何使用C++ API进行分类模型的推理:
#include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> #include <opencv2/opencv.hpp> int main(int argc, char *argv[]) { // 加载模型 migraphx::program net = migraphx::parse_onnx("ResNet50.onnx"); // 获取模型输入/输出节点信息 std::cout << "inputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> inputs = net.get_inputs(); for (auto i : inputs) { std::cout << i.first << ":" << i.second << std::endl; } std::cout << "outputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> outputs = net.get_outputs(); for (auto i : outputs) { std::cout << i.first << ":" << i.second << std::endl; } std::string inputName = inputs.begin()->first; migraphx::shape inputShape = inputs.begin()->second; int N = inputShape.lens()[0]; int C = inputShape.lens()[1]; int H = inputShape.lens()[2]; int W = inputShape.lens()[3]; // 编译模型 migraphx::compile_options options; options.device_id = 0; // 设置GPU设备,默认为0号设备 options.offload_copy = true; net.compile(migraphx::gpu::target{}, options); // 数据预处理并转换为NCHW格式 int batchSize = N; cv::Mat srcImage = cv::imread("Test.jpg"); std::vector<cv::Mat> srcImages; for (int i = 0; i < batchSize; ++i) { srcImages.push_back(srcImage); } cv::Mat inputBlob; cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125, cv::Size(W, H), cv::Scalar(127.5, 127.5, 127.5), false, false); // 创建输入数据 std::unordered_map<std::string, migraphx::argument> inputData; inputData[inputName] = migraphx::argument{inputShape, inputBlob.data}; // 推理 std::vector<migraphx::argument> results = net.eval(inputData); // 获取输出节点的属性 migraphx::argument result = results[0]; // 获取第一个输出节点的数据 migraphx::shape outputShape = result.get_shape(); // 输出节点的shape std::vector<std::size_t> outputSize = outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) int numberOfOutput = outputShape.elements(); // 输出节点元素的个数 float *resultData = (float *)result.data(); // 输出节点数据指针 // 打印推理结果 for (int i = 0; i < numberOfOutput; ++i) { std::cout << resultData[i] << ","; } std::cout << std::endl; return 0; }
本节主要说明如何在MIGraphX中使用FP16进行推理,在MIGraphX中可以通过下面两种方式实现FP16推理:
下面是两种方式的具体使用说明。
实现FP16推理可以直接使用FP32格式的ONNX模型,然后在编译前调用migraphx::quantize_fp16()。这种方式的优点是不需要转换模型格式,只需要修改少量代码,推荐使用该方式。具体使用方法如下:
#include <migraphx/quantization.hpp> // FP16头文件
// 使用FP16
migraphx::quantize_fp16(net);
// 编译模型(下面步骤跟FP32推理相同)
...
除了调用quantize_fp16的方式外,还可以通过将模型转换为FP16格式来实现FP16的推理。通过下面的方法可以将FP32格式的模型转换为FP16格式:
安装onnx和onnxconverter-common
pip install onnx onnxconverter-common
通过convert_float_to_float16函数转换模型
import onnx
from onnxconverter_common import float16
model = onnx.load("path/to/model.onnx")
model_fp16 = float16.convert_float_to_float16(model)
onnx.save(model_fp16, "path/to/model_fp16.onnx")
转换好之后,可以直接使用model_fp16.onnx文件进行FP16的推理,使用方式与FP32推理一致,注意:由于模型为FP16格式,所以输入数据需要转换为FP16类型。
注:这种方式可能会导致MIGraphX加载FP16格式的模型报错。
使用INT8模式进行推理需要用户提供量化校准数据,通过校准数据计算量化参数并生成量化模型。为了保证量化精度,建议使用验证集或者测试集中多个典型的数据作为量化校准数据,如果用户没有提供量化校准数据,MIGraphX会使用默认的量化参数,这样可能会导致严重的精度下降。
使用INT8模式推理需要在编译模型之前加上下面一段代码:
#include <migraphx/quantization.hpp> // INT8头文件 // 读取校准数据 cv::Mat srcImage = cv::imread("CalibrationData.jpg", 1); std::vector<cv::Mat> srcImages; for (int i = 0; i < inputShape.lens()[0]; ++i) { srcImages.push_back(srcImage); } cv::Mat inputBlob; cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125, cv::Size(W, H), cv::Scalar(127.5, 127.5, 127.5), false, false); std::unordered_map<std::string, migraphx::argument> inputData; inputData[inputName] = migraphx::argument{inputShape, (float *)inputBlob.data}; // 创建量化数据,这里只使用了一张图像,实际使用时为了提高量化精度,建议使用多张图像创建多个inputData进行量化 std::vector<std::unordered_map<std::string, migraphx::argument>> calibrationData = {inputData}; // INT8量化 migraphx::quantize_int8(net, migraphx::gpu::target{}, calibrationData);
有的时候我们希望使用随机数作为模型的输入,MIGraphX提供了生成随机数的函数migraphx::generate_argument,使用方法如下:
migraphx::argument data = migraphx::generate_argument(inputShape);
返回的data就是一个包含随机数的argument,可以作为模型的输入。
如果需要查看模型推理过程中需要使用的显存大小,可以使用下面的方法:
...
// 编译模型
net.compile(migraphx::gpu::target{},options);
// 查看显存,单位为字节
std::size_t memoryUsage = net.get_memory_usage();
如果想要指定输出节点,可以在eval()方法中通过提供outputNames参数来实现:
...
// 推理
std::vector<std::string> outputNames = {"output1","output2","output3"}; // 设置输出节点名
std::vector<migraphx::argument> results = net.eval(inputData,outputNames);
...
如果没有指定outputName参数,则默认输出所有输出节点,此时输出节点的顺序与ONNX中输出节点顺序保持一致,可以通过netron查看ONNX文件的输出节点的顺序。
这里介绍如何在python中使用MIGraphX。
将MIGraphX库路径加入PYTHONPATH:
export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
下面的示例展示了如何使用python进行ResNet50分类模型的推理。
# -*- coding: utf-8 -*- import cv2 import numpy as np import migraphx def ReadImage(pathOfImage,inputShape): srcImage = cv2.imread(pathOfImage, cv2.IMREAD_COLOR) # resize并转换为CHW resizedImage = cv2.resize(srcImage,(inputShape[3], inputShape[2])) resizedImage_Float = resizedImage.astype("float32") # 转换为float32 srcImage_CHW = np.transpose(resizedImage_Float, (2, 0, 1)) # 转换为CHW # 预处理 mean = np.array([127.5, 127.5, 127.5]) scale = np.array([0.0078125, 0.0078125, 0.0078125]) inputData = np.zeros(inputShape).astype("float32") # NCHW for i in range(srcImage_CHW.shape[0]): inputData[0,i, :, :] = (srcImage_CHW[i, :, :] - mean[i]) * scale[i] for i in range(inputData.shape[0]): if i!=0: inputData[i,:, :, :]=inputData[0,:, :, :] return inputData if __name__ == '__main__': # 加载模型 model = migraphx.parse_onnx("ResNet50.onnx") # 获取模型输入输出节点信息 print("inputs:") inputs=model.get_inputs() for key,value in inputs.items(): print("{}:{}".format(key,value)) print("outputs:") outputs=model.get_outputs() for key,value in outputs.items(): print("{}:{}".format(key,value)) inputName=list(model.get_inputs().keys())[0] inputShape=inputs[inputName].lens() # 编译模型 model.compile(t=migraphx.get_target("gpu"),device_id=0) # device_id: 设置GPU设备,默认为0号设备 # 数据预处理并转换为NCHW格式 pathOfImage ="Test.jpg" image = ReadImage(pathOfImage,inputShape) # 推理 results = model.run({inputName:image}) # 获取输出节点属性 result=results[0] # 获取第一个输出节点的数据,migraphx.argument类型 outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型 outputSize=outputShape.lens() # 每一维大小,维度顺序为(N,C,H,W),list类型 numberOfOutput=outputShape.elements() # 输出节点元素的个数 # 转换为numpy result = np.array(results[0]) # 打印结果 print(result)
如果需要在python中使用FP16进行推理,只需要在编译前面加上如下语句即可:
# 使用FP16
migraphx.quantize_fp16(model)
# 编译模型
...
与C++中的INT8推理类似,在Python中使用INT8进行推理,只需要在编译前加上如下语句即可:
# 读取量化校准数据
image = ReadImage()
inputData[inputName] = migraphx.argument(image)
# 创建量化数据,这里只使用了一张图像,实际使用时为了提高量化精度,建议使用多张图像创建多个inputData进行量化
calibrationData = [inputData]
migraphx.quantize_int8(model, migraphx.get_target("gpu"), calibrationData)
前面的示例中,我们都是使用的host端数据做推理,但是在某些场景下我们的数据是在device上的,如果将device数据拷贝到host上再做推理性能会受到一定的影响,MIGraphX支持直接输入device数据做推理,返回的推理结果也是在device端。以ResNet50分类模型为例,看一下如何直接使用device数据。
#include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> #include <migraphx/gpu/hip.hpp> // allocate_gpu(),to_gpu(),from_gpu()头文件 #include <opencv2/opencv.hpp> std::unordered_map<std::string, migraphx::argument> AllocateOutputMemory(migraphx::program &p) { std::unordered_map<std::string, migraphx::argument> outputData; for (auto x : p.get_outputs()) { // 为每个输出分配device内存 std::string outputName = x.first; migraphx::shape outputShape = x.second; outputData[outputName] = migraphx::gpu::allocate_gpu(outputShape); } return outputData; } int main(int argc, char *argv[]) { // 加载模型 migraphx::program net = migraphx::parse_onnx("ResNet50.onnx"); // 获取模型输入/输出节点信息 std::cout << "inputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> inputs = net.get_inputs(); for (auto i : inputs) { std::cout << i.first << ":" << i.second << std::endl; } std::cout << "outputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> outputs = net.get_outputs(); for (auto i : outputs) { std::cout << i.first << ":" << i.second << std::endl; } std::string inputName = inputs.begin()->first; migraphx::shape inputShape = inputs.begin()->second; int N = inputShape.lens()[0]; int C = inputShape.lens()[1]; int H = inputShape.lens()[2]; int W = inputShape.lens()[3]; // 编译模型 migraphx::compile_options options; options.device_id = 0; // 设置GPU设备,默认为0号设备 options.offload_copy = false; // 一定要设置为false net.compile(migraphx::gpu::target{}, options); // 为输出节点分配device内存,用于保存输出数据 std::unordered_map<std::string, migraphx::argument> modelData = AllocateOutputMemory(net); // 数据预处理并转换为NCHW格式 int batchSize = N; cv::Mat srcImage = cv::imread("Test.jpg"); std::vector<cv::Mat> srcImages; for (int i = 0; i < batchSize; ++i) { srcImages.push_back(srcImage); } cv::Mat inputBlob; cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125, cv::Size(W, H), cv::Scalar(127.5, 127.5, 127.5), false, false); // 将输入数据从host数据转换为device数据 migraphx::argument inputData = migraphx::gpu::to_gpu(migraphx::argument{inputShape, (float *)inputBlob.data}); // 使用device数据作为输入数据,inputData.data()返回的是device地址 modelData[inputName] = migraphx::argument{inputShape, inputData.data()}; // 执行推理,模型的推理结果保存在AllocateOutputMemory方法分配的device内存中,并通过results返回,results与AllocateOutputMemory方法分配的device内存共享内存 // 这是一个同步方法 std::vector<migraphx::argument> results = net.eval(modelData); // 获取输出节点 migraphx::argument result = migraphx::gpu::from_gpu(results[0]); // 将第一个输出节点的数据拷贝到host端 migraphx::shape outputShape = result.get_shape(); // 输出节点的shape std::vector<std::size_t> outputSize = outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) int numberOfOutput = outputShape.elements(); // 输出节点元素的个数 float *resultData = (float *)result.data(); // 输出节点数据指针 // 打印推理结果 for (int i = 0; i < numberOfOutput; ++i) { std::cout << resultData[i] << ","; } std::cout << std::endl; return 0; }
目前曙光提供的最新版本的MIGraphX已经能够很好的支持动态shape了,而且性能优异。MIGraphX的动态shape使用方式与静态shape基本一致,动态推理只需要在静态程序基础上设置一个最大输入shape。
本示例使用ResNet50模型说明动态shape模型的基本运行流程。
动态推理需要动态ONNX模型,下面是Pytorch模型导出为动态batch的ONNX模型示例:
torch.onnx.export(model, # 模型
torch.randn(1, 3, 224, 224), # 用于确定输入大小和类型
"./ResNet50.onnx", # 输出onnx的名称
verbose=False, # 是否以字符串的形式显示计算图
input_names=["input"], # 输入节点的名称,可以是一个list
output_names=["output"], # 输出节点的名称
opset_version=16, # onnx 支持采用的operator set
do_constant_folding=True, # 是否压缩常量
# 设置动态维度,此处指明input节点的第0维度可变,命名为batch_size
dynamic_axes={"input":{0: "batch_size"}, "output":{0: "batch_size"}}
)
这样就导出了一个batchsize可变的模型。
#include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> #include <opencv2/opencv.hpp> int main(int argc, char *argv[]) { // 设置最大输入shape: input表示输入节点名,{8,3,224,224}表示最大输入shape migraphx::onnx_options onnx_options; onnx_options.map_input_dims["input"] = {8, 3, 224, 224}; // 加载模型 migraphx::program net = migraphx::parse_onnx("ResNet50.onnx", onnx_options); // 获取模型输入/输出节点信息 std::cout << "inputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> inputs = net.get_inputs(); for (auto i : inputs) { std::cout << i.first << ":" << i.second << std::endl; } std::cout << "outputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> outputs = net.get_outputs(); for (auto i : outputs) { std::cout << i.first << ":" << i.second << std::endl; } std::string inputName = inputs.begin()->first; migraphx::shape inputShape = inputs.begin()->second; int N = inputShape.lens()[0]; int C = inputShape.lens()[1]; int H = inputShape.lens()[2]; int W = inputShape.lens()[3]; // 编译模型 migraphx::compile_options options; options.device_id = 0; // 设置GPU设备,默认为0号设备 options.offload_copy = true; net.compile(migraphx::gpu::target{}, options); // 设置动态输入,这里添加了2个不同的输入shape std::vector<std::vector<std::size_t>> inputShapes; inputShapes.push_back({1, 3, 224, 224}); inputShapes.push_back({2, 3, 224, 224}); cv::Mat srcImage = cv::imread("Test.jpg", 1); for (int i = 0; i < inputShapes.size(); ++i) { // 数据预处理并转换为NCHW格式 std::vector<cv::Mat> srcImages; for (int j = 0; j < inputShapes[i][0]; ++j) { srcImages.push_back(srcImage); } cv::Mat inputBlob; cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125, cv::Size(inputShapes[i][3], inputShapes[i][2]), cv::Scalar(127.5, 127.5, 127.5), false, false); // 创建输入数据 std::unordered_map<std::string, migraphx::argument> inputData; inputData[inputName] = migraphx::argument{migraphx::shape(inputShape.type(), inputShapes[i]), (float *)inputBlob.data}; // 推理 std::vector<migraphx::argument> results = net.eval(inputData); // 获取输出节点的属性 migraphx::argument result = results[0]; // 获取第一个输出节点的数据 migraphx::shape outputShape = result.get_shape(); // 输出节点的shape std::vector<std::size_t> outputSize = outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) int numberOfOutput = outputShape.elements(); // 输出节点元素的个数 float *resultData = (float *)result.data(); // 输出节点数据指针 // 打印输出 printf("output size:%d\n", numberOfOutput); for (int i = 0; i < numberOfOutput; ++i) { printf("%f,", resultData[i]); } printf("\n"); } return 0; }
更多动态shape示例程序参考ModelZoo。
import cv2 import numpy as np import migraphx def ReadImage(pathOfImage,inputShape): srcImage = cv2.imread(pathOfImage, cv2.IMREAD_COLOR) # resize并转换为CHW resizedImage = cv2.resize(srcImage,(inputShape[3], inputShape[2])) resizedImage_Float = resizedImage.astype("float32") # 转换为float32 srcImage_CHW = np.transpose(resizedImage_Float, (2, 0, 1)) # 转换为CHW # 预处理 mean = np.array([127.5, 127.5, 127.5]) scale = np.array([0.0078125, 0.0078125, 0.0078125]) inputData = np.zeros(inputShape).astype("float32") # NCHW for i in range(srcImage_CHW.shape[0]): inputData[0,i, :, :] = (srcImage_CHW[i, :, :] - mean[i]) * scale[i] for i in range(inputData.shape[0]): if i!=0: inputData[i,:, :, :]=inputData[0,:, :, :] return inputData if __name__ == '__main__': # 设置最大输入shape: input表示输入节点名,{8,3,224,224}表示最大输入shape maxInput={"input":[8,3,224,224]} # 加载模型 model = migraphx.parse_onnx("ResNet50.onnx",map_input_dims=maxInput) # 获取模型输入输出节点信息 print("inputs:") inputs=model.get_inputs() for key,value in inputs.items(): print("{}:{}".format(key,value)) print("outputs:") outputs=model.get_outputs() for key,value in outputs.items(): print("{}:{}".format(key,value)) inputName=list(model.get_inputs().keys())[0] # 编译 model.compile(t=migraphx.get_target("gpu"),device_id=0) # 设置动态输入,这里添加了2个不同的输入shape inputShapes=[[1,3,224,224],[2,3,224,224]] for inputShape in inputShapes: # 数据预处理并转换为NCHW pathOfImage ="Test.jpg" image = ReadImage(pathOfImage,inputShape) # 推理 results = model.run({inputName:image}) # 获取输出节点属性 result=results[0] # 获取第一个输出节点的数据,migraphx.argument类型 outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型 outputSize=outputShape.lens() # 表示每一维大小,维度顺序为(N,C,H,W),list类型 numberOfOutput=outputShape.elements() # 输出节点元素的个数 # 转换为numpy result = np.array(results[0]) print(result)
下表为MIGraphX对部分常用动态模型的支持情况(不在列表中的模型支持情况未知)。
支持的模型 | 支持的动态模式 |
---|---|
ResNet50 | 支持N,H,W维度动态 |
InceptionV3 | 支持N,H,W维度动态 |
MobileNetV2 | 支持N,H,W维度动态 |
MTCNN | 支持N,H,W维度动态 |
SSD-VGG16 | 支持N,H,W维度动态 |
RetinaNet | 支持N,H,W维度动态 |
RetinaFace | 支持N,H,W维度动态 |
YOLOV3 | 支持N,H,W维度动态 |
YOLOV4 | 支持N,H,W维度动态 |
YOLOV5 | 支持N,H,W维度动态 |
YOLOV8 | 支持N,H,W维度动态 |
YOLOX | 支持N,H,W维度动态 |
FasterRCNN | 不支持动态 |
DBNet | 支持N,H,W维度动态 |
EAST | 支持N,H,W维度动态 |
FCN | 支持N,H,W维度动态 |
UNet | 支持N,H,W维度动态 |
MaskRCNN | 不支持动态 |
CRNN | 支持N,W维度动态 |
SVTR | 支持N,W维度动态 |
BERT | 支持序列长度动态 |
T5 | 支持序列长度动态 |
Transformer | 支持序列长度动态 |
GPT2 | 支持序列长度动态 |
Code Llama | 支持序列长度动态 |
由于MIGraphX执行推理之前,需要对模型进行编译,编译过程是非常耗时的,特别是对于复杂的模型,如果第一次编译好模型之后能将编译好的模型进行序列化并保存到⽂件系统中,下次启动的时候直接加载就可以大大减少启动时间,MIGraphX中提供了save和load两个函数来实现该功能。
保存编译好的模型:
#include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> #include <migraphx/load_save.hpp> // save和load头文件 int main(int argc, char *argv[]) { // 加载模型 migraphx::program net = migraphx::parse_onnx("ResNet50.onnx"); // 编译模型 migraphx::compile_options options; options.device_id = 0; // 设置GPU设备,默认为0号设备 options.offload_copy = true; net.compile(migraphx::gpu::target{}, options); // 序列化并保存编译好的模型 migraphx::save(net, "ResNet50.mxr"); return 0; }
加载编译好的模型并执行推理:
#include <migraphx/onnx.hpp> #include <migraphx/gpu/target.hpp> #include <migraphx/load_save.hpp> // save和load头文件 #include <opencv2/opencv.hpp> int main(int argc, char *argv[]) { // 加载编译好的模型 migraphx::file_options options; options.device_id = 0; migraphx::program net = migraphx::load("ResNet50.mxr", options); // 获取模型输入/输出节点信息 std::cout << "inputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> inputs = net.get_inputs(); for (auto i : inputs) { std::cout << i.first << ":" << i.second << std::endl; } std::cout << "outputs:" << std::endl; std::unordered_map<std::string, migraphx::shape> outputs = net.get_outputs(); for (auto i : outputs) { std::cout << i.first << ":" << i.second << std::endl; } std::string inputName = inputs.begin()->first; migraphx::shape inputShape = inputs.begin()->second; int N = inputShape.lens()[0]; int C = inputShape.lens()[1]; int H = inputShape.lens()[2]; int W = inputShape.lens()[3]; // 数据预处理并转换为NCHW格式 int batchSize = N; cv::Mat srcImage = cv::imread("Test.jpg"); std::vector<cv::Mat> srcImages; for (int i = 0; i < batchSize; ++i) { srcImages.push_back(srcImage); } cv::Mat inputBlob; cv::dnn::blobFromImages(srcImages, inputBlob, 0.0078125, cv::Size(W, H), cv::Scalar(127.5, 127.5, 127.5), false, false); // 创建输入数据 std::unordered_map<std::string, migraphx::argument> inputData; inputData[inputName] = migraphx::argument{inputShape, (float *)inputBlob.data}; // 推理 std::vector<migraphx::argument> results = net.eval(inputData); // 获取输出节点的属性 migraphx::argument result = results[0]; // 获取第一个输出节点的数据 migraphx::shape outputShape = result.get_shape(); // 输出节点的shape std::vector<std::size_t> outputSize = outputShape.lens(); // 每一维大小,维度顺序为(N,C,H,W) int numberOfOutput = outputShape.elements(); // 输出节点元素的个数 float *resultData = (float *)result.data(); // 输出节点数据指针 // 打印推理结果 for (int i = 0; i < numberOfOutput; ++i) { std::cout << resultData[i] << ","; } std::cout << std::endl; return 0; }
我们可以看到加载编译好的模型之后不需要再次执行编译操作了,可以直接输入数据执行推理,节省了编译时间,加快了启动速度,同时使用这种方式还可以一定程度上实现对ONNX模型的加密。
在使用序列化功能的时候,需要注意MXR的版本和当前系统中的MIGraphX版本是否兼容。
通过migraphx-driver工具可以更方便的对模型进行序列化,以ResNet50模型为例:
/opt/dtk/bin/migraphx-driver compile --enable-offload-copy --binary --output ./ResNet50.mxr --onnx ./ResNet50.onnx
上面的 命令可以将ResNet50.onnx模型序列化保存为ResNet50.mxr,并设置offload-copy参数为true,其中–binary参数表示以mxr格式输出,–output表示输出文件的路径。
MIGraphX版本 | MXR版本 |
---|---|
2.5.0 | 5 |
2.5.1 | 5 |
2.5.2 | 5 |
2.5.3 | 5 |
3.0.0 | 6 |
3.1.0 | 6 |
3.1.1 | 6 |
3.1.2 | 6 |
3.1.3 | 6 |
3.2.0 | 6 |
3.2.1 | 6 |
4.0.0 | 7 |
4.1.0 | 7 |
4.2.0 | 8 |
MIGraphX提供了一个命令行工具migraphx-driver,该工具在MIGraphX安装目录下的bin文件中。
通过下面的命令可以查看模型的输入输出节点信息:
/opt/dtk/bin/migraphx-driver params --onnx ./ResNet50.onnx
输出如下结果:
Reading: ./resnet50.onnx
inputs:
input: float_type, {1, 3, 224, 224}, {150528, 50176, 224, 1}
outputs:
output: float_type, {1, 1000}, {1000, 1}
inputs后面表示输入节点,每个输入节点信息占一行:
input: float_type, {1, 3, 224, 224}, {150528, 50176, 224, 1}
其中input表示输入节点名,float_type表示输入的数据类型是float类型,{1, 3, 224, 224}表示输入数据每一维大小,{150528, 50176, 224, 1}表示输入数据每一维的步长。
outputs后面表示输出节点,格式与inputs相同。
如果需要查看MXR文件的输入输出节点信息,则需要设置–migraphx参数,该参数表示mxr文件的路径:
/opt/dtk/bin/migraphx-driver params --migraphx ./ResNet50.mxr
通过version命令可以查看当前系统安装的MIGraphX版本以及对应的ONNX Opset版本和MXR版本:
/opt/dtk/bin/migraphx-driver version
输出:
MIGraphX version: 4.0.0
ONNX Opset version: 17
MXR version: 7
表示当前系统安装的MIGraphX版本为4.0.0,对应的MXR版本为7,同时支持的ONNX Opset版本为17
通过设置version命令的–migraphx参数可以查看MXR文件的版本信息,包括MIGraphX版本和MXR版本:
/opt/dtk/bin/migraphx-driver version --migraphx ./ResNet50.mxr
如果MXR版本与当前系统中的MIGraphX版本不兼容,则该MXR文件不能在当前MIGraphX版本中使用。注意,该命令在4.0.0版本以后支持。
通过下面的命令可以查看当前MIGraphX支持的ONNX算子:
/opt/dtk/bin/migraphx-driver onnx -l
通过下面的命令可以查看模型的计算图结构:
/opt/dtk/bin/migraphx-driver read --onnx ResNet50.onnx
运行该命令后会输出如下结果:
Reading: ResNet50.onnx
module: "main"
input = @param:input -> float_type, {1, 3, 224, 224}, {150528, 50176, 224, 1}
...
main:@269 = convolution[padding={3, 3, 3, 3},stride={2, 2},dilation={1, 1},group=1,padding_mode=0,use_dynamic_same_auto_pad=0](input,main:@264) -> float_type, {1, 64, 112, 112}, {802816, 12544, 112, 1}
main:@270 = batch_norm_inference[epsilon=1e-05,momentum=0.9,bn_mode=1](main:@269,main:@265,main:@268,main:@267,main:@266) -> float_type, {1, 64, 112, 112}, {802816, 12544, 112, 1}
main:@271 = relu(main:@270) -> float_type, {1, 64, 112, 112}, {802816, 12544, 112, 1}
main:@272 = pooling[mode=max,padding={1, 1, 1, 1},stride={2, 2},lengths={3, 3},ceil_mode=0,lp_order=2,global=0](main:@271) -> float_type, {1, 64, 56, 56}, {200704, 3136, 56, 1}
main:@273 = convolution[padding={0, 0, 0, 0},stride={1, 1},dilation={1, 1},group=1,padding_mode=0,use_dynamic_same_auto_pad=0](main:@272,main:@249) -> float_type, {1, 64, 56, 56}, {200704, 3136, 56, 1}
main:@274 = batch_norm_inference[epsilon=1e-05,momentum=0.9,bn_mode=1](main:@273,main:@258,main:@261,main:@260,main:@259) -> float_type, {1, 64, 56, 56}, {200704, 3136, 56, 1}
main:@275 = relu(main:@274) -> float_type, {1, 64, 56, 56}, {200704, 3136, 56, 1}
...
如果需要查看MXR文件的计算图,则需要设置–migraphx参数:
/opt/dtk/bin/migraphx-driver read --migraphx ./ResNet50.mxr
测试环境:
下表中的数据表示FPS,值越大表示性能越好
Batchsize=1:
Batchsize=32:
从测试结果来看,在Z100上静态性能平均可以达到V100的60.46%,由于曙光Z100的峰值为V100的70%,所以计算效率达到V100的86.37%。
由于目前行业没有动态推理的评测标准,本次实验选取的评价标准就是连续推理多个不同shaep的输入,然后取平均FPS(等价于平均耗时),这里对常用的几个动态模型分别选取了多个不同shape的测试样本,为了尽量模拟真实场景,测试样本的size取值范围从小到大。
下面是本次实验选取的常用动态模型:
本次实验测试的数据集如下:
说明:表格中每个数据表示一个输入数据的shape,数据排布为NCHW,比如[1,3,224,224]表示输入数据的shape的batchsize为1,通道为3,宽高为224
下面是测试结果,测试环境与静态推理相同:
在Z100上动态性能平均可以达到V100的54%,由于Z100峰值为V100的70%,效率达到V100的77.14%。
从上面的测试数据可以看出MIGraphX推理性能还是非常不错的。
本文只是简单介绍了MIGraphX的基本概念、基本设计思想和基本的使用方法,并做了部分的性能测试,对于更加深入的框架解析后面有空再展开讨论。欢迎大家留言一起讨论。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。