从零开始 TensorRT(3)Python 篇:解析 ONNX、PyTorch TensorRT 接口

前言

学习资料:
TensorRT 源码示例
官方文档:Working With TensorRT Using The Python API
官方文档:TensorRT Python
官方文档:CUDA Python
B站视频教程
视频配套代码 cookbook

示例:解析 ONNX 模型

参考源码:cookbook → 04-BuildEngineByONNXParser → pyTorch-ONNX-TensorRT

源码

  cookbook 中自定义了一个网络并在 MNIST 数据集上进行训练,然后保存为 ONNX 并用 TensorRT 读取生成引擎并进行推理。这里对代码进行了简化,直接用 Pytorch 提供的训练好的 ResNet18,存为 ONNX 然后推理。
  测试数据使用了 TensorRT 中自带的数据,./TensorRT-8.6.1.6/data/resnet50 下有4张图片以及标签 class_labels.txt。将图像复制到 ./data/images 下,标签复制到 ./data 下即可。

import os
import numpy as np
from PIL import Image

import torch
import torchvision.models as models
import torchvision.transforms as transforms

import tensorrt as trt
from cuda import cudart

bUseFP16Mode = False
bUseINT8Mode = True

h, w = 224, 224
dataPath = 'data/images'
imgFiles = [os.path.join(dataPath, f) for f in os.listdir(dataPath)]
labelFile = 'data/class_labels.txt'
onnxFile = 'resnet18.onnx'
if bUseFP16Mode:
    trtFile = 'fp16.plan'
elif bUseINT8Mode:
    trtFile = 'int8.plan'
else:
    trtFile = 'fp32.plan'

batch_size = len(imgFiles)
with open(labelFile, 'r') as f:
    label = np.array(f.readlines())

# 准备数据
transform = transforms.Compose([
    transforms.Resize((h, w)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])


def load_images(image_paths):
    images = [Image.open(image_path) for image_path in image_paths]
    tensors = [transform(image).unsqueeze(0) for image in images]
    res = torch.cat(tensors, dim=0)
    return res


input_tensor = load_images(imgFiles).cuda()

weights = models.ResNet18_Weights.DEFAULT
model = models.resnet18(weights=weights, progress=False).eval().cuda()

result = model(input_tensor)

print(f'PyTorch results: {label[result.argmax(dim=1).cpu()]}')

# 导出 ONNX
torch.onnx.export(
    model,
    torch.randn(1, 3, h, w, device='cuda'),
    onnxFile,
    input_names=['x'],
    output_names=['y'],
    do_constant_folding=True,
    verbose=True,
    keep_initializers_as_inputs=True,
    opset_version=12,
    dynamic_axes={'x': {0: 'nBatchSize'}, 'y': {0: 'nBatchSize'}}
)


# INT8 模式校准器
class MyCalibrator(trt.IInt8EntropyCalibrator2):

    def __init__(self, data_path, n_calibration, input_shape, cache_file):
        trt.IInt8EntropyCalibrator2.__init__(self)
        self.imageList = [os.path.join(data_path, f) for f in os.listdir(data_path)]
        self.nCalibration = n_calibration
        self.shape = input_shape
        self.bufferSize = trt.volume(input_shape) * trt.float32.itemsize
        self.cacheFile = cache_file
        _, self.dIn = cudart.cudaMalloc(self.bufferSize)
        self.oneBatch = self.batch_generator()

    def __del__(self):
        cudart.cudaFree(self.dIn)

    def batch_generator(self):
        for i in range(self.nCalibration):
            print("> calibration %d" % i)
            sub_images = np.random.choice(self.imageList, self.shape[0], replace=False)
            yield np.ascontiguousarray(load_images(sub_images).numpy())

    def get_batch_size(self):  # necessary API
        return self.shape[0]

    def get_batch(self, nameList=None, inputNodeName=None):  # necessary API
        try:
            data = next(self.oneBatch)
            cudart.cudaMemcpy(self.dIn, data.ctypes.data, self.bufferSize, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice)
            return [int(self.dIn)]
        except StopIteration:
            return None

    def read_calibration_cache(self):  # necessary API
        if os.path.exists(self.cacheFile):
            print("Succeed finding cache file: %s" % self.cacheFile)
            with open(self.cacheFile, "rb") as f:
                cache = f.read()
                return cache
        else:
            print("Failed finding int8 cache!")
            return

    def write_calibration_cache(self, cache):  # necessary API
        with open(self.cacheFile, "wb") as f:
            f.write(cache)
        print("Succeed saving int8 cache!")
        return


# 构建期
logger = trt.Logger(trt.Logger.ERROR)
builder = trt.Builder(logger)
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
profile = builder.create_optimization_profile()
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)
if bUseFP16Mode:
    config.set_flag(trt.BuilderFlag.FP16)
