Python API 文档#

注意

TensorRT Python API 使 Python 基础开发环境中的开发人员以及希望试验 TensorRT 的人员能够轻松解析模型(例如,来自 ONNX)并生成和运行 PLAN 文件。

本节说明了 Python API 的基本用法,假设您从 ONNX 模型开始。onnx_resnet50.py 示例更详细地说明了此用例。

可以通过 tensorrt 模块访问 Python API

import tensorrt as trt

构建阶段#

要创建构建器,您必须首先创建一个记录器。Python 绑定包含一个简单的记录器实现,该实现将所有先于特定严重性的消息记录到 stdout。它可以像这样使用

logger = trt.Logger(trt.Logger.WARNING)

或者,可以通过派生自 ILogger 类来定义您自己的记录器实现

class MyLogger(trt.ILogger):
    def __init__(self):
       trt.ILogger.__init__(self)

    def log(self, severity, msg):
        pass # Your custom logging implementation here

logger = MyLogger()

然后,您可以创建一个构建器

builder = trt.Builder(logger)

构建引擎旨在作为离线过程,因此可能需要相当长的时间。优化构建器性能 部分提供了有关使构建器运行更快的技巧。

创建网络定义#

创建构建器后,优化模型的第一步是创建网络定义。网络定义选项是使用标志的组合(OR 运算)指定的。

您可以使用 NetworkDefinitionCreationFlag.STRONGLY_TYPED 标志指定应将网络视为强类型。有关更多信息,请参阅 强类型网络 部分。

最后,创建一个网络

network = builder.create_network(flag)

从头开始创建网络定义(高级)#

除了使用解析器之外,您还可以通过网络定义 API 直接向 TensorRT 定义网络。此方案假定每层权重已在主机内存中准备就绪,以便在网络创建期间传递给 TensorRT。

与本节对应的代码可以在 network_api_pytorch_mnist 中找到。

此示例创建了一个简单的网络,其中包含输入、卷积、池化、矩阵乘法、Shuffle、激活和 Softmax 层。此示例使用一个辅助类来保存有关模型的一些元数据

class ModelData(object):
    INPUT_NAME = "data"
    INPUT_SHAPE = (1, 1, 28, 28)
    OUTPUT_NAME = "prob"
    OUTPUT_SIZE = 10
    DTYPE = trt.float32

在此示例中,权重从 PyTorch MNIST 模型导入。

weights = mnist_model.get_weights()

创建记录器、构建器和网络类。

TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
builder = trt.Builder(TRT_LOGGER)
network = builder.create_network(0)

接下来,为网络创建输入张量,指定张量的名称、数据类型和形状。

input_tensor = network.add_input(name=ModelData.INPUT_NAME, dtype=ModelData.DTYPE, shape=ModelData.INPUT_SHAPE)

添加卷积层,指定输入、输出映射的数量、内核形状、权重、偏差和步幅

conv1_w = weights["conv1.weight"].cpu().numpy()
    conv1_b = weights["conv1.bias"].cpu().numpy()
    conv1 = network.add_convolution_nd(
    input=input_tensor, num_output_maps=20, kernel_shape=(5, 5), kernel=conv1_w, bias=conv1_b
    )
    conv1.stride_nd = (1, 1)

添加池化层,指定输入(先前卷积层的输出)、池化类型、窗口大小和步幅

pool1 = network.add_pooling_nd(input=conv1.get_output(0), type=trt.PoolingType.MAX, window_size=(2, 2))
    pool1.stride_nd = trt.Dims2(2, 2)

添加下一对卷积层和池化层

conv2_w = weights["conv2.weight"].cpu().numpy()
conv2_b = weights["conv2.bias"].cpu().numpy()
conv2 = network.add_convolution_nd(pool1.get_output(0), 50, (5, 5), conv2_w, conv2_b)
conv2.stride_nd = (1, 1)

pool2 = network.add_pooling_nd(conv2.get_output(0), trt.PoolingType.MAX, (2, 2))
pool2.stride_nd = trt.Dims2(2, 2)

添加 Shuffle 层以重塑输入,为矩阵乘法做准备

batch = input.shape[0]
mm_inputs = np.prod(input.shape[1:])
input_reshape = net.add_shuffle(input)
input_reshape.reshape_dims = trt.Dims2(batch, mm_inputs)

现在,添加一个矩阵乘法层。模型导出器提供了转置权重,因此指定了 kTRANSPOSE 选项。

auto prob = network->addSoftMax(*relu1->getOutput(0));

添加偏差,它将在批次维度上广播

bias_const = net.add_constant(trt.Dims2(1, nbOutputs), weights["fc1.bias"].numpy())
bias_add = net.add_elementwise(mm.get_output(0), bias_const.get_output(0), trt.ElementWiseOperation.SUM)

添加 ReLU 激活层

relu1 = network.add_activation(input=fc1.get_output(0), type=trt.ActivationType.RELU)

添加最终全连接层,并将此层的输出标记为整个网络的输出

fc2_w = weights['fc2.weight'].numpy()
fc2_b = weights['fc2.bias'].numpy()
fc2 = add_matmul_as_fc(network, relu1.get_output(0), ModelData.OUTPUT_SIZE, fc2_w, fc2_b)

fc2.get_output(0).name = ModelData.OUTPUT_NAME
network.mark_output(tensor=fc2.get_output(0))

现在已完全构建表示 MNIST 模型的网络。有关如何构建引擎并使用此网络运行推理的说明,请参阅构建引擎执行推理 部分。

有关图层的更多信息,请参阅 TensorRT 运算符文档

