MIGraphX是一款用于DCU上的深度学习推理引擎。MIGraphX能将深度学习框架(Tensorflow,Pytorch等)训练好的算法模型转换为MIGraphX IR表示的计算图,并提供端到端的模型优化、代码⽣成以及推理业务部署能⼒ 。 MIGraphX致⼒于为用户提供灵活、易⽤的编程接⼝以及配套⼯具,让用户能够专注于推理业务开发和部署本⾝ ,而无需过多关注底层硬件细节,显著提高用户的开发效率。
特性
- 支持多种精度推理,比如FP32,FP16,INT8
- 支持多语言API,包括C++和Python
- 支持动态shape
- 支持模型序列化
- 支持调试
- 提供性能分析⼯具
整体架构
- 中间表示层:用户训练好的算法模型(onnx)会统⼀转换为用MIGraphX IR 表示的计算图,后续的模型优化和代码生成都基于该计算图完成。
- 编译优化层:基于MIGraphX IR完成各种优化,比如常量折叠,内存复用优化,算子融合等,提高推理性能。
- 计算引擎层:主要包含了底层计算库的接口,包括MIOpen和rocblas,MIGraphX后端的实现主要是通过调用计算库的方式实现的
AI编译中的IR从层级上分一般可以分为两种类型:多级IR和单级IR。使用多级IR可以使得系统优化更加灵 活,各级IR只需要负责本级优化,多级IR的代表就是MLIR,但是多级IR会带来如下的问题:
- 需要在不同IR之间进行转换,IR转换做到完全兼容很难而且工作量大。
- 不同IR转换可能带来信息的损失。
- 多级IR有些优化既可以在上一层IR进行, 也可以在下一层IR进行, 让系统开发者很难选择。 MIGraphX采用了单级IR的设计,这种形式的IR可以表达计算图中的控制流信息和数据依赖关系,方便后 面的编译优化。
MIGraphX采用静态图模式,在编译优化阶段,MIGrahpX实现了如下的优化:
- 机器无关优化:比如删除公共子表达式,删除无用的代码,常量传播,常量折叠,代数化简,算子 融合等。
- 内存复用优化:MIGraphX采用了图着色的方法实现无计算依赖的节点间的内存复用,显著减低内 存消耗。
- 指令调度:根据计算图分析指令之间的依赖关系,根据这些依赖关系优化各指令的执行顺序,从而 提高计算性能。
支持的算子
migraphx-driver onnx -l
查看支持的onnx算子
支持的模型
目前 MIGraphX支持常用的 CNN 、LSTM 、Transformer和Bert等模型:
- Classification:AlexNet,VGG,Inception,ResNet,DenseNet,EfficientNet等
- Detection :SSD,YOLO,DBNet等
- Segmentation :FCN,UNet,MaskRCNN等
- LSTM:CRNN等
- Transformer:Vision Transformer(ViT)等
- BERT:BERT-Squad等
安装方法
- 使用镜像(推荐) 下载地址,根据需要选择合适的镜像
例如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无法正常使用。
- 安装half
wget 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对象:
shape(type_t t, std::vector < std::size_t > l);
shape(type_t t, std::vector < std::size_t > l, std::vector < std::size_t > s);
其中:
- t:shape的类型,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
- l:每一个维度的大小
- s:每一个维度的步长,如果没有指定步长,则按照shape为standard的形式根据l自动计算出步长,比如对于一个内存排布为 [N,C,H,W]格式的数据,对应的每一维的步长为[C * H * W,H * W,W,1]
shape中常用的成员函数:
const std::vector<std::size_t>& lens() const
返回每一维的大小,维度顺序为(N,C,H,W)std::size_t elements() const
返回所有元素的个数std::size_t bytes() const
返回所有元素的字节数
示例: resnet50中第一个卷积层的卷积核大小为7x7,输出特征图个数为64,即有64个7x7的卷积核,如果输入的是一个3通道的图像,则该卷积核的shape可以表示为migraphx::shape{migraphx::shape::float_type, {64, 3, 7, 7}},其中float_type表示shape的数据类型,这里采用float类型, {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,常用来保存模型的输入和输出数据。
可以通过如下方式构造一个argument对象:
- argument(const shape& s)
template<class T> argument(shape s, T* d)
第1种方式只需要提供shape就可以,系统会自动申请一段内存,该内存的大小等于shape的bytes()方法返回值的大小。
第2种方式除了提供shape之外,还需要提供该argument的数据指针,argument不会自动释放该数据。
argument中常用的成员函数:
const shape& get_shape() const
返回数据的形状char* data() const
返回argument的数据,可以通过data()的返回值访问推理结果。
与cv::Mat之间的转换
cv::Mat转换为migraphx::argument:
c++
cv::Mat inputData;// inputData表示一张224x224的3通道图像,数据类型为float类型,且为NCHW
形式
migraphx::shape inputShape=migraphx::shape{migraphx::shape::float_type, {1, 3,224, 224}};
migraphx::argument input= migraphx::argument{inputShape,(float*)inputData.data};// 注意,migraphx::argument不会释放inputData中的数据
migraphx::argument转换为cv::Mat:
c++
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表示卷积的权重。实际上literal是一种特殊的 argument,literal中的值不能修改,而argument中的值可以修改。
可以通过如下方式构造一个literal对象:
template<class T> literal(const shape& s, const std::vector<T>& x)
template<class T> literal(const shape& s, T* x)
template<class T> literal(const shape& s, const std::initializer_list<T> &x)
第一种构造方法是使用std::vector来创建一个常量,第二种使用数据指针来构造,第三种是使用 std::initializer_list来构造。
literal中常用的成员函数:
const shape& get_shape() const
返回常量的形状const char* data() const
返回常量的数据指针,注意:不能通过data()返回的指针修改literal的值
target
target表示支持的硬件平台,目前支持CPU模式和GPU模式,在编译模型的时候,需要指定一个target。
program
表示一个神经网络模型
program中常用的成员函数:
void compile(const target& t, compile_options options = compile_options{})
编译模型。第一个参数t是一个target,第二个参数options表示编译的一些设置,比如可以通过options.device_id设 置使用哪一块显卡。std::vector<argument> eval(parameter_map params) const
执行推理并返回推理结果,参数params表示模型的输入数据,params中保存模型每个输入节点对应的输入数据, parameter_map类型是std::unordered_map< std::string, argument>
的别名,注意这是一个同步的方法。std::unordered_map<std::string, shape> get_parameter_shapes()
返回模型的输入或输出参数信息,常用来获取模型的输入参数信息。module* get_main_module()
获取主计算图,module表示模型中的子图
std::unordered_map
是哈希容器,它可以存储一组键值对,并且支持快速的查找、插入和删除操作
module
现代神经网络模型中可能存在多个子图,MIGraphX中使用module表示子图,每个子图又是由指令组成。 创建program的时候,会自动创建一个主计算图,可以通过program的get_main_module()方法获取主计算图。
module中常用的成员函数:
instruction_ref add_parameter(std::string name, shape s)
主要用来添加模型的输入,name表示输入名,s表示输入形状,返回值表示添加到模型中的该条指令的引用。instruction_ref add_literal(literal l)
添加常量,比如可以使用该成员函数添加卷积算子的权重,返回值表示添加到模型中的该条指令的引用。instruction_ref add_instruction(const operation& op, std::vector args)
添加指令,第一个参数op表示算子,args表示算子的参数,返回值表示添加到模型中的该条指令的引用。instruction_ref add_return(std::vector args)
添加结束指令,通常表示模型的结尾,args表示模型最后的指令。
注意:
- add_parameter(),add_literal(),add_return()添加的是模型中特殊的指令,这些指令不能使用add_instruction()添加, add_instruction()一般用来添加除了输入,常量和结束指令之外的其他指令。
- 上述所有添加指令的成员函数返回添加的这条指令的引用,MIGraphX中使用instruction_ref这个类型表示指令的引 用,后续指令如果需要使用该条指令作为输入,可以通过该引用来获取该指令。
instruction
instruction表示指令,可以通过module中的add_instruction()成员函数添加指令。MIGraphX中的指令相当 于ONNX模型中的一个节点或者caffe模型中的一个层。指令由操作符(算子)和操作数组成。
视图
我们知道Pytorch中支持视图操作(view),Pytorch中一个tensor可以是另一个tensor的视图,视图tensor与原tensor 共享内存,视图可以避免不必要的内存拷贝,让操作更加高效。比如我们可以通过view()方法获取一个tensor的视 图:
python
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]。
c++
// 视图包含的成员
{
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了。