吞吐提升30倍:CV流水線走向全棧并行化
引言
?斯坦福教授、Tcl 語言發明者 John Ousterhout 曾寫過一本書《軟件設計的哲學》,系統討論了軟件設計的通用原則和方法論,整書的核心觀點是:軟件設計的核心在于降低復雜性。
實際上,這個觀點也適用于涉及底層硬件適配的軟件設計。
以視覺模型開發為例,以往視覺模型開發過程中,人們一般會更加關注模型本身的優化來提升速度與效果。而對于圖像前處理(預處理)和后處理階段,人們則很少關注。
當模型計算即模型訓練和推理主階段的效率越來越高時,圖像的前后處理階段愈發成為圖像處理任務的性能瓶頸。?
具體而言,在傳統的圖像處理流程中,前后處理部分通常都是用 CPU 進行操作的,這會導致整個流程中 50% 到 90% 以上的工作負荷都和前后處理相關,從而它們會成為整個算法流程的性能瓶頸。
一、主流CV庫的局限性
上述問題是目前市面上的主流 CV 庫在應用場景上的主要局限性,也就是說,對底層硬件依賴的不一致性導致了復雜性和性能瓶頸。正如 John Ousterhout 總結復雜性原因時所道:復雜性源于依賴性。
?主流的圖像處理庫 OpenCV,其應用場景非常廣泛,但在實際使用的時候也會面臨一些問題。
比如用 OpenCV 的 CPU 版本先做訓練再做推理的時候,在推理階段可能需要一個性能比較高的版本。
因為在訓練場景里,前后處理與模型推理可以在時間上進行覆蓋,從而覆蓋前處理的時間。但推理流水線中,模型只包含前向推理,且經過 Tensor RT 加速后耗時急劇減小,這時前處理的耗時占比會非常高,難以被模型推理所覆蓋。
要想減少推理場景的耗時,提高推理場景的性能,一般會用 OpenCV 的 GPU 版本進行加速。
但是 OpenCV 的 CPU 版本和 GPU 版本之間可能會出現結果不一致的情況。典型的例子是 resize 算子,其在 CPU 版本和 GPU 版本上對于差值的計算方式是不一致的。
OpenCV 在訓練和推理的時候會使用不同版本的算子,在訓練的時候一般用 CPU,因為其 CPU 算子覆蓋度比較高,在推理的時候一般用 GPU,因為性能比較好。因此,這也會導致結果對齊的問題。也就是說,當用 CPU 做模型訓練,并用 GPU 做模型推理的時候,會導致最終的輸出結果無法對齊。
其次,部分 GPU 算子的性能會有所退化。在 OpenCV 中,部分 GPU 算子本身的耗時比較大,從而導致整個算子的性能回退,甚至差于 CPU 版本。
第三,OpenCV 的 GPU 算子覆蓋度是有限的,部分算子只有 CPU 版本。還有一些 GPU 算子在參數、數據類型等方面的覆蓋度也沒有 CPU 版本高,從而帶來使用上的限制。
最后,如果在使用中將 CPU 算子和 GPU 算子交互使用,就會帶來大量的 CPU 和 GPU 之間的數據拷貝和同步操作,進而導致整體的加速性能不夠高。
另外一個常用的圖像處理庫是 TorchVision。?
?TorchVision 在做模型推理的時候,有些算子缺乏 C++ 接口,從而在調用的時候缺乏靈活性。如果要生成 C++ 版本,必須通過 TorchScript 生成。這會導致使用上的許多不便,因為在流程中間插入其它庫的算子來交互使用會帶來額外開銷和工作量。TorchVision 還有一個缺點是算子的覆蓋度不高。
以上就是目前的主流 CV 庫的局限性。?
二、統一 CV 流水線
既然前后處理的性能瓶頸主要在于使用 CPU 計算,而模型計算階段使用 GPU 的技術已經越來越成熟。
那么,一個很自然的解決方案是,用 GPU 對前后處理進行加速,對整個算法流水線將會有非常大的性能提升。
?為此,NVIDIA英偉達攜手字節跳動開源了圖像預處理算子庫 CV-CUDA。CV-CUDA 能高效地在 GPU 上運行,算子速度能達到 OpenCV 的百倍左右。
2023 年 1 月 15 日,9:30-11:30,由 NVIDIA英偉達 主辦的『CV-CUDA 首次公開課』,邀請了來自 NVIDIA英偉達、字節跳動、新浪微博的 3 位技術專家(張毅、盛一耀、龐鋒),就相關主題進行深度分享,本文匯總了三位專家的演講精華。?
?采用 GPU 替換 CPU 有很多好處。首先,前后處理的算子遷移到 GPU 上以后,可以提高算子的計算效率。
其次,由于所有的流程都是在 GPU 上進行的,可以減少 CPU 和 GPU 之間的數據拷貝。
最后,把 CPU 的負載遷移到 GPU 上后,可以降低 CPU 的負載,將 CPU 用于處理其它需要很復雜邏輯的任務。?
將整個流程遷移到 GPU 上后,對于整個流水線可以帶來近 30 倍的提升,從而節省計算開銷,降低運營成本。
通過圖中數據對比可以看到,在相同的服務器和參數配置下,對于 30fps 1080p 視頻流,OpenCV 最多可以開 2-3 個并行流,PyTorch(CPU)最多可以開 1.5 個并行流,而 CV-CUDA 最多可以開 60 個并行流。可以看出整體性能提升程度非常大,涉及到的前處理算子有 resize、padding、normalize 等,后處理算子有 crop、resize、compose 等。
三、異步化
為什么 GPU 可以適配前后處理的加速需求?得益于模型計算與前后處理之間的異步化,并與 GPU 的并行計算能力相適應。
我們以模型訓練和模型推理的預處理異步化分別進行說明。
1、模型訓練的預處理異步化
模型訓練可以分為兩部分,第一個是數據準備,第二個是模型計算。
目前主流的機器學習框架,比如 PyTorch、TensorFlow,它們在數據準備和模型計算之間是異步的。以 PyTorch 為例,其會開啟多個子進程進行數據的準備。
如圖中所示,其包含兩個狀態,即模型計算和數據準備,兩者存在時間先后關系,比如當 D0 完成之后,就可以進行 B0,以此類推。
從性能角度看,我們期望數據準備的速度能夠跟得上模型計算的速度。但實際情況中,一些數據讀取和數據預處理過程的耗時很長,導致相應的模型計算在進行前有一定的空窗期,從而導致 GPU 利用率下降。
數據準備可以分成數據讀取和數據預處理,這兩個階段可以串行執行,也可以并行執行,比如在 PyTorch 的框架下是串行執行的。
影響數據讀取的性能因素有很多,比如數據存儲介質、存儲格式、并行度、執行進程數等。
相比之下,數據預處理的性能影響因素比較簡單,就是并行度。并行度越高,數據預處理的性能越好。也就是說,讓數據預處理與模型計算異步化,并提高數據預處理的并行度,可以提高數據預處理的性能。
2、模型推理的預處理異步化
?在模型推理階段,其性能有兩個指標,第一個是吞吐,第二個是延時。一定程度上,這兩個指標是彼此互斥的。
對于單個 query 而言,當 server 接收到數據之后,會進行數據的預處理,再進行模型推理。所以對于單個 query 而言,一定程度上它是一個串行的過程。
但這樣做在效率上是很低的,會浪費很多計算資源。為了提高吞吐量,很多推理引擎會采用和訓練階段一樣的策略,將數據準備和模型計算異步化。在數據準備階段,會積累一定量的 query,組合成一個 batch,再進行后續的計算,以提高整體的吞吐量。
從吞吐而言,模型推理和模型訓練是比較類似的。把數據預處理階段從 CPU 搬到 GPU 上,可以得到吞吐上的收益。
同時,從延時的角度上看,對于每條 query 語句,如果能夠減少預處理過程所花費的時間,對于每條 query 而言,其延時也會得到相應的縮短。
模型推理還有一個特點是,其模型計算量比較小,因為只涉及前向計算,不涉及后向計算。這意味著模型推理對數據預處理的需求更高。?
3、核心問題:CPU 資源競爭
假設有足夠的 CPU 資源用于計算,理論上預處理不會成為性能瓶頸。因為一旦發現性能跟不上,只需要增加進程做預處理操作即?可。
因此,只有當 CPU 出現資源競爭的時候,數據預處理才可能成為性能瓶頸。
在實際業務中,CPU 資源競爭的情況是很常見的,這會導致后續訓練和推理階段中 GPU 利用率降低,進而訓練速度降低。
隨著 GPU 算力不斷增加,可以預見,對數據準備階段的速度要求會越來越高。
為此,將預處理部分搬上 GPU,來緩解 CPU 資源競爭問題,提高 GPU 利用率,就成了很自然的選擇。
總體而言,這種設計降低了系統的復雜性,將模型流水線的主體與 GPU 直接適配,對于提高 GPU 和 CPU 的利用率都能帶來很大的助益。同時,它也避免了不同版本之間的結果對齊問題,減少了依賴性,符合 John Ousterhout 提出的軟件設計原則。?
四、 CV-CUDA
把預處理以及后處理過程搬上 GPU 需要滿足多個條件。
第一是其性能至少要好于 CPU。這主要基于 GPU 的高并發計算能力。
第二是對預處理的加速,不能造成對其它流程比如模型推理的負面影響。對于第二個需求,CV-CUDA 的每個算子都留有 stream 和 CUDA 顯存的接口,從而可以更合理地配置GPU的資源,使得在 GPU 上運行這些預處理算子的時候,不會過于影響到模型計算本身。
第三,互聯網企業中有非常多樣的業務需求,涉及的模型種類很多,相應的預處理邏輯也是種類繁多,因此預處理算子需要開發成定制化的,從而有更大的靈活性來實現復雜的邏輯。
總體而言,CV-CUDA 從硬件、軟件、算法、語言等方面對模型流水線中的前后處理階段進行了加速,以及整個流水線的統一。
1、硬件
?硬件方面,CV-CUDA 基于 GPU 的并行計算能力,能夠大幅提高前后處理的速度和吞吐,減少模型計算的等待時間,提高 GPU 的利用率。
CV-CUDA 支持 Batch 和 Variable Shape 模式。Batch模式支持批處理,可以充分發揮 GPU 的并行特性,而 OpenCV 不管是 CPU 還是 GPU 版本都只能對單張圖片進行調用。
Variable Shape 模式是指在一個batch當中,每張圖片的長和寬可以不一樣。網絡上的圖片一般長寬都是不一致的,主流框架的做法是把長和寬分別 resize 到同一個大小,再對同一長寬的圖片打包為一個 batch,再對 batch 進行處理。CV-CUDA 可以直接把不同長和寬的圖像直接放在一個 batch 中進行處理,不僅能提升效率,使用上也很方便。
Variable Shape 的另外一層含義是在對圖像進行處理的時候,可以指定每張圖片的某些參數,比如 rotate,對一個batch的圖像可以指定每張圖片的旋轉角度。?
2、軟件
軟件方面,CV-CUDA 開發了大量的軟件優化方法來做進一步的優化,包括性能優化(比如訪存優化)和資源利用優化(比如顯存預分配),從而可以高效地運行在云端的訓練和推理場景中。
首先是顯存預分配設置。OpenCV在 調用 GPU 版本的時候,部分算子會在內部執行 cudaMalloc,這會導致耗時大量增加。在 CV-CUDA 中,所有的顯存預分配都是在初始化階段執行,而在訓練和推理階段,不會進行任何顯存分配操作,從而提高效率。
其次,所有的算子都是異步操作的。CV-CUDA 對大量kernel進行了融合,從而減少 kernel 的數量,進而減少 kernel 的啟動時間以及數據拷貝擦做,提高整體運行的效率。
第三,CV-CUDA 還對訪存進行了優化,比如合并訪存、向量化讀寫等,提高帶寬的利用率,還利用 shared memory 來提高訪存讀寫效率。
最后,CV-CUDA 在計算上也做了很多優化,比如 fast math、warp reduce/block reduce 等。
3、算法
?算法方面,CV-CUDA 的算子都是獨立設計的、定制化的,從而可以支持非常復雜的邏輯實現,并且方便進行使用和調試。
如何理解獨立設計?圖像處理庫的算子調用有兩種形式,一種是整體性的 pipeline 形式,只能獲取 pipeline 的結果,比如 DALI,另一種是模塊化的獨立算子的形式,可以獲取每一個算子的單獨結果,比如 OpenCV。CV-CUDA 采用了和 OpenCV 相同的調用形式,在使用和調試上會比較方便。?
4、語言
語言方面,CV-CUDA 支持豐富的 API,可以無縫將前后處理銜接訓練和推理場景。
這些API包括常用的 C、C++、Python 的接口等,這使得我們可以同時支持訓練和推理場景,它還支持 PyTorch、TensorRT 的接口,在未來,CV-CUDA 還將支持 Triton、TensorFlow、JAX 等接口。
推理階段,可以直接用 Python 或 C++ 的接口進行推理,只要保證推理的時候將前后處理、模型、GPU 放在一個 stream 上即可。
五、應用案例
通過展示 CV-CUDA 在 NVIDIA 英偉達、字節跳動、新浪微博的應用案例,我們可以體會到 CV-CUDA 帶來的性能提升有多顯著。
首先是 NVIDIA英偉達展示的圖片分類案例。
在圖片分類的流水線中,首先是 JPEG decode,其對圖片進行解碼;綠色部分是前處理步驟,包含 resize、convert data type、normalize 和 reformat;藍色部分是使用 PyTorch 的前向推理過程,最后對分類的結果進行打分和排序。
將 CV-CUDA 與 OpenCV 的 CPU 版本與 GPU 版本進行性能對比可以發現,OpenCV 的 GPU 版本相比于 CPU 版本能得到較大的性能提升,而通過應用 CV-CUDA,又能將性能翻倍。比如 OpenCV 的 CPU 算子每毫秒處理的圖片數是 22 張,GPU 算子每毫秒處理的圖片數是 200 多張,CV-CUDA 則每毫秒可以處理 500 多張圖片,其吞吐量是 OpenCV 的 CPU 版本的 20 多倍,是 GPU 版本的兩倍,性能提升很明顯。
其次是字節跳動展示的 OCR1、OCR2、視頻多模態三個案例。
在模型訓練上,可以看到在 OCR1、OCR2、視頻多模態三個任務上,使用了 CV-CUDA 后獲得了 50% 到 100% 的性能收益。
為什么有這么大的性能收益?實際上這三個任務比較大的一個共同點是,它們的圖片預處理邏輯非常復雜,比如 decode、resize、crop 等,而且這些還是大類,實際上每個算子類中還可能有很多小類或子類預處理。對于這三個任務而言,其涉及到預處理鏈路上的數據增強種類可能就有十幾種,所以其對于 CPU 的計算壓力非常大,如果能把這部分計算搬到 GPU上,CPU 的資源競爭就會明顯下降,整體吞吐也能提高很多。
最后是新浪微博展示的視頻處理案例。
對于視頻處理流程,傳統的做法是把視頻幀先在 CPU 環境中解碼,把原始的字節流解碼成圖片數據,再做一些常規操作,比如 resize、crop 等,再把數據上傳到 GPU 上做具體的模型計算。
而 CV-CUDA 的處理方式是將 CPU 解碼之后放在內存中的字節流上傳到 GPU 上,并且預處理也位于 GPU 上,從而跟模型計算進行無縫銜接,不需要從顯存和內存之間的拷貝操作。
圖中給出了采用 OpenCV(奇數)和 CV-CUDA(偶數)各自的處理時間,藍色指的是模型的消耗時間,橙色指的是解碼的消耗時間,綠色指的是預處理的消耗時間。
OpenCV 可以分為 CPU 解碼和 GPU 解碼兩種模式,CV-CUDA 只采用 GPU 解碼模式。
可以看到,對于 CPU 解碼的 OpenCV,OpenCV 的解碼和預處理都比 CV-CUDA 的耗時高得多。
再看 OpenCV 采用 GPU 解碼的情況,可以看到,OpenCV 和 CV-CUDA 在模型和解碼部分的耗時是接近的,而預處理方面仍然差距很大。
在 pipeline 整體對比上,CV-CUDA 也有很明顯的優勢,一方面 CV-CUDA 更節省 CPU 資源,也就是將 GPU 利用率打滿的情況下,CV-CUDA 只需要 OpenCV 的 10%CPU 配置;同時,CV-CUDA 也更節省 GPU 資源,在整體 pipeline 上,CV-CUDA 效率提升70%。
六、未來展望
?CV-CUDA 在模型訓練和推理階段都能有效地解決 CPU 資源競爭的問題,從而能夠提高模型訓練和推理的效率。
但如何正確理解 CV-CUDA 的優勢?需要理解其發揮作用的根本前提,并且其優勢相對于 CPU、OpenCV 并不是絕對的。
首先, CV-CUDA 實際上也不是萬靈藥。比如在模型訓練階段,如果瓶頸不是在預處理上,而是在數據讀取、模型推理上。這時候,如果用 CV-CUDA 來替換原來的預處理方案,實際上也是沒有任何用處的。
此外,在使用 CV-CUDA 的過程中,如果對預處理邏輯合理分配 CPU 和 GPU 的工作量,實際上有時候能夠達到更好的性能效果。
比如,CPU 仍然可以進行圖片解碼和 resize,resize 之后再放到 GPU 上進行處理。
為什么把解碼和 resize 放到 CPU 上做?首先,對于圖片解碼而言,其實 GPU 的硬解碼單元是有限的。其次,對于 resize 而言,通常情況下,resize 都會把一張較大的圖片,轉換成一張較小的圖片。
如果在 resize 之前,把數據拷貝到 GPU 上,可能會占用很多的顯存數據搬運的帶寬。
當然,CPU 和 GPU 之間的工作量具體怎么分配,還是需要結合實際情況來判斷的。
而最重要的原則是,不要將 CP?U 和 GPU 之間的計算交替穿插進行,因為跨 device 傳輸數據都是有開銷的。如果交替過于頻繁,反而可能將計算本身帶來的收益抹平,進而導致性能不增反降。
2022 年 12 月,CV-CUDA 發布了 alpha 版本,其中包含 20 多個算子,比如常用的 Flip、Rotate、Perspective、Resize 等。
目前 OpenCV 的算子更多,有數千個算子,CV-CUDA 目前只對比較常用的算子進行加速,后續會不斷增加新的算子。
今年 3 月 CV-CUDA 還會發布 beta 版本,會增加 20 多的算子,達到 50 多個算子。beta 版本將包含一些非常用的算子,比如 ConvexHull、FindContours 等。
七、尾聲?
回過頭來看 CV-CUDA 的設計方案,可以發現,其背后并沒有太復雜的原理,甚至可以說一目了然。
從復雜性的角度,這可以說是 CV-CUDA 的優點。《軟件設計的哲學》提過一個判斷軟件復雜性的原則——如果一個軟件系統難以理解和修改,那就很復雜;如果很容易理解和修改,那就很簡單。
可以將 CV-CUDA 的有效性理解為,模型計算階段與 GPU 的適配性,帶動了前后處理階段與 GPU 的適配性。而這個趨勢,其實才剛剛開始。