特性
- 支持多种精度推理,比如FP32,FP16,INT8
- 支持多语言API,包括C++和Python
- 支持动态shape 、模型序列化
- 支持调试
- 提供性能分析⼯具
它的整体架构包括三个主要部分:中间表示层、编译优化层和计算引擎层。
中间表示层:将训练好的模型(如ONNX格式)转换为MIGraphX IR表示的计算图,后续的模型优化和代码生成都基于该计算图完成。
编译优化层:基于MIGraphX IR完成各种优化,比如常量折叠,内存复用优化,算子融合等,提高推理性能。
计算引擎层:主要包含底层计算库的接口,如MIOpen和rocblas。MIGraphX的后端实现主要通过调用这些计算库完成。
MIGraphX 使用单级 IR 设计,方便编译优化。在编译优化阶段,MIGraphX 实现了机器无关优化、内存复用优化和指令调度等多种优化措施。此外,MIGraphX 支持的模型包括但不限于 CNN、LSTM、Transformer、Bert 类型的模型,例如 AlexNet、VGG、Inception、ResNet、DenseNet、EfficientNet、SSD、YOLO、DBNet、FCN、UNet、MaskRCNN、CRNN、Vision Transformer(ViT)、BERT-Squad 等。我们还可以使用migraphx-driver onnx -l查看支持的onnx算子。
安装方法
- 使用镜像(推荐) 下载地址,根据需要选择合适的镜像
例如docker pull image.sourcefind.cn:5000/dcu/admin/base/migraphx:4.0.0-centos7.6-dtk23.04.1-py38-latest
在使用MIGraphX之前,需要设置容器中的环境变量:source /opt/dtk/env.sh,如果需要在python中使用migraphx,还需要设置PYTHONPATH :export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
使用安装包,安装包下载地址,根据不同的系统选择合适的安装包
安装dtk,上面的光源dtk镜像或者安装包,然后将下载好的安装包安装到/opt目录下,最后创建一个软连接/opt/dtk,使得该软连接指向dtk的安装目录,注意:一定要创建软连接/opt/dtk,否则MIGraphX无法正常使用。
安装halfwget https://github.com/pfultz2/half/archive/1.12.0.tar.gz,解压(tar -xvf ...tar.gz)后将include目录下的half.hpp拷贝到dtk目录下的include目录:cp half-1.12.0/include/half.hpp /opt/dtk/include/
安装sqlite:下载地址,解压,切换目录,然后./configure && make && make install,最后设置环境变量:export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:$PKG_CONFIG_PATH和export LD_LIBRARY_PATH=/usr/local/lib/:$LD_LIBRARY_PATH
下载MIGraphX: centos还要同时下载devel包
设置环境变量:source /opt/dtk/env.sh,如果需要在python中使用migraphx,还需要设置PYTHONPATH :export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
验证是否安装成功:/opt/dtk/bin/migraphx-driver onnx -l,输出支持的算子即可
编程模型
shape
Shape 用于描述数据的形状和类型。例如,假设有一个3通道的224x224的图像,其 Shape 可以这样表示:migraphx::shape{migraphx::shape::float_type, {1, 3, 224, 224}}
其中 float_type 表示数据类型为浮点型,{1, 3, 224, 224} 分别代表批量大小(batch size)、通道数(channel)、高度和宽度,如果是变量传入,它得是std::vector类型的。
构造函数还有第三个可选参考:std::vector类型,它用来表示每一维度的步长,如果没有指定步长,则按照shape为standard的形式根据l自动计算出步长,比如对于一个内存排布为 [N,C,H,W]格式的数据,对应的每一维的步长为[C * H * W,H * W,W,1]。
其中:
shape支持的类型包括:bool_type,half_type,float_type,double_type,uint8_type,int8_type,uint16_type,int16_type,int32_type,int64_type,uint32_type,uint64_type
shape中常用的成员函数:
lens(): 返回每一维的大小。例如,shape.lens() 可能返回 {1, 3, 224, 224},表示批量大小、通道数、高度和宽度。
elements(): 返回所有元素的个数。例如,shape.elements() 可能返回 1*3*224*224。
bytes(): 返回所有元素的字节数。例如,对于浮点类型的数据,shape.bytes() 可能返回 1*3*224*224*sizeof(float)。
在shape中,无论是图像还是卷积,都要是NCHW格式的,例如有一卷积核大小为7x7,输出特征图个数为64,输入的是一个3通道的图像,则该卷积核的shape可以表示为migraphx::shape{migraphx::shape::float_type, {64, 3, 7, 7}},注意{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。一个float占4个字节。
argument
类似Pytorch中的Tensor,常用来保存模型的输入和输出数据。
假设 inputData 是一个 cv::Mat 对象,代表输入图像数据,则 Argument 可以这样构建:
migraphx::argument input = migraphx::argument{inputShape, (float*)inputData.data};
这里 inputShape 是数据的 Shape,(float*)inputData.data 是数据的指针。argument不会自动释放该数据。
当然,可以只需要提供shape就可以,系统会自动申请一段内存,该内存的大小等于shape的bytes()方法返回值的大小。
argument中常用的成员函数:
get_shape()
: 返回数据的形状。例如,argument.get_shape()
。data()
: 返回指向数据的指针。例如,argument.data()
可以用来访问或修改存储在Argument
中的推理结果。
前面介绍了cv::Mat转换migraphx::argument,它也支持重新转回去的。
migraphx::argument result;// result表示推理返回的结果,数据布局为NCHW int shapeOfResult[]={result.get_shape().lens()[0],result.get_shape().lens() [1],result.get_shape().lens()[2],result.get_shape().lens()[3]};// shapeOfResult表 示的维度顺序为N,C,H,W cv::Mat output(4, shapeOfResult, CV_32F, (void *)(result.data()));// 注意,cv::Mat 不会释放result中的数据
literal
使用literal表示常量,比如卷积的权重。实际上literal是一种特殊的 argument。
如果有一个权重数组 weights,其 Shape 为 weightShape,则可以这样创建一个 Literal:
migraphx::literal weightLiteral = migraphx::literal{weightShape, weights.data()};
这里设 weights 是一个包含权重数据的标准容器,如 std::vector。第二个参数可以传入一个连续的数据指针(data方法),或者直接使用weights变量。
literal中常用的成员函数:
get_shape(): 与 Argument 中的同名函数类似,返回常量的形状。
data(): 返回指向常量数据的指针。不同于 Argument,Literal 中的数据不可修改。
target
target表示支持的硬件平台,目前支持CPU模式和GPU模式,在编译模型的时候,需要指定一个target。
program表示一个神经网络模型
构造一个program,很简单:
migraphx::program net;
program中常用的成员函数:
compile(target, options): 编译模型。target 参数指定硬件平台,options 提供编译设置。比如可以通过options.device_id设 置使用哪一块显卡。
eval(params): 执行推理并返回结果。params 是一个包含模型输入的 parameter_map。parameter_map类型是std::unordered_map< std::string, argument>(哈希容器)的别名。注意这是一个同步的方法。
get_parameter_shapes(): 返回模型输入或输出参数的形状。返回哈希容器类型。
get_main_module(): 返回程序的主计算图,通常用于添加或修改模型层。
module
创建program的时候,会自动创建一个主计算图。而现代神经网络模型中可能存在多个子图,MIGraphX中使用module表示子图,每个子图又是由指令组成。
例如,在主模块中添加输入:
//获取主计算图 migraphx::module *mainModule = net.get_main_module(); // 添加模型的输入 migraphx::instruction_ref input =mainModule->add_parameter("input", migraphx::shape{migraphx::shape::float type, {1, 1,4,6}});
module中常用的成员函数:
add_parameter(name, shape): 添加模型输入。name 是输入的名称,shape 是输入的形状。返回值表示添加到模型中的该条指令的引用。
add_literal(literal): 向模块添加一个 Literal 对象。返回值表示添加到模型中的该条指令的引用。
add_instruction(op, args): 添加指令。op 是算子,args 是算子的参数。返回值表示添加到模型中的该条指令的引用。
add_return(args): 添加结束指令,通常表示模型的输出。
MIGraphX中使用instruction_ref这个类型表示指令的引用
instruction
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 print(t[0][0]) # 3.14
与Pytorch一样,MIGraphX也支持视图,一个argument可以是另一个argument的视图,视图和原argument共享内存, MIGraphX中支持视图的操作有
broadcast
slice
transpose
reshape
下面表示一个4行6列的二维数组,该数组按照行主序的方式在内存中连续存储(与C语言中的数组一致),所以在列这个维度上步长为1,在行这个维度上的步长为6,假设该二维数组的数据类型为float类型,则该二维数组的shape可以表示为{migraphx::shape::float_type, {4,6}},这里没有显式指定每一维的步长,migraphx会自动计算出步长:{migraphx::shape::float_type, {4,6},{6,1}}。
□ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □ □
现在有一个切片操作(slice),该切片操作参数为:starts=[0,2],ends =[4,5],steps = [1, 1] ,切片操作的结果为原二维数组的一个视图,该视图与原数据共享内存,该视图如下所示。
切片左闭右开,实际上应该是[0,2]到[3,4]
0 1 2 3 4 5 0 □ □ ■ ■ ■ □ 1 □ □ ■ ■ ■ □ 2 □ □ ■ ■ ■ □ 3 □ □ ■ ■ ■ □
具体实现的时候,视图包含一个数据指针以及该数据的shape,为了方便说明,将shape拆分为2个部分表示:每一 维的大小和步长,本示例中该视图的数据指针指向原数组第三个元素,该视图的shape可以表示为{migraphx::shape::float_type, {4,3},{6,1}},所以视图中的成员lens为[4,3],strides为[6,1],注意由于与原数据共享内 存,所以该视图的步长为[6,1]而不是[3,1]。
// 视图包含的成员 { float *data_ptr; std::vector<std::size_t> lens; std::vector<std::size_t> strieds; }
视图中元素的访问
通过shape可以访问到正确的视图中的数据比如要访问该视图的第2行第1列的元素"🫣",该元素在视图中的二维索引index可以表示为[1,0],则在实际内存中的索引(相当于“😜”)为二维索引和步长的内积: index*strides=1 * 6 + 0 * 1 =6,“😜”是视图的data_ptr,则二维索引为[1,0]表示的数据在内存中对应的数据为data_ptr+6,所以可以通过二维索引与步长的内积得到实际的内存索引。
0 1 2 3 4 5 0 □ □ 😜 ■ ■ □ 1 □ □ 🫣 ■ ■ □ 2 □ □ ■ ■ ■ □ 3 □ □ ■ ■ ■ □
MIGraphX中部分算子是不支持输入视图的,所以对于这些算子,如果输入的是一个视图,就需要通过contiguous操 作将内存变得连续。对于上面slice操作返回的视图,contiguous算子会创建一个新的内存空间,将转换后得到的内存连续的数据保存在新的内存空间中。contiguous算子的输出的shape可以表示为{migraphx::shape::float_type, {4,3},{3,1}},此时行步长是3而不是之前共享内存时的6了。