騰訊邱東洋:深度模型推理加速的術與道zz

導讀:隨着業務規模的不斷髮展,算法模型複雜度不斷增加,實時性要求很高的場景,對在線推理優化提出很大挑戰。本文將和大家分享騰訊智能對話產品中模型推理優化的常見方法和聚焦GPU推理的方法論。主要內容包括以下幾大方面:

  • 背景介紹
  • 推理性能優化的常用方法
  • GPU並行加速的方法論
  • 總結

--

01 背景介紹

 

騰訊遊戲知幾是騰訊遊戲的智能對話機器人,這款產品主要功能包括QA對話、閒聊、語音陪伴助手等,已覆蓋和平精英、王者榮耀、天涯明月刀手遊、QQ飛車等100+騰訊遊戲。

從行業來看,基於transformer預訓練模型出現以後,尤其是bert模型,各種NLP任務的效果都有了質的飛躍。然而這些模型越來越複雜,參數量非常的大,導致在使用這些模型的時候存在較大的性能瓶頸,實現在線實時推理幾乎成爲一件不可能的事情,所以做好推理加速是一個非常棘手的技術問題。同時,在遊戲流量非常大的場景下,在性能上做好優化可以爲在線服務帶來質的飛躍,提高機器負載會使成本極大降低。

--

02 推理性能優化的常用方法

 

推理性能優化的常用方法主要包括兩大類,一類是模型的壓縮技術,比如模型的剪枝、量化、蒸餾等,在較大的預訓練模型下被廣泛使用;另外是推理加速的技術,在CPU、GPU加速上分別有一些方法。

1. 網絡剪枝

在神經網絡模型中,存在大量冗餘的神經元和權重,它們對最終的結果產生的影響的權重又很低。基於此,我們如果對大的模型採取一些有效的壓縮手段,就可以在保證模型效果的前提下,讓模型變得更小,方便在線部署。

網絡剪枝是從大型網絡中篩選出不重要的神經元以及權重,將它們從網絡中刪除,同時儘可能地保留網絡的性能。

剪枝主要分爲兩大類,如上圖所示:一個是非結構化的剪枝,一個是結構化的剪枝。

  • 結構化剪枝

剪除的基本單元是神經元,這種方式在現有的硬件條件下,可以實現比較明顯的推理加速和存儲的優勢。缺點是剪枝力度有點大,會對模型精度產生較大的影響,導致模型效果下跌,無法達到預期效果。

  • 非結構化剪枝

剪除的是單個權重,這樣對精度的損失就會小一些,但最終產生的是稀疏矩陣。這種情況下就需要下層的硬件以及計算庫有良好的支持,才能實現最終的推理加速和存儲優勢。

稀疏動量剪枝,是從頭訓練的一種方法,它的目的是在給定的網絡稀疏度下,選擇更有效的連接方式。

算法首先是要確定網絡的剪枝比例,作爲網絡的稀疏度,構建初始的稀疏網絡,然後再開始訓練初始網絡。初始網絡不是最優的結構,要確認哪些連接需要刪除,哪些需要重新添加網絡。通過這種先剪枝後生長的方式可以達到更優的目標。

2. 量化技術

量化技術是把高精度表示的網絡權重和激活值,用低精度來近似表示,實現網絡的輕量化。優勢如下:

  • 網絡存儲:每個層權重量化後,32位的比特就可以壓縮到8比特,就是浮點型到整形的量化,整個模型佔的空間就會變小;
  • 激活值: 通過使用較少位的數值表示,在處理同樣數據時需要讀/寫的內容就更短,內存帶寬的壓力就變得更小;
  • 計算時間:單位時間內處理定點運算指令就會比浮點運算的指令多。

量化一般分爲對稱和非對稱。

對稱量化是通過收縮因子,將32的浮點型的最大值映射到8比特的最大值,這是一種線性的量化技術。

如果原始的這個浮點型分佈不均勻的話,就會造成大量的數據集中。這是一種不飽和的線性量化,數據扎堆在某一個範圍內。這樣的話會存在一些範圍利用少的情況,導致精度的損失比較大。

爲了解決非飽和截取帶來的區間浪費以及數據失真問題,英偉達提出飽和截取,即截取一段數據分佈較爲均勻的區域映射到-127至127之間。具體操作是計算一個閾值T,將-T~T之間的區域進行線性映射,而超出區域的數據點則統一賦予邊界值+-127。這樣做可以保證數據的大部分分佈跟之前是一樣的,同時將超出截斷值的數據歸一化到邊界上,避免了數據的丟失。

