BERT 模型到 TensorRT 的转换

总体流程

1. 准备环境:安装TensorRT和相关依赖库,确保环境可用。

2. 获取预训练BERT模型:可以直接下载官方提供的PyTorch模型,或训练自定义BERT模型。

3. 模型验证:在PyTorch环境中加载模型,给定样本输入,验证模型输出正确性。

4. 导出ONNX模型:使用torch.onnx.export()将PyTorch模型导出为ONNX格式。要注意输入输出格式。

5. 简化ONNX模型:使用onnx-simplifier等工具对ONNX模型进行优化和简化。这一步可以省略。

6. 导入TensorRT:使用TensorRT的API(Python或C++)加载ONNX模型。

7. 配置TensorRT引擎:设置最大batchsize、workspace大小等参数。开启FP16精度模式。

8. 生成序列化引擎:使用trt.Builder的build_serialized_network()方法生成序列化后的TensorRT引擎。

9. 反序列化和验证:在另一个Python脚本中加载序列化后的引擎,验证模型输出正确性。

10. 制作数据输入:根据模型输入格式,准备calib数据集或随机生成输入tensor。

11. TRT模型校准:运行trt.Calibrator对引擎进行校准,生成量化后的TensorRT引擎。

12. TRT模型评估:用calib数据集进行多轮测试,记录推理时间,计算加速比。与PyTorch原模型对比。

13. 模型部署:将量化后的TensorRT引擎保存为文件,以用于实际部署。可以编译为C++引擎。

14. 结果分析:分析日志,整理转换前后模型精度和速度指标,总结优化经验。

详解:

1. 准备工作- 安装TensorRT库及依赖
- 确保CUDA可用
- 准备PyTorch环境,安装onnx等模块
- 获取预训练BERT模型或训练自定义BERT
- 设定评测设备(GPU或CPU)

2. PyTorch模型检查- 在PyTorch环境中加载BERT模型
- 给定样本输入,获取模型输出
- 检查输出结果正确性
- 分析模型参数大小、浮点运算量等

3. 导出ONNX模型- 使用torch.onnx.export()导出模型
- 输入需是torch.Tensor,输出也为tensor
- 检查ONNX模型输入输出和PyTorch一致
- 可视化ONNX模型结构,分析层信息

import torch
from torch.nn import functional as F
import numpy as np
import os
from transformers import BertTokenizer, BertForMaskedLM
import logging

# import onnxruntime as ort
import transformers
import time

current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())

print("pytorch:", torch.__version__)
# print("onnxruntime version:", ort.__version__)
# print("onnxruntime device:", ort.get_device())
print("transformers:", transformers.__version__)

BERT_PATH = 'bert-base-uncased'

logging.basicConfig(filename='test.log', level=logging.DEBUG)
'''
logging.basicConfig(level=logging.DEBUG)这行代码是用于设置logging模块的日志级别和基本配置的。
具体来说:
- logging是Python的日志模块。使用该模块可以方便地进行日志记录和调试。
- basicConfig是logging模块中的一个函数,用于进行日志的基本配置。
- level=logging.DEBUG 这部分是设置日志级别为DEBUG。
logging模块定义了多个日志级别,按严重程度从低到高为:
- DEBUG:调试信息,日志量大,用于追踪程序的所有流程。
- INFO:信息提示,确认程序按预期运行。
- WARNING:警告信息,可能会有潜在问题,但程序还能执行。
- ERROR:错误信息,程序无法按预期执行。
- CRITICAL:严重错误,程序可能无法继续运行。
通过设置level参数,可以控制打印哪些级别的日志信息。
setLevel为DEBUG时,会打印所有级别的日志,这对调试程序非常有用。
总结一下:
1. logging.basicConfig配置日志模块
2. 设置level=logging.DEBUG将日志级别设置为最低级别DEBUG
3. 这样可以打印调试程序过程中的所有日志信息,方便调试和追踪程序执行流程
'''

