ps: 在gpu上推理, 使用onnx模型获得与tensorRT相媲美的推理速度, 是不是很诱人…
一、为什么要用onnxruntime
1.通用,多硬件平台支持。
通过Execution Provider(EP)支持在不同的硬件平台上使用硬件特有的加速库,如通过在GPU上使用 TensorRT EP,在Intel CPU/GPU上使用 OpenVINO EP,在保持代码简洁一致的同时达到与直接使用这些推理库相媲美的速度。
2. 简单,易用。
直接使用onnx 模型,一个字,香…
简而言之: 如果你公司的模型需要部署在不同的硬件平台上,那么请尝试一下onnxruntime。
详情请查看官方文档:onnxruntime-docs
二、c++ 推理
以TensorRT EP为例,
onnxruntime release
推理环境: 按照链接中准备即可, 唯一需要注意的是:根据你自己的cuda/tensorRT版本选择相应的onnxruntime-gpu版本, 其他都是日常操作。
推理示例参考: https://github.com/microsoft/onnxruntime-inference-examples
下载官方release包解压后如下:
代码中: 只需要连接onnxruntime库,包含onnxruntime_cxx_api.h 头文件即可。
#include <iostream>
#include "onnxruntime_cxx_api.h"
#include <google/logging.h>
// tensorRT EP options
void prepare_trt_opt(const OrtApi& api, Ort::SessionOptions& session_options)
{
OrtTensorRTProviderOptionsV2* tensorrt_options = nullptr;
api.CreateTensorRTProviderOptions(&tensorrt_options);
std::unique_ptr<OrtTensorRTProviderOptionsV2, decltype(api.ReleaseTensorRTProviderOptions)> rel_trt_options(
tensorrt_options, api.ReleaseTensorRTProviderOptions);
std::vector<const char*> keys{"device_id", "trt_fp16_enable", "trt_int8_enable", "trt_engine_cache_enable","trt_engine_cache_path"};
std::vector<const char*> values{"0", "1", "0", "1","trt_engine_cache_path"};
api.UpdateTensorRTProviderOptions(rel_trt_options.get(), keys.data(), values.data(), keys.size());
api.SessionOptionsAppendExecutionProvider_TensorRT_V2(static_cast<OrtSessionOptions*>(session_options),
rel_trt_options.get());
}
// cuda EP options
void prepare_cuda_opt(const OrtApi& api, Ort::SessionOptions& session_options)
{
LOG(INFO) << "append_opt: using cuda options";
OrtCUDAProviderOptionsV2* options = nullptr;
api.CreateCUDAProviderOptions(&options);
std::unique_ptr<OrtCUDAProviderOptionsV2, decltype(api.ReleaseCUDAProviderOptions)> rel_cuda_options(
options, api.ReleaseCUDAProviderOptions);
std::vector<const char*> keys{"device_id"};
std::vector<const char*> values{"1"};
api.UpdateCUDAProviderOptions(rel_cuda_options.get(), keys.data(), values.data(), keys.size());
api.SessionOptionsAppendExecutionProvider_CUDA_V2(static_cast<OrtSessionOptions*>(session_options),
rel_cuda_options.get());
}
int main(int argc, char* argv[])
{
std::string model_fie = "./test.onnx";
const auto& api = Ort::GetApi();
// TensorRT EP options
Ort::SessionOptions session_options_;
prepare_trt_opt(api, session_options_); // 如果想使用CPU推理,注释掉该行即可
// Session
static Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "infer"); **//注意这哥们的声明周期不能比sesstion短**
auto session_ = std::move(std::unique_ptr<Ort::Session>(new Ort::Session(env, model_fie.c_str(), session_options_)));
// input tensor
std::vector<Ort::Value> input_tensor_vec;
Ort::AllocatorWithDefaultOptions allocator;
auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
auto input_num_ = session_->GetInputCount();
std::vector<const char*> input_names_;
for (size_t i = 0; i < input_num_; i++) {
std::string input_name = session_->GetInputNameAllocated(i, allocator).get();
input_names_.push_back(input_name.data());
std::cout << "input name :" << input_name << std::endl;
// dims
Ort::TypeInfo type_info = session_->GetInputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
auto input_shape_ = tensor_info.GetShape();
std::cout << "input dims: ";
for (const auto& dim : input_shape_) {
std::cout << dim << " ";
}
std::cout << std::endl;
// dtype
std::cout << "dtype: " << tensor_info.GetElementType() << std::endl;
// input tensor data
void* input_buffer = nullptr; //经过预处理的输入数据拷贝到这个地址就可以
auto ele_cout = tensor_info.GetElementCount();
auto status = cudaMallocHost(&input_buffer, sizeof(float) * tensor_info.GetElementCount());
CHECK(cudaSuccess == status) << "cudaMallocHost err, code=" << status;
auto input_tensor = Ort::Value::CreateTensor<float>(memory_info, (float*)input_buffer,
ele_cout, input_shape_.data(), input_shape_.size());
input_tensor_vec.emplace(input_tensor);
}
// output tensor
std::vector<Ort::Value> output_tensor_vec;
auto output_num_ = session_->GetOutputCount();
std::vector<const char*> output_names_;
for (size_t i = 0; i < output_num_; i++) {
std::string output_name = session_->GetOutputNameAllocated(i, allocator).get();
output_names_.push_back(output_name.data());
std::cout << "output name :" << output_name << std::endl;
// dims
Ort::TypeInfo type_info = session_->GetOutputTypeInfo(i);
auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
auto output_shape_ = tensor_info.GetShape();
std::cout << "output dims: ";
for (const auto& dim : output_shape_) {
std::cout << dim << " ";
}
std::cout << std::endl;
// dtype
std::cout << "dtype: " << tensor_info.GetElementType() << std::endl;
// output tensor data
void* output_buffer = nullptr; //输出地址, 推理后,从这个地址将数据拷贝走即可
auto ele_cout = tensor_info.GetElementCount();
auto status = cudaMallocHost(&output_buffer, sizeof(float) * tensor_info.GetElementCount());
CHECK(cudaSuccess == status) << "cudaMallocHost err, code=" << status;
auto output_tensor = Ort::Value::CreateTensor<float>(memory_info, (float*)output_buffer,
ele_cout, output_shape_.data(), output_shape_.size());
output_tensor_vec.emplace(output_tensor);
}
// 预处理, 向input tensor 地址填充数据 to do
// infer
session_->Run(Ort::RunOptions{nullptr},
input_names_.data(), input_tensor_vec.data(), input_names_.size(),
output_names_.data(), output_tensor_vec.data(), output_names_.size());
// 从output tensor 地址拷贝出数据, 后处理 to do
// clear
for (auto & input:input_tensor_vec) {
cudaFreeHost(input.GetTensorMutableData<uint8_t*>());
}
for (auto & output:output_tensor_vec) {
cudaFreeHost(output.GetTensorMutableData<uint8_t*>());
}
}
三、后记
1. 关于推理速度
看文档介绍,onnxruntime中 类似TensorRT/OpenVINO这种EP,是官方维护的,在推理性能上有保证,另外一些EP例如huaweiCann/TVM 是社区在维护, 有时一些特有的硬件特性在onnxruntime中可能会滞后,性能可能会有一定影响。
其他影响推理速度的因素可以参考: IOBinding
上文代码中展示的是不使用IOBinding模式的, 推理时session_->Run()时, 框架会将数据从CPU拷贝到device上,推理完成后再将设备上的数据拷贝到CPU中, 这种模式下直接benchmark session_->Run()的速度是会比原生tensorRT 慢一些的,而且设备使用率也不是100%, 可以通过IOBinding 来解决这个问题。
使用了IOBinding后, 纯推理速度与原生TensorRT持平。
2. 其他跨平台推理框架
端测的跨平台推理框架国内的还有百度的PaddleLite,通过他们的NNAdapter 支持多种不同的硬件设备, 虽然模型需要转成paddle格式,但工具链做的也挺完善,实操比较简单。感兴趣的可以尝试一下,github 地址: PaddleLite。
3. 后记的后记
查看onnxruntime的文档,在使用tensorRT EP时,并没有找到有关CudaStream、cudaStreamEvent 的相关描述, 也就是说之前我们在使用TensorRT时,可以利用多个Cuda流进行预处理的操作,在使用ORT时是不行的。
这样的话串行执行的pipeline速度, onnxruntime with TRT EP 与tensorRT 不分伯仲,但是业务上,不能利用多流, 性能肯定不如TensorRT(2023.06.24)。
另外,api.SetSessionExecutionMode(session_options, ORT_PARALLEL); 设置模型内部的算子运行模型, 例如计算图中有分支时,两个分支并行计算,实测,对性能也有较大的影响(2023.06.25)。