大規模 Transformer 模型 8 位元矩陣乘簡介

HuggingFace發表於2023-05-02

引言

語言模型一直在變大。截至撰寫本文時,PaLM 有 5400 億引數,OPT、GPT-3 和 BLOOM 有大約 1760 億引數,而且我們仍在繼續朝著更大的模型發展。下圖總結了最近的一些語言模型的尺寸。

LLM

由於這些模型很大,因此它們很難在一般的裝置上執行。舉個例子,僅推理 BLOOM-176B 模型,你就需要 8 個 80GB A100 GPU (每個約 15,000 美元)。而如果要微調 BLOOM-176B 的話,你需要 72 個這樣的 GPU!更大的模型,如 PaLM,還需要更多資源。

由於這些龐大的模型需要大量 GPU 才能執行,因此我們需要找到降低資源需求而同時保持模型效能的方法。目前已有一些試圖縮小模型尺寸的技術,比如你可能聽說過的量化和蒸餾等技術。

完成 BLOOM-176B 的訓練後,Hugging Face 和 BigScience 一直在尋找能讓這個大模型更容易在更少的 GPU 上執行的方法。透過我們的 BigScience 社群,我們瞭解到一些有關 Int8 推理的研究,它不會降低大模型的預測效能,而且可以將大模型的記憶體佔用量減少 2 倍。很快我們就開始合作進行這項研究,最終將其完全整合到 Hugging Face transformers 中。本文我們將詳述我們整合在 Hugging Face 中的 LLM.int8() 方案,它適用於所有 Hugging Face 模型。如果你想了解更多研究細節,可以閱讀我們的論文 LLM.int8(): 8-bit Matrix Multiplication for Transformers at Scale

本文將主要介紹 LLM.int8() 量化技術,討論將其納入 transformers 庫的過程中經歷的困難,並對後續工作進行了計劃。

在這裡,你將瞭解到究竟是什麼讓一個大模型佔用這麼多記憶體?是什麼讓 BLOOM 佔用了 350GB 記憶體?我們先從一些基礎知識開始,慢慢展開。

機器學習中常用的資料型別

我們從理解不同浮點資料型別開始,這些資料型別在機器學習中也被稱為“精度”。

模型的大小由其引數量及其精度決定,精度通常為 float32、float16 或 bfloat16 之一 (下圖來源)。

Summary

Float32 (FP32) 是標準的 IEEE 32 位浮點表示。使用該資料型別,可以表示大範圍的浮點數。在 FP32 中,為“指數”保留了 8 位,為“尾數”保留了 23 位,為符號保留了 1 位。因為是標準資料型別,所以大部分硬體都支援 FP32 運算指令。

而在 Float16 (FP16) 資料型別中,指數保留 5 位,尾數保留 10 位。這使得 FP16 數字的數值範圍遠低於 FP32。因此 FP16 存在上溢 (當用於表示非常大的數時) 和下溢 (當用於表示非常小的數時) 的風險。

例如,當你執行 10k * 10k 時,最終結果應為 100M,FP16 無法表示該數,因為 FP16 能表示的最大數是 64k。因此你最終會得到 NaN (Not a Number,不是數字),在神經網路的計算中,因為計算是按層和 batch 順序進行的,因此一旦出現 NaN,之前的所有計算就全毀了。一般情況下,我們可以透過縮放損失 (loss scaling) 來緩解這個問題,但該方法並非總能奏效。

於是我們發明了一種新格式 Bfloat16 (BF16) 來規避這些限制。BF16 為指數保留了 8 位 (與 FP32 相同),為小數保留了 7 位。這意味著使用 BF16 我們可以保留與 FP32 相同的動態範圍。但是相對於 FP16,我們損失了 3 位精度。因此,在使用 BF16 精度時,大數值絕對沒有問題,但是精度會比 FP16 差。

在 Ampere 架構中,NVIDIA 還引入了 TensorFloat-32(TF32) 精度格式,它使用 19 位表示,結合了 BF16 的範圍和 FP16 的精度。目前,它僅在某些操作的內部使用 [譯者注: 即 TF32 是一個計算資料型別而不是儲存資料型別]。

在機器學習術語中,FP32 稱為全精度 (4 位元組),而 BF16 和 FP16 稱為半精度 (2 位元組)。除此以外,還有 Int8 (INT8) 資料型別,它是一個 8 位的整型資料表示,可以儲存 $2^8$ 個不同的值 (對於有符號整數,區間為 [-128, 127],而對於無符號整數,區間為 [0, 255])。

