8.7.tensorRT高级(3)封装系列-调试方法、思想讨论

目录

    • 前言
    • 1. 模型调试技巧
    • 总结

前言

杜老师推出的 tensorRT从零起步高性能部署 课程,之前有看过一遍,但是没有做笔记,很多东西也忘了。这次重新撸一遍,顺便记记笔记。

本次课程学习 tensorRT 高级-调试方法、思想讨论

课程大纲可看下面的思维导图

在这里插入图片描述

1. 模型调试技巧

这节我们学习模型的调试技巧,debug 方法

调试法则

1. 善用 python 工作流,联合 python/cpp 一起进行问题调试(python 工作流比较完善,把 C++ 作为处理工具,Python 作为分析可视化工具)

2. 去掉前后处理情况下,确保 onnx 与 pytorch 结果一致,排除所有因素。这一点 engine 通常是能够保证的。例如都输入全为 5 的张量,必须使得输出之间差距小于 1e-4,确保中间没有例外情况发生

3. 预处理一般很难保证完全一样,考虑把 pytorch 的预处理结果储存文件,c++ 加载后推理,得到的结果应该差异小于 1e-4(尤其是写插件的时候)

4. 考虑把 python 模型推理后的结果储存为文件,先用 numpy 写一遍后处理。然后用 c++ 复现

5. 如果出现 bug,应该把 tensor 从 c++ 中储存文件后,放到 python 上调试查看。避免在 c++ 中 debug

不要急着写 C++,多用 python 调试好

在之前的多线程 yolov5 的代码中,我们在 tensor 封装中拥有两个函数,一个是 save_to_file 可以将我们的 tensor 保存成二进制文件,另一个是 load_from_file 可以从二进制文件中读入 tensor,它们的定义如下:

bool Tensor::save_to_file(const std::string& file) const{if(empty()) return false;FILE* f = fopen(file.c_str(), "wb");if(f == nullptr) return false;int ndims = this->ndims();unsigned int head[3] = {0xFCCFE2E2, ndims, static_cast<unsigned int>(dtype_)};fwrite(head, 1, sizeof(head), f);fwrite(shape_.data(), 1, sizeof(shape_[0]) * shape_.size(), f);fwrite(cpu(), 1, bytes_, f);fclose(f);return true;
}bool Tensor::load_from_file(const std::string& file){FILE* f = fopen(file.c_str(), "rb");if(f == nullptr){INFOE("Open %s failed.", file.c_str());return false;}unsigned int head[3] = {0};fread(head, 1, sizeof(head), f);if(head[0] != 0xFCCFE2E2){fclose(f);INFOE("Invalid tensor file %s, magic number mismatch", file.c_str());return false;}int ndims = head[1];auto dtype = (TRT::DataType)head[2];vector<int> dims(ndims);fread(dims.data(), 1, ndims * sizeof(dims[0]), f);this->dtype_ = dtype;this->resize(dims);fread(this->cpu(), 1, bytes_, f);fclose(f);return true;
}

save_to_file 函数用于将 Tensor 对象保存到指定的文件中,首先写入一个包含魔术数字、维度数量和数据类型的头部,接着写入 Tensor 的形状,最后写入 Tensor 的数据

load_from_file 函数则用于从指定的文件中加载 Tensor 对象,首先读取并验证文件的头部以获取 Tensor 的维度数量和数据类型,接着读取 Tensor 的形状和数据,并将这些信息设置到当前的 Tensor 对象中。

以上是 C++ 中 tensor 的保持和加载,在 python 中我们同样可以实现,具体实现如下:

import numpy as npdef load_tensor(file):with open(file, "rb") as f:binary_data = f.read()magic_number, ndims, dtype = np.frombuffer(binary_data, np.uint32, count=3, offset=0)assert magic_number == 0xFCCFE2E2, f"{file} not a tensor file."dims = np.frombuffer(binary_data, np.uint32, count=ndims, offset=3 * 4)if dtype == 0:np_dtype = np.float32elif dtype == 1:np_dtype = np.float16else:assert False, f"Unsupport dtype = {dtype}, can not convert to numpy dtype"return np.frombuffer(binary_data, np_dtype, offset=(ndims + 3) * 4).reshape(*dims)def save_tensor(tensor, file):with open(file, "wb") as f:typeid = 0if tensor.dtype == np.float32:typeid = 0elif tensor.dtype == np.float16:typeid = 1elif tensor.dtype == np.int32:typeid = 2elif tensor.dtype == np.uint8:typeid = 3head = np.array([0xFCCFE2E2, tensor.ndim, typeid], dtype=np.uint32).tobytes()f.write(head)f.write(np.array(tensor.shape, dtype=np.uint32).tobytes())f.write(tensor.tobytes())

