在現代 CPU 上擴充套件 BERT 類模型推理 - 第 2 部分

釋出於 2021 年 11 月 4 日
在 GitHub 上更新

引言:使用英特爾軟體最佳化 CPU 上的 AI 效率

正如我們在上一篇博文中詳細介紹的,英特爾至強 CPU 提供了一套專為 AI 工作負載設計的功能,例如 AVX512 或 VNNI(向量神經網路指令),用於使用整數量化神經網路進行高效推理,以及額外的系統工具以確保工作以最有效的方式完成。在這篇博文中,我們將重點介紹軟體最佳化,並向您展示英特爾新一代 Ice Lake 至強 CPU 的效能。我們的目標是向您全面展示軟體方面可用的功能,以充分利用您的英特爾硬體。與上一篇博文一樣,我們透過基準測試結果和圖表展示效能,並提供新工具,使所有這些旋鈕和功能易於使用。

早在 4 月份,英特爾就釋出了其最新一代英特爾至強處理器,代號 Ice Lake,旨在實現更高效、更高效能的 AI 工作負載。更確切地說,與上一代 Cascade Lake 至強處理器相比,Ice Lake 至強 CPU 在各種 NLP 任務上的推理速度可提高高達 75%。這得益於硬體和軟體的結合改進,例如新指令和新 Sunny Cove 架構中 PCIe 4.0 的特性,以支援機器學習和深度學習工作負載。最後但同樣重要的是,英特爾致力於針對各種框架進行專門最佳化,這些框架現在都帶有英特爾的版本,例如英特爾 Scikit Learn 擴充套件英特爾 TensorFlow英特爾 PyTorch 擴充套件

所有這些功能在資料科學家和機器學習工程師日常使用的工具棧中都處於非常低的級別。在絕大多數情況下,更常見的是依賴更高級別的框架和庫來處理多維陣列操作,例如PyTorchTensorFlow,並利用高度最佳化的數學運算子,例如用於計算部分的BLAS (基本線性代數子程式)

在此領域,英特爾透過提供 oneAPI 傘形下的軟體元件發揮著重要作用,這使得透過英特爾 oneMKL (數學核心庫)、具有英特爾 OpenMP 或 執行緒構建塊 (oneTBB) 的更高級別並行化框架,可以非常容易地使用高效的線性代數例程。此外,oneAPI 還提供了一些領域特定的庫,例如英特爾 oneDNN 用於深度神經網路原語(ReLU、全連線等)或 oneCCL 用於集體通訊,當使用分散式設定訪問多個主機上的高效 all-reduce 操作時尤其有用。

其中一些庫,特別是 MKL 或 oneDNN,已原生包含在 PyTorch 和 TensorFlow 等框架中(自 2.5.0 版本起),以便為終端使用者開箱即用地帶來所有效能改進。當想要針對非常具體的硬體功能時,英特爾提供了最常用軟體的自定義版本,特別是針對英特爾平臺最佳化的版本。例如,對於 TensorFlow,英特爾提供了高度調整和最佳化的自定義框架版本,或者對於英特爾 PyTorch 擴充套件 (IPEX) 框架,可以將其視為一個在 PyTorch 上游化之前的特性實驗室。

深入探討:利用高階英特爾功能提高 AI 效能

效能調優旋鈕

如上所述,我們將介紹一套新的可調項,以提高我們 AI 應用程式的效能。從宏觀角度來看,每個機器學習和深度學習框架都由相同的組成部分構成:

  1. 記憶體中資料(向量、矩陣等)的結構化表示方式
  2. 數學運算子的實現
  3. 在目標硬體上高效並行化計算

除了上面列出的幾點,深度學習框架還提供了表示資料流和依賴關係以計算梯度的 S方式。這超出了本部落格的範圍,它利用了上面列出的相同元件!


圖 1. oneAPI 傘形下的英特爾庫概述

1. 記憶體分配和管理庫

這篇部落格文章將刻意跳過關於資料表示的第一點,因為它更多地是框架特定的。作為參考,PyTorch 使用其自己的實現,稱為ATen,而 TensorFlow 則為此目的依賴於開源庫Eigen

