構建 LLM 應用:構建 LLM 服務(第九部分)

作者:Vipra Singh

編譯:ronghuaiyang

導讀

LLM Serving 指的是部署和運行大型語言模型(LLMs)以處理用戶請求的過程。它涉及到將通常離線訓練的 LLM 設置爲實時響應查詢。

LLM Serving 指的是部署和運行大型語言模型(LLMs)以處理用戶請求的過程。它涉及到將通常離線訓練的 LLM 設置爲實時響應查詢。

以下是 LLM Serving 包含的主要方面:

市面上存在多種用於 LLM 服務的框架,各有其獨特優勢。讓我們詳細探討一下。

本地運行 LLM

像 PrivateGPT、llama.cpp、Ollama、GPT4All、llamafile 等項目的流行,凸顯了在本地設備上運行 LLM 的需求。

這樣做至少有兩大明顯益處:

  1. 隱私性:數據不會傳輸至第三方,也不受商業服務條款的約束。

  2. 成本:無需支付推理費用,這對那些依賴大量 tokens 的應用(如長期運行的模擬、摘要生成等)尤爲重要。

要在本地運行 LLM,需要滿足以下條件:

  1. 開源LLM:一個可自由修改和共享的開源 LLM。

  2. 推理能力:能夠在本地設備上運行此 LLM,同時保持可接受的延遲。

開源 LLM

用戶現在可以接觸到一套快速擴大的開源 LLM 集合。

這些 LLM 至少可以從兩個維度進行評估(參見圖表):

  1. 基礎模型:基礎模型是什麼,它是如何訓練的?

  2. 微調方法:基礎模型是否經過微調,如果微調過,使用了哪套指令集?

![img](Building LLM Applications Serving LLMs (Part 9).assets/0JTle7ssXyuN_3z6T.png)

這些模型的相對性能可以通過多個排行榜來評估,包括:

  1. LmSys

  2. GPT4All

  3. HuggingFace

一些框架已經出現,用於支持在不同設備上對開源 LLM 進行推理:

  1. llama.cpp:用 C++ 實現的 llama 推理代碼,帶有權重優化 / 量化功能。

  2. gpt4all:優化的 C 後端,用於推理。

  3. Ollama:將模型權重和環境打包成一個應用程序,在設備上運行並提供 LLM 服務。

  4. llamafile:將模型權重及運行模型所需的一切封裝在一個文件中,使我們能夠直接從該文件本地運行 LLM,無需額外的安裝步驟。

總的來說,這些框架通常會完成幾項工作:

  1. 量化:減少原始模型權重的內存佔用。

  2. 高效的推理實現:支持在消費級硬件(如 CPU 或筆記本 GPU)上進行推理。

精度降低後,存儲 LLM 所需的內存大幅度減少。

此外,我們還可以看到 GPU 內存帶寬的重要性表格!

得益於更大的 GPU 內存帶寬,Mac M2 Max 在推理速度上比 M1 快 5-6 倍。

下面我們將詳細討論這些內容。

高效加載 LLM

接下來,我們將探討如何通過幾種(量化)標準來加載本地的 LLM。由於分片、量化以及不同的保存和壓縮策略的存在,要弄清楚哪種方法適合我們並不容易。

在所有示例中,我們將使用 Zephyr 7B,這是 Mistral 7B 的一個微調變體,採用直接偏好優化(DPO)訓練而成。

🔥 小貼士:在加載 LLM 的每個示例之後,建議重啓筆記本以避免出現內存溢出錯誤。加載多個 LLM 需要大量的 RAM/VRAM。我們可以通過刪除模型並重置緩存來釋放內存,操作如下:

# Delete any models if previously created
del model, tokenizer, pipe
# Empty VRAM cache
import torch
torch.cuda.empty_cache()
  1. HuggingFace

加載 LLM 最直接、最基本的方式是通過 Transformers。HuggingFace 開發了一整套強大的包,讓我們能夠用 LLM 做許多驚人的事情!

我們首先從主分支安裝 HuggingFace 等包,以支持更新的模型:

# Latest HF transformers version for Mistral-like models
!pip install git+https://github.com/huggingface/transformers.git
!pip install accelerate bitsandbytes xformers

安裝完成後,我們可以使用以下的 pipeline 輕鬆加載 LLM:

from torch import bfloat16
from transformers import pipeline
# Load in the LLM without any compression tricks
pipe = pipeline(
    "text-generation", 
    model="HuggingFaceH4/zephyr-7b-beta", 
    torch_dtype=bfloat16, 
    device_map="auto"
)

這種加載 LLM 的方法通常不會執行任何壓縮技巧來節省 VRAM 或提高效率。

爲了生成我們的提示信息,我們首先需要創建必要的模板。幸運的是,如果聊天模板保存在底層的分詞器中,這可以自動完成:

# We use the tokenizer's chat template to format each message
# See https://huggingface.co/docs/transformers/main/en/chat_templating
messages = [
    {
        "role""system",
        "content""You are a friendly chatbot.",
    },
    {
        "role""user", 
        "content""Tell me a funny joke about Large Language Models."
    },
]
prompt = pipe.tokenizer.apply_chat_template(
    messages, 
    tokenize=False, 
    add_generation_prompt=True
)

使用內置的提示模板生成的提示信息,構建方式如下:

然後,我們可以開始將提示傳遞給 LLM 以生成答案:

outputs = pipe(
    prompt, 
    max_new_tokens=256, 
    do_sample=True, 
    temperature=0.1, 
    top_p=0.95
)
print(outputs[0]["generated_text"])

這會給我們如下輸出:

Why did the Large Language Model go to the party?

To network and expand its vocabulary!

這個笑點或許有點俗套,但 LLM 正是通過不斷擴充詞彙和與其他模型交流來提升其語言技能的。所以,這個笑話對於它們來說恰到好處!

在純粹的推理場景下,這種方法通常效率最低,因爲我們加載整個模型時沒有采取任何壓縮或量化策略。

然而,作爲起步方法,它非常出色,因爲它讓模型的加載和使用變得極爲便捷!

  1. LangChain

另一種在本地運行 LLM 的方式是使用 LangChain。LangChain 是一個用於構建 AI 應用的 Python 框架。它提供了抽象層和中間件,讓你能夠基於其支持的模型之一來開發 AI 應用。例如,下面的代碼向 microsoft/DialoGPT-medium 模型提出一個問題:

from langchain.llms.huggingface_pipeline import HuggingFacePipeline

hf = HuggingFacePipeline.from_model_id(
    model_id="microsoft/DialoGPT-medium"task="text-generation"pipeline_kwargs={"max_new_tokens": 200, "pad_token_id": 50256},
)
from langchain.prompts import PromptTemplate

template = """Question: {question}
Answer: Let's think step by step."""

prompt = PromptTemplate.from_template(template)

chain = prompt | hf

question = "What is electroencephalography?"

print(chain.invoke({"question": question}))

LangChain 的優點:

LangChain 的缺點:

  1. Llama.cpp

Llama.cpp 是一個基於 C 和 C++ 的 LLM 推理引擎,針對蘋果芯片進行了優化,並運行 Meta 的 Llama2 模型。

克隆倉庫並構建項目後,我們可以用以下命令運行一個模型:

$ ./main -m /path/to/model-file.gguf -p "Hi there!"

Llama.cpp 的優點:

Llama.cpp 的缺點:

  1. Llamafile

