前言

之前写过TNN框架解析,其实早在TNN之前我就调研过AMD开源的推理框架MIGraphX,现在也是中科曙光(海光)主推的推理框架,对标的是英伟达的TensorRT。MIGraphX的架构跟TNN完全不同,TNN其实还是受到Caffe的影响比较大,里面很多方面的设计都有Caffe的影子,很多地方命名都是基本一致的,但是MIGraphX不同,MIGraphX的算子粒度更细,更加灵活,整体架构是按照AI编译器思路来构造的。其实MIGraphX整体架构还是非常清晰的,里面有很多东西值得我们去学习,比如里面用到了很多高级的编程技法(比如模板和函数式编程的各种高级特性),还有很多pass也值得学习,比如内存复用优化pass用到了图着色算法,指令调度pass。本文先重点阐述MIGraphX的基本设计思想以及基本使用方法,对框架更加深入的解析等后面有时间再详细展开讨论。

注:曙光提供的官方文档参考这里



MIGraphX整体架构

在这里插入图片描述

MIGraphX整体架构主要分为三层:

  1. 中间表示层。主要将用户训练好的ONNX格式的算法模型转换为MIGraphX IR。
  2. 编译优化层。基于MIGraphX IR完成各种优化,比如常量折叠,内存复用优化,算子融合等。
  3. 计算引擎层。这一层主要包含了底层计算库的接口,包括MIOpen和rocblas 。

MIGraphX IR

AI编译中的IR从层级上分一般可以分为两种类型:多级IR和单级IR。使用多级IR可以使得系统优化更加灵活,各级IR只需要负责本级优化,但是多级IR会带来如下的问题:

  1. 需要在不同IR之间进行转换,IR转换做到完全兼容很难而且工作量大。
  2. 不同IR转换可能带来信息的损失。
  3. 多级IR有些优化既可以在上一层IR进行, 也可以在下一层IR进行, 让系统开发者很难选择。

MIGraphX采用了单级IR的设计,MIGraphX IR是一种基于SSA形式的线性IR,这种形式的IR可以表达计算图中的控制流信息和数据依赖关系,方便后面的编译优化。MIGraphX IR由program,module,instruction等基本结构组成。

编译优化

MIGraphX采用静态图模式,在编译优化阶段,MIGrahpX实现了如下的优化:

  1. 机器无关优化:包括删除公共子表达式,删除无用的代码,常量传播,常量折叠,代数化简,算子融合等。
  2. 内存复用优化:MIGraphX采用了图着色的方法实现无计算依赖的节点间的内存复用,显著减低内存消耗。
  3. 指令调度:主要是根据计算图分析指令之间的依赖关系,根据这些依赖关系优化各指令的执行顺序,从而提高计算性能。
  4. 代码生成:MIGraphX利用代码生成技术生成算子的实现,然后采用JIT的方式将源码编译为可执行代码。

MIGraphX编程模型

这里简要阐述MIGraphX中的一些基本概念和基本设计思想。

Shape

用来表示数据的形状。

可以通过如下方式构造一个shape对象:

  1. shape(type_t t, std::vector < std::size_t > l);
  2. shape(type_t t, std::vector < std::size_t > l, std::vector < std::size_t > s);

其中:

  1. t:shape的类型,shape支持的类型包括:1. bool_type,half_type,float_type,double_type,uint8_type,int8_type,uint16_type,int16_type,int32_type,int64_type,uint32_type,uint64_type
  2. l:每一个维度的大小
  3. s:每一个维度的步长,如果没有指定步长,MIGraphX会根据每一维的大小自动计算出步长,比如对于一个内
    存排布为[N,C,H,W]格式的数据,对应的每一维的步长为[C * H * W,H * W,W,1]。

shape中常用的成员函数:

  1. lens():返回每一维的大小,维度顺序为(N,C,H,W),类型为std::vector < std::size_t >
  2. elements():返回所有元素的个数,类型为std::size_t
  3. bytes():返回所有元素的字节数,类型为std::size_t

示例:
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