雖然對不同的物件結構和佈局應用通用最佳化非常複雜,但有一個領域我們可以施加影響:記憶體分配。簡單來說,記憶體分配在這裡指的是程式化地向作業系統請求一個動態(事先未知)的系統區域,我們可以在其中儲存專案,例如 C 中的 malloc 和派生函式或 C++ 中的 new 運算子。記憶體效率,無論是速度還是碎片化方面,都是一個廣泛的科學和工程主題,根據任務和底層硬體有多種解決方案。在過去的幾年裡,我們在這個領域看到了越來越多的工作,尤其是

每一個都提出了不同的方法來改進各種軟體上的記憶體分配和管理方面。

2. 高效的計算並行化

現在我們有了表示資料的高效方法,我們需要一種方法來充分利用我們可用的計算硬體。有趣的是,在推理方面,CPU 在某種程度上比 GPU 具有潛在優勢,因為它們無處不在,而且不需要特定的應用程式元件和管理人員來操作它們。

現代 CPU 擁有眾多核心和複雜的機制,旨在提高軟體的整體效能。然而,正如我們在第一篇部落格文章中強調的那樣,它們還具有可以根據您所針對的工作負載型別(CPU 密集型或 I/O 密集型)進行調整的功能,以進一步提高應用程式的效能。

儘管如此,實現並行演算法可能不像投入更多核心來完成工作那麼簡單。許多因素,例如使用的資料結構、併發資料訪問、CPU 快取失效——所有這些都可能阻礙您的演算法有效提高速度。作為參考,如果您有興趣深入探討該主題,我們推薦觀看 Scott Meyers 的演講:CPU 快取及其重要性

幸運的是,有一些庫可以使這種並行演算法的開發過程更容易且不易出錯。在最常見的並行庫中,我們可以提到 OpenMP 和 TBB(執行緒構建塊),它們在不同級別上工作,從 C/C++ 中的程式設計 API 到環境變數調整和動態排程。在英特爾硬體上,建議使用 OpenMP 規範的英特爾實現,通常稱為“IOMP”,它是 英特爾 oneAPI 工具包 的一部分。


圖 2. 透過 OpenMP 完成平行計算的程式碼片段

3. 最佳化的數學運算子

現在我們已經介紹了設計高效資料結構和並行演算法所需的構建模組,剩下最後一塊是執行計算的,即實現各種數學運算子和神經網路層,以完成我們最喜歡的事情——設計神經網路!😊

在每個程式設計師工具包中,都有多個級別可以提供數學運算支援,這些支援可以根據各種因素進行不同的最佳化,例如使用的資料儲存佈局(連續記憶體、分塊、打包等)、表示每個標量元素的資料格式(Float32、Integer、Long、Bfloat16 等),當然還有您的處理器支援的各種指令。

如今,幾乎所有處理器都支援對標量項(一次一個項)或向量化模式(意味著它們在相同的 CPU 指令中對多個項進行操作,稱為 SIMD“單指令多資料”)進行基本數學運算。著名的 SIMD 指令集包括 SSE2、AVX、AVX2 和最新一代英特爾 CPU 中存在的 AVX-512,它們能夠在單個 CPU 時鐘內操作超過 16 位元組的內容。

大多數時候,人們不必過多擔心為執行兩個向量之間的簡單元素級加法而生成的實際彙編程式碼,但如果您確實需要,也有一些庫允許您在編寫呼叫 CPU 特定內在函式的程式碼之上再高一層,以實現高效的數學核心。例如,英特爾的 MKL“數學核心庫”就提供了這一點,以及著名的 BLAS“基本線性代數子程式”介面來實現線性代數的所有基本操作。

最後,在此之上,還可以找到一些領域特定的庫,例如英特爾的 oneDNN,它提供了實現神經網路層所需的所有最常見和基本的構建塊。英特爾 MKL 和 oneDNN 原生整合在 PyTorch 框架中,可以為某些操作(如線性 + ReLU 或卷積)實現效能加速。在 TensorFlow 方面,可以透過設定環境變數 TF_ENABLE_ONEDNN_OPTS=1 (TensorFlow >= 2.5.0) 來啟用 oneDNN,以在底層實現類似的機制。

在最新的英特爾 Ice Lake CPU 上實現更高效的 AI 處理