由 Mozilla 開發的 Llamafile 爲運行 LLM 提供了用戶友好的替代方案。Llamafile 以其便攜性和能夠創建單文件可執行程序而著稱。

下載 Llamafile 和任何 GGUF 格式的模型後,我們可以通過以下命令啓動本地瀏覽器會話

$ ./llamafile -m /path/to/model.gguf

Llamafile 的優點

Llamafile 的缺點

  1. Ollama

Ollama 是 Llama.cpp 和 Llamafile 的更加用戶友好的替代品。你下載一個可執行文件,它會在你的機器上安裝一項服務。安裝完畢後,你打開終端並運行:

1$ ollama run llama2

Ollama 會下載模型並開始一個交互式會話。

Ollama 的優點

Ollama 的缺點

  1. GPT4ALL

GPT4ALL 是一個易於使用的桌面應用,擁有直觀的圖形用戶界面。它支持本地模型運行,並提供通過 API 密鑰連接 OpenAI 的功能。它的一大亮點在於能夠處理本地文檔以提供上下文,確保了隱私性。

優點:

缺點:

  1. 分片

在深入探討量化策略之前,還有一個技巧我們可以用來減少加載模型所需的 VRAM。通過分片,我們實際上是將模型分割成較小的部分,即碎片

每個碎片包含模型的一部分,通過在不同設備之間分配模型權重,旨在繞過 GPU 內存限制。

還記得我之前說我們沒有進行任何壓縮技巧嗎?

那不是完全正確……

我們加載的模型,Zephyr-7B-β,實際上已經被分片了!如果我們前往模型頁面並點擊 “文件和版本” 鏈接,我們會發現模型已經被分割成了八塊。

雖然我們可以自己對模型進行分片,但通常建議尋找已經量化的模型,或者自己進行量化。

使用 Accelerate 包進行分片是非常直接的:

from accelerate import Accelerator
# Shard our model into pieces of 1GB
accelerator = Accelerator()
accelerator.save_model(
    model=pipe.model, 
    save_directory="/content/model", 
    max_shard_size="4GB"
)

就這樣!因爲我們把模型分片成 4GB 而不是 2GB 的大小,所以我們創建了更少的加載文件:

  1. 使用 Bitsandbytes 進行量化

LLM 由一系列權重和激活值構成。這些值通常以常見的 32 位浮點數(float32)數據類型表示。

位數的多少決定了它可以表示的數值範圍。Float32 能夠表示介於 1.18e-38 和 3.4e38 之間的數值,這是一個相當廣泛的範圍!位數越少,所能表示的數值就越有限。

正如我們所預期的那樣,如果我們選擇更低的位數,那麼模型的精確度會下降,但它需要表示的數值也會減少,從而降低了模型的大小和內存需求。

量化是指將 LLM 從原來的 Float32 表示形式轉換爲更小的數據類型。然而,我們並不只是想要使用更小的位數變體,而是希望將大位數的表示映射到小位數而不丟失太多信息。

實踐中,我們經常看到使用一種名爲 **4 位正則浮點(NF4)**的新格式來實現這一點。這種數據類型通過幾個特殊技巧高效地表示更大位數的數據類型。它包括三個步驟:

  1. 歸一化:模型的權重被歸一化,以便我們期望權重落在某個範圍內。這允許更有效地表示更常見的值。

  2. 量化:權重被量化爲 4 位。在 NF4 中,量化級別相對於歸一化的權重均勻間隔,從而有效地表示原始的 32 位權重。

  3. 反量化:儘管權重以 4 位存儲,但在計算期間會進行反量化,這在推理過程中提供了性能提升。

要使用 HuggingFace 執行此量化,我們需要使用 Bitsandbytes 定義量化配置:

from transformers import BitsAndBytesConfig
from torch import bfloat16
# Our 4-bit configuration to load the LLM with less GPU memory
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,  # 4-bit quantization
    bnb_4bit_quant_type='nf4',  # Normalized float 4
    bnb_4bit_use_double_quant=True,  # Second quantization after the first
    bnb_4bit_compute_dtype=bfloat16  # Computation type
)

此配置使我們能夠指定要採用的量化級別。通常,我們希望使用 4 位量化來表示權重,但在 16 位下進行推理。

在 pipeline 中加載模型就變得很簡單:

from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
# Zephyr with BitsAndBytes Configuration
tokenizer = AutoTokenizer.from_pretrained("HuggingFaceH4/zephyr-7b-alpha")
model = AutoModelForCausalLM.from_pretrained(
    "HuggingFaceH4/zephyr-7b-alpha",
    quantization_config=bnb_config,
    device_map='auto',
)
# Create a pipeline
pipe = pipeline(model=model, tokenizer=tokenizer, task='text-generation')

接下來,我們可以使用與之前相同的提示:

# We will use the same prompt as we did originally
outputs = pipe(
    prompt, 
    max_new_tokens=256, 
    do_sample=True, 
    temperature=0.7, 
    top_p=0.95
)
print(outputs[0]["generated_text"])

這將給我們以下輸出:

Why did the Large Language Model go to the party?

量化是一種強大技術,能夠在保持相似性能的同時減少模型的內存需求。它使得即使在較小的 GPU 上也能更快地加載、使用和微調 LLM。

  1. 預量化(GPTQ vs. AWQ vs. GGUF)

到目前爲止,我們已經探討了分片和量化技術。儘管這些是值得掌握的有用技巧,但每次加載模型時都必須應用它們似乎有些浪費。

相反,這些模型往往已經被提前爲我們分片和量化。特別是 HuggingFace 上的用戶 TheBloke,爲我們執行了一系列的量化工作,可供我們使用。

撰寫本文時,他已經上傳了超過 2000 個量化的模型供我們使用!

這些量化的模型形態各異、大小不一。其中最常用的格式是 GPTQ、GGUF 和 AWQ,主要用於進行 4 位量化。

GPTQ:針對 GPT 模型的後訓練量化

GPTQ 是一種針對 4 位量化設計的後訓練量化(PTQ)方法,主要關注 GPU 推理和性能。

該方法的核心思想是嘗試通過最小化權重的均方誤差將其壓縮至 4 位量化。在推理過程中,它會動態地將權重反量化至 float16,以提高性能同時保持較低的內存佔用。

首先,我們需要安裝一些必要的包,以便在 HuggingFace Transformers 中加載類似 GPTQ 的模型:

pip install optimum
pip install auto-gptq --extra-index-url https://huggingface.github.io/autogptq-index/whl/cu118/

完成上述步驟後,我們可以導航到想要加載的模型,即 TheBloke/zephyr-7B-beta-GPTQ,並選擇一個特定的版本。

這些版本基本上指示了量化方法、壓縮程度、模型大小等信息。

目前,我們堅持使用 “main” 分支,因爲它通常在壓縮和準確性之間取得了不錯的平衡:

from transformers import AutoModelForCausalLM, AutoTokenizer, pipeline
# Load LLM and Tokenizer
model_id = "TheBloke/zephyr-7B-beta-GPTQ"
tokenizer = AutoTokenizer.from_pretrained(model_id, use_fast=True)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto",
    trust_remote_code=False,
    revision="main"
)
# Create a pipeline
pipe = pipeline(model=model, tokenizer=tokenizer, task='text-generation')

儘管我們安裝了一些額外的依賴項,但我們仍然可以使用之前相同的 pipeline,這是使用 GPTQ 的一大好處。