argument类型用来保存数据,类似Pytorch中的Tensor,常用来保存模型的输入和输出数据。

可以通过如下方式构造一个argument对象:

  1. argument(const shape& s);
  2. template argument(shape s, T* d)

第1种方式只需要提供shape就可以,系统会自动申请一段内存,该内存的大小等于shape的bytes()方法
返回值的大小。第2种方式除了提供shape之外,还需要提供该argument的数据指针,argument不会自动释放该数据。

argument中常用的成员函数:

  1. get_shape():返回数据的形状,类型为shape
  2. data():返回argument的数据,类型为char *

literal

MIGraphX中使用literal表示常量,比如可以使用literal表示卷积的权重。实际上literal是一种特殊的argument,literal中的值不能修改,而argument中的值可以修改。

可以通过如下方式构造一个literal对象:

  1. template literal(const shape& s, const std::vector& x)
  2. template literal(const shape& s, T* x)
  3. template literal(const shape& s, const std::initializer_list& x)

第一种构造方法是使用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中常用的成员函数:

  1. get_shape():返回数据的形状,类型为shape
  2. data():返回literal的数据,类型为const char *,注意:不能通过data()返回的指针修改literal的值

target

target表示支持的硬件平台,目前支持CPU和GPU,在编译模型的时候,需要指定一个target。

program

MIGraphX中使用program结构表示一个神经网络模型。

program中常用的成员函数:

  1. compile():编译模型,其参数是一个target
  2. eval(): 执行推理并返回推理结果,返回类型为std::vector,注意这是一个同步的方法
  3. get_inputs():返回模型的输入节点信息,每个输入节点包含输入名和输入shape
  4. get_outputs() : 返回模型的输出节点信息,,每个输出节点包含输出名和输出shape。
  5. get_memory_usage(): 返回模型推理需要的显存大小,单位为字节

注意:如果需要在不同的线程中使用MIGraphX推理,不同线程不能共用同一个program对象,每个线
程需要单独创建一个program对象执行推理。

module

现代神经网络模型中可能存在多个子图,MIGraphX中使用module表示子图,每个子图又是由指令组
成。创建program的时候,会自动创建一个主计算图,可以通过program的get_main_module()方法获
取主计算图。

module中常用的成员函数:

  1. add_parameter():用来添加模型的输入
  2. add_literal():添加常量,比如可以使用该成员函数添加卷积算子的权重
  3. add_instruction():添加指令
  4. add_return():添加结束指令,通常表示模型的结尾

注意:

  1. add_parameter(),add_literal(),add_return()添加的是模型中特殊的指令,这些指令不能使用add_instruction()添加,add_instruction()一般用来添加除了输入,常量和结束指令之外的其他指令。
  2. 上述所有添加指令的成员函数返回添加的这条指令的引用,MIGraphX中使用instruction_ref这个类型表示指令的引用,后续指令如果需要使用该条指令作为输入,可以通过该引用来获取该指令。

instruction

instruction表示指令,可以通过module中的add_instruction()成员函数添加指令。MIGraphX中的指令相当于ONNX模型中的一个节点或者caffe模型中的一个层。指令由操作符(算子)和操作数组成。

MIGraphX中的视图(View)

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中支持视图操作的算子有:

  1. broadcast
  2. slice
  3. transpose
  4. reshape
    这些算子返回原argument的一个视图,下面以slice操作为例,阐述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中没有采用手动管理内存的方式,因为这样容易导致内存泄漏,特别是在发生异常的时候。MIGraphX中的自动内存管理主要采用如下两种方式:

  1. 对于原始内存的申请,使用std::make_unique 或者 std::make_shared ,对于数组类型的元素,使用std::vector
  2. 对于非内存类型的资源,比如文件FILE*,使用MIGRAPHX_MANAGE_PTR 宏来创建一个std::unique_ptr,MIGRAPHX_MANAGE_PTR 宏是对std::unique_ptr的一种封装,通过MIGRAPHX_MANAGE_PTR可以正确的调用和释放资源。

