基于 OpenVINO 在 CPU 上部署模型

在实际场景中,深度学习模型运行的机器不总是有 GPU,这时候基于 tensorrt 的部署方案就无法满足要求,本文基于 openvino 压缩量化模型,然后将其部署到 CPU 上,实验证明:推理效果相当、推理耗时接近

1. 模型优化与性能提升

在深度学习模型应用中,模型的大小和复杂度往往与其推理速度和内存占用密切相关。为了在资源有限的环境中部署大型网络,通常需要对模型进行优化。这一优化过程主要包括以下几方面:

  • 量化(Quantization):通过将浮点数向量的精度降低,将模型权重和激活值从 32 位转换为更高效的表示形式(如 8 位或 4 位),从而减少内存占用并加快推理速度。
  • 模型压缩(Model Compression):通过去除冗余的网络结构、迁移学习等技术,削弱不必要的计算步骤,进一步降低模型复杂度。
  • 优化后端(Post-Tuning):针对特定硬件架构(如 CPU)的性能特点,对模型进行微调,使其在资源利用上达到最佳状态。

OpenVINO 框架通过集成了这些技术,为模型优化提供了通用的解决方案,支持多种深度学习模型的部署和推理任务。

2. 基于 ncff 量化模型

NCFF(NNAfter Calibration and Folding)是一种用于对 ONNX 模型进行量化的工具。其核心原理是通过对模型中的神经网络层参数和激活值进行量化,减少数据类型的精度,从而降低计算复杂度和内存占用

NCFF 量化模型的原理主要包括以下几个步骤:首先,选择适合的量化位数;然后,使用训练数据对量化后的模型进行校准,以找到最佳的量化参数;最后,将原始模型中的神经网络层、权重和激活值转换为量化形式,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
def quantiz_by_onnx(onnx_path,save_dir=''):
# 1. 准备量化需要的校准数据集
train_dataset = DatasetBuilder() # 针对自己数据编写加载逻辑,和训练时加载逻辑一样
train_loader = DataLoader(train_dataset, batch_size=1, shuffle=True)

# 校准时只需要图片数据,不需要标签
def transform_fn(data_item):
img, label, weight, is_ng = data_item
return {'input': img.numpy()}

# 构建校准数据集
calibration_dataset = nncf.Dataset(train_loader, transform_fn)

# 2. 使用nncf量化onnx
print('calibration....')
onnx_model = onnx.load(onnx_path)
quantized_model = nncf.quantize(
model=onnx_model,
calibration_dataset=calibration_dataset
)

# 3. 保存量化模型
quantized_onnx_path=os.path.join(save_dir,os.path.basename(onnx_path))
onnx.save(quantized_model, quantized_onnx_path)
print('quantized_onnx_path:',quantized_onnx_path)

return quantized_onnx_path

3. 基于 openvino 部署量化模型

按照以下步骤基于 openvino 部署量化模型

  1. 模型转换:把 ONNX 文件转换成 OpenVINO 的 XML 格式。这一步应该涉及到 OpenVINO 的 API,比如 ov.convert_model 和 ov.save_model
  2. 编译模型:导入了一些性能优化的设置,比如启用 Hyper-threading 和 CPU 固定,这可能是在准备模型进行推理时的设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 1. onnx->openvino
quantized_openvino_path=quantized_onnx_path.replace('.onnx','.xml')
ov_quantized_model = ov.convert_model(quantized_onnx_path)
ov.save_model(ov_quantized_model, quantized_openvino_path)

# 2. 编译模型
config = {
hints.performance_mode: hints.PerformanceMode.LATENCY,
hints.enable_hyper_threading(): False,
hints.enable_cpu_pinning(): True}
core = ov.Core()
core.set_property({props.cache_dir: r"E:\MyCode\python-openvino\cache"})
ov_model = core.read_model(model=quantized_openvino_path)

compiled_model = core.compile_model(model=ov_quantized_model, device_name="CPU",config=config)

基于编译好的模型,我们使用代码去测试其推理效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 模型推理
img_path=r'1.png'
img = cv2.imdecode(np.fromfile(img_path, dtype=np.uint8), 1)
img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# 前处理
img = cv2.resize(img, (512,512), interpolation=cv2.INTER_NEAREST)
img_data=img.astype(np.float32)
img_data/=255.0
print(img_data.shape)
img_data-=0.5
img_data/=0.5
h,w=img_data.shape[:2]
img_data=img_data.reshape(1,1,h,w)

# 推理
print('infer....')
output_layer = compiled_model.output(0)

iters=100
start_time = time.time() # 获取当前时间
for i in range(iters):
output=compiled_model([img_data])[output_layer]
end_time = time.time() # 获取当前时间
print(f"运行时间:{(end_time - start_time)*1.0/iters}秒")

# 后处理
pred = np.zeros_like(output)
pred[output>0.5]=255
pred=pred[0,0].astype(np.uint8)
result=cv2.addWeighted(img,1.0,pred,0.3,0)
cv2.imwrite('ai.png',result)

4. 在 C++ 部署 openvino 推理过程

基于 openvino 的 cpp 版,在 CPU 上部署模型,关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int OpenvinoRuntime::sync_infer(std::vector<cv::Mat>& frames)
{
for (int si = 0; si < frames.size(); si++)
{
int imgWidth = frames[si].size().width;
int imgHeight = frames[si].size().height;

// 1.前处理
int retn = preprocess(frames[si]);
if (retn < 0)
{
return -1;
}

// 2.推理
try {
const ov::Tensor input_tensor = ov::Tensor(compiled_model.input().get_element_type(), ov::Shape(modelInputShape), (float*)(frames[si].data));
curr_request.set_input_tensor(input_tensor);
curr_request.infer();
}
catch (ov::Exception& e)
{
std::cout << e.what() << endl;
return -2;
}

// 3.后处理
float* detections = curr_request.get_output_tensor().data<float>();
cv::Mat predMat(cv::Size(modelInputWidth, modelInputHeight), CV_32F, detections);
retn = postprocess(frames[si],predMat, imgWidth, imgHeight,si);
if (retn < 0)
{
return -3;
}
}

return 0;
}

针对 openvino 提供的批处理及异步推理,相应还编写以下推理方法

1
2
3
4
5
int sync_infer(std::vector<cv::Mat>& frames);
int sync_infer_batch(std::vector<cv::Mat>& frames);
int async_infer(std::vector<cv::Mat>& frames);
int sync_infer_preprocess(std::vector<cv::Mat>& frames);
int async_infer_preprocess(std::vector<cv::Mat>& frames);

除此之外,将以上的 C 代码编译为 dll,并使用 CLR 封装其接口,以便供 C 或者 C# 使用

5. 效果分析

以下统计在不同软硬件平台部署模型的耗时,可以看出在 CPU 部署模型时,openvino 部署方式更快,已经接近在 GPU 部署

软件硬件耗时备注
onnxruntimeCPU1000ms
openvinoCPU30ms输入图片 (512x512)
openvinoCPU12ms输入图片 (320x512)
tensorrtGPU8ms输入图片 (320x512)

总结

本文基于 openvino 构建了一个 onnx 模型的部署流程,其耗时和 GPU 部署接近,并最终编译为 dll,提供到不同的软件开发平台使用。

使用 openvino 丰富了模型的应用场景,从传统的要求使用 GPU,推广到 CPU,降低了模型应用的成本。