iT邦幫忙

2023 iThome 鐵人賽

DAY 19
2

簡介

Offloading Inference 主要在探討如何讓 GPU 與其他裝置一起協同推論,例如有些運算放在 CPU,有些記憶體暫存在硬碟裡面。這類的方法通常用在參數量大到不行的模型上,例如我的 GPU 記憶體可能就 10GB 而已,想要使用參數量 30B 的模型進行推論,光是把模型讀進記憶體裡面都是個問題,更不用說是進行推論了。

Devices

正當 GPU 努力工作的時候,看了一眼在旁邊喝咖啡聊是非的 CPU & Disk 們,心中鐵定很不是滋味。這些裝置也是有能力負擔一些 Tensor 的存放,甚至能做些運算,閒置在一旁總是不太好。今天我們透過 FlexGen 這個套件來一同看看 Offloading Inference 的概念。

可愛貓貓 Day 19

(Powered By Microsoft Designer)

背景

Decoder LM 的 Inference 可以分成兩個階段:

  1. Prefill 階段
  2. Decoding 階段

Prefill 階段,模型會對最一開始的輸入進行第一次推論,這時除了預測下一個 Token 的機率以外,還會產生一個很大的初始 KV 快取。

接下來進入 Decoding 階段,這個階段模型會 Token By Token 的做文本生成,每次推論都會產生一組新的 KV 快取,並與原本的 KV 快取串接在一起,所以 KV 快取會越來越大。

在這個過程中,主要的記憶體消耗為模型權重本身以及 KV 快取,其中 KV 快取佔用的記憶體會越來越大,最終超越模型權重本身佔用的記憶體。

問題定義 Problem Formulation

FlexGen 將這個 Offloading Generative Inference 視為一種圖形拜訪問題,在這邊一個方格就是一個 Batch,一樣顏色的方塊代表模型的同一個 Layer。

Graph

在這個拜訪問題裡面有以下限制:

  1. 只有在左邊的方格都完成計算時,才能計算右邊的方格。
  2. 每個方格的輸入,包含權重本身、隱藏層狀態、KV 快取等,都要被讀取進同一個裝置才能運算。
  3. 隱藏層狀態必須等到正右邊的一個方格完成計算後才能被捨棄。
  4. KV 快取必須等一整行都完成計算後才能捨棄。
  5. 任意 Tensor 的大小都不能超過當前裝置的總記憶體容量。

計算規劃 Compute Schedule

多數的 Offloading 系統都是以 Row-By-Row 的方式完成計算,因為這樣可以最快完成一筆推論,然後把那個超肥大的 KV 快取丟掉,再繼續進行下一筆推論。

Row-By-Row

但因為模型沒辦法被完整讀取進 GPU 記憶體裡面,所以這些 Layers 是輪流被放進 GPU 運算的,因此 Row-By-Row 的做法會不斷重複讀取模型權重,造成記憶體讀寫很大的負擔。

但如果我們以 Col-By-Col 的方式做計算,就可以減少模型權重的 IO 負擔。這時 IO 問題就會轉移到隱藏層狀態與 KV 上面。上圖的每個方格在輪流運算時,會發生六件事情:

  1. 讀取下個 Layer 的權重
  2. 存放前個 Batch 算出來的隱藏層狀態 (Output of Prev Batch)
  3. 存放前個 Batch 算出來的 KV 快取 (Output of Prev Batch)
  4. 讀取下個 Batch 需要的隱藏層狀態 (Input of Next Batch)
  5. 讀取下個 Batch 需要的 KV 快取 (Input of Next Batch)
  6. 進行當前 Batch 的運算 (Compute Current Batch)

這些操作基本上都是互相獨立的,因此可以同時進行來縮短需要的時間,這個技巧被稱為 Overlapping。

Col-By-Col

張量配置 Tensor Placement

Offloading Inference 對 Tensor 存放的位置比較講究,切分 Tensor 的方式有很多種粒度 (Granularity),例如:

  • Model Granularity
    • 把 Model 的每個 Layer 切開
  • Layer Granularity
    • 把 Layer 的每個 Tensor 切開
  • Tensor Granularity
    • 把 Tensor 的每個 Element 切開

較粗的粒度 (Coarser Granularity) 會有比較小的運行開銷 (Runtime Overhead),例如模型都不切,能夠整份放進 GPU 記憶體,那就不用一直讀讀寫寫,直接算完就好,但是在 Offloading Inference 裡面沒辦法這麼理想。

較細的粒度 (Finer Granularity) 會有更好的靈活度,FlexGen 更偏好細粒度的切法,因此在模型權重上採用 Layer Granularity,而在隱藏層狀態和 KV 快取上採用 Tensor Granularity。

計算委派 Computation Delegation

雖然 CPU 比 GPU 慢很多,但是在計算 Attention 時還是有幫助的,考慮以下情況:

  1. 現在 KV 快取不在 GPU 裡面。
  2. 現在隱藏層狀態不在 CPU 裡面。
  3. 如果要用 GPU 運算,需要將 KV 快取整份搬進 GPU 記憶體。
  4. 如果要用 CPU 計算,只要把隱藏層狀態從 GPU 搬進 CPU 記憶體。