Python 版本的实现其实和 C++ 版本没有什么区别

load_tensor 函数用于从指定的文件中读取二进制数据,首先解析头部以获取魔术数字、维度数量和数据类型,然后根据读取到的信息解析 tensor 的形状和数据,最后返回形状和数据类型都已经设置好的 numpy 数组。

save_tensor 函数则用于将 numpy 数组保存到指定的文件中,首先将魔术数字、数组的维度数量和数据类型编码为一个二进制头部,接着写入数组的形状,最后写入数组的数据。

那现在我们就来走一个流程,在 python 中保持一个 tensor,在 C++ 中进行加载,Python 代码如下:

def save_tensor(tensor, file):with open(file, "wb") as f:typeid = 0if tensor.dtype == np.float32:typeid = 0elif tensor.dtype == np.float16:typeid = 1elif tensor.dtype == np.int32:typeid = 2elif tensor.dtype == np.uint8:typeid = 3head = np.array([0xFCCFE2E2, tensor.ndim, typeid], dtype=np.uint32).tobytes()f.write(head)f.write(np.array(tensor.shape, dtype=np.uint32).tobytes())f.write(tensor.tobytes())data = np.arange(100, dtype=np.float32).reshape(10, 10, 1)
save_tensor(data, "data.tensor")

C++ 代码如下:

#include "trt-tensor.hpp"int main(){TRT::Tensor tensor;tensor.load_from_file("../data.tensor");float* ptr = tensor.cpu<float>();INFO("tensor.shape = %s, dtype = %d", tensor.shape_string(), tensor.type());for(int i = 0; i < tensor.count(); ++i){INFO("%d -> = %f", i, prt[i]);}return 0;
}

执行效果如下:

在这里插入图片描述

图1-1 python存储C++加载

可以看到结果和我们预期的一样,没有损失,我们可以把它的类型换成 uint8 再来看下,运行效果如下:

在这里插入图片描述

图1-2 python存储C++加载(uint8)

可以看到也没有问题,那这边是 python 保存 c++ 读取没有问题,接下来我们来看下 c++ 保存,python 读取

yolov5.cpp 中
214/216 行input->save_to_file("input.tensor")
output->save_to_file("output.tensor")

运行如下:

在这里插入图片描述

图1-3 C++存储

接下来我们去 python 中去加载保存的 input 和 output,代码如下:

import numpy as npdef load_tensor(file):with open(file, "rb") as f:binary_data = f.read()magic_number, ndims, dtype = np.frombuffer(binary_data, np.uint32, count=3, offset=0)assert magic_number == 0xFCCFE2E2, f"{file} not a tensor file."dims = np.frombuffer(binary_data, np.uint32, count=ndims, offset=3 * 4)if dtype == 0:np_dtype = np.float32elif dtype == 1:np_dtype = np.float16else:assert False, f"Unsupport dtype = {dtype}, can not convert to numpy dtype"return np.frombuffer(binary_data, np_dtype, offset=(ndims + 3) * 4).reshape(*dims)input = load_tensor("workspace/input.tensor")
output = load_tensor("workspace/output.tensor")print(input.shape, output.shape)# 恢复成源图像
image = input * 255
image = image.transpose(0, 2, 3, 1)[0].astype(np.uint8)[..., ::-1]import cv2
cv2.imwrite("image.jpg", image)
print("save done.")

运行效果如下:

在这里插入图片描述

图1-4 python加载

在这里插入图片描述

图1-5 python恢复出的图像

可以看到我们恢复出来的效果完全一样,说明中间是没有问题的

这边我们讲解了怎么 save tensor,怎么 load tensor,怎么和 C++ 去做交互,自己也可以去进行封装。

最后我们来看下实现一个模型的流程:

1. 先把代码跑通 predict,单张图作为输入。屏蔽一切与该目标不符的东西,可以修改删除任意多余的东西

2. 自行写一个 python 程序,简化 predict 的流程,掌握 predict 所需要的最小依赖和最少代码

3. 如果第二步比较困难,则可以考虑直接在 pred = model(x) 这个步骤上研究,例如直接在此处写 torch.onnx.export(model, (pred,) …),或者直接把 pred 的结果储存下来研究等等

4. 把前处理、后处理分析出来并实现一个最简化版本

5. 利用简化版本进行 debug、理解分析。然后考虑预处理后处理的合理安排,例如是否可以把部分后处理放到 onnx 中

6. 导出 onnx,在 C++ 上先复现预处理部分,使得其结果和 python 接近(大多数时候并不能得到一样的结果)