為了報告 Ice Lake 產品線的效能,我們將嚴格遵循本系列第一篇部落格中使用的測試方法。提醒一下,我們將採用完全相同的模式來測試本第二篇部落格中將重點介紹的各種設定。更具體地說,以下部分中顯示的結果基於:

  • PyTorch: 1.9.0
  • TensorFlow: 2.5.0
  • 批大小:1、4、8、16、32、128
  • 序列長度:8、16、32、64、128、384、512

我們將透過該領域公認的指標來呈現結果,以建立所提出最佳化的效能:

  • 延遲:執行單個推理請求(即“前向呼叫”)透過模型所需的時間,以毫秒錶示。
  • 吞吐量:系統在規定時間內可以維持的推理請求(即“前向呼叫”)數量,以呼叫/秒錶示。

我們還將提供一個初始基線,顯示開箱即用的結果,以及一個應用了我們在第一篇部落格文章中強調的所有不同最佳化的第二個基線。所有測試均在英特爾提供的雲實例上執行,該例項配備了 Ice Lake 至強鉑金 8380 CPU,執行 Ubuntu 20.04.2 LTS。

您可以在各種雲提供商處找到相同的處理器:


圖 3. 英特爾 Ice Lake 至強 8380 規格

建立基線

如前所述,基線將由兩種不同的設定組成:- 開箱即用:我們按原樣執行工作負載,不進行任何調整 - 最佳化:我們應用部落格 #1 中存在的各種旋鈕

此外,根據我們對上一篇博文的評論,我們希望更改在最終基準測試中呈現框架的方式。因此,在第二篇博文的其餘部分,我們將根據以下內容劃分框架基準測試結果:

  • 使用“急切”模式進行計算的框架(PyTorch、TensorFlow)
  • 使用“圖”模式進行計算的框架(TorchScript、TensorFlow Graph、Intel Tensorflow)

基線:急切框架的延遲

以急切模式執行的框架通常在執行時發現實際圖。更確切地說,實際計算圖事先是未知的,您逐漸(*急切地*)執行一個運算子,它將成為下一個運算子的輸入,依此類推,直到您到達葉節點(輸出)。

這些框架通常在您實現的演算法中提供更大的靈活性,但代價是增加了執行時開銷,並可能略微增加記憶體使用量以跟蹤反向傳播所需的所有元素。

最後但同樣重要的是,透過這些框架通常更難啟用圖最佳化,例如運算子融合。例如,許多深度學習庫(如 oneDNN)針對卷積 + ReLU 具有最佳化的核心,但您實際上需要在執行圖之前知道這種模式將出現在操作序列中,這在急切框架中透過設計是不可能的。


圖 4. PyTorch 延遲與所涉及核心數量的關係


圖 5. Google TensorFlow 延遲與所涉及核心數量的關係


圖 6. 啟用 oneDNN 的 Google TensorFlow 延遲與所涉及核心數量的關係


圖 7. 英特爾 TensorFlow 延遲與所涉及核心數量的關係

整體趨勢突顯了核心數量對觀測到的延遲的積極影響。在大多數情況下,增加核心數量可以減少不同工作負載大小的計算時間。儘管如此,投入更多核心並不總是導致單調的延遲降低,在工作負載大小和分配給執行任務的資源數量之間始終存在權衡。

正如您在上面的圖表中看到的,在一個以上 CPU(多於一個插槽)的系統上使用所有可用核心時,一種非常常見的模式傾向於出現。插槽間通訊引入了顯著的延遲開銷,導致總體延遲的改進非常小。

此外,這種插槽間通訊開銷隨著工作負載的增大而變得越來越不明顯,這意味著所有計算資源的利用都受益於使用所有可用核心。在這個領域,PyTorch(圖 1)和 Intel TensorFlow(圖 4)似乎具有略好的並行支援,正如序列長度 384 和 512 的結果所示,其中使用所有核心仍然可以降低觀察到的延遲。

基線:圖框架延遲

這次我們比較使用“圖”模式的框架的效能,在這種模式下,圖是事先完全已知的,並且可以進行所有的分配和最佳化,例如圖剪枝和運算子融合。


圖 8. TorchScript 延遲與所涉及核心數量的關係


圖 9. Google TensorFlow 延遲與所涉及核心數量的關係