def model_test(model, tokenizer, text):
    #print("==============model test===================")
    logging.info("==============model test===================")
    encoded_input = tokenizer.encode_plus(text, return_tensors = "pt")
    '''
    1. tokenizer.encode_plus()会返回一个字典,包含:
    - input_ids: 输入序列的数字化表示
    - token_type_ids: 对于每个输入元组的第一个和第二个序列的区分(对BERT等双序列模型需要)
    - attention_mask: 指定对哪些词元进行self-attention操作的掩码(可选择性返回)
    2. tokenizer.encode()只返回输入的数字化表示input_ids,不返回其他信息。
    '''
    # encoded_input = {k: v.unsqueeze(0) for k,v in encoded_input.items()}
    '''
    这行使用tokenizer对文本进行编码,返回的encoded_input是一个字典,包含 input_ids, token_type_ids等字段。
    然后这行:
    encoded_input = {k: v.unsqueeze(0) for k,v in encoded_input.items()}
    是将encoded_input中的每个tensor进行unsqueeze(0)操作,即在tensor的第一个维度上增加一个大小为1的维度。
    因为在PyTorch中,batch size对应的是tensor的第一个维度。通过unsqueeze(0),可以将batch size从原来的不定,变为1。
    举个例子,输入的input_ids大小可能原来是(5, 20)(5是batch size,20是序列长度)。
    经过unsqueeze(0)后,大小变为(1, 5, 20),其中第一个维度被增加为1,即batch size变为1。
    这样做的目的是为了固定batch size,之后测试模型的时候输入是一个固定batch size的tensor,方便调试。
    unsqueeze(0)后返回的仍是一个字典,字典的key保持不变,value是进行过unsqueeze的tensor。
    所以这行代码的作用就是:
    1. 对编码后的输入进行遍历,遍历字典中的每一个tensor
    2. 对每个tensor进行unsqueeze(0),以固定batch size为1
    3. 将处理后的tensor重新组成字典,赋值给encoded_input
    '''
    mask_index = torch.where(encoded_input["input_ids"][0] == tokenizer.mask_token_id)
    #mask_index = torch.where(encoded_input["input_ids"] == tokenizer.mask_token_id)
    '''
    之所以这样修改,是因为修改后的代码设置了batch size为1,所以encoded_input["input_ids"]中只有一个示例,不需要再取[0]。
    取[0]主要是为了兼容batch size大于1的情况,但现在batch size固定为1,所以可以简化代码,直接判断整个tensor。
    另外判断一个tensor中哪些位置等于某值,torch.where()是比较方便的操作。
    总结一下:
    1. 原代码中需要取[0],是为了兼容batch size大于1的情况
    2. 修改后代码固定了batch size为1,所以可以简化判断,不再需要取[0]
    3. torch.where可以直接判断tensor中哪些位置符合条件,很方便实现mask token的索引查找
    4. 这种修改使代码更简洁,同时也更符合batch size为1的情况
    '''

    output = model(**encoded_input)
    #print(output[0].shape)
    logging.debug(f"Output shape: {output[0].shape}")

    logits = output.logits
    softmax = F.softmax(logits, dim = -1)
    mask_word = softmax[0, mask_index, :]
    top_10 = torch.topk(mask_word, 10, dim = 1)[1][0]
    # print("model test topk10 output:")
    logging.info("model test topk10 output:")
    for token in top_10:
        word = tokenizer.decode([token])
        new_sentence = text.replace(tokenizer.mask_token, word)
        print(new_sentence)

    # save inputs and output
    # print("Saving inputs and output to case_data.npz ...")
    logging.info("Saving inputs and output to case_data.npz ...")
    position_ids = torch.arange(0, encoded_input['input_ids'].shape[1]).int().view(1, -1)
    #position_ids = torch.arange(0, encoded_input['input_ids'].shape[1]).unsqueeze(0).int()
    '''
    主要区别在于使用view和unsqueeze的不同。
    view是对tensor进行reshape,可能会造成原tensor被覆盖。
    而unsqueeze是返回一个新的tensor,对原tensor无影响。
    之所以修改成unsqueeze,主要有以下几点考虑:
    1. unsqueeze更安全,不会改变原tensor,生成的是新的tensor。
    2. 这里的position_ids只需要增加一个维度,使其size为(1, seq_len),unsqueeze可以很方便地做到。
    3. view需要指定-1这个特殊大小,unsqueeze无需指定size。
    4. unsqueeze更符合“增加维度”这个操作的语义。
    所以修改成unsqueeze可以:
    1. 生成一个新的安全的tensor,不影响原tensor
    2. 简化代码,不需要指定-1这个特殊大小
    3. 更符合逻辑地增加维度
    '''
    # print(position_ids)
    logging.debug(f"Position ids shape: {position_ids.shape}")

    input_ids=encoded_input['input_ids'].int().detach().numpy()
    '''
    这行代码是将PyTorch的tensor转换为numpy数组的过程:
1. encoded_input['input_ids'] 取出编码后的input_ids的tensor
2. .int() 将tensor转换为整数类型(int32)
3. .detach() 将这个tensor与计算图分离,即得到一个纯tensor,不再与计算图相关
4. .numpy() 将这个detach的整数tensor转换为numpy数组

需要注意的是,由于默认PyTorch tensor是int32,转换的numpy数组也是int32。
如果模型需要int64类型,这里就需要额外处理,比如:
input_ids = encoded_input['input_ids'].int().to(torch.int64).detach().numpy().astype(np.int64)
先转换为int64 tensor,再转为int64 numpy数组。
在后面ONNXRuntime推理时就出现了这个问题,从npz加载的数据为int32,模型的输入类型定义为int64
    '''
    logging.debug(f"Input_ids: {input_ids}")
    token_type_ids=encoded_input['token_type_ids'].int().detach().numpy()
    logging.debug(f"Token_type_ids: {token_type_ids}")
    # print(input_ids.shape)
    logging.debug(f"Input ids shape: {input_ids.shape}")
    # save data
    npz_file = BERT_PATH + '/case_data.npz'
    np.savez(npz_file,
             input_ids=input_ids,
             token_type_ids=token_type_ids,
             position_ids=position_ids,
             logits=output[0].detach().numpy())

    data = np.load(npz_file)
    # print(data['input_ids'])
    logging.debug(f"Saved input ids: {data['input_ids']}")