KV 快起的大小為 (B, S, H, 4) 位元組,而隱藏層狀態僅為 (B, H, 4) 位元組。

  • B 代表 Batch Size
  • S 代表 Sequence Length
  • H 代表 Hidden Size
  • 4 代表每個權重佔 4 Bytes

註:因為一次只 Decode 一個 Token,所以隱藏層狀態實際上的長度 S = 1。

理論上 KV 快取會比隱藏層狀態大很多,所以算上記憶體搬運的時間,當 Sequence Length 越長,搬進 CPU 運算與搬進 GPU 運算的時間就會差越多。因此 FlexGen 在規劃運算流程時,也會考量到裝置的 IO 速度。

成本模型 Cost Model

成本模型 (Cost Model) 是在估算某組設定下所需要的花費時間,基本上就是:

  1. Prefill Latency 乘上模型層數
  2. Generation Latency 乘上生成長度

這裡成本模型的概念很簡單,就是以上兩者加在一起,這個時間成本被定義為 T

因為每個 Layer 的運算裡面,記憶體的 Load/Unload 與實際運算都是平行的,所以 Latency 就是這幾個操作裡面最慢的那個。

張量只會在 (GPU <=> CPU)(CPU <=> Disk) 之間做傳輸,因此在 GPU 裡的記憶體不會直接傳進硬碟裡面。

策略搜索 Policy Search

將問題定義與成本模型建立之後,就可以開始針對一系列的參數搜尋最佳的 Offloading 運算策略。首先定義以下參數:

  • bls 代表 Block Size
  • gbs 代表 GPU Batch Size
  • g, c, d 分別代表 GPU, CPU, Disk
  • wg, wc, wd 代表模型權重在三個裝置上的放置比例
  • hg, hc, hd 代表隱藏層狀態在三個裝置上的放置比例
  • cg, cc, cd 代表 KV 快取在三個裝置上的放置比例

目標是找到一組參數,可以使 T / bls 最小,且符合以下限制:

  • GPU Peak Memory < GPU 記憶體容量
  • CPU Peak Memory < CPU 記憶體容量
  • Disk Peak Memory < Disk 記憶體容量
  • wg + wc + wd = 1
  • hg + hc + hd = 1
  • cg + cc + cd = 1

至此 FlexGen 將 Offloading Inference 變成一個線性規劃 (Linear Programming) 的問題,而這個問題並不難解。首先列舉 bls, gbs 的組合,因為 gbs 為 4 的倍數,且 bls 小於 20,所以能列舉的組合並不多。

接著拿一些 Sample Data Point 去實際估算硬體的速度參數,就能求得完整的一組策略參數。有了這組策略參數後,就可以開始進行推論啦!

量化 Quantization

量化能減少模型權重佔用的記憶體,在 Offloading Inference 裡面也是如此。但 FlexGen 不僅對模型權重兩化,甚至連 KV 快取都被量化了!系統直接使用 Min/Max Quantization 轉換成 INT4,FlexGen 實驗證實這樣做依然可以維持很好的 PPL 或準確率。

將權重與 KV 快取量化為 INT4 後,可以減少四倍 (FP16 => INT4) 的 IO 量。

這其實令筆者相當驚訝,因為直接做 INT4 量化通常會帶來很大的誤差,這裡不僅將模型量化成 INT,連 KV 快取都變成 INT4,這樣的結果卻不會跟原本的模型差很多,也許這就是 LLM 相當 Robust 的展現吧。

註:若是有做 CPU Delegation 就不做 Quantization。

稀疏注意力 Sparse Attention

稀疏注意力 (Sparse Attention) 的概念相對單純,注意力機制會針對一個序列上的每個 Token 算出一個注意力分數 (Attention Score),用來代表當前位置要給該 Token 多少「注目」。而 Sparse Attention 是在算完 Attention Score 之後,只取 Top-K 分數最高的分數繼續做運算,其他 Token 的分數就通通丟掉掉。

論文實驗 Experiments

論文作者使用 Google Cloud 的 NVIDIA T4,以 OPT 6.7B, 30B, 175B 三種規模的 LLM 做測試,並使用合成的資料集,每個 Prompt 都被 Padding 到相同長度,然後計算整個模型的吞吐量做為評估公式。

對標的 Baseline 系統為 DeepSpeed ZeRO, Hugging Face AcceleratePetals

以下是主要的實驗結果:

Exp

其他系統多是輸在無法將模型放入 GPU,所以必須使用原本就很慢的 CPU Offloading。或者是很勉強的放進一張 GPU 裡面,但是 Batch Size 超級小,導致吞吐量極低。使用 Pipeline 平行化可以大幅提昇吞吐量,尤其是生成的 Tokens 越多時,Pipeline 平行化的優勢就會展現出來。

更令筆者感到興趣的是 Quantization 的實驗:

Quantization