雖然理想情況下訓練和推理都應該在 FP32 中完成,但 FP32 比 FP16/BF16 慢兩倍,因此實踐中常常使用混合精度方法,其中,使用 FP32 權重作為精確的 “主權重 (master weight)”,而使用 FP16/BF16 權重進行前向和後向傳播計算以提高訓練速度,最後在梯度更新階段再使用 FP16/BF16 梯度更新 FP32 主權重。

在訓練期間,主權重始終為 FP32。而在實踐中,在推理時,半精度權重通常能提供與 FP32 相似的精度 —— 因為只有在模型梯度更新時才需要精確的 FP32 權重。這意味著在推理時我們可以使用半精度權重,這樣我們僅需一半 GPU 視訊記憶體就能獲得相同的結果。

Model-storage

以位元組為單位計算模型大小時,需要將引數量乘以所選精度的大小 (以位元組為單位)。例如,如果我們使用 BLOOM-176B 模型的 Bfloat16 版本,其大小就應為 $176 \times 10^{9} \times 2 位元組 = 352GB$!如前所述,這個大小需要多個 GPU 才能裝得下,這是一個相當大的挑戰。

但是,如果我們可以使用另外的資料型別來用更少的記憶體儲存這些權重呢?深度學習社群已廣泛使用的方法是量化。

模型量化簡介

透過實驗,我們發現不使用 4 位元組 FP32 精度轉而使用 2 位元組 BF16/FP16 半精度可以獲得幾乎相同的推理結果,同時模型大小會減半。這促使我們想進一步削減記憶體,但隨著我們使用更低的精度,推理結果的質量也開始急劇下降。

為了解決這個問題,我們引入了 8 位量化。僅用四分之一精度,因此模型大小也僅需 1/4!但這次,我們不能簡單地丟棄另一半位寬了。

基本上講,量化過程是從一種資料型別“舍入”到另一種資料型別。舉個例子,如果一種資料型別的範圍為 0..9,而另一種資料型別的範圍為 0..4,則第一種資料型別中的值 4 將舍入為第二種資料型別中的 2 。但是,如果在第一種資料型別中有值 3,它介於第二種資料型別的 12 之間,那麼我們通常會四捨五入為 2。也就是說,第一種資料型別的值 43 在第二種資料型別中具有相同的值 2。這充分表明量化是一個有噪過程,會導致資訊丟失,是一種有失真壓縮。

兩種最常見的 8 位量化技術是零點量化 (zero-point quantization) 和最大絕對值 (absolute maximum quantization,absmax) 量化。它們都將浮點值對映為更緊湊的 Int8 (1 位元組) 值。這些方法的第一步都是用量化常數對輸入進行歸一化縮放。

在零點量化中,如果我的數值範圍是 -1.0…1.0,我想量化到 -127…127,我需要先縮放 127倍,然後四捨五入到 8 位精度。要恢復原始值,我需要將 Int8 值除以相同的量化因子 127。在這個例子中,值 0.3 將縮放為 0.3*127 = 38.1。四捨五入後得到值 38。恢復時,我們會得到 38/127=0.2992 —— 因此最終會有 0.008 的量化誤差。這些看似微小的誤差在沿著模型各層傳播時往往會累積和增長,從而導致最終的精度下降。

譯者注: 這個例子舉得不好,因為浮點範圍和整型範圍都是對稱的,所以不存在零點調整了,而零點調整是零點量化中最能體現其命名原因的部分。簡而言之,零點量化分為兩步,第一步值域對映,即透過縮放將原始的數值範圍對映為量化後的數值範圍; 第二步零點調整,即透過平移將對映後的資料的最小值對齊為目標值域的最小值

quantization

(圖源)

現在我們再看下 absmax 量化的細節。要計算 absmax 量化中 fp16 數與其對應的 int8 數之間的對映,你必須先除以張量的最大絕對值,然後再乘以資料型別的最大可表示值。

例如,假設你要用 absmax 對向量 [1.2, -0.5, -4.3, 1.2, -3.1, 0.8, 2.4, 5.4] 進行量化。首先需要計算該向量元素的最大絕對值,在本例中為 5.4。 Int8 的範圍為 [-127, 127],因此我們將 127 除以 5.4,得到縮放因子 23.5。最後,將原始向量乘以縮放因子得到最終的量化向量 [28, -12, -101, 28, -73, 19, 56, 127]

