tensorRT基础操作

本文介绍TensorRT10.3.0的编程基础。

1、创建引擎

1、创建log日志

1
2
3
4
5
6
7
8
9
10
11
12
class Logger : public ILogger
{
public:
void log(Severity severity, const char *msg) noexcept override
{
if (severity <= Severity::kWARNING)
{
std::cout << msg << std::endl;
}
}
};

2、创建推理构建器

1
IBuilder *builder = createInferBuilder(logger);

3、构建网络

1
2
uint32_t flag = 0; // 使用默认标志,不使用 kEXPLICIT_BATCH
INetworkDefinition *network = builder->createNetworkV2(flag);

4、加载onnx模型并解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 创建 ONNX 解析器
IParser *parser = createParser(*network, logger);

// 解析 ONNX 模型文件
const std::string onnxModelPath = "model.onnx";
std::ifstream modelFile(onnxModelPath, std::ios::binary);
if (!modelFile)
{
std::cerr << "无法打开模型文件:" << onnxModelPath << std::endl;
return -1;
}
modelFile.seekg(0, std::ios::end);
size_t modelSize = modelFile.tellg();
modelFile.seekg(0, std::ios::beg);
std::vector<char> modelData(modelSize);
modelFile.read(modelData.data(), modelSize);
modelFile.close();

if (!parser->parse(modelData.data(), modelSize))
{
std::cerr << "解析 ONNX 模型失败:" << std::endl;
for (int i = 0; i < parser->getNbErrors(); ++i)
{
std::cerr << parser->getError(i)->desc() << std::endl;
}
return -1;
}

5、构建配置并且序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 创建构建配置
IBuilderConfig *config = builder->createBuilderConfig();
// config->setMaxWorkspaceSize(1U << 30); // 设置最大工作空间大小为 1GB

// 序列化网络并构建引擎
IHostMemory *serializedModel = builder->buildSerializedNetwork(*network, *config);
if (!serializedModel)
{
std::cerr << "构建序列化网络失败" << std::endl;
return -1;
}

// 将序列化的引擎保存到文件
const std::string engineFilePath = "model.engine";
std::ofstream engineFile(engineFilePath, std::ios::binary);
engineFile.write(reinterpret_cast<const char *>(serializedModel->data()), serializedModel->size());
engineFile.close();

// 清理资源
delete parser;
delete network;
delete config;
delete builder;

std::cout << "引擎构建并保存成功:" << engineFilePath << std::endl;
return 0;

6、完整基础代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <NvInfer.h>
#include <NvOnnxParser.h>
#include <iostream>
#include <fstream>
#include <vector>

using namespace nvinfer1;
using namespace nvonnxparser;

class Logger : public ILogger
{
public:
void log(Severity severity, const char *msg) noexcept override
{
if (severity <= Severity::kWARNING)
{
std::cout << msg << std::endl;
}
}
};

int main()
{
// 创建日志记录器
Logger logger;

// 创建推理构建器
IBuilder *builder = createInferBuilder(logger);

// 创建网络定义,使用 kDEFAULT 来替代 kEXPLICIT_BATCH
uint32_t flag = 0; // 使用默认标志,不使用 kEXPLICIT_BATCH
INetworkDefinition *network = builder->createNetworkV2(flag);

// 创建 ONNX 解析器
IParser *parser = createParser(*network, logger);

// 解析 ONNX 模型文件
const std::string onnxModelPath = "model.onnx";
std::ifstream modelFile(onnxModelPath, std::ios::binary);
if (!modelFile)
{
std::cerr << "无法打开模型文件:" << onnxModelPath << std::endl;
return -1;
}
modelFile.seekg(0, std::ios::end);
size_t modelSize = modelFile.tellg();
modelFile.seekg(0, std::ios::beg);
std::vector<char> modelData(modelSize);
modelFile.read(modelData.data(), modelSize);
modelFile.close();

if (!parser->parse(modelData.data(), modelSize))
{
std::cerr << "解析 ONNX 模型失败:" << std::endl;
for (int i = 0; i < parser->getNbErrors(); ++i)
{
std::cerr << parser->getError(i)->desc() << std::endl;
}
return -1;
}

// 创建构建配置
IBuilderConfig *config = builder->createBuilderConfig();
// config->setMaxWorkspaceSize(1U << 30); // 设置最大工作空间大小为 1GB

// 序列化网络并构建引擎
IHostMemory *serializedModel = builder->buildSerializedNetwork(*network, *config);
if (!serializedModel)
{
std::cerr << "构建序列化网络失败" << std::endl;
return -1;
}

// 将序列化的引擎保存到文件
const std::string engineFilePath = "model.engine";
std::ofstream engineFile(engineFilePath, std::ios::binary);
engineFile.write(reinterpret_cast<const char *>(serializedModel->data()), serializedModel->size());
engineFile.close();

// 清理资源
delete parser;
delete network;
delete config;
delete builder;

std::cout << "引擎构建并保存成功:" << engineFilePath << std::endl;
return 0;
}