嘗試不同的T值,然後返回使量化後分布與原分佈差距最小的T值。可以用分bins的方法來得到一組數據的分佈,再利用KL距離來衡量量化前後的數據分佈是否相似。

3. 模型蒸餾

蒸餾是一種模型壓縮常見方法,將複雜、學習能力強的網絡學到的特徵,表示“知識”蒸餾出來,傳遞給參數量小、學習能力弱的網絡。模型的參數量和所能捕獲的知識量之間,其實並非一個穩定的線性關係,如下圖所示:

並非如圖中曲線1,而是接近邊際效益,邊際收益逐漸減少的一種增長曲線,如曲線2、3。這兩個曲線還說明完全相同模型架構和模型的參數量,使用完全的訓練數據,能獲得的知識量也並不一定完全相同,這主要是訓練方法的不同。

知識蒸餾需要兩種類型的網絡:Teacher Model和Student Model。前者參數量大、結構複雜,後者參數量較小、結構相對簡單。二者可以是不同的網絡結構,但是採用相似的網絡結構,蒸餾效果會更好。訓練流程如下圖所示:

首先訓練Teacher Model,然後用其指導Student Model的訓練。將Teacher Model在Softmax層的輸出作爲數據的soft label,Student Model的loss function將是對soft label預測和hard label預測的loss的線性加權和。Student Model訓練好後,按照常規模型使用即可。知識蒸餾通過將Teacher Model的知識遷移到Student Model中,使Student Model達到與Teacher Model相當的性能,同時又能起到模型壓縮的目的。其侷限性在於,由於使用Softmax層的輸出作爲知識,所以一般多用於具有Softmax層面的分類任務,知識蒸餾在其它任務上的表現並不好。

4. Caching

CPU頻率遠快於主存訪問速度,在處理器時鐘週期內,CPU常常需要等待主存,浪費計算資源。爲了緩解CPU和內存之間速度的不匹配問題,增加了CPU cache 來解決。

cache 利用局部性原理來提高緩存命中率:

  • A. 時間局部性:如果某個數據被訪問,那麼在不久的將來它很可能被再次訪問;
  • B. 空間局部性:如果某個數據被訪問,那麼與它相鄰的數據很快也可能被訪問。

代碼匹配好局部性規則,可以提高緩存命中率進而提升性能。下面舉一個矩陣乘法的實例,如下圖所示:

上面遍歷計算符合矩陣計算公式,行列相乘得到結果。數組在內存中是按照行進行存儲的。上面計算的內存讀取流程如下圖所示,緩存矩陣B的緩存命中率很低。

交換j,k遍歷順序,計算結果不變,內存讀取示意如下圖所示,矩陣B按行讀取,緩存命中率顯著提高。

5. 指令集加速

英特爾指令集擴展是可提高性能的附加指令。當同一操作在多個數據對象上執行時,SIMD即單指令多數據的一種方式,是向量計算的一種方法,可以提高計算吞吐量。對於浮點數向量計算,用標量和向量指令集實現方式對比如下:

當向量很大時,標量版本循環次數很多,效率低。指令集細節可以查閱英特爾官網。

6. 多算子融合

算子融合是GPU上一個很重要的推理加速的一個優化手段,尤其是針對NLP這樣的大模型,會帶來比較顯著的效果的提升。對於GPU異構編程,每一次op操作都會有一個內核的調用和多次的顯存的讀取;對於小op來說啓動GPU kernel的時間會大於GPU計算時間,顯存的讀取開銷也很大;op數目太多的話,效率會變低;所以將算子合併,可以有效地提高計算的性能。

對照下圖AlexNet網絡結構,我們可以進行垂直和水平的算子融合;Conv + Bias + ReLU 垂直融合爲CBR算子;水平方向將輸入爲相同張量且執行相同操作的層融合在一起。

還有Parallel Reduction,Remove Padding等優化手段,這裏就不展開說了。

--

03 GPU並行加速的方法論

 

1. GPU概述

GPU本來的任務是做圖形圖像的,圖像有個特點就是並行度很高,屬於並行任務。隨着深度學習的發展,大家逐漸將目光轉向GPU並行計算。x86 CPU+GPU的這種異構應該是最常見的,通過異構計算來提高吞吐量。下圖是異構框架示意圖:

左圖:一個四核CPU一般有四個ALU,ALU是完成邏輯計算的核心,也是我們平時說四核八核的核,控制單元,緩存也在片上,DRAM是內存不在片上,CPU通過總線訪問內存,CPU適合低並行邏輯複雜程序。