其中 FP16 是原本的權重,4-Bit 代表有量化,而 4-Bit-S 代表有量化且加上 Sparse Attention。可以看到無論有沒有加上 Sparse Attention,4-Bit Quantization 都跟 FP16 差距沒有很大。

FlexGen

看完論文並瞭解 FlexGen 的工作原理後,我們就來實際操作看看吧!首先使用 pip 安裝 FlexGen 套件:

pip install flexgen

建議開個新的 Conda 環境來安裝,這東西大概裝了十幾分鐘才裝完。

接著筆者借了一台 GPU 只有 12GB 記憶體的電腦來跑以下指令:

export TRANSFORMERS_CACHE="./Models/Cache"
python3 -m flexgen.flex_opt \
    --model facebook/opt-30b --percent 0 100 100 0 100 0

因為那台電腦的 Home 目錄所在硬碟快被塞爆了,所以把 TRANSFORMERS_CACHE 指定到另外一顆硬碟上。開始執行後,那台電腦就用他孱弱的網速開始努力下載 OPT-30B 的模型權重並進行格式轉換。

(Few Days Later ...)

經過兩個小時的奮鬥之後,程式終於跑完了!得到以下結果:

Outputs:
----------------------------------------------------------------------
0: Paris is the capital city of France and the most populous city in the country. It is the second largest city in the European Union after London. Paris is also the seat of the French government
----------------------------------------------------------------------
3: Paris is the capital city of France and the most populous city in the country. It is the second largest city in the European Union after London. Paris is also the seat of the French government
----------------------------------------------------------------------

TorchDevice: cuda:0
  cur_mem: 0.0079 GB,  peak_mem: 4.4778 GB
TorchDevice: cpu
  cur_mem: 56.5031 GB,  peak_mem: 0.0000 GB
model size: 55.803 GB   cache size: 2.789 GB    hidden size (p): 0.029 GB
peak gpu mem: 4.478 GB  projected: False
prefill latency: 41.840 s       prefill throughput: 48.948 token/s
decode latency: 300.872 s       decode throughput: 0.412 token/s
total latency: 342.713 s        total throughput: 0.373 token/s

對一個只有 12GB 記憶體的 Pascal GPU 來說,平均 2.7 秒處理一個 Token 應該算不錯吧?至少他跑起來了 XD

我想這就是理論與現實之間的差距,當我們把過多的 IO 負擔丟向別的地方時,就會在其他地方產生更多成本出來,其中就包含時間成本。

Petals

看完實驗結果後,真正引起筆者興趣的反而是這個 Petals 套件,這個套件由 BigScience 所開發,使用 BitTorrent-Style 的 Offloading 框架。首先安裝此套件:

pip install petals

這個套件裝起來就滿快了,然後使用以下程式碼做測試:

from petals import AutoDistributedModelForCausalLM as ModelCls
from transformers import AutoTokenizer as TkCls
from transformers import TextStreamer

# 微調過的 Llama 2 70B
model_name = "petals-team/StableBeluga2"
model: ModelCls = ModelCls.from_pretrained(model_name)

tokenizer: TkCls = TkCls.from_pretrained(model_name)
streamer = TextStreamer(tokenizer)

prompt = "This is a long story, "
tokens = tokenizer(prompt, return_tensors="pt")
input_ids = tokens["input_ids"]

# 開始進行文本生成
outputs = model.generate(
    input_ids,
    max_new_tokens=32,
    streamer=streamer,
)

可以到此網頁確認哪些模型可用,看到這網頁就不難理解為何官方說他們是 "BitTorrent-Style" 的了。

雖然是跑 70B 的模型,但過程只需要下載其中一個 Shard 就能跑(約 1GB 左右),因此很快就能開始進入生成環節。接著就會看到程式努力的吐出生成 Tokens,約 1 秒多可以生成 1 個 Token。

其運作原理是把大家的 GPU 運算能力集合起來,把 Shard 切細一點,我們一人跑一點點,輪流接力做運算,過程中透過網路來傳輸中間層的運算結果。這真的是很有意思的想法,雖然很慢但至少能跑。其中也有 Falcon 180B 這種模型能跑,各位可以去玩看看。

結論

今天不知為何開始很熱血的在看論文,並跟大家分享了一下 FlexGen 的技術細節,看完這篇 Paper 後,筆者對整個 Offloading Framework 也有比較初步的瞭解與概念。可惜的是現在大公司都有幾萬張 A100 工作站,且小型研究單位也能充分利用量化技術的情況下,真的需要使用 Offloading 的情境實在太少見了。從 FlexGen 最後的 Commit 時間來看,這一塊在 LLM 領域裡面相對不活躍一些。

對於底層推論框架的介紹大致到這邊告一段落,接下來將會談論一些開發 LLM 相關應用時,會用到的一些技巧,例如 In-Context Learning, Retrieval Model 等等,明天見啦!

參考


上一篇
LLM Note Day 18 - Hugging Face Text Generation Inference
下一篇
LLM Note Day 20 - 上下文學習 In-Context Learning
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言