使用 ONNX 解析器导入模型#

现在,必须从 ONNX 表示形式填充网络定义。您可以创建一个 ONNX 解析器来填充网络,如下所示

parser = trt.OnnxParser(network, logger)

然后,读取模型文件并处理任何错误

success = parser.parse_from_file(model_path)
for idx in range(parser.num_errors):
   print(parser.get_error(idx))

if not success:
    pass # Error handling code here

构建引擎#

下一步是创建一个构建配置,指定 TensorRT 应如何优化模型

config = builder.create_builder_config()

此接口具有许多属性,您可以设置这些属性来控制 TensorRT 如何优化网络。一个重要的属性是最大工作区大小。层实现通常需要一个临时工作区,此参数限制了网络中任何层可以使用的最大大小。如果提供的工作区不足,TensorRT 可能无法为层找到实现。默认情况下,工作区设置为给定设备的全局内存总大小;必要时限制它,例如,当要在单个设备上构建多个引擎时。

config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 20) # 1 MiB

指定配置后,可以构建引擎并使用以下命令序列化

serialized_engine = builder.build_serialized_network(network, config)

将引擎保存到文件以供将来使用可能很有用。您可以这样做

with open(“sample.engine”, “wb”) as f:
   f.write(serialized_engine)

注意

序列化引擎在平台之间不可移植。它们特定于构建它们的精确 GPU 模型(除了平台之外)。

反序列化 Plan#

当您有一个先前序列化的优化模型并且想要执行推理时,您必须首先创建 Runtime 接口的实例。与构建器一样,运行时需要记录器的实例

runtime = trt.Runtime(logger)

可以使用以下方法反序列化引擎:[1]

内存中反序列化

此方法简单明了,适用于较小的模型或内存不是约束的情况。将 plan 文件读取到内存缓冲区中。

with open("model.plan", "rb") as f:
    model_data = f.read()
engine = runtime.deserialize_cuda_engine(model_data)

您可以从序列化的引擎对象反序列化引擎

serialized_engine = builder.build_serialized_network(network, config)
engine = runtime.deserialize_cuda_engine(serialized_engine)

IStreamReaderV2 反序列化

这是最先进的方法,支持读取到主机和设备指针,并实现潜在的性能改进。使用此方法,无需将整个 plan 文件读取到缓冲区进行反序列化,因为 IStreamReaderV2 允许根据需要以块的形式读取文件。

class StreamReaderV2(trt.IStreamReaderV2):
    def __init__(self, bytes):
        trt.IStreamReaderV2.__init__(self)
        self.bytes = bytes
        self.len = len(bytes)
        self.index = 0

    def read(self, size, cudaStreamPtr):
        assert self.index + size <= self.len
        data = self.bytes[self.index:self.index + size]
        self.index += size
        return data

    def seek(self, offset, where):
        if where == SeekPosition.SET:
            self.index = offset
        elif where == SeekPosition.CUR:
            self.index += offset
        elif where == SeekPosition.END:
            self.index = self.len - offset
        else:
            raise ValueError(f"Invalid seek position: {where}")

reader_v2 = MyStreamReaderV2("model.plan")
engine = runtime.deserialize_cuda_engine(reader_v2)

trt.IStreamReaderV2 方法对于大型模型或使用高级功能(如 GPUDirect 或权重流式传输)尤其有利。它可以显着减少引擎加载时间和内存使用量。

选择反序列化方法时,请考虑您的具体要求

  • 对于小型模型或简单的用例,内存中反序列化通常就足够了。

  • 对于大型模型或内存效率至关重要的情况,请考虑使用 trt.IStreamReaderV2

  • 如果您需要自定义文件处理或流式传输功能,trt.IStreamReaderV2 提供了必要的灵活性。

执行推理#

引擎保存优化的模型,但推理需要中间激活的附加状态。这通过 IExecutionContext 接口完成

context = engine.create_execution_context()

一个引擎可以有多个执行上下文,允许一组权重用于多个重叠的推理任务。(当前的一个例外是使用动态形状时,除非指定了预览功能 PROFILE_SHARING_0806,否则每个优化配置文件只能有一个执行上下文。)

要执行推理,您必须为输入和输出指定缓冲区

context.set_tensor_address(name, ptr)

多个 Python 包允许您在 GPU 上分配内存,包括但不限于官方 CUDA Python 绑定、PyTorch、cuPy 和 Numba。

填充输入缓冲区后,您可以调用 TensorRT 的 execute_async_v3 方法以使用 CUDA 流启动推理。网络将异步或同步执行,具体取决于网络的结构和功能。可能导致同步行为的非详尽列表包括数据相关的形状、DLA 使用、循环和同步插件。

首先,创建 CUDA 流。如果您已经有一个,请使用指向它的指针,例如,对于 PyTorch CUDA 流 torch.cuda.Stream(),您可以使用 cuda_stream 属性访问指针;对于 Polygraphy CUDA 流,使用 ptr 属性;或者您可以通过调用 cudaStreamCreate() 直接使用 CUDA Python 绑定创建流。

接下来,启动推理

context.execute_async_v3(buffers, stream_ptr)

通常在内核之前和之后排队异步传输 (cudaMemcpyAsync()) 以将数据从 GPU 移动,如果数据尚未在那里。

要确定推理(和异步传输)何时完成,请使用标准 CUDA 同步机制,例如事件或等待流。例如,对于 PyTorch CUDA 流或 Polygraphy CUDA 流,发出 stream。要 synchronize() 与使用 CUDA Python 绑定创建的流同步,请发出 cudaStreamSynchronize(stream)

脚注