圖 10. 啟用 oneDNN 的 Google TensorFlow 延遲與所涉及核心數量的關係


圖 11. 英特爾 TensorFlow 延遲與所涉及核心數量的關係

這通常被稱為“跟蹤”圖,正如您在這裡看到的,TorchScript(PyTorch 的圖執行模式)與 TensorFlow(s) 的結果並沒有太大不同。所有 TensorFlow 實現的效能似乎都優於 TorchScript,當並行度受限時(操作內部計算涉及的核心數量較少),但隨著計算資源的增加,這種優勢似乎無法有效擴充套件,而 TorchScript 似乎能夠更好地利用現代 CPU 的能力。

儘管如此,在大多數情況下,所有這些框架之間的差距都非常有限。

調整記憶體分配器:這會影響觀察到的延遲嗎?

每個動態分配記憶體的程式都依賴一個關鍵元件:記憶體分配器。如果您熟悉 C/C++ 程式設計,此元件提供 malloc/free 或 new/delete 的低位功能。大多數情況下,您不必過多擔心它,並且預設的分配器(例如大多數 Linux 發行版上的 glibc)將提供出色的開箱即用效能。儘管如此,在某些情況下,它可能無法提供最有效的效能,因為這些預設分配器通常旨在在大多數情況下“良好”,而不是針對特定工作負載或並行性進行微調。

那麼,有哪些替代方案,以及它們何時比預設方案更適用呢?這再次取決於您的軟體所處的上下文。

可能的情況是:大量的分配/釋放操作隨著時間的推移導致碎片化;您執行軟體的特定硬體和/或架構;最後是您應用程式的並行度。

你明白這是怎麼回事了嗎?深度學習,以及所有進行大量計算的應用程式,都是高度多執行緒的,PyTorch、TensorFlow 和任何其他面向機器學習工作負載的軟體庫也是如此。

預設的記憶體分配器策略通常依賴於全域性記憶體池,這需要使用同步原語來操作,從而增加了系統的整體壓力,降低了應用程式的效能。谷歌、Facebook 和微軟等公司最近的一些工作提供了替代的記憶體分配策略,這些策略在自定義記憶體分配器庫中實現,可以直接整合到其軟體元件中,或使用動態共享庫預載入來替換用於分配/釋放的庫。

在這些庫中,我們可以舉幾個例子,如tcmallocjemallocmimalloc




圖 12. 在不同任務上進行基準測試的各種記憶體分配器

透過這篇部落格文章,我們將只專注於對 tcmalloc 和 jemalloc 作為潛在的記憶體分配器替代品進行基準測試。為了完全透明,對於以下結果的範圍,我們使用了 Ubuntu 2.9 版本上 gperftools 包中的 tcmalloc 和 jemalloc 5.1.0-1。

記憶體分配器基準測試

再次,我們首先比較在急切模式下執行框架的效能。這可能是分配器可以發揮最大作用的用例:由於圖在執行之前是未知的,每個框架必須在遇到上層節點的實際執行時管理每個操作所需的記憶體,無法提前規劃。在這種情況下,由於所有用於分配和回收記憶體的系統呼叫,分配器是一個主要組成部分。


圖 13. PyTorch 記憶體分配器和核心擴充套件延遲


圖 14. Google TensorFlow 記憶體分配器和核心擴充套件延遲


圖 15. 啟用 oneDNN 的 Google TensorFlow 記憶體分配器和核心擴充套件延遲


圖 16. 英特爾 TensorFlow 記憶體分配器和核心擴充套件延遲

根據上圖,您可以注意到標準庫分配器 (glibc) 的效能通常落後,但提供了合理的效能。Jemalloc 分配器有時是速度最快的,但在併發度不高的非常特定情況下,這可以透過 jemalloc 內部使用的底層結構來解釋,這超出了本部落格的範圍,但如果您想了解更多資訊,可以閱讀 Facebook 工程部落格

最後,tcmalloc 似乎在所有此處基準測試的工作負載中都提供了最佳效能。同樣,tcmalloc 在分配資源方面與 Jemalloc 採取了不同的方法,特別是 tcmalloc 為每個執行緒在本地維護一個記憶體段池,這減少了對全域性、獨佔、關鍵路徑的需求。