7. 把 python 上的 pred 结果储存后,使用 C++ 读取并复现所需要的后处理部分。确保结果正确

8. 把前后处理与 onnx 对接起来,形成完整的推理

总结

本次课程学习了调试方法,首先我们封装了 tensor 保存和加载,这点可以保证 python 与 c++ 之间的交互。然后我们对实现一个模型的流程进行了讨论,拿到一个新的项目后我们先要做的是跑通单张图片的 predict,然后可以自行实现一个简易的 predict 程序,也可以考虑直接导出 onnx 或者把 pred 预测结果保存下来,接着我们通过 debug 分析把预处理、后处理抽出来,导出 onnx。现在 C++ 上复现预处理,看结果和 python 是否接近,另外把 python 存储的 pred 利用 c++ 读取复现后处理,最后把整个对接起来,完成推理。

这是杜老师推荐的工作流,可以简化过程,并且方便开发调试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/108713.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Sping源码(七)— 后置处理器(自定义后置处理器)

上一篇中简单介绍了Spring中invokeBeanFactoryPostProcessors方法的执行流程&#xff0c;以及BFPP和BDRPP类的介绍&#xff0c;这篇文章我们来自定义实现一个类的后置处理器。 自定义PostProcessor 自定义PostProcessor的方式一共两种&#xff0c;都是根据invokeBeanFactoryPo…

2023年高教社杯数学建模思路 - 复盘:光照强度计算的优化模型

文章目录 0 赛题思路1 问题要求2 假设约定3 符号约定4 建立模型5 模型求解6 实现代码 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 问题要求 现在已知一个教室长为15米&#xff0c;宽为12米&…

软件工程(十八) 行为型设计模式(四)

1、状态模式 简要说明 允许一个对象在其内部改变时改变它的行为 速记关键字 状态变成类 类图如下 状态模式主要用来解决对象在多种状态转换时,需要对外输出不同的行为的问题。比如订单从待付款到待收货的咋黄台发生变化,执行的逻辑是不一样的。 所以我们将状态抽象为一…

电商项目part05 分布式ID服务实战

背景 日常开发中&#xff0c;需要对系统中的各种数据使用 ID 唯一表示&#xff0c;比如用户 ID 对应且仅对应一个人&#xff0c;商品 ID 对应且仅对应一件商品&#xff0c;订单 ID 对应且仅对应 一个订单。现实生活中也有各种 ID&#xff0c;比如身份证 ID 对应且仅对应一个人…

openCV实战-系列教程9:傅里叶变换(傅里叶概述/频域变换结果/低通与高通滤波)、原理解析、源码解读

OpenCV实战系列总目录 打印图像直接用这个函数&#xff1a; def cv_show(img,name):cv2.imshow(name,img)cv2.waitKey()cv2.destroyAllWindows()1、傅里叶变换 在生活中&#xff0c;我们的大部分事情都是以时间为参照的&#xff0c;用时间为参照的为时域分析&#xff0c;在频…

Batbot电力云平台在智能配电室中的应用

智能配电室管理系统是物联网应用中的底层应用场景&#xff0c;无论是新基建下的智能升级&#xff0c;还是双碳目标下的能源管理&#xff0c;都离不开智能配电运维对传统配电室的智慧改造。Batbot智慧电力&#xff08;运维&#xff09;云平台通过对配电室关键电力设备部署传感器…

怎么对App进行功能测试

测试人员常被看作是bug的寻找者&#xff0c;但你曾想过他们实际是如何开展测试的吗&#xff1f;你是否好奇他们究竟都做些什么&#xff0c;以及他们如何在一个典型的技术项目中体现价值&#xff1f;本文将带你经历测试人员的思维过程&#xff0c;探讨他们测试app时的各种考虑. …

STM32 CubeMX (H750)RGB屏幕 LTDC

STM32 CubeMX STM32 RGB888 LTDC STM32 CubeMX一、STM32 CubeMX 设置时钟树LTDC使能设置屏幕参数修改RGB888的GPIO 二、代码部分效果 RGB屏幕线束定义&#xff1a; 一、STM32 CubeMX 设置 时钟树 这里设置的时钟&#xff0c;关于刷新速度 举例子&#xff1a;LCD_CLK24MHz 时…

使用树莓派Pico、DHT11和SSD1306搭建一个温度湿度计(只使用官方库,以及官方案例代码的错误之处和解决方案)

最近想树莓派 Pico、DHT11 温湿度传感器和 SSD1306 OLED 屏幕做一个温度湿度计&#xff0c;树莓派官方案例也分别有这两个设备的案例&#xff0c;我就想做个简单的温度湿度计作为学习微控制器的开始&#xff0c;结果遇到了一个大坑&#xff0c;所以写本文记录一下整个过程。 本…