def model2onnx(model, tokenizer, text):
    # print("===================model2onnx=======================")
    logging.info("===================model2onnx=======================")
    encoded_input = tokenizer.encode_plus(text, return_tensors = "pt")
    # encoded_input = {k: v.unsqueeze(0) for k,v in encoded_input.items()}
    # print(encoded_input)
    logging.debug(f"Encoded input: \n {encoded_input}")
    logging.debug(f"tuple(encoded_input.values()): \n {tuple(encoded_input.values())}")

    # convert model to onnx
    model.eval()
    export_model_path = BERT_PATH + "/model.onnx"
    opset_version = 12
    #batch_size = 1
    symbolic_names = {0: 'batch_size', 1: 'max_seq_len'}
    '''
    dynamic_axes的格式是:
    {输出张量名: {轴编号: 轴名称}}
    dynamic_axes中的数字表示固定大小,字符串表示动态轴。
    '''
    # input_names = ['input_ids', 'attention_mask', 'token_type_ids']
    # input_shapes = [encoded_input[name].shape for name in input_names]
    # shape_tuples = [tuple(shape) for shape in input_shapes]
    torch.onnx.export(model,                                            # model being run
                      args = tuple(encoded_input.values()),                      # model input (or a tuple for multiple inputs)
                      f=export_model_path,                              # where to save the model (can be a file or file-like object)
                      opset_version=opset_version,                      # the ONNX version to export the model to
                      do_constant_folding=False,                         # whether to execute constant folding for optimization
                      input_names=['input_ids',                         # the model's input names
                                   'attention_mask',
                                   'token_type_ids'],
                    output_names=['logits'],                    # the model's output names
                    dynamic_axes={'input_ids': symbolic_names,        # variable length axes
                                  'attention_mask' : symbolic_names,
                                  'token_type_ids' : symbolic_names,
                                  'logits' : symbolic_names})
    #print("Model exported at ", export_model_path)
    logging.info("Model exported at " + export_model_path)

