Skip to main content

我們如何將 Bert 擴展到可以在 CPU 上單日處理超過 10 億個請求

7 月

31, 2020

by machynist & kippinitreal


技術

先給資料科學家和機器學習工程師一個經典的「雞與蛋」問題。您為公司開發新的機器學習模型時,您會先注重它的準確度,等到推出之後再提升它的速度嗎? 還是您會先注重它的速度,再提升它的準確度? 這個問題一出,一定會引起一陣熱烈的討論。

我們在 2019 年初期開始使用了當時嶄新的 Bert 深度學習模組來開發我們新時代的文字分類器,面對的就是這個問題。 Bert 模型是又大又深的神經網路,最「入門」的模型就有 1.1 億個參數,而我們不確定它是否能達到我們在推出時對速度和通量的要求。 我們最後決定把重心先放在我們的 Bert 文字分類器的準確度,而在接下來緊湊的幾個月之間,我們很幸運的在正式推出前把速度也拉到了合理的水平。 回到上面的問題,我們是先有「雞」(準確度),再有「蛋」(速度)。 雖然這對我們是一段艱苦的經歷,看完這篇文章裡我們對於 Bert 推理速度最佳化的心得之後,您就不用和我們有一樣的遭遇。 這樣子,您就可以先顧好蛋(使某些 Bert 模型能在推出時快速運行的指南),再把目光放到雞(讓 Bert 模型變得準確)上面。

我們讓 Bert 在推出時快速運行的旅程

Bert 對自然語言處理(Natural Language Processing,簡稱 NLP)的魔力在 2019 年已經被大幅報導了,而我們在處理我們自己的資料的時候也親眼見證了這個魔力。 我們藉由微調 Bert 的深度學習模型而巨幅改變了我們許多文字分類和命名實體識別(Named Entity Recognition,簡稱 NER)程式,模型效能(F1 分數)通常比先前的模型提高了至少 10%。

儘管我們的 Bert 模型開發過程仰賴許多有用的資源一級棒的函式庫而可以加快進行,我們找不太到關於如何在 PyTorch 上以低延遲、高通量的方向擴展 Bert 的資源。 我們許多 NLP 服務都要在 20 毫秒的延遲內每秒處理超過 2.5 萬個推理(也就是每天超過 10 億個推理)。

這篇文章的目的就是分享我們在 Roblox 文字分類方面將 Bert 推理速度加快超過 30 倍的旅程。 透過下述的優化工作,我們不僅能將 Bert 的速度提高到使用者可以接受的程度,也讓 Bert 在推出後不過度佔用 CPU 的承載量。

效能測試

為了切合這篇文章的主題,以下內容會注重在我們如何在透過微調 Bert 開發出的文字分類服務上提升其推理速度。 「推理」在這裡的意思是以下步驟組成的一串過程:接受文字當作輸入,將其代碼化成輸入張量,將該張量放入我們的 Bert 模型,並傳回模型生出的分類標籤。

Glass Bridge in China

上圖位於中國的玻璃橋就是延遲與通量很好的比喻。 延遲可以比喻成一個人要走過這座橋須花的時間,通量則表示在一定時間內有多少人可以走過這座橋。

我們要系統化地提升速度地時候都要制定效能標準。 我們為 Bert 分類器選定的標準有兩個部分:

  • 延遲:完成一個推理請求的時間的中位數(我們內部也有第 99 個百分位以上的標準)
  • 通量:一秒內可以完成的推理數

為了能穩定地比較通量,我們的程式碼開啟了整整 32 個工作者處理序,每一個都依序跑了 1000 個推理請求。 這次測試在一個擁有 36 個 Xeon Scalable Processor 核心的伺服器運行。

另外,它也模擬了一個文字分類服務依靠一群工作者處理序運行的生產環境。 測試開始後,每一個工作者就持續地忙著處理接踵而來的推理請求。 在實際的生產環境裡,我們有超過 100 個工作者分布在數台機器上,讓我們可以在延遲中位數 20 毫秒以下的情況下每秒處理超過 2.5 萬個請求。

工具

我們使用了 HuggingFace Transformers 函式庫 2.3.0 版本來開發我們的微調 Bert 模型。 我們特地看中了 HuggingFace 對 PyTorch 的支援,目前在生產環境的也是 PyTorch 1.4.0 版本。 我們使用 Huggingface 之後,就可以輕鬆地嘗試 RobertaXLNetDistilBertAlbert 等 Transformer 類型模型。 因為這樣,我們才發現了可以大幅提升推理速度的 DistilBert。(這個我們之後會再提到。)

首先:推理要用 CPU 還是 GPU?

我們第一個大問題就是 Bert 文字分類器的推理工作應該交給 CPU 還是 GPU。

在我們的模型訓練過程中,GPU 的處理速度無疑地比 CPU 快很多。 我們在 CPU 上微調(訓練)我們的文字分類 Bert 模型所花的時間比在 GPU 上花的時間多了超過 10 倍,這還是用 Tesla V100 GPU 跟價格相近的大型 36 核心 Xeon Scalable CPU 伺服器來比較。