示例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中使用标准库中提供的算法来代替使用原始的循环结构,因为原始的循环接口有如下缺点:

  1. 带来了隐式的性能开销
  2. 容易出错,特别是在处理边界的时候
  3. 难以解释并且难以证明后续条件
    使用算法比原始的循环结果更加高效,而且算法更加容易优化,如果没有一个合适的算法来替代原始循环结果,添加一个新的算法是个好办法。

示例:
对于下面一段程序

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);
	    // ...
	}

通过类型擦除(Type Erasure)机制实现多态

MIGraphX中有许多函数的实现需要使用到多态机制,比如在MIGraphX中神经网络是使用program表示,program中包含了许多指令,添加指令需要用到如下函数:

instruction_ref module::add_instruction(const operation& op, std::vector<instruction_ref> args)

该函数的第一个参数表示该指令执行的操作,MIGraphX中使用算子表示,但是实际中有很多算子,比如卷积算子、relu算子等,也就是说operation类型需要能够被多种类型的算子赋值,能够表示不同类型的算子,这就是多态机制,MIGraphX采用了类型擦除机制来实现多态。

关于类型擦除的原理,参考这篇博客:C++多态的另一种实现:类型擦除


如何使用MIGraphX

这里以ResNet50为例来说明如何通过C++ API加载ONNX模型进行图像分类模型的推理。

将模型转换为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进行分类模型的推理

使用C++ API加载ONNX模型进行推理主要包含三个步骤:

  1. 加载ONNX模型
  2. 编译模型
  3. 执行推理

主要步骤示例代码如下:

// 头文件
#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;
}
  • 首先通过parse_onnx()函数加载onnx模型,加载好模型之后,可以通过program的get_inputs()和get_outputs()函数获取模型的输入和输出节点信息,两个函数返回值类型都是std::unordered_map<std::string, migraphx::shape>类型,每个输入节点或者输出节点都有一个节点名和shape,由于ResNet50模型只有一个输入,所以这里可以通过inputs.begin()获取模型的输入。
  • 如果需要采用FP16模式进行推理,可以通过quantize_fp16()函数实现,MIGraphX同时也支持int8推理。
  • 加载onnx模型之后,需要使用compile()方法编译模型,这里将模型编译为GPU模式,如果需要编译为CPU模式,需要使用migraphx::cpu::target{},注意:如果你的输入数据在host端,则在设置编译选项的时候,需要设置offload_copy为true。
  • 编译好模型之后,需要输入数据,输入数据需要经过预处理并转换为NCHW的格式,这里使用了OpenCV的blobFromImage函数将图像转换为了NCHW格式。并通过构造一个std::unordered_map<std::string, migraphx::argument>类型的对象保存输入数据,模型的每个输入都会对应一个输入数据,输入数据使用argument保存。
  • 最后通过program的eval()方法执行推理计算,推理结果是一个std::vector< migraphx::argument >类型,由于offload_copy参数设置为true,所以推理的结果是host端数据,然后我们就可以通过argument提供的方法访问推理结果了,如果需要指定输出节点,可以设置eval()中的outputNames参数。
  • 完整的ResNet50示例程序参考ModelZoo

使用FP16推理

本节主要说明如何在MIGraphX中使用FP16进行推理,在MIGraphX中可以通过下面两种方式实现FP16推理:

  1. 方式1:使用FP32格式的ONNX模型,然后通过调用migraphx::quantize_fp16()实现FP16的推理
  2. 方式2:先将FP32格式的ONNX模型转换为FP16格式模型,然后使用FP16格式的ONNX模型执行推理

下面是两种方式的具体使用说明。

使用migraphx::quantize_fp16

实现FP16推理可以直接使用FP32格式的ONNX模型,然后在编译前调用migraphx::quantize_fp16()。这种方式的优点是不需要转换模型格式,只需要修改少量代码,推荐使用该方式。具体使用方法如下:

#include <migraphx/quantization.hpp> // FP16头文件

// 使用FP16
migraphx::quantize_fp16(net);

// 编译模型(下面步骤跟FP32推理相同)
...

将模型转换为FP16格式