out-quant.gif

要恢復原向量,可以將 int8 量化值除以縮放因子,但由於上面的過程是“四捨五入”的,我們將丟失一些精度。

quant-freeze

對於無符號 Int8,我們可以先減去最小值然後再用最大絕對值來縮放,這與零點量化的做法相似。其做法也與最小 - 最大縮放 (min-max scaling) 類似,但後者在縮放時會額外保證輸入中的 0 始終對映到一個整數,從而保證 0 的量化是無誤差的。

當進行矩陣乘法時,我們可以透過組合各種技巧,例如逐行或逐向量量化,來獲取更精確的結果。舉個例子,對矩陣乘法 $A \times B=C$,我們不會直接使用常規量化方式,即用整個張量的最大絕對值對張量進行歸一化,而會轉而使用向量量化方法,找到 A 的每一行和 B 的每一列的最大絕對值,然後逐行或逐列歸一化 A 和 B 。最後將 A 與 B 相乘得到 C。最後,我們再計算與 A 和 B 的最大絕對值向量的外積,並將此與 C 求哈達瑪積來反量化回 FP16。有關此技術的更多詳細資訊可以參考 LLM.int8() 論文 或 Tim 的部落格上的 關於量化和湧現特徵的博文

雖然這些基本技術能夠幫助我們量化深度學習模型,但它們通常會導致大模型準確性的下降。我們整合到 Hugging Face Transformers 和 Accelerate 庫中的 LLM.int8() 是第一個適用於大模型 (如 BLOOM-176B) 且不會降低準確性的量化技術。

簡要總結 LLM.int8(): 大語言模型的零退化矩陣乘法

在 LLM.int8() 中,我們已經證明理解 transformer 模型表現出的與模型規模相關的湧現特性對於理解為什麼傳統量化對大模型失效至關重要。我們證明效能下降是由離群特徵 (outlier feature) 引起的,下一節我們會詳細解釋。LLM.int8() 演算法本身如下。

本質上,LLM.int8() 透過三個步驟完成矩陣乘法計算:

  1. 從輸入的隱含狀態中,按列提取異常值 (即大於某個閾值的值)。
  2. 對 FP16 離群值矩陣和 Int8 非離群值矩陣分別作矩陣乘法。
  3. 反量化非離群值的矩陣乘結果並其與離群值矩陣乘結果相加,獲得最終的 FP16 結果。

該過程可以總結為如下動畫:

Mixed-int8.gif

離群特徵的重要性

超出某個分佈範圍的值通常稱為離群值。離群值檢測已得到廣泛應用,在很多文獻中也有涉及,且獲取特徵的先驗分佈對離群值檢測任務很有助益。更具體地說,我們觀察到對於引數量大於 6B 的 transformer 模型,經典的量化方法會失效。雖然離群值特徵也存在於較小的模型中,但在大於 6B 的 transformer 模型中,我們觀察到幾乎每層都會出現超出特定閾值的離群點,而且這些離群點呈現出一定的系統性模式。有關該現象的更多詳細資訊,請參閱 LLM.int8() 論文湧現特徵的博文

如前所述,8 位精度的動態範圍極其有限,因此量化具有多個大值的向量會產生嚴重誤差。此外,由於 transformer 架構的固有特性,它會將所有元素互相關聯起來,這樣的話,這些誤差在傳播幾層後往往會混雜在一起。因此,我們發明了混合精度分解的方法,以對此類極端離群值進行有效量化。接下來我們對此方法進行討論。

MatMul 內部

計算隱含狀態後,我們使用自定義閾值提取離群值,並將矩陣分解為兩部分,如上所述。我們發現,以這種方式提取所有幅度大於等於 6 的離群值可以完全恢復推理精度。離群值部分使用 FP16 表示,因此它是一個經典的矩陣乘法,而 8 位矩陣乘法是透過使用向量量化將權重和隱含狀態分別量化為 8 位精度 - 即按行量化權重矩陣,並按列量化隱含狀態,然後再進行相應向量乘加操作。最後,將結果反量化至半精度,以便與第一個矩陣乘法的結果相加。

Matmul.png

0 退化是什麼意思?

