目录
4.3.1 使用migraphx::quantize_fp16
11.3.1 onnx的upsample算子与pytorch不等价
1 MIGraphX简介
MIGraphX是一款用于DCU上的高性能深度学习推理引擎。MIGraphX能将深度学习框架(Tensorflow,Pytorch等)训练好的算法模型转换为MIGraphX IR表示的计算图,并提供端到端的模型优化、代码⽣成以及推理业务部署能力。MIGraphX致力于为用户提供高性能、灵活、易用的编程接口以及配套⼯具,让用户能够专注于推理业务开发和部署本⾝ ,而无需过多关注底层硬件细节,显著提高用户的开发效率。
1.1 MIGraphX适用场景
- 图像处理,比如分类(VGG16,ResNet50)、检测(SSD,YOLO)、分割(UNet)等
- 视频处理
- 自如语言处理,比如BERT
- 搜索、推荐
1.2 MIGraphX特性
- 支持多种精度推理,比如FP32,FP16,INT8
- 支持多语言API,包括C++和Python
- 支持动态shape
- 支持模型序列化
- 支持调试
- 提供性能分析⼯具
1.3 MIGraphX整体架构
图1.1:MIGraphX整体架构
MIGraphX整体架构主要分为三层:
- 中间表示层:用户训练好的算法模型会统⼀转换为用MIGraphX IR 表示的计算图,后续的模型优化和代码生成都基于该计算图完成。
- 编译优化层:基于MIGraphX IR完成各种优化,比如常量折叠,内存复用优化,算子融合,代码生成等,提高推理性能。
- 计算引擎层:主要包含了底层计算库的接口,包括MIOpen和rocblas。
1.3.1 MIGraphX IR
AI编译中的IR从层级上分一般可以分为两种类型:多级IR和单级IR。使用多级IR可以使得系统优化更加灵活,各级IR只需要负责本级优化,多级IR的代表就是MLIR,但是多级IR会带来如下的问题:
- 需要在不同IR之间进行转换,IR转换做到完全兼容很难而且工作量大。
- 不同IR转换可能带来信息的损失。
- 多级IR有些优化既可以在上一层IR进行, 也可以在下一层IR进行, 让系统开发者很难选择。
MIGraphX采用了单级IR的设计,MIGraphX IR以基于SSA的线性IR为基本组成形式 ,这种形式的IR可以表达计算图中的控制流信息和数据依赖关系,方便后面的编译优化。MIGraphX IR由program,module,instruction等基本结构组成,关于这些基本结构的详细介绍参考第3章编程模型。
1.3.2 编译
MIGraphX采用静态图模式,在编译阶段,MIGrahpX会执行如下操作:
- 机器无关优化:比如删除公共子表达式,删除无用的代码,常量传播,常量折叠,代数化简等。
- 内存复用优化:MIGraphX采用了图着色的方法实现无计算依赖的节点间的内存复用,显著减低内存消耗。
- 指令调度:根据计算图分析指令之间的依赖关系,根据这些依赖关系优化各指令的执行顺序,从而提高计算性能。
- 代码生成:MIGraphX利用代码生成技术生成算子的实现,然后利用JIT编译为可执行代码。
1.4 支持的ONNX算子
MIGraphX4.0.0支持ONNX Opset 17,建议导出ONNX模型的时候Opset不低于13,如果Opset版本过低,可能会由于部分算子定义不同导致模型精度有差异或者模型推理报错。最新的ONNX算子信息可以在这里查看。 MIGraphX支持的ONNX数据类型有DOUBLE, FLOAT32, FLOAT16, INT8, 和 BOOL。具体支持情况见下表:
算子 | 是否支持 | 数据类型 |
---|---|---|
Abs | Y | FP32, FP16, INT32 |
Acos | Y | FP32, FP16 |
Acosh | Y | FP32, FP16 |
Add | Y | FP32, FP16, INT32 |
And | Y | BOOL |
ArgMax | Y | FP32, FP16, INT32 |
ArgMin | Y | FP32, FP16, INT32 |
Asin | Y | FP32, FP16 |
Asinh | Y | FP32, FP16 |
Atan | Y | FP32, FP16 |
Atanh | Y | FP32, FP16 |
AveragePool | Y | FP32, FP16, INT8, INT32 |
BatchNormalization | Y | FP32, FP16 |
Bernoulli | N | |
BitShift | N | |
BlackmanWindow | N | |
Cast | Y | FP32, FP16, INT32, INT8, UINT8, BOOL |
Ceil | Y | FP32, FP16 |
Celu | Y | FP32, FP16 |
Clip | Y | FP32, FP16, INT8 |
Compress | N | |
Concat | Y | FP32, FP16, INT32, INT8, BOOL |
ConcatFromSequence | N | |
Constant | Y | FP32, FP16, INT32, INT8, BOOL |
ConstantOfShape | Y | FP32 |
Conv | Y | FP32, FP16, INT8 |
ConvInteger | Y | INT8,INT32 |
ConvTranspose | Y | FP32, FP16, INT8 |
Cos | Y | FP32, FP16 |
Cosh | Y | FP32, FP16 |
CumSum | Y | FP32, FP16 |
DFT | N | |
DepthToSpace | Y | FP32, FP16, INT32 |
DequantizeLinear | Y | INT8 |
Det | N | |
Div | Y | FP32, FP16, INT32 |
Dropout | Y | FP32, FP16 |
DynamicQuantizeLinear | N | |
Einsum | Y | FP32, FP16 |
Elu | Y | FP32, FP16, INT8 |
Equal | Y | FP32, FP16, INT32 |
Erf | Y | FP32, FP16 |
Exp | Y | FP32, FP16 |
Expand | Y | FP32, FP16, INT32, BOOL |
EyeLike | Y | FP32, FP16, INT32, BOOL |
Flatten | Y | FP32, FP16, INT32, BOOL |
Floor | Y | FP32, FP16 |
Gather | Y | FP32, FP16, INT8, INT32, BOOL |
GatherElements | Y | FP32, FP16, INT8, INT32, BOOL |
GatherND | Y | FP32, FP16, INT8, INT32, BOOL |
Gemm | Y | FP32, FP16, INT8 |
GlobalAveragePool | Y | FP32, FP16, INT8 |
GlobalLpPool | Y | FP32, FP16, INT8 |
GlobalMaxPool | Y | FP32, FP16, INT8 |
Greater | Y | FP32, FP16, INT32 |
GreaterOrEqual | Y | FP32, FP16, INT32 |
GridSample | Y | FP32, FP16 |
GroupNormalization | Y | FP32, FP16 |
GRU | Y | FP32, FP16 |
HammingWindow | N | |
HannWindow | N | |
HardSwish | Y | FP32, FP16, INT8 |
HardSigmoid | Y | FP32, FP16, INT8 |
Hardmax | N | |
Identity | Y | FP32, FP16, INT32, INT8, BOOL |
If | Y | FP32, FP16, INT32, BOOL |
ImageScaler | Y | FP32, FP16 |
InstanceNormalization | Y | FP32, FP16 |
IsInf | Y | FP32, FP16 |
IsNaN | Y | FP32, FP16, INT32 |
LayerNormalization | Y | FP32, FP16 |
LeakyRelu | Y | FP32, FP16, INT8 |
Less | Y | FP32, FP16, INT32 |
LessOrEqual | Y | FP32, FP16, INT32 |
Log | Y | FP32, FP16 |
LogSoftmax | Y | FP32, FP16 |
Loop | Y | FP32, FP16, INT32, BOOL |
LRN | Y | FP32, FP16 |
LSTM | Y | FP32, FP16 |
LpNormalization | Y | FP32, FP16 |
LpPool | Y | FP32, FP16, INT8 |
MatMul | Y | FP32, FP16 |
MatMulInteger | Y | INT8,INT32 |
Max | Y | FP32, FP16, INT32 |
MaxPool | Y | FP32, FP16, INT8 |
MaxRoiPool | N | |
MaxUnpool | N | |
Mean | Y | FP32, FP16, INT32 |
MeanVarianceNormalization | Y | FP32, FP16 |
MelWeightMatrix | N | |
Min | Y | FP32, FP16, INT32 |
Mod | Y | FP32, FP16, INT32 |
Mul | Y | FP32, FP16, INT32 |
Multinomial | Y | FP32, FP16, INT32 |
Neg | Y | FP32, FP16, INT32 |
NegativeLogLikelihoodLoss | N | |
NonMaxSuppression | Y | FP32, FP16 |
NonZero | Y | FP32, FP16 |
Not | Y | BOOL |
OneHot | Y | FP32, FP16, INT32, BOOL |
Optional | N | |
OptionalGetElement | N | |
OptionalHasElement | N | |
Or | Y | BOOL |
Pad | Y | FP32, FP16, INT8, INT32 |
ParametricSoftplus | Y | FP32, FP16, INT8 |
Pow | Y | FP32, FP16 |
PRelu | Y | FP32, FP16, INT8 |
QLinearConv | N | |
QLinearMatMul | N | |
QuantizeLinear | Y | FP32, FP16 |
RandomNormal | Y | FP32, FP16 |
RandomNormalLike | Y | FP32, FP16 |
RandomUniform | Y | FP32, FP16 |
RandomUniformLike | Y | FP32, FP16 |
Range | Y | FP32, FP16, INT32 |
Reciprocal | Y | FP32, FP16 |
ReduceL1 | Y | FP32, FP16 |
ReduceL2 | Y | FP32, FP16 |
ReduceLogSum | Y | FP32, FP16 |
ReduceLogSumExp | Y | FP32, FP16 |
ReduceMax | Y | FP32, FP16 |
ReduceMean | Y | FP32, FP16 |
ReduceMin | Y | FP32, FP16 |
ReduceProd | Y | FP32, FP16 |
ReduceSum | Y | FP32, FP16 |
ReduceSumSquare | Y | FP32, FP16 |
Relu | Y | FP32, FP16, INT8 |
Reshape | Y | FP32, FP16, INT32, INT8, BOOL |
Resize | Y | FP32, FP16 |
ReverseSequence | Y | FP32, FP16, INT32, INT8, BOOL |
RNN | Y | FP32, FP16 |
RoiAlign | Y | FP32, FP16 |
Round | Y | FP32, FP16, INT8 |
STFT | N | |
ScaledTanh | Y | FP32, FP16, INT8 |
Scan | Y | FP32, FP16 |
Scatter | Y | FP32, FP16, INT8, INT32 |
ScatterElements | Y | FP32, FP16, INT8, INT32 |
ScatterND | Y | FP32, FP16, INT8, INT32 |
Selu | Y | FP32, FP16, INT8 |
SequenceAt | N | |
SequenceConstruct | N | |
SequenceEmpty | N | |
SequenceErase | N | |
SequenceInsert | N | |
SequenceLength | N | |
SequenceMap | N | |
Shape | Y | FP32, FP16, INT32, INT8, BOOL |
Shrink | Y | FP32, FP16, INT32 |
Sigmoid | Y | FP32, FP16, INT8 |
Sign | Y | FP32, FP16, INT8, INT32 |
Sin | Y | FP32, FP16 |
Sinh | Y | FP32, FP16 |
Size | Y | FP32, FP16, INT32, INT8, BOOL |
Slice | Y | FP32, FP16, INT32, INT8, BOOL |
Softmax | Y | FP32, FP16 |
SoftmaxCrossEntropyLoss | N | |
Softplus | Y | FP32, FP16, INT8 |
Softsign | Y | FP32, FP16, INT8 |
SpaceToDepth | Y | FP32, FP16, INT32 |
Split | Y | FP32, FP16, INT32, BOOL |
SplitToSequence | N | |
Sqrt | Y | FP32, FP16 |
Squeeze | Y | FP32, FP16, INT32, INT8, BOOL |
StringNormalizer | N | |
Sub | Y | FP32, FP16, INT32 |
Sum | Y | FP32, FP16, INT32 |
Tan | Y | FP32, FP16 |
Tanh | Y | FP32, FP16, INT8 |
TfIdfVectorizer | N | |
ThresholdedRelu | Y | FP32, FP16, INT8 |
Tile | Y | FP32, FP16, INT32, BOOL |
TopK | Y | FP32, FP16, INT32 |
Transpose | Y | FP32, FP16, INT32, INT8, BOOL |
Trilu | Y | FP32, FP16, INT32, INT8, BOOL |
Unique | N | |
Unsqueeze | Y | FP32, FP16, INT32, INT8, BOOL |
Upsample | Y | FP32, FP16 |
Where | Y | FP32, FP16, INT32, BOOL |
Xor | Y | BOOL |
1.5 支持的模型
目前 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等
- GPT:GPT2等
以下是已测试通过的模型列表
支持的模型 |
---|
AlexNet |
VGG16,VGG19 |
GoogLeNet,InceptionV3 |
ResNet50 |
DenseNet |
ShuffleNet |
SqueezeNet |
MobileNetV1,MobileNetV2,MobileNetV3 |
EfficientNet-B3,EfficientNet-B5,EfficientNet-B7 |
FasterRCNN |
MTCNN |
SSD-VGG16 |
RetinaNet |
RetinaFace |
YOLOV3,YOLOV4,YOLOV5,YOLOV7,YOLOV8 |
FCN |
UNet |
MaskRCNN |
DBNet |
EAST |
CRNN |
Vision Transformer(ViT) |
Swin Transformer |
BERT |
GPT2 |
1.6 示例程序
MIGraphX提供了丰富的示例程序帮助用户更快的部署深度学习模型,示例程序可以在ModelZoo中下载,在搜索框中通过"MIGraphX"关键字可以搜索MIGraphX相关的示例程序。
2 安装MIGraphX
2.1 使用DTK中的MIGraphX
2.1.1 安装DTK
有两种方式安装DTK:
- 使用DTK镜像,镜像下载地址:光源 Find source, find chance.
- 使用DTK安装包,安装包下载地址:https://cancon.hpccube.com:65024/1/main, 将下载好的安装包解压到/opt目录,解压后的目录结构应为:/opt/dtk-xx.xx,最后创建一个软连接/opt/dtk指向该目录。
2.1.2 设置环境变量
安装好DTK后需要设置环境变量:
source /opt/dtk/env.sh
如果需要在python中使用MIGraphX,还需要设置PYTHONPATH :
export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
2.2 使用MIGraphX镜像
除了可以使用DTK中自带的MIGraphX,还可以通过MIGraphX镜像获取MIGraphX,镜像下载地址:光源 Find source, find chance. , 用户可以根据需要选择合适的镜像,比如需要在Centos7.6中使用DTK23.04.1和MIGraphX4.0.0,可以通过下面的命令下载镜像:
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
3 MIGraphX编程模型
本章简要阐述MIGraphX中的一些基本概念和基本设计思想,希望帮助读者更好的使用和理解MIGraphX。
3.1 shape
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:每一个维度的步长,如果没有指定步长,MIGraphX会根据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::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。
3.2 argument
argument类型用来保存数据,类似Pytorch中的Tensor,常用来保存模型的输入和输出数据。
可以通过如下方式构造一个argument对象:
- argument(const shape& s)
- template argument(shape s, T* d)
第1种方式只需要提供shape就可以,系统会自动申请一段内存,该内存的大小等于shape的bytes()方法返回值的大小。第2种方式除了提供shape之外,还需要提供该argument的数据指针,argument不会自动释放该数据。
argument中常用的成员函数:
-
const shape& get_shape() const 返回数据的形状
-
char* data() const 返回argument的数据,可以通过data()的返回值访问推理结果。
3.3 literal
MIGraphX中使用literal表示常量,比如可以使用literal表示卷积的权重。实际上literal是一种特殊的argument,literal中的值不能修改,而argument中的值可以修改。
可以通过如下方式构造一个literal对象:
- template literal(const shape& s, const std::vector& x)
- template literal(const shape& s, T* x)
- 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中常用的成员函数:
- const shape& get_shape() const 返回常量的形状
- const char* data() const 返回常量的数据指针,注意:不能通过data()返回的指针修改literal的值
3.4 target
target表示支持的硬件平台,目前支持CPU和GPU,在编译模型的时候,需要指定一个target。
3.5 program
MIGraphX中使用program类型表示一个神经网络模型。
program中常用的成员函数:
- void compile(const target& t, compile_options options = compile_options{}) 编译模型。第一个参数t是一个target,第二个参数options表示编译的一些设置,比如可以通过options.device_id设置使用哪一块显卡。
- std::vector< argument > eval(std::unordered_map< std::string, argument > params,const std::vector< std::string > &output_names) const 执行推理并返回推理结果。参数params表示模型的输入数据,params中保存模型每个输入节点对应的输入数据,当offload_copy为false的时候,params也包含输出节点的数据,此时推理的输出结果会保存在params中对应的输出内存中,output_names表示需要输出哪些输出节点,如果不提供该参数,则默认输出所有输出节点,注意这是一个同步的方法。
- std::unordered_map get_inputs() const 返回模型的输入节点信息,每个输入节点包含输入名和输入shape。
- std::unordered_map get_outputs() const 返回模型的输出节点信息,,每个输出节点包含输出名和输出shape。
- std::size_t get_memory_usage() const 返回模型推理需要的显存大小,单位为字节
注意:如果需要在不同的线程中使用MIGraphX推理,不同线程不能共用同一个program对象,每个线程需要单独创建一个program对象执行推理。
3.6 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这个类型表示指令的引用,后续指令如果需要使用该条指令作为输入,可以通过该引用来获取该指令。
3.7 instruction
instruction表示指令,可以通过module中的add_instruction()成员函数添加指令。MIGraphX中的指令相当于ONNX模型中的一个节点或者caffe模型中的一个层。指令由操作符(算子)和操作数组成。
3.8 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中支持视图操作的算子有:
- broadcast
- slice
- transpose
- reshape
这些算子返回原argument的一个视图,下面以slice操作为例,阐述MIGraphX中视图的基本实现原理。
图3.1表示一个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}}。
图3.1:二维数组
现在有一个切片操作(slice),该切片操作参数为:starts=[0,2],ends =[4,5],steps = [1, 1] ,由于MIGraphX中的slice算子是一个视图算子,所以切片操作的结果为原二维数组的一个视图,并与原数据共享内存,该视图表示的数据如图3.2中黄色区域所示:
图3.2:原二维数组的视图
具体实现的时候,视图包含一个数据指针以及该数据的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列的元素,即图3.3中红色元素,该元素在视图中的二维索引index可以表示为[1,0],则在实际内存中的索引为二维索引和步长的内积: indexstrides=1 6 + 0 * 1 =6,则二维索引为[1,0]表示的数据在内存中对应的数据为data_ptr+6,所以可以通过二维索引与步长的内积得到实际的内存索引。
图3.3:索引转换
MIGraphX中部分算子是不支持输入视图的,所以对于这些算子,如果输入的是一个视图,就需要通过contiguous操作将内存变得连续。对于上面slice操作返回的视图,contiguous算子会创建一个新的内存空间,将转换后得到的内存连续的数据保存在新的内存空间中,图3.4展示了转换过程。
图3.4:数据转换过程
从上图可以看出,经过contiguous操作之后,slice算子的输出变得内存连续了,所以contiguous算子的输出的shape可以表示为{migraphx::shape::float_type, {4,3},{3,1}},此时行步长是3而不是之前共享内存时的6了。
3.9 使用MIGraphX进行推理
使用MIGraphX进行推理一般包括下面几个步骤:
- 创建模型
- 编译模型
- 执行推理
3.9.1 创建模型
MIGraphX中主要有两种方式创建模型:
- 加载ONNX模型
- 使用MIGraphX提供的API手动创建模型
加载ONNX模型的方式参考本文档中的第4章,这里主要说明如何通过使用MIGraphX提供的API来创建模型,我们以3.8节视图中的示例为例,该示例中包含了两个算子:slice和contiguous,这里对该示例做如下修改,在slice前面加上一个1*1的卷积算子,这里忽略卷积的偏置,在contiguous算子后面再加入一个flatten算子,MIGraphX中的flatten算子不支持视图作为输入,所以需要在flatten算子前面加上contiguous算子,该模型的输入数据格式为NCHW,并设置N=1,C=1,H=4,W=6。下面的代码说明了如何通过C++ API创建该模型:
migraphx::program CreateNet()
{
// 创建一个模型
migraphx::program net;
// 获取主计算图
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}});
// 添加卷积权重
std::vector<float> weightData(1 * 1 * 1 * 1);
for (int i = 0; i < weightData.size(); ++i)
{
weightData[i] = 1.0;
}
migraphx::shape weightShape{migraphx::shape::float_type, {1, 1, 1, 1}};
migraphx::literal convWeight{weightShape, weightData};
migraphx::instruction_ref convKernel = mainModule->add_literal(convWeight);
// 添加卷积算子
migraphx::instruction_ref conv = mainModule->add_instruction(
migraphx::make_json_op("convolution", "{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"),
input,
convKernel);
// 添加slice算子
migraphx::instruction_ref slice = mainModule->add_instruction(
migraphx::make_json_op("slice", "{axes:[2,3],starts:[0,2],ends:[4,5]}"),
conv);
// 添加contiguous算子
migraphx::instruction_ref contiguous = mainModule->add_instruction(migraphx::make_op("contiguous"), slice);
// 添加flatten算子
migraphx::instruction_ref flatten = mainModule->add_instruction(migraphx::make_op("flatten"), contiguous);
// 添加return
mainModule->add_return({flatten});
return net;
}
- 首先通过net.get_main_module()获取主计算图,并通过module的add_parameter()方法添加模型的输入,本示例模型的输入为一个{1,1,4,6}的矩阵,add_parameter()的返回值类型是migraphx::instruction_ref,instruction_ref表示指令的引用。
- 添加好模型的输入后,就可以添加卷积算子了,首先创建卷积的权重,为了便于说明,本示例使用1*1的卷积,且权重都设置为1,通过migraphx::literal convWeight{weightShape,weightData}创建好权重后,通过add_literal()方法将权重添加到模型中。
- 创建好权重后,使用add_instruction()方法创建卷积指令,该方法的第一个参数为卷积算子,这里我们通过migraphx::make_json_op()方法创建算子,第二个参数"{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"表示卷积的属性,如果创建的算子没有属性,则可以直接通过migraphx::make_op()方法创建,比如migraphx::make_op("contiguous"),由于卷积算子需要两个输入:输入数据和卷积权重,所以add_instruction()的第二个和第三个参数分别表示这两个输入参数,后面的slice,contiguous和flatten算子的添加方法与卷积算子类似。
- 最后通过add_return()添加结束指令,到这里整个模型就创建完成了。
3.9.2 推理
创建好模型后,我们就可以输入数据进行推理了,完整的C++代码如下:
#include <migraphx/onnx.hpp>
#include <migraphx/make_op.hpp>
#include <migraphx/gpu/target.hpp>
migraphx::program CreateNet()
{
// 创建一个模型
migraphx::program net;
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}});
// 添加卷积权重
std::vector<float> weightData(1 * 1 * 1 * 1);
for (int i = 0; i < weightData.size(); ++i)
{
weightData[i] = 1.0;
}
migraphx::shape weightShape{migraphx::shape::float_type, {1, 1, 1, 1}};
migraphx::literal convWeight{weightShape, weightData};
migraphx::instruction_ref convKernel = mainModule->add_literal(convWeight);
// 添加卷积算子
migraphx::instruction_ref conv = mainModule->add_instruction(
migraphx::make_json_op("convolution", "{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"),
input,
convKernel);
// 添加slice算子
migraphx::instruction_ref slice = mainModule->add_instruction(
migraphx::make_json_op("slice", "{axes:[2,3],starts:[0,2],ends:[4,5]}"),
conv);
// 添加contiguous算子
migraphx::instruction_ref contiguous = mainModule->add_instruction(migraphx::make_op("contiguous"), slice);
// 添加flatten算子
migraphx::instruction_ref flatten = mainModule->add_instruction(migraphx::make_op("flatten"), contiguous);
// 添加return
mainModule->add_return({flatten});
return net;
}
int main(int argc, char *argv[])
{
// 创建模型
migraphx::program net = CreateNet();
// 编译模型
migraphx::compile_options options;
options.device_id = 0; // 设置GPU设备,默认为0号设备
options.offload_copy = true;
net.compile(migraphx::gpu::target{}, options);
// 输入数据
std::vector<float> inputData(1 * 1 * 4 * 6);
for (int i = 0; i < inputData.size(); ++i)
{
inputData[i] = i;
}
migraphx::shape inputShape{migraphx::shape::float_type, {1, 1, 4, 6}};
migraphx::argument data{inputShape, inputData.data()};
std::unordered_map<std::string, migraphx::argument> inputDataMap;
inputDataMap["input"] = data;
// 推理
std::vector<migraphx::argument> results = net.eval(inputDataMap);
// 获取输出节点的属性
migraphx::argument result = results[0]; // 获取第一个输出节点的数据
migraphx::shape outputShape = result.get_shape(); // 输出节点的shape
int numberOfOutput = outputShape.elements(); // 输出节点元素的个数
float *resultData = (float *)result.data(); // 输出节点数据指针
// 打印推理结果
printf("output size:%d\n", numberOfOutput);
printf("result:\n");
for (int i = 0; i < numberOfOutput; ++i)
{
printf("%d,", (int)resultData[i]);
}
printf("\n");
return 0;
}
执行该程序,输出如下:
output size:12
result:
2,3,4,8,9,10,14,15,16,20,21,22,
图3.5:输入数据
下面我们分析一下该程序:
- 首先通过migraphx::program net= CreateNet()创建定义好的模型,然后通过net.compile()方法编译该模型。
- 编译好模型之后就可以输入数据进行推理了,为了便于说明,本示例将输入数据设置为他们在内存中对应的索引值,如图3.5所示,通过migraphx::argument data{inputShape,inputData.data()};可以创建一个保存输入数据的argument ,第一个参数为输入的shape,第二个参数是一个指向输入数据的指针,然后通过一个std::unordered_map类型的对象创建模型的实际输入,inputDataMap表示输入的映射关系,模型的每个输入都需要有一个对应的输入数据,本示例中只有一个输入,对应的名字为input,所以使用inputDataMap["input"]=data就可以创建模型的输入了,最后通过net.eval()执行推理并返回结果。
- 该模型最终的输出是图3.5中黄色区域,通过观察输出值我们可以发现输出结果是对的。
- 如果想使用FP16推理,可以参考第4章的分类示例。
3.10 MIGraphX中的计算图
计算图是用来表示深度学习网络模型在训练与推理过程中计算逻辑与状态的工具。计算图由基本数据结构张量(Tensor)和基本运算单元算子(Operator)构成。 在计算图中通常使用节点来表示算子,节点间的有向线段来表示张量状态, 同时也描述了计算间的依赖关系。 3.9节中CreateNet()函数定义的模型的计算图如下图所示:
图3.6:CreateNet()模型的计算图表示
MIGraphX中可以通过如下方式打印出计算图的文本表示形式:
migraphx::program net;
std::cout << net << std::endl;
MIGraphX中可以打印出两类计算图,未编译的计算图和编译后的计算图。以3.9节中的模型为例,分别打印出该模型的两个计算图:
int main(int argc, char *argv[])
{
// 创建模型
migraphx::program net = CreateNet();
// 打印未编译的计算图
std::cout << net << std::endl;
// 编译模型
migraphx::compile_options options;
options.device_id = 0; // 设置GPU设备,默认为0号设备
options.offload_copy = true;
net.compile(migraphx::gpu::target{}, options);
// 打印编译后的计算图
std::cout << net << std::endl;
// 输入数据
std::vector<float> inputData(1 * 1 * 4 * 6);
for (int i = 0; i < inputData.size(); ++i)
{
inputData[i] = i;
}
migraphx::shape inputShape{migraphx::shape::float_type, {1, 1, 4, 6}};
migraphx::argument data{inputShape, inputData.data()};
std::unordered_map<std::string, migraphx::argument> inputDataMap;
inputDataMap["input"] = data;
// 推理
std::vector<migraphx::argument> results = net.eval(inputDataMap);
// 获取输出节点的属性
migraphx::argument result = results[0]; // 获取第一个输出节点的数据
migraphx::shape outputShape = result.get_shape(); // 输出节点的shape
int numberOfOutput = outputShape.elements(); // 输出节点元素的个数
float *resultData = (float *)result.data(); // 输出节点数据指针
// 打印推理结果
printf("output size:%d\n", numberOfOutput);
printf("result:\n");
for (int i = 0; i < numberOfOutput; ++i)
{
printf("%d,", (int)resultData[i]);
}
printf("\n");
return 0;
}
程序输出如下:
module: "main"
main:@0 = @literal{1} -> float_type, {1, 1, 1, 1}, {1, 1, 1, 1}
input = @param:input -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@2 = convolution[padding={0, 0},stride={1, 1},dilation={1, 1},group=1,padding_mode=0,use_dynamic_same_auto_pad=0](input,main:@0) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@3 = slice[axes={2, 3},starts={0, 2},ends={4, 5}](main:@2) -> float_type, {1, 1, 4, 3}, {24, 24, 6, 1}
main:@4 = contiguous(main:@3) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@5 = flatten[axis=1](main:@4) -> float_type, {1, 12}, {12, 1}
main:@6 = @return(main:@5)
module: "main"
main:@0 = check_context::migraphx::version_1::gpu::context -> float_type, {}, {}
main:@1 = hip::hip_allocate_memory[shape=float_type, {48}, {1},id=main:scratch] -> float_type, {48}, {1}
main:@2 = load[offset=0,end=96](main:@1) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
input = @param:input -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@4 = hip::copy_to_gpu(input,main:@2) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@5 = hip::hip_copy_literal[id=main:@literal:0] -> float_type, {1, 1, 1, 1}, {1, 1, 1, 1}
main:@6 = load[offset=96,end=192](main:@1) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@7 = load[offset=0,end=0](main:@1) -> int8_type, {0}, {1}
main:@8 = gpu::convolution[padding={0, 0, 0, 0},stride={1, 1},dilation={1, 1},group=1,padding_mode=0,solution_id=128](main:@4,main:@5,main:@7,main:@6) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@9 = load[offset=0,end=48](main:@1) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@10 = slice[axes={2, 3},starts={0, 2},ends={4, 5}](main:@8) -> float_type, {1, 1, 4, 3}, {24, 24, 6, 1}
main:@11 = gpu::code_object[code_object=13624,symbol_name=contiguous_kernel,global=12,local=256,](main:@10,main:@9) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@12 = flatten[axis=1](main:@11) -> float_type, {1, 12}, {12, 1}
main:@13 = hip::copy_from_gpu[shape=float_type, {1, 12}, {12, 1},id=hip::copy_from_gpu0](main:@12) -> float_type, {1, 12}, {12, 1}
main:@14 = hip::sync_stream(main:@13) -> float_type, {1, 12}, {12, 1}
main:@15 = @return(main:@14)
output size:12
result:
2,3,4,8,9,10,14,15,16,20,21,22,
首先看一下未编译的计算图:
module: "main"
main:@0 = @literal{1} -> float_type, {1, 1, 1, 1}, {1, 1, 1, 1}
input = @param:input -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@2 = convolution[padding={0, 0},stride={1, 1},dilation={1, 1},group=1,padding_mode=0,use_dynamic_same_auto_pad=0](input,main:@0) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@3 = slice[axes={2, 3},starts={0, 2},ends={4, 5}](main:@2) -> float_type, {1, 1, 4, 3}, {24, 24, 6, 1}
main:@4 = contiguous(main:@3) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@5 = flatten[axis=1](main:@4) -> float_type, {1, 12}, {12, 1}
main:@6 = @return(main:@5)
第一行module: "main"表示主计算图,下面每一行表示该主计算图中的一条指令,下面以卷积算子为例说明每条指令的格式:
图3.7:指令格式
main:@2表示该指令的ID,其中main表示的是MIGraphX中的主计算图,如果模型有其他子图,会使用其他名字来命名。@符号后面的2表示的是该指令的序号,convolution表示的是算子的名称,这里表示的是卷积算子,[ ]里面的内容表示的是算子的属性,算子属性后面的(input,main:@0)表示该指令的输入参数,卷积算子的输入参数包括卷积的输入数据和卷积的权重,->后面的float_type, {1, 1, 4, 6}, {24, 24, 6, 1}表示的是输出的数据类型和shape,其中float_type表示输出的数据类型是float,{1, 1, 4, 6}表示每一维的大小,{1, 1, 4, 6}对应的维度顺序为NCHW,{24, 24, 6, 1}表示的是每一维的步长。
从上面的计算图可以看到由于slice是一个视图算子,其输出结果是convolution算子输出结果的一个视图,所以slice算子的输出shape为float_type, {1, 1, 4, 3}, {24, 24, 6, 1},步长和convolution算子的输出步长一致。但是经过contiguous算子之后,由于内存变得连续了,所以步长为{12, 12, 3, 1}。
MIGraphX中有几个特殊的指令,这些指令的算子名以@开头:
- main:@0 = @literal{1} -> float_type, {1, 1, 1, 1}, {1, 1, 1, 1}表示常量指令,这个常量就是我们创建的卷积权重
- input = @param:input -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}表示模型输入指令
- main:@6 = @return(main:@5)表示模型的结束指令
下面我们再看一下编译后的计算图:
module: "main"
main:@0 = check_context::migraphx::version_1::gpu::context -> float_type, {}, {}
main:@1 = hip::hip_allocate_memory[shape=float_type, {48}, {1},id=main:scratch] -> float_type, {48}, {1}
main:@2 = load[offset=0,end=96](main:@1) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
input = @param:input -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@4 = hip::copy_to_gpu(input,main:@2) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@5 = hip::hip_copy_literal[id=main:@literal:0] -> float_type, {1, 1, 1, 1}, {1, 1, 1, 1}
main:@6 = load[offset=96,end=192](main:@1) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@7 = load[offset=0,end=0](main:@1) -> int8_type, {0}, {1}
main:@8 = gpu::convolution[padding={0, 0, 0, 0},stride={1, 1},dilation={1, 1},group=1,padding_mode=0,solution_id=128](main:@4,main:@5,main:@7,main:@6) -> float_type, {1, 1, 4, 6}, {24, 24, 6, 1}
main:@9 = load[offset=0,end=48](main:@1) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@10 = slice[axes={2, 3},starts={0, 2},ends={4, 5}](main:@8) -> float_type, {1, 1, 4, 3}, {24, 24, 6, 1}
main:@11 = gpu::code_object[code_object=13624,symbol_name=contiguous_kernel,global=12,local=256,](main:@10,main:@9) -> float_type, {1, 1, 4, 3}, {12, 12, 3, 1}
main:@12 = flatten[axis=1](main:@11) -> float_type, {1, 12}, {12, 1}
main:@13 = hip::copy_from_gpu[shape=float_type, {1, 12}, {12, 1},id=hip::copy_from_gpu0](main:@12) -> float_type, {1, 12}, {12, 1}
main:@14 = hip::sync_stream(main:@13) -> float_type, {1, 12}, {12, 1}
main:@15 = @return(main:@14)
我们发现编译后的计算图和未编译的计算图有很多不一样的地方,这是由于MIGraphX在编译计算图的时候,对原始计算图做了很多优化,包括常量传播,算子融合等,所以会导致计算图发生很大的变化,这里我们不对MIGraphX编译细节做过多深入的探讨,只简单说明编译后的计算图的基本结构。
- hip::hip_allocate_memory指令表示内存分配指令,注意该指令分配的内存包含了计算图中所有指令需要使用到的内存,后面每条指令的输出都是通过load指令获取hip::hip_allocate_memory已经分配好的内存。
- hip::copy_to_gpu表示将输入数据拷贝到gpu,由于offload_copy设置为true,所以会将输入数据拷贝到gpu。
- hip::hip_copy_literal表示拷贝常量到gpu中,这里是将卷积权重拷贝到了gpu中。
- 编译后的计算图将原始的convolution转换为了gpu::convolution算子,(main:@4,main:@5,main:@7,main:@6)中main:@4,main:@5,main:@7表示输入,main:@6表示输出,main:@6是load指令,该指令从hip::hip_allocate_memory获取内存并将gpu::convolution算子的输出结果保存到该内存中。
- hip::copy_from_gpu表示将输出结果拷贝到host端,便于访问。
- hip::sync_stream表示流同步,MIGraphX会为每个模型创建一个单独的非阻塞流来执行所有指令,所以需要添加同步。
4 分类示例
本章以ResNet50为例来说明如何通过C++ API加载ONNX模型进行图像分类模型的推理。
4.1 将模型转换为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模型都是使用的该模型。
4.2 使用C++ API进行分类模型的推理
使用C++ API加载ONNX模型进行推理主要包含三个步骤:
- 加载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);
下面以4.1节转好的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类型,每个输入节点或者输出节点都有一个节点名和shape,由于ResNet50模型只有一个输入,所以这里可以通过inputs.begin()获取模型的输入。
- 如果需要采用FP16模式进行推理,可以通过quantize_fp16()函数实现,具体实现见4.3小节,MIGraphX同时也支持int8推理,具体实现见4.4小节。
- 加载onnx模型之后,需要使用compile()方法编译模型,这里将模型编译为GPU模式,如果需要编译为CPU模式,需要使用migraphx::cpu::target{},注意:如果你的输入数据在host端,则在设置编译选项的时候,需要设置offload_copy为true。第6章会说明如何直接使用device端的数据进行推理。
- 编译好模型之后,需要输入数据,输入数据需要经过预处理并转换为NCHW的格式,这里使用了OpenCV的blobFromImage函数将图像转换为了NCHW格式。并通过构造一个std::unordered_map类型的对象保存输入数据,模型的每个输入都会对应一个输入数据,输入数据使用argument保存。
- 最后通过program的eval()方法执行推理计算,推理结果是一个std::vector< migraphx::argument >类型,由于offload_copy参数设置为true,所以推理的结果是host端数据,然后我们就可以通过argument提供的方法访问推理结果了,如果需要指定输出节点,可以设置eval()中的outputNames参数,具体实现参考4.7小节。
- 完整的ResNet50示例程序参考ModelZoo。
4.3 使用FP16推理
本节主要说明如何在MIGraphX中使用FP16进行推理,在MIGraphX中可以通过下面两种方式实现FP16推理:
- 方式1:使用FP32格式的ONNX模型,然后通过调用migraphx::quantize_fp16()实现FP16的推理
- 方式2:先将FP32格式的ONNX模型转换为FP16格式模型,然后使用FP16格式的ONNX模型执行推理
下面是两种方式的具体使用说明。
4.3.1 使用migraphx::quantize_fp16
实现FP16推理可以直接使用FP32格式的ONNX模型,然后在编译前调用migraphx::quantize_fp16()。这种方式的优点是不需要转换模型格式,只需要修改少量代码,推荐使用该方式。具体使用方法如下:
#include <migraphx/quantization.hpp> // FP16头文件
// 使用FP16
migraphx::quantize_fp16(net);
// 编译模型(下面步骤跟FP32推理相同)
...
4.3.2 将模型转换为FP16格式
除了4.3.1中调用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格式的模型报错,如果加载FP16格式的模型报错,可以采用4.3.1中的方式实现FP16的推理。
4.4 使用INT8推理
使用INT8模式进行推理需要用户提供量化校准数据,MIGraphX采用线性量化算法,通过校准数据计算量化参数并生成量化模型。为了保证量化精度,建议使用验证集或者测试集中多个典型的数据作为量化校准数据,如果用户没有提供量化校准数据,MIGraphX会使用默认的量化参数,这样可能会导致严重的精度下降。MIGraphX的INT8量化流程如下:
图4.1:INT8量化流程
使用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);
4.5 使用随机数作为模型输入
有的时候我们希望使用随机数作为模型的输入,MIGraphX提供了生成随机数的函数migraphx::generate_argument,使用方法如下:
migraphx::argument data = migraphx::generate_argument(inputShape);
返回的data就是一个包含随机数的argument,可以作为模型的输入。
4.6 查看推理需要的显存大小
如果需要查看模型推理过程中需要使用的显存大小,可以使用下面的方法:
...
// 编译模型
net.compile(migraphx::gpu::target{},options);
// 查看显存,单位为字节
std::size_t memoryUsage = net.get_memory_usage();
4.7 自定义输出节点
如果想要指定输出节点,可以在eval()方法中通过提供outputNames参数来实现:
...
// 推理
std::vector<std::string> outputNames = {"output1","output2","output3"}; // 设置输出节点名
std::vector<migraphx::argument> results = net.eval(inputData,outputNames);
...
如果没有指定outputName参数,则默认输出所有输出节点,此时输出节点的顺序与ONNX中输出节点顺序保持一致,可以通过netron查看ONNX文件的输出节点的顺序。
5 在Python中使用MIGraphX
第4章中我们知道了如何使用C++ API进行分类模型的推理,本章介绍如何在python中使用MIGraphX。
5.1 设置环境变量
将MIGraphX库路径加入PYTHONPATH:
export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
5.2 在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。
5.3 在Python中使用FP16推理
如果需要在python中使用FP16进行推理,只需要在编译前面加上如下语句即可:
# 使用FP16
migraphx.quantize_fp16(model)
# 编译模型
...
5.4 在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)
6 使用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 。
Python示例程序
下面的示例说明了如何在Python中直接使用device数据做推理。
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
def AllocateOutputMemory(model):
outputData={}
for key in model.get_outputs().keys():
outputData[key] = migraphx.allocate_gpu(s=model.get_outputs()[key])
return outputData
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"),offload_copy=False,device_id=0)
# 为输出节点分配device内存,用于保存输出数据
modelData=AllocateOutputMemory(model)
# 预处理并转换为NCHW
pathOfImage ="Test.jpg"
image = ReadImage(pathOfImage,inputShape)
# 将输入数据转换为device数据作为输入数据
modelData[inputName]=migraphx.to_gpu(migraphx.argument(image))
# 推理
results = model.run(modelData)
# 获取输出节点属性
result=migraphx.from_gpu(results[0]) # 将第一个输出节点的数据拷贝到host端,migraphx.argument类型
outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型
outputSize=outputShape.lens() # 每一维大小,维度顺序为(N,C,H,W),list类型
numberOfOutput=outputShape.elements() # 输出节点元素的个数
# 转换为numpy
result = np.array(result)
print(result)
7 模型序列化
由于MIGraphX执行推理之前,需要对模型进行编译,编译过程是非常耗时的,特别是对于复杂的模型,如果第一次编译好模型之后能将编译好的模型进行序列化并保存到⽂件系统中,下次启动的时候直接加载就可以大大减少启动时间,MIGraphX中提供了save和load两个函数来实现该功能。
图7.1:序列化流程
保存编译好的模型:
#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版本是否兼容。如何查看MXR和MIGraphX版本信息可以参考第10章。
使用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 |
8 性能分析
MIGraphX提供了性能分析工具migraphx-driver,该工具在MIGraphX安装目录下的bin文件中。具体使用方法如下:
migraphx-driver perf [--enable-offload-copy] [-h] [--batch] [--input-dim] [-n] [--fp16] [--gpu/--cpu] --onnx/--migraphx
参数说明:
- --enable-offload-copy:设置offload_copy模式,如果输入数据是在device端,则不需要添加该参数
- -h:显示帮助文档
- --batch:batchsize,一般不用手动指定,程序会根据onnx文件中的输入大小自动识别,如果你的onnx文件中没有指定batchsize,则需要设置该选项
- --input-dim:设置模型输入shape,动态shape模型需要通过--input-dim参数来设置最大shape,静态模型不需要设置该参数
- -n:迭代次数,比如-n 10表示迭代10次计算平均耗时,如果没有指定,默认值为100
- --fp16:表示使用FP16模式,如果没有指定,则默认使用FP32模式
- --gpu/--cpu:--gpu表示以gpu模式运行,--cpu表示以cpu模式运行,如果没有指定,默认采用gpu模式
- --onnx:设置onnx模型文件路径
- --migraphx:设置MXR模型文件路径,如果需要对序列化后的MXR文件做性能分析,需要设置--migraphx参数
示例:
对batchsize为32的ResNet50以fp16模式进行性能测试:
/opt/dtk/bin/migraphx-driver perf --enable-offload-copy --fp16 --onnx ./ResNet50.onnx
输出的结果中首先是编译后模型的计算图,计算图的含义参考3.10节:
Compiling ...
Reading: ./resnet50.onnx
module: "main"
main:@0 = check_context::migraphx::version_1::gpu::context -> float_type, {}, {}
main:@1 = hip::hip_allocate_memory[shape=float_type, {38535168}, {1},id=main:scratch] -> float_type, {38535168}, {1}
main:@2 = load[offset=0,end=19267584](main:@1) -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}
input = @param:input -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}
main:@4 = hip::copy_to_gpu(input,main:@2) -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}
main:@5 = hip::hip_copy_literal[id=main:@literal:32] -> half_type, {64, 3, 7, 7}, {147, 49, 7, 1}
main:@6 = load[offset=51380224,end=61014016](main:@1) -> half_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}
main:@7 = gpu::code_object[code_object=13616,symbol_name=convert_kernel,global=1204224,local=256,](main:@4,main:@6) -> half_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}
main:@8 = load[offset=0,end=0](main:@1) -> int8_type, {0}, {1}
main:@9 = load[offset=0,end=51380224](main:@1) -> half_type, {32, 64, 112, 112}, {802816, 12544, 112, 1}
main:@10 = gpu::convolution[padding={3, 3, 3, 3},stride={2, 2},dilation={1, 1},group=1,padding_mode=0,solution_id=124](main:@7,main:@5,main:@8,main:@9) -> half_type, {32, 64, 112, 112}, {802816, 12544, 112, 1}
main:@11 = hip::hip_copy_literal[id=main:@literal:99] -> half_type, {64}, {1}
main:@12 = broadcast[axis=1,out_lens={32, 64, 112, 112}](main:@11) -> half_type, {32, 64, 112, 112}, {0, 1, 0, 0}
...
在计算图后面会显示计算图中每一条指令执行的时间:
Allocating params ...
Running performance report ...
main:@0 = check_context::migraphx::version_1::gpu::context -> float_type, {}, {}: 0.00221422ms, 0.00596443%
main:@1 = hip::hip_allocate_memory[shape=float_type, {38535168}, {1},id=main:scratch] -> float_type, {38535168}, {1}: 0.00177054ms, 0.00476929%
main:@2 = load[offset=0,end=19267584](main:@1) -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}: 0.00172432ms, 0.00464479%
input = @param:input -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}: 0.00196368ms, 0.00528955%
main:@4 = hip::copy_to_gpu(input,main:@2) -> float_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}: 2.56138ms, 6.89958%
main:@5 = hip::hip_copy_literal[id=main:@literal:32] -> half_type, {64, 3, 7, 7}, {147, 49, 7, 1}: 0.00624828ms, 0.0168309%
main:@6 = load[offset=51380224,end=61014016](main:@1) -> half_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}: 0.00174752ms, 0.00470728%
main:@7 = gpu::code_object[code_object=13616,symbol_name=convert_kernel,global=1204224,local=256,](main:@4,main:@6) -> half_type, {32, 3, 224, 224}, {150528, 50176, 224, 1}: 0.0660672ms, 0.177965%
main:@8 = load[offset=0,end=0](main:@1) -> int8_type, {0}, {1}: 0.00208836ms, 0.0056254%
main:@9 = load[offset=0,end=51380224](main:@1) -> half_type, {32, 64, 112, 112}, {802816, 12544, 112, 1}: 0.00127286ms, 0.00342869%
main:@10 = gpu::convolution[padding={3, 3, 3, 3},stride={2, 2},dilation={1, 1},group=1,padding_mode=0,solution_id=124](main:@7,main:@5,main:@8,main:@9) -> half_type, {32, 64, 112, 112}, {802816, 12544, 112, 1}: 1.4883ms, 4.00903%
main:@11 = hip::hip_copy_literal[id=main:@literal:99] -> half_type, {64}, {1}: 0.0055109ms, 0.0148447%
main:@12 = broadcast[axis=1,out_lens={32, 64, 112, 112}](main:@11) -> half_type, {32, 64, 112, 112}, {0, 1, 0, 0}: 0.00233432ms, 0.00628794%
...
每条指令的后面会显示该指令的耗时以及占所有指令耗时的百分比,根据每一条指令的执行时间我们可以更加精细的对模型每个算子的性能进行分析。
最后会输出整个模型的性能分析结果:
Summary:
gpu::convolution: 28.9018ms / 49 = 0.589832ms, 77.8525%
hip::copy_to_gpu: 2.56138ms / 1 = 2.56138ms, 6.89958%
gpu::code_object::add_relu_kernel: 2.23548ms / 37 = 0.0604184ms, 6.02169%
gpu::code_object::add_add_relu_kernel: 1.58218ms / 12 = 0.131848ms, 4.26189%
gpu::code_object::concat_kernel: 0.372519ms / 4 = 0.0931298ms, 1.00345%
hip::hip_copy_literal: 0.333883ms / 100 = 0.00333883ms, 0.899379%
load: 0.282893ms / 157 = 0.00180186ms, 0.762026%
gpu::pooling: 0.255849ms / 1 = 0.255849ms, 0.689178%
gpu::code_object::reduce_kernel: 0.228384ms / 1 = 0.228384ms, 0.615197%
gpu::gemm: 0.134929ms / 1 = 0.134929ms, 0.363458%
broadcast: 0.0934075ms / 49 = 0.00190628ms, 0.251611%
gpu::code_object::convert_kernel: 0.0660672ms / 1 = 0.0660672ms, 0.177965%
hip::copy_from_gpu: 0.0353791ms / 1 = 0.0353791ms, 0.0953003%
gpu::code_object::add_convert_kernel: 0.0202363ms / 1 = 0.0202363ms, 0.0545102%
step: 0.00539336ms / 3 = 0.00179779ms, 0.0145281%
reshape: 0.00331784ms / 2 = 0.00165892ms, 0.00893724%
hip::sync_stream: 0.00318896ms / 1 = 0.00318896ms, 0.00859008%
check_context::migraphx::version_1::gpu::context: 0.00221422ms / 1 = 0.00221422ms, 0.00596443%
@param: 0.00196368ms / 1 = 0.00196368ms, 0.00528955%
hip::hip_allocate_memory: 0.00177054ms / 1 = 0.00177054ms, 0.00476929%
flatten: 0.0015631ms / 1 = 0.0015631ms, 0.00421051%
Batch size: 32
Rate: 975.796/sec
Total time: 32.7937ms
Total instructions time: 37.1238ms
Overhead time: 0.189906ms, -4.33002ms
Overhead: 1%, -13%
每项具体含义:
-
Summary后面表示每个算子的耗时,比如gpu::convolution: 28.9018ms / 49 = 0.589832ms, 77.8525%,表示卷积算子的耗时,其中:
- 28.9018ms表示模型中所有卷积算子的耗时(n次求平均)
- 49表示模型中卷积算子的调用次数
- 0.589832ms表示每个卷积算子的平均耗时
- 77.8525%表示卷积算子的耗时占total instructions time的比例,这里为28.9018/37.1238=77.8525%
- Summary中的hip::copy_to_gpu表示数据从Host拷贝到Device的耗时,hip::copy_from_gpu表示数据从Device拷贝到Host的耗时。
- Total time表示整个模型的耗时(n次求平均)。
- Rate表示每秒处理的数据量,Rate=1000.0/total time*batchsize,这里表示每秒可以处理975张图片。
- Total instructions time是每个算子耗时的求和,由于在计算每个算子的耗时的时候加入了同步,所以total instructions time是要大于total time的。
- Overhead time表示的是额外开销,是除了算子执行之外的耗时,比如遍历计算图的耗时,这里注意overhead time后面有两个数值,前面一个表示实际测试出来的额外开销,后面的表示total time - total instruction time,由于 total instruction time大于total time所以会产生负值。overhead表示额外的开销占据total time的比例。
- 通常使用total time或者rate来表示模型的实际推理性能。
注意:对于动态shape模型,需要通过--input-dim参数来设置最大shape:
/opt/dtk/bin/migraphx-driver perf --enable-offload-copy --input-dim @input 64 3 224 224 --fp16 --onnx ./ResNet50_Dynamic.onnx
其中@input表示ONNX的输入节点名,后面的64 3 224 224表示输入shape,格式为NCHW。注意:命令行中所有参数中间以空格分隔。
9 动态shape
在实际业务中,我们会遇到动态shape模型,即有多种输入shape的模型,比如CV领域的目标检测模型YOLO以及NLP领域的GPT模型,MIGraphX支持动态shape推理,MIGraphX的动态shape使用方式与静态shape基本一致,动态推理只需要在静态程序基础上设置一个最大输入shape。
注意:
- 从3.2.0版本开始,动态shape不再需要设置环境变量MIGRAPHX_DYNAMIC_SHAPE了。
9.1 动态shape示例
本示例使用第4章中的ResNet50模型说明动态shape模型的基本运行流程。
9.1.1 生成动态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可变的模型。
9.1.2 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。
9.1.3 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)
9.2 动态shape的限制
- 目前MIGraphX只支持shape维度大小的动态,不支持维度个数的动态,比如无法处理从3维数据到4维数据的推理
- 目前MIGraphX的动态推理对if语句和Loop语句支持不完善,设计模型结构的时候尽量少用或者不用这些语句
9.3 支持的动态模型
下表为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对动态shape的支持还不完善,建议使用目前MIGraphX能够支持的动态模型,如果使用了不支持的动态模型导致动态推理失败,可以考虑转换为静态推理的方式,具体实现可以参考9.4小节。
9.4 不支持动态shape的解决方案
如果MIGraphX不能支持某个模型的动态推理,可以将模型转换为静态推理,参考方案:
- 将输入图像resize到一个固定大小,这种做法有可能会影响精度
- 将不同大小的图像填充到一个固定大小,可以使用0来填充,比如下图将128x128的图像用0填充到256x256,这种方式对精度影响较小,推荐使用该方式
图9.1:填充图
- 对于NLP类型的动态模型,可以参考图像的处理方式,通过padding的方式来实现静态推理
9.5 动态shape性能优化
相比于静态推理,动态推理的性能通常较差,如果对推理性能有较高的要求,可以将动态推理转换为静态推理,具体转换方式参考9.4节,下表是常用动态模型的动态推理和静态推理性能对比的参考数据,表中数据表示静态加速比,比如2.0表示静态推理的性能是动态推理性能的2倍。
测试环境:Z100,CentOS7,MIGraphX4.1.0, DTK23.10.1,batchsize=1
FP32静态加速比 | FP16静态加速比 | |
---|---|---|
ResNet50 | 1.31 | 1.44 |
YOLOV5s | 1.54 | 1.62 |
YOLOV8s | 1.76 | 1.97 |
DBNet | 2.46 | 2.06 |
CRNN | 1.49 | 1.50 |
BERT | 1.92 | 2.13 |
从上表可以看出,采用静态推理可以显著提升推理性能。
除了可以通过将动态推理转换为静态推理来提升推理性能,还可以从下面几个方面优化动态推理:
- 最大shape不要设置过大,减小最大shape可以降低显存占用,动态推理中MIGraphX会根据你设置的最大shape分配最大显存,如果设置的过大,可能会导致显存溢出,对于CV类模型建议最大shape不超过1024x1024。
- 设计模型结构的时候建议多使用1x1卷积和3x3卷积,少用或者不用分组卷积和深度可分离卷积(depthwise卷积),MIGraphX的动态推理对于包含有分组卷积和深度可分离卷积的模型性能较差。
- 对于卷积神经网络,MIGraphX目前仅针对N(batchsize)维度可变进行了性能优化,如果对于H和W维度没有动态需求,可以只设置N维度动态,这样可以加速推理。
10 migraphx-driver的使用
MIGraphX提供了一个命令行工具migraphx-driver,该工具在MIGraphX安装目录下的bin文件中。第8章性能分析中我们已经使用过该工具,该工具除了可以做性能分析外还可以实现许多其他功能,本章介绍常用的几种功能。
10.1 查看模型的输入输出节点信息
通过下面的命令可以查看模型的输入输出节点信息:
/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
10.2 查看版本信息
通过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
10.3 查看MXR文件的版本
通过设置version命令的--migraphx参数可以查看MXR文件的版本信息,包括MIGraphX版本和MXR版本:
/opt/dtk/bin/migraphx-driver version --migraphx ./ResNet50.mxr
如果MXR版本与当前系统中的MIGraphX版本不兼容,则该MXR文件不能在当前MIGraphX版本中使用。注意,该命令在4.0.0版本以后支持。
10.4 查看支持的ONNX算子
通过下面的命令可以查看当前MIGraphX支持的ONNX算子:
/opt/dtk/bin/migraphx-driver onnx -l
10.5 查看模型的计算图
通过下面的命令可以查看模型的计算图结构:
/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}
...
如果想查看编译后的计算图结构,可以参考第8章中的性能分析,性能分析的输出结果中包含了编译后的计算图。
如果需要查看MXR文件的计算图,则需要设置--migraphx参数:
/opt/dtk/bin/migraphx-driver read --migraphx ./ResNet50.mxr
11 常见问题
11.1 找不到MIGraphX
如果发现程序找不到MIGraphX库,可能是由于没有设置环境变量,需要执行dtk目录下的env.sh设置相关的环境变量:
source /opt/dtk/env.sh
11.2 找不到Python库
在使用MIGraphX的Python接口的时候,如果出现了找不到MIGraphX库,是因为没有设置环境变量PYTHONPATH。需要将MIGraphX库路径加入环境变量中:
export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
11.3 模型转换问题
这里列举了一些常见的由于模型转换问题导致计算结果不正确的示例。
11.3.1 onnx的upsample算子与pytorch不等价
将pytorch模型转换为onnx模型时,onnx的upsample算子与pytorch不等价。
解决方案:1)更新pytorch;2)导出onnx模型时,设置opset_version>=11,代码如下:
torch.onnx.export(model, input, filename, verbose=False,
opset_version=11,...) # or other number greater than 11
11.3.2 batchnorm参数不固定问题
将pytorch模型转换为onnx模型时,没有将pytorch切换到推理模式,导致batchnorm参数不固定。
解决方案:导出onnx模型前,切换pytorch到推理模式,代码如下: torch_model.eval()
or torch_model.train(False)
11.4 argument与cv::Mat之间的转换
在实际使用中会遇到argument与OpenCV的Mat结构之间的转换,下面给出两种数据类型相互转换的参考实现。
cv::Mat转换为migraphx::argument:
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:
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中的数据