if __name__ == '__main__':

    if not os.path.exists(BERT_PATH):
        print(f"Download {BERT_PATH} model first!")
        assert(0)
    logging.info("---------------------------------------------------------------------------------------------")
    logging.info(current_time)
    tokenizer = BertTokenizer.from_pretrained(BERT_PATH)
    model = BertForMaskedLM.from_pretrained(BERT_PATH, return_dict = True)
    text = "The capital of France, " + tokenizer.mask_token + ", contains the Eiffel Tower."
    # bias = model.cls.predictions.bias.data
    # logging.info(f"bias: {bias.shape}")
    # bias = torch.unsqueeze(bias, dim=0)
    # bias = torch.nn.Parameter(bias)
    # logging.info(f"n_bias: {bias.shape}")
    # model.cls.predictions.bias = bias

    model_test(model, tokenizer, text)
    model2onnx(model, tokenizer, text)

4. 优化ONNX模型- 使用onnx-simplifier等工具简化模型
- 删除冗余/无用节点,减小模型大小
- 可选:使用ONNXRuntime测试简化后模型

import onnxsim
import onnx
import onnxruntime as ort
import os
import logging
import time
import numpy as np
import torch
from transformers import BertTokenizer


def model_simplify(model_path, model_simp_path):

    model = onnx.load(model_path)
    model_simp, check = onnxsim.simplify(model)
    onnx.save(model_simp, model_simp_path)
    logging.info("Model exported at " + model_simp_path)
    # 把输入都打印出来
    for i in range(len(model.graph.input)):
        logging.debug(f"input[{i}]: {model.graph.input[i].type.tensor_type.shape}")
    for i in range(len(model_simp.graph.input)):
        logging.debug(f"input[{i}]: {model_simp.graph.input[i].type.tensor_type.shape}")
    # 把输出都打印出来
    logging.debug(model.graph.output[0].type.tensor_type.shape)
    logging.debug(model_simp.graph.output[0].type.tensor_type.shape)

    # logging.debug(f"bias: {model.cls.predictions.bias.data.shape}")
    # logging.debug(f"bias_sim: {model.cls.predictions.bias.data.shape}")
    
    logging.info("尺寸对比...")
    model_size = os.path.getsize(model_path)
    model_simp_size = os.path.getsize(model_simp_path)
    logging.debug(f"Simplified ONNX model from {model_size} to {model_simp_size} bytes")
    
