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() 是否一致来发现。

Plugin 注册与序列化/反序列化数据流 构建阶段(Build) addPluginV2("SwishPlugin", fields) Creator::createPlugin() Plugin::serialize(buffer) 序列化字节流 name="SwishPlugin" | version="1" beta(float) + weights(float[]) 加载阶段(Deserialize) deserializeCudaEngine() 查找 Creator(name, version) deserializePlugin(data, len) 全局 Plugin 注册表 REGISTER_TENSORRT_PLUGIN(SwishPluginCreator) → 静态注册对象 键:(name="SwishPlugin", version="1", namespace="") 版本兼容性检查点 序列化 version 与 Creator version 不一致 → 加载失败或走兼容分支 serialize() 与 deserializePlugin() 读写顺序必须严格配对

四、版本兼容:为什么 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 的构建与部署。