我們如何正確評估該方法是否會對效能造成下降?使用 8 位模型時,我們的生成質量損失了多少?

我們使用 lm-eval-harness 在 8 位和原始模型上執行了幾個常見的基準測試,結果如下。

對 OPT-175B 模型:

測試基準----差值
測試基準名指標指標值 - int8指標值 - fp16標準差 - fp16-
hellaswagacc_norm0.78490.78490.00410
hellaswagacc0.59210.59310.00490.001
piqaacc0.79650.79590.00940.0006
piqaacc_norm0.81010.81070.00910.0006
lambadappl3.01423.01520.05520.001
lambadaacc0.74640.74660.00610.0002
winograndeacc0.71740.72450.01250.0071

對 BLOOM-176 模型:

測試基準----差值
測試基準名指標指標值 - int8指標值 - fp16標準差 - fp16-
hellaswagacc_norm0.72740.73030.00440.0029
hellaswagacc0.55630.55840.0050.0021
piqaacc0.78350.78840.00950.0049
piqaacc_norm0.79220.79110.00950.0011
lambadappl3.91913.9310.08460.0119
lambadaacc0.68080.67180.00650.009
winograndeacc0.70480.70480.01280

我們切實地看到上述這些模型的效能下降為 0,因為指標的絕對差異均低於原始模型的標準誤差 (BLOOM-int8 除外,它在 lambada 上略好於原始模型)。如果想要知道 LLM.int8() 與當前其他先進方法的更詳細的效能比較,請檢視 論文

比原始模型更快嗎?

LLM.int8() 方法的主要目的是在不降低效能的情況下降低大模型的應用門檻。但如果速度非常慢,該方法用處也不會很大。所以我們對多個模型的生成速度進行了基準測試。

我們發現使用了 LLM.int8() 的 BLOOM-176B 比 FP16 版本慢了大約 15% 到 23% —— 這應該是完全可以接受的。我們發現較小模型 (如 T5-3B 和 T5-11B) 的降速幅度更大。我們還在努力最佳化這些小模型的推理速度。在一天之內,我們可以將 T5-3B 的每詞元推理延遲從 312 毫秒降低到 173 毫秒,將 T5-11B 從 45 毫秒降低到 25 毫秒。此外,我們 已經找到原因,在即將釋出的版本中,LLM.int8() 在小模型上的推理速度可能會更快。下表列出了當前版本的一些效能資料。

精度引數量硬體每詞元延遲 (單位: 毫秒,batch size: 1)每詞元延遲 (單位: 毫秒,batch size: 8)每詞元延遲 (單位: 毫秒,batch size: 32)
bf16176B8xA100 80GB239329.9
int8176B4xA100 80GB28237.510.2
bf16176B14xA100 40GB28536.510.4
int8176B5xA100 40GB36746.4oom
fp1611B2xT4 15GB11.71.70.5
int811B1xT4 15GB43.55.31.3
fp323B2xT4 15GB457.23.1
int83B1xT4 15GB31239.110.2

上表中的 3 個模型分別為 BLOOM-176B、T5-11B 和 T5-3B。

Hugging Face transformers 整合細節

接下來讓我們討論在 Hugging Face transformers 整合該方法的細節,向你展示常見的用法及在使用過程中可能遇到的常見問題。

用法

所有的操作都整合在 Linear8bitLt 模組中,你可以輕鬆地從 bitsandbytes 庫中匯入它。它是 torch.nn.modules 的子類,你可以仿照下述程式碼輕鬆地將其應用到自己的模型中。

下面以使用 bitsandbytes 將一個小模型轉換為 int8 為例,並給出相應的步驟。

  1. 首先匯入模組,如下。

    import torch
    import torch.nn as nn
    
    import bitsandbytes as bnb
    from bnb.nn import Linear8bitLt
  2. 然後就可以定義自己的模型了。請注意,我們支援將任何精度的 checkpoint 或模型轉換為 8 位 (FP16、BF16 或 FP32),但目前,僅當模型的輸入張量資料型別為 FP16 時,我們的 Int8 模組才能工作。因此,這裡我們稱模型為 fp16 模型。

    fp16_model = nn.Sequential(
        nn.Linear(64, 64),
        nn.Linear(64, 64)
    )
  3. 假設你已經在你的資料集和任務上訓完了你的模型!現在需要儲存模型:

    [... train the model ...]
    torch.save(fp16_model.state_dict(), "model.pt")
  4. 至此,state_dict 已儲存,我們需要定義一個 int8 模型:

    int8_model = nn.Sequential(
        Linear8bitLt(64, 64, has_fp16_weights=False),
        Linear8bitLt(64, 64, has_fp16_weights=False)
    )

    此處標誌變數 has_fp16_weights 非常重要。預設情況下,它設定為 True,用於在訓練時使能 Int8/FP16 混合精度。但是,因為在推理中我們對記憶體節省更感興趣,因此我們需要設定 has_fp16_weights=False

  5. 現在載入 8 位模型!

    int8_model.load_state_dict(torch.load("model.pt"))
    int8_model = int8_model.to(0) # 量化發生在此處