class ortInfer:

    def __init__(self, model_path, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']):
        self.model_path = model_path
        self.providers = providers
        self.session = None

    def load_model(self):
        try:
            self.session = ort.InferenceSession(self.model_path, providers=self.providers)
        except Exception as e:
            logging.debug(f"Failed to load model: {e}")
            self.session = None

    def check_input(self, input_dataA, input_dataB, input_dataC, dtype):
        is_match = (input_dataA.dtype == dtype and 
                    input_dataB.dtype == dtype and
                    input_dataC.dtype == dtype )
        return is_match

    def release(self):
        if self.session:
            self.session.release()

    def onnxRuntime_gpu(self):
        data = np.load("bert-base-uncased/case_data.npz", allow_pickle=True)
        # np.load()注意检查加载数据类型,可能出现类型转换
        '''
        encoded_input = tokenizer.encode_plus(text, return_tensors="pt")
        input_ids = encoded_input["input_ids"]
        attention_mask = encoded_input["attention_mask"]
        token_type_ids = encoded_input["token_type_ids"]
        '''
        input_ids = data['input_ids']
        # 构造attention_mask
        atten_mask = np.where(input_ids == 0, 0, 1) 
        # 保持shape一致
        attention_mask = atten_mask.reshape(input_ids.shape)
        token_type_ids = data['token_type_ids']

        # 这里检查出数据类型不对
        logging.debug(f"input_ids.dtype: {input_ids.dtype}")
        logging.debug(f"attention_mask.dtype: {attention_mask.dtype}")
        logging.debug(f"token_type_ids.dtype: {token_type_ids.dtype}")
        # 类型转换
        input_ids = input_ids.astype(np.int64)
        # attention_mask = attention_mask.astype(np.int64) 
        token_type_ids = token_type_ids.astype(np.int64)

        if not self.check_input(input_ids, attention_mask, token_type_ids, np.int64):
            logging.debug(f"input_ids.dtype: {input_ids.dtype}")
            logging.debug(f"attention_mask.dtype: {attention_mask.dtype}")
            logging.debug(f"token_type_ids.dtype: {token_type_ids.dtype}")
        
            raise ValueError("Input dada type mismatch!")

        
        # 对于bert模型,主要的输入是token_ids和attention_mask,输出是推理结果logits
        #ort.set_default_logger_severity(3) 
        self.load_model()

        start = time.time()
        outputs = self.session.run(None, {"input_ids": input_ids,
                                    "attention_mask": attention_mask,
                                    "token_type_ids": token_type_ids
                                    })
        end = time.time()  
        for output in outputs:        
            logging.debug(f"output.shape: {output.shape}    output.ndim: {output.ndim}")
        # logits = outputs[0]
        # softmax = torch.softmax(torch.tensor(logits), dim=-1)  
        # values, indices = torch.topk(softmax, 5)

        # for i in indices[0]:
        #     logging.info(tokenizer.decode([i])) 

        name = (((self.model_path.split('/'))[1]).split('.'))[0]
        Tort = end - start
        logging.info(self.session.get_providers())
        # self.session.get_default_device()
        # logging.info(name + "by ONNX Runtime GPU inference time:" + Tort)
        logging.info("{} by ONNX Runtime GPU inference time: {}s".format(name, Tort))

'''
case_data.npz:
- 这是在PyTorch模型测试时保存输入输出的文件,使用numpy的npz格式保存。
- 里面包含了input_ids、token_type_ids、position_ids和模型输出logits。
- 这些数据可以作为ONNX Runtime推理的输入。
data:
- 通过numpy.load()加载case_data.npz文件,得到一个字典。
- 里面包含输入和输出的numpy数组。
input_ids:
- BERT模型输入中的一个,对应输入文本的id化结果。
- shape类似(1,序列长度),1是batch_size。
token_type_ids:
- BERT模型输入中的另一个,标识token属于句子1还是2。
- shape同input_ids。
outputs:
- ONNX Runtime推理后的模型输出,是一个list。
- 里面按序包含了模型所有的输出tensor。
logits:
- outputs[0],也就是模型输出的第一个tensor。
- 对应BERT模型推理后的logits结果,后续可以通过该数组得到预测结果。
- shape类似(1,序列长度,词表大小)。
举个输入数据的例子:

input_ids = [[101, 2054, 2003, ...]]  
# 输入序列token id

token_type_ids = [[0, 0, 0, ...]]
# 全部为句子1  

position_ids = [[0, 1, 2, ...]] 
# 对应位置

outputs = [logits_array, ...]  
# 模型输出 

logits = logits_array
# shape (1, 序列长度, 词表大小)
'''

'''
set_default_logger_severity方法可以设置ONNX Runtime的默认日志级别。
日志级别包括:
0 - ORT_LOGGING_LEVEL_VERBOSE
1 - ORT_LOGGING_LEVEL_INFO
2 - ORT_LOGGING_LEVEL_WARNING
3 - ORT_LOGGING_LEVEL_ERROR
4 - ORT_LOGGING_LEVEL_FATAL
数字越大,日志级别越高。
ort.set_default_logger_severity(3)表示设置ONNX Runtime的日志级别为ORT_LOGGING_LEVEL_ERROR。
也就是说,只会打印错误和致命级别的日志信息,屏蔽信息、警告和详细日志。
这通常在生产环境中使用,可以避免输出过多无用的运行日志,只打印错误信息。
我们也可以根据需要设置其他级别,如:
python
# 打印所有详细日志
ort.set_default_logger_severity(0) 

# 只打印错误
ort.set_default_logger_severity(3)  

# 完全不打印日志
ort.set_default_logger_severity(4)
'''

if __name__ == '__main__':
    current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    logging.basicConfig(filename='simp.log', level=logging.DEBUG)

    model_path = 'bert-base-uncased/model.onnx'
    model_simp_path = 'bert-base-uncased/model_simp.onnx'
    logging.info("---------------------------------------------------------------------------------------------")
    logging.info(current_time)

    model_simplify(model_path, model_simp_path)

    model_simp = ortInfer(model_simp_path)
    model_simp.onnxRuntime_gpu()

    model = ortInfer(model_path)
    model.onnxRuntime_gpu()

5. 引入TensorRT- 加载TensorRT Python API
- 从ONNX模型导入TensorRT网络
- 打印网络层信息,与ONNX对比
- 为Builder配置工作参数(batchsize等)

6. 构建TensorRT引擎- 使用Builder构建引擎
- 生成序列化模型,并保存为文件
- 在新的脚本中反序列化加载
- 重新检查模型输出正确性

#include <fstream> 
#include <iostream> 
#include <memory>
 
#include <NvInfer.h> 
#include <NvOnnxParser.h> 

 
using namespace nvinfer1; 
using namespace nvonnxparser; 



class Logger : public nvinfer1::ILogger{
public:
    void log(Severity severity, const char* msg) noexcept override {
        // suppress info-level messages
        if (severity != Severity::kINFO)
            std::cout << msg << std::endl;
    }
} gLogger;

int main(int argc, char** argv) 
{ 
        // Create builder 
        // std::unique_ptr<ICudaEngine> engine{nullptr};
        IBuilder* builder = nvinfer1::createInferBuilder(gLogger); 
        const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH); 
        IBuilderConfig* config = builder->createBuilderConfig(); 

        // Create model to populate the network 
        INetworkDefinition* network = builder->createNetworkV2(explicitBatch); 
 
        // Parse ONNX file 
        IParser* parser = nvonnxparser::createParser(*network, gLogger); 
        bool parser_status = parser->parseFromFile("../bert-base-uncased/model_simp.onnx", static_cast<int>(ILogger::Severity::kWARNING)); 
 
        // Get the name of network input 
        Dims dim = network->getInput(0)->getDimensions(); 
        if (dim.d[0] == -1 && dim.nbDims > 0)  // -1 means it is a dynamic model 
        { 
                const char* name0 = network->getInput(0)->getName(); 
                IOptimizationProfile* profile = builder->createOptimizationProfile(); 
                
                profile->setDimensions(name0, OptProfileSelector::kMIN, Dims2(1, 6)); 
                profile->setDimensions(name0, OptProfileSelector::kOPT, Dims2(1, 64)); 
                profile->setDimensions(name0, OptProfileSelector::kMAX, Dims2(1, 256)); 
                
                const char* name1 = network->getInput(1)->getName(); 

                profile->setDimensions(name1, OptProfileSelector::kMIN, Dims2(1, 6)); 
                profile->setDimensions(name1, OptProfileSelector::kOPT, Dims2(1, 64)); 
                profile->setDimensions(name1, OptProfileSelector::kMAX, Dims2(1, 256)); 
                
                const char* name2 = network->getInput(2)->getName(); 

                profile->setDimensions(name2, OptProfileSelector::kMIN, Dims2(1, 6)); 
                profile->setDimensions(name2, OptProfileSelector::kOPT, Dims2(1, 64)); 
                profile->setDimensions(name2, OptProfileSelector::kMAX, Dims2(1, 256)); 
                config->addOptimizationProfile(profile); 
        }    
 
        // Build engine 
        config->setMaxWorkspaceSize(10000 << 20); 
        // ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config); 
        // engine.reset(builder->buildEngineWithConfig(*network, *config));
 
        // Serialize the model to engine file 
        IHostMemory* serialized_engine{ nullptr }; 
        // assert(engine != nullptr); 
        serialized_engine = builder->buildSerializedNetwork(*network, *config);; 

        // config->setEngineCapability(nvinfer1::EngineCapability::kSAFETY);
        // config->setMemoryPoolLimit(nvinfer1::MemoryPoolType::kWORKSPACE, 1<<40); // 10GB workspace
        // nvinfer1::IHostMemory* serialized_engine = builder->buildSerializedNetwork(*network, *config);

        std::ofstream output("model.plan", std::ios::binary);
        if (!output) {
                std::cerr << "Failed to open plan file: " << "model.plan" << std::endl;
                return -1;
        }
        // 通过reinterpret_cast将数据指针转换为const char*
        // 将序列化后的引擎数据写入文件,写入的数据来自serialized_engine->data(),大小为serialized_engine->size()
        output.write(reinterpret_cast<const char*>(serialized_engine->data()), serialized_engine->size());
        std::cout << "generate file success!" << std::endl;
        output.close();

        // Release resources 
        delete serialized_engine;
        // delete engine;
        delete config;
        delete parser;
        delete network;
        delete builder;
        // modelStream->destroy();
        // engine->destroy();
        // config->destroy();
        // parser->destroy();
        // network->destroy();
        // builder->destroy();

        return 0; 
}