图论(基础)

知识&#xff1a; 顶点&#xff0c;边 | 权&#xff0c;度数 1.图的种类&#xff1a; 有向图 | 无向图 有环 | 无环 联通性 基础1&#xff1a;图的存储&#xff08;主要是邻接矩阵和邻接表&#xff09; 例一&#xff1a;B3643 图的存储 - 洛谷 | 计算机科学教育新生态 (…

spring boot 项目整合 websocket

1.业务背景 负责的项目有一个搜索功能&#xff0c;搜索的范围几乎是全表扫&#xff0c;且数据源类型贼多。目前对搜索的数据量量级未知&#xff0c;但肯定不会太少&#xff0c;不仅需要搜索还得点击下载文件。 关于搜索这块类型 众多&#xff0c;未了避免有个别极大数据源影响整…

深入理解回调函数qsort:从入门到模拟实现

&#x1f493;博客主页&#xff1a;江池俊的博客⏩收录专栏&#xff1a;C语言进阶之路&#x1f449;专栏推荐&#xff1a;✅C语言初阶之路 ✅数据结构探索&#x1f4bb;代码仓库&#xff1a;江池俊的代码仓库​&#x1f3aa; 社区&#xff1a;GeekHub社区 ​&#x1f389;欢迎大…

微信开发之一键修改群聊名称的技术实现

修改群名称 修改群名后&#xff0c;如看到群名未更改&#xff0c;是手机缓存问题&#xff0c;可以连续点击进入其他群&#xff0c;在点击进入修改的群&#xff0c;再返回即可看到修改后的群名 请求URL&#xff1a; http://域名地址/modifyGroupName 请求方式&#xff1a; …

基于SpringBoot+MybatisPlus+Shiro+mysql+redis智慧云智能教育平台

基于SpringBootMybatisPlusShiromysqlredis智慧云智能教育平台 一、系统介绍二、功能展示三.其他系统实现五.获取源码 一、系统介绍 声明&#xff1a;Java智慧云智能教育平台源码 前后端分离、 开发语言&#xff1a;JAVA 数据库&#xff1a;MySQL5.7以上 开发工具&#xff…

从哈希表到红黑树:探讨 epoll 是如何管理事件的?

揭开pkill的秘密&#xff1a;在Linux中杀死进程的完整指南 一、引言二、 传统事件管理的局限性三、epoll 概述3.1、epoll 的基本概念和工作原理3.2、epoll 在 Linux 内核中的实现方式 四、哈希表在事件管理中的挑战五、 红黑树在 epoll 中的应用六、epoll 中的事件注册与触发七…

功能强大的网站检测工具Web-Check

什么是 Web-Check &#xff1f; Web-Check是一款功能强大的一体化工具&#xff0c;用于查找有关网站/主机的信息。目前仪表版上可以显示&#xff1a;IP 信息、SSL 信息、DNS 记录、cookie、请求头、域信息、搜索爬虫规则、页面地图、服务器位置、开放端口、跟踪路由、DNS 安全扩…

自定义Chronometer实现定时器

概述 自定义Chronometer实现定时器,引用方便&#xff0c;操作简单。 详细 前言 在Android开发过程中&#xff0c;计时控件是经常回使用到的&#xff0c;在Android控件库中有一个能快捷实现计时功能的控件&#xff0c;它就是Chronometer&#xff0c;今天我们基于它自定义实现…

DataFrame.set_index()方法--Pandas

1.函数功能 为DataFrame重新设置索引&#xff08;行标签&#xff09; 2. 函数语法 DataFrame.set_index(keys, *, dropTrue, appendFalse, inplaceFalse, verify_integrityFalse)3. 函数参数 参数含义keys作为行标签的列名&#xff0c;可以DataFrame中的是单个列或者多列组…

C语言——指针进阶(一)

目录 ​编辑 一.字符指针 1.1 基本概念 1.2 面试题 二.指针数组 三.数组指针 3.1 数组指针的定义 3.2 &数组名VS数组名 3.3 数组指针的使用 四.数组参数、指针参数 4.1 一维数组传参 ​编辑 4.2 二维数组传参 4.3 一级指针传参 4.4 二级指针传参 ​编辑 五.…

好用的可视化大屏适配方案

1、scale方案 优点&#xff1a;使用scale适配是最快且有效的&#xff08;等比缩放&#xff09; 缺点&#xff1a; 等比缩放时&#xff0c;项目的上下或者左右是肯定会有留白的 实现步骤 <div className"screen-wrapper"><div className"screen"…