加載模型後,我們可以如下運行一個提示:

# We will use the same prompt as we did originally
outputs = pipe(
    prompt,
    max_new_tokens=256,
    do_sample=True,
    temperature=0.1,
    top_p=0.95
)
print(outputs[0]["generated_text"])

這給我們帶來了以下生成的文本:

Why did the Large Language Model go to the party?

To show off its wit and charm, of course!

But unfortunately, it got lost in the crowd and couldn’t find its way back to its owner. The partygoers were impressed by its ability to blend in so seamlessly with the crowd, but the Large Language Model was just confused and wanted to go home. In the end, it was found by a group of humans who recognized its unique style and brought it back to its rightful place. From then on, the Large Language Model made sure to wear a name tag at all parties, just to be safe.

GPTQ 是最常用的壓縮方法,因爲它針對 GPU 使用進行了優化。從 GPTQ 開始,如果 GPU 無法處理如此大的模型,再轉向專注於 CPU 的方法,比如 GGUF,是值得的。

GGUF:GPT 生成統一格式

儘管 GPTQ 在壓縮方面做得很好,但如果我們的硬件受限,其對 GPU 的關注可能成爲一個劣勢。

GGUF,前身爲 GGML,是一種量化方法,允許用戶使用 CPU 運行 LLM,同時也可將某些層卸載到 GPU 上以加速。

儘管使用 CPU 進行推理通常比使用 GPU 慢,但對於在 CPU 或蘋果設備上運行模型的人來說,這是一種令人驚歎的格式。特別是隨着更小、更強大的模型如 Mistral 7B 的出現,GGUF 格式可能會持續存在!

使用 GGUF 相對直接,首先需要安裝 ctransformers 包:

1pip install ctransformers[cuda]

完成安裝後,我們可以導航到想要加載的模型,即 TheBloke/zephyr-7B-beta-GGUF,並選擇一個特定的文件。

就像 GPTQ 一樣,這些文件標明瞭量化方法、壓縮水平、模型大小等信息。

我們使用 “zephyr-7b-beta.Q4_K_M.gguf”,因爲我們專注於 4 位量化:

from ctransformers import AutoModelForCausalLM
from transformers import AutoTokenizer, pipeline
# Load LLM and Tokenizer
# Use `gpu_layers` to specify how many layers will be offloaded to the GPU.
model = AutoModelForCausalLM.from_pretrained(
    "TheBloke/zephyr-7B-beta-GGUF",
    model_file="zephyr-7b-beta.Q4_K_M.gguf",
    model_type="mistral"gpu_layers=50, hf=True
)
tokenizer = AutoTokenizer.from_pretrained(
    "HuggingFaceH4/zephyr-7b-beta"use_fast=True
)
# Create a pipeline
pipe = pipeline(model=model, tokenizer=tokenizer, task='text-generation')

加載模型後,我們可以如下運行一個提示:

# We will use the same prompt as we did originally
outputs = pipe(prompt, max_new_tokens=256)
print(outputs[0]["generated_text"])

這將給我們以下輸出:

Why did the Large Language Model go to the party? To impress everyone with its vocabulary! But unfortunately, it kept repeating the same jokes over and over again, making everyone groan and roll their eyes. The partygoers soon realized that the Large Language Model was more of a party pooper than a party animal. Moral of the story: Just because a Large Language Model can generate a lot of words, doesn’t mean it knows how to be funny or entertaining. Sometimes, less is more!

如果你在 GPU 資源緊張的情況下,想要同時利用 CPU 和 GPU,GGUF 是一個出色的選擇,尤其在沒有最新高性能 GPU 的情況下。

AWQ:激活感知權重量化

近期出現的一個新格式是 AWQ(激活感知權重量化),這是一種類似於 GPTQ 的量化方法。AWQ 和 GPTQ 作爲方法之間有幾個區別,但最重要的一點是 AWQ 認爲並非所有的權重對於 LLM 的性能同等重要。

換句話說,在量化過程中會跳過一小部分權重,這有助於減少量化帶來的損失。

因此,他們的論文提到相比 GPTQ 有着顯著的速度提升,同時保持了類似的,有時甚至是更好的性能。

該方法仍相對較新,尚未像 GPTQ 和 GGUF 那樣廣泛採用,因此有趣的是看這些方法是否能夠共存。

對於 AWQ,我們將使用 vLLM 包,因爲在我的經驗中,這是使用 AWQ 阻力最小的途徑:

pip install vllm

使用 vLLM,加載和使用我們的模型變得毫無痛苦:

from vllm import LLM, SamplingParams
# Load the LLM
sampling_params = SamplingParams(temperature=0.0, top_p=1.0, max_tokens=256)
llm = LLM(
    model="TheBloke/zephyr-7B-beta-AWQ", 
    quantization='awq', 
    dtype='half', 
    gpu_memory_utilization=.95, 
    max_model_len=4096
)

然後,我們可以使用.generate輕鬆運行模型:

# Generate output based on the input prompt and sampling parameters
output = llm.generate(prompt, sampling_params)
print(output[0].outputs[0].text)

給我們的輸出:

Why did the Large Language Model go to the party? To network and expand its vocabulary! Why did the Large Language Model blush? Because it overheard another model saying it was a little too wordy! Why did the Large Language Model get kicked out of the library? It was being too loud and kept interrupting other models’ conversations with its endless chatter! …

儘管它是一種新格式,AWQ 因其速度和壓縮質量而迅速獲得人氣!

推理優化

堆疊 transformer 層以構建大規模模型能夠帶來更高的準確性、在少樣本學習能力,甚至在廣泛的語言任務上展現出接近人類的新興能力。這些基礎模型訓練成本高昂,在推理過程中(一項持續發生的費用)也極其耗費內存和計算資源。當今最流行的大型語言模型(LLMs)參數規模可達數十億乃至數百億,具體取決於應用場景,可能需要處理長輸入(或上下文),這同樣會增加開銷。例如,增強檢索生成(RAG)管道需要向模型輸入大量信息,這大大增加了 LLM 所需處理的工作量。

本文討論了 LLM 推理中最緊迫的挑戰,以及一些實用的解決方案。讀者應該對 Transformer 架構和一般的注意力機制有基本瞭解。理解 LLM 推理的複雜性至關重要,我們將在下一節中對此進行探討。

理解 LLM 推理

大多數流行的僅解碼器型 LLM(如 GPT-3)都是在因果建模目標上進行預訓練的,本質上是下一個詞預測器。這些 LLM 接受一系列的 token 作爲輸入,自迴歸地生成後續的 token,直到滿足停止準則(例如,生成 token 的數量上限或停止詞列表)或生成一個特殊的<end>標記,表示生成的結束。這個過程包含兩個階段:預填充階段和解碼階段。

請注意,token 是模型處理的語言的基本組成部分。一個 token 大約相當於四個英文字符。所有自然語言輸入在輸入模型之前都會轉換成 token。

預填充階段或處理輸入

在預填充階段,LLM 處理輸入 token 以計算中間狀態(鍵和值),這些狀態用於生成第一個新的 token。每個新的 token 都依賴於所有前面的 token,但由於整個輸入的範圍是已知的,從高層次上看,這是一個高度並行化的矩陣 - 矩陣運算。它有效地使 GPU 利用率飽和。

解碼階段或生成輸出

