基础声明
cpp
//! sampleINT8API.cpp
//! This file contains implementation showcasing usage of INT8 calibration and precision APIs.
//! It creates classification networks such as mobilenet, vgg19, resnet-50 from onnx model file.
//! This sample showcae setting per-tensor dynamic range overriding calibrator generated scales if it exists.
//! This sample showcase how to set computation precision of layer. It involves forcing output tensor type of the layer
//! to particular precision. It can be run with the following command line: Command: ./sample_int8_api [-h or --help]
//! [-m modelfile] [-s per_tensor_dynamic_range_file] [-i image_file] [-r reference_file] [-d path/to/data/dir]
//! [--verbose] [-useDLA <id>]
#define DEFINE_TRT_ENTRYPOINTS 1
#define DEFINE_TRT_LEGACY_PARSER_ENTRYPOINT 0
#include "argsParser.h"
#include "buffers.h"
#include "common.h"
#include "logger.h"
#include "NvInfer.h"
#include "NvOnnxParser.h"
#include <cstdlib>
#include <cuda_runtime_api.h>
#include <fstream>
#include <iostream>
#include <sstream>
#include <unordered_map>
#include <vector>
using namespace nvinfer1;
using samplesCommon::SampleUniquePtr;
const std::string gSampleName = "TensorRT.sample_int8_api";
void printDims(const nvinfer1::Dims &dims){
sample::gLogInfo<<"Dims Dimension is "<<dims.nbDims<<" :";
sample::gLogInfo<<"[";
for(int i=0;i<dims.nbDims;i++){
sample::gLogInfo<<dims.d[i];
if(i!=dims.nbDims-1) sample::gLogInfo<<",";
}
sample::gLogInfo<<"]"<<std::endl;
}
struct SampleINT8APIPreprocessing{
std::vector<int> inputDims{1,3,224,224};
};
//!
//! \brief SampleINT8APIParams 结构体
//!
//! 用于存放 INT8 API 示例所需的额外参数
//!
struct SampleINT8APIParams{
//是否输出详细日志
bool verbose{false};
//是否将网络中间张量写入文件(用于调试或可视化)
bool writeNetworkTensors{false};
//使用的 DLA 核心编号,-1 表示不使用 DLA
int dlaCore{-1};
//! INT8 预处理参数
SampleINT8APIPreprocessing mPreproc;
//! 模型文件路径
std::string modelFileName;
//! 输入数据目录列表
std::vector<std::string> dataDirs;
//! 动态量化范围文件路径(dynamic range file)
std::string dynamicRangeFileName;
//! 待推理的单张图像文件路径
std::string imageFileName;
//! 参考结果文件路径(用于验证推理输出)
std::string referenceFileName;
//! 网络中间张量输出文件路径
std::string networkTensorsFileName;
//! Timing Cache 文件路径(用于加速构建)
std::string timingCacheFile;
};
//!
//! \brief SampleINT8API 类
//!
//! 实现了在分类网络上的 INT8 推理示例。
//!
//! \details 演示如何为每个输入层设置自定义的 INT8 动态量化范围(dynamic range),
//! 并展示如何在不使用校准表的情况下执行 INT8 推理。
//
class SampleINT8API{
public:
// 构造函数,初始化参数
SampleINT8API(const SampleINT8APIParams ¶ms):mParams(params){}
//!
//! \brief 构建网络引擎
//!
sample::Logger::TestResult build();
//!
//! \brief 执行 TensorRT 推理
//!
sample::Logger::TestResult infer();
//!
//! \brief 清理示例类中创建的状态或资源
//!
sample::Logger::TestResult teardown();
SampleINT8APIParams mParams;//!< 保存 INT8 示例所需的参数
private:
SampleUniquePtr<IRuntime> mRuntime{};//!< TensorRT Runtime,用于反序列化引擎
std::shared_ptr<nvinfer1::ICudaEngine> mEngine{nullptr};//!< 用于执行网络推理的 TensorRT 引擎
std::map<std::string,std::string> mInOut;//!< 网络输入输出名称映射
nvinfer1::Dims mInputDims;//!< 网络输入张量的维度
nvinfer1::Dims mOutputDims;//!< 网络输出张量的维度
std::unordered_map<std::string,float> mPerTensorDynamicRangeMap;//!< 每个张量的动态范围最大绝对值映射(tensor name -> max abs value)
void getInputOutputNames();//!< 获取网络输入输出名称并填充 mInOut 映射
//!
//! \brief 读取 ppm 输入图像,进行预处理,并存储到管理缓冲区
//!
bool prepareInput(const samplesCommon::BufferManager &buffers);
//!
//! \brief 验证推理输出结果是否正确,并打印结果
//!
bool verifyOutput(const samplesCommon::BufferManager& buffers) const;
//!
//! \brief 读取每个张量的动态范围值
//!
bool readPerTensorDynamicRangeValues();
//!
//! \brief 为网络张量设置自定义动态范围
//!
bool setDynamicRange(SampleUniquePtr<nvinfer1::INetworkDefinition>& network);
//!
//! \brief 为网络各层设置计算精度(如 INT8/FP16/FP32)
//!
void setLayerPrecision(SampleUniquePtr<nvinfer1::INetworkDefinition>& network);
//!
//! \brief 将网络中张量名称写入文件,用于调试或可视化
//!
void writeNetworkTensorNames(const SampleUniquePtr<nvinfer1::INetworkDefinition>& network);
};
SampleINT8API::getInputOutputNames
cpp
void SampleINT8API::getInputOutputNames(){
int32_t nbindings=mEngine->getNbIOTensors();
ASSERT(nbindings==2);//应该就只有输入输出两个张量
for(int32_t b=0;b<nbindings;b++){
//获取名字
const char *bindingName=mEngine->getIOTensorName(b);
nvinfer1::Dims dims=mEngine->getTensorShape(bindingName);
if(mEngine->getTensorIOMode(bindingName)==TensorIOMode::kINPUT){
//输出详细信息
if (mParams.verbose)
{
sample::gLogInfo << "Found input: " << bindingName << " shape=" << dims
<< " dtype=" << static_cast<int32_t>(mEngine->getTensorDataType(bindingName))
<< std::endl;
}
mInOut["input"] = bindingName;
}
else
{
if (mParams.verbose)
{
sample::gLogInfo << "Found output: " << bindingName << " shape=" << dims
<< " dtype=" << static_cast<int32_t>(mEngine->getTensorDataType(bindingName))
<< std::endl;
}
mInOut["output"] = bindingName;
}
}
}
nvinfer1::ICudaEngine::getTensorIOMode
cpp
inline nvinfer1::TensorIOMode
nvinfer1::ICudaEngine::getTensorIOMode(const char* tensorName) const noexcept
这个函数用于判断指定名称的张量(tensor)在 TensorRT 引擎中是输入还是输出。
TensorRT 中,张量(tensor)除了模型中间的计算张量以外,只有两类对用户可见:
-
输入张量(kINPUT)
-
输出张量(kOUTPUT)
这个 API 就是用来查询一个 tensor 名字到底属于哪类。
tensorName
模型中某个张量的名称字符串。
要求:
-
必须是 以 '\0' 结尾的字符串
-
总长度 不能超过 4096 字节
⚠️ 如果长度过大或不是 null-terminated,则行为未定义。
返回值说明
返回类型是 TensorIOMode,可能是:
| 返回枚举 | 含义 |
|---|---|
kINPUT |
该张量是输入张量 |
kOUTPUT |
该张量是输出张量 |
kNONE |
该名称不是输入也不是输出(可能是中间 tensor) |
nvinfer1::TensorIOMode
cpp
//!
//! \enum TensorIOMode
//!
//! \brief Definition of tensor IO Mode.
//!
enum class TensorIOMode : int32_t
{
//! Tensor is not an input or output.
kNONE = 0,
//! Tensor is input to the engine.
kINPUT = 1,
//! Tensor is output by the engine.
kOUTPUT = 2
};
nvinfer1::ICudaEngine::getTensorShape
该函数用于 获取指定输入/输出张量(tensor)的"模型静态 shape"。
-
这里的 shape 是指 TensorRT 网络定义中的维度信息(维度结构)
-
如果张量的某些维度是 动态的 (即使用 -1 或 profile 指定的维度),返回值中对应位置会是
-1
此函数不会返回真实运行时的 shape(因为没有执行上下文),只返回模型定义阶段的维度模板。
tensorName
要查询 shape 的输入或输出 tensor 的名字。
要求:
-
必须是 null-terminated 的 C 字符串
-
长度必须 ≤ 4096 字节(包括结尾 '\0')
如果名字不存在,返回默认错误值(见下)。
返回值说明
返回类型:
nvinfer1::Dims
返回结果有三种情况:
| 场景 | 返回值说明 |
|---|---|
tensorName 是合法的输入或输出 tensor |
返回对应的模型维度,动态维度位置为 -1 |
tensorName 不是输入或输出 |
返回 Dims{-1, {}}(表示非法) |
| 维度中有动态 shape(例如 batch = -1) | 返回中该位置为 -1 |
sample::gLogInfo<<dims
https://blog.csdn.net/ouliten/article/details/154695165?spm=1001.2014.3001.5502#t3
在这里已经介绍过日志类,这个日志类重载了<<dims的操作,所以可以直接输出
SampleINT8API::readPerTensorDynamicRangeValues
cpp
//!
//! \brief 填充每个张量的动态范围最大绝对值
//!
//! \details 文件每行格式如下
//! gpu_0/conv1_1: 5.43116
bool SampleINT8API::readPerTensorDynamicRangeValues(){
//创建文件流
std::ifstream iDynamicRangeStream(mParams.dynamicRangeFileName);
if(!iDynamicRangeStream){
sample::gLogError<<"Could not find per-tensor scales file: "<<mParams.dynamicRangeFileName<<std::endl;
return false;
}
std::string line;
//张量名和最大值之间用:分割
char delim=':';
//读入一整行
while(std::getline(iDynamicRangeStream,line)){
std::istringstream iline(line);
//读入名字
std::string token;
std::getline(iline,token,delim);
//读入最大值
std::string tensorName=token;
std::getline(iline,token,delim);
float dynamicRange=std::stof(token);
//记录到map里
mPerTensorDynamicRangeMap[tensorName]=dynamicRange;
}
return true;
}
SampleINT8API::setLayerPrecision
cpp
//!
//! \brief 为网络中的每一层设置计算精度(INT8)
//!
void SampleINT8API::setLayerPrecision(SampleUniquePtr<nvinfer1::INetworkDefinition>& network){
sample::gLogInfo<<"Setting Per Layer Computation Precision"<<std::endl;
// 遍历整个网络的所有层
for(int i=0;i<network->getNbLayers();i++){
auto layer=network->getLayer(i);
// 如果开启 verbose,则打印当前层的名称和精度信息
if(mParams.verbose){
std::string layerName=layer->getName();
sample::gLogInfo<<"Layer: "<<layerName<<". Precision:INT8"<<std::endl;
}
// 对于某些"非计算层",不能设置 INT8 精度,否则会报错
// 这些层没有算子计算步骤,比如常量层、拼接层、shape 层
if(layer->getType()!=LayerType::kCONSTANT&&layer->getType()!=LayerType::kCONCATENATION&&
layer->getType()!=LayerType::kSHAPE){
// 设置该层的计算精度为 INT8
layer->setPrecision(nvinfer1::DataType::kINT8);
}
// 遍历该层的所有输出 Tensor
for(int j=0;j<layer->getNbOutputs();j++){
std::string tensorName=layer->getOutput(j)->getName();
if(mParams.verbose){
std::string tensorName = layer->getOutput(j)->getName();
sample::gLogInfo << "Tensor: " << tensorName << ". OutputType: INT8" << std::endl;
}
// 仅对"执行张量"设置输出数据类型
// shape 张量不会参与计算,因此不能设置为 INT8
if(layer->getOutput(j)->isExecutionTensor()){
// 设置输出 tensor 的执行类型为 INT8
layer->setOutputType(j,nvinfer1::DataType::kINT8);
}
}
}
}
nvinfer1::INetworkDefinition::getNbLayers
cpp
inline int32_t nvinfer1::INetworkDefinition::getNbLayers() const noexcept;
获取网络中层的数量
返回值:
层的个数
nvinfer1::INetworkDefinition::getLayer
cpp
inline nvinfer1::ILayer* nvinfer1::INetworkDefinition::getLayer(int32_t index) const noexcept;
作用:
从 TensorRT 的网络定义(INetworkDefinition)中,按序号获取某一层(Layer)对象。
简而言之:
你已经把各层添加到 Network 里了,这个函数让你按 index 拿出来某一层。
index
网络中层的编号,从 0 ~ getNbLayers() - 1。
-
如果 index 合法 → 返回对应
ILayer* -
如果 index ≥ getNbLayers() → 返回
nullptr
nvinfer1::ILayer::getName
cpp
inline const char* nvinfer1::ILayer::getName() const noexcept;
作用:
返回当前 Layer 的 名字(字符串指针)。
名字是用于标识该 Layer 的,用作调试、日志输出、分析网络结构等用途。
nvinfer1::ILayer::getType
cpp
inline nvinfer1::LayerType nvinfer1::ILayer::getType() const noexcept;
该函数用于 获取当前 Layer 的类型 (LayerType 枚举)。
-
LayerType 表示层的计算类型,例如卷积、激活、池化、拼接、常量等。
-
与
getName()不同,getType()返回的是 枚举类型而非字符串。
nvinfer1::LayerType
cpp
//!
//! \enum LayerType
//!
//! \brief 各种层的类型枚举
//!
//! \see ILayer::getType()
//!
enum class LayerType : int32_t
{
kCONVOLUTION = 0, //!< 卷积层
kCAST = 1, //!< 类型转换层(Cast)
kACTIVATION = 2, //!< 激活层(ReLU, Sigmoid, etc.)
kPOOLING = 3, //!< 池化层(Max/Avg Pooling)
kLRN = 4, //!< 局部响应归一化层(LRN)
kSCALE = 5, //!< 缩放层(Scale)
kSOFTMAX = 6, //!< SoftMax 层
kDECONVOLUTION = 7, //!< 反卷积层(Deconvolution)
kCONCATENATION = 8, //!< 拼接层(Concatenation)
kELEMENTWISE = 9, //!< 元素级操作层(加、乘等)
kPLUGIN = 10, //!< 插件层(Plugin)
kUNARY = 11, //!< 一元操作层(UnaryOp)
kPADDING = 12, //!< 填充层(Padding)
kSHUFFLE = 13, //!< Shuffle 层(用于重排/reshape)
kREDUCE = 14, //!< Reduce 层(求和、平均、最大值等)
kTOPK = 15, //!< TopK 层(取最大 K 个元素)
kGATHER = 16, //!< Gather 层(按索引收集元素)
kMATRIX_MULTIPLY = 17, //!< 矩阵乘法层(MatMul)
kRAGGED_SOFTMAX = 18, //!< Ragged SoftMax 层(稀疏 SoftMax)
kCONSTANT = 19, //!< 常量层
kIDENTITY = 20, //!< 恒等层(Identity)
kPLUGIN_V2 = 21, //!< 插件 V2 层
kSLICE = 22, //!< Slice 层(切片操作)
kSHAPE = 23, //!< Shape 层(获取张量形状)
kPARAMETRIC_RELU = 24, //!< Parametric ReLU 层
kRESIZE = 25, //!< Resize 层(缩放/插值)
kTRIP_LIMIT = 26, //!< 循环次数限制层(Loop Trip limit)
kRECURRENCE = 27, //!< 循环递归层(Loop Recurrence)
kITERATOR = 28, //!< 循环迭代器层(Loop Iterator)
kLOOP_OUTPUT = 29, //!< 循环输出层(Loop output)
kSELECT = 30, //!< 条件选择层(Select)
kFILL = 31, //!< 填充层(Fill)
kQUANTIZE = 32, //!< 量化层(Quantize)
kDEQUANTIZE = 33, //!< 反量化层(Dequantize)
kCONDITION = 34, //!< 条件层(Condition)
kCONDITIONAL_INPUT = 35, //!< 条件输入层
kCONDITIONAL_OUTPUT = 36, //!< 条件输出层
kSCATTER = 37, //!< Scatter 层(分散写)
kEINSUM = 38, //!< 爱因斯坦求和层(Einsum)
kASSERTION = 39, //!< 断言层(Assertion)
kONE_HOT = 40, //!< OneHot 层
kNON_ZERO = 41, //!< NonZero 层(非零元素索引)
kGRID_SAMPLE = 42, //!< Grid Sample 层(栅格采样)
kNMS = 43, //!< 非极大值抑制层(NMS)
kREVERSE_SEQUENCE = 44, //!< 序列反转层(Reverse sequence)
kNORMALIZATION = 45, //!< 归一化层(Normalization)
kPLUGIN_V3 = 46 //!< 插件 V3 层
};
nvinfer1::ILayer::setPrecision
cpp
inline void nvinfer1::ILayer::setPrecision(nvinfer1::DataType dataType) noexcept;
该函数用于 设置当前层在弱类型网络(weakly-typed network)中的首选或要求的计算精度。
比如设置了INT8,内部的激活值和权重全部都尽量会量化成INT8然后计算(当然激活值需要提供精度设置。权重的量化是TensorRT自己实现的)。当然输出如果不设置,还是fp。
注意TensorRT的低精度计算永远是(int8为例):INT8 activation*INT8 weight->FP32 accumulate(输出精度可以设置)。在python的一些框架里面是有FP16 activation*INT8 weight->FP16 accumulate。千万不要搞混了(因为我就把python的语义迁移到这里理解了,发现是错误的)。
-
计算精度 (
DataType)包括:-
kFLOAT(FP32) -
kHALF(FP16) -
kINT8(INT8) -
kUINT8(UINT8,针对某些输入/输出)
-
-
设置精度会 指示 TensorRT 优先选择该精度执行层计算。
-
但 TensorRT 可能仍然选择最快的实现而忽略请求精度,除非配合 builder flag 强制约束。
TensorRT 提供两种控制模式:
| Flag | 行为 |
|---|---|
BuilderFlag::kOBEY_PRECISION_CONSTRAINTS |
如果无法实现请求的精度,则构建失败并报错 |
BuilderFlag::kPREFER_PRECISION_CONSTRAINTS |
如果无法实现请求精度,TensorRT 会选择其他实现(退化到默认精度) |
- 如果没有设置精度,或者回退,TensorRT 会基于 全局性能策略 和 builder flag 自动选择计算精度及输入输出类型。
特殊情况
IIdentityLayer
-
如果层是恒等层,并且涉及 float/half/int8/uint8 类型转换,则 precision 必须是这些类型之一。
-
否则 precision 必须与输入或输出类型一致。
强类型网络(strongly-typed network)
-
强类型网络拒绝调用
setPrecision()。 -
在强类型网络中,精度通常通过 对输入张量做类型转换 来控制。
参数说明
| 参数 | 说明 |
|---|---|
dataType |
希望设置的计算精度(nvinfer1::DataType 枚举) |
nvinfer1::ITensor::isExecutionTensor
cpp
inline bool nvinfer1::ITensor::isExecutionTensor() const noexcept;
该函数用于 判断当前张量是否是执行张量(execution tensor)。
-
执行张量 :在推理过程中 参与实际计算 的张量,一般会被 TensorRT 分配内存用于存储中间结果。
-
非执行张量:
-
仅用于形状计算(shape tensor)。
-
或者内容不参与最终输出计算的张量。
-
-
在网络尚未完全构建时,如果张量没有路径到输出,
isExecutionTensor()会返回 false。 -
当网络路径完成后,该张量会变为执行张量。
注意事项
-
结果可靠性:仅在网络构建完成后才可靠。
-
与 shape tensor 区分:
-
isShapeTensor() == true→ 仅用于计算形状,不参与执行。 -
isExecutionTensor() == true→ 会参与计算。
-
-
内存分配:只有执行张量需要 TensorRT 分配实际内存存储中间计算结果。
nvinfer1::ILayer::setOutputType
cpp
inline void nvinfer1::ILayer::setOutputType(int32_t index, nvinfer1::DataType dataType) noexcept;
该函数用于 设置弱类型网络(weakly-typed network)中某一层输出的计算类型。
-
作用:指示 TensorRT 生成指定类型的输出数据。
-
如果不设置输出类型:
-
TensorRT 会基于层计算精度自动选择输出类型。
-
TensorRT 可能仍选择最快实现,而忽略请求的输出类型。
-
-
配合 BuilderFlag 可以控制行为:
-
kOBEY_PRECISION_CONSTRAINTS:如果无法生成指定类型,构建失败。 -
kPREFER_PRECISION_CONSTRAINTS:如果无法生成指定类型,退回到非要求类型。
-
-
对于连续层的低精度计算很有必要。因为Layer启动低精度计算不代表输出默认是低精度的。如果不设置,下一层的输入精度可能不匹配。
特殊情况
-
TopK 层
- 第二个输出张量的数据类型 总是 Int32,无法修改。
-
Shape 操作层
- 输出类型必须为
DataType::kINT32,尝试修改为其他类型会被忽略,仅报错提示。
- 输出类型必须为
-
输出类型与张量类型不同
-
setOutputType()只约束层输出类型,并不直接改变张量类型。 -
如果需要修改输出张量类型,应调用:
cpplayer->getOutput(i)->setType(type);对于网络输出张量,只有
setType()会影响最终绑定的数据表示。
-
强类型网络
-
强类型网络不允许调用
setOutputType()。 -
输出类型只能通过 特定层的
setToType()方法 设置:- 支持层:
ICastLayer、IDequantizeLayer、IFillLayer、IQuantizeLayer
- 支持层:
参数说明
| 参数 | 说明 |
|---|---|
index |
要设置的输出索引(第几个输出张量) |
dataType |
输出类型(nvinfer1::DataType 枚举) |
SampleINT8API::writeNetworkTensorNames
cpp
//!
//! \brief 将网络中所有张量的名称写入文件
//!
void SampleINT8API::writeNetworkTensorNames(const SampleUniquePtr<nvinfer1::INetworkDefinition> &network){
// 日志提示:此样例需要为每个张量提供动态范围
sample::gLogInfo<<"Sample requires to run with per-tensor dynamic range."<<std::endl;
sample::gLogInfo
<< "In order to run Int8 inference without calibration, user will need to provide dynamic range for all "
"the network tensors."
<< std::endl;
// 打开用于保存张量名称的文件
std::ofstream tensorsFile{mParams.networkTensorsFileName};
// 遍历网络输入张量,将输入张量名称写入文件
for(int i=0;i<network->getNbInputs();i++){
std::string tName=network->getInput(i)->getName();
tensorsFile<<"TensorName: "<<tName<<std::endl;// 写入文件
if (mParams.verbose)// verbose 模式打印到控制台
{
sample::gLogInfo << "TensorName: " << tName << std::endl;
}
}
// 遍历网络的每一层
for(int i=0;i<network->getNbLayers();i++){
// 遍历层的每一个输出张量,将输出张量名称写入文件
for(int j=0;j<network->getLayer(i)->getNbOutputs();j++){
std::string tName=network->getLayer(i)->getOutput(j)->getName();
tensorsFile << "TensorName: " << tName << std::endl;
if (mParams.verbose)
{
sample::gLogInfo << "TensorName: " << tName << std::endl;
}
}
}
// 关闭文件
tensorsFile.close();
// 日志提示:成功生成网络张量名称文件
sample::gLogInfo << "Successfully generated network tensor names. Writing: " << mParams.networkTensorsFileName
<< std::endl;
sample::gLogInfo
<< "Use the generated tensor names file to create dynamic range file for Int8 inference. Follow README.md "
"for instructions to generate dynamic_ranges.txt file."
<< std::endl;
}
SampleINT8API::setDynamicRange
cpp
//!
//! \brief 为网络的张量设置自定义动态范围(Dynamic Range)
//!
//! 在 INT8 推理中,如果不使用校准(calibration),就必须为网络中每个张量提供动态范围。
//! 动态范围定义了 TensorRT 如何将 FP32 / FP16 数值映射为 INT8(-128 到 127)。
//!
bool SampleINT8API::setDynamicRange(SampleUniquePtr<nvinfer1::INetworkDefinition>& network){
// 1. 读取用户提供的 per-tensor 动态范围值
if(!readPerTensorDynamicRangeValues()){
return false;// 如果没读到,则无法进行后续的量化处理
}
sample::gLogInfo<<"Setting Per Tensor Dynamic Range"<<std::endl;
if(mParams.verbose){
sample::gLogInfo
<< "If dynamic range for a tensor is missing, TensorRT will run inference assuming dynamic range for "
"the tensor as optional."
<< std::endl;
sample::gLogInfo
<< "If dynamic range for a tensor is required then inference will fail. Follow README.md to generate "
"missing per-tensor dynamic range."
<< std::endl;
}
// 2. 为网络输入张量设置动态范围
for(int i=0;i<network->getNbInputs();i++){
std::string tName=network->getInput(i)->getName();
// 如果用户提供了该张量的动态范围
if(mPerTensorDynamicRangeMap.find(tName)!=mPerTensorDynamicRangeMap.end()){
// 设置动态范围:[-max, max]
if(!network->getInput(i)->setDynamicRange(-mPerTensorDynamicRangeMap.at(tName),mPerTensorDynamicRangeMap.at(tName))){
return false;
}
}else{
// 如果没提供,打印警告
if (mParams.verbose)
{
sample::gLogWarning << "Missing dynamic range for tensor: " << tName << std::endl;
}
}
}
// 3. 遍历每个 layer 的输出张量,为其设置动态范围
for (int i = 0; i < network->getNbLayers(); ++i)
{
auto lyr = network->getLayer(i);
for (int j = 0, e = lyr->getNbOutputs(); j < e; ++j)
{
std::string tName = lyr->getOutput(j)->getName();
// (1) 如果用户提供了动态范围
if (mPerTensorDynamicRangeMap.find(tName) != mPerTensorDynamicRangeMap.end())
{
// 优先使用用户提供的值
if (!lyr->getOutput(j)->setDynamicRange(
-mPerTensorDynamicRangeMap.at(tName), mPerTensorDynamicRangeMap.at(tName)))
{
return false;
}
}
// (2) 如果是 Constant Layer(常量输入,例如权重)
else if(lyr->getType()==LayerType::kCONSTANT){
IConstantLayer *cLyr=static_cast<IConstantLayer*>(lyr);
if(mParams.verbose){
sample::gLogWarning << "Computing missing dynamic range for tensor, " << tName << ", from weights."
<< std::endl;
}
// 自动扫描 Constant weights 的最大绝对值作为动态范围
auto wts=cLyr->getWeights();
double max=std::numeric_limits<double>::min();
// 遍历所有 weight 元素,求最大绝对值
for(int64_t wb=0,we=wts.count;wb<we;wb++){
double val{};
switch(wts.type){
case DataType::kFLOAT: val = static_cast<const float*>(wts.values)[wb]; break;
case DataType::kBOOL: val = static_cast<const bool*>(wts.values)[wb]; break;
case DataType::kINT8: val = static_cast<const int8_t*>(wts.values)[wb]; break;
case DataType::kHALF: val = static_cast<const half_float::half*>(wts.values)[wb]; break;
case DataType::kINT32: val = static_cast<const int32_t*>(wts.values)[wb]; break;
case DataType::kUINT8: val = static_cast<uint8_t const*>(wts.values)[wb]; break;
case DataType::kFP8:
case DataType::kBF16:
case DataType::kINT4:
case DataType::kINT64:ASSERT(false && "Unsupported data type");
}
max=std::max(max,std::abs(val));
}
// 使用权重最大值作为动态范围
if(!lyr->getOutput(j)->setDynamicRange(-max,max)){
return false;
}
}else{
// (3) 其他 layer 缺少动态范围
if (mParams.verbose)
{
sample::gLogWarning << "Missing dynamic range for tensor: " << tName << std::endl;
}
}
}
}
// 打印所有读取到的动态范围
if (mParams.verbose)
{
sample::gLogInfo << "Per Tensor Dynamic Range Values for the Network:" << std::endl;
for (auto iter = mPerTensorDynamicRangeMap.begin(); iter != mPerTensorDynamicRangeMap.end(); ++iter)
sample::gLogInfo << "Tensor: " << iter->first << ". Max Absolute Dynamic Range: " << iter->second
<< std::endl;
}
return true;
}
nvinfer1::ITensor::setDynamicRange
cpp
inline bool nvinfer1::ITensor::setDynamicRange(float min, float max) noexcept
作用: 为某个张量设置其动态范围(dynamic range),供 TensorRT 在无校准表(calibration)情况下进行 INT8 量化使用。
要点:
-
目前仅支持 对称范围(symmetric range) 。库会使用
max(abs(min), abs(max))作为最终范围值(即只需关心最大绝对值)。 -
要求
min与max是有限数(finite),且min <= max。 -
返回
true/false表示是否成功设置。 -
已弃用(Deprecated):在 TensorRT 10.1 中被明确量化(explicit quantization)流程取代,但理解其原理仍有价值。
为什么要给张量设置动态范围?有什么用?
在 INT8 推理中,需要把浮点(FP32/FP16)数值映射到 8 位整数(int8)上。为了做到这点,必须为每个张量确定一个数值范围(min, max),也就是该张量在运行时可能出现值的边界。这个范围用于计算量化比例(scale )和**(对称量化时)零点(zero point)**,从而把真实数值压入整数域并在需要时恢复(dequantize)。
-
决定 quantize/dequantize 的 scale(影响精度与溢出)。
-
在构建时帮助 TensorRT 选择能满足该范围约束的内核或推断策略。
-
在没有 calibration table 的场景下手动提供范围,能让你在"无校准"模式下尝试 INT8 推理。
对称量化的计算(公式)
取最大绝对值:A = max(|min|, |max|)
选择量化整数域的最大正值 Qmax:
-
对于有符号 int8,常用
Qmax = 127(一些实现也使用 127 而不是 128,以避免 -128 的不对称性)。(工程实践中通常用 127 作为量化上界以方便对称映射)
计算 scale(浮点到 int 的比例):scale = A / Qmax
量化(示意):
cpp
q = round(x / scale)
// 并裁剪到 [-Qmax, Qmax]
反量化(恢复浮点):
对称量化的零点(zero point)通常为 0(无需偏移),这简化了运算并常用于加速器实现。
SampleINT8API::prepareInput
cpp
//!
//! \brief 预处理输入数据,并为主机/设备端输入缓冲区准备数据
//!
bool SampleINT8API::prepareInput(const samplesCommon::BufferManager &buffers){
// 检查输入图像格式是否为 ppm
if(samplesCommon::toLower(samplesCommon::getFileType(mParams.imageFileName)).compare("ppm")!=0){
sample::gLogError<<"Wrong format: "<<mParams.imageFileName<< " is not a ppm file."<<std::endl;
return false;
}
// 从预处理参数中读取网络输入的维度
// 一般是 NCHW,这里假设 batch=1
int channels=mParams.mPreproc.inputDims.at(1);
int height=mParams.mPreproc.inputDims.at(2);
int width=mParams.mPreproc.inputDims.at(3);
int max=0;// ppm 文件中像素最大值(通常为 255)
std::string magic;// ppm 文件头标识(如 P6)
// 用于存储从 ppm 文件中读取的原始图像数据(uint8)
// 数据排列方式是 HWC
std::vector<uint8_t> fileData(channels*height*width);
// 以二进制方式打开 ppm 文件
std::ifstream infile(mParams.imageFileName,std::ifstream::binary);
ASSERT(infile.is_open()&&"Attempting to read from a file that is not open.");
// 读取 ppm 文件头:
// magic: P6
// width, height: 图像尺寸
// max: 像素最大值
infile>>magic>>width>>height>>max;
// 跳过一个字节(通常是换行符)
infile.seekg(1,infile.cur);
// 读取真正的像素数据(RGB,uint8)
infile.read(reinterpret_cast<char*>(fileData.data()),width*height*channels);
// 获取 TensorRT 输入张量对应的 Host 端缓冲区
// 注意:这里是 float*,说明输入张量的数据类型是 FP32
float *hostInputBuffer=static_cast<float*>(buffers.getHostBuffer(mInOut["input"]));
// 图像预处理:
// 1. 数据布局从 HWC 转换为 CHW
// 2. 像素归一化到 [-1, 1] 区间
for(int c=0;c<channels;c++){
for(int h=0;h<height;h++){
for(int w=0;w<width;w++){
// CHW 布局下的目标索引
int dstIdx=c*height*width+h*width+w;
// HWC 布局下的源索引
int srcIdx=h*width*channels+w*channels+c;
// 归一化:
// 原始值 ∈ [0, 255]
// 映射到 [-1, 1]
hostInputBuffer[dstIdx]=(2.0F/255.0F)*static_cast<float>(fileData[srcIdx])-1.0F;
}
}
}
return true;
}
samplesCommon::toLower
cpp
inline std::string toLower(const std::string& inp)
{
std::string out = inp;
std::transform(out.begin(), out.end(), out.begin(), ::tolower);
return out;
}
把输入字符串 inp 中的每个字符转换为小写字符,返回一个新的字符串。
std::transform对输入区间的每个元素应用一个函数,把结果写到输出区间。
samplesCommon::getFileType
cpp
inline std::string getFileType(const std::string& filepath)
{
return filepath.substr(filepath.find_last_of(".") + 1);
}
返回文件的后缀名
SampleINT8API::verifyOutput
cpp
//!
//! \brief 校验网络输出结果的正确性,并打印推理结果
//!
bool SampleINT8API::verifyOutput(const samplesCommon::BufferManager &buffers)const{
// 从 BufferManager 中获取输出张量对应的 Host 端缓冲区
// 注意:这里的数据类型是 float,说明网络最终输出仍然是 FP32
const float *probPtr=static_cast<const float*>(buffers.getHostBuffer(mInOut.at("output")));
// 将输出概率拷贝到 std::vector 方便后续处理
// mOutputDims.d[1] 通常表示类别数(例如 ImageNet 的 1000 类)
std::vector<float> output(probPtr,probPtr+mOutputDims.d[1]);
// 根据输出值的"幅值大小"对索引进行排序
// 返回的是索引数组,而不是排序后的值
// inds[0] 对应最大概率的类别索引
auto inds=samplesCommon::argMagnitudeSort(output.cbegin(),output.cend());
// 读取参考标签文件(如 ImageNet 的 labels.txt)
// 用于将类别索引映射为可读的类别名称
std::vector<std::string> referenceVector;
if (!samplesCommon::readReferenceFile(mParams.referenceFileName, referenceVector))
{
sample::gLogError << "Unable to read reference file: " << mParams.referenceFileName << std::endl;
return false;
}
// 根据网络输出概率,结合标签文件,生成 Top-5 分类结果
// 返回的是 Top-5 的类别名称字符串
std::vector<std::string> top5Result=samplesCommon::classify(referenceVector,output,5);
// 打印推理结果
sample::gLogInfo << "SampleINT8API result: Detected:" << std::endl;
for (int i = 1; i <= 5; ++i)
{
sample::gLogInfo << "[" << i << "] " << top5Result[i - 1] << std::endl;
}
return true;
}
samplesCommon::argMagnitudeSort
cpp
//返回一个索引向量,使得原序列的幅值(绝对值)按降序排列。
template <class Iter>
std::vector<size_t> argMagnitudeSort(Iter begin, Iter end)
{
std::vector<size_t> indices(end - begin);
//用从0开始逐渐加1的值填充索引数组
std::iota(indices.begin(), indices.end(), 0);
//绝对值从大到小排序(降序)
std::sort(indices.begin(), indices.end(), [&begin](size_t i, size_t j) { return std::abs(begin[j]) < std::abs(begin[i]); });
return indices;
}
samplesCommon::readReferenceFile
cpp
inline bool readReferenceFile(const std::string& fileName, std::vector<std::string>& refVector)
{
std::ifstream infile(fileName);
if (!infile.is_open())
{
std::cout << "ERROR: readReferenceFile: Attempting to read from a file that is not open." << std::endl;
return false;
}
std::string line;
while (std::getline(infile, line))
{
if (line.empty())
continue;
refVector.push_back(line);
}
infile.close();
return true;
}
这个就没什么好说的了,按行读取文件。
samplesCommon::classify
cpp
template <typename T>
std::vector<std::string> classify(
const std::vector<std::string>& refVector, const std::vector<T>& output, const size_t topK)
{
const auto inds = samplesCommon::argMagnitudeSort(output.cbegin(), output.cend());
std::vector<std::string> result;
result.reserve(topK);
for (size_t k = 0; k < topK; ++k)
{
result.push_back(refVector[inds[k]]);
}
return result;
}
返回绝对值最大的k个标签
SampleINT8API::build
cpp
sample::Logger::TestResult SampleINT8API::build()
{
// 1. 创建 TensorRT Builder(负责构建 Engine)
auto builder = SampleUniquePtr<nvinfer1::IBuilder>(
nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));
if (!builder)
{
sample::gLogError << "Unable to create builder object." << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 2. 创建 NetworkDefinition(用于存放计算图)
// createNetworkV2(0) 表示使用弱类型网络(允许 setPrecision / setOutputType)
auto network = SampleUniquePtr<nvinfer1::INetworkDefinition>(
builder->createNetworkV2(0));
if (!network)
{
sample::gLogError << "Unable to create network object."
<< mParams.referenceFileName << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 3. 创建 BuilderConfig(控制精度、优化策略、INT8/FP16 等)
auto config = SampleUniquePtr<nvinfer1::IBuilderConfig>(
builder->createBuilderConfig());
if (!config)
{
sample::gLogError << "Unable to create config object."
<< mParams.referenceFileName << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 4. 创建 ONNX Parser,用于解析 ONNX 模型到 TensorRT Network
auto parser = SampleUniquePtr<nvonnxparser::IParser>(
nvonnxparser::createParser(*network, sample::gLogger.getTRTLogger()));
if (!parser)
{
sample::gLogError << "Unable to create parser object."
<< mParams.referenceFileName << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 5. 解析 ONNX 模型文件,填充 TensorRT 网络
int verbosity = static_cast<int>(nvinfer1::ILogger::Severity::kERROR);
if (!parser->parseFromFile(mParams.modelFileName.c_str(), verbosity))
{
sample::gLogError << "Unable to parse ONNX model file: "
<< mParams.modelFileName << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 6. 如果用户只想导出网络中所有 tensor 名称(用于生成 dynamic range 文件)
if (mParams.writeNetworkTensors)
{
writeNetworkTensorNames(network);
return sample::Logger::TestResult::kWAIVED;
}
// ====================== Builder 配置部分 ======================
// 7. 启用 GPU fallback
// 如果某些 layer 在 DLA / 特定硬件上不支持,则回退到 GPU
config->setFlag(BuilderFlag::kGPU_FALLBACK);
// 8. 启用 INT8 精度
// 启用后 TensorRT 才会考虑使用 INT8 kernel
config->setFlag(BuilderFlag::kINT8);
// 9. 不使用校准器(calibrator)
// 因为本 sample 使用的是"手动 per-tensor dynamic range"
config->setInt8Calibrator(nullptr);
// 10. 强制每一层使用 INT8 计算精度(setPrecision + setOutputType)
setLayerPrecision(network);
// 11. 为所有 tensor 设置 INT8 动态范围(激活值量化范围)
if (!setDynamicRange(network))
{
sample::gLogError << "Unable to set per-tensor dynamic range." << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// ====================== 构建与优化阶段 ======================
// 12. 创建 CUDA stream,用于 Builder 内部 profiling(kernel benchmark)
auto profileStream = samplesCommon::makeCudaStream();
if (!profileStream)
{
return sample::Logger::TestResult::kFAILED;
}
config->setProfileStream(*profileStream);
// 13. (可选)加载 Timing Cache
// Timing cache 用于加速重复 build(避免重新 benchmark kernel)
SampleUniquePtr<nvinfer1::ITimingCache> timingCache;
if (!mParams.timingCacheFile.empty())
{
timingCache = samplesCommon::buildTimingCacheFromFile(
sample::gLogger.getTRTLogger(),
*config,
mParams.timingCacheFile,
std::cerr);
}
// 14. 构建序列化的 Engine(plan)
// 这是 TensorRT 最核心的一步
SampleUniquePtr<IHostMemory> plan{
builder->buildSerializedNetwork(*network, *config)};
if (!plan)
{
sample::gLogError << "Unable to build serialized plan." << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 15. 更新 Timing Cache(将本次 build 的 profiling 结果写回文件)
if (timingCache != nullptr && !mParams.timingCacheFile.empty())
{
samplesCommon::updateTimingCacheFile(
sample::gLogger.getTRTLogger(),
mParams.timingCacheFile,
timingCache.get(),
*builder);
}
// ====================== Runtime & Engine ======================
// 16. 创建 TensorRT Runtime(用于反序列化 Engine)
if (!mRuntime)
{
mRuntime = SampleUniquePtr<IRuntime>(
createInferRuntime(sample::gLogger.getTRTLogger()));
}
if (!mRuntime)
{
sample::gLogError << "Unable to create runtime." << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 17. 从序列化 plan 反序列化得到 ICudaEngine
mEngine = std::shared_ptr<nvinfer1::ICudaEngine>(
mRuntime->deserializeCudaEngine(plan->data(), plan->size()),
samplesCommon::InferDeleter());
if (!mEngine)
{
sample::gLogError << "Unable to build cuda engine." << std::endl;
return sample::Logger::TestResult::kFAILED;
}
// 18. 获取输入/输出 tensor 名称映射
getInputOutputNames();
// 19. 获取输入/输出 tensor 的 shape(用于 buffer 分配)
mInputDims = mEngine->getTensorShape(mInOut["input"].c_str());
mOutputDims = mEngine->getTensorShape(mInOut["output"].c_str());
return sample::Logger::TestResult::kRUNNING;
}
大部分都是以前build的老套路。
BuilderFlag
cpp
//!
//! \enum BuilderFlag
//!
//! \brief Builder 在根据网络定义创建 Engine 时可以启用的模式列表
//!
//! \see IBuilderConfig::setFlags(), IBuilderConfig::getFlags()
//!
enum class BuilderFlag : int32_t
{
//! 启用 FP16 层选择;如果某层不支持 FP16,则回退到 FP32
kFP16 = 0,
//! 启用 INT8 层选择;
//! 若某层不支持 INT8,则回退到 FP32;
//! 如果同时指定了 kFP16,则优先回退到 FP16
kINT8 = 1,
//! 启用调试模式:在每一层执行后进行同步(严重影响性能)
kDEBUG = 2,
//! 如果某层无法在 DLA 上执行,则允许回退到 GPU 上执行
kGPU_FALLBACK = 3,
//! 启用可 refit(重新绑定权重)的 Engine 构建
kREFIT = 4,
//! 禁止在相同层之间复用 timing(性能测试)信息
kDISABLE_TIMING_CACHE = 5,
//! 允许(但不强制)对 DataType::kFLOAT 类型的计算使用 TF32
//! TF32 会在乘法前将输入舍入为 10 位尾数,
//! 但累加仍使用 23 位尾数
//! 默认启用
kTF32 = 6,
//! 允许 Builder 检查权重稀疏性,
//! 当权重满足结构化稀疏条件时使用优化后的计算函数
kSPARSE_WEIGHTS = 7,
//! 将 EngineCapability::kSTANDARD 的参数限制
//! 改为与 EngineCapability::kSAFETY(GPU)
//! 或 EngineCapability::kDLA_STANDALONE(DLA)一致
//!
//! 如果在 build 时使用 EngineCapability::kSAFETY,
//! 且未显式设置该 flag,则该 flag 会被强制启用
//!
//! 仅在 NVIDIA Drive® 产品中支持
kSAFETY_SCOPE = 8,
//! **强制** 所有层必须使用指定的精度执行
//! 如果无法满足精度要求,则构建失败
kOBEY_PRECISION_CONSTRAINTS = 9,
//! **尽量** 使用指定的精度执行层
//! 如果无法满足,会给出警告并回退到其他精度
kPREFER_PRECISION_CONSTRAINTS = 10,
//! 要求在调用 ITensor::setAllowedFormats 的网络 I/O tensor
//! 与其相邻层之间不插入任何 reformat(格式转换)
//!
//! 如果功能正确性需要 reformat,则构建失败
kDIRECT_IO = 11,
//! 如果 IAlgorithmSelector::selectAlgorithms 返回空算法集合,
//! 则构建失败
kREJECT_EMPTY_ALGORITHMS = 12,
//! 限制使用精简(lean)runtime 运算符,
//! 以提供 plan 文件的跨版本前向兼容性
//!
//! 仅支持 NVIDIA Volta 及之后架构的 GPU
//! 不支持 NVIDIA Drive® 产品
kVERSION_COMPATIBLE = 13,
//! 在启用版本前向兼容时,将 lean runtime 从 plan 中排除
//!
//! 默认不启用,即 plan 中会包含 lean runtime
//!
//! 若未设置 kVERSION_COMPATIBLE,则该 flag 被忽略
kEXCLUDE_LEAN_RUNTIME = 14,
//! 启用输入/输出为 FP8 的插件
//!
//! 在硬件兼容模式下不支持
//!
//! \see HardwareCompatibilityLevel
kFP8 = 15,
//! 当某个 tactic 不存在于 timing cache 中时,
//! 在进行性能测试(timing)时直接报错
//!
//! 仅在 IBuilderConfig 绑定了 ITimingCache 时生效
kERROR_ON_TIMING_CACHE_MISS = 16,
//! 启用 BF16(bfloat16)层选择;若不支持则回退到 FP32
//!
//! 仅支持 NVIDIA Ampere 及之后架构的 GPU
kBF16 = 17,
//! 禁止在 Engine 构建过程中缓存 JIT 编译结果
//!
//! 默认情况下,JIT 编译的代码会被序列化进 timing cache,
//! 可能显著增大 cache 文件大小
//!
//! 当设置该 flag 时,JIT 代码不会被序列化
//!
//! 仅在未设置 kDISABLE_TIMING_CACHE 时生效
kDISABLE_COMPILATION_CACHE = 18,
//! 从 Engine plan 文件中剥离(移除)可 refit 的权重
kSTRIP_PLAN = 19,
//! \deprecated
//! TensorRT 10.0 起弃用,被 kSTRIP_PLAN 取代
kWEIGHTLESS TRT_DEPRECATED_ENUM = kSTRIP_PLAN,
//! 创建一个可 refit 的 Engine,
//! 并假设 refit 时提供的权重与 build 时完全一致
//!
//! 生成的 Engine 性能与不可 refit 的 Engine 相同
//!
//! 所有可 refit 权重都可以通过 refit API 修改,
//! 但如果新权重与 build 时权重不同,行为是未定义的
//!
//! 与 kSTRIP_PLAN 一起使用时,
//! 会生成一个非常小的 plan 文件,
//! 权重需在之后通过 refit 提供
//!
//! 适用于:
//! - 多推理后端共用一套权重
//! - 不同 GPU 架构使用同一模型权重
kREFIT_IDENTICAL = 20,
//! 启用权重流式加载(Weight Streaming)
//!
//! 允许执行无法完全放入 GPU 显存的模型,
//! TensorRT 会智能地从 CPU DRAM 向 GPU 流式加载权重
//!
//! 启用后,IRuntime::deserializeCudaEngine
//! 会将所有权重先分配在 CPU 内存中
//!
//! ICudaEngine::createExecutionContext
//! 会决定权重在 CPU/GPU 间的最优分配
//!
//! 未来 TensorRT 版本可能默认启用该功能
//!
//! \warning 启用后会略微增加 build 时间
//! \warning 会显著增加 createExecutionContext 的延迟
//!
//! \see IRuntime::deserializeCudaEngine
//! \see ICudaEngine::getMinimumWeightStreamingBudget
//! \see ICudaEngine::setWeightStreamingBudget
kWEIGHT_STREAMING = 21,
//! 启用输入/输出为 INT4 的插件
kINT4 = 22,
//! 启用"细粒度可 refit"Engine 构建模式
//!
//! 允许通过 INetworkDefinition::markWeightsRefittable /
//! unmarkWeightsRefittable 精确控制哪些权重可 refit
//!
//! 默认情况下,所有权重都不可 refit
//!
//! 该 flag 不能与 kREFIT 或 kREFIT_IDENTICAL 同时使用
kREFIT_INDIVIDUAL = 23,
};
IBuilderConfig::setInt8Calibrator
cpp
inline void IBuilderConfig::setInt8Calibrator(IInt8Calibrator* calibrator) noexcept;
设置 INT8 校准接口。该接口在 TensorRT 10.1 中已弃用,被「显式量化」取代。
所谓显示量化,就是我们本文一直在做的事情。明确告诉 TensorRT它的 dynamic range 是多少,不再需要 calibrator
SampleINT8API::infer
cpp
//!
//! \brief 执行一次 TensorRT 推理流程
//!
sample::Logger::TestResult SampleINT8API::infer(){
// 创建一个 RAII 风格的 BufferManager
// 负责为 engine 的所有输入 / 输出 tensor 分配:
samplesCommon::BufferManager buffers(mEngine);
// 为该 engine 创建一个执行上下文(ExecutionContext)
auto context=SampleUniquePtr<nvinfer1::IExecutionContext>(mEngine->createExecutionContext());
if(!context){
return sample::Logger::TestResult::kFAILED;
}
// 遍历 engine 中的所有 I/O Tensor(包括输入和输出)
for(int32_t i=0,e=mEngine->getNbIOTensors();i<e;i++){
// 获取第 i 个 I/O tensor 的名字
const auto name=mEngine->getIOTensorName(i);
// 将该 tensor 绑定到对应的 GPU 设备内存地址
context->setTensorAddress(name,buffers.getDeviceBuffer(name));
}
// 读取并准备输入数据,写入 host buffer
if(!prepareInput(buffers)){
return sample::Logger::TestResult::kFAILED;
}
// 创建一个 CUDA stream,用于异步执行本次推理
cudaStream_t stream;
CHECK(cudaStreamCreate(&stream));
// 异步将输入数据从 Host buffer 拷贝到 Device buffer
buffers.copyInputToDeviceAsync(stream);
// 将推理任务异步加入到 CUDA stream 中执行
if (!context->enqueueV3(stream))
{
return sample::Logger::TestResult::kFAILED;
}
// 异步将输出数据从 Device buffer 拷贝回 Host buffer
buffers.copyOutputToHostAsync(stream);
// 等待 stream 中的所有异步操作完成
CHECK(cudaStreamSynchronize(stream));
// 销毁 CUDA stream
CHECK(cudaStreamDestroy(stream));
// 校验并打印推理输出结果
return verifyOutput(buffers) ? sample::Logger::TestResult::kRUNNING : sample::Logger::TestResult::kFAILED;
}
这个成员函数里出现的所有API我都有讲过,可以自行去我的历史文章里翻找
其他有关参数的函数和main函数
cpp
//!
//! \brief Used to clean up any state created in the sample class
//!
sample::Logger::TestResult SampleINT8API::teardown()
{
return sample::Logger::TestResult::kRUNNING;
}
//!
//! \brief The SampleINT8APIArgs structures groups the additional arguments required by
//! the INT8 API sample
//!
struct SampleINT8APIArgs : public samplesCommon::Args
{
bool verbose{false};
bool writeNetworkTensors{false};
std::string modelFileName{"resnet50.onnx"};
std::string imageFileName{"airliner.ppm"};
std::string referenceFileName{"reference_labels.txt"};
std::string dynamicRangeFileName{"resnet50_per_tensor_dynamic_range.txt"};
std::string networkTensorsFileName{"network_tensors.txt"};
};
//! \brief This function parses arguments specific to SampleINT8API
//!
bool parseSampleINT8APIArgs(SampleINT8APIArgs& args, int argc, char* argv[])
{
for (int i = 1; i < argc; ++i)
{
if (!strncmp(argv[i], "--model=", 8))
{
args.modelFileName = (argv[i] + 8);
}
else if (!strncmp(argv[i], "--image=", 8))
{
args.imageFileName = (argv[i] + 8);
}
else if (!strncmp(argv[i], "--reference=", 12))
{
args.referenceFileName = (argv[i] + 12);
}
else if (!strncmp(argv[i], "--write_tensors", 15))
{
args.writeNetworkTensors = true;
}
else if (!strncmp(argv[i], "--network_tensors_file=", 23))
{
args.networkTensorsFileName = (argv[i] + 23);
}
else if (!strncmp(argv[i], "--ranges=", 9))
{
args.dynamicRangeFileName = (argv[i] + 9);
}
else if (!strncmp(argv[i], "--int8", 6))
{
args.runInInt8 = true;
}
else if (!strncmp(argv[i], "--fp16", 6))
{
args.runInFp16 = true;
}
else if (!strncmp(argv[i], "--useDLACore=", 13))
{
args.useDLACore = std::stoi(argv[i] + 13);
}
else if (!strncmp(argv[i], "--data=", 7))
{
std::string dirPath = (argv[i] + 7);
if (dirPath.back() != '/')
{
dirPath.push_back('/');
}
args.dataDirs.push_back(dirPath);
}
else if (!strncmp(argv[i], "--timingCacheFile=", 18))
{
args.timingCacheFile = (argv[i] + 18);
}
else if (!strncmp(argv[i], "--verbose", 9) || !strncmp(argv[i], "-v", 2))
{
args.verbose = true;
}
else if (!strncmp(argv[i], "--help", 6) || !strncmp(argv[i], "-h", 2))
{
args.help = true;
}
else
{
sample::gLogError << "Invalid Argument: " << argv[i] << std::endl;
return false;
}
}
return true;
}
void validateInputParams(SampleINT8APIParams& params)
{
sample::gLogInfo << "Please follow README.md to generate missing input files." << std::endl;
sample::gLogInfo << "Validating input parameters. Using following input files for inference." << std::endl;
params.modelFileName = locateFile(params.modelFileName, params.dataDirs);
sample::gLogInfo << " Model File: " << params.modelFileName << std::endl;
if (params.writeNetworkTensors)
{
sample::gLogInfo << " Writing Network Tensors File to: " << params.networkTensorsFileName << std::endl;
return;
}
params.imageFileName = locateFile(params.imageFileName, params.dataDirs);
sample::gLogInfo << " Image File: " << params.imageFileName << std::endl;
params.referenceFileName = locateFile(params.referenceFileName, params.dataDirs);
sample::gLogInfo << " Reference File: " << params.referenceFileName << std::endl;
params.dynamicRangeFileName = locateFile(params.dynamicRangeFileName, params.dataDirs);
sample::gLogInfo << " Dynamic Range File: " << params.dynamicRangeFileName << std::endl;
return;
}
//!
//! \brief This function initializes members of the params struct using the command line args
//!
SampleINT8APIParams initializeSampleParams(SampleINT8APIArgs args)
{
SampleINT8APIParams params;
if (args.dataDirs.empty()) // Use default directories if user hasn't provided directory paths
{
params.dataDirs.push_back("data/samples/int8_api/");
params.dataDirs.push_back("data/int8_api/");
}
else // Use the data directory provided by the user
{
params.dataDirs = args.dataDirs;
}
params.dataDirs.push_back(""); // In case of absolute path search
params.verbose = args.verbose;
params.modelFileName = args.modelFileName;
params.imageFileName = args.imageFileName;
params.referenceFileName = args.referenceFileName;
params.dynamicRangeFileName = args.dynamicRangeFileName;
params.dlaCore = args.useDLACore;
params.writeNetworkTensors = args.writeNetworkTensors;
params.networkTensorsFileName = args.networkTensorsFileName;
params.timingCacheFile = args.timingCacheFile;
validateInputParams(params);
return params;
}
//!
//! \brief This function prints the help information for running this sample
//!
void printHelpInfo()
{
std::cout << "Usage: ./sample_int8_api [-h or --help] [--model=model_file] "
"[--ranges=per_tensor_dynamic_range_file] [--image=image_file] [--reference=reference_file] "
"[--data=/path/to/data/dir] [--useDLACore=<int>] [-v or --verbose] "
"[--timingCacheFile=timing_cache_file]\n";
std::cout << "-h or --help. Display This help information" << std::endl;
std::cout << "--model=model_file.onnx or /absolute/path/to/model_file.onnx. Generate model file using README.md in "
"case it does not exists. Default to resnet50.onnx"
<< std::endl;
std::cout << "--image=image.ppm or /absolute/path/to/image.ppm. Image to infer. Defaults to airlines.ppm"
<< std::endl;
std::cout << "--reference=reference.txt or /absolute/path/to/reference.txt. Reference labels file. Defaults to "
"reference_labels.txt"
<< std::endl;
std::cout << "--ranges=ranges.txt or /absolute/path/to/ranges.txt. Specify custom per-tensor dynamic range for the "
"network. Defaults to resnet50_per_tensor_dynamic_range.txt"
<< std::endl;
std::cout << "--write_tensors. Option to generate file containing network tensors name. By default writes to "
"network_tensors.txt file. To provide user defined file name use additional option "
"--network_tensors_file. See --network_tensors_file option usage for more detail."
<< std::endl;
std::cout << "--network_tensors_file=network_tensors.txt or /absolute/path/to/network_tensors.txt. This option "
"needs to be used with --write_tensors option. Specify file name (will write to current execution "
"directory) or absolute path to file name to write network tensor names file. Dynamic range "
"corresponding to each network tensor is required to run the sample. Defaults to network_tensors.txt"
<< std::endl;
std::cout << "--data=/path/to/data/dir. Specify data directory to search for above files in case absolute paths to "
"files are not provided. Defaults to data/samples/int8_api/ or data/int8_api/"
<< std::endl;
std::cout << "--useDLACore=N. Specify a DLA engine for layers that support DLA. Value can range from 0 to n-1, "
"where n is the number of DLA engines on the platform."
<< std::endl;
std::cout << "--timingCacheFile=functional.cache or /absolute/path/to/functional.cache. Specify path for timing "
"cache file. If it does not already exist, it will be created. Defaults to not using a timing cache."
<< std::endl;
std::cout << "--verbose. Outputs per-tensor dynamic range and layer precision info for the network" << std::endl;
}
int main(int argc, char** argv)
{
SampleINT8APIArgs args;
bool argsOK = parseSampleINT8APIArgs(args, argc, argv);
if (!argsOK)
{
sample::gLogError << "Invalid arguments" << std::endl;
printHelpInfo();
return EXIT_FAILURE;
}
if (args.help)
{
printHelpInfo();
return EXIT_SUCCESS;
}
if (args.verbose)
{
sample::gLogger.setReportableSeverity(nvinfer1::ILogger::Severity::kVERBOSE);
}
auto sampleTest = sample::gLogger.defineTest(gSampleName, argc, argv);
sample::gLogger.reportTestStart(sampleTest);
SampleINT8APIParams params;
params = initializeSampleParams(args);
SampleINT8API sample(params);
sample::gLogInfo << "Building and running a INT8 GPU inference engine for " << params.modelFileName << std::endl;
auto buildStatus = sample.build();
if (buildStatus == sample::Logger::TestResult::kWAIVED)
{
return sample::gLogger.reportWaive(sampleTest);
}
else if (buildStatus == sample::Logger::TestResult::kFAILED)
{
return sample::gLogger.reportFail(sampleTest);
}
if (sample.infer() != sample::Logger::TestResult::kRUNNING)
{
return sample::gLogger.reportFail(sampleTest);
}
if (sample.teardown() != sample::Logger::TestResult::kRUNNING)
{
return sample::gLogger.reportFail(sampleTest);
}
return sample::gLogger.reportPass(sampleTest);
}
执行
执行该代码需要用到一个训练好的onnx模型,和需要量化层的数值范围,官方都已经发布,详见
https://github.com/NVIDIA/TensorRT/releases/download/v10.14/tensorrt_sample_data_20251106.zip
执行命令示例
bash
./my_cuda_program --model=../resnet50/ResNet50.onnx \
--image=../int8_api/airliner.ppm \
--reference=../int8_api/reference_labels.txt \
--ranges=../int8_api/resnet50_per_tensor_dynamic_range.txt \
--verbose
会打印许多日志,在成功量化的层会打印类似如下信息
bash
Layer(CaskPooling): node_of_gpu_0/pool5_1, Tactic: 0xa3a1a62d21de759d, gpu_0/res5_2_branch2c_bn_3 (Int8[1,2048:32,7,7]) -> gpu_0/pool5_1 (Int8[1,2048:32,1,1])
bash
[01/06/2026-19:01:37] [I] [TRT] [MemUsageStats] Peak memory usage of TRT CPU/GPU memory allocators: CPU 2 MiB, GPU 74 MiB
[01/06/2026-19:01:37] [V] [TRT] Adding 1 engine(s) to plan file.
[01/06/2026-19:01:37] [I] [TRT] [MemUsageStats] Peak memory usage during Engine building and serialization: CPU: 3453 MiB
[01/06/2026-19:01:37] [I] [TRT] Loaded engine size: 27 MiB
[01/06/2026-19:01:37] [V] [TRT] Deserialization required 43827 microseconds.
[01/06/2026-19:01:37] [I] Found input: gpu_0/data_0 shape=1x3x224x224 dtype=0
[01/06/2026-19:01:37] [I] Found output: gpu_0/softmax_1 shape=1x1000 dtype=0
[01/06/2026-19:01:38] [V] [TRT] Total per-runner device persistent memory is 22528
[01/06/2026-19:01:38] [V] [TRT] Total per-runner host persistent memory is 281920
[01/06/2026-19:01:38] [V] [TRT] Allocated device scratch memory of size 2408448
[01/06/2026-19:01:38] [V] [TRT] - Runner scratch: 2408448 bytes
[01/06/2026-19:01:38] [I] [TRT] [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +3, now: CPU 0, GPU 27 (MiB)
[01/06/2026-19:01:38] [V] [TRT] CUDA lazy loading is enabled.
[01/06/2026-19:01:38] [I] SampleINT8API result: Detected:
[01/06/2026-19:01:38] [I] [1] airliner
[01/06/2026-19:01:38] [I] [2] warplane
[01/06/2026-19:01:38] [I] [3] projectile
[01/06/2026-19:01:38] [I] [4] space shuttle
[01/06/2026-19:01:38] [I] [5] wing
输出的五个类别代表在SampleINT8API::verifyOutput里这五个是最高概率的标签