iT邦幫忙

2023 iThome 鐵人賽

DAY 16
2

簡介

ggmlggerganov 開發的一個機器學習框架,主打純 C 語言、輕量化且可以在 Apple 裝置上執行等功能。大概 2022 年底的時候,就常常看到 ggml 登上 GitHub Trending 趨勢榜。在 Meta 推出 LLaMA 之後,作者順勢製作了一個 llama.cpp 專案,其高速讀取模型、低硬體需求、極低位元量化等功能深深吸引各大 LLM 使用者,並在 GitHub 上迅速竄紅。今天就來介紹一下 ggml 與 llama.cpp 這兩份專案。

可愛貓貓 Day 16

(Powered By Microsoft Designer)

概覽

ggml

ggml 是個泛用型的機器學習框架,可以應用在很多模型上。從 ggml 的 GitHub 首頁可以看到除了 GPT-2, Llama 等純文字的語言模型以外,甚至連 CLIP, Whisper, Stable Diffusion 這些圖片語音相關的模型都有。缺點是 ggml 遇到新的模型架構時,必須自行實做沒看過的模組,不像 ONNX 遇到任何架構都能直接輸出一個統一格式的檔案。

llama.cpp

llama.cpp 是 ggml 作者實做 LLaMA 模型架構的專案,但因為 LLaMA 實在太紅了,這份專案甚至開始喧賓奪主,很多 ggml 相關的改動都是從 llama.cpp 流過來的。也因此,有很多 llama.cpp 有的功能或工具,在原生的 ggml 可能都還沒開始支援。

因為 llama.cpp 是針對特定模型架構運行的專案,因此並不是所有模型都能透過這個專案操作,例如 GPT, StarCoder 這些模型,但他們都有各自的 ggml 實做專案。接下來我們以 llama.cpp 為主,介紹其環境建置與相關工具的使用。

在此之前,建議可以先下載一個 GGUF 格式的 Llama 2 7B Chat 模型下來,等等編譯完可以直接做測試。如果只是想稍微試試可以下載 Q2_K 版本,而使用 Q8_0 版本的效果會比較好。如果想使用中文的話,可以考慮使用 Vicuna 或唐鳳上傳的 Taiwan Llama GGUF 版模型。

環境建置

llama.cpp 支援使用 cuBLAS 進行 GPU 運算,cuBLAS 是基於 CUDA 的一種函式庫。因此我們需要準備一個 CUDA 環境來讓 llama.cpp 可以操作 GPU。作者並沒有特別描述需要哪個 CUDA 版本以上,筆者是使用 CUDA 11.8 版。但無論如何,最重要的是編譯時的版本要與執行時的版本一樣,比較不容易出問題。除了 CUDA 以外還需要 CMake 編譯工具,以下示範使用 Conda 建立環境:

conda create -n ggml python=3.11
conda activate ggml
conda install -c "nvidia/label/cuda-11.8.0" cuda
pip install cmake

將 llama.cpp 複製下來:

git clone https://github.com/ggerganov/llama.cpp --depth 1
cd llama.cpp

我們使用 CMake 進行建置:

mkdir build && cd build
cmake .. -DLLAMA_CUBLAS=ON --fresh

使用 -DLLAMA_CUBLAS=ON 參數啟用 cuBLAS 函式庫,若只想使用 CPU 的話把這個選項移掉就好。筆者這邊加上 --fresh 來強制 CMake 重新建立編譯資訊。在 cmake 執行的過程,確保以下訊息有出現:

-- Found CUDAToolkit: /home/user/.miniconda3/envs/ggml/include (found version "11.8.89")
-- cuBLAS found
-- The CUDA compiler identification is NVIDIA 11.8.89

如果沒有出現這些訊息的話,可能要確認一下 CUDA 環境是否設置正確,否則建置出來的程式會不能使用 GPU 而跑得很慢。

最後進行完整建置:

cmake --build . --config Release -j
./bin/main --help  # 顯示參數說明
./bin/main -m /path/to/model.gguf  # 測試模型運算

建置過程約需一兩分鐘左右,視 CPU 效能而有所不同。完成建置後,相關的主程式與工具會放在 llama.cpp/build/bin 底下。

其實 llama.cpp 也支援在 Windows 或 Mac 建置,可以參考 GitHub 的說明進行。

格式轉換