在解碼階段,LLM 一次生成一個輸出 token,直到滿足停止準則爲止。每個連續的輸出 token 都需要知道所有前一迭代的輸出狀態(鍵和值)。這類似於矩陣 - 向量運算,相比於預填充階段,它對 GPU 計算能力的利用不足。數據(權重、鍵、值、激活)從內存傳輸到 GPU 的速度主導了延遲,而不是計算實際發生的速度。換句話說,這是一個受內存限制的操作。

本文中提到的許多推理挑戰及其相應的解決方案都集中在優化這個解碼階段:高效的注意力模塊、有效管理 key 和 value 等。

不同的 LLM 可能使用不同的分詞器,因此,比較它們之間的輸出 token 可能並不直接。在比較推理吞吐量時,即使兩個 LLM 每秒輸出的 token 數相似,如果它們使用不同的分詞器,也可能不等價。這是因爲對應的 token 可能代表不同數量的字符。

請求批處理

LLM 服務的一個重要方面是批處理用戶請求。與其爲每個新請求重新加載參數,一種高效的方法是一次將參數加載到 GPU 上,並儘可能一次性處理儘可能多的輸入序列。這種方法不僅能提高服務器吞吐量和優化計算資源利用,還能顯著提高成本效益。但是,採取簡單的策略,如等待固定數量的用戶請求累積後再處理批次,會帶來挑戰。這意味着在批次內,每個請求生成序列結束標記的時間各不相同。因此,批次計算速度受最長生成時間的限制,導致用戶不希望的等待時間(延遲)。序列間完成時間的差異會導致 GPU 利用率下降,削弱了批處理預期帶來的效率提升。

由於我們所討論的所有挑戰,連續批處理被提出以解決這些問題。

連續批處理

連續批處理是一種專門爲 LLM 設計的批調度類型。與動態批處理相比,後者根據配置的時間閾值和最大批大小動態確定批大小,而連續批處理則允許新請求在下一個解碼週期加入當前批次,而不是等待當前批次結束。由於 LLM 的自迴歸生成過程,這種方法對 LLM 來說易於實施,並且極大地提高了模型的吞吐量。

連續批處理在動態批處理請求方面表現出色。然而,我們還面臨着另一個問題:內存限制。設想一下聊天機器人的情景——一個用戶可能只用一句話提問,而另一個用戶可能向我們的應用發送一段長篇大論——我們無法預知輸入(和輸出)序列的長度。這種不確定性引出了內存消耗的關鍵問題。在不知道序列確切的內存需求情況下,人們被迫採取最壞情況的假設,爲整個批次預留儘可能多的內存。

問題在於:GPU 的內存是有限的,需要空間用於

  1. 模型參數

  2. 用戶請求計算(KV 緩存)

  3. 整個批次的計算。

如果不進行優化,這些會佔用大量空間,迫使我們縮小批處理大小,不幸的是,這也降低了吞吐量。但我們追求高吞吐量。我們如何優化這一點?關鍵是內存。

讓我們從內存的角度更深入地瞭解一下解碼過程中發生了什麼。LLM 的生成過程始於處理輸入序列,並以自迴歸的方式逐個生成下一個 token(見下圖)。這個生成過程包括自注意力計算,需要計算迄今爲止處理過的每個 token 的所有鍵值(KV)得分。舉例來說,爲了生成第 t 個 token,我們需要從第 t-1,t-2,...,1 個 token 計算出的鍵和值。‍

爲了優化重複計算,引入了 KV 緩存的概念。該方法旨在存儲解碼器中先前計算的 K 和 V 張量,隨後在後續迭代中重用它們。然而,這種優化策略是以增加內存空間爲代價的,當爲了提高吞吐量而增大批處理大小時,這一點尤爲關鍵。由於序列長度的不可預測性,傳統注意力機制導致了顯著的內存浪費,範圍從 60% 到 80%,這是由於碎片化和過度分配造成的。

PagedAttention:以內存爲中心的解決方案

爲了解決這一挑戰,提出了 PagedAttention。借鑑傳統操作系統(OS)管理內存碎片和共享的策略,PagedAttention 採用了帶有分頁的虛擬內存方法。它允許鍵和值向量存儲在非連續的內存空間中。這使得鍵和值向量可以駐留在非連續的內存空間中,被組織成塊。每個塊容納固定數量 token 的注意力鍵和值。在執行計算時,PagedAttention 內核能夠高效地識別和獲取這些塊。

鍵值緩存

解碼階段的一個常見優化是 KV 緩存。解碼階段每次生成一個 token,但每個 token 都依賴於所有先前 token 的鍵和值張量(包括預填充時計算的輸入 token 的 KV 張量,以及直到當前時間步長計算出的所有新 KV 張量)。

爲了避免在每個時間步驟重新計算所有這些張量,可以在 GPU 內存中緩存它們。每次迭代,當計算出新元素時,它們會被簡單地添加到正在運行的緩存中,供下一次迭代使用。在某些實現中,模型的每一層都有一個 KV 緩存。

LLM 內存需求

實際上,GPU 上 LLM 的主要內存需求來源於模型權重和 KV 緩存。

在批處理中,批中每個請求的 KV 緩存仍然需要單獨分配,可能會佔用大量內存。下面的公式概述了適用於當今大多數常見 LLM 架構的 KV 緩存大小。

每個 token 的 KV 緩存大小(字節)= 2 * (層數) * (頭數 * 頭維度) * 精度大小(字節)

第一個因子 2 是考慮到 K 和 V 矩陣。通常,(頭數 * 頭維度) 的值與變換器的隱藏尺寸(或模型維度,d_model)相同。這些模型屬性通常可以在模型卡片或相關配置文件中找到。

此內存大小對於輸入序列中的每個 token 都是必需的,貫穿整個輸入批次。假設半精度,KV 緩存的總大小由以下公式給出。

KV 緩存總大小(字節)= (批大小) * (序列長度) * 2 * (層數) * (隱藏尺寸) * sizeof(FP16)

例如,使用 16 位精度的 Llama 2 7B 模型和批大小爲 1,KV 緩存的大小將是 1 * 4096 * 2 * 32 * 4096 * 2 字節,大約等於 2GB。

高效管理這個 KV 緩存是一項艱鉅的任務。隨着批大小和序列長度線性增長,內存需求可以迅速擴大。因此,它限制了可提供的吞吐量,並對長上下文輸入帶來了挑戰。這就是本篇文章中提到的多種優化措施的動機所在。

通過模型並行化擴展 LLM

減少模型權重在單個設備上的內存佔用的一種方法是將模型分佈在多個 GPU 上。分散內存和計算佔用使得運行更大模型或更大批次的輸入成爲可能。模型並行化對於處理需要比單一設備可用內存更多的模型進行訓練或推理是必要的,同時使訓練時間和推理指標(延遲或吞吐量)適合特定的應用場景。根據模型權重的拆分方式,有幾種並行化模型的方法。

值得注意的是,數據並行性也是一種經常與下面列出的其他方法一起提及的技術。在這種情況下,模型的權重被複制到多個設備上,全局輸入批大小在每個設備上被劃分爲微批次。它通過處理更大的批次來減少總體執行時間。然而,這是一種訓練時間優化,在推理期間的相關性較低。

管道並行性

管道並行性涉及將模型(垂直地)分成片段,其中每個片段包含在單獨設備上執行的一組層。圖 2a 展示了四路管道並行性,其中模型被順序分割,所有層的四分之一子集在每個設備上執行。一組操作在一個設備上的輸出被傳遞給下一個設備,繼續執行後續的片段。Fn & Bn 分別表示設備 n 上的前向和後向傳遞。存儲模型權重所需內存在每個設備上被有效四分之一。