当然也可以如下自定义网络不用onnx:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
INetworkDefinition* network = builder->createNetworkV2(0);

// 创建输入层
auto input = network->addInput("input", DataType::kFLOAT, Dims3(1, 28, 28));

// 创建卷积层
auto conv = network->addConvolution(*input, 32, DimsHW(3, 3), weightTensor, biasTensor);
conv->setStride(DimsHW(1, 1));

// 创建 ReLU 激活层
auto relu = network->addActivation(*conv->getOutput(0), ActivationType::kRELU);

// 创建池化层
auto pool = network->addPooling(*relu->getOutput(0), PoolingType::kMAX, DimsHW(2, 2));
pool->setStride(DimsHW(2, 2));

// 创建输出层
auto output = network->addOutput(*pool->getOutput(0));

// 创建引擎配置并构建引擎
IBuilderConfig* config = builder->createBuilderConfig();
ICudaEngine* engine = builder->buildCudaEngine(*network);

7、序列化的意义

TensorRT 序列化的作用和意义主要体现在以下几个方面:

1. 提高推理效率

TensorRT 序列化的主要作用之一是将经过优化的网络模型保存为一个二进制文件(通常是 .engine 文件)。这个文件包含了网络的结构、权重以及与推理相关的优化信息。在推理时,TensorRT 可以直接加载这个序列化的引擎文件,而不需要每次都重新解析原始的 ONNX 模型或进行优化。这样可以显著减少推理的初始化时间,并提高推理的效率。

2. 优化推理性能

通过序列化,TensorRT 会在创建引擎的过程中进行一系列的优化,例如:

  • 层融合:将多个算子融合成一个更高效的算子,减少计算量。

  • 精度降低:通过使用 FP16 或 INT8 等低精度进行推理,以提高速度并减少内存使用。

  • 内存管理:优化内存的使用方式,使得内存的占用最小化,尤其是在 GPU 上,能够有效利用显存。

  • 动态批量大小支持:序列化后的引擎通常支持动态批量大小,这意味着引擎可以根据实际输入的批量大小自动调整,从而避免了不必要的内存浪费。

3. 便于模型部署

通过序列化,TensorRT 将网络模型变成了一个标准化的文件(.engine),这个文件可以被部署到不同的环境中。例如:

  • 在 GPU 上进行推理时,.engine 文件可以直接加载并执行,而无需重新构建或重新解析原始模型。

  • 该序列化引擎也可以在不同的平台之间共享,如从开发机器迁移到生产环境,或者在不同的硬件设备上进行推理。

序列化使得模型从训练到部署的过程变得更加高效和简单。

4. 节省存储和计算资源

  • 减少加载时间:与直接从原始 ONNX 模型文件加载相比,加载已序列化的 TensorRT 引擎文件速度要快得多。序列化的引擎已经包含了所有必要的优化和硬件加速信息,所以加载时间大大缩短。

  • 减少计算资源消耗:由于 TensorRT 在序列化过程中已应用了多种优化,重新加载并执行时,所需的计算资源(如 GPU 核心数、内存等)更少,推理速度更快。

5. 与硬件加速紧密结合

TensorRT 序列化后的引擎文件通常会根据目标硬件(如 GPU 类型)进行特定优化。例如:

  • 如果引擎在 NVIDIA A100 GPU 上序列化,它可能会利用 A100 GPU 上的特定硬件加速功能(如 Tensor Cores)。

  • 序列化后的引擎文件可以直接使用硬件特性进行推理,而无需每次都进行繁重的优化步骤。

6. 支持多种精度

TensorRT 序列化时,可以指定精度模式(如 FP32、FP16 或 INT8)。通过在序列化过程中将精度降低为较低精度(如 FP16 或 INT8),可以大幅提高推理速度,并减少显存占用。这些低精度模式对于推理任务非常重要,尤其是在性能和功耗要求严格的环境中(如边缘设备)。

7. 简化部署和集成

使用 TensorRT 序列化的引擎文件,用户不需要重新编译或重新构建推理代码。只需要将引擎文件和相应的加载代码部署到目标设备上,程序就可以直接加载并进行推理。这种方式简化了部署过程,并减少了配置和测试的复杂性。

2、使用引擎推理

