1 TensorRT构建的阶段最高级别的接口是Bulider(c++,Python)。构建器负责优化并且生成引擎ENgine
为了构建引擎需要完成以下工作
创建网络定义
为创建器指定配置
调用的builder创建引擎
NetworkDefinition接口( C++ 、 Python )用于定义模型。将模型传输到 TensorRT 的最常见途径是以 ONNX 格式从框架中导出模型,并使用 TensorRT 的 ONNX 解析器来填充网络定义。但是,您也可以使用 TensorRT 的Layer ( C++ , Python ) 和Tensor ( C++ , Python ) 接口逐步构建定义。
无论您选择哪种方式,您还必须定义哪些张量是网络的输入和输出。未标记为输出的张量被认为是可以由构建器优化掉的瞬态值。输入和输出张量必须命名,以便在运行时,TensorRT 知道如何将输入和输出缓冲区绑定到模型。
BuilderConfig接口( C++ 、 Python )用于指定TensorRT如何优化模型。在可用的配置选项中,您可以控制 TensorRT 降低计算精度的能力,控制内存和运行时执行速度之间的权衡,以及限制对 CUDA ®内核的选择。由于构建器可能需要几分钟或更长时间才能运行,因此您还可以控制构建器搜索内核的方式,以及缓存搜索结果以供后续运行使用。
一旦有了网络定义和构建器配置,就可以调用构建器来创建引擎。构建器消除了无效计算、折叠常量、重新排序和组合操作以在 GPU 上更高效地运行。它可以选择性地降低浮点计算的精度,方法是简单地在 16 位浮点中运行它们,或者通过量化浮点值以便可以使用 8 位整数执行计算。它还使用不同的数据格式对每一层的多次实现进行计时,然后计算执行模型的最佳时间表,从而最大限度地降低内核执行和格式转换的综合成本。
3.TensorRT中文版开发教程-----TensorRT的C++接口解析_扫地的小何尚的博客-CSDN博客_c++ tensorrt
TensorRT C++ API 中的接口类以前缀I开头,例如ILogger 、 IBuilder等。
CUDA 上下文会在 TensorRT 第一次调用 CUDA 时自动创建,如果在该点之前不存在。通常最好在第一次调用 TensoRT 之前自己创建和配置 CUDA 上下文。
为了说明对象的生命周期,本章中的代码不使用智能指针;但是,建议将它们与 TensorRT 接口一起使用。
3.1. The Build Phase
要创建构建器,首先需要实例化ILogger接口。此示例捕获所有警告消息,但忽略信息性消息:
class Logger : public ILogger
{
void log(Severity severity, const char* msg) noexcept override
{
// suppress info-level messages
if (severity <= Severity::kWARNING)
std::cout << msg << std::endl;
}
} logger;
然后,您可以创建构建器的实例:
IBuilder* builder = createInferBuilder(logger);
3.1.1. Creating a Network Definition
创建构建器后,优化模型的第一步是创建网络定义:
uint32_t flag = 1U <<static_cast<uint32_t>
(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
INetworkDefinition* network = builder->createNetworkV2(flag);
为了使用 ONNX 解析器导入模型,需要kEXPLICIT_BATCH标志。有关详细信息,请参阅显式与隐式批处理部分。
3.1.2. Importing a Model using the ONNX Parser
现在,需要从 ONNX 表示中填充网络定义。 ONNX 解析器 API 位于文件NvOnnxParser.h中,解析器位于nvonnxparser
C++ 命名空间中。
#include “NvOnnxParser.h”
using namespace nvonnxparser;
您可以创建一个 ONNX 解析器来填充网络,如下所示
IParser* parser = createParser(*network, logger);
然后,读取模型文件并处理任何错误
parser->parseFromFile(modelFile,
static_cast<int32_t>(ILogger::Severity::kWARNING));
for (int32_t i = 0; i < parser.getNbErrors(); ++i)
{
std::cout << parser->getError(i)->desc() << std::endl;
}
TensorRT 网络定义的一个重要方面是它包含指向模型权重的指针,这些指针由构建器复制到优化的引擎中。由于网络是通过解析器创建的,解析器拥有权重占用的内存,因此在构建器运行之前不应删除解析器对象。
3.1.3. Building an Engine
下一步是创建一个构建配置,指定 TensorRT 应该如何优化模型。
IBuilderConfig* config = builder->createBuilderConfig();
这个接口有很多属性,你可以设置这些属性来控制 TensorRT 如何优化网络。一个重要的属性是最大工作空间大小。层实现通常需要一个临时工作空间,并且此参数限制了网络中任何层可以使用的最大大小。如果提供的工作空间不足,TensorRT 可能无法找到层的实现。默认情况下,工作区设置为给定设备的总全局内存大小;必要时限制它,例如,在单个设备上构建多个引擎时。
config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1U << 20);
构建引擎
IHostMemory* serializedModel = builder->buildSerializedNetwork(*network, *config);
由于序列化引擎包含权重的必要拷贝,因此不再需要解析器、网络定义、构建器配置和构建器,可以安全地删除:
delete parser;
delete network;
delete config;
delete builder;
然后可以将引擎保存到磁盘,并且可以删除它被序列化到的缓冲区
delete serializedModel
3.2. Deserializing a Plan
假设您之前已经序列化了一个优化模型并希望执行推理,您将需要创建一个运行时接口的实例。与构建器一样,运行时需要一个记录器实例
IRuntime* runtime = createInferRuntime(logger);
假设您已将模型从缓冲区中读取,然后可以对其进行反序列化以获得引擎:
ICudaEngine* engine =
runtime->deserializeCudaEngine(modelData, modelSize);
3.3. Performing Inference
引擎拥有优化的模型,但要执行推理,我们需要管理中间激活的额外状态。这是通过ExecutionContext
接口完成的:
IExecutionContext *context = engine->createExecutionContext();
一个引擎可以有多个执行上下文,允许一组权重用于多个重叠的推理任务。 (当前的一个例外是使用动态形状时,每个优化配置文件只能有一个执行上下文。)
要执行推理,您必须为输入和输出传递 TensorRT 缓冲区,TensorRT 要求您在指针数组中指定。您可以使用为输入和输出张量提供的名称查询引擎,以在数组中找到正确的位置:
int32_t inputIndex = engine->getBindingIndex(INPUT_NAME);
int32_t outputIndex = engine->getBindingIndex(OUTPUT_NAME);
使用这些索引,设置一个缓冲区数组,指向 GPU 上的输入和输出缓冲区:
void* buffers[2];
buffers[inputIndex] = inputBuffer;
buffers[outputIndex] = outputBuffer;
,您可以调用 TensorRT 的 enqueue 方法以使用CUDA 流异步启动推理:
context->enqueueV2(buffers, stream, nullptr);
通常在内核之前和之后将cudaMemcpyAsync() 排入队列以从 GPU 中移动数据(如果数据尚不存在)。 enqueueV2()的最后一个参数是一个可选的 CUDA 事件,当输入缓冲区被消耗时发出信号,并且可以安全地重用它们的内存。
要确定内核(可能还有memcpy() )何时完成,请使用标准 CUDA 同步机制,例如事件或等待流。
4.TensorRT的Python接口解析
Python API 可以通过tensorrt模块访问:
import tensorrt as trt
4.1. The Build Phase
要创建构建器,您需要首先创建一个记录器。 Python 绑定包括一个简单的记录器实现,它将高于特定严重性的所有消息记录到stdout
。
logger = trt.Logger(trt.Logger.WARNING)
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)
4.1.1. Creating a Network Definition in Python
创建构建器后,优化模型的第一步是创建网络定义:
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))
为了使用 ONNX 解析器导入模型,需要EXPLICIT_BATCH
标志。有关详细信息,请参阅显式与隐式批处理部分。
4.1.2. Importing a Model using the ONNX Parser
现在,需要从 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
4.1.3. Building an Engine
下一步是创建一个构建配置,指定 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)
4.2. Deserializing a Plan
要执行推理,您首先需要使用Runtime接口反序列化引擎。与构建器一样,运行时需要记录器的实例。
runtime = trt.Runtime(logger)
然后,您可以从内存缓冲区反序列化引擎:
engine = runtime.deserialize_cuda_engine(serialized_engine)
如果您需要首先从文件加载引擎,请运行:
with open(“sample.engine”, “rb”) as f:
serialized_engine = f.read()
4.3. Performing Inference
引擎拥有优化的模型,但要执行推理需要额外的中间激活状态。这是通过IExecutionContext
接口完成的
context = engine.create_execution_context()
一个引擎可以有多个执行上下文,允许一组权重用于多个重叠的推理任务。 (当前的一个例外是使用动态形状时,每个优化配置文件只能有一个执行上下文。)
要执行推理,您必须为输入和输出传递 TensorRT 缓冲区,TensorRT 要求您在 GPU 指针列表中指定。您可以使用为输入和输出张量提供的名称查询引擎,以在数组中找到正确的位置:
input_idx = engine[input_name]
output_idx = engine[output_name]
使用这些索引,为每个输入和输出设置 GPU 缓冲区。多个 Python 包允许您在 GPU 上分配内存,包括但不限于 PyTorch、Polygraphy CUDA 包装器和 PyCUDA。
然后,创建一个 GPU 指针列表。例如,对于 PyTorch CUDA 张量,您可以使用data_ptr()方法访问 GPU 指针;对于 Polygraphy DeviceArray ,使用ptr属性:
buffers = [None] * 2 # Assuming 1 input and 1 output
buffers[input_idx] = input_ptr
buffers[output_idx] = output_ptr
首先,创建 CUDA 流。如果您已经有 CUDA 流,则可以使用指向现有流的指针。例如,对于 PyTorch CUDA 流,即torch.cuda.Stream() ,您可以使用cuda_stream属性访问指针;对于 Polygraphy CUDA 流,使用ptr属性。
接下来,开始推理:
context.execute_async_v2(buffers, stream_ptr)
// 使用enqueue方法对CUDA内核在流上排队,进行异步处理
context->enqueue(batchSize, buffers, stream, nullptr);
// 最后的参数是个可选CUDA事件,用于输入缓存区被使用并可安全复用时发出信号
通常在内核之前和之后将异步memcpy()
排入队列以从 GPU 中移动数据(如果数据尚不存在)。
要确定内核(可能还有memcpy() )何时完成,请使用标准 CUDA 同步机制,例如事件或等待流。例如,对于 Polygraphy,使用:
stream.synchronize()