TensorRT笔记(6):INT8API的使用

基础声明

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 &params):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。

  • 当网络路径完成后,该张量会变为执行张量。

注意事项

  1. 结果可靠性:仅在网络构建完成后才可靠。

  2. 与 shape tensor 区分

    • isShapeTensor() == true → 仅用于计算形状,不参与执行。

    • isExecutionTensor() == true → 会参与计算。

  3. 内存分配:只有执行张量需要 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启动低精度计算不代表输出默认是低精度的。如果不设置,下一层的输入精度可能不匹配。

特殊情况

  1. TopK 层

    • 第二个输出张量的数据类型 总是 Int32,无法修改。
  2. Shape 操作层

    • 输出类型必须为 DataType::kINT32,尝试修改为其他类型会被忽略,仅报错提示。
  3. 输出类型与张量类型不同

    • setOutputType() 只约束层输出类型,并不直接改变张量类型。

    • 如果需要修改输出张量类型,应调用:

      cpp 复制代码
      layer->getOutput(i)->setType(type);

      对于网络输出张量,只有 setType() 会影响最终绑定的数据表示。

强类型网络

  • 强类型网络不允许调用 setOutputType()

  • 输出类型只能通过 特定层的 setToType() 方法 设置:

    • 支持层:ICastLayerIDequantizeLayerIFillLayerIQuantizeLayer

参数说明

参数 说明
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)) 作为最终范围值(即只需关心最大绝对值)。

  • 要求 minmax 是有限数(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里这五个是最高概率的标签

相关推荐
im_AMBER几秒前
Leetcode 97 移除链表元素
c++·笔记·学习·算法·leetcode·链表
AI视觉网奇5 分钟前
ue 动画重定向 实战笔记2026
笔记·ue5
代码游侠5 分钟前
学习笔记——MQTT协议
开发语言·笔记·php
love530love6 分钟前
Flash Attention 2.8.3 在 Windows + RTX 3090 上成功编译与运行复盘笔记(2026年1月版)
人工智能·windows·笔记·python·flash_attn·flash attention·z-image
Aliex_git9 分钟前
性能指标笔记
前端·笔记·性能优化
@zulnger12 分钟前
python 学习笔记(异常对象)
笔记·python·学习
其美杰布-富贵-李12 分钟前
x-transformers 完整学习笔记
笔记·学习·transformer
sunfove25 分钟前
光学学习笔记:详解光通量、照度、强度与亮度
笔记·学习
clorisqqq26 分钟前
人工智能现代方法 第二章2.1-2.3节 笔记
人工智能·笔记
斯特凡今天也很帅29 分钟前
Windows CUDA12.9本地安装
windows·cuda