使用结构体提前存放常用变量
在编写前后处理函数时,通常会多次用到一些变量,比如模型输入tensor的shape,count等等,若在每个处理函数中都重复计算一次,会增加部署时的计算量。对于这种情况,可以考虑使用结构体,并定义一个初始化函数。先计算好需要的值,之后需要用到该变量的时候直接引用(&)传递即可。
函数使用引用代替值传递
考虑到C++的特性,函数的参数建议使用引用 (&) 来代替值传递,有这几个显著优点:
只将原对象的引用传递给函数,避免不必要的拷贝,降低计算耗时
因为不会复制数据,所以引用相比值传递可以避免内存的重复开销,降低内存占用
但需要注意,引用会允许函数修改原始数据,因此若不希望原始数据被修改,请不要使用引用方法。
量化/反量化融合
在前后处理的循环中融合
在前后处理中通常会遍历数据,而量化/反量化也会遍历数据,因此可以考虑合并计算,以减少数据遍历耗时。这是最常见的量化/反量化融合思路,可以直接参考ai benchmark中的大量源码示例。
将数据存进tensor时融合
如果在前处理中没找到融合的机会,那么也可以在数据复制进input tensor的时候做量化计算。
填充初始值时,提前计算量化后的值
有时我们想给模型准备特定的输入,比如生成一个全0数组,再为数组的特定区域填充某个固定的浮点值。在这种情况下,如果先生成完整的浮点数组,再遍历整个数组做量化,会产生不必要的遍历耗时,常见的优化思路是先提前计算好填充值量化后的结果,填充的时候直接填入定点值,这样就可以避免多余的量化耗时。
根据后处理的实际作用,跳过反量化
在某些情况下,比如后处理只做argmax时,完全没有必要做反量化,直接使用整型数据做argmax即可。需要用户根据后处理的具体原理来判断是否使用这种优化方法。
循环推理同个模型时,输出数据直接存进输入tensor
在某些情况下,我们希望C++程序能重复推理同一个模型,并且模型上一帧的输出可以作为下一帧的输入。如果按照常规手段,我们可能会将输出tensor的内容保存到特定数组,再把这个数组拷贝到输入tensor,这样一来一回就产生了两次数据拷贝的耗时,也占用了更多内存。实际上,我们可以将模型的输出tensor地址直接指向输入tensor,这样模型第一帧的推理结果会直接写在输入tensor上,推理第二帧的时候就可以直接利用这份数据,不需要再单独准备输入,可以节省大量耗时。
如果想使用该方法,需要模型输入输出对应节点的shape/stride等信息完全相同。此外,如果模型删除了量化/反量化算子,并且对应的scale完全相同,那么重复利用的这部分tensor是不需要flush的(因为不涉及CPU操作),还可进一步节约耗时。
这里举个例子详细说明一下。
假设我们有一个模型,这个模型有59个输入节点(0-58),57个输出节点(0-56),量化/反量化算子均已删除,且输入输出最后56个节点对应的scale/shape/stride等信息均相同。在第一帧推理完成后,输出节点1-56的值需要传递给输入节点的3-58,那么我们在分配模型输入输出tensor的时候,输出tensor只需要为1分配即可,在分配输入tensor时,3-58的tensor可以同时push_back给输出tensor。具体来说,可以这样写:
在模型推理时,重复利用的这部分tensor不需要再flush,因此只需要给output_tensor的0,以及input_tensor的0/1/2进行flush操作即可(这几个tensor和CPU产生了交互)。
此外,如果使用了这种优化方法,那么在模型推理结束释放内存时,要避免同一块内存的重复释放。对于该案例,input_tensor全部释放完毕后,output_tensor只需要释放output_tensor 0。
多线程后处理
对于yolo v5这种有三个输出头的模型,可以考虑使用三个线程同时对三个输出头做后处理,以显著提升性能。