以推理方面來看的話,選擇 CPU 或 GPU 這個決定要看實際的運用情形。 以下因素讓我們最後決定用 CPU 進行推理工作:

  • 簡易度。 GPU 在輸入分批的情況下最容易處理大量輸入。 但是,我們的文字分類器是在一個即時請求-回應的環境下運行的。 如果我們將這些即時請求分批,我們只會讓系統更加複雜和承受更多負荷。 在沒有將輸入分批的情況下,我們的 Bert 模型在 CPU 上的效能依然很出色,因此在 CPU 上進行推理會更簡單直接。
  • 金錢效益。 我們的文字分類器在 CPU 上進行推理所獲得的金錢效益比在 GPU 上更高。 在完成此文章所提到的擴展方面優化工作後,我們在 CPU 上的文字分類服務每花 1 美元所獲得的通量比在 GPU 上高出了 6 倍。 以實際數字說明的話,我們擴張 Bert 服務後每一秒可以在一個 Intel Xeon Scalable 36 核心伺服器上進行超過 3000 萬個推理,而在一個價格相近的 Tesla V100 GPU 只能進行 500 個推理。

推薦 CPU

我們在測試過程中發現了第二代 Intel Xeon Scalable 處理器對深度學習推理有非常好的支援,是最適合我們工作的處理器。 舉例來說,我們運用第二代 Intel Xeon Scalable 處理器的向量神經網路指令集與雙融合乘加核心大幅提升了運行速度。 我們在這篇文章裡提到的成果都是使用 Intel Xeon Scalable Processor 系列的 CPU 獲得的。 我們沒有對其他 CPU 進行太多的測試,因此您可能會因為使用其他 CPU 而獲得不一樣的成效。

執行緒微調

我們在 CPU 上擴大 Bert 推理量遇到的第一個問題就是在多個工作者處理序可以同時進行模型推理之前,我們必須先為 PyTorch 進行執行緒微調。 這是因為 PyTorch 模型在每一個處理緒內處理區區一個推理請求時都會嘗試使用多顆核心。 因此,每一個處理緒都在為有限的資源(核心)而競爭。 這樣會導致太多工作者在同一台機器上同時運行時造成停滯。

幸運的是,解決這個務的方法很簡單。如下圖第 9 行所示,我們只要在每一個進行推理的工作者處理序中把執行序數量改成 1 就好了。

這樣做有了什麼影響? 當多個工作者處理序在同一台機器運行時,因為每一個工作者只使用到一顆核心,整個流程在工作量擴大時可以更井然有序地進行。 我們這樣子才可以在一台機器上放置那麼多處理序,在維持合理延遲的情況下增加每一台機器的通量。

情境 1:Bert 基線

第一個底線是原始的 Bert 文字分類模型,也就是第一篇 Bert 論文所描述的架構。 這個 Bert 模型是使用 HuggingFace Transformers 2.3.0 函式庫裡的 BertForSequenceClassification PyTorch 模型建立的。 在這樣的情境下,我們刻意做了一個天真的設計決策;我們將所有張量輸入補零,讓它們變成 128 碼的固定長度。 這樣做的原因是將輸入分批的時候必須進行補零才能讓所有輸入的長度一樣。 因為就算批大小是 1 的時候還是很容易不小心將輸入補零,我們想將在這個基線情境裡發生這種情況的效能影響數量化。

下圖裡的粉紅線方塊就是我們「Bert 基線」情境的測結果。 330 毫秒的延遲對於生產環境的需求完全不夠低,而通量在考慮測試過程中用到的核心數量的情況下也是非常糟糕的。

情境 2:更小的模型(DistilBert)

自從 Bert 在 2018 年 10 月推出後,有許多擁有更多參數的模型漸漸誕生,試圖將 Bert 的發展推到更高的境界。 原始的 Bert 擁有 1.1 億個參數,而部分新推出的模型卻擁有超過 10 億個參數。這些模型的數據效能比原始 Bert 好一點,但是運算效能糟非常非常多。 我們本身要的就是更好的運算效能,就算犧牲一點數據效能也沒關係。

幸運的是,Sanh 等人在 2019 年 10 月發表了 DistilBert,它與其他模型翻轉了 Bert 模型越來越大的趨勢。 這些「較小模型」注重於降低 Bert 的大小,在犧牲極小量的數據效能的情況下大幅提升了推理與訓練速度。

我們選擇 DistilBert 的原因有兩個。 第一,DistilBert 的速度大概是 Bert 的兩倍,而它在文字分類的數據效能(F1 分數) 只差了 Bert 不到 1%。 第二,我們利用 Huggingface Transformers 函式庫可以很直接地將 Bert 換到 DistilBert,之前建立的訓練與推理管道斯完全不受影響。

以下是我們使用較小的 DistilBert 模型所得到的測試結果。 與較大的 Bert 相比,延遲差不多少了一半,而通量高出了快一倍。

情境 3:更小的輸入(動態形狀)

