模型转换的主要任务是实现模型在不同框架之间的流转。随着深度学习技术的发展,训练框架和推理框架的功能逐渐分化。训练框架通常侧重于易用性和研究人员的算法设计,提供了分布式训练、自动求导、混合精度等功能,旨在让研究人员能够更快地生成高性能模型。
而推理框架则更专注于针对特定硬件平台的极致优化和加速,以实现模型在生产环境中的快速执行。由于训练框架和推理框架的职能和侧重点不同,且各个框架内部的模型表示方式各异,因此没有一个框架能够完全涵盖所有方面。模型转换成为了必不可少的环节,用于连接训练框架和推理框架,实现模型的顺利转换和部署。
推理引擎是推理系统中用来完成推理功能的模块。推理引擎分为 2 个主要的阶段:
模型转换工具模块有两个部分:
Ⅰ. AI 框架算子的统一
神经网络模型本身包含众多算子,它们的重合度高但不完全相同。推理引擎需要用有限的算子去实现不同框架的算子。
框架 | 导出方式 | 导出成功率 | 算子数(不完全统计) | 冗余度 |
---|---|---|---|---|
Caffe | Caffe | 高 | 52 | 低 |
TensorFlow | I.X | 高 | 1566 | 高 |
Tflite | 中 | 141 | 低 | |
Self | 中 | 1200- | 高 | |
Pytorch | Onnx | 中 | 165 | 低 |
TorchScripts | 高 | 566 | 高 |
不同 AI 框架的算子冲突度非常高,其算子的定义也不太一样,例如 AI 框架 PyTorch 的 Padding 和 TensorFlow 的 Padding,它们 pad 的方式和方向不同。Pytorch 的 Conv 类可以任意指定 padding 步长,而 TensorFlow 的 Conv 类不可以指定 padding 步长,如果有此需求,需要用 tf.pad 类来指定。
一个推理引擎对接多个不同的 AI 框架,因此不可能把每一个 AI 框架的算子都实现一遍,需要推理引擎用有限的算子去对接或者实现不同的 AI 框架训练出来的网络模型。
目前比较好的解决方案是让推理引擎定义属于自己的算子定义和格式,来对接不同 AI 框架的算子层。
Ⅱ. 支持不同框架的模型文件格式
主流的 PyTorch、MindSpore、PaddlePaddle、TensorFlow、Keras 等框架导出的模型文件格式不同,不同的 AI 框架训练出来的网络模型、算子之间是有差异的。同一框架的不同版本间也存在算子的增改。
这些模型文件格式通常包含了网络结构、权重参数、优化器状态等信息,以便于后续的模型部署和推理。以下是一些主流框架的模型文件格式示例:
AI 框架 | 模型文件格式 |
---|---|
PyTorch | .pt, .pth |
MindSpore | .ckpt, .mindir, .air, .onnx |
PaddlePaddle | .pdparams, .pdopt, .pdmodel |
TensorFlow | .pb(Protocol Buffers), .h5(HDF5) |
Keras | .h5, .keras |
要解决这些问题,需要一个推理引擎,能够支持自定义计算图 IR,以便对接不同 AI 框架及其不同版本,将不同框架训练出的模型文件转换成统一的中间表示,然后再进行推理过程,从而实现模型文件格式的统一和跨框架的推理。
Ⅲ. 支持主流网络结构
如 CNN、RNN、Transformer 等不同网络结构有各自擅长的领域,CNN 常用于图像处理(如图像分类、目标检测、语义分割等)、RNN 适合处理序列数据(如时间序列分析、语音识别等)、Transformer 则适用于自然语言处理领域(如机器翻译、文本生成等)。
推理引擎需要有丰富 Demo 和 Benchmark,展示如何使用推理引擎加载和执行不同的网络结构,并通过 Benchmark 来评估推理引擎在处理不同网络结构时的性能,提供主流模型性能和功能基准,来保证推理引擎的可用性。
以英伟达的 TensorRT 为例,TensorRT Demos提供了一些示例,展示了如何使用 TensorRT 优化 Caffe、TensorFlow、DarkNet 和 PyTorch 模型。MLPerf Benchmarks提供了一套全面的基准测试,能够评估不同硬件、软件和服务在机器学习任务上的性能。MLPerf 测试套件包括多种工作负载和场景,如图像分类、自然语言处理、推荐系统、目标检测、医学图像分割等,覆盖了从云端到边缘计算的多样化需求。
Ⅳ. 支持各类输入输出
在神经网络当中有多输入多输出,任意维度的输入输出,动态输入(即输入数据的形状可能在运行时改变),带控制流的模型(即模型中包含条件语句、循环语句等)。
为了解决这些问题,推理引擎需要具备一些特性,比如可扩展性(即能够灵活地适应不同的输入输出形式)和 AI 特性(例如动态形状,即能够处理动态变化的输入形状)。
以 ONNX 为例,要实现 ONNX 模型的动态输入尺寸,首先需要加载原始 ONNX 模型,可以通过 ONNX 提供的 Python API 实现,例如使用onnxruntime.InferenceSession
加载模型。
然后创建输入张量,并将其尺寸设置为想要的动态尺寸。这里的关键是要了解哪些维度是可以动态变化的,哪些维度是固定的。例如,对于图像分类任务,输入图像的高度和宽度可能是可变的,而通道数通常是固定的。可以使用 Python 的 numpy 库创建一个具有动态尺寸的输入张量。
将创建的输入张量传递给 ONNX 运行时库,并调用 InferenceSession
的run
方法进行模型推理。这个方法会接受输入张量,并返回模型的输出张量。这一步会执行模型的前向传播,产生输出结果。
最后使用 ONNX 运行时库获取输出张量并处理结果。输出张量可能包含模型的预测结果或其他相关信息,可以根据具体任务的需要对其进行处理和分析。
以下是一个完整的示例,首先定义一个简单的神经网络模型,并将其导出为动态输入的 ONNX 格式:
import torch
import torch.nn as nn
class Model_Net(nn.Module):
def __init__(self):
super(Model_Net, self).__init__()
self.layer1 = nn.Sequential(
nn.Conv2d(in_channels=3, out_channels=64, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(64),
nn.ReLU(inplace=True),
nn.Conv2d(in_channels=64, out_channels=256, kernel_size=3, stride=1, padding=1),
nn.BatchNorm2d(256),
nn.ReLU(inplace=True),
)
def forward(self, data):
data = self.layer1(data)
return data
if __name__ == "__main__":
# 设置输入参数
Batch_size = 8
Channel = 3
Height = 256
Width = 256
input_data = torch.rand((Batch_size, Channel, Height, Width))
# 实例化模型
model = Model_Net()
# 导出为动态输入
input_name = 'input'
output_name = 'output'
torch.onnx.export(model,
input_data,
"Dynamics_InputNet.onnx",
opset_version=11,
input_names=[input_name],
output_names=[output_name],
dynamic_axes={
input_name: {0: 'batch_size', 2: 'input_height', 3: 'input_width'},
output_name: {0: 'batch_size', 2: 'output_height', 3: 'output_width'}})
接下来测试刚刚保存的 ONNX 模型:
import numpy as np
import onnx
import onnxruntime
# 生成两个随机输入数据
input_data1 = np.random.rand(4, 3, 256, 256).astype(np.float32)
input_data2 = np.random.rand(8, 3, 512, 512).astype(np.float32)
# 导入 ONNX 模型
Onnx_file = "./Dynamics_InputNet.onnx" # 模型文件路径
Model = onnx.load(Onnx_file) # 加载 ONNX 模型
onnx.checker.check_model(Model) # 验证 ONNX 模型是否准确
# 使用 onnxruntime 进行推理
# 创建推理会话
model = onnxruntime.InferenceSession(Onnx_file, providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'])
input_name = model.get_inputs()[0].name # 获取模型输入的名称
output_name = model.get_outputs()[0].name # 获取模型输出的名称
# 对两组输入数据进行推理
output1 = model.run([output_name], {input_name: input_data1}) # 对第一组输入数据进行推理
output2 = model.run([output_name], {input_name: input_data2}) # 对第二组输入数据进行推理
# 打印输出结果的形状
print('output1.shape: ', np.squeeze(np.array(output1), 0).shape) # 打印第一组输入数据的输出结果形状
print('output2.shape: ', np.squeeze(np.array(output2), 0).shape) # 打印第二组输入数据的输出结果形状
得到以下结果:
output1.shape: (4, 256, 256, 256)
output2.shape: (8, 256, 512, 512)
由输出结果可知,动态输入模型可以接受不同形状的输入数据,其输出的形状也会随之变化。
Ⅰ. 结构冗余
神经网络模型中存在的一些无效计算节点(在训练过程中,可能会产生一些在推理时不必要的计算节点)、重复的计算子图(模型的不同部分执行了相同的计算)或相同的结构模块,它们在保留相同计算图语义的情况下可以被无损地移除。
通过计算图优化,采取算子融合(将多个算子合并成一个,例如,将卷积操作和批量归一化操作融合成一个操作,这样可以减少内存带宽消耗并提升计算效率)、算子替换(用更高效的算子替换低效的,例如,使用更高效的矩阵乘法库(如 cuBLAS)替换标准的矩阵乘法算子)、常量折叠(在推理过程中,如果某些算子的输入是常量,可以提前计算这些常量表达式,将结果直接作为输入,减少推理时的计算量)等方法来减少结构冗余。
Ⅱ. 精度冗余
精度冗余是指在神经网络模型中,使用的数值精度(如 FP32 浮点数)可能超出实际需求,导致不必要的计算资源浪费。例如,在某些推理任务中,FP32 精度可能远高于实际需要的精度水平。通过降低数值精度(如使用 FP16 或 INT8),可以显著减少存储和计算成本,而对模型性能的影响微乎其微。
可以通过模型压缩技术来减少模型大小和计算复杂度,同时尽量保持模型的性能:
Ⅲ. 算法冗余
算法冗余指的是在神经网络模型的实现中,算子或者 Kernel 层面的实现算法本身存在计算冗余,比如均值模糊的滑窗与拉普拉斯的滑窗实现方式相同。这种冗余会导致额外的计算开销和资源浪费,影响模型的性能和效率。
推理引擎需要统一算子和计算图表达,针对发现的计算冗余进行统一。下面介绍一些常用的消除算法冗余的方法:
Ⅳ. 读写冗余
读写冗余指的是在计算过程中,存在不必要的内存读写操作,或者内存访问模式低效,导致内存带宽浪费和性能下降。例如重复读写内存(同一数据在计算过程中被多次读写)、内存访问不连续(数据在内存中的布局不连续,导致缓存命中率低,增加了内存访问延迟)、内存对齐不当(数据在内存中的对齐方式不合适,不能充分利用硬件的高效读写特性)。
通过数据排布优化和内存分配优化等方法来减少读写冗余,提高内存访问的效率:
Converter 转换模块由前端转换部分 Frontends 和图优化部分 Graph Optimize 构成。前者 Frontends 负责支持不同的 AI 训练框架;后者 Graph Optimize 通过算子融合、算子替代、布局调整等方式优化计算图。
通过不同的转换器,把不同 AI 框架训练出来的网络模型转换成推理引擎的 IR,再进行后续的优化。优化模块分成三段。
Ⅰ. Pre Optimize:主要进行语法检查和初步的优化,确保计算图在语法和结构上的简洁性和正确性。以下是几种常用的方法:
Ⅱ. Optimize:主要针对计算图中的算子进行优化,以提高执行效率和性能。
z = ReLU(Conv(x, w)) // 合并为一个算子
z = DepthwiseConv(x, w_depth) + PointwiseConv(x, w_point)
Ⅲ. Pos Optimize:主要针对内存和数据访问模式进行优化,以减少读写冗余和提高数据访问效率。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。