使用MIGraphX进行推理一般包括下面几个步骤:
- 创建模型
- 低精度优化
- 编译
- 执行推理,并返回结果
创建模型
MIGraphX 支持两种方式创建模型:加载 ONNX 模型和使用 API 手动创建。
ONNX 模型
首先要将常用的权重模型文件转换onnx格式,下面展示了常见的pytorch框架转向onnx。
转换
import torch
import torchvision
# 模型文件
# https://download.pytorch.org/models/resnet50-19c8e357.pth
pathOfModel = "resnet50-19c8e357.pth"
# 创建 PyTorch ResNet50 模型实例
net = torchvision.models.resnet50(pretrained=False)
# 定义一个 PyTorch 张量来模拟输入数据
input_data = torch.randn(32,3,224,224)
# 将模型转换为 ONNX 格式
output_path = "resnet50.onnx"
net.load_state_dict(torch.load(pathOfModel))
net.eval()
torch.onnx.export(net, input_data, output_path,
input_names=["input"])
加载模型
加载头文件:#include <migraphx/onnx.hpp>
// 加载模型
migraphx::program net= migraphx::parse_onnx("resnet50.onnx");
这种办法十分方便。加载好模型之后,可以通过program的get_parameter_shapes()函数获取网络的输入属性。
低精度优化
加载头文件#include <migraphx/quantization.hpp>
。
如果需要采用FP16模式进行推理,可以通过migraphx::quantize_fp16(net);
函数实现,如果没有设置,则默认采用FP32模式。MIGraphX同时也支持int8推理,我们会在后面讲到如何使用int8模式。
编译
加载头文件:#include <migraphx/gpu/target.hpp>
加载onnx模型之后,需要使用net.compile(migraphx::gpu::target{},options)
方法编译模型。这里将模型编译为GPU模式,如果需要编译为CPU模式,需要使用migraphx::cpu::target{}
。
推理
- 编译好模型之后,需要输入数据,输入数据需要经过预处理并转换为NCHW的格式。
- 通过net的
eval()
方法执行推理计算,eval()
方法执行完成之后,会返回推理结果,推理结果是一个std::vector<argument>
类型,推理的结果是host端数据,然后我们就可以通过argument提供的成员函数去访问推理结果了。完整代码
增加了softmax的计算,参考了附录2的代码
#include <string>
#include <vector>
#include <migraphx/onnx.hpp>
#include <migraphx/gpu/target.hpp>
#include <migraphx/quantization.hpp>
#include <opencv2/opencv.hpp>
using namespace std;
using namespace cv;
using namespace cv::dnn;
using namespace migraphx;
typedef struct _ResultOfPrediction
{
float confidence;
int label;
_ResultOfPrediction():confidence(0.0f),label(0){}
}ResultOfPrediction;
std::vector<float> ComputeSoftmax(const std::vector<float>& results)
{
// 计算最大值
float maxValue=-3.40e+38F; // min negative value
for(int i=0;i<results.size();++i)
{
if(results[i]>maxValue)
{
maxValue=results[i];
}
}
// 计算每一类的softmax概率
std::vector<float> softmaxResults(results.size());
float sum=0.0;
for(int i=0;i<results.size();++i)
{
softmaxResults[i]= exp((float)(results[i] - maxValue));
sum+=softmaxResults[i];
}
for(int i=0;i<results.size();++i)
{
softmaxResults[i]= softmaxResults[i]/sum;
}
return softmaxResults;
}
int main(int argc, char *argv[])
{
// 加载模型
migraphx::program net= migraphx::parse_onnx("resnet50.onnx");
// 获取模型输入属性
std::pair<std::string, migraphx::shape> inputAttribute=*(net.get_parameter_shapes().begin());
string inputName=inputAttribute.first;
migraphx::shape inputShape=inputAttribute.second;
int N=inputShape.lens()[0];
int C=inputShape.lens()[1];
int H=inputShape.lens()[2];
int W=inputShape.lens()[3];
printf("input name:%s\n",inputName.c_str());
printf("input shape:%d,%d,%d,%d\n",N,C,H,W);
// 使用FP16
migraphx::quantize_fp16(net);
// 编译模型
migraphx::compile_options options;
options.device_id=0;//默认为0号设备
// 注意:如果你的输入数据在host端,则在设置编译选项的时候,需要设置offload_copy为true。
options.offload_copy=true;
net.compile(migraphx::gpu::target{},options);// GPU模式
// 预处理并转换为NCHW
int batchSize=N;
Mat srcImage=imread("Test.jpg");
vector<Mat> srcImages;
for(int i=0;i<batchSize;++i)
{
srcImages.push_back(srcImage);
}
Mat inputBlob;
blobFromImages(srcImages,inputBlob,0.0078125,cv::Size(W,H),cv::Scalar(127.5,127.5,127.5),false,false);
// 输入数据
migraphx::parameter_map 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();// 输出节点数据指针
// 获取推理结果
int numberOfPerImage=numberOfOutput/N; // 每张图像的输出个数
printf("output size:%d\n",numberOfPerImage);
for(int i=0;i<N;++i)
{
printf("==========%d image output=============\n",i);
int startIndex=numberOfPerImage*i;
// 获取每幅图像对应的输出
std::vector<float> logit;
for(int j=0;j<numberOfPerImage;++j)
{
printf("%f,",resultData[startIndex+j]);
logit.push_back(resultData[startIndex+j]);
}
printf("\n");
// 计算softmax
std::vector<float> probs;
probs = ComputeSoftmax(logit);
std::vector<ResultOfPrediction> resultOfPredictions;
for(int j=0;j<numberOfPerImage;++j)
{
ResultOfPrediction prediction;
prediction.label=j;
prediction.confidence=probs[j];
resultOfPredictions.push_back(prediction);
}
// 一个batch中第i幅图像的结果
printf("========== %d result ==========\n",i);
for(int j=0;j<resultOfPredictions.size();++j)
{
ResultOfPrediction prediction=resultOfPredictions[j];
printf("label:%d,confidence:%f\n",prediction.label,prediction.confidence);
}
}
return 0;
}
模型转换注意点
调整模型输入Shape
在加载onnx模型时,如果需要修改模型的输入shape,可以通过onnx_options
参数来实现。
// 设置模型输入shape
migraphx::onnx_options onnx_options;
onnx_options.map_input_dims["input"]={32,3,224,224}
// 加载模型
migraphx::program net= migraphx::parse_onnx("resnet50.onnx",onnx_options);
input表示输入节点名,可以在导出onnx模型时候自定义。
upsample算子不等价
如果遇到ONNX的Upsample算子和PyTorch版本不一致的问题:
更新PyTorch到最新版本。
在导出ONNX模型时,设置opset_version参数为11或更高版本。示例代码如下:
torch.onnx.export(model, input, filename, verbose=False,opset_version=11,...) # or other number greater than 11
batchnorm参数不固定
在将PyTorch模型转换为ONNX模型时,如果PyTorch未切换到推理模式,可能导致BatchNorm参数不固定。解决方案是,在导出ONNX模型前,将PyTorch切换到推理模式。示例代码如下:torch_model.eval()
ortorch_model.train(False)
C++ API
创建模型
创建一个模型:migraphx::program net;
十分简单,但却是一个空网络,因此我们要往里面添加模块。
添加Module
模块的添加要根据MIGraphX的定义,它是一个主计算图,通过migraphx::module *mainModule = net.get_main_module();
获得,然后往主计算图添加子图。
子图可以是输入,可以是权重,可以是算子,几乎要用的,都要通过子图添加。但是这些使用的函数是不一样的,如果不清楚,请看前面发的内容~
注意:创建算子的时候,如果创建的算子没有属性,则可以直接通过 migraphx::make_op()方法创建。 否则需要通过migraphx::make_json_op("convolution","{padding:[0,0],stride:[1,1],dilation:[1,1],group:1,padding_mode:0}"),input,convKernel);
make_json_op函数创建算子。
最后通过add_return()
添加结束指令mainModule->add_return({fflatten});
,到这里整个模型就创建完成了。然后通过net
就可以调用使用了,注意不是module
了,而是net
!
完整代码
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({fflatten});
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; // 设置offload_copy
net.compile(migraphx::gpu::target{},options);// GPU模式
// 输入数据
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()};
migraphx::parameter_map 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();// 输出节点数据指针
for(int i=0;i<numberofoutput;++i) printf("%d,",resultData[i]);
printf("\n");
return 0;
}
//输出:2,3,4,8,9,10,14,155,16,20,21,22
- 通过
migraphx::parameter_map
创建模型的输入,parameter_map
表示输入的映射关系。 data
变量如下所示:NCHW,所以存储的是4行6列的数据。
0 1 |2 3 4| 5
6 7 |8 9 10| 11
12 13 |14 15 16| 17
18 19 |20 21 22| 23
Python API
使用python接口首先设置环境变量:export PYTHONPATH=/opt/dtk/lib:$PYTHONPATH
下面看一下基本使用方法,之后可以直接在这个代码基础上进行修改。
from PIL import Image
import numpy as np
import migraphx
def ReadImage(pathOfImage,inputShape):
resizedImage = Image.open(pathOfImage).resize( (inputShape[3], inputShape[2]) )
srcImage = np.asarray(resizedImage).astype("float32")
# 转换为NCHW
srcImage_NCHW = np.transpose(srcImage, (2, 0, 1))
# 预处理
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
inputData = np.zeros(srcImage_NCHW.shape).astype("float32")
for i in range(srcImage_NCHW.shape[0]):
inputData[i, :, :] = (srcImage_NCHW[i, :, :]/ 255 - mean[i]) / std[i]
# 增加batch维度
imageData = np.expand_dims(inputData, axis=0)
return imageData
def Softmax(x):
return np.exp(x)/sum(np.exp(x))
if __name__ == '__main__':
# 加载模型
model = migraphx.parse_onnx("resnet50.onnx")
inputName=model.get_parameter_names()[0]
inputShape=model.get_parameter_shapes()[inputName].lens()
print("inputName:{0} \ninputShape:{1}".format(inputName,inputShape))
# FP16
migraphx.quantize_fp16(model)
# 编译
model.compile(migraphx.get_target("gpu"),device_id=0)
# 读取图像
pathOfImage ="Test.jpg"
image = ReadImage(pathOfImage,inputShape)
# 推理
results = model.run({
inputName: migraphx.argument(image)})
# 获取输出节点属性
result=results[0] # 获取第一个输出节点的数据,migraphx.argument类型
outputShape=result.get_shape() # 输出节点的shape,migraphx.shape类型
outputSize=outputShape.lens() # 每一维大小,维度顺序为(N,C,H,W),list类型
numberOfOutput=outputShape.elements() # 输出节点元素的个数
# 获取输出结果
resultData=result.tolist() # 输出数据转换为list
result = np.array(resultData)
scores = Softmax(result) # 计算softmax
print(scores)
上面提供了opencv版的读取图片,这里是使用PIL版本:
def ReadImage(pathOfImage, inputShape):
srcImage = cv2.imread(pathOfImage, cv2.IMREAD_COLOR)# numpy类型, HWC# 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]# 复制到batch中的其他图像
for i in range(inputData.shape[0]):
if i != 0:
inputData[i, : , : , : ] = inputData[0, : , : , : ]
return inputData
int8优化
fp16优化migraphx::quantize_fp16(net);
一句话即可,而int8需要经过3步,输入量化校准数据、计算量化参数、生成量化参数。
代码如下:
// 读取校准数据,本示例这里采用OpenCV读取
Mat srcImage=imread("CalibrationData.jpg",1);
std::vector<cv::Mat> srcImages;
for(int i=0;i<inputShape.lens()[0];++i)
{
srcImages.push_back(srcImage);
}
Mat inputBlob;
blobFromImages(srcImages,inputBlob,0.0078125,cv::Size(W,H),cv::Scalar(127.5,127.5,127.5),false,false);
migraphx::parameter_map inputData;
inputData[inputName]= migraphx::argument{inputShape, (float*)inputBlob.data};
// 创建量化数据,这里只使用了一张图像,实际使用时为了提高量化精度,建议使用多张图像创建多个inputData进行量化
std::vector<migraphx::parameter_map> calibrationData = {inputData};
// INT8量化
migraphx::quantize_int8(net, migraphx::gpu::target{}, calibrationData);
为了保证量化精度,建议使用验证集或者测试集中多个典型的数据作为量化校准数据,如果用户没有提供量化校准数据,MIGraphX会使用默认的量化参数,这样可能会导致严重的精度下降。
使用设备数据推理
在某些情况下,输入数据直接位于设备(如 GPU)内存中,可以直接使用设备数据进行推理。
#include <migraphx/gpu/hip.hpp>
// 创建参数映射函数,将程序 `p` 的每个参数转移到 GPU 上。
migraphx::parameter_map CreateParameterMap(migraphx::program &p) {
migraphx::parameter_map parameterMap;
for (std::pair<std::string, migraphx::shape> x : p.get_parameter_shapes()) {
parameterMap[x.first] =
migraphx::gpu::to_gpu(migraphx::generate_argument(x.second));
}
return parameterMap;
}
int main(int argc, char *argv[]) {
// 加载模型...
// 获取模型输入属性...
// 编译模型...
options.offload_copy =false; // 设置offload_copy,这里注意:一定要设置为false!
net.compile(migraphx::gpu::target{}, options); // GPU模式
// 为输出节点分配内存
migraphx::parameter_map parameterMap = CreateParameterMap(net);
// 预处理并转换为NCHW...
// 转换为device数据,这里的inputData中的数据是device数据
migraphx::argument inputData = migraphx::gpu::to_gpu(migraphx::argument{inputShape, (float *)inputBlob.data});
// 这里直接使用device数据作为输入数据,inputData.data()返回的是device地址
parameterMap[inputName] = migraphx::argument{inputShape, inputData.data()};
// 推理...
// 获取输出节点的属性
migraphx::argument result = migraphx::gpu::from_gpu(results[0]); // 将第一个输出节点的数据拷贝到host端
//输出...
}
- 一定要将offload_copy设置为false,这样才可以直接使用device数据。
- 示例中通过
migraphx::gpu::to_gpu
创建一个device数据,并输入到模型中。 - 推理的结果需要通过
migraphx::gpu::from_gpu()
拷贝到host端。
python代码:
import cv2
import numpy as np
import migraphx
import torch
def ReadImage(pathOfImage, inputShape):
...
def CreateParameterMap(model):
parameterMap = {
}
parameter_shapes = model.get_parameter_shapes()
for key in parameter_shapes.keys():
parameterMap[key] =
migraphx.to_gpu(migraphx.generate_argument(s = parameter_shapes[key]))
return parameterMap
if __name__ == '__main__': #加载模型
//...
image = ReadImage(pathOfImage, inputShape)# 转换为gpu tensor
input_tensor = torch.from_numpy(image).to(torch.device("cuda"))
# 使用device数据作为输入数据
parameterMap[inputName] = migraphx.gpudata_to_argument(shape = model.get_parameter_s hapes()[inputName], address = input_tensor.data_ptr())
# 推理
results = model.run(parameterMap)
# 获取输出节点属性
result = migraphx.from_gpu(results[0])# 将第一个输出节点的数据拷贝到host端,migraphx.argument类型
//...
模型序列化
我们看到每次都要编译模型,实际上加载编译好的模型之后不需要再次执行编译操作了,可以直接输入数据执行推理,节省编译时间,加快启动速度,同时使用这种方式还可以一定程度上实现对onnx模型的加密。
注意加载头文件#include <migraphx/load_save.hpp> // 添加save和load头文件
onnx序列化到mxr
// 序列化并保存编译好的模型 migraphx::save(net, "ResNet50.mxr");
mxr反序列化到推理
// 加载编译好的模型
migraphx::file_options options;
options.device_id = 1; // 设置GPU设备,默认为0号设备
migraphx::program net = migraphx::load("ResNet50.mxr", options);
// 获取模型输入属性...
动态Shape
动态shape是在深度学习模型中处理可变输入尺寸的一种技术。这在处理图像数据时尤为重要,因为不同的图像可能有不同的分辨率。利用动态shape,同一个模型可以处理各种尺寸的输入,无需针对每种尺寸重构或调整模型。
在MIGraphX框架中,实现动态shape主要包括以下步骤:
- 设置环境变量:通过设置
export MIGRAPHX_DYNAMIC_SHAPE=1
,启用动态shape功能。 - 定义模型的最大输入尺寸:这个尺寸是模型处理输入数据的上限。如果输入超过这个尺寸,模型会报错。
- 使用reshape方法适配输入尺寸:通过程序(program)类的reshape方法调整模型,使其适应不同的输入尺寸。
伪代码:
以下是动态shape在C++和Python中的具体实现示例:
c++代码:
int main(int argc, char *argv[]) {
// 设置最大输入shape
migraphx::onnx_options onnx_options;
onnx_options.map_input_dims["input"] = {2, 3, 512,
512}; // input表示输入节点名
// 加载模型。。。
// 编译。。。
// 设置动态输入,这里添加了2个不同的输入shape
std::vector<std::vector<std::size_t>> inputShapes;
inputShapes.push_back({2, 3, 16, 16});
inputShapes.push_back({2, 3, 32, 32});
cv::Mat srcImage = cv::imread("Test.jpg", 1);
for (int i = 0; i < inputShapes.size(); ++i) {
// 设置输入shape并执行reshape
std::unordered_map<std::string, std::vector<std::size_t>> inputShapeMap;
inputShapeMap[inputName] = inputShapes[i];
net.reshape(inputShapeMap);
std::vector<cv::Mat> srcImages;
for (int j = 0; j < inputShapes[i][0]; ++j) {
srcImages.push_back(srcImage);
}
// 预处理并转换为NCHW
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);
// 输入数据
// 推理
// 获取输出节点的属性
// 打印输出
}
return 0;
}
python代码:
import cv2
import numpy as np
import migraphx
if __name__ == '__main__':
#设置最大输入shape
maxInput = {
"input": [2, 3, 512, 512]
}
#加载模型..
# 编译..
# 设置动态输入, 这里添加了2个不同的输入shape
inputShapes = [
[2, 3, 16, 16],
[2, 3, 32, 32]
]
for inputShape in inputShapes:
inputShapeMap = {
inputName: inputShape
}
# 执行reshape
model.reshape(inputs = inputShapeMap)
# 预处理并转换为NCHW
# 推理
# 获取输出节点属性
总结:代码中,很多前面的代码如输入数据,推理,打印等等是在for循环里的,因为shape会变,每次的输出也都不一样!
动态shape的限制
全连接层:对于包含全连接层的模型,需要在全连接层之前添加全局池化层以保持C、H、W维度上的一致性。
LSTM模型:在包含LSTM的模型中,batch size必须为1。如果模型不支持动态Shape,可以:
- 将图像调整到固定大小。
- 使用零填充方法,将不同大小的图像填充到统一尺寸。
支持动态Shape的模型
支持N,H,W维度变化的模型 | 仅支持N维度变化的模型 | 仅支持H,W维度变化的模型 |
---|---|---|
AlexNet | ShuffleNet | CRNN-LSTM |
VGG16 | SqueezeNet | |
VGG19 | EfficientNet-B3 | |
GoogLeNet | EfficientNet-B5 | |
InceptionV3 | EfficientNet-B7 | |
ResNet50 | YOLOV2 | |
DenseNet | YOLOV3 | |
MobileNetV1 | YOLOV5 | |
MobileNetV2 | UNet | |
MobileNetV3 | PaddleOCR | |
MTCNN | ||
SSD-VGG16 | ||
RetinaNet | ||
RetinaFace | ||
DBNET | ||
FCN |