4. 模型序列化与反序列化:.plan文件格式、序列化流程、反序列化流程、模型加密与安全考量

好,咱们今天聊聊模型序列化。说白了,就是把训练好的模型存成文件,下次要用的时候直接加载,不用重新构建和优化。在TensorRT里,这个文件就是.plan文件。

我刚开始接触TensorRT时,觉得序列化不就是个保存加载嘛,有啥好讲的?直到有一次,我在客户现场部署模型,发现同样的.plan文件,在不同显卡上跑出来的结果不一样……嗯,从那以后,我对序列化这件事就再也不敢马虎了。

4.1 .plan文件格式:不只是二进制

.plan文件,全称是TensorRT Plan File。它不是一个简单的权重存储文件,而是一个完整的执行计划。

它里面包含了什么?我拆开给你看:

  • 网络结构:每一层的定义、连接关系、输入输出张量信息
  • 权重参数:经过量化、融合后的最终权重值
  • 优化后的内核选择:针对特定GPU架构选择的CUDA kernel
  • 内存分配策略:TensorRT计算出的最优内存复用方案
  • 硬件绑定信息:生成该plan时所用的CUDA计算能力版本、显存大小等

你想想看,这其实就是一个可执行的推理蓝图。它不依赖原始模型框架,也不依赖TensorRT的构建器,只要运行时库就能跑。

核心要点:.plan文件是硬件绑定的。在A100上生成的plan,不一定能在T4上跑。反过来,T4上生成的plan,在A100上可能浪费了性能。

4.2 序列化流程:从网络到文件

序列化,就是把构建好的ICudaEngine对象写入文件。流程其实不复杂,但有几个坑。

标准做法是这样的:

// 1. 创建构建器
IBuilder* builder = createInferBuilder(gLogger);

// 2. 创建网络定义
INetworkDefinition* network = builder->createNetwork();

// 3. 解析模型(比如用ONNX解析器)
// ... 省略解析代码 ...

// 4. 创建构建配置
IBuilderConfig* config = builder->createBuilderConfig();
config->setMemoryPoolLimit(MemoryPoolType::kWORKSPACE, 1 << 30); // 1GB

// 5. 构建引擎
ICudaEngine* engine = builder->buildSerializedNetwork(*network, *config);

// 6. 序列化到文件
IHostMemory* serializedModel = engine->serialize();
std::ofstream file("model.plan", std::ios::binary);
file.write(reinterpret_cast<const char*>(serializedModel->data()), 
           serializedModel->size());
file.close();

我个人习惯在序列化前,先检查一下引擎是否真的构建成功了。曾经有一次,我忘了检查buildSerializedNetwork的返回值,结果写了个空文件到磁盘,部署时排查了半天……

我的经验:序列化时,建议在文件名中加入GPU型号和TensorRT版本号。比如resnet50_a100_trt861.plan。这样以后看到文件就知道它是在什么环境下生成的,避免混淆。

4.3 反序列化流程:从文件到推理

反序列化,就是把.plan文件加载回来,创建可执行的引擎。这个过程比序列化快得多,因为它跳过了所有优化步骤。

// 1. 读取文件
std::ifstream file("model.plan", std::ios::binary);
file.seekg(0, std::ios::end);
size_t size = file.tellg();
file.seekg(0, std::ios::beg);

std::vector<char> buffer(size);
file.read(buffer.data(), size);
file.close();

// 2. 创建运行时
IRuntime* runtime = createInferRuntime(gLogger);

// 3. 反序列化
ICudaEngine* engine = runtime->deserializeCudaEngine(buffer.data(), size);

// 4. 创建执行上下文
IExecutionContext* context = engine->createExecutionContext();

这里有个关键点:反序列化时,GPU必须和生成plan时的GPU兼容。什么叫兼容?就是计算能力版本要匹配。

举个例子:你在RTX 3090(计算能力8.6)上生成的plan,可以在RTX 3080(计算能力8.6)上跑,但不能在GTX 1080(计算能力6.1)上跑。为什么?因为plan里保存的CUDA kernel是针对8.6架构编译的,6.1的硬件不支持这些指令。

避坑指南:我曾经在A100上生成plan,然后部署到V100上,结果反序列化直接报错。后来我养成了习惯:在目标部署环境的GPU上生成plan,或者至少保证计算能力版本一致。

4.4 模型加密与安全考量

说到安全,很多同学会问:.plan文件里包含了模型权重,别人拿到文件是不是就能窃取我的模型?

答案是:能,但没那么容易

.plan文件是二进制格式,不是标准的模型格式(比如ONNX、TensorFlow SavedModel)。直接解析它需要深入理解TensorRT的内部结构。但话说回来,只要有人愿意花时间逆向,总能提取出权重信息。

那怎么办?我常用的几种方案:

方案 实现方式 安全等级 性能影响
文件加密 对.plan文件进行AES加密,运行时解密到内存 中等 无(仅加载时解密)
自定义序列化 自己实现序列化/反序列化,加入校验逻辑 较高
硬件绑定 将plan与特定GPU的UUID绑定
模型混淆 在序列化前对权重进行混淆变换 较高 轻微(反混淆开销)

我个人最推荐的是文件加密 + 硬件绑定的组合方案。加密防止文件被直接读取,硬件绑定防止plan被复制到其他机器上使用。

实现硬件绑定的思路:

// 获取当前GPU的UUID
cudaDeviceProp prop;
cudaGetDeviceProperties(&prop, 0);
std::string deviceUUID(prop.uuid.bytes, 16);

// 将UUID作为元数据写入plan文件头部
// 反序列化时,先读取UUID,与当前GPU的UUID比对
// 如果不匹配,拒绝加载

小技巧:如果你只是做内部部署,不涉及客户分发,其实不用太担心安全问题。TensorRT的plan文件本身就不是标准格式,普通用户拿到也没法直接用。真正需要加密的场景,是你要把模型分发到不受控的环境中。

4.5 序列化与反序列化的最佳实践

总结一下我这些年积累的经验:

  1. 版本对齐:生成plan的TensorRT版本和加载plan的TensorRT版本必须一致。小版本不同也可能出问题。
  2. 硬件兼容性检查:反序列化前,检查当前GPU的计算能力是否与plan匹配。
  3. 文件完整性校验:在plan文件末尾添加CRC32或MD5校验码,防止文件损坏。
  4. 内存管理:反序列化后的engine对象会占用显存,记得在不用时释放。
  5. 多线程安全:同一个engine可以被多个context共享,但每个context只能被一个线程使用。

嗯,序列化这块其实不难,但细节决定成败。我见过太多人因为版本不匹配、硬件不兼容这种低级问题浪费一整天。希望今天的分享能帮你少踩几个坑。

模型序列化与反序列化核心流程 序列化流程 1. 创建构建器 (IBuilder) 2. 构建网络与配置 3. buildSerializedNetwork() 4. 序列化到 .plan 文件 反序列化流程 1. 读取 .plan 文件到内存 2. 创建运行时 (IRuntime) 3. deserializeCudaEngine() 4. 创建执行上下文 安全考量 文件加密 (AES) 硬件绑定 (UUID) 完整性校验 (CRC)
专注资料整理