這種方法的主要限制是,由於處理的順序性質,一些設備或層在等待前一層的輸出(激活、梯度)時可能保持空閒。這在前向和後向傳遞中都導致了效率低下或 “管道氣泡”。在圖 2b 中,空白的空曠區域是大的管道氣泡,設備處於空閒和未充分利用狀態。

微批處理可以在一定程度上緩解這個問題,如圖 2c 所示。全局輸入批大小被分成子批,依次處理,最後累積梯度。請注意,Fn,m & Bn,m 分別表示設備 n 上的微批 m 的前向和後向傳遞。

這種方法縮小了管道氣泡的大小,但它並沒有完全消除它們。

張量並行性

張量並行性涉及將模型的各個層(水平地)分割成更小、獨立的計算塊,這些塊可以在不同的設備上執行。注意力塊和多層感知機(MLP)層是 Transformer 的主要組件,可以利用張量並行性。在多頭注意力塊中,每個頭或頭的組可以被分配到不同的設備上,以便它們可以獨立並行地計算。

上圖 a 展示了一個兩路張量並行的例子,應用於兩層 MLP 上,每一層用一個圓角框表示。在第一層中,權重矩陣 A 被分割成 A1A2。計算 XA1XA2 可以在同一批次(f 是一個恆等操作)的輸入 X 上,在兩個不同的設備上獨立執行。這有效地將每個設備上存儲權重的內存需求減半。一個約簡操作 g 在第二層中組合輸出。

上圖 b 是一個在自注意力層中兩路張量並行的例子。多個注意力頭本質上是並行的,可以在設備之間分割。

序列並行性

張量並行性有其侷限性,因爲它要求層被分割成獨立、可管理的塊。它不適用於像 LayerNorm 和 Dropout 這樣的操作,這些操作反而在整個張量並行組中複製。儘管 LayerNorm 和 Dropout 計算成本低廉,但它們確實需要相當多的內存來存儲(冗餘的)激活值。

如在大型 Transformer 模型中減少激活重計算中所示,這些操作在輸入序列上是獨立的,這些操作可以沿着所謂的 “序列維度” 進行分割,使其更加內存高效。這被稱爲序列並行性。

模型並行性的技術並非相互排斥,可以結合使用。它們有助於擴大規模並降低 LLM 在每個 GPU 上的內存佔用,但也存在專門針對注意力模塊的優化技術。

優化注意力機制

縮放點積注意力(SDPA)運算將查詢和鍵值對映射到輸出。

多頭注意力

作爲 SDPA 的增強,通過不同的、學習得到的 Q、K 和 V 矩陣投影,並行執行多次注意力層,使模型能夠在不同位置同時關注來自不同表示子空間的信息。這些子空間獨立學習,爲模型提供了對輸入中不同位置更豐富的理解。

如下圖所示,多個並行注意力操作的輸出被串聯起來,並通過線性投影進行組合。每個並行注意力層稱爲一個‘頭’,這種方法被稱爲多頭注意力(MHA)。

在原始工作中,每個注意力頭都在模型的降維版本上操作(例如)

當使用八個並行注意力頭時。這樣保持了計算成本與單頭注意力相似。

多查詢注意力

多頭注意力(MHA)的一種推理優化,被稱爲多查詢注意力(MQA),如快速 Transformer 解碼中提出的,它在多個注意力頭之間共享鍵和值。查詢向量仍然像以前一樣被多次投影。

雖然 MQA 中完成的計算量與 MHA 相同,但從內存中讀取的數據量(鍵、值)只是之前的一部分。當受到內存帶寬限制時,這使得計算利用率更高。它還減少了內存中 KV 緩存的大小,爲更大的批處理大小騰出了空間。

鍵值頭的減少伴隨着潛在的準確性下降。此外,需要在推理時利用這種優化的模型需要在啓用 MQA 的情況下進行訓練(或至少微調大約 5% 的訓練量)。

分組查詢注意力

分組查詢注意力(GQA)通過將鍵和值投影到少量的查詢頭組中(下圖),在 MHA 和 MQA 之間找到了平衡。在每組內部,它表現得像是多查詢注意力。

下圖顯示,多頭注意力有多個鍵值頭(左)。分組查詢注意力(中心)的鍵值頭數量多於一個,但少於查詢頭的數量,這是在內存需求和模型質量之間的平衡。多查詢注意力(右)有一個鍵值頭,以幫助節省內存。

最初使用 MHA 訓練的模型,可以使用 GQA 進行 “升級訓練”,只需要一小部分原始訓練計算。它們在保持接近 MQA 的計算效率的同時,達到了接近 MHA 的質量。Llama 2 70B 就是一個利用 GQA 的模型示例。

像 MQA 和 GQA 這樣的優化通過減少存儲的鍵和值頭的數量,幫助減少了 KV 緩存所需的內存。然而,KV 緩存的管理方式可能仍然存在效率問題。與優化注意力模塊本身不同,下一節介紹了一種更高效的 KV 緩存管理技術。

Flash attention

優化注意力機制的另一種方法是修改某些計算的順序,以更好地利用 GPU 的內存層次結構。神經網絡通常用層來描述,大多數實現也是按照這種方式佈局的,每次只對輸入數據執行一種類型的計算。這並不總是帶來最優性能,因爲對已經進入內存層次結構中更高、更高效級別的值進行更多計算可能是有益的。

在實際計算中融合多層可以最小化 GPU 需要從其內存讀取和寫入的次數,並將需要相同數據的計算組合在一起,即使它們屬於神經網絡中不同層的部分。

一個非常流行的融合是 Flash Attention,這是一個 I/O 感知的精確注意力算法,詳細情況見於 FlashAttention: Fast and Memory-Efficient Exact Attention with IO-Awareness。精確注意力意味着它在數學上與標準多頭注意力(有適用於多查詢和分組查詢注意力的變體)完全相同,因此可以無修改地替換到現有模型架構中,甚至是已經訓練好的模型中。

I/O 感知意味着它在融合操作時考慮了一些之前討論過的內存移動成本。具體來說,Flash Attention 使用 “分塊” 一次性完全計算和寫出最終矩陣的小部分,而不是分步對整個矩陣進行部分計算,中間寫出中間值。

下圖展示了 40GB GPU 上的分塊 Flash Attention 計算模式和內存層次結構。右側的圖表顯示了通過融合和重新排序注意力機制的不同組成部分所帶來的相對加速效果。

通過分頁高效管理 KV 緩存

有時,爲了應對最大可能的輸入(即支持的序列長度),KV 緩存會靜態地 “過度預分配”,因爲輸入的大小是不可預測的。例如,如果模型支持的最大序列長度是 2,048,那麼無論請求中的輸入和產生的輸出大小如何,都會在內存中預留 2,048 大小的空間。這個空間可能會連續分配,而且,很多時候,其中的大部分空間並未被使用,導致內存浪費或碎片化。這個預留的空間在整個請求的生命週期內都會被佔用。

受操作系統中分頁的啓發,PagedAttention 算法允許將連續的鍵和值存儲在內存中不連續的空間裏。它將每個請求的 KV 緩存劃分爲代表固定數量 token 的塊,這些塊可以不連續地存儲。

