写在前面
本篇文章程序链接:https://github.com/Shao-junliang/TensorRT-Course-Notes/tree/master/cuda-tensorrt-basic-api
模型构建
TensorRT 的工作流程如下图:
TensorRT 最终推理时需要一个 TensorRT Engine,这个 Engine 是由 TensorRT Builder 构建,而 Builder 构建 Engine 时需要传入两个参数分别是:network, config;其中 network 是由 TensorRT Parser 解析的 ONNX 模型构建的。
定义 builder, config 和network。
createInferBuilder()
创建 IBuilder 类的实例。createBuilderConfig()
创建构建器配置对象。createNetworkV2
创建网络定义对象。
直接构建:设置模型结构、输入与输出信息。具体方法参考类:
INetworkDefinition
、IBuilderConfig
;ONNX导入:
- 创建ONNX解析器
createParser()
,通过parseFromFile()
导入ONNX模型; - 设置最大工作空间:
setMaxWorkspaceSize()
; - 设置优化配置文件:
createOptimizationProfile()
创建,配置最小、最大、最优的输入范围; - 添加到配置对象中
addOptimizationProfile()
,当模型有多个输入时,必须有多个 profile;
- 创建ONNX解析器
生成 Engine 模型文件。
buildEngineWithConfig()
为给定的network
和config
构建引擎。buildEngineWithConfig()
在 TensorRT 8.0 中已弃用,新的构建方法:buildSerializedNetwork()
;
模型序列化:
serialize()
将网络序列化为流stream
,保存时直接保存stream->data()
。
注意:builder、config等指针在结束时记得释放,否则会有内存泄漏,使用
ptr->destroy()
释放;ptr->destroy()
在 TRT 8.0 中已弃用,改用delete
。
模型推理
执行推理的步骤:
准备模型并加载。
创建 runtime:
createInferRuntime(logger)
。反序列化创建 engine, 得为 engine 提供数据:
runtime->deserializeCudaEngine(modelData, modelSize)
,其中modelData
包含的是 input 和 output 的名字,形状,大小和数据类型。class ModelData(object): INPUT_NAME = "data" INPUT_SHAPE = (1, 1, 28, 28) // [B, C, H, W] OUTPUT_NAME = "prob" OUTPUT_SIZE = 10 DTYPE = trt.float32
从 engine 创建执行上下文:
engine->createExecutionContext()
。创建 CUDA 流
cudaStreamCreate(&stream)
:- CUDA 编程流是组织异步工作的一种方式,创建流来确定batch推理的独立;
- 为每个独立 batch 使用
IExecutionContext
(第4步中已经创建了),并为每个独立批次使用 cudaStreamCreate 创建 CUDA 流。
数据准备:
在 host 上初始化
input
数据并声明output
数组大小,然后将input
数据搬运到 gpu 上。推理时,如果是动态输入,还需要明确输入数据的大小,利用
setBindingDimensions()
函数。要执行 inference,必须用一个指针数组(bindings)指定
input
和output
在gpu中的指针。bindings 为指针数组,表示 input 和 output 在 gpu 中的指针。比如 input 有 a,output 有 b, c, d,那么 bindings = [a, b, c, d],bindings[0] = a,bindings[2] = c。
可以通过 engine->getBindingDimensions(0) 获取指定 binding 索引(这里是索引 0)的张量维度信息。binding 索引是指模型输入和输出张量在 bindings 数组中的位置。
binding 索引:在 TensorRT 中,模型的输入和输出被称为 binding。这些 binding 在引擎创建时被确定,并且每个 binding 都有一个唯一的索引。
float *bindings[] = {input_data_device, output_data_device}; // 获取第0个binding的维度 nvinfer1::Dims inputDims = engine->getBindingDimensions(0); std::cout << "Input dimensions: "; for (int i = 0; i < inputDims.nbDims; ++i) { std::cout << inputDims.d[i] << " "; } std::cout << std::endl;
推理并将
output
搬运回CPU。
启动所有工作后,与所有流同步以等待结果:
cudaStreamSynchronize
。按照与创建相反的顺序释放内存。
动态shape
- 网络构建阶段:
- 必须在模型定义时,输入维度给定为 -1,否则该维度不会动态。
- 配置
profile
:- create:
builder->createOptimizationProfile()
; - set:
setDimensions()
设置kMIN
,kOPT
,kMAX
的输入尺寸范围; - add:
config->addOptimizationProfile(profile);
添加profile到网络配置中。
- create:
- 网络推理阶段:
- 在选择 profile 的索引后设置
input
维度:execution_context->setBindingDimensions(0, nvinfer1::Dims4());
- 注意: 在运行时,向 engine 请求绑定维度会返回用于构建网络的相同维度。 即
engine.getBindingDimensions(0)
得到的还是动态的维度 [-1, in_channel, -1, -1];获取当前的实际维度,需要查询执行上下文:context.getBindingDimensions(0)
。
- 在选择 profile 的索引后设置
ONNX模型
ONNX 模型是由微软和 Facebook 提出的一种表示深度学习的格式,主作用要是用来加速模型预测;也可以是作为深度学习模型互相转换之间的一个中转站,比如 torch–>onnx–>tensorrt;也可以利用 onnx 实现 torch–>onnx–>tensorflow;其中 torch 到 onnx 再到 tensorflow 的转换比较容易,但是 tensorflow–>onnx–>torch 个人感觉比较困难。
onnx 模型加速原理:主要通过动态量化实现,利用动态量化将 float32 得模型向量权重转化为 Int 类型,从而节省保存空间以及加速运算。
动态量化原理:根据四元组(min_val,max_val,qmin, qmax)来计算2个量化的参数,qmax 与 qmin 一般为127与-128,min_val 和 max_val 是从输入数据中的最大值与最小值,量化分数 scale = max_val / (( qmax-qmin)/2), zero_point 这个参数一般为0,量化后的权重 Q=zero_point + W/(scale)。
- torch 的量化函数:torch.quantization.quantize_dynamic
- https://netron.app/ 该网站可以可视化 onnx 模型,当然有时候也可以可视化一些其它的深度学习模型。
onnx模型构成
onnx 模型:定义了可扩展的计算图模型、标准数据类型以及内置的运算符。主要是由图和节点组成的一层一层的层级结构。每一个计算流图都定义为由节点组成的列表,每个节点是一个 OP算子,可能有一个或多个输入与输出,并由这些节点构建有向无环图。 有的 onnx 算子未实现,比如pytorch中的 BILSTM
层转到 onnx 模型中的 DynamicQuantizeLSTM
算子就未实现。
- onnx 模型已实现的一些 OP 算子:https://onnx.ai/onnx/operators/index.html
下图是一个 onnx 模型的所有参数了,其中最核心的就是 Graph(GraphProto)。
最核心的几个对象为:
- ModelProto、GraphProto 、NodeProto 、 ValueInfoProto 、 TensorProto 、 AttributeProto
当我们加载了一个 ONNX 之后,我们获得的就是一个 ModelProto
,它包含了一些版本信息,生产者信息和一个 GraphProto
。在 GraphProto
里面又包含了四个 repeated
数组,它们分别是 node
(NodeProto
类型),input
(ValueInfoProto
类型),output
(ValueInfoProto
类型)和initializer
(TensorProto
类型),其中 node
中存放了模型中所有的计算节点,input
存放了模型的输入节点,output
存放了模型中所有的输出节点,initializer
存放了模型的所有权重参数。 每个计算节点中还包含了一个 AttributeProto
数组,用来描述该节点的属性,比如 Conv 节点或者说卷积层的属性包含 group,pad,strides
等等;
一个 node 主要由:input、output、name、op_type、domain、attribute 等组成。
制作一个onnx节点
当下我们知道了一个 onnx 模型是由一个个的节点构成的图,在制作一个 onnx 模型时就是先搭建一个个的节点,然后将所有节点连接成一个图,最后将图与一些其他输入输出等信息组合在一起变成了一个Model。
在构建一个 onnx 模型时,可以利用 onnx.helper 方法提供的接口实现。具体可以参考onnx的官网onnx/helper.py文件,具体如下:
helper.make_tensor(name, data_type, dims, vals, raw)
"""
name: tensor 名字
data_type: 一个值,例如 onnx.TensorProto.FLOAT
dims(List[int]): 维度
vals: 值
raw(bool): 如果为真,则 vals 包含张量的序列化内容,否则,vals 应该是 *data_type* 定义的类型的值列表
"""
helper.make_tensor_value_info(name: str,
elem_type: int,
shape: Optional[Sequence[Union[str, int, None]]],
doc_string: str = "",
shape_denotation: Optional[List[str]] = None,)
""" 根据数据类型和形状创建一个 ValueInfoProto。 """
helper.make_attribute(key: str, value: Any, doc_string: Optional[str] = None)
""" 根据值类型创建一个 AttributeProto。 """
helper.make_node(op_type, inputs, outputs, name, doc_string, domain, **kwargs)
"""
inputs: 输入名称列表
outputs: 输出名称列表
doc_string: NodeProto 的可选文档字符串
domain: NodeProto 的可选域。 如果它是 None,我们将只使用默认域(它是空的)
kwargs: dict类型,节点的属性。 可接受的值记录在 make_attribute 方法中。
"""
helper.make_graph(nodes, name, inputs, outputs, initializer, doc_string, value_info, sparse_initializer)
"""
nodes: NodeProto 的列表
inputs: ValueInfoProto 列表
outputs: ValueInfoProto 列表
initializer: TensorProto 列表
value_info: ValueInfoProto 列表
sparse_initializer: SparseTensorProto 列表
"""
helper.make_model(graph, **kwargs)
"""
graph(GraphProto): make_graph 方法返回的图
kwargs: 添加到返回实例的任何属性
"""
另外两个比较常用的方法是对构造完的模型的检查和保存,分别定义在onnx/checker.py和onnx/init.py中:
onnx.checker.check_model()
onnx.save()
总结一下构造一个 onnx 模型的具体流程:
1)根据自己的网络结构调用 make_node() 来创建相关节点,节点的 inputs 和 outputs 参数决定了后续 graph 的连接情况,节点的权值和信息通过调用 make_tensor() 和 make_tensor_value_info() 来创建,它们和节点的联系在于节点的 name;
2)上述三个方法构造的结构分别对应 make_graph() 中的三个参数,具体如下所示:
a.nodes: make_node()
b.inputs: make_tensor_value_info()
c.initializer: make_tensor()
3)最后检查和保存模型即可
参考链接:https://blog.csdn.net/u014090429/article/details/120488773
onnx导出注意事项
- 对于任何用到 shape、size 返回值的参数时,例如:tensor.view(tensor.size(0), -1) 这类操作,避免直接使用 tensor.size 的返回值,而是加上int转换,tensor.view(int(tensor.size(0)), -1),断开跟踪。
- 对于 nn.Upsample 或 nn.functional.interpolate 函数,使用 scale_factor 指定倍率,而不是使用 size 参数指定大小。
- 对于 reshape、view 操作时,-1 的指定请放到 batch 维度。其他维度可以计算出来即可。batch 维度禁止指定为大于 -1 的明确数字。
- torch.onnx.export 指定 dynamic_axes 参数,并且只指定 batch 维度,禁止其他动态。
- 使用 opset_version=11,不要低于 11。
- 避免使用 inplace 操作,例如 y[…, 0:2] = y[…, 0:2] * 2 - 0.5。
- 尽量少的出现 5 个维度,例如 ShuffleNet Module,可以考虑合并 wh 避免出现 5 维。
- 尽量把预处理、后处理部分在 onnx 模型中实现,可以提高推理速度。
ONNX解析器
onnx 解析器有两种情况,libnvonnxparser.so 或者源代码,so 文件一般用 CUDA\include 路径下的NvOnnxParser.h
文件。使用源代码可以更好的进行自定义封装,简化插件开发或者模型编译的过程,更加具有定制化,遇到问题可以调试;
源代码编译时需要注意 onnx 与 onnx-tensorrt 版本对应,如 onnx-tensorrt-8.0 版本对应关系:
- Protobuf >= 3.0.x
- TensorRT 8.0.1.6
查看环境的 Protobuf 版本程序:
import google.protobuf print(google.protobuf.__version__)
查看环境中 TensorRT 版本:
直接查看 TensorRT 路径下 include/NvInferVersion.h 文件;
#define NV_TENSORRT_MAJOR 8 //!< TensorRT major version. #define NV_TENSORRT_MINOR 4 //!< TensorRT minor version. #define NV_TENSORRT_PATCH 2 //!< TensorRT patch version. #define NV_TENSORRT_BUILD 4 //!< TensorRT build number.
自定义插件(op)
导出 onnx 的时候,为 module 增加 symbolic 函数,实现自定义 op 算子。
class MySigmoid(torch.autograd.Function): @staticmethod def forward(ctx, x): # 这样写会报错:IndexError: Argument passed to at() was not in the map. # return x * 1 / (1 + torch.exp(-x)) return torch.sigmoid(x) @staticmethod def symbolic(g, x): return g.op("MySigmoid", x, g.op("Constant", value_t=torch.tensor([3, 2, 1], dtype=torch.float32)), attr1_s="string_attr", attr2_i=[1, 2, 3], attr3_f=222.0) class MyModel(nn.Module): def __init__(self) -> None: super().__init__() self.conv = nn.Conv2d(1, 1, 3, padding=1) self.conv.weight.data.fill_(1) self.conv.bias.data.fill_(0) def forward(self, x): x = self.conv(x) x = MySigmoid.apply(x) return x if __name__ == "__main__": model = Model().eval() dummy = torch.zeros(1, 1, 3, 3) torch.onnx.export( model, # 输入给model的参数,需要传递tuple,因此用括号 (dummy,), # 模型保存路径 "./src/cuda-tensorrt-basic-api/static/plugin_demo.onnx", # 是否打印详细信息 verbose=False, # 输入节点与输出节点名称 input_names=["image"], output_names=["output"], # opset版本, opset_version=11, # 通常,只设置batch为动态,其他的避免动态 dynamic_axes={ "image": {0: "batch", 2: "height", 3: "width"}, "output": {0: "batch", 2: "height", 3: "width"}, }, # 对于插件,需要禁用onnx检查,这个参数只在 torch < 1.10.0 版本存在 # enable_onnx_checker=False operator_export_type=OperatorExportTypes.ONNX_FALLTHROUGH )
在 onnx-tensorrt 源代码解析的 builtin_op_importers.cpp 文件中,添加对自定义 op 的解析。添加方法可以参考
FallbackPluginImporter
算子。测试发现这一步跳过后也可以实现自定义插件DEFINE_BUILTIN_OP_IMPORTER(MySigmoid) { printf("\033[31m=======================call MySigmoid=======================033[0m\n"); /* * OnnxAttrs 类用于处理和提取 ONNX 节点的属性。 * OnnxAttrs 类的构造函数接受一个节点和上下文对象, * OnnxAttrs.get() 方法以键值对形式返回节点的属性; */ OnnxAttrs attrs(node, ctx); const std::string pluginName{node.op_type()}; const std::string pluginVersion{attrs.get<std::string>("plugin_version", "1")}; const std::string pluginNamespace{attrs.get<std::string>("plugin_namespace", "")}; LOG_INFO("Searching for plugin: " << pluginName << ", plugin_version: " << pluginVersion << ", plugin_namespace: " << pluginNamespace); // 通过插件名称、版本和命名空间获取插件创建器 nvinfer1::IPluginCreator *creator = importPluginCreator(pluginName, pluginVersion, pluginNamespace); ASSERT(creator && "Plugin not found, are the plugin name, version, and namespace correct?", ErrorCode::kUNSUPPORTED_NODE); // 获取插件的字段名称集合 const nvinfer1::PluginFieldCollection *feildNames = creator->getFieldNames(); // 字段数据需要进行类型擦除,用 fieldData 进行临时分配 string_map<std::vector<uint8_t>> fieldData{}; // 加载字段,将字段数据和属性进行绑定 std::vector<nvinfer1::PluginField> fields = loadFields(fieldData, attrs, feildNames, ctx); // 创建插件 const auto plugin = createPlugin(getNodeName(node), creator, fields); ASSERT(plugin && "Could not create plugin", ErrorCode::kUNSUPPORTED_NODE); // 创建一个插件输入的向量 std::vector<nvinfer1::ITensor *> pluginInputs{}; for (auto &input : inputs) { // 将每个输入转换为 TensorRT 的张量并添加到插件输入向量中 pluginInputs.emplace_back(&convertToTensor(input, ctx)); } LOG_INFO("Successfully created plugin: " << pluginName); // 在 TensorRT 网络中添加插件层 auto *layer = ctx->network()->addPluginV2(pluginInputs.data(), pluginInputs.size(), *plugin); // 注册插件层 ctx->registerLayer(layer, getNodeName(node)); RETURN_ALL_OUTPUTS(layer); }
插件类继承
nvinfer1::IPluginV2DynamicExt
类,完成插件的具体实现。class MySigmoidPlugin : public IPluginV2DynamicExt { public: MySigmoidPlugin(const std::string name, const std::string attr1, float attr3); MySigmoidPlugin(const std::string name, const void *data, size_t length); MySigmoidPlugin() = delete; // ============================== 1.IPluginV2DynamicExt 类的纯虚函数 ============================== // 输出数据的尺寸 virtual DimsExprs getOutputDimensions(int32_t outputIndex, DimsExprs const *inputs, int32_t nbInputs, IExprBuilder &exprBuilder) noexcept override; // 支持的数据类型,int8,float16,float32等 virtual bool supportsFormatCombination(int32_t pos, PluginTensorDesc const *inOut, int32_t nbInputs, int32_t nbOutputs) noexcept override; // 配置插件格式 virtual void configurePlugin(DynamicPluginTensorDesc const *in, int32_t nbInputs, DynamicPluginTensorDesc const *out, int32_t nbOutputs) noexcept; // 需要的额外空间大小 virtual size_t getWorkspaceSize(PluginTensorDesc const *inputs, int32_t nbInputs, PluginTensorDesc const *outputs, int32_t nbOutputs) const noexcept override { return 0; } // 推理具体逻辑 virtual int32_t enqueue(PluginTensorDesc const *inputDesc, PluginTensorDesc const *outputDesc, void const *const *inputs, void *const *outputs, void *workspace, cudaStream_t stream) noexcept override; // ============================== 2.IPluginV2Ext 类的纯虚函数 ============================== virtual nvinfer1::DataType getOutputDataType(int32_t index, nvinfer1::DataType const *inputTypes, int32_t nbInputs) const noexcept override { return inputTypes[0]; } // ============================== 3.IPluginV2 类的纯虚函数 ============================== const char *getPluginType() const noexcept override; const char *getPluginVersion() const noexcept override; int initialize() noexcept override; void terminate() noexcept override; size_t getSerializationSize() const noexcept override; int getNbOutputs() const noexcept override; void serialize(void *buffer) const noexcept override; void destroy() noexcept override; nvinfer1::IPluginV2DynamicExt *clone() const noexcept override; void setPluginNamespace(const char *pluginNamespace) noexcept override; const char *getPluginNamespace() const noexcept override; private: const std::string m_LayerName; std::string m_attr1; float m_attr3; size_t m_InputVolume; std::string m_Namespace; };
插件创建器,继承
nvinfer1::IPluginCreator
类,重写该基类的纯虚函数。creator 类主要记录了该 plugin 的名字、版本与命名空间等信息,并且可以生成该plugin类型,返回继承关系内更上层的nvinfer1::IPluginV2*
指针。可以认为 creator 类是一个插件工厂类,用于插件的实例创建 。class MySigmoidPluginCreator : public IPluginCreator { public: MySigmoidPluginCreator(); /* * const char*:函数的返回类型是一个指向常量字符的指针。 * const: 类型限定符,表示这个成员函数是 const 的。const 成员函数保证不会修改它所属对象的成员变量。 * noexcept: 异常说明符,表示这个函数不会抛出异常。 * override: 表示这个成员函数重载了基类中的虚函数。使用 override 可以让编译器检查该函数是否确实覆盖了基类中的某个虚拟函数,如果没有覆盖成功,编译器会报错。 */ const char *getPluginName() const noexcept override; const char *getPluginVersion() const noexcept override; const PluginFieldCollection *getFieldNames() noexcept override; IPluginV2 *createPlugin(const char *name, const PluginFieldCollection *fc) noexcept override; IPluginV2 *deserializePlugin(const char *name, const void *serialData, size_t serialLength) noexcept override; void setPluginNamespace(const char *pluginNamespace) noexcept override; const char *getPluginNamespace() const noexcept override; private: static PluginFieldCollection m_FC; static std::vector<PluginField> m_PluginAttributes; std::string m_Namespace; };
在实现 plugin 类和 creator 类的 cpp 文件中对该 plugin 的 creator 进行注册, 采用宏
REGISTER_TENSORRT_PLUGIN
注册插件:
REGISTER_TENSORRT_PLUGIN(MySigmoidPluginCreator);
参考链接: Developer Guide :: NVIDIA Deep Learning TensorRT Documentation
简易插件封装集成
在实现自定义插件时,主要实现的是 enqueue() 函数,其他函数主要是一些信息读取等操作。所以将一些重复度高或者说是不需要每次都改变的函数封装起来,每次只重写插件类里面的推理具体逻辑函数,即可完成插件的封装集成,提高工作效率。
TensorRT 量化
简介
TensorRT 支持使用 8 位整数来表示量化浮点值,量化方案包括激活和权重的量化。
激活的量化方案取决于所选的校准算法,以找到最佳平衡特定数据的舍入误差和精度误差的比例 msub 。
权重的量化方案如下:
$$ s = {max(abs(x_{min}),abs(x_{max}))\over 127}$$
其中 \(x_{min}\) 和 \(x_{max}\) 是权重张量的浮点最小值和最大值。
要启用任何量化操作,必须在构建器配置中设置 INT8 标志。
量化工作流程
创建量化网络有两种工作流程:
- 训练后量化 (Post-training quantization,PTQ) 在网络训练后得出比例因子。TensorRT 为 PTQ 提供了一个称为校准的工作流程,当网络在代表性输入数据上执行时,它测量每个激活张量内激活的分布,然后使用该分布来估计张量的尺度值。
- 量化感知训练 (Quantization-aware training,QAT) 在训练期间计算比例因子。这允许训练过程补偿量化和反量化操作的影响。
显示量化与隐式量化
量化网络有两种方式表示:显示量化与隐式量化。
在隐式量化网络中,每个量化张量都有一个关联的 scale。当读取和写入张量时,scale 用于隐式量化和反量化值。在处理隐式量化网络时,TensorRT 在应用图优化时将模型视为浮点模型,并适时使用 INT8 来优化层执行时间。如果某个层在 INT8 中运行得更快,那么它就会在 INT8 中执行。否则,将使用 FP32 或 FP16。在此模式下,TensorRT 仅针对性能进行优化,并且几乎无法控制 INT8 的使用位置 - 即使在 API 级别显式设置了层的精度,TensorRT 也可能会在图形优化期间将该层与另一个层融合,并丢失必须在 INT8 中执行的信息。 TensorRT 的 PTQ 功能会生成隐式量化网络。
在显式量化网络中,量化值和未量化值之间转换的缩放操作由图中的 IQuantizeLayer
和 IDequantizeLayer
节点显式表示,这些节点可以称为 Q/DQ
节点。与隐式量化相比,显式形式准确指定执行与 INT8 之间的转换的位置,并且优化器将仅执行由模型语义决定的精度转换,即使:
- 添加额外的转换可以提高层精度(例如,选择 FP16 内核实现而不是 INT8 实现)。
- 添加额外的转换会导致引擎执行速度更快(例如,选择 INT8 内核实现来执行指定为具有浮点精度的层,反之亦然)。
ONNX 使用显式量化表示 - 当 PyTorch 或 TensorFlow 中的模型导出到 ONNX 时,框架图中的每个假量化操作都导出为 Q,后跟 DQ。由于 TensorRT 保留了这些层的语义,因此模型准确性非常接近原框架中的精度。虽然优化保留了量化和反量化的位置,但它们可能会更改模型中浮点运算的顺序,因此结果不会按位相同。
张量量化与通道量化
- 张量量化(Per-tensor quantization):对每个张量使用相同的量化参数,使用单个 scale(标量)来缩放整个张量。
- 通道量化(Per-channel quantization):对每个通道(例如卷积层的每个滤波器或特征图)使用不同的量化参数。
使用显式量化时,权重可以使用张量量化或通道量化来进行量化。在这两种情况下,量化尺度的精度都是FP32。激活(activation)只能使用张量量化。
设置动态范围
TensorRT 提供了直接设置动态范围(必须由量化张量表示的范围)的 API,以支持隐式量化,其中这些值是在 TensorRT 之外计算的。该 API 允许使用最小值和最大值来设置张量的动态范围。由于 TensorRT 目前仅支持对称范围,因此使用 max(abs(min_float), abs(max_float))
计算缩放比例。请注意,当 abs(min_float) != abs(max_float)
时,TensorRT 使用比配置更大的动态范围,这可能会增加舍入误差。对于将在 INT8 中执行的操作,所有浮点输入和输出都需要动态范围。可以利用 setDynamicRange
设置张量的动态范围:
tensor->setDynamicRange(min_float, max_float);
使用校准进行训练后量化
在训练后量化中,TensorRT 会为网络中的每个张量计算一个缩放值( scale )。这个过程称为校准,需要提供具有代表性的输入数据,TensorRT 在这些数据上运行网络以收集每个激活张量的统计信息。所需的输入数据量取决于具体应用。
对于激活张量的统计信息,决定最佳缩放值并不是一门精确的科学,这需要在量化表示中的两种误差来源之间进行平衡:离散化误差(当每个量化值所表示的范围变大时增加)和截断误差(值被钳制到可表示范围的极限)。因此,TensorRT 提供了多种不同的校准器,这些校准器以不同方式计算 scale 。校准器包含:IInt8EntropyCalibrator2
、 IInt8EntropyCalibrator
、IInt8MinMaxCalibrator
、IInt8LegacyCalibrator
。
校准批次大小也会影响 IInt8EntropyCalibrator2
和 IInt8EntropyCalibrator
的截断误差。
构建 INT8 引擎时,构建器执行以下步骤:
- 构建一个 32 位引擎,在校准集上运行它,并为每个张量记录激活值分布的直方图。
- 根据直方图构建一个校准表,为每个张量提供一个缩放值。
- 使用校准表和网络定义构建 INT8 引擎。
校准可能很慢,因此第 2 步(校准表)的输出可以缓存和重用。这在给定平台上多次构建相同网络时非常有用,且所有校准器都支持这一功能。
在运行校准之前,TensorRT 会查询校准器实现,查看它是否有访问缓存表的权限。如果有,它会直接进入第 3 步。缓存数据作为指针和长度传递。可以在此处找到一个示例校准表。
只要校准在层融合之前进行,校准缓存数据在不同设备之间是可移植的。
使用c++进行 INT8 校准
要向 TensorRT 提供校准数据,请实现 IInt8Calibrator
接口。
构建器按以下步骤调用校准器:
- 首先,它会查询接口以获取批量大小,并调用
getBatchSize()
来确定预期的输入批量大小。 - 然后,它会反复调用
getBatch()
以获取输入批次。批次必须完全符合getBatchSize()
返回的批量大小。当没有更多批次时,getBatch()
必须返回false
。
在实现校准器后,可以将其配置到构建器中使用:
config->setInt8Calibrator(calibrator.get());
要缓存校准表,请实现 writeCalibrationCache()
和 readCalibrationCache()
方法。