下一個改良的地方是縮小另一樣東西── DistilBert 模型的張量輸入。

首先我們要介紹一下補零。 深度學習模型一般來說會把一批批張量當作輸入。 對我們的文字分類器而言,我們就要把文字輸入透過 Bert 代碼化轉換成張量,再把這些張量分批。 因為文字的長度不定,我們必須將得到的可變長度張量補零來生出一批固定形狀的張量,如下圖所示:

之前有提到我們的文字分類器是在一個即時請求-回應的環境下運行的。 因此,我們最理想的批大小為 1。 這樣讓我們想到,因為批大小為 1 時所有張量的大小都會一樣,我們完全不用將張量補零。 當我們沒有把上圖的輸入文字補零而分批給模型處理的時候,我們得到了下圖的結果:

因為我們允許單批內含有可變長度張量,我們也形容這些輸入為「動態形狀」。 使用動態形狀輸入後,我們的通量和延遲都獲得了大幅的進步。 因為我們沒有進行補零而使模型要處理的輸入變小,這樣的結果是合乎常理。 另外,因為不管使用動態形狀還是補零都會獲得一樣的模型輸出機率,我們的 F1 分數沒有負面影響。

以下是我們 DistilBert 和動態形狀一起使用後的測試結果。 我們可以看到,我們在延遲減了超過一半的情況下將通量提升了超過一倍。

情境 4:更小的權重(量化)

我們強化效能幅度最大的步驟是透過「量化」降低權重。

量化的意思是使用更小的模型權重表示(例如以 8 位元整數呈現 32 位元浮點數權重)來提升深度學習運算的效率。 我們為自己的 DistilBert 模型使用的量化方法叫做「動態量化」。 這個方法會在訓練後量化權重,與之相反的是在訓練中量化權重。

我們在 PyTorch 1.3+ 很容易引入動態量化的資源,只改了一行程式碼就可以在 DistilBert 模型所有線性層面使用 8 位元的權重表示。

下圖可以看到量化如何改變原本的 DistilBert 模型。 您會發現 PyTorch 會將注意力機制的 QKV(Query、Key、Value)層面等「線性」層面換成一個「動態量化線性」層面,該層面會使用量化的 8 位元整數進行內部乘加運算。

這裡是我們 DistilBert、動態形狀及動態量化一起使用的測試結果。 我們可以看到經過這些最佳化過程之後,不管是延遲還是通量都比原始的 Bert 基線進步了 30 倍之多。

我們在量化後有注意到 F1 分數 <1% 的微量下降,但是我們藉此為延遲與通量換來了非常大的效益。

情境 5:更小的請求數量(快取)

我們最後最佳化的地方就是有效地減少傳到 DistilBert 模型的請求。 達成這個目的的方法是把權杖 ID 當作鑰匙,將一些常見地文字輸入快取。 模型收到相同文字輸入時都會給予相同回應,所以這個方法可行。 除此之外,底下的文字分布越不整齊,這個方法就越有效。

當我們將自己的 100 萬則資料快取到處理緒的時候,我們在生產環境親眼見證了 40% 的快取命中率。 快取命中時,推理的實際成本就是零,所以我們的快取使整體通量變成了快兩倍。

因為快取的效果會因資料而變,我們沒有把快取列入測試標準的項目。 儘管如此,我們還是想讓各位知道簡單如快取的東西也可以造成深遠的影響。

關於水平可擴展性的說明

這篇文之提到的測試結果是 32 個工作者在同一個伺服器同時運行所得的。 我們只要使用多個伺服器水平擴大工作者的數量,我們就可以在生產環境裡一天處理超過 10 億個請求。

未來的可能性

我們談到了 Bert PyTorch 模型從開發到推出的最佳化過程的刁鑽細節,但是這個領域的持續創新也有可能影響到我們往後的擴展方向。 像 Onnx Runtime 就與我們非量化的測試標準很接近,非常值得關注。 Microsoft 在我們發布這篇文章的時候還在開放 Onnx Runtime Bert 最佳化的原始碼。我們也在與 Intel 密切合作探索 OpenVino 可能的優化方向。

結論與心得

我們主要的結論是 DistilBert 或 Bert 在 CPU 上可以進行大規模的擴展,尤其是針對即時文字分類的工作上。 我們說明了我們如何透過「變小」讓 Bert 文字分類的延遲和通量獲得了超過 30 倍的進步。 「變小」指的是更小的模型(DistilBert)、更小的輸入(動態形狀)及更小的權重(量化)。 我們很高興可以使用自己的深度學習分類器每天處理超過 10 億個請求,而且是在合理的 CPU 負荷量與不到 20 毫秒的延遲的情況下。

非常感謝 Edin Mulalić 和 Saša Anđelković 探討許多這篇文章所提到的技巧。


Roblox Corporation 與此部落格不為任何公司或服務宣傳或背書。 另外,我們不保證此部落格裡的資訊的準確度、可靠度及完整度。

此部落格文章原本在 Roblox 科技部落格發布。