iT邦幫忙

2023 iThome 鐵人賽

DAY 14
6

簡介

量化 (Quantization) 是我們這些平民 LLM 玩家最好的夥伴,一般模型在訓練時多使用 32-Bit 或 16-Bit 的浮點數,即便是 7B 16-Bit 的模型,也要消耗掉 13 GiB 以上的 GPU 記憶體,更不用說是 13B 以上的模型,一般單張 24GB 消費級顯卡根本放不下。

這時候量化超人就來拯救我們了!這個技術可以將模型轉換成 8-Bit 或 4-Bit 甚至更低,大幅減少模型佔用的 GPU 記憶體,使我們能夠操作 13B 甚至 30B 以上的模型。接下來,我們就來看看這個量化技術到底是怎麼辦到的吧!

可愛貓貓 Day 14

(Powered By Microsoft Designer)

量化

說文解字

「量化」一詞有多個用法,其中一個是把抽象的概念給「數字化」,舉例來說:

@penut 你今天有多努力?
:我今天寫了 30 行程式碼我超認真

「多努力」就是一種抽象的概念,而我們用 30 行程式碼這種具體的數字與單位,來量化一個人今天有多偷懶 ((x

但是這樣的量化方法,會忽略掉許多細節。也許對方今天做了很多寫程式以外的事情,而在這個量化方法下都被無視了,因此產生了誤差。所以選擇一個能夠減少誤差的量化方法,是這個領域最關注的重點。

基本概念

在機器學習領域裡面,量化則被用來進行值域的轉換。例如說,我們將 16 位元的浮點數 (FP16) 量化成 8 位元的整數 (INT8),這種將高位元的資料轉換為低位元的過程。

其實也能把 FP16 量化成 INT16,但比較少見。

量化技術的初始概念相當單純,假設我們有以下 FP16 數列:

[1.2, 0.3, -0.1, -0.8]

觀察這個數列,他的值域介在 [-0.8, 1.2] 之間。若我們要用 INT8 來表示,那我們需要將這個值域對應到 [-128, 127] 之間。最簡單的做法就是透過正規化將其 Normalize 到那個值域裡面。

正規化是機器學習裡面相當重要的技巧,透過將輸入正規化到 [-1.0, 1.0] 之間,減少 Bias 使模型更容易收斂。若要使用正規化進行量化,其步驟如下:

首先將整組數列減去最小值 -0.8 得到:

[2.0, 1.1, 0.7, 0.0]

接著計算原本值域的間距 (1.2 - -0.8) = 2.0 並將新的數列除以間距:

[1.0, 0.55, 0.35, 0.0]

最後縮放到 INT8 的值域 x * 255 - 128 並四捨五入:

[127, 12, -39, -128]

到這一步,我們就完成 FP16 到 INT8 的量化 (Quantize) 動作。在存放這組數列時,最小值與間距也要一起被存下來,通常會是原本的資料型態。除了精準度的考量以外,也要考慮極值的問題,避免間距或最小值因為過大過小而發生溢位的問題。

接著在運算時,通常會將數列反向量化 (Dequantize) 回來,也就是按照量化的流程反向計算回去

(x + 128) / 255 * 2.0 + (-0.8)

反向量化後的數列如下:

[1.2, 0.298, -0.102, -0.8]

可以看到數列並沒有被精準的還原,數值內容是有誤差的。因此量化技術探討的不僅只是將數值壓縮,更重要的目標在於減少誤差

但這樣的做法有一點點的小問題,在於需要額外存放最小值與間距兩個數值。以上例而言,雖然四個數值省下了 8 * 4 = 32 個位元,但是因為額外存了兩個 FP16 的資訊,所以兩者就抵銷了。雖然一般而言不會只拿著四個數值就在那邊做量化,但我們可以使用其他方法來進一步減少額外資訊。

這裡我們可以先對數列取絕對值再做量化,這樣的好處在於最小值必定為零,以上例而言,數列的值域就被縮減到 [0.0, 1.2] 之間。按照正規化的概念,我們應該先將數列除以 1.2 來正規化到 [-1.0, 1.0] 之間,並乘上 127 來得到 INT8 數列:

(x / 1.2) * 127

但我們透一些簡單的數學原理,可以將算式整理如下:

(x / 1.2) * 127
 = x * (1 / 1.2) * 127  # 改寫為乘法
 = x * 127 * (1 / 1.2)  # 根據交換律
 = x * (127 / 1.2)      # 根據結合律

這個 127 / 1.2 就是所謂的量化常數 (Quantization Constant),以此例而言為 127 / 1.2 = 105.83,這也是這個量化方法裡面唯一一個額外資訊。將此量化常數與原本的數列相乘並四捨五入,得到以下數列:

[127, 32, -11, -85]

而反向量化也只要除以量化常數即可,結果如下:

[1.2, 0.302, -0.104, -0.803]

這樣的做法誤差值稍大一點,但是計算也比較方便一些。以上這種做法也稱為 AbsMax Quantization,是概念最單純的一種量化方法。按照這個方法,其實也能直接做 INT4 的量化,但是誤差就更大了!所以一般 8-Bit 以下的量化通常並不是這種做法。

我們可以透過 Python 簡單實做出這種量化方法:

import numpy as np

def quantize_fp16_to_int8(fp16_weights):
    # 找到權重的絕對最大值
    abs_max_val = np.max(np.abs(fp16_weights))

    # 量化常數 Quantization Constant
    quant_const = 127 / abs_max_val

    # 將 FP16 權重轉換為 Int8
    weights = fp16_weights * quant_const
    int8_weights = np.round(weights).astype(np.int8)

    return int8_weights, quant_const


def dequantize_int8_to_fp16(int8_weights, quant_const):
    # 將 Int8 權重轉換回 FP16
    return int8_weights.astype(np.float16) / quant_const

可以參考這份 Colab Demo 看看 INT8 的效果如何,筆者也在這份 Colab 裡面示範,如果直接將這個方法套用到 INT4 上誤差會有多大,各位可以參考看看。

bitsandbytes

bitsandbytes 簡稱 BNB,是來自 Tim Dettmers 的專案,HF Transformers 做 8-Bit 量化的後端技術。其做法與上個章節描述大致相同,之前的文章也經常提到,只要加上 load_in_8bit=True 就可以將模型轉成 8-Bit 格式了:

from transformers import LlamaForCausalLM as ModelCls

model: ModelCls = ModelCls.from_pretrained(
    "TheBloke/Llama-2-7b-chat-fp16",
    device_map="auto",
    load_in_8bit=True,
)

也可以將 8-Bit 模型透過 .save_pretrained 存下來,這樣下次再讀取就會直接是 8-Bit 的型態,是個節省硬碟空間的手段。但是不太建議這樣做,因為變成 8-Bit 的格式之後,就不能再轉回 16-Bit 或者降成 4-Bit 之類的,而且也會強制讀進 GPU 而不能在 CPU 裡使用。

HF 4-Bit

4-Bit NormalFloat, NF4 是 QLoRA 論文裡面提出的資料型態,作者同樣也是 BNB 的 Tim Dettmers。在 HF Transformers 裡面調用 4-Bit 模型同樣可以簡單的加上 load_in_4bit=True 即可,但是 4-Bit 其實還有滿多參數的,我們可以透過 BitsAndBytesConfig 類別來進行設定:

import torch
from transformers import BitsAndBytesConfig
from transformers import LlamaForCausalLM as ModelCls


quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_compute_dtype=torch.float16,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
)

model: ModelCls = ModelCls.from_pretrained(
    "TheBloke/Llama-2-7b-chat-fp16",
    device_map="auto",
    quantization_config=quantization_config,
)

雖然模型被量化成 4-Bit 但實際運算時還是會反向量化回浮點數,預設是用 FP32 進行運算,也可以透過 bnb_4bit_compute_dtype 設定為 FP16 會算的比較快。預設 4-Bit 的資料型態是一般的 FP4,將 bnb_4bit_quant_type 設定為 NF4 就可以使用論文裡面提到的資料型態。最後如果開啟 bnb_4bit_use_double_quant 可以進一步減少模型的大小。

與 8-Bit 不同,目前 Transformers 尚未支援直接將模型存成 BNB 4-Bit 的格式。

使用 4-Bit 模型不僅能讓我們順利在單張 24GB 消費級顯卡上使用 30B 的模型進行推論,也可以結合 LoRA 並訓練 30B 的模型,這在未來講到 LoRA 訓練時會再做進一步介紹。

GPTQ

GPTQ 是個相當受歡迎的量化技術。前面介紹的 BNB Quantization 雖然可以快速的將模型量化到低位元,但是其準確率與運算效率是個經常受到挑戰的部份。GPTQ 與 BNB 不同,他是透過校準資料集進行 Post Training 的方法,來對每個權重逐步進行量化,因為是使用實際資料進行校準,所以模型的失真度會比較低,但也因此需要花上很多時間進行量化。

AutoGPTQ 是個讓我們能夠更簡單操作 GPTQ 的套件,此套件已被 HF Transformers 整合進去,若我們要使用 GPTQ 首先需要安裝相關套件:

pip install optimum auto-gptq

以下程式碼示範如何透過 Transformers 對模型進行 GPTQ 量化:

from transformers import GPTQConfig
from transformers import LlamaForCausalLM as ModelCls
from transformers import LlamaTokenizerFast as TkCls

model_id = "TheBloke/Llama-2-7b-chat-fp16"

tk: TkCls = TkCls.from_pretrained(model_id)
quantization_config = GPTQConfig(
    bits=4,
    dataset="c4",
    tokenizer=tk,
    model_seqlen=2048,  # 影響 GPU 使用量
)

model: ModelCls = ModelCls.from_pretrained(
    model_id,
    device_map="auto",
    low_cpu_mem_usage=True,
    quantization_config=quantization_config,
)

model.save_pretrained(f"Llama-2-7b-chat-gptq")

筆者實測 RTX 3090 只能做到 2048 長度而已,因此 model_seqlen 這個參數很重要,如果沒有設定的話就會直接 CUDA OOM 掛掉。

GPTQ 4-Bit 的優點之一就是他能以 4-Bit 的格式存在硬碟裡面,原本 Llama 2 7B FP16 佔用了 13GB 的硬碟空間,若是轉成 8-Bit 則會降到 6.6GB 大小,進行 GPTQ 後更是縮小到了 3.7GB 而已!

我們也能透過 AutoGPTQ 直接對模型進行量化,這邊可以參考量化大神 TheBloke 的 Script,這份程式甚至可以在 24GB 的顯卡上量化一個 30B 的模型,真的相當厲害!

筆者嘗試在 TITAN RTX 24GB 上對 CodeLlama 34B 進行 GPTQ 量化,花費了 110 分鐘,將近兩個小時才完成,但是量化完之後,消耗的硬碟空間從 63GB 降到了 18GB。

如果你不想花很多時間進行 GPTQ 校準,可以考慮直接去 Hugging Face Hub 上面尋找有沒有人已經做完 GPTQ 訓練,例如 TheBloke 大神就上傳相當多使用 GPTQ 量化過的模型

混淆度 Perplexity

在量化的過程中,會相當重視模型的損失程度,也就是說量化完之後的誤差有多大,這在語言模型裡面通常以混淆度 (Perplexity, PPL) 來測量,PPL 是用來表示模型預測下一個字詞的時候其不確定性如何。

因為 Decoder LM 每次 Inference 時會產生一份機率表,如果這個機率表顯示「下個字是 X 的機率為 100%!」那就代表模型非常明確,這時混淆度就會很低。但如果模型覺得「嗯 ... 好像每個字都有可能」那就代表模型相當混淆,此時的 PPL 就會很高。

一般語言模型都是以交叉熵 (Cross Entropy) 當作 Loss,只要對 Loss 取 Exponential 就能算出 PPL 了。

在量化與參數量之間有個相當重要的觀念,這裡引用 llama.cpp 裡面某個 PR 的一張圖片:

PPL

橫軸是模型的大小,縱軸則是模型的混淆度。我們可以看到在合理的量化下,模型的參數量還是比模型的大小來的重要。也就是說,即使我們用 16-Bit 去跑一個 7B 的模型,其效果也不會比 4-Bit 的 13B 模型還要好,雖然 7B 16-Bit 的模型大小比 13B 4-Bit 還要大的多。因此模型的能力還是取決於參數量的多寡,而資料型態僅影響精準度而已。

其次是即便模型變小了,但是計算量還是差不多的,只是每個權重的資料型態變小,因此在運算上可能可以減少傳輸頻寬,但是這並不代表計算量變少了。

結論

今天介紹了 Transformers 框架底下常用的量化方法,這些量化技術大幅降低了遊玩 LLM 的門檻,即便只有一張 24GB 的 GPU 也能玩轉 30B 規模的模型。雖然量化這門學問已經發展了一段時間,但依然是個快速成長中的技術,例如筆者今天查看 TGI 的參數說明時才發現他們居然要將 BNB Deprecate 了 😱

Text Generation Inference, TGI 是 Hugging Face 製作的一個推論框架,未來也會仔細介紹這個專案。TGI 表示接下來將會使用 EETQ 套件來取代 BNB 框架。感覺 BNB 還只是個出現沒多久的東西,一下子就準備要被換掉了。技術迭代的速度如此之快,也許不久的將來又會出現其他量化框架。但無論如何,對筆者這種貧窮玩家而言都是一大利多 🥰

接下來將會開始介紹推論常用的框架,包含 ONNX, GGML, vLLM 與 TGI 等,那我們明天見啦!

參考


上一篇
LLM Note Day 13 - Code LLMs 專門寫程式的語言模型
下一篇
LLM Note Day 15 - ONNX & ONNX Runtime
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言