請注意,一旦將模型的裝置設定為 GPU,量化過程就會在第二行程式碼中完成。如果在呼叫 .to 函式之前列印 int8_model[0].weight,你會看到:

int8_model[0].weight
Parameter containing:
tensor([[ 0.0031, -0.0438, 0.0494, ..., -0.0046, -0.0410, 0.0436],
        [-0.1013, 0.0394, 0.0787, ..., 0.0986, 0.0595, 0.0162],
        [-0.0859, -0.1227, -0.1209, ..., 0.1158, 0.0186, -0.0530],
        ...,
        [ 0.0804, 0.0725, 0.0638, ..., -0.0487, -0.0524, -0.1076],
        [-0.0200, -0.0406, 0.0663, ..., 0.0123, 0.0551, -0.0121],
        [-0.0041, 0.0865, -0.0013, ..., -0.0427, -0.0764, 0.1189]],
       dtype=torch.float16)

而如果你在第二行之後列印它,你會看到:

int8_model[0].weight
Parameter containing:
tensor([[ 3, -47, 54, ..., -5, -44, 47],
        [-104, 40, 81, ..., 101, 61, 17],
        [ -89, -127, -125, ..., 120, 19, -55],
        ...,
        [ 82, 74, 65, ..., -49, -53, -109],
        [ -21, -42, 68, ..., 13, 57, -12],
        [ -4, 88, -1, ..., -43, -78, 121]],
        device='cuda:0', dtype=torch.int8, requires_grad=True)

正如我們在前面部分解釋量化方法時所講,權重值被“截斷”了。此外,這些值的分佈看上去在 [-127, 127] 之間。

你可能還想知道如何獲取 FP16 權重以便在 FP16 中執行離群值的矩陣乘?很簡單:

(int8_model[0].weight.CB * int8_model[0].weight.SCB) / 127

你會看到:

tensor([[ 0.0028, -0.0459, 0.0522, ..., -0.0049, -0.0428, 0.0462],
        [-0.0960, 0.0391, 0.0782, ..., 0.0994, 0.0593, 0.0167],
        [-0.0822, -0.1240, -0.1207, ..., 0.1181, 0.0185, -0.0541],
        ...,
        [ 0.0757, 0.0723, 0.0628, ..., -0.0482, -0.0516, -0.1072],
        [-0.0194, -0.0410, 0.0657, ..., 0.0128, 0.0554, -0.0118],
        [-0.0037, 0.0859, -0.0010, ..., -0.0423, -0.0759, 0.1190]],
       device='cuda:0')

這跟第一次列印的原始 FP16 值很接近!

  1. 現在你只需將輸入推給正確的 GPU 並確保輸入資料型別是 FP16 的,你就可以使用該模型進行推理了:
input_ = torch.randn(64, dtype=torch.float16)
hidden_states = int8_model(input_.to(torch.device('cuda', 0)))

你可以檢視 示例指令碼,獲取完整的示例程式碼!

多說一句, Linear8bitLtnn.Linear 模組略有不同,主要在 Linear8bitLt 的引數屬於 bnb.nn.Int8Params 類而不是 nn.Parameter 類。稍後你會看到這給我們帶來了一些小麻煩!

現在我們開始瞭解如何將其整合到 transformers 庫中!

accelerate 足矣

在處理大模型時, accelerate 庫包含許多有用的工具。init_empty_weights 方法特別有用,因為任何模型,無論大小,都可以在此方法的上下文 (context) 內進行初始化,而無需為模型權重分配任何記憶體。

import torch.nn as nn
from accelerate import init_empty_weights