在注意力計算過程中,根據需要通過一個記錄塊位置的表格來獲取這些塊。隨着新 token 的生成,會分配新的塊。這些塊的大小是固定的,消除了因不同請求需要不同大小的分配而產生的效率低下問題。這極大地減少了內存浪費,使得可以使用更大的批處理大小(進而提高了吞吐量)。

模型優化技術

到目前爲止,我們討論了 LLM 如何消耗內存、如何將內存分佈在多個 GPU 上的一些方法,以及優化注意力機制和 KV 緩存的方法。還有一些模型優化技術,通過修改模型權重本身來減少每個 GPU 上的內存使用。GPU 還具有專門的硬件來加速這些修改後的值上的操作,爲模型提供更多的加速。

量化

量化是減少模型權重和激活值精度的過程。大多數模型使用 32 位或 16 位的精度進行訓練,其中每個參數和激活元素佔據 32 位或 16 位的內存——一個單精度浮點數。然而,大多數深度學習模型可以用每個值 8 位甚至更少的位數有效表示。

下圖展示了量化前後的值分佈。在這種情況下,一些精度由於舍入而丟失,一些動態範圍由於裁剪而丟失,這使得值可以以更小的格式表示。

降低模型的精度可以帶來諸多好處。如果模型在內存中佔用的空間更少,你就可以在相同的硬件上容納更大的模型。量化也意味着你可以通過相同的帶寬傳輸更多的參數,這對於帶寬受限的模型加速尤其有幫助。

對於 LLM,有許多不同的量化技術,涉及降低激活值、權重或兩者的精度。量化權重更爲直接,因爲它們在訓練後是固定的。然而,這可能會留下一些性能未發揮,因爲激活值仍然保持在較高的精度。GPU 沒有專門的硬件來乘以 INT8 和 FP16 數字,所以權重必須轉換回更高的精度來進行實際操作。

也可以量化激活值,即變壓器塊和網絡層的輸入,但這帶來了自身的挑戰。激活向量通常包含異常值,實質上增加了它們的動態範圍,使得以比權重更低的精度表示這些值變得更加困難。

一種選擇是通過將代表性數據集傳遞給模型來找出這些異常值可能出現的位置,並選擇以比其他激活值更高的精度表示某些激活值(LLM.int8())。另一種選擇是從容易量化的權重中借用動態範圍,並在激活值中重用該範圍。

稀疏性

與量化類似,研究顯示許多深度學習模型對修剪具有魯棒性,即可以將某些接近 0 的值替換爲 0 本身。稀疏矩陣是其中許多元素爲 0 的矩陣。這些矩陣可以以比完整密集矩陣佔用空間更少的壓縮形式表示。

特別是 GPU 具有對某種結構性稀疏的硬件加速功能,其中每四個值中有兩個被零表示。稀疏表示還可以與量化結合,以實現執行速度的進一步提升。尋找以稀疏格式表示大型語言模型的最佳方式仍然是一個活躍的研究領域,爲未來推斷速度的改進提供了有前景的方向。

蒸餾

縮小模型規模的另一種方法是通過一個稱爲蒸餾的過程,將模型的知識轉移到較小的模型上。這一過程涉及訓練一個較小的模型(稱爲學生)來模仿較大模型(教師)的行爲。

成功的蒸餾模型例子包括 DistilBERT,它將 BERT 模型壓縮了 40%,同時以 60% 更快的速度保留了 97% 的語言理解能力。

儘管在 LLM 中蒸餾是一個活躍的研究領域,但神經網絡中的通用方法最早在 Distilling the Knowledge in a Neural Network 中進行了描述:

下圖展示了一個知識蒸餾的一般框架。教師的 logits 是學生使用蒸餾損失進行優化的軟目標。其他蒸餾方法可能使用其他損失度量從教師中 “蒸餾” 知識。

蒸餾的一種替代方法是使用由教師模型合成的數據來監督訓練學生 LLM,當人類註釋稀缺或不可獲得時,這種方法特別有用。Distilling Step by Step!:https://arxiv.org/abs/2305.02301 更進一步,除了作爲事實依據的標籤外,還從教師 LLM 中提取推理依據。這些推理依據作爲中間推理步驟,以數據高效的方式訓練小型學生 LLM。

值得注意的是,當今許多最先進的 LLM 擁有嚴格的許可條款,禁止使用其輸出來訓練其他 LLM,這使得找到合適的教師模型變得具有挑戰性。

模型服務技術

模型執行經常受到內存帶寬的限制——特別是在權重方面受到帶寬的約束。即使應用了前面描述的所有模型優化,仍然很可能處於內存受限的狀態。因此,當模型權重加載時,應儘可能多地利用它們。換句話說,嘗試並行處理。有兩種方法可以採取:

飛行中批處理

LLM 具有一些獨特的執行特性,這在實踐中可能使得有效地批量處理請求變得困難。單一模型可以同時用於各種外觀截然不同的任務。從聊天機器人中的簡單問答響應到文檔摘要或長段代碼的生成,工作負載高度動態,輸出大小相差幾個數量級。

這種多樣性使得有效批量處理請求並並行執行它們變得具有挑戰性——這是服務神經網絡時的常見優化手段。這可能導致一些請求比其他請求早得多完成。

爲了管理這些動態負載,許多 LLM 服務解決方案包括一種優化的調度技術,稱爲連續或飛行中批處理。這利用了這樣一個事實:LLM 的整體文本生成過程可以分解爲模型上的多次執行迭代。

在飛行中批處理中,服務器運行時不會等待整個批次完成後再開始下一批請求,而是立即從批次中移除已完成的序列。然後,當其他請求仍在進行中時,開始執行新請求。因此,飛行中批處理可以在實際應用場景中大大提高整體 GPU 利用率。

推測性推理

也被稱爲推測性採樣、輔助生成或逐塊並行解碼,推測性推理是並行執行 LLM 的另一種方式。通常,GPT 風格的大規模語言模型是自迴歸模型,逐個 token 生成文本。

生成的每個 token 依賴於它之前的全部 token 來提供上下文。這意味着在常規執行中,不可能並行生成來自同一序列的多個 token——你必須等到第 n 個 token 生成後才能生成第 n+1 個。

下圖展示了推測性推理的一個例子,其中草稿模型暫時預測多個未來的步驟,這些步驟並行地被驗證或拒絕。在這個例子中,草稿中預測的前兩個 token 被接受,而最後一個被拒絕並在繼續生成之前被移除。

推測性採樣提供了一種變通方案。這種方法的基本思想是使用某種 “成本較低” 的過程生成一個較長的草稿延續。然後,在多個步驟中並行執行主要的 “驗證” 模型,使用低成本的草稿作爲 “推測性” 上下文,用於需要它的執行步驟。

如果驗證模型生成的 token 與草稿相同,則可以確定接受這些 token 作爲輸出。否則,可以從第一個不匹配的 token 之後丟棄所有內容,並使用新的草稿重複此過程。

生成草稿 token 有多種不同的選項,每種都有不同的權衡。你可以訓練多個模型,或者在單個預訓練模型上微調多個頭部,以預測未來的多個步驟的 token。或者,你可以使用一個小模型作爲草稿模型,一個更大、更強大的模型作爲驗證者。

LLM 服務中的關鍵指標

那麼,我們究竟應該如何考慮推理速度呢?

