4. Plugin注册与序列化机制
自定义 Plugin 写完之后,TensorRT 并不会自动认识它。运行时构建 engine 时,TensorRT 需要根据网络定义中的算子类型名找到对应的创建入口;在加载已序列化的 engine 时,又需要根据序列化数据中的类型名和版本号重建 Plugin 实例。这两件事分别由 PluginCreator 注册 和 序列化/反序列化协议 完成。理解这条链路,是让自定义算子真正可部署、可复现、可升级的关键。
一、PluginCreator 是注册与创建的统一入口
TensorRT 通过 IPluginCreator 接口管理 Plugin 的“元信息 + 工厂方法”。一个 Creator 至少需要提供三样东西:
- name:Plugin 类型名,必须与网络中添加该 Plugin 时使用的类型名完全一致,例如
"SwishPlugin"。 - version:Plugin 版本字符串,例如
"1"。它参与序列化兼容性判断,不是随便填的装饰字段。 - pluginNamespace:命名空间,默认通常为空字符串
""。同一进程内,(name, version, namespace)三元组唯一确定一个 Creator。
Creator 的核心方法是 createPlugin(),它接收 pluginName、const void* serialData、size_t serialLength 以及 PluginFieldCollection*。当 TensorRT 从网络定义创建 Plugin 时,serialData 为 nullptr,参数通过 PluginFieldCollection 传入;当从序列化数据恢复时,serialData 指向之前 serialize() 写出的字节流,Creator 需要据此重建对象。
二、REGISTER_TENSORRT_PLUGIN 宏做了什么
手写注册代码容易遗漏,TensorRT 提供了 REGISTER_TENSORRT_PLUGIN(CreatorClass) 宏。它通常在 Plugin 实现文件末尾调用一次,展开后会生成一个静态注册对象,在动态库加载时把 Creator 实例注册到全局注册表中。典型写法如下:
class SwishPluginCreator : public nvinfer1::IPluginCreator {
public:
const char* getPluginName() const noexcept override { return "SwishPlugin"; }
const char* getPluginVersion() const noexcept override { return "1"; }
const nvinfer1::PluginFieldCollection* getFieldNames() noexcept override {
return &mFieldNames;
}
nvinfer1::IPluginV2* createPlugin(
const char* name, const nvinfer1::PluginFieldCollection* fc) noexcept override {
float beta = 1.0f;
for (int i = 0; i < fc->nbFields; ++i) {
if (strcmp(fc->fields[i].name, "beta") == 0)
beta = *static_cast<const float*>(fc->fields[i].data);
}
return new SwishPlugin(beta);
}
nvinfer1::IPluginV2* deserializePlugin(
const char* name, const void* data, size_t length) noexcept override {
return new SwishPlugin(data, length);
}
private:
nvinfer1::PluginFieldCollection mFieldNames{0, nullptr};
};
REGISTER_TENSORRT_PLUGIN(SwishPluginCreator);
注意 getFieldNames() 返回的字段集合描述了该 Plugin 支持哪些构造参数。如果这里返回空集合,而 createPlugin() 又去读取 beta,运行时就会拿不到参数。字段名、类型、顺序必须与添加 Plugin 时传入的 PluginField 一致。
三、序列化与反序列化的数据流
序列化发生在 engine 构建完成、调用 serialize() 时。TensorRT 会遍历网络中的 Plugin,调用每个 Plugin 的 serialize(),把返回的字节流写入 engine 文件。反序列化发生在 deserializeCudaEngine() 或 Runtime::deserializeCudaEngine() 时,TensorRT 读取字节流,解析出 Plugin 类型名、版本号和序列化数据,再通过注册表找到对应 Creator,调用 deserializePlugin() 重建实例。
因此,serialize() 和 deserializePlugin() 必须严格配对:写入什么顺序、什么类型、多少字节,读取时就要按同样顺序和类型还原。常见做法是先写一个版本号或魔数,再写参数,最后写权重数据。例如:
void SwishPlugin::serialize(void* buffer) const noexcept {
char* p = static_cast<char*>(buffer);
write(p, mBeta); // 先写标量参数
write(p, mWeights); // 再写权重数组
}
SwishPlugin::SwishPlugin(const void* data, size_t length) {
const char* p = static_cast<const char*>(data);
read(p, mBeta);
read(p, mWeights);
}
如果 serialize() 写入了 beta 和权重,而反序列化构造函数只读了 beta,那么加载后的 Plugin 权重就是未初始化的,推理结果会静默错误。这类问题不会在编译期报错,只能通过加载后对比输出或检查 getSerializationSize() 是否一致来发现。
四、版本兼容:为什么 version 不能随便改
Plugin 的 version 字段是 TensorRT 判断“这个序列化数据能不能被当前 Creator 正确解析”的依据。假设 v1 的 serialize() 只写了 beta,v2 增加了 weights。如果直接把 Creator 的 getPluginVersion() 从 "1" 改成 "2",那么用 v1 构建的 engine 在加载时会发现序列化数据里的版本是 "1",而注册表中只有 "2" 的 Creator,匹配失败,加载报错。
正确的兼容策略通常有两种。第一种是保留旧版本 Creator:同时注册 SwishPluginCreatorV1 和 SwishPluginCreatorV2,分别返回 "1" 和 "2",各自实现对应的反序列化逻辑。第二种是在反序列化时读取数据内部版本号:Creator 的 version 保持稳定,但序列化数据开头写入一个内部格式版本,deserializePlugin() 根据这个内部版本决定按哪种布局解析。前者更清晰,后者更灵活,但都要求旧 engine 能被正确还原。
常见陷阱:修改了 serialize() 的写入内容,却没有同步修改 getSerializationSize()。这会导致 TensorRT 分配的缓冲区大小与实际写入字节数不一致,轻则数据截断,重则内存越界。每次调整序列化格式,都要同时检查 getSerializationSize()、serialize() 和反序列化构造函数三处。
五、实操检查清单
在交付一个带自定义 Plugin 的 engine 之前,建议按以下顺序自查:
| 检查项 | 预期结果 | 失败表现 |
|---|---|---|
| Creator 是否已注册 | getPluginRegistry()->getCreator(name, version, ns) 返回非空 |
构建时报 “Plugin not found” |
| name/version/namespace 是否一致 | 添加 Plugin 时使用的三元组与 Creator 完全匹配 | 构建或加载时找不到 Creator |
| getSerializationSize 是否等于实际写入 | 两者数值一致 | 加载后输出异常或崩溃 |
| serialize/deserialize 读写顺序是否配对 | 字段顺序、类型、数量完全一致 | 参数错位、权重错乱 |
| 旧版本 engine 是否仍可加载 | 保留旧 Creator 或内部版本分支 | 升级后旧模型无法部署 |
把注册、序列化、反序列化、版本兼容这四件事串起来看,本质上是在维护一份“Plugin 的持久化契约”。Creator 负责按名字和版本找到工厂,serialize() 负责把对象状态写成字节,deserializePlugin() 负责把字节还原成对象,而 version 则是这份契约的版本号。只要契约双方读写一致、版本可追溯,自定义 Plugin 就能像内置算子一样稳定地参与 engine 的构建与部署。