目前 llama.cpp 可以執行的模型文件格式稱為 GGUF 格式,若要將 Hugging Face 格式的模型轉換成 GGUF 格式,需要使用 llama.cpp 提供的 convert.py 工具。首先安裝必要套件:

pip install numpy sentencepiece

按照以下指令轉換格式:

python convert.py /path/to/hf-model --outfile model.gguf

這樣就可以輕鬆做轉換了,而且速度相當快 👍

另外可以設定 --outtype 參數決定資料型態,這個階段支援 f32, f16q8_0,推薦使用 f16 比較平衡一點。

主程式 main

main 是最基本的用法,有相當豐富的參數可以使用。最基本的是 -m 指定模型路徑與 -ngl 指定 GPU 讀取層數,如果 GPU 記憶體足夠的話,建議設定 -ngl 100 比較能確定把所有權重都讀進 GPU 裡面:

./llama.cpp/build/bin/main -m llama-2-7b-chat.Q8_0.gguf -ngl 100

在沒有其他參數的情況下,程式會快速讀取完模型,然後自己開始瘋狂輸出,這通常是用來測試運作的速度有多快。調用 GPU 時,要確認前面讀取的時候有出現類似以下的訊息:

llm_load_tensors: offloading 32 repeating layers to GPU
llm_load_tensors: offloading non-repeating layers to GPU
llm_load_tensors: offloaded 35/35 layers to GPU
llm_load_tensors: VRAM used: 6695.83 MB

請確保是 35/35 而不是 30/35 之類的,否則就是代表有些 Layers 沒有放進 GPU,這些 Layers 會放到 CPU 裡面做 Offloading 運算,因此速度會大打折扣。

如果想要進入互動模式,可以加上 -ins 參數:

./llama.cpp/build/bin/main -m llama-2-7b-chat.Q8_0.gguf -ngl 100 -ins

用起來大概像這樣:

> hi
Hi there! How are you?

> good!
Great! I'm glad to hear that. Is there anything you want to chat about or ask?

這個參數其實是用在 Alpaca 類的 Prompt Template,但是在 Llama 2 上也可以使用。

一般來說會用 -n 指定最多生成多少 Tokens,並搭配 --ignore-eos 來評測模型或硬體的速度。透過 -c 參數可以指定模型的輸入長度,預設是 512,但目前的 llama 都支援到 2048 以上了。如果有相關應用會消耗到這麼大量的 Token 數的話,這個參數記得一定要開大,不然模型的輸出會看起來很奇怪。

在純 CPU 的使用情況下,參數 --threads 對速度的影響滿大的。

其他像是 --top-k, --top-p, --temp 等都是老面孔的 Sample Parameters 了,但 llama.cpp 還支援很多其他相當豐富的取樣參數,可以自行研究看看。

最近 llama.cpp 增加了紀錄檔案的功能,不過頻繁測試的時候會長很多檔案出來,可以加上 --log-disable 來取消此功能。

量化工具 quantize

使用 quantize 程式可以對模型進行量化,llama.cpp 支援的相當廣泛,從 8-Bit, 6-Bit 到 2-Bit 都有。使用方法如下:

./llama.cpp/build/bin/quantize model.fp16.gguf model.q2.gguf Q2_K

這是量化成 2-Bit 的指令,其他的量化格式請參考 ./quantize --help 的描述。根據筆者的經驗,8-Bit 和 6-Bit 幾乎沒有損失的感覺,到了 4-Bit 以下才會有明顯感受到不同,但 GPU 使用量下降的也很多。不過這是我個人的主觀感受,選擇使用多少位元的量化,還是要根據實際應用的情況為主,並確保有充分的測試。

伺服器 server

基本服務

server 是筆者相當喜歡的一個應用,他可以將模型變成一個 Service,並透過 HTTP API 來存取模型。參數與 main 大致相同,例如:

./llama.cpp/build/bin/server -m vicuna-7b-v1.5.Q8_0.gguf -ngl 100

預設會在 http://127.0.0.1:8080/ 開啟一個 LLM Service,也可以透過 --host--port 可以指定連接資訊,例如要公開在所有 IP 上可以用 --host 0.0.0.0 之類的。可以使用瀏覽器打開該網址,會有個網頁介面可以互動:

Web

這裡可以設定一些 Prompt Template 與 Sample Parameters 等參數,在下面輸入訊息後就可以開始聊天:

Chat