除了调用quantize_fp16的方式外,还可以通过将模型转换为FP16格式来实现FP16的推理。通过下面的方法可以将FP32格式的模型转换为FP16格式:

  1. 安装onnx和onnxconverter-common

    pip install onnx onnxconverter-common
    
  2. 通过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推理

使用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

这里介绍如何在python中使用MIGraphX。

设置环境变量

将MIGraphX库路径加入PYTHONPATH:

export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH

在Python中使用MIGraphX

下面的示例展示了如何使用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程序的流程与C++基本一致
  • 更多Python示例程序参考ModelZoo

在Python中使用FP16推理

如果需要在python中使用FP16进行推理,只需要在编译前面加上如下语句即可:

# 使用FP16
migraphx.quantize_fp16(model)

# 编译模型
...

在Python中使用INT8推理

与C++中的INT8推理类似,在Python中使用INT8进行推理,只需要在编译前加上如下语句即可:

# 读取量化校准数据
image = ReadImage()
inputData[inputName] = migraphx.argument(image)

# 创建量化数据,这里只使用了一张图像,实际使用时为了提高量化精度,建议使用多张图像创建多个inputData进行量化
calibrationData = [inputData]
migraphx.quantize_int8(model, migraphx.get_target("gpu"), calibrationData)

使用Device数据做推理

前面的示例中,我们都是使用的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::compile_options的时候需要注意,一定要将offload_copy设置为false,这样才可以直接使用device数据。
  • 编译好模型之后,需要为输出节点分配device内存,用于保存推理结果,这里通过AllocateOutputMemory()方法中的migraphx::gpu::allocate_gpu()方法实现。
  • 示例中通过migraphx::gpu::to_gpu()方法将输入数据从host端拷贝到device端。
  • 模型的推理结果保存在AllocateOutputMemory方法分配的device内存中,并通过results返回,results与AllocateOutputMemory方法分配的device内存共享内存,如果需要在host端使用推理返回的结果,可以使用migraphx::gpu::from_gpu()拷贝到host端使用。
  • 使用migraphx::gpu::allocate_gpu(),migraphx::gpu::to_gpu()和migraphx::gpu::from_gpu()方法需要加入头文件 #include <migraphx/gpu/hip.hpp>。

MIGraphX中的动态shape

目前曙光提供的最新版本的MIGraphX已经能够很好的支持动态shape了,而且性能优异。MIGraphX的动态shape使用方式与静态shape基本一致,动态推理只需要在静态程序基础上设置一个最大输入shape

动态shape示例

本示例使用ResNet50模型说明动态shape模型的基本运行流程。

生成动态ONNX模型

动态推理需要动态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可变的模型。

C++示例程序

#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

Python示例程序

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)

动态shape的限制

  1. 目前MIGraphX只支持shape维度大小的动态,不支持维度个数的动态,比如无法处理从3维数据到4维数据的推理
  2. 目前MIGraphX的动态推理对if语句和Loop语句支持不完善,设计模型结构的时候尽量少用或者不用这些语句

支持的动态模型

下表为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进行模型序列化

通过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版本对应关系

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-driver的使用

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

查看MXR文件的版本

通过设置version命令的–migraphx参数可以查看MXR文件的版本信息,包括MIGraphX版本和MXR版本:

/opt/dtk/bin/migraphx-driver version --migraphx ./ResNet50.mxr

如果MXR版本与当前系统中的MIGraphX版本不兼容,则该MXR文件不能在当前MIGraphX版本中使用。注意,该命令在4.0.0版本以后支持。

查看支持的ONNX算子

通过下面的命令可以查看当前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

与TensorRT的性能对比

静态性能对比

测试环境:

  1. 曙光 Z100,DTK23.10.1,MIGraphX4.1.0
  2. 英伟达V100,CUDA11.8,TensorRT8.5.3
  3. 数据类型采用FP32

下表中的数据表示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的基本概念、基本设计思想和基本的使用方法,并做了部分的性能测试,对于更加深入的框架解析后面有空再展开讨论。欢迎大家留言一起讨论。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