萬字技術干貨!LLM工程師必讀量化指南,可視化圖解揭秘大模型如何壓縮
大語言模型(LLM)通常過于龐大,無法在消費級硬件上運行。這些模型的參數可能超過數十億,通常需要顯存較大的GPU來加速推理過程。
因此,越來越多的研究開始關注如何縮小模型,比如改進訓練方法或使用適配器。該領域的一項主要技術被稱為量化(quantization)。
ML工程師Maarten Grootendorst撰寫了一篇博客文章,在語言建模背景下專門介紹了量化技術,并通過可視化的方法逐一探索相關概念,以幫助我們建立對該技術的直觀理解。
在這篇博文中,Maarten將探討各種方法、使用案例以及量化背后的原理。
文章目錄以及涵蓋內容如下圖所示,主要介紹了訓練后量化(PTQ)以及量化感知訓練(QAT)兩種方法,建議有AI基礎的讀者直接跳轉至對稱量化部分:
第一部分:LLM的「問題」
「大語言模型」就是大在模型參數量上,規(guī)模通常達到數十億的級別(其中主要是權重)。
這些參數不僅存儲成本相當高,推理階段的計算量也很大。
在推理過程中,激活值是輸入和權重的乘積,因此權重數量越多,激活值也會越大。
因此,我們希望盡可能高效地表示數十億個值,從而盡可能減少存儲參數所需的空間。
讓我們從頭開始,探索數值是如何表示的,然后再進行優(yōu)化。
如何表示數值
數值存儲的形式通常是浮點數(floting point number,或簡稱為floats):一個帶有小數點的正數或負數。
這些值由每一位(bit)上的二進制數字表示。
IEEE-754標準描述了每一位上的數字如何表示具體數值,具體來說共有三種映射:符號、指數或小數(尾數)。
這三個部分可以結合起來,根據一組bit值計算出所表示的數值:
使用的位數越多,表示的數值值通常越精確,比如FP32形式就能比FP16精確到小數點后更多位數:
內存限制
可用的位數越多,不僅數值越精確,可表示的數值范圍也越廣。
給定位數和表示形式,可表示的數值區(qū)間稱為動態(tài)范圍(dynamic range),而兩個相鄰值之間的距離稱為精度(precision)。
這種表達形式的一個巧妙特性在于,我們可以計算出設備需要多少內存來存儲某個給定值。
由于內存中的每個字節(jié)含有8位,我們可以為大多數形式的浮點數創(chuàng)建一個基本公式——
在實際應用中,還有更多因素會影響推理過程中所需的顯存/內存大小,例如上下文大小和模型架構
現在假設我們有一個包含700億參數的模型。大多數模型本身使用32位浮點數(通常稱為全精度)表示,這需要280GB的內存來加載模型。
但如果能將所有參數用16位浮點數表示,所需的內存大小就可以直接減少一倍。
因此,將模型參數的表示位數最小化(不僅是推理,還有訓練過程)是非常有吸引力的。
然而,這種方法不是沒有代價的。隨著表示位數減少導致精度降低,模型的準確性通常也會下降。
我們希望在保持準確性的同時減少表示數值的位數……此時,量化技術就派上用場了。
第二部分:量化入門
現在我們知道,量化的目的是將模型參數的精度從較高位寬(如32位浮點數)降低到較低位寬(如8位整數)。
在減少表示原始參數的位數時,通常也會伴隨一些精度(粒度,granularity)的損失。
為了讓這種效果更直觀,我們可以用照片顏色作為類比。比如,選擇任意圖像(左圖),但只用8種顏色表示(右圖):
注意看,放大的曲奇餅干看起來比原來更有「顆粒感」。
與之相似,量化的主要目標是減少表示原始參數所需的比特數(顏色),同時盡可能保留原始參數的精度。
常見數據類型
首先,讓我們看看常見的數據類型以及使用它們替代32位(稱為全精度或FP32)表示的影響。
FP16
首先是一個從32位到16位(稱為半精度或FP16)浮點數的例子:
FP16可取的數值范圍比FP32小得多。
BF16
為了獲得與原始FP32相似的數值范圍,引入了bfloat 16作為一種「截斷的FP32」類型:
BF16使用的位數與FP16相同,但增加了指數位,因此能取到更廣泛的數值范圍,常用于深度學習領域。
INT8
進一步減少位數時,就更接近整數而非浮點數的表示方法。比如,從FP32到只具有8位的INT8,只有原始位數的1/4:
每次減少位數時,都會進行映射,將初始的FP32表示「壓縮」到較少的位數中。
但在實際操作中,我們不需要將整個FP32范圍[-3.4e38, 3.4e38]全部映射到INT8中。我們只需找到一種方法,將實際模型參數的數據范圍映射到INT8中。
常見的壓縮/映射方法可以有對稱量化和非對稱量化兩種,都屬于線性映射。
接下來將要探討的就是從FP32到INT8的量化方法。
對稱量化
在對稱量化中,原始浮點值的范圍被映射到量化空間中以零為中心的對稱范圍,量化前后的范圍都以零為中點。
這意味著,原來浮點空間中的零,映射到量化空間后也恰好是零。
一種對稱量化的典型例子是最大絕對值(absmax)量化。
給定一個數值列表,我們取其中最高的絕對值(α)作為執(zhí)行線性映射的范圍。
[-127, 127]表示受限范圍(restricted range),未受限范圍是[-128, 127],取決于量化方法
由于這是一個以零為中心的線性映射,公式很簡單。
首先用以下公式計算比例因子(s):
- b是我們要量化到的字節(jié)數(8)
- α是最高的絕對值
然后,我們使用s來量化輸入x:
如上圖所示,最大絕對值α為10.8,將FP32映射到INT8時,即有如下公式:
如果要恢復原始的FP32值,也可以使用先前計算的比例因子(s)來進行反量化。
先量化,再反量化以恢復原始值,全過程如下所示:
可以看到某些值,如3.08和3.02,在量化為INT8時都是36。因此進行反量化恢復到FP32時,它們失去了一些精度并且不再可區(qū)分。
這種原始值和反量化值之間的差異被稱為量化誤差。通常,量化結果的位數越少,誤差越大。
非對稱量化
與對稱量化不同,非對稱量化不是以零為中心的對稱。相反,它將浮點范圍內的最小值(β)和最大值(α)分別映射到量化范圍的最小值和最大值。
這里我們探討的方法被稱為零點量化(zero-point quantization)。
注意0的位置是如何移動的。這就是為什么它被稱為非對稱量化。在范圍[-7.59, 10.8]中,最大值和最小值到0的距離不同。
由于零點位置的偏移,我們必須計算INT8范圍內的零點才能執(zhí)行線性映射。與之前一樣,我們還必須計算比例因子(s),但使用INT8范圍的差值[-128, 127]。
由于需要在INT8范圍內計算零點(z)以移動權重,這有點復雜。
像之前一樣,讓我們填入公式:
為了將量化后的值從INT8反量化回FP32,我們需要使用先前計算的比例因子(s)和零點(z)。
除此之外,反量化很簡單:
當我們將對稱和非對稱量化并排放置時,可以快速看出兩種方法之間的區(qū)別:
在上圖中,我們能看到對稱量化的零中心特性與非對稱量化的偏移。
范圍映射和剪裁(Clipping)
在之前的例子中,我們探討了如何將給定向量中的值范圍映射到低位表示。雖然這樣可以映射整個向量值的范圍,但有一個主要缺點,即異常值(outlier)。
想象一下,你有一個包含以下值的向量:
一個值比其他所有值都大得多,可以被認為是異常值。如果我們映射整個向量的范圍,所有小值將被映射到相同的低位表示,并失去它們的區(qū)分度:
這是之前使用的absmax方法。如果不進行剪裁,非對稱量化也會發(fā)生同樣的情況
相反,我們可以選擇剪裁某些值。剪裁是指設置原始值的不同動態(tài)范圍,使所有異常值都被設為相同的值。
在下面的例子中,我們手動將動態(tài)范圍設置為[-5, 5],所有超出該范圍的值將被映射到-127或127,無論它們的實際值是多少:
這種方法的主要優(yōu)點是非異常值的量化誤差顯著減少。然而會導致異常值的量化誤差增加。
校準(Calibration)
上面的例子中,我們隨機將動態(tài)范圍設置為[-5, 5],但其實應該通過「校準」過程做出決定,找到一個合適的范圍,包含盡可能多的值,同時最小化量化誤差。
校準步驟的具體執(zhí)行對于不同類型的參數是不一樣的。
權重(和偏置)
我們可以將大語言模型(LLM)的權重和偏置(weights & biases)視為靜態(tài)值,因為它們在運行模型之前是已知的。例如,Llama 3的約20GB文件大部分由其權重和偏置組成。
由于偏置變量的數量(數百萬)顯著少于權重(數十億),因此偏置通常保持較高精度(如INT16),而量化的主要工作集中在權重上。
對于已知的靜態(tài)權重,選擇范圍的校準技術包括:
- 手動選擇輸入范圍的百分位數
- 優(yōu)化原始權重和量化權重之間的均方誤差(MSE)
- 最小化原始值和量化值之間的熵(KL散度)
例如,選擇一個百分位數,會導致類似于我們之前看到的剪裁行為。
激活值
在整個大語言模型中不斷更新的輸入通常被稱為激活值(activations)。
之所以被稱為激活值,因為它們通常會經過一些激活函數,如sigmoid或relu
與權重不同,激活值在推理過程中隨輸入數據而變化,因此難以準確量化。
由于這些值在每個隱藏層之后都會更新,因此在推理階段,只有輸入數據通過模型后才能得知它們的具體數值。
總體來說,有兩種方法用于校準權重和激活值,應用于模型的不同階段:
- 訓練后量化(Post-Training Quantization,PTQ)
- 顧名思義,即訓練后進行的量化
- 量化感知訓練(Quantization Aware Training,QAT)
- 訓練/微調期間的量化
第三部分:訓練后量化(PTQ)
訓練后量化(PTQ)是最流行的量化技術之一。它是在模型訓練完成后,對模型的參數(包括權重和激活值)進行量化。
權重的量化可以采用對稱量化或非對稱量化的方法。
然而,激活值的量化需要經過推理階段來獲取其潛在分布,因為我們事先并不知道它們的范圍。
激活值的量化有兩種形式:
- 動態(tài)量化(dynamic quantization)
- 靜態(tài)量化(static quantization)
動態(tài)量化
數據通過隱藏層后,其激活值會被收集,比較出每一層的最大值(α)和最小值(β):
然后利用這些激活值的分布來計算量化輸出所需的零點(zeropoint,z)和比例因子(scale factor,s)值:
每次數據通過新的網絡層時,這個過程都會重復。因此,每一層都有其獨立的z和s值,從而使用不同的量化方案。
靜態(tài)量化
與動態(tài)量化不同,靜態(tài)量化并不是在推理過程中計算零點(zeropoint,z)和比例因子(scale factor,s),而是在推理之前計算這些值。
為了找到這些值,我們會使用一個校準數據集,并將其輸入模型以收集這些潛在的激活值分布。
收集到這些分布之后,就可以計算出在推理過程中進行量化所需的s和z值。
在實際推理時,不會重新計算s和z值,而是在所有激活中全局使用它們來對其進行量化。
總體來說,動態(tài)量化為每個隱藏層計算s和z值,往往更準確。然而,這可能會增加計算時間,因為這些值需要在每次推理時計算。
相反,靜態(tài)量化雖然不如動態(tài)量化準確,但速度更快,因為它已經預先知道用于量化的s和z值。
4-bit量化領域
低于8-bit的量化一直是一個挑戰(zhàn),因為每減少一位,量化誤差就會增加。幸運的是,有幾種巧妙的方法可以將位數減少到6、4,甚至2-bit(盡管通常不建議將位數降到低于4-bit)。
我們將探討在HuggingFace上常見的兩種方法:
- GPTQ(全模型在GPU上運行)
- GGUF(可能將層卸載到CPU上)
GPTQ
GPTQ可以說是實際應用中最著名的4-bit量化方法之一。
它使用非對稱量化,并逐層進行處理,每層獨立處理后再繼續(xù)處理下一層:
在這個逐層量化過程中,它首先將層的權重轉換為逆Hessian矩陣。逆Hessian矩陣是模型損失函數的二階導數,表示模型輸出對每個權重變化的敏感性。
簡單來說,它本質上展示了每個層中權重的重要性(逆重要性)。
Hessian矩陣中較小值的權重更為重要,因為這些權重的微小變化可能導致模型性能的顯著變化。
在逆Hessian矩陣中,較低的值表示更「重要」的權重
接下來,我們量化并反量化權重矩陣的第一行:
這一過程使我們能夠計算量化誤差(q),我們可以使用之前計算的逆Hessian值(h_1)來加權這個量化誤差。
本質上,我們是在基于權重的重要性創(chuàng)建加權量化誤差:
接下來,我們將這個加權量化誤差重新分配到該行的其他權重上。這有助于保持網絡的整體功能和輸出。
例如,如果對第二個權重(即x_2=0.3)進行此操作,我們會將量化誤差(q)乘以第二個權重的逆Hessian(h_2)加上去:
接下來,繼續(xù)對給定行中的第三個權重進行相同的操作:
重復這個重新分配加權量化誤差q的過程,直到所有值都被量化。
這個方法所以有效,是因為權重通常是相互關聯的。因此,當一個權重有量化誤差時,相關的權重會通過逆Hessian進行相應的更新。
GGUF
雖然GPTQ是一種很好的在GPU上運行整個大語言模型(LLM)的量化方法,但如果沒有相應的硬件條件,也可以通過GGUF將LLM的任意層卸載到CPU上。
相當于同時用CPU和GPU運行模型,以彌補顯存(VRAM)不足的情況。
量化方法GGUF經常更新,而且依賴于具體的量化位數,但基本原理如下。
首先,給定層的權重被分成「超級塊」,每個「超級塊」包含一組「子塊」。從這些「子塊」中,我們計算出比例因子(s)和α值:
為了量化給定的「子塊」,可以使用之前提到的absmax量化,將給定的權重乘以比例因子(s_sub):
比例因子s_sub是使用「子塊」中的信息計算的,但用「超級塊」中的信息s_super進行量化:
總而言之,這種以塊為單位的量化使用「超級塊」的比例因子(s_super)來量化「子塊」的比例因子(s_sub)。
每個比例因子的量化級別可能不同,「超級塊」的比例因子通常比「子塊」有更高的精度。
為了說明這一點,讓我們探討幾個量化級別(2-bit、4-bit和6-bit):
根據量化類型,還需要一個額外的最小值(m)來調整零點,這些與比例因子(s)一樣被量化
第四部分:量化感知訓練(QAT)
第三部分講述了如何在訓練后對模型進行量化。這種方法的缺點在于,沒有考慮到實際的訓練過程。
這就是量化感知訓練(QAT)派上用場的地方。與訓練后量化(PTQ)不同,QAT的目標是在訓練中學習量化過程。
QAT往往比PTQ更準確,因為在訓練過程中已經考慮了量化。其工作原理如下:
在訓練過程中,引入了所謂的「假」量化。比如先將權重量化為INT4,然后再反量化回FP32:
這一過程讓模型在訓練階段進行損失計算和權重更新時,就已經考慮到了量化誤差。
如下圖所示,QAT嘗試探索「寬」極小值情況下的損失值,以減少量化誤差,因為「窄」極小值往往會導致更大的量化誤差。
假設在反向傳播過程中沒有考慮量化,梯度下降過程就會選擇損失值最小的權重。然而,如果它處于「窄」極小值中,那將引入更大的量化誤差。
相反,如果我們考慮量化,將在「寬」極小值中選擇一個不同的更新權重,其量化誤差要小得多。
因此,盡管PTQ方法在高精度(例如FP32)有較低的損失值,但QAT在低精度(例如INT4)下損失值也很低,這是我們所追求的。
1-bit時代:BitNet
之前我們看到,將量化精度降低到4-bit已經相當小了,但如果我們進一步降低呢?
這就是BitNet的用武之地,它將模型的權重表示為單個比特,即-1或1,通過將量化過程直接注入到Transformer架構中來實現這一點。
Transformer架構是大多數LLM的基礎,由涉及線性層的計算組成:
這些線性層通常以更高的精度表示,如FP16,而且是大多數權重所在的位置。
BitNet用BitLinear層替換了這些線性層:
BitLinear層的工作原理與普通線性層相同,用權重乘以激活值來計算輸出。
但不同的是,BitLinear層僅用1位表示模型的權重,使用INT8表示激活值:
BitLinear層像量化感知訓練(QAT)一樣,在訓練期間執(zhí)行一種「假」量化,以分析權重和激活值的量化效果:
讓我們一步步地了解BitLinear。
權重量化
在訓練過程中,權重以INT8存儲,然后使用一種稱為符號函數(signum function)的基本策略將其量化為1位。
本質上,它將權重的分布移動至以0為中心,然后將所有小于0的值分配為-1,將所有大于0的值分配為1:
此外,它還跟蹤一個值β(平均絕對值),我們將在之后的反量化過程中使用。
激活值量化
為了量化激活值,BitLinear使最大絕對值方法(absmax)將激活值從FP16轉換為INT8,因為它們需要以較高的精度進行矩陣乘法(×)。
此外,它還跟蹤一個值α(最大絕對值),我們將在之后的反量化過程中使用。
反量化
我們跟蹤了α(激活值的最大絕對值)和β(權重的平均絕對值),這些值將幫助我們將激活值反量化回FP16。
輸出激活值使用{α, γ}重新縮放,以將其反量化到原始精度:
這個過程相對簡單,并且允許模型僅用兩個值表示,即-1或1。
通過這種方法,作者觀察到,隨著模型規(guī)模的增長,1位訓練和FP16訓練之間的性能差距越來越小。
然而,這僅適用于較大的模型(>30B參數),較小模型之間的差距仍然很大。
所有LLM都是1.58位
為了改善之前提到的擴展性問題,BitNet 1.58b被引入。
在這種新方法中,模型的每個權重不僅可以是-1或1,還可以取0,使每個變量成為三元值(ternary)。
有趣的是,僅僅是添加0這個簡單的操作,就大大改進了BitNet,加速了計算過程。
0的力量
為什么添加0是一個重大改進呢?
這與矩陣乘法有關!
首先,讓我們探討一下矩陣乘法的基本工作原理。
當計算輸出時,我們將權重矩陣與輸入向量相乘。下面是權重矩陣的第一層的第一行乘法的可視化:
這種乘法涉及兩個動作,將單個權重與輸入相乘,然后將它們全部相加。
相比之下,BitNet 1.58b設法避免了乘法的動作,因為三值權重本質上告訴你以下內容:
- 1:我想加上這個值
- 0:我不想要這個值
- -1:我想減去這個值
因此,如果你的權重被量化到1.58 bit,你只需要執(zhí)行加法:
這不僅可以顯著加快計算速度,還允許特征過濾。
將給定的權重設置為0就相當于忽略了這個輸入,而不是像1-bit那樣,表示加上或減去輸入值。
量化
為了執(zhí)行權重量化,BitNet 1.58b使用平均絕對值量化(absmean),這是我們之前見過的最大絕對值量化(absmax)的變體。
它只是壓縮權重的分布并使用絕對均值(α)來量化值。然后將它們四舍五入為-1、0或1:
與BitNet相比,激活值量化是相同的,除了一個方面:不是將激活值縮放到范圍[0, 2??1],而是使用最大絕對值方法縮放到[-2??1, 2??1]。
總結一下,1.58-bit量化主要涉及兩個技巧:
- 添加0以創(chuàng)建三值表示[-1, 0, 1]
- 權重的絕對均值量化
BitNet論文中有這樣的結論:「13B BitNet b1.58在延遲、內存使用和能耗方面比3B FP16 LLM更高效?!?/span>
論文地址:https://arxiv.org/abs/2402.17764
由于只有1.58個計算效率高的bit,我們得到了輕量級模型。