with init_empty_weights():
    model = nn.Sequential([nn.Linear(100000, 100000) for _ in range(1000)]) # This will take ~0 RAM!

初始化過的模型將放在 PyTorch 的 meta 裝置上,這是一種用於表徵向量的形狀和資料型別而無需實際的記憶體分配的超酷的底層機制。

最初,我們在 .from_pretrained 函式內部呼叫 init_empty_weights,並將所有引數過載為 torch.nn.Parameter。這不是我們想要的,因為在我們的情況中,我們希望為 Linear8bitLt 模組保留 Int8Params 類,如上所述。我們最後成功使用 此 PR 修復了該問題,它將下述程式碼:

module._parameters[name] = nn.Parameter(module._parameters[name].to(torch.device("meta")))

修改成:

param_cls = type(module._parameters[name])
kwargs = module._parameters[name].__dict__
module._parameters[name] = param_cls(module._parameters[name].to(torch.device("meta")), **kwargs)

現在這個問題已經解決了,我們可以輕鬆地在一個自定義函式中利用這個上下文管理器將所有 nn.Linear 模組替換為 bnb.nn.Linear8bitLt 而無需佔用記憶體!

def replace_8bit_linear(model, threshold=6.0, module_to_not_convert="lm_head"):
    for name, module in model.named_children():
        if len(list(module.children())) > 0:
            replace_8bit_linear(module, threshold, module_to_not_convert)

        if isinstance(module, nn.Linear) and name != module_to_not_convert:
            with init_empty_weights():
                model._modules[name] = bnb.nn.Linear8bitLt(
                    module.in_features,
                    module.out_features,
                    module.bias is not None,
                    has_fp16_weights=False,
                    threshold=threshold,
                )
    return model

此函式遞迴地將 meta 裝置上初始化的給定模型的所有 nn.Linear 層替換為 Linear8bitLt 模組。這裡,必須將 has_fp16_weights 屬性設定為 False,以便直接將權重載入為 Int8,並同時載入其量化統計資訊。

我們放棄了對某些模組 (這裡時 lm_head) 進行替換,因為我們希望保持輸出層的原始精度以獲得更精確、更穩定的結果。

但還沒完!上面的函式在 init_empty_weights 上下文管理器中執行,這意味著新模型將仍在 meta 裝置中。

對於在此上下文管理器中初始化的模型, accelerate 將手動載入每個模組的引數並將它們複製到正確的裝置上。因此在 bitsandbytes 中,設定 Linear8bitLt 模組的裝置是至關重要的一步 (感興趣的讀者可以檢視 此程式碼),正如你在我們上面提供的指令碼中所見。

而且,第二次呼叫量化過程時會失敗!我們必須想出一個與 accelerateset_module_tensor_to_device 函式相應的實現 (稱為 set_module_8bit_tensor_to_device),以確保我們不會呼叫兩次量化。我們將在下面的部分中詳細討論這個問題!

accelerate 設定裝置要當心

這方面,我們對 accelerate 庫進行了精巧的修改,以取得平衡!

在模型被載入且設定到正確的裝置上後,有時你仍需呼叫 set_module_tensor_to_device 以便向所有裝置分派加了 hook 的模型。該操作在使用者呼叫 acceleratedispatch_model 函式時會被觸發,這意味著我們有可能多次呼叫 .to,我們需要避免該行為。

我們透過兩個 PR 實現了目的,這裡 的第一個 PR 破壞了一些測試,但 這個 PR 成功修復了所有問題!

總結

因此,最終我們完成了:

  1. 使用正確的模組在 meta 裝置上初始化模型。
  2. 不重不漏地對目標 GPU 逐一設定引數,確保不要對同一個 GPU 重複設定!
  3. 將新加的引數變數更新到所有需要的地方,並新增好文件。
  4. 新增高覆蓋度的測試! 你可以從 此處 檢視更多關於測試的詳細資訊。

知易行難,在此過程中,我們經歷了許多艱難的除錯局,其中很多跟 CUDA 核函式有關!

總而言之,這次整合的過程充滿了冒險和趣味; 從深入研究並對不同的庫做一些“手術”,到整合一切並最終使其發揮作用,每一步都充滿挑戰!

現在,我們看看如何在 transformers 中成功使用它並從中獲益!

如何在 transformers 中使用它

硬體要求