同樣,有關更多詳細資訊,我邀請您閱讀 Google Abseil 團隊的完整部落格

現在,回到圖模式,我們對擁有整體計算圖全知表示的框架進行基準測試。


圖 17. TorchScript 記憶體分配器和核心擴充套件延遲


圖 18. Google TensorFlow 記憶體分配器和核心擴充套件延遲


圖 19. 啟用 oneDNN 的 Google TensorFlow 記憶體分配器和核心擴充套件延遲


圖 20. 英特爾 TensorFlow 記憶體分配器和核心擴充套件延遲

這次,通過了解運算子流和涉及的矩陣形狀的底層結構,框架可以提前規劃和預留所需的資源。在這種情況下,如上圖所示,框架之間的差異非常小,jemalloc 和 tcmalloc 之間沒有明顯的贏家。當然,glibc 作為通用記憶體分配器仍然略微落後,但其差距不如急切設定中那麼顯著。總而言之,調整記憶體分配器可以在最佳化過程結束時提供一個有趣的項來爭取最後的毫秒改進,特別是如果您已經在使用跟蹤計算圖。

OpenMP

在上一節中,我們討論了機器學習軟體中的記憶體管理,主要涉及 CPU 密集型工作負載。此類軟體通常依賴於 PyTorch 或 TensorFlow 等中間框架進行深度學習,這些框架通常抽象出所有底層高度並行的運算子實現。

編寫如此高度並行和最佳化的演算法是一個真正的工程挑戰,它需要對 CPU 操作的所有實際元素(同步、記憶體快取、快取有效性等)有非常底層的理解。在這種情況下,能夠利用原語來實現如此強大的演算法非常重要,與從頭開始實現所有內容相比,可以大幅縮短交付時間和計算時間。

有許多庫可用於提供此類高階功能,以加速演算法開發。其中最常見的包括 OpenMP、Thread Building Blocks 以及直接從 C++ 中針對最新標準版本的功能。在本文的以下部分中,我們將僅限於 OpenMP,特別是比較 GNU 的開源和社群實現與英特爾的 OpenMP。後者專門針對英特爾 CPU 進行了最佳化,在替代 GNU OpenMP 時,可提供一流的效能。

OpenMP 暴露了許多環境變數,用於自動配置將參與計算的底層資源,例如用於分發計算的執行緒數(操作內執行緒),系統排程程式應如何根據 CPU 資源(執行緒、核心、套接字)繫結每個執行緒,以及其他一些為使用者提供進一步控制的變數。英特爾 OpenMP 暴露了更多此類環境變數,為使用者提供更大的靈活性,以調整其軟體的效能。


圖 21. 執行 PyTorch 時 OpenMP 與 Intel OpenMP 的延遲比較


圖 22. 執行 PyTorch 時 OpenMP 與 Intel OpenMP 的延遲比較

如前所述,調整 OpenMP 是在您嘗試了所有其他與系統相關的調整旋鈕之後可以開始嘗試的。它只需設定一個環境變數,即可為您的模型帶來最終的加速。

此外,值得注意的是,調優 OpenMP 庫僅適用於內部使用 OpenMP API 的軟體。更具體地說,目前只有 PyTorch 和 TorchScript 真正使用了 OpenMP,因此受益於 OpenMP 後端調優。

這也解釋了我們為什麼只報告了這兩個框架的延遲。

自動效能調優:使用 Intel SigOpt 進行貝葉斯最佳化

如上所述,可以調整許多旋鈕來提高英特爾 CPU 上的延遲和吞吐量,但由於旋鈕眾多,調整所有旋鈕以獲得最佳效能可能會很繁瑣。例如,在我們的實驗中,調整了以下旋鈕:

  • 核心數量:雖然使用盡可能多的核心通常是一個好主意,但它並不總是能提供最佳效能,因為它也意味著不同執行緒之間的更多通訊。最重要的是,使用更少的核心獲得更好的效能非常有用,因為它允許同時執行多個例項,從而提高延遲和吞吐量。
  • 記憶體分配器:預設的 malloc、谷歌的 tcmalloc 和 Facebook 的 jemalloc 中,哪個記憶體分配器提供最佳效能?
  • 並行庫:GNU OpenMP 和 Intel OpenMP 中,哪個並行庫提供最佳效能?
  • 透明大頁:在系統上啟用透明大頁 (THP) 是否能提供更好的效能?
  • KMP 阻塞時間引數:設定執行緒在完成並行區域執行後,進入休眠之前應等待的時間(以毫秒為單位)。