Completion Endpoint

把 LLM Service 跑起來之後,我們就可以把模型當成一個 API 來使用:

import json
import requests

url = "http://127.0.0.1:8080/completion"
params = {"prompt": "Hello, ", "n_predict": 16}
resp = requests.post(url, json=params)
content = json.loads(resp.text)["content"]
print(content)  # sweetie! How can I help you today?

若要透過 Streaming 的方式顯示輸出,則需要設定 stream 參數為 True,且 requests.post() 也要加上 stream=True 的參數,並使用 resp.iter_lines() 接收串流輸出:

url = "http://127.0.0.1:8080/completion"
prompt = """### USER: 什麼是語言模型?

### ASSISTANT: """

params = {"prompt": prompt, "stream": True}
resp = requests.post(url, json=params, stream=True)
for chunk in resp.iter_lines():
    if not chunk:
        continue
    # 會有固定的 "data:" 前綴,需要跳掉 5 個字元
    content = json.loads(chunk[5:])["content"]
    print(end=content, flush=True)

可以透過 stop 參數來輕鬆設定模型輸出的停止點,這部份比 HF Transformers 簡單多了:

params = {
    "prompt": "### USER: 什麼是語言模型?\n\n### ASSISTANT: ",
    "stream": True,
    "stop": ["###", "\n\n\n"],
}

此 Prompt Template 可以根據 "###" 是否出現來決定模型是否已經完成回覆。

Tokenizer Endpoints

llama.cpp server 也有提供 tokenize/detokenize 等功能,可以用來做 Truncation 之類的操作,例如:

url = "http://127.0.0.1:8080/tokenize"
params = {"content": "hello, llama.cpp!"}

resp = requests.post(url, json=params)
tokens = json.loads(resp.text)["tokens"]
print(tokens)  # [22172, 29892, 11148, 3304, 29889, 8223, 29991]

假設我們只要最後 3 個 Tokens 的話:

url = "http://127.0.0.1:8080/detokenize"
params = {"tokens": tokens[-3:]}

resp = requests.post(url, json=params)
content = json.loads(resp.text)["content"]
print(content)  # .cpp!

再拿這個結果去丟 Completion API 即可,以此完成簡單的 Truncation & Completion 組合操作。

Embedding Endpoint

開啟 server 時可以加上 --embedding 參數來啟用 Embedding API,例如:

./llama.cpp/build/bin/server \
    -m vicuna-7b-v1.5.Q8_0.gguf -ngl 100 --embedding

用法也是同樣單純:

url = "http://127.0.0.1:8080/embedding"
params = {"content": "Hello, llama.cpp"}

resp = requests.post(url, json=params)
embedding = json.loads(resp.text)["embedding"]
print(len(embedding))  # 4096

但這個 Embedding 功能用途筆者是感到相當困惑,過往要做 Retrieval 使用的 Embedding 通常是來自 Encoder LM,但 Decoder LM 的 Embedding 也會有一樣的效果嗎?於是筆者實驗了一下:

import json

import numpy as np
import requests
from sklearn.metrics.pairwise import cosine_similarity


def get_embedding(content):
    url = "http://127.0.0.1:8080/embedding"
    params = {"content": content}

    resp = requests.post(url, json=params)
    embedding = json.loads(resp.text)["embedding"]
    return embedding


value = [
    "Today is a good day",
    "今天天氣真好",
    "今日はとてもいい天気ですね",
    "I want to buy a movie ticket",
    "我想買電影票",
    "映画のチケットを購入したいのですが",
]

embs = [get_embedding(v) for v in value]
embs = np.array(embs)

res = cosine_similarity(embs, embs)
np.set_printoptions(precision=2)
print(res)

這裡將兩個語意不同的句子翻譯成不同語言,並將其 Embedding 拿來計算餘弦相似度,使用 llama-2-7b-chat.Q8_0.gguf 模型做測試的結果:

[[1.   0.23 0.22 0.63 0.04 0.27]
 [0.23 1.   0.56 0.16 0.6  0.5 ]
 [0.22 0.56 1.   0.15 0.48 0.58]
 [0.63 0.16 0.15 1.   0.2  0.26]
 [0.04 0.6  0.48 0.2  1.   0.49]
 [0.27 0.5  0.58 0.26 0.49 1.  ]]

同樣的輸入,使用 Universal Sentence Encoder 計算相似度:

import tensorflow_hub as hub
import numpy as np
import tensorflow_text

hub_url = "https://tfhub.dev/google/universal-sentence-encoder-multilingual/3"
embed = hub.load(hub_url)

embs = embed(value)
res = cosine_similarity(embs, embs)
print(res)

得到的結果:

[[ 1.    0.81  0.74  0.03  0.05 -0.02]
 [ 0.81  1.    0.83  0.01  0.09 -0.02]
 [ 0.74  0.83  1.    0.05  0.05 -0.  ]
 [ 0.03  0.01  0.05  1.    0.73  0.73]
 [ 0.05  0.09  0.05  0.73  1.    0.71]
 [-0.02 -0.02 -0.    0.73  0.71  1.  ]]

顯然效果是差滿多的!也許 Decoder LM 的 Embedding 還有其他用途,但如果是要做 Retrieval 的話還是用 Encoder LM 比較好 🤔

Python Binding

llama-cpp-python 是 llama.cpp 的 Python 介面,透過以下指令安裝 CUDA 版的套件:

CMAKE_ARGS="-DLLAMA_CUBLAS=on" pip install llama-cpp-python

以下是個簡單的 Streaming 範例:

from llama_cpp import Llama

llm = Llama(
    model_path="llama-2-7b-chat.Q8_0.gguf",
    n_gpu_layers=100,
    verbose=True,
)

output = llm(
    "### USER: Hello!\n\n### ASSISTANT:",
    max_tokens=256,
    stop=["###"],
    stream=True,
)

for token in output:
    print(end=token["choices"][0]["text"], flush=True)

若想要隱藏原本 llama.cpp 的訊息紀錄,將 Llama 初始化的 verbose 參數設定為 False 即可。用法原則上大同小異,詳細資訊可以參考官方文件

速度比較

我們來比較一下 HF Transformers 與 llama.cpp 的推論速度,在這邊附上筆者的硬體設備資訊:

  • CPU: 12th Gen Intel(R) Core(TM) i7-12700K
  • GPU: NVIDIA GeForce RTX 3090

測量方法是給一個空 Prompt 並固定生成 128 個 Tokens,然後計算平均每個 Token 需要花多少時間生成。

使用 HF Transformers 4.33.3 + BNB 0.41.1 測量的數據:

7B FP32-CPU   - 1.4 s
7B FP16       - 27 ms
7B BNB  8-Bit - 66 ms
7B BNB  4-Bit - 25 ms
7B GPTQ 4-Bit - 17 ms

因為 CPU 實在太慢了,所以我只測了 16 個 Tokens 而已。觀察數據可以發現,BNB 8-Bit 是真的滿慢的,而 BNB 4-Bit 與原本 FP16 差不多,但 GPTQ 就快很多了。

比較 llama.cpp 的 CPU 與 GPU 速度:

7B-Q8 GPU -  13 ms
7B-Q8 CPU - 218 ms

GPU 雖然海放 CPU,但是 CPU 這生成速度其實也是相當快了。

比較 llama.cpp 不同量化層級的速度:

7B-FP16 20 ms
7B-Q8_0 13 ms
7B-Q4_0  8 ms
7B-Q2_K 10 ms

有量化與 FP16 明顯有差,但有量化的版本彼此之間速度差距不大。

最後比較 llama.cpp 跑不同參數量模型的速度:

 7B-Q4_0  8 ms
13B-Q4_0 13 ms
34B-Q4_0 28 ms

小結:llama.cpp 在進行單筆推論的情況下,幾乎都比 HF Transformers 快的多。

雖然 llama.cpp 單筆推論的優勢很大,但 HF Transformers 是可以做批次推論的:

7B-FP16 Batch Size  1 - 27.75 ms
7B-FP16 Batch Size  2 - 13.49 ms
7B-FP16 Batch Size  4 -  6.85 ms
7B-FP16 Batch Size 72 -  1.23 ms
7B-FP16 Batch Size 73 -      OOM

若要同時進行多筆推論,HF Transformers 就贏過 llama.cpp 了。不過這是因為我們把生成長度縮的很小,所以 Batch Size 才能開到這麼大。

在 Transformer Decoder LM 裡面,理論上 GPU 記憶體的消耗會跟 Batch Size 呈現線性關係,但是會跟長度呈現平方關係。如果要們要處理長度 2048 的文本,以 7B 模型而言 Batch Size 最多只能開到 4 左右而已。然而多數的應用沒有個一兩千 Token 是很難用的,而且實際服務時,只能開個四線八線的,很容易就被灌爆了。