CPU 不支援 8 位張量核心 [*]。 bitsandbytes 可以在支援 8 位張量核心的硬體上執行,這些硬體有 Turing 和 Ampere GPU (RTX 20s、RTX 30s、A40-A100、T4+)。例如,Google Colab GPU 通常是 NVIDIA T4 GPU,而最新的 T4 是支援 8 位張量核心的。我們後面的演示將會基於 Google Colab!

*: 譯者注: Intel 最新的 Sapphire Rapids CPU 已支援 8 位張量指令集: AMX

安裝

使用以下命令安裝最新版本的庫 (確保你的 python>=3.8)。

pip install accelerate
pip install bitsandbytes
pip install git+https://github.com/huggingface/transformers.git

演示示例 - 在 Google Colab 上執行 T5 11B

以下是執行 T5-11B 的演示。 T5-11B 模型的 checkpoint 精度為 FP32,需要 42GB 記憶體,Google Colab 裡跑不動。使用我們的 8 位模組,它僅需 11GB 記憶體,因此能輕易跑通:

T5-11B 的 Colab 演示: <url>https://colab.research.google.com/drive/1YORPWx4okIHXnjW7MSAi...</url>

或者,你還可以看看下面這個使用 8 位 BLOOM-3B 模型進行推理的演示!

BLOOM-3B 的 Colab 演示: <url>https://colab.research.google.com/github/huggingface/blog/blo...</url>

影響範圍

我們認為,該方法讓超大模型不再是陽春白雪,而是人人皆可觸及。在不降低效能的情況下,它使擁有較少算力的使用者能夠使用以前無法使用的模型。

我們已經發現了幾個可以在繼續改進的領域,以使該方法對大模型更友好!

較小模型的推理加速

正如我們在 基準測試部分 中看到的那樣,我們可以將小模型 (<=6B 引數) 的執行速度提高近 2 倍。然而,雖然推理速度對於像 BLOOM-176B 這樣的大模型來說比較穩定,但對小模型而言仍有改進的餘地。我們已經定位到了問題並有希望恢復與 FP16 相同的效能,甚至還可能會有小幅加速。我們將在接下來的幾周內合入這些改進。

支援 Kepler GPU (GTX 1080 等)

雖然我們只支援過去四年的所有 GPU,但現實是某些舊的 GPU (如 GTX 1080) 現在仍然被大量使用。雖然這些 GPU 沒有 Int8 張量核心,但它們有 Int8 向量單元 (一種“弱”張量核心)。因此,這些 GPU 也可以體驗 Int8 加速。然而,它需要一個完全不同的軟體棧來最佳化推理速度。雖然我們確實計劃整合對 Kepler GPU 的支援以使 LLM.int8() 的應用更廣泛,但由於其複雜性,實現這一目標需要一些時間。

在 Hub 上儲存 8 位 checkpoint

目前 8 位模型無法直接載入被推送到 Hub 上的 8 位 checkpoint。這是因為模型計算所需的統計資料 (還記得上文提到的 weight.CBweight.SCB 嗎?) 目前沒有儲存在 state_dict 中,而且 state_dict 的設計也未考慮這一資訊的儲存,同時 Linear8bitLt 模組也還尚未支援該特性。

但我們認為儲存它並將其推送到 Hub 可能有助於提高模型的可訪問性。

CPU 的支援

正如本文開頭所述,CPU 裝置不支援 8 位張量核。然而,我們能克服它嗎?在 CPU 上執行此模組可以顯著提高可用性和可訪問性。[譯者注: 如上文,最新的 Intel CPU 已支援 8 位張量核]

擴充套件至其他模態

目前,大模型以語言模型為主。在超大視覺、音訊和多模態模型上應用這種方法可能會很有意思,因為隨著這些模型在未來幾年變得越來越多,它們的易用性也會越來越重要。

致謝

非常感謝以下為提高文章的可讀性以及在 transformers 中的整合過程做出貢獻的人 (按字母順序列出):
JustHeuristic (Yozh),
Michael Benayoun,
Stas Bekman,
Steven Liu,
Sylvain Gugger,
Tim Dettmers


英文原文: https://hf.co/blog/hf-bitsandbytes-integration

原文作者: Younes Belkada,Tim Dettmers

譯者: Matrix Yao (姚偉峰),英特爾深度學習工程師,工作方向為 transformer-family 模型在各模態資料上的應用及大規模模型的訓練推理。

排版: zhongdongy (阿東)

相關文章