Deepstream提供了nvinfer plugin供封装调用模型使用, Deepstream的sample代码deepstream app里实现调用多个模型时是针对一主多从的使用方案,也就是一个主模型(primary inference engine),一个或者多个次要从属模型(secondary inference engine),因为模型使用前都转换成了TensorRT的engine文件,所以一般叫engine。下面是典型的基于Deepstream的调用多个模型推理的app的结构:
各部分的plugin的功能的简要说明如下:
Samples里的deepstream-app里使用了四个模型(对应创建了四个nvinfer plugin),一个主模型(由下图中的左侧的nvinfer plugin负责封装调用),三个次要模型(由下图中右侧的三个nvinfer plugin负责调用),主模型用来识别车,三个次要从属模型分别用来识别车的颜色、种类、厂商。
模型文件位于/opt/nvidia/deepstream/deepstream/samples/models/下,主模型和次要从属模型的分工完全是由配置文件来配置的,封装调用它们的nvinfer plugin是相同的,运行deepstream app时指定config文件为/opt/nvidia/deepstream/deepstream/samples/config/deepstream-app/source4_1080p_dec_infer-resnet_tracker_sgie_tiled_display_int8.txt
cd/opt/nvidia/deepstream/deepstream/samples/config/deepstream-app
deepstream-app -c source4_1080p_dec_infer-resnet_tracker_sgie_tiled_display_int8.txt
即可将四个模型跑起来。
四个nvinfer plugin 和tracker plugin以及tee和4个queue element的结构组成如下:
最左侧的是primary inference engine对应的plugin,挨着的是tracker plugin,再往右边是一个tee element用于视频流的复制分叉,和tee直连的是用于缓冲的4个queue element,上面三个queue element分别连对应的三个secondary inference engine对应的plugin,最下面的第四个queue element用于连接往下游走用于osd画图和eglsink显示的分支,在这个queue elemen的pad上和它上游连接的tee element的pad上注册pad probe监听函数监控gstbuf的引用计数即可实现控制三个secondary inference engine的推理和写入推理结果到metadata的同步。
关于source4_1080p_dec_infer-resnet_tracker_sgie_tiled_display_int8.txt里各种配置的说明参见DeepStream Reference Application - deepstream-app — DeepStream 6.0 Release documentation
source4_1080p_dec_infer-resnet_tracker_sgie_tiled_display_int8.txt里与模型调用相关的配置部分是Primary GIE and Secondary GIE Group,其中最重要的我觉得就两项:enable和config-file,其他的都和通过config-file给每个模型指定的配置文件的里面的配置项重复了,没必要设置。source4_1080p_dec_infer-resnet_tracker_sgie_tiled_display_int8.txt里为四个模型都指定了针对每个模型的配置文件config-file(分别为config_infer_primary.txt、config_infer_secondary_carcolor.txt,config_infer_secondary_carmake.txt,config_infer_secondary_vehicletypes.txt),并且可以通过设置enable=1或者0来启用或关闭这个模型的使用。
在针对每个模型的配置文件里最重要的配置项主要是:
- gie-unique-id 指定本infer plugin的唯一id,当模型之间存在主从关系需要合作时需要设置这个id
- gpu-id 指定模型推理使用哪个GPU,在服务器上有多个GPU是可以特别设置,对于jetson板子上一般就一个GPU没什么可挑的一律设为0
- model-engine-file 指定模型的engine文件的路径
- nvbuf-memory-type 指定nvbuf的内存类型,实际上是将buffer放到什么内存设备上,是host的内存还是GPU的内存(也就是device memory),对于Jetson板子上没什么可选的一律设为为0(default),服务器上可以根据需要选择设置0,1,2,3其中之一,具体含义:
- batch-size 每次提交给模型进行推理的帧数
- interval 指定nvinfer plugin推理时跳帧的帧数,设置为0时不跳帧,设置为1时跳1帧也就是每间隔一帧进行推理。注意!这个参数实际上是有点坑人的!如果你需要在目标检测后无论是使用tracker plugin还是使用自己实现的plugin进行目标跟踪,建议保持设置这个interval=0,也就是不要跳过推理,否则,表面上是快了一点,实际上跟踪效果会大打折扣。为什么呢?因为nvidia实现的这里跳帧就根本不合理,只是简单地跳过模型推理,并不是真的把帧数据丢了!所以被跳过的帧的数据照样往下游发了,这样相当于这些被跳过推理的帧都没有目标识别出来没有任何目标检测结果写入meta data, 结果是什么可想而知,那跟踪插件部分对目标进行跟踪时就只能全靠自己内部预测了而不能靠目标检测结果来校正了,所以跟踪效果结果自然变差,实验了一下当interval设置为4-5时deepsort跟踪效果已经很烂!
- operate-on-gie-id 对于次要从属模型,这个id必须设置为上游主模型的gie-unique-id值,如果多个模型之间都是平等的,没有主从关系,这个完全可以不设置。
- operate-on-class-ids 设置只需要检测那些类别,这个一般和operate-on-gie-id配合使用实现主从模型之间的协作,单独使用也是可以的。
- labelfile-path 指定内含类别名称列表的标签文件的路径
- plugin-type 当没有使用Triton inference server时一律设置为0,否则设置为1
- infer-raw-output-dir 当需要dump out推理结果buffer里的数据时才需要设置
- input-tensor-meta 需要将预处理数据写入meta data并且后面从meta data里获取时才需要设置,具体可以参考sample里的代码
Deepstream app播放的视频则是使用的是deepstream samples里自带的demo视频,在Jetson Nano上跑起来后可以看到效果是比较卡的, 原因除了Nano的硬件资源比较低端并且调用的模型多外,与这里三个次要模型是放在不同的sub-bin里并行调用的也有关系,因为只有当这三个次要模型都调用完了,推理的结果都写入了meta data里,才允许帧数据往下走,所以这里需要技巧性地在包含三个次要模型的sub-bin前的tee的pad上以及sub-bin里帧数据下发的分支的queue的pad上增加pad probe代码,基于检查buffer的引用计数和加锁实现三个次要模型的并行调用的同步:
gboolean g_first_flush = TRUE;
static GstPadProbeReturn
wait_queue_buf_probe (GstPad *pad, GstPadProbeInfo *info, gpointer u_data)
{
if (info->type & GST_PAD_PROBE_TYPE_EVENT_BOTH) {
GstEvent *event = (GstEvent *) info->data;
if (event->type == GST_EVENT_EOS) {
return GST_PAD_PROBE_OK;
}
}
if (info->type & GST_PAD_PROBE_TYPE_BUFFER) {
g_mutex_lock (&g_bin_wait_lock);
while (GST_OBJECT_REFCOUNT_VALUE (GST_BUFFER (info->data)) > 1
&& !g_bin_stop) {
gint64 end_time;
end_time = g_get_monotonic_time () + G_TIME_SPAN_SECOND / 1000;
g_cond_wait_until (&g_bin_wait_cond, &g_bin_wait_lock, end_time);
}
g_mutex_unlock (&g_bin_wait_lock);
}
return GST_PAD_PROBE_OK;
}
/**
* Probe function on sink pad of tee element. It is being used to
* capture EOS event. So that wait for all secondary to finish can be stopped.
* see ::wait_queue_buf_probe
*/
static GstPadProbeReturn
wait_queue_buf_probe1 (GstPad * pad, GstPadProbeInfo * info, gpointer u_data)
{
if (info->type & GST_PAD_PROBE_TYPE_EVENT_BOTH) {
GstEvent *event = (GstEvent *) info->data;
if (event->type == GST_EVENT_EOS) {
g_bin_stop = TRUE;
}
}
return GST_PAD_PROBE_OK;
}
实际上这里的代码只对deepstream app这种简单实现的app能运行,因为它最终只是显示视频和推理结果,如果用于自己的app,并且下游还有sink之类的用于recording或者http live的分支的某种sink plugin话,上面wait_queue_buf_probe ()里的代码还需要改造才行,不然pipeline启动过程中会卡死,根本进入不到PLAYING状态!
我在我们自己的app里实现照着上面那样实现多个模型并行调用时就被这个 问题困扰了好一阵,后来打开GST_DEBUG看输出日志发现从sub-bin里分出去的分支的下游末端只要有用于recording或者live playing的sink plugin打开整个pipeline状态就进入不到PLAYING状态,关掉那些分支就可以进入到PLAYING状态,然后发现那些sink plugin的状态到READY后一直都处于等待,在等什么呢?实验和思考了些时间,猜测原因是*sink plugin依赖于第一帧数据将其状态从READY转为PLAYING,于是改了下程序,将第一帧数据不受限制下发,结果看到pipeline能播放了!原因就是*sink plugin依赖于第一帧数据将其状态从READY转为PLAYING,但是app开始播放视频时往往一下好几帧批量到达了sub-bin前的tee这里,导致三个次要模型需要连续推理多帧才能满足(GST_OBJECT_REFCOUNT_VALUE (GST_BUFFER (info->data)) == 1,从而从而退出while循环解锁,从而将帧数据下发,但是由于pipeline末端的*sink插件等不到帧数据状态一直处于READY状态不能进入PLAYING状态,所以整个pipeline处于READY状态而不是PLAYING状态,三个次要模型对应的nvinfer plugin在推理一帧后就阻住不再处理数据了,这样就形成了整个pipeline等待末端的*sink plugin进入PLAYING状态,可是那些*sink plugin却在等待第一帧数据到来这样的相互等待的状态。
要解决这个问题就是让第一帧数据能不受影响能下发到全部*sink plugin,所以代码需要稍微修改一下:
gboolean g_first_flush = TRUE;
static GstPadProbeReturn
wait_queue_buf_probe (GstPad *pad, GstPadProbeInfo *info, gpointer u_data)
{
if (info->type & GST_PAD_PROBE_TYPE_EVENT_BOTH) {
GstEvent *event = (GstEvent *) info->data;
if (event->type == GST_EVENT_EOS) {
return GST_PAD_PROBE_OK;
}
}
if (info->type & GST_PAD_PROBE_TYPE_BUFFER) {
g_mutex_lock (&g_bin_wait_lock);
if (g_first_flush)
g_first_flush = FALSE;
else {
while (GST_OBJECT_REFCOUNT_VALUE (GST_BUFFER (info->data)) > 1
&& !g_bin_stop) {
gint64 end_time;
end_time = g_get_monotonic_time () + G_TIME_SPAN_SECOND / 1000;
g_cond_wait_until (&g_bin_wait_cond, &g_bin_wait_lock, end_time);
}
}
g_mutex_unlock (&g_bin_wait_lock);
}
return GST_PAD_PROBE_OK;
}
另外,前面也说了,多个模型并行调用需要同步加锁才能保证都推理完后全部推理结果都写入了meta data后帧数据才能往下游继续走,由于这个原因,对于要调用的模型只有少量,例如两个模型时,我做了实验,实际上这时串行调用比并行调用的性能要好得多(两个模型串行调用就是创建两个nvinfer plugin分别负责封装调用两个模型,把这两个nvinfer plugin按业务逻辑的需要按顺序link在一起(将这两个plugin作为参数调用gst_element_link()进行link)),所以,要调用的模型不多时,并行调用模型未必是好的选择。
当多个模型直接并没有主从先后推理关系时,也就是这些模型在调用先后上没有顺序要求时,可以将所有的模型放入sub-bin,并行调用,例如我们自己实现的两个目标检测模型分别用于检测不同类别的目标,那么它们的结构可以组织类似如下:
当然,前面也说了,当模型只有两个时,由于Jetson Nano上GPU和其他硬件资源有限以及并行调用时的同步控制等原因,并行调用还不如串行调用速度快,所以完全可以不用tee和多个queue分支这些复杂结构,直接将两个推理模型串行连接并将tracker plugin放在第二个推理模型的后面连接即可,既简单易于控制又性能较好。