Python 模型优化:使用 ONNX 和 TensorRT 加速推理
大家好,今天我们来聊聊如何利用 ONNX 和 TensorRT 这两大利器来优化 Python 模型,显著提升推理速度。在深度学习应用中,模型推理的速度至关重要,尤其是在实时性要求高的场景下,例如自动驾驶、视频分析等。一个经过优化的模型,能够以更低的延迟提供服务,从而改善用户体验,降低硬件成本。
本次讲座主要分为以下几个部分:
- 模型优化背景与必要性: 为什么需要优化模型,以及优化带来的好处。
- ONNX (Open Neural Network Exchange): ONNX 的概念、作用,以及如何将 PyTorch 或 TensorFlow 模型转换为 ONNX 格式。
- TensorRT: TensorRT 的概念、原理,以及如何利用 TensorRT 加速 ONNX 模型。
- 实战案例: 以一个简单的 PyTorch 模型为例,演示如何使用 ONNX 和 TensorRT 进行优化。
- 性能评估与分析: 如何评估优化后的模型性能,并分析影响性能的因素。
- 高级优化技巧: 介绍一些更高级的模型优化技巧,例如量化、层融合等。
1. 模型优化背景与必要性
在深度学习模型的部署过程中,模型推理速度是一个非常重要的指标。一个“笨重”的模型,即使精度很高,也可能因为推理速度慢而无法满足实际应用的需求。以下是一些需要优化模型推理速度的常见场景:
- 实时应用:例如,在自动驾驶系统中,模型需要实时处理来自摄像头和传感器的信息,做出决策。如果模型推理速度慢,可能会导致安全问题。
- 移动设备:在移动设备上运行模型,需要考虑设备的计算能力和电池寿命。优化模型可以降低功耗,延长电池续航时间。
- 高并发请求:在高并发的服务器环境中,快速的模型推理速度可以支持更多的用户请求,降低服务器成本。
模型优化的目标是在保证模型精度不下降的前提下,尽可能地提高推理速度。常见的模型优化方法包括:
- 模型结构优化:例如,使用更轻量级的网络结构,如 MobileNet、ShuffleNet 等。
- 模型压缩:例如,剪枝、量化、蒸馏等。
- 硬件加速:利用 GPU、TPU 等硬件加速器来加速模型推理。
- 编译器优化:使用专门的编译器,如 TensorRT,对模型进行优化,使其更好地利用硬件资源。
ONNX 和 TensorRT 就是实现硬件加速和编译器优化的重要工具。
2. ONNX (Open Neural Network Exchange)
2.1 ONNX 简介
ONNX (Open Neural Network Exchange) 是一种开放的模型表示格式。它的目标是使不同深度学习框架之间可以互相转换模型。例如,你可以使用 PyTorch 训练一个模型,然后将其转换为 ONNX 格式,再导入到 TensorFlow 或其他框架中使用。
2.2 ONNX 的作用
ONNX 的主要作用是:
- 互操作性:允许在不同的深度学习框架之间共享模型。
- 硬件加速:许多硬件加速器,如 TensorRT、OpenVINO 等,都支持 ONNX 格式,可以直接加载 ONNX 模型进行推理。
- 简化部署:使用 ONNX 可以简化模型部署流程,减少对特定框架的依赖。
2.3 将 PyTorch 模型转换为 ONNX 格式
PyTorch 提供了 torch.onnx.export
函数,可以将 PyTorch 模型转换为 ONNX 格式。以下是一个简单的例子:
import torch
import torch.nn as nn
# 定义一个简单的模型
class SimpleModel(nn.Module):
def __init__(self):
super(SimpleModel, self).__init__()
self.linear = nn.Linear(10, 5)
def forward(self, x):
return self.linear(x)
# 创建一个模型实例
model = SimpleModel()
# 创建一个随机输入
dummy_input = torch.randn(1, 10)
# 指定 ONNX 文件的保存路径
onnx_path = "simple_model.onnx"
# 使用 torch.onnx.export 将模型转换为 ONNX 格式
torch.onnx.export(
model,
dummy_input,
onnx_path,
verbose=True, # 打印导出过程的信息
input_names=["input"], # 输入张量的名称
output_names=["output"], # 输出张量的名称
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}}, # 允许动态batch size
opset_version=11 #指定ONNX opset 版本
)
print(f"模型已成功转换为 ONNX 格式,保存到 {onnx_path}")
代码解释:
torch.onnx.export
函数用于将 PyTorch 模型转换为 ONNX 格式。model
是要转换的 PyTorch 模型实例。dummy_input
是一个随机输入,用于指定模型的输入形状。onnx_path
是 ONNX 文件的保存路径。verbose=True
会在控制台打印导出过程的信息,方便调试。input_names
和output_names
分别指定输入和输出张量的名称,这些名称在后续使用 ONNX 模型时会用到。dynamic_axes
允许模型接受动态的 batch size。opset_version
指定 ONNX opset 版本,不同的版本支持不同的算子,需要根据硬件加速器的支持情况选择合适的版本。
2.4 将 TensorFlow 模型转换为 ONNX 格式
TensorFlow 模型转换为 ONNX 格式可以使用 tf2onnx
工具。首先需要安装 tf2onnx
:
pip install tf2onnx
然后,可以使用以下代码将 TensorFlow 模型转换为 ONNX 格式:
import tensorflow as tf
import tf2onnx
# 定义一个简单的 TensorFlow 模型
model = tf.keras.models.Sequential([
tf.keras.layers.Dense(5, activation='relu', input_shape=(10,)),
tf.keras.layers.Dense(1)
])
# 创建一个随机输入
dummy_input = tf.random.normal((1, 10))
# 将 TensorFlow 模型转换为 ONNX 格式
onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=[tf.TensorSpec((None, 10), tf.float32)])
# 保存 ONNX 模型
with open("tensorflow_model.onnx", "wb") as f:
f.write(onnx_model.SerializeToString())
print("TensorFlow模型已转换为ONNX格式,并保存为 tensorflow_model.onnx")
代码解释:
tf2onnx.convert.from_keras
函数用于将 Keras 模型转换为 ONNX 格式。model
是要转换的 Keras 模型实例。input_signature
指定模型的输入形状和数据类型。onnx_model.SerializeToString()
将 ONNX 模型序列化为字符串,然后保存到文件中。
3. TensorRT
3.1 TensorRT 简介
TensorRT 是 NVIDIA 推出的一款高性能深度学习推理优化器和运行时引擎。它可以将深度学习模型转换为高度优化的运行时代码,从而显著提高推理速度。
3.2 TensorRT 的原理
TensorRT 通过以下方式优化模型推理速度:
- 层融合:将多个计算层合并成一个计算层,减少 Kernel Launch 的开销。
- 算子选择:根据硬件特性选择最优的算子实现。
- 精度校准:将模型权重和激活值量化到 INT8 或 FP16 精度,减少计算量和内存占用。
- 自动调优:根据硬件特性自动选择最优的优化策略。
3.3 使用 TensorRT 加速 ONNX 模型
使用 TensorRT 加速 ONNX 模型通常需要以下步骤:
- 创建 TensorRT Builder:用于构建 TensorRT Engine。
- 解析 ONNX 模型:将 ONNX 模型解析为 TensorRT 可以理解的格式。
- 配置 Builder:设置优化选项,如精度模式、最大 Batch Size 等。
- 构建 TensorRT Engine:根据配置构建 TensorRT Engine。
- 序列化 Engine:将构建好的 Engine 序列化到文件,方便后续加载。
- 加载 Engine:从文件加载序列化的 Engine。
- 创建 Execution Context:用于执行推理。
- 分配输入输出 Buffer:为输入输出张量分配内存空间。
- 执行推理:将输入数据复制到输入 Buffer,执行推理,并将结果从输出 Buffer 复制到输出张量。
以下是一个使用 TensorRT 加速 ONNX 模型的例子:
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
import numpy as np
# 定义日志记录器
TRT_LOGGER = trt.Logger()
class HostDeviceMem(object):
def __init__(self, host_mem, device_mem):
self.host = host_mem
self.device = device_mem
def __str__(self):
return "Host:n" + str(self.host) + "nDevice:n" + str(self.device)
def __repr__(self):
return self.__str__()
# 分配输入输出内存
def allocate_buffers(engine, batch_size, data_type):
inputs = []
outputs = []
bindings = []
stream = cuda.Stream()
for binding in engine:
size = trt.volume(engine.get_binding_shape(binding)) * batch_size
host_mem = cuda.pagelocked_empty(size, dtype=trt.nptype(data_type))
device_mem = cuda.mem_alloc(host_mem.nbytes)
inputs.append(HostDeviceMem(host_mem, device_mem)) if engine.binding_is_input(binding) else outputs.append(HostDeviceMem(host_mem, device_mem))
bindings.append(int(device_mem))
return inputs, outputs, bindings, stream
# 将数据从 Host 复制到 Device
def do_inference(context, bindings, inputs, outputs, stream, batch_size):
[cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]
context.execute_async(batch_size=batch_size, bindings=bindings, stream_handle=stream.handle)
[cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]
stream.synchronize()
return [out.host for out in outputs]
def build_engine(onnx_file_path, engine_file_path="", fp16_mode=False, int8_mode=False, calibration_data=None):
"""构建 TensorRT Engine"""
TRT_LOGGER = trt.Logger(trt.Logger.INFO)
builder = trt.Builder(TRT_LOGGER)
config = builder.create_builder_config()
config.max_workspace_size = 1 << 30 # 1GiB
# builder.max_workspace_size = (1 << 30)
# Enable the flag to allow TensorRT to use FP16 precision
if fp16_mode:
config.set_flag(trt.BuilderFlag.FP16)
if int8_mode:
config.set_flag(trt.BuilderFlag.INT8)
config.int8_calibrator = calibration_data # 必须提供校准数据
explicit_batch = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
network = builder.create_network(explicit_batch)
parser = trt.OnnxParser(network, TRT_LOGGER)
with open(onnx_file_path, 'rb') as model:
parser.parse(model.read())
if parser.num_errors > 0:
for i in range(parser.num_errors):
print(parser.get_error(i))
return None
print("构建 TensorRT Engine...")
engine = builder.build_engine(network, config)
if engine is None:
print("构建 TensorRT Engine 失败!")
return None
print("构建 TensorRT Engine 成功!")
# 保存 engine
if engine_file_path != "":
with open(engine_file_path, "wb") as f:
f.write(engine.serialize())
return engine
def load_engine(engine_file_path):
"""加载 TensorRT Engine"""
TRT_LOGGER = trt.Logger(trt.Logger.INFO)
with open(engine_file_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
engine = runtime.deserialize_cuda_engine(f.read())
return engine
if __name__ == "__main__":
# ONNX 模型路径
onnx_file_path = "simple_model.onnx"
# TensorRT Engine 保存路径
engine_file_path = "simple_model.trt"
# 构建 TensorRT Engine (如果不存在)
try:
with open(engine_file_path, "rb") as f:
pass
except FileNotFoundError:
engine = build_engine(onnx_file_path, engine_file_path, fp16_mode=False)
# 加载 TensorRT Engine
engine = load_engine(engine_file_path)
# 创建 Execution Context
context = engine.create_execution_context()
# 分配输入输出 Buffer
batch_size = 1
data_type = trt.float32
inputs, outputs, bindings, stream = allocate_buffers(engine, batch_size, data_type)
# 创建随机输入数据
input_data = np.random.randn(batch_size, 10).astype(np.float32)
# 将输入数据复制到输入 Buffer
np.copyto(inputs[0].host, input_data.ravel())
# 执行推理
output = do_inference(context, bindings, inputs, outputs, stream, batch_size)
# 打印输出结果
print("输出结果:", output[0])
代码解释:
build_engine
函数用于构建 TensorRT Engine。load_engine
函数用于加载 TensorRT Engine。allocate_buffers
函数用于分配输入输出 Buffer。do_inference
函数用于执行推理。fp16_mode
和int8_mode
分别用于启用 FP16 和 INT8 精度模式。calibration_data
用于 INT8 量化校准。
3.4 INT8 量化校准
INT8 量化是一种将模型权重和激活值量化到 INT8 精度,从而减少计算量和内存占用的技术。但是,直接将模型量化到 INT8 精度可能会导致精度下降。为了解决这个问题,需要进行量化校准。
量化校准的步骤如下:
- 准备校准数据集:选择具有代表性的数据集作为校准数据集。
- 运行校准器:使用校准数据集运行校准器,收集模型中每一层的激活值范围。
- 生成校准表:根据收集到的激活值范围生成校准表。
- 构建 INT8 Engine:使用校准表构建 INT8 Engine。
TensorRT 提供了一些内置的校准器,例如 EntropyCalibrator
和 MinMaxCalibrator
。你也可以自定义校准器。
3.5 动态 Shape 支持
TensorRT 支持动态 Shape,允许模型接受不同大小的输入。在构建 TensorRT Engine 时,需要指定输入张量的 Shape 范围。
4. 实战案例
我们将以一个简单的 MNIST 手写数字识别模型为例,演示如何使用 ONNX 和 TensorRT 进行优化。
4.1 定义 MNIST 模型
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torchvision import datasets, transforms
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 10, kernel_size=5)
self.conv2 = nn.Conv2d(10, 20, kernel_size=5)
self.fc1 = nn.Linear(320, 50)
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
x = F.relu(F.max_pool2d(self.conv1(x), 2))
x = F.relu(F.max_pool2d(self.conv2(x), 2))
x = x.view(-1, 320)
x = F.relu(self.fc1(x))
x = self.fc2(x)
return F.log_softmax(x, dim=1)
# 加载 MNIST 数据集
transform = transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,), (0.3081,))
])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
train_loader = torch.utils.data.DataLoader(train_dataset, batch_size=64, shuffle=True)
test_dataset = datasets.MNIST('./data', train=False, download=True, transform=transform)
test_loader = torch.utils.data.DataLoader(test_dataset, batch_size=1000, shuffle=False)
# 创建模型实例
model = Net()
# 定义优化器
optimizer = optim.Adam(model.parameters(), lr=0.001)
# 训练模型
def train(model, device, train_loader, optimizer, epoch):
model.train()
for batch_idx, (data, target) in enumerate(train_loader):
data, target = data.to(device), target.to(device)
optimizer.zero_grad()
output = model(data)
loss = F.nll_loss(output, target)
loss.backward()
optimizer.step()
if batch_idx % 100 == 0:
print('Train Epoch: {} [{}/{} ({:.0f}%)]tLoss: {:.6f}'.format(
epoch, batch_idx * len(data), len(train_loader.dataset),
100. * batch_idx / len(train_loader), loss.item()))
# 测试模型
def test(model, device, test_loader):
model.eval()
test_loss = 0
correct = 0
with torch.no_grad():
for data, target in test_loader:
data, target = data.to(device), target.to(device)
output = model(data)
test_loss += F.nll_loss(output, target, reduction='sum').item() # sum up batch loss
pred = output.argmax(dim=1, keepdim=True) # get the index of the max log-probability
correct += pred.eq(target.view_as(pred)).sum().item()
test_loss /= len(test_loader.dataset)
print('nTest set: Average loss: {:.4f}, Accuracy: {}/{} ({:.0f}%)n'.format(
test_loss, correct, len(test_loader.dataset),
100. * correct / len(test_loader.dataset)))
# 训练和测试模型
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
for epoch in range(1, 3): # 训练 2 个 epoch
train(model, device, train_loader, optimizer, epoch)
test(model, device, test_loader)
# 保存模型
torch.save(model.state_dict(), "mnist_model.pth")
4.2 将 PyTorch 模型转换为 ONNX 格式
import torch
from Net import Net # 假设模型定义在 Net.py 文件中
# 加载模型
model = Net()
model.load_state_dict(torch.load("mnist_model.pth"))
model.eval()
# 创建一个随机输入
dummy_input = torch.randn(1, 1, 28, 28)
# 指定 ONNX 文件的保存路径
onnx_path = "mnist_model.onnx"
# 使用 torch.onnx.export 将模型转换为 ONNX 格式
torch.onnx.export(
model,
dummy_input,
onnx_path,
verbose=True,
input_names=["input"],
output_names=["output"],
dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}},
opset_version=11
)
print(f"模型已成功转换为 ONNX 格式,保存到 {onnx_path}")
4.3 使用 TensorRT 加速 ONNX 模型
参考前面 TensorRT 的例子,将 onnx_file_path
设置为 mnist_model.onnx
,然后构建和加载 TensorRT Engine,执行推理。
5. 性能评估与分析
在完成模型优化后,需要对优化后的模型进行性能评估,以确定优化效果。常用的性能指标包括:
- 推理速度:每秒处理的图像数量 (FPS) 或处理一张图像所需的时间 (Latency)。
- 内存占用:模型在 GPU 或 CPU 上占用的内存大小。
- 精度:模型预测的准确率。
可以使用以下代码来评估模型的推理速度:
import time
# 加载 TensorRT Engine (或原始 PyTorch 模型)
# ...
# 创建随机输入数据
input_data = np.random.randn(batch_size, 1, 28, 28).astype(np.float32)
# 推理次数
num_iterations = 100
# 预热
for _ in range(10):
# 执行推理
# ...
# 计时
start_time = time.time()
for _ in range(num_iterations):
# 执行推理
# ...
end_time = time.time()
# 计算平均推理时间
average_time = (end_time - start_time) / num_iterations
print(f"平均推理时间:{average_time:.4f} 秒")
# 计算 FPS
fps = 1 / average_time
print(f"FPS:{fps:.2f}")
性能分析
影响模型推理速度的因素有很多,包括:
- 模型结构:更复杂的模型通常需要更长的推理时间。
- 硬件设备:GPU 比 CPU 更适合进行深度学习推理。
- Batch Size:增加 Batch Size 可以提高 GPU 的利用率,但也会增加延迟。
- 精度模式:FP16 和 INT8 精度模式可以减少计算量,但可能会导致精度下降。
- TensorRT 配置:合理的 TensorRT 配置可以显著提高推理速度。
6. 高级优化技巧
除了上述介绍的基本优化方法外,还有一些更高级的优化技巧可以进一步提高模型推理速度:
- 量化感知训练 (Quantization Aware Training):在训练过程中模拟量化操作,使模型对量化误差更加鲁棒。
- 剪枝 (Pruning):移除模型中不重要的连接或神经元,减少模型参数量和计算量。
- 知识蒸馏 (Knowledge Distillation):使用一个大型的教师模型来指导训练一个小型学生模型,使学生模型在保证精度的前提下,具有更快的推理速度。
- Custom Layer Plugin:对于 TensorRT 不支持的算子,可以自定义 Layer Plugin,实现硬件加速。
表格:不同优化方法的对比
优化方法 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
模型结构优化 | 简单易行,无需重新训练模型 | 可能导致精度下降 | 对精度要求不高,但对速度要求高的场景 |
量化 | 显著减少计算量和内存占用,提高推理速度 | 可能导致精度下降,需要进行校准或量化感知训练 | 对速度要求高,对精度有一定要求的场景 |
剪枝 | 减少模型参数量和计算量,提高推理速度 | 可能导致精度下降,需要仔细选择剪枝策略 | 对速度和模型大小都有要求的场景 |
知识蒸馏 | 在保证精度的前提下,提高推理速度 | 需要训练教师模型和学生模型,训练成本较高 | 需要一个小型但高性能的模型的场景 |
TensorRT | 自动优化模型,提高推理速度 | 需要 NVIDIA GPU,对模型有一定的限制 | 需要在 NVIDIA GPU 上进行部署,对速度要求高的场景 |
Custom Plugin | 对于 TensorRT 不支持的算子,可以自定义 Plugin,实现硬件加速 | 需要一定的编程能力,开发成本较高 | 模型中包含 TensorRT 不支持的算子,且需要对其进行硬件加速的场景 |
总结:模型优化带来更快速度与更低资源消耗
通过 ONNX 统一模型格式,我们可以利用 TensorRT 对模型进行深度优化,例如层融合和精度校准,从而实现更快的推理速度。 同时,根据实际场景选择合适的优化方法,并结合性能评估,可以达到最佳的优化效果,在部署模型时,兼顾速度与资源占用。