7. 模型校准- 准备校准数据集,或生成随机输入
- 使用EntropyCalibrator进行INT8校准
- 校准后生成量化的TensorRT引擎

8. 模型评估- 用校准数据集进行多轮测试
- 统计平均推理时间
- 计算与PyTorch模型的加速比
- 保存量化后的TensorRT引擎

9. 模型部署- 将TensorRT引擎封装为Python接口
- 可选:用C++ API加载引擎,编译为.so库
- 在实际环境中部署TensorRT模型

10. 结果分析- 整理日志,生成统计报告 
- 分析每个转换步骤的优化效果
- 总结经验教训,改进流程

注意事项:

1. 模型转换要注意输入输出格式的对应,确保ONNX模型和原始PyTorch模型的输入输出一致。

2. 在转为TensorRT模型时,可以适当调整最大batchsize、workspace大小等参数,以取得最佳的性能。

3. 可以在转TRT模型时开启FP16精度,可能会进一步提升性能。

4. TRT模型测速时,要运行足够多次取平均,以消除误差。同时要注意需要进行模型和数据的预热,避免第一次运行有额外的开销。

5. 最终模型测速要对比ONNX模型和原始PyTorch模型,看TensorRT优化的效果如何。同时也要和规定的baseline作比较。

6. 如果条件允许,可以尝试更大的序列长度,看对转换后的性能的影响。