我們使用四個關鍵指標來衡量 LLM 服務:

  1. 首次 token 時間(TTFT):用戶在輸入查詢後看到模型輸出的速度。實時交互中對響應等待時間的要求很低,但在離線工作負載中則不太重要。該指標取決於處理提示和生成第一個輸出 token 所需的時間。

  2. 每個輸出 token 時間(TPOT):爲每個查詢我們系統的用戶生成輸出 token 的時間。該指標對應於每個用戶感知的模型 “速度”。例如,TPOT 爲 100 毫秒 / token 將爲每個用戶提供每秒 10 個 token,或約每分鐘 450 個單詞,這比普通人閱讀速度還要快。

  3. 延遲:模型爲用戶生成完整響應所需的總時間。總體響應延遲可以使用前兩個指標計算得出:延遲 = (TTFT) + (TPOT)*(待生成的 token 數)。

  4. 吞吐量:推理服務器在所有用戶和請求中每秒可以生成的輸出 token 數。

我們需要什麼來實現一個 LLM 服務?

在服務基於 LLM 的應用程序時,主要有兩大組件:引擎和服務器。引擎負責處理所有關於模型和請求批處理的工作,而服務器則負責轉發用戶的請求。

引擎

引擎是運行模型的地方,涵蓋了我們迄今爲止討論過的所有生成過程及其各種優化技術。本質上,這些都是 Python 庫。它們處理從用戶到我們聊天機器人的請求批處理,併爲這些請求生成響應。

服務器

服務器負責協調來自用戶的 HTTP/gRPC 請求。在實際應用中,我們會有許多用戶在一天的不同時間向我們的聊天機器人提問。服務器將這些請求排隊,並將其轉發給引擎以生成響應。服務器還提供了諸如吞吐量和延遲這樣的指標,對於跟蹤模型服務非常重要。

功能

引擎

服務器

到目前爲止,我們討論了一個模型處理單一請求的簡單場景。然而,現實世界的應用要求能夠同時服務於數百,甚至數千名用戶。現在,我們的關注點轉向了成本優化和吞吐量提升,引導我們進入下一個關鍵議題:請求批處理和利用 PagedAttention 進行內存優化。這些優化措施對於高效承載模型至關重要,確保在面對大量用戶需求時,既經濟高效又具備高吞吐量。

LLM 服務的框架

在介紹了關鍵指標、權衡及處理 LLM 服務中重大挑戰的技術之後,一個重要的問題是:我們如何將這些技術付諸實踐?哪些工具最適合我們的需求?在深入研究之前,我們應該瞭解哪些關於這些框架的信息?

在本節中,我們將深入探討這些關鍵框架的細節,分享我們基準測試實驗的主要發現。我們選擇了行業中最受歡迎和廣泛使用的框架。每個框架在優化和增強大規模語言模型(LLM)推理性能方面具有獨特價值。我們將這些框架分爲兩大類:服務器和引擎。最終,我們將對現有工具及其潛在適應性有清晰的認識,以滿足我們具體的 LLM 服務需求。

  1. vLLM =======

一個快速且易於使用的庫,用於 LLM 推理和服務。它實現了比 HuggingFace Transformers (HF) 高 14 到 24 倍的吞吐量,比 HuggingFace Text Generation Inference (TGI) 高 2.2 到 2.5 倍的吞吐量。

使用

# pip install vllm
from vllm import LLM, SamplingParams
prompts = [
    "Funniest joke ever:",
    "The capital of France is",
    "The future of AI is",
]
sampling_params = SamplingParams(temperature=0.95, top_p=0.95, max_tokens=200)
llm = LLM(model="huggyllama/llama-13b")
outputs = llm.generate(prompts, sampling_params)
for output in outputs:
    prompt = output.prompt
    generated_text = output.outputs[0].text
    print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")

API 服務:

# Start the server:
python -m vllm.entrypoints.api_server --env MODEL_NAME=huggyllama/llama-13b
# Query the model in shell:
curl http://localhost:8000/generate \
    -d '{
        "prompt": "Funniest joke ever:",
        "n": 1,
        "temperature": 0.95,
        "max_tokens": 200
    }'

殺手級功能

優點

侷限性

雖然該庫提供了用戶友好的特性和廣泛的功能,但我確實遇到了一些侷限:

這是進行 LLM 推理最快的庫。由於其內部優化,它在性能上遠遠超過了競爭對手。然而,它在支持的模型範圍方面存在弱點。

  1. Text Generation Inference ============================

Text Generation Inference(TGI)是一個用於部署和提供大型語言模型(LLM)的工具包。TGI 爲最受歡迎的開源 LLM,包括 Llama、Falcon、StarCoder、BLOOM、GPT-NeoX 和 T5,實現了高性能文本生成。

一個用於文本生成推理的 Rust、Python 和 gRPC 服務器。在 HuggingFace 的生產環境中使用,用來爲 LLM API 推理小組件。

使用

使用 docker 運行 web 服務器:

mkdir data
docker run --gpus all --shm-size 1g -p 8080:80 \
-v data:/data ghcr.io/huggingface/text-generation-inference:0.9 \
  --model-id huggyllama/llama-13b \
  --num-shard 1

製作請求:

# pip install text-generation
from text_generation import Client
client = Client("http://127.0.0.1:8080")
prompt = "Funniest joke ever:"
print(client.generate(prompt, max_new_tokens=17 temperature=0.95).generated_text)

優點

侷限性

我認爲這是競賽中的領先者之一。該庫編寫得非常好,我在部署模型時幾乎沒遇到什麼難題。如果希望與 HuggingFace 進行原生集成,這絕對值得考慮。請注意,項目團隊最近變更了許可證。

  1. CTranslate2 ==============

CTranslate2 是一個用於 Transformer 模型高效推理的 C++ 和 Python 庫。

使用

首先,轉換模型:

pip install -qqq transformers ctranslate2
# The model should be first converted into the CTranslate2 model format:
ct2-transformers-converter --model huggyllama/llama-13b --output_dir llama-13b-ct2 --force

製作請求:

import ctranslate2
import transformers
generator = ctranslate2.Generator("llama-13b-ct2"device="cuda"compute_type="float16")
tokenizer = transformers.AutoTokenizer.from_pretrained("huggyllama/llama-13b")
prompt = "Funniest joke ever:"
tokens = tokenizer.convert_ids_to_tokens(tokenizer.encode(prompt))
results = generator.generate_batch(
    [tokens], 
    sampling_topk=1, 
    max_length=200, 
)
tokens = results[0].sequences_ids[0]
output = tokenizer.decode(tokens)
print(output)

殺手級特性

優勢

侷限性

我發現這個庫很吸引人。開發者們在積極維護它,這一點從 GitHub 上的發佈和提交可見一斑,他們還分享了一些關於應用該庫的博客文章。庫中大量的優化令人印象深刻,而它的主要亮點在於能夠在 CPU 上執行 LLM 推理。

  1. DeepSpeed-MII ================

MII 利用了 DeepSpeed,實現了低延遲和高吞吐量的推理。

使用

運行 Web 服務器:

# DON'T INSTALL USING pip install deepspeed-mii
# git clone https://github.com/microsoft/DeepSpeed-MII.git
# git reset --hard 60a85dc3da5bac3bcefa8824175f8646a0f12203
# cd DeepSpeed-MII && pip install .
# pip3 install -U deepspeed
# ... and make sure that you have same CUDA versions:
# python -c "import torch;print(torch.version.cuda)" == nvcc --version
import mii
mii_configs = {
    "dtype""fp16",
    'max_tokens': 200,
    'tensor_parallel': 1,
    "enable_load_balancing": False
}
mii.deploy(task="text-generation",
           model="huggyllama/llama-13b",
           deployment_,
           mii_config=mii_configs)