當然,蠻力方法,即嘗試所有可能性,將提供最佳旋鈕值以獲得最佳效能,但是,搜尋空間的大小為 N x 3 x 2 x 2 x 2 = 24N,這可能需要很長時間:在一臺具有 80 個物理核心的機器上,這意味著最多嘗試 24 x 80 = 1920 種不同的設定!😱

幸運的是,英特爾的 SigOpt 透過貝葉斯最佳化,使這些調優實驗更快、更方便分析,同時提供與蠻力方法相似的效能。

當我們分析絕對最佳延遲與 SigOpt 提供結果之間的相對差異時,我們發現雖然它通常不如暴力搜尋(除了序列長度 = 512 的特定情況),但它提供了非常接近的效能,在此圖中最大的差距為 8.6%

圖 23. SigOpt 自動調優找到的絕對最佳延遲與暴力搜尋的比較
圖 24. SigOpt 自動調優找到的相對最佳延遲與暴力搜尋的比較

SigOpt 在分析方面也很有用:它提供了許多資料和有價值的資訊。首先,它給出了它能夠找到的最佳值,相應的旋鈕,以及試驗歷史以及隨著試驗的進行它如何改進,例如,當序列長度 = 20 時:

圖 25. SigOpt 最佳值報告
圖 26. SigOpt 最佳值報告

在此特定設定中,16 個核心以及其他旋鈕能夠提供最佳結果,這一點非常重要,因為如前所述,這意味著可以並行執行模型的多個例項,同時每個例項仍能獲得最佳延遲。

它還表明它在大約 20 次試驗後收斂,這意味著也許 25 次試驗而不是 40 次就足夠了。還有更多有價值的資訊可用,例如引數重要性:

正如預期的那樣,核心數量是迄今為止最重要的引數,但其他引數也發揮了作用,並且它非常依賴於實驗。例如,對於序列長度 = 512 的實驗,引數重要性如下:

圖 27. SigOpt 最佳值,批大小 = 1,序列長度 = 20
圖 28. SigOpt 最佳值,批大小 = 1,序列長度 = 512

在這裡,使用 OpenMP 與 Intel OpenMP 的影響不僅大於分配器的影響,而且每個旋鈕的相對重要性也比序列長度 = 20 的實驗中更加平衡。SigOpt 還提供更多圖表,通常是互動式的,例如:

  • 2D 實驗歷史,允許比較旋鈕與旋鈕或旋鈕與目標
  • 3D 實驗歷史,允許進行與 2D 實驗歷史相同的事情,增加一個旋鈕/目標。

結論 - 加速生產中的 Transformers

在這篇文章中,我們展示了新的英特爾 Ice Lake 至強 CPU 如何適用於大規模執行 AI 工作負載,以及您可以互換和調整的軟體元素,以充分發揮硬體的潛力。所有這些專案都應在設定上一篇部落格中詳述的各種低階旋鈕之後進行考慮,以最大限度地利用所有核心和資源。

在 Hugging Face,我們的使命是使最先進的機器學習民主化,我們工作的關鍵部分是使這些最先進的模型儘可能高效,以在規模上消耗更少的能源和記憶體,並使各種規模的公司都能更經濟地執行。

我們與英特爾透過 🤗 硬體合作伙伴計劃的合作使我們能夠透過我們新的 🤗 Optimum 開源庫,將先進的效率和最佳化技術輕鬆地提供給社群,該庫專門致力於生產效能。

對於希望加速其 Transformer 模型推理的公司,我們新的 🤗 Infinity 產品提供即插即用的容器化解決方案,在 GPU 上實現低至 1 毫秒的延遲,在 Intel Xeon Ice Lake CPU 上實現 2 毫秒的延遲。

如果您覺得這篇文章有趣或對您的工作有幫助,請考慮給 Optimum 加星。如果這篇文章讓您感到心動,請考慮加入我們的機器學習最佳化團隊

社群

註冊登入以評論

© . This site is unofficial and not affiliated with Hugging Face, Inc.