本文經自動駕駛之心公眾號授權轉載,轉載請聯絡來源。
相信除了少數自研晶片的大廠,絕大多數自動駕駛公司都會使用英偉達NVIDIA晶片,那就離不開TensorRT. TensorRT是在NVIDIA各種GPU硬體平台下運行的一個C 推理框架。我們利用Pytorch、TF或其他框架訓練好的模型,可以先轉化為onnx格式,再轉化為TensorRT的格式,然後利用TensorRT推理引擎去運行我們這個模型,從而提升這個模型在英偉達GPU上運行的速度。
一般來說,onnx和TensorRT僅支援相對比較固定的模型(包括各級的輸入輸出格式固定,單分支等),最多支援最外層動態輸入(導出onnx可以透過設定dynamic_axes參數確定允許動態變化的維度).但活躍在感知算法前沿的小伙伴們都會知道,目前一個重要發展趨勢就是端到端(End-2-End),可能涵蓋了目標檢測,目標跟踪,軌跡預測,決策規劃等全部自動駕駛環節,而且必定是前後幀緊密相關的時序模型.實現了目標檢測和目標跟踪端到端的MUTR3D模型可以作為一個典型例子(模型介紹可參考:)
#在MOTR/MUTR3D中,我們將詳細解釋Label Assignment機制的理論和實例,以實現真正的端到端多目標追蹤。請點擊連結閱讀更多:https://zhuanlan.zhihu.com/p/609123786
這種模型的轉換為TensorRT格式並實現精度對齊,甚至fp16的精度對齊,可能會面臨一系列的動態元素,例如多個if-else分支、子網路輸入形狀的動態變化以及其他需要動態處理的操作和算子等
##圖片
#MUTR3D架構因為整個過程涉及多個細節,情況各不一樣,縱觀全網的參考資料,甚至google搜索,也很難找到即插即用的方案,只能通過不斷拆分和實驗來逐個解決.透過部落客一個多月的艱苦探索實踐(之前對TensorRT的經驗不多,沒有摸清它的脾氣),動了不少腦筋,也踩了不少坑,最後終於成功轉換並實現fp32/ fp16精度對齊,且時延相比單純的目標偵測增加非常小。想在此做一個簡單的整理,並為大家提供參考(沒錯,一直寫綜述,終於寫實踐了!)1.數據格式問題首先是MUTR3D的數據格式比較特殊,都是採用實例形式,這是因為每個query綁定的信息比較多,都打包成實例更容易一對一的訪問.但對於部署而言,輸入輸出只能是tensor,所以首先要對實例資料進行拆解,變成多個tensor變數.並且由於當前幀的query和其他變數是在模型中生成,所以只要輸入前序幀保留的query和其他變數即可,在模型中對二者進行拼接.2.padding解決輸入動態shape的問題對於輸入的前序幀query和其他變量,有一個重要問題是shape是不確定的。這是因為MUTR3D僅保留前序幀中曾經檢出過目標的query。這個問題還是比較容易解決的,最簡單的方法就是padding,也就是padding到一個固定大小。對於query可以用全0做padding,數量具體多少合適,可以依照自己的數據做實驗確定。太少容易漏掉目標,太多比較浪費空間。雖然onnx的dynamic_axes參數可以實現動態輸入,但因為涉及後續transformer計算的size,應該是有問題的。我沒有嘗試,讀者可以試驗一下3.padding對於主transformer中self-attention模組的影響如果不使用特殊算子,經過填充後就可以成功轉換為ONNX和TensorRT。實際上肯定會遇到這種情況,但不在本篇討論的範圍內。例如,在MUTR3D中,當在幀間移動參考點時,使用torch.linalg.inv算子來求偽逆矩陣是不支援的。如果遇到不支援的算子,只能嘗試替換,如果不行,就只能在模型外部使用,有經驗的人還可以自己寫算子。但由於這一步驟可以放在模型的預處理和後處理中,我選擇將其移到模型外部,編寫自己的算子會更困難成功轉換並不意味著一切順利,答案往往是否定的。我們會發現精度差距非常大。這是因為模型有很多模組,讓我們先說第一個原因。在Transformer的自註意力階段,會進行多個查詢之間的資訊互動。然而,原始模型只保留了曾經檢測到目標的查詢(模型中稱為活躍查詢),應該只有這些查詢與當前幀的查詢進行交互。而現在,由於填充了許多無效的查詢,如果所有查詢一起交互,勢必會影響結果解決這個問題受了DN-DETR[1]的啟發,那就是使用attention_mask,在nn.MultiheadAttention中對應'attn_mask'參數,作用就是屏蔽掉不需要進行信息交互的query,最初是因為在NLP中每個句子長度不一致而設定的,正好符合我現在的需求,只是需要注意True代表需要屏蔽的query,False代表有效query.
圖片
attention mask示意圖因為計算attention_mask邏輯稍微有點複雜,很多操作轉換TensorRT可能出現新問題,所以也應該在模型外計算好之後作為一個輸入變量輸入模型,再傳遞給transformer.以下是示例代碼:
data['attn_masks'] = attn_masks_init.clone().to(device)data['attn_masks'][active_prev_num:max_num, :] = Truedata['attn_masks'][:, active_prev_num:max_num] = True[1]DN-DETR: Accelerate DETR Training by Introducing Query DeNoising
QIM是MUTR3D中對transformer輸出的query進行的後處理模組,主要分三步,第一步是篩選active query,即在在當前幀中檢測出目標的query,依據是obj_idxs是否>=0(在訓練階段還包括隨機drop query,和隨機加入fp query,推理階段不涉及),第二步是update query,即針對第一步中篩選的query做一個更新,包括query 輸出值的self-attention,ffn,和與query輸入值的shortcut連接,第三步是將更新的query與重新生成的初始query拼接,作為下一幀的輸入.可見第二步中仍然存在我們在第3點中提到的問題,即self-attention不做全部query之間的交互,而是只進行active query之間的信息交互.所以在這裡又要使用attention mask.
雖然QIM模組是可選的,但實驗表明對模型精度的提升是有幫助的.如果要使用QIM的話,這個attention mask必須在模型裡計算,因為模型外部無法得知當前幀的檢測結果.由於tensorRT的語法限制,很多操作要么會轉換不成功,要么不會得到想要的結果,經過多次實驗,結論是直接用索引切片賦值(類似於第3點的示例程式碼)操作一般不支援,最好用矩陣計算的方式,但涉及計算必須將attention mask的bool類型轉為float類型,最後attention mask需要轉回bool類型才能使用.以下是實例代碼:
obj_mask = (obj_idxs >= 0).float()attn_mask = torch.matmul(obj_mask.unsqueeze(-1), obj_mask.unsqueeze(0)).bool()attn_mask = ~attn_mask
進行完以上四點,我們基本上可以保證模型轉換tensorRT的邏輯沒有問題,但輸出結果經過多次驗證後某些幀仍然存在問題一度讓我很不解.但一幀幀從數據上分析,就會發現竟然在某些幀padding的query雖然沒有參與transformer計算,卻可以得到一個較高的score,進而得到錯誤的結果.這種情況在資料量大的情況下確實是可能的,因為padding的query只是初始值是0,reference points也是[0,0],與其他隨機初始化的query進行了同樣的操作.但由於畢竟是padding的query ,我們並不打算使用他們的結果,所以必須要進行過濾.
如何過濾填充查詢的結果呢?填充查詢的標誌只有它們的索引位置,其他資訊都沒有特異性。而索引資訊實際上記錄在第3點使用的注意力遮罩中,這個注意力掩碼是從模型外部傳入的。這個遮罩是二維的,我們可以使用其中的一維(任一行或任一列),將填入的track_score直接置為0。請記住仍然要注意第4步的注意事項,即盡量使用矩陣計算來代替索引切片賦值,並且計算必須轉換為float類型。以下是程式碼範例:
mask = (~attention_mask[-1]).float()track_scores = track_scores * mask
除了模型主體,其實還有非常關鍵的一步,就是動態更新track_id,這也是模型能做到端到端的一個重要因素.但在原模型中更新track_id的方式是一個相對複雜的循環判斷, 即高於score thresh且是新目標的,賦一個新的obj_idx, 低於filter score thresh且是老目標的,對應的disappear time 1,如果disappear time超過miss_tolerance, 對應的obj idx置為-1,即丟棄這個目標.
我們知道tensorRT是不支援if-else多分支語句的(好吧,我一開始不知道),這是個頭痛的問題.如果將更新track_id也放到模型外部,不僅影響了模型端到端的架構,而且也會導致無法使用QIM,因為QIM篩選query的依據是更新後的track_id.所以絞盡腦汁也要把更新track_id放到模型裡面去.
再次發揮聰明才智(快用完了),if-else語句也不是不能代替的,比如使用mask並行操作.例如將條件轉換為mask(例如tensor[mask] = 0).這裡面值得慶幸的是雖然第4,第5點提到tensorRT不支援索引切片賦值操作,但是卻支援bool索引賦值,猜測可能因為切片操作隱性改變了tensor的shape吧.但經過多次實驗,也不是所有情況下的bool索引賦值都支持的,出現了以下幾種頭疼的情況:
需要重新写的内容是:赋值的值必须是一个,不能是多个。例如,当我更新新出现的目标时,我不会统一赋值为某个ID,而是需要为每个目标赋予连续递增的ID。我想到的解决办法是先统一赋值为一个比较大且不可能出现的数字,比如1000,以避免与之前的ID重复,然后在后续处理中将1000替换为唯一且连续递增的数字。(我真是个天才)
如果要进行递增操作(+=1),只能使用简单的掩码,即不能涉及复杂的逻辑计算。例如,对disappear_time的更新,本来需要同时判断obj_idx >= 0且track_scores = 0这个条件。虽然看似不合理,但经过分析发现,即使将obj_idx=-1的非目标的disappear_time递增,因为后续这些目标并不会被选入,所以对整体逻辑影响不大
综上,最后的动态更新track_id示例代码如下,在后处理环节要记得替换obj_idx为1000的数值.:
def update_trackid(self, track_scores, disappear_time, obj_idxs):disappear_time[track_scores >= 0.4] = 0obj_idxs[(obj_idxs == -1) & (track_scores >= 0.4)] = 1000disappear_time[track_scores 5] = -1
至此模型部分的处理就全部结束了,是不是比较崩溃,但是没办法,部署端到端模型肯定比一般模型要复杂很多.模型最后会输出固定shape的结果,还需要在后处理阶段根据obj_idx是否>0判断需要保留到下一帧的query,再根据track_scores是否>filter score thresh判断当前最终的输出结果.总体来看,需要在模型外进行的操作只有三步:帧间移动reference_points,对输入query进行padding,对输出结果进行过滤和转换格式,基本上实现了端到端的目标检测+目标跟踪.
需要重新写的内容是:以上六点的操作顺序需要说明一下。我在这里按照问题分类来写,实际上可能的顺序是1->2->3->5->6->4,因为第五点和第六点是使用QIM的前提,它们之间也存在依赖关系。另外一个问题是我没有使用memory bank,即时序融合的模块,因为经过实验发现这个模块的提升效果并不明显,而且对于端到端跟踪机制来说,已经天然地使用了时序融合(因为直接将前序帧的查询信息带到下一帧),所以时序融合并不是非常必要
好了,现在我们可以对比TensorRT的推理结果和PyTorch的推理结果,会发现在FP32精度下可以实现精度对齐,非常棒!但是,如果需要转换为FP16(可以大幅降低部署时延),第一次推理会发现结果完全变成None(再次崩溃)。导致FP16结果为None一般都是因为出现数据溢出,即数值大小超限(FP16最大支持范围是-65504~+65504)。如果你的代码使用了一些特殊的操作,或者你的数据天然数值较大,例如内外参、姿态等数据很可能超限,一般可以通过缩放等方式解决。这里再说一下和我以上6点相关的一个原因:
7.使用attention_mask导致的fp16结果为none的问题
这个问题非常隐蔽,因为问题隐藏在torch.nn.MultiheadAttention源码中,具体在torch.nn.functional.py文件中,有以下几句:
if attn_mask is not None and attn_mask.dtype == torch.bool:new_attn_mask = torch.zeros_like(attn_mask, dtype=q.dtype)new_attn_mask.masked_fill_(attn_mask, float("-inf"))attn_mask = new_attn_mask
可以看到,这一步操作是对attn_mask中值为True的元素用float("-inf")填充,这也是attention mask的原理所在,也就是值为1的位置会被替换成负无穷,这样在后续的softmax操作中,这个位置的输入会被加上负无穷,输出的结果就可以忽略不记,不会对其他位置的输出产生影响.大家也能看出来了,这个float("-inf")是fp32精度,肯定超过fp16支持的范围了,所以导致结果为none.我在这里把它替换为fp16支持的下限,即-65504,转fp16就正常了,虽然说一般不要修改源码,但这个确实没办法.不要问我怎么知道这么隐蔽的问题的,因为不是我一个人想到的.但如果使用attention_mask之前仔细研究了原理,想到也不难.
好的,以下是我在端到端模型部署方面的全部经验分享,我保证这不是标题党。由于我对tensorRT的接触时间不长,所以可能有些描述不准确的地方
需要进行改写的内容是:原文链接:https://mp.weixin.qq.com/s/EcmNH2to2vXBsdnNvpo0xw
以上是實戰部署:動態時序網路用於端到端偵測和追蹤的詳細內容。更多資訊請關注PHP中文網其他相關文章!