製作: 請求:

import mii
generator = mii.mii_query_handle("llama_13b_deployment")
result = generator.query(  
  {"query"["Funniest joke ever:"]}, 
  do_sample=True,
  max_new_tokens=200
)
print(result)

殺手級特性

優勢

侷限性

該項目基於穩定可靠的 DeepSpeed 庫,已在社區內贏得了良好口碑。如果我們追求穩定性和經過驗證的方案,MII 會是不錯的選擇。根據我的實驗,該庫在處理單個提示時表現出最快的響應速度。然而,我還是建議在將其整合進系統前,先在具體任務上測試框架的適用性。

  1. OpenLLM ==========

一個用於在生產環境中運營大型語言模型(LLM)的開放平臺。

使用

pip install openllm scipy
openllm start llama --model-id huggyllama/llama-13b \
  --max-new-tokens 200 \
  --temperature 0.95 \
  --api-workers 1 \
  --workers-per-resource 1

製作請求:

import openllm
client = openllm.client.HTTPClient('http://localhost:3000')
print(client.query("Funniest joke ever:"))

殺手級特性

優勢

侷限性

這是一個具有廣泛功能的優秀框架。它讓我們能夠以最小的開銷創建靈活的應用程序。雖然文檔中可能有些方面沒有完全覆蓋,但在深入探索這個庫的過程中,我們很可能會發現一些令人驚喜的附加功能。

  1. Ray Serve ============

Ray Serve 是一個可擴展的模型服務庫,用於構建在線推理 API。Serve 與框架無關,因此我們可以使用同一套工具來服務從深度學習模型到各類模型的所有需求。

Ray AIR 支持端到端的機器學習開發,並提供了多種選項以與其他 MLOps 生態系統中的工具和庫集成。

使用

運行 Web 服務器:

# pip install ray[serve] accelerate>=0.16.0 transformers>=4.26.0 torch starlette pandas
# ray_serve.py
import pandas as pd
import ray
from ray import serve
from starlette.requests import Request
@serve.deployment(ray_actor_options={"num_gpus": 1})
class PredictDeployment:
    def __init__(self, model_id: str):
        from transformers import AutoModelForCausalLM, AutoTokenizer
        import torch
self.model = AutoModelForCausalLM.from_pretrained(
            model_id,
            torch_dtype=torch.float16,
            device_map="auto",
        )
        self.tokenizer = AutoTokenizer.from_pretrained(model_id)
def generate(self, text: str) -> pd.DataFrame:
        input_ids = self.tokenizer(text, return_tensors="pt").input_ids.to(
            self.model.device
        )
        gen_tokens = self.model.generate(
            input_ids,
            temperature=0.9,
            max_length=200,
        )
        return pd.DataFrame(
            self.tokenizer.batch_decode(gen_tokens)columns=["responses"]
        )
async def __call__(self, http_request: Request) -> str:
        json_request: str = await http_request.json()
        return self.generate(prompt["text"])
deployment = PredictDeployment.bind(model_id="huggyllama/llama-13b")
# then run from CLI command:
# serve run ray_serve:deployment

製作請求:

import requests
sample_input = {"text""Funniest joke ever:"}
output = requests.post("http://localhost:8000/"json=[sample_input]).json()
print(output)

殺手級特性

優勢

侷限性

如果我們需要的是一個不僅限於深度學習領域、且最爲適合生產環境的解決方案,那麼 Ray Serve 是一個不錯的選擇。它最適合那些重視可用性、可擴展性和可觀測性的企業場景。此外,我們還可以利用其龐大的生態系統進行數據處理、訓練、微調和模型服務。最後,它被從 OpenAI 到 Shopify 和 Instacart 的多家公司所採用。

  1. MLC LLM ==========

MLC LLM(機器學習編譯針對 LLM)是一個通用的部署解決方案,它使 LLM 能夠高效地在消費級設備上運行,充分利用本地硬件加速。

使用

運行 web 服務:

# 1. Make sure that you have python >= 3.9
# 2. You have to run it using conda:
conda create -n mlc-chat-venv -c mlc-ai -c conda-forge mlc-chat-nightly
conda activate mlc-chat-venv
# 3. Then install package:
pip install --pre --force-reinstall mlc-ai-nightly-cu118 \
  mlc-chat-nightly-cu118 \
  -f https://mlc.ai/wheels
# 4. Download the model weights from HuggingFace and binary libraries:
git lfs install && mkdir -p dist/prebuilt && \
  git clone https://github.com/mlc-ai/binary-mlc-llm-libs.git dist/prebuilt/lib && \
  cd dist/prebuilt &&  
  git clone https://huggingface.co/huggyllama/llama-13b dist/ && \
  cd ../..
  
  
# 5. Run server:
python -m mlc_chat.rest --device-name cuda --artifact-path dist

製作請求:

import requests
payload = {
   "model""lama-30b",
   "messages"[{"role""user""content""Funniest joke ever:"}],
   "stream": False
}
r = requests.post("http://127.0.0.1:8000/v1/chat/completions"json=payload)
print(r.json()['choices'][0]['message']['content'])

殺手級特性

優勢

侷限性

如果我們需要在 iOS 或 Android 設備上部署應用,這個庫正是我們需要的。它將使我們能夠迅速地在設備上原生編譯和部署模型。然而,如果我們需要的是高負載服務器,我不會推薦選擇這個框架。

結論

在我們的白皮書中,我們使用不同設置評估了這些框架及其提供的功能。無論是像 TensorRT-LLM 和 vLLM 這樣的引擎,還是像 RayLLM 與 RayServe、帶有 TensorRT-LLM 的 Triton 以及 Text Generation Inference (TGI) 這樣的服務器,每個框架都帶來了獨特的功能,對於不同應用場景具有重要價值。我們的基準測試研究揭示了一些微妙的發現,從內存分配挑戰到預佔空比的戰略權衡,以及序列長度對吞吐量的影響。以下是我們在實驗中學到的一些要點概述:

儘管存在大量的 LLM 推理框架,但每個框架都有其特定的用途。以下是一些關鍵考量點:

  1. 當需要爲批量提示提供最大速度時,使用 vLLM。

  2. 如果我們需要原生 HuggingFace 支持,並且不打算爲核心模型使用多個適配器,可以選擇 Text generation inference。

  3. 如果速度對我們至關重要,且我們計劃在 CPU 上運行推理,可以考慮 CTranslate2。

  4. 如果想要將適配器連接到核心模型,並利用 HuggingFace Agents,特別是在不完全依賴 PyTorch 的情況下,可以選擇 OpenLLM。

  5. 對於穩定的流水線和靈活的部署,可以考慮 Ray Serve,它尤其適合較爲成熟的項目。

  6. 如果希望在客戶端(邊緣計算)原生部署 LLM,例如在 Android 或 iPhone 平臺上,可以利用 MLC LLM。

  7. 如果我們已經熟悉 DeepSpeed 庫,並希望繼續使用它來部署 LLM,可以選擇 DeepSpeed-MII。

—END—

英文原文:https://medium.com/@vipra_singh/building-llm-applications-serving-llms-part-9-68baa19cef79

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/03ve267RFFGAdlERbEDHBg