if bUseINT8Mode:
    config.set_flag(trt.BuilderFlag.INT8)
    config.int8_calibrator = MyCalibrator(dataPath, 1, (4, 3, h, w), 'int8.cache')

# 加载 ONNX
parser = trt.OnnxParser(network, logger)
with open(onnxFile, "rb") as model:
    if not parser.parse(model.read()):
        print("Failed parsing .onnx file!")
        for error in range(parser.num_errors):
            print(parser.get_error(error))
        exit()
    print("Succeeded parsing .onnx file!")

inputTensor = network.get_input(0)
profile.set_shape(inputTensor.name, [1, 3, h, w], [4, 3, h, w], [8, 3, h, w])
config.add_optimization_profile(profile)
# 生成序列化网络
engineString = builder.build_serialized_network(network, config)
with open(trtFile, "wb") as f:
    f.write(engineString)

# 运行期
engine = trt.Runtime(logger).deserialize_cuda_engine(engineString)
nIO = engine.num_io_tensors
lTensorName = [engine.get_tensor_name(i) for i in range(nIO)]
nInput = [engine.get_tensor_mode(lTensorName[i]) for i in range(nIO)].count(trt.TensorIOMode.INPUT)

context = engine.create_execution_context()
context.set_input_shape(lTensorName[0], [batch_size, 3, h, w])

inputHost = np.ascontiguousarray(input_tensor.cpu().numpy())
outputHost = np.empty(
    context.get_tensor_shape(lTensorName[1]),
    dtype=trt.nptype(engine.get_tensor_dtype(lTensorName[1]))
)

_, inputDevice = cudart.cudaMalloc(inputHost.nbytes)
_, outputDevice = cudart.cudaMalloc(outputHost.nbytes)
context.set_tensor_address(lTensorName[0], inputDevice)
context.set_tensor_address(lTensorName[1], outputDevice)

cudart.cudaMemcpy(inputDevice, inputHost.ctypes.data, inputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyHostToDevice)
context.execute_async_v3(0)
cudart.cudaMemcpy(outputHost.ctypes.data, outputDevice, outputHost.nbytes, cudart.cudaMemcpyKind.cudaMemcpyDeviceToHost)

print(f'TensorRT results: {label[outputHost.argmax(axis=1)]}')

cudart.cudaFree(inputDevice)
cudart.cudaFree(outputDevice)

代码解析

  代码整体流程就是加载 PyTorch 提供的预训练模型 ResNet-18 并在几张 ImageNet 数据上进行推理,然后把模型保存为 ONNX,最后用 TensorRT 中的 Parser 加载模型进行推理。
  PyTorch 模型的存储路径为 /home/xxx/.cache/torch/hub/checkpoints
  比较陌生的部分在于导出 ONNX 所使用的 API torch.onnx.export(),以及使用 INT8 模式时需要额外编写一个校准器。

(1)导出 ONNX 模型:官方文档
部分参数解释:
do_constant_folding=True 是否进行常量折叠
verbose=True 是否打印详细信息
keep_initializers_as_inputs=True 是否将模型参数作为输入,个人理解是当设为 True 时,模型的参数是不固定的,是输入的一部分,这样可以加载不同参数的模型。而设为 False 时,模型的参数被固定了,但会更有利于优化加速。
dynamic_axes 设定动态轴

