高性能视频推理引擎优化技术
正文字?jǐn)?shù):9404 ?閱讀時長:15分鐘
本文整理自騰訊高級技術(shù)專家鮑金龍?jiān)贚iveVideoStack線上分享上的演講。他通過自身的實(shí)踐經(jīng)驗(yàn),詳細(xì)講解了高性能視頻推理引擎優(yōu)化技術(shù)。
?
文 /?鮑金龍
整理 / LiveVideoStack
?
大家晚上好,非常榮幸又有這個機(jī)會來LVS,與大家一起探討一些問題。我第一次參加LVS應(yīng)該是2017年,現(xiàn)在已經(jīng)接近4年的時間了。
今天的內(nèi)容是推理引擎優(yōu)化技術(shù),當(dāng)然有一個前提,主要是在端上。馮諾依曼體系的存儲矛盾,幾十年以來一直都是存在的主要矛盾。對于這個問題,如果是在NVIDIA顯卡上,或者是國內(nèi)的燧原NPU上,他們的解決方案是用最快的HBM內(nèi)存、HBM2.5 3D堆疊的內(nèi)存來增加總線的帶寬,中間加上3~6層的Cache。但是在端上這么做其實(shí)是有問題的,比如端上的芯片的功耗,以及面積來出發(fā)的話,無論是GPU,還是DSP,L2的Cache基本是1 M左右,同時內(nèi)存是所有的芯片共享的,還有LPDDR3、LPDDR4,功耗非常低,但是性能跟臺式機(jī)DDR和HBM內(nèi)存差10~20倍。所以,在端上的優(yōu)化,還是需要從推理引擎的總體設(shè)計(jì)、算子本身的執(zhí)行速度上,還有算子本身的可替換性上來入手,即從軟件開發(fā)上來進(jìn)行優(yōu)化,因?yàn)橛布虝r間內(nèi)想提高10倍、20倍,實(shí)際上是非常困難的。
?
01
—
優(yōu)化思路
目前的優(yōu)化思路,雖然有五項(xiàng),實(shí)際上可以分為三類。
第一類是從Framework角度來說,比如第一、二項(xiàng),思路是改變一下數(shù)據(jù)排列,使計(jì)算流水線執(zhí)行得更加有效。這是從Cache的利用率角度來做優(yōu)化,可以降低總線訪問,同時也能降低功耗。第三項(xiàng)就是算子本身做些優(yōu)化,在不改變算子算法原理的情況下,看能不能執(zhí)行得更快。第四項(xiàng)是旁路優(yōu)化,就是通過等效替換的原理來進(jìn)行。這里只列了兩點(diǎn),更復(fù)雜的優(yōu)化還有,但目前時間有限,我們探討不了太復(fù)雜的內(nèi)容。第五項(xiàng)是,如果想使用運(yùn)動補(bǔ)償,那么就有運(yùn)動向量從哪里來的問題,后面附加有一個運(yùn)動向量的估計(jì)。
?
第一部分就是數(shù)據(jù)排列。這是一個非常基本的問題,視頻數(shù)據(jù)是一個在時間上連續(xù)的二維圖像序列,在空域上即單幀圖像上,有水平和垂直兩個方向的維度,在時間上和內(nèi)容上有大量的冗余。視頻數(shù)據(jù)有一個很強(qiáng)烈的特點(diǎn)就是,在幀內(nèi),比如分塊的二維數(shù)據(jù)塊內(nèi)相關(guān)性比較強(qiáng),比如平坦塊都是平坦塊,如果有紋理的話紋理都是連續(xù)的。第二個特點(diǎn)就是,臨近的二維數(shù)據(jù)塊相關(guān)性也比較強(qiáng)。第三個特點(diǎn)是,幀和幀之間,通過運(yùn)動向量關(guān)聯(lián)起來,它實(shí)際上就是一個Patch,三維數(shù)組相關(guān)性比較強(qiáng)。那么相關(guān)性強(qiáng)的表現(xiàn)是,不只紋理特征相似,還有高頻和低頻信號也分別相似(就是頻譜特征相似)。這里是從時域和空域兩個角度來看的。相關(guān)性就造成了可以讓算法加速的很多機(jī)會,后面將逐漸展開講解這些內(nèi)容。
目前見到的常規(guī)推理引擎數(shù)據(jù)處理方式實(shí)際是Planar結(jié)構(gòu),它可能是多通道的,比如RGB,也可能是三個Channel,比如YUV、420格式、444格式。執(zhí)行方式是行掃描,一行從左執(zhí)行到右,然后第二行從左到右,跟早年電視機(jī)的掃描方式是一樣的,掃描到一幀圖處理完。這個方式是通用的,不管是什么類型的數(shù)據(jù)都可以執(zhí)行完。但它也有一些問題。第一,這是按行執(zhí)行的,那么數(shù)據(jù)局部的相關(guān)性沒法從這種處理方式得到照顧。第二,這是整張圖掃完,那么數(shù)據(jù)的吞吐量非常大。整張圖掃描的方式可以理解為整張圖是一個塊,這個塊就是最大塊。一般來說,如果圖比較大,比如Y通道數(shù)據(jù)有1~2 M大小,L2 Cache基本上就1 M,這就是一個很典型的Cache顛簸現(xiàn)象,就是說,處理完一個Filter的時候,接著處理下一個Filter的時候,你上次讀的數(shù)據(jù)已經(jīng)完全刷出去了,你需要重新再load進(jìn)來。那么,問題相對來說比較嚴(yán)重了,無論是速度還是功耗,表現(xiàn)都非常差。
那么我們要解決這個問題,可以按照視頻的編解碼一樣將數(shù)據(jù)分塊,常規(guī)的分塊就是8*8。一般來說,8*8的塊能夠充分照顧到紋理,有局部性,同時它內(nèi)部也包含足夠多的特征。塊過大或者過小都各有利弊。那么,一般的分塊存放是有跨度的。如果是8*8的塊,第一個8個像素是在第一行,依次有一個很大的跨度,如果用現(xiàn)在的適合人工智能、大并發(fā)的指令來處理,比如AVX512,一次能做64個int 8的乘法,DSP就更大了,一般有128~768個像素,768的數(shù)據(jù)組織是RGB 16*16的分塊,有三個通道,一次就能處理完。還有更多的,比如32*32。我們常常在網(wǎng)上看到一些NPU的設(shè)計(jì),如馬斯克的汽車導(dǎo)航用的NPU,或者是谷歌的TPU-1、2、3代,并發(fā)程度都一直在改進(jìn),但基本上都停留在這個水平上,不會太大,也不會太小。如果你是用420格式下有跨度地來讀取,會發(fā)現(xiàn)這個效率是非常低的。比如一個8*8的塊,需要8次尋址,8次讀入,每次讀入8個字節(jié),只執(zhí)行了一次計(jì)算指令,有8次寫出。這樣的效率其實(shí)非常非常低。那如何改進(jìn)呢?
其實(shí)這很簡單。改進(jìn)方式就是,將8*8的塊取消跨度,連續(xù)存放。比如將8個行連續(xù)放到64個字節(jié)里,后面的7個行都取消了,即這一行放了8行的數(shù)據(jù)。這樣處理的優(yōu)勢是,比如處理8*8的塊,只需要1次尋址,而且尋址是非常整齊的對齊地址,然后需要1次讀入,1次計(jì)算,1次寫出,這個速度就會快了7、8倍。但是,一般來說,我們達(dá)不到7、8倍的速度。原因就是,TILE格式數(shù)據(jù)想達(dá)到高效的處理,還要有數(shù)據(jù)轉(zhuǎn)換、堆疊過程,運(yùn)算過程中還有數(shù)據(jù)重排(后面依次會講到)。
TILE格式是需要數(shù)據(jù)重排的。首先,輸入的數(shù)據(jù)都是常規(guī)Planar格式的、RGB或YUV 420格式的,要轉(zhuǎn)換就是取消跨度。但是一般的推理引擎都是需要復(fù)制一次,它不會在原始數(shù)據(jù)上直接處理,需要Pad-邊界填充,同時可能也存在格式轉(zhuǎn)換,重排可以和這個融合起來。轉(zhuǎn)換函數(shù)permutation是一個執(zhí)行非常快的指令,整個過程跟memcpy一樣快,就是說,融合起來的代價就可以忽略了。其次,運(yùn)算時候的數(shù)據(jù)并不是永遠(yuǎn)地址對齊的,有可能需要對運(yùn)動向量地址不對齊的情況去做重排。上面這個圖指示的就是如何在輸入格式轉(zhuǎn)換的時候重排得更高效。重排的時候,并不是一次讀8個字節(jié),或者一次讀8行,而是一次讀64字節(jié)*8行,經(jīng)過重排以后,形成為8個8*8的數(shù)組。那么這個效率就是最高的,指令并行度都是飽和的、最大的。
?
接下來看一個地址重排的例子,這是最簡單的例子。更多例子的大家有機(jī)會去試試。
當(dāng)前假設(shè)A塊地址是(0,0),相鄰的是B、C、D,它們的間隔都是8,地址是對齊的。需要訪問G塊,地址是(6,5),所以,有一個運(yùn)算向量。如果是420格式下,做一個不對齊的訪問就可以了,或者做一個對齊的簡單拼接。但是在TILE格式下,要多做一些工作。首先需要拼接到E,就是在A、C兩塊之間拼接E塊,B、D兩個塊之間拼接出F塊。但是這兩個塊跟G塊還是有區(qū)別,它們位于一個水平線上的區(qū)域內(nèi),還要進(jìn)行X坐標(biāo)的重排。目前,AVX512的指令是這樣的,移動64位,用三條指令來完成。如果以后有更先進(jìn)的指令一次完成,那就更好。但很明顯,設(shè)計(jì)AVX512的時候,沒考慮到TILE格式。同時,注意,指令的后綴加了x,意思就是有個擴(kuò)展。基本的Intel指令,比如c= 5,就是常量,但常量的效率太低了,一般用的是變量。下面有一個等價的實(shí)現(xiàn),是通過另外一個permute、a,b兩個向量重排之后,形成一個等價的變量align過程。
如果在高通的HVX上,就容易得多。高通的執(zhí)行同樣也是5行指令,但是前4個都是一類指令valign,它既支持常量,也支持變量,執(zhí)行動作完全是一樣的。最后,是類似AVX512 Blend的一個vswap操作,融合生成一個塊。這些指令都是低延時指令,訪存要快得多,所以代價基本上非常小。如果后續(xù)執(zhí)行有進(jìn)一步優(yōu)化,可以把代價分散一下,那效果就會更好。
02
—
計(jì)算流水線
接下來,我們看第二部分。想要計(jì)算的時候需要一個新的方式Framework,原來是一幀、一個Filter在整幀上以raster scan的方式執(zhí)行完,比如s0、s1、s2、s3、s4執(zhí)行完,實(shí)際上Cache是永不命中的狀態(tài),Cache在移動端的速度非常慢。一般來說,根據(jù)我們實(shí)驗(yàn)數(shù)據(jù),DDR內(nèi)存訪存的功耗,比芯片計(jì)算指令功耗的數(shù)據(jù)更大。要解決這個問題,我們想把所有這些過程的中間數(shù)據(jù)壓縮到最小,比如Cache利用率,能提高一些。那怎么做呢?要設(shè)計(jì)一個超級流水線。就是說,在從s0到s4的多個步驟,這些Filter可以在Pipeline上并行執(zhí)行。并行指的是數(shù)據(jù)的處理順序從原來的每幀順序依賴,轉(zhuǎn)換為每行順序依賴,這樣從幀的角度來看,數(shù)據(jù)處理就是并行的。這樣,數(shù)據(jù)基本都在Cache中,讀取的時候并不進(jìn)行過多的總線訪問,功耗也非常小。
我們現(xiàn)在是以TILE或ROW,如果是420格式的也可以采用超級流水線,那么就是一行為一ROW。如果是TILE格式,那么除了行ROW,還需要一個二級結(jié)構(gòu)TILE。
我們首先講的是引擎的優(yōu)化,但是,無論是普通引擎,還是優(yōu)化的、特殊的、緊密堆積的引擎,數(shù)據(jù)預(yù)取是非常重要的。因?yàn)橐苿有酒蠑?shù)據(jù)Cache非常小,如果數(shù)據(jù)不預(yù)取,基本都是不命中的狀態(tài)。常規(guī)下,我們看到行掃描下的也是一種流水線結(jié)構(gòu)。首先在循環(huán)開始之前,需要預(yù)取一行的數(shù)據(jù),然后循環(huán)中首先要預(yù)取下一行的數(shù)據(jù),然后處理當(dāng)前行的數(shù)據(jù)。process_row函數(shù)處理時間比較長,一般都會有1000個Clock以上。那么一個數(shù)據(jù)從總線開始加載到Cache中去,時間一般粗略認(rèn)為是100個Clock,有可能更慢,好一點(diǎn)機(jī)器可能是80個Clock,差一點(diǎn)機(jī)器是150個Clock。所以如果process_row執(zhí)行過快,那么預(yù)取的效率就不夠。比如處理的時候,prefecth_row并沒有預(yù)取到,那么預(yù)取就失敗了。但是如果處理一行,時間完全是夠的。如果有很多很多種Filter同時進(jìn)行,流水線有多級,數(shù)據(jù)預(yù)取跟raster scan相比效率就好多了。如圖中所示,這個預(yù)取是依次執(zhí)行的,比如黑色的一行,把預(yù)取和數(shù)據(jù)load放在一塊,那么預(yù)取就做一次,后面都不需要做預(yù)取了,因?yàn)橹虚g數(shù)據(jù)很小。原來你做了n次預(yù)取,現(xiàn)在實(shí)際上只做了第一次預(yù)取。如圖所示,預(yù)取指針指向了綠色行,后面紅色處理的要比它Delay 1行,后面黃色的就是處理過的。所以預(yù)取非常重要,如果沒有預(yù)取,那么引擎就不可能達(dá)到很高的性能。
接下來是超級流水線的設(shè)計(jì)。如圖所示,看看我們現(xiàn)在這個比較簡單的例子,有四級。第一級是預(yù)取一個1*1的load,綠色是5*5的卷積,藍(lán)色是3*3的反卷積(你就認(rèn)為有就可以了),然后黃色是7*7的后處理。這顯然對前方有一個數(shù)據(jù)依賴,如果其Tap條件不滿足,后續(xù)的操作基本上不能繼續(xù)。這里需要一個處理邏輯。看看上圖中的代碼,一次處理一個行,實(shí)際上就是使用4個process函數(shù)的指針,每次依次從上往下執(zhí)行,比如執(zhí)行l(wèi)oad一行之后馬上執(zhí)行第二個。那么數(shù)據(jù)完整性顯然不能滿足。因?yàn)榈诙塅ilter是5*5的,需要第5步處理完之后,第6步才能執(zhí)行,所以它就Fail了 。那么一直走到5之后再去6執(zhí)行,就發(fā)現(xiàn)數(shù)據(jù)完備了,那就6執(zhí)行了。6執(zhí)行后,下一級再處理了,嘗試到9,那9顯然不行。那么就從6直接break,返回到執(zhí)行7,處理完之后,8就完備了。那再繼續(xù)嘗試的時候,發(fā)現(xiàn)9完備。之后就是下一級到15,15并不完備,然后又返回到11。11之后是12,12完了之后發(fā)現(xiàn)13和14都產(chǎn)生了。因?yàn)檫@是一個上采樣操作,所以處理兩行,這時候15就可以執(zhí)行了。執(zhí)行之后,20還沒滿足,返回到16。這個邏輯就是依次循環(huán)下去。實(shí)際上,這里執(zhí)行效率比原來的、整張?zhí)幚淼男室叩枚?#xff0c;同時代碼也非常簡單。大家可以看一下,代碼其實(shí)是很優(yōu)美的,越優(yōu)美的代碼效率越高。
前面并沒有明確講是420格式還是TILE格式,無論哪種格式,數(shù)據(jù)依賴關(guān)系是一樣的。TILE是一種特殊的格式,有一些特殊性。首先,預(yù)取就不應(yīng)以行來執(zhí)行,而是按照TILE塊來執(zhí)行。如圖所示,綠色的是預(yù)取指針,紅色的是處理指針指的是后面一塊,黃色的塊是已經(jīng)處理過的。所以這里一個函數(shù)process_tile_row處理一行的時候,實(shí)際上是在循環(huán)外,預(yù)取第一個TILE,然后取下一個TILE,處理當(dāng)前TILE,循環(huán)就執(zhí)行完了。依賴性判斷是,因?yàn)橐粋€TILE是8*8,最大支持一個Filter的Tap是17,這很容易算出來,中心點(diǎn)加上兩翼8+8。無論是卷積核還是濾波器都沒有這么大,7*7、9*9就是極限了,一般是3*3、5*5就足夠了。所以,這個判斷比較簡單,只需要看前面Filter的下一個TILE處理沒有,沒有處理就返回Fail,處理了就繼續(xù)執(zhí)行下去。
TILE格式下,一個TILE相當(dāng)于原來的ROW的8行,Cache的消耗更大。一般來說,一個1080P單通道,在前面的流水線下,消耗是50 k。這個例子里只消耗50 k,這個應(yīng)該遠(yuǎn)遠(yuǎn)超過大家的想象。我知道很多引擎在處理數(shù)據(jù)的時候,動輒消耗2、300 M的數(shù)據(jù)緩沖。如果設(shè)計(jì)這么一個引擎,只消耗了50 k的數(shù)據(jù),那就是很驚人了。但實(shí)際上,這里有一個格式轉(zhuǎn)換、輸入輸出轉(zhuǎn)換,還最少需要2幀的緩沖。前面說過,(int數(shù)據(jù)類型的話)Cache算是50 k,但是數(shù)據(jù)類型有可能是浮點(diǎn)的、或者是int 32的,50*4 = 200 k就相對來說大一些,如果再乘8,就超過了L2的容量。那這個情況的處理很簡單,在執(zhí)行的時候,把數(shù)據(jù)分為多個TILE,現(xiàn)在把它叫作Segment。現(xiàn)在這一個Segment就是原來的四分之一,那么流水線的消耗自然也是四分之一,即壓到了L2 Cache消耗范圍內(nèi)。這是一個很簡單的變換操作。
03
—
算子
我們很快將TILE格式和流水線講完了,下面進(jìn)入第三部分。首先是算子合并。計(jì)算量非常小的算子的讀取和寫出的代價相對于計(jì)算來說是太大了。這里要做一個格式轉(zhuǎn)換,比如int 8變成了int 32,float變成了int。比如簡單的加減,甚至開方,有的軟件模擬性的開方指令的計(jì)算量是比較大的,但是一般是硬件指令,這個Delay就很小。這種情況下,我們思路是,這個函數(shù)是一個加,執(zhí)行就是一個循環(huán),原來的算法是按行執(zhí)行,這一行作為加寫出。指令load a需要5個周期,load b需要5個周期,add只需要1個周期,寫出store需要5個周期。效率確實(shí)很慢。實(shí)際上,如果load的時候,沒有預(yù)取,那么周期可能就是50個或100個,這就是非常惡劣的情況,我們在很多工程代碼里都看到。如果預(yù)取了,那么就是5個周期的損失。
?
圖的右邊是一個開方函數(shù),也進(jìn)行了類似的操作,讀取、計(jì)算,然后寫出。那么,我們認(rèn)為開方需要消耗6個周期。這里,我們進(jìn)行三個計(jì)算,對a和b分別進(jìn)行開方計(jì)算,再將a和b開方的結(jié)果加起來,那么指令的代價加起來是48。
那么我們重新選一個函數(shù),把這些操作都合并了,簡簡單單寫成一行代碼。這種情況下,我們再看一下代價,load a和load b都是5,一共是27,這樣快了一倍。這是很簡單的思路。
類似的還有一個多通道。通常,卷積是8組、16組、32組。如果我們做一個卷積,比如用AVX512、DPBUSD,在3*3的情況下,是不對稱的,所以均勻分布到3個向量里,占用每個向量的Slot里面的低3位。數(shù)據(jù)也是要同樣進(jìn)行組織,每個向量經(jīng)過DPBUSD計(jì)算,執(zhí)行三次得出SUM。同時還有其他的代價,如讀入。所以要盡可能一次將所有卷積和全做完,因?yàn)榕抨嚪绞绞怯写鷥r的。需要Swap重排,重排了之后最好把它放在寄存器中,一次用完。這里的例子是8個,卷積核可以認(rèn)為是const數(shù)組,一次一個循環(huán)就全做完了。完成之后直接輸出。
前面說的算子融合,我們可能會有一個問題。我們在做模型的時候,一個Element、一個運(yùn)算,單獨(dú)是一個函數(shù),是一個靜態(tài)的依次執(zhí)行。如果合并的話,那么排列組合非常非常多。假設(shè)我們現(xiàn)在支持180種運(yùn)算,里面至少有90種是合并的,這個排列組合就是天文數(shù)字了,靜態(tài)引擎是不可能的。在讀入模型的時候,為每個模型單獨(dú)構(gòu)建一個執(zhí)行代碼。從時機(jī)來說,構(gòu)建分三種。第一,在開發(fā)端上,比如用Mac筆記本,或者用服務(wù)器來編譯的時候,直接生成一個SO,發(fā)行的時候把SO導(dǎo)到APK里面去。第二種,我們把模型放在端上,譬如手機(jī)端用ARM CPU來執(zhí)行編譯過程,將源代碼算子合并,最后調(diào)用LLVM編譯器,生成優(yōu)化代碼,讓ARM CPU來完成。第三種,是在Device上完成,OpenCL就是在GPU上編譯的,在DSP上也是可以的,實(shí)際上,這完全看你自己的選擇。從安全性角度來說,我們才需要區(qū)分Host和Device,否則Host就可以了。但是,這里有一個問題,因?yàn)榇蠹叶加眠^OpenCL,在端上需要編譯代碼,編譯代碼的時間實(shí)際上比較長,一般是幾百毫秒。一般我們在做A/B實(shí)驗(yàn)的時候,APP啟動多出來幾百毫秒,這是比較致命的。
?
如果你覺得這個比較費(fèi)勁,那么還有另一種辦法。第二條我建議的是,優(yōu)化好的二進(jìn)制不需要用編譯器來編譯了,直接把每個算子變成二進(jìn)制unsigned char數(shù)據(jù),然后在運(yùn)行前根據(jù)模型把這些算子unsigned char數(shù)據(jù)直接拼起來,這時候就不需要優(yōu)化過程。假設(shè)拼接代碼優(yōu)化水平很高的話,拼接代價是可以忽略的,一般拼接速度快到千分之一秒。
04
—
算子旁路優(yōu)化
接下來是算子旁路優(yōu)化。前面首先講的是如何讓Cache利用率更高,功耗更低,然后是讓算子執(zhí)行效率更高,但是算子本身并沒有進(jìn)行算法優(yōu)化。旁路優(yōu)化有一個基本原則。一方面是處理的簡單特征,梯度方向,當(dāng)前8*8的塊分為兩部分區(qū)域,梯形的一個Edge,這些特征可以通過簡單快速算法獲取,不需要比較復(fù)雜的模型,性價比比較高。另一方面,復(fù)雜特征想要進(jìn)行計(jì)算,只有用深度方法。這兩方面是相輔相成的,在一定條件下,它們可以互換,快速算法可以替換深度算法。也就是說,某些數(shù)據(jù)塊用簡單算法的輸出,誤差可以接受,或者是零誤差,那么局部替換就是可行的,即一塊變化了,整個網(wǎng)絡(luò)輸出不會受影響。一般來說,判斷替換需要一個算法。比如運(yùn)動向量,殘差是否是0,預(yù)判算法都是有代價的,代價比收益要低很多才行。如果替換了之后,速度提升了3倍,同時有百分之十的代價,那么這兩部分比較,收益還是比較大的。否則,這件事就不用做了。我有一個親身經(jīng)歷,對一個算法做了紋理復(fù)雜性的預(yù)判,用TV(Totally Variance)方法,但實(shí)現(xiàn)有問題。比如一個720的圖需要判斷其代價是3~6ms,如果算法不進(jìn)行任何旁路全走一遍,在3~6 ms內(nèi)也走完了,而經(jīng)過預(yù)判后,速度反而更慢了。
接下來我們看看紋理復(fù)雜度分析。常規(guī)復(fù)雜度實(shí)際上也是一個TV算法,但是它的實(shí)現(xiàn)方式可以非常快,代價沒有多大。比如一個8*8的塊,首先要判斷它是平坦的,算一個平均值avg。如果它是一個平坦塊,每個點(diǎn)跟平均值的方差,或者等價的abs,代價小于一個值或者就是0,那么紋理就是平坦的。這樣做的好處是,比如一個卷積運(yùn)算,每個點(diǎn)需要做一個乘法,但在平坦情況下,它會退化,avg值都是一樣的,所以提出來之后,等號右邊的參數(shù)和就變成一個常量,o(n**2)或者o(n**3)的運(yùn)算退化為o(1)了。當(dāng)然還有其他更復(fù)雜的情況,思路是類似的,先要判斷,在旁路成立的情況下進(jìn)行等價替換。我們看看下面的例子:梯度方向的判斷,就是在一個方向上進(jìn)行TV運(yùn)算。比如在45度的方向取了8個點(diǎn),8個點(diǎn)跟平均值的方差非常小,或者是0,這個方向顯然是一個邊界。也可以計(jì)算多個方向的梯度,用Sobel算子、拉普拉斯算子,但是從可靠性角度來說,還是推薦用TV的方法。
平坦性紋理可以有一個很明顯的退化。跟它類似的還有一個明顯的o(1)的退化,即運(yùn)動補(bǔ)償。編解碼只有一個像素補(bǔ)償,但是深度推理引擎必須各個環(huán)節(jié)都做,不能因?yàn)橄袼貜?fù)制了,周圍的網(wǎng)絡(luò)中間數(shù)據(jù)就不計(jì)算了,網(wǎng)絡(luò)輸出就不等價了。所以在各個層次上都需要做足運(yùn)動補(bǔ)償。第一個是像素補(bǔ)償。兩個數(shù)據(jù)塊殘差為0,我們對這兩個塊分別做濾波器處理,可以認(rèn)為這兩個塊的結(jié)果也是殘差為0。做完F(b0)后,F(b1)就不用做了,直接復(fù)制過來就可以了。這實(shí)際上也是o(1)的操作。第二個是卷積補(bǔ)償。卷積有中間輸出結(jié)果,如果只是做了運(yùn)動補(bǔ)償,中間數(shù)據(jù)空了,那么卷積像素是有依賴的,網(wǎng)絡(luò)的輸出結(jié)果就不正常了,所以,卷積的中間結(jié)果也就是等價濾波器的中間結(jié)果,也需要進(jìn)行運(yùn)動補(bǔ)償。同時輸出的重要中間數(shù)據(jù),比如featuremap,也需要補(bǔ)償。這些補(bǔ)償都是有代價的,要估算一下復(fù)制的代價,或者直接硬算的代價。一般來說,在PC上,或者沒有功耗壓力、帶寬比較高的情況下,運(yùn)動補(bǔ)償?shù)氖找媸欠浅4蟮摹T谝苿悠脚_上,如果是內(nèi)存總線非常慢的情況下,要衡量一下替換多復(fù)雜的濾波器。一般有一個平衡點(diǎn),過了平衡點(diǎn)就是有收益的,如果不到的話,那么運(yùn)動補(bǔ)償就是失敗的。
05
—
快速運(yùn)動估計(jì)
下面進(jìn)入到最后一部分,我們介紹快速運(yùn)動估計(jì)。運(yùn)動補(bǔ)償都需要一個Block Match的過程,或者是光流,每一個像素都需要一個運(yùn)動向量,一般我們就使用8*8塊的運(yùn)動向量。通常,我們的算法跟解碼器相結(jié)合,可以獲得一個運(yùn)動向量,但是編解碼的時候都是有殘差的,這可能對你的算法的干擾非常大,導(dǎo)致大多數(shù)情況的運(yùn)動向量并不能使用。這種情況下,你需要自己獲取這個運(yùn)動向量。如果是常見的編碼器上的運(yùn)動估計(jì),代價是特別龐大的,會比你本身的深度算法還要慢,所以沒有必要用這種方法來獲取。但是,近年來涌現(xiàn)了一些快速算法,就是快速運(yùn)動估計(jì),這個算法有幾個特征。第一,跟原來算法都使用一個大的搜索窗口相比,快速算法窗口上有初始化的預(yù)測運(yùn)動向量,因?yàn)檫@個預(yù)測運(yùn)動向量的存在,窗口可以變得很小。我們假設(shè)匹配的目標(biāo)如上圖所示,原來沒有優(yōu)化過的搜索算法要使用一個很大的搜索窗口,而快速算法有了預(yù)測向量后,窗口可以做得很小,即搜索次數(shù)很小,就可以收斂了。因?yàn)轭A(yù)測向量是通過不同渠道獲取的,它不一定符合,它有各種變化,跟像素的復(fù)制不一樣,向量不能直接用,你要重新執(zhí)行搜索過程,但是這個搜索比沒有預(yù)測的窗口要快得多。
那么,預(yù)測向量如何獲取呢?我們假設(shè)有一個序列,從frame0到frame1,mv1已經(jīng)通過了搜索的方式獲取了。我們可以預(yù)測,比如從frame0到frame2做一次搜索,那么就可以認(rèn)為當(dāng)前這個塊是勻速運(yùn)動的,那就把這個運(yùn)動向量引申到frame2上,mv2就是很簡單,mv1乘以2就可以了,在此基礎(chǔ)上確定這個窗口,再重新進(jìn)行搜索。但是,還有其他的方法,比如反向搜索,從frame1到frame0。這個點(diǎn)所在的Block就可以用這個運(yùn)動向量,運(yùn)動向量反向指向frame0的搜索,同時frame2也是可以用的,也可以反向進(jìn)行。可能性很多,在新版本VVC里應(yīng)該有類似的算法,這并不復(fù)雜。
接下來是第二種獲取方法,即Ankor(錨)。什么是Ankor?如圖中所示,數(shù)據(jù)分了很多TILE,但不是所有的TILE都進(jìn)行搜索,我們只搜索隔行隔列,是總數(shù)的四分之一,再將這四分之一的TILE作為Ankor,進(jìn)行運(yùn)動搜索。剩下的四分之三,就使用臨近的Ankor獲得的運(yùn)動向量作為預(yù)測向量。不同的體系結(jié)構(gòu),Ankor的選取和執(zhí)行順序有變化。比如是GPU,那么Ankor之間沒有時間依賴,同時搜索就可以了。如果是DSP CPU,可以用raster scan,那么下一個Ankor可以用前一個Ankor,這樣就更快些。如果是并行,也沒有任何問題,四分之一數(shù)量Block搜索的代價比較小。
接下來還有一個問題,失敗的可能性有兩種。第一,預(yù)測的基本向量方向不對。第二,窗口不夠大。這兩種情況都可能造成Ankor搜索失敗。一般,我們不會放大窗口,或者原地轉(zhuǎn)圈換方向的操作,而是在低分辨率上進(jìn)行完全一樣的搜索,就是搜索算法不變。但是因?yàn)榉直媛适撬姆种?#xff0c;實(shí)際上等價窗口是2*2,在低分辨率的TILE進(jìn)行搜索,把運(yùn)動向量乘以2,就作為當(dāng)前TILE的初始化。我們獲得新的窗口,避免使用速度比較慢的較大的搜索窗口。實(shí)際上,可以將其看作是2級的金字塔,更復(fù)雜的會有3級。但因?yàn)槲覀冇腥N預(yù)測方法,那么3級的收益就不是很大了,用2級就可以了。
最后一點(diǎn)是搜索。我們現(xiàn)在有運(yùn)動向量,有窗口了,那么搜索怎么進(jìn)行呢?傳統(tǒng)的raster scan就是一行一行地搜索,16*16的結(jié)構(gòu),要搜索256次,這顯然是不可以接受的。我們引出了下山法搜索,用變步長。第一次步長是4。比如16*16的窗口下,搜索13個點(diǎn)的菱形,第一次搜索13個點(diǎn),然后用殘差最小的點(diǎn)作為新的方向。第二次換成步長2,就是藍(lán)色的區(qū)域變成新的搜索窗口,如果得到一個非常荒唐的結(jié)果那就失敗了。如果殘差繼續(xù)縮小,步長換為1,就是綠色的窗口,最后輸出最小殘差的運(yùn)動向量。
06
—
優(yōu)化收益數(shù)據(jù)
到此為止,快速的運(yùn)動估計(jì)就介紹完了。前面講了三種優(yōu)化方法,更多方法這次沒有討論。一般這三種優(yōu)化方法都做的話,會得到一個很明顯的收益。比如,第一種方法有3倍收益,第二種方法也有3倍收益,第三種方法有4倍收益。如圖所示,這里舉了兩個例子。第一個是超分辨率。如果是用傳統(tǒng)的420格式,AVX2指令優(yōu)化,那就不到200 fps。因?yàn)檫@個算法是面向移動端的。如果在PC上,數(shù)據(jù)是比較嚇人的。但是優(yōu)化之后,比如使用了TILE格式,沒有使用紋理分析運(yùn)動補(bǔ)償?shù)那闆r下,速度非常快,到1000 fps以上。那么,再進(jìn)一步,把紋理分析、旁路分析都加入的時候,速度又拔升了百分之四十到百分之六十。第二個例子是傳統(tǒng)算法VBM3D,它的收益也很夸張,因?yàn)槲覀冇昧丝焖龠\(yùn)動估計(jì)算法。它的主要運(yùn)算都是在Block Match上,我們把Block Match進(jìn)行加速之后,收益非常明顯。如果是420格式下,網(wǎng)上的開源實(shí)現(xiàn),用5幀序列,AVX2優(yōu)化,1080的數(shù)據(jù)的性能是1 fps,如果是8*8 AVX512,那么就是超過100 fps。如果是加上Early Skip 模式,跟運(yùn)動補(bǔ)償不一樣,這個不是運(yùn)動參考的方式。這個模式是指進(jìn)行DCT三維變換的時候,某些情況下可以超前處理一部分?jǐn)?shù)據(jù)。Early Skip也會產(chǎn)生1倍的收益,最終在1080p上達(dá)到220 fps。這基本上認(rèn)為已經(jīng)達(dá)到實(shí)時的程度。一個非常緩慢的1 fps的視頻算法,提升到220 fps,就是從離線場景進(jìn)入到實(shí)時場景這個過程。
?
今天的分享就是這些內(nèi)容。謝謝大家。
講師招募?LiveVideoStackCon 2021 北京站
LiveVideoStackCon 2021 北京站(9月3-4日)正在面向社會公開招募講師,歡迎通過?speaker@livevideostack.com?提交個人及議題資料,無論你的公司大小,title高低,老鳥還是菜鳥,只要你的內(nèi)容對技術(shù)人有幫助,其他都是次要的,我們將會在24小時內(nèi)給予反饋。點(diǎn)擊[閱讀原文]了解大會更多內(nèi)容。
總結(jié)
以上是生活随笔為你收集整理的高性能视频推理引擎优化技术的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: LiveVideoStack 2021招
- 下一篇: WebRTC对你意味着什么