結論

今天簡單介紹了 ggml 框架與熱門的 llama.cpp 專案。llama.cpp 不僅在推論速度上有相當大的優勢,其格式轉換速度、權重讀取速度與各種操作介面也都是相當優秀的。然而 llama.cpp 最大的缺點在於,目前他不能進行 Batch Inference。因此當 LLM 真的要作為服務來實際應用時,這個缺點是個相當大的障礙。

最近 llama.cpp 其實有整合 Batch Inference 的機制了!但目前為止還沒有詳細的文件說明,筆者相當期待此功能完整釋出。

llama.cpp 裡面還提供了相當多程式可以用,例如 perplexity 可以用來計算模型混淆度,甚至能用來訓練模型的 train-text-from-scratchfinetune 等等,可以到 examples 資料夾底下探索所有工具與說明。

接下來介紹突破推論吞吐量瓶頸的關鍵技術 vLLM 以及他背後的 Paged Attention 技術,明天見!

參考


上一篇
LLM Note Day 15 - ONNX & ONNX Runtime
下一篇
LLM Note Day 17 - vLLM & Paged Attention
系列文
LLM 學習筆記33
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
codingbaby
iT邦新手 5 級 ‧ 2024-03-06 18:04:24

不好意思~想請問已成功建立好模型,但是目前還是只能用英文問答,要如何讓他擁有中文問答的能力呢?

Penut Chen iT邦研究生 5 級 ‧ 2024-03-06 20:09:22 檢舉

中英文回答通常與模型的能力較為相關,文章使用的 Llama2-7B 與 Vicuna-7B 其實都還是相對偏重英文一點

這裡推薦臺大的 Taiwan LLM 或 MTK 的 Breeze 模型,他們的中文能力相對好一點,可以參考唐鳳委員上傳的 GGUF 版本

audreyt/Taiwan-LLM-7B-v2.0.1-chat-GGUF | audreyt/Breeze-7B-Instruct-v0.1-GGUF

以 Taiwan-LLM-7B 為例,參考指令如下:

./bin/main -m Taiwan-LLM-7B-v2.0.1-chat-Q8_0.gguf -ngl 100 -p "USER: 什麼是語言模型? ASSISTANT: "

我這邊的模型輸出如下:

 USER: 什麼是語言模型? ASSISTANT: 語言模型是一種機器學習算法,用於分析文字的結構和內容。通常用於自然語言處理(NLP)任務,例如文本生成、語音識別或機器翻譯。

語言模型預測一段文字的下一個單詞或字串中可能出現的最有可能的單詞。通常基於前面一定數量的單詞集合來生成預測,例如 20 到 100 個單詞,因此被稱為“n”-gram模型。

語言模型的主要目標是在較長文字上預測下一個單詞或子序列的機率,通常使用貝葉斯定理計算。基於假設每個可能的單詞序列都有相同的預期出現次數,語言模型根據已知文字生成與給定文字最不相似的句子。

語言模型在 NLP 中有多種用途:

1. 語音識別:語音到文本 (STT) 演算法使用語言模型來預測下一個字符或子序列的可能性,以提高文本生成的準確性。
2. 機器翻譯:為了更好地理解源語言句子併產生目標語言的等效內容,機器翻譯系統使用語言模型。
3. 文本分類:語言模型可以根據上下文、主題和潛在意圖來區分不同的文字,幫助識別其類別或情緒。
4. 問答:語言模型可以用於根據上下文和使用者意圖提供詳細而有用的回答。
5. 自然語言生成:語言模型可以在多樣化的應用中用作生成式人工智能 (AI) 應用的基礎,例如對話管理、故事情節和廣告。
6. 文本分類:語言模型可以根據上下文、主題和潛在意圖來區分不同的文字,幫助識別其類別或情緒。
7. 輸入欄位:語言模型可以用於生成更自然、更人性化和更引人注目的輸入文字,從而提高使用者體驗並推動交付。
綜上所述,語言模型可以在多方面發揮重要作用。其中一些最常見的包括:自然語言處理、文字分類、問答系統和生成式人工智能。 [end of text]

原來如此~非常感謝您幫我解惑~~

我要留言

立即登入留言