(2)INT8 模式下的校准器
config.int8_calibrator = MyCalibrator(dataPath, 1, (4, 3, h, w), 'int8.cache')

  INT8 模式需要确定每个权重张量的量化范围,以便在量化时保持模型的精度。此处算是作弊了,用推理的数据作为校准数据,并且校准时也把 4 张图像作为一个 batch 输入,从而得到的结果相同。若设为 config.int8_calibrator = MyCalibrator(dataPath, 5, (1, 3, h, w), 'int8.cache') 会发现结果有所不同。

  input_shape 对应了输入形状和 batch size。校准器的作用就是从输入的数据集中随机挑选 batch size 个样本,循环校准 n_calibration 次。

踩坑

  最初仅使用 Numpy 做数据预处理:

image = Image.open(imgFile).resize((224, 224))
mean = np.array([0.485, 0.456, 0.406])
std = np.array([0.229, 0.224, 0.225])
image = (np.array(image, dtype=np.float32) / 255 - mean) / std
image = np.expand_dims(image.transpose((2, 0, 1)), axis=0)
input_tensor = torch.from_numpy(image).cuda()

  报错:RuntimeError: Input type (torch.cuda.DoubleTensor) and weight type (torch.cuda.FloatTensor) should be the same
  原因是第 4 行做标准化计算时,会把数据类型自动转变为 float64。若使用 torch.FloatTensor(image) 可解决报错,但是在 TensorRT 推理时使用 inputHost = np.ascontiguousarray(image) 会导致推理结果不同但没有报错,必须使用一样的 float32 数据。这里因为在 Pytorch 上推理了,后续用 input_tensor.cpu().numpy() 很安全,但实际使用会跳过这一步直接在 TensorRT 上推理,要小心数据类型问题。

示例:PyTorch 框架内 TensorRT 接口

参考源码:cookbook → 06-UseFrameworkTRT → Torch-TensorRT
PyTorch 官方示例

源码

  省略加载数据部分,与上个示例相同。测试时发现启用 TorchScript 会让推理速度加快很多。

import torch_tensorrt


TorchScript = True
if TorchScript:
    model = torch.jit.trace(model, torch.randn(batch_size, 3, h, w, device="cuda"))
    
optimized_model = torch_tensorrt.compile(
    model,
    inputs=[torch.randn((batch_size, 3, h, w)).float().cuda()],
    enabled_precisions=torch.float,
    debug=True,
)

optimized_result = optimized_model(input_tensor)
print(f'Torch TensorRT results: {label[optimized_result.argmax(dim=1).cpu()]}')
  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以回答这个问题。首先,需要安装TensorRTONNX Runtime。然后,按照以下步骤进行操作: 1. 使用ONNX将模型导出为ONNX格式。例如,使用以下命令: ```python import torch import onnx from onnxruntime.quantization import QuantType, quantize # 加载 PyTorch 模型 model = torch.load("model.pth") # 将 PyTorch 模型转换为 ONNX 格式 dummy_input = torch.randn(1, 3, 224, 224) input_names = ["input"] output_names = ["output"] onnx_model_path = "model.onnx" torch.onnx.export(model, dummy_input, onnx_model_path, input_names=input_names, output_names=output_names) ``` 2. 使用TensorRTONNX模型转换为TensorRT引擎。例如,使用以下代码: ```python import tensorrt as trt import onnx # 加载 ONNX 模型 onnx_model_path = "model.onnx" onnx_model = onnx.load(onnx_model_path) # 创建 TensorRT 的构建器 TRT_LOGGER = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(TRT_LOGGER) # 设置最大批处理大小和最大工作空间 max_batch_size = 1 max_workspace_size = 1 << 30 builder.max_batch_size = max_batch_size builder.max_workspace_size = max_workspace_size # 创建 TensorRT 的优化器 config = builder.create_builder_config() config.max_workspace_size = max_workspace_size config.set_flag(trt.BuilderFlag.FP16) # 创建 TensorRT 的网络 network = builder.create_network() # 将 ONNX 模型转换为 TensorRT 的网络 parser = trt.OnnxParser(network, TRT_LOGGER) success = parser.parse(onnx_model.SerializeToString()) if not success: print("Failed to parse ONNX model.") exit() # 创建 TensorRT 的引擎 engine = builder.build_cuda_engine(network) # 将 TensorRT 引擎保存到文件 engine_path = "model.engine" with open(engine_path, "wb") as f: f.write(engine.serialize()) ``` 这样就可以将ONNX模型转换为TensorRT引擎,并将其保存到文件中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值