7. 可以考虑使用Python API,因为它更方便调试和修改代码。C++代码可以在最终确定模型后使用。

8. 推荐使用onnx-simplifier等工具简化模型,可以减少不必要的运算,提高转换性能。

9. 模型转换和测速都要记录详细日志,包括每一步的时间、精度数字等,方便后期分析和改进。

10. TensorRT 在创建引擎时,默认情况下会确定网络中的所有 tensor(包括输入和输出)的形状和大小信息。但是对于输入 tensors,TensorRT 允许用户在执行时 Override 它们的形状,这就是 setBindingDimensions 的作用。而对于输出 tensors,其形状和大小是已固定的,不能再次 Override。这么做的设计考虑是:1. 输入形状可能需要根据不同的 Batch Size 进行调整,所以提供了Override的能力。2. 但输出形状应该是固定的,防止用户误操作导致内存出错。3. 引擎内部会根据输入形状自动计算输出形状和大小。这样既提供了输入形状的灵活性,也保证了输出的安全性。

PyTorch到ONNX的转换代码

1. 模型测试部分可以保留,用来验证ONNX模型转换后的正确性。

2. encode_plus()编码后的输入不需要保存在NPZ文件中,可以直接传给export接口。

3. export时输入名应该对应encode_plus的输出字典key,比如input_ids, attention_mask等。

4. 输出名logits可以更详细一些,如‘predictions’或‘output_logits’。

5. dynamic_axes参数可以设置为None,直接使用批量大小和序列长度作为常量。

6. 可以设置do_constant_folding=True开启常量折叠优化。

7. 导出后可以用onnxruntime等工具推理,和PyTorch输出对比验证正确性。

8. 可以使用onnx-simplifier等工具进一步优化ONNX模型。

9. 为了部署,可以通过--opset指定较老的opset版本,如11。

10. 需要注意ONNX模型文件大小,避免过大导致部署问题。

11. 可以参考官方示例,导出时设置训练是否为True,以及排除不需要的层。

12. 需要记录日志,保存模型转换的参数设置,方便排查问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值