右圖:GPU,綠色小方塊是ALU,我們注意紅色框內的部分SM,這一組ALU公用一個Control單元和Cache,這個部分相當於一個完整的多核CPU,但是不同的是多了很多計算核心,而control部分變小,可見計算能力提升了,控制能力減弱了;GPU適合高並行邏輯簡單的大數據計算。

CPU/GPU線程區別:

  • CPU線程是重量級實體,操作系統交替執行線程上下文切換花銷很大;GPU線程是輕量級的,包括成千上萬個,多數在排隊狀態,線程之間的切換基本沒有開銷。
  • CPU的核被設計用來儘可能減少一個或二個線程運行時間的延遲;而GPU則是大量線程,最大幅度地提高吞吐量。

2. CUDA基礎

(1) 編程結構

一個完整的CUDA應用可能的執行順序如下圖:

 

從host的串行到調用核函數-->核函數被調用後控制馬上歸還主機線程-->繼續執行host代碼。

(2) 內存管理

  • 主機和設備之間的數據傳輸

設備與GPU之間的峯值理論帶寬遠高於主機和設備之間的理論帶寬;

基於這種設計,我們在使用時應儘量避免主機和設備之間的數據傳輸;

優化:固定內存、異步、統一虛擬地址等。

  • 設備上有層次的內存空間

全局內存:資源豐富,訪問延時最大;

共享內存:在片上,訪問更快。

(3) 線程管理

  • 分層結構讓並行過程更加靈活

如上圖所示,1個核函數調用只能有1個網格,但1個Grid可以包含多個塊,1個塊又可以包含多個線程。在網格中塊的組織形式,以及在塊中線程的組織形式,可以是一維、二維或三維,這樣就可以在並行計算中有很靈活的組織方式。

  • 塊內/塊間

塊內可以實現線程同步,共享內存;

塊間物理隔離,不能相互影響。

  • 線程編號

塊的維度和塊的索引相乘,再加上線程的索引。

(4) 執行模型

下圖從邏輯角度和硬件角度描述CUDA編程模型對應的組件。

SM中共享內存和寄存器是關鍵的資源,線程塊中線程通過共享內存和寄存器相互通信協調。因爲SM有限,雖然我們的編程模型層面看所有線程都是並行執行的,但是在微觀上看,所有線程塊也是分批次地在物理層面的機器上執行,線程塊裏不同的線程可能進度都不一樣,但是同一個線程束內的線程擁有相同的進度。

並行就會引起競爭,多線程以未定義的順序訪問同一個數據,會導致不可預測的行爲,CUDA只提供了一種塊內同步的方式,塊之間沒辦法同步。

同一個SM上可以有不止一個常駐的線程束,有些在執行,有些在等待,他們之間狀態的轉換是不需要開銷的。

3. 性能分析工具

在使用tensorflow的過程中,我們經常需要使用工具來監測模型的運行性能。合理的利用優化工具可以提高工作效率,讓我們把主要精力花在關鍵代碼的優化上。

nvvp,nvprof是cuda toolkit集成的工具,用於生成GPU timeline的工具。nvprof是命令行工具,我們的模型常常是運行在遠端的服務器上,我們需要把輸出的監測數據拷貝至本地查看,這個時候需要用到nvvp進行可視化分析。nsight是NVIDIA最新的用於監測kernel timeline的工具。

下圖是nvvp可視化分析各個函數在調用過程中的時間佔比情況。

上圖是一個矩陣加法例子,可以看到通過nvprof工具可以分析到GPU活躍活動中最耗時的部分是數據從主機到設備的傳輸,它佔了64%的時間。還可以看到API調用的一些熱點的分析,CudaMalloc這個函數被調用了三次,耗時大概500ms,佔比在一半以上,通過nvprof命令可以看到整個程序執行熱點的概述。

還可以通過這個指令結合參數分析程序具體的一些指標,分析它的achieved_occupancy(佔用率),gld_throughput(內存吞吐)和gld_efficiency(內存使用效率)。佔用率是在sm上活躍的線程數,佔最大的線程數的比例。

可以看到下圖中是0.7,這已經非常高。除了這三個指標之外,其實還有很多指標可以使用,具體可以參考NVIDA官網。

4. 理論計算極限

我們在優化之前需要搞清楚一個問題,我們這次優化究竟可以取得多大收益?

這裏介紹兩個定律,可以幫助大家在優化過程中做一個簡單理論上的評估。

先引入強擴展定義:規模固定的情況下,通過提高多處理器的數量來減少代碼執行的時間。