1、加载引擎并且反序列化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
ICudaEngine *loadEngine(const std::string &engineFilePath, Logger &logger)
{
std::ifstream engineFile(engineFilePath, std::ios::binary);
if (!engineFile)
{
std::cerr << "无法打开引擎文件:" << engineFilePath << std::endl;
return nullptr;
}

// 读取引擎文件
std::vector<char> engineData((std::istreambuf_iterator<char>(engineFile)),
std::istreambuf_iterator<char>());
engineFile.close();

// 创建 TensorRT 运行时
IRuntime *runtime = createInferRuntime(logger);
if (!runtime)
{
std::cerr << "无法创建推理运行时" << std::endl;
return nullptr;
}

// 反序列化引擎
ICudaEngine *engine = runtime->deserializeCudaEngine(engineData.data(), engineData.size());
if (!engine)
{
std::cerr << "无法反序列化引擎" << std::endl;
return nullptr;
}

return engine;
}

在使用 TensorRT 序列化引擎时,反序列化的过程是将保存为 .engine 文件的序列化引擎加载到内存中,并准备好用于推理。反序列化过程的主要目的是将之前序列化并保存的引擎文件转换回一个可以执行推理的对象。

2、创建上下文进行推理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void doInference(ICudaEngine *engine, void *inputData, void *outputData)
{
// 创建执行上下文
IExecutionContext *context = engine->createExecutionContext();
if (!context)
{
std::cerr << "无法创建执行上下文" << std::endl;
return;
}

// 分配输入输出缓冲区
void *buffers[2];
buffers[0] = inputData; // 输入数据
buffers[1] = outputData; // 输出数据

// 执行推理
context->executeV2(buffers);

// 销毁执行上下文
delete context;
}

3、使用实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int main()
{
// 创建日志记录器
Logger logger;

// 加载已序列化的引擎
const std::string engineFilePath = "model.engine";
ICudaEngine *engine = loadEngine(engineFilePath, logger);
if (!engine)
{
std::cerr << "加载引擎失败" << std::endl;
return -1;
}

// 准备输入输出数据
int inputSize = 1 * 3 * 224 * 224; // 假设输入是一个 RGB 图像,大小为 224x224
int outputSize = 1 * 1000; // 假设输出是 1000 维的概率分布

float *inputData = new float[inputSize]; // 你可以在这里加载输入数据
float *outputData = new float[outputSize]; // 用于保存输出数据

// 在 GPU 上分配内存
void *inputDevice = nullptr;
void *outputDevice = nullptr;
cudaMalloc(&inputDevice, inputSize * sizeof(float)); // 分配输入内存
cudaMalloc(&outputDevice, outputSize * sizeof(float)); // 分配输出内存

// 将输入数据从主机拷贝到 GPU
cudaMemcpy(inputDevice, inputData, inputSize * sizeof(float), cudaMemcpyHostToDevice);

// 执行推理
doInference(engine, inputDevice, outputDevice);

// 获取输出数据,从 GPU 拷贝回主机
cudaMemcpy(outputData, outputDevice, outputSize * sizeof(float), cudaMemcpyDeviceToHost);

// 输出前五个结果
for (int i = 0; i < 5; i++)
{
std::cout << "Output[" << i << "] = " << outputData[i] << std::endl;
}

// 清理资源
delete[] inputData;
delete[] outputData;
cudaFree(inputDevice);
cudaFree(outputDevice);

// 销毁引擎
delete engine;

return 0;
}

3、关于tensorRT的优化

TensorRT 的高性能推理优化方法主要体现在以下几个方面:

  1. **层融合 (Layer Fusion)**:
  • TensorRT 会将多个神经网络中的层进行融合,将这些操作合并为一个更高效的计算内核,从而减少内存访问和计算开销。例如,将卷积层和激活函数层合并为一个操作,以减少中间结果的存储需求和不必要的数据传输。

  • 这种层融合能够显著提升推理效率,因为每次内存访问和计算都需要时间,融合多个操作后可以减少这些操作之间的中间数据传输。

  1. **内核自动调优 (Kernel Auto-Tuning)**:
  • 针对不同的 GPU 架构,TensorRT 会自动选择最优的计算内核。这意味着 TensorRT 可以为每种不同的硬件(例如 NVIDIA 的 Volta 或 Turing 架构 GPU)选择最适合的计算内核实现,从而最大化性能。

  • 自动调优的核心思想是通过试验不同的内核实现和参数设置,选出最合适的内核配置,以达到最佳的推理速度。

  1. **动态张量内存管理 (Dynamic Tensor Memory Management)**:
  • TensorRT 对内存使用进行优化,自动管理张量内存,减少内存占用并降低数据传输时间。通过精确控制内存的分配与回收,TensorRT 能够避免不必要的内存开销,从而提升推理效率。

  • 在推理过程中,TensorRT 会根据需要动态调整内存大小,减少内存碎片,并通过优化的数据传输方式提高内存带宽的利用率,最终减少推理延迟。

这些优化使得 TensorRT 在推理任务中能够显著提高速度,尤其在大规模模型部署时,能够充分发挥硬件的性能优势。

作者

Gary

发布于

2025-05-19

更新于

2025-05-19

许可协议

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...