對應Amdahl's Law(阿姆達爾定律):提升一個系統的一個部分的性能對整個系統有多大影響。問題轉變爲怎麼測算加速比S,P是可以並行化代碼分數,N是並行部分可以得到的多處理器的數量。通過公式我們可以看到P越大,加速比就會越大,意味着優化空間就越大。如果P很小的情況下,不論增加多少計算資源,其實整個收益都會不佳。

對應弱擴展:問題的規模可能會隨着資源的增加而增加,可以用古斯塔夫森定律來理論分析,這裏加速比可以看隨着N的增加,整個問題規模也是越來越大。我們在優化的時候首先就是要分析問題屬於哪一類場景,再根據每類場景對應公式做一個簡單的推測。

在優化過程當中,每一項指標其實都是受到硬件的限制,那麼每個指標的理論的峯值天花板是多少呢?這裏也給大家簡單介紹一下。

Tesla K10 單精度FLOPS極限:

• 745 MHz 內核時鐘 * 2 GPU * (8 多處理器 * 192 浮點計算/ 多處理器) * 2 ops/cycle = 4.58 TFLOPS

Tesla K10 內存帶寬極限:

• 2 GPU * 256 bit * 2500 MHz 內存時鐘 * 2 DDR / 8 bits/ byte =320 GB/s

指令比 bytes:

• 4.58 TFLOPS / 320 GB/s yields 14.3 instructions: 1 byte

5. APOD方法論

  • Assess:對於一個項目,第一步要做的是接觸 (Assess) 項目,得到項目代碼中每部分的執行時間。有了這部分內容,開發者就可以找到並行優化的瓶頸所在,並開始嘗試用GPU加速。根據前面提到的Amdahl's and Gustafson's laws,可以確定並行優化的性能上界。
  • Parallelize:找到瓶頸所在並確定了優化的目標和期望,就可以優化代碼。調用一些如cuBLAS, cuFFT, orThrust的GPU-optimized library可能會很有效。另一方面,有些應用需要開發者重構代碼,以此讓可以被並行優化得部分被暴露出來。
  • Optimize:確定了需要被並行優化的部分之後,就要考慮具體的實現方式了。具體的實現方式通常只有一種,所以充分理解應用的需求是很有必要的。要記得,APOD 是一個反覆迭代的過程(找到可以優化的點,實現並測試優化,驗證優化效果,然後再重複)。因爲對於開發者來說,沒有必要最初就找到解決所有性能瓶頸的策略。優化可以在不同的 level 上進行,配合性能分析工具是很有幫助的。
  • Deploy:大的原則是當優化完一處之後,立刻將這一部分部署到生產環境,而不是再去尋找其他可以優化的地方。這樣做有很多重要的原因,比如這會使得用戶儘早從這個優化中收益。逐步迭代的方式,有助於減少風險,保證線上的穩定性。

6. 優化案例分享

(1) 矩陣加法

對比CPU,GPU執行矩陣加法,對比耗時變化;通過nvprof分析GPU執行性能,可以看到GPU的整體在GPU上雖然執行核函數只有十毫秒,但是整體的耗時也差不多有800ms,主要是從設備到儲存的數據傳輸,耗時比較大。代碼可以重點看第四步核函數的實現部分,可以改變grid,block來看性能的變化。

 

(2) 矩陣乘法

該例子分析使用共享內存和不使用共享內存性能差距。在Tesla P40 GPU機器上,對於16000多維的矩陣乘法矩陣,不使用共享內存耗時40多秒,而使用共享內存時間大概是8秒多,下圖展示了使用共享內存減少對全局內存的訪問次數分析。

 

使用共享內存設計核函數,下面兩個概念很重要:

  • 跨內存存儲體映射數據元素
  • 從線程索引到共享內存偏移的映射

GPU共享內存並不是線性的,而是二維的,分成不同存儲體的,並行也不是循環。對於一個二維的塊,數據是按照行放進存儲體中,線程束中取數據按照行來進行的訪問方式是最優x[threadIdx.y][threadIdx.x],threadIdx.x在線程束中爲連續變化的,對應到共享內存中也是遍歷共享內存的同一行的不同列。

--

04 總結

 

最後總結一下今天的分享內容。首先介紹了推理優化的“術”,即常見方法,包括模型壓縮(剪枝、量化和整理)、推理加速(Caching、指令集加速、多算子融合、其他)等;後面介紹了推理加速的“道”,聚焦GPU講了一些方法論,包括理解硬件,分析問題,建立目標和APOD循環迭代,還介紹了問題分析工具以及理論峯值的預估。本文介紹的是相對基礎的知識,大家實踐過程當中可